2875 lines
95 KiB
JavaScript
2875 lines
95 KiB
JavaScript
// ================= CONFIG =================
|
|
const INITIAL_CENTER = [-0.0263, 109.3425];
|
|
const INITIAL_ZOOM = 13;
|
|
|
|
// ================= MAP =================
|
|
const map = L.map('map').setView(INITIAL_CENTER, INITIAL_ZOOM);
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 20,
|
|
attribution: '© OpenStreetMap contributors'
|
|
}).addTo(map);
|
|
|
|
// ================= GROUPS =================
|
|
const editableGroup = L.featureGroup().addTo(map); // pending draw layer (unsaved)
|
|
const points24Layer = L.layerGroup().addTo(map);
|
|
const pointsNon24Layer = L.layerGroup().addTo(map);
|
|
const jalanLayer = L.layerGroup().addTo(map);
|
|
const parsilLayer = L.layerGroup().addTo(map);
|
|
const pemukimanLayer = L.layerGroup().addTo(map);
|
|
const rumahIbadahLayer = L.layerGroup().addTo(map);
|
|
const rumahIbadahRadiusLayer = L.layerGroup().addTo(map);
|
|
const sektorLayer = L.layerGroup().addTo(map);
|
|
|
|
// ================= STATE =================
|
|
const state = {
|
|
featureType: null, // point | jalan | parsil
|
|
currentLayer: null, // LatLng | L.Layer
|
|
pendingLayer: null, // L.Layer (polyline/polygon) before saved
|
|
activeDrawer: null,
|
|
pointFilter: 'all', // all | 24 | non24
|
|
pointsData: [],
|
|
pemukimanData: [],
|
|
rumahIbadahData: [],
|
|
counts: { points: 0, jalan: 0, parsil: 0, pemukiman: 0, rumahIbadah: 0, sektor: 0 },
|
|
indices: {
|
|
points: new Map(),
|
|
jalan: new Map(),
|
|
parsil: new Map(),
|
|
pemukiman: new Map(),
|
|
rumahIbadah: new Map(),
|
|
sektor: new Map()
|
|
},
|
|
selected: {
|
|
points: null,
|
|
jalan: null,
|
|
parsil: null,
|
|
pemukiman: null,
|
|
rumahIbadah: null
|
|
},
|
|
editing: null, // { type, id, layer, backup } for Edit Data (modal)
|
|
locationEditing: null // { type, id, layer, backup, attrs, translate, vertexDeleteMode }
|
|
};
|
|
|
|
const warned = new Set();
|
|
function warnOnce(key, message) {
|
|
if (warned.has(key)) return;
|
|
warned.add(key);
|
|
showToast(message, 'error', 6500);
|
|
}
|
|
|
|
// ================= DRAW (kept, but toolbar is hidden via CSS) =================
|
|
const drawControl = new L.Control.Draw({
|
|
draw: {
|
|
polyline: true,
|
|
polygon: true,
|
|
marker: false,
|
|
rectangle: false,
|
|
circle: false,
|
|
circlemarker: false
|
|
},
|
|
edit: { featureGroup: editableGroup }
|
|
});
|
|
map.addControl(drawControl);
|
|
|
|
// ================= DOM HELPERS =================
|
|
const el = {
|
|
modal: document.getElementById('featureModal'),
|
|
modalTitle: document.getElementById('modalTitle'),
|
|
modeInfo: document.getElementById('modeInfo'),
|
|
toast: document.getElementById('toast'),
|
|
countPoints: document.getElementById('countPoints'),
|
|
countJalan: document.getElementById('countJalan'),
|
|
countParsil: document.getElementById('countParsil'),
|
|
countRumahIbadah: document.getElementById('countRumahIbadah'),
|
|
countPemukiman: document.getElementById('countPemukiman'),
|
|
countSektor: document.getElementById('countSektor'),
|
|
togglePoints: document.getElementById('togglePoints'),
|
|
toggleJalan: document.getElementById('toggleJalan'),
|
|
toggleParsil: document.getElementById('toggleParsil'),
|
|
toggleRumahIbadah: document.getElementById('toggleRumahIbadah'),
|
|
togglePemukiman: document.getElementById('togglePemukiman'),
|
|
toggleSektor: document.getElementById('toggleSektor'),
|
|
pointsList: document.getElementById('pointsList'),
|
|
jalanList: document.getElementById('jalanList'),
|
|
parsilList: document.getElementById('parsilList'),
|
|
rumahIbadahList: document.getElementById('rumahIbadahList'),
|
|
pemukimanList: document.getElementById('pemukimanList'),
|
|
sektorList: document.getElementById('sektorList'),
|
|
btnModePoint: document.getElementById('btnModePoint'),
|
|
btnModeJalan: document.getElementById('btnModeJalan'),
|
|
btnModeParsil: document.getElementById('btnModeParsil'),
|
|
btnModeRumahIbadah: document.getElementById('btnModeRumahIbadah'),
|
|
btnModePemukiman: document.getElementById('btnModePemukiman'),
|
|
btnResetMode: document.getElementById('btnResetMode'),
|
|
btnResetView: document.getElementById('btnResetView'),
|
|
btnMyLocation: document.getElementById('btnMyLocation'),
|
|
btnFilter24: document.getElementById('btnFilter24'),
|
|
btnFilterNon24: document.getElementById('btnFilterNon24'),
|
|
editBar: document.getElementById('editBar'),
|
|
editBarTitle: document.getElementById('editBarTitle'),
|
|
btnSaveLocation: document.getElementById('btnSaveLocation'),
|
|
btnCancelLocation: document.getElementById('btnCancelLocation'),
|
|
btnDeleteVertex: document.getElementById('btnDeleteVertex'),
|
|
pointFields: document.getElementById('pointFields'),
|
|
roadFields: document.getElementById('roadFields'),
|
|
parsilFields: document.getElementById('parsilFields'),
|
|
rumahIbadahFields: document.getElementById('rumahIbadahFields'),
|
|
pemukimanFields: document.getElementById('pemukimanFields'),
|
|
featureForm: document.getElementById('featureForm'),
|
|
formNotice: document.getElementById('formNotice'),
|
|
btnSubmitFeature: document.getElementById('btnSubmitFeature'),
|
|
statusJalan: document.getElementById('statusJalan'),
|
|
statusParsil: document.getElementById('statusParsil'),
|
|
namaPoint: document.getElementById('namaPoint'),
|
|
noPoint: document.getElementById('noPoint'),
|
|
deskripsiPoint: document.getElementById('deskripsiPoint'),
|
|
status24Jam: document.getElementById('status24Jam'),
|
|
latPoint: document.getElementById('latPoint'),
|
|
lngPoint: document.getElementById('lngPoint'),
|
|
namaJalan: document.getElementById('namaJalan'),
|
|
|
|
// Rumah Ibadah
|
|
namaRumahIbadah: document.getElementById('namaRumahIbadah'),
|
|
jenisRumahIbadah: document.getElementById('jenisRumahIbadah'),
|
|
kontakRumahIbadah: document.getElementById('kontakRumahIbadah'),
|
|
radiusRumahIbadah: document.getElementById('radiusRumahIbadah'),
|
|
alamatRumahIbadah: document.getElementById('alamatRumahIbadah'),
|
|
latRumahIbadah: document.getElementById('latRumahIbadah'),
|
|
lngRumahIbadah: document.getElementById('lngRumahIbadah'),
|
|
|
|
// Pemukiman Miskin
|
|
kkNamaPemukiman: document.getElementById('kkNamaPemukiman'),
|
|
nikPemukiman: document.getElementById('nikPemukiman'),
|
|
jumlahAnggotaPemukiman: document.getElementById('jumlahAnggotaPemukiman'),
|
|
statusBantuanPemukiman: document.getElementById('statusBantuanPemukiman'),
|
|
jenisBantuanPemukiman: document.getElementById('jenisBantuanPemukiman'),
|
|
tanggalBantuanPemukiman: document.getElementById('tanggalBantuanPemukiman'),
|
|
alamatPemukiman: document.getElementById('alamatPemukiman'),
|
|
kelurahanPemukiman: document.getElementById('kelurahanPemukiman'),
|
|
kecamatanPemukiman: document.getElementById('kecamatanPemukiman'),
|
|
latPemukiman: document.getElementById('latPemukiman'),
|
|
lngPemukiman: document.getElementById('lngPemukiman')
|
|
};
|
|
|
|
let toastTimer = null;
|
|
function showToast(message, type = 'info', timeoutMs = 3500) {
|
|
if (!el.toast) return;
|
|
if (!el.toast.__dismissBound) {
|
|
el.toast.__dismissBound = true;
|
|
el.toast.title = 'Klik untuk menutup';
|
|
el.toast.addEventListener('click', () => {
|
|
el.toast.classList.add('hidden');
|
|
});
|
|
}
|
|
if (toastTimer) window.clearTimeout(toastTimer);
|
|
|
|
el.toast.classList.remove('hidden', 'toast-success', 'toast-error', 'toast-info');
|
|
el.toast.classList.add(`toast-${type}`);
|
|
el.toast.textContent = message;
|
|
|
|
toastTimer = window.setTimeout(() => {
|
|
el.toast.classList.add('hidden');
|
|
}, timeoutMs);
|
|
}
|
|
|
|
function setFormNotice(message, type = 'info') {
|
|
if (!el.formNotice) return;
|
|
el.formNotice.textContent = message || '';
|
|
el.formNotice.classList.remove('hidden');
|
|
el.formNotice.style.borderColor =
|
|
type === 'error' ? '#fecaca' :
|
|
type === 'success' ? '#bbf7d0' :
|
|
'#bfdbfe';
|
|
el.formNotice.style.background =
|
|
type === 'error' ? '#fef2f2' :
|
|
type === 'success' ? '#f0fdf4' :
|
|
'#eff6ff';
|
|
el.formNotice.style.color =
|
|
type === 'error' ? '#991b1b' :
|
|
type === 'success' ? '#166534' :
|
|
'#1d4ed8';
|
|
}
|
|
|
|
function clearFormNotice() {
|
|
if (!el.formNotice) return;
|
|
el.formNotice.textContent = '';
|
|
el.formNotice.classList.add('hidden');
|
|
}
|
|
|
|
function setSubmitting(isSubmitting) {
|
|
if (!el.btnSubmitFeature) return;
|
|
el.btnSubmitFeature.disabled = Boolean(isSubmitting);
|
|
el.btnSubmitFeature.textContent = isSubmitting ? 'Menyimpan...' : 'Simpan Data';
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? '')
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|
|
|
|
function parseApiJson(response) {
|
|
// Support both {success:true} and {status:"success"} styles.
|
|
const success = Boolean(response?.success) || response?.status === 'success';
|
|
const message =
|
|
response?.message ||
|
|
(success ? 'Berhasil' : 'Gagal memproses permintaan');
|
|
return { success, message };
|
|
}
|
|
|
|
async function fetchMaybeJson(url, options) {
|
|
const res = await fetch(url, options);
|
|
const text = await res.text();
|
|
try {
|
|
return { res, json: JSON.parse(text), text };
|
|
} catch {
|
|
return { res, json: null, text };
|
|
}
|
|
}
|
|
|
|
function getJalanColor(status) {
|
|
if (status === 'Nasional') return '#ef4444';
|
|
if (status === 'Provinsi') return '#3b82f6';
|
|
if (status === 'Kabupaten') return '#22c55e';
|
|
return '#64748b';
|
|
}
|
|
|
|
function getParsilColor(status) {
|
|
if (status === 'SHM') return '#f59e0b';
|
|
if (status === 'HGB') return '#8b5cf6';
|
|
if (status === 'HGU') return '#14b8a6';
|
|
if (status === 'HP') return '#94a3b8';
|
|
return '#cbd5e1';
|
|
}
|
|
|
|
function makeSpbuIcon(is24) {
|
|
const dotClass = is24 ? 'spbu-dot spbu-dot--green' : 'spbu-dot spbu-dot--red';
|
|
return L.divIcon({
|
|
className: 'spbu-icon',
|
|
html: `<div class="${dotClass}"></div>`,
|
|
iconSize: [18, 18],
|
|
iconAnchor: [9, 9],
|
|
popupAnchor: [0, -8]
|
|
});
|
|
}
|
|
|
|
function makePemukimanIcon({ isEdit = false, isCovered = false } = {}) {
|
|
// Keep it distinct from SPBU but not too big/flashy.
|
|
const size = isEdit ? 34 : 26;
|
|
const inner = isEdit ? 20 : 16;
|
|
const hitClass = isCovered ? 'pm-hit pm-hit--green' : 'pm-hit pm-hit--red';
|
|
const html = `
|
|
<div class="${hitClass}" style="--pm-size:${size}px;--pm-inner:${inner}px" aria-hidden="true">
|
|
<svg class="pm-svg" viewBox="0 0 24 24" width="${inner}" height="${inner}" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M4 11.5L12 5l8 6.5" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
<path d="M6.5 10.8V20h11V10.8" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
<path d="M10 20v-6h4v6" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
</svg>
|
|
</div>
|
|
`;
|
|
|
|
return L.divIcon({
|
|
className: 'pm-icon',
|
|
html,
|
|
iconSize: [size, size],
|
|
iconAnchor: [size / 2, size / 2],
|
|
popupAnchor: [0, -size / 2]
|
|
});
|
|
}
|
|
|
|
function makeRumahIbadahIcon() {
|
|
// Simple + recognizable icon (not too "perfect").
|
|
const size = 24;
|
|
const html = `
|
|
<div class="ri-hit" aria-hidden="true">
|
|
<svg class="ri-svg" viewBox="0 0 24 24" width="14" height="14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M12 4v16" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
|
|
<path d="M4 12h16" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>
|
|
</div>
|
|
`;
|
|
|
|
return L.divIcon({
|
|
className: 'ri-icon',
|
|
html,
|
|
iconSize: [size, size],
|
|
iconAnchor: [size / 2, size / 2],
|
|
popupAnchor: [0, -size / 2]
|
|
});
|
|
}
|
|
|
|
function computePemukimanCoverage(latlng) {
|
|
if (!latlng || !Array.isArray(state.rumahIbadahData) || state.rumahIbadahData.length === 0) {
|
|
return { covered: null, rumahIbadahId: null, jarakMeter: null };
|
|
}
|
|
|
|
let best = null;
|
|
for (const row of state.rumahIbadahData) {
|
|
const lat = Number(row?.latitude);
|
|
const lng = Number(row?.longitude);
|
|
const radius = Number(row?.radius_meter);
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lng) || !Number.isFinite(radius) || radius <= 0) continue;
|
|
|
|
const d = map.distance(latlng, L.latLng(lat, lng));
|
|
if (d <= radius) {
|
|
if (!best || d < best.jarakMeter) {
|
|
best = { rumahIbadahId: row?.id ?? null, jarakMeter: d };
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!best) return { covered: false, rumahIbadahId: null, jarakMeter: null };
|
|
return { covered: true, rumahIbadahId: best.rumahIbadahId, jarakMeter: best.jarakMeter };
|
|
}
|
|
|
|
|
|
|
|
function updateStats() {
|
|
if (el.countPoints) el.countPoints.textContent = state.counts.points;
|
|
if (el.countJalan) el.countJalan.textContent = state.counts.jalan;
|
|
if (el.countParsil) el.countParsil.textContent = state.counts.parsil;
|
|
if (el.countRumahIbadah) el.countRumahIbadah.textContent = state.counts.rumahIbadah;
|
|
if (el.countPemukiman) el.countPemukiman.textContent = state.counts.pemukiman;
|
|
if (el.countSektor) el.countSektor.textContent = state.counts.sektor;
|
|
|
|
// Robust update for any additional stat cards (e.g., Rumah Ibadah, Pemukiman Miskin)
|
|
try {
|
|
const cards = document.querySelectorAll('.stat-card');
|
|
for (const card of cards) {
|
|
const labelEl = card.querySelector('.stat-label');
|
|
const valueEl = card.querySelector('.stat-value');
|
|
const label = String(labelEl?.textContent ?? '').trim().toLowerCase();
|
|
if (!label || !valueEl) continue;
|
|
|
|
if (label.includes('rumah') && label.includes('ibadah')) {
|
|
valueEl.textContent = String(state.counts.rumahIbadah ?? 0);
|
|
}
|
|
if (label.includes('pemukiman')) {
|
|
valueEl.textContent = String(state.counts.pemukiman ?? 0);
|
|
}
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
function isSpbu24(row) {
|
|
const v = row?.status_24jam;
|
|
if (v === true) return true;
|
|
if (typeof v === 'number') return v === 1;
|
|
|
|
const s = String(v ?? '').trim().toLowerCase();
|
|
if (!s) return false;
|
|
if (s === '1' || s === 'ya' || s === 'yes') return true;
|
|
if (s === '0' || s === 'tidak' || s === 'no') return false;
|
|
|
|
const has24 = s.includes('24');
|
|
const isNeg = s.includes('tidak') || s.includes('non');
|
|
if (has24 && isNeg) return false;
|
|
if (has24) return true;
|
|
return false;
|
|
}
|
|
|
|
function applyPointFilter() {
|
|
const mode = state.pointFilter || 'all';
|
|
if (el.btnFilter24) el.btnFilter24.classList.toggle('is-active', mode === '24');
|
|
if (el.btnFilterNon24) el.btnFilterNon24.classList.toggle('is-active', mode === 'non24');
|
|
|
|
const shouldShow = (row) => {
|
|
if (mode === '24') return isSpbu24(row);
|
|
if (mode === 'non24') return !isSpbu24(row);
|
|
return true;
|
|
};
|
|
|
|
// Map visibility is handled by applyLayerVisibility based on state.pointFilter
|
|
applyLayerVisibility();
|
|
|
|
const filteredRows = Array.isArray(state.pointsData)
|
|
? state.pointsData.filter((row) => shouldShow(row))
|
|
: [];
|
|
renderPointsList(filteredRows);
|
|
}
|
|
|
|
function togglePointFilter(next) {
|
|
state.pointFilter = state.pointFilter === next ? 'all' : next;
|
|
if (el.togglePoints) el.togglePoints.checked = true;
|
|
applyLayerVisibility();
|
|
applyPointFilter();
|
|
|
|
const rows = Array.isArray(state.pointsData) ? state.pointsData : [];
|
|
const n24 = rows.filter((r) => isSpbu24(r)).length;
|
|
const nNon = rows.length - n24;
|
|
if (state.pointFilter === '24') showToast(`Filter: SPBU 24 Jam (${n24})`, 'info', 2200);
|
|
else if (state.pointFilter === 'non24') showToast(`Filter: SPBU Tidak 24 Jam (${nNon})`, 'info', 2200);
|
|
else showToast(`Filter: Semua SPBU (${rows.length})`, 'info', 1800);
|
|
}
|
|
|
|
function applyLayerVisibility() {
|
|
if (el.togglePoints) {
|
|
if (!el.togglePoints.checked) {
|
|
map.removeLayer(points24Layer);
|
|
map.removeLayer(pointsNon24Layer);
|
|
} else {
|
|
const mode = state.pointFilter || 'all';
|
|
if (mode === '24') {
|
|
points24Layer.addTo(map);
|
|
map.removeLayer(pointsNon24Layer);
|
|
} else if (mode === 'non24') {
|
|
pointsNon24Layer.addTo(map);
|
|
map.removeLayer(points24Layer);
|
|
} else {
|
|
points24Layer.addTo(map);
|
|
pointsNon24Layer.addTo(map);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (el.toggleJalan) {
|
|
if (el.toggleJalan.checked) jalanLayer.addTo(map);
|
|
else map.removeLayer(jalanLayer);
|
|
}
|
|
|
|
if (el.toggleParsil) {
|
|
if (el.toggleParsil.checked) parsilLayer.addTo(map);
|
|
else map.removeLayer(parsilLayer);
|
|
}
|
|
|
|
if (el.toggleRumahIbadah) {
|
|
if (el.toggleRumahIbadah.checked) {
|
|
rumahIbadahLayer.addTo(map);
|
|
rumahIbadahRadiusLayer.addTo(map);
|
|
} else {
|
|
map.removeLayer(rumahIbadahLayer);
|
|
map.removeLayer(rumahIbadahRadiusLayer);
|
|
}
|
|
}
|
|
|
|
if (el.togglePemukiman) {
|
|
if (el.togglePemukiman.checked) pemukimanLayer.addTo(map);
|
|
else map.removeLayer(pemukimanLayer);
|
|
}
|
|
|
|
if (el.toggleSektor) {
|
|
if (el.toggleSektor.checked) sektorLayer.addTo(map);
|
|
else map.removeLayer(sektorLayer);
|
|
}
|
|
|
|
updateStats();
|
|
}
|
|
|
|
function setActiveModeButton(type) {
|
|
const buttons = [
|
|
el.btnModePoint,
|
|
el.btnModeJalan,
|
|
el.btnModeParsil,
|
|
el.btnModeRumahIbadah,
|
|
el.btnModePemukiman
|
|
].filter(Boolean);
|
|
for (const btn of buttons) btn.classList.remove('is-active');
|
|
if (type === 'point' && el.btnModePoint) el.btnModePoint.classList.add('is-active');
|
|
if (type === 'jalan' && el.btnModeJalan) el.btnModeJalan.classList.add('is-active');
|
|
if (type === 'parsil' && el.btnModeParsil) el.btnModeParsil.classList.add('is-active');
|
|
if (type === 'rumahIbadah' && el.btnModeRumahIbadah) el.btnModeRumahIbadah.classList.add('is-active');
|
|
if (type === 'pemukiman' && el.btnModePemukiman) el.btnModePemukiman.classList.add('is-active');
|
|
}
|
|
|
|
function updateModeInfo() {
|
|
if (!el.modeInfo) return;
|
|
|
|
if (state.locationEditing?.type === 'point') {
|
|
el.modeInfo.textContent = 'Mode: Edit Lokasi Point (geser marker)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (state.locationEditing?.type === 'jalan') {
|
|
el.modeInfo.textContent = 'Mode: Edit Lokasi Jalan (drag garis / geser vertex)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (state.locationEditing?.type === 'parsil') {
|
|
el.modeInfo.textContent = 'Mode: Edit Lokasi Parsil (drag area / geser vertex)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (state.locationEditing?.type === 'pemukiman') {
|
|
el.modeInfo.textContent = 'Mode: Edit Lokasi Pemukiman Miskin (geser marker)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (state.locationEditing?.type === 'rumahIbadah') {
|
|
el.modeInfo.textContent = 'Mode: Edit Lokasi Rumah Ibadah (geser marker)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (state.editing?.type === 'point') {
|
|
el.modeInfo.textContent = 'Mode: Edit Point (geser marker / ubah atribut)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (state.editing?.type === 'jalan') {
|
|
el.modeInfo.textContent = 'Mode: Edit Jalan (geser titik garis)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (state.editing?.type === 'parsil') {
|
|
el.modeInfo.textContent = 'Mode: Edit Parsil (geser titik poligon)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (state.editing?.type === 'rumahIbadah') {
|
|
el.modeInfo.textContent = 'Mode: Edit Rumah Ibadah (geser marker / ubah atribut)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (state.editing?.type === 'pemukiman') {
|
|
el.modeInfo.textContent = 'Mode: Edit Pemukiman Miskin (geser marker / ubah atribut)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (state.featureType === 'point') {
|
|
el.modeInfo.textContent = 'Mode: Tambah Point (klik peta)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (state.featureType === 'jalan') {
|
|
el.modeInfo.textContent = 'Mode: Tambah Jalan (gambar garis)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (state.featureType === 'parsil') {
|
|
el.modeInfo.textContent = 'Mode: Tambah Parsil (gambar poligon)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (state.featureType === 'rumahIbadah') {
|
|
el.modeInfo.textContent = 'Mode: Tambah Rumah Ibadah (klik peta)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
if (state.featureType === 'pemukiman') {
|
|
el.modeInfo.textContent = 'Mode: Tambah Pemukiman Miskin (klik peta)';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = false;
|
|
return;
|
|
}
|
|
|
|
el.modeInfo.textContent = 'Mode: Tidak aktif';
|
|
if (el.btnResetMode) el.btnResetMode.disabled = true;
|
|
}
|
|
|
|
function clearPendingLayer() {
|
|
if (state.pendingLayer) {
|
|
editableGroup.removeLayer(state.pendingLayer);
|
|
state.pendingLayer = null;
|
|
}
|
|
state.currentLayer = null;
|
|
}
|
|
|
|
function showSection(type) {
|
|
const setEnabled = (section, enabled) => {
|
|
if (!section) return;
|
|
section.querySelectorAll('input, textarea, select, button').forEach((node) => {
|
|
node.disabled = !enabled;
|
|
});
|
|
};
|
|
|
|
el.pointFields?.classList.add('hidden');
|
|
el.roadFields?.classList.add('hidden');
|
|
el.parsilFields?.classList.add('hidden');
|
|
el.rumahIbadahFields?.classList.add('hidden');
|
|
el.pemukimanFields?.classList.add('hidden');
|
|
setEnabled(el.pointFields, false);
|
|
setEnabled(el.roadFields, false);
|
|
setEnabled(el.parsilFields, false);
|
|
setEnabled(el.rumahIbadahFields, false);
|
|
setEnabled(el.pemukimanFields, false);
|
|
|
|
if (type === 'point') {
|
|
el.pointFields?.classList.remove('hidden');
|
|
setEnabled(el.pointFields, true);
|
|
}
|
|
if (type === 'jalan') {
|
|
el.roadFields?.classList.remove('hidden');
|
|
setEnabled(el.roadFields, true);
|
|
}
|
|
if (type === 'parsil') {
|
|
el.parsilFields?.classList.remove('hidden');
|
|
setEnabled(el.parsilFields, true);
|
|
}
|
|
|
|
if (type === 'rumahIbadah') {
|
|
el.rumahIbadahFields?.classList.remove('hidden');
|
|
setEnabled(el.rumahIbadahFields, true);
|
|
}
|
|
|
|
if (type === 'pemukiman') {
|
|
el.pemukimanFields?.classList.remove('hidden');
|
|
setEnabled(el.pemukimanFields, true);
|
|
}
|
|
}
|
|
|
|
function openModal() {
|
|
if (!el.modal) return;
|
|
|
|
if (state.featureType === 'point') el.modalTitle.textContent = 'Input Data Point SPBU';
|
|
if (state.featureType === 'jalan') el.modalTitle.textContent = 'Input Data Jalan';
|
|
if (state.featureType === 'parsil') el.modalTitle.textContent = 'Input Data Parsil';
|
|
if (state.featureType === 'rumahIbadah') el.modalTitle.textContent = 'Input Data Rumah Ibadah';
|
|
if (state.featureType === 'pemukiman') el.modalTitle.textContent = 'Input Data Pemukiman Miskin';
|
|
|
|
clearFormNotice();
|
|
setSubmitting(false);
|
|
if (el.btnSubmitFeature) {
|
|
el.btnSubmitFeature.textContent = state.editing ? 'Update Data' : 'Simpan Data';
|
|
}
|
|
showSection(state.featureType);
|
|
el.modal.classList.remove('hidden');
|
|
|
|
// focus first input of the active section
|
|
if (state.featureType === 'point') el.namaPoint?.focus();
|
|
if (state.featureType === 'jalan') el.namaJalan?.focus();
|
|
if (state.featureType === 'parsil') el.statusParsil?.focus();
|
|
if (state.featureType === 'rumahIbadah') el.namaRumahIbadah?.focus();
|
|
if (state.featureType === 'pemukiman') el.kkNamaPemukiman?.focus();
|
|
}
|
|
|
|
function backupLayerGeometry(layer) {
|
|
if (layer?.getLatLng && layer?.setLatLng) {
|
|
const ll = layer.getLatLng();
|
|
return { kind: 'marker', latlng: [ll.lat, ll.lng] };
|
|
}
|
|
|
|
if (layer?.getLatLngs && layer?.setLatLngs) {
|
|
const toTuple = (latlng) => [latlng.lat, latlng.lng];
|
|
const clone = (latlngs) =>
|
|
Array.isArray(latlngs)
|
|
? latlngs.map((v) => (Array.isArray(v) ? clone(v) : toTuple(v)))
|
|
: latlngs;
|
|
return { kind: 'path', latlngs: clone(layer.getLatLngs()) };
|
|
}
|
|
|
|
return { kind: 'unknown' };
|
|
}
|
|
|
|
function restoreLayerGeometry(layer, backup) {
|
|
if (!layer || !backup) return;
|
|
|
|
if (backup.kind === 'marker' && Array.isArray(backup.latlng)) {
|
|
layer.setLatLng(L.latLng(backup.latlng[0], backup.latlng[1]));
|
|
return;
|
|
}
|
|
|
|
if (backup.kind === 'path' && backup.latlngs && layer.setLatLngs) {
|
|
const toLatLng = (pair) => L.latLng(pair[0], pair[1]);
|
|
const rebuild = (latlngs) =>
|
|
Array.isArray(latlngs)
|
|
? latlngs.map((v) => (Array.isArray(v) && typeof v[0] === 'number' ? toLatLng(v) : rebuild(v)))
|
|
: latlngs;
|
|
layer.setLatLngs(rebuild(backup.latlngs));
|
|
}
|
|
}
|
|
|
|
function enableLayerEditing(layer) {
|
|
if (!layer) return;
|
|
if (layer.dragging && layer.dragging.enable) {
|
|
layer.dragging.enable();
|
|
}
|
|
if (layer.editing && layer.editing.enable) {
|
|
layer.editing.enable();
|
|
}
|
|
}
|
|
|
|
function disableLayerEditing(layer) {
|
|
if (!layer) return;
|
|
if (layer.dragging && layer.dragging.disable) {
|
|
layer.dragging.disable();
|
|
}
|
|
if (layer.editing && layer.editing.disable) {
|
|
layer.editing.disable();
|
|
}
|
|
}
|
|
|
|
function startEditPoint(id, marker, row) {
|
|
resetMode();
|
|
const backup = backupLayerGeometry(marker);
|
|
backup.is24 = Number(row?.status_24jam) === 1;
|
|
state.editing = { type: 'point', id: Number(id), layer: marker, backup };
|
|
state.featureType = 'point';
|
|
state.currentLayer = null;
|
|
|
|
el.namaPoint.value = row?.nama ?? '';
|
|
el.noPoint.value = row?.no ?? '';
|
|
el.deskripsiPoint.value = row?.deskripsi ?? '';
|
|
el.status24Jam.checked = Number(row?.status_24jam) === 1;
|
|
|
|
const ll = marker.getLatLng();
|
|
el.latPoint.value = ll.lat;
|
|
el.lngPoint.value = ll.lng;
|
|
|
|
marker.setIcon(makeSpbuIcon(el.status24Jam.checked));
|
|
if (marker.__spbuDragHandler) {
|
|
marker.off('drag', marker.__spbuDragHandler);
|
|
marker.off('dragend', marker.__spbuDragHandler);
|
|
}
|
|
marker.__spbuDragHandler = () => {
|
|
const current = marker.getLatLng();
|
|
el.latPoint.value = current.lat;
|
|
el.lngPoint.value = current.lng;
|
|
};
|
|
marker.on('drag', marker.__spbuDragHandler);
|
|
marker.on('dragend', marker.__spbuDragHandler);
|
|
|
|
enableLayerEditing(marker);
|
|
updateModeInfo();
|
|
setActiveListItem('points', id);
|
|
setFormNotice('Edit: geser marker jika perlu, lalu klik "Update Data".', 'info');
|
|
openModal();
|
|
}
|
|
|
|
function startEditJalan(id, layer, props) {
|
|
resetMode();
|
|
const backup = backupLayerGeometry(layer);
|
|
backup.status = props?.status_jalan ?? 'Nasional';
|
|
state.editing = { type: 'jalan', id: Number(id), layer, backup };
|
|
state.featureType = 'jalan';
|
|
state.currentLayer = null;
|
|
|
|
el.namaJalan.value = props?.nama_jalan ?? '';
|
|
el.statusJalan.value = props?.status_jalan ?? 'Nasional';
|
|
|
|
enableLayerEditing(layer);
|
|
updateModeInfo();
|
|
setActiveListItem('jalan', id);
|
|
setFormNotice('Edit: geser titik garis, panjang akan dihitung otomatis saat update.', 'info');
|
|
openModal();
|
|
}
|
|
|
|
function startEditParsil(id, layer, props) {
|
|
resetMode();
|
|
const backup = backupLayerGeometry(layer);
|
|
backup.status = props?.status_kepemilikan ?? 'SHM';
|
|
state.editing = { type: 'parsil', id: Number(id), layer, backup };
|
|
state.featureType = 'parsil';
|
|
state.currentLayer = null;
|
|
|
|
el.statusParsil.value = props?.status_kepemilikan ?? 'SHM';
|
|
|
|
enableLayerEditing(layer);
|
|
updateModeInfo();
|
|
setActiveListItem('parsil', id);
|
|
setFormNotice('Edit: geser titik poligon, luas akan dihitung otomatis saat update.', 'info');
|
|
openModal();
|
|
}
|
|
|
|
function startEditRumahIbadah(id, marker, row) {
|
|
resetMode();
|
|
const backup = backupLayerGeometry(marker);
|
|
state.editing = { type: 'rumahIbadah', id: Number(id), layer: marker, backup };
|
|
state.featureType = 'rumahIbadah';
|
|
state.currentLayer = null;
|
|
|
|
el.namaRumahIbadah.value = row?.nama ?? '';
|
|
el.jenisRumahIbadah.value = row?.jenis ?? 'Lainnya';
|
|
el.kontakRumahIbadah.value = row?.kontak ?? '';
|
|
el.radiusRumahIbadah.value = row?.radius_meter ?? '';
|
|
el.alamatRumahIbadah.value = row?.address ?? '';
|
|
|
|
const ll = marker.getLatLng();
|
|
el.latRumahIbadah.value = ll.lat;
|
|
el.lngRumahIbadah.value = ll.lng;
|
|
|
|
if (marker.__riDragHandler) {
|
|
marker.off('drag', marker.__riDragHandler);
|
|
marker.off('dragend', marker.__riDragHandler);
|
|
}
|
|
marker.__riDragHandler = () => {
|
|
const current = marker.getLatLng();
|
|
el.latRumahIbadah.value = current.lat;
|
|
el.lngRumahIbadah.value = current.lng;
|
|
};
|
|
marker.on('drag', marker.__riDragHandler);
|
|
marker.on('dragend', marker.__riDragHandler);
|
|
|
|
enableLayerEditing(marker);
|
|
updateModeInfo();
|
|
setActiveListItem('rumahIbadah', id);
|
|
setFormNotice('Edit: geser marker jika perlu, lalu klik "Update Data".', 'info');
|
|
openModal();
|
|
}
|
|
|
|
function startEditPemukiman(id, marker, row) {
|
|
resetMode();
|
|
const backup = backupLayerGeometry(marker);
|
|
const ibadahId = row?.rumah_ibadah_id;
|
|
backup.covered = ibadahId !== null && ibadahId !== undefined && String(ibadahId).trim() !== '' && String(ibadahId) !== '0';
|
|
state.editing = { type: 'pemukiman', id: Number(id), layer: marker, backup, dragEase: null };
|
|
state.featureType = 'pemukiman';
|
|
state.currentLayer = null;
|
|
|
|
el.kkNamaPemukiman.value = row?.kk_nama ?? row?.nama ?? '';
|
|
el.nikPemukiman.value = row?.nik ?? '';
|
|
el.jumlahAnggotaPemukiman.value = row?.jumlah_anggota ?? '';
|
|
el.statusBantuanPemukiman.value = row?.status_bantuan ?? 'Belum dibantu';
|
|
el.jenisBantuanPemukiman.value = row?.jenis_bantuan ?? '';
|
|
el.tanggalBantuanPemukiman.value = row?.tanggal_bantuan ?? '';
|
|
el.alamatPemukiman.value = row?.address ?? '';
|
|
el.kelurahanPemukiman.value = row?.kelurahan ?? '';
|
|
el.kecamatanPemukiman.value = row?.kecamatan ?? '';
|
|
|
|
const ll = marker.getLatLng();
|
|
el.latPemukiman.value = ll.lat;
|
|
el.lngPemukiman.value = ll.lng;
|
|
|
|
marker.setIcon(makePemukimanIcon({ isEdit: true, isCovered: Boolean(backup.covered) }));
|
|
if (marker.__pmDragHandler) {
|
|
marker.off('drag', marker.__pmDragHandler);
|
|
marker.off('dragend', marker.__pmDragHandler);
|
|
}
|
|
marker.__currentCovered = Boolean(backup.covered);
|
|
marker.__pmDragHandler = () => {
|
|
const current = marker.getLatLng();
|
|
el.latPemukiman.value = current.lat;
|
|
el.lngPemukiman.value = current.lng;
|
|
|
|
const cov = computePemukimanCoverage(current);
|
|
if (cov.covered !== null && marker.__currentCovered !== cov.covered) {
|
|
marker.__currentCovered = cov.covered;
|
|
marker.setIcon(makePemukimanIcon({ isEdit: true, isCovered: cov.covered === true }));
|
|
}
|
|
};
|
|
marker.on('drag', marker.__pmDragHandler);
|
|
marker.on('dragend', marker.__pmDragHandler);
|
|
|
|
enableLayerEditing(marker);
|
|
updateModeInfo();
|
|
setActiveListItem('pemukiman', id);
|
|
setFormNotice('Edit: geser marker jika perlu, lalu klik "Update Data".', 'info');
|
|
openModal();
|
|
}
|
|
|
|
async function deleteFeature(type, id) {
|
|
const label =
|
|
type === 'point' ? 'point SPBU' :
|
|
type === 'jalan' ? 'jalan' :
|
|
type === 'parsil' ? 'parsil' :
|
|
type === 'rumahIbadah' ? 'rumah ibadah' :
|
|
'pemukiman miskin';
|
|
const ok = window.confirm(`Hapus data ${label} ini?`);
|
|
if (!ok) return;
|
|
|
|
const url =
|
|
type === 'point' ? 'php/delete_point.php' :
|
|
type === 'jalan' ? 'php/delete_jalan.php' :
|
|
type === 'parsil' ? 'php/delete_parsil.php' :
|
|
type === 'rumahIbadah' ? 'php/delete_rumah_ibadah.php' :
|
|
'php/delete_pemukiman_miskin.php';
|
|
|
|
try {
|
|
const { res, json, text } = await fetchMaybeJson(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id: Number(id) })
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat hapus data.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
|
|
showToast('Data berhasil dihapus.', 'success');
|
|
if (type === 'point') await loadPoints();
|
|
if (type === 'jalan') await loadJalan();
|
|
if (type === 'parsil') await loadParsil();
|
|
if (type === 'rumahIbadah') {
|
|
await loadPemukimanMiskin();
|
|
await loadRumahIbadah();
|
|
}
|
|
if (type === 'pemukiman') {
|
|
await loadPemukimanMiskin();
|
|
await loadRumahIbadah();
|
|
}
|
|
} catch (e) {
|
|
showToast(e?.message || String(e), 'error', 5000);
|
|
}
|
|
}
|
|
|
|
function closeModal({ keepMode = false } = {}) {
|
|
if (!el.modal) return;
|
|
|
|
// Cancel edit: restore geometry
|
|
if (state.editing?.layer && state.editing?.backup) {
|
|
const { type, layer, backup } = state.editing;
|
|
restoreLayerGeometry(layer, backup);
|
|
|
|
if (type === 'point' && layer.setIcon && typeof backup?.is24 === 'boolean') {
|
|
layer.setIcon(makeSpbuIcon(backup.is24));
|
|
}
|
|
|
|
if (type === 'jalan' && layer.setStyle && backup?.status) {
|
|
layer.setStyle({ color: getJalanColor(backup.status), weight: 4, opacity: 0.95 });
|
|
}
|
|
|
|
if (type === 'parsil' && layer.setStyle && backup?.status) {
|
|
layer.setStyle({ color: '#0f172a', weight: 2, fillColor: getParsilColor(backup.status), fillOpacity: 0.35 });
|
|
}
|
|
|
|
if (type === 'pemukiman' && layer.setIcon) {
|
|
layer.setIcon(makePemukimanIcon({ isCovered: Boolean(backup?.covered) }));
|
|
if (layer.__pmDragHandler) {
|
|
layer.off('drag', layer.__pmDragHandler);
|
|
layer.off('dragend', layer.__pmDragHandler);
|
|
layer.__pmDragHandler = null;
|
|
}
|
|
if (state.editing?.dragEase?.disable) state.editing.dragEase.disable();
|
|
}
|
|
|
|
if (type === 'rumahIbadah' && layer.setIcon) {
|
|
layer.setIcon(makeRumahIbadahIcon());
|
|
if (layer.__riDragHandler) {
|
|
layer.off('drag', layer.__riDragHandler);
|
|
layer.off('dragend', layer.__riDragHandler);
|
|
layer.__riDragHandler = null;
|
|
}
|
|
}
|
|
|
|
disableLayerEditing(layer);
|
|
state.editing = null;
|
|
}
|
|
|
|
el.modal.classList.add('hidden');
|
|
el.featureForm?.reset();
|
|
clearFormNotice();
|
|
setSubmitting(false);
|
|
clearPendingLayer();
|
|
|
|
if (!keepMode) resetMode({ skipModalClose: true });
|
|
}
|
|
|
|
function resetMode({ skipModalClose = false } = {}) {
|
|
if (!skipModalClose && el.modal && !el.modal.classList.contains('hidden')) {
|
|
// Close modal first, but don't recurse into resetMode again.
|
|
closeModal({ keepMode: true });
|
|
}
|
|
|
|
if (state.locationEditing) {
|
|
cancelLocationEdit({ silent: true });
|
|
}
|
|
|
|
if (state.activeDrawer) {
|
|
state.activeDrawer.disable();
|
|
state.activeDrawer = null;
|
|
}
|
|
|
|
state.featureType = null;
|
|
document.body.classList.remove('crosshair');
|
|
setActiveModeButton(null);
|
|
updateModeInfo();
|
|
}
|
|
|
|
function setMode(type) {
|
|
resetMode();
|
|
state.editing = null;
|
|
state.locationEditing = null;
|
|
state.featureType = type;
|
|
setActiveModeButton(type);
|
|
updateModeInfo();
|
|
|
|
if (type === 'point' || type === 'rumahIbadah' || type === 'pemukiman') {
|
|
document.body.classList.add('crosshair');
|
|
return;
|
|
}
|
|
|
|
if (type === 'jalan') {
|
|
state.activeDrawer = new L.Draw.Polyline(map, {
|
|
shapeOptions: { color: getJalanColor(el.statusJalan?.value), weight: 4, opacity: 0.95 }
|
|
});
|
|
state.activeDrawer.enable();
|
|
return;
|
|
}
|
|
|
|
if (type === 'parsil') {
|
|
state.activeDrawer = new L.Draw.Polygon(map, {
|
|
allowIntersection: false,
|
|
showArea: true,
|
|
shapeOptions: { color: '#0f172a', weight: 2, fillColor: getParsilColor(el.statusParsil?.value), fillOpacity: 0.35 }
|
|
});
|
|
state.activeDrawer.enable();
|
|
}
|
|
}
|
|
|
|
// expose for inline onclick
|
|
window.setMode = setMode;
|
|
window.resetMode = resetMode;
|
|
window.closeModal = closeModal;
|
|
|
|
function showEditBar(title) {
|
|
if (!el.editBar) return;
|
|
if (el.editBarTitle) el.editBarTitle.textContent = title || 'Edit Lokasi';
|
|
el.editBar.classList.remove('hidden');
|
|
}
|
|
|
|
function hideEditBar() {
|
|
if (!el.editBar) return;
|
|
el.editBar.classList.add('hidden');
|
|
}
|
|
|
|
function updateDeleteVertexUi() {
|
|
if (!el.btnDeleteVertex) return;
|
|
const type = state.locationEditing?.type ?? null;
|
|
const shouldShow = type === 'jalan' || type === 'parsil';
|
|
el.btnDeleteVertex.classList.toggle('hidden', !shouldShow);
|
|
|
|
if (!shouldShow) {
|
|
el.btnDeleteVertex.classList.remove('is-active');
|
|
el.btnDeleteVertex.textContent = 'Hapus Vertex';
|
|
} else {
|
|
const enabled = Boolean(state.locationEditing?.vertexDeleteMode);
|
|
el.btnDeleteVertex.classList.toggle('is-active', enabled);
|
|
el.btnDeleteVertex.textContent = enabled ? 'Hapus Vertex: ON' : 'Hapus Vertex';
|
|
}
|
|
}
|
|
|
|
function setVertexDeleteMode(enabled) {
|
|
if (!state.locationEditing) return;
|
|
state.locationEditing.vertexDeleteMode = Boolean(enabled);
|
|
updateDeleteVertexUi();
|
|
if (state.locationEditing.vertexDeleteMode) {
|
|
showToast('Mode hapus vertex aktif: klik vertex (titik putih) untuk menghapus.', 'info', 6000);
|
|
}
|
|
}
|
|
|
|
function refreshEditingHandles(layer) {
|
|
if (!layer?.editing?.disable || !layer?.editing?.enable) return;
|
|
const wasEnabled = layer.editing.enabled?.();
|
|
if (wasEnabled === false) return;
|
|
layer.editing.disable();
|
|
layer.editing.enable();
|
|
}
|
|
|
|
function deleteNearestVertex(layer, latlng) {
|
|
if (!layer?.getLatLngs || !layer?.setLatLngs) return false;
|
|
|
|
const clickPt = map.latLngToContainerPoint(latlng);
|
|
const maxDistPx = 18;
|
|
const type = state.locationEditing?.type;
|
|
|
|
if (type === 'parsil') {
|
|
const rings = layer.getLatLngs();
|
|
const ring = Array.isArray(rings?.[0]) ? rings[0] : null;
|
|
if (!ring) return { ok: false, reason: 'not_found' };
|
|
if (ring.length <= 3) return { ok: false, reason: 'min' };
|
|
|
|
let bestIdx = -1;
|
|
let bestDist = Infinity;
|
|
for (let i = 0; i < ring.length; i++) {
|
|
const d = clickPt.distanceTo(map.latLngToContainerPoint(ring[i]));
|
|
if (d < bestDist) {
|
|
bestDist = d;
|
|
bestIdx = i;
|
|
}
|
|
}
|
|
if (bestDist > maxDistPx || bestIdx < 0) return { ok: false, reason: 'not_found' };
|
|
|
|
ring.splice(bestIdx, 1);
|
|
layer.setLatLngs([ring]);
|
|
if (layer.redraw) layer.redraw();
|
|
refreshEditingHandles(layer);
|
|
return { ok: true };
|
|
}
|
|
|
|
// polyline
|
|
let latlngs = layer.getLatLngs();
|
|
if (Array.isArray(latlngs?.[0])) latlngs = latlngs[0]; // guard for MultiLineString
|
|
if (!Array.isArray(latlngs)) return { ok: false, reason: 'not_found' };
|
|
if (latlngs.length <= 2) return { ok: false, reason: 'min' };
|
|
|
|
let bestIdx = -1;
|
|
let bestDist = Infinity;
|
|
for (let i = 0; i < latlngs.length; i++) {
|
|
const d = clickPt.distanceTo(map.latLngToContainerPoint(latlngs[i]));
|
|
if (d < bestDist) {
|
|
bestDist = d;
|
|
bestIdx = i;
|
|
}
|
|
}
|
|
if (bestDist > maxDistPx || bestIdx < 0) return { ok: false, reason: 'not_found' };
|
|
|
|
latlngs.splice(bestIdx, 1);
|
|
layer.setLatLngs(latlngs);
|
|
if (layer.redraw) layer.redraw();
|
|
refreshEditingHandles(layer);
|
|
return { ok: true };
|
|
}
|
|
|
|
function enableLayerTranslation(layer) {
|
|
if (!layer) return null;
|
|
|
|
const ctx = {
|
|
isDragging: false,
|
|
startPoint: null,
|
|
originLatLngs: null,
|
|
originPoints: null,
|
|
raf: null,
|
|
pendingDelta: null,
|
|
original: {
|
|
dragging: map.dragging.enabled(),
|
|
boxZoom: map.boxZoom?.enabled?.() ?? false,
|
|
doubleClickZoom: map.doubleClickZoom?.enabled?.() ?? false,
|
|
touchZoom: map.touchZoom?.enabled?.() ?? false
|
|
}
|
|
};
|
|
|
|
const cloneLatLngs = (latlngs) => {
|
|
if (!Array.isArray(latlngs)) return latlngs;
|
|
return latlngs.map((v) => (Array.isArray(v) ? cloneLatLngs(v) : L.latLng(v.lat, v.lng)));
|
|
};
|
|
|
|
const toPoints = (latlngs) => {
|
|
if (!Array.isArray(latlngs)) return latlngs;
|
|
return latlngs.map((v) => (Array.isArray(v) ? toPoints(v) : map.latLngToLayerPoint(v)));
|
|
};
|
|
|
|
const setTranslatedFromPoints = (originPoints, delta) => {
|
|
const apply = (pts) => {
|
|
if (!Array.isArray(pts)) return pts;
|
|
return pts.map((v) => {
|
|
if (Array.isArray(v)) return apply(v);
|
|
return map.layerPointToLatLng(v.add(delta));
|
|
});
|
|
};
|
|
layer.setLatLngs(apply(originPoints));
|
|
};
|
|
|
|
const scheduleUpdate = () => {
|
|
if (!ctx.isDragging || !ctx.pendingDelta) return;
|
|
if (ctx.raf) return;
|
|
ctx.raf = window.requestAnimationFrame(() => {
|
|
ctx.raf = null;
|
|
if (!ctx.isDragging || !ctx.pendingDelta) return;
|
|
setTranslatedFromPoints(ctx.originPoints, ctx.pendingDelta);
|
|
if (layer.redraw) layer.redraw();
|
|
});
|
|
};
|
|
|
|
const onDown = (e) => {
|
|
// Avoid capturing drag from vertex markers (edit handles)
|
|
if (e?.originalEvent?.target && String(e.originalEvent.target.className || '').includes('leaflet-editing-icon')) {
|
|
return;
|
|
}
|
|
ctx.isDragging = true;
|
|
|
|
ctx.startPoint = map.latLngToLayerPoint(e.latlng);
|
|
ctx.originLatLngs = cloneLatLngs(layer.getLatLngs());
|
|
ctx.originPoints = toPoints(ctx.originLatLngs);
|
|
ctx.pendingDelta = L.point(0, 0);
|
|
|
|
map.dragging.disable();
|
|
map.boxZoom?.disable?.();
|
|
map.doubleClickZoom?.disable?.();
|
|
map.touchZoom?.disable?.();
|
|
|
|
if (e?.originalEvent) L.DomEvent.stop(e.originalEvent);
|
|
};
|
|
|
|
const onMove = (e) => {
|
|
if (!ctx.isDragging) return;
|
|
const current = map.latLngToLayerPoint(e.latlng);
|
|
ctx.pendingDelta = current.subtract(ctx.startPoint);
|
|
scheduleUpdate();
|
|
};
|
|
|
|
const onUp = () => {
|
|
if (!ctx.isDragging) return;
|
|
ctx.isDragging = false;
|
|
ctx.startPoint = null;
|
|
ctx.originLatLngs = null;
|
|
ctx.originPoints = null;
|
|
ctx.pendingDelta = null;
|
|
if (ctx.raf) {
|
|
window.cancelAnimationFrame(ctx.raf);
|
|
ctx.raf = null;
|
|
}
|
|
|
|
if (ctx.original.dragging) map.dragging.enable();
|
|
if (ctx.original.boxZoom) map.boxZoom?.enable?.();
|
|
if (ctx.original.doubleClickZoom) map.doubleClickZoom?.enable?.();
|
|
if (ctx.original.touchZoom) map.touchZoom?.enable?.();
|
|
};
|
|
|
|
layer.on('mousedown touchstart', onDown);
|
|
map.on('mousemove touchmove', onMove);
|
|
map.on('mouseup touchend touchcancel', onUp);
|
|
|
|
return {
|
|
disable: () => {
|
|
layer.off('mousedown touchstart', onDown);
|
|
map.off('mousemove touchmove', onMove);
|
|
map.off('mouseup touchend touchcancel', onUp);
|
|
if (ctx.raf) {
|
|
window.cancelAnimationFrame(ctx.raf);
|
|
ctx.raf = null;
|
|
}
|
|
if (ctx.original.dragging) map.dragging.enable();
|
|
if (ctx.original.boxZoom) map.boxZoom?.enable?.();
|
|
if (ctx.original.doubleClickZoom) map.doubleClickZoom?.enable?.();
|
|
if (ctx.original.touchZoom) map.touchZoom?.enable?.();
|
|
}
|
|
};
|
|
}
|
|
|
|
async function saveLocationEdit() {
|
|
if (!state.locationEditing) return;
|
|
|
|
const { type, id, layer, attrs } = state.locationEditing;
|
|
try {
|
|
if (type === 'point') {
|
|
const ll = layer.getLatLng();
|
|
const payload = {
|
|
id,
|
|
nama: attrs.nama,
|
|
no: attrs.no,
|
|
deskripsi: attrs.deskripsi,
|
|
status_24jam: attrs.status_24jam,
|
|
latitude: ll.lat,
|
|
longitude: ll.lng
|
|
};
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/update_point.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat update lokasi point.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
if (type === 'jalan') {
|
|
const payload = {
|
|
id,
|
|
nama_jalan: attrs.nama_jalan,
|
|
status_jalan: attrs.status_jalan,
|
|
panjang_meter: hitungPanjang(layer),
|
|
geometry: layer.toGeoJSON().geometry
|
|
};
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/update_jalan.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat update lokasi jalan.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
if (type === 'parsil') {
|
|
const payload = {
|
|
id,
|
|
status_kepemilikan: attrs.status_kepemilikan,
|
|
luas_m2: hitungLuas(layer),
|
|
geometry: layer.toGeoJSON().geometry
|
|
};
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/update_parsil.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat update lokasi parsil.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
if (type === 'pemukiman') {
|
|
const ll = layer.getLatLng();
|
|
const payload = { id, latitude: ll.lat, longitude: ll.lng };
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/update_pemukiman_miskin_location.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat update lokasi pemukiman miskin.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
if (type === 'rumahIbadah') {
|
|
const ll = layer.getLatLng();
|
|
const payload = {
|
|
id,
|
|
nama: attrs.nama,
|
|
jenis: attrs.jenis,
|
|
kontak: attrs.kontak,
|
|
radius_meter: attrs.radius_meter,
|
|
latitude: ll.lat,
|
|
longitude: ll.lng,
|
|
address: attrs.address
|
|
};
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/update_rumah_ibadah.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat update lokasi rumah ibadah.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
showToast('Lokasi berhasil diupdate.', 'success');
|
|
state.locationEditing.vertexDeleteMode = false;
|
|
cancelLocationEdit({ silent: true });
|
|
|
|
if (type === 'point') await loadPoints();
|
|
if (type === 'jalan') await loadJalan();
|
|
if (type === 'parsil') await loadParsil();
|
|
if (type === 'pemukiman') {
|
|
await loadPemukimanMiskin();
|
|
await loadRumahIbadah();
|
|
}
|
|
if (type === 'rumahIbadah') {
|
|
await loadPemukimanMiskin();
|
|
await loadRumahIbadah();
|
|
}
|
|
} catch (e) {
|
|
showToast(e?.message || String(e), 'error', 5000);
|
|
}
|
|
}
|
|
|
|
function cancelLocationEdit({ silent = false } = {}) {
|
|
if (!state.locationEditing) return;
|
|
const { type, layer, backup, translate, dragEase } = state.locationEditing;
|
|
if (translate && translate.disable) translate.disable();
|
|
if (dragEase && dragEase.disable) dragEase.disable();
|
|
state.locationEditing.vertexDeleteMode = false;
|
|
updateDeleteVertexUi();
|
|
restoreLayerGeometry(layer, backup);
|
|
|
|
if (type === 'jalan' && layer.setStyle && backup?.status) {
|
|
layer.setStyle({ color: getJalanColor(backup.status), weight: 4, opacity: 0.95 });
|
|
}
|
|
if (type === 'parsil' && layer.setStyle && backup?.status) {
|
|
layer.setStyle({ color: '#0f172a', weight: 2, fillColor: getParsilColor(backup.status), fillOpacity: 0.35 });
|
|
}
|
|
if (type === 'point' && layer.setIcon && typeof backup?.is24 === 'boolean') {
|
|
layer.setIcon(makeSpbuIcon(backup.is24));
|
|
}
|
|
|
|
if (type === 'pemukiman' && layer.setIcon) {
|
|
layer.setIcon(makePemukimanIcon({ isCovered: Boolean(backup?.covered) }));
|
|
if (layer.__pmCoverHandler) {
|
|
layer.off('drag', layer.__pmCoverHandler);
|
|
layer.off('dragend', layer.__pmCoverHandler);
|
|
layer.__pmCoverHandler = null;
|
|
}
|
|
}
|
|
|
|
if (type === 'rumahIbadah' && layer.setIcon) {
|
|
layer.setIcon(makeRumahIbadahIcon());
|
|
}
|
|
|
|
disableLayerEditing(layer);
|
|
hideEditBar();
|
|
state.locationEditing = null;
|
|
updateDeleteVertexUi();
|
|
updateModeInfo();
|
|
if (!silent) showToast('Edit lokasi dibatalkan.', 'info');
|
|
}
|
|
|
|
function startEditLocationPoint(id, marker, row) {
|
|
resetMode();
|
|
const backup = backupLayerGeometry(marker);
|
|
backup.is24 = Number(row?.status_24jam) === 1;
|
|
state.locationEditing = {
|
|
type: 'point',
|
|
id: Number(id),
|
|
layer: marker,
|
|
backup,
|
|
vertexDeleteMode: false,
|
|
attrs: {
|
|
nama: row?.nama ?? '',
|
|
no: row?.no ?? '',
|
|
deskripsi: row?.deskripsi ?? '',
|
|
status_24jam: Number(row?.status_24jam) === 1 ? 1 : 0
|
|
}
|
|
};
|
|
|
|
enableLayerEditing(marker); // enables dragging
|
|
showEditBar('Edit Lokasi: Point');
|
|
updateDeleteVertexUi();
|
|
updateModeInfo();
|
|
showToast('Geser marker ke posisi baru, lalu klik "Simpan Lokasi".', 'info', 6000);
|
|
}
|
|
|
|
function startEditLocationPemukiman(id, marker, row) {
|
|
resetMode();
|
|
const backup = backupLayerGeometry(marker);
|
|
const ibadahId = row?.rumah_ibadah_id;
|
|
backup.covered = ibadahId !== null && ibadahId !== undefined && String(ibadahId).trim() !== '' && String(ibadahId) !== '0';
|
|
state.locationEditing = {
|
|
type: 'pemukiman',
|
|
id: Number(id),
|
|
layer: marker,
|
|
backup,
|
|
vertexDeleteMode: false,
|
|
attrs: {}
|
|
};
|
|
|
|
marker.setIcon(makePemukimanIcon({ isEdit: true, isCovered: Boolean(backup.covered) }));
|
|
enableLayerEditing(marker);
|
|
|
|
if (marker.__pmCoverHandler) {
|
|
marker.off('drag', marker.__pmCoverHandler);
|
|
marker.off('dragend', marker.__pmCoverHandler);
|
|
}
|
|
marker.__currentCovered = Boolean(backup.covered);
|
|
marker.__pmCoverHandler = () => {
|
|
const current = marker.getLatLng();
|
|
const cov = computePemukimanCoverage(current);
|
|
if (cov.covered !== null && marker.__currentCovered !== cov.covered) {
|
|
marker.__currentCovered = cov.covered;
|
|
marker.setIcon(makePemukimanIcon({ isEdit: true, isCovered: cov.covered === true }));
|
|
}
|
|
};
|
|
marker.on('drag', marker.__pmCoverHandler);
|
|
marker.on('dragend', marker.__pmCoverHandler);
|
|
marker.__pmCoverHandler();
|
|
|
|
showEditBar('Edit Lokasi: Pemukiman Miskin');
|
|
updateDeleteVertexUi();
|
|
updateModeInfo();
|
|
|
|
const label = escapeHtml(row?.kk_nama ?? row?.nama ?? `ID ${id}`);
|
|
showToast(`Geser marker pemukiman (${label}) ke posisi baru, lalu klik "Simpan Lokasi".`, 'info', 6500);
|
|
}
|
|
|
|
function startEditLocationRumahIbadah(id, marker, row) {
|
|
resetMode();
|
|
const radiusVal = Number(row?.radius_meter);
|
|
if (!Number.isFinite(radiusVal) || radiusVal <= 0) {
|
|
showToast('Radius rumah ibadah tidak valid. Silakan Edit Data dulu untuk isi radius.', 'error', 5500);
|
|
return;
|
|
}
|
|
const backup = backupLayerGeometry(marker);
|
|
state.locationEditing = {
|
|
type: 'rumahIbadah',
|
|
id: Number(id),
|
|
layer: marker,
|
|
backup,
|
|
vertexDeleteMode: false,
|
|
attrs: {
|
|
nama: row?.nama ?? 'Rumah Ibadah',
|
|
jenis: row?.jenis ?? 'Lainnya',
|
|
kontak: row?.kontak ?? '',
|
|
radius_meter: radiusVal,
|
|
address: row?.address ?? ''
|
|
}
|
|
};
|
|
|
|
enableLayerEditing(marker);
|
|
showEditBar('Edit Lokasi: Rumah Ibadah');
|
|
updateDeleteVertexUi();
|
|
updateModeInfo();
|
|
|
|
const label = escapeHtml(row?.nama ?? `ID ${id}`);
|
|
showToast(`Geser marker rumah ibadah (${label}) ke posisi baru, lalu klik "Simpan Lokasi".`, 'info', 6500);
|
|
}
|
|
|
|
function startEditLocationJalan(id, layer, props) {
|
|
resetMode();
|
|
const backup = backupLayerGeometry(layer);
|
|
backup.status = props?.status_jalan ?? 'Nasional';
|
|
state.locationEditing = {
|
|
type: 'jalan',
|
|
id: Number(id),
|
|
layer,
|
|
backup,
|
|
vertexDeleteMode: false,
|
|
attrs: {
|
|
nama_jalan: props?.nama_jalan ?? 'Jalan',
|
|
status_jalan: props?.status_jalan ?? 'Nasional'
|
|
}
|
|
};
|
|
|
|
enableLayerEditing(layer);
|
|
layer.setStyle?.({ color: getJalanColor(props?.status_jalan), weight: 4, opacity: 0.95 });
|
|
state.locationEditing.translate = enableLayerTranslation(layer);
|
|
|
|
showEditBar('Edit Lokasi: Jalan');
|
|
updateDeleteVertexUi();
|
|
updateModeInfo();
|
|
showToast('Geser garis untuk pindah, atau geser vertex untuk ubah bentuk. Lalu simpan.', 'info', 7000);
|
|
}
|
|
|
|
function startEditLocationParsil(id, layer, props) {
|
|
resetMode();
|
|
const backup = backupLayerGeometry(layer);
|
|
backup.status = props?.status_kepemilikan ?? 'SHM';
|
|
state.locationEditing = {
|
|
type: 'parsil',
|
|
id: Number(id),
|
|
layer,
|
|
backup,
|
|
vertexDeleteMode: false,
|
|
attrs: {
|
|
status_kepemilikan: props?.status_kepemilikan ?? 'SHM'
|
|
}
|
|
};
|
|
|
|
enableLayerEditing(layer);
|
|
layer.setStyle?.({ color: '#0f172a', weight: 2, fillColor: getParsilColor(props?.status_kepemilikan), fillOpacity: 0.35 });
|
|
state.locationEditing.translate = enableLayerTranslation(layer);
|
|
|
|
showEditBar('Edit Lokasi: Parsil');
|
|
updateDeleteVertexUi();
|
|
updateModeInfo();
|
|
showToast('Geser area untuk pindah, atau geser vertex untuk ubah bentuk. Lalu simpan.', 'info', 7000);
|
|
}
|
|
|
|
// ================= DRAW EVENT =================
|
|
map.on(L.Draw.Event.CREATED, function (e) {
|
|
state.currentLayer = e.layer;
|
|
state.pendingLayer = e.layer;
|
|
editableGroup.addLayer(e.layer);
|
|
|
|
// Disable drawer while modal is open
|
|
if (state.activeDrawer) {
|
|
state.activeDrawer.disable();
|
|
state.activeDrawer = null;
|
|
}
|
|
|
|
if (!state.featureType) {
|
|
if (e.layerType === 'polyline') state.featureType = 'jalan';
|
|
if (e.layerType === 'polygon') state.featureType = 'parsil';
|
|
setActiveModeButton(state.featureType);
|
|
updateModeInfo();
|
|
}
|
|
|
|
// Apply initial style based on selected status
|
|
if (state.featureType === 'jalan' && e.layer.setStyle) {
|
|
e.layer.setStyle({ color: getJalanColor(el.statusJalan?.value), weight: 4, opacity: 0.95 });
|
|
}
|
|
if (state.featureType === 'parsil' && e.layer.setStyle) {
|
|
e.layer.setStyle({ color: '#0f172a', weight: 2, fillColor: getParsilColor(el.statusParsil?.value), fillOpacity: 0.35 });
|
|
}
|
|
|
|
openModal();
|
|
});
|
|
|
|
// ================= POINT =================
|
|
map.on('click', function (e) {
|
|
if (state.locationEditing?.vertexDeleteMode && (state.locationEditing.type === 'jalan' || state.locationEditing.type === 'parsil')) {
|
|
const result = deleteNearestVertex(state.locationEditing.layer, e.latlng);
|
|
if (result?.ok) {
|
|
showToast('Vertex dihapus.', 'success');
|
|
} else if (result?.reason === 'min') {
|
|
const min = state.locationEditing.type === 'jalan' ? 2 : 3;
|
|
showToast(`Tidak bisa menghapus (minimal ${min} vertex).`, 'error', 2500);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (state.featureType !== 'point' && state.featureType !== 'rumahIbadah' && state.featureType !== 'pemukiman') return;
|
|
if (el.modal && !el.modal.classList.contains('hidden')) return;
|
|
|
|
state.currentLayer = e.latlng;
|
|
if (state.featureType === 'point') {
|
|
el.latPoint.value = e.latlng.lat;
|
|
el.lngPoint.value = e.latlng.lng;
|
|
}
|
|
if (state.featureType === 'rumahIbadah') {
|
|
el.latRumahIbadah.value = e.latlng.lat;
|
|
el.lngRumahIbadah.value = e.latlng.lng;
|
|
}
|
|
if (state.featureType === 'pemukiman') {
|
|
el.latPemukiman.value = e.latlng.lat;
|
|
el.lngPemukiman.value = e.latlng.lng;
|
|
}
|
|
openModal();
|
|
});
|
|
|
|
// ================= SUBMIT =================
|
|
el.featureForm.addEventListener('submit', async function (e) {
|
|
e.preventDefault();
|
|
|
|
const submittingType = state.featureType;
|
|
|
|
if (!submittingType) {
|
|
showToast('Mode belum dipilih.', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!state.currentLayer && !state.editing?.layer) {
|
|
showToast('Layer tidak ada!', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
clearFormNotice();
|
|
setSubmitting(true);
|
|
|
|
// UPDATE mode
|
|
if (state.editing && state.editing.type === submittingType) {
|
|
if (submittingType === 'point') {
|
|
const marker = state.editing.layer;
|
|
const ll = marker.getLatLng();
|
|
const payload = {
|
|
id: state.editing.id,
|
|
nama: el.namaPoint.value,
|
|
no: el.noPoint.value,
|
|
deskripsi: el.deskripsiPoint.value,
|
|
status_24jam: el.status24Jam.checked ? 1 : 0,
|
|
latitude: ll.lat,
|
|
longitude: ll.lng
|
|
};
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/update_point.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat update point.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
if (submittingType === 'jalan') {
|
|
const layer = state.editing.layer;
|
|
const payload = {
|
|
id: state.editing.id,
|
|
nama_jalan: el.namaJalan.value,
|
|
status_jalan: el.statusJalan.value,
|
|
panjang_meter: hitungPanjang(layer),
|
|
geometry: layer.toGeoJSON().geometry
|
|
};
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/update_jalan.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat update jalan.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
if (submittingType === 'parsil') {
|
|
const layer = state.editing.layer;
|
|
const payload = {
|
|
id: state.editing.id,
|
|
status_kepemilikan: el.statusParsil.value,
|
|
luas_m2: hitungLuas(layer),
|
|
geometry: layer.toGeoJSON().geometry
|
|
};
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/update_parsil.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat update parsil.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
if (submittingType === 'rumahIbadah') {
|
|
const marker = state.editing.layer;
|
|
const ll = marker.getLatLng();
|
|
const payload = {
|
|
id: state.editing.id,
|
|
nama: el.namaRumahIbadah.value,
|
|
jenis: el.jenisRumahIbadah.value,
|
|
kontak: el.kontakRumahIbadah.value,
|
|
radius_meter: el.radiusRumahIbadah.value,
|
|
latitude: ll.lat,
|
|
longitude: ll.lng,
|
|
address: el.alamatRumahIbadah.value
|
|
};
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/update_rumah_ibadah.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat update rumah ibadah.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
if (submittingType === 'pemukiman') {
|
|
const marker = state.editing.layer;
|
|
const ll = marker.getLatLng();
|
|
const payload = {
|
|
id: state.editing.id,
|
|
kk_nama: el.kkNamaPemukiman.value,
|
|
nik: el.nikPemukiman.value,
|
|
jumlah_anggota: el.jumlahAnggotaPemukiman.value,
|
|
latitude: ll.lat,
|
|
longitude: ll.lng,
|
|
address: el.alamatPemukiman.value,
|
|
kelurahan: el.kelurahanPemukiman.value,
|
|
kecamatan: el.kecamatanPemukiman.value,
|
|
status_bantuan: el.statusBantuanPemukiman.value,
|
|
jenis_bantuan: el.jenisBantuanPemukiman.value,
|
|
tanggal_bantuan: el.tanggalBantuanPemukiman.value,
|
|
anggota: []
|
|
};
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/update_pemukiman_miskin.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat update pemukiman miskin.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
showToast('Data berhasil diupdate.', 'success');
|
|
if (state.editing?.dragEase?.disable) state.editing.dragEase.disable();
|
|
disableLayerEditing(state.editing.layer);
|
|
state.editing = null;
|
|
closeModal();
|
|
|
|
if (submittingType === 'point') await loadPoints();
|
|
if (submittingType === 'jalan') await loadJalan();
|
|
if (submittingType === 'parsil') await loadParsil();
|
|
if (submittingType === 'rumahIbadah') {
|
|
await loadPemukimanMiskin();
|
|
await loadRumahIbadah();
|
|
}
|
|
if (submittingType === 'pemukiman') await loadPemukimanMiskin();
|
|
return;
|
|
}
|
|
|
|
// POINT
|
|
if (submittingType === 'point') {
|
|
const payload = {
|
|
nama: el.namaPoint.value,
|
|
no: el.noPoint.value,
|
|
deskripsi: el.deskripsiPoint.value,
|
|
status_24jam: el.status24Jam.checked ? 1 : 0,
|
|
latitude: el.latPoint.value,
|
|
longitude: el.lngPoint.value
|
|
};
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/create_point.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat menyimpan point.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
// JALAN
|
|
if (submittingType === 'jalan') {
|
|
const layer = state.currentLayer;
|
|
|
|
const payload = {
|
|
nama_jalan: el.namaJalan.value,
|
|
status_jalan: el.statusJalan.value,
|
|
panjang_meter: hitungPanjang(layer),
|
|
geometry: layer.toGeoJSON().geometry
|
|
};
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/create_jalan.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat menyimpan jalan.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
// PARSIL
|
|
if (submittingType === 'parsil') {
|
|
const layer = state.currentLayer;
|
|
|
|
const payload = {
|
|
status_kepemilikan: el.statusParsil.value,
|
|
luas_m2: hitungLuas(layer),
|
|
geometry: layer.toGeoJSON().geometry
|
|
};
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/create_parsil.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat menyimpan parsil.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
// RUMAH IBADAH
|
|
if (submittingType === 'rumahIbadah') {
|
|
const payload = {
|
|
nama: el.namaRumahIbadah.value,
|
|
jenis: el.jenisRumahIbadah.value,
|
|
kontak: el.kontakRumahIbadah.value,
|
|
radius_meter: el.radiusRumahIbadah.value,
|
|
latitude: el.latRumahIbadah.value,
|
|
longitude: el.lngRumahIbadah.value,
|
|
address: el.alamatRumahIbadah.value
|
|
};
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/create_rumah_ibadah.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat menyimpan rumah ibadah.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
// PEMUKIMAN MISKIN
|
|
if (submittingType === 'pemukiman') {
|
|
const payload = {
|
|
kk_nama: el.kkNamaPemukiman.value,
|
|
nik: el.nikPemukiman.value,
|
|
jumlah_anggota: el.jumlahAnggotaPemukiman.value,
|
|
latitude: el.latPemukiman.value,
|
|
longitude: el.lngPemukiman.value,
|
|
address: el.alamatPemukiman.value,
|
|
kelurahan: el.kelurahanPemukiman.value,
|
|
kecamatan: el.kecamatanPemukiman.value,
|
|
status_bantuan: el.statusBantuanPemukiman.value,
|
|
jenis_bantuan: el.jenisBantuanPemukiman.value,
|
|
tanggal_bantuan: el.tanggalBantuanPemukiman.value,
|
|
anggota: []
|
|
};
|
|
|
|
const { res, json, text } = await fetchMaybeJson('php/create_pemukiman_miskin.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!json) throw new Error(text || 'Respon server tidak valid saat menyimpan pemukiman miskin.');
|
|
const api = parseApiJson(json);
|
|
if (!api.success) throw new Error(api.message);
|
|
if (!res.ok) throw new Error(api.message || `HTTP ${res.status}`);
|
|
}
|
|
|
|
showToast('Data berhasil disimpan.', 'success');
|
|
closeModal();
|
|
|
|
if (submittingType === 'point') await loadPoints();
|
|
if (submittingType === 'jalan') await loadJalan();
|
|
if (submittingType === 'parsil') await loadParsil();
|
|
if (submittingType === 'rumahIbadah') {
|
|
await loadPemukimanMiskin();
|
|
await loadRumahIbadah();
|
|
}
|
|
if (submittingType === 'pemukiman') await loadPemukimanMiskin();
|
|
} catch (err) {
|
|
const msg = err?.message || String(err);
|
|
setFormNotice(msg, 'error');
|
|
showToast(msg, 'error', 5000);
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
});
|
|
|
|
// live style update for pending layer
|
|
el.statusJalan?.addEventListener('change', function () {
|
|
const target =
|
|
state.pendingLayer && state.featureType === 'jalan'
|
|
? state.pendingLayer
|
|
: state.editing?.type === 'jalan'
|
|
? state.editing.layer
|
|
: null;
|
|
|
|
if (target && target.setStyle) {
|
|
target.setStyle({ color: getJalanColor(el.statusJalan.value), weight: 4, opacity: 0.95 });
|
|
}
|
|
});
|
|
|
|
el.statusParsil?.addEventListener('change', function () {
|
|
const target =
|
|
state.pendingLayer && state.featureType === 'parsil'
|
|
? state.pendingLayer
|
|
: state.editing?.type === 'parsil'
|
|
? state.editing.layer
|
|
: null;
|
|
|
|
if (target && target.setStyle) {
|
|
target.setStyle({ fillColor: getParsilColor(el.statusParsil.value), fillOpacity: 0.35 });
|
|
}
|
|
});
|
|
|
|
el.status24Jam?.addEventListener('change', function () {
|
|
if (state.editing?.type === 'point' && state.editing.layer?.setIcon) {
|
|
state.editing.layer.setIcon(makeSpbuIcon(el.status24Jam.checked));
|
|
}
|
|
});
|
|
|
|
// ================= LOAD LAYERS =================
|
|
function setActiveListItem(kind, id) {
|
|
if (kind === 'points' && el.pointsList) {
|
|
const items = el.pointsList.querySelectorAll('.list-item');
|
|
items.forEach((node) => node.classList.toggle('is-active', node.dataset.id === String(id)));
|
|
state.selected.points = id;
|
|
}
|
|
if (kind === 'jalan' && el.jalanList) {
|
|
const items = el.jalanList.querySelectorAll('.list-item');
|
|
items.forEach((node) => node.classList.toggle('is-active', node.dataset.id === String(id)));
|
|
state.selected.jalan = id;
|
|
}
|
|
if (kind === 'parsil' && el.parsilList) {
|
|
const items = el.parsilList.querySelectorAll('.list-item');
|
|
items.forEach((node) => node.classList.toggle('is-active', node.dataset.id === String(id)));
|
|
state.selected.parsil = id;
|
|
}
|
|
|
|
if (kind === 'rumahIbadah' && el.rumahIbadahList) {
|
|
const items = el.rumahIbadahList.querySelectorAll('.list-item');
|
|
items.forEach((node) => node.classList.toggle('is-active', node.dataset.id === String(id)));
|
|
state.selected.rumahIbadah = id;
|
|
}
|
|
|
|
if (kind === 'pemukiman' && el.pemukimanList) {
|
|
const items = el.pemukimanList.querySelectorAll('.list-item');
|
|
items.forEach((node) => node.classList.toggle('is-active', node.dataset.id === String(id)));
|
|
state.selected.pemukiman = id;
|
|
}
|
|
}
|
|
|
|
function renderEmptyList(container, text) {
|
|
if (!container) return;
|
|
container.innerHTML = '';
|
|
const div = document.createElement('div');
|
|
div.className = 'list-empty';
|
|
div.textContent = text;
|
|
container.appendChild(div);
|
|
}
|
|
|
|
function renderPointsList(rows) {
|
|
if (!el.pointsList) return;
|
|
if (!Array.isArray(rows) || rows.length === 0) {
|
|
renderEmptyList(el.pointsList, 'Belum ada data SPBU.');
|
|
return;
|
|
}
|
|
|
|
el.pointsList.innerHTML = '';
|
|
for (const row of rows) {
|
|
const id = row?.id ?? '';
|
|
const nama = row?.nama ?? 'SPBU';
|
|
const no = row?.no ?? '';
|
|
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'list-item';
|
|
btn.dataset.id = String(id);
|
|
|
|
const title = document.createElement('div');
|
|
title.className = 'list-title';
|
|
title.textContent = nama;
|
|
|
|
const meta = document.createElement('div');
|
|
meta.className = 'list-meta';
|
|
meta.textContent = no ? `No: ${no}` : 'Klik untuk zoom';
|
|
|
|
btn.appendChild(title);
|
|
btn.appendChild(meta);
|
|
|
|
btn.addEventListener('click', () => {
|
|
const lat = parseFloat(row?.latitude);
|
|
const lng = parseFloat(row?.longitude);
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
|
|
|
|
if (el.togglePoints) el.togglePoints.checked = true;
|
|
applyLayerVisibility();
|
|
|
|
const record = state.indices.points.get(String(id));
|
|
map.flyTo([lat, lng], Math.max(map.getZoom(), 17), { duration: 0.9 });
|
|
if (record?.layer?.openPopup) record.layer.openPopup();
|
|
if (record?.layer?.setStyle) {
|
|
record.layer.setStyle({ radius: 10 });
|
|
window.setTimeout(() => record.layer.setStyle({ radius: 7 }), 900);
|
|
}
|
|
setActiveListItem('points', id);
|
|
});
|
|
|
|
el.pointsList.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
function renderJalanList(features) {
|
|
if (!el.jalanList) return;
|
|
if (!Array.isArray(features) || features.length === 0) {
|
|
renderEmptyList(el.jalanList, 'Belum ada data jalan.');
|
|
return;
|
|
}
|
|
|
|
el.jalanList.innerHTML = '';
|
|
for (const f of features) {
|
|
const props = f?.properties ?? {};
|
|
const id = props?.id ?? '';
|
|
const nama = props?.nama_jalan ?? 'Jalan';
|
|
const status = props?.status_jalan ?? '';
|
|
const panjang = Number.isFinite(Number(props?.panjang_meter))
|
|
? `${Number(props.panjang_meter).toFixed(0)} m`
|
|
: '';
|
|
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'list-item';
|
|
btn.dataset.id = String(id);
|
|
|
|
const title = document.createElement('div');
|
|
title.className = 'list-title';
|
|
title.textContent = nama;
|
|
|
|
const meta = document.createElement('div');
|
|
meta.className = 'list-meta';
|
|
meta.textContent = [status, panjang].filter(Boolean).join(' • ') || 'Klik untuk zoom';
|
|
|
|
btn.appendChild(title);
|
|
btn.appendChild(meta);
|
|
|
|
btn.addEventListener('click', () => {
|
|
if (el.toggleJalan) el.toggleJalan.checked = true;
|
|
applyLayerVisibility();
|
|
|
|
const record = state.indices.jalan.get(String(id));
|
|
const layer = record?.layer;
|
|
if (layer?.getBounds) {
|
|
map.fitBounds(layer.getBounds(), { padding: [30, 30] });
|
|
}
|
|
if (layer?.openPopup) layer.openPopup();
|
|
setActiveListItem('jalan', id);
|
|
});
|
|
|
|
el.jalanList.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
function renderParsilList(features) {
|
|
if (!el.parsilList) return;
|
|
if (!Array.isArray(features) || features.length === 0) {
|
|
renderEmptyList(el.parsilList, 'Belum ada data parsil.');
|
|
return;
|
|
}
|
|
|
|
el.parsilList.innerHTML = '';
|
|
for (const f of features) {
|
|
const props = f?.properties ?? {};
|
|
const id = props?.id ?? '';
|
|
const status = props?.status_kepemilikan ?? '';
|
|
const luas = Number.isFinite(Number(props?.luas_m2))
|
|
? `${Number(props.luas_m2).toFixed(0)} m²`
|
|
: '';
|
|
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'list-item';
|
|
btn.dataset.id = String(id);
|
|
|
|
const title = document.createElement('div');
|
|
title.className = 'list-title';
|
|
title.textContent = `Parsil ${status || ''}`.trim();
|
|
|
|
const meta = document.createElement('div');
|
|
meta.className = 'list-meta';
|
|
meta.textContent = luas ? `Luas: ${luas}` : 'Klik untuk zoom';
|
|
|
|
btn.appendChild(title);
|
|
btn.appendChild(meta);
|
|
|
|
btn.addEventListener('click', () => {
|
|
if (el.toggleParsil) el.toggleParsil.checked = true;
|
|
applyLayerVisibility();
|
|
|
|
const record = state.indices.parsil.get(String(id));
|
|
const layer = record?.layer;
|
|
if (layer?.getBounds) {
|
|
map.fitBounds(layer.getBounds(), { padding: [30, 30] });
|
|
}
|
|
if (layer?.openPopup) layer.openPopup();
|
|
setActiveListItem('parsil', id);
|
|
});
|
|
|
|
el.parsilList.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
function renderRumahIbadahList(rows) {
|
|
if (!el.rumahIbadahList) return;
|
|
if (!Array.isArray(rows) || rows.length === 0) {
|
|
renderEmptyList(el.rumahIbadahList, 'Belum ada data rumah ibadah.');
|
|
return;
|
|
}
|
|
|
|
el.rumahIbadahList.innerHTML = '';
|
|
for (const row of rows) {
|
|
const lat = parseFloat(row?.latitude);
|
|
const lng = parseFloat(row?.longitude);
|
|
const id = String(row?.id ?? (Number.isFinite(lat) && Number.isFinite(lng) ? `${lat},${lng}` : ''));
|
|
const nama = row?.nama ?? 'Rumah Ibadah';
|
|
const jenis = row?.jenis ?? '';
|
|
const radius = Number(row?.radius_meter);
|
|
const metaText = [jenis, Number.isFinite(radius) && radius > 0 ? `Radius: ${radius} m` : ''].filter(Boolean).join(' • ') || 'Klik untuk zoom';
|
|
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'list-item';
|
|
btn.dataset.id = String(id);
|
|
|
|
const title = document.createElement('div');
|
|
title.className = 'list-title';
|
|
title.textContent = nama;
|
|
|
|
const meta = document.createElement('div');
|
|
meta.className = 'list-meta';
|
|
meta.textContent = metaText;
|
|
|
|
btn.appendChild(title);
|
|
btn.appendChild(meta);
|
|
|
|
btn.addEventListener('click', () => {
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
|
|
|
|
if (el.toggleRumahIbadah) el.toggleRumahIbadah.checked = true;
|
|
applyLayerVisibility();
|
|
|
|
const record = state.indices.rumahIbadah.get(String(id));
|
|
map.flyTo([lat, lng], Math.max(map.getZoom(), 17), { duration: 0.9 });
|
|
if (record?.layer?.openPopup) record.layer.openPopup();
|
|
setActiveListItem('rumahIbadah', id);
|
|
});
|
|
|
|
el.rumahIbadahList.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
function renderPemukimanList(rows) {
|
|
if (!el.pemukimanList) return;
|
|
if (!Array.isArray(rows) || rows.length === 0) {
|
|
renderEmptyList(el.pemukimanList, 'Belum ada data pemukiman miskin.');
|
|
return;
|
|
}
|
|
|
|
el.pemukimanList.innerHTML = '';
|
|
for (const row of rows) {
|
|
const lat = parseFloat(row?.latitude);
|
|
const lng = parseFloat(row?.longitude);
|
|
const id = String(row?.id ?? (Number.isFinite(lat) && Number.isFinite(lng) ? `${lat},${lng}` : ''));
|
|
const nama = row?.kk_nama ?? row?.nama ?? 'Pemukiman Miskin';
|
|
const status = row?.status_bantuan ?? '';
|
|
const ibadah = row?.rumah_ibadah_nama ?? '';
|
|
const metaText = [status, ibadah ? `Binaan: ${ibadah}` : ''].filter(Boolean).join(' • ') || 'Klik untuk zoom';
|
|
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'list-item';
|
|
btn.dataset.id = String(id);
|
|
|
|
const title = document.createElement('div');
|
|
title.className = 'list-title';
|
|
title.textContent = nama;
|
|
|
|
const meta = document.createElement('div');
|
|
meta.className = 'list-meta';
|
|
meta.textContent = metaText;
|
|
|
|
btn.appendChild(title);
|
|
btn.appendChild(meta);
|
|
|
|
btn.addEventListener('click', () => {
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
|
|
|
|
if (el.togglePemukiman) el.togglePemukiman.checked = true;
|
|
applyLayerVisibility();
|
|
|
|
const record = state.indices.pemukiman.get(String(id));
|
|
map.flyTo([lat, lng], Math.max(map.getZoom(), 17), { duration: 0.9 });
|
|
if (record?.layer?.openPopup) record.layer.openPopup();
|
|
setActiveListItem('pemukiman', id);
|
|
});
|
|
|
|
el.pemukimanList.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
async function loadPoints() {
|
|
points24Layer.clearLayers();
|
|
pointsNon24Layer.clearLayers();
|
|
state.counts.points = 0;
|
|
state.indices.points.clear();
|
|
|
|
let rows = [];
|
|
try {
|
|
const res = await fetch('php/read_point.php', { cache: 'no-store' });
|
|
rows = await res.json();
|
|
} catch {
|
|
rows = [];
|
|
}
|
|
|
|
let count = 0;
|
|
if (Array.isArray(rows)) {
|
|
for (const row of rows) {
|
|
const lat = parseFloat(row?.latitude);
|
|
const lng = parseFloat(row?.longitude);
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) continue;
|
|
|
|
const id = String(row?.id ?? `${lat},${lng}`);
|
|
const nama = escapeHtml(row?.nama ?? 'SPBU');
|
|
const no = escapeHtml(row?.no ?? '-');
|
|
const deskripsi = escapeHtml(row?.deskripsi ?? '');
|
|
const is24 = isSpbu24(row);
|
|
const status24 = is24 ? 'Ya' : 'Tidak';
|
|
|
|
const marker = L.marker([lat, lng], { icon: makeSpbuIcon(is24), draggable: true });
|
|
marker.on('add', () => {
|
|
if (marker.dragging && marker.dragging.disable) {
|
|
marker.dragging.disable();
|
|
}
|
|
});
|
|
|
|
const popupEl = document.createElement('div');
|
|
popupEl.innerHTML = `
|
|
<div style="font-weight:800;margin-bottom:6px;">${nama}</div>
|
|
<div><b>No:</b> ${no}</div>
|
|
<div><b>24 Jam:</b> ${status24}</div>
|
|
${deskripsi ? `<div style="margin-top:6px;color:#475569;">${deskripsi}</div>` : ''}
|
|
`;
|
|
|
|
const actions = document.createElement('div');
|
|
actions.className = 'popup-actions';
|
|
const btnEditData = document.createElement('button');
|
|
btnEditData.type = 'button';
|
|
btnEditData.className = 'popup-btn edit';
|
|
btnEditData.textContent = 'Edit Data';
|
|
const btnEditLoc = document.createElement('button');
|
|
btnEditLoc.type = 'button';
|
|
btnEditLoc.className = 'popup-btn spatial';
|
|
btnEditLoc.textContent = 'Edit Lokasi';
|
|
const btnDelete = document.createElement('button');
|
|
btnDelete.type = 'button';
|
|
btnDelete.className = 'popup-btn delete';
|
|
btnDelete.textContent = 'Hapus';
|
|
actions.appendChild(btnEditData);
|
|
actions.appendChild(btnEditLoc);
|
|
actions.appendChild(btnDelete);
|
|
popupEl.appendChild(actions);
|
|
|
|
btnEditData.addEventListener('click', () => startEditPoint(id, marker, row));
|
|
btnEditLoc.addEventListener('click', () => startEditLocationPoint(id, marker, row));
|
|
btnDelete.addEventListener('click', () => deleteFeature('point', id));
|
|
|
|
marker.bindPopup(popupEl);
|
|
marker.addTo(is24 ? points24Layer : pointsNon24Layer);
|
|
marker.on('click', () => setActiveListItem('points', id));
|
|
state.indices.points.set(String(id), { layer: marker, data: row });
|
|
count += 1;
|
|
}
|
|
}
|
|
|
|
state.counts.points = count;
|
|
state.pointsData = Array.isArray(rows) ? rows : [];
|
|
updateStats();
|
|
applyPointFilter();
|
|
}
|
|
|
|
async function loadJalan() {
|
|
jalanLayer.clearLayers();
|
|
state.counts.jalan = 0;
|
|
state.indices.jalan.clear();
|
|
|
|
let fc = null;
|
|
try {
|
|
const res = await fetch('php/read_jalan.php', { cache: 'no-store' });
|
|
fc = await res.json();
|
|
} catch {
|
|
fc = null;
|
|
}
|
|
if (!fc || fc.type !== 'FeatureCollection' || !Array.isArray(fc.features)) {
|
|
updateStats();
|
|
renderJalanList([]);
|
|
return;
|
|
}
|
|
|
|
const geo = L.geoJSON(fc, {
|
|
style: (feature) => {
|
|
const status = feature?.properties?.status_jalan;
|
|
return { color: getJalanColor(status), weight: 4, opacity: 0.95 };
|
|
},
|
|
onEachFeature: (feature, layer) => {
|
|
const props = feature?.properties ?? {};
|
|
const id = String(props?.id ?? '');
|
|
const nama = escapeHtml(props.nama_jalan ?? 'Jalan');
|
|
const status = escapeHtml(props.status_jalan ?? '-');
|
|
const panjang = Number.isFinite(Number(props.panjang_meter))
|
|
? `${Number(props.panjang_meter).toFixed(1)} m`
|
|
: '-';
|
|
|
|
const popupEl = document.createElement('div');
|
|
popupEl.innerHTML = `
|
|
<div style="font-weight:800;margin-bottom:6px;">${nama}</div>
|
|
<div><b>Status:</b> ${status}</div>
|
|
<div><b>Panjang:</b> ${panjang}</div>
|
|
`;
|
|
|
|
const actions = document.createElement('div');
|
|
actions.className = 'popup-actions';
|
|
const btnEditData = document.createElement('button');
|
|
btnEditData.type = 'button';
|
|
btnEditData.className = 'popup-btn edit';
|
|
btnEditData.textContent = 'Edit Data';
|
|
const btnEditLoc = document.createElement('button');
|
|
btnEditLoc.type = 'button';
|
|
btnEditLoc.className = 'popup-btn spatial';
|
|
btnEditLoc.textContent = 'Edit Lokasi';
|
|
const btnDelete = document.createElement('button');
|
|
btnDelete.type = 'button';
|
|
btnDelete.className = 'popup-btn delete';
|
|
btnDelete.textContent = 'Hapus';
|
|
actions.appendChild(btnEditData);
|
|
actions.appendChild(btnEditLoc);
|
|
actions.appendChild(btnDelete);
|
|
popupEl.appendChild(actions);
|
|
|
|
btnEditData.addEventListener('click', () => startEditJalan(id, layer, props));
|
|
btnEditLoc.addEventListener('click', () => startEditLocationJalan(id, layer, props));
|
|
btnDelete.addEventListener('click', () => deleteFeature('jalan', id));
|
|
|
|
layer.bindPopup(popupEl);
|
|
|
|
if (id) {
|
|
state.indices.jalan.set(id, { layer, data: props });
|
|
layer.on('click', () => setActiveListItem('jalan', id));
|
|
}
|
|
}
|
|
});
|
|
|
|
geo.addTo(jalanLayer);
|
|
state.counts.jalan = fc.features.length;
|
|
updateStats();
|
|
renderJalanList(fc.features);
|
|
}
|
|
|
|
async function loadParsil() {
|
|
parsilLayer.clearLayers();
|
|
state.counts.parsil = 0;
|
|
state.indices.parsil.clear();
|
|
|
|
let fc = null;
|
|
try {
|
|
const res = await fetch('php/read_parsil.php', { cache: 'no-store' });
|
|
fc = await res.json();
|
|
} catch {
|
|
fc = null;
|
|
}
|
|
if (!fc || fc.type !== 'FeatureCollection' || !Array.isArray(fc.features)) {
|
|
updateStats();
|
|
renderParsilList([]);
|
|
return;
|
|
}
|
|
|
|
const geo = L.geoJSON(fc, {
|
|
style: (feature) => {
|
|
const status = feature?.properties?.status_kepemilikan;
|
|
return {
|
|
color: '#0f172a',
|
|
weight: 2,
|
|
fillColor: getParsilColor(status),
|
|
fillOpacity: 0.35
|
|
};
|
|
},
|
|
onEachFeature: (feature, layer) => {
|
|
const props = feature?.properties ?? {};
|
|
const status = escapeHtml(props.status_kepemilikan ?? '-');
|
|
const luas = Number.isFinite(Number(props.luas_m2))
|
|
? `${Number(props.luas_m2).toFixed(1)} m²`
|
|
: '-';
|
|
|
|
const id = String(props?.id ?? '');
|
|
const popupEl = document.createElement('div');
|
|
popupEl.innerHTML = `
|
|
<div style="font-weight:800;margin-bottom:6px;">Parsil</div>
|
|
<div><b>Status:</b> ${status}</div>
|
|
<div><b>Luas:</b> ${luas}</div>
|
|
`;
|
|
|
|
const actions = document.createElement('div');
|
|
actions.className = 'popup-actions';
|
|
const btnEditData = document.createElement('button');
|
|
btnEditData.type = 'button';
|
|
btnEditData.className = 'popup-btn edit';
|
|
btnEditData.textContent = 'Edit Data';
|
|
const btnEditLoc = document.createElement('button');
|
|
btnEditLoc.type = 'button';
|
|
btnEditLoc.className = 'popup-btn spatial';
|
|
btnEditLoc.textContent = 'Edit Lokasi';
|
|
const btnDelete = document.createElement('button');
|
|
btnDelete.type = 'button';
|
|
btnDelete.className = 'popup-btn delete';
|
|
btnDelete.textContent = 'Hapus';
|
|
actions.appendChild(btnEditData);
|
|
actions.appendChild(btnEditLoc);
|
|
actions.appendChild(btnDelete);
|
|
popupEl.appendChild(actions);
|
|
|
|
btnEditData.addEventListener('click', () => startEditParsil(id, layer, props));
|
|
btnEditLoc.addEventListener('click', () => startEditLocationParsil(id, layer, props));
|
|
btnDelete.addEventListener('click', () => deleteFeature('parsil', id));
|
|
|
|
layer.bindPopup(popupEl);
|
|
if (id) {
|
|
state.indices.parsil.set(id, { layer, data: props });
|
|
layer.on('click', () => setActiveListItem('parsil', id));
|
|
}
|
|
}
|
|
});
|
|
|
|
geo.addTo(parsilLayer);
|
|
state.counts.parsil = fc.features.length;
|
|
updateStats();
|
|
renderParsilList(fc.features);
|
|
}
|
|
|
|
async function loadPemukimanMiskin() {
|
|
pemukimanLayer.clearLayers();
|
|
state.counts.pemukiman = 0;
|
|
state.indices.pemukiman.clear();
|
|
|
|
let rows = [];
|
|
try {
|
|
const { res, json, text } = await fetchMaybeJson('php/read_pemukiman_miskin.php', { cache: 'no-store' });
|
|
if (!res.ok) {
|
|
warnOnce('pemukiman_fetch', `Gagal load Pemukiman Miskin (HTTP ${res.status}). Cek koneksi database / endpoint.`);
|
|
rows = [];
|
|
} else if (!Array.isArray(json)) {
|
|
warnOnce('pemukiman_parse', text ? `Respon pemukiman bukan JSON: ${text.slice(0, 120)}` : 'Respon pemukiman bukan JSON.');
|
|
rows = [];
|
|
} else {
|
|
rows = json;
|
|
}
|
|
} catch (e) {
|
|
warnOnce('pemukiman_err', `Gagal load Pemukiman Miskin: ${e?.message || String(e)}`);
|
|
rows = [];
|
|
}
|
|
|
|
let count = 0;
|
|
if (Array.isArray(rows)) {
|
|
for (const row of rows) {
|
|
const lat = parseFloat(row?.latitude);
|
|
const lng = parseFloat(row?.longitude);
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) continue;
|
|
|
|
const id = String(row?.id ?? `${lat},${lng}`);
|
|
const nama = escapeHtml(row?.kk_nama ?? row?.nama ?? 'Pemukiman Miskin');
|
|
const status = escapeHtml(row?.status_bantuan ?? '-');
|
|
const ibadah = escapeHtml(row?.rumah_ibadah_nama ?? '-');
|
|
const jarakNum = Number(row?.jarak_meter);
|
|
const jarak = Number.isFinite(jarakNum) ? `${jarakNum.toFixed(0)} m` : '-';
|
|
const ibadahId = row?.rumah_ibadah_id;
|
|
const covered = ibadahId !== null && ibadahId !== undefined && String(ibadahId).trim() !== '' && String(ibadahId) !== '0';
|
|
|
|
const marker = L.marker([lat, lng], { icon: makePemukimanIcon({ isCovered: covered }), draggable: true, autoPan: true, riseOnHover: true });
|
|
marker.on('add', () => {
|
|
if (marker.dragging && marker.dragging.disable) marker.dragging.disable();
|
|
});
|
|
|
|
const popupEl = document.createElement('div');
|
|
popupEl.innerHTML = `
|
|
<div style="font-weight:800;margin-bottom:6px;">${nama}</div>
|
|
<div><b>Status:</b> ${status}</div>
|
|
<div><b>Rumah Ibadah:</b> ${ibadah}</div>
|
|
<div><b>Jarak:</b> ${jarak}</div>
|
|
`;
|
|
|
|
const actions = document.createElement('div');
|
|
actions.className = 'popup-actions';
|
|
const btnEditData = document.createElement('button');
|
|
btnEditData.type = 'button';
|
|
btnEditData.className = 'popup-btn edit';
|
|
btnEditData.textContent = 'Edit Data';
|
|
const btnEditLoc = document.createElement('button');
|
|
btnEditLoc.type = 'button';
|
|
btnEditLoc.className = 'popup-btn spatial';
|
|
btnEditLoc.textContent = 'Edit Lokasi';
|
|
const btnDelete = document.createElement('button');
|
|
btnDelete.type = 'button';
|
|
btnDelete.className = 'popup-btn delete';
|
|
btnDelete.textContent = 'Hapus';
|
|
|
|
actions.appendChild(btnEditData);
|
|
actions.appendChild(btnEditLoc);
|
|
actions.appendChild(btnDelete);
|
|
popupEl.appendChild(actions);
|
|
|
|
btnEditData.addEventListener('click', () => startEditPemukiman(id, marker, row));
|
|
btnEditLoc.addEventListener('click', () => startEditLocationPemukiman(id, marker, row));
|
|
btnDelete.addEventListener('click', () => deleteFeature('pemukiman', id));
|
|
|
|
marker.bindPopup(popupEl);
|
|
marker.addTo(pemukimanLayer);
|
|
state.indices.pemukiman.set(String(id), { layer: marker, data: row });
|
|
marker.on('click', () => setActiveListItem('pemukiman', id));
|
|
count += 1;
|
|
}
|
|
}
|
|
|
|
state.counts.pemukiman = count;
|
|
state.pemukimanData = Array.isArray(rows) ? rows : [];
|
|
updateStats();
|
|
renderPemukimanList(state.pemukimanData);
|
|
}
|
|
|
|
async function loadRumahIbadah() {
|
|
rumahIbadahLayer.clearLayers();
|
|
rumahIbadahRadiusLayer.clearLayers();
|
|
state.indices.rumahIbadah.clear();
|
|
|
|
let rows = [];
|
|
try {
|
|
const { res, json, text } = await fetchMaybeJson('php/read_rumah_ibadah.php', { cache: 'no-store' });
|
|
if (!res.ok) {
|
|
warnOnce('ibadah_fetch', `Gagal load Rumah Ibadah (HTTP ${res.status}). Cek koneksi database / endpoint.`);
|
|
rows = [];
|
|
} else if (!Array.isArray(json)) {
|
|
warnOnce('ibadah_parse', text ? `Respon rumah ibadah bukan JSON: ${text.slice(0, 120)}` : 'Respon rumah ibadah bukan JSON.');
|
|
rows = [];
|
|
} else {
|
|
rows = json;
|
|
}
|
|
} catch (e) {
|
|
warnOnce('ibadah_err', `Gagal load Rumah Ibadah: ${e?.message || String(e)}`);
|
|
rows = [];
|
|
}
|
|
|
|
if (!Array.isArray(rows)) rows = [];
|
|
state.rumahIbadahData = rows;
|
|
|
|
let count = 0;
|
|
for (const row of rows) {
|
|
const lat = parseFloat(row?.latitude);
|
|
const lng = parseFloat(row?.longitude);
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) continue;
|
|
|
|
const id = String(row?.id ?? `${lat},${lng}`);
|
|
const nama = escapeHtml(row?.nama ?? 'Rumah Ibadah');
|
|
const jenis = escapeHtml(row?.jenis ?? '-');
|
|
const kontak = escapeHtml(row?.kontak ?? '-');
|
|
const radius = Number(row?.radius_meter);
|
|
const binaan = row?.binaan_count != null ? escapeHtml(String(row.binaan_count)) : null;
|
|
|
|
const marker = L.marker([lat, lng], { icon: makeRumahIbadahIcon(), riseOnHover: true });
|
|
const popupEl = document.createElement('div');
|
|
popupEl.innerHTML = `
|
|
<div style="font-weight:800;margin-bottom:6px;">${nama}</div>
|
|
<div><b>Jenis:</b> ${jenis}</div>
|
|
<div><b>Kontak:</b> ${kontak}</div>
|
|
${Number.isFinite(radius) ? `<div><b>Radius:</b> ${radius} m</div>` : ''}
|
|
${binaan !== null ? `<div><b>Binaan:</b> ${binaan}</div>` : ''}
|
|
`;
|
|
|
|
const actions = document.createElement('div');
|
|
actions.className = 'popup-actions';
|
|
const btnEditData = document.createElement('button');
|
|
btnEditData.type = 'button';
|
|
btnEditData.className = 'popup-btn edit';
|
|
btnEditData.textContent = 'Edit Data';
|
|
const btnEditLoc = document.createElement('button');
|
|
btnEditLoc.type = 'button';
|
|
btnEditLoc.className = 'popup-btn spatial';
|
|
btnEditLoc.textContent = 'Edit Lokasi';
|
|
const btnDelete = document.createElement('button');
|
|
btnDelete.type = 'button';
|
|
btnDelete.className = 'popup-btn delete';
|
|
btnDelete.textContent = 'Hapus';
|
|
|
|
actions.appendChild(btnEditData);
|
|
actions.appendChild(btnEditLoc);
|
|
actions.appendChild(btnDelete);
|
|
popupEl.appendChild(actions);
|
|
|
|
btnEditData.addEventListener('click', () => startEditRumahIbadah(id, marker, row));
|
|
btnEditLoc.addEventListener('click', () => startEditLocationRumahIbadah(id, marker, row));
|
|
btnDelete.addEventListener('click', () => deleteFeature('rumahIbadah', id));
|
|
|
|
marker.bindPopup(popupEl);
|
|
marker.addTo(rumahIbadahLayer);
|
|
|
|
if (Number.isFinite(radius) && radius > 0) {
|
|
const circle = L.circle([lat, lng], {
|
|
radius,
|
|
color: '#2563eb',
|
|
weight: 2,
|
|
opacity: 0.7,
|
|
fillColor: '#60a5fa',
|
|
fillOpacity: 0.08
|
|
});
|
|
circle.addTo(rumahIbadahRadiusLayer);
|
|
}
|
|
|
|
state.indices.rumahIbadah.set(id, { layer: marker, data: row });
|
|
marker.on('click', () => setActiveListItem('rumahIbadah', id));
|
|
count += 1;
|
|
}
|
|
|
|
state.counts.rumahIbadah = count;
|
|
updateStats();
|
|
renderRumahIbadahList(rows);
|
|
}
|
|
|
|
async function loadSektor() {
|
|
sektorLayer.clearLayers();
|
|
state.counts.sektor = 0;
|
|
state.indices.sektor.clear();
|
|
|
|
let fc = null;
|
|
try {
|
|
const res = await fetch('js/pontianak_kecamatan.geojson', { cache: 'no-store' });
|
|
if (!res.ok) throw new Error('Network response was not ok');
|
|
fc = await res.json();
|
|
} catch (e) {
|
|
console.error('Failed to load Sektor Kecamatan:', e);
|
|
fc = null;
|
|
}
|
|
|
|
if (!fc || fc.type !== 'FeatureCollection' || !Array.isArray(fc.features)) {
|
|
updateStats();
|
|
renderSektorList([]);
|
|
return;
|
|
}
|
|
|
|
const colors = ['#f43f5e', '#a855f7', '#3b82f6', '#10b981', '#f59e0b', '#64748b'];
|
|
let count = 0;
|
|
|
|
const geo = L.geoJSON(fc, {
|
|
style: (feature) => {
|
|
return {
|
|
color: '#1e293b',
|
|
weight: 1,
|
|
fillColor: colors[count % colors.length],
|
|
fillOpacity: 0.25
|
|
};
|
|
},
|
|
onEachFeature: (feature, layer) => {
|
|
const name = feature?.properties?.Kecamatan ?? feature?.properties?.Name ?? 'Kecamatan';
|
|
const id = name.replace(/\s+/g, '-').toLowerCase();
|
|
|
|
const popupEl = document.createElement('div');
|
|
popupEl.innerHTML = `
|
|
<div style="font-weight:800;margin-bottom:6px;">${name}</div>
|
|
<div><b>Tipe:</b> Sektor Kecamatan</div>
|
|
`;
|
|
|
|
layer.bindPopup(popupEl);
|
|
|
|
if (id) {
|
|
state.indices.sektor.set(id, { layer, data: feature.properties });
|
|
}
|
|
count++;
|
|
}
|
|
});
|
|
|
|
geo.addTo(sektorLayer);
|
|
state.counts.sektor = count;
|
|
updateStats();
|
|
renderSektorList(fc.features);
|
|
}
|
|
|
|
function renderSektorList(features) {
|
|
if (!el.sektorList) return;
|
|
if (!Array.isArray(features) || features.length === 0) {
|
|
renderEmptyList(el.sektorList, 'Belum ada data sektor kecamatan.');
|
|
return;
|
|
}
|
|
|
|
el.sektorList.innerHTML = '';
|
|
for (const f of features) {
|
|
const props = f?.properties ?? {};
|
|
const name = props?.Kecamatan ?? props?.Name ?? 'Kecamatan';
|
|
const id = name.replace(/\s+/g, '-').toLowerCase();
|
|
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'list-item';
|
|
btn.dataset.id = String(id);
|
|
|
|
const title = document.createElement('div');
|
|
title.className = 'list-title';
|
|
title.textContent = name;
|
|
|
|
const meta = document.createElement('div');
|
|
meta.className = 'list-meta';
|
|
meta.textContent = 'Klik untuk zoom';
|
|
|
|
btn.appendChild(title);
|
|
btn.appendChild(meta);
|
|
|
|
btn.addEventListener('click', () => {
|
|
if (el.toggleSektor) el.toggleSektor.checked = true;
|
|
applyLayerVisibility();
|
|
|
|
const record = state.indices.sektor.get(String(id));
|
|
const layer = record?.layer;
|
|
if (layer?.getBounds) {
|
|
map.fitBounds(layer.getBounds(), { padding: [30, 30] });
|
|
}
|
|
if (layer?.openPopup) layer.openPopup();
|
|
setActiveListItem('sektor', id);
|
|
});
|
|
|
|
el.sektorList.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
async function loadAll() {
|
|
try {
|
|
await Promise.all([loadPoints(), loadJalan(), loadParsil(), loadPemukimanMiskin(), loadRumahIbadah(), loadSektor()]);
|
|
} catch {
|
|
updateStats();
|
|
}
|
|
}
|
|
|
|
// ================= MAP TOOLBAR =================
|
|
el.btnResetView?.addEventListener('click', function () {
|
|
map.setView(INITIAL_CENTER, INITIAL_ZOOM);
|
|
});
|
|
|
|
el.btnMyLocation?.addEventListener('click', function () {
|
|
if (!navigator.geolocation) {
|
|
showToast('Browser tidak mendukung geolocation.', 'error');
|
|
return;
|
|
}
|
|
|
|
el.btnMyLocation.disabled = true;
|
|
el.btnMyLocation.textContent = 'Mencari...';
|
|
|
|
const restore = () => {
|
|
el.btnMyLocation.disabled = false;
|
|
el.btnMyLocation.textContent = 'Lokasi Saya';
|
|
};
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
(pos) => {
|
|
const lat = pos.coords.latitude;
|
|
const lng = pos.coords.longitude;
|
|
map.flyTo([lat, lng], Math.max(map.getZoom(), 15), { duration: 1.2 });
|
|
showToast('Lokasi ditemukan.', 'success');
|
|
restore();
|
|
},
|
|
(err) => {
|
|
if (err?.code === 1) {
|
|
showToast('Permintaan lokasi dibatalkan.', 'info');
|
|
} else {
|
|
showToast(err?.message || 'Gagal mendapatkan lokasi.', 'error');
|
|
}
|
|
restore();
|
|
},
|
|
{ enableHighAccuracy: true, timeout: 10000 }
|
|
);
|
|
});
|
|
|
|
el.btnFilter24?.addEventListener('click', () => togglePointFilter('24'));
|
|
el.btnFilterNon24?.addEventListener('click', () => togglePointFilter('non24'));
|
|
|
|
// ================= EDIT BAR =================
|
|
el.btnSaveLocation?.addEventListener('click', saveLocationEdit);
|
|
el.btnCancelLocation?.addEventListener('click', () => cancelLocationEdit());
|
|
el.btnDeleteVertex?.addEventListener('click', () => {
|
|
if (!state.locationEditing) return;
|
|
setVertexDeleteMode(!state.locationEditing.vertexDeleteMode);
|
|
});
|
|
|
|
// ================= UI EVENTS =================
|
|
el.togglePoints?.addEventListener('change', applyLayerVisibility);
|
|
el.toggleJalan?.addEventListener('change', applyLayerVisibility);
|
|
el.toggleParsil?.addEventListener('change', applyLayerVisibility);
|
|
el.toggleRumahIbadah?.addEventListener('change', applyLayerVisibility);
|
|
el.togglePemukiman?.addEventListener('change', applyLayerVisibility);
|
|
el.toggleSektor?.addEventListener('change', applyLayerVisibility);
|
|
|
|
// Prevent toolbar clicks from being treated as map clicks/drag.
|
|
try {
|
|
const toolbar = document.querySelector('.map-toolbar');
|
|
if (toolbar && window.L?.DomEvent) {
|
|
L.DomEvent.disableClickPropagation(toolbar);
|
|
L.DomEvent.disableScrollPropagation(toolbar);
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape') resetMode();
|
|
});
|
|
|
|
// ================= INIT =================
|
|
updateModeInfo();
|
|
applyLayerVisibility();
|
|
loadAll();
|
|
|
|
if (window.location.protocol === 'file:') {
|
|
showToast('Buka aplikasi lewat server (contoh: http://localhost/webgis-spbu/) supaya tombol Simpan & Load data berfungsi.', 'error', 7000);
|
|
}
|
|
|
|
// ================= UTIL =================
|
|
function hitungPanjang(layer) {
|
|
const latlngs = layer.getLatLngs();
|
|
let total = 0;
|
|
for (let i = 0; i < latlngs.length - 1; i++) {
|
|
total += map.distance(latlngs[i], latlngs[i + 1]);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
function hitungLuas(layer) {
|
|
return L.GeometryUtil.geodesicArea(layer.getLatLngs()[0]);
|
|
}
|