/* ============================================================ 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}
PIC${d.pic}
Radius${fmtNum(d.radius,0)} m
100m5000m
`; } 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 => `` ).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}
`; } 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 = `
${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 = '▼'; } } /* ============================================================ 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(`
🕌 Tambah Masjid
100m${defaultRadius} m5000m
📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}
`, { 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
📍 ${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}
`, { 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.')); }