From 027f1cc26b692fa4128ef782b01bc3a273a23465 Mon Sep 17 00:00:00 2001 From: desztraa Date: Thu, 11 Jun 2026 21:14:16 +0700 Subject: [PATCH] first commit --- db/spbu.sql | 86 +++ index.html | 934 +++++++++++++++++++++++++++++++ js/line.js | 93 ++++ js/loadData.js | 1007 ++++++++++++++++++++++++++++++++++ js/map.js | 567 +++++++++++++++++++ js/polygon.js | 77 +++ php/create.php | 38 ++ php/create_kemiskinan.php | 42 ++ php/create_line.php | 36 ++ php/create_masjid.php | 43 ++ php/create_polygon.php | 36 ++ php/delete.php | 28 + php/delete_kemiskinan.php | 28 + php/delete_line.php | 28 + php/delete_masjid.php | 28 + php/delete_polygon.php | 28 + php/koneksi.php | 35 ++ php/read.php | 17 + php/read_kemiskinan.php | 17 + php/read_line.php | 17 + php/read_masjid.php | 17 + php/read_polygon.php | 17 + php/update.php | 36 ++ php/update_kemiskinan.php | 36 ++ php/update_line.php | 35 ++ php/update_masjid.php | 37 ++ php/update_masjid_radius.php | 30 + php/update_polygon.php | 35 ++ 28 files changed, 3428 insertions(+) create mode 100644 db/spbu.sql create mode 100644 index.html create mode 100644 js/line.js create mode 100644 js/loadData.js create mode 100644 js/map.js create mode 100644 js/polygon.js create mode 100644 php/create.php create mode 100644 php/create_kemiskinan.php create mode 100644 php/create_line.php create mode 100644 php/create_masjid.php create mode 100644 php/create_polygon.php create mode 100644 php/delete.php create mode 100644 php/delete_kemiskinan.php create mode 100644 php/delete_line.php create mode 100644 php/delete_masjid.php create mode 100644 php/delete_polygon.php create mode 100644 php/koneksi.php create mode 100644 php/read.php create mode 100644 php/read_kemiskinan.php create mode 100644 php/read_line.php create mode 100644 php/read_masjid.php create mode 100644 php/read_polygon.php create mode 100644 php/update.php create mode 100644 php/update_kemiskinan.php create mode 100644 php/update_line.php create mode 100644 php/update_masjid.php create mode 100644 php/update_masjid_radius.php create mode 100644 php/update_polygon.php 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 + + + + + + + + + + + + + +
+
+ + + +
+
+

Statistik Wilayah

+
+
+
+
0
+
Total SPBU
+
+
+
0
+
Masjid
+
+
+
0
+
Jalan
+
+
+
0
+
Kavling
+
+
+
+ + +
+ + + + + + + + + + + + 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); +?>