// --- INISIALISASI PETA (Koordinat Pontianak) --- const map = L.map('map').setView([-0.055348, 109.349512], 13); L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map); const popup = L.popup(); const daftarMarker = {}, daftarPolyline = {}, daftarPolygon = {}; // 1. BUAT KERTAS TRANSPARAN (LAYER GROUPS) const layerSPBU24 = L.layerGroup().addTo(map); // SPBU 24 Jam const layerSPBUNon24 = L.layerGroup().addTo(map); // SPBU Tidak 24 Jam const layerJalan = L.layerGroup().addTo(map); // Jaringan Jalan const layerKavling = L.layerGroup().addTo(map); // Area Kavling Tanah // 2. DAFTARKAN LAYER-LAYER TERSEBUT KE DALAM CONTROL PANEL const overlays = { " SPBU (Buka 24 Jam)": layerSPBU24, " SPBU (Tidak 24 Jam)": layerSPBUNon24, " Jaringan Jalan": layerJalan, " Kavling Tanah": layerKavling }; // 3. MUNCULKAN KONTROL DI POJOK KANAN ATAS PETA L.control.layers(null, overlays, { collapsed: false }).addTo(map); let currentMode = 'point'; let tempLatLngs = [], tempPolyline = null; let tempPolygonLatLngs = [], tempPolygon = null; let targetEditJalanId = null; // Menyimpan ID jalan yang sedang diedit let targetEditKavlingId = null; // Menyimpan ID kavling yang sedang diedit // Konfigurasi SweetAlert2 (Toast Pojok Kanan Atas) const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 3000, timerProgressBar: true }); // Definisi Ikon Kustom const ikonHijau = new L.Icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] }); const ikonMerah = new L.Icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] }); // --- MANAJEMEN MODE UI --- function ubahMode(mode) { currentMode = mode; // Matikan mode edit Geoman global jika sedang aktif map.pm.disableGlobalEditMode(); // Reset state edit if (targetEditJalanId) batalkanEditJalan(); if (targetEditKavlingId) batalkanEditKavling(); // Atur tampilan tombol aktif document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active')); document.querySelector(`.mode-btn[data-mode="${mode}"]`).classList.add('active'); // Munculkan menu yang sesuai di sidebar document.getElementById('polyline-actions').style.display = (mode === 'polyline') ? 'block' : 'none'; document.getElementById('polygon-actions').style.display = (mode === 'polygon') ? 'block' : 'none'; document.getElementById('edit-jalan-actions').style.display = (mode === 'edit_jalan') ? 'block' : 'none'; document.getElementById('edit-kavling-actions').style.display = (mode === 'edit_kavling') ? 'block' : 'none'; map.closePopup(); if (mode !== 'polyline') batalPolyline(); if (mode !== 'polygon') batalPolygon(); } // --- FUNGSI BANTUAN --- function hitungPanjangJalan(latlngs) { let t=0; for (let i=0; i res.json()).then(data => { data.forEach(spbu => { let targetLayer = (spbu.buka_24jam == '1') ? layerSPBU24 : layerSPBUNon24; let marker = L.marker([spbu.lat, spbu.lng], { icon: (spbu.buka_24jam == '1') ? ikonHijau : ikonMerah, draggable: true }).addTo(targetLayer); marker.bindPopup(`${spbu.nama_spbu}
WA: ${spbu.no_wa}
24 Jam: ${spbu.buka_24jam == '1' ? 'Ya' : 'Tidak'}
`); marker.on('dragend', function(e) { updateLokasiSPBU(spbu.id, e.target.getLatLng().lat, e.target.getLatLng().lng); }); daftarMarker[spbu.id] = marker; }); }); fetch('load_jalan.php').then(res => res.json()).then(data => { data.forEach(jalan => { let geojsonGroup = L.geoJSON(jalan.geojson, { style: { color: warnaJalan(jalan.status), weight: 4 } }).addTo(layerJalan); let polylineLayer = geojsonGroup.getLayers()[0]; // Ambil layer aslinya polylineLayer.bindPopup(`Jalan: ${jalan.nama_jalan}
Status: ${jalan.status}
Panjang: ${parseFloat(jalan.panjang_m).toFixed(2)} m
`); // Tambahkan event click khusus untuk mode edit polylineLayer.on('click', function(e) { if (currentMode === 'edit_jalan') { aktifkanEditJalan(jalan.id, jalan.nama_jalan); } }); daftarPolyline[jalan.id] = polylineLayer; }); }); fetch('load_area.php').then(res => res.json()).then(data => { data.forEach(area => { let geojsonGroup = L.geoJSON(area.geojson, { style: { color: warnaPolygon(area.status_kepemilikan), fillOpacity: 0.4 } }).addTo(layerKavling); let polygonLayer = geojsonGroup.getLayers()[0]; // Ambil layer aslinya polygonLayer.bindPopup(`Kavling: ${area.nama_kavling}
Status: ${area.status_kepemilikan}
Luas: ${parseFloat(area.luas_m2).toFixed(2)} m²
`); // Tambahkan event click khusus untuk mode edit polygonLayer.on('click', function(e) { if (currentMode === 'edit_kavling') { aktifkanEditKavling(area.id, area.nama_kavling); } }); daftarPolygon[area.id] = polygonLayer; }); }); } loadDB(); // --- FUNGSI UNTUK ME-REFRESH PETA SECARA OTOMATIS (SOFT REFRESH) --- function refreshPeta() { // 1. Bersihkan semua layer dari peta layerSPBU24.clearLayers(); layerSPBUNon24.clearLayers(); layerJalan.clearLayers(); layerKavling.clearLayers(); // 2. Bersihkan memori agar ID tidak menumpuk/ganda for (let id in daftarMarker) delete daftarMarker[id]; for (let id in daftarPolyline) delete daftarPolyline[id]; for (let id in daftarPolygon) delete daftarPolygon[id]; // 3. Panggil ulang data terbaru dari database loadDB(); } // --- EVENT KLIK PETA --- function onMapClick(e) { if (currentMode === 'point') { let formHtml = `

