/* ============================================================ draw_sosial_public.js — Dashboard Publik (READ ONLY) v3.0 — Heatmap, Masjid Penanggung, Stats, Blind Spot Tanpa fitur CRUD (tambah/edit/hapus) ============================================================ */ /* ── MAP INIT ── */ var map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13); L.control.zoom({ position: 'bottomright' }).addTo(map); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 19 }).addTo(map); /* ── LAYER GROUPS ── */ var layerGroupMasjid = L.featureGroup().addTo(map); var layerGroupPenduduk = L.featureGroup().addTo(map); var heatmapLayer = null; var showMasjid = true; var showPenduduk = true; var showHeatmap = false; /* ── DATA STORAGE ── */ var masjidData = []; var pendudukData = []; var blindSpotIds = []; /* ── CONSTANTS ── */ const JENIS_BANTUAN_OPTS = [ { value: 'BLT / Modal Usaha', icon: '💰' }, { value: 'Bedah Rumah', icon: '🏠' }, { value: 'Air Bersih / PDAM', icon: '💧' }, { value: 'Sembako', icon: '🍚' }, { value: 'Beasiswa Pendidikan', icon: '📚' }, ]; function getJenisIcon(jenis) { const found = JENIS_BANTUAN_OPTS.find(o => o.value === jenis); return found ? found.icon : '🤝'; } /* ============================================================ TOGGLE LAYERS ============================================================ */ function toggleLayer(type) { if (type === 'masjid') { showMasjid = !showMasjid; showMasjid ? map.addLayer(layerGroupMasjid) : map.removeLayer(layerGroupMasjid); document.getElementById('btnMasjid').classList.toggle('active', showMasjid); } else if (type === 'penduduk') { showPenduduk = !showPenduduk; showPenduduk ? map.addLayer(layerGroupPenduduk) : map.removeLayer(layerGroupPenduduk); document.getElementById('btnPenduduk').classList.toggle('active', showPenduduk); if (!showPenduduk && showHeatmap) toggleHeatmap(); } else if (type === 'heatmap') { toggleHeatmap(); } } function toggleHeatmap() { showHeatmap = !showHeatmap; var btn = document.getElementById('btnHeatmap'); if (showHeatmap) { map.removeLayer(layerGroupPenduduk); showPenduduk = false; document.getElementById('btnPenduduk').classList.remove('active'); if (heatmapLayer) map.addLayer(heatmapLayer); if (btn) btn.classList.add('active'); } else { if (heatmapLayer) map.removeLayer(heatmapLayer); map.addLayer(layerGroupPenduduk); showPenduduk = true; document.getElementById('btnPenduduk').classList.add('active'); if (btn) btn.classList.remove('active'); } } /* ============================================================ HELPERS ============================================================ */ function fmtNum(n, dec=2) { return parseFloat(n).toLocaleString('id-ID', { maximumFractionDigits: dec }); } function getSkorLabel(skor) { if (skor >= 70) return { label: 'Sangat Rentan', color: '#ef4444', emoji: '🔴' }; if (skor >= 40) return { label: 'Rentan', color: '#f97316', emoji: '🟠' }; return { label: 'Cukup', color: '#eab308', emoji: '🟡' }; } function findMasjidPenanggung(pLat, pLng) { var nearest = null, minDist = Infinity; masjidData.forEach(m => { var d = map.distance([pLat, pLng], [m.lat, m.lng]); if (d <= m.radius && d < minDist) { minDist = d; nearest = m; } }); return nearest; } /* ── Icons ── */ function makeMasjidIcon() { return L.divIcon({ html:`🕌`, iconSize:[30,42], iconAnchor:[15,42], popupAnchor:[0,-44], className:'' }); } function makePendudukIcon(inRadius, isBlindspot=false) { var c = isBlindspot ? '#ef4444' : (inRadius ? '#f59e0b' : '#e879f9'); return L.divIcon({ html:``, iconSize:[22,32], iconAnchor:[11,32], popupAnchor:[0,-34], className: isBlindspot ? 'blindspot-marker' : '' }); } /* ============================================================ POPUP BUILDERS — READ ONLY ============================================================ */ /* ── POPUP MASJID ── */ function buildMasjidPopup(d) { var penerima = pendudukData .map(p => ({ ...p, dist: map.distance([p.lat, p.lng], [d.lat, d.lng]) })) .filter(p => p.dist <= d.radius) .sort((a, b) => b.skor - a.skor); var rowsPenerima = penerima.length === 0 ? `
Belum ada penduduk dalam radius ini.
` : penerima.map(p => { var sl = getSkorLabel(p.skor); return `
${sl.emoji}
${p.nama || '—'}
Skor: ${fmtNum(p.skor,1)} · ${sl.label}
${getJenisIcon(p.jenis)}
`; }).join(''); return `
🕌 ${d.nama}
PIC${d.pic}
Radius Layanan${fmtNum(d.radius,0)} m
Jml. Ditanggung${penerima.length} KK
`; } function switchTab(id, tab) { document.getElementById('tabContent1_' + id).style.display = tab === 1 ? '' : 'none'; document.getElementById('tabContent2_' + id).style.display = tab === 2 ? '' : 'none'; document.getElementById('tab1_' + id).style.background = tab === 1 ? '#1e2233' : 'transparent'; document.getElementById('tab1_' + id).style.color = tab === 1 ? '#f59e0b' : '#6b7280'; document.getElementById('tab2_' + id).style.background = tab === 2 ? '#1e2233' : 'transparent'; document.getElementById('tab2_' + id).style.color = tab === 2 ? '#f59e0b' : '#6b7280'; } /* ── POPUP PENDUDUK ── */ function buildPendudukPopup(d) { var skor = parseFloat(d.skor_kerentanan) || 0; var sl = getSkorLabel(skor); var pLat = parseFloat(d.latitude), pLng = parseFloat(d.longitude); var penanggung = findMasjidPenanggung(pLat, pLng); var penanggungHtml = penanggung ? `
Masjid Penanggung🕌 ${penanggung.nama}
` : `
Status⚠️ Blind Spot
`; var jenisIcon = getJenisIcon(d.jenis_bantuan); return `
👨‍👩‍👧‍👦 KK — ${d.nama_kk}
Jml. Anggota KK${d.jumlah_anggota} orang
Alamat${d.alamat}
Penghasilan${d.penghasilan}
Kondisi Rumah${d.kondisi_rumah}
Sumber Air${d.sumber_air}
Skor Kerentanan ${sl.emoji} ${fmtNum(skor,1)} — ${sl.label}
${penanggungHtml}
Jenis Bantuan ${jenisIcon} ${d.jenis_bantuan || '—'}
`; } /* ============================================================ LOAD DATA ============================================================ */ fetch('../php/show_sosial.php') .then(res => res.json()) .then(data => { var pendudukRaw = []; data.forEach(d => { if (d.type === 'masjid') { var mLat=parseFloat(d.latitude), mLng=parseFloat(d.longitude); var mR = parseFloat(d.radius_layanan) || parseFloat(d.radius) || 500; var circle = L.circle([mLat,mLng], { radius:mR, color:'#f59e0b', fillColor:'#f59e0b', fillOpacity:0.08, weight:2, dashArray:'6 4' }); layerGroupMasjid.addLayer(circle); var mEntry = { id:parseInt(d.id), lat:mLat, lng:mLng, radius:mR, nama:d.nama, pic:d.pic, circleLayer:circle }; masjidData.push(mEntry); var marker = L.marker([mLat,mLng], { icon:makeMasjidIcon() }); marker._type='masjid'; marker._db_id=d.id; mEntry.markerLayer = marker; layerGroupMasjid.addLayer(marker); } if (d.type === 'penduduk') pendudukRaw.push(d); }); var heatPoints = []; pendudukRaw.forEach(d => { var pLat=parseFloat(d.latitude), pLng=parseFloat(d.longitude); var skor = parseFloat(d.skor_kerentanan) || 0; var inRadius=false, minDist=Infinity; masjidData.forEach(m => { var dist = map.distance([pLat,pLng],[m.lat,m.lng]); if (dist<=m.radius && dist buildPendudukPopup(d), { maxWidth:300 }); layerGroupPenduduk.addLayer(marker); heatPoints.push([pLat, pLng, skor / 100]); }); // Bind masjid popups masjidData.forEach(m => { m.markerLayer.bindPopup(() => buildMasjidPopup(m), { maxWidth:310 }); }); // Init heatmap if (typeof L.heatLayer !== 'undefined') { heatmapLayer = L.heatLayer(heatPoints, { radius: 35, blur: 25, maxZoom: 16, gradient: { 0.0:'#3b82f6', 0.4:'#eab308', 0.7:'#f97316', 1.0:'#ef4444' } }); } checkBlindSpots(); renderStats(); }) .catch(err => console.error('Gagal load data:', err)); /* ============================================================ BLIND SPOT ============================================================ */ function checkBlindSpots() { blindSpotIds = []; pendudukData.forEach(p => { if (!p.inRadius && p.skor >= 70) { blindSpotIds.push(p.id); p.markerLayer.setIcon(makePendudukIcon(false, true)); } }); var panel = document.getElementById('blindSpotPanel'); if (!panel) return; if (blindSpotIds.length > 0) { panel.style.display = 'flex'; document.getElementById('blindSpotCount').textContent = blindSpotIds.length; } else { panel.style.display = 'none'; } } function zoomToBlindSpots() { if (blindSpotIds.length === 0) return; var bounds = pendudukData.filter(p => blindSpotIds.includes(p.id)).map(p => [p.lat, p.lng]); if (bounds.length > 0) map.fitBounds(bounds, { padding:[60,60] }); } /* ============================================================ STATS PANEL ============================================================ */ function renderStats() { var total = pendudukData.length; var sangat = pendudukData.filter(p => p.skor >= 70).length; var rentan = pendudukData.filter(p => p.skor >= 40 && p.skor < 70).length; var cukup = pendudukData.filter(p => p.skor < 40).length; var bs = blindSpotIds.length; var jenisCounts = {}; JENIS_BANTUAN_OPTS.forEach(o => jenisCounts[o.value] = 0); pendudukData.forEach(p => { if (jenisCounts[p.jenis] !== undefined) jenisCounts[p.jenis]++; else jenisCounts['BLT / Modal Usaha']++; }); var jenisRows = JENIS_BANTUAN_OPTS.map(o => `
${o.icon} ${o.value} ${jenisCounts[o.value]}
` ).join(''); var el = document.getElementById('statsContent'); if (!el) return; el.innerHTML = `
${total}
Total Warga
${sangat}
🔴 Sgt. Rentan
${rentan}
🟠 Rentan
${cukup}
🟡 Cukup
Jenis Bantuan Dibutuhkan
${jenisRows}
⚠️ Blind Spot (skor ≥70) ${bs} KK
`; } function toggleStats() { var content = document.getElementById('statsContent'); var btn = document.getElementById('statsToggleBtn'); if (content.style.display === 'none') { content.style.display = 'block'; btn.textContent = '▲'; } else { content.style.display = 'none'; btn.textContent = '▼'; } }