Files
WebGIS-Project/webgis-v3/assets/js/map.js
T
2026-06-10 19:48:00 +07:00

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 };
})();