function escapeHtml(value) { return String(value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function setStatus(statusLabel, text, type) { statusLabel.textContent = text; statusLabel.classList.remove('error', 'success'); if (type) { statusLabel.classList.add(type); } } function buildPointForm(lat, lng) { return `

Tambah Data SPBU

Klik Save untuk menyimpan data ke database.
`; } function roadColorByStatus(status) { if (status === 'nasional') { return '#dc2626'; } if (status === 'provinsi') { return '#2563eb'; } return '#16a34a'; } function parcelColorByStatus(status) { if (status === 'SHM') { return '#ea580c'; } if (status === 'HGB') { return '#0ea5e9'; } if (status === 'HGU') { return '#22c55e'; } return '#a855f7'; } function calculatePolylineLengthMeters(latlngs) { let total = 0; for (let index = 1; index < latlngs.length; index += 1) { total += latlngs[index - 1].distanceTo(latlngs[index]); } return total; } function calculatePolygonAreaMeters(latlngs) { if (window.L && L.GeometryUtil && typeof L.GeometryUtil.geodesicArea === 'function') { return L.GeometryUtil.geodesicArea(latlngs); } const earthRadius = 6378137; const toRad = (degree) => degree * Math.PI / 180; let area = 0; for (let index = 0; index < latlngs.length; index += 1) { const pointA = latlngs[index]; const pointB = latlngs[(index + 1) % latlngs.length]; area += toRad(pointB.lng - pointA.lng) * (2 + Math.sin(toRad(pointA.lat)) + Math.sin(toRad(pointB.lat))); } return Math.abs(area * earthRadius * earthRadius / 2); } function polylineToWkt(latlngs) { const coordinates = latlngs.map((point) => `${point.lng} ${point.lat}`).join(', '); return `LINESTRING(${coordinates})`; } function polygonToWkt(latlngs) { const ring = latlngs.map((point) => `${point.lng} ${point.lat}`); if (ring[0] !== ring[ring.length - 1]) { ring.push(ring[0]); } return `POLYGON((${ring.join(', ')}))`; } async function parseJsonResponse(response) { const rawText = await response.text(); try { return JSON.parse(rawText); } catch (error) { const preview = String(rawText || '').slice(0, 180).trim(); if (preview) { throw new Error(`Respons server bukan JSON yang valid: ${preview}`); } throw new Error('Respons server bukan JSON yang valid.'); } } async function initializeMap() { if (typeof L === 'undefined') { alert('Leaflet gagal dimuat. Periksa koneksi internet.'); return; } const mapContainer = document.getElementById('map'); if (!mapContainer) { return; } const map = L.map('map', { zoomControl: true }).setView([-0.06054796552220232, 109.34490231930592], 13); L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors' }).addTo(map); if (typeof window.addKecamatanLayer === 'function') { window.addKecamatanLayer(map, { fitBounds: true }).catch((error) => { console.warn('Data kecamatan belum termuat:', error); }); } const markersLayer = L.featureGroup().addTo(map); const spatialLayer = L.featureGroup().addTo(map); let activePopupMarker = null; let isDrawing = false; let activeRefineState = null; const blueIcon = L.icon({ iconUrl: 'assets/vendor/leaflet/images/marker-icon.png', iconRetinaUrl: 'assets/vendor/leaflet/images/marker-icon-2x.png', shadowUrl: 'assets/vendor/leaflet/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] }); const yellowIcon = L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-yellow.png', iconRetinaUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-yellow.png', shadowUrl: 'assets/vendor/leaflet/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] }); const modalBackdrop = document.getElementById('modal-backdrop'); const modalTitle = document.getElementById('modal-title'); const featureForm = document.getElementById('feature-form'); const roadFields = document.getElementById('road-fields'); const parcelFields = document.getElementById('parcel-fields'); const formStatus = document.getElementById('form-status'); const cancelButton = document.getElementById('btn-cancel'); let pendingLayer = null; let pendingType = null; let pendingMode = 'create'; function setSectionEnabled(sectionElement, enabled) { if (!sectionElement) { return; } const fields = sectionElement.querySelectorAll('input, select, textarea, button'); fields.forEach((field) => { field.disabled = !enabled; }); } function getPointIcon(status24Jam) { const normalizedStatus = String(status24Jam || '').toLowerCase(); if (normalizedStatus === '24jam') { return blueIcon; } return yellowIcon; } function fitAllData() { const combined = L.featureGroup(); markersLayer.eachLayer((layer) => combined.addLayer(layer)); spatialLayer.eachLayer((layer) => combined.addLayer(layer)); if (combined.getLayers().length < 1) { return; } const bounds = combined.getBounds(); if (!bounds.isValid()) { return; } map.fitBounds(bounds.pad(0.2), { maxZoom: 16 }); } function cloneLatLngs(latlngs) { return JSON.parse(JSON.stringify(latlngs)); } function setRefineButtonsState(refineButton, saveRefineButton, cancelRefineButton, isActive) { if (refineButton) { refineButton.disabled = isActive; } if (saveRefineButton) { saveRefineButton.disabled = !isActive; } if (cancelRefineButton) { cancelRefineButton.disabled = !isActive; } } async function persistSpatialGeometry(layer) { if (!layer || !layer.featureType || !layer.featureProps) { return; } if (layer.featureType === 'road') { const lengthMeters = calculatePolylineLengthMeters(layer.getLatLngs()); const payload = { id: layer.featureId, nama_jalan: layer.featureProps.nama_jalan, status: layer.featureProps.status, panjang_meter: Number(lengthMeters.toFixed(2)), wkt: polylineToWkt(layer.getLatLngs()) }; const response = await fetch('api/road_update.php', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payload) }); const result = await parseJsonResponse(response); if (!response.ok || !result.success) { throw new Error(result.message || 'Gagal update geometri jalan.'); } layer.featureProps.panjang_meter = payload.panjang_meter; attachRoadPopup(layer); return; } if (layer.featureType === 'parcel') { const areaM2 = calculatePolygonAreaMeters(layer.getLatLngs()[0]); const payload = { id: layer.featureId, nama_pemilik: layer.featureProps.nama_pemilik, status_kepemilikan: layer.featureProps.status_kepemilikan, luas_m2: Number(areaM2.toFixed(2)), wkt: polygonToWkt(layer.getLatLngs()[0]) }; const response = await fetch('api/parcel_update.php', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payload) }); const result = await parseJsonResponse(response); if (!response.ok || !result.success) { throw new Error(result.message || 'Gagal update geometri parsil.'); } layer.featureProps.luas_m2 = payload.luas_m2; attachParcelPopup(layer); } } function closeModal(removePendingLayer) { if (!modalBackdrop || !featureForm || !formStatus) { return; } modalBackdrop.classList.remove('open'); modalBackdrop.setAttribute('aria-hidden', 'true'); setStatus(formStatus, '', ''); if (removePendingLayer && pendingMode === 'create' && pendingLayer) { spatialLayer.removeLayer(pendingLayer); } pendingLayer = null; pendingType = null; pendingMode = 'create'; featureForm.reset(); setSectionEnabled(roadFields, false); setSectionEnabled(parcelFields, false); } function openRoadModal(layer, mode, initialData) { if (!modalBackdrop || !modalTitle || !roadFields || !parcelFields) { return; } pendingLayer = layer; pendingType = 'road'; pendingMode = mode; modalTitle.textContent = mode === 'create' ? 'Tambah Data Jalan' : 'Update Data Jalan'; roadFields.hidden = false; parcelFields.hidden = true; setSectionEnabled(roadFields, true); setSectionEnabled(parcelFields, false); const latlngs = layer.getLatLngs(); const lengthMeters = calculatePolylineLengthMeters(latlngs); document.getElementById('nama_jalan').value = initialData && initialData.nama_jalan ? initialData.nama_jalan : ''; document.getElementById('status_jalan').value = initialData && initialData.status ? initialData.status : 'nasional'; document.getElementById('panjang_meter').value = lengthMeters.toFixed(2); modalBackdrop.classList.add('open'); modalBackdrop.setAttribute('aria-hidden', 'false'); } function openParcelModal(layer, mode, initialData) { if (!modalBackdrop || !modalTitle || !roadFields || !parcelFields) { return; } pendingLayer = layer; pendingType = 'parcel'; pendingMode = mode; modalTitle.textContent = mode === 'create' ? 'Tambah Data Parsil Tanah' : 'Update Data Parsil Tanah'; roadFields.hidden = true; parcelFields.hidden = false; setSectionEnabled(roadFields, false); setSectionEnabled(parcelFields, true); const latlngs = layer.getLatLngs()[0]; const areaM2 = calculatePolygonAreaMeters(latlngs); document.getElementById('nama_pemilik').value = initialData && initialData.nama_pemilik ? initialData.nama_pemilik : ''; document.getElementById('status_kepemilikan').value = initialData && initialData.status_kepemilikan ? initialData.status_kepemilikan : 'SHM'; document.getElementById('luas_m2').value = areaM2.toFixed(2); modalBackdrop.classList.add('open'); modalBackdrop.setAttribute('aria-hidden', 'false'); } function renderPoint(point) { const pointId = Number(point.id); const latitude = Number(point.latitude); const longitude = Number(point.longitude); if (!Number.isFinite(pointId) || !Number.isFinite(latitude) || !Number.isFinite(longitude)) { return; } const marker = L.marker([latitude, longitude], { icon: getPointIcon(point.status_24jam), draggable: true }); marker.addTo(markersLayer); marker.bindPopup(`
${escapeHtml(point.nama || '')}
No: ${escapeHtml(point.no || '')}
Status: ${escapeHtml(point.status_24jam || '')}
Lat: ${latitude.toFixed(6)}
Lng: ${longitude.toFixed(6)}
Geser marker untuk memperbarui posisi titik.
ID: ${pointId}
`); let previousLatLng = null; marker.on('dragstart', () => { previousLatLng = marker.getLatLng(); }); marker.on('dragend', async () => { const latestLatLng = marker.getLatLng(); try { await updatePointPosition(pointId, latestLatLng.lat, latestLatLng.lng); await loadPoints(); } catch (error) { if (previousLatLng) { marker.setLatLng(previousLatLng); } alert(error.message || 'Gagal memperbarui posisi titik.'); } }); marker.on('popupopen', (popupEvent) => { const popupElement = popupEvent.popup.getElement(); if (!popupElement) { return; } const deleteButton = popupElement.querySelector('[data-action="delete"]'); const deleteStatus = popupElement.querySelector('[data-role="delete-status"]'); if (!deleteButton || !deleteStatus) { return; } deleteButton.addEventListener('click', async () => { const requestedId = Number(deleteButton.getAttribute('data-point-id')); if (!Number.isFinite(requestedId) || requestedId <= 0) { setStatus(deleteStatus, 'ID titik tidak valid.', 'error'); return; } const isConfirmed = window.confirm('Hapus titik ini dari peta dan database?'); if (!isConfirmed) { return; } deleteButton.disabled = true; setStatus(deleteStatus, 'Menghapus data...', ''); try { await deletePoint(requestedId); setStatus(deleteStatus, 'Data berhasil dihapus.', 'success'); map.closePopup(); await loadPoints(); } catch (error) { setStatus(deleteStatus, error.message || 'Gagal menghapus data.', 'error'); deleteButton.disabled = false; } }); }); } function attachRoadPopup(layer) { const props = layer.featureProps; layer.setStyle({ color: roadColorByStatus(props.status), weight: 5 }); layer.bindPopup(` Jalan: ${escapeHtml(props.nama_jalan)}
Status: ${escapeHtml(props.status)}
Panjang: ${Number(props.panjang_meter).toFixed(2)} m
`); layer.off('popupopen'); layer.on('popupopen', (event) => { const popupElement = event.popup.getElement(); if (!popupElement) { return; } const editButton = popupElement.querySelector('[data-action="edit"]'); const refineButton = popupElement.querySelector('[data-action="refine"]'); const saveRefineButton = popupElement.querySelector('[data-action="save-refine"]'); const cancelRefineButton = popupElement.querySelector('[data-action="cancel-refine"]'); const deleteButton = popupElement.querySelector('[data-action="delete"]'); const popupStatus = popupElement.querySelector('[data-role="popup-status"]'); if (!editButton || !refineButton || !saveRefineButton || !cancelRefineButton || !deleteButton || !popupStatus) { return; } const canRefine = !!(layer.editing && typeof layer.editing.enable === 'function'); refineButton.disabled = !canRefine; setRefineButtonsState(refineButton, saveRefineButton, cancelRefineButton, activeRefineState && activeRefineState.layer === layer); refineButton.addEventListener('click', () => { if (!canRefine) { setStatus(popupStatus, 'Mode rapikan tidak didukung pada layer ini.', 'error'); return; } if (activeRefineState && activeRefineState.layer && activeRefineState.layer !== layer && activeRefineState.layer.editing) { activeRefineState.layer.editing.disable(); } activeRefineState = { layer: layer, snapshot: cloneLatLngs(layer.getLatLngs()) }; layer.editing.enable(); setRefineButtonsState(refineButton, saveRefineButton, cancelRefineButton, true); setStatus(popupStatus, 'Mode rapikan aktif. Geser titik vertex lalu klik Simpan Rapikan.', ''); }); saveRefineButton.addEventListener('click', async () => { if (!activeRefineState || activeRefineState.layer !== layer) { return; } saveRefineButton.disabled = true; setStatus(popupStatus, 'Menyimpan geometri...', ''); try { await persistSpatialGeometry(layer); if (layer.editing) { layer.editing.disable(); } activeRefineState = null; setRefineButtonsState(refineButton, saveRefineButton, cancelRefineButton, false); setStatus(popupStatus, 'Geometri jalan berhasil dirapikan.', 'success'); } catch (error) { setStatus(popupStatus, error.message || 'Gagal menyimpan geometri.', 'error'); saveRefineButton.disabled = false; } }); cancelRefineButton.addEventListener('click', () => { if (!activeRefineState || activeRefineState.layer !== layer) { return; } layer.setLatLngs(activeRefineState.snapshot); if (typeof layer.redraw === 'function') { layer.redraw(); } if (layer.editing) { layer.editing.disable(); } activeRefineState = null; setRefineButtonsState(refineButton, saveRefineButton, cancelRefineButton, false); setStatus(popupStatus, 'Perubahan geometri dibatalkan.', ''); }); editButton.addEventListener('click', () => { map.closePopup(); openRoadModal(layer, 'update', layer.featureProps); }); deleteButton.addEventListener('click', async () => { if (!window.confirm('Hapus data jalan ini?')) { return; } setStatus(popupStatus, 'Menghapus data...', ''); deleteButton.disabled = true; try { const response = await fetch('api/road_delete.php', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ id: layer.featureId }) }); const result = await parseJsonResponse(response); if (!response.ok || !result.success) { throw new Error(result.message || 'Gagal menghapus data jalan.'); } spatialLayer.removeLayer(layer); map.closePopup(); } catch (error) { setStatus(popupStatus, error.message || 'Gagal menghapus data.', 'error'); deleteButton.disabled = false; } }); }); } function attachParcelPopup(layer) { const props = layer.featureProps; const color = parcelColorByStatus(props.status_kepemilikan); layer.setStyle({ color: color, fillColor: color, fillOpacity: 0.35, weight: 3 }); layer.bindPopup(` Pemilik: ${escapeHtml(props.nama_pemilik)}
Status: ${escapeHtml(props.status_kepemilikan)}
Luas: ${Number(props.luas_m2).toFixed(2)} m2
`); layer.off('popupopen'); layer.on('popupopen', (event) => { const popupElement = event.popup.getElement(); if (!popupElement) { return; } const editButton = popupElement.querySelector('[data-action="edit"]'); const refineButton = popupElement.querySelector('[data-action="refine"]'); const saveRefineButton = popupElement.querySelector('[data-action="save-refine"]'); const cancelRefineButton = popupElement.querySelector('[data-action="cancel-refine"]'); const deleteButton = popupElement.querySelector('[data-action="delete"]'); const popupStatus = popupElement.querySelector('[data-role="popup-status"]'); if (!editButton || !refineButton || !saveRefineButton || !cancelRefineButton || !deleteButton || !popupStatus) { return; } const canRefine = !!(layer.editing && typeof layer.editing.enable === 'function'); refineButton.disabled = !canRefine; setRefineButtonsState(refineButton, saveRefineButton, cancelRefineButton, activeRefineState && activeRefineState.layer === layer); refineButton.addEventListener('click', () => { if (!canRefine) { setStatus(popupStatus, 'Mode rapikan tidak didukung pada layer ini.', 'error'); return; } if (activeRefineState && activeRefineState.layer && activeRefineState.layer !== layer && activeRefineState.layer.editing) { activeRefineState.layer.editing.disable(); } activeRefineState = { layer: layer, snapshot: cloneLatLngs(layer.getLatLngs()) }; layer.editing.enable(); setRefineButtonsState(refineButton, saveRefineButton, cancelRefineButton, true); setStatus(popupStatus, 'Mode rapikan aktif. Geser titik vertex lalu klik Simpan Rapikan.', ''); }); saveRefineButton.addEventListener('click', async () => { if (!activeRefineState || activeRefineState.layer !== layer) { return; } saveRefineButton.disabled = true; setStatus(popupStatus, 'Menyimpan geometri...', ''); try { await persistSpatialGeometry(layer); if (layer.editing) { layer.editing.disable(); } activeRefineState = null; setRefineButtonsState(refineButton, saveRefineButton, cancelRefineButton, false); setStatus(popupStatus, 'Geometri parsil berhasil dirapikan.', 'success'); } catch (error) { setStatus(popupStatus, error.message || 'Gagal menyimpan geometri.', 'error'); saveRefineButton.disabled = false; } }); cancelRefineButton.addEventListener('click', () => { if (!activeRefineState || activeRefineState.layer !== layer) { return; } layer.setLatLngs(activeRefineState.snapshot); if (typeof layer.redraw === 'function') { layer.redraw(); } if (layer.editing) { layer.editing.disable(); } activeRefineState = null; setRefineButtonsState(refineButton, saveRefineButton, cancelRefineButton, false); setStatus(popupStatus, 'Perubahan geometri dibatalkan.', ''); }); editButton.addEventListener('click', () => { map.closePopup(); openParcelModal(layer, 'update', layer.featureProps); }); deleteButton.addEventListener('click', async () => { if (!window.confirm('Hapus data parsil ini?')) { return; } setStatus(popupStatus, 'Menghapus data...', ''); deleteButton.disabled = true; try { const response = await fetch('api/parcel_delete.php', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ id: layer.featureId }) }); const result = await parseJsonResponse(response); if (!response.ok || !result.success) { throw new Error(result.message || 'Gagal menghapus data parsil.'); } spatialLayer.removeLayer(layer); map.closePopup(); } catch (error) { setStatus(popupStatus, error.message || 'Gagal menghapus data.', 'error'); deleteButton.disabled = false; } }); }); } async function loadPoints() { const response = await fetch('api/read_points.php', { headers: { 'Accept': 'application/json' } }); const result = await parseJsonResponse(response); if (!response.ok || !result.success) { throw new Error(result && result.message ? result.message : 'Gagal memuat data titik.'); } markersLayer.clearLayers(); (result.data || []).forEach(renderPoint); } async function loadRoads() { const response = await fetch('api/road_read.php', { headers: { 'Accept': 'application/json' } }); const result = await parseJsonResponse(response); if (!response.ok || !result.success) { throw new Error(result && result.message ? result.message : 'Gagal memuat data jalan.'); } (result.data || []).forEach((item) => { if (!item.geometry_geojson) { return; } const parsed = L.geoJSON(item.geometry_geojson); const layer = parsed.getLayers()[0]; if (!layer) { return; } layer.featureType = 'road'; layer.featureId = item.id; layer.featureProps = { nama_jalan: item.nama_jalan, status: item.status, panjang_meter: item.panjang_meter }; attachRoadPopup(layer); spatialLayer.addLayer(layer); }); } async function loadParcels() { const response = await fetch('api/parcel_read.php', { headers: { 'Accept': 'application/json' } }); const result = await parseJsonResponse(response); if (!response.ok || !result.success) { throw new Error(result && result.message ? result.message : 'Gagal memuat data parsil.'); } (result.data || []).forEach((item) => { if (!item.geometry_geojson) { return; } const parsed = L.geoJSON(item.geometry_geojson); const layer = parsed.getLayers()[0]; if (!layer) { return; } layer.featureType = 'parcel'; layer.featureId = item.id; layer.featureProps = { nama_pemilik: item.nama_pemilik, status_kepemilikan: item.status_kepemilikan, luas_m2: item.luas_m2 }; attachParcelPopup(layer); spatialLayer.addLayer(layer); }); } async function savePoint(payload) { const response = await fetch('api/create_point.php', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payload) }); const result = await parseJsonResponse(response); if (!response.ok || !result.success) { throw new Error(result && result.message ? result.message : 'Gagal menyimpan data titik.'); } return result; } async function deletePoint(id) { const response = await fetch('api/delete_point.php', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ id: id }) }); const result = await parseJsonResponse(response); if (!response.ok || !result.success) { throw new Error(result && result.message ? result.message : 'Gagal menghapus data titik.'); } return result; } async function updatePointPosition(id, latitude, longitude) { const response = await fetch('api/update_point_position.php', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ id: id, latitude: latitude, longitude: longitude }) }); const result = await parseJsonResponse(response); if (!response.ok || !result.success) { throw new Error(result && result.message ? result.message : 'Gagal memperbarui posisi titik.'); } return result; } if (L.Control && L.Control.Draw) { const drawControl = new L.Control.Draw({ position: 'topleft', draw: { polyline: { shapeOptions: { color: '#dc2626', weight: 5 } }, polygon: { allowIntersection: false, showArea: true, shapeOptions: { color: '#ea580c', fillOpacity: 0.35, weight: 3 } }, rectangle: false, circle: false, marker: false, circlemarker: false }, edit: { featureGroup: spatialLayer, remove: true } }); map.addControl(drawControl); map.on(L.Draw.Event.DRAWSTART, () => { isDrawing = true; }); map.on(L.Draw.Event.DRAWSTOP, () => { isDrawing = false; }); map.on(L.Draw.Event.CREATED, (event) => { const layer = event.layer; spatialLayer.addLayer(layer); if (event.layerType === 'polyline') { openRoadModal(layer, 'create', null); return; } if (event.layerType === 'polygon') { openParcelModal(layer, 'create', null); return; } spatialLayer.removeLayer(layer); }); map.on(L.Draw.Event.EDITED, async (event) => { const layers = event.layers.getLayers(); for (const layer of layers) { try { await persistSpatialGeometry(layer); } catch (error) { alert(error.message || 'Gagal menyimpan update geometri.'); } } }); map.on(L.Draw.Event.DELETED, async (event) => { const layers = event.layers.getLayers(); for (const layer of layers) { if (!layer.featureType || !layer.featureId) { continue; } try { const endpoint = layer.featureType === 'road' ? 'api/road_delete.php' : 'api/parcel_delete.php'; const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ id: layer.featureId }) }); const result = await parseJsonResponse(response); if (!response.ok || !result.success) { throw new Error(result.message || 'Gagal menghapus data spasial.'); } } catch (error) { alert(error.message || 'Gagal sinkron delete ke database.'); } } }); } if (featureForm) { setSectionEnabled(roadFields, false); setSectionEnabled(parcelFields, false); featureForm.addEventListener('submit', async (event) => { event.preventDefault(); if (!pendingLayer || !pendingType || !formStatus) { return; } setStatus(formStatus, 'Menyimpan data...', ''); try { if (pendingType === 'road') { const namaJalan = document.getElementById('nama_jalan').value.trim(); const statusJalan = document.getElementById('status_jalan').value; const panjangMeter = Number(document.getElementById('panjang_meter').value); const wkt = polylineToWkt(pendingLayer.getLatLngs()); const endpoint = pendingMode === 'create' ? 'api/road_create.php' : 'api/road_update.php'; const payload = { nama_jalan: namaJalan, status: statusJalan, panjang_meter: panjangMeter, wkt: wkt }; if (pendingMode === 'update') { payload.id = pendingLayer.featureId; } const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payload) }); const result = await parseJsonResponse(response); if (!response.ok || !result.success) { throw new Error(result.message || 'Gagal menyimpan data jalan.'); } pendingLayer.featureType = 'road'; pendingLayer.featureId = pendingMode === 'create' ? result.id : pendingLayer.featureId; pendingLayer.featureProps = { nama_jalan: namaJalan, status: statusJalan, panjang_meter: panjangMeter }; attachRoadPopup(pendingLayer); } if (pendingType === 'parcel') { const namaPemilik = document.getElementById('nama_pemilik').value.trim(); const statusKepemilikan = document.getElementById('status_kepemilikan').value; const luasM2 = Number(document.getElementById('luas_m2').value); const wkt = polygonToWkt(pendingLayer.getLatLngs()[0]); const endpoint = pendingMode === 'create' ? 'api/parcel_create.php' : 'api/parcel_update.php'; const payload = { nama_pemilik: namaPemilik, status_kepemilikan: statusKepemilikan, luas_m2: luasM2, wkt: wkt }; if (pendingMode === 'update') { payload.id = pendingLayer.featureId; } const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payload) }); const result = await parseJsonResponse(response); if (!response.ok || !result.success) { throw new Error(result.message || 'Gagal menyimpan data parsil.'); } pendingLayer.featureType = 'parcel'; pendingLayer.featureId = pendingMode === 'create' ? result.id : pendingLayer.featureId; pendingLayer.featureProps = { nama_pemilik: namaPemilik, status_kepemilikan: statusKepemilikan, luas_m2: luasM2 }; attachParcelPopup(pendingLayer); } setStatus(formStatus, 'Data berhasil disimpan.', 'success'); setTimeout(() => closeModal(false), 300); } catch (error) { setStatus(formStatus, error.message || 'Gagal menyimpan data.', 'error'); } }); } if (cancelButton) { cancelButton.addEventListener('click', () => { closeModal(true); }); } if (modalBackdrop) { modalBackdrop.addEventListener('click', (event) => { if (event.target === modalBackdrop) { closeModal(true); } }); } map.on('click', (event) => { if (activeRefineState && activeRefineState.layer && activeRefineState.layer.editing && activeRefineState.layer.editing.enabled()) { return; } if (isDrawing || (modalBackdrop && modalBackdrop.classList.contains('open'))) { return; } if (event.originalEvent && event.originalEvent.target && event.originalEvent.target.closest('.leaflet-interactive')) { return; } if (activePopupMarker) { map.removeLayer(activePopupMarker); } const lat = event.latlng.lat; const lng = event.latlng.lng; activePopupMarker = L.marker([lat, lng]).addTo(map); activePopupMarker.bindPopup(buildPointForm(lat, lng)); activePopupMarker.once('popupopen', (popupEvent) => { const popupElement = popupEvent.popup.getElement(); if (!popupElement) { return; } const saveButton = popupElement.querySelector('[data-action="save"]'); const cancelPopupButton = popupElement.querySelector('[data-action="cancel"]'); const statusLabel = popupElement.querySelector('[data-role="status"]'); const namaInput = popupElement.querySelector('[data-field="nama"]'); const noInput = popupElement.querySelector('[data-field="no"]'); const statusInput = popupElement.querySelector('[data-field="status_24jam"]'); const latitudeInput = popupElement.querySelector('[data-field="latitude"]'); const longitudeInput = popupElement.querySelector('[data-field="longitude"]'); if (!saveButton || !cancelPopupButton || !statusLabel || !namaInput || !noInput || !statusInput || !latitudeInput || !longitudeInput) { return; } cancelPopupButton.addEventListener('click', () => { map.closePopup(); if (activePopupMarker) { map.removeLayer(activePopupMarker); activePopupMarker = null; } }); saveButton.addEventListener('click', async () => { const nama = namaInput.value.trim(); const no = noInput.value.trim(); const status24jam = statusInput.value; const latitude = latitudeInput.value; const longitude = longitudeInput.value; if (!nama || !no) { setStatus(statusLabel, 'Nama dan No wajib diisi.', 'error'); return; } saveButton.disabled = true; setStatus(statusLabel, 'Menyimpan data...', ''); try { await savePoint({ nama: nama, no: no, status_24jam: status24jam, latitude: latitude, longitude: longitude }); setStatus(statusLabel, 'Data berhasil disimpan.', 'success'); if (activePopupMarker) { map.removeLayer(activePopupMarker); activePopupMarker = null; } await loadPoints(); map.closePopup(); } catch (error) { setStatus(statusLabel, error.message || 'Terjadi kesalahan saat menyimpan data.', 'error'); saveButton.disabled = false; } }); }); activePopupMarker.openPopup(); }); spatialLayer.clearLayers(); try { await loadPoints(); } catch (error) { console.error(error); alert(error.message || 'Gagal memuat data titik.'); } try { await loadRoads(); } catch (error) { console.warn('Data jalan belum termuat:', error); } try { await loadParcels(); } catch (error) { console.warn('Data parsil belum termuat:', error); } fitAllData(); const syncMapSize = () => { map.invalidateSize(); }; window.addEventListener('resize', syncMapSize); window.addEventListener('load', () => { setTimeout(syncMapSize, 0); }); } document.addEventListener('DOMContentLoaded', () => { initializeMap().catch((error) => { console.error(error); alert('Terjadi kesalahan saat inisialisasi peta.'); }); });