// ============================================ // map.js — Logic Peta WebGIS Terpadu // ============================================ // ------- Konfigurasi endpoint backend ------- const API_READ_SPBU = 'backend/read.php'; const API_CREATE_SPBU = 'backend/create.php'; const API_READ_JALAN = 'backend/read_jalan.php'; const API_CREATE_JALAN= 'backend/create_jalan.php'; const API_READ_PARSIL = 'backend/read_parsil.php'; const API_CREATE_PARSIL='backend/create_parsil.php'; const API_READ_IBADAH = 'backend/read_ibadah.php'; const API_CREATE_IBADAH='backend/create_ibadah.php'; const API_READ_MISKIN = 'backend/read_miskin.php'; const API_CREATE_MISKIN='backend/create_miskin.php'; // ------- State Internal & Data Storage ------- window.appData = { spbu: [], jalan: [], parsil: [], ibadah: [], miskin: [] }; // ------- Inisialisasi Basemap ------- const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }); const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19, attribution: 'Tiles © Esri' }); const dark = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 19, attribution: '© CartoDB' }); // ------- Inisialisasi Peta ------- const map = L.map('map', { center: [-0.0260, 109.3425], // Pusat Kota Pontianak zoom: 13, zoomControl: true, layers: [osm] // Default basemap }); // Tambah Basemap & Overlay Switcher (Layer Control) const baseMaps = { "Peta Standar (OSM)": osm, "Citra Satelit": satellite, "Peta Gelap": dark }; // Inisialisasi Layer Groups const layerSpbu24 = L.layerGroup().addTo(map); const layerSpbuTidak = L.layerGroup().addTo(map); const layerJalan = L.layerGroup().addTo(map); const layerParsil = L.layerGroup().addTo(map); const layerIbadah = L.layerGroup().addTo(map); const layerMiskin = L.layerGroup().addTo(map); const overlays = { "SPBU Buka 24 Jam": layerSpbu24, "SPBU Tidak 24 Jam": layerSpbuTidak, "Informasi Jalan": layerJalan, "Parsil Tanah": layerParsil, "Rumah Ibadah": layerIbadah, "Penduduk Miskin": layerMiskin }; L.control.layers(baseMaps, overlays, { position: 'bottomleft' }).addTo(map); // ------- Tombol Geolocation (Lokasi Saya) ------- const LocateControl = L.Control.extend({ options: { position: 'topleft' }, onAdd: function (map) { const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control'); container.style.backgroundColor = 'white'; container.style.width = '34px'; container.style.height = '34px'; container.style.cursor = 'pointer'; container.style.display = 'flex'; container.style.justifyContent = 'center'; container.style.alignItems = 'center'; container.innerHTML = '📍'; container.onclick = function(e) { e.stopPropagation(); if (!window.isSecureContext) { showToast('Fitur lokasi hanya tersedia melalui HTTPS.', 'error'); return; } showToast('Mencari lokasi...', 'success'); map.locate({ setView: true, maxZoom: 17, enableHighAccuracy: true }); } return container; } }); map.addControl(new LocateControl()); let gpsMarker = null; let gpsPulse = null; map.on('locationfound', function(e) { if (gpsMarker) map.removeLayer(gpsMarker); if (gpsPulse) map.removeLayer(gpsPulse); const pulseIcon = L.divIcon({ className: 'gps-pulse', iconSize: [40, 40] }); const dotIcon = L.divIcon({ className: 'gps-marker', iconSize: [16, 16] }); gpsPulse = L.marker(e.latlng, { icon: pulseIcon }).addTo(map); gpsMarker = L.marker(e.latlng, { icon: dotIcon }).addTo(map); showToast('Lokasi ditemukan!', 'success'); }); map.on('locationerror', function(e) { showToast('Tidak dapat mengakses GPS.', 'error'); }); // ------- Mode State ------- let currentMode = 'spbu'; // 'spbu', 'jalan', 'parsil', 'ibadah', 'miskin' let inputPopup = null; let currentDrawnLayer = null; // layer temp saat menggambar // Inisialisasi Leaflet-Geoman map.pm.addControls({ position: 'topleft', drawMarker: false, drawCircleMarker: false, drawPolyline: false, drawRectangle: false, drawPolygon: false, drawCircle: false, drawText: false, editMode: false, dragMode: false, cutPolygon: false, removalMode: true }); // Transisi Mode UI const btnSpbu = document.getElementById('btn-mode-spbu'); const btnJalan = document.getElementById('btn-mode-jalan'); const btnParsil = document.getElementById('btn-mode-parsil'); const btnIbadah = document.getElementById('btn-mode-ibadah'); const btnMiskin = document.getElementById('btn-mode-miskin'); const hintBar = document.getElementById('hint-bar'); function switchMode(mode) { currentMode = mode; btnSpbu.classList.toggle('active', mode === 'spbu'); btnJalan.classList.toggle('active', mode === 'jalan'); btnParsil.classList.toggle('active', mode === 'parsil'); btnIbadah.classList.toggle('active', mode === 'ibadah'); btnMiskin.classList.toggle('active', mode === 'miskin'); // Matikan PM Draw map.pm.disableDraw(); if (inputPopup) map.closePopup(inputPopup); if (currentDrawnLayer) { map.removeLayer(currentDrawnLayer); currentDrawnLayer = null; } if (mode === 'spbu') { hintBar.innerHTML = 'Klik pada peta untuk menambahkan SPBU baru'; } else if (mode === 'ibadah') { hintBar.innerHTML = 'Klik pada peta untuk menambahkan Rumah Ibadah'; } else if (mode === 'miskin') { hintBar.innerHTML = 'Klik pada peta untuk menambahkan Penduduk Miskin'; } else if (mode === 'jalan') { hintBar.innerHTML = 'Klik start lalu gambar garis jalan, klik titik terakhir untuk selesai'; map.pm.enableDraw('Line', { snappable: true, snapDistance: 20, pathOptions: { color: '#3b82f6', weight: 4 } }); } else if (mode === 'parsil') { hintBar.innerHTML = 'Klik start lalu gambar poligon, klik titik awal untuk selesai'; map.pm.enableDraw('Polygon', { snappable: true, snapDistance: 20, pathOptions: { color: '#3b82f6', weight: 2, fillColor: '#3b82f6', fillOpacity: 0.4 } }); } } btnSpbu.addEventListener('click', () => switchMode('spbu')); btnJalan.addEventListener('click', () => switchMode('jalan')); btnParsil.addEventListener('click', () => switchMode('parsil')); btnIbadah.addEventListener('click', () => switchMode('ibadah')); btnMiskin.addEventListener('click', () => switchMode('miskin')); // ============================================ // SPBU LOGIC // ============================================ let totalSpbu = 0; function updateCounter(count) { totalSpbu = count; document.getElementById('spbu-count').textContent = totalSpbu; } function createSpbuIcon(status) { const color = status === '24jam' ? '#22c55e' : '#f59e0b'; const svg = ` SPBU `; return L.divIcon({ html: svg, className: '', iconSize: [36, 48], iconAnchor: [18, 48], popupAnchor: [0, -50] }); } function spbuBadge(status) { return status === '24jam' ? '24 Jam' : 'Tidak 24 Jam'; } function buildSpbuPopup(d) { return ``; } function addSpbuToMap(d) { const marker = L.marker([d.latitude, d.longitude], { icon: createSpbuIcon(d.status) }); marker.bindPopup(buildSpbuPopup(d), { maxWidth: 260 }); if (d.status === '24jam') { marker.addTo(layerSpbu24); } else { marker.addTo(layerSpbuTidak); } window.appData.spbu.push({ data: d, layer: marker }); } // SPBU, IBADAH, MISKIN Map Click logic map.on('click', async function (e) { // Hide search results if clicking map document.getElementById('search-results').classList.remove('show'); if (!['spbu', 'ibadah', 'miskin'].includes(currentMode)) return; // abaikan jika bukan point mode if (inputPopup) map.closePopup(inputPopup); const { lat, lng } = e.latlng; let formHtml = ''; if (currentMode === 'spbu') { formHtml = ` `; } else if (currentMode === 'ibadah') { showToast('Mendapatkan alamat otomatis...', 'success'); let autoAddress = ''; try { const resp = await fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}`); const jData = await resp.json(); if(jData && jData.display_name) autoAddress = jData.display_name; } catch(err) { console.error(err); } formHtml = ` `; } else if (currentMode === 'miskin') { formHtml = ` `; } inputPopup = L.popup({ maxWidth: 320, closeButton: false }) .setLatLng(e.latlng).setContent(formHtml).openOn(map); setTimeout(() => { document.getElementById('btn-cancel')?.addEventListener('click', () => map.closePopup(inputPopup)); // SPBU Handler document.getElementById('form-spbu')?.addEventListener('submit', async (ev) => { ev.preventDefault(); const payload = { nama: document.getElementById('f-nama').value.trim(), no_spbu: document.getElementById('f-no').value.trim(), status: document.getElementById('f-status').value, latitude: lat, longitude: lng }; if(!payload.nama || !payload.no_spbu) return; document.getElementById('btn-save').disabled = true; try { const res = await fetch(API_CREATE_SPBU, { method: 'POST', body: JSON.stringify(payload) }); const json = await res.json(); if (json.success) { map.closePopup(inputPopup); addSpbuToMap(json.data); updateCounter(totalSpbu + 1); showToast('SPBU berhasil ditambahkan!'); } else showToast(json.message, 'error'); } catch (err) { showToast('Error server', 'error'); } }); // Ibadah Handler document.getElementById('form-ibadah')?.addEventListener('submit', async (ev) => { ev.preventDefault(); const payload = { nama: document.getElementById('fi-nama').value.trim(), alamat: document.getElementById('fi-alamat').value.trim(), latitude: lat, longitude: lng }; if(!payload.nama || !payload.alamat) return; document.getElementById('btn-save').disabled = true; try { const res = await fetch(API_CREATE_IBADAH, { method: 'POST', body: JSON.stringify(payload) }); const json = await res.json(); if (json.success) { map.closePopup(inputPopup); addIbadahToMap(json.data); showToast('Rumah Ibadah ditambahkan!'); } else showToast(json.message, 'error'); } catch (err) { showToast('Error server', 'error'); } }); // Miskin Handler document.getElementById('form-miskin')?.addEventListener('submit', async (ev) => { ev.preventDefault(); const payload = { nama_kk: document.getElementById('fm-nama').value.trim(), keterangan: document.getElementById('fm-ket').value.trim(), latitude: lat, longitude: lng }; if(!payload.nama_kk) return; document.getElementById('btn-save').disabled = true; try { const res = await fetch(API_CREATE_MISKIN, { method: 'POST', body: JSON.stringify(payload) }); const json = await res.json(); if (json.success) { map.closePopup(inputPopup); addMiskinToMap(json.data); showToast('Data keluarga ditambahkan!'); } else showToast(json.message, 'error'); } catch (err) { showToast('Error server', 'error'); } }); }, 0); }); // ============================================ // IBADAH & MISKIN LOGIC (Radius Analysis) // ============================================ function createIbadahIcon() { const svg = ` 🕌 `; return L.divIcon({ html: svg, className: '', iconSize: [36, 48], iconAnchor: [18, 48], popupAnchor: [0, -48] }); } let activeRadiusCircle = null; window.updateRadius = function(id, radius, lat, lng) { document.getElementById(`radius-val-${id}`).innerText = `${radius} Meter`; if (activeRadiusCircle) map.removeLayer(activeRadiusCircle); activeRadiusCircle = L.circle([lat, lng], { color: '#8b5cf6', fillColor: '#8b5cf6', fillOpacity: 0.2, radius: parseInt(radius) }).addTo(map); let caughtCount = 0; const centerPoint = turf.point([lng, lat]); window.appData.miskin.forEach(m => { const targetPoint = turf.point([m.data.longitude, m.data.latitude]); const distance = turf.distance(centerPoint, targetPoint, { units: 'meters' }); if (distance <= radius) { caughtCount++; // Set point menjadi merah m.layer.setStyle({ fillColor: '#ef4444', color: '#ef4444', radius: 8 }); } else { // Kembali menjadi hitam m.layer.setStyle({ fillColor: '#000000', color: '#000000', radius: 6 }); } }); document.getElementById(`radius-res-${id}`).innerHTML = `${caughtCount} rumah keluarga miskin dalam radius.`; }; // When popup closes, remove radius circle map.on('popupclose', () => { if (activeRadiusCircle) { map.removeLayer(activeRadiusCircle); activeRadiusCircle = null; window.appData.miskin.forEach(m => { m.layer.setStyle({ fillColor: '#000000', color: '#000000', radius: 6 }); }); } }); function buildIbadahPopup(d) { return ``; } function buildMiskinPopup(d) { return ``; } function addIbadahToMap(d) { const marker = L.marker([d.latitude, d.longitude], { icon: createIbadahIcon() }); marker.bindPopup(buildIbadahPopup(d), { maxWidth: 300, minWidth: 260 }); marker.addTo(layerIbadah); window.appData.ibadah.push({ data: d, layer: marker }); } function addMiskinToMap(d) { // Gunakan L.circleMarker sebagai point sederhana (warna bawaan Hitam) const marker = L.circleMarker([d.latitude, d.longitude], { radius: 6, fillColor: "#000000", color: "#000000", weight: 2, opacity: 1, fillOpacity: 0.8 }); marker.bindPopup(buildMiskinPopup(d), { maxWidth: 260 }); marker.addTo(layerMiskin); window.appData.miskin.push({ data: d, layer: marker }); } // ============================================ // JALAN & PARSIL DRAW LOGIC // ============================================ // Pemetaan Warna function getJalanColor(status) { if(status === 'Nasional') return '#ef4444'; if(status === 'Provinsi') return '#eab308'; return '#10b981'; // Kabupaten } function getParsilStyle(status) { if (status === 'SHM') return { color: '#2563eb', fillColor: '#3b82f6', fillOpacity: 0.5 }; if (status === 'HGB') return { color: '#7c3aed', fillColor: '#8b5cf6', fillOpacity: 0.5 }; if (status === 'HGU') return { color: '#db2777', fillColor: '#ec4899', fillOpacity: 0.5 }; return { color: '#0d9488', fillColor: '#14b8a6', fillOpacity: 0.5 }; // HP } // Leaflet Geoman event saat entitas selesai digambar map.on('pm:create', (e) => { currentDrawnLayer = e.layer; const geom = currentDrawnLayer.toGeoJSON().geometry; if (currentMode === 'jalan' && e.shape === 'Line') { // Hitung panjang dalam meter via Turf.js const lengthKm = turf.length(currentDrawnLayer.toGeoJSON()); const panjangMeter = (lengthKm * 1000).toFixed(2); openJalanForm(e.layer, geom, panjangMeter); // Refresh mode drawing map.pm.enableDraw('Line'); } else if (currentMode === 'parsil' && e.shape === 'Polygon') { // Hitung luas dalam meter persegi via Turf.js const areaSqMeters = turf.area(currentDrawnLayer.toGeoJSON()); const luasMeter = areaSqMeters.toFixed(2); openParsilForm(e.layer, geom, luasMeter); // Refresh mode drawing map.pm.enableDraw('Polygon'); } }); function openJalanForm(layer, geom, panjang) { const centerLatlng = layer.getBounds().getCenter(); const formHtml = ` `; inputPopup = L.popup({ maxWidth: 320, closeButton: false }) .setLatLng(centerLatlng).setContent(formHtml).openOn(map); setTimeout(() => { document.getElementById('btn-cancel-draw')?.addEventListener('click', () => { map.closePopup(inputPopup); map.removeLayer(layer); }); document.getElementById('form-jalan')?.addEventListener('submit', async (ev) => { ev.preventDefault(); const payload = { nama_jalan: document.getElementById('fj-nama').value.trim(), status: document.getElementById('fj-status').value, panjang: panjang, geom: geom }; if(!payload.nama_jalan) return; document.getElementById('btn-save-draw').disabled = true; try { const res = await fetch(API_CREATE_JALAN, { method: 'POST', body: JSON.stringify(payload) }); const json = await res.json(); if (json.success) { map.closePopup(inputPopup); map.removeLayer(layer); currentDrawnLayer = null; addJalanToMap(json.data); showToast('Jalan berhasil ditambahkan!'); } else showToast(json.message, 'error'); } catch (e) { showToast('Error server', 'error'); } document.getElementById('btn-save-draw').disabled = false; }); }, 0); } function openParsilForm(layer, geom, luas) { const centerLatlng = layer.getBounds().getCenter(); const formHtml = ` `; inputPopup = L.popup({ maxWidth: 320, closeButton: false }) .setLatLng(centerLatlng).setContent(formHtml).openOn(map); setTimeout(() => { document.getElementById('btn-cancel-draw')?.addEventListener('click', () => { map.closePopup(inputPopup); map.removeLayer(layer); }); document.getElementById('form-parsil')?.addEventListener('submit', async (ev) => { ev.preventDefault(); const payload = { nama_pemilik: document.getElementById('fp-nama').value.trim(), status: document.getElementById('fp-status').value, luas: luas, geom: geom }; if(!payload.nama_pemilik) return; document.getElementById('btn-save-draw').disabled = true; try { const res = await fetch(API_CREATE_PARSIL, { method: 'POST', body: JSON.stringify(payload) }); const json = await res.json(); if (json.success) { map.closePopup(inputPopup); map.removeLayer(layer); currentDrawnLayer = null; addParsilToMap(json.data); showToast('Parsil berhasil ditambahkan!'); } else showToast(json.message, 'error'); } catch (e) { showToast('Error server', 'error'); } document.getElementById('btn-save-draw').disabled = false; }); }, 0); } // Popups Data Terload function buildJalanPopup(d) { return ``; } function buildParsilPopup(d) { return ``; } // Add GeoJSON to Layer function addJalanToMap(d) { const layer = L.geoJSON(d.geom, { style: { color: getJalanColor(d.status), weight: 5, opacity: 0.9 } }).bindPopup(buildJalanPopup(d)).addTo(layerJalan); window.appData.jalan.push({ data: d, layer: layer }); } // ------ Fitur Hover Info Parsil ------ function highlightParsil(e) { const layer = e.target; layer.setStyle({ weight: 4, color: '#fff', dashArray: '', fillOpacity: 0.8 }); layer.bringToFront(); } function resetParsilHighlight(e) { const layer = e.target; const status = layer.featureProps.status; layer.setStyle(getParsilStyle(status)); } function addParsilToMap(d) { const layerGroup = L.geoJSON(d.geom, { style: getParsilStyle(d.status), onEachFeature: function(feature, layer) { layer.featureProps = d; // Simpan properti referensi ke dalam layer const tooltipContent = `

