From c6087ed7ddc0cd637a5a49d55954e7f5d26e5f01 Mon Sep 17 00:00:00 2001 From: BobbyAndrean Date: Mon, 8 Jun 2026 20:24:54 +0700 Subject: [PATCH] Project UAS Sistem Informasi Geografis (SIG) --- README.md | 366 +++++++++++++++++++++ css/style.css | 596 ++++++++++++++++++++++++++++++++++ geometri/index.php | 47 +++ index.php | 263 +++++++++++++++ js/draw.js | 425 ++++++++++++++++++++++++ js/draw_geometri.js | 331 +++++++++++++++++++ js/draw_sosial.js | 584 +++++++++++++++++++++++++++++++++ js/draw_sosial_public.js | 334 +++++++++++++++++++ js/draw_spbu.js | 226 +++++++++++++ js/draw_spbu_point.js | 137 ++++++++ php/db.php | 7 + php/delete.php | 33 ++ php/insert.php | 31 ++ php/insert_line.php | 39 +++ php/insert_masjid.php | 25 ++ php/insert_penduduk.php | 51 +++ php/insert_polygon.php | 39 +++ php/show.php | 38 +++ php/show_geometri.php | 35 ++ php/show_sosial.php | 20 ++ php/show_spbu.php | 16 + php/update_line.php | 24 ++ php/update_masjid.php | 41 +++ php/update_penduduk.php | 25 ++ php/update_polygon.php | 24 ++ sosial/admin.php | 127 ++++++++ sosial/erd.drawio | 184 +++++++++++ sosial/index.php | 99 ++++++ sosial/login.php | 209 ++++++++++++ sosial/logout.php | 6 + sosial/readme.md | 15 + sosial/usecase_diagram.drawio | 85 +++++ spbu/index.php | 67 ++++ 33 files changed, 4549 insertions(+) create mode 100644 README.md create mode 100644 css/style.css create mode 100644 geometri/index.php create mode 100644 index.php create mode 100644 js/draw.js create mode 100644 js/draw_geometri.js create mode 100644 js/draw_sosial.js create mode 100644 js/draw_sosial_public.js create mode 100644 js/draw_spbu.js create mode 100644 js/draw_spbu_point.js create mode 100644 php/db.php create mode 100644 php/delete.php create mode 100644 php/insert.php create mode 100644 php/insert_line.php create mode 100644 php/insert_masjid.php create mode 100644 php/insert_penduduk.php create mode 100644 php/insert_polygon.php create mode 100644 php/show.php create mode 100644 php/show_geometri.php create mode 100644 php/show_sosial.php create mode 100644 php/show_spbu.php create mode 100644 php/update_line.php create mode 100644 php/update_masjid.php create mode 100644 php/update_penduduk.php create mode 100644 php/update_polygon.php create mode 100644 sosial/admin.php create mode 100644 sosial/erd.drawio create mode 100644 sosial/index.php create mode 100644 sosial/login.php create mode 100644 sosial/logout.php create mode 100644 sosial/readme.md create mode 100644 sosial/usecase_diagram.drawio create mode 100644 spbu/index.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ff0fce --- /dev/null +++ b/README.md @@ -0,0 +1,366 @@ +# WebGIS Pontianak + +WebGIS Pontianak adalah proyek Sistem Informasi Geografis berbasis web untuk menampilkan dan mengelola data spasial di wilayah Pontianak. Proyek ini berisi tiga modul utama: WebGIS geometri dasar, WebGIS lokasi SPBU, dan WebGIS sosial untuk pemetaan masjid serta penduduk miskin. + +Aplikasi dibuat menggunakan PHP native, MySQL, JavaScript, Leaflet.js, Leaflet Draw, dan CSS vanilla. Proyek ini cocok dijalankan di lingkungan lokal seperti XAMPP. + +## Fitur Utama + +- Dashboard utama untuk memilih modul WebGIS. +- Peta interaktif berbasis Leaflet. +- CRUD data spasial melalui marker, polyline, dan polygon. +- Penyimpanan data ke database MySQL. +- Panel admin untuk pengelolaan data sosial. +- Analisis radius layanan masjid terhadap penduduk miskin. +- Visualisasi heatmap dan indikator blind spot pada modul sosial. + +## Modul Aplikasi + +### 1. WebGIS Geometri + +Lokasi: `geometri/index.php` + +Modul ini digunakan untuk latihan dan pengelolaan data geometri dasar. + +Fitur: + +- Menampilkan data point, jalan/polyline, dan polygon/parsil. +- Menambah point dengan nama dan kode point. +- Menambah jalan dengan nama dan klasifikasi. +- Menambah polygon/parsil dengan nama area dan status kepemilikan. +- Mengedit bentuk jalan dan polygon langsung dari peta. +- Menghapus data melalui tools Leaflet Draw. + +### 2. WebGIS SPBU + +Lokasi: `spbu/index.php` + +Modul ini digunakan untuk memetakan lokasi SPBU di Pontianak. + +Fitur: + +- Menampilkan titik SPBU. +- Membedakan SPBU 24 jam dan tidak 24 jam. +- Menambah titik SPBU dari peta. +- Mengisi nama SPBU, nomor SPBU, dan status operasional. +- Menghapus titik SPBU. +- Toggle layer untuk menampilkan/menyembunyikan SPBU berdasarkan status. + +### 3. WebGIS Sosial + +Lokasi publik: `sosial/index.php` +Lokasi admin: `sosial/admin.php` + +Modul ini digunakan untuk pemetaan sosial, khususnya lokasi masjid dan penduduk miskin. Sistem menghitung apakah penduduk miskin berada di dalam radius layanan masjid atau masuk kategori blind spot. + +Fitur publik: + +- Menampilkan lokasi masjid dan penduduk miskin. +- Menampilkan radius layanan masjid. +- Menampilkan status penduduk miskin, apakah berada dalam jangkauan masjid atau blind spot. +- Menampilkan heatmap kepadatan penduduk miskin. +- Menampilkan statistik sosial dan rekomendasi jenis bantuan. + +Fitur admin: + +- Login admin. +- Menambah titik masjid. +- Menambah titik penduduk miskin. +- Mengatur radius layanan masjid dari 100 sampai 5000 meter. +- Mengubah jenis bantuan yang direkomendasikan. +- Menghapus data masjid atau penduduk miskin. + +## Teknologi yang Digunakan + +- HTML5 +- CSS3 vanilla +- JavaScript vanilla +- PHP native +- MySQL +- Leaflet.js +- Leaflet Draw +- Leaflet Heat +- Leaflet GeometryUtil +- OpenStreetMap tile layer +- Nominatim reverse geocoding untuk membantu mengambil alamat penduduk + +## Struktur Folder + +```text +WebGIS2026/ +├── css/ +│ └── style.css +├── geometri/ +│ └── index.php +├── js/ +│ ├── draw_geometri.js +│ ├── draw_sosial.js +│ ├── draw_sosial_public.js +│ ├── draw_spbu_point.js +│ └── ... +├── php/ +│ ├── db.php +│ ├── insert.php +│ ├── insert_line.php +│ ├── insert_masjid.php +│ ├── insert_penduduk.php +│ ├── insert_polygon.php +│ ├── show_geometri.php +│ ├── show_sosial.php +│ ├── show_spbu.php +│ ├── update_masjid.php +│ ├── update_penduduk.php +│ └── delete.php +├── sosial/ +│ ├── index.php +│ ├── login.php +│ ├── admin.php +│ └── logout.php +├── spbu/ +│ └── index.php +├── index.php +└── README.md +``` + +## Persyaratan + +Pastikan perangkat sudah memiliki: + +- XAMPP atau web server lokal lain yang mendukung PHP dan MySQL. +- PHP 7.4 atau lebih baru. +- MySQL/MariaDB. +- Browser modern. +- Koneksi internet untuk memuat library CDN Leaflet, Google Fonts, OpenStreetMap, dan Nominatim. + +## Instalasi + +1. Clone repository ini atau salin folder proyek ke direktori `htdocs`. + + ```bash + git clone + ``` + +2. Jika menggunakan XAMPP, letakkan folder proyek di: + + ```text + C:/xampp/htdocs/WebGIS2026 + ``` + +3. Jalankan Apache dan MySQL dari XAMPP Control Panel. + +4. Buat database MySQL, contoh: + + ```sql + CREATE DATABASE webgis_spbu; + ``` + +5. Sesuaikan konfigurasi koneksi database pada file `php/db.php`. + + ```php + $conn = new mysqli("localhost", "root", "password_database", "webgis_spbu"); + ``` + +6. Buat tabel yang dibutuhkan. Jika belum memiliki file `.sql`, gunakan struktur dasar berikut sebagai acuan. + + ```sql + CREATE TABLE spbu ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(150) NOT NULL, + no_spbu VARCHAR(100) NOT NULL, + status VARCHAR(50) NOT NULL, + latitude DOUBLE NOT NULL, + longitude DOUBLE NOT NULL + ); + + CREATE TABLE jalan ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(150) NOT NULL, + status VARCHAR(50) NOT NULL, + panjang DOUBLE DEFAULT 0, + geom LONGTEXT NOT NULL + ); + + CREATE TABLE parsil ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(150) NOT NULL, + status VARCHAR(50) NOT NULL, + luas DOUBLE DEFAULT 0, + geom LONGTEXT NOT NULL + ); + + CREATE TABLE masjid ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(150) NOT NULL, + pic VARCHAR(150) NOT NULL, + radius DOUBLE DEFAULT 500, + latitude DOUBLE NOT NULL, + longitude DOUBLE NOT NULL, + radius_layanan DOUBLE DEFAULT 500 + ); + + CREATE TABLE penduduk_miskin ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama_kk VARCHAR(150) NOT NULL, + jumlah_anggota INT DEFAULT 1, + alamat TEXT NOT NULL, + latitude DOUBLE NOT NULL, + longitude DOUBLE NOT NULL, + penghasilan VARCHAR(50), + kondisi_rumah VARCHAR(50), + sumber_air VARCHAR(50), + skor_kerentanan DOUBLE DEFAULT 0, + jenis_bantuan VARCHAR(100) + ); + ``` + +7. Buka aplikasi melalui browser: + + ```text + http://localhost/WebGIS2026/ + ``` + +## Cara Penggunaan + +### Membuka Dashboard Utama + +1. Buka `http://localhost/WebGIS2026/`. +2. Pilih salah satu modul: + - WebGIS Geometri + - WebGIS SPBU + - WebGIS Sosial + +### Menggunakan Modul Geometri + +1. Buka `http://localhost/WebGIS2026/geometri/`. +2. Gunakan toolbar gambar di sisi peta. +3. Pilih marker untuk menambah point. +4. Pilih polyline untuk menambah jalan. +5. Pilih polygon untuk menambah parsil/area. +6. Isi form yang muncul pada popup. +7. Klik tombol simpan. +8. Untuk mengedit jalan atau polygon, gunakan tombol edit pada toolbar Leaflet Draw. +9. Untuk menghapus data, gunakan tombol delete pada toolbar atau tombol hapus pada popup jika tersedia. + +### Menggunakan Modul SPBU + +1. Buka `http://localhost/WebGIS2026/spbu/`. +2. Klik tombol marker pada toolbar peta. +3. Klik lokasi SPBU pada peta. +4. Isi data: + - Nama SPBU + - Nomor SPBU + - Status operasional: `24 Jam` atau `Tidak 24 Jam` +5. Klik simpan. +6. Gunakan tombol layer `24 Jam` dan `Tidak 24 Jam` untuk memfilter tampilan. +7. Klik marker SPBU untuk melihat detail atau menghapus data. + +### Login Admin Modul Sosial + +1. Buka `http://localhost/WebGIS2026/sosial/`. +2. Klik tombol `Admin Login`. +3. Masukkan akun default: + + ```text + Username: admin + Password: admin123 + ``` + +4. Setelah berhasil login, sistem akan membuka panel admin di `sosial/admin.php`. + +Catatan: ubah kredensial default di `sosial/login.php` sebelum proyek digunakan di lingkungan publik. + +### Menambah Data Masjid + +1. Login ke panel admin sosial. +2. Pilih mode `Tambah Masjid`. +3. Klik lokasi masjid pada peta. +4. Isi nama masjid dan PIC atau penanggung jawab. +5. Atur radius deteksi menggunakan slider. +6. Klik simpan. +7. Sistem akan menampilkan marker masjid dan lingkaran radius layanan. + +### Menambah Data Penduduk Miskin + +1. Login ke panel admin sosial. +2. Pilih mode `Tambah Pend. Miskin`. +3. Klik lokasi penduduk pada peta. +4. Isi data: + - Nama kepala keluarga + - Jumlah anggota keluarga + - Alamat + - Penghasilan + - Kondisi rumah + - Sumber air +5. Klik simpan. +6. Sistem akan menghitung skor kerentanan dan rekomendasi jenis bantuan. +7. Jika penduduk berada di dalam radius masjid, marker akan ditandai sebagai terjangkau. Jika tidak, penduduk berisiko tinggi dapat masuk ke daftar blind spot. + +### Mengubah Radius Layanan Masjid + +1. Login ke panel admin sosial. +2. Klik marker masjid. +3. Geser slider radius layanan. +4. Klik `Simpan Radius`. +5. Sistem akan menghitung ulang status jangkauan penduduk miskin secara otomatis. + +### Mengubah Jenis Bantuan + +1. Login ke panel admin sosial. +2. Klik marker penduduk miskin. +3. Pilih jenis bantuan dari dropdown. +4. Klik `Simpan Bantuan`. + +### Logout Admin + +Klik tombol `Keluar` pada panel admin sosial untuk mengakhiri sesi admin. + +## Alur Singkat Sistem Sosial + +1. Admin memasukkan data masjid beserta radius layanan. +2. Admin memasukkan data penduduk miskin. +3. Sistem menghitung jarak penduduk miskin terhadap masjid terdekat. +4. Penduduk yang berada dalam radius masjid dianggap masuk cakupan sosial masjid. +5. Penduduk yang berada di luar radius, terutama dengan skor kerentanan tinggi, ditandai sebagai blind spot. +6. Data ini dapat digunakan sebagai dasar prioritas bantuan, distribusi program sosial, dan evaluasi wilayah yang belum terjangkau. + +## Endpoint PHP + +Beberapa endpoint utama yang digunakan aplikasi: + +| Endpoint | Fungsi | +| --- | --- | +| `php/show_geometri.php` | Mengambil data point, jalan, dan parsil | +| `php/show_spbu.php` | Mengambil data SPBU | +| `php/show_sosial.php` | Mengambil data masjid dan penduduk miskin | +| `php/insert.php` | Menambah data SPBU/point | +| `php/insert_line.php` | Menambah data jalan | +| `php/insert_polygon.php` | Menambah data parsil | +| `php/insert_masjid.php` | Menambah data masjid | +| `php/insert_penduduk.php` | Menambah data penduduk miskin | +| `php/update_line.php` | Mengubah geometri jalan | +| `php/update_polygon.php` | Mengubah geometri parsil | +| `php/update_masjid.php` | Mengubah radius layanan masjid | +| `php/update_penduduk.php` | Mengubah jenis bantuan penduduk | +| `php/delete.php` | Menghapus data berdasarkan tipe | + +## Catatan Keamanan + +- Jangan gunakan kredensial admin default di server publik. +- Jangan menyimpan password database asli di repository publik. +- Pertimbangkan menggunakan file konfigurasi lokal seperti `.env` jika proyek dikembangkan lebih lanjut. +- Tambahkan validasi session untuk seluruh endpoint admin jika aplikasi akan dipakai di production. +- Batasi akses database sesuai kebutuhan aplikasi. + +## Rencana Pengembangan + +- Import/export data GeoJSON. +- Filter data berdasarkan kecamatan atau kelurahan. +- Role user untuk admin, petugas, dan viewer. +- Upload dokumentasi/foto penduduk atau masjid. +- Dashboard laporan bantuan sosial. +- Integrasi data batas administrasi. +- Autentikasi admin berbasis database. + +## Lisensi + +Proyek ini dibuat untuk kebutuhan pembelajaran dan pengembangan WebGIS. Silakan sesuaikan lisensi sesuai kebutuhan repository. diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..9ab060e --- /dev/null +++ b/css/style.css @@ -0,0 +1,596 @@ +/* ============================================================ + style.css — WebGIS SPBU Pontianak + ============================================================ */ + +/* ── CSS VARIABLES ── */ +:root { + --bg: #0f1117; + --panel: #1a1d27; + --border: #2a2d3a; + --accent: #4ade80; + --accent2: #38bdf8; + --danger: #f87171; + --text: #e2e8f0; + --muted: #64748b; + --header-h: 56px; +} + +/* ── RESET ── */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +/* ── BASE ── */ +body { + font-family: 'DM Sans', sans-serif; + background: var(--bg); + color: var(--text); + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* ============================================================ + HEADER + ============================================================ */ +header { + height: var(--header-h); + background: var(--panel); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + padding: 0 20px; + gap: 16px; + z-index: 1000; + flex-shrink: 0; +} + +.logo { + display: flex; + align-items: center; + gap: 10px; +} + +.logo-icon { + width: 32px; + height: 32px; + background: var(--accent); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; +} + +.logo-text { + font-family: 'Space Mono', monospace; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.05em; + color: var(--text); +} + +.logo-text span { + color: var(--accent); +} + +.header-divider { + width: 1px; + height: 28px; + background: var(--border); + margin: 0 4px; +} + +/* ── LEGEND ── */ +.legend { + display: flex; + align-items: center; + gap: 16px; + margin-left: auto; +} + +.legend-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--muted); +} + +.legend-dot { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.legend-line { + width: 20px; + height: 3px; + border-radius: 2px; +} + +.legend-poly { + width: 14px; + height: 10px; + border-radius: 2px; + opacity: 0.7; +} + +/* ── LAYER CONTROL ── */ +.layer-control { + display: flex; + align-items: center; + gap: 6px; + margin-left: 8px; +} + +.layer-btn { + display: flex; + align-items: center; + gap: 7px; + padding: 5px 12px; + border-radius: 99px; + border: 1.5px solid var(--border); + background: var(--panel); + color: var(--muted); + font-family: 'DM Sans', sans-serif; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all .18s; + user-select: none; +} + +.layer-btn .dot { + width: 9px; + height: 9px; + border-radius: 50%; + flex-shrink: 0; + transition: opacity .18s; +} + +.layer-btn.active { + color: var(--text); + border-color: transparent; +} + +.layer-btn.active.btn-24 { background: #064e3b; border-color: #4ade80; color: #4ade80; } +.layer-btn.active.btn-tidak { background: #450a0a; border-color: #f87171; color: #f87171; } + +.layer-btn:not(.active) .dot { + opacity: 0.3; +} + +.layer-sep { + width: 1px; + height: 22px; + background: var(--border); + margin: 0 2px; +} + +/* ============================================================ + MAP + ============================================================ */ +#map { + flex: 1; + width: 100%; +} + +/* ============================================================ + POPUP FORM (input saat tambah data) + ============================================================ */ +.pf { + font-family: 'DM Sans', sans-serif; + width: 230px; + background: #1e2130; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 8px 32px rgba(0, 0, 0, .5); +} + +.pf-head { + padding: 10px 14px; + font-size: 12px; + font-weight: 600; + letter-spacing: .06em; + text-transform: uppercase; + display: flex; + align-items: center; + gap: 8px; +} + +.pf-head-point { background: linear-gradient(135deg, #1d4ed8, #3b82f6); color: #fff; } +.pf-head-line { background: linear-gradient(135deg, #7c3aed, #a855f7); color: #fff; } +.pf-head-polygon { background: linear-gradient(135deg, #065f46, #10b981); color: #fff; } + +.pf-body { + padding: 12px 14px; +} + +.pf-body label { + display: block; + font-size: 11px; + color: #94a3b8; + margin-bottom: 3px; + margin-top: 8px; + font-weight: 500; +} + +.pf-body label:first-child { + margin-top: 0; +} + +.pf-body input, +.pf-body select { + width: 100%; + background: #0f1117; + border: 1px solid #2a2d3a; + color: #e2e8f0; + border-radius: 6px; + padding: 7px 10px; + font-size: 13px; + font-family: 'DM Sans', sans-serif; + outline: none; + transition: border .15s; +} + +.pf-body input:focus, +.pf-body select:focus { + border-color: #4ade80; +} + +.pf-body select option { + background: #1a1d27; +} + +.pf-info { + margin-top: 8px; + padding: 6px 10px; + background: #0f1117; + border-radius: 6px; + font-size: 12px; + color: #94a3b8; + font-family: 'Space Mono', monospace; +} + +.pf-btn { + display: block; + width: 100%; + margin-top: 12px; + padding: 9px; + background: #4ade80; + color: #0f1117; + border: none; + border-radius: 7px; + font-size: 13px; + font-weight: 600; + font-family: 'DM Sans', sans-serif; + cursor: pointer; + transition: background .15s, transform .1s; +} + +.pf-btn:hover { background: #22c55e; transform: translateY(-1px); } +.pf-btn:active { transform: translateY(0); } + +/* ============================================================ + INFO POPUP (tampil data dari DB) + ============================================================ */ +.ip { + font-family: 'DM Sans', sans-serif; + min-width: 190px; + background: #1e2130; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 8px 32px rgba(0, 0, 0, .5); +} + +.ip-head { + padding: 10px 14px; + font-size: 12px; + font-weight: 700; + letter-spacing: .04em; + color: #fff; +} + +.ip-body { + padding: 10px 14px; +} + +.ip-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 4px 0; + border-bottom: 1px solid #2a2d3a; + font-size: 12px; +} + +.ip-row:last-of-type { border-bottom: none; } + +.ip-label { color: #64748b; } +.ip-val { color: #e2e8f0; font-weight: 500; } + +/* ── BADGES ── */ +.ip-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 99px; + font-size: 11px; + font-weight: 600; +} + +.badge-green { background: #064e3b; color: #4ade80; } +.badge-red { background: #450a0a; color: #f87171; } +.badge-blue { background: #0c1a4a; color: #38bdf8; } +.badge-gray { background: #1e293b; color: #94a3b8; } + +/* ── DELETE BUTTON ── */ +.ip-del { + display: block; + width: 100%; + margin-top: 10px; + padding: 7px; + background: transparent; + border: 1px solid #f87171; + color: #f87171; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all .15s; +} + +.ip-del:hover { background: #f87171; color: #0f1117; } + +/* ============================================================ + LEAFLET OVERRIDES + ============================================================ */ +.leaflet-popup-content-wrapper { + background: transparent !important; + border-radius: 10px !important; + box-shadow: none !important; + padding: 0 !important; +} + +.leaflet-popup-content { + margin: 0 !important; + line-height: 1 !important; +} + +.leaflet-popup-tip-container { display: none; } + +/* Draw toolbar */ +.leaflet-draw-toolbar a, +.leaflet-bar a { + background-color: #1a1d27 !important; + border-color: #2a2d3a !important; + color: #e2e8f0 !important; +} + +.leaflet-bar a:hover { + background-color: #2a2d3a !important; +} + +.leaflet-draw-toolbar .leaflet-draw-draw-marker, +.leaflet-draw-toolbar .leaflet-draw-draw-polyline, +.leaflet-draw-toolbar .leaflet-draw-draw-polygon { + filter: invert(1) brightness(0.8); +} + +/* ============================================================ + MASJID & PENDUDUK MISKIN + ============================================================ */ + +/* Header subtitle kecil */ +.header-sub { + font-size: 11px; + color: var(--muted); +} + +/* Radius slider di popup */ +.radius-slider-wrap { + margin-top: 8px; +} + +.radius-slider-wrap input[type=range] { + width: 100%; + accent-color: #f59e0b; + cursor: pointer; +} + +.radius-val { + display: flex; + justify-content: space-between; + font-size: 11px; + color: #94a3b8; + margin-top: 2px; + font-family: 'Space Mono', monospace; +} + +/* Badge masjid */ +.badge-amber { background: #451a03; color: #f59e0b; } +.badge-orange { background: #431407; color: #fb923c; } + +/* Warna marker penduduk dalam radius */ +.marker-in-radius path { + fill: #f59e0b !important; +} + +/* Panel info radius di pojok kiri bawah */ +.radius-info-panel { + position: absolute; + bottom: 30px; + left: 10px; + z-index: 900; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 10px; + padding: 10px 14px; + font-size: 12px; + color: var(--text); + min-width: 200px; + display: none; + font-family: 'DM Sans', sans-serif; +} + +.radius-info-panel.show { display: block; } +.radius-info-panel strong { color: #f59e0b; } + +/* ============================================================ + MODE PANEL (pojok kiri bawah) + ============================================================ */ +.mode-panel-box { + background: rgba(26, 29, 39, 0.85); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 12px 14px; + font-family: 'DM Sans', sans-serif; + font-size: 12px; + color: #94a3b8; + min-width: 210px; + box-shadow: 0 8px 32px rgba(0,0,0,.5); + user-select: none; +} + +.mode-panel-box.glass { + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.mode-panel-title { + font-size: 10px; + color: #64748b; + margin-bottom: 10px; + letter-spacing: .08em; + text-transform: uppercase; + font-weight: 700; +} + +.mode-options { + display: flex; + flex-direction: column; + gap: 4px; +} + +.mode-panel-label { + position: relative; + cursor: pointer; + display: block; +} + +.mode-panel-label input[type=radio] { + position: absolute; + opacity: 0; + cursor: pointer; +} + +.mode-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border-radius: 8px; + transition: all .2s cubic-bezier(0.4, 0, 0.2, 1); + border: 1px solid transparent; +} + +.mode-panel-label:hover .mode-item { + background: rgba(255, 255, 255, 0.05); + color: #e2e8f0; +} + +.mode-panel-label input:checked + .mode-item { + background: linear-gradient(135deg, rgba(245, 158, 11, 0.15), rgba(245, 158, 11, 0.05)); + border-color: rgba(245, 158, 11, 0.3); + color: #f59e0b; +} + +.mode-panel-icon { + font-size: 16px; + filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); +} + +/* Pastikan Leaflet control tidak terpotong */ +.leaflet-bottom.leaflet-left { + z-index: 900 !important; + margin-bottom: 10px; + margin-left: 10px; +} + +/* ── BACK BUTTON (Kembali ke landing page) ── */ +.back-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 8px; + background: var(--border); + border: 1px solid rgba(255,255,255,0.08); + color: var(--muted); + text-decoration: none; + font-size: 16px; + font-weight: 700; + flex-shrink: 0; + transition: background 0.18s, color 0.18s, transform 0.15s; +} +.back-btn:hover { + background: rgba(255,255,255,0.1); + color: var(--text); + transform: translateX(-2px); +} + +/* ── ACTIVE STATES untuk Masjid & Penduduk buttons ── */ +.layer-btn.active.btn-masjid { background: #451a03; border-color: #f59e0b; color: #f59e0b; } +.layer-btn.active.btn-penduduk { background: #3b0764; border-color: #e879f9; color: #e879f9; } + +/* ── HEATMAP BUTTON ── */ +.layer-btn.active#btnHeatmap { background: #431407; border-color: #f97316; color: #f97316; } + +/* ── STATS PANEL ── */ +.stat-row { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 6px; +} +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + background: #1e2233; + border-radius: 6px; + padding: 6px 4px; + text-align: center; +} +.stat-num { + font-size: 18px; + font-weight: 700; + color: #e2e8f0; + line-height: 1.1; +} +.stat-lbl { + font-size: 9px; + color: #6b7280; + margin-top: 2px; + line-height: 1.2; +} + +/* ── BLIND SPOT MARKER PULSE ── */ +@keyframes blindspot-pulse { + 0% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.4); opacity: 0.7; } + 100% { transform: scale(1); opacity: 1; } +} +.blindspot-marker svg { + animation: blindspot-pulse 1.2s ease-in-out infinite; + transform-origin: center bottom; +} + +/* ── GLASS effect ── */ +.glass { + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} \ No newline at end of file diff --git a/geometri/index.php b/geometri/index.php new file mode 100644 index 0000000..a226996 --- /dev/null +++ b/geometri/index.php @@ -0,0 +1,47 @@ + + + + + + WebGIS Geometri - Point, Polyline, Polygon + + + + + + + + + + +
+ + +
+ Point, Polyline, Polygon + +
+
+
Point +
+
+
Polyline/Jalan +
+
+
Polygon/Parsil +
+
+
+ +
+ + + + + + + + diff --git a/index.php b/index.php new file mode 100644 index 0000000..cd79f69 --- /dev/null +++ b/index.php @@ -0,0 +1,263 @@ + + + + + + WebGIS Pontianak - Pilih Proyek + + + + + + + + +
+
+
Sistem Informasi Geografis
+

