init: publish to github

This commit is contained in:
Syariffullah
2026-05-06 10:05:49 +07:00
commit 448083d2ea
31 changed files with 2746 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
<?php
$host = "localhost";
$user = "root";
$pass = "";
$db = "webgis";
// Coba koneksi ke server dan pilih database
$conn = new mysqli($host, $user, $pass, $db);
if ($conn->connect_error) {
die("Koneksi gagal: " . $conn->connect_error);
}
// Set charset
$conn->set_charset("utf8mb4");
// Jika di-include oleh file API, biarkan $conn tersedia
?>
+23
View File
@@ -0,0 +1,23 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+20
View File
@@ -0,0 +1,20 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+25
View File
@@ -0,0 +1,25 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$query = "SELECT id, nama, status, panjang, ST_AsGeoJSON(geom) as geom FROM jalan";
$result = $conn->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" => []]);
}
?>
+29
View File
@@ -0,0 +1,29 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+23
View File
@@ -0,0 +1,23 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+20
View File
@@ -0,0 +1,20 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+25
View File
@@ -0,0 +1,25 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$query = "SELECT id, nama, status, luas, ST_AsGeoJSON(geom) as geom FROM parsil";
$result = $conn->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" => []]);
}
?>
+29
View File
@@ -0,0 +1,29 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+23
View File
@@ -0,0 +1,23 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+20
View File
@@ -0,0 +1,20 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+25
View File
@@ -0,0 +1,25 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$query = "SELECT * FROM penduduk_miskin";
$result = $conn->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" => []]);
}
?>
+24
View File
@@ -0,0 +1,24 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+24
View File
@@ -0,0 +1,24 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+20
View File
@@ -0,0 +1,20 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+26
View File
@@ -0,0 +1,26 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$query = "SELECT * FROM rumah_ibadah";
$result = $conn->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" => []]);
}
?>
+25
View File
@@ -0,0 +1,25 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+24
View File
@@ -0,0 +1,24 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+20
View File
@@ -0,0 +1,20 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+26
View File
@@ -0,0 +1,26 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$query = "SELECT * FROM spbu";
$result = $conn->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" => []]);
}
?>
+25
View File
@@ -0,0 +1,25 @@
<?php
header("Content-Type: application/json; charset=UTF-8");
include_once '../db_connect.php';
$data = json_decode(file_get_contents("php://input"));
if(!empty($data->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."]);
}
?>
+439
View File
@@ -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); }
+141
View File
@@ -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 = `
<div class="form-group">
<label>Nama Jalan</label>
<input type="text" id="modalJalanNama" placeholder="Nama Jalan">
</div>
<div class="form-group">
<label>Status</label>
<select id="modalJalanStatus">
<option value="Nasional">Nasional</option>
<option value="Provinsi">Provinsi</option>
<option value="Kabupaten" selected>Kabupaten</option>
</select>
</div>
<div class="form-group">
<label>Panjang: ${length.toFixed(2)} m</label>
</div>
`;
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 = `
<div class="form-group">
<label>Nama Pemilik/Area</label>
<input type="text" id="modalParsilNama" placeholder="Contoh: Budi atau Sawah A">
</div>
<div class="form-group">
<label>Status Tanah</label>
<select id="modalParsilStatus">
<option value="SHM" selected>SHM</option>
<option value="HGB">HGB</option>
<option value="HGU">HGU</option>
<option value="HP">HP</option>
</select>
</div>
<div class="form-group">
<label>Luas: ${area.toFixed(2)} m²</label>
</div>
`;
openModal("Tambah Parsil Tanah", bodyHTML, function() {
window.saveNewParsil(geoJsonStr, area, 'modalParsilNama', 'modalParsilStatus');
});
}
});
+84
View File
@@ -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 = '<h4>Informasi Feature</h4><ul style="list-style:none; padding:0;">';
for (let key in feature.properties) {
popupContent += `<li><strong>${key}:</strong> ${feature.properties[key]}</li>`;
}
popupContent += '</ul>';
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 = `<input type="checkbox" id="chk_${layerId}" checked> ${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);
});
+30
View File
@@ -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 = '<i class="fas fa-crosshairs fa-lg"></i>';
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.");
});
+162
View File
@@ -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 = `
<div style="font-family: Arial, sans-serif; min-width: 150px;">
<h4 style="margin:0 0 5px 0;">Jalan ${d.nama}</h4>
<p style="margin: 0 0 5px 0;"><b>Status:</b> ${d.status}</p>
<p style="margin: 0 0 10px 0;"><b>Panjang:</b> ${d.panjang.toFixed(2)} m</p>
<div style="display:flex; gap:5px;">
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditJalanModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteJalan(${d.id})">Hapus</button>
</div>
</div>
`;
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 = `
<div class="form-group">
<label>Nama Jalan</label>
<input type="text" id="editJalanNama" value="${d.nama}">
</div>
<div class="form-group">
<label>Status</label>
<select id="editJalanStatus">
<option value="Nasional" ${d.status === 'Nasional' ? 'selected' : ''}>Nasional</option>
<option value="Provinsi" ${d.status === 'Provinsi' ? 'selected' : ''}>Provinsi</option>
<option value="Kabupaten" ${d.status === 'Kabupaten' ? 'selected' : ''}>Kabupaten</option>
</select>
</div>
<div class="form-group">
<label>Panjang: ${d.panjang.toFixed(2)} m</label>
</div>
`;
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();
+487
View File
@@ -0,0 +1,487 @@
// --- Fitur Pemetaan Kemiskinan ---
// Emoji Bubble Icon builder
function makeIbadahIcon() {
return L.divIcon({
className: '',
html: `<div class="emoji-marker"><div class="bubble ibadah"><span>🕌</span></div></div>`,
iconSize: [38, 38],
iconAnchor: [19, 38],
popupAnchor: [0, -40]
});
}
function makeMiskinIcon(inRadius) {
const cls = inRadius ? 'miskin-in' : 'miskin-out';
return L.divIcon({
className: '',
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>🏠</span></div></div>`,
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 = `
<div style="font-family: Arial, sans-serif; min-width: 150px;">
<h4 style="margin:0 0 5px 0;">Rumah Ibadah ${d.nama}</h4>
<p style="margin: 0 0 5px 0;"><b>Alamat:</b> ${d.alamat}</p>
<p style="margin: 0 0 10px 0;"><b>Radius:</b> ${d.radius} m</p>
<div style="display:flex; gap:5px;">
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditIbadahModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteIbadah(${d.id})">Hapus</button>
</div>
</div>
`;
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 = `
<div class="popup-form">
<h4>Tambah Titik</h4>
<button class="btn-danger" style="margin-top:5px;" onclick="formAddMiskin(${e.latlng.lat}, ${e.latlng.lng})">Tambah Penduduk Miskin</button>
</div>
`;
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 = `
<div class="form-group">
<label>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</label>
</div>
<div class="form-group">
<label>Nama Rumah Ibadah</label>
<input type="text" id="modalIbadahNama" placeholder="Nama Rumah Ibadah">
</div>
<div class="form-group">
<label>Alamat (Auto)</label>
<textarea id="modalIbadahAlamat" rows="3">${alamat}</textarea>
</div>
<div class="form-group">
<label>Radius (m)</label>
<input type="number" id="modalIbadahRadius" value="500">
</div>
`;
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 = `
<div class="form-group">
<label>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</label>
</div>
<div class="form-group">
<label>Nama Rumah Ibadah</label>
<input type="text" id="modalIbadahNama" placeholder="Nama Rumah Ibadah">
</div>
<div class="form-group">
<label>Alamat (Manual)</label>
<textarea id="modalIbadahAlamat" rows="3" placeholder="Masukkan alamat manual">${alamat}</textarea>
</div>
<div class="form-group">
<label>Radius (m)</label>
<input type="number" id="modalIbadahRadius" value="500">
</div>
`;
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 = `
<div class="form-group">
<label>Nama Rumah Ibadah</label>
<input type="text" id="editIbadahNama" value="${d.nama}">
</div>
<div class="form-group">
<label>Alamat</label>
<textarea id="editIbadahAlamat" rows="2">${d.alamat}</textarea>
</div>
<div class="form-group">
<label>Radius (m)</label>
<input type="number" id="editIbadahRadius" value="${d.radius}" oninput="previewRadius(${d.id}, this.value)">
</div>
`;
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 = `
<div style="font-family: Arial, sans-serif; min-width: 150px;">
<h4 style="margin:0 0 5px 0;">Penduduk Miskin ${d.nama}</h4>
<p style="margin: 0 0 10px 0;"><b>Bantuan:</b> ${d.kategori_bantuan}</p>
<div style="display:flex; gap:5px;">
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditMiskinModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteMiskin(${d.id})">Hapus</button>
</div>
</div>
`;
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 = `
<div class="popup-form">
<h4>Penduduk Miskin Baru</h4>
<input type="text" id="miskinNama" placeholder="Nama Penduduk">
<select id="miskinKategori">
<option value="Makan">Bantuan Makan</option>
<option value="Pemberdayaan">Pemberdayaan</option>
</select>
<button onclick="saveNewMiskin(${lat}, ${lng})">Simpan</button>
</div>
`;
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 = `
<div class="form-group">
<label>Nama Penduduk</label>
<input type="text" id="editMiskinNama" value="${d.nama}">
</div>
<div class="form-group">
<label>Bantuan</label>
<select id="editMiskinKategori">
<option value="Makan" ${d.kategori_bantuan === 'Makan' ? 'selected' : ''}>Bantuan Makan</option>
<option value="Pemberdayaan" ${d.kategori_bantuan === 'Pemberdayaan' ? 'selected' : ''}>Pemberdayaan</option>
</select>
</div>
`;
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();
+149
View File
@@ -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 = `
<div style="font-family: Arial, sans-serif; min-width: 150px;">
<h4 style="margin:0 0 5px 0;">Parsil ${d.nama || ''}</h4>
<p style="margin: 0 0 5px 0;"><b>Status:</b> ${d.status}</p>
<p style="margin: 0 0 10px 0;"><b>Luas:</b> ${d.luas.toFixed(2)} m²</p>
<div style="display:flex; gap:5px;">
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditParsilModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteParsil(${d.id})">Hapus</button>
</div>
</div>
`;
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 = `
<div class="form-group">
<label>Nama Pemilik/Area</label>
<input type="text" id="editParsilNama" value="${d.nama || ''}">
</div>
<div class="form-group">
<label>Status</label>
<select id="editParsilStatus">
<option value="SHM" ${d.status === 'SHM' ? 'selected' : ''}>SHM</option>
<option value="HGB" ${d.status === 'HGB' ? 'selected' : ''}>HGB</option>
<option value="HGU" ${d.status === 'HGU' ? 'selected' : ''}>HGU</option>
<option value="HP" ${d.status === 'HP' ? 'selected' : ''}>HP</option>
</select>
</div>
<div class="form-group">
<label>Luas: ${d.luas.toFixed(2)} m²</label>
</div>
`;
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();
+204
View File
@@ -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: `<div class="emoji-marker"><div class="bubble ${cls}"><span>⛽</span></div></div>`,
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 = `
<div style="font-family: Arial, sans-serif; min-width: 150px;">
<h4 style="margin:0 0 5px 0;">SPBU ${d.nama}</h4>
<p style="margin: 0 0 5px 0;"><b>No. WA:</b> ${d.no_wa}</p>
<p style="margin: 0 0 10px 0;"><b>Buka 24 Jam:</b> ${d.is_24_jam ? 'Ya' : 'Tidak'}</p>
<div style="display:flex; gap:5px;">
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditSpbuModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteSpbu(${d.id})">Hapus</button>
</div>
</div>
`;
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 = `
<div class="form-group">
<label>Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}</label>
</div>
<div class="form-group">
<label>Nama SPBU</label>
<input type="text" id="modalSpbuNama" placeholder="Nama SPBU">
</div>
<div class="form-group">
<label>No. WA</label>
<input type="text" id="modalSpbuWa" placeholder="No. WA">
</div>
<div class="form-group">
<label>Buka 24 Jam?</label>
<div class="radio-group" style="margin-top:5px;">
<label><input type="radio" name="modalSpbu24" value="1"> Ya</label>
<label><input type="radio" name="modalSpbu24" value="0" checked> Tidak</label>
</div>
</div>
`;
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 = `
<div class="form-group">
<label>Nama SPBU</label>
<input type="text" id="editSpbuNama" value="${d.nama}">
</div>
<div class="form-group">
<label>No. WA</label>
<input type="text" id="editSpbuWa" value="${d.no_wa}">
</div>
<div class="form-group">
<label>Buka 24 Jam?</label>
<div class="radio-group" style="margin-top:5px;">
<label><input type="radio" name="editSpbu24" value="1" ${d.is_24_jam ? 'checked' : ''}> Ya</label>
<label><input type="radio" name="editSpbu24" value="0" ${!d.is_24_jam ? 'checked' : ''}> Tidak</label>
</div>
</div>
`;
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();
+326
View File
@@ -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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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 = '<div style="padding: 10px 15px; color: #666; font-size: 14px;">Tidak ada hasil</div>';
} 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 = `<strong>${res.type}:</strong> ${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';
}
});
+231
View File
@@ -0,0 +1,231 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Pemetaan Kemiskinan</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Google+Sans+Flex:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- FontAwesome (Icons) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<!-- Leaflet Draw CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
<!-- Custom CSS -->
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<!-- UI Container over map -->
<div class="ui-container">
<!-- Search Bar -->
<div class="search-bar">
<div class="search-icon">
<i class="fas fa-search"></i>
</div>
<input type="text" class="search-input" id="searchInput" placeholder="Cari SPBU, jalan, parsil, rumah ibadah...">
<button class="search-clear" id="searchClear">
<i class="fas fa-times"></i>
</button>
</div>
<!-- Search Results -->
<div id="searchResults" style="display: none; background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-height: 200px; overflow-y: auto;">
</div>
</div>
<!-- Custom Zoom Control -->
<div class="custom-zoom-control" style="position: absolute; top: 20px; left: 20px; z-index: 1000; display: flex; flex-direction: column; gap: 5px;">
<button class="custom-layer-btn" id="zoomInBtn" style="position: relative; top: 0; left: 0; right: auto;" title="Zoom In"><i class="fas fa-plus"></i></button>
<button class="custom-layer-btn" id="zoomOutBtn" style="position: relative; top: 0; left: 0; right: auto;" title="Zoom Out"><i class="fas fa-minus"></i></button>
</div>
<!-- Action Menu Toggle Button -->
<button class="custom-layer-btn" id="actionMenuBtn" style="top: 185px; left: 20px; right: auto;" title="Menu Tambah Data">
<i class="fas fa-chevron-right"></i>
</button>
<!-- Action Menu (Kiri) -->
<div class="action-menu" id="actionMenu" style="display: none; top: 240px; left: 20px;">
<button id="btnMenuSpbu" onclick="window.activateAddMode('spbu')"><i class="fas fa-gas-pump"></i> Tambah SPBU</button>
<button id="btnMenuIbadah" onclick="window.activateAddMode('rumah_ibadah')"><i class="fas fa-mosque"></i> Tambah Rumah Ibadah</button>
<button id="btnMenuJalan" onclick="window.activateDraw('polyline')"><i class="fas fa-road"></i> Tambah Jalan</button>
<button id="btnMenuParsil" onclick="window.activateDraw('polygon')"><i class="fas fa-draw-polygon"></i> Tambah Parsil Tanah</button>
</div>
<!-- Custom Layer Control Button -->
<button class="custom-layer-btn" id="layerBtn" title="Atur Layer">
<i class="fas fa-layer-group fa-lg"></i>
</button>
<!-- Custom Layer Control Panel -->
<div class="custom-layer-panel" id="layerPanel">
<h3>Daftar Layer</h3>
<!-- SPBU -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerSpbu" checked> SPBU
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subSpbu', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subSpbu" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-spbu" value="1" checked onchange="applySubFilter('spbu')"> 24 Jam
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-spbu" value="0" checked onchange="applySubFilter('spbu')"> Tidak 24 Jam
</label>
</div>
</div>
<!-- Jalan -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerJalan" checked> Jalan
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subJalan', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subJalan" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-jalan" value="Nasional" checked onchange="applySubFilter('jalan')"> Nasional
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-jalan" value="Provinsi" checked onchange="applySubFilter('jalan')"> Provinsi
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-jalan" value="Kabupaten" checked onchange="applySubFilter('jalan')"> Kabupaten
</label>
</div>
</div>
<!-- Parsil -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerParsil" checked> Parsil Tanah
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subParsil', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subParsil" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="SHM" checked onchange="applySubFilter('parsil')"> SHM
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HGB" checked onchange="applySubFilter('parsil')"> HGB
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HGU" checked onchange="applySubFilter('parsil')"> HGU
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-parsil" value="HP" checked onchange="applySubFilter('parsil')"> HP
</label>
</div>
</div>
<!-- Rumah Ibadah -->
<label class="layer-option">
<input type="checkbox" id="layerRumahIbadah" checked> Rumah Ibadah
</label>
<!-- Penduduk Miskin -->
<div class="layer-group-item">
<div class="layer-group-header">
<label class="layer-option" style="margin:0; flex:1;">
<input type="checkbox" id="layerMiskin" checked> Penduduk Miskin
</label>
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subMiskin', this)"><i class="fas fa-chevron-down"></i></span>
</div>
<div id="subMiskin" class="sub-layer-list" style="display:none;">
<label class="layer-option sub-option">
<input type="checkbox" class="sub-miskin" value="Makan" checked onchange="applySubFilter('miskin')"> Bantuan Makan
</label>
<label class="layer-option sub-option">
<input type="checkbox" class="sub-miskin" value="Pemberdayaan" checked onchange="applySubFilter('miskin')"> Pemberdayaan
</label>
</div>
</div>
<!-- GeoJSON Eksternal -->
<div class="layer-group" style="margin-top: 10px; border-top: 1px solid #eee; padding-top: 10px;">
<div class="layer-group-header" onclick="window.toggleGeoJsonMenu()" style="cursor:pointer; display:flex; justify-content:space-between; align-items:center; font-size:14px; margin-bottom:8px; font-weight: 500;">
<span><i class="fas fa-folder"></i> GeoJSON Eksternal</span>
<i id="geoJsonToggleIcon" class="fas fa-plus"></i>
</div>
<div id="geoJsonFileList" style="display:none; padding-left:15px; margin-bottom:10px;">
<div id="geoJsonLayersContainer"></div>
<div style="margin-top: 10px; padding-top: 10px; border-top: 1px dashed #ddd;">
<label style="font-size: 12px; display:block; margin-bottom: 5px;">Import GeoJSON Baru:</label>
<input type="file" id="fileGeoJson" accept=".geojson,.json" style="font-size: 12px; max-width: 100%;">
</div>
</div>
</div>
</div>
<!-- Map Container -->
<div id="map"></div>
<!-- Unified Modal -->
<div id="unifiedModal" class="unified-modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalTitle">Judul Form</h3>
<span class="modal-close" onclick="closeModal()">&times;</span>
</div>
<div class="modal-body" id="modalBody">
<!-- Konten dinamis -->
</div>
<div class="modal-footer" id="modalFooter">
<button id="modalSaveBtn" class="btn-save">Simpan</button>
<button class="btn-cancel" onclick="closeModal()">Batal</button>
</div>
</div>
</div>
<!-- Confirm Modal -->
<div id="confirmModal" class="unified-modal">
<div class="modal-content" style="width: 300px;">
<div class="modal-header">
<h3>Konfirmasi</h3>
</div>
<div class="modal-body">
<p id="confirmMessage"></p>
</div>
<div class="modal-footer" style="display: flex; gap: 10px; justify-content: flex-end;">
<button id="confirmYesBtn" class="btn-save" style="background-color: #dc3545;">Ya, Hapus</button>
<button class="btn-cancel" onclick="closeConfirmModal()">Batal</button>
</div>
</div>
</div>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Leaflet Draw JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<!-- Leaflet TextPath JS (untuk teks diagonal) -->
<script src="https://cdn.jsdelivr.net/npm/leaflet-textpath@1.2.3/leaflet.textpath.min.js"></script>
<!-- Main Map Initialization -->
<script src="assets/js/map.js"></script>
<!-- Fitur & Komponen -->
<script src="assets/js/features/spbu.js"></script>
<script src="assets/js/features/jalan.js"></script>
<script src="assets/js/features/parsil.js"></script>
<script src="assets/js/features/draw_control.js"></script>
<script src="assets/js/features/geolocation.js"></script>
<script src="assets/js/features/kemiskinan.js"></script>
<script src="assets/js/features/geojson.js"></script>
<!-- (Akan ditambahkan sesuai tahapan) -->
</body>
</html>