commit 448083d2eaf2eb0cb1b105722f3f68638c819ae3 Author: Syariffullah Date: Wed May 6 10:05:49 2026 +0700 init: publish to github diff --git a/api/db_connect.php b/api/db_connect.php new file mode 100644 index 0000000..a3922ed --- /dev/null +++ b/api/db_connect.php @@ -0,0 +1,17 @@ +connect_error) { + die("Koneksi gagal: " . $conn->connect_error); +} +// Set charset +$conn->set_charset("utf8mb4"); + +// Jika di-include oleh file API, biarkan $conn tersedia +?> diff --git a/api/jalan/create.php b/api/jalan/create.php new file mode 100644 index 0000000..a2bcd5b --- /dev/null +++ b/api/jalan/create.php @@ -0,0 +1,23 @@ +geom)) { + $nama = $conn->real_escape_string($data->nama ?? 'Jalan Baru'); + $status = $conn->real_escape_string($data->status ?? 'Kabupaten'); + $panjang = (float)($data->panjang ?? 0); + $geom = $conn->real_escape_string(json_encode($data->geom)); // GeoJSON string + + $query = "INSERT INTO jalan (nama, status, panjang, geom) VALUES ('$nama', '$status', $panjang, ST_GeomFromGeoJSON('$geom'))"; + + if($conn->query($query)) { + echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Jalan berhasil ditambahkan."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal menambahkan jalan: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "Data geometri tidak lengkap."]); +} +?> diff --git a/api/jalan/delete.php b/api/jalan/delete.php new file mode 100644 index 0000000..5ad8416 --- /dev/null +++ b/api/jalan/delete.php @@ -0,0 +1,20 @@ +id)) { + $id = (int)$data->id; + + $query = "DELETE FROM jalan WHERE id=$id"; + + if($conn->query($query)) { + echo json_encode(["status" => "success", "message" => "Jalan berhasil dihapus."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal hapus jalan: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]); +} +?> diff --git a/api/jalan/read.php b/api/jalan/read.php new file mode 100644 index 0000000..ade48aa --- /dev/null +++ b/api/jalan/read.php @@ -0,0 +1,25 @@ +query($query); + +$jalan_arr = array(); + +if($result->num_rows > 0) { + while($row = $result->fetch_assoc()) { + $jalan_item = array( + "id" => $row['id'], + "nama" => $row['nama'], + "status" => $row['status'], + "panjang" => (float)$row['panjang'], + "geom" => json_decode($row['geom']) + ); + array_push($jalan_arr, $jalan_item); + } + echo json_encode(["status" => "success", "data" => $jalan_arr]); +} else { + echo json_encode(["status" => "success", "data" => []]); +} +?> diff --git a/api/jalan/update.php b/api/jalan/update.php new file mode 100644 index 0000000..08702d7 --- /dev/null +++ b/api/jalan/update.php @@ -0,0 +1,29 @@ +id)) { + $id = (int)$data->id; + $nama = $conn->real_escape_string($data->nama ?? ''); + $status = $conn->real_escape_string($data->status ?? 'Kabupaten'); + + // Jika ada update geometri + if(!empty($data->geom)) { + $panjang = (float)($data->panjang ?? 0); + $geom = $conn->real_escape_string(json_encode($data->geom)); + $query = "UPDATE jalan SET nama='$nama', status='$status', panjang=$panjang, geom=ST_GeomFromGeoJSON('$geom') WHERE id=$id"; + } else { + $query = "UPDATE jalan SET nama='$nama', status='$status' WHERE id=$id"; + } + + if($conn->query($query)) { + echo json_encode(["status" => "success", "message" => "Jalan berhasil diupdate."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal update jalan: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]); +} +?> diff --git a/api/parsil/create.php b/api/parsil/create.php new file mode 100644 index 0000000..2f0d584 --- /dev/null +++ b/api/parsil/create.php @@ -0,0 +1,23 @@ +geom)) { + $nama = $conn->real_escape_string($data->nama ?? ''); + $status = $conn->real_escape_string($data->status ?? 'SHM'); + $luas = (float)($data->luas ?? 0); + $geom = $conn->real_escape_string(json_encode($data->geom)); // GeoJSON string + + $query = "INSERT INTO parsil (nama, status, luas, geom) VALUES ('$nama', '$status', $luas, ST_GeomFromGeoJSON('$geom'))"; + + if($conn->query($query)) { + echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Parsil berhasil ditambahkan."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal menambahkan parsil: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "Data geometri tidak lengkap."]); +} +?> diff --git a/api/parsil/delete.php b/api/parsil/delete.php new file mode 100644 index 0000000..67f8229 --- /dev/null +++ b/api/parsil/delete.php @@ -0,0 +1,20 @@ +id)) { + $id = (int)$data->id; + + $query = "DELETE FROM parsil WHERE id=$id"; + + if($conn->query($query)) { + echo json_encode(["status" => "success", "message" => "Parsil berhasil dihapus."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal hapus parsil: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]); +} +?> diff --git a/api/parsil/read.php b/api/parsil/read.php new file mode 100644 index 0000000..f24bacf --- /dev/null +++ b/api/parsil/read.php @@ -0,0 +1,25 @@ +query($query); + +$parsil_arr = array(); + +if($result->num_rows > 0) { + while($row = $result->fetch_assoc()) { + $parsil_item = array( + "id" => $row['id'], + "nama" => $row['nama'], + "status" => $row['status'], + "luas" => (float)$row['luas'], + "geom" => json_decode($row['geom']) + ); + array_push($parsil_arr, $parsil_item); + } + echo json_encode(["status" => "success", "data" => $parsil_arr]); +} else { + echo json_encode(["status" => "success", "data" => []]); +} +?> diff --git a/api/parsil/update.php b/api/parsil/update.php new file mode 100644 index 0000000..edb5621 --- /dev/null +++ b/api/parsil/update.php @@ -0,0 +1,29 @@ +id)) { + $id = (int)$data->id; + $nama = $conn->real_escape_string($data->nama ?? ''); + $status = $conn->real_escape_string($data->status ?? 'SHM'); + + // Jika ada update geometri + if(!empty($data->geom)) { + $luas = (float)($data->luas ?? 0); + $geom = $conn->real_escape_string(json_encode($data->geom)); + $query = "UPDATE parsil SET nama='$nama', status='$status', luas=$luas, geom=ST_GeomFromGeoJSON('$geom') WHERE id=$id"; + } else { + $query = "UPDATE parsil SET nama='$nama', status='$status' WHERE id=$id"; + } + + if($conn->query($query)) { + echo json_encode(["status" => "success", "message" => "Parsil berhasil diupdate."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal update parsil: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]); +} +?> diff --git a/api/penduduk_miskin/create.php b/api/penduduk_miskin/create.php new file mode 100644 index 0000000..0e05e2f --- /dev/null +++ b/api/penduduk_miskin/create.php @@ -0,0 +1,23 @@ +lat) && !empty($data->lng)) { + $nama = $conn->real_escape_string($data->nama ?? ''); + $kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan'); + $lat = (float)$data->lat; + $lng = (float)$data->lng; + + $query = "INSERT INTO penduduk_miskin (nama, kategori_bantuan, lat, lng) VALUES ('$nama', '$kategori', $lat, $lng)"; + + if($conn->query($query)) { + echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Berhasil ditambahkan."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "Data tidak lengkap."]); +} +?> diff --git a/api/penduduk_miskin/delete.php b/api/penduduk_miskin/delete.php new file mode 100644 index 0000000..50f5f9d --- /dev/null +++ b/api/penduduk_miskin/delete.php @@ -0,0 +1,20 @@ +id)) { + $id = (int)$data->id; + + $query = "DELETE FROM penduduk_miskin WHERE id=$id"; + + if($conn->query($query)) { + echo json_encode(["status" => "success", "message" => "Berhasil dihapus."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal hapus: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]); +} +?> diff --git a/api/penduduk_miskin/read.php b/api/penduduk_miskin/read.php new file mode 100644 index 0000000..c07f393 --- /dev/null +++ b/api/penduduk_miskin/read.php @@ -0,0 +1,25 @@ +query($query); + +$arr = array(); + +if($result->num_rows > 0) { + while($row = $result->fetch_assoc()) { + $item = array( + "id" => $row['id'], + "nama" => $row['nama'], + "kategori_bantuan" => $row['kategori_bantuan'], + "lat" => (float)$row['lat'], + "lng" => (float)$row['lng'] + ); + array_push($arr, $item); + } + echo json_encode(["status" => "success", "data" => $arr]); +} else { + echo json_encode(["status" => "success", "data" => []]); +} +?> diff --git a/api/penduduk_miskin/update.php b/api/penduduk_miskin/update.php new file mode 100644 index 0000000..ffa4d9f --- /dev/null +++ b/api/penduduk_miskin/update.php @@ -0,0 +1,24 @@ +id)) { + $id = (int)$data->id; + $nama = $conn->real_escape_string($data->nama ?? ''); + $kategori = $conn->real_escape_string($data->kategori_bantuan ?? 'Makan'); + $lat = (float)$data->lat; + $lng = (float)$data->lng; + + $query = "UPDATE penduduk_miskin SET nama='$nama', kategori_bantuan='$kategori', lat=$lat, lng=$lng WHERE id=$id"; + + if($conn->query($query)) { + echo json_encode(["status" => "success", "message" => "Update berhasil."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal update: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]); +} +?> diff --git a/api/rumah_ibadah/create.php b/api/rumah_ibadah/create.php new file mode 100644 index 0000000..307c6d1 --- /dev/null +++ b/api/rumah_ibadah/create.php @@ -0,0 +1,24 @@ +lat) && !empty($data->lng)) { + $nama = $conn->real_escape_string($data->nama ?? ''); + $alamat = $conn->real_escape_string($data->alamat ?? ''); + $radius = (float)($data->radius ?? 100); + $lat = (float)$data->lat; + $lng = (float)$data->lng; + + $query = "INSERT INTO rumah_ibadah (nama, alamat, radius, lat, lng) VALUES ('$nama', '$alamat', $radius, $lat, $lng)"; + + if($conn->query($query)) { + echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Rumah Ibadah berhasil ditambahkan."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal menambahkan: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "Data tidak lengkap."]); +} +?> diff --git a/api/rumah_ibadah/delete.php b/api/rumah_ibadah/delete.php new file mode 100644 index 0000000..c7d4f5c --- /dev/null +++ b/api/rumah_ibadah/delete.php @@ -0,0 +1,20 @@ +id)) { + $id = (int)$data->id; + + $query = "DELETE FROM rumah_ibadah WHERE id=$id"; + + if($conn->query($query)) { + echo json_encode(["status" => "success", "message" => "Berhasil dihapus."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal hapus: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]); +} +?> diff --git a/api/rumah_ibadah/read.php b/api/rumah_ibadah/read.php new file mode 100644 index 0000000..aa5181c --- /dev/null +++ b/api/rumah_ibadah/read.php @@ -0,0 +1,26 @@ +query($query); + +$arr = array(); + +if($result->num_rows > 0) { + while($row = $result->fetch_assoc()) { + $item = array( + "id" => $row['id'], + "nama" => $row['nama'], + "alamat" => $row['alamat'], + "radius" => (float)$row['radius'], + "lat" => (float)$row['lat'], + "lng" => (float)$row['lng'] + ); + array_push($arr, $item); + } + echo json_encode(["status" => "success", "data" => $arr]); +} else { + echo json_encode(["status" => "success", "data" => []]); +} +?> diff --git a/api/rumah_ibadah/update.php b/api/rumah_ibadah/update.php new file mode 100644 index 0000000..1b19cde --- /dev/null +++ b/api/rumah_ibadah/update.php @@ -0,0 +1,25 @@ +id)) { + $id = (int)$data->id; + $nama = $conn->real_escape_string($data->nama ?? ''); + $alamat = $conn->real_escape_string($data->alamat ?? ''); + $radius = (float)($data->radius ?? 100); + $lat = (float)$data->lat; + $lng = (float)$data->lng; + + $query = "UPDATE rumah_ibadah SET nama='$nama', alamat='$alamat', radius=$radius, lat=$lat, lng=$lng WHERE id=$id"; + + if($conn->query($query)) { + echo json_encode(["status" => "success", "message" => "Update berhasil."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal update: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]); +} +?> diff --git a/api/spbu/create.php b/api/spbu/create.php new file mode 100644 index 0000000..c4f125e --- /dev/null +++ b/api/spbu/create.php @@ -0,0 +1,24 @@ +lat) && !empty($data->lng)) { + $nama = $conn->real_escape_string($data->nama ?? ''); + $no_wa = $conn->real_escape_string($data->no_wa ?? ''); + $is_24_jam = isset($data->is_24_jam) ? (int)$data->is_24_jam : 0; + $lat = (float)$data->lat; + $lng = (float)$data->lng; + + $query = "INSERT INTO spbu (nama, no_wa, is_24_jam, lat, lng) VALUES ('$nama', '$no_wa', $is_24_jam, $lat, $lng)"; + + if($conn->query($query)) { + echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "SPBU berhasil ditambahkan."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal menambahkan SPBU: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "Data tidak lengkap."]); +} +?> diff --git a/api/spbu/delete.php b/api/spbu/delete.php new file mode 100644 index 0000000..106a386 --- /dev/null +++ b/api/spbu/delete.php @@ -0,0 +1,20 @@ +id)) { + $id = (int)$data->id; + + $query = "DELETE FROM spbu WHERE id=$id"; + + if($conn->query($query)) { + echo json_encode(["status" => "success", "message" => "SPBU berhasil dihapus."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal hapus SPBU: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]); +} +?> diff --git a/api/spbu/read.php b/api/spbu/read.php new file mode 100644 index 0000000..942d823 --- /dev/null +++ b/api/spbu/read.php @@ -0,0 +1,26 @@ +query($query); + +$spbu_arr = array(); + +if($result->num_rows > 0) { + while($row = $result->fetch_assoc()) { + $spbu_item = array( + "id" => $row['id'], + "nama" => $row['nama'], + "no_wa" => $row['no_wa'], + "is_24_jam" => (bool)$row['is_24_jam'], + "lat" => (float)$row['lat'], + "lng" => (float)$row['lng'] + ); + array_push($spbu_arr, $spbu_item); + } + echo json_encode(["status" => "success", "data" => $spbu_arr]); +} else { + echo json_encode(["status" => "success", "data" => []]); +} +?> diff --git a/api/spbu/update.php b/api/spbu/update.php new file mode 100644 index 0000000..5fabf84 --- /dev/null +++ b/api/spbu/update.php @@ -0,0 +1,25 @@ +id) && !empty($data->lat) && !empty($data->lng)) { + $id = (int)$data->id; + $nama = $conn->real_escape_string($data->nama ?? ''); + $no_wa = $conn->real_escape_string($data->no_wa ?? ''); + $is_24_jam = isset($data->is_24_jam) ? (int)$data->is_24_jam : 0; + $lat = (float)$data->lat; + $lng = (float)$data->lng; + + $query = "UPDATE spbu SET nama='$nama', no_wa='$no_wa', is_24_jam=$is_24_jam, lat=$lat, lng=$lng WHERE id=$id"; + + if($conn->query($query)) { + echo json_encode(["status" => "success", "message" => "SPBU berhasil diupdate."]); + } else { + echo json_encode(["status" => "error", "message" => "Gagal update SPBU: " . $conn->error]); + } +} else { + echo json_encode(["status" => "error", "message" => "Data tidak lengkap."]); +} +?> diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..f28612a --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,439 @@ +/* Reset & Base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body, html { + height: 100%; + font-family: 'Google Sans Flex', 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + overflow: hidden; +} + +/* Map Container */ +#map { + width: 100vw; + height: 100vh; + z-index: 1; /* Di bawah UI element */ +} + +/* Custom UI Container over Map */ +.ui-container { + position: absolute; + top: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 1000; + width: 100%; + max-width: 400px; + display: flex; + flex-direction: column; + gap: 10px; + padding: 0 15px; +} + +/* Search Bar */ +.search-bar { + display: flex; + align-items: center; + background-color: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + overflow: hidden; + height: 48px; + transition: box-shadow 0.3s; +} + +.search-bar:focus-within { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); +} + +.search-icon { + padding: 0 15px; + color: #666; + display: flex; + align-items: center; + justify-content: center; +} + +.search-input { + flex: 1; + border: none; + outline: none; + font-size: 16px; + padding: 10px 0; + color: #333; +} + +.search-clear { + padding: 0 15px; + color: #999; + cursor: pointer; + background: none; + border: none; + display: flex; + align-items: center; + justify-content: center; + visibility: hidden; /* Muncul jika ada input */ +} + +.search-clear:hover { + color: #333; +} + +/* Custom Layer Control Button */ +.custom-layer-btn { + position: absolute; + top: 20px; + right: 20px; + z-index: 1000; + background-color: white; + border-radius: 8px; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + cursor: pointer; + border: none; + color: #333; +} + +.custom-layer-btn:hover { + background-color: #f8f9fa; +} + +.custom-layer-panel { + position: absolute; + top: 80px; + right: 20px; + z-index: 1000; + background-color: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + padding: 15px; + width: 250px; + display: none; /* Default tertutup */ +} + +.custom-layer-panel h3 { + margin-bottom: 10px; + font-size: 16px; + border-bottom: 1px solid #eee; + padding-bottom: 5px; +} + +.layer-option { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; + font-size: 14px; +} + +/* Popup Form */ +.popup-form { + display: flex; + flex-direction: column; + gap: 10px; + width: 200px; +} + +.popup-form label { + font-size: 12px; + font-weight: bold; + color: #555; +} + +.popup-form input[type="text"], +.popup-form input[type="number"] { + width: 100%; + padding: 6px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.popup-form .radio-group { + display: flex; + gap: 10px; +} + +.popup-form button { + padding: 8px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: bold; +} + +.popup-form button.btn-danger { + background-color: #dc3545; +} + +.popup-form button:hover { + opacity: 0.9; +} + +/* Action Menu (Kiri) */ +.action-menu { + position: absolute; + z-index: 1000; + background: transparent; + box-shadow: none; + padding: 0; + display: flex; + flex-direction: column; + gap: 10px; + width: 200px; +} + +.action-menu button { + padding: 10px 15px; + background-color: white; + border: none; + border-radius: 25px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + cursor: pointer; + text-align: left; + font-size: 13px; + font-weight: 500; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 8px; +} + +.action-menu button i { + width: 16px; + text-align: center; +} + +.action-menu button:hover { + background-color: #f8f9fa; + transform: translateX(5px); +} + +.action-menu button.active { + background-color: #007bff; + color: white; + box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3); +} + +/* Unified Modal */ +.unified-modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.5); + z-index: 2000; + align-items: center; + justify-content: center; +} + +.unified-modal.show { + display: flex; +} + +.modal-content { + background-color: white; + border-radius: 8px; + width: 400px; + max-width: 90%; + box-shadow: 0 4px 20px rgba(0,0,0,0.2); + display: flex; + flex-direction: column; +} + +.modal-header { + padding: 15px; + border-bottom: 1px solid #eee; + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal-header h3 { + font-size: 16px; + margin: 0; +} + +.modal-close { + font-size: 20px; + font-weight: bold; + cursor: pointer; + color: #888; +} + +.modal-close:hover { + color: #333; +} + +.modal-body { + padding: 15px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.modal-body label { + font-size: 13px; + font-weight: 600; + color: #444; +} + +.modal-body input[type="text"], +.modal-body input[type="number"], +.modal-body select, +.modal-body textarea { + width: 100%; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; +} + +.modal-footer { + padding: 15px; + border-top: 1px solid #eee; + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.btn-save { + background-color: #007bff; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; +} + +.btn-cancel { + background-color: #f8f9fa; + border: 1px solid #ddd; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 4px; +} + +/* Custom Cursor Tooltip */ +.custom-cursor-tooltip { + position: absolute; + display: none; + background-color: rgba(0, 0, 0, 0.75); + color: white; + padding: 6px 12px; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + pointer-events: none; /* Supaya tidak menghalangi klik ke map */ + z-index: 9999; + white-space: nowrap; + box-shadow: 0 2px 6px rgba(0,0,0,0.2); +} + +/* Sembunyikan default tooltip Leaflet Draw */ +.leaflet-draw-tooltip { + display: none !important; +} + +/* Sub-layer collapsible */ +.layer-group-item { + margin-bottom: 6px; +} + +.layer-group-item > .layer-group-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.layer-toggle-icon { + cursor: pointer; + padding: 2px 6px; + color: #666; + font-size: 11px; + border-radius: 4px; + transition: background 0.2s; + flex-shrink: 0; +} + +.layer-toggle-icon:hover { + background: #f0f0f0; + color: #333; +} + +.layer-toggle-icon i { + transition: transform 0.2s; +} + +.layer-toggle-icon.collapsed i { + transform: rotate(-90deg); +} + +.sub-layer-list { + padding-left: 18px; + margin-top: 4px; + border-left: 2px solid #eee; + overflow: hidden; + transition: max-height 0.2s ease; +} + +.sub-option { + font-size: 12px !important; + color: #555; + margin-bottom: 3px !important; +} + +/* ===== Emoji Marker (Pin Bubble) ===== */ +.emoji-marker { + display: flex; + flex-direction: column; + align-items: center; + pointer-events: none; +} + +.emoji-marker .bubble { + width: 38px; + height: 38px; + border-radius: 50% 50% 50% 0; + transform: rotate(-45deg); + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 3px 10px rgba(0,0,0,0.25); + border: 2px solid rgba(255,255,255,0.8); + animation: markerPop 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275) both; +} + +.emoji-marker .bubble span { + transform: rotate(45deg); + font-size: 18px; + line-height: 1; +} + +@keyframes markerPop { + 0% { transform: rotate(-45deg) scale(0); } + 100% { transform: rotate(-45deg) scale(1); } +} + +/* Warna bubble per tipe */ +.emoji-marker .bubble.spbu-24 { background: linear-gradient(135deg, #22c55e, #16a34a); } +.emoji-marker .bubble.spbu-not24 { background: linear-gradient(135deg, #ef4444, #dc2626); } +.emoji-marker .bubble.ibadah { background: linear-gradient(135deg, #f97316, #ea580c); } +.emoji-marker .bubble.miskin-in { background: linear-gradient(135deg, #ef4444, #dc2626); } +.emoji-marker .bubble.miskin-out { background: linear-gradient(135deg, #22c55e, #16a34a); } diff --git a/assets/js/features/draw_control.js b/assets/js/features/draw_control.js new file mode 100644 index 0000000..3fedfd5 --- /dev/null +++ b/assets/js/features/draw_control.js @@ -0,0 +1,141 @@ +// --- Setup Leaflet Draw --- + +// Layer terpisah untuk hasil gambar sementara sebelum disimpan +const drawnItems = new L.FeatureGroup(); +map.addLayer(drawnItems); + +const drawControl = new L.Control.Draw({ + draw: { + polygon: { + allowIntersection: false, + showArea: true + }, + polyline: { + metric: true + }, + circle: false, + circlemarker: false, + marker: false, // Marker default dimatikan, kita pakai klik peta untuk SPBU + rectangle: false + }, + edit: { + featureGroup: drawnItems, // Kita tidak mengedit di drawnItems, karena data aslinya di jalanLayer/parsilLayer + edit: false, + remove: false + } +}); +// Hapus map.addControl(drawControl); agar tidak tampil UI bawaan LeafletJS + + +// Flag untuk menghindari munculnya form SPBU saat sedang menggambar +map.on(L.Draw.Event.DRAWSTART, function (e) { + isDrawingMode = true; +}); + +map.on(L.Draw.Event.DRAWSTOP, function (e) { + setTimeout(() => { + isDrawingMode = false; + window.currentDrawMode = null; + window.activeDrawHandler = null; + deactivateAddMode(); + }, 200); +}); + +window.activateDraw = function(type) { + // Toggle: cek dari class button, lebih reliable daripada state variable + const btnJalan = document.getElementById('btnMenuJalan'); + const btnParsil = document.getElementById('btnMenuParsil'); + const isJalanActive = btnJalan.classList.contains('active'); + const isParsilActive = btnParsil.classList.contains('active'); + + if ((type === 'polyline' && isJalanActive) || (type === 'polygon' && isParsilActive)) { + window.deactivateAddMode(); + return; + } + + deactivateAddMode(); + window.currentDrawMode = type; + + if (type === 'polyline') { + const handler = new L.Draw.Polyline(map, drawControl.options.draw.polyline); + handler.enable(); + window.activeDrawHandler = handler; + btnJalan.classList.add('active'); + if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk menggambar Jalan'; + } else if (type === 'polygon') { + const handler = new L.Draw.Polygon(map, drawControl.options.draw.polygon); + handler.enable(); + window.activeDrawHandler = handler; + btnParsil.classList.add('active'); + if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk menggambar Parsil'; + } +}; + +map.on(L.Draw.Event.CREATED, function (e) { + const type = e.layerType; + const layer = e.layer; + + // Convert layer to GeoJSON geometry + const geoJson = layer.toGeoJSON().geometry; + const geoJsonStr = JSON.stringify(geoJson); + + if (type === 'polyline') { + let length = 0; + const latlngs = layer.getLatLngs(); + for (let i = 0; i < latlngs.length - 1; i++) { + length += latlngs[i].distanceTo(latlngs[i + 1]); + } + + // Gunakan Modal alih-alih prompt + const bodyHTML = ` +
+ + +
+
+ + +
+
+ +
+ `; + openModal("Tambah Jalan Baru", bodyHTML, function() { + window.saveNewJalan(geoJsonStr, length, 'modalJalanNama', 'modalJalanStatus'); + }); + + } + else if (type === 'polygon') { + const latlngs = layer.getLatLngs()[0]; + let area = 0; + if (L.GeometryUtil && L.GeometryUtil.geodesicArea) { + area = L.GeometryUtil.geodesicArea(latlngs); + } + + const bodyHTML = ` +
+ + +
+
+ + +
+
+ +
+ `; + openModal("Tambah Parsil Tanah", bodyHTML, function() { + window.saveNewParsil(geoJsonStr, area, 'modalParsilNama', 'modalParsilStatus'); + }); + } +}); diff --git a/assets/js/features/geojson.js b/assets/js/features/geojson.js new file mode 100644 index 0000000..f44330b --- /dev/null +++ b/assets/js/features/geojson.js @@ -0,0 +1,84 @@ +// --- Fitur Import GeoJSON --- + +const fileGeoJson = document.getElementById('fileGeoJson'); +let geoJsonLayers = {}; +let geoJsonCounter = 0; + +window.toggleGeoJsonMenu = function() { + const list = document.getElementById('geoJsonFileList'); + const icon = document.getElementById('geoJsonToggleIcon'); + if (list.style.display === 'none') { + list.style.display = 'block'; + icon.className = 'fas fa-minus'; + } else { + list.style.display = 'none'; + icon.className = 'fas fa-plus'; + } +}; + +fileGeoJson.addEventListener('change', function(e) { + const file = e.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = function(event) { + try { + const geoJsonData = JSON.parse(event.target.result); + const fileName = file.name; + const layerId = 'gj_' + (++geoJsonCounter); + + // Buat FeatureGroup khusus untuk file ini + const individualLayer = L.featureGroup(); + + // Tambahkan ke map + L.geoJSON(geoJsonData, { + onEachFeature: function (feature, layer) { + if (feature.properties) { + let popupContent = '

Informasi Feature

'; + layer.bindPopup(popupContent); + } + }, + style: function(feature) { + return { + color: feature.properties.color || '#3388ff', + weight: 2, + fillOpacity: 0.4 + }; + } + }).addTo(individualLayer); + + individualLayer.addTo(map); + geoJsonLayers[layerId] = individualLayer; + + // Tambahkan checkbox ke UI + const container = document.getElementById('geoJsonLayersContainer'); + const label = document.createElement('label'); + label.className = 'layer-option'; + label.innerHTML = ` ${fileName}`; + + label.querySelector('input').addEventListener('change', function(e) { + if (e.target.checked) { + map.addLayer(geoJsonLayers[layerId]); + } else { + map.removeLayer(geoJsonLayers[layerId]); + } + }); + + container.appendChild(label); + + // Reset input file agar bisa import file yang sama jika dihapus + fileGeoJson.value = ''; + + alert('File GeoJSON berhasil dimuat!'); + + } catch (error) { + alert('Gagal memproses file GeoJSON. Pastikan format valid.'); + console.error(error); + } + }; + reader.readAsText(file); +}); diff --git a/assets/js/features/geolocation.js b/assets/js/features/geolocation.js new file mode 100644 index 0000000..aa6179b --- /dev/null +++ b/assets/js/features/geolocation.js @@ -0,0 +1,30 @@ +// --- Fitur Geolocation --- + +// Tambahkan tombol di dalam wadah zoom control +const geoBtn = document.createElement('button'); +geoBtn.className = 'custom-layer-btn'; +geoBtn.style.position = 'relative'; +geoBtn.style.top = '0'; +geoBtn.style.left = '0'; +geoBtn.style.right = 'auto'; +geoBtn.innerHTML = ''; +geoBtn.title = 'Lokasi Saya'; +document.querySelector('.custom-zoom-control').appendChild(geoBtn); + +let userMarker = null; + +geoBtn.addEventListener('click', function() { + map.locate({setView: true, maxZoom: 16}); +}); + +map.on('locationfound', function(e) { + if (userMarker) { + map.removeLayer(userMarker); + } + userMarker = L.marker(e.latlng).addTo(map) + .bindPopup("Anda berada di sini!").openPopup(); +}); + +map.on('locationerror', function(e) { + alert("Gagal mendapatkan lokasi Anda."); +}); diff --git a/assets/js/features/jalan.js b/assets/js/features/jalan.js new file mode 100644 index 0000000..ccd8541 --- /dev/null +++ b/assets/js/features/jalan.js @@ -0,0 +1,162 @@ +// --- Fitur Jalan --- + +const jalanColors = { + 'Nasional': '#ff0000', // Merah + 'Provinsi': '#0000ff', // Biru + 'Kabupaten': '#00ff00' // Hijau +}; + +function loadJalan() { + jalanLayer.clearLayers(); + fetch('api/jalan/read.php') + .then(res => res.json()) + .then(data => { + if (data.status === 'success' && data.data) { + data.data.forEach(item => { + addJalanToMap(item); + }); + } + }); +} + +function addJalanToMap(item) { + // geom adalah GeoJSON {type: "LineString", coordinates: [[lng, lat], ...]} + const latlngs = item.geom.coordinates.map(coord => [coord[1], coord[0]]); + + const polyline = L.polyline(latlngs, { + color: jalanColors[item.status] || '#3388ff', + weight: 5 + }); + + polyline.jalanData = item; + + // Tampilkan label nama jalan sejajar dengan garis (diagonal) + polyline.setText(item.nama, { + center: true, + offset: 0, + attributes: { + fill: '#000000', + 'font-weight': 'bold', + 'font-size': '14px', + 'dy': '7' + } + }); + + // Hitung popupContent sekali saat jalan dibuat + const d = item; + const popupContent = ` +
+

