/* ============================================================
draw_sosial.js — WebGIS Pendataan Sosial Pontianak
v3.0 — Smart DSS: Heatmap, Masjid Penanggung, Bantuan, Blind Spot
============================================================ */
/* ── 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; // inisialisasi setelah data dimuat
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(); // heatmap off jika penduduk off
} else if (type === 'heatmap') {
toggleHeatmap();
}
}
function toggleHeatmap() {
showHeatmap = !showHeatmap;
var btn = document.getElementById('btnHeatmap');
if (showHeatmap) {
// Heatmap ON: sembunyikan marker penduduk
map.removeLayer(layerGroupPenduduk);
showPenduduk = false;
document.getElementById('btnPenduduk').classList.remove('active');
if (heatmapLayer) map.addLayer(heatmapLayer);
if (btn) btn.classList.add('active');
} else {
// Heatmap OFF: kembalikan marker penduduk
if (heatmapLayer) map.removeLayer(heatmapLayer);
map.addLayer(layerGroupPenduduk);
showPenduduk = true;
document.getElementById('btnPenduduk').classList.add('active');
if (btn) btn.classList.remove('active');
}
}
/* ============================================================
DRAW CONTROL — hanya Marker
============================================================ */
var drawnItems = new L.FeatureGroup().addTo(map);
var drawControl = new L.Control.Draw({
position: 'topleft',
edit: { featureGroup: drawnItems },
draw: { marker:true, polyline:false, polygon:false, rectangle:false, circle:false, circlemarker:false }
});
map.addControl(drawControl);
/* ============================================================
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;
var 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' : '' });
}
/* ============================================================
RADIUS LOGIC
============================================================ */
function recalcAllRadius(previewM = null) {
pendudukData.forEach(p => {
var inAny = false;
var minDist = Infinity;
masjidData.forEach(m => {
var d = map.distance([p.lat, p.lng], [m.lat, m.lng]);
if (d <= m.radius) { inAny = true; if (d < minDist) minDist = d; }
});
if (previewM) {
var d = map.distance([p.lat, p.lng], [previewM.lat, previewM.lng]);
if (d <= previewM.radius) { inAny = true; if (d < minDist) minDist = d; }
}
if (p.inRadius !== inAny) {
p.inRadius = inAny;
p.markerLayer.setIcon(makePendudukIcon(inAny));
}
});
}
/* ============================================================
POPUP BUILDERS
============================================================ */
/* ── POPUP MASJID (2 Tab) ── */
function buildMasjidPopup(d) {
// Cari daftar penerima di dalam radius masjid ini
var penerima = pendudukData
.map(p => {
var dist = map.distance([p.lat, p.lng], [d.lat, d.lng]);
return { ...p, dist, rawData: p.rawData };
})
.filter(p => p.dist <= d.radius)
.sort((a,b) => b.skor - a.skor);
var rowsPenerima = '';
if (penerima.length === 0) {
rowsPenerima = `
Belum ada penduduk dalam radius ini.
`;
} else {
penerima.forEach((p, i) => {
var sl = getSkorLabel(p.skor);
rowsPenerima += `
${sl.emoji}
${p.nama || '—'}
Skor: ${fmtNum(p.skor,1)} · ${sl.label}
${getJenisIcon(p.jenis)}
`;
});
}
return `
🕌 ${d.nama}
📋 Profil
👥 Penerima (${penerima.length})
PIC ${d.pic}
Radius ${fmtNum(d.radius,0)} m
💾 Simpan Radius
🗑 Hapus
${rowsPenerima}
`;
}
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);
// Cari masjid penanggung
var penanggung = findMasjidPenanggung(pLat, pLng);
var penanggungHtml = penanggung
? `Masjid Penanggung 🕌 ${penanggung.nama}
`
: `Status ⚠️ Blind Spot
`;
// Dropdown jenis bantuan
var optionsHtml = JENIS_BANTUAN_OPTS.map(o =>
`${o.icon} ${o.value} `
).join('');
var selectStyle = `width:100%;padding:6px;border-radius:6px;border:1px solid #1e2233;background:#0a0d14;color:#e2e8f0;font-size:12px;`;
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 Direkomendasikan
${optionsHtml}
💾 Simpan Bantuan
🗑 Hapus
`;
}
function saveJenisBantuan(id) {
var sel = document.getElementById('jenisBantuan_' + id);
if (!sel) return;
fetch('../php/update_penduduk.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, jenis_bantuan: sel.value })
}).then(r => r.json()).then(r => {
if (r.message && r.message.includes('Berhasil')) {
// Update local data
var p = pendudukData.find(x => x.id === id);
if (p) p.jenis = sel.value;
renderStats();
} else {
alert('Gagal: ' + r.message);
}
});
}
/* ── RADIUS SLIDER ── */
function onRadiusSlide(masjidId, val) {
val = parseFloat(val);
var el = document.getElementById('radiusDisplay_' + masjidId);
if (el) el.textContent = fmtNum(val,0) + ' m';
var m = masjidData.find(x => x.id === masjidId);
if (m) { m.radius = val; m.circleLayer.setRadius(val); recalcAllRadius(); }
}
function saveRadius(masjidId) {
var m = masjidData.find(x => x.id === masjidId);
if (!m) return;
fetch('../php/update_masjid.php', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ id: masjidId, radius: m.radius })
}).then(r => r.json()).then(r => {
if (r.message && r.message.includes('Berhasil')) alert('Radius berhasil disimpan!');
else alert('Gagal: ' + r.message);
});
}
/* ============================================================
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;
// Bind popup after pendudukData is populated (defer)
layerGroupMasjid.addLayer(marker);
}
if (d.type === 'penduduk') pendudukRaw.push(d);
});
/* Load penduduk setelah masjid siap */
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);
// Heatmap point: [lat, lng, intensity 0-1]
heatPoints.push([pLat, pLng, skor / 100]);
});
// Now bind masjid popups (pendudukData is ready)
masjidData.forEach(m => {
m.markerLayer.bindPopup(() => buildMasjidPopup(m), { maxWidth:310 });
});
// Init heatmap layer
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' }
});
}
// Post-load tasks
checkBlindSpots();
renderStats();
})
.catch(err => console.error('Gagal load data:', err));
/* ============================================================
BLIND SPOT DETECTION
============================================================ */
function checkBlindSpots() {
blindSpotIds = [];
pendudukData.forEach(p => {
if (!p.inRadius && p.skor >= 70) {
blindSpotIds.push(p.id);
// Mark icon merah
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.forEach(p => {
if (blindSpotIds.includes(p.id)) bounds.push([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 = `
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 = '▼';
}
}
/* ============================================================
HAPUS
============================================================ */
function hapusData(id, type) {
if (!confirm('Yakin ingin menghapus?')) return;
fetch(`../php/delete.php?id=${id}&type=${type}`)
.then(r=>r.json()).then(r=>{
if (r.message && r.message.toLowerCase().includes('berhasil')) location.reload();
else alert('Gagal: ' + (r.error||r.message));
});
}
/* ============================================================
ON CREATED — hanya Marker
============================================================ */
map.on(L.Draw.Event.CREATED, function(e) {
var layer = e.layer;
if (e.layerType !== 'marker') return;
var activeMode = 'masjid';
var modeInput = document.querySelector('input[name="mode"]:checked');
if (modeInput) activeMode = modeInput.value;
if (activeMode === 'masjid') { handleMasjid(layer); }
else if (activeMode === 'penduduk') { handlePenduduk(layer); }
});
/* ── MASJID ── */
function handleMasjid(layer) {
var ll = layer.getLatLng();
var defaultRadius = 500;
window._previewLatLng = ll;
layer.addTo(map);
var previewCircle = L.circle([ll.lat,ll.lng], { radius:defaultRadius, color:'#f59e0b', fillColor:'#f59e0b', fillOpacity:0.08, weight:2, dashArray:'6 4' }).addTo(map);
window._previewCircle = previewCircle;
recalcAllRadius({ lat:ll.lat, lng:ll.lng, radius:defaultRadius });
layer.bindPopup(``, { maxWidth:290 }).openPopup();
layer.on('popupclose', function() {
map.removeLayer(previewCircle);
window._previewCircle=null; window._previewLatLng=null;
map.removeLayer(layer);
recalcAllRadius();
});
}
function previewRadius(val) {
val = parseFloat(val);
document.getElementById('mRadiusVal').textContent = fmtNum(val,0)+' m';
if (window._previewCircle) window._previewCircle.setRadius(val);
if (window._previewLatLng) {
recalcAllRadius({ lat:window._previewLatLng.lat, lng:window._previewLatLng.lng, radius:val });
}
}
function saveMasjid(lat, lng) {
var nama=document.getElementById('mNama').value.trim(), pic=document.getElementById('mPIC').value.trim(), radius=parseFloat(document.getElementById('mRadiusSlider').value);
if (!nama||!pic) { alert('Nama dan PIC wajib diisi!'); return; }
fetch('../php/insert_masjid.php',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({nama,pic,radius,lat,lng}) })
.then(r=>r.json())
.then(r=>{ if(r.id) location.reload(); else alert('Gagal: '+(r.error||r.message)); })
.catch(e => alert('Terjadi kesalahan jaringan atau server saat menyimpan masjid.'));
}
/* ── PENDUDUK ── */
function handlePenduduk(layer) {
var ll = layer.getLatLng();
layer.addTo(map);
var selectStyle = "width:100%; margin-bottom:10px; padding:6px; border-radius:6px; border:1px solid #1e2233; background:#0a0d14; color:#e2e8f0;";
layer.bindPopup(`👨👩👧👦 Tambah Penduduk Miskin
`, { maxWidth:310 }).openPopup();
// Reverse Geocoding OSM Nominatim
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${ll.lat}&lon=${ll.lng}&zoom=18&addressdetails=1`)
.then(r => r.json())
.then(data => {
var addrInput = document.getElementById('pdAlamat');
if (addrInput) {
if (data && data.display_name) {
addrInput.value = data.display_name;
} else {
addrInput.placeholder = 'Gagal menemukan alamat, harap isi manual';
}
}
})
.catch(err => {
var addrInput = document.getElementById('pdAlamat');
if (addrInput) addrInput.placeholder = 'Koneksi API OSM gagal, isi manual';
});
layer.on('popupclose', function() { map.removeLayer(layer); });
}
function savePenduduk(lat, lng) {
var nama_kk=document.getElementById('pdNama').value.trim(),
jumlah_anggota=parseInt(document.getElementById('pdJml').value)||1,
alamat=document.getElementById('pdAlamat').value.trim(),
penghasilan=document.getElementById('pdPenghasilan').value,
kondisi_rumah=document.getElementById('pdRumah').value,
sumber_air=document.getElementById('pdAir').value;
if (!nama_kk||!alamat) { alert('Nama KK dan Alamat wajib diisi!'); return; }
fetch('../php/insert_penduduk.php',{
method:'POST', headers:{'Content-Type':'application/json'},
body:JSON.stringify({nama_kk, jumlah_anggota, alamat, lat, lng, penghasilan, kondisi_rumah, sumber_air})
})
.then(r=>r.json())
.then(r=>{ if(r.id) location.reload(); else alert('Gagal: '+(r.error||r.message)); })
.catch(e => alert('Terjadi kesalahan jaringan atau server saat menyimpan penduduk. Coba periksa koneksi atau error log.'));
}