Informasi Parsil Tanah

Pemilik: ${escapeHtml(d.nama_pemilik)}
Sertifikat: ${d.status}
Luas: ${d.luas} m²
`; layer.bindTooltip(tooltipContent, { sticky: true, // Tooltip bergeser mengekori kursor kursor direction: 'top', className: 'custom-tooltip' }); layer.on({ mouseover: highlightParsil, mouseout: resetParsilHighlight }); } }); layerGroup.bindPopup(buildParsilPopup(d)).addTo(layerParsil); window.appData.parsil.push({ data: d, layer: layerGroup }); } // ============================================ // LOGIKA PENCARIAN (SEARCH) // ============================================ const searchInput = document.getElementById('search-input'); const searchResults = document.getElementById('search-results'); const filterBtns = document.querySelectorAll('.filter-btn'); let currentSearchFilter = 'all'; filterBtns.forEach(btn => { btn.addEventListener('click', (e) => { filterBtns.forEach(b => b.classList.remove('active')); e.target.classList.add('active'); currentSearchFilter = e.target.dataset.filter; performSearch(searchInput.value); }); }); searchInput.addEventListener('input', (e) => performSearch(e.target.value)); // Tambahkan event focus jika user kembali nge-klik kolom search yg sudah ada isinya searchInput.addEventListener('focus', (e) => performSearch(e.target.value)); function performSearch(query) { query = query.toLowerCase().trim(); searchResults.innerHTML = ''; if (!query) { searchResults.classList.remove('show'); return; } let results = []; // Filter SPBU if (currentSearchFilter === 'all' || currentSearchFilter === 'spbu') { appData.spbu.forEach(item => { if (item.data.nama.toLowerCase().includes(query) || item.data.no_spbu.toLowerCase().includes(query)) { results.push({ type: 'SPBU', title: item.data.nama, meta: `No: ${item.data.no_spbu}`, item: item }); } }); } // Filter Jalan if (currentSearchFilter === 'all' || currentSearchFilter === 'jalan') { appData.jalan.forEach(item => { if (item.data.nama_jalan.toLowerCase().includes(query)) { results.push({ type: 'Jalan', title: item.data.nama_jalan, meta: `${item.data.status} · ${item.data.panjang}m`, item: item }); } }); } // Filter Parsil if (currentSearchFilter === 'all' || currentSearchFilter === 'parsil') { appData.parsil.forEach(item => { if (item.data.nama_pemilik.toLowerCase().includes(query)) { results.push({ type: 'Parsil', title: item.data.nama_pemilik, meta: `Sertifikat: ${item.data.status}`, item: item }); } }); } if (results.length === 0) { searchResults.innerHTML = '
Tidak ada hasil pemetaan
'; } else { results.forEach(r => { const div = document.createElement('div'); div.className = 'search-result-item'; div.innerHTML = `
${escapeHtml(r.title)}
${r.type} ${escapeHtml(r.meta)}
`; div.addEventListener('click', () => zoomToResult(r)); searchResults.appendChild(div); }); } searchResults.classList.add('show'); } function zoomToResult(result) { searchResults.classList.remove('show'); searchInput.value = ''; // Opsional: clear search box saat dipilih const layer = result.item.layer; if (result.type === 'SPBU') { // FlyTo SPBU marker map.flyTo(layer.getLatLng(), 18, { duration: 1.5 }); // Buka popup saat flyTo selesai (mendekati target) setTimeout(() => layer.openPopup(), 1000); } else { // GeoJSON Layers berbentuk grup (bisa lebih dari satu poligon/garis) const bounds = layer.getBounds(); map.flyToBounds(bounds, { duration: 1.5, maxZoom: 18 }); setTimeout(() => layer.openPopup(), 1000); } } // ============================================ // DATA LOADER // ============================================ async function loadAllData() { try { // Load SPBU const r1 = await fetch(API_READ_SPBU); const j1 = await r1.json(); if(j1.success) { j1.data.forEach(d => addSpbuToMap(d)); updateCounter(j1.count); } // Load Jalan const r2 = await fetch(API_READ_JALAN); const j2 = await r2.json(); if(j2.success) j2.data.forEach(d => addJalanToMap(d)); // Load Parsil const r3 = await fetch(API_READ_PARSIL); const j3 = await r3.json(); if(j3.success) j3.data.forEach(d => addParsilToMap(d)); // Load Ibadah const r4 = await fetch(API_READ_IBADAH); const j4 = await r4.json(); if(j4.success) j4.data.forEach(d => addIbadahToMap(d)); // Load Miskin const r5 = await fetch(API_READ_MISKIN); const j5 = await r5.json(); if(j5.success) j5.data.forEach(d => addMiskinToMap(d)); } catch (err) { showToast('Gagal memuat beberapa data.', 'error'); } } // ------- Helpers ------- function showToast(message, type = 'success') { const container = document.getElementById('toast-container'); const t = document.createElement('div'); t.className = `toast toast-${type}`; t.innerHTML = `${type === 'success' ? '✓' : '✕'}${message}`; container.appendChild(t); requestAnimationFrame(() => t.classList.add('toast-show')); setTimeout(() => { t.classList.remove('toast-show'); t.addEventListener('transitionend', () => t.remove(), { once: true }); }, 3000); } function escapeHtml(str) { return String(str) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } // Start loadAllData();