Jalan ${d.nama}

+

Status: ${d.status}

+

Panjang: ${d.panjang.toFixed(2)} m

+
+ + +
+
+ `; + + polyline.bindPopup(popupContent); + + jalanLayer.addLayer(polyline); +} + +window.openEditJalanModal = function(id) { + let d = null; + jalanLayer.eachLayer(function(layer) { + if (layer.jalanData && layer.jalanData.id == id) d = layer.jalanData; + }); + if (!d) return; + + const bodyHTML = ` +
+ + +
+
+ + +
+
+ +
+ `; + map.closePopup(); + openModal("Edit Jalan", bodyHTML, function() { + window.saveEditJalan(d.id, 'editJalanNama', 'editJalanStatus'); + }); +}; + +window.saveEditJalan = function(id, namaId, statusId) { + const nama = document.getElementById(namaId).value; + const status = document.getElementById(statusId).value; + + fetch('api/jalan/update.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id, nama, status }) + }) + .then(res => res.json()) + .then(data => { + if(data.status === 'success') { + map.closePopup(); + loadJalan(); + } else { + alert(data.message); + } + }); +}; + +window.deleteJalan = function(id) { + openConfirmModal("Yakin hapus jalan ini?", function() { + fetch('api/jalan/delete.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id }) + }) + .then(res => res.json()) + .then(data => { + if(data.status === 'success') { + map.closePopup(); + loadJalan(); + } else { + alert(data.message); + } + }); + }); +}; + +window.saveNewJalan = function(geoJsonStr, panjang, namaId, statusId) { + const nama = document.getElementById(namaId).value; + if (!nama) { + alert("Nama jalan harus diisi!"); + return false; + } + + const status = document.getElementById(statusId).value; + + const geom = JSON.parse(geoJsonStr); + + fetch('api/jalan/create.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nama, status, panjang, geom }) + }) + .then(res => res.json()) + .then(data => { + if(data.status === 'success') { + closeModal(); + loadJalan(); + } else { + alert(data.message); + } + }); + return true; +}; + +// Initial Load +loadJalan(); diff --git a/assets/js/features/kemiskinan.js b/assets/js/features/kemiskinan.js new file mode 100644 index 0000000..91acce0 --- /dev/null +++ b/assets/js/features/kemiskinan.js @@ -0,0 +1,487 @@ +// --- Fitur Pemetaan Kemiskinan --- + +// Emoji Bubble Icon builder +function makeIbadahIcon() { + return L.divIcon({ + className: '', + html: `
🕌
`, + iconSize: [38, 38], + iconAnchor: [19, 38], + popupAnchor: [0, -40] + }); +} + +function makeMiskinIcon(inRadius) { + const cls = inRadius ? 'miskin-in' : 'miskin-out'; + return L.divIcon({ + className: '', + html: `
🏠
`, + iconSize: [38, 38], + iconAnchor: [19, 38], + popupAnchor: [0, -40] + }); +} + +const ibadahIcon = makeIbadahIcon(); +const miskinMerahIcon = makeMiskinIcon(true); +const miskinHijauIcon = makeMiskinIcon(false); + + +let ibadahDataList = []; +let miskinMarkerList = []; // Simpan referensi marker untuk update warna + +let isResizing = false; +let resizingCircle = null; +let resizingIbadah = null; + +map.on('mousemove', function(e) { + if (isResizing && resizingCircle && resizingIbadah) { + const center = resizingCircle.getLatLng(); + const newRadius = center.distanceTo(e.latlng); + resizingCircle.setRadius(newRadius); + resizingIbadah.radius = newRadius; + + // Update data radius di marker juga + rumahIbadahLayer.eachLayer(function(layer) { + if (layer.ibadahData && layer.ibadahData.id === resizingIbadah.id && layer instanceof L.Marker) { + layer.ibadahData.radius = newRadius; + } + }); + + updateSemuaWarnaMiskin(); + } +}); + +map.on('mouseup', function(e) { + if (isResizing) { + isResizing = false; + map.dragging.enable(); + if (resizingIbadah) { + updateIbadah(resizingIbadah.id, resizingIbadah.nama, resizingIbadah.alamat, resizingIbadah.radius, resizingIbadah.lat, resizingIbadah.lng); + } + resizingCircle = null; + resizingIbadah = null; + } +}); + +// --- Rumah Ibadah --- +function loadRumahIbadah() { + rumahIbadahLayer.clearLayers(); + ibadahDataList = []; + fetch('api/rumah_ibadah/read.php') + .then(res => res.json()) + .then(data => { + if (data.status === 'success' && data.data) { + ibadahDataList = data.data; + data.data.forEach(item => { + addIbadahMarker(item); + }); + updateSemuaWarnaMiskin(); // Update warna setelah memuat rumah ibadah + } + }); +} + +function addIbadahMarker(item) { + const marker = L.marker([item.lat, item.lng], { icon: ibadahIcon, draggable: true }); + + // Lingkaran radius + const circle = L.circle([item.lat, item.lng], { + radius: item.radius, + color: '#ff7800', + weight: 2, + fillColor: '#ff7800', + fillOpacity: 0.2 + }); + + circle.on('mousemove', function(e) { + if (isResizing) return; + const center = circle.getLatLng(); + const radius = circle.getRadius(); + const dist = center.distanceTo(e.latlng); + + const metersPerPixel = map.distance(map.containerPointToLatLng([0,0]), map.containerPointToLatLng([0,1])); + const tolerance = Math.max(radius * 0.1, metersPerPixel * 15); // toleransi 15px atau 10% + + if (Math.abs(dist - radius) <= tolerance) { + if (circle._path) circle._path.style.cursor = 'ew-resize'; + circle.nearEdge = true; + } else { + if (circle._path) circle._path.style.cursor = 'pointer'; + circle.nearEdge = false; + } + }); + + circle.on('mousedown', function(e) { + if (circle.nearEdge) { + map.dragging.disable(); + isResizing = true; + resizingCircle = circle; + resizingIbadah = item; + L.DomEvent.stopPropagation(e); + } + }); + + marker.ibadahData = item; + marker.circleLayer = circle; + + const d = item; + const popupContent = ` +
+

