diff --git a/db/spbu.sql b/db/spbu.sql
new file mode 100644
index 0000000..4fb7779
--- /dev/null
+++ b/db/spbu.sql
@@ -0,0 +1,86 @@
+CREATE DATABASE IF NOT EXISTS webgis_spbu;
+
+USE webgis_spbu;
+
+CREATE TABLE IF NOT EXISTS spbu (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ nama VARCHAR(100) NOT NULL,
+ no_spbu VARCHAR(50) NOT NULL,
+ status ENUM('24jam', 'tidak') NOT NULL,
+ latitude DOUBLE NOT NULL,
+ longitude DOUBLE NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS jalan (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ nama VARCHAR(100) NOT NULL,
+ status ENUM('Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten') NOT NULL,
+ panjang DOUBLE NOT NULL,
+ geom LONGTEXT NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS kavling (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ nama VARCHAR(100) NOT NULL,
+ status ENUM('SHM', 'HGB', 'HGU', 'HP') NOT NULL,
+ luas DOUBLE NOT NULL,
+ geom LONGTEXT NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS kemiskinan (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ nama_kepala_keluarga VARCHAR(150) NOT NULL,
+ jumlah_kk INT NOT NULL,
+ alamat TEXT NOT NULL,
+ latitude DOUBLE NOT NULL,
+ longitude DOUBLE NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS masjid (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ nama_masjid VARCHAR(150) NOT NULL,
+ pic_masjid VARCHAR(150) NOT NULL,
+ alamat TEXT NOT NULL,
+ latitude DOUBLE NOT NULL,
+ longitude DOUBLE NOT NULL,
+ radius DOUBLE NOT NULL DEFAULT 500,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+ALTER TABLE spbu
+ MODIFY COLUMN nama VARCHAR(100) NOT NULL,
+ MODIFY COLUMN no_spbu VARCHAR(50) NOT NULL,
+ MODIFY COLUMN status ENUM('24jam', 'tidak') NOT NULL,
+ MODIFY COLUMN latitude DOUBLE NOT NULL,
+ MODIFY COLUMN longitude DOUBLE NOT NULL;
+
+ALTER TABLE jalan
+ MODIFY COLUMN nama VARCHAR(100) NOT NULL,
+ MODIFY COLUMN status ENUM('Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten') NOT NULL,
+ MODIFY COLUMN panjang DOUBLE NOT NULL,
+ MODIFY COLUMN geom LONGTEXT NOT NULL;
+
+ALTER TABLE kavling
+ MODIFY COLUMN nama VARCHAR(100) NOT NULL,
+ MODIFY COLUMN status ENUM('SHM', 'HGB', 'HGU', 'HP') NOT NULL,
+ MODIFY COLUMN luas DOUBLE NOT NULL,
+ MODIFY COLUMN geom LONGTEXT NOT NULL;
+
+ALTER TABLE kemiskinan
+ MODIFY COLUMN nama_kepala_keluarga VARCHAR(150) NOT NULL,
+ MODIFY COLUMN jumlah_kk INT NOT NULL,
+ MODIFY COLUMN alamat TEXT NOT NULL,
+ MODIFY COLUMN latitude DOUBLE NOT NULL,
+ MODIFY COLUMN longitude DOUBLE NOT NULL;
+
+ALTER TABLE masjid
+ MODIFY COLUMN nama_masjid VARCHAR(150) NOT NULL,
+ MODIFY COLUMN pic_masjid VARCHAR(150) NOT NULL,
+ MODIFY COLUMN alamat TEXT NOT NULL,
+ MODIFY COLUMN latitude DOUBLE NOT NULL,
+ MODIFY COLUMN longitude DOUBLE NOT NULL,
+ MODIFY COLUMN radius DOUBLE NOT NULL DEFAULT 500;
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..88e9ac1
--- /dev/null
+++ b/index.html
@@ -0,0 +1,934 @@
+
+
+
+ WebGIS SPBU
+
+
+
+
+
+
+
+
+
+
+
+
+
+
□
+
+
Admin WebGIS SPBU
+
Mode view aktif. Pindah ke edit jika ingin menambah atau menghapus data.
+
+
+
+ Admin SPBU
+
+
+
+
+
+
+
+
+
+
+
Statistik Wilayah
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/line.js b/js/line.js
new file mode 100644
index 0000000..0f50f27
--- /dev/null
+++ b/js/line.js
@@ -0,0 +1,93 @@
+window.getRoadColor = function (status) {
+ if (status === "Jalan Nasional") {
+ return "#dc3545";
+ }
+
+ if (status === "Jalan Provinsi") {
+ return "#007bff";
+ }
+
+ return "#28a745";
+};
+
+window.normalizeRoadStatus = function (value) {
+ var normalized = String(value || "").trim().toLowerCase();
+
+ if (normalized === "jalan nasional" || normalized === "nasional") {
+ return "Jalan Nasional";
+ }
+
+ if (normalized === "jalan provinsi" || normalized === "provinsi") {
+ return "Jalan Provinsi";
+ }
+
+ if (normalized === "jalan kabupaten" || normalized === "kabupaten") {
+ return "Jalan Kabupaten";
+ }
+
+ return "";
+};
+
+window.serializeGeometry = function (latlngs) {
+ return JSON.stringify(latlngs.map(function (point) {
+ return [
+ Number(point.lat.toFixed(6)),
+ Number(point.lng.toFixed(6))
+ ];
+ }));
+};
+
+window.parseGeometry = function (geom) {
+ try {
+ var parsed = JSON.parse(geom);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch (error) {
+ return [];
+ }
+};
+
+window.collectRoadData = async function () {
+ return window.openEditModal({
+ title: "Tambah Jalan",
+ subtitle: "Lengkapi atribut jalan untuk garis yang sudah digambar.",
+ fields: [
+ { name: "nama", label: "Nama Jalan", value: "", required: true },
+ {
+ name: "status",
+ label: "Status Jalan",
+ type: "select",
+ value: "Jalan Nasional",
+ options: [
+ { value: "Jalan Nasional", label: "Jalan Nasional" },
+ { value: "Jalan Provinsi", label: "Jalan Provinsi" },
+ { value: "Jalan Kabupaten", label: "Jalan Kabupaten" }
+ ]
+ }
+ ],
+ validate: function (data) {
+ if (!data.nama || !window.normalizeRoadStatus(data.status)) {
+ return "Nama dan status jalan wajib valid.";
+ }
+ return "";
+ }
+ });
+};
+
+window.buildRoadLayer = function (jalan) {
+ var coordinates = window.parseGeometry(jalan.geom).map(function (point) {
+ return L.latLng(point[0], point[1]);
+ });
+
+ if (coordinates.length < 2) {
+ return null;
+ }
+
+ return L.polyline(coordinates, {
+ color: window.getRoadColor(jalan.status),
+ weight: 4
+ }).bindPopup(
+ "" + window.escapeHtml(jalan.nama) + " " +
+ "Status: " + window.escapeHtml(jalan.status) + " " +
+ "Panjang: " + window.formatMeters(jalan.panjang)
+ );
+};
diff --git a/js/loadData.js b/js/loadData.js
new file mode 100644
index 0000000..1801a6e
--- /dev/null
+++ b/js/loadData.js
@@ -0,0 +1,1007 @@
+window.fetchJson = async function (url, fallbackValue) {
+ var response = await fetch(url, {
+ cache: "no-store"
+ });
+ var text = await response.text();
+
+ if (!response.ok) {
+ if (fallbackValue !== undefined) {
+ console.error("Gagal memuat " + url + ": HTTP " + response.status);
+ return fallbackValue;
+ }
+
+ throw new Error("HTTP " + response.status + " saat memuat " + url);
+ }
+
+ if (!text.trim()) {
+ if (fallbackValue !== undefined) {
+ console.error("Respons kosong saat memuat " + url);
+ return fallbackValue;
+ }
+
+ throw new Error("Respons kosong saat memuat " + url);
+ }
+
+ try {
+ return JSON.parse(text);
+ } catch (error) {
+ if (fallbackValue !== undefined) {
+ console.error("Gagal parse JSON dari " + url + ":", error, text);
+ return fallbackValue;
+ }
+
+ throw error;
+ }
+};
+
+window.isValidCoordinate = function (latitude, longitude) {
+ return Number.isFinite(Number(latitude)) && Number.isFinite(Number(longitude));
+};
+
+window.isValidRadius = function (radius) {
+ return Number.isFinite(Number(radius)) && Number(radius) >= 100 && Number(radius) <= 5000;
+};
+
+window.deleteRecord = async function (url, id, label) {
+ if (window.appState.mode !== "edit") {
+ alert("Pindah ke mode edit untuk menghapus data.");
+ return;
+ }
+
+ var confirmed = window.confirm("Hapus data " + label + " ini?");
+ if (!confirmed) {
+ return;
+ }
+
+ try {
+ await window.postForm(url, { id: id });
+ await window.loadAllData();
+ alert("Data berhasil dihapus.");
+ } catch (error) {
+ alert("Gagal menghapus data: " + error.message);
+ }
+};
+
+window.updateMasjidRadius = async function (masjid) {
+ if (window.appState.mode !== "edit") {
+ alert("Pindah ke mode edit untuk mengubah radius masjid.");
+ return;
+ }
+
+ var radiusInput = window.prompt("Masukkan radius masjid (100 - 5000 meter):", String(Math.round(masjid.radius)));
+ if (radiusInput === null) {
+ return;
+ }
+
+ var radius = parseInt(radiusInput.trim(), 10);
+ if (!Number.isInteger(radius) || radius < 100 || radius > 5000) {
+ alert("Radius harus berupa angka antara 100 sampai 5000 meter.");
+ return;
+ }
+
+ try {
+ await window.postForm("php/update_masjid_radius.php", {
+ id: masjid.id,
+ radius: radius
+ });
+ await window.loadAllData();
+ alert("Radius masjid berhasil diperbarui.");
+ } catch (error) {
+ alert("Gagal memperbarui radius masjid: " + error.message);
+ }
+};
+
+window.openEditModal = function (config) {
+ return new Promise(function (resolve) {
+ var backdrop = document.getElementById("editModalBackdrop");
+ var form = document.getElementById("editModalForm");
+ var title = document.getElementById("editModalTitle");
+ var subtitle = document.getElementById("editModalSubtitle");
+ var fieldsWrap = document.getElementById("editModalFields");
+ var errorBox = document.getElementById("editModalError");
+ var closeButton = document.getElementById("editModalClose");
+ var cancelButton = document.getElementById("editModalCancel");
+
+ if (!backdrop || !form || !title || !subtitle || !fieldsWrap || !errorBox || !closeButton || !cancelButton) {
+ resolve(null);
+ return;
+ }
+
+ var isResolved = false;
+
+ var cleanup = function (value) {
+ if (isResolved) {
+ return;
+ }
+
+ isResolved = true;
+ form.onsubmit = null;
+ closeButton.onclick = null;
+ cancelButton.onclick = null;
+ backdrop.onclick = null;
+ document.removeEventListener("keydown", onKeydown);
+ backdrop.classList.remove("is-open");
+ backdrop.setAttribute("aria-hidden", "true");
+ resolve(value);
+ };
+
+ var onKeydown = function (event) {
+ if (event.key === "Escape") {
+ cleanup(null);
+ }
+ };
+
+ title.textContent = config.title;
+ subtitle.textContent = config.subtitle || "Perbarui atribut data terpilih.";
+ fieldsWrap.innerHTML = "";
+ errorBox.textContent = "";
+ errorBox.classList.remove("is-visible");
+
+ config.fields.forEach(function (field) {
+ var fieldWrap = document.createElement("div");
+ fieldWrap.className = "form-field";
+
+ var label = document.createElement("label");
+ label.setAttribute("for", "edit-field-" + field.name);
+ label.textContent = field.label;
+ fieldWrap.appendChild(label);
+
+ var input;
+ if (field.type === "select") {
+ input = document.createElement("select");
+ field.options.forEach(function (optionConfig) {
+ var option = document.createElement("option");
+ option.value = optionConfig.value;
+ option.textContent = optionConfig.label;
+ input.appendChild(option);
+ });
+ } else if (field.type === "textarea") {
+ input = document.createElement("textarea");
+ } else {
+ input = document.createElement("input");
+ input.type = field.type || "text";
+ }
+
+ input.id = "edit-field-" + field.name;
+ input.name = field.name;
+ input.value = field.value ?? "";
+ if (field.required) {
+ input.required = true;
+ }
+ if (field.min !== undefined) {
+ input.min = field.min;
+ }
+ if (field.max !== undefined) {
+ input.max = field.max;
+ }
+ if (field.step !== undefined) {
+ input.step = field.step;
+ }
+
+ fieldWrap.appendChild(input);
+ fieldsWrap.appendChild(fieldWrap);
+ });
+
+ var showError = function (message) {
+ errorBox.textContent = message;
+ errorBox.classList.add("is-visible");
+ };
+
+ form.onsubmit = function (event) {
+ event.preventDefault();
+
+ var values = {};
+ config.fields.forEach(function (field) {
+ var input = form.elements[field.name];
+ values[field.name] = field.type === "number"
+ ? Number(input.value)
+ : String(input.value).trim();
+ });
+
+ var validationError = typeof config.validate === "function" ? config.validate(values) : "";
+ if (validationError) {
+ showError(validationError);
+ return;
+ }
+
+ cleanup(values);
+ };
+
+ closeButton.onclick = function () {
+ cleanup(null);
+ };
+ cancelButton.onclick = function () {
+ cleanup(null);
+ };
+ backdrop.onclick = function (event) {
+ if (event.target === backdrop) {
+ cleanup(null);
+ }
+ };
+ document.addEventListener("keydown", onKeydown);
+
+ backdrop.classList.add("is-open");
+ backdrop.setAttribute("aria-hidden", "false");
+
+ var firstInput = fieldsWrap.querySelector("input, select, textarea");
+ if (firstInput) {
+ firstInput.focus();
+ if (typeof firstInput.select === "function" && firstInput.tagName !== "SELECT") {
+ firstInput.select();
+ }
+ }
+ });
+};
+
+window.editSpbu = async function (spbu) {
+ if (window.appState.mode !== "edit") {
+ alert("Pindah ke mode edit untuk mengubah data.");
+ return;
+ }
+
+ var values = await window.openEditModal({
+ title: "Edit SPBU",
+ subtitle: "Ubah nama, nomor, dan status operasional SPBU.",
+ fields: [
+ { name: "nama", label: "Nama SPBU", value: spbu.nama, required: true },
+ { name: "no", label: "Nomor SPBU", value: spbu.no_spbu, required: true },
+ {
+ name: "status",
+ label: "Status Operasional",
+ type: "select",
+ value: spbu.status,
+ options: [
+ { value: "24jam", label: "24 Jam" },
+ { value: "tidak", label: "Tidak 24 Jam" }
+ ]
+ }
+ ],
+ validate: function (data) {
+ if (!data.nama || !data.no) {
+ return "Nama dan nomor SPBU wajib diisi.";
+ }
+ return "";
+ }
+ });
+
+ if (!values) {
+ return;
+ }
+
+ try {
+ await window.postForm("php/update.php", {
+ id: spbu.id,
+ nama: values.nama,
+ no: values.no,
+ status: values.status
+ });
+ await window.loadAllData();
+ alert("Data SPBU berhasil diperbarui.");
+ } catch (error) {
+ alert("Gagal memperbarui SPBU: " + error.message);
+ }
+};
+
+window.editKemiskinan = async function (kemiskinan) {
+ if (window.appState.mode !== "edit") {
+ alert("Pindah ke mode edit untuk mengubah data.");
+ return;
+ }
+
+ var values = await window.openEditModal({
+ title: "Edit Data Kemiskinan",
+ subtitle: "Ubah kepala keluarga, jumlah KK, dan alamat.",
+ fields: [
+ { name: "nama_kepala_keluarga", label: "Nama Kepala Keluarga", value: kemiskinan.nama_kepala_keluarga, required: true },
+ { name: "jumlah_kk", label: "Jumlah KK", type: "number", min: 1, step: 1, value: kemiskinan.jumlah_kk, required: true },
+ { name: "alamat", label: "Alamat", type: "textarea", value: kemiskinan.alamat, required: true }
+ ],
+ validate: function (data) {
+ if (!data.nama_kepala_keluarga || !data.alamat) {
+ return "Nama kepala keluarga dan alamat wajib diisi.";
+ }
+ if (!Number.isInteger(data.jumlah_kk) || data.jumlah_kk < 1) {
+ return "Jumlah KK harus angka minimal 1.";
+ }
+ return "";
+ }
+ });
+
+ if (!values) {
+ return;
+ }
+
+ try {
+ await window.postForm("php/update_kemiskinan.php", {
+ id: kemiskinan.id,
+ nama_kepala_keluarga: values.nama_kepala_keluarga,
+ jumlah_kk: values.jumlah_kk,
+ alamat: values.alamat
+ });
+ await window.loadAllData();
+ alert("Data kemiskinan berhasil diperbarui.");
+ } catch (error) {
+ alert("Gagal memperbarui data kemiskinan: " + error.message);
+ }
+};
+
+window.editMasjid = async function (masjid) {
+ if (window.appState.mode !== "edit") {
+ alert("Pindah ke mode edit untuk mengubah data.");
+ return;
+ }
+
+ var values = await window.openEditModal({
+ title: "Edit Masjid",
+ subtitle: "Ubah identitas masjid, alamat, dan radius jangkauan.",
+ fields: [
+ { name: "nama_masjid", label: "Nama Masjid", value: masjid.nama_masjid, required: true },
+ { name: "pic_masjid", label: "PIC Masjid", value: masjid.pic_masjid, required: true },
+ { name: "alamat", label: "Alamat Masjid", type: "textarea", value: masjid.alamat, required: true },
+ { name: "radius", label: "Radius Jangkauan (meter)", type: "number", min: 100, max: 5000, step: 1, value: Math.round(masjid.radius), required: true }
+ ],
+ validate: function (data) {
+ if (!data.nama_masjid || !data.pic_masjid || !data.alamat) {
+ return "Nama, PIC, dan alamat masjid wajib diisi.";
+ }
+ if (!Number.isInteger(data.radius) || data.radius < 100 || data.radius > 5000) {
+ return "Radius harus angka antara 100 sampai 5000 meter.";
+ }
+ return "";
+ }
+ });
+
+ if (!values) {
+ return;
+ }
+
+ try {
+ await window.postForm("php/update_masjid.php", {
+ id: masjid.id,
+ nama_masjid: values.nama_masjid,
+ pic_masjid: values.pic_masjid,
+ alamat: values.alamat,
+ radius: values.radius
+ });
+ await window.loadAllData();
+ alert("Data masjid berhasil diperbarui.");
+ } catch (error) {
+ alert("Gagal memperbarui data masjid: " + error.message);
+ }
+};
+
+window.editRoad = async function (jalan) {
+ if (window.appState.mode !== "edit") {
+ alert("Pindah ke mode edit untuk mengubah data.");
+ return;
+ }
+
+ var values = await window.openEditModal({
+ title: "Edit Jalan",
+ subtitle: "Ubah nama dan status jalan. Bentuk garis tetap mengikuti geometri yang sudah digambar.",
+ fields: [
+ { name: "nama", label: "Nama Jalan", value: jalan.nama, required: true },
+ {
+ name: "status",
+ label: "Status Jalan",
+ type: "select",
+ value: jalan.status,
+ options: [
+ { value: "Jalan Nasional", label: "Jalan Nasional" },
+ { value: "Jalan Provinsi", label: "Jalan Provinsi" },
+ { value: "Jalan Kabupaten", label: "Jalan Kabupaten" }
+ ]
+ }
+ ],
+ validate: function (data) {
+ if (!data.nama || !window.normalizeRoadStatus(data.status)) {
+ return "Nama dan status jalan wajib valid.";
+ }
+ return "";
+ }
+ });
+
+ if (!values) {
+ return;
+ }
+
+ try {
+ await window.postForm("php/update_line.php", {
+ id: jalan.id,
+ nama: values.nama,
+ status: values.status
+ });
+ await window.loadAllData();
+ alert("Data jalan berhasil diperbarui.");
+ } catch (error) {
+ alert("Gagal memperbarui jalan: " + error.message);
+ }
+};
+
+window.editPolygon = async function (kavling) {
+ if (window.appState.mode !== "edit") {
+ alert("Pindah ke mode edit untuk mengubah data.");
+ return;
+ }
+
+ var values = await window.openEditModal({
+ title: "Edit Kavling",
+ subtitle: "Ubah nama dan status kavling. Bentuk polygon tetap mengikuti geometri yang sudah digambar.",
+ fields: [
+ { name: "nama", label: "Nama Kavling", value: kavling.nama, required: true },
+ {
+ name: "status",
+ label: "Status Kavling",
+ type: "select",
+ value: kavling.status,
+ options: [
+ { value: "SHM", label: "SHM" },
+ { value: "HGB", label: "HGB" },
+ { value: "HGU", label: "HGU" },
+ { value: "HP", label: "HP" }
+ ]
+ }
+ ],
+ validate: function (data) {
+ if (!data.nama || !window.normalizePolygonStatus(data.status)) {
+ return "Nama dan status kavling wajib valid.";
+ }
+ return "";
+ }
+ });
+
+ if (!values) {
+ return;
+ }
+
+ try {
+ await window.postForm("php/update_polygon.php", {
+ id: kavling.id,
+ nama: values.nama,
+ status: values.status
+ });
+ await window.loadAllData();
+ alert("Data kavling berhasil diperbarui.");
+ } catch (error) {
+ alert("Gagal memperbarui kavling: " + error.message);
+ }
+};
+
+window.createSidebarActionButton = function (config) {
+ var button = document.createElement("button");
+ button.className = config.className;
+ button.type = "button";
+ button.textContent = config.label;
+ button.disabled = !!config.disabled;
+ button.onclick = function (event) {
+ event.stopPropagation();
+ config.onClick();
+ };
+ return button;
+};
+
+window.createSidebarCardElement = function (config) {
+ var card = document.createElement("div");
+ card.className = "card";
+ card.onclick = function () {
+ if (window.appState.mode === "edit" && typeof config.onEdit === "function") {
+ config.onEdit();
+ return;
+ }
+
+ config.onFocus();
+ };
+
+ var title = document.createElement("div");
+ title.className = "title";
+ title.textContent = config.title;
+ card.appendChild(title);
+
+ var body = document.createElement("div");
+ body.className = "card-body";
+ body.textContent = config.body;
+ card.appendChild(body);
+
+ var meta = document.createElement("div");
+ meta.className = "meta";
+ meta.textContent = config.meta;
+ card.appendChild(meta);
+
+ var actions = document.createElement("div");
+ actions.className = "card-actions";
+
+ var badgeWrap = document.createElement("div");
+ badgeWrap.className = "meta";
+ var badge = document.createElement("span");
+ badge.className = "badge " + config.badgeClass;
+ badge.textContent = config.badgeText;
+ badgeWrap.appendChild(badge);
+ actions.appendChild(badgeWrap);
+
+ var actionButtons = document.createElement("div");
+ actionButtons.style.display = "flex";
+ actionButtons.style.gap = "6px";
+
+ if (Array.isArray(config.buttons)) {
+ config.buttons.forEach(function (buttonConfig) {
+ actionButtons.appendChild(window.createSidebarActionButton(buttonConfig));
+ });
+ }
+
+ actions.appendChild(actionButtons);
+ card.appendChild(actions);
+ return card;
+};
+
+window.createSectionElement = function (title, items) {
+ var section = document.createElement("div");
+ section.className = "section";
+
+ var heading = document.createElement("h3");
+ var headingText = document.createElement("span");
+ headingText.textContent = title;
+ heading.appendChild(headingText);
+
+ var count = document.createElement("span");
+ count.className = "section-count";
+ count.textContent = items.length;
+ heading.appendChild(count);
+ section.appendChild(heading);
+
+ if (!items.length) {
+ var emptyState = document.createElement("div");
+ emptyState.className = "empty-state";
+ emptyState.textContent = "Belum ada data.";
+ section.appendChild(emptyState);
+ return section;
+ }
+
+ items.forEach(function (item) {
+ section.appendChild(item);
+ });
+
+ return section;
+};
+
+window.getLayerFilterDefinitions = function () {
+ return [
+ { key: "spbu24", label: "SPBU 24 Jam" },
+ { key: "spbuTidak", label: "SPBU Tidak 24 Jam" },
+ { key: "kemiskinan", label: "Kemiskinan" },
+ { key: "masjid", label: "Masjid" },
+ { key: "jalan", label: "Jalan" },
+ { key: "kavling", label: "Kavling" }
+ ];
+};
+
+window.createLayerFilterSection = function () {
+ var section = document.createElement("div");
+ section.className = "panel-section";
+
+ var heading = document.createElement("h3");
+ heading.textContent = "Filter Layer Peta";
+ section.appendChild(heading);
+
+ var filterBox = document.createElement("div");
+ filterBox.className = "filter-box";
+
+ window.getLayerFilterDefinitions().forEach(function (filter) {
+ var row = document.createElement("label");
+ row.className = "filter-item";
+
+ var checkbox = document.createElement("input");
+ checkbox.type = "checkbox";
+ checkbox.dataset.layerKey = filter.key;
+ checkbox.checked = !!appState.layerVisibility[filter.key];
+ checkbox.onchange = function () {
+ window.setLayerVisibility(filter.key, checkbox.checked);
+ };
+
+ var text = document.createElement("span");
+ text.textContent = filter.label;
+
+ row.appendChild(checkbox);
+ row.appendChild(text);
+ filterBox.appendChild(row);
+ });
+
+ section.appendChild(filterBox);
+ return section;
+};
+
+window.refreshLayerFilterUI = function () {
+ var checkboxes = document.querySelectorAll("input[data-layer-key]");
+ checkboxes.forEach(function (checkbox) {
+ var key = checkbox.dataset.layerKey;
+ checkbox.checked = !!appState.layerVisibility[key];
+ });
+};
+
+window.updateDashboardStats = function (spbus, masjid, roads, lands) {
+ var statSpbu = document.getElementById("statSpbu");
+ var statMasjid = document.getElementById("statMasjid");
+ var statJalan = document.getElementById("statJalan");
+ var statKavling = document.getElementById("statKavling");
+
+ if (statSpbu) {
+ statSpbu.textContent = spbus.length;
+ }
+
+ if (statMasjid) {
+ statMasjid.textContent = masjid.length;
+ }
+
+ if (statJalan) {
+ statJalan.textContent = roads.length;
+ }
+
+ if (statKavling) {
+ statKavling.textContent = lands.length;
+ }
+};
+
+window.renderDataPanelHeader = function (sidebar) {
+ var header = document.createElement("div");
+ header.className = "sidebar-header";
+
+ var headerText = document.createElement("div");
+
+ var title = document.createElement("h2");
+ title.className = "sidebar-title";
+ title.textContent = "▰ Manajemen Data";
+ headerText.appendChild(title);
+
+ var subtitle = document.createElement("p");
+ subtitle.className = "sidebar-subtitle";
+ subtitle.textContent = appState.mode === "edit"
+ ? "Mode edit aktif. Klik kartu untuk mengubah data, gunakan Hapus untuk menghapus."
+ : "Mode lihat aktif. Klik kartu untuk fokus ke objek di peta.";
+ headerText.appendChild(subtitle);
+
+ var closeButton = document.createElement("button");
+ closeButton.className = "panel-close";
+ closeButton.type = "button";
+ closeButton.textContent = "×";
+
+ header.appendChild(headerText);
+ header.appendChild(closeButton);
+ sidebar.appendChild(header);
+
+};
+
+window.renderLayerFilters = function () {
+ var target = document.getElementById("layerFilters");
+ if (!target) {
+ return;
+ }
+
+ target.innerHTML = "";
+ target.appendChild(window.createLayerFilterSection());
+};
+
+window.loadAllData = async function () {
+ var sidebar = document.getElementById("sidebar");
+ if (!sidebar || !window.map || !window.appState.layers.spbu24 || !window.appState.layers.spbuTidak || !window.appState.layers.kemiskinan || !window.appState.layers.masjid) {
+ return;
+ }
+
+ sidebar.innerHTML =
+ '';
+
+ appState.layers.spbu24.clearLayers();
+ appState.layers.spbuTidak.clearLayers();
+ appState.layers.kemiskinan.clearLayers();
+ appState.layers.masjid.clearLayers();
+ appState.layers.jalan.clearLayers();
+ appState.layers.kavling.clearLayers();
+
+ try {
+ var results = await Promise.all([
+ window.fetchJson("php/read.php", []),
+ window.fetchJson("php/read_kemiskinan.php", []),
+ window.fetchJson("php/read_masjid.php", []),
+ window.fetchJson("php/read_line.php", []),
+ window.fetchJson("php/read_polygon.php", [])
+ ]);
+ var spbus = results[0];
+ var dataKemiskinan = results[1];
+ var masjid = results[2];
+ var roads = results[3];
+ var lands = results[4];
+
+ sidebar.innerHTML = "";
+ window.renderDataPanelHeader(sidebar);
+ window.renderLayerFilters();
+ window.updateDashboardStats(spbus, masjid, roads, lands);
+
+ sidebar.appendChild(window.renderSpbu(spbus));
+ sidebar.appendChild(window.renderKemiskinan(dataKemiskinan));
+ sidebar.appendChild(window.renderMasjid(masjid));
+ sidebar.appendChild(window.renderRoads(roads));
+ sidebar.appendChild(window.renderPolygons(lands));
+ } catch (error) {
+ sidebar.innerHTML =
+ '' +
+ '' + window.escapeHtml(error.message) + "
";
+ console.error(error);
+ }
+};
+
+window.renderSpbu = function (spbus) {
+ var items = [];
+
+ spbus.forEach(function (spbu) {
+ if (!window.isValidCoordinate(spbu.latitude, spbu.longitude)) {
+ return;
+ }
+
+ var targetGroup = spbu.status === "24jam" ? appState.layers.spbu24 : appState.layers.spbuTidak;
+ var marker = L.marker([spbu.latitude, spbu.longitude], {
+ icon: spbu.status === "24jam" ? greenIcon : redIcon
+ }).addTo(targetGroup).bindPopup(
+ "" + window.escapeHtml(spbu.nama) + " " +
+ "No: " + window.escapeHtml(spbu.no_spbu) + " " +
+ "Status: " + (spbu.status === "24jam" ? "24 Jam" : "Tidak")
+ );
+
+ items.push(
+ window.createSidebarCardElement({
+ title: spbu.nama,
+ body: "No: " + spbu.no_spbu,
+ meta: "Koordinat: " + Number(spbu.latitude).toFixed(6) + ", " + Number(spbu.longitude).toFixed(6),
+ badgeClass: spbu.status === "24jam" ? "green" : "red",
+ badgeText: spbu.status === "24jam" ? "24 Jam" : "Tidak",
+ onFocus: function () {
+ map.setView([spbu.latitude, spbu.longitude], 16);
+ marker.openPopup();
+ },
+ onEdit: function () {
+ window.editSpbu(spbu);
+ },
+ buttons: [
+ {
+ label: "Edit",
+ className: "secondary-btn",
+ disabled: window.appState.mode !== "edit",
+ onClick: function () {
+ window.editSpbu(spbu);
+ }
+ },
+ {
+ label: "Hapus",
+ className: "delete-btn",
+ disabled: window.appState.mode !== "edit",
+ onClick: function () {
+ window.deleteRecord("php/delete.php", spbu.id, "SPBU");
+ }
+ }
+ ]
+ })
+ );
+ });
+
+ return window.createSectionElement("SPBU", items);
+};
+
+window.renderKemiskinan = function (dataKemiskinan) {
+ var items = [];
+
+ dataKemiskinan.forEach(function (kemiskinan) {
+ if (!window.isValidCoordinate(kemiskinan.latitude, kemiskinan.longitude)) {
+ return;
+ }
+
+ var marker = L.marker([kemiskinan.latitude, kemiskinan.longitude], {
+ icon: orangeIcon
+ }).addTo(appState.layers.kemiskinan).bindPopup(
+ "" + window.escapeHtml(kemiskinan.nama_kepala_keluarga) + " " +
+ "Jumlah KK: " + window.escapeHtml(kemiskinan.jumlah_kk) + " " +
+ "Alamat: " + window.escapeHtml(kemiskinan.alamat)
+ );
+
+ items.push(
+ window.createSidebarCardElement({
+ title: kemiskinan.nama_kepala_keluarga,
+ body: "Jumlah KK: " + kemiskinan.jumlah_kk,
+ meta: kemiskinan.alamat,
+ badgeClass: "orange",
+ badgeText: "Kemiskinan",
+ onFocus: function () {
+ map.setView([kemiskinan.latitude, kemiskinan.longitude], 16);
+ marker.openPopup();
+ },
+ onEdit: function () {
+ window.editKemiskinan(kemiskinan);
+ },
+ buttons: [
+ {
+ label: "Edit",
+ className: "secondary-btn",
+ disabled: window.appState.mode !== "edit",
+ onClick: function () {
+ window.editKemiskinan(kemiskinan);
+ }
+ },
+ {
+ label: "Hapus",
+ className: "delete-btn",
+ disabled: window.appState.mode !== "edit",
+ onClick: function () {
+ window.deleteRecord("php/delete_kemiskinan.php", kemiskinan.id, "Kemiskinan");
+ }
+ }
+ ]
+ })
+ );
+ });
+
+ return window.createSectionElement("Kemiskinan", items);
+};
+
+window.renderMasjid = function (masjidList) {
+ var items = [];
+
+ masjidList.forEach(function (masjid) {
+ var radius = Number(masjid.radius);
+ if (!window.isValidCoordinate(masjid.latitude, masjid.longitude) || !window.isValidRadius(radius)) {
+ return;
+ }
+
+ var popupContent =
+ "" + window.escapeHtml(masjid.nama_masjid) + " " +
+ "PIC: " + window.escapeHtml(masjid.pic_masjid) + " " +
+ "Alamat: " + window.escapeHtml(masjid.alamat) + " " +
+ "Radius: " + window.formatMeters(radius);
+
+ var circle = L.circle([masjid.latitude, masjid.longitude], {
+ radius: radius,
+ color: "#007bff",
+ fillColor: "#60a5fa",
+ fillOpacity: 0.15,
+ weight: 2
+ }).addTo(appState.layers.masjid).bindPopup(popupContent);
+
+ var marker = L.marker([masjid.latitude, masjid.longitude], {
+ icon: blueIcon
+ }).addTo(appState.layers.masjid).bindPopup(popupContent);
+
+ items.push(
+ window.createSidebarCardElement({
+ title: masjid.nama_masjid,
+ body: "PIC: " + masjid.pic_masjid,
+ meta: masjid.alamat + " | Radius: " + window.formatMeters(radius),
+ badgeClass: "blue",
+ badgeText: "Masjid",
+ onFocus: function () {
+ map.fitBounds(circle.getBounds(), { padding: [40, 40] });
+ marker.openPopup();
+ },
+ onEdit: function () {
+ window.editMasjid(masjid);
+ },
+ buttons: [
+ {
+ label: "Edit",
+ className: "secondary-btn",
+ disabled: window.appState.mode !== "edit",
+ onClick: function () {
+ window.editMasjid(masjid);
+ }
+ },
+ {
+ label: "Hapus",
+ className: "delete-btn",
+ disabled: window.appState.mode !== "edit",
+ onClick: function () {
+ window.deleteRecord("php/delete_masjid.php", masjid.id, "Masjid");
+ }
+ }
+ ]
+ })
+ );
+ });
+
+ return window.createSectionElement("Masjid", items);
+};
+
+window.renderRoads = function (roads) {
+ var items = [];
+
+ roads.forEach(function (jalan) {
+ var layer = window.buildRoadLayer(jalan);
+ if (!layer) {
+ return;
+ }
+
+ layer.addTo(appState.layers.jalan);
+
+ items.push(
+ window.createSidebarCardElement({
+ title: jalan.nama,
+ body: "Status: " + jalan.status,
+ meta: "Panjang: " + window.formatMeters(jalan.panjang),
+ badgeClass: jalan.status === "Jalan Nasional" ? "red" : (jalan.status === "Jalan Provinsi" ? "blue" : "green"),
+ badgeText: "Jalan",
+ onFocus: function () {
+ map.fitBounds(layer.getBounds(), { padding: [30, 30] });
+ layer.openPopup();
+ },
+ onEdit: function () {
+ window.editRoad(jalan);
+ },
+ buttons: [
+ {
+ label: "Edit",
+ className: "secondary-btn",
+ disabled: window.appState.mode !== "edit",
+ onClick: function () {
+ window.editRoad(jalan);
+ }
+ },
+ {
+ label: "Hapus",
+ className: "delete-btn",
+ disabled: window.appState.mode !== "edit",
+ onClick: function () {
+ window.deleteRecord("php/delete_line.php", jalan.id, "Jalan");
+ }
+ }
+ ]
+ })
+ );
+ });
+
+ return window.createSectionElement("Jalan", items);
+};
+
+window.renderPolygons = function (lands) {
+ var items = [];
+
+ lands.forEach(function (kavling) {
+ var layer = window.buildPolygonLayer(kavling);
+ if (!layer) {
+ return;
+ }
+
+ layer.addTo(appState.layers.kavling);
+
+ items.push(
+ window.createSidebarCardElement({
+ title: kavling.nama,
+ body: "Status: " + kavling.status,
+ meta: "Luas: " + window.formatSquareMeters(kavling.luas),
+ badgeClass: kavling.status === "SHM" ? "orange" : (kavling.status === "HP" ? "gray" : "blue"),
+ badgeText: "Kavling",
+ onFocus: function () {
+ map.fitBounds(layer.getBounds(), { padding: [30, 30] });
+ layer.openPopup();
+ },
+ onEdit: function () {
+ window.editPolygon(kavling);
+ },
+ buttons: [
+ {
+ label: "Edit",
+ className: "secondary-btn",
+ disabled: window.appState.mode !== "edit",
+ onClick: function () {
+ window.editPolygon(kavling);
+ }
+ },
+ {
+ label: "Hapus",
+ className: "delete-btn",
+ disabled: window.appState.mode !== "edit",
+ onClick: function () {
+ window.deleteRecord("php/delete_polygon.php", kavling.id, "Kavling");
+ }
+ }
+ ]
+ })
+ );
+ });
+
+ return window.createSectionElement("Kavling", items);
+};
diff --git a/js/map.js b/js/map.js
new file mode 100644
index 0000000..d03422d
--- /dev/null
+++ b/js/map.js
@@ -0,0 +1,567 @@
+window.appState = {
+ mode: "view",
+ activeType: "spbu",
+ drawControl: null,
+ layers: {},
+ layerControl: null,
+ layerVisibility: {
+ spbu24: true,
+ spbuTidak: true,
+ kemiskinan: true,
+ masjid: true,
+ jalan: true,
+ kavling: true
+ }
+};
+
+window.escapeHtml = function (value) {
+ return String(value ?? "")
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+};
+
+window.createPointIcon = function (color) {
+ return L.divIcon({
+ className: "custom-point-icon",
+ html: ' ',
+ iconSize: [18, 18],
+ iconAnchor: [9, 9],
+ popupAnchor: [0, -12]
+ });
+};
+
+window.greenIcon = window.createPointIcon("#28a745");
+window.redIcon = window.createPointIcon("#dc3545");
+window.orangeIcon = window.createPointIcon("#ff8c00");
+window.blueIcon = window.createPointIcon("#007bff");
+
+window.calculateLineLength = function (latlngs) {
+ var total = 0;
+
+ for (var i = 1; i < latlngs.length; i += 1) {
+ total += map.distance(latlngs[i - 1], latlngs[i]);
+ }
+
+ return total;
+};
+
+window.calculatePolygonArea = function (latlngs) {
+ if (L.GeometryUtil && typeof L.GeometryUtil.geodesicArea === "function") {
+ return L.GeometryUtil.geodesicArea(latlngs);
+ }
+
+ if (!latlngs.length) {
+ return 0;
+ }
+
+ var earthRadius = 6378137;
+ var avgLat = latlngs.reduce(function (sum, point) {
+ return sum + point.lat;
+ }, 0) / latlngs.length;
+ var avgLatRad = avgLat * Math.PI / 180;
+ var projected = latlngs.map(function (point) {
+ return {
+ x: earthRadius * point.lng * Math.PI / 180 * Math.cos(avgLatRad),
+ y: earthRadius * point.lat * Math.PI / 180
+ };
+ });
+ var area = 0;
+
+ for (var i = 0; i < projected.length; i += 1) {
+ var next = projected[(i + 1) % projected.length];
+ area += (projected[i].x * next.y) - (next.x * projected[i].y);
+ }
+
+ return Math.abs(area / 2);
+};
+
+window.formatMeters = function (value) {
+ return Number(value).toLocaleString("id-ID", {
+ maximumFractionDigits: 2
+ }) + " m";
+};
+
+window.formatSquareMeters = function (value) {
+ return Number(value).toLocaleString("id-ID", {
+ maximumFractionDigits: 2
+ }) + " m²";
+};
+
+window.postForm = async function (url, payload) {
+ var response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ body: new URLSearchParams(payload)
+ });
+ var text = (await response.text()).trim();
+
+ if (!response.ok || text !== "success") {
+ throw new Error(text || "Permintaan gagal.");
+ }
+};
+
+window.collectSpbuData = async function () {
+ return window.openEditModal({
+ title: "Tambah SPBU",
+ subtitle: "Lengkapi atribut SPBU untuk titik yang dipilih di peta.",
+ fields: [
+ { name: "nama", label: "Nama SPBU", value: "", required: true },
+ { name: "no", label: "Nomor SPBU", value: "", required: true },
+ {
+ name: "status",
+ label: "Status Operasional",
+ type: "select",
+ value: "24jam",
+ options: [
+ { value: "24jam", label: "24 Jam" },
+ { value: "tidak", label: "Tidak 24 Jam" }
+ ]
+ }
+ ],
+ validate: function (data) {
+ if (!data.nama || !data.no) {
+ return "Nama dan nomor SPBU wajib diisi.";
+ }
+ return "";
+ }
+ });
+};
+
+window.collectKemiskinanData = async function () {
+ return window.openEditModal({
+ title: "Tambah Data Kemiskinan",
+ subtitle: "Lengkapi data keluarga untuk titik yang dipilih di peta.",
+ fields: [
+ { name: "nama_kepala_keluarga", label: "Nama Kepala Keluarga", value: "", required: true },
+ { name: "jumlah_kk", label: "Jumlah KK", type: "number", min: 1, step: 1, value: 1, required: true },
+ { name: "alamat", label: "Alamat", type: "textarea", value: "", required: true }
+ ],
+ validate: function (data) {
+ if (!data.nama_kepala_keluarga || !data.alamat) {
+ return "Nama kepala keluarga dan alamat wajib diisi.";
+ }
+ if (!Number.isInteger(data.jumlah_kk) || data.jumlah_kk < 1) {
+ return "Jumlah KK harus angka minimal 1.";
+ }
+ return "";
+ }
+ });
+};
+
+window.collectMasjidData = async function () {
+ return window.openEditModal({
+ title: "Tambah Masjid",
+ subtitle: "Lengkapi identitas masjid untuk titik yang dipilih di peta.",
+ fields: [
+ { name: "nama_masjid", label: "Nama Masjid", value: "", required: true },
+ { name: "pic_masjid", label: "PIC Masjid", value: "", required: true },
+ { name: "alamat", label: "Alamat Masjid", type: "textarea", value: "", required: true },
+ { name: "radius", label: "Radius Jangkauan (meter)", type: "number", min: 100, max: 5000, step: 1, value: 500, required: true }
+ ],
+ validate: function (data) {
+ if (!data.nama_masjid || !data.pic_masjid || !data.alamat) {
+ return "Nama, PIC, dan alamat masjid wajib diisi.";
+ }
+ if (!Number.isInteger(data.radius) || data.radius < 100 || data.radius > 5000) {
+ return "Radius harus angka antara 100 sampai 5000 meter.";
+ }
+ return "";
+ }
+ });
+};
+
+window.updateMapHint = function () {
+ var hint = document.getElementById("mapHint");
+ if (!hint) {
+ return;
+ }
+
+ if (appState.mode === "view") {
+ hint.textContent = "Mode lihat aktif. Data hanya ditampilkan di peta dan sidebar.";
+ return;
+ }
+
+ if (appState.activeType === "spbu") {
+ hint.textContent = "Mode edit SPBU aktif. Klik peta untuk menambahkan titik SPBU.";
+ return;
+ }
+
+ if (appState.activeType === "kemiskinan") {
+ hint.textContent = "Mode edit Kemiskinan aktif. Klik peta untuk menambahkan titik data kemiskinan.";
+ return;
+ }
+
+ if (appState.activeType === "masjid") {
+ hint.textContent = "Mode edit Masjid aktif. Klik peta untuk menambahkan titik masjid dengan radius awal 500 meter.";
+ return;
+ }
+
+ if (appState.activeType === "jalan") {
+ hint.textContent = "Mode edit Jalan aktif. Gunakan tool draw polyline pada peta.";
+ return;
+ }
+
+ hint.textContent = "Mode edit Kavling aktif. Gunakan tool draw polygon pada peta.";
+};
+
+window.updateToolbarState = function () {
+ var buttonStates = {
+ modeViewBtn: appState.mode === "view",
+ modeEditBtn: appState.mode === "edit",
+ typeSpbuBtn: appState.activeType === "spbu",
+ typeKemiskinanBtn: appState.activeType === "kemiskinan",
+ typeMasjidBtn: appState.activeType === "masjid",
+ typeJalanBtn: appState.activeType === "jalan",
+ typeKavlingBtn: appState.activeType === "kavling"
+ };
+
+ Object.keys(buttonStates).forEach(function (id) {
+ var button = document.getElementById(id);
+ if (button) {
+ button.classList.toggle("active", buttonStates[id]);
+ }
+ });
+};
+
+window.configureDrawControl = function () {
+ if (!window.map) {
+ return;
+ }
+
+ if (appState.drawControl) {
+ map.removeControl(appState.drawControl);
+ appState.drawControl = null;
+ }
+
+ if (appState.mode !== "edit" || appState.activeType === "spbu" || appState.activeType === "kemiskinan" || appState.activeType === "masjid") {
+ return;
+ }
+
+ var drawOptions = {
+ polyline: false,
+ polygon: false,
+ rectangle: false,
+ circle: false,
+ marker: false,
+ circlemarker: false
+ };
+
+ if (appState.activeType === "jalan") {
+ drawOptions.polyline = {
+ shapeOptions: {
+ color: "#dc3545",
+ weight: 4
+ }
+ };
+ }
+
+ if (appState.activeType === "kavling") {
+ drawOptions.polygon = {
+ allowIntersection: false,
+ shapeOptions: {
+ color: "#ff8c00",
+ weight: 3,
+ fillOpacity: 0.35
+ }
+ };
+ }
+
+ appState.drawControl = new L.Control.Draw({
+ edit: false,
+ draw: drawOptions
+ });
+
+ map.addControl(appState.drawControl);
+};
+
+window.setMode = function (mode) {
+ appState.mode = mode;
+ window.updateToolbarState();
+ window.updateMapHint();
+ window.configureDrawControl();
+
+ if (typeof window.loadAllData === "function") {
+ window.loadAllData();
+ }
+};
+
+window.setDataType = function (type) {
+ appState.activeType = type;
+ window.updateToolbarState();
+ window.updateMapHint();
+ window.configureDrawControl();
+};
+
+window.getLayerGroupByKey = function (key) {
+ if (key === "spbu24") {
+ return appState.layers.spbu24;
+ }
+
+ if (key === "spbuTidak") {
+ return appState.layers.spbuTidak;
+ }
+
+ if (key === "kemiskinan") {
+ return appState.layers.kemiskinan;
+ }
+
+ if (key === "masjid") {
+ return appState.layers.masjid;
+ }
+
+ if (key === "jalan") {
+ return appState.layers.jalan;
+ }
+
+ if (key === "kavling") {
+ return appState.layers.kavling;
+ }
+
+ return null;
+};
+
+window.setLayerVisibility = function (key, isVisible) {
+ appState.layerVisibility[key] = !!isVisible;
+
+ var layer = window.getLayerGroupByKey(key);
+ if (!layer || !window.map) {
+ return;
+ }
+
+ if (isVisible) {
+ if (!map.hasLayer(layer)) {
+ map.addLayer(layer);
+ }
+ return;
+ }
+
+ if (map.hasLayer(layer)) {
+ map.removeLayer(layer);
+ }
+};
+
+document.addEventListener("DOMContentLoaded", function () {
+ window.map = L.map("map").setView([-0.0263, 109.3425], 13);
+
+ appState.layers.spbu24 = L.layerGroup().addTo(map);
+ appState.layers.spbuTidak = L.layerGroup().addTo(map);
+ appState.layers.kemiskinan = L.layerGroup().addTo(map);
+ appState.layers.masjid = L.layerGroup().addTo(map);
+ appState.layers.jalan = L.layerGroup().addTo(map);
+ appState.layers.kavling = L.layerGroup().addTo(map);
+
+ L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
+ attribution: "© OpenStreetMap"
+ }).addTo(map);
+
+ appState.layerControl = L.control.layers(null, {
+ "SPBU 24 Jam": appState.layers.spbu24,
+ "SPBU Tidak 24 Jam": appState.layers.spbuTidak,
+ "Kemiskinan": appState.layers.kemiskinan,
+ "Masjid": appState.layers.masjid,
+ "Jalan": appState.layers.jalan,
+ "Kavling": appState.layers.kavling
+ }).addTo(map);
+
+ map.on("overlayadd", function (event) {
+ if (event.layer === appState.layers.spbu24) {
+ appState.layerVisibility.spbu24 = true;
+ } else if (event.layer === appState.layers.spbuTidak) {
+ appState.layerVisibility.spbuTidak = true;
+ } else if (event.layer === appState.layers.kemiskinan) {
+ appState.layerVisibility.kemiskinan = true;
+ } else if (event.layer === appState.layers.masjid) {
+ appState.layerVisibility.masjid = true;
+ } else if (event.layer === appState.layers.jalan) {
+ appState.layerVisibility.jalan = true;
+ } else if (event.layer === appState.layers.kavling) {
+ appState.layerVisibility.kavling = true;
+ }
+
+ if (typeof window.refreshLayerFilterUI === "function") {
+ window.refreshLayerFilterUI();
+ }
+ });
+
+ map.on("overlayremove", function (event) {
+ if (event.layer === appState.layers.spbu24) {
+ appState.layerVisibility.spbu24 = false;
+ } else if (event.layer === appState.layers.spbuTidak) {
+ appState.layerVisibility.spbuTidak = false;
+ } else if (event.layer === appState.layers.kemiskinan) {
+ appState.layerVisibility.kemiskinan = false;
+ } else if (event.layer === appState.layers.masjid) {
+ appState.layerVisibility.masjid = false;
+ } else if (event.layer === appState.layers.jalan) {
+ appState.layerVisibility.jalan = false;
+ } else if (event.layer === appState.layers.kavling) {
+ appState.layerVisibility.kavling = false;
+ }
+
+ if (typeof window.refreshLayerFilterUI === "function") {
+ window.refreshLayerFilterUI();
+ }
+ });
+
+ map.on("click", async function (event) {
+ if (appState.mode !== "edit") {
+ return;
+ }
+
+ if (appState.activeType === "spbu") {
+ var spbuData = await window.collectSpbuData();
+ if (!spbuData) {
+ return;
+ }
+
+ try {
+ await window.postForm("php/create.php", {
+ nama: spbuData.nama,
+ no: spbuData.no,
+ status: spbuData.status,
+ lat: event.latlng.lat.toFixed(6),
+ lng: event.latlng.lng.toFixed(6)
+ });
+
+ await window.loadAllData();
+ alert("Data SPBU berhasil disimpan.");
+ } catch (error) {
+ alert("Gagal menyimpan SPBU: " + error.message);
+ }
+
+ return;
+ }
+
+ if (appState.activeType === "kemiskinan") {
+ var kemiskinanData = await window.collectKemiskinanData();
+ if (!kemiskinanData) {
+ return;
+ }
+
+ try {
+ await window.postForm("php/create_kemiskinan.php", {
+ nama_kepala_keluarga: kemiskinanData.nama_kepala_keluarga,
+ jumlah_kk: kemiskinanData.jumlah_kk,
+ alamat: kemiskinanData.alamat,
+ lat: event.latlng.lat.toFixed(6),
+ lng: event.latlng.lng.toFixed(6)
+ });
+
+ await window.loadAllData();
+ alert("Data kemiskinan berhasil disimpan.");
+ } catch (error) {
+ alert("Gagal menyimpan data kemiskinan: " + error.message);
+ }
+
+ return;
+ }
+
+ if (appState.activeType === "masjid") {
+ var masjidData = await window.collectMasjidData();
+ if (!masjidData) {
+ return;
+ }
+
+ try {
+ await window.postForm("php/create_masjid.php", {
+ nama_masjid: masjidData.nama_masjid,
+ pic_masjid: masjidData.pic_masjid,
+ alamat: masjidData.alamat,
+ radius: masjidData.radius,
+ lat: event.latlng.lat.toFixed(6),
+ lng: event.latlng.lng.toFixed(6)
+ });
+
+ await window.loadAllData();
+ alert("Data masjid berhasil disimpan.");
+ } catch (error) {
+ alert("Gagal menyimpan data masjid: " + error.message);
+ }
+ }
+ });
+
+ map.on(L.Draw.Event.CREATED, async function (event) {
+ var layer = event.layer;
+
+ if (appState.mode !== "edit") {
+ alert("Pindah ke mode edit sebelum menggambar data.");
+ return;
+ }
+
+ if (event.layerType === "polyline") {
+ if (appState.activeType !== "jalan") {
+ alert("Tipe aktif saat ini bukan Jalan.");
+ return;
+ }
+
+ var lineLatLngs = layer.getLatLngs();
+ var panjang = window.calculateLineLength(lineLatLngs);
+ var roadData = await window.collectRoadData();
+
+ if (!roadData) {
+ return;
+ }
+
+ try {
+ await window.postForm("php/create_line.php", {
+ nama: roadData.nama,
+ status: roadData.status,
+ panjang: panjang.toFixed(2),
+ geom: window.serializeGeometry(lineLatLngs)
+ });
+
+ await window.loadAllData();
+ alert("Data jalan berhasil disimpan.");
+ } catch (error) {
+ alert("Gagal menyimpan jalan: " + error.message);
+ }
+
+ return;
+ }
+
+ if (event.layerType === "polygon") {
+ if (appState.activeType !== "kavling") {
+ alert("Tipe aktif saat ini bukan Kavling.");
+ return;
+ }
+
+ var polygonLatLngs = layer.getLatLngs()[0] || [];
+ var luas = window.calculatePolygonArea(polygonLatLngs);
+ var landData = await window.collectPolygonData();
+
+ if (!landData) {
+ return;
+ }
+
+ try {
+ await window.postForm("php/create_polygon.php", {
+ nama: landData.nama,
+ status: landData.status,
+ luas: luas.toFixed(2),
+ geom: window.serializeGeometry(polygonLatLngs)
+ });
+
+ await window.loadAllData();
+ alert("Data kavling berhasil disimpan.");
+ } catch (error) {
+ alert("Gagal menyimpan kavling: " + error.message);
+ }
+ }
+ });
+
+ window.setMode("view");
+ window.setDataType("spbu");
+
+ setTimeout(function () {
+ map.invalidateSize();
+ }, 300);
+
+ if (typeof window.loadAllData === "function") {
+ window.loadAllData();
+ }
+});
diff --git a/js/polygon.js b/js/polygon.js
new file mode 100644
index 0000000..ed8d58d
--- /dev/null
+++ b/js/polygon.js
@@ -0,0 +1,77 @@
+window.getPolygonColor = function (status) {
+ if (status === "SHM") {
+ return "#ff8c00";
+ }
+
+ if (status === "HGB") {
+ return "#6f42c1";
+ }
+
+ if (status === "HGU") {
+ return "#20c997";
+ }
+
+ return "#6c757d";
+};
+
+window.normalizePolygonStatus = function (value) {
+ var normalized = String(value || "").trim().toUpperCase();
+ var allowed = ["SHM", "HGB", "HGU", "HP"];
+
+ if (allowed.indexOf(normalized) === -1) {
+ return "";
+ }
+
+ return normalized;
+};
+
+window.collectPolygonData = async function () {
+ return window.openEditModal({
+ title: "Tambah Kavling",
+ subtitle: "Lengkapi atribut kavling untuk polygon yang sudah digambar.",
+ fields: [
+ { name: "nama", label: "Nama Kavling", value: "", required: true },
+ {
+ name: "status",
+ label: "Status Kavling",
+ type: "select",
+ value: "SHM",
+ options: [
+ { value: "SHM", label: "SHM" },
+ { value: "HGB", label: "HGB" },
+ { value: "HGU", label: "HGU" },
+ { value: "HP", label: "HP" }
+ ]
+ }
+ ],
+ validate: function (data) {
+ if (!data.nama || !window.normalizePolygonStatus(data.status)) {
+ return "Nama dan status kavling wajib valid.";
+ }
+ return "";
+ }
+ });
+};
+
+window.buildPolygonLayer = function (kavling) {
+ var coordinates = window.parseGeometry(kavling.geom).map(function (point) {
+ return L.latLng(point[0], point[1]);
+ });
+
+ if (coordinates.length < 3) {
+ return null;
+ }
+
+ var color = window.getPolygonColor(kavling.status);
+
+ return L.polygon(coordinates, {
+ color: color,
+ fillColor: color,
+ fillOpacity: 0.35,
+ weight: 3
+ }).bindPopup(
+ "" + window.escapeHtml(kavling.nama) + " " +
+ "Status: " + window.escapeHtml(kavling.status) + " " +
+ "Luas: " + window.formatSquareMeters(kavling.luas)
+ );
+};
diff --git a/php/create.php b/php/create.php
new file mode 100644
index 0000000..1a64ed2
--- /dev/null
+++ b/php/create.php
@@ -0,0 +1,38 @@
+getMessage();
+}
+
+mysqli_stmt_close($statement);
+?>
diff --git a/php/create_kemiskinan.php b/php/create_kemiskinan.php
new file mode 100644
index 0000000..b7ccf98
--- /dev/null
+++ b/php/create_kemiskinan.php
@@ -0,0 +1,42 @@
+getMessage();
+}
+
+if ($statement) {
+ mysqli_stmt_close($statement);
+}
+?>
diff --git a/php/create_line.php b/php/create_line.php
new file mode 100644
index 0000000..6d43668
--- /dev/null
+++ b/php/create_line.php
@@ -0,0 +1,36 @@
+getMessage();
+}
+
+mysqli_stmt_close($statement);
+?>
diff --git a/php/create_masjid.php b/php/create_masjid.php
new file mode 100644
index 0000000..e314c58
--- /dev/null
+++ b/php/create_masjid.php
@@ -0,0 +1,43 @@
+ 5000) {
+ http_response_code(400);
+ exit('error');
+}
+
+$statement = null;
+
+try {
+ $statement = mysqli_prepare(
+ $conn,
+ "INSERT INTO masjid (nama_masjid, pic_masjid, alamat, latitude, longitude, radius) VALUES (?, ?, ?, ?, ?, ?)"
+ );
+
+ mysqli_stmt_bind_param($statement, "sssddd", $namaMasjid, $picMasjid, $alamat, $latitude, $longitude, $radius);
+
+ if (mysqli_stmt_execute($statement)) {
+ echo 'success';
+ } else {
+ http_response_code(500);
+ echo 'error';
+ }
+} catch (mysqli_sql_exception $exception) {
+ http_response_code(500);
+ echo 'error: ' . $exception->getMessage();
+}
+
+if ($statement) {
+ mysqli_stmt_close($statement);
+}
+?>
diff --git a/php/create_polygon.php b/php/create_polygon.php
new file mode 100644
index 0000000..5c9df99
--- /dev/null
+++ b/php/create_polygon.php
@@ -0,0 +1,36 @@
+getMessage();
+}
+
+mysqli_stmt_close($statement);
+?>
diff --git a/php/delete.php b/php/delete.php
new file mode 100644
index 0000000..4a4da7c
--- /dev/null
+++ b/php/delete.php
@@ -0,0 +1,28 @@
+getMessage();
+}
+
+mysqli_stmt_close($statement);
+?>
diff --git a/php/delete_kemiskinan.php b/php/delete_kemiskinan.php
new file mode 100644
index 0000000..bb6b8e6
--- /dev/null
+++ b/php/delete_kemiskinan.php
@@ -0,0 +1,28 @@
+getMessage();
+}
+
+mysqli_stmt_close($statement);
+?>
diff --git a/php/delete_line.php b/php/delete_line.php
new file mode 100644
index 0000000..1634224
--- /dev/null
+++ b/php/delete_line.php
@@ -0,0 +1,28 @@
+getMessage();
+}
+
+mysqli_stmt_close($statement);
+?>
diff --git a/php/delete_masjid.php b/php/delete_masjid.php
new file mode 100644
index 0000000..1a87242
--- /dev/null
+++ b/php/delete_masjid.php
@@ -0,0 +1,28 @@
+getMessage();
+}
+
+mysqli_stmt_close($statement);
+?>
diff --git a/php/delete_polygon.php b/php/delete_polygon.php
new file mode 100644
index 0000000..bdb6b35
--- /dev/null
+++ b/php/delete_polygon.php
@@ -0,0 +1,28 @@
+getMessage();
+}
+
+mysqli_stmt_close($statement);
+?>
diff --git a/php/koneksi.php b/php/koneksi.php
new file mode 100644
index 0000000..5c55102
--- /dev/null
+++ b/php/koneksi.php
@@ -0,0 +1,35 @@
+
diff --git a/php/read.php b/php/read.php
new file mode 100644
index 0000000..e3bb016
--- /dev/null
+++ b/php/read.php
@@ -0,0 +1,17 @@
+
diff --git a/php/read_kemiskinan.php b/php/read_kemiskinan.php
new file mode 100644
index 0000000..670a495
--- /dev/null
+++ b/php/read_kemiskinan.php
@@ -0,0 +1,17 @@
+
diff --git a/php/read_line.php b/php/read_line.php
new file mode 100644
index 0000000..f6b68c4
--- /dev/null
+++ b/php/read_line.php
@@ -0,0 +1,17 @@
+
diff --git a/php/read_masjid.php b/php/read_masjid.php
new file mode 100644
index 0000000..4f823ea
--- /dev/null
+++ b/php/read_masjid.php
@@ -0,0 +1,17 @@
+
diff --git a/php/read_polygon.php b/php/read_polygon.php
new file mode 100644
index 0000000..f5db37f
--- /dev/null
+++ b/php/read_polygon.php
@@ -0,0 +1,17 @@
+
diff --git a/php/update.php b/php/update.php
new file mode 100644
index 0000000..eee0e92
--- /dev/null
+++ b/php/update.php
@@ -0,0 +1,36 @@
+getMessage();
+}
+
+mysqli_stmt_close($statement);
+?>
diff --git a/php/update_kemiskinan.php b/php/update_kemiskinan.php
new file mode 100644
index 0000000..2df3df7
--- /dev/null
+++ b/php/update_kemiskinan.php
@@ -0,0 +1,36 @@
+getMessage();
+}
+
+mysqli_stmt_close($statement);
+?>
diff --git a/php/update_line.php b/php/update_line.php
new file mode 100644
index 0000000..b246cba
--- /dev/null
+++ b/php/update_line.php
@@ -0,0 +1,35 @@
+getMessage();
+}
+
+mysqli_stmt_close($statement);
+?>
diff --git a/php/update_masjid.php b/php/update_masjid.php
new file mode 100644
index 0000000..0ad0c13
--- /dev/null
+++ b/php/update_masjid.php
@@ -0,0 +1,37 @@
+ 5000) {
+ http_response_code(400);
+ exit('error');
+}
+
+$statement = mysqli_prepare(
+ $conn,
+ "UPDATE masjid SET nama_masjid = ?, pic_masjid = ?, alamat = ?, radius = ? WHERE id = ?"
+);
+
+mysqli_stmt_bind_param($statement, "sssdi", $namaMasjid, $picMasjid, $alamat, $radius, $id);
+
+try {
+ if (mysqli_stmt_execute($statement)) {
+ echo 'success';
+ } else {
+ http_response_code(500);
+ echo 'error';
+ }
+} catch (mysqli_sql_exception $exception) {
+ http_response_code(500);
+ echo 'error: ' . $exception->getMessage();
+}
+
+mysqli_stmt_close($statement);
+?>
diff --git a/php/update_masjid_radius.php b/php/update_masjid_radius.php
new file mode 100644
index 0000000..2c99126
--- /dev/null
+++ b/php/update_masjid_radius.php
@@ -0,0 +1,30 @@
+ 5000) {
+ http_response_code(400);
+ exit('error');
+}
+
+$statement = mysqli_prepare($conn, "UPDATE masjid SET radius = ? WHERE id = ?");
+mysqli_stmt_bind_param($statement, "di", $radius, $id);
+
+try {
+ if (mysqli_stmt_execute($statement)) {
+ echo 'success';
+ } else {
+ http_response_code(500);
+ echo 'error';
+ }
+} catch (mysqli_sql_exception $exception) {
+ http_response_code(500);
+ echo 'error: ' . $exception->getMessage();
+}
+
+mysqli_stmt_close($statement);
+?>
diff --git a/php/update_polygon.php b/php/update_polygon.php
new file mode 100644
index 0000000..89738ac
--- /dev/null
+++ b/php/update_polygon.php
@@ -0,0 +1,35 @@
+getMessage();
+}
+
+mysqli_stmt_close($statement);
+?>