const MapApp = (() => { let map, clusterLayer, riLayer, laporanLayer, radiusLayer; let allPenduduk = [], allRI = [], allLaporan = []; let layerState = { penduduk:true, ri:true, laporan:false, radius:true }; let ready = false; const COLOR = { miskin:'#ff4d6d', rentan:'#ffd166', mampu:'#06d6a0' }; const RI_EMOJI = { Masjid:'🕌', Gereja:'⛪', Vihara:'🛕', Pura:'🛕', Klenteng:'🏮', Lainnya:'🏛️' }; function mkIcon(color, emoji='') { return L.divIcon({ className:'', html:`
${emoji||'👤'}
`, iconSize:[30,30],iconAnchor:[15,15],popupAnchor:[0,-16] }); } function riIcon(emoji) { return L.divIcon({ className:'', html:`
${emoji}
`, iconSize:[34,34],iconAnchor:[17,17],popupAnchor:[0,-18] }); } function init() { if (ready) { setTimeout(()=>map.invalidateSize(),100); return; } ready = true; map = L.map('map',{ center:[-0.028,109.338],zoom:13,zoomControl:true }); const tiles = { 'Dark': L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',{attribution:'© CARTO',maxZoom:19}), 'Street': L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OSM',maxZoom:19}), 'Satellite': L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',{attribution:'© Esri',maxZoom:19}), }; tiles['Dark'].addTo(map); L.control.layers(tiles,{},{collapsed:false,position:'topright'}).addTo(map); clusterLayer = L.markerClusterGroup({ maxClusterRadius:60, showCoverageOnHover:false }); riLayer = L.layerGroup(); laporanLayer = L.layerGroup(); radiusLayer = L.layerGroup(); map.addLayer(clusterLayer); map.addLayer(riLayer); map.addLayer(radiusLayer); // Koordinat kursor const coordDiv = L.control({position:'bottomleft'}); coordDiv.onAdd = ()=>{ const d=L.DomUtil.create('div'); d.style.cssText='background:rgba(8,12,20,.85);color:#aaa;padding:4px 12px;border-radius:6px;font-size:11px;font-family:JetBrains Mono,monospace;border:1px solid rgba(255,255,255,.1)'; map.on('mousemove',e=>d.innerHTML=`${e.latlng.lat.toFixed(6)}, ${e.latlng.lng.toFixed(6)}`); return d; }; coordDiv.addTo(map); loadAll(); } async function loadAll() { try { const [gP, gR, gL] = await Promise.all([ Api.get('/peta?layer=penduduk'), Api.get('/peta?layer=rumah_ibadah'), Api.get('/peta?layer=laporan'), ]); allPenduduk = gP.features || []; allRI = gR.features || []; allLaporan = gL.features || []; } catch { // Demo data allPenduduk = buildDemoPenduduk(); allRI = buildDemoRI(); allLaporan = buildDemoLaporan(); } renderAll(); } function renderAll() { renderPenduduk(allPenduduk); renderRI(allRI); renderLaporan(allLaporan); } function renderPenduduk(features) { clusterLayer.clearLayers(); features.forEach(f => { const p = f.properties; const [lng,lat] = f.geometry.coordinates; const icon = mkIcon(COLOR[p.status_ekonomi]||COLOR.rentan); L.marker([lat,lng],{icon}).bindPopup(`
🏠 ${p.nama_kepala||p.nama_lengkap}

No. KK${p.no_kk||p.nik||'—'}
Alamat${p.alamat||'—'}
Anggota${p.jumlah_anggota||1} jiwa
Status${(p.status_ekonomi||'').toUpperCase()}
Bantuan${p.total_bantuan||0}x diterima
Rumah Ibadah${p.nama_ri||'—'}
`).addTo(clusterLayer); }); } function renderRI(features) { riLayer.clearLayers(); radiusLayer.clearLayers(); features.forEach(f => { const r = f.properties; const [lng,lat] = f.geometry.coordinates; const emoji = RI_EMOJI[r.jenis]||'🏛️'; L.marker([lat,lng],{icon:riIcon(emoji)}).bindPopup(`
${emoji} ${r.nama}
Jenis${r.jenis}
Alamat${r.alamat||'—'}
Kontak${r.kontak||'—'}
Radius${r.radius_meter}m
`).addTo(riLayer); L.circle([lat,lng],{ radius:r.radius_meter,color:'#4361ee',fillColor:'#4361ee', fillOpacity:.06,weight:1.5,dashArray:'6 4' }).addTo(radiusLayer); }); } function renderLaporan(features) { laporanLayer.clearLayers(); const UC = { darurat:'#ff4d6d', tinggi:'#ff8c42', sedang:'#ffd166', rendah:'#06d6a0' }; const UE = { darurat:'🚨', tinggi:'⚠️', sedang:'📋', rendah:'📝' }; features.forEach(f => { const l = f.properties; const [lng,lat] = f.geometry.coordinates; const icon = mkIcon(UC[l.urgensi]||'#888', UE[l.urgensi]||'📋'); L.marker([lat,lng],{icon}).bindPopup(`
${UE[l.urgensi]||'📋'} Laporan ${(l.urgensi||'').toUpperCase()}

${l.deskripsi||'—'}
Alamat${l.alamat_laporan||'—'}
Status${l.status||'—'}
`).addTo(laporanLayer); }); } function applyFilter() { const status = ge('map-status-filter')?.value; const data = status ? allPenduduk.filter(f => f.properties.status_ekonomi === status) : allPenduduk; renderPenduduk(data); } function toggleLayer(name, btn) { layerState[name] = !layerState[name]; const on = layerState[name]; const layerMap = { penduduk:clusterLayer, ri:riLayer, laporan:laporanLayer, radius:radiusLayer }; const activeCls = { penduduk:'active-green', ri:'active-blue', laporan:'active-purple', radius:'active-orange' }; on ? map.addLayer(layerMap[name]) : map.removeLayer(layerMap[name]); if (btn) { btn.classList.toggle(activeCls[name], on); } } // Demo seed data (offline mode) function buildDemoPenduduk() { const seeds = [ { nama_kepala:'Ahmad Suryana', alamat:'Gg. Mawar No.5', lat:-0.027, lng:109.344, status_ekonomi:'miskin', jumlah_anggota:3, no_kk:'6171010101010001', nama_ri:"Masjid Jami'", total_bantuan:2 }, { nama_kepala:'Samsul Bahri', alamat:'Jl. Veteran No.12', lat:-0.0315, lng:109.339, status_ekonomi:'rentan', jumlah_anggota:2, no_kk:'6171010101010002', nama_ri:"Masjid Jami'", total_bantuan:0 }, { nama_kepala:'Maria Magdalena',alamat:'Jl. Kemuning No.3', lat:-0.033, lng:109.331, status_ekonomi:'miskin', jumlah_anggota:3, no_kk:'6171010101010003', nama_ri:'Katedral', total_bantuan:1 }, { nama_kepala:'Hendra Wijaya', alamat:'Jl. Ahmad Yani', lat:-0.049, lng:109.326, status_ekonomi:'rentan', jumlah_anggota:2, no_kk:'6171010101010004', nama_ri:'Mujahidin', total_bantuan:0 }, { nama_kepala:'Lim Ah Kow', alamat:'Jl. Diponegoro', lat:-0.022, lng:109.337, status_ekonomi:'miskin', jumlah_anggota:2, no_kk:'6171010101010005', nama_ri:'Vihara', total_bantuan:1 }, { nama_kepala:'Mulyadi', alamat:'Gg. Teratai No.8', lat:-0.0265, lng:109.346, status_ekonomi:'mampu', jumlah_anggota:2, no_kk:'6171010101010006', nama_ri:"Masjid Jami'", total_bantuan:0 }, { nama_kepala:'Yohanes Kristian',alamat:'Jl. HOS Cokro', lat:-0.034, lng:109.330, status_ekonomi:'rentan', jumlah_anggota:2, no_kk:'6171010101010007', nama_ri:'Katedral', total_bantuan:0 }, { nama_kepala:'Tan Eng Huat', alamat:'Jl. Gusti Situt', lat:-0.020, lng:109.4415,status_ekonomi:'miskin', jumlah_anggota:1, no_kk:'6171010101010008', nama_ri:'Klenteng', total_bantuan:1 }, ]; return seeds.map(s=>({type:'Feature',geometry:{type:'Point',coordinates:[s.lng,s.lat]},properties:s})); } function buildDemoRI() { const ri = [ { nama:"Masjid Jami' Pontianak", jenis:'Masjid', lat:-0.02596, lng:109.34307, radius_meter:350, alamat:'Jl. Tanjungpura', kontak:'0561-732001' }, { nama:'Gereja Katedral Pontianak', jenis:'Gereja', lat:-0.03200, lng:109.33200, radius_meter:300, alamat:'Jl. Patimura No.9', kontak:'0561-766000' }, { nama:'Vihara Bodhisattva Karaniya Metta', jenis:'Vihara', lat:-0.02100, lng:109.33800, radius_meter:300, alamat:'Jl. Diponegoro', kontak:'0561-733456' }, { nama:'Klenteng Kwan Ti Miau', jenis:'Klenteng', lat:-0.01900, lng:109.34100, radius_meter:280, alamat:'Jl. Gusti Situt', kontak:'0561-740123' }, { nama:'Masjid Mujahidin Pontianak', jenis:'Masjid', lat:-0.04800, lng:109.32500, radius_meter:400, alamat:'Jl. Ahmad Yani', kontak:'0561-764567' }, { nama:'Gereja GPIB Immanuel', jenis:'Gereja', lat:-0.02800, lng:109.34500, radius_meter:300, alamat:'Jl. Rahadi Usman', kontak:'0561-735890' }, ]; return ri.map(r=>({type:'Feature',geometry:{type:'Point',coordinates:[r.lng,r.lat]},properties:r})); } function buildDemoLaporan() { return [ { type:'Feature',geometry:{type:'Point',coordinates:[109.3311,-0.0330]},properties:{ deskripsi:'Ibu Maria sakit keras, tidak bisa berobat', urgensi:'darurat', status:'diverifikasi', alamat_laporan:'Jl. Kemuning No.3' }}, { type:'Feature',geometry:{type:'Point',coordinates:[109.3415,-0.0200]},properties:{ deskripsi:'Pak Tan penyandang disabilitas butuh bantuan', urgensi:'tinggi', status:'diproses', alamat_laporan:'Jl. Gusti Situt No.7' }}, ]; } return { init, applyFilter, toggleLayer, loadAll }; })();