SPBU Baru


 

`; popup.setLatLng(e.latlng).setContent(formHtml).openOn(map); } else if (currentMode === 'polyline') { tempLatLngs.push(e.latlng); if (!tempPolyline) tempPolyline = L.polyline(tempLatLngs, {color: '#3498db', weight: 4}).addTo(map); else tempPolyline.setLatLngs(tempLatLngs); } else if (currentMode === 'polygon') { tempPolygonLatLngs.push(e.latlng); if (!tempPolygon) tempPolygon = L.polygon(tempPolygonLatLngs, {color: '#2ecc71', fillColor: '#2ecc71', fillOpacity: 0.4}).addTo(map); else tempPolygon.setLatLngs(tempPolygonLatLngs); } } map.on('click', onMapClick); // --- LOGIKA SPBU --- function simpanSPBU(event, lat, lng) { event.preventDefault(); let dataKirim = { lat: lat, lng: lng, nama_spbu: document.getElementById('nama_spbu').value, no_wa: document.getElementById('no_wa').value, buka_24jam: document.querySelector('input[name="buka_24jam"]:checked').value }; fetch('simpan_spbu.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(dataKirim) }) .then(res => res.json()).then(data => { if (data.status === 'success') { Toast.fire({ icon: 'success', title: 'SPBU Berhasil Disimpan!' }); map.closePopup(); refreshPeta(); let targetLayerBaru = (dataKirim.buka_24jam === '1') ? layerSPBU24 : layerSPBUNon24; let markerBaru = L.marker([lat, lng], { icon: (dataKirim.buka_24jam === '1') ? ikonHijau : ikonMerah, draggable: true }).addTo(targetLayerBaru); markerBaru.bindPopup(`${dataKirim.nama_spbu}
WA: ${dataKirim.no_wa}
24 Jam: ${dataKirim.buka_24jam == '1' ? 'Ya' : 'Tidak'}
`); markerBaru.on('dragend', function(e) { updateLokasiSPBU(data.id, e.target.getLatLng().lat, e.target.getLatLng().lng); }); daftarMarker[data.id] = markerBaru; } else Toast.fire({ icon: 'error', title: 'Gagal Menyimpan SPBU!' }); }); } function updateLokasiSPBU(id, latBaru, lngBaru) { fetch('update_spbu.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: id, lat: latBaru, lng: lngBaru }) }) .then(res => res.json()).then(data => { if (data.status === 'success') Toast.fire({ icon: 'info', title: 'Posisi Diperbarui' }); }); } function hapusSPBU(id) { Swal.fire({ title: 'Hapus Titik SPBU?', text: "Data tidak dapat dikembalikan!", icon: 'warning', showCancelButton: true, confirmButtonColor: '#d33', cancelButtonColor: '#3085d6', confirmButtonText: 'Ya, Hapus!', cancelButtonText: 'Batal' }).then((result) => { if (result.isConfirmed) { fetch('hapus_spbu.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: id }) }) .then(res => res.json()).then(data => { if (data.status === 'success') { Toast.fire({ icon: 'success', title: 'Titik SPBU Dihapus!' }); if (daftarMarker[id]) { map.removeLayer(daftarMarker[id]); delete daftarMarker[id]; } } else Toast.fire({ icon: 'error', title: 'Gagal Menghapus!' }); }); } }); } // --- LOGIKA JALAN --- function batalPolyline() { if (tempPolyline) { map.removeLayer(tempPolyline); tempPolyline = null; } tempLatLngs = []; } function simpanPolyline() { if (tempLatLngs.length < 2) return Toast.fire({ icon: 'warning', title: 'Klik minimal 2 titik di peta!' }); let nama = document.getElementById('input_nama_jalan').value, status = document.getElementById('input_status_jalan').value; if (!nama) return Toast.fire({ icon: 'warning', title: 'Nama Jalan harus diisi!' }); let panjang = hitungPanjangJalan(tempLatLngs); let dataKirim = { nama_jalan: nama, status: status, panjang_m: panjang, geojson: tempPolyline.toGeoJSON() }; fetch('simpan_jalan.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(dataKirim) }) .then(res => res.json()).then(data => { if (data.status === 'success') { Toast.fire({ icon: 'success', title: `Jalan Disimpan (${panjang.toFixed(0)}m)` }); tempPolyline.setStyle({color: warnaJalan(status)}); tempPolyline.bindPopup(`Jalan: ${nama}
Status: ${status}
Panjang: ${panjang.toFixed(2)} m
`).openPopup(); layerJalan.addLayer(tempPolyline); map.removeLayer(tempPolyline); tempPolyline.on('click', function(e) { if (currentMode === 'edit_jalan') { aktifkanEditJalan(data.id, nama); } }); daftarPolyline[data.id] = tempPolyline; document.getElementById('input_nama_jalan').value = ''; refreshPeta(); // PERBAIKAN BUG: Hanya kosongkan variabel penampung sementara, jangan removeLayer dari map. tempPolyline = null; tempLatLngs = []; } else Toast.fire({ icon: 'error', title: 'Gagal Menyimpan!' }); }); } function hapusPolyline(id) { Swal.fire({ title: 'Hapus Jalan?', text: "Data permanen dihapus!", icon: 'warning', showCancelButton: true, confirmButtonColor: '#d33', cancelButtonColor: '#3085d6', confirmButtonText: 'Ya, Hapus!' }).then((result) => { if (result.isConfirmed) { fetch('hapus_jalan.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: id }) }) .then(res => res.json()).then(data => { if (data.status === 'success') { Toast.fire({ icon: 'success', title: 'Jalan Dihapus!' }); if (daftarPolyline[id]) { map.removeLayer(daftarPolyline[id]); delete daftarPolyline[id]; } } else Toast.fire({ icon: 'error', title: 'Gagal Menghapus!' }); }); } }); } // --- LOGIKA KAVLING --- function batalPolygon() { if (tempPolygon) { map.removeLayer(tempPolygon); tempPolygon = null; } tempPolygonLatLngs = []; } function simpanPolygon() { if (tempPolygonLatLngs.length < 3) return Toast.fire({ icon: 'warning', title: 'Minimal 3 titik untuk area!' }); let nama = document.getElementById('input_nama_kavling').value, status = document.getElementById('input_status_tanah').value; if (!nama) return Toast.fire({ icon: 'warning', title: 'Nama kavling harus diisi!' }); let luas = hitungLuasArea(tempPolygonLatLngs); let dataKirim = { nama_kavling: nama, status: status, luas_m2: luas, geojson: tempPolygon.toGeoJSON() }; fetch('simpan_area.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(dataKirim) }) .then(res => res.json()).then(data => { if (data.status === 'success') { Toast.fire({ icon: 'success', title: `Area Disimpan (${luas.toFixed(0)}m²)` }); tempPolygon.setStyle({color: warnaPolygon(status), fillOpacity: 0.5}); tempPolygon.bindPopup(`Kavling: ${nama}
Status: ${status}
Luas: ${luas.toFixed(2)} m²
`).openPopup(); layerKavling.addLayer(tempPolygon); map.removeLayer(tempPolygon); tempPolygon.on('click', function(e) { if (currentMode === 'edit_kavling') { aktifkanEditKavling(data.id, nama); } }); daftarPolygon[data.id] = tempPolygon; document.getElementById('input_nama_kavling').value = ''; refreshPeta(); // PERBAIKAN BUG: Sama seperti Polyline tempPolygon = null; tempPolygonLatLngs = []; } else Toast.fire({ icon: 'error', title: 'Gagal Menyimpan!' }); }); } function hapusPolygon(id) { Swal.fire({ title: 'Hapus Kavling?', text: "Area ini akan dihapus permanen!", icon: 'warning', showCancelButton: true, confirmButtonColor: '#d33', cancelButtonColor: '#3085d6', confirmButtonText: 'Ya, Hapus!' }).then((result) => { if (result.isConfirmed) { fetch('hapus_area.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: id }) }) .then(res => res.json()).then(data => { if (data.status === 'success') { Toast.fire({ icon: 'success', title: 'Kavling Dihapus!' }); if (daftarPolygon[id]) { map.removeLayer(daftarPolygon[id]); delete daftarPolygon[id]; } } else Toast.fire({ icon: 'error', title: 'Gagal Menghapus!' }); }); } }); } // ========================================== // --- LOGIKA MODE EDIT POLYLINE (JALAN) --- // ========================================== function aktifkanEditJalan(id, namaJalan) { let layer = daftarPolyline[id]; if (!layer) return; // Jika sedang edit jalan lain, batalkan dulu if (targetEditJalanId && targetEditJalanId !== id) { batalkanEditJalan(); } targetEditJalanId = id; map.closePopup(); // Tutup popup info // Tampilkan panel info dan tombol simpan di sidebar document.getElementById('info-edit-jalan').style.display = 'block'; document.getElementById('nama-jalan-edit').innerText = "Edit: " + namaJalan; document.getElementById('btn-simpan-edit-jalan').style.display = 'block'; // Aktifkan Leaflet.Geoman khusus pada layer ini layer.pm.enable({ allowSelfIntersection: false, }); } function batalkanEditJalan() { if (targetEditJalanId && daftarPolyline[targetEditJalanId]) { daftarPolyline[targetEditJalanId].pm.disable(); } targetEditJalanId = null; document.getElementById('info-edit-jalan').style.display = 'none'; document.getElementById('btn-simpan-edit-jalan').style.display = 'none'; } function simpanEditPolyline() { if (!targetEditJalanId) return; let layer = daftarPolyline[targetEditJalanId]; if (!layer) return; // 1. Matikan mode edit layer.pm.disable(); // 2. Kalkulasi ulang panjang let latlngsBaru = layer.getLatLngs(); let panjangBaru = hitungPanjangJalan(latlngsBaru); // 3. Konversi ke GeoJSON let geojsonBaru = layer.toGeoJSON(); let dataKirim = { id: targetEditJalanId, panjang_m: panjangBaru, geojson: geojsonBaru }; fetch('update_jalan.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(dataKirim) }) .then(res => res.json()) .then(data => { if (data.status === 'success') { Toast.fire({ icon: 'success', title: `Bentuk Jalan Diperbarui!\nPanjang: ${panjangBaru.toFixed(0)}m` }); batalkanEditJalan(); // Reload halaman secara otomatis setelah 1.5 detik agar data popup ikut ter-refresh setTimeout(() => { window.location.reload(); }, 1500); } else { Toast.fire({ icon: 'error', title: 'Gagal memperbarui jalan: ' + data.message }); batalkanEditJalan(); } }).catch(error => console.error('Error:', error)); } // =========================================== // --- LOGIKA MODE EDIT POLYGON (KAVLING) --- // =========================================== function aktifkanEditKavling(id, namaKavling) { let layer = daftarPolygon[id]; if (!layer) return; // Jika sedang edit kavling lain, batalkan dulu if (targetEditKavlingId && targetEditKavlingId !== id) { batalkanEditKavling(); } targetEditKavlingId = id; map.closePopup(); // Tampilkan panel info dan tombol simpan di sidebar document.getElementById('info-edit-kavling').style.display = 'block'; document.getElementById('nama-kavling-edit').innerText = "Edit: " + namaKavling; document.getElementById('btn-simpan-edit-kavling').style.display = 'block'; // Aktifkan Leaflet.Geoman khusus pada layer ini layer.pm.enable({ allowSelfIntersection: false, }); } function batalkanEditKavling() { if (targetEditKavlingId && daftarPolygon[targetEditKavlingId]) { daftarPolygon[targetEditKavlingId].pm.disable(); } targetEditKavlingId = null; document.getElementById('info-edit-kavling').style.display = 'none'; document.getElementById('btn-simpan-edit-kavling').style.display = 'none'; } function simpanEditPolygon() { if (!targetEditKavlingId) return; let layer = daftarPolygon[targetEditKavlingId]; if (!layer) return; layer.pm.disable(); let latlngsBaru = layer.getLatLngs(); let luasBaru = hitungLuasArea(latlngsBaru); let geojsonBaru = layer.toGeoJSON(); let dataKirim = { id: targetEditKavlingId, luas_m2: luasBaru, geojson: geojsonBaru }; fetch('update_area.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(dataKirim) }) .then(res => res.json()) .then(data => { if (data.status === 'success') { Toast.fire({ icon: 'success', title: `Bentuk Kavling Diperbarui!\nLuas: ${luasBaru.toFixed(0)}m²` }); batalkanEditKavling(); setTimeout(() => { window.location.reload(); }, 1500); } else { Toast.fire({ icon: 'error', title: 'Gagal memperbarui kavling: ' + data.message }); batalkanEditKavling(); } }).catch(error => console.error('Error:', error)); }