Rumah Ibadah ${d.nama}

+

Alamat: ${d.alamat}

+

Radius: ${d.radius} m

+
+ + +
+
+ `; + marker.bindPopup(popupContent); + + marker.on('dragend', function(e) { + const newPos = marker.getLatLng(); + circle.setLatLng(newPos); + item.lat = newPos.lat; + item.lng = newPos.lng; + // Update ke DB + updateIbadah(item.id, item.nama, item.alamat, item.radius, newPos.lat, newPos.lng); + }); + + rumahIbadahLayer.addLayer(circle); + rumahIbadahLayer.addLayer(marker); +} + +// Konteks Menu untuk Tambah Penduduk Miskin (klik kanan peta) +map.on('contextmenu', function(e) { + if (isDrawingMode) return; + + const popupContent = ` + + `; + L.popup().setLatLng(e.latlng).setContent(popupContent).openOn(map); +}); + +// Map Click -> Form Add Rumah Ibadah +map.on('click', function(e) { + if (isDrawingMode) return; + + if (window.currentAddMode === 'rumah_ibadah') { + const lat = e.latlng.lat; + const lng = e.latlng.lng; + + // Reverse Geocoding + fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`) + .then(res => res.json()) + .then(geoData => { + const alamat = geoData.display_name || 'Alamat tidak ditemukan'; + const bodyHTML = ` +
+ +
+
+ + +
+
+ + +
+
+ + +
+ `; + + openModal("Tambah Rumah Ibadah", bodyHTML, function() { + window.saveNewIbadah(lat, lng, 'modalIbadahNama', 'modalIbadahAlamat', 'modalIbadahRadius'); + }); + + window.deactivateAddMode(); + }) + .catch(err => { + // Tetap buka modal meskipun gagal mendapatkan alamat + const alamat = ''; + const bodyHTML = ` +
+ +
+
+ + +
+
+ + +
+
+ + +
+ `; + + openModal("Tambah Rumah Ibadah", bodyHTML, function() { + window.saveNewIbadah(lat, lng, 'modalIbadahNama', 'modalIbadahAlamat', 'modalIbadahRadius'); + }); + + window.deactivateAddMode(); + }); + } +}); + +window.saveNewIbadah = function(lat, lng, namaId, alamatId, radiusId) { + const nama = document.getElementById(namaId).value; + const alamat = document.getElementById(alamatId).value; + const radius = document.getElementById(radiusId).value; + + if (!nama) { + alert("Nama Rumah Ibadah harus diisi!"); + return; + } + + fetch('api/rumah_ibadah/create.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nama, alamat, radius, lat, lng }) + }) + .then(res => res.json()) + .then(data => { + if(data.status === 'success') { + closeModal(); + loadRumahIbadah(); + } else { alert(data.message); } + }); +}; + +window.openEditIbadahModal = function(id) { + let d = null; + rumahIbadahLayer.eachLayer(function(layer) { + if (layer.ibadahData && layer.ibadahData.id == id) d = layer.ibadahData; + }); + if (!d) return; + + const bodyHTML = ` +
+ + +
+
+ + +
+
+ + +
+ `; + map.closePopup(); + openModal("Edit Rumah Ibadah", bodyHTML, function() { + window.saveEditIbadah(d.id, d.lat, d.lng, 'editIbadahNama', 'editIbadahAlamat', 'editIbadahRadius'); + }); +}; + +window.previewRadius = function(id, newRadius) { + rumahIbadahLayer.eachLayer(function(layer) { + // layer ini bisa marker atau circle, kita cek yang punya circleLayer (marker) + if (layer.ibadahData && layer.ibadahData.id === id && layer.circleLayer) { + layer.circleLayer.setRadius(parseFloat(newRadius)); + layer.ibadahData.radius = parseFloat(newRadius); + updateSemuaWarnaMiskin(); // Update warna miskin real-time + } + }); +}; + +window.saveEditIbadah = function(id, lat, lng, namaId, alamatId, radiusId) { + const nama = document.getElementById(namaId).value; + const alamat = document.getElementById(alamatId).value; + const radius = document.getElementById(radiusId).value; + updateIbadah(id, nama, alamat, radius, lat, lng); +}; + +function updateIbadah(id, nama, alamat, radius, lat, lng) { + fetch('api/rumah_ibadah/update.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id, nama, alamat, radius, lat, lng }) + }).then(res => res.json()).then(data => { + if(data.status === 'success') { map.closePopup(); loadRumahIbadah(); } + }); +} + +window.deleteIbadah = function(id) { + openConfirmModal("Yakin hapus rumah ibadah ini?", function() { + fetch('api/rumah_ibadah/delete.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id }) + }).then(res => res.json()).then(data => { + if(data.status === 'success') { map.closePopup(); loadRumahIbadah(); } + }); + }); +}; + + +// --- Penduduk Miskin --- +function loadPendudukMiskin() { + pendudukMiskinLayer.clearLayers(); + miskinMarkerList = []; + fetch('api/penduduk_miskin/read.php') + .then(res => res.json()) + .then(data => { + if (data.status === 'success' && data.data) { + data.data.forEach(item => { + addMiskinMarker(item); + }); + updateSemuaWarnaMiskin(); + } + }); +} + +function addMiskinMarker(item) { + const marker = L.marker([item.lat, item.lng], { icon: miskinHijauIcon, draggable: true }); + marker.miskinData = item; + + const d = item; + const popupContent = ` +
+

Penduduk Miskin ${d.nama}

+

Bantuan: ${d.kategori_bantuan}

+
+ + +
+
+ `; + marker.bindPopup(popupContent); + + marker.on('dragend', function(e) { + const newPos = marker.getLatLng(); + item.lat = newPos.lat; + item.lng = newPos.lng; + updateSemuaWarnaMiskin(); // Update warna segera + // Simpan ke DB + fetch('api/penduduk_miskin/update.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(item) + }); + }); + + miskinMarkerList.push(marker); + pendudukMiskinLayer.addLayer(marker); +} + +window.formAddMiskin = function(lat, lng) { + const popupContent = ` + + `; + map.closePopup(); + L.popup().setLatLng([lat, lng]).setContent(popupContent).openOn(map); +}; + +window.saveNewMiskin = function(lat, lng) { + const nama = document.getElementById('miskinNama').value; + const kategori_bantuan = document.getElementById('miskinKategori').value; + + fetch('api/penduduk_miskin/create.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nama, kategori_bantuan, lat, lng }) + }) + .then(res => res.json()) + .then(data => { + if(data.status === 'success') { + map.closePopup(); + loadPendudukMiskin(); + } else { alert(data.message); } + }); +}; + +window.openEditMiskinModal = function(id) { + let d = null; + pendudukMiskinLayer.eachLayer(function(layer) { + if (layer.miskinData && layer.miskinData.id == id) d = layer.miskinData; + }); + if (!d) return; + + const bodyHTML = ` +
+ + +
+
+ + +
+ `; + map.closePopup(); + openModal("Edit Penduduk Miskin", bodyHTML, function() { + window.saveEditMiskin(d.id, d.lat, d.lng, 'editMiskinNama', 'editMiskinKategori'); + }); +}; + +window.saveEditMiskin = function(id, lat, lng, namaId, kategoriId) { + const nama = document.getElementById(namaId).value; + const kategori_bantuan = document.getElementById(kategoriId).value; + fetch('api/penduduk_miskin/update.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id, nama, kategori_bantuan, lat, lng }) + }).then(res => res.json()).then(data => { + if(data.status === 'success') { closeModal(); loadPendudukMiskin(); } + }); +}; + +window.deleteMiskin = function(id) { + openConfirmModal("Yakin hapus penduduk miskin ini?", function() { + fetch('api/penduduk_miskin/delete.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id }) + }).then(res => res.json()).then(data => { + if(data.status === 'success') { map.closePopup(); loadPendudukMiskin(); } + }); + }); +}; + +// Logika dinamis warna: Merah jika di dalam salah satu radius Rumah Ibadah, Hijau jika di luar +function updateSemuaWarnaMiskin() { + miskinMarkerList.forEach(marker => { + let inRadius = false; + const pLatlng = L.latLng(marker.miskinData.lat, marker.miskinData.lng); + + for (let i = 0; i < ibadahDataList.length; i++) { + const ibadah = ibadahDataList[i]; + const iLatlng = L.latLng(ibadah.lat, ibadah.lng); + const dist = pLatlng.distanceTo(iLatlng); // meter + + if (dist <= ibadah.radius) { + inRadius = true; + break; // Jika sudah masuk satu radius, langsung merah + } + } + + if (inRadius) { + marker.setIcon(makeMiskinIcon(true)); + } else { + marker.setIcon(makeMiskinIcon(false)); + } + }); +} + +// Initial Load +loadRumahIbadah(); +loadPendudukMiskin(); diff --git a/assets/js/features/parsil.js b/assets/js/features/parsil.js new file mode 100644 index 0000000..bc78b1d --- /dev/null +++ b/assets/js/features/parsil.js @@ -0,0 +1,149 @@ +// --- Fitur Parsil --- + +const parsilColors = { + 'SHM': '#28a745', // Hijau + 'HGB': '#17a2b8', // Biru Muda + 'HGU': '#ffc107', // Kuning + 'HP': '#fd7e14' // Oranye +}; + +function loadParsil() { + parsilLayer.clearLayers(); + fetch('api/parsil/read.php') + .then(res => res.json()) + .then(data => { + if (data.status === 'success' && data.data) { + data.data.forEach(item => { + addParsilToMap(item); + }); + } + }); +} + +function addParsilToMap(item) { + // geom adalah GeoJSON Polygon + // coordinates pada Polygon formatnya: [[[lng, lat], [lng, lat], ...]] + // Leaflet Polygon butuh array of [lat, lng] + const latlngs = item.geom.coordinates[0].map(coord => [coord[1], coord[0]]); + + const polygon = L.polygon(latlngs, { + color: parsilColors[item.status] || '#3388ff', + fillColor: parsilColors[item.status] || '#3388ff', + fillOpacity: 0.5, + weight: 2 + }); + + polygon.parsilData = item; + + const d = item; + const popupContent = ` +
+

Parsil ${d.nama || ''}

+

Status: ${d.status}

+

Luas: ${d.luas.toFixed(2)} m²

+
+ + +
+
+ `; + polygon.bindPopup(popupContent); + + parsilLayer.addLayer(polygon); +} + +window.openEditParsilModal = function(id) { + let d = null; + parsilLayer.eachLayer(function(layer) { + if (layer.parsilData && layer.parsilData.id == id) d = layer.parsilData; + }); + if (!d) return; + + const bodyHTML = ` +
+ + +
+
+ + +
+
+ +
+ `; + map.closePopup(); + openModal("Edit Parsil Tanah", bodyHTML, function() { + window.saveEditParsil(d.id, 'editParsilNama', 'editParsilStatus'); + }); +}; + +window.saveEditParsil = function(id, namaId, statusId) { + const nama = document.getElementById(namaId).value; + const status = document.getElementById(statusId).value; + + fetch('api/parsil/update.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id, nama, status }) + }) + .then(res => res.json()) + .then(data => { + if(data.status === 'success') { + map.closePopup(); + loadParsil(); + } else { + alert(data.message); + } + }); +}; + +window.deleteParsil = function(id) { + openConfirmModal("Yakin hapus parsil ini?", function() { + fetch('api/parsil/delete.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id }) + }) + .then(res => res.json()) + .then(data => { + if(data.status === 'success') { + map.closePopup(); + loadParsil(); + } else { + alert(data.message); + } + }); + }); +}; + +window.saveNewParsil = function(geoJsonStr, luas, namaId, statusId) { + const nama = document.getElementById(namaId).value; + const status = document.getElementById(statusId).value; + + const geom = JSON.parse(geoJsonStr); + + fetch('api/parsil/create.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nama, status, luas, geom }) + }) + .then(res => res.json()) + .then(data => { + if(data.status === 'success') { + closeModal(); + loadParsil(); + } else { + alert(data.message); + } + }); + return true; +}; + +// Initial Load +loadParsil(); diff --git a/assets/js/features/spbu.js b/assets/js/features/spbu.js new file mode 100644 index 0000000..0974c9e --- /dev/null +++ b/assets/js/features/spbu.js @@ -0,0 +1,204 @@ +// --- Fitur SPBU --- + +// Emoji Bubble Icon builder +function makeSpbuIcon(is24) { + const cls = is24 ? 'spbu-24' : 'spbu-not24'; + return L.divIcon({ + className: '', + html: `
`, + iconSize: [38, 38], + iconAnchor: [19, 38], + popupAnchor: [0, -40] + }); +} + +const spbuGreenIcon = makeSpbuIcon(true); +const spbuRedIcon = makeSpbuIcon(false); + +let isDrawingMode = false; // Akan diset true saat draw mode aktif (jalan/parsil) + + +// Fetch SPBU +function loadSpbu() { + spbuLayer.clearLayers(); + fetch('api/spbu/read.php') + .then(res => res.json()) + .then(data => { + if (data.status === 'success' && data.data) { + data.data.forEach(item => { + addSpbuMarker(item); + }); + } + }); +} + +function addSpbuMarker(item) { + const icon = makeSpbuIcon(item.is_24_jam); + const marker = L.marker([item.lat, item.lng], { + icon: icon, + draggable: true + }); + + marker.spbuData = item; // Simpan data di objek marker + + // Hitung popupContent sekali saat marker dibuat + const d = item; + const popupContent = ` +
+

SPBU ${d.nama}

+

No. WA: ${d.no_wa}

+

Buka 24 Jam: ${d.is_24_jam ? 'Ya' : 'Tidak'}

+
+ + +
+
+ `; + marker.bindPopup(popupContent); + + // Event drag untuk update koordinat + marker.on('dragend', function(e) { + const newPos = marker.getLatLng(); + updateSpbu(item.id, item.nama, item.no_wa, item.is_24_jam, newPos.lat, newPos.lng); + }); + + spbuLayer.addLayer(marker); +} + +// Map Click -> Form Add +map.on('click', function(e) { + if (isDrawingMode) return; // Jangan muncul form jika sedang gambar garis/polygon + + // Hanya proses jika mode tambah SPBU aktif + if (window.currentAddMode === 'spbu') { + const bodyHTML = ` +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ `; + + openModal("Tambah SPBU Baru", bodyHTML, function() { + window.saveNewSpbu(e.latlng.lat, e.latlng.lng, 'modalSpbuNama', 'modalSpbuWa', 'modalSpbu24'); + }); + + // Nonaktifkan mode tambah setelah modal muncul + window.deactivateAddMode(); + } +}); + +window.saveNewSpbu = function(lat, lng, namaId, waId, radioName) { + const nama = document.getElementById(namaId).value; + if (!nama) { + alert("Nama SPBU harus diisi!"); + return; + } + const no_wa = document.getElementById(waId).value; + const is_24_jam = document.querySelector(`input[name="${radioName}"]:checked`).value; + + fetch('api/spbu/create.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ nama, no_wa, is_24_jam, lat, lng }) + }) + .then(res => res.json()) + .then(data => { + if(data.status === 'success') { + closeModal(); + loadSpbu(); + } else { + alert(data.message); + } + }); +}; + +window.openEditSpbuModal = function(id) { + let d = null; + spbuLayer.eachLayer(function(layer) { + if (layer.spbuData && layer.spbuData.id == id) d = layer.spbuData; + }); + if (!d) return; + + const bodyHTML = ` +
+ + +
+
+ + +
+
+ +
+ + +
+
+ `; + map.closePopup(); + openModal("Edit SPBU", bodyHTML, function() { + window.saveEditSpbu(d.id, d.lat, d.lng, 'editSpbuNama', 'editSpbuWa', 'editSpbu24'); + }); +}; + +window.saveEditSpbu = function(id, lat, lng, namaId, waId, radioName) { + const nama = document.getElementById(namaId).value; + const no_wa = document.getElementById(waId).value; + const is_24_jam = document.querySelector(`input[name="${radioName}"]:checked`).value; + + updateSpbu(id, nama, no_wa, is_24_jam, lat, lng); +}; + +function updateSpbu(id, nama, no_wa, is_24_jam, lat, lng) { + fetch('api/spbu/update.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id, nama, no_wa, is_24_jam, lat, lng }) + }) + .then(res => res.json()) + .then(data => { + if(data.status === 'success') { + map.closePopup(); + loadSpbu(); + } else { + alert(data.message); + } + }); +} + +window.deleteSpbu = function(id) { + openConfirmModal("Yakin hapus SPBU ini?", function() { + fetch('api/spbu/delete.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id }) + }) + .then(res => res.json()) + .then(data => { + if(data.status === 'success') { + map.closePopup(); + loadSpbu(); + } else { + alert(data.message); + } + }); + }); +}; + +// Initial Load +loadSpbu(); diff --git a/assets/js/map.js b/assets/js/map.js new file mode 100644 index 0000000..96e8a3e --- /dev/null +++ b/assets/js/map.js @@ -0,0 +1,326 @@ +// Inisialisasi Peta +// Koordinat awal: [-0.0263, 109.3425] (Pontianak) +const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13); + +// Custom Zoom Control Logic +document.getElementById('zoomInBtn').addEventListener('click', function() { map.zoomIn(); }); +document.getElementById('zoomOutBtn').addEventListener('click', function() { map.zoomOut(); }); + +// Base Map dari OpenStreetMap +L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' +}).addTo(map); + +// Inisialisasi FeatureGroups untuk masing-masing layer +const spbuLayer = L.featureGroup().addTo(map); +const jalanLayer = L.featureGroup().addTo(map); +const parsilLayer = L.featureGroup().addTo(map); +const rumahIbadahLayer = L.featureGroup().addTo(map); +const pendudukMiskinLayer = L.featureGroup().addTo(map); +const geoJsonLayer = L.featureGroup().addTo(map); + +const searchInput = document.getElementById('searchInput'); +const searchClear = document.getElementById('searchClear'); + +searchInput.addEventListener('input', function() { + const val = this.value.toLowerCase(); + if (val.length > 0) { + searchClear.style.visibility = 'visible'; + window.showSearchResults(val); + } else { + searchClear.style.visibility = 'hidden'; + document.getElementById('searchResults').style.display = 'none'; + } +}); + +searchClear.addEventListener('click', function() { + searchInput.value = ''; + searchClear.style.visibility = 'hidden'; + document.getElementById('searchResults').style.display = 'none'; + searchInput.focus(); +}); + +window.showSearchResults = function(query) { + const resultsContainer = document.getElementById('searchResults'); + resultsContainer.innerHTML = ''; + let results = []; + + if (typeof spbuLayer !== 'undefined') { + spbuLayer.eachLayer(layer => { + if (layer.spbuData && layer.spbuData.nama && layer.spbuData.nama.toLowerCase().includes(query)) { + results.push({ type: 'SPBU', nama: layer.spbuData.nama, layer: layer }); + } + }); + } + + if (typeof jalanLayer !== 'undefined') { + jalanLayer.eachLayer(layer => { + if (layer.jalanData && layer.jalanData.nama && layer.jalanData.nama.toLowerCase().includes(query)) { + results.push({ type: 'Jalan', nama: layer.jalanData.nama, layer: layer }); + } + }); + } + + if (typeof parsilLayer !== 'undefined') { + parsilLayer.eachLayer(layer => { + if (layer.parsilData && layer.parsilData.nama && layer.parsilData.nama.toLowerCase().includes(query)) { + results.push({ type: 'Parsil', nama: layer.parsilData.nama, layer: layer }); + } + }); + } + + if (typeof rumahIbadahLayer !== 'undefined') { + rumahIbadahLayer.eachLayer(layer => { + if (layer.ibadahData && layer.ibadahData.nama && layer.ibadahData.nama.toLowerCase().includes(query)) { + if(layer instanceof L.Marker) { + results.push({ type: 'Rumah Ibadah', nama: layer.ibadahData.nama, layer: layer }); + } + } + }); + } + + if (results.length === 0) { + resultsContainer.innerHTML = '
Tidak ada hasil
'; + } else { + results.forEach(res => { + const item = document.createElement('div'); + item.style.padding = '10px 15px'; + item.style.cursor = 'pointer'; + item.style.borderBottom = '1px solid #eee'; + item.style.fontSize = '14px'; + item.innerHTML = `${res.type}: ${res.nama}`; + item.addEventListener('mouseenter', () => item.style.backgroundColor = '#f8f9fa'); + item.addEventListener('mouseleave', () => item.style.backgroundColor = 'white'); + item.addEventListener('click', () => { + if (res.layer instanceof L.Marker) { + map.setView(res.layer.getLatLng(), 17); + } else if (res.layer.getBounds) { + map.fitBounds(res.layer.getBounds()); + } + res.layer.openPopup(); + resultsContainer.style.display = 'none'; + }); + resultsContainer.appendChild(item); + }); + } + resultsContainer.style.display = 'block'; +}; + +// UI Logic: Custom Layer Control Toggle +const layerBtn = document.getElementById('layerBtn'); +const layerPanel = document.getElementById('layerPanel'); + +layerBtn.addEventListener('click', function() { + if (layerPanel.style.display === 'block') { + layerPanel.style.display = 'none'; + } else { + layerPanel.style.display = 'block'; + } +}); + +// Sembunyikan layer panel saat klik di luar area (pada map) +map.on('click', function() { + layerPanel.style.display = 'none'; +}); + +// Logic untuk Toggle Layer Visibility +document.getElementById('layerSpbu').addEventListener('change', function(e) { + e.target.checked ? map.addLayer(spbuLayer) : map.removeLayer(spbuLayer); +}); +document.getElementById('layerJalan').addEventListener('change', function(e) { + e.target.checked ? map.addLayer(jalanLayer) : map.removeLayer(jalanLayer); +}); +document.getElementById('layerParsil').addEventListener('change', function(e) { + e.target.checked ? map.addLayer(parsilLayer) : map.removeLayer(parsilLayer); +}); +document.getElementById('layerRumahIbadah').addEventListener('change', function(e) { + e.target.checked ? map.addLayer(rumahIbadahLayer) : map.removeLayer(rumahIbadahLayer); +}); +document.getElementById('layerMiskin').addEventListener('change', function(e) { + e.target.checked ? map.addLayer(pendudukMiskinLayer) : map.removeLayer(pendudukMiskinLayer); +}); + +// --- Sub-layer Toggle (expand/collapse) --- +window.toggleSubLayer = function(subId, iconEl) { + const sub = document.getElementById(subId); + if (!sub) return; + const isHidden = sub.style.display === 'none'; + sub.style.display = isHidden ? '' : 'none'; + iconEl.classList.toggle('collapsed', !isHidden); +}; + +// --- Sub-layer Filter --- +window.applySubFilter = function(type) { + if (type === 'spbu') { + const checked = [...document.querySelectorAll('.sub-spbu:checked')].map(el => el.value); + spbuLayer.eachLayer(layer => { + if (!layer.spbuData) return; + const val = layer.spbuData.is_24_jam ? '1' : '0'; + if (checked.includes(val)) { + if (!spbuLayer.hasLayer(layer)) spbuLayer.addLayer(layer); + layer.getElement && layer.getElement() && (layer.getElement().style.display = ''); + } else { + layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none'); + } + }); + } + + if (type === 'jalan') { + const checked = [...document.querySelectorAll('.sub-jalan:checked')].map(el => el.value); + jalanLayer.eachLayer(layer => { + if (!layer.jalanData) return; + const visible = checked.includes(layer.jalanData.status); + const display = visible ? '' : 'none'; + // Sembunyikan garis polyline + if (layer._path) layer._path.style.display = display; + // Sembunyikan label teks (leaflet-textpath simpan di _textNode) + if (layer._textNode) layer._textNode.style.display = display; + }); + } + + if (type === 'parsil') { + const checked = [...document.querySelectorAll('.sub-parsil:checked')].map(el => el.value); + parsilLayer.eachLayer(layer => { + if (!layer.parsilData) return; + if (checked.includes(layer.parsilData.status)) { + layer._path && (layer._path.style.display = ''); + } else { + layer._path && (layer._path.style.display = 'none'); + } + }); + } + + if (type === 'miskin') { + const checked = [...document.querySelectorAll('.sub-miskin:checked')].map(el => el.value); + pendudukMiskinLayer.eachLayer(layer => { + if (!layer.miskinData) return; + if (checked.includes(layer.miskinData.kategori_bantuan)) { + layer.getElement && layer.getElement() && (layer.getElement().style.display = ''); + } else { + layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none'); + } + }); + } +}; + +// --- Modal Logic --- +const unifiedModal = document.getElementById('unifiedModal'); +const modalTitle = document.getElementById('modalTitle'); +const modalBody = document.getElementById('modalBody'); + +window.openModal = function(title, bodyHTML, saveCallback) { + modalTitle.textContent = title; + modalBody.innerHTML = bodyHTML; + unifiedModal.classList.add('show'); + + // Ambil elemen tombol save terbaru dari DOM + const currentSaveBtn = document.getElementById('modalSaveBtn'); + + // Clone untuk menghapus semua event listener lama + const newSaveBtn = currentSaveBtn.cloneNode(true); + currentSaveBtn.parentNode.replaceChild(newSaveBtn, currentSaveBtn); + + newSaveBtn.addEventListener('click', saveCallback); +}; + +window.closeModal = function() { + unifiedModal.classList.remove('show'); +}; + +const confirmModal = document.getElementById('confirmModal'); +const confirmMessage = document.getElementById('confirmMessage'); + +window.openConfirmModal = function(msg, confirmCallback) { + confirmMessage.textContent = msg; + confirmModal.classList.add('show'); + + const currentYesBtn = document.getElementById('confirmYesBtn'); + const newYesBtn = currentYesBtn.cloneNode(true); + currentYesBtn.parentNode.replaceChild(newYesBtn, currentYesBtn); + + newYesBtn.addEventListener('click', function() { + confirmModal.classList.remove('show'); + confirmCallback(); + }); +}; + +window.closeConfirmModal = function() { + confirmModal.classList.remove('show'); +}; + +// --- Action Menu Logic --- +const actionMenuBtn = document.getElementById('actionMenuBtn'); +const actionMenu = document.getElementById('actionMenu'); + +actionMenuBtn.addEventListener('click', function() { + if (actionMenu.style.display === 'flex') { + actionMenu.style.display = 'none'; + window.deactivateAddMode(); + } else { + actionMenu.style.display = 'flex'; + } +}); + +window.currentAddMode = null; // 'spbu' atau 'rumah_ibadah' +window.currentDrawMode = null; // 'polyline' atau 'polygon' + +window.activateAddMode = function(mode) { + // Toggle: jika mode yang sama diklik lagi, batalkan + if (window.currentAddMode === mode) { + window.deactivateAddMode(); + return; + } + + // Reset draw mode if active + if (window.activeDrawHandler) { + window.activeDrawHandler.disable(); + window.activeDrawHandler = null; + } + + window.currentAddMode = mode; + window.currentDrawMode = null; + + // Update button UI + document.querySelectorAll('.action-menu button').forEach(btn => btn.classList.remove('active')); + + if (mode === 'spbu') { + document.getElementById('btnMenuSpbu').classList.add('active'); + if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk Tambah SPBU'; + } + if (mode === 'rumah_ibadah') { + document.getElementById('btnMenuIbadah').classList.add('active'); + if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk Tambah Rumah Ibadah'; + } + + // Ubah kursor map menjadi crosshair + document.getElementById('map').style.cursor = 'crosshair'; +}; + +window.deactivateAddMode = function() { + window.currentAddMode = null; + window.currentDrawMode = null; + if (window.activeDrawHandler) { + window.activeDrawHandler.disable(); + window.activeDrawHandler = null; + } + document.querySelectorAll('.action-menu button').forEach(btn => btn.classList.remove('active')); + document.getElementById('map').style.cursor = ''; + if (window.cursorTooltip) window.cursorTooltip.style.display = 'none'; +}; + +// --- Custom Tooltip Cursor --- +window.cursorTooltip = document.createElement('div'); +window.cursorTooltip.className = 'custom-cursor-tooltip'; +document.body.appendChild(window.cursorTooltip); + +// Gunakan document-level mousemove agar selalu terpicu +// bahkan saat Leaflet Draw overlay aktif menangkap event map +document.addEventListener('mousemove', function(e) { + if (window.currentAddMode || window.currentDrawMode) { + window.cursorTooltip.style.display = 'block'; + window.cursorTooltip.style.left = e.pageX + 15 + 'px'; + window.cursorTooltip.style.top = e.pageY + 15 + 'px'; + } else { + window.cursorTooltip.style.display = 'none'; + } +}); diff --git a/index.php b/index.php new file mode 100644 index 0000000..597aff5 --- /dev/null +++ b/index.php @@ -0,0 +1,231 @@ + + + + + + WebGIS Pemetaan Kemiskinan + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + +
+ + +
+ + + + + + + + + + + +
+

Daftar Layer

+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + + + + +
+
+ + +
+ +
+ + +
+
+ GeoJSON Eksternal + +
+ +
+
+ + +
+ + +
+ +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +