200 lines
10 KiB
JavaScript
200 lines
10 KiB
JavaScript
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:`<div style="width:30px;height:30px;border-radius:50%;background:${color};display:flex;align-items:center;justify-content:center;font-size:13px;box-shadow:0 2px 8px rgba(0,0,0,.5);border:2px solid rgba(255,255,255,.25)">${emoji||'👤'}</div>`,
|
|
iconSize:[30,30],iconAnchor:[15,15],popupAnchor:[0,-16]
|
|
});
|
|
}
|
|
|
|
function riIcon(emoji) {
|
|
return L.divIcon({
|
|
className:'',
|
|
html:`<div style="font-size:28px;filter:drop-shadow(0 2px 6px rgba(0,0,0,.6));line-height:1">${emoji}</div>`,
|
|
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(`
|
|
<div class="map-popup">
|
|
<div class="mp-title">🏠 ${p.nama_kepala||p.nama_lengkap}</div>
|
|
<hr class="mp-divider">
|
|
<div class="mp-row"><span>No. KK</span><strong class="mono">${p.no_kk||p.nik||'—'}</strong></div>
|
|
<div class="mp-row"><span>Alamat</span><strong>${p.alamat||'—'}</strong></div>
|
|
<div class="mp-row"><span>Anggota</span><strong>${p.jumlah_anggota||1} jiwa</strong></div>
|
|
<div class="mp-row"><span>Status</span><strong style="color:${COLOR[p.status_ekonomi]}">${(p.status_ekonomi||'').toUpperCase()}</strong></div>
|
|
<div class="mp-row"><span>Bantuan</span><strong>${p.total_bantuan||0}x diterima</strong></div>
|
|
<div class="mp-row"><span>Rumah Ibadah</span><strong>${p.nama_ri||'—'}</strong></div>
|
|
</div>`).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(`
|
|
<div class="map-popup">
|
|
<div class="mp-title">${emoji} ${r.nama}</div>
|
|
<div class="mp-row"><span>Jenis</span><strong>${r.jenis}</strong></div>
|
|
<div class="mp-row"><span>Alamat</span><strong>${r.alamat||'—'}</strong></div>
|
|
<div class="mp-row"><span>Kontak</span><strong>${r.kontak||'—'}</strong></div>
|
|
<div class="mp-row"><span>Radius</span><strong>${r.radius_meter}m</strong></div>
|
|
</div>`).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(`
|
|
<div class="map-popup">
|
|
<div class="mp-title">${UE[l.urgensi]||'📋'} Laporan ${(l.urgensi||'').toUpperCase()}</div>
|
|
<hr class="mp-divider">
|
|
<div style="font-size:12px;color:#aaa;margin-bottom:8px">${l.deskripsi||'—'}</div>
|
|
<div class="mp-row"><span>Alamat</span><strong>${l.alamat_laporan||'—'}</strong></div>
|
|
<div class="mp-row"><span>Status</span><strong>${l.status||'—'}</strong></div>
|
|
</div>`).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 };
|
|
})();
|