335 lines
16 KiB
JavaScript
335 lines
16 KiB
JavaScript
/* ============================================================
|
|
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:`<svg xmlns="http://www.w3.org/2000/svg" width="30" height="42" viewBox="0 0 30 42"><path d="M15 0C6.7 0 0 6.7 0 15c0 11.25 15 27 15 27S30 26.25 30 15C30 6.7 23.3 0 15 0z" fill="#f59e0b" stroke="rgba(255,255,255,.3)" stroke-width="1.5"/><text x="15" y="20" text-anchor="middle" font-size="13" fill="white">🕌</text></svg>`, 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:`<svg xmlns="http://www.w3.org/2000/svg" width="22" height="32" viewBox="0 0 22 32"><path d="M11 0C4.9 0 0 4.9 0 11c0 8.25 11 21 11 21S22 19.25 22 11C22 4.9 17.1 0 11 0z" fill="${c}" stroke="rgba(255,255,255,.3)" stroke-width="1.5"/><circle cx="11" cy="11" r="4" fill="rgba(0,0,0,.35)"/><circle cx="11" cy="11" r="2.5" fill="white"/></svg>`, 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
|
|
? `<div style="color:#6b7280;font-size:12px;text-align:center;padding:12px">Belum ada penduduk dalam radius ini.</div>`
|
|
: penerima.map(p => {
|
|
var sl = getSkorLabel(p.skor);
|
|
return `<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid #1e2233;">
|
|
<span style="font-size:16px;min-width:20px">${sl.emoji}</span>
|
|
<div style="flex:1;min-width:0">
|
|
<div style="font-size:12px;font-weight:600;color:#e2e8f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${p.nama || '—'}</div>
|
|
<div style="font-size:10px;color:#6b7280">Skor: ${fmtNum(p.skor,1)} · ${sl.label}</div>
|
|
</div>
|
|
<span title="${p.jenis}" style="font-size:16px">${getJenisIcon(p.jenis)}</span>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
return `<div class="ip" style="min-width:280px">
|
|
<div class="ip-head" style="background:linear-gradient(135deg,#92400e,#f59e0b)">🕌 ${d.nama}</div>
|
|
<div style="display:flex;border-bottom:1px solid #1e2233;margin-bottom:8px">
|
|
<button id="tab1_${d.id}" onclick="switchTab(${d.id},1)" style="flex:1;padding:7px;font-size:11px;background:#1e2233;border:none;color:#f59e0b;cursor:pointer;font-weight:600">📋 Profil</button>
|
|
<button id="tab2_${d.id}" onclick="switchTab(${d.id},2)" style="flex:1;padding:7px;font-size:11px;background:transparent;border:none;color:#6b7280;cursor:pointer">👥 Penerima (${penerima.length})</button>
|
|
</div>
|
|
<div id="tabContent1_${d.id}" class="ip-body">
|
|
<div class="ip-row"><span class="ip-label">PIC</span><span class="ip-val">${d.pic}</span></div>
|
|
<div class="ip-row"><span class="ip-label">Radius Layanan</span><span class="ip-val">${fmtNum(d.radius,0)} m</span></div>
|
|
<div class="ip-row"><span class="ip-label">Jml. Ditanggung</span><span class="ip-val" style="color:#f59e0b;font-weight:700">${penerima.length} KK</span></div>
|
|
</div>
|
|
<div id="tabContent2_${d.id}" class="ip-body" style="display:none;max-height:220px;overflow-y:auto">
|
|
${rowsPenerima}
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
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
|
|
? `<div class="ip-row"><span class="ip-label">Masjid Penanggung</span><span class="ip-val" style="color:#f59e0b;font-weight:600">🕌 ${penanggung.nama}</span></div>`
|
|
: `<div class="ip-row"><span class="ip-label">Status</span><span class="ip-val" style="color:#ef4444;font-weight:600">⚠️ Blind Spot</span></div>`;
|
|
|
|
var jenisIcon = getJenisIcon(d.jenis_bantuan);
|
|
|
|
return `<div class="ip">
|
|
<div class="ip-head" style="background:linear-gradient(135deg,#701a75,#e879f9)">👨👩👧👦 KK — ${d.nama_kk}</div>
|
|
<div class="ip-body">
|
|
<div class="ip-row"><span class="ip-label">Jml. Anggota KK</span><span class="ip-val">${d.jumlah_anggota} orang</span></div>
|
|
<div class="ip-row"><span class="ip-label">Alamat</span><span class="ip-val" style="font-size:11px">${d.alamat}</span></div>
|
|
<div class="ip-row"><span class="ip-label">Penghasilan</span><span class="ip-val">${d.penghasilan}</span></div>
|
|
<div class="ip-row"><span class="ip-label">Kondisi Rumah</span><span class="ip-val">${d.kondisi_rumah}</span></div>
|
|
<div class="ip-row"><span class="ip-label">Sumber Air</span><span class="ip-val">${d.sumber_air}</span></div>
|
|
<div class="ip-row">
|
|
<span class="ip-label">Skor Kerentanan</span>
|
|
<span class="ip-val" style="color:${sl.color};font-weight:700">${sl.emoji} ${fmtNum(skor,1)} — ${sl.label}</span>
|
|
</div>
|
|
${penanggungHtml}
|
|
<div class="ip-row">
|
|
<span class="ip-label">Jenis Bantuan</span>
|
|
<span class="ip-val">${jenisIcon} ${d.jenis_bantuan || '—'}</span>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
/* ============================================================
|
|
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<minDist) { minDist=dist; inRadius=true; }
|
|
});
|
|
|
|
var pEntry = { id:parseInt(d.id), lat:pLat, lng:pLng, skor, jenis:d.jenis_bantuan, nama:d.nama_kk, inRadius, rawData:d };
|
|
pendudukData.push(pEntry);
|
|
|
|
var marker = L.marker([pLat,pLng], { icon:makePendudukIcon(inRadius) });
|
|
marker._type='penduduk'; marker._db_id=d.id;
|
|
pEntry.markerLayer = marker;
|
|
marker.bindPopup(() => 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 =>
|
|
`<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;font-size:11px">
|
|
<span style="color:#9ca3af">${o.icon} ${o.value}</span>
|
|
<span style="font-weight:700;color:#e2e8f0">${jenisCounts[o.value]}</span>
|
|
</div>`
|
|
).join('');
|
|
|
|
var el = document.getElementById('statsContent');
|
|
if (!el) return;
|
|
el.innerHTML = `
|
|
<div class="stat-row" style="margin-bottom:10px">
|
|
<div class="stat-item"><div class="stat-num">${total}</div><div class="stat-lbl">Total Warga</div></div>
|
|
<div class="stat-item"><div class="stat-num" style="color:#ef4444">${sangat}</div><div class="stat-lbl">🔴 Sgt. Rentan</div></div>
|
|
<div class="stat-item"><div class="stat-num" style="color:#f97316">${rentan}</div><div class="stat-lbl">🟠 Rentan</div></div>
|
|
<div class="stat-item"><div class="stat-num" style="color:#eab308">${cukup}</div><div class="stat-lbl">🟡 Cukup</div></div>
|
|
</div>
|
|
<div style="border-top:1px solid #1e2233;padding-top:8px;margin-bottom:8px">
|
|
<div style="font-size:10px;color:#6b7280;font-weight:600;margin-bottom:6px;text-transform:uppercase;letter-spacing:0.05em">Jenis Bantuan Dibutuhkan</div>
|
|
${jenisRows}
|
|
</div>
|
|
<div style="border-top:1px solid #1e2233;padding-top:8px">
|
|
<div style="display:flex;justify-content:space-between;font-size:11px">
|
|
<span style="color:#9ca3af">⚠️ Blind Spot (skor ≥70)</span>
|
|
<span style="font-weight:700;color:${bs>0?'#ef4444':'#4ade80'}">${bs} KK</span>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
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 = '▼';
|
|
}
|
|
}
|