Project UAS Sistem Informasi Geografis (SIG)
This commit is contained in:
@@ -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 <url-repository>
|
||||
```
|
||||
|
||||
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.
|
||||
+596
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS Geometri - Point, Polyline, Polygon</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw/dist/leaflet.draw.css"/>
|
||||
<link rel="stylesheet" href="../css/style.css"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<a href="../" class="back-btn" title="Kembali ke Menu">←</a>
|
||||
<div class="logo">
|
||||
<div class="logo-icon" style="background:#22c55e;">G</div>
|
||||
<div class="logo-text">WEB<span>GIS</span> GEOMETRI</div>
|
||||
</div>
|
||||
<div class="header-divider"></div>
|
||||
<span style="font-size:12px;color:var(--muted);">Point, Polyline, Polygon</span>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot" style="background:#3b82f6"></div> Point
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-line" style="background:#a855f7"></div> Polyline/Jalan
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-poly" style="background:#10b981"></div> Polygon/Parsil
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-draw/dist/leaflet.draw.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-geometryutil/src/leaflet.geometryutil.js"></script>
|
||||
<script src="../js/draw_geometri.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,263 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS Pontianak - Pilih Proyek</title>
|
||||
<meta name="description" content="Dashboard proyek WebGIS: geometri dasar, pemetaan SPBU, dan pengentasan kemiskinan.">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #0a0d14;
|
||||
--panel: #111520;
|
||||
--border: #1e2233;
|
||||
--text: #e2e8f0;
|
||||
--muted: #64748b;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
background:
|
||||
linear-gradient(rgba(255,255,255,.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,.025) 1px, transparent 1px),
|
||||
var(--bg);
|
||||
background-size: 48px 48px;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: min(1120px, 100%);
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
margin-bottom: 34px;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid rgba(74, 222, 128, 0.3);
|
||||
border-radius: 999px;
|
||||
background: rgba(74, 222, 128, 0.06);
|
||||
color: #4ade80;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 11px;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.hero-badge::before {
|
||||
content: '';
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #4ade80;
|
||||
box-shadow: 0 0 10px #4ade80;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: clamp(28px, 5vw, 44px);
|
||||
line-height: 1.15;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.hero-title span { color: #4ade80; }
|
||||
|
||||
.hero-sub {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
color: var(--muted);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.card {
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 26px;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
transition: transform .2s, border-color .2s, box-shadow .2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.card-geometri:hover {
|
||||
border-color: rgba(34, 197, 94, .55);
|
||||
box-shadow: 0 18px 50px rgba(34, 197, 94, .12);
|
||||
}
|
||||
|
||||
.card-spbu:hover {
|
||||
border-color: rgba(59, 130, 246, .55);
|
||||
box-shadow: 0 18px 50px rgba(59, 130, 246, .12);
|
||||
}
|
||||
|
||||
.card-sosial:hover {
|
||||
border-color: rgba(245, 158, 11, .55);
|
||||
box-shadow: 0 18px 50px rgba(245, 158, 11, .12);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 22px;
|
||||
margin-bottom: 18px;
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.card-geometri .card-icon { background: rgba(34,197,94,.14); color: #22c55e; border: 1px solid rgba(34,197,94,.3); }
|
||||
.card-spbu .card-icon { background: rgba(59,130,246,.14); color: #60a5fa; border: 1px solid rgba(59,130,246,.3); }
|
||||
.card-sosial .card-icon { background: rgba(245,158,11,.14); color: #f59e0b; border: 1px solid rgba(245,158,11,.3); }
|
||||
|
||||
.card-title {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.card-geometri .card-title { color: #22c55e; }
|
||||
.card-spbu .card-title { color: #60a5fa; }
|
||||
.card-sosial .card-title { color: #f59e0b; }
|
||||
|
||||
.card-desc {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
margin-top: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tag-blue { background: rgba(59,130,246,.1); border-color: rgba(59,130,246,.3); color: #60a5fa; }
|
||||
.tag-cyan { background: rgba(56,189,248,.1); border-color: rgba(56,189,248,.3); color: #38bdf8; }
|
||||
.tag-green2 { background: rgba(34,197,94,.1); border-color: rgba(34,197,94,.3); color: #22c55e; }
|
||||
.tag-green { background: rgba(16,185,129,.1); border-color: rgba(16,185,129,.3); color: #34d399; }
|
||||
.tag-purple { background: rgba(168,85,247,.1); border-color: rgba(168,85,247,.3); color: #c084fc; }
|
||||
.tag-amber { background: rgba(245,158,11,.1); border-color: rgba(245,158,11,.3); color: #f59e0b; }
|
||||
.tag-pink { background: rgba(232,121,249,.1); border-color: rgba(232,121,249,.3); color: #e879f9; }
|
||||
|
||||
.card-cta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.card-geometri .card-cta { background: rgba(34,197,94,.12); color: #22c55e; }
|
||||
.card-spbu .card-cta { background: rgba(59,130,246,.12); color: #60a5fa; }
|
||||
.card-sosial .card-cta { background: rgba(245,158,11,.12); color: #f59e0b; }
|
||||
|
||||
.footer {
|
||||
margin-top: 28px;
|
||||
text-align: center;
|
||||
font-family: 'Space Mono', monospace;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
letter-spacing: .05em;
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.cards { grid-template-columns: 1fr; }
|
||||
.card { min-height: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<section class="hero">
|
||||
<div class="hero-badge">Sistem Informasi Geografis</div>
|
||||
<h1 class="hero-title">WEB<span>GIS</span> Pontianak</h1>
|
||||
<p class="hero-sub">Pilih salah satu proyek WebGIS. Tiap folder sudah memiliki dashboard peta mandiri dan tetap memakai koneksi database yang sama.</p>
|
||||
</section>
|
||||
|
||||
<section class="cards">
|
||||
<a href="geometri/" class="card card-geometri">
|
||||
<div class="card-icon">G</div>
|
||||
<div class="card-title">WebGIS Geometri</div>
|
||||
<p class="card-desc">Dashboard untuk menggambar dan mengelola data point, polyline, dan polygon.</p>
|
||||
<div class="card-tags">
|
||||
<span class="tag tag-green2">Point</span>
|
||||
<span class="tag tag-purple">Polyline</span>
|
||||
<span class="tag tag-green">Polygon</span>
|
||||
</div>
|
||||
<div class="card-cta"><span>Buka Geometri</span><span>→</span></div>
|
||||
</a>
|
||||
|
||||
<a href="spbu/" class="card card-spbu">
|
||||
<div class="card-icon">S</div>
|
||||
<div class="card-title">WebGIS SPBU</div>
|
||||
<p class="card-desc">Dashboard khusus untuk menambah, melihat, dan menghapus titik lokasi SPBU.</p>
|
||||
<div class="card-tags">
|
||||
<span class="tag tag-blue">Titik SPBU</span>
|
||||
<span class="tag tag-green">24 Jam</span>
|
||||
<span class="tag tag-purple">Tidak 24 Jam</span>
|
||||
</div>
|
||||
<div class="card-cta"><span>Buka SPBU</span><span>→</span></div>
|
||||
</a>
|
||||
|
||||
<a href="sosial/" class="card card-sosial">
|
||||
<div class="card-icon">K</div>
|
||||
<div class="card-title">WebGIS Sosial</div>
|
||||
<p class="card-desc">Dashboard pengentasan kemiskinan berbasis lokasi masjid, penduduk miskin, radius layanan, dan blind spot.</p>
|
||||
<div class="card-tags">
|
||||
<span class="tag tag-amber">Masjid</span>
|
||||
<span class="tag tag-pink">Penduduk Miskin</span>
|
||||
<span class="tag tag-amber">Analisis Radius</span>
|
||||
</div>
|
||||
<div class="card-cta"><span>Buka Sosial</span><span>→</span></div>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<p class="footer">© 2026 - WebGIS Pontianak</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
+425
@@ -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;i<ll.length-1;i++) t+=ll[i].distanceTo(ll[i+1]);
|
||||
return t;
|
||||
}
|
||||
function fmtNum(n, dec=2) {
|
||||
return parseFloat(n).toLocaleString('id-ID', { maximumFractionDigits: dec });
|
||||
}
|
||||
function statusBadge(s, type) {
|
||||
if (type==='point') return s==='24 Jam' ? `<span class="ip-badge badge-green">24 Jam</span>` : `<span class="ip-badge badge-red">Tidak 24 Jam</span>`;
|
||||
if (type==='line') { let c=s==='Nasional'?'badge-red':s==='Provinsi'?'badge-blue':'badge-gray'; return `<span class="ip-badge ${c}">${s}</span>`; }
|
||||
return `<span class="ip-badge ${s==='SHM'?'badge-green':'badge-blue'}">${s}</span>`;
|
||||
}
|
||||
|
||||
/* ── Icons ── */
|
||||
function makeIcon(status) {
|
||||
var c = status==='24 Jam' ? '#4ade80' : '#f87171';
|
||||
return L.divIcon({ html:`<svg xmlns="http://www.w3.org/2000/svg" width="26" height="38" viewBox="0 0 26 38"><path d="M13 0C5.8 0 0 5.8 0 13c0 9.75 13 25 13 25S26 22.75 26 13C26 5.8 20.2 0 13 0z" fill="${c}" stroke="rgba(255,255,255,.25)" stroke-width="1.5"/><circle cx="13" cy="13" r="5" fill="rgba(0,0,0,.4)"/><circle cx="13" cy="13" r="3" fill="white"/></svg>`, iconSize:[26,38], iconAnchor:[13,38], popupAnchor:[0,-40], className:'' });
|
||||
}
|
||||
function makeMasjidIcon() {
|
||||
return L.divIcon({ html:`<svg xmlns="http://www.w3.org/2000/svg" width="30" height="42" viewBox="0 0 30 42"><path d="M15 0C6.7 0 0 6.7 0 15c0 11.25 15 27 15 27S30 26.25 30 15C30 6.7 23.3 0 15 0z" fill="#f59e0b" stroke="rgba(255,255,255,.3)" stroke-width="1.5"/><text x="15" y="20" text-anchor="middle" font-size="13" fill="white">🕌</text></svg>`, iconSize:[30,42], iconAnchor:[15,42], popupAnchor:[0,-44], className:'' });
|
||||
}
|
||||
function makePendudukIcon(inRadius) {
|
||||
var c = inRadius ? '#f59e0b' : '#e879f9';
|
||||
return L.divIcon({ html:`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="32" viewBox="0 0 22 32"><path d="M11 0C4.9 0 0 4.9 0 11c0 8.25 11 21 11 21S22 19.25 22 11C22 4.9 17.1 0 11 0z" fill="${c}" stroke="rgba(255,255,255,.3)" stroke-width="1.5"/><circle cx="11" cy="11" r="4" fill="rgba(0,0,0,.35)"/><circle cx="11" cy="11" r="2.5" fill="white"/></svg>`, 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 = `<div class="ip-row"><span class="ip-label">No. SPBU</span><span class="ip-val">${d.no_spbu}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Status</span><span class="ip-val">${statusBadge(d.status,'point')}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Koordinat</span><span class="ip-val" style="font-size:10px">${parseFloat(d.latitude).toFixed(5)}, ${parseFloat(d.longitude).toFixed(5)}</span></div>`;
|
||||
} else if (type==='line') {
|
||||
rows = `<div class="ip-row"><span class="ip-label">Status</span><span class="ip-val">${statusBadge(d.status,'line')}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Panjang</span><span class="ip-val">${fmtNum(d.panjang)} m</span></div>`;
|
||||
} else if (type==='polygon') {
|
||||
rows = `<div class="ip-row"><span class="ip-label">Status</span><span class="ip-val">${statusBadge(d.status,'polygon')}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Luas</span><span class="ip-val">${fmtNum(d.luas)} m²</span></div>`;
|
||||
} else if (type==='masjid') {
|
||||
rows = `<div class="ip-row"><span class="ip-label">PIC</span><span class="ip-val">${d.pic}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Radius</span><span class="ip-val" id="radiusDisplay_${d.id}">${fmtNum(d.radius,0)} m</span></div>
|
||||
<div class="radius-slider-wrap">
|
||||
<input type="range" min="100" max="5000" step="50" value="${d.radius}" id="radiusSlider_${d.id}" oninput="onRadiusSlide(${d.id}, this.value)">
|
||||
<div class="radius-val"><span>100m</span><span>5000m</span></div>
|
||||
</div>
|
||||
<button class="pf-btn" style="margin-top:10px" onclick="saveRadius(${d.id})">💾 Simpan Radius</button>`;
|
||||
} else if (type==='penduduk') {
|
||||
rows = `<div class="ip-row"><span class="ip-label">Jml. Anggota KK</span><span class="ip-val">${d.jumlah_anggota} orang</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Alamat</span><span class="ip-val" style="font-size:11px">${d.alamat}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Koordinat</span><span class="ip-val" style="font-size:10px">${parseFloat(d.latitude).toFixed(5)}, ${parseFloat(d.longitude).toFixed(5)}</span></div>`;
|
||||
}
|
||||
|
||||
return `<div class="ip"><div class="ip-head" style="background:${hc[type]}">${ic[type]} ${ti[type]}</div><div class="ip-body">${rows}<button class="ip-del" onclick="hapusData(${d.id},'${type}')">🗑 Hapus</button></div></div>`;
|
||||
}
|
||||
|
||||
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<minDist) { minDist=dist; inRadius=true; }
|
||||
});
|
||||
var marker = L.marker([pLat,pLng], { icon:makePendudukIcon(inRadius) });
|
||||
marker._type='penduduk'; marker._db_id=d.id;
|
||||
marker.bindPopup(buildInfoPopup(d,'penduduk'), { maxWidth:260 });
|
||||
layerGroupPenduduk.addLayer(marker);
|
||||
pendudukData.push({ id:parseInt(d.id), lat:pLat, lng:pLng, markerLayer:marker, inRadius });
|
||||
});
|
||||
})
|
||||
.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));
|
||||
});
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
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(`<div class="pf"><div class="pf-head pf-head-point">⛽ Tambah SPBU</div><div class="pf-body">
|
||||
<label>Nama SPBU</label><input id="pNama" placeholder="contoh: SPBU Sungai Jawi">
|
||||
<label>No. SPBU</label><input id="pNo" placeholder="contoh: 64.761.xx">
|
||||
<label>Status Operasional</label>
|
||||
<select id="pStatus"><option value="24 Jam">24 Jam</option><option value="Tidak">Tidak 24 Jam</option></select>
|
||||
<div class="pf-info">📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}</div>
|
||||
<button class="pf-btn" onclick="savePoint(${ll.lat},${ll.lng})">💾 Simpan</button>
|
||||
</div></div>`, { 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(`<div class="pf"><div class="pf-head" style="background:linear-gradient(135deg,#92400e,#f59e0b);color:#fff;">🕌 Tambah Masjid</div><div class="pf-body">
|
||||
<label>Nama Masjid</label><input id="mNama" placeholder="contoh: Masjid Al-Falah">
|
||||
<label>PIC Masjid</label><input id="mPIC" placeholder="contoh: Ust. Ahmad">
|
||||
<label>Radius Deteksi</label>
|
||||
<div class="radius-slider-wrap">
|
||||
<input type="range" min="100" max="5000" step="50" value="${defaultRadius}" id="mRadiusSlider" oninput="previewRadius(this.value)">
|
||||
<div class="radius-val"><span>100m</span><span id="mRadiusVal">${defaultRadius} m</span><span>5000m</span></div>
|
||||
</div>
|
||||
<div class="pf-info">📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}</div>
|
||||
<button class="pf-btn" onclick="saveMasjid(${ll.lat},${ll.lng})">💾 Simpan</button>
|
||||
</div></div>`, { 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(`<div class="pf"><div class="pf-head" style="background:linear-gradient(135deg,#701a75,#e879f9);color:#fff;">👨👩👧👦 Tambah Penduduk Miskin</div><div class="pf-body">
|
||||
<label>Nama Kepala Keluarga</label><input id="pdNama" placeholder="contoh: Bapak Suratman">
|
||||
<label>Jumlah Anggota KK</label><input id="pdJml" type="number" min="1" value="1">
|
||||
<label>Alamat</label><input id="pdAlamat" placeholder="contoh: Jl. Imam Bonjol No. 5">
|
||||
<div class="pf-info">📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}</div>
|
||||
<button class="pf-btn" onclick="savePenduduk(${ll.lat},${ll.lng})">💾 Simpan</button>
|
||||
</div></div>`, { 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(`<div class="pf"><div class="pf-head pf-head-line">🛣️ Tambah Jalan</div><div class="pf-body">
|
||||
<label>Nama Jalan</label><input id="lNama" placeholder="contoh: Jl. Ahmad Yani">
|
||||
<label>Klasifikasi</label><select id="lStatus"><option value="Nasional">Nasional</option><option value="Provinsi">Provinsi</option><option value="Kabupaten">Kabupaten</option></select>
|
||||
<div class="pf-info">📏 ${fmtNum(p)} m</div>
|
||||
<button class="pf-btn" onclick="saveLine()">💾 Simpan</button>
|
||||
</div></div>`).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(`<div class="pf"><div class="pf-head pf-head-polygon">📐 Tambah Parsil</div><div class="pf-body">
|
||||
<label>Nama Area</label><input id="gNama" placeholder="contoh: Lahan A">
|
||||
<label>Status Kepemilikan</label><select id="gStatus"><option value="SHM">SHM</option><option value="HGB">HGB</option><option value="HGU">HGU</option><option value="HP">HP</option></select>
|
||||
<div class="pf-info">📐 ${fmtNum(luas)} m²</div>
|
||||
<button class="pf-btn" onclick="savePolygon()">💾 Simpan</button>
|
||||
</div></div>`).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}`);
|
||||
});
|
||||
});
|
||||
@@ -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 `<span class="ip-badge ${c}">${status}</span>`;
|
||||
}
|
||||
return `<span class="ip-badge ${status === 'SHM' ? 'badge-green' : 'badge-blue'}">${status}</span>`;
|
||||
}
|
||||
|
||||
function makePointIcon() {
|
||||
var c = '#3b82f6';
|
||||
return L.divIcon({
|
||||
html: `<svg xmlns="http://www.w3.org/2000/svg" width="26" height="38" viewBox="0 0 26 38"><path d="M13 0C5.8 0 0 5.8 0 13c0 9.75 13 25 13 25S26 22.75 26 13C26 5.8 20.2 0 13 0z" fill="${c}" stroke="rgba(255,255,255,.25)" stroke-width="1.5"/><circle cx="13" cy="13" r="5" fill="rgba(0,0,0,.4)"/><circle cx="13" cy="13" r="3" fill="white"/></svg>`,
|
||||
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 = `<div class="ip-row"><span class="ip-label">Nama</span><span class="ip-val">${d.nama}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Kode Point</span><span class="ip-val">${d.no_spbu}</span></div>`;
|
||||
} else if (type === 'line') {
|
||||
rows = `<div class="ip-row"><span class="ip-label">Status</span><span class="ip-val">${statusBadge(d.status, 'line')}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Panjang</span><span class="ip-val">${fmtNum(d.panjang)} m</span></div>`;
|
||||
} else if (type === 'polygon') {
|
||||
rows = `<div class="ip-row"><span class="ip-label">Status</span><span class="ip-val">${statusBadge(d.status, 'polygon')}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Luas</span><span class="ip-val">${fmtNum(d.luas)} m²</span></div>`;
|
||||
}
|
||||
|
||||
return `<div class="ip">
|
||||
<div class="ip-head" style="background:${headerColors[type]}">${icons[type]} ${titles[type]}</div>
|
||||
<div class="ip-body">${rows}<button class="ip-del" onclick="hapusData(${d.id},'${type}')">Hapus</button></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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(`<div class="pf">
|
||||
<div class="pf-head pf-head-point">📍 Tambah Point</div>
|
||||
<div class="pf-body">
|
||||
<label>Nama Point</label><input id="pNama" placeholder="contoh: Titik Survei A">
|
||||
<label>Kode Point</label><input id="pNo" placeholder="contoh: P-001">
|
||||
<div class="pf-info">📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}</div>
|
||||
<button class="pf-btn" onclick="savePoint(${ll.lat},${ll.lng})">Simpan</button>
|
||||
</div>
|
||||
</div>`, { 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(`<div class="pf">
|
||||
<div class="pf-head pf-head-line">▱ Tambah Jalan</div>
|
||||
<div class="pf-body">
|
||||
<label>Nama Jalan</label><input id="lNama" placeholder="contoh: Jl. Ahmad Yani">
|
||||
<label>Klasifikasi</label><select id="lStatus"><option value="Nasional">Nasional</option><option value="Provinsi">Provinsi</option><option value="Kabupaten">Kabupaten</option></select>
|
||||
<div class="pf-info">${fmtNum(panjang)} m</div>
|
||||
<button class="pf-btn" onclick="saveLine()">Simpan</button>
|
||||
</div>
|
||||
</div>`).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(`<div class="pf">
|
||||
<div class="pf-head pf-head-polygon">▰ Tambah Parsil</div>
|
||||
<div class="pf-body">
|
||||
<label>Nama Area</label><input id="gNama" placeholder="contoh: Lahan A">
|
||||
<label>Status Kepemilikan</label><select id="gStatus"><option value="SHM">SHM</option><option value="HGB">HGB</option><option value="HGU">HGU</option><option value="HP">HP</option></select>
|
||||
<div class="pf-info">${fmtNum(luas)} m²</div>
|
||||
<button class="pf-btn" onclick="savePolygon()">Simpan</button>
|
||||
</div>
|
||||
</div>`).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}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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:`<svg xmlns="http://www.w3.org/2000/svg" width="30" height="42" viewBox="0 0 30 42"><path d="M15 0C6.7 0 0 6.7 0 15c0 11.25 15 27 15 27S30 26.25 30 15C30 6.7 23.3 0 15 0z" fill="#f59e0b" stroke="rgba(255,255,255,.3)" stroke-width="1.5"/><text x="15" y="20" text-anchor="middle" font-size="13" fill="white">🕌</text></svg>`, 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:`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="32" viewBox="0 0 22 32"><path d="M11 0C4.9 0 0 4.9 0 11c0 8.25 11 21 11 21S22 19.25 22 11C22 4.9 17.1 0 11 0z" fill="${c}" stroke="rgba(255,255,255,.3)" stroke-width="1.5"/><circle cx="11" cy="11" r="4" fill="rgba(0,0,0,.35)"/><circle cx="11" cy="11" r="2.5" fill="white"/></svg>`, 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 = `<div style="color:#6b7280;font-size:12px;text-align:center;padding:12px">Belum ada penduduk dalam radius ini.</div>`;
|
||||
} else {
|
||||
penerima.forEach((p, i) => {
|
||||
var sl = getSkorLabel(p.skor);
|
||||
rowsPenerima += `
|
||||
<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid #1e2233;">
|
||||
<span style="font-size:16px;min-width:20px">${sl.emoji}</span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:12px;font-weight:600;color:#e2e8f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${p.nama || '—'}</div>
|
||||
<div style="font-size:10px;color:#6b7280">Skor: ${fmtNum(p.skor,1)} · ${sl.label}</div>
|
||||
</div>
|
||||
<span title="${p.jenis}" style="font-size:16px">${getJenisIcon(p.jenis)}</span>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
return `<div class="ip" style="min-width:280px">
|
||||
<div class="ip-head" style="background:linear-gradient(135deg,#92400e,#f59e0b)">🕌 ${d.nama}</div>
|
||||
<div style="display:flex;border-bottom:1px solid #1e2233;margin-bottom:8px">
|
||||
<button id="tab1_${d.id}" onclick="switchTab(${d.id},1)" style="flex:1;padding:7px;font-size:11px;background:#1e2233;border:none;color:#f59e0b;cursor:pointer;font-weight:600;border-radius:0">📋 Profil</button>
|
||||
<button id="tab2_${d.id}" onclick="switchTab(${d.id},2)" style="flex:1;padding:7px;font-size:11px;background:transparent;border:none;color:#6b7280;cursor:pointer;border-radius:0">👥 Penerima (${penerima.length})</button>
|
||||
</div>
|
||||
<div id="tabContent1_${d.id}" class="ip-body">
|
||||
<div class="ip-row"><span class="ip-label">PIC</span><span class="ip-val">${d.pic}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Radius</span><span class="ip-val" id="radiusDisplay_${d.id}">${fmtNum(d.radius,0)} m</span></div>
|
||||
<div class="radius-slider-wrap">
|
||||
<input type="range" min="100" max="5000" step="50" value="${d.radius}" id="radiusSlider_${d.id}" oninput="onRadiusSlide(${d.id}, this.value)">
|
||||
<div class="radius-val"><span>100m</span><span>5000m</span></div>
|
||||
</div>
|
||||
<button class="pf-btn" style="margin-top:10px" onclick="saveRadius(${d.id})">💾 Simpan Radius</button>
|
||||
<button class="ip-del" onclick="hapusData(${d.id},'masjid')">🗑 Hapus</button>
|
||||
</div>
|
||||
<div id="tabContent2_${d.id}" class="ip-body" style="display:none;max-height:220px;overflow-y:auto">
|
||||
${rowsPenerima}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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
|
||||
? `<div class="ip-row"><span class="ip-label">Masjid Penanggung</span><span class="ip-val" style="color:#f59e0b;font-weight:600">🕌 ${penanggung.nama}</span></div>`
|
||||
: `<div class="ip-row"><span class="ip-label">Status</span><span class="ip-val" style="color:#ef4444;font-weight:600">⚠️ Blind Spot</span></div>`;
|
||||
|
||||
// Dropdown jenis bantuan
|
||||
var optionsHtml = JENIS_BANTUAN_OPTS.map(o =>
|
||||
`<option value="${o.value}" ${d.jenis_bantuan === o.value ? 'selected' : ''}>${o.icon} ${o.value}</option>`
|
||||
).join('');
|
||||
|
||||
var selectStyle = `width:100%;padding:6px;border-radius:6px;border:1px solid #1e2233;background:#0a0d14;color:#e2e8f0;font-size:12px;`;
|
||||
|
||||
return `<div class="ip">
|
||||
<div class="ip-head" style="background:linear-gradient(135deg,#701a75,#e879f9)">👨👩👧👦 KK — ${d.nama_kk}</div>
|
||||
<div class="ip-body">
|
||||
<div class="ip-row"><span class="ip-label">Jml. Anggota KK</span><span class="ip-val">${d.jumlah_anggota} orang</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Alamat</span><span class="ip-val" style="font-size:11px">${d.alamat}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Penghasilan</span><span class="ip-val">${d.penghasilan}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Kondisi Rumah</span><span class="ip-val">${d.kondisi_rumah}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Sumber Air</span><span class="ip-val">${d.sumber_air}</span></div>
|
||||
<div class="ip-row">
|
||||
<span class="ip-label">Skor Kerentanan</span>
|
||||
<span class="ip-val" style="color:${sl.color};font-weight:700">${sl.emoji} ${fmtNum(skor,1)} — ${sl.label}</span>
|
||||
</div>
|
||||
${penanggungHtml}
|
||||
<div style="margin-top:10px">
|
||||
<label style="font-size:11px;color:#9ca3af;display:block;margin-bottom:4px">🤝 Jenis Bantuan Direkomendasikan</label>
|
||||
<select id="jenisBantuan_${d.id}" style="${selectStyle}">${optionsHtml}</select>
|
||||
<button class="pf-btn" style="margin-top:6px" onclick="saveJenisBantuan(${d.id})">💾 Simpan Bantuan</button>
|
||||
</div>
|
||||
<button class="ip-del" onclick="hapusData(${d.id},'penduduk')">🗑 Hapus</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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<minDist) { minDist=dist; inRadius=true; }
|
||||
});
|
||||
|
||||
var pEntry = {
|
||||
id:parseInt(d.id), lat:pLat, lng:pLng,
|
||||
skor, jenis: d.jenis_bantuan, nama: d.nama_kk,
|
||||
inRadius, rawData: d
|
||||
};
|
||||
pendudukData.push(pEntry);
|
||||
|
||||
var marker = L.marker([pLat,pLng], { icon:makePendudukIcon(inRadius) });
|
||||
marker._type='penduduk'; marker._db_id=d.id;
|
||||
pEntry.markerLayer = marker;
|
||||
marker.bindPopup(() => 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 =>
|
||||
`<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;font-size:11px">
|
||||
<span style="color:#9ca3af">${o.icon} ${o.value}</span>
|
||||
<span style="font-weight:700;color:#e2e8f0">${jenisCounts[o.value]}</span>
|
||||
</div>`
|
||||
).join('');
|
||||
|
||||
var el = document.getElementById('statsContent');
|
||||
if (!el) return;
|
||||
el.innerHTML = `
|
||||
<div class="stat-row" style="margin-bottom:10px">
|
||||
<div class="stat-item"><div class="stat-num">${total}</div><div class="stat-lbl">Total Warga</div></div>
|
||||
<div class="stat-item" style="color:#ef4444"><div class="stat-num" style="color:#ef4444">${sangat}</div><div class="stat-lbl">🔴 Sgt. Rentan</div></div>
|
||||
<div class="stat-item"><div class="stat-num" style="color:#f97316">${rentan}</div><div class="stat-lbl">🟠 Rentan</div></div>
|
||||
<div class="stat-item"><div class="stat-num" style="color:#eab308">${cukup}</div><div class="stat-lbl">🟡 Cukup</div></div>
|
||||
</div>
|
||||
<div style="border-top:1px solid #1e2233;padding-top:8px;margin-bottom:8px">
|
||||
<div style="font-size:10px;color:#6b7280;font-weight:600;margin-bottom:6px;text-transform:uppercase;letter-spacing:0.05em">Jenis Bantuan Dibutuhkan</div>
|
||||
${jenisRows}
|
||||
</div>
|
||||
<div style="border-top:1px solid #1e2233;padding-top:8px">
|
||||
<div style="display:flex;justify-content:space-between;font-size:11px">
|
||||
<span style="color:#9ca3af">⚠️ Blind Spot (skor ≥70)</span>
|
||||
<span style="font-weight:700;color:${bs>0?'#ef4444':'#4ade80'}">${bs} KK</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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(`<div class="pf"><div class="pf-head" style="background:linear-gradient(135deg,#92400e,#f59e0b);color:#fff;">🕌 Tambah Masjid</div><div class="pf-body">
|
||||
<label>Nama Masjid</label><input id="mNama" placeholder="contoh: Masjid Al-Falah">
|
||||
<label>PIC Masjid</label><input id="mPIC" placeholder="contoh: Ust. Ahmad">
|
||||
<label>Radius Deteksi</label>
|
||||
<div class="radius-slider-wrap">
|
||||
<input type="range" min="100" max="5000" step="50" value="${defaultRadius}" id="mRadiusSlider" oninput="previewRadius(this.value)">
|
||||
<div class="radius-val"><span>100m</span><span id="mRadiusVal">${defaultRadius} m</span><span>5000m</span></div>
|
||||
</div>
|
||||
<div class="pf-info">📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}</div>
|
||||
<button class="pf-btn" onclick="saveMasjid(${ll.lat},${ll.lng})">💾 Simpan</button>
|
||||
</div></div>`, { 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(`<div class="pf"><div class="pf-head" style="background:linear-gradient(135deg,#701a75,#e879f9);color:#fff;">👨👩👧👦 Tambah Penduduk Miskin</div><div class="pf-body">
|
||||
<label>Nama Kepala Keluarga</label><input id="pdNama" placeholder="contoh: Bapak Suratman">
|
||||
<label>Jumlah Anggota KK</label><input id="pdJml" type="number" min="1" value="1">
|
||||
<label>Alamat</label><input id="pdAlamat" placeholder="Sedang mengambil alamat riil... ⏳">
|
||||
<label>Penghasilan Per Bulan (50%)</label>
|
||||
<select id="pdPenghasilan" style="${selectStyle}">
|
||||
<option value="< 500rb">< 500rb</option>
|
||||
<option value="500rb - 1jt">500rb - 1jt</option>
|
||||
<option value="> 1jt">> 1jt</option>
|
||||
</select>
|
||||
<label>Kondisi Rumah (30%)</label>
|
||||
<select id="pdRumah" style="${selectStyle}">
|
||||
<option value="Bambu/Tepas">Bambu/Tepas</option>
|
||||
<option value="Kayu">Kayu</option>
|
||||
<option value="Tembok">Tembok</option>
|
||||
</select>
|
||||
<label>Sumber Air Bersih (20%)</label>
|
||||
<select id="pdAir" style="${selectStyle}">
|
||||
<option value="Sungai">Sungai</option>
|
||||
<option value="Sumur">Sumur</option>
|
||||
<option value="PDAM">PDAM</option>
|
||||
</select>
|
||||
<div class="pf-info">📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}</div>
|
||||
<button class="pf-btn" onclick="savePenduduk(${ll.lat},${ll.lng})">💾 Simpan</button>
|
||||
</div></div>`, { 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.'));
|
||||
}
|
||||
@@ -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:`<svg xmlns="http://www.w3.org/2000/svg" width="30" height="42" viewBox="0 0 30 42"><path d="M15 0C6.7 0 0 6.7 0 15c0 11.25 15 27 15 27S30 26.25 30 15C30 6.7 23.3 0 15 0z" fill="#f59e0b" stroke="rgba(255,255,255,.3)" stroke-width="1.5"/><text x="15" y="20" text-anchor="middle" font-size="13" fill="white">🕌</text></svg>`, 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:`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="32" viewBox="0 0 22 32"><path d="M11 0C4.9 0 0 4.9 0 11c0 8.25 11 21 11 21S22 19.25 22 11C22 4.9 17.1 0 11 0z" fill="${c}" stroke="rgba(255,255,255,.3)" stroke-width="1.5"/><circle cx="11" cy="11" r="4" fill="rgba(0,0,0,.35)"/><circle cx="11" cy="11" r="2.5" fill="white"/></svg>`, 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
|
||||
? `<div style="color:#6b7280;font-size:12px;text-align:center;padding:12px">Belum ada penduduk dalam radius ini.</div>`
|
||||
: penerima.map(p => {
|
||||
var sl = getSkorLabel(p.skor);
|
||||
return `<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid #1e2233;">
|
||||
<span style="font-size:16px;min-width:20px">${sl.emoji}</span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:12px;font-weight:600;color:#e2e8f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${p.nama || '—'}</div>
|
||||
<div style="font-size:10px;color:#6b7280">Skor: ${fmtNum(p.skor,1)} · ${sl.label}</div>
|
||||
</div>
|
||||
<span title="${p.jenis}" style="font-size:16px">${getJenisIcon(p.jenis)}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
return `<div class="ip" style="min-width:280px">
|
||||
<div class="ip-head" style="background:linear-gradient(135deg,#92400e,#f59e0b)">🕌 ${d.nama}</div>
|
||||
<div style="display:flex;border-bottom:1px solid #1e2233;margin-bottom:8px">
|
||||
<button id="tab1_${d.id}" onclick="switchTab(${d.id},1)" style="flex:1;padding:7px;font-size:11px;background:#1e2233;border:none;color:#f59e0b;cursor:pointer;font-weight:600">📋 Profil</button>
|
||||
<button id="tab2_${d.id}" onclick="switchTab(${d.id},2)" style="flex:1;padding:7px;font-size:11px;background:transparent;border:none;color:#6b7280;cursor:pointer">👥 Penerima (${penerima.length})</button>
|
||||
</div>
|
||||
<div id="tabContent1_${d.id}" class="ip-body">
|
||||
<div class="ip-row"><span class="ip-label">PIC</span><span class="ip-val">${d.pic}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Radius Layanan</span><span class="ip-val">${fmtNum(d.radius,0)} m</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Jml. Ditanggung</span><span class="ip-val" style="color:#f59e0b;font-weight:700">${penerima.length} KK</span></div>
|
||||
</div>
|
||||
<div id="tabContent2_${d.id}" class="ip-body" style="display:none;max-height:220px;overflow-y:auto">
|
||||
${rowsPenerima}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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
|
||||
? `<div class="ip-row"><span class="ip-label">Masjid Penanggung</span><span class="ip-val" style="color:#f59e0b;font-weight:600">🕌 ${penanggung.nama}</span></div>`
|
||||
: `<div class="ip-row"><span class="ip-label">Status</span><span class="ip-val" style="color:#ef4444;font-weight:600">⚠️ Blind Spot</span></div>`;
|
||||
|
||||
var jenisIcon = getJenisIcon(d.jenis_bantuan);
|
||||
|
||||
return `<div class="ip">
|
||||
<div class="ip-head" style="background:linear-gradient(135deg,#701a75,#e879f9)">👨👩👧👦 KK — ${d.nama_kk}</div>
|
||||
<div class="ip-body">
|
||||
<div class="ip-row"><span class="ip-label">Jml. Anggota KK</span><span class="ip-val">${d.jumlah_anggota} orang</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Alamat</span><span class="ip-val" style="font-size:11px">${d.alamat}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Penghasilan</span><span class="ip-val">${d.penghasilan}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Kondisi Rumah</span><span class="ip-val">${d.kondisi_rumah}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Sumber Air</span><span class="ip-val">${d.sumber_air}</span></div>
|
||||
<div class="ip-row">
|
||||
<span class="ip-label">Skor Kerentanan</span>
|
||||
<span class="ip-val" style="color:${sl.color};font-weight:700">${sl.emoji} ${fmtNum(skor,1)} — ${sl.label}</span>
|
||||
</div>
|
||||
${penanggungHtml}
|
||||
<div class="ip-row">
|
||||
<span class="ip-label">Jenis Bantuan</span>
|
||||
<span class="ip-val">${jenisIcon} ${d.jenis_bantuan || '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
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<minDist) { minDist=dist; inRadius=true; }
|
||||
});
|
||||
|
||||
var pEntry = { id:parseInt(d.id), lat:pLat, lng:pLng, skor, jenis:d.jenis_bantuan, nama:d.nama_kk, inRadius, rawData:d };
|
||||
pendudukData.push(pEntry);
|
||||
|
||||
var marker = L.marker([pLat,pLng], { icon:makePendudukIcon(inRadius) });
|
||||
marker._type='penduduk'; marker._db_id=d.id;
|
||||
pEntry.markerLayer = marker;
|
||||
marker.bindPopup(() => 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 =>
|
||||
`<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;font-size:11px">
|
||||
<span style="color:#9ca3af">${o.icon} ${o.value}</span>
|
||||
<span style="font-weight:700;color:#e2e8f0">${jenisCounts[o.value]}</span>
|
||||
</div>`
|
||||
).join('');
|
||||
|
||||
var el = document.getElementById('statsContent');
|
||||
if (!el) return;
|
||||
el.innerHTML = `
|
||||
<div class="stat-row" style="margin-bottom:10px">
|
||||
<div class="stat-item"><div class="stat-num">${total}</div><div class="stat-lbl">Total Warga</div></div>
|
||||
<div class="stat-item"><div class="stat-num" style="color:#ef4444">${sangat}</div><div class="stat-lbl">🔴 Sgt. Rentan</div></div>
|
||||
<div class="stat-item"><div class="stat-num" style="color:#f97316">${rentan}</div><div class="stat-lbl">🟠 Rentan</div></div>
|
||||
<div class="stat-item"><div class="stat-num" style="color:#eab308">${cukup}</div><div class="stat-lbl">🟡 Cukup</div></div>
|
||||
</div>
|
||||
<div style="border-top:1px solid #1e2233;padding-top:8px;margin-bottom:8px">
|
||||
<div style="font-size:10px;color:#6b7280;font-weight:600;margin-bottom:6px;text-transform:uppercase;letter-spacing:0.05em">Jenis Bantuan Dibutuhkan</div>
|
||||
${jenisRows}
|
||||
</div>
|
||||
<div style="border-top:1px solid #1e2233;padding-top:8px">
|
||||
<div style="display:flex;justify-content:space-between;font-size:11px">
|
||||
<span style="color:#9ca3af">⚠️ Blind Spot (skor ≥70)</span>
|
||||
<span style="font-weight:700;color:${bs>0?'#ef4444':'#4ade80'}">${bs} KK</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 = '▼';
|
||||
}
|
||||
}
|
||||
+226
@@ -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;i<ll.length-1;i++) t+=ll[i].distanceTo(ll[i+1]);
|
||||
return t;
|
||||
}
|
||||
function fmtNum(n, dec=2) {
|
||||
return parseFloat(n).toLocaleString('id-ID', { maximumFractionDigits: dec });
|
||||
}
|
||||
function statusBadge(s, type) {
|
||||
if (type==='point') return s==='24 Jam' ? `<span class="ip-badge badge-green">24 Jam</span>` : `<span class="ip-badge badge-red">Tidak 24 Jam</span>`;
|
||||
if (type==='line') { let c=s==='Nasional'?'badge-red':s==='Provinsi'?'badge-blue':'badge-gray'; return `<span class="ip-badge ${c}">${s}</span>`; }
|
||||
return `<span class="ip-badge ${s==='SHM'?'badge-green':'badge-blue'}">${s}</span>`;
|
||||
}
|
||||
|
||||
/* ── Icons ── */
|
||||
function makeIcon(status) {
|
||||
var c = status==='24 Jam' ? '#4ade80' : '#f87171';
|
||||
return L.divIcon({ html:`<svg xmlns="http://www.w3.org/2000/svg" width="26" height="38" viewBox="0 0 26 38"><path d="M13 0C5.8 0 0 5.8 0 13c0 9.75 13 25 13 25S26 22.75 26 13C26 5.8 20.2 0 13 0z" fill="${c}" stroke="rgba(255,255,255,.25)" stroke-width="1.5"/><circle cx="13" cy="13" r="5" fill="rgba(0,0,0,.4)"/><circle cx="13" cy="13" r="3" fill="white"/></svg>`, 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 = `<div class="ip-row"><span class="ip-label">No. SPBU</span><span class="ip-val">${d.no_spbu}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Status</span><span class="ip-val">${statusBadge(d.status,'point')}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Koordinat</span><span class="ip-val" style="font-size:10px">${parseFloat(d.latitude).toFixed(5)}, ${parseFloat(d.longitude).toFixed(5)}</span></div>`;
|
||||
} else if (type==='line') {
|
||||
rows = `<div class="ip-row"><span class="ip-label">Status</span><span class="ip-val">${statusBadge(d.status,'line')}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Panjang</span><span class="ip-val">${fmtNum(d.panjang)} m</span></div>`;
|
||||
} else if (type==='polygon') {
|
||||
rows = `<div class="ip-row"><span class="ip-label">Status</span><span class="ip-val">${statusBadge(d.status,'polygon')}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Luas</span><span class="ip-val">${fmtNum(d.luas)} m²</span></div>`;
|
||||
}
|
||||
|
||||
return `<div class="ip"><div class="ip-head" style="background:${hc[type]}">${ic[type]} ${ti[type]}</div><div class="ip-body">${rows}<button class="ip-del" onclick="hapusData(${d.id},'${type}')">🗑 Hapus</button></div></div>`;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
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(`<div class="pf"><div class="pf-head pf-head-point">⛽ Tambah SPBU</div><div class="pf-body">
|
||||
<label>Nama SPBU</label><input id="pNama" placeholder="contoh: SPBU Sungai Jawi">
|
||||
<label>No. SPBU</label><input id="pNo" placeholder="contoh: 64.761.xx">
|
||||
<label>Status Operasional</label>
|
||||
<select id="pStatus"><option value="24 Jam">24 Jam</option><option value="Tidak">Tidak 24 Jam</option></select>
|
||||
<div class="pf-info">📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}</div>
|
||||
<button class="pf-btn" onclick="savePoint(${ll.lat},${ll.lng})">💾 Simpan</button>
|
||||
</div></div>`, { 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(`<div class="pf"><div class="pf-head pf-head-line">🛣️ Tambah Jalan</div><div class="pf-body">
|
||||
<label>Nama Jalan</label><input id="lNama" placeholder="contoh: Jl. Ahmad Yani">
|
||||
<label>Klasifikasi</label><select id="lStatus"><option value="Nasional">Nasional</option><option value="Provinsi">Provinsi</option><option value="Kabupaten">Kabupaten</option></select>
|
||||
<div class="pf-info">📏 ${fmtNum(p)} m</div>
|
||||
<button class="pf-btn" onclick="saveLine()">💾 Simpan</button>
|
||||
</div></div>`).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(`<div class="pf"><div class="pf-head pf-head-polygon">📐 Tambah Parsil</div><div class="pf-body">
|
||||
<label>Nama Area</label><input id="gNama" placeholder="contoh: Lahan A">
|
||||
<label>Status Kepemilikan</label><select id="gStatus"><option value="SHM">SHM</option><option value="HGB">HGB</option><option value="HGU">HGU</option><option value="HP">HP</option></select>
|
||||
<div class="pf-info">📐 ${fmtNum(luas)} m²</div>
|
||||
<button class="pf-btn" onclick="savePolygon()">💾 Simpan</button>
|
||||
</div></div>`).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}`);
|
||||
});
|
||||
});
|
||||
@@ -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: `<svg xmlns="http://www.w3.org/2000/svg" width="26" height="38" viewBox="0 0 26 38"><path d="M13 0C5.8 0 0 5.8 0 13c0 9.75 13 25 13 25S26 22.75 26 13C26 5.8 20.2 0 13 0z" fill="${c}" stroke="rgba(255,255,255,.25)" stroke-width="1.5"/><circle cx="13" cy="13" r="5" fill="rgba(0,0,0,.4)"/><circle cx="13" cy="13" r="3" fill="white"/></svg>`,
|
||||
iconSize: [26, 38],
|
||||
iconAnchor: [13, 38],
|
||||
popupAnchor: [0, -40],
|
||||
className: ''
|
||||
});
|
||||
}
|
||||
|
||||
function statusBadge(status) {
|
||||
return status === '24 Jam'
|
||||
? `<span class="ip-badge badge-green">24 Jam</span>`
|
||||
: `<span class="ip-badge badge-red">Tidak 24 Jam</span>`;
|
||||
}
|
||||
|
||||
function buildInfoPopup(d) {
|
||||
return `<div class="ip">
|
||||
<div class="ip-head" style="background:linear-gradient(135deg,#1d4ed8,#3b82f6)">⛽ SPBU - ${d.nama}</div>
|
||||
<div class="ip-body">
|
||||
<div class="ip-row"><span class="ip-label">No. SPBU</span><span class="ip-val">${d.no_spbu}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Status</span><span class="ip-val">${statusBadge(d.status)}</span></div>
|
||||
<div class="ip-row"><span class="ip-label">Koordinat</span><span class="ip-val" style="font-size:10px">${parseFloat(d.latitude).toFixed(5)}, ${parseFloat(d.longitude).toFixed(5)}</span></div>
|
||||
<button class="ip-del" onclick="hapusData(${d.id},'point')">Hapus</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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(`<div class="pf">
|
||||
<div class="pf-head pf-head-point">⛽ Tambah SPBU</div>
|
||||
<div class="pf-body">
|
||||
<label>Nama SPBU</label><input id="pNama" placeholder="contoh: SPBU Sungai Jawi">
|
||||
<label>No. SPBU</label><input id="pNo" placeholder="contoh: 64.761.xx">
|
||||
<label>Status Operasional</label>
|
||||
<select id="pStatus"><option value="24 Jam">24 Jam</option><option value="Tidak">Tidak 24 Jam</option></select>
|
||||
<div class="pf-info">📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}</div>
|
||||
<button class="pf-btn" onclick="savePoint(${ll.lat},${ll.lng})">Simpan</button>
|
||||
</div>
|
||||
</div>`, { 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));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
$conn = new mysqli("localhost", "root", "bobbyandreanjapri", "webgis_spbu");
|
||||
if ($conn->connect_error) {
|
||||
die(json_encode(["error" => "Koneksi gagal: " . $conn->connect_error]));
|
||||
}
|
||||
$conn->set_charset("utf8");
|
||||
?>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include 'db.php';
|
||||
|
||||
$id = intval($_GET['id'] ?? 0);
|
||||
$type = $_GET['type'] ?? '';
|
||||
|
||||
if ($id == 0 || $type == '') {
|
||||
echo json_encode(["message" => "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]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include 'db.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
if (!$data) {
|
||||
echo json_encode(["message" => "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]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include 'db.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
if (!$data) {
|
||||
echo json_encode(["message" => "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();
|
||||
?>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include 'db.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$data) { echo json_encode(["message"=>"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();
|
||||
?>
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include 'db.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$data) { echo json_encode(["message"=>"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();
|
||||
?>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include 'db.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
if (!$data) {
|
||||
echo json_encode(["message" => "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();
|
||||
?>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include 'db.php';
|
||||
|
||||
$data = [];
|
||||
|
||||
/* POINT — SPBU */
|
||||
$res = $conn->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);
|
||||
?>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include 'db.php';
|
||||
|
||||
$data = [];
|
||||
|
||||
/* POINT */
|
||||
$res = $conn->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);
|
||||
?>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include 'db.php';
|
||||
|
||||
$data = [];
|
||||
|
||||
/* 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);
|
||||
?>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include 'db.php';
|
||||
|
||||
$data = [];
|
||||
|
||||
$res = $conn->query("SELECT * FROM spbu");
|
||||
if ($res) {
|
||||
while ($r = $res->fetch_assoc()) {
|
||||
$r['type'] = 'point';
|
||||
$data[] = $r;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode($data);
|
||||
?>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include 'db.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
if (!$data) {
|
||||
echo json_encode(["message" => "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]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include 'db.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$data) {
|
||||
echo json_encode(["message" => "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();
|
||||
?>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include 'db.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
if (!$data) { echo json_encode(["message"=>"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();
|
||||
?>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
include 'db.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
|
||||
if (!$data) {
|
||||
echo json_encode(["message" => "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]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['is_admin']) || $_SESSION['is_admin'] !== true) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Panel Admin — WebGIS Sosial</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Leaflet -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw/dist/leaflet.draw.css"/>
|
||||
|
||||
<!-- App styles -->
|
||||
<link rel="stylesheet" href="../css/style.css"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<a href="index.php" class="back-btn" title="Lihat Dashboard Publik">👁</a>
|
||||
<div class="logo">
|
||||
<div class="logo-icon" style="background:#ef4444;">🛡</div>
|
||||
<div class="logo-text">PANEL<span style="color:#ef4444;">ADMIN</span></div>
|
||||
</div>
|
||||
<div class="header-divider"></div>
|
||||
<span style="font-size:12px;color:var(--muted);">Kelola Data & Parameter</span>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot" style="background:#f59e0b"></div> Masjid
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot" style="background:#e879f9"></div> Pend. Miskin
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot" style="background:#f59e0b; opacity:0.5;"></div> Dlm. Radius
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layer-sep"></div>
|
||||
|
||||
<!-- Toggle Masjid & Penduduk -->
|
||||
<div class="layer-control">
|
||||
<button class="layer-btn btn-masjid active" id="btnMasjid" onclick="toggleLayer('masjid')">
|
||||
<span class="dot" style="background:#f59e0b"></span>
|
||||
Masjid
|
||||
</button>
|
||||
<button class="layer-btn btn-penduduk active" id="btnPenduduk" onclick="toggleLayer('penduduk')">
|
||||
<span class="dot" style="background:#e879f9"></span>
|
||||
Pend. Miskin
|
||||
</button>
|
||||
<button class="layer-btn" id="btnHeatmap" onclick="toggleLayer('heatmap')">
|
||||
🔥 Heatmap
|
||||
</button>
|
||||
<div class="layer-sep"></div>
|
||||
<a href="logout.php" class="layer-btn" style="text-decoration:none; color:#ef4444; border-color:#ef444433;">Keluar</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Stats Panel -->
|
||||
<div class="stats-panel-container glass" style="position:absolute; top:80px; right:10px; z-index:1000; width:220px; border-radius:8px; padding:12px; background:#0a0d14ea; border:1px solid #1e2233; color:#fff;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; border-bottom:1px solid #1e2233; padding-bottom:5px;">
|
||||
<span style="font-weight:700; font-size:12px;">📊 STATISTIK SOSIAL</span>
|
||||
<button id="statsToggleBtn" onclick="toggleStats()" style="background:none;border:none;color:#9ca3af;cursor:pointer">▲</button>
|
||||
</div>
|
||||
<div id="statsContent">
|
||||
<!-- Rendered by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blind Spot Alert -->
|
||||
<div id="blindSpotPanel" class="glass" style="display:none; position:absolute; bottom:30px; right:10px; z-index:1000; background:#7f1d1d; border:1px solid #ef4444; border-radius:8px; padding:12px; color:#fff; cursor:pointer; align-items:center; gap:10px; box-shadow:0 10px 15px -3px rgba(239,68,68,0.3)" onclick="zoomToBlindSpots()">
|
||||
<span style="font-size:24px">⚠️</span>
|
||||
<div>
|
||||
<div style="font-weight:700;font-size:14px"><span id="blindSpotCount">0</span> Keluarga di Blind Spot!</div>
|
||||
<div style="font-size:11px;color:#fca5a5">Sangat Rentan & Tidak terjangkau masjid (Klik untuk Zoom)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet.heat/dist/leaflet-heat.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-draw/dist/leaflet.draw.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-geometryutil/src/leaflet.geometryutil.js"></script>
|
||||
|
||||
<!-- Mode Selector Panel (Bottom Left) -->
|
||||
<div class="mode-panel-container" style="position: absolute; bottom: 30px; left: 10px; z-index: 1000;">
|
||||
<div class="mode-panel-box glass">
|
||||
<div class="mode-panel-title">Pilih Mode Pointing</div>
|
||||
<div class="mode-options">
|
||||
<label class="mode-panel-label">
|
||||
<input type="radio" name="mode" value="masjid" checked onclick="setMode('masjid')">
|
||||
<div class="mode-item">
|
||||
<span class="mode-panel-icon">🕌</span> <span>Tambah Masjid</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="mode-panel-label">
|
||||
<input type="radio" name="mode" value="penduduk" onclick="setMode('penduduk')">
|
||||
<div class="mode-item">
|
||||
<span class="mode-panel-icon">👨👩👧👦</span> <span>Tambah Pend. Miskin</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App script -->
|
||||
<script src="../js/draw_sosial.js?v=<?= time() ?>"></script>
|
||||
<script>
|
||||
window.setMode = function(mode) {
|
||||
console.log('Mode changed to: ' + mode);
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,184 @@
|
||||
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36" version="29.3.7">
|
||||
<diagram id="erd" name="ERD">
|
||||
<mxGraphModel dx="918" dy="498" grid="0" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="1100" pageHeight="700" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="title" parent="1" style="text;html=1;fontSize=15;fontStyle=1;align=center;" value="Entity Relationship Diagram — WebGIS Sosial" vertex="1">
|
||||
<mxGeometry height="30" width="600" x="205" y="27" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="tbl-masjid" parent="1" style="shape=table;startSize=30;container=1;collapsible=0;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;strokeColor=#82b366;fillColor=#d5e8d4;fontSize=13;" value="MASJID" vertex="1">
|
||||
<mxGeometry height="222" width="280" x="80" y="100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="m1" parent="tbl-masjid" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="280" y="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="m1k" parent="m1" style="shape=partialRectangle;connectable=0;fillColor=#d5e8d4;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;" value="PK" vertex="1">
|
||||
<mxGeometry height="32" width="40" as="geometry">
|
||||
<mxRectangle height="32" width="40" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="m1v" parent="m1" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="id (INT, AUTO_INCREMENT)" vertex="1">
|
||||
<mxGeometry height="32" width="240" x="40" as="geometry">
|
||||
<mxRectangle height="32" width="240" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="m2" parent="tbl-masjid" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="280" y="62" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="m2k" parent="m2" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="40" as="geometry">
|
||||
<mxRectangle height="32" width="40" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="m2v" parent="m2" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="nama (VARCHAR 100)" vertex="1">
|
||||
<mxGeometry height="32" width="240" x="40" as="geometry">
|
||||
<mxRectangle height="32" width="240" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="m3" parent="tbl-masjid" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="280" y="94" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="m3k" parent="m3" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="40" as="geometry">
|
||||
<mxRectangle height="32" width="40" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="m3v" parent="m3" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="pic (VARCHAR 100)" vertex="1">
|
||||
<mxGeometry height="32" width="240" x="40" as="geometry">
|
||||
<mxRectangle height="32" width="240" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="m4" parent="tbl-masjid" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="280" y="126" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="m4k" parent="m4" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="40" as="geometry">
|
||||
<mxRectangle height="32" width="40" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="m4v" parent="m4" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="radius (FLOAT)" vertex="1">
|
||||
<mxGeometry height="32" width="240" x="40" as="geometry">
|
||||
<mxRectangle height="32" width="240" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="m5" parent="tbl-masjid" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="280" y="158" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="m5k" parent="m5" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="40" as="geometry">
|
||||
<mxRectangle height="32" width="40" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="m5v" parent="m5" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="latitude (DOUBLE)" vertex="1">
|
||||
<mxGeometry height="32" width="240" x="40" as="geometry">
|
||||
<mxRectangle height="32" width="240" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="m6" parent="tbl-masjid" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="280" y="190" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="m6k" parent="m6" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="40" as="geometry">
|
||||
<mxRectangle height="32" width="40" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="m6v" parent="m6" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="longitude (DOUBLE)" vertex="1">
|
||||
<mxGeometry height="32" width="240" x="40" as="geometry">
|
||||
<mxRectangle height="32" width="240" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="rel-box" parent="1" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=10;align=center;" value="Melayani" vertex="1">
|
||||
<mxGeometry height="68" width="74" x="466" y="177" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="tbl-pm" parent="1" style="shape=table;startSize=30;container=1;collapsible=0;childLayout=tableLayout;fixedRows=1;rowLines=0;fontStyle=1;align=center;resizeLast=1;strokeColor=#d79b00;fillColor=#ffe6cc;fontSize=13;" value="PENDUDUK_MISKIN" vertex="1">
|
||||
<mxGeometry height="222" width="300" x="650" y="100" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="p1" parent="tbl-pm" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="300" y="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="p1k" parent="p1" style="shape=partialRectangle;connectable=0;fillColor=#ffe6cc;top=0;left=0;bottom=0;right=0;fontStyle=1;overflow=hidden;" value="PK" vertex="1">
|
||||
<mxGeometry height="32" width="40" as="geometry">
|
||||
<mxRectangle height="32" width="40" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="p1v" parent="p1" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="id (INT, AUTO_INCREMENT)" vertex="1">
|
||||
<mxGeometry height="32" width="260" x="40" as="geometry">
|
||||
<mxRectangle height="32" width="260" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="p2" parent="tbl-pm" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="300" y="62" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="p2k" parent="p2" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="40" as="geometry">
|
||||
<mxRectangle height="32" width="40" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="p2v" parent="p2" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="nama_kk (VARCHAR 150)" vertex="1">
|
||||
<mxGeometry height="32" width="260" x="40" as="geometry">
|
||||
<mxRectangle height="32" width="260" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="p3" parent="tbl-pm" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="300" y="94" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="p3k" parent="p3" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="40" as="geometry">
|
||||
<mxRectangle height="32" width="40" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="p3v" parent="p3" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="jumlah_anggota (INT)" vertex="1">
|
||||
<mxGeometry height="32" width="260" x="40" as="geometry">
|
||||
<mxRectangle height="32" width="260" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="p4" parent="tbl-pm" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="300" y="126" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="p4k" parent="p4" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="40" as="geometry">
|
||||
<mxRectangle height="32" width="40" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="p4v" parent="p4" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="alamat (TEXT)" vertex="1">
|
||||
<mxGeometry height="32" width="260" x="40" as="geometry">
|
||||
<mxRectangle height="32" width="260" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="p5" parent="tbl-pm" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="300" y="158" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="p5k" parent="p5" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="40" as="geometry">
|
||||
<mxRectangle height="32" width="40" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="p5v" parent="p5" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="latitude (DOUBLE)" vertex="1">
|
||||
<mxGeometry height="32" width="260" x="40" as="geometry">
|
||||
<mxRectangle height="32" width="260" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="p6" parent="tbl-pm" style="shape=tableRow;horizontal=0;startSize=0;swimlaneHead=0;swimlaneBody=0;fillColor=none;collapsible=0;dropTarget=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;top=0;left=0;right=0;bottom=1;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="300" y="190" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="p6k" parent="p6" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="" vertex="1">
|
||||
<mxGeometry height="32" width="40" as="geometry">
|
||||
<mxRectangle height="32" width="40" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="p6v" parent="p6" style="shape=partialRectangle;connectable=0;fillColor=none;top=0;left=0;bottom=0;right=0;overflow=hidden;" value="longitude (DOUBLE)" vertex="1">
|
||||
<mxGeometry height="32" width="260" x="40" as="geometry">
|
||||
<mxRectangle height="32" width="260" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="rel1" edge="1" parent="1" source="tbl-masjid" style="endArrow=ERone;startArrow=ERone;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeColor=#d6b656;fontStyle=1;" target="rel-box" value="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="rel2" edge="1" parent="1" source="rel-box" style="endArrow=ERmanyToOne;startArrow=ERmanyToOne;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeColor=#d6b656;fontStyle=1;" target="tbl-pm" value="N">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS Sosial — Penduduk Miskin & Masjid</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Leaflet -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
|
||||
|
||||
<!-- App styles -->
|
||||
<link rel="stylesheet" href="../css/style.css"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<a href="../" class="back-btn" title="Kembali ke Menu">←</a>
|
||||
<div class="logo">
|
||||
<div class="logo-icon" style="background:#f59e0b;">🕌</div>
|
||||
<div class="logo-text">WEB<span style="color:#f59e0b;">GIS</span> SOSIAL</div>
|
||||
</div>
|
||||
<div class="header-divider"></div>
|
||||
<span style="font-size:12px;color:var(--muted);">Pendataan Penduduk Miskin & Masjid</span>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot" style="background:#f59e0b"></div> Masjid
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot" style="background:#e879f9"></div> Pend. Miskin
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot" style="background:#f59e0b; opacity:0.5;"></div> Dlm. Radius
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layer-sep"></div>
|
||||
|
||||
<!-- Toggle Masjid & Penduduk -->
|
||||
<div class="layer-control">
|
||||
<button class="layer-btn btn-masjid active" id="btnMasjid" onclick="toggleLayer('masjid')">
|
||||
<span class="dot" style="background:#f59e0b"></span>
|
||||
Masjid
|
||||
</button>
|
||||
<button class="layer-btn btn-penduduk active" id="btnPenduduk" onclick="toggleLayer('penduduk')">
|
||||
<span class="dot" style="background:#e879f9"></span>
|
||||
Pend. Miskin
|
||||
</button>
|
||||
<button class="layer-btn" id="btnHeatmap" onclick="toggleLayer('heatmap')">
|
||||
🔥 Heatmap
|
||||
</button>
|
||||
<div class="layer-sep"></div>
|
||||
<a href="login.php" class="layer-btn" style="text-decoration:none; color:#f59e0b; border-color:#f59e0b33;">Admin Login</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet.heat/dist/leaflet-heat.js"></script>
|
||||
|
||||
<!-- App script (Public - Read Only) -->
|
||||
<script src="../js/draw_sosial_public.js?v=<?= time() ?>"></script>
|
||||
|
||||
<!-- Stats Panel -->
|
||||
<div class="stats-panel-container glass" style="position:absolute; top:80px; right:10px; z-index:1000; width:220px; border-radius:8px; padding:12px; background:#0a0d14ea; border:1px solid #1e2233; color:#fff;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; border-bottom:1px solid #1e2233; padding-bottom:5px;">
|
||||
<span style="font-weight:700; font-size:12px;">📊 STATISTIK SOSIAL</span>
|
||||
<button id="statsToggleBtn" onclick="toggleStats()" style="background:none;border:none;color:#9ca3af;cursor:pointer">▲</button>
|
||||
</div>
|
||||
<div id="statsContent">
|
||||
<!-- Rendered by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blind Spot Alert -->
|
||||
<div id="blindSpotPanel" class="glass" style="display:none; position:absolute; bottom:30px; right:10px; z-index:1000; background:#7f1d1d; border:1px solid #ef4444; border-radius:8px; padding:12px; color:#fff; cursor:pointer; align-items:center; gap:10px; box-shadow:0 10px 15px -3px rgba(239,68,68,0.3)" onclick="zoomToBlindSpots()">
|
||||
<span style="font-size:24px">⚠️</span>
|
||||
<div>
|
||||
<div style="font-weight:700;font-size:14px"><span id="blindSpotCount">0</span> Keluarga di Blind Spot!</div>
|
||||
<div style="font-size:11px;color:#fca5a5">Sangat Rentan & Tidak terjangkau masjid (Klik untuk Zoom)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- App script -->
|
||||
<script src="../js/draw_sosial.js?v=<?= time() ?>"></script>
|
||||
<script>
|
||||
window.setMode = function(mode) {
|
||||
console.log('Mode changed to: ' + mode);
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// Kredensial Admin — bisa diganti sesuai kebutuhan
|
||||
$ADMIN_USER = 'admin';
|
||||
$ADMIN_PASS = 'admin123';
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$user = $_POST['username'] ?? '';
|
||||
$pass = $_POST['password'] ?? '';
|
||||
if ($user === $ADMIN_USER && $pass === $ADMIN_PASS) {
|
||||
$_SESSION['is_admin'] = true;
|
||||
header('Location: admin.php');
|
||||
exit;
|
||||
} else {
|
||||
$error = 'Username atau password salah.';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login Admin — WebGIS Sosial</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
background: #0a0d14;
|
||||
color: #e2e8f0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Background grid effect */
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(245,158,11,0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(245,158,11,0.03) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
/* Glow blob */
|
||||
body::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -20%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: radial-gradient(circle, rgba(245,158,11,0.08) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.login-card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
background: #1a1d27;
|
||||
border: 1px solid #2a2d3a;
|
||||
border-radius: 16px;
|
||||
padding: 36px 32px;
|
||||
box-shadow: 0 25px 60px rgba(0,0,0,0.5);
|
||||
}
|
||||
.login-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.login-logo-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: linear-gradient(135deg, #92400e, #f59e0b);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
}
|
||||
.login-logo-text {
|
||||
font-family: 'Space Mono', monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.login-logo-text span { color: #f59e0b; }
|
||||
.login-logo-sub {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
margin-top: 1px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.login-desc {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 24px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
input[type=text], input[type=password] {
|
||||
width: 100%;
|
||||
background: #0f1117;
|
||||
border: 1px solid #2a2d3a;
|
||||
color: #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
input[type=text]:focus, input[type=password]:focus {
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
.error-msg {
|
||||
background: rgba(239,68,68,0.1);
|
||||
border: 1px solid rgba(239,68,68,0.3);
|
||||
color: #f87171;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.btn-login {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
padding: 12px;
|
||||
background: linear-gradient(135deg, #92400e, #f59e0b);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s, transform 0.1s;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.btn-login:hover { opacity: 0.9; transform: translateY(-1px); }
|
||||
.btn-login:active { transform: translateY(0); }
|
||||
.back-link {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.back-link:hover { color: #e2e8f0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<div class="login-logo">
|
||||
<div class="login-logo-icon">🕌</div>
|
||||
<div>
|
||||
<div class="login-logo-text">WEB<span>GIS</span> SOSIAL</div>
|
||||
<div class="login-logo-sub">Portal Administrasi</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>🔐 Masuk sebagai Admin</h1>
|
||||
<p class="login-desc">Halaman ini khusus untuk petugas yang berwenang mengelola data penduduk dan masjid.</p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="error-msg">⚠️ <?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" placeholder="admin" autocomplete="off" required>
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" placeholder="••••••••" required>
|
||||
|
||||
<button type="submit" class="btn-login">Masuk ke Panel Admin →</button>
|
||||
</form>
|
||||
|
||||
<a href="index.php" class="back-link">← Kembali ke Dashboard Publik</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_destroy();
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
?>
|
||||
@@ -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.
|
||||
@@ -0,0 +1,85 @@
|
||||
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36" version="29.3.7">
|
||||
<diagram id="ucd" name="Use Case Diagram">
|
||||
<mxGraphModel dx="872" dy="473" grid="0" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="1100" pageHeight="850" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="title" parent="1" style="text;html=1;fontSize=15;fontStyle=1;align=center;" value="Use Case Diagram — WebGIS Sosial (Pengentasan Kemiskinan)" vertex="1">
|
||||
<mxGeometry height="30" width="620" x="130" y="10" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="sys" parent="1" style="points=[[0,0],[0.25,0],[0.5,0],[0.75,0],[1,0],[1,0.25],[1,0.5],[1,0.75],[1,1],[0.75,1],[0.5,1],[0.25,1],[0,1],[0,0.75],[0,0.5],[0,0.25]];shape=mxgraph.uml.systemBoundary;whiteSpace=wrap;html=1;align=center;verticalAlign=top;spacingTop=8;fontSize=13;fontStyle=1;strokeColor=#1a73e8;fillColor=#EBF3FB;" value="Sistem WebGIS Sosial" vertex="1">
|
||||
<mxGeometry height="568" width="445" x="180" y="55" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="uc1" parent="1" style="ellipse;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=11;" value="Melihat Peta
Persebaran" vertex="1">
|
||||
<mxGeometry height="55" width="160" x="217.5" y="99" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="uc2" parent="1" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=11;" value="Tambah Titik Masjid" vertex="1">
|
||||
<mxGeometry height="55" width="155" x="220" y="175" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="uc3" parent="1" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=11;" value="Ubah Radius
Pelayanan Masjid" vertex="1">
|
||||
<mxGeometry height="55" width="155" x="220" y="338" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="uc4" parent="1" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=11;" value="Hapus Data Masjid" vertex="1">
|
||||
<mxGeometry height="55" width="155" x="220" y="420" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="uc5" parent="1" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=11;" value="Tambah Titik
Penduduk Miskin" vertex="1">
|
||||
<mxGeometry height="55" width="155" x="220" y="255" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="uc6" parent="1" style="ellipse;whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;fontSize=11;" value="Hapus Data
Penduduk Miskin" vertex="1">
|
||||
<mxGeometry height="55" width="155" x="222.5" y="508" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="uc7" parent="1" style="ellipse;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;fontSize=11;fontStyle=2;" value="Analisis Spasial
Radius (Otomatis)" vertex="1">
|
||||
<mxGeometry height="60" width="160" x="450" y="243" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="leg" parent="1" style="text;html=0;fontSize=10;fillColor=#f5f5f5;strokeColor=#666666;align=left;spacingLeft=8;" value="Keterangan: 🟡 Umum 🟢 Fitur Masjid 🟠 Fitur Penduduk Miskin 🔴 Proses Otomatis Sistem" vertex="1">
|
||||
<mxGeometry height="28" width="580" x="180" y="710" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="e1" edge="1" parent="1" source="ajy3wIZuD7tetmBaQCAl-1" style="endArrow=none;" target="uc1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="102" y="306.875" as="sourcePoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e2" edge="1" parent="1" source="ajy3wIZuD7tetmBaQCAl-1" style="endArrow=none;" target="uc2">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="102" y="311.73605640810365" as="sourcePoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e3" edge="1" parent="1" source="ajy3wIZuD7tetmBaQCAl-1" style="endArrow=none;" target="uc3">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="102" y="324.1986429723855" as="sourcePoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e4" edge="1" parent="1" source="ajy3wIZuD7tetmBaQCAl-1" style="endArrow=none;" target="uc4">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="102" y="336.6649670004532" as="sourcePoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e5" edge="1" parent="1" source="ajy3wIZuD7tetmBaQCAl-1" style="endArrow=none;" target="uc5">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="102" y="317.1094832911109" as="sourcePoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="e6" edge="1" parent="1" source="ajy3wIZuD7tetmBaQCAl-1" style="endArrow=none;" target="uc6">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="102" y="323" as="sourcePoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="i1" edge="1" parent="1" source="uc1" style="dashed=1;endArrow=open;endFill=0;fontSize=10;" target="uc7" value="<<include>>">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="i2" edge="1" parent="1" source="uc2" style="dashed=1;endArrow=open;endFill=0;fontSize=10;" target="uc7" value="<<include>>">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="i3" edge="1" parent="1" source="uc3" style="dashed=1;endArrow=open;endFill=0;fontSize=10;" target="uc7" value="<<include>>">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="i4" edge="1" parent="1" source="uc5" style="dashed=1;endArrow=open;endFill=0;fontSize=10;" target="uc7" value="<<include>>">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="ajy3wIZuD7tetmBaQCAl-1" parent="1" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" value="Admin" vertex="1">
|
||||
<mxGeometry height="60" width="30" x="59" y="288" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS SPBU — Pontianak</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Leaflet -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw/dist/leaflet.draw.css"/>
|
||||
|
||||
<!-- App styles -->
|
||||
<link rel="stylesheet" href="../css/style.css"/>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<a href="../" class="back-btn" title="Kembali ke Menu">←</a>
|
||||
<div class="logo">
|
||||
<div class="logo-icon">⛽</div>
|
||||
<div class="logo-text">WEB<span>GIS</span> SPBU</div>
|
||||
</div>
|
||||
<div class="header-divider"></div>
|
||||
<span style="font-size:12px;color:var(--muted);">Pontianak, Kalimantan Barat</span>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot" style="background:#4ade80"></div> SPBU 24 Jam
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot" style="background:#f87171"></div> Tidak 24 Jam
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-dot" style="background:#3b82f6"></div> Titik SPBU
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layer-sep"></div>
|
||||
|
||||
<!-- Toggle SPBU -->
|
||||
<div class="layer-control">
|
||||
<button class="layer-btn btn-24 active" id="btn24" onclick="toggleLayer('24jam')">
|
||||
<span class="dot" style="background:#4ade80"></span>
|
||||
24 Jam
|
||||
</button>
|
||||
<button class="layer-btn btn-tidak active" id="btnTidak" onclick="toggleLayer('tidak')">
|
||||
<span class="dot" style="background:#f87171"></span>
|
||||
Tidak 24 Jam
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet-draw/dist/leaflet.draw.js"></script>
|
||||
|
||||
<!-- App script -->
|
||||
<script src="../js/draw_spbu_point.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user