WEBGIS Pontianak

+

Pilih salah satu proyek WebGIS. Tiap folder sudah memiliki dashboard peta mandiri dan tetap memakai koneksi database yang sama.

+
+ +
+ +
G
+
WebGIS Geometri
+

Dashboard untuk menggambar dan mengelola data point, polyline, dan polygon.

+
+ Point + Polyline + Polygon +
+
Buka Geometri
+
+ + +
S
+
WebGIS SPBU
+

Dashboard khusus untuk menambah, melihat, dan menghapus titik lokasi SPBU.

+
+ Titik SPBU + 24 Jam + Tidak 24 Jam +
+
Buka SPBU
+
+ + +
K
+
WebGIS Sosial
+

Dashboard pengentasan kemiskinan berbasis lokasi masjid, penduduk miskin, radius layanan, dan blind spot.

+
+ Masjid + Penduduk Miskin + Analisis Radius +
+
Buka Sosial
+
+
+ + +
+ + diff --git a/js/draw.js b/js/draw.js new file mode 100644 index 0000000..e559217 --- /dev/null +++ b/js/draw.js @@ -0,0 +1,425 @@ +/* ============================================================ + draw.js — WebGIS SPBU Pontianak + Handles: SPBU, Jalan, Parsil, Masjid + Radius, Penduduk Miskin + ============================================================ */ + + var map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13); + L.control.zoom({ position: 'bottomright' }).addTo(map); + + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', maxZoom: 19 + }).addTo(map); + + /* ── LAYER GROUPS ── */ + var drawnItems = new L.FeatureGroup().addTo(map); + var layerGroup24 = L.featureGroup().addTo(map); + var layerGroupTidak = L.featureGroup().addTo(map); + var layerGroupMasjid = L.featureGroup().addTo(map); + var layerGroupPenduduk = L.featureGroup().addTo(map); + + var show24 = true, showTidak = true; + var showMasjid = true, showPenduduk = true; + + /* Storage kalkulasi radius */ + var masjidData = []; + var pendudukData = []; + + let currentLayer = null; + + window.setMode = function(mode) { + console.log('Mode Pointing changed to: ' + mode); + }; + + /* ============================================================ + TOGGLE LAYERS + ============================================================ */ + function toggleLayer(type) { + if (type === '24jam') { + show24 = !show24; + show24 ? map.addLayer(layerGroup24) : map.removeLayer(layerGroup24); + document.getElementById('btn24').classList.toggle('active', show24); + } else if (type === 'tidak') { + showTidak = !showTidak; + showTidak ? map.addLayer(layerGroupTidak) : map.removeLayer(layerGroupTidak); + document.getElementById('btnTidak').classList.toggle('active', showTidak); + } else if (type === 'masjid') { + showMasjid = !showMasjid; + showMasjid ? map.addLayer(layerGroupMasjid) : map.removeLayer(layerGroupMasjid); + document.getElementById('btnMasjid').classList.toggle('active', showMasjid); + } else if (type === 'penduduk') { + showPenduduk = !showPenduduk; + showPenduduk ? map.addLayer(layerGroupPenduduk) : map.removeLayer(layerGroupPenduduk); + document.getElementById('btnPenduduk').classList.toggle('active', showPenduduk); + } + } + + /* ============================================================ + DRAW CONTROL + ============================================================ */ + var drawControl = new L.Control.Draw({ + position: 'topleft', + edit: { featureGroup: drawnItems }, + draw: { + marker: true, + polyline: { shapeOptions: { color: '#a855f7', weight: 3 } }, + polygon: { shapeOptions: { color: '#10b981', weight: 2, fillOpacity: 0.35 } }, + rectangle: false, circle: false, circlemarker: false + } + }); + map.addControl(drawControl); + + /* ============================================================ + HELPERS + ============================================================ */ + function getLineColor(s) { + return s==='Nasional' ? '#ef4444' : s==='Provinsi' ? '#38bdf8' : '#a855f7'; + } + function getLength(ll) { + let t=0; + for(let i=0;i24 Jam` : `Tidak 24 Jam`; + if (type==='line') { let c=s==='Nasional'?'badge-red':s==='Provinsi'?'badge-blue':'badge-gray'; return `${s}`; } + return `${s}`; + } + + /* ── Icons ── */ + function makeIcon(status) { + var c = status==='24 Jam' ? '#4ade80' : '#f87171'; + return L.divIcon({ html:``, iconSize:[26,38], iconAnchor:[13,38], popupAnchor:[0,-40], className:'' }); + } + function makeMasjidIcon() { + return L.divIcon({ html:`🕌`, iconSize:[30,42], iconAnchor:[15,42], popupAnchor:[0,-44], className:'' }); + } + function makePendudukIcon(inRadius) { + var c = inRadius ? '#f59e0b' : '#e879f9'; + return L.divIcon({ html:``, iconSize:[22,32], iconAnchor:[11,32], popupAnchor:[0,-34], className:'' }); + } + + /* ============================================================ + RADIUS LOGIC — cek semua penduduk vs semua masjid + Jika kena ≥2 radius → pilih masjid terdekat + ============================================================ */ + function recalcAllRadius(previewM = null) { + pendudukData.forEach(p => { + var inAny = false; + var minDist = Infinity; + + // Cek semua masjid yang sudah tersimpan + masjidData.forEach(m => { + var d = map.distance([p.lat, p.lng], [m.lat, m.lng]); + if (d <= m.radius) { inAny = true; if (d < minDist) minDist = d; } + }); + + // Cek juga masjid yang sedang di-preview (sebelum simpan) + if (previewM) { + var d = map.distance([p.lat, p.lng], [previewM.lat, previewM.lng]); + if (d <= previewM.radius) { inAny = true; if (d < minDist) minDist = d; } + } + + // Update icon jika status "dalam radius" berubah + if (p.inRadius !== inAny) { + p.inRadius = inAny; + p.markerLayer.setIcon(makePendudukIcon(inAny)); + } + }); + } + + /* ============================================================ + POPUP BUILDERS + ============================================================ */ + function buildInfoPopup(d, type) { + const hc = { point:'linear-gradient(135deg,#1d4ed8,#3b82f6)', line:'linear-gradient(135deg,#7c3aed,#a855f7)', polygon:'linear-gradient(135deg,#065f46,#10b981)', masjid:'linear-gradient(135deg,#92400e,#f59e0b)', penduduk:'linear-gradient(135deg,#701a75,#e879f9)' }; + const ic = { point:'⛽', line:'🛣️', polygon:'📐', masjid:'🕌', penduduk:'👨‍👩‍👧‍👦' }; + const ti = { point:`SPBU — ${d.nama}`, line:`Jalan — ${d.nama}`, polygon:`Parsil — ${d.nama}`, masjid:`Masjid — ${d.nama}`, penduduk:`KK — ${d.nama_kk}` }; + + let rows = ''; + if (type==='point') { + rows = `
No. SPBU${d.no_spbu}
+
Status${statusBadge(d.status,'point')}
+
Koordinat${parseFloat(d.latitude).toFixed(5)}, ${parseFloat(d.longitude).toFixed(5)}
`; + } else if (type==='line') { + rows = `
Status${statusBadge(d.status,'line')}
+
Panjang${fmtNum(d.panjang)} m
`; + } else if (type==='polygon') { + rows = `
Status${statusBadge(d.status,'polygon')}
+
Luas${fmtNum(d.luas)} m²
`; + } else if (type==='masjid') { + rows = `
PIC${d.pic}
+
Radius${fmtNum(d.radius,0)} m
+
+ +
100m5000m
+
+ `; + } else if (type==='penduduk') { + rows = `
Jml. Anggota KK${d.jumlah_anggota} orang
+
Alamat${d.alamat}
+
Koordinat${parseFloat(d.latitude).toFixed(5)}, ${parseFloat(d.longitude).toFixed(5)}
`; + } + + return `
${ic[type]} ${ti[type]}
${rows}
`; + } + + function onRadiusSlide(masjidId, val) { + val = parseFloat(val); + var el = document.getElementById('radiusDisplay_' + masjidId); + if (el) el.textContent = fmtNum(val,0) + ' m'; + var m = masjidData.find(x => x.id === masjidId); + if (m) { m.radius = val; m.circleLayer.setRadius(val); recalcAllRadius(); } + } + + function saveRadius(masjidId) { + var m = masjidData.find(x => x.id === masjidId); + if (!m) return; + fetch('php/update_masjid_radius.php', { + method:'POST', headers:{'Content-Type':'application/json'}, + body: JSON.stringify({ id: masjidId, radius: m.radius }) + }).then(r=>r.json()).then(r=>{ + if (r.message && r.message.includes('Berhasil')) alert('Radius berhasil disimpan!'); + else alert('Gagal: ' + r.message); + }); + } + + /* ============================================================ + LOAD DATA + ============================================================ */ + fetch('php/show.php') + .then(res => res.json()) + .then(data => { + var pendudukRaw = []; + + data.forEach(d => { + if (d.type === 'point') { + var layer = L.marker([d.latitude, d.longitude], { icon: makeIcon(d.status) }); + layer._type='point'; layer._db_id=d.id; + layer.bindPopup(buildInfoPopup(d,'point'), { maxWidth:260 }); + d.status==='24 Jam' ? layerGroup24.addLayer(layer) : layerGroupTidak.addLayer(layer); + } + if (d.type === 'line') { + var coords = JSON.parse(d.geom); + var layer = L.polyline(coords, { color:getLineColor(d.status), weight:4, opacity:0.9 }); + layer._type='line'; layer._db_id=d.id; + layer.bindPopup(buildInfoPopup(d,'line'), { maxWidth:260 }); + drawnItems.addLayer(layer); + } + if (d.type === 'polygon') { + var coords = JSON.parse(d.geom); + var layer = L.polygon(coords, { color:'#10b981', weight:2, fillColor:'#10b981', fillOpacity:0.3 }); + layer._type='polygon'; layer._db_id=d.id; + layer.bindPopup(buildInfoPopup(d,'polygon'), { maxWidth:260 }); + drawnItems.addLayer(layer); + } + if (d.type === 'masjid') { + var mLat=parseFloat(d.latitude), mLng=parseFloat(d.longitude), mR=parseFloat(d.radius); + var circle = L.circle([mLat,mLng], { radius:mR, color:'#f59e0b', fillColor:'#f59e0b', fillOpacity:0.08, weight:2, dashArray:'6 4' }); + layerGroupMasjid.addLayer(circle); + var marker = L.marker([mLat,mLng], { icon:makeMasjidIcon() }); + marker._type='masjid'; marker._db_id=d.id; + marker.bindPopup(buildInfoPopup(d,'masjid'), { maxWidth:290 }); + layerGroupMasjid.addLayer(marker); + masjidData.push({ id:parseInt(d.id), lat:mLat, lng:mLng, radius:mR, circleLayer:circle, markerLayer:marker }); + } + if (d.type === 'penduduk') pendudukRaw.push(d); + }); + + /* Load penduduk setelah masjid siap */ + pendudukRaw.forEach(d => { + var pLat=parseFloat(d.latitude), pLng=parseFloat(d.longitude); + var inRadius=false, minDist=Infinity; + masjidData.forEach(m => { + var dist = map.distance([pLat,pLng],[m.lat,m.lng]); + if (dist<=m.radius && dist console.error('Gagal load data:', err)); + + /* ============================================================ + HAPUS + ============================================================ */ + function hapusData(id, type) { + if (!confirm('Yakin ingin menghapus?')) return; + fetch(`php/delete.php?id=${id}&type=${type}`) + .then(r=>r.json()).then(r=>{ + if (r.message && r.message.toLowerCase().includes('berhasil')) location.reload(); + else alert('Gagal: ' + (r.error||r.message)); + }); + } + + /* ============================================================ + MODE SELECTOR (pojok kiri bawah) + ============================================================ */ + + /* ============================================================ + ON CREATED + ============================================================ */ + map.on(L.Draw.Event.CREATED, function(e) { + var layer = e.layer; + + // Get mode directly from the DOM to ensure reliability + var activeMode = 'spbu'; + var modeInput = document.querySelector('input[name="mode"]:checked'); + if (modeInput) activeMode = modeInput.value; + + console.log('Creating marker in mode: ' + activeMode); + + if (e.layerType === 'marker') { + if (activeMode==='spbu') { drawnItems.addLayer(layer); handlePoint(layer); } + else if (activeMode==='masjid') { handleMasjid(layer); } + else if (activeMode==='penduduk') { handlePenduduk(layer); } + } + if (e.layerType==='polyline') { drawnItems.addLayer(layer); handleLine(layer); } + if (e.layerType==='polygon') { drawnItems.addLayer(layer); handlePolygon(layer); } + }); + + /* ── SPBU ── */ + function handlePoint(layer) { + var ll = layer.getLatLng(); + layer.bindPopup(`
⛽ Tambah SPBU
+ + + + +
📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}
+ +
`, { maxWidth:280 }).openPopup(); + } + function savePoint(lat, lng) { + var nama=document.getElementById('pNama').value.trim(), no=document.getElementById('pNo').value.trim(), status=document.getElementById('pStatus').value; + if (!nama||!no) { alert('Nama dan No. SPBU wajib diisi!'); return; } + fetch('php/insert.php',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({nama,no,status,lat,lng}) }) + .then(r=>r.json()).then(r=>{ if(r.message&&r.message.includes('berhasil')) location.reload(); else alert('Gagal: '+r.message); }); + } + + /* ── MASJID ── */ + function handleMasjid(layer) { + var ll = layer.getLatLng(); + var defaultRadius = 500; + window._previewLatLng = ll; + + // Tambahkan marker ke peta agar popup bisa muncul + layer.addTo(map); + + var previewCircle = L.circle([ll.lat,ll.lng], { radius:defaultRadius, color:'#f59e0b', fillColor:'#f59e0b', fillOpacity:0.08, weight:2, dashArray:'6 4' }).addTo(map); + window._previewCircle = previewCircle; + + recalcAllRadius({ lat:ll.lat, lng:ll.lng, radius:defaultRadius }); + + layer.bindPopup(`
🕌 Tambah Masjid
+ + + +
+ +
100m${defaultRadius} m5000m
+
+
📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}
+ +
`, { maxWidth:290 }).openPopup(); + + layer.on('popupclose', function() { + map.removeLayer(previewCircle); + window._previewCircle=null; + window._previewLatLng=null; + map.removeLayer(layer); + recalcAllRadius(); + }); + } + function previewRadius(val) { + val = parseFloat(val); + document.getElementById('mRadiusVal').textContent = fmtNum(val,0)+' m'; + if (window._previewCircle) window._previewCircle.setRadius(val); + if (window._previewLatLng) { + recalcAllRadius({ lat:window._previewLatLng.lat, lng:window._previewLatLng.lng, radius:val }); + } + } + function saveMasjid(lat, lng) { + var nama=document.getElementById('mNama').value.trim(), pic=document.getElementById('mPIC').value.trim(), radius=parseFloat(document.getElementById('mRadiusSlider').value); + if (!nama||!pic) { alert('Nama dan PIC wajib diisi!'); return; } + fetch('php/insert_masjid.php',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({nama,pic,radius,lat,lng}) }) + .then(r=>r.json()).then(r=>{ if(r.id) location.reload(); else alert('Gagal: '+(r.error||r.message)); }); + } + + /* ── PENDUDUK ── */ + function handlePenduduk(layer) { + var ll = layer.getLatLng(); + layer.addTo(map); // Tambahkan marker ke peta agar popup bisa muncul + + layer.bindPopup(`
👨‍👩‍👧‍👦 Tambah Penduduk Miskin
+ + + +
📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}
+ +
`, { maxWidth:290 }).openPopup(); + layer.on('popupclose', function() { map.removeLayer(layer); }); + } + function savePenduduk(lat, lng) { + var nama_kk=document.getElementById('pdNama').value.trim(), jumlah_anggota=parseInt(document.getElementById('pdJml').value)||1, alamat=document.getElementById('pdAlamat').value.trim(); + if (!nama_kk||!alamat) { alert('Nama KK dan Alamat wajib diisi!'); return; } + fetch('php/insert_penduduk.php',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({nama_kk,jumlah_anggota,alamat,lat,lng}) }) + .then(r=>r.json()).then(r=>{ if(r.id) location.reload(); else alert('Gagal: '+(r.error||r.message)); }); + } + + /* ── LINE ── */ + function handleLine(layer) { + currentLayer=layer; + var ll=layer.getLatLngs(), p=getLength(ll); layer._temp={latlngs:ll,panjang:p}; + L.popup({maxWidth:280}).setLatLng(layer.getBounds().getCenter()).setContent(`
🛣️ Tambah Jalan
+ + +
📏 ${fmtNum(p)} m
+ +
`).openOn(map); + } + function saveLine() { + var layer=currentLayer; if(!layer){alert('Layer tidak ditemukan');return;} + var nama=document.getElementById('lNama').value.trim(), status=document.getElementById('lStatus').value; + if(!nama){alert('Nama wajib diisi!');return;} + fetch('php/insert_line.php',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({nama,status,panjang:layer._temp.panjang,geom:JSON.stringify(layer._temp.latlngs)})}) + .then(r=>r.json()).then(r=>{ if(r.id){layer._db_id=r.id;layer._type='line';map.closePopup();alert('Berhasil!');} else alert('Gagal: '+(r.error||r.message)); }); + } + + /* ── POLYGON ── */ + function handlePolygon(layer) { + currentLayer=layer; + var ll=layer.getLatLngs(), luas=L.GeometryUtil.geodesicArea(ll[0]); layer._temp={latlngs:ll,luas}; + L.popup({maxWidth:280}).setLatLng(layer.getBounds().getCenter()).setContent(`
📐 Tambah Parsil
+ + +
📐 ${fmtNum(luas)} m²
+ +
`).openOn(map); + } + function savePolygon() { + var layer=currentLayer; if(!layer){alert('Layer tidak ditemukan');return;} + var nama=document.getElementById('gNama').value.trim(), status=document.getElementById('gStatus').value; + if(!nama){alert('Nama wajib diisi!');return;} + fetch('php/insert_polygon.php',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({nama,status,luas:layer._temp.luas,geom:JSON.stringify(layer._temp.latlngs)})}) + .then(r=>r.json()).then(r=>{ if(r.id){layer._db_id=r.id;layer._type='polygon';map.closePopup();alert('Berhasil!');} else alert('Gagal: '+(r.error||r.message)); }); + } + + /* ── EDIT & DELETE ── */ + map.on('draw:edited', function(e) { + e.layers.eachLayer(layer => { + if (!layer._db_id) return; + if (layer._type==='line') fetch('php/update_line.php',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:layer._db_id,geom:JSON.stringify(layer.getLatLngs()),panjang:getLength(layer.getLatLngs())})}); + if (layer._type==='polygon') fetch('php/update_polygon.php',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:layer._db_id,geom:JSON.stringify(layer.getLatLngs()),luas:L.GeometryUtil.geodesicArea(layer.getLatLngs()[0])})}); + }); + }); + map.on('draw:deleted', function(e) { + e.layers.eachLayer(layer => { + if (layer._db_id&&layer._type) fetch(`php/delete.php?id=${layer._db_id}&type=${layer._type}`); + }); + }); \ No newline at end of file diff --git a/js/draw_geometri.js b/js/draw_geometri.js new file mode 100644 index 0000000..5ed1537 --- /dev/null +++ b/js/draw_geometri.js @@ -0,0 +1,331 @@ +/* ============================================================ + draw_geometri.js - WebGIS Point, Polyline, Polygon + ============================================================ */ + +var map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13); +L.control.zoom({ position: 'bottomright' }).addTo(map); + +L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + maxZoom: 19 +}).addTo(map); + +var drawnItems = new L.FeatureGroup().addTo(map); +var layerGroupPoint = L.featureGroup().addTo(map); +var currentLayer = null; + +var drawControl = new L.Control.Draw({ + position: 'topleft', + edit: { featureGroup: drawnItems }, + draw: { + marker: true, + polyline: { shapeOptions: { color: '#a855f7', weight: 3 } }, + polygon: { shapeOptions: { color: '#10b981', weight: 2, fillOpacity: 0.35 } }, + rectangle: false, + circle: false, + circlemarker: false + } +}); +map.addControl(drawControl); + +function getLineColor(status) { + return status === 'Nasional' ? '#ef4444' : status === 'Provinsi' ? '#38bdf8' : '#a855f7'; +} + +function getLength(latlngs) { + var total = 0; + for (var i = 0; i < latlngs.length - 1; i++) { + total += latlngs[i].distanceTo(latlngs[i + 1]); + } + return total; +} + +function fmtNum(n, dec = 2) { + return parseFloat(n).toLocaleString('id-ID', { maximumFractionDigits: dec }); +} + +function statusBadge(status, type) { + if (type === 'line') { + var c = status === 'Nasional' ? 'badge-red' : status === 'Provinsi' ? 'badge-blue' : 'badge-gray'; + return `${status}`; + } + return `${status}`; +} + +function makePointIcon() { + var c = '#3b82f6'; + return L.divIcon({ + html: ``, + iconSize: [26, 38], + iconAnchor: [13, 38], + popupAnchor: [0, -40], + className: '' + }); +} + +function buildInfoPopup(d, type) { + var headerColors = { + point: 'linear-gradient(135deg,#1d4ed8,#3b82f6)', + line: 'linear-gradient(135deg,#7c3aed,#a855f7)', + polygon: 'linear-gradient(135deg,#065f46,#10b981)' + }; + var icons = { point: '📍', line: '▱', polygon: '▰' }; + var titles = { + point: `Point - ${d.nama}`, + line: `Jalan - ${d.nama}`, + polygon: `Parsil - ${d.nama}` + }; + + var rows = ''; + if (type === 'point') { + rows = `
Nama${d.nama}
+
Kode Point${d.no_spbu}
`; + } else if (type === 'line') { + rows = `
Status${statusBadge(d.status, 'line')}
+
Panjang${fmtNum(d.panjang)} m
`; + } else if (type === 'polygon') { + rows = `
Status${statusBadge(d.status, 'polygon')}
+
Luas${fmtNum(d.luas)} m²
`; + } + + return `
+
${icons[type]} ${titles[type]}
+
${rows}
+
`; +} + +fetch('../php/show_geometri.php') + .then(res => res.json()) + .then(data => { + data.forEach(d => { + if (d.type === 'point') { + var pointLayer = L.marker([d.latitude, d.longitude], { icon: makePointIcon() }); + pointLayer._type = 'point'; + pointLayer._db_id = d.id; + pointLayer.bindPopup(buildInfoPopup(d, 'point'), { maxWidth: 260 }); + layerGroupPoint.addLayer(pointLayer); + } + + if (d.type === 'line') { + var lineCoords = JSON.parse(d.geom); + var lineLayer = L.polyline(lineCoords, { color: getLineColor(d.status), weight: 4, opacity: 0.9 }); + lineLayer._type = 'line'; + lineLayer._db_id = d.id; + lineLayer.bindPopup(buildInfoPopup(d, 'line'), { maxWidth: 260 }); + drawnItems.addLayer(lineLayer); + } + + if (d.type === 'polygon') { + var polygonCoords = JSON.parse(d.geom); + var polygonLayer = L.polygon(polygonCoords, { + color: '#10b981', + weight: 2, + fillColor: '#10b981', + fillOpacity: 0.3 + }); + polygonLayer._type = 'polygon'; + polygonLayer._db_id = d.id; + polygonLayer.bindPopup(buildInfoPopup(d, 'polygon'), { maxWidth: 260 }); + drawnItems.addLayer(polygonLayer); + } + }); + }) + .catch(err => console.error('Gagal load data:', err)); + +function hapusData(id, type) { + if (!confirm('Yakin ingin menghapus?')) return; + fetch(`../php/delete.php?id=${id}&type=${type}`) + .then(r => r.json()) + .then(r => { + if (r.message && r.message.toLowerCase().includes('berhasil')) location.reload(); + else alert('Gagal: ' + (r.error || r.message)); + }); +} + +map.on(L.Draw.Event.CREATED, function(e) { + var layer = e.layer; + + if (e.layerType === 'marker') { + drawnItems.addLayer(layer); + handlePoint(layer); + } + if (e.layerType === 'polyline') { + drawnItems.addLayer(layer); + handleLine(layer); + } + if (e.layerType === 'polygon') { + drawnItems.addLayer(layer); + handlePolygon(layer); + } +}); + +function handlePoint(layer) { + var ll = layer.getLatLng(); + layer.bindPopup(`
+
📍 Tambah Point
+
+ + +
📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}
+ +
+
`, { maxWidth: 280 }).openPopup(); +} + +function savePoint(lat, lng) { + var nama = document.getElementById('pNama').value.trim(); + var no = document.getElementById('pNo').value.trim(); + var status = '24 Jam'; + if (!nama || !no) { + alert('Nama dan kode point wajib diisi!'); + return; + } + fetch('../php/insert.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nama, no, status, lat, lng }) + }) + .then(r => r.json()) + .then(r => { + if (r.message && r.message.includes('berhasil')) location.reload(); + else alert('Gagal: ' + (r.error || r.message)); + }); +} + +function handleLine(layer) { + currentLayer = layer; + var latlngs = layer.getLatLngs(); + var panjang = getLength(latlngs); + layer._temp = { latlngs, panjang }; + L.popup({ maxWidth: 280 }).setLatLng(layer.getBounds().getCenter()).setContent(`
+
▱ Tambah Jalan
+
+ + +
${fmtNum(panjang)} m
+ +
+
`).openOn(map); +} + +function saveLine() { + var layer = currentLayer; + if (!layer) { + alert('Layer tidak ditemukan'); + return; + } + var nama = document.getElementById('lNama').value.trim(); + var status = document.getElementById('lStatus').value; + if (!nama) { + alert('Nama wajib diisi!'); + return; + } + fetch('../php/insert_line.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + nama, + status, + panjang: layer._temp.panjang, + geom: JSON.stringify(layer._temp.latlngs) + }) + }) + .then(r => r.json()) + .then(r => { + if (r.id) { + layer._db_id = r.id; + layer._type = 'line'; + map.closePopup(); + alert('Berhasil!'); + } else { + alert('Gagal: ' + (r.error || r.message)); + } + }); +} + +function handlePolygon(layer) { + currentLayer = layer; + var latlngs = layer.getLatLngs(); + var luas = L.GeometryUtil.geodesicArea(latlngs[0]); + layer._temp = { latlngs, luas }; + L.popup({ maxWidth: 280 }).setLatLng(layer.getBounds().getCenter()).setContent(`
+
▰ Tambah Parsil
+
+ + +
${fmtNum(luas)} m²
+ +
+
`).openOn(map); +} + +function savePolygon() { + var layer = currentLayer; + if (!layer) { + alert('Layer tidak ditemukan'); + return; + } + var nama = document.getElementById('gNama').value.trim(); + var status = document.getElementById('gStatus').value; + if (!nama) { + alert('Nama wajib diisi!'); + return; + } + fetch('../php/insert_polygon.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + nama, + status, + luas: layer._temp.luas, + geom: JSON.stringify(layer._temp.latlngs) + }) + }) + .then(r => r.json()) + .then(r => { + if (r.id) { + layer._db_id = r.id; + layer._type = 'polygon'; + map.closePopup(); + alert('Berhasil!'); + } else { + alert('Gagal: ' + (r.error || r.message)); + } + }); +} + +map.on('draw:edited', function(e) { + e.layers.eachLayer(layer => { + if (!layer._db_id) return; + if (layer._type === 'line') { + fetch('../php/update_line.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: layer._db_id, + geom: JSON.stringify(layer.getLatLngs()), + panjang: getLength(layer.getLatLngs()) + }) + }); + } + if (layer._type === 'polygon') { + fetch('../php/update_polygon.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: layer._db_id, + geom: JSON.stringify(layer.getLatLngs()), + luas: L.GeometryUtil.geodesicArea(layer.getLatLngs()[0]) + }) + }); + } + }); +}); + +map.on('draw:deleted', function(e) { + e.layers.eachLayer(layer => { + if (layer._db_id && layer._type) { + fetch(`../php/delete.php?id=${layer._db_id}&type=${layer._type}`); + } + }); +}); diff --git a/js/draw_sosial.js b/js/draw_sosial.js new file mode 100644 index 0000000..b3ea5e4 --- /dev/null +++ b/js/draw_sosial.js @@ -0,0 +1,584 @@ +/* ============================================================ + draw_sosial.js — WebGIS Pendataan Sosial Pontianak + v3.0 — Smart DSS: Heatmap, Masjid Penanggung, Bantuan, Blind Spot + ============================================================ */ + +/* ── MAP INIT ── */ +var map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13); +L.control.zoom({ position: 'bottomright' }).addTo(map); +L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', maxZoom: 19 +}).addTo(map); + +/* ── LAYER GROUPS ── */ +var layerGroupMasjid = L.featureGroup().addTo(map); +var layerGroupPenduduk = L.featureGroup().addTo(map); +var heatmapLayer = null; // inisialisasi setelah data dimuat +var showMasjid = true; +var showPenduduk = true; +var showHeatmap = false; + +/* ── DATA STORAGE ── */ +var masjidData = []; +var pendudukData = []; +var blindSpotIds = []; + +/* ── CONSTANTS ── */ +const JENIS_BANTUAN_OPTS = [ + { value: 'BLT / Modal Usaha', icon: '💰' }, + { value: 'Bedah Rumah', icon: '🏠' }, + { value: 'Air Bersih / PDAM', icon: '💧' }, + { value: 'Sembako', icon: '🍚' }, + { value: 'Beasiswa Pendidikan', icon: '📚' }, +]; + +function getJenisIcon(jenis) { + const found = JENIS_BANTUAN_OPTS.find(o => o.value === jenis); + return found ? found.icon : '🤝'; +} + +/* ============================================================ + TOGGLE LAYERS + ============================================================ */ +function toggleLayer(type) { + if (type === 'masjid') { + showMasjid = !showMasjid; + showMasjid ? map.addLayer(layerGroupMasjid) : map.removeLayer(layerGroupMasjid); + document.getElementById('btnMasjid').classList.toggle('active', showMasjid); + } else if (type === 'penduduk') { + showPenduduk = !showPenduduk; + showPenduduk ? map.addLayer(layerGroupPenduduk) : map.removeLayer(layerGroupPenduduk); + document.getElementById('btnPenduduk').classList.toggle('active', showPenduduk); + if (!showPenduduk && showHeatmap) toggleHeatmap(); // heatmap off jika penduduk off + } else if (type === 'heatmap') { + toggleHeatmap(); + } +} + +function toggleHeatmap() { + showHeatmap = !showHeatmap; + var btn = document.getElementById('btnHeatmap'); + if (showHeatmap) { + // Heatmap ON: sembunyikan marker penduduk + map.removeLayer(layerGroupPenduduk); + showPenduduk = false; + document.getElementById('btnPenduduk').classList.remove('active'); + if (heatmapLayer) map.addLayer(heatmapLayer); + if (btn) btn.classList.add('active'); + } else { + // Heatmap OFF: kembalikan marker penduduk + if (heatmapLayer) map.removeLayer(heatmapLayer); + map.addLayer(layerGroupPenduduk); + showPenduduk = true; + document.getElementById('btnPenduduk').classList.add('active'); + if (btn) btn.classList.remove('active'); + } +} + +/* ============================================================ + DRAW CONTROL — hanya Marker + ============================================================ */ +var drawnItems = new L.FeatureGroup().addTo(map); +var drawControl = new L.Control.Draw({ + position: 'topleft', + edit: { featureGroup: drawnItems }, + draw: { marker:true, polyline:false, polygon:false, rectangle:false, circle:false, circlemarker:false } +}); +map.addControl(drawControl); + +/* ============================================================ + HELPERS + ============================================================ */ +function fmtNum(n, dec=2) { + return parseFloat(n).toLocaleString('id-ID', { maximumFractionDigits: dec }); +} + +function getSkorLabel(skor) { + if (skor >= 70) return { label: 'Sangat Rentan', color: '#ef4444', emoji: '🔴' }; + if (skor >= 40) return { label: 'Rentan', color: '#f97316', emoji: '🟠' }; + return { label: 'Cukup', color: '#eab308', emoji: '🟡' }; +} + +function findMasjidPenanggung(pLat, pLng) { + var nearest = null; + var minDist = Infinity; + masjidData.forEach(m => { + var d = map.distance([pLat, pLng], [m.lat, m.lng]); + if (d <= m.radius && d < minDist) { minDist = d; nearest = m; } + }); + return nearest; +} + +/* ── Icons ── */ +function makeMasjidIcon() { + return L.divIcon({ html:`🕌`, iconSize:[30,42], iconAnchor:[15,42], popupAnchor:[0,-44], className:'' }); +} +function makePendudukIcon(inRadius, isBlindspot=false) { + var c = isBlindspot ? '#ef4444' : (inRadius ? '#f59e0b' : '#e879f9'); + return L.divIcon({ html:``, iconSize:[22,32], iconAnchor:[11,32], popupAnchor:[0,-34], className: isBlindspot ? 'blindspot-marker' : '' }); +} + +/* ============================================================ + RADIUS LOGIC + ============================================================ */ +function recalcAllRadius(previewM = null) { + pendudukData.forEach(p => { + var inAny = false; + var minDist = Infinity; + masjidData.forEach(m => { + var d = map.distance([p.lat, p.lng], [m.lat, m.lng]); + if (d <= m.radius) { inAny = true; if (d < minDist) minDist = d; } + }); + if (previewM) { + var d = map.distance([p.lat, p.lng], [previewM.lat, previewM.lng]); + if (d <= previewM.radius) { inAny = true; if (d < minDist) minDist = d; } + } + if (p.inRadius !== inAny) { + p.inRadius = inAny; + p.markerLayer.setIcon(makePendudukIcon(inAny)); + } + }); +} + +/* ============================================================ + POPUP BUILDERS + ============================================================ */ + +/* ── POPUP MASJID (2 Tab) ── */ +function buildMasjidPopup(d) { + // Cari daftar penerima di dalam radius masjid ini + var penerima = pendudukData + .map(p => { + var dist = map.distance([p.lat, p.lng], [d.lat, d.lng]); + return { ...p, dist, rawData: p.rawData }; + }) + .filter(p => p.dist <= d.radius) + .sort((a,b) => b.skor - a.skor); + + var rowsPenerima = ''; + if (penerima.length === 0) { + rowsPenerima = `
Belum ada penduduk dalam radius ini.
`; + } else { + penerima.forEach((p, i) => { + var sl = getSkorLabel(p.skor); + rowsPenerima += ` +
+ ${sl.emoji} +
+
${p.nama || '—'}
+
Skor: ${fmtNum(p.skor,1)} · ${sl.label}
+
+ ${getJenisIcon(p.jenis)} +
`; + }); + } + + return `
+
🕌 ${d.nama}
+
+ + +
+
+
PIC${d.pic}
+
Radius${fmtNum(d.radius,0)} m
+
+ +
100m5000m
+
+ + +
+ +
`; +} + +function switchTab(id, tab) { + document.getElementById('tabContent1_' + id).style.display = tab === 1 ? '' : 'none'; + document.getElementById('tabContent2_' + id).style.display = tab === 2 ? '' : 'none'; + document.getElementById('tab1_' + id).style.background = tab === 1 ? '#1e2233' : 'transparent'; + document.getElementById('tab1_' + id).style.color = tab === 1 ? '#f59e0b' : '#6b7280'; + document.getElementById('tab2_' + id).style.background = tab === 2 ? '#1e2233' : 'transparent'; + document.getElementById('tab2_' + id).style.color = tab === 2 ? '#f59e0b' : '#6b7280'; +} + +/* ── POPUP PENDUDUK ── */ +function buildPendudukPopup(d) { + var skor = parseFloat(d.skor_kerentanan) || 0; + var sl = getSkorLabel(skor); + var pLat = parseFloat(d.latitude), pLng = parseFloat(d.longitude); + + // Cari masjid penanggung + var penanggung = findMasjidPenanggung(pLat, pLng); + var penanggungHtml = penanggung + ? `
Masjid Penanggung🕌 ${penanggung.nama}
` + : `
Status⚠️ Blind Spot
`; + + // Dropdown jenis bantuan + var optionsHtml = JENIS_BANTUAN_OPTS.map(o => + `` + ).join(''); + + var selectStyle = `width:100%;padding:6px;border-radius:6px;border:1px solid #1e2233;background:#0a0d14;color:#e2e8f0;font-size:12px;`; + + return `
+
👨‍👩‍👧‍👦 KK — ${d.nama_kk}
+
+
Jml. Anggota KK${d.jumlah_anggota} orang
+
Alamat${d.alamat}
+
Penghasilan${d.penghasilan}
+
Kondisi Rumah${d.kondisi_rumah}
+
Sumber Air${d.sumber_air}
+
+ Skor Kerentanan + ${sl.emoji} ${fmtNum(skor,1)} — ${sl.label} +
+ ${penanggungHtml} +
+ + + +
+ +
+
`; +} + +function saveJenisBantuan(id) { + var sel = document.getElementById('jenisBantuan_' + id); + if (!sel) return; + fetch('../php/update_penduduk.php', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id, jenis_bantuan: sel.value }) + }).then(r => r.json()).then(r => { + if (r.message && r.message.includes('Berhasil')) { + // Update local data + var p = pendudukData.find(x => x.id === id); + if (p) p.jenis = sel.value; + renderStats(); + } else { + alert('Gagal: ' + r.message); + } + }); +} + +/* ── RADIUS SLIDER ── */ +function onRadiusSlide(masjidId, val) { + val = parseFloat(val); + var el = document.getElementById('radiusDisplay_' + masjidId); + if (el) el.textContent = fmtNum(val,0) + ' m'; + var m = masjidData.find(x => x.id === masjidId); + if (m) { m.radius = val; m.circleLayer.setRadius(val); recalcAllRadius(); } +} + +function saveRadius(masjidId) { + var m = masjidData.find(x => x.id === masjidId); + if (!m) return; + fetch('../php/update_masjid.php', { + method:'POST', headers:{'Content-Type':'application/json'}, + body: JSON.stringify({ id: masjidId, radius: m.radius }) + }).then(r => r.json()).then(r => { + if (r.message && r.message.includes('Berhasil')) alert('Radius berhasil disimpan!'); + else alert('Gagal: ' + r.message); + }); +} + +/* ============================================================ + LOAD DATA + ============================================================ */ +fetch('../php/show_sosial.php') + .then(res => res.json()) + .then(data => { + var pendudukRaw = []; + + data.forEach(d => { + if (d.type === 'masjid') { + var mLat=parseFloat(d.latitude), mLng=parseFloat(d.longitude); + var mR = parseFloat(d.radius_layanan) || parseFloat(d.radius) || 500; + var circle = L.circle([mLat,mLng], { radius:mR, color:'#f59e0b', fillColor:'#f59e0b', fillOpacity:0.08, weight:2, dashArray:'6 4' }); + layerGroupMasjid.addLayer(circle); + var mEntry = { id:parseInt(d.id), lat:mLat, lng:mLng, radius:mR, nama:d.nama, pic:d.pic, circleLayer:circle }; + masjidData.push(mEntry); + var marker = L.marker([mLat,mLng], { icon:makeMasjidIcon() }); + marker._type='masjid'; marker._db_id=d.id; + mEntry.markerLayer = marker; + // Bind popup after pendudukData is populated (defer) + layerGroupMasjid.addLayer(marker); + } + if (d.type === 'penduduk') pendudukRaw.push(d); + }); + + /* Load penduduk setelah masjid siap */ + var heatPoints = []; + pendudukRaw.forEach(d => { + var pLat=parseFloat(d.latitude), pLng=parseFloat(d.longitude); + var skor = parseFloat(d.skor_kerentanan) || 0; + var inRadius=false, minDist=Infinity; + masjidData.forEach(m => { + var dist = map.distance([pLat,pLng],[m.lat,m.lng]); + if (dist<=m.radius && dist buildPendudukPopup(d), { maxWidth:300 }); + layerGroupPenduduk.addLayer(marker); + + // Heatmap point: [lat, lng, intensity 0-1] + heatPoints.push([pLat, pLng, skor / 100]); + }); + + // Now bind masjid popups (pendudukData is ready) + masjidData.forEach(m => { + m.markerLayer.bindPopup(() => buildMasjidPopup(m), { maxWidth:310 }); + }); + + // Init heatmap layer + if (typeof L.heatLayer !== 'undefined') { + heatmapLayer = L.heatLayer(heatPoints, { + radius: 35, blur: 25, maxZoom: 16, + gradient: { 0.0: '#3b82f6', 0.4: '#eab308', 0.7: '#f97316', 1.0: '#ef4444' } + }); + } + + // Post-load tasks + checkBlindSpots(); + renderStats(); + }) + .catch(err => console.error('Gagal load data:', err)); + +/* ============================================================ + BLIND SPOT DETECTION + ============================================================ */ +function checkBlindSpots() { + blindSpotIds = []; + pendudukData.forEach(p => { + if (!p.inRadius && p.skor >= 70) { + blindSpotIds.push(p.id); + // Mark icon merah + p.markerLayer.setIcon(makePendudukIcon(false, true)); + } + }); + + var panel = document.getElementById('blindSpotPanel'); + if (!panel) return; + if (blindSpotIds.length > 0) { + panel.style.display = 'flex'; + document.getElementById('blindSpotCount').textContent = blindSpotIds.length; + } else { + panel.style.display = 'none'; + } +} + +function zoomToBlindSpots() { + if (blindSpotIds.length === 0) return; + var bounds = []; + pendudukData.forEach(p => { + if (blindSpotIds.includes(p.id)) bounds.push([p.lat, p.lng]); + }); + if (bounds.length > 0) map.fitBounds(bounds, { padding: [60,60] }); +} + +/* ============================================================ + STATS PANEL + ============================================================ */ +function renderStats() { + var total = pendudukData.length; + var sangat = pendudukData.filter(p => p.skor >= 70).length; + var rentan = pendudukData.filter(p => p.skor >= 40 && p.skor < 70).length; + var cukup = pendudukData.filter(p => p.skor < 40).length; + var bs = blindSpotIds.length; + + var jenisCounts = {}; + JENIS_BANTUAN_OPTS.forEach(o => jenisCounts[o.value] = 0); + pendudukData.forEach(p => { + if (jenisCounts[p.jenis] !== undefined) jenisCounts[p.jenis]++; + else jenisCounts['BLT / Modal Usaha']++; + }); + + var jenisRows = JENIS_BANTUAN_OPTS.map(o => + `
+ ${o.icon} ${o.value} + ${jenisCounts[o.value]} +
` + ).join(''); + + var el = document.getElementById('statsContent'); + if (!el) return; + el.innerHTML = ` +
+
${total}
Total Warga
+
${sangat}
🔴 Sgt. Rentan
+
${rentan}
🟠 Rentan
+
${cukup}
🟡 Cukup
+
+
+
Jenis Bantuan Dibutuhkan
+ ${jenisRows} +
+
+
+ ⚠️ Blind Spot (skor ≥70) + ${bs} KK +
+
`; +} + +function toggleStats() { + var content = document.getElementById('statsContent'); + var btn = document.getElementById('statsToggleBtn'); + if (content.style.display === 'none') { + content.style.display = 'block'; + btn.textContent = '▲'; + } else { + content.style.display = 'none'; + btn.textContent = '▼'; + } +} + +/* ============================================================ + HAPUS + ============================================================ */ +function hapusData(id, type) { + if (!confirm('Yakin ingin menghapus?')) return; + fetch(`../php/delete.php?id=${id}&type=${type}`) + .then(r=>r.json()).then(r=>{ + if (r.message && r.message.toLowerCase().includes('berhasil')) location.reload(); + else alert('Gagal: ' + (r.error||r.message)); + }); +} + +/* ============================================================ + ON CREATED — hanya Marker + ============================================================ */ +map.on(L.Draw.Event.CREATED, function(e) { + var layer = e.layer; + if (e.layerType !== 'marker') return; + var activeMode = 'masjid'; + var modeInput = document.querySelector('input[name="mode"]:checked'); + if (modeInput) activeMode = modeInput.value; + if (activeMode === 'masjid') { handleMasjid(layer); } + else if (activeMode === 'penduduk') { handlePenduduk(layer); } +}); + +/* ── MASJID ── */ +function handleMasjid(layer) { + var ll = layer.getLatLng(); + var defaultRadius = 500; + window._previewLatLng = ll; + layer.addTo(map); + var previewCircle = L.circle([ll.lat,ll.lng], { radius:defaultRadius, color:'#f59e0b', fillColor:'#f59e0b', fillOpacity:0.08, weight:2, dashArray:'6 4' }).addTo(map); + window._previewCircle = previewCircle; + recalcAllRadius({ lat:ll.lat, lng:ll.lng, radius:defaultRadius }); + layer.bindPopup(`
🕌 Tambah Masjid
+ + + +
+ +
100m${defaultRadius} m5000m
+
+
📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}
+ +
`, { maxWidth:290 }).openPopup(); + layer.on('popupclose', function() { + map.removeLayer(previewCircle); + window._previewCircle=null; window._previewLatLng=null; + map.removeLayer(layer); + recalcAllRadius(); + }); +} +function previewRadius(val) { + val = parseFloat(val); + document.getElementById('mRadiusVal').textContent = fmtNum(val,0)+' m'; + if (window._previewCircle) window._previewCircle.setRadius(val); + if (window._previewLatLng) { + recalcAllRadius({ lat:window._previewLatLng.lat, lng:window._previewLatLng.lng, radius:val }); + } +} +function saveMasjid(lat, lng) { + var nama=document.getElementById('mNama').value.trim(), pic=document.getElementById('mPIC').value.trim(), radius=parseFloat(document.getElementById('mRadiusSlider').value); + if (!nama||!pic) { alert('Nama dan PIC wajib diisi!'); return; } + fetch('../php/insert_masjid.php',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({nama,pic,radius,lat,lng}) }) + .then(r=>r.json()) + .then(r=>{ if(r.id) location.reload(); else alert('Gagal: '+(r.error||r.message)); }) + .catch(e => alert('Terjadi kesalahan jaringan atau server saat menyimpan masjid.')); +} + +/* ── PENDUDUK ── */ +function handlePenduduk(layer) { + var ll = layer.getLatLng(); + layer.addTo(map); + var selectStyle = "width:100%; margin-bottom:10px; padding:6px; border-radius:6px; border:1px solid #1e2233; background:#0a0d14; color:#e2e8f0;"; + layer.bindPopup(`
👨‍👩‍👧‍👦 Tambah Penduduk Miskin
+ + + + + + + + + +
📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}
+ +
`, { maxWidth:310 }).openPopup(); + + // Reverse Geocoding OSM Nominatim + fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${ll.lat}&lon=${ll.lng}&zoom=18&addressdetails=1`) + .then(r => r.json()) + .then(data => { + var addrInput = document.getElementById('pdAlamat'); + if (addrInput) { + if (data && data.display_name) { + addrInput.value = data.display_name; + } else { + addrInput.placeholder = 'Gagal menemukan alamat, harap isi manual'; + } + } + }) + .catch(err => { + var addrInput = document.getElementById('pdAlamat'); + if (addrInput) addrInput.placeholder = 'Koneksi API OSM gagal, isi manual'; + }); + + layer.on('popupclose', function() { map.removeLayer(layer); }); +} +function savePenduduk(lat, lng) { + var nama_kk=document.getElementById('pdNama').value.trim(), + jumlah_anggota=parseInt(document.getElementById('pdJml').value)||1, + alamat=document.getElementById('pdAlamat').value.trim(), + penghasilan=document.getElementById('pdPenghasilan').value, + kondisi_rumah=document.getElementById('pdRumah').value, + sumber_air=document.getElementById('pdAir').value; + if (!nama_kk||!alamat) { alert('Nama KK dan Alamat wajib diisi!'); return; } + fetch('../php/insert_penduduk.php',{ + method:'POST', headers:{'Content-Type':'application/json'}, + body:JSON.stringify({nama_kk, jumlah_anggota, alamat, lat, lng, penghasilan, kondisi_rumah, sumber_air}) + }) + .then(r=>r.json()) + .then(r=>{ if(r.id) location.reload(); else alert('Gagal: '+(r.error||r.message)); }) + .catch(e => alert('Terjadi kesalahan jaringan atau server saat menyimpan penduduk. Coba periksa koneksi atau error log.')); +} diff --git a/js/draw_sosial_public.js b/js/draw_sosial_public.js new file mode 100644 index 0000000..4392a33 --- /dev/null +++ b/js/draw_sosial_public.js @@ -0,0 +1,334 @@ +/* ============================================================ + draw_sosial_public.js — Dashboard Publik (READ ONLY) + v3.0 — Heatmap, Masjid Penanggung, Stats, Blind Spot + Tanpa fitur CRUD (tambah/edit/hapus) + ============================================================ */ + +/* ── MAP INIT ── */ +var map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13); +L.control.zoom({ position: 'bottomright' }).addTo(map); +L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', maxZoom: 19 +}).addTo(map); + +/* ── LAYER GROUPS ── */ +var layerGroupMasjid = L.featureGroup().addTo(map); +var layerGroupPenduduk = L.featureGroup().addTo(map); +var heatmapLayer = null; +var showMasjid = true; +var showPenduduk = true; +var showHeatmap = false; + +/* ── DATA STORAGE ── */ +var masjidData = []; +var pendudukData = []; +var blindSpotIds = []; + +/* ── CONSTANTS ── */ +const JENIS_BANTUAN_OPTS = [ + { value: 'BLT / Modal Usaha', icon: '💰' }, + { value: 'Bedah Rumah', icon: '🏠' }, + { value: 'Air Bersih / PDAM', icon: '💧' }, + { value: 'Sembako', icon: '🍚' }, + { value: 'Beasiswa Pendidikan', icon: '📚' }, +]; +function getJenisIcon(jenis) { + const found = JENIS_BANTUAN_OPTS.find(o => o.value === jenis); + return found ? found.icon : '🤝'; +} + +/* ============================================================ + TOGGLE LAYERS + ============================================================ */ +function toggleLayer(type) { + if (type === 'masjid') { + showMasjid = !showMasjid; + showMasjid ? map.addLayer(layerGroupMasjid) : map.removeLayer(layerGroupMasjid); + document.getElementById('btnMasjid').classList.toggle('active', showMasjid); + } else if (type === 'penduduk') { + showPenduduk = !showPenduduk; + showPenduduk ? map.addLayer(layerGroupPenduduk) : map.removeLayer(layerGroupPenduduk); + document.getElementById('btnPenduduk').classList.toggle('active', showPenduduk); + if (!showPenduduk && showHeatmap) toggleHeatmap(); + } else if (type === 'heatmap') { + toggleHeatmap(); + } +} + +function toggleHeatmap() { + showHeatmap = !showHeatmap; + var btn = document.getElementById('btnHeatmap'); + if (showHeatmap) { + map.removeLayer(layerGroupPenduduk); + showPenduduk = false; + document.getElementById('btnPenduduk').classList.remove('active'); + if (heatmapLayer) map.addLayer(heatmapLayer); + if (btn) btn.classList.add('active'); + } else { + if (heatmapLayer) map.removeLayer(heatmapLayer); + map.addLayer(layerGroupPenduduk); + showPenduduk = true; + document.getElementById('btnPenduduk').classList.add('active'); + if (btn) btn.classList.remove('active'); + } +} + +/* ============================================================ + HELPERS + ============================================================ */ +function fmtNum(n, dec=2) { + return parseFloat(n).toLocaleString('id-ID', { maximumFractionDigits: dec }); +} +function getSkorLabel(skor) { + if (skor >= 70) return { label: 'Sangat Rentan', color: '#ef4444', emoji: '🔴' }; + if (skor >= 40) return { label: 'Rentan', color: '#f97316', emoji: '🟠' }; + return { label: 'Cukup', color: '#eab308', emoji: '🟡' }; +} +function findMasjidPenanggung(pLat, pLng) { + var nearest = null, minDist = Infinity; + masjidData.forEach(m => { + var d = map.distance([pLat, pLng], [m.lat, m.lng]); + if (d <= m.radius && d < minDist) { minDist = d; nearest = m; } + }); + return nearest; +} + +/* ── Icons ── */ +function makeMasjidIcon() { + return L.divIcon({ html:`🕌`, iconSize:[30,42], iconAnchor:[15,42], popupAnchor:[0,-44], className:'' }); +} +function makePendudukIcon(inRadius, isBlindspot=false) { + var c = isBlindspot ? '#ef4444' : (inRadius ? '#f59e0b' : '#e879f9'); + return L.divIcon({ html:``, iconSize:[22,32], iconAnchor:[11,32], popupAnchor:[0,-34], className: isBlindspot ? 'blindspot-marker' : '' }); +} + +/* ============================================================ + POPUP BUILDERS — READ ONLY + ============================================================ */ + +/* ── POPUP MASJID ── */ +function buildMasjidPopup(d) { + var penerima = pendudukData + .map(p => ({ ...p, dist: map.distance([p.lat, p.lng], [d.lat, d.lng]) })) + .filter(p => p.dist <= d.radius) + .sort((a, b) => b.skor - a.skor); + + var rowsPenerima = penerima.length === 0 + ? `
Belum ada penduduk dalam radius ini.
` + : penerima.map(p => { + var sl = getSkorLabel(p.skor); + return `
+ ${sl.emoji} +
+
${p.nama || '—'}
+
Skor: ${fmtNum(p.skor,1)} · ${sl.label}
+
+ ${getJenisIcon(p.jenis)} +
`; + }).join(''); + + return `
+
🕌 ${d.nama}
+
+ + +
+
+
PIC${d.pic}
+
Radius Layanan${fmtNum(d.radius,0)} m
+
Jml. Ditanggung${penerima.length} KK
+
+ +
`; +} + +function switchTab(id, tab) { + document.getElementById('tabContent1_' + id).style.display = tab === 1 ? '' : 'none'; + document.getElementById('tabContent2_' + id).style.display = tab === 2 ? '' : 'none'; + document.getElementById('tab1_' + id).style.background = tab === 1 ? '#1e2233' : 'transparent'; + document.getElementById('tab1_' + id).style.color = tab === 1 ? '#f59e0b' : '#6b7280'; + document.getElementById('tab2_' + id).style.background = tab === 2 ? '#1e2233' : 'transparent'; + document.getElementById('tab2_' + id).style.color = tab === 2 ? '#f59e0b' : '#6b7280'; +} + +/* ── POPUP PENDUDUK ── */ +function buildPendudukPopup(d) { + var skor = parseFloat(d.skor_kerentanan) || 0; + var sl = getSkorLabel(skor); + var pLat = parseFloat(d.latitude), pLng = parseFloat(d.longitude); + var penanggung = findMasjidPenanggung(pLat, pLng); + + var penanggungHtml = penanggung + ? `
Masjid Penanggung🕌 ${penanggung.nama}
` + : `
Status⚠️ Blind Spot
`; + + var jenisIcon = getJenisIcon(d.jenis_bantuan); + + return `
+
👨‍👩‍👧‍👦 KK — ${d.nama_kk}
+
+
Jml. Anggota KK${d.jumlah_anggota} orang
+
Alamat${d.alamat}
+
Penghasilan${d.penghasilan}
+
Kondisi Rumah${d.kondisi_rumah}
+
Sumber Air${d.sumber_air}
+
+ Skor Kerentanan + ${sl.emoji} ${fmtNum(skor,1)} — ${sl.label} +
+ ${penanggungHtml} +
+ Jenis Bantuan + ${jenisIcon} ${d.jenis_bantuan || '—'} +
+
+
`; +} + +/* ============================================================ + LOAD DATA + ============================================================ */ +fetch('../php/show_sosial.php') + .then(res => res.json()) + .then(data => { + var pendudukRaw = []; + + data.forEach(d => { + if (d.type === 'masjid') { + var mLat=parseFloat(d.latitude), mLng=parseFloat(d.longitude); + var mR = parseFloat(d.radius_layanan) || parseFloat(d.radius) || 500; + var circle = L.circle([mLat,mLng], { radius:mR, color:'#f59e0b', fillColor:'#f59e0b', fillOpacity:0.08, weight:2, dashArray:'6 4' }); + layerGroupMasjid.addLayer(circle); + var mEntry = { id:parseInt(d.id), lat:mLat, lng:mLng, radius:mR, nama:d.nama, pic:d.pic, circleLayer:circle }; + masjidData.push(mEntry); + var marker = L.marker([mLat,mLng], { icon:makeMasjidIcon() }); + marker._type='masjid'; marker._db_id=d.id; + mEntry.markerLayer = marker; + layerGroupMasjid.addLayer(marker); + } + if (d.type === 'penduduk') pendudukRaw.push(d); + }); + + var heatPoints = []; + pendudukRaw.forEach(d => { + var pLat=parseFloat(d.latitude), pLng=parseFloat(d.longitude); + var skor = parseFloat(d.skor_kerentanan) || 0; + var inRadius=false, minDist=Infinity; + masjidData.forEach(m => { + var dist = map.distance([pLat,pLng],[m.lat,m.lng]); + if (dist<=m.radius && dist buildPendudukPopup(d), { maxWidth:300 }); + layerGroupPenduduk.addLayer(marker); + + heatPoints.push([pLat, pLng, skor / 100]); + }); + + // Bind masjid popups + masjidData.forEach(m => { + m.markerLayer.bindPopup(() => buildMasjidPopup(m), { maxWidth:310 }); + }); + + // Init heatmap + if (typeof L.heatLayer !== 'undefined') { + heatmapLayer = L.heatLayer(heatPoints, { + radius: 35, blur: 25, maxZoom: 16, + gradient: { 0.0:'#3b82f6', 0.4:'#eab308', 0.7:'#f97316', 1.0:'#ef4444' } + }); + } + + checkBlindSpots(); + renderStats(); + }) + .catch(err => console.error('Gagal load data:', err)); + +/* ============================================================ + BLIND SPOT + ============================================================ */ +function checkBlindSpots() { + blindSpotIds = []; + pendudukData.forEach(p => { + if (!p.inRadius && p.skor >= 70) { + blindSpotIds.push(p.id); + p.markerLayer.setIcon(makePendudukIcon(false, true)); + } + }); + var panel = document.getElementById('blindSpotPanel'); + if (!panel) return; + if (blindSpotIds.length > 0) { + panel.style.display = 'flex'; + document.getElementById('blindSpotCount').textContent = blindSpotIds.length; + } else { + panel.style.display = 'none'; + } +} + +function zoomToBlindSpots() { + if (blindSpotIds.length === 0) return; + var bounds = pendudukData.filter(p => blindSpotIds.includes(p.id)).map(p => [p.lat, p.lng]); + if (bounds.length > 0) map.fitBounds(bounds, { padding:[60,60] }); +} + +/* ============================================================ + STATS PANEL + ============================================================ */ +function renderStats() { + var total = pendudukData.length; + var sangat = pendudukData.filter(p => p.skor >= 70).length; + var rentan = pendudukData.filter(p => p.skor >= 40 && p.skor < 70).length; + var cukup = pendudukData.filter(p => p.skor < 40).length; + var bs = blindSpotIds.length; + + var jenisCounts = {}; + JENIS_BANTUAN_OPTS.forEach(o => jenisCounts[o.value] = 0); + pendudukData.forEach(p => { + if (jenisCounts[p.jenis] !== undefined) jenisCounts[p.jenis]++; + else jenisCounts['BLT / Modal Usaha']++; + }); + + var jenisRows = JENIS_BANTUAN_OPTS.map(o => + `
+ ${o.icon} ${o.value} + ${jenisCounts[o.value]} +
` + ).join(''); + + var el = document.getElementById('statsContent'); + if (!el) return; + el.innerHTML = ` +
+
${total}
Total Warga
+
${sangat}
🔴 Sgt. Rentan
+
${rentan}
🟠 Rentan
+
${cukup}
🟡 Cukup
+
+
+
Jenis Bantuan Dibutuhkan
+ ${jenisRows} +
+
+
+ ⚠️ Blind Spot (skor ≥70) + ${bs} KK +
+
`; +} + +function toggleStats() { + var content = document.getElementById('statsContent'); + var btn = document.getElementById('statsToggleBtn'); + if (content.style.display === 'none') { + content.style.display = 'block'; btn.textContent = '▲'; + } else { + content.style.display = 'none'; btn.textContent = '▼'; + } +} diff --git a/js/draw_spbu.js b/js/draw_spbu.js new file mode 100644 index 0000000..6e30131 --- /dev/null +++ b/js/draw_spbu.js @@ -0,0 +1,226 @@ +/* ============================================================ + draw_spbu.js — WebGIS SPBU Pontianak + Handles: SPBU (Point), Jalan (Line), Parsil (Polygon) + ============================================================ */ + +var map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13); +L.control.zoom({ position: 'bottomright' }).addTo(map); + +L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', maxZoom: 19 +}).addTo(map); + +/* ── LAYER GROUPS ── */ +var drawnItems = new L.FeatureGroup().addTo(map); +var layerGroup24 = L.featureGroup().addTo(map); +var layerGroupTidak = L.featureGroup().addTo(map); + +var show24 = true; +var showTidak = true; + +let currentLayer = null; + +/* ============================================================ + TOGGLE LAYERS + ============================================================ */ +function toggleLayer(type) { + if (type === '24jam') { + show24 = !show24; + show24 ? map.addLayer(layerGroup24) : map.removeLayer(layerGroup24); + document.getElementById('btn24').classList.toggle('active', show24); + } else if (type === 'tidak') { + showTidak = !showTidak; + showTidak ? map.addLayer(layerGroupTidak) : map.removeLayer(layerGroupTidak); + document.getElementById('btnTidak').classList.toggle('active', showTidak); + } +} + +/* ============================================================ + DRAW CONTROL + ============================================================ */ +var drawControl = new L.Control.Draw({ + position: 'topleft', + edit: { featureGroup: drawnItems }, + draw: { + marker: true, + polyline: { shapeOptions: { color: '#a855f7', weight: 3 } }, + polygon: { shapeOptions: { color: '#10b981', weight: 2, fillOpacity: 0.35 } }, + rectangle: false, circle: false, circlemarker: false + } +}); +map.addControl(drawControl); + +/* ============================================================ + HELPERS + ============================================================ */ +function getLineColor(s) { + return s==='Nasional' ? '#ef4444' : s==='Provinsi' ? '#38bdf8' : '#a855f7'; +} +function getLength(ll) { + let t=0; + for(let i=0;i24 Jam` : `Tidak 24 Jam`; + if (type==='line') { let c=s==='Nasional'?'badge-red':s==='Provinsi'?'badge-blue':'badge-gray'; return `${s}`; } + return `${s}`; +} + +/* ── Icons ── */ +function makeIcon(status) { + var c = status==='24 Jam' ? '#4ade80' : '#f87171'; + return L.divIcon({ html:``, iconSize:[26,38], iconAnchor:[13,38], popupAnchor:[0,-40], className:'' }); +} + +/* ============================================================ + POPUP BUILDERS + ============================================================ */ +function buildInfoPopup(d, type) { + const hc = { point:'linear-gradient(135deg,#1d4ed8,#3b82f6)', line:'linear-gradient(135deg,#7c3aed,#a855f7)', polygon:'linear-gradient(135deg,#065f46,#10b981)' }; + const ic = { point:'⛽', line:'🛣️', polygon:'📐' }; + const ti = { point:`SPBU — ${d.nama}`, line:`Jalan — ${d.nama}`, polygon:`Parsil — ${d.nama}` }; + + let rows = ''; + if (type==='point') { + rows = `
No. SPBU${d.no_spbu}
+
Status${statusBadge(d.status,'point')}
+
Koordinat${parseFloat(d.latitude).toFixed(5)}, ${parseFloat(d.longitude).toFixed(5)}
`; + } else if (type==='line') { + rows = `
Status${statusBadge(d.status,'line')}
+
Panjang${fmtNum(d.panjang)} m
`; + } else if (type==='polygon') { + rows = `
Status${statusBadge(d.status,'polygon')}
+
Luas${fmtNum(d.luas)} m²
`; + } + + return `
${ic[type]} ${ti[type]}
${rows}
`; +} + +/* ============================================================ + LOAD DATA + ============================================================ */ +fetch('../php/show_spbu.php') + .then(res => res.json()) + .then(data => { + data.forEach(d => { + if (d.type === 'point') { + var layer = L.marker([d.latitude, d.longitude], { icon: makeIcon(d.status) }); + layer._type='point'; layer._db_id=d.id; + layer.bindPopup(buildInfoPopup(d,'point'), { maxWidth:260 }); + d.status==='24 Jam' ? layerGroup24.addLayer(layer) : layerGroupTidak.addLayer(layer); + } + if (d.type === 'line') { + var coords = JSON.parse(d.geom); + var layer = L.polyline(coords, { color:getLineColor(d.status), weight:4, opacity:0.9 }); + layer._type='line'; layer._db_id=d.id; + layer.bindPopup(buildInfoPopup(d,'line'), { maxWidth:260 }); + drawnItems.addLayer(layer); + } + if (d.type === 'polygon') { + var coords = JSON.parse(d.geom); + var layer = L.polygon(coords, { color:'#10b981', weight:2, fillColor:'#10b981', fillOpacity:0.3 }); + layer._type='polygon'; layer._db_id=d.id; + layer.bindPopup(buildInfoPopup(d,'polygon'), { maxWidth:260 }); + drawnItems.addLayer(layer); + } + }); + }) + .catch(err => console.error('Gagal load data:', err)); + +/* ============================================================ + HAPUS + ============================================================ */ +function hapusData(id, type) { + if (!confirm('Yakin ingin menghapus?')) return; + fetch(`../php/delete.php?id=${id}&type=${type}`) + .then(r=>r.json()).then(r=>{ + if (r.message && r.message.toLowerCase().includes('berhasil')) location.reload(); + else alert('Gagal: ' + (r.error||r.message)); + }); +} + +/* ============================================================ + ON CREATED + ============================================================ */ +map.on(L.Draw.Event.CREATED, function(e) { + var layer = e.layer; + + if (e.layerType === 'marker') { drawnItems.addLayer(layer); handlePoint(layer); } + if (e.layerType === 'polyline') { drawnItems.addLayer(layer); handleLine(layer); } + if (e.layerType === 'polygon') { drawnItems.addLayer(layer); handlePolygon(layer); } +}); + +/* ── SPBU ── */ +function handlePoint(layer) { + var ll = layer.getLatLng(); + layer.bindPopup(`
⛽ Tambah SPBU
+ + + + +
📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}
+ +
`, { maxWidth:280 }).openPopup(); +} +function savePoint(lat, lng) { + var nama=document.getElementById('pNama').value.trim(), no=document.getElementById('pNo').value.trim(), status=document.getElementById('pStatus').value; + if (!nama||!no) { alert('Nama dan No. SPBU wajib diisi!'); return; } + fetch('../php/insert.php',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({nama,no,status,lat,lng}) }) + .then(r=>r.json()).then(r=>{ if(r.message&&r.message.includes('berhasil')) location.reload(); else alert('Gagal: '+r.message); }); +} + +/* ── LINE ── */ +function handleLine(layer) { + currentLayer=layer; + var ll=layer.getLatLngs(), p=getLength(ll); layer._temp={latlngs:ll,panjang:p}; + L.popup({maxWidth:280}).setLatLng(layer.getBounds().getCenter()).setContent(`
🛣️ Tambah Jalan
+ + +
📏 ${fmtNum(p)} m
+ +
`).openOn(map); +} +function saveLine() { + var layer=currentLayer; if(!layer){alert('Layer tidak ditemukan');return;} + var nama=document.getElementById('lNama').value.trim(), status=document.getElementById('lStatus').value; + if(!nama){alert('Nama wajib diisi!');return;} + fetch('../php/insert_line.php',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({nama,status,panjang:layer._temp.panjang,geom:JSON.stringify(layer._temp.latlngs)})}) + .then(r=>r.json()).then(r=>{ if(r.id){layer._db_id=r.id;layer._type='line';map.closePopup();alert('Berhasil!');} else alert('Gagal: '+(r.error||r.message)); }); +} + +/* ── POLYGON ── */ +function handlePolygon(layer) { + currentLayer=layer; + var ll=layer.getLatLngs(), luas=L.GeometryUtil.geodesicArea(ll[0]); layer._temp={latlngs:ll,luas}; + L.popup({maxWidth:280}).setLatLng(layer.getBounds().getCenter()).setContent(`
📐 Tambah Parsil
+ + +
📐 ${fmtNum(luas)} m²
+ +
`).openOn(map); +} +function savePolygon() { + var layer=currentLayer; if(!layer){alert('Layer tidak ditemukan');return;} + var nama=document.getElementById('gNama').value.trim(), status=document.getElementById('gStatus').value; + if(!nama){alert('Nama wajib diisi!');return;} + fetch('../php/insert_polygon.php',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({nama,status,luas:layer._temp.luas,geom:JSON.stringify(layer._temp.latlngs)})}) + .then(r=>r.json()).then(r=>{ if(r.id){layer._db_id=r.id;layer._type='polygon';map.closePopup();alert('Berhasil!');} else alert('Gagal: '+(r.error||r.message)); }); +} + +/* ── EDIT & DELETE ── */ +map.on('draw:edited', function(e) { + e.layers.eachLayer(layer => { + if (!layer._db_id) return; + if (layer._type==='line') fetch('../php/update_line.php',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:layer._db_id,geom:JSON.stringify(layer.getLatLngs()),panjang:getLength(layer.getLatLngs())})}); + if (layer._type==='polygon') fetch('../php/update_polygon.php',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:layer._db_id,geom:JSON.stringify(layer.getLatLngs()),luas:L.GeometryUtil.geodesicArea(layer.getLatLngs()[0])})}); + }); +}); +map.on('draw:deleted', function(e) { + e.layers.eachLayer(layer => { + if (layer._db_id&&layer._type) fetch(`../php/delete.php?id=${layer._db_id}&type=${layer._type}`); + }); +}); diff --git a/js/draw_spbu_point.js b/js/draw_spbu_point.js new file mode 100644 index 0000000..c71db2e --- /dev/null +++ b/js/draw_spbu_point.js @@ -0,0 +1,137 @@ +/* ============================================================ + draw_spbu_point.js - WebGIS SPBU Point Only + ============================================================ */ + +var map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13); +L.control.zoom({ position: 'bottomright' }).addTo(map); + +L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + maxZoom: 19 +}).addTo(map); + +var drawnItems = new L.FeatureGroup().addTo(map); +var layerGroup24 = L.featureGroup().addTo(map); +var layerGroupTidak = L.featureGroup().addTo(map); +var show24 = true; +var showTidak = true; + +function toggleLayer(type) { + if (type === '24jam') { + show24 = !show24; + show24 ? map.addLayer(layerGroup24) : map.removeLayer(layerGroup24); + document.getElementById('btn24').classList.toggle('active', show24); + } else if (type === 'tidak') { + showTidak = !showTidak; + showTidak ? map.addLayer(layerGroupTidak) : map.removeLayer(layerGroupTidak); + document.getElementById('btnTidak').classList.toggle('active', showTidak); + } +} + +var drawControl = new L.Control.Draw({ + position: 'topleft', + edit: { featureGroup: drawnItems }, + draw: { + marker: true, + polyline: false, + polygon: false, + rectangle: false, + circle: false, + circlemarker: false + } +}); +map.addControl(drawControl); + +function makeIcon(status) { + var c = status === '24 Jam' ? '#4ade80' : '#f87171'; + return L.divIcon({ + html: ``, + iconSize: [26, 38], + iconAnchor: [13, 38], + popupAnchor: [0, -40], + className: '' + }); +} + +function statusBadge(status) { + return status === '24 Jam' + ? `24 Jam` + : `Tidak 24 Jam`; +} + +function buildInfoPopup(d) { + return `
+
⛽ SPBU - ${d.nama}
+
+
No. SPBU${d.no_spbu}
+
Status${statusBadge(d.status)}
+
Koordinat${parseFloat(d.latitude).toFixed(5)}, ${parseFloat(d.longitude).toFixed(5)}
+ +
+
`; +} + +fetch('../php/show_spbu.php') + .then(res => res.json()) + .then(data => { + data.forEach(d => { + var layer = L.marker([d.latitude, d.longitude], { icon: makeIcon(d.status) }); + layer._type = 'point'; + layer._db_id = d.id; + layer.bindPopup(buildInfoPopup(d), { maxWidth: 260 }); + d.status === '24 Jam' ? layerGroup24.addLayer(layer) : layerGroupTidak.addLayer(layer); + }); + }) + .catch(err => console.error('Gagal load data SPBU:', err)); + +function hapusData(id, type) { + if (!confirm('Yakin ingin menghapus SPBU ini?')) return; + fetch(`../php/delete.php?id=${id}&type=${type}`) + .then(r => r.json()) + .then(r => { + if (r.message && r.message.toLowerCase().includes('berhasil')) location.reload(); + else alert('Gagal: ' + (r.error || r.message)); + }); +} + +map.on(L.Draw.Event.CREATED, function(e) { + if (e.layerType !== 'marker') return; + var layer = e.layer; + drawnItems.addLayer(layer); + handlePoint(layer); +}); + +function handlePoint(layer) { + var ll = layer.getLatLng(); + layer.bindPopup(`
+
⛽ Tambah SPBU
+
+ + + + +
📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}
+ +
+
`, { maxWidth: 280 }).openPopup(); +} + +function savePoint(lat, lng) { + var nama = document.getElementById('pNama').value.trim(); + var no = document.getElementById('pNo').value.trim(); + var status = document.getElementById('pStatus').value; + if (!nama || !no) { + alert('Nama dan No. SPBU wajib diisi!'); + return; + } + fetch('../php/insert.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nama, no, status, lat, lng }) + }) + .then(r => r.json()) + .then(r => { + if (r.message && r.message.includes('berhasil')) location.reload(); + else alert('Gagal: ' + (r.error || r.message)); + }); +} diff --git a/php/db.php b/php/db.php new file mode 100644 index 0000000..8a4ee47 --- /dev/null +++ b/php/db.php @@ -0,0 +1,7 @@ +connect_error) { + die(json_encode(["error" => "Koneksi gagal: " . $conn->connect_error])); +} +$conn->set_charset("utf8"); +?> diff --git a/php/delete.php b/php/delete.php new file mode 100644 index 0000000..1a40537 --- /dev/null +++ b/php/delete.php @@ -0,0 +1,33 @@ + "Parameter tidak valid"]); + exit; +} + +$table = ''; +if ($type == 'point') $table = 'spbu'; +if ($type == 'line') $table = 'jalan'; +if ($type == 'polygon') $table = 'parsil'; +if ($type == 'masjid') $table = 'masjid'; +if ($type == 'penduduk') $table = 'penduduk_miskin'; + +if ($table == '') { + echo json_encode(["message" => "Type tidak dikenali"]); + exit; +} + +$stmt = $conn->prepare("DELETE FROM $table WHERE id = ?"); +$stmt->bind_param("i", $id); + +if ($stmt->execute()) { + echo json_encode(["message" => "Data berhasil dihapus"]); +} else { + echo json_encode(["message" => "Gagal hapus", "error" => $stmt->error]); +} +?> \ No newline at end of file diff --git a/php/insert.php b/php/insert.php new file mode 100644 index 0000000..a53b795 --- /dev/null +++ b/php/insert.php @@ -0,0 +1,31 @@ + "Data tidak diterima"]); + exit; +} + +$nama = $data['nama'] ?? ''; +$no = $data['no'] ?? ''; +$status = $data['status'] ?? ''; +$lat = $data['lat'] ?? ''; +$lng = $data['lng'] ?? ''; + +if ($nama == '' || $no == '' || $lat == '' || $lng == '') { + echo json_encode(["message" => "Field tidak lengkap"]); + exit; +} + +$stmt = $conn->prepare("INSERT INTO spbu (nama, no_spbu, status, latitude, longitude) VALUES (?, ?, ?, ?, ?)"); +$stmt->bind_param("sssdd", $nama, $no, $status, $lat, $lng); + +if ($stmt->execute()) { + echo json_encode(["message" => "Data berhasil disimpan", "id" => $stmt->insert_id]); +} else { + echo json_encode(["message" => "Gagal simpan", "error" => $stmt->error]); +} +?> diff --git a/php/insert_line.php b/php/insert_line.php new file mode 100644 index 0000000..d931389 --- /dev/null +++ b/php/insert_line.php @@ -0,0 +1,39 @@ + "Data kosong"]); + exit; +} + +$nama = $data['nama'] ?? ''; +$status = $data['status'] ?? ''; +$panjang = $data['panjang'] ?? 0; +$geom = $data['geom'] ?? ''; // ✅ sudah string JSON dari JS, JANGAN json_encode lagi + +if ($nama === '' || $geom === '') { + echo json_encode(["message" => "Field tidak lengkap"]); + exit; +} + +$stmt = $conn->prepare("INSERT INTO jalan (nama, status, panjang, geom) VALUES (?, ?, ?, ?)"); + +if (!$stmt) { + echo json_encode(["message" => "Prepare gagal", "error" => $conn->error]); + exit; +} + +$stmt->bind_param("ssds", $nama, $status, $panjang, $geom); + +if ($stmt->execute()) { + echo json_encode(["message" => "Berhasil", "id" => $stmt->insert_id]); +} else { + echo json_encode(["message" => "Gagal", "error" => $stmt->error]); +} + +$stmt->close(); +$conn->close(); +?> diff --git a/php/insert_masjid.php b/php/insert_masjid.php new file mode 100644 index 0000000..6d168b7 --- /dev/null +++ b/php/insert_masjid.php @@ -0,0 +1,25 @@ +"Data kosong"]); exit; } + +$nama = $data['nama'] ?? ''; +$pic = $data['pic'] ?? ''; +$radius = $data['radius'] ?? 500; +$lat = $data['lat'] ?? 0; +$lng = $data['lng'] ?? 0; + +if ($nama==='' || $pic==='') { echo json_encode(["message"=>"Field tidak lengkap"]); exit; } + +$stmt = $conn->prepare("INSERT INTO masjid (nama, pic, radius, latitude, longitude, radius_layanan) VALUES (?,?,?,?,?,?)"); +$stmt->bind_param("ssdddd", $nama, $pic, $radius, $lat, $lng, $radius); + +if ($stmt->execute()) { + echo json_encode(["message"=>"Berhasil", "id"=>$stmt->insert_id]); +} else { + echo json_encode(["message"=>"Gagal", "error"=>$stmt->error]); +} +$stmt->close(); $conn->close(); +?> \ No newline at end of file diff --git a/php/insert_penduduk.php b/php/insert_penduduk.php new file mode 100644 index 0000000..46c3c6a --- /dev/null +++ b/php/insert_penduduk.php @@ -0,0 +1,51 @@ +"Data kosong"]); exit; } + +$nama_kk = $data['nama_kk'] ?? ''; +$jumlah_anggota = $data['jumlah_anggota'] ?? 1; +$alamat = $data['alamat'] ?? ''; +$lat = $data['lat'] ?? 0; +$lng = $data['lng'] ?? 0; + +// Parameter baru +$penghasilan = $data['penghasilan'] ?? '< 500rb'; +$kondisi_rumah = $data['kondisi_rumah'] ?? 'Kayu'; +$sumber_air = $data['sumber_air'] ?? 'Sumur'; + +if ($nama_kk==='' || $alamat==='') { echo json_encode(["message"=>"Field tidak lengkap"]); exit; } + +// Kalkulasi Bobot +$penghasilan_opsi = ['< 500rb' => 100, '500rb - 1jt' => 50, '> 1jt' => 10]; +$rumah_opsi = ['Bambu/Tepas' => 100, 'Kayu' => 50, 'Tembok' => 10]; +$air_opsi = ['Sungai' => 100, 'Sumur' => 50, 'PDAM' => 10]; + +$val_p = $penghasilan_opsi[$penghasilan] ?? 50; +$val_r = $rumah_opsi[$kondisi_rumah] ?? 50; +$val_a = $air_opsi[$sumber_air] ?? 50; + +// Rumus: Penghasilan 50%, Rumah 30%, Air 20% +$skor_kerentanan = ($val_p * 0.50) + ($val_r * 0.30) + ($val_a * 0.20); + +// Rekomendasi jenis bantuan berdasarkan indikator terburuk +$skor_per_jenis = [ + 'BLT / Modal Usaha' => $val_p, + 'Bedah Rumah' => $val_r, + 'Air Bersih / PDAM' => $val_a, +]; +arsort($skor_per_jenis); +$jenis_bantuan = array_key_first($skor_per_jenis); + +$stmt = $conn->prepare("INSERT INTO penduduk_miskin (nama_kk, jumlah_anggota, alamat, latitude, longitude, penghasilan, kondisi_rumah, sumber_air, skor_kerentanan, jenis_bantuan) VALUES (?,?,?,?,?,?,?,?,?,?)"); +$stmt->bind_param("sisddsssds", $nama_kk, $jumlah_anggota, $alamat, $lat, $lng, $penghasilan, $kondisi_rumah, $sumber_air, $skor_kerentanan, $jenis_bantuan); + +if ($stmt->execute()) { + echo json_encode(["message"=>"Berhasil", "id"=>$stmt->insert_id, "skor"=>$skor_kerentanan]); +} else { + echo json_encode(["message"=>"Gagal", "error"=>$stmt->error]); +} +$stmt->close(); $conn->close(); +?> \ No newline at end of file diff --git a/php/insert_polygon.php b/php/insert_polygon.php new file mode 100644 index 0000000..c607130 --- /dev/null +++ b/php/insert_polygon.php @@ -0,0 +1,39 @@ + "Data kosong / tidak valid"]); + exit; +} + +$nama = $data['nama'] ?? ''; +$status = $data['status'] ?? ''; +$luas = $data['luas'] ?? 0; +$geom = $data['geom'] ?? ''; // ✅ sudah string JSON dari JS, JANGAN json_encode lagi + +if ($nama === '' || $status === '' || $geom === '') { + echo json_encode(["message" => "Field tidak lengkap", "data" => $data]); + exit; +} + +$stmt = $conn->prepare("INSERT INTO parsil (nama, status, luas, geom) VALUES (?, ?, ?, ?)"); + +if (!$stmt) { + echo json_encode(["message" => "Prepare gagal", "error" => $conn->error]); + exit; +} + +$stmt->bind_param("ssds", $nama, $status, $luas, $geom); + +if ($stmt->execute()) { + echo json_encode(["message" => "Berhasil", "id" => $stmt->insert_id]); +} else { + echo json_encode(["message" => "Gagal", "error" => $stmt->error]); +} + +$stmt->close(); +$conn->close(); +?> diff --git a/php/show.php b/php/show.php new file mode 100644 index 0000000..a827868 --- /dev/null +++ b/php/show.php @@ -0,0 +1,38 @@ +query("SELECT * FROM spbu"); +if ($res) { + while ($r = $res->fetch_assoc()) { $r['type']='point'; $data[]=$r; } +} + +/* LINE — Jalan */ +$res = $conn->query("SELECT * FROM jalan"); +if ($res) { + while ($r = $res->fetch_assoc()) { $r['type']='line'; $data[]=$r; } +} + +/* POLYGON — Parsil */ +$res = $conn->query("SELECT * FROM parsil"); +if ($res) { + while ($r = $res->fetch_assoc()) { $r['type']='polygon'; $data[]=$r; } +} + +/* MASJID */ +$res = $conn->query("SELECT * FROM masjid"); +if ($res) { + while ($r = $res->fetch_assoc()) { $r['type']='masjid'; $data[]=$r; } +} + +/* PENDUDUK MISKIN */ +$res = $conn->query("SELECT * FROM penduduk_miskin"); +if ($res) { + while ($r = $res->fetch_assoc()) { $r['type']='penduduk'; $data[]=$r; } +} + +echo json_encode($data); +?> \ No newline at end of file diff --git a/php/show_geometri.php b/php/show_geometri.php new file mode 100644 index 0000000..913d3c5 --- /dev/null +++ b/php/show_geometri.php @@ -0,0 +1,35 @@ +query("SELECT * FROM spbu"); +if ($res) { + while ($r = $res->fetch_assoc()) { + $r['type'] = 'point'; + $data[] = $r; + } +} + +/* POLYLINE */ +$res = $conn->query("SELECT * FROM jalan"); +if ($res) { + while ($r = $res->fetch_assoc()) { + $r['type'] = 'line'; + $data[] = $r; + } +} + +/* POLYGON */ +$res = $conn->query("SELECT * FROM parsil"); +if ($res) { + while ($r = $res->fetch_assoc()) { + $r['type'] = 'polygon'; + $data[] = $r; + } +} + +echo json_encode($data); +?> diff --git a/php/show_sosial.php b/php/show_sosial.php new file mode 100644 index 0000000..04fe4e5 --- /dev/null +++ b/php/show_sosial.php @@ -0,0 +1,20 @@ +query("SELECT * FROM masjid"); +if ($res) { + while ($r = $res->fetch_assoc()) { $r['type']='masjid'; $data[]=$r; } +} + +/* PENDUDUK MISKIN */ +$res = $conn->query("SELECT * FROM penduduk_miskin"); +if ($res) { + while ($r = $res->fetch_assoc()) { $r['type']='penduduk'; $data[]=$r; } +} + +echo json_encode($data); +?> diff --git a/php/show_spbu.php b/php/show_spbu.php new file mode 100644 index 0000000..6ed29c5 --- /dev/null +++ b/php/show_spbu.php @@ -0,0 +1,16 @@ +query("SELECT * FROM spbu"); +if ($res) { + while ($r = $res->fetch_assoc()) { + $r['type'] = 'point'; + $data[] = $r; + } +} + +echo json_encode($data); +?> diff --git a/php/update_line.php b/php/update_line.php new file mode 100644 index 0000000..2a8a5cf --- /dev/null +++ b/php/update_line.php @@ -0,0 +1,24 @@ + "Data kosong"]); + exit; +} + +$id = intval($data['id'] ?? 0); +$geom = $data['geom'] ?? ''; +$panjang = $data['panjang'] ?? 0; + +$stmt = $conn->prepare("UPDATE jalan SET geom = ?, panjang = ? WHERE id = ?"); +$stmt->bind_param("sdi", $geom, $panjang, $id); + +if ($stmt->execute()) { + echo json_encode(["message" => "Berhasil diupdate"]); +} else { + echo json_encode(["message" => "Gagal update", "error" => $stmt->error]); +} +?> diff --git a/php/update_masjid.php b/php/update_masjid.php new file mode 100644 index 0000000..8ef1d52 --- /dev/null +++ b/php/update_masjid.php @@ -0,0 +1,41 @@ + "Data kosong"]); + exit; +} + +$id = intval($data['id'] ?? 0); +$radius = floatval($data['radius'] ?? 0); + +if ($id <= 0 || $radius <= 0) { + echo json_encode(["message" => "Parameter tidak valid"]); + exit; +} + +// Sesuaikan dengan batas slider di draw_sosial.js. +if ($radius < 100 || $radius > 5000) { + echo json_encode(["message" => "Radius harus berada di antara 100 sampai 5000 meter"]); + exit; +} + +$stmt = $conn->prepare("UPDATE masjid SET radius = ?, radius_layanan = ? WHERE id = ?"); +if (!$stmt) { + echo json_encode(["message" => "Gagal menyiapkan query", "error" => $conn->error]); + exit; +} + +$stmt->bind_param("ddi", $radius, $radius, $id); + +if ($stmt->execute()) { + echo json_encode(["message" => "Berhasil", "radius" => $radius]); +} else { + echo json_encode(["message" => "Gagal", "error" => $stmt->error]); +} + +$stmt->close(); +$conn->close(); +?> diff --git a/php/update_penduduk.php b/php/update_penduduk.php new file mode 100644 index 0000000..66c96f3 --- /dev/null +++ b/php/update_penduduk.php @@ -0,0 +1,25 @@ +"Data kosong"]); exit; } + +$id = intval($data['id'] ?? 0); +$jenis_bantuan = $data['jenis_bantuan'] ?? ''; + +if ($id === 0 || $jenis_bantuan === '') { + echo json_encode(["message"=>"Parameter tidak valid"]); + exit; +} + +$stmt = $conn->prepare("UPDATE penduduk_miskin SET jenis_bantuan=? WHERE id=?"); +$stmt->bind_param("si", $jenis_bantuan, $id); + +if ($stmt->execute()) { + echo json_encode(["message"=>"Berhasil"]); +} else { + echo json_encode(["message"=>"Gagal", "error"=>$stmt->error]); +} +$stmt->close(); $conn->close(); +?> diff --git a/php/update_polygon.php b/php/update_polygon.php new file mode 100644 index 0000000..5594a8c --- /dev/null +++ b/php/update_polygon.php @@ -0,0 +1,24 @@ + "Data kosong"]); + exit; +} + +$id = intval($data['id'] ?? 0); +$geom = $data['geom'] ?? ''; +$luas = $data['luas'] ?? 0; + +$stmt = $conn->prepare("UPDATE parsil SET geom = ?, luas = ? WHERE id = ?"); +$stmt->bind_param("sdi", $geom, $luas, $id); + +if ($stmt->execute()) { + echo json_encode(["message" => "Berhasil diupdate"]); +} else { + echo json_encode(["message" => "Gagal update", "error" => $stmt->error]); +} +?> diff --git a/sosial/admin.php b/sosial/admin.php new file mode 100644 index 0000000..0ba500b --- /dev/null +++ b/sosial/admin.php @@ -0,0 +1,127 @@ + + + + + + + Panel Admin — WebGIS Sosial + + + + + + + + + + + + + + +
+ 👁 + +
+ Kelola Data & Parameter + +
+
+
Masjid +
+
+
Pend. Miskin +
+
+
Dlm. Radius +
+
+ +
+ + +
+ + + +
+ Keluar +
+
+ +
+ + +
+
+ 📊 STATISTIK SOSIAL + +
+
+ +
+
+ + + + + + + + + + + +
+
+
Pilih Mode Pointing
+
+ + +
+
+
+ + + + + + + diff --git a/sosial/erd.drawio b/sosial/erd.drawio new file mode 100644 index 0000000..77d7102 --- /dev/null +++ b/sosial/erd.drawio @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sosial/index.php b/sosial/index.php new file mode 100644 index 0000000..a4107ff --- /dev/null +++ b/sosial/index.php @@ -0,0 +1,99 @@ + + + + + + WebGIS Sosial — Penduduk Miskin & Masjid + + + + + + + + + + + + + +
+ + +
+ Pendataan Penduduk Miskin & Masjid + +
+
+
Masjid +
+
+
Pend. Miskin +
+
+
Dlm. Radius +
+
+ +
+ + +
+ + + +
+ Admin Login +
+
+ +
+ + + + + + + + + +
+
+ 📊 STATISTIK SOSIAL + +
+
+ +
+
+ + + + + + + + + + diff --git a/sosial/login.php b/sosial/login.php new file mode 100644 index 0000000..061d35b --- /dev/null +++ b/sosial/login.php @@ -0,0 +1,209 @@ + + + + + + + Login Admin — WebGIS Sosial + + + + + + + + diff --git a/sosial/logout.php b/sosial/logout.php new file mode 100644 index 0000000..e5ab4b6 --- /dev/null +++ b/sosial/logout.php @@ -0,0 +1,6 @@ + diff --git a/sosial/readme.md b/sosial/readme.md new file mode 100644 index 0000000..eafe1b8 --- /dev/null +++ b/sosial/readme.md @@ -0,0 +1,15 @@ +Buatkan saya dokumen Spesifikasi Kebutuhan Perangkat Lunak (SKPL) atau Software Requirement Specification (SRS) untuk sebuah sistem WebGIS yang bertujuan untuk mengentaskan kemiskinan dengan melibatkan partisipasi masyarakat, khususnya rumah ibadah (Masjid). + +Sistem ini adalah aplikasi berbasis web yang dibangun menggunakan HTML, CSS (Vanilla), JavaScript (Leaflet.js untuk pemetaan), dan PHP native dengan database MySQL. Aplikasi ini memiliki dua entitas data spasial utama: titik lokasi Masjid dan titik lokasi Penduduk Miskin. Admin atau pengguna dapat menambahkan titik lokasi Masjid pada peta beserta informasi Penanggung Jawab (PIC) dan menentukan "Radius Deteksi" (jangkauan pelayanan/perhatian sosial dari masjid tersebut dalam satuan meter). Pengguna juga dapat menambahkan titik lokasi Penduduk Miskin beserta data Nama Kepala Keluarga, Jumlah Anggota Keluarga, dan Alamat. + +Fitur inti dari aplikasi ini adalah analisis spasial otomatis (berbasis radius). Ketika titik Penduduk Miskin ditambahkan atau ketika radius Masjid disesuaikan, sistem akan secara real-time mengevaluasi jarak antara penduduk miskin dengan masjid-masjid terdekat. Jika seorang penduduk miskin berada di dalam radius pelayanan salah satu masjid, indikator visual (warna marker) pada peta akan berubah untuk menandakan bahwa keluarga tersebut "terjangkau" atau berada dalam wilayah cakupan sosial masjid. Hal ini memungkinkan pemetaan yang jelas mana warga miskin yang sudah berada dalam pengawasan program bantuan masjid dan mana warga miskin yang posisinya berada di luar radius ("blind spot") sehingga membutuhkan intervensi dari pemerintah atau pihak lain. + +Tolong buatkan SKPL/SRS lengkap yang mencakup: +1. Pendahuluan (Tujuan, lingkup masalah, definisi/istilah, referensi) +2. Deskripsi Umum (Perspektif produk, fungsi produk, karakteristik pengguna, batasan) +3. Kebutuhan Fungsional (Uraian rinci fitur sistem, input, proses, output) +4. Kebutuhan Non-Fungsional (Performa, keamanan, usability, reliabilitas, portabilitas) +5. Antarmuka Eksternal (UI, hardware, software, komunikasi) +6. Definisi Fungsi dan Kebutuhan Pengguna + +Lakukan analisis bisnis proses yang lebih mendalam mengenai bagaimana aplikasi ini digunakan dalam skenario nyata pengentasan kemiskinan agar dokumen SKPL ini terlihat komprehensif dan relevan dengan tujuan akhirnya. diff --git a/sosial/usecase_diagram.drawio b/sosial/usecase_diagram.drawio new file mode 100644 index 0000000..c7c084f --- /dev/null +++ b/sosial/usecase_diagram.drawio @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spbu/index.php b/spbu/index.php new file mode 100644 index 0000000..e5a4bf7 --- /dev/null +++ b/spbu/index.php @@ -0,0 +1,67 @@ + + + + + + WebGIS SPBU — Pontianak + + + + + + + + + + + + + + +
+ + +
+ Pontianak, Kalimantan Barat + +
+
+
SPBU 24 Jam +
+
+
Tidak 24 Jam +
+
+
Titik SPBU +
+
+ +
+ + +
+ + +
+
+ +
+ + + + + + + + + +