Project Webgis2 SIG

This commit is contained in:
2026-06-11 10:38:09 +07:00
commit eb4d6b7eb0
22 changed files with 2914 additions and 0 deletions
+916
View File
@@ -0,0 +1,916 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8" />
<title>SIG Pendataan Warga Kurang Mampu - Pontianak</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<style>
* { box-sizing: border-box; }
body { margin: 0; font-family: Arial; }
#map { height: 100vh; }
/* ══════════════════════════════════════
TOPBAR
══════════════════════════════════════ */
#topbar {
position: absolute;
top: 0; left: 0; right: 0;
height: 46px;
background: linear-gradient(90deg, #1a3a5c, #2e6da4);
z-index: 2000;
display: flex;
align-items: center;
padding: 0 14px;
gap: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
}
#topbar .tb-logo { font-size: 20px; }
#topbar .tb-title {
color: white;
font-size: 14px;
font-weight: 700;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#topbar .tb-badge {
background: rgba(255,255,255,0.18);
color: white;
font-size: 11px;
font-weight: 600;
padding: 3px 9px;
border-radius: 20px;
border: 1px solid rgba(255,255,255,0.3);
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
#topbar .tb-badge.admin-badge { background: rgba(46,204,113,0.3); border-color: rgba(46,204,113,0.5); }
#topbar .tb-badge.user-badge { background: rgba(255,255,255,0.15); }
#topbar .tb-user {
color: rgba(255,255,255,0.85);
font-size: 12px;
white-space: nowrap;
}
#topbar .tb-logout {
background: rgba(255,255,255,0.15);
border: 1px solid rgba(255,255,255,0.3);
color: white;
padding: 5px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: background 0.2s;
white-space: nowrap;
}
#topbar .tb-logout:hover { background: rgba(255,255,255,0.28); }
/* ══════════════════════════════════════
INFO BANNER (khusus pengguna biasa)
══════════════════════════════════════ */
#infoBanner {
position: absolute;
top: 46px; left: 0; right: 0;
background: linear-gradient(90deg, #e8f4fd, #d0eaf8);
border-bottom: 1px solid #aed6f1;
padding: 7px 14px;
font-size: 12px;
color: #1a5276;
z-index: 1500;
display: none;
align-items: center;
gap: 8px;
}
/* ══════════════════════════════════════
PANELS — offset turun untuk topbar
══════════════════════════════════════ */
.panel-offset { top: 56px; } /* 46px topbar + 10px gap */
.control-box {
position: absolute;
top: 56px;
left: 50px;
background: white;
padding: 12px;
z-index: 1000;
border-radius: 8px;
width: 240px;
box-shadow: 0 0 5px rgba(0,0,0,0.3);
}
.control-box label {
font-size: 14px;
display: block;
margin-top: 5px;
}
/* ── readonly-mode: sembunyikan control box ── */
.control-box.readonly { display: none; }
/* ══════════════════════════════════════
TOMBOL TOGGLE — posisi tetap di kiri,
tidak ikut panel buka/tutup
══════════════════════════════════════ */
.toggle-btn {
position: absolute;
left: 8px;
width: 34px;
height: 34px;
background: white;
border: none;
border-radius: 6px;
box-shadow: 0 0 5px rgba(0,0,0,0.3);
cursor: pointer;
font-size: 17px;
z-index: 1100;
display: flex;
align-items: center;
justify-content: center;
}
.toggle-btn:hover { background: #f0f4f8; }
/* Posisi vertikal tiap tombol — Admin (ada control-box) */
#toggleStatsBtn { top: 172px; }
#toggleLayerBtn { top: 214px; }
#toggleHeatmapBtn { top: 256px; }
/* Posisi vertikal — Pengguna (tidak ada control-box) */
body.readonly-mode #toggleStatsBtn { top: 66px; }
body.readonly-mode #toggleLayerBtn { top: 108px; }
body.readonly-mode #toggleHeatmapBtn { top: 150px; }
/* ══════════════════════════════════════
PANEL — muncul di samping kanan tombol
══════════════════════════════════════ */
.panel-box {
position: absolute;
left: 50px;
background: white;
padding: 12px;
border-radius: 8px;
box-shadow: 0 0 5px rgba(0,0,0,0.3);
z-index: 1000;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
.panel-box.hidden {
visibility: hidden;
opacity: 0;
pointer-events: none;
}
/* Ukuran & posisi per panel — Admin */
.stats-box { top: 166px; width: 240px; font-size: 14px; max-height: 300px; overflow-y: auto; }
.layer-box { top: 208px; width: 220px; }
.heatmap-box { top: 250px; width: 220px; }
/* Posisi panel — Pengguna */
body.readonly-mode .stats-box { top: 60px; }
body.readonly-mode .layer-box { top: 102px; }
body.readonly-mode .heatmap-box { top: 144px; }
/* Tooltip */
.toggle-btn .tooltip {
position: absolute;
left: 40px; top: 50%;
transform: translateY(-50%);
background: rgba(0,0,0,0.75);
color: white;
padding: 3px 8px;
border-radius: 4px;
font-size: 11px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s;
}
.toggle-btn:hover .tooltip { opacity: 1; }
.popup-btn {
padding: 4px 10px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
margin-top: 4px;
}
.popup-btn.edit { background: #eaf2fb; color: #1a3a5c; }
.popup-btn.hapus { background: #fdecea; color: #c0392b; }
.popup-btn:hover { opacity: 0.82; }
/* ── Readonly notice di popup ── */
.popup-readonly-note {
font-size: 11px;
color: #888;
font-style: italic;
margin-top: 6px;
}
@media (max-width: 768px) {
.stats-box, .layer-box { left: 10px; width: 220px; }
#topbar .tb-title { font-size: 12px; }
}
</style>
</head>
<body>
<!-- ══════════════════════════════════════
TOPBAR
══════════════════════════════════════ -->
<div id="topbar">
<span class="tb-logo">🗺️</span>
<span class="tb-title">SIG Pendataan Warga Kurang Mampu Pontianak</span>
<span class="tb-badge" id="roleBadge">&nbsp;</span>
<span class="tb-user" id="usernameLabel"></span>
<button class="tb-logout" onclick="doLogout()">🚪 Keluar</button>
</div>
<!-- ══════════════════════════════════════
INFO BANNER (Pengguna biasa)
══════════════════════════════════════ -->
<div id="infoBanner">
&nbsp;Anda masuk sebagai <strong>Pengguna</strong>. Anda hanya dapat melihat data. Hubungi Admin untuk mengubah data.
</div>
<!-- ══════════════════════════════════════
PANEL MODE INPUT (Admin only)
══════════════════════════════════════ -->
<div class="control-box" id="controlBox">
<b>Mode Input:</b>
<label><input type="radio" name="mode" value="none" checked> Tidak ada</label>
<label><input type="radio" name="mode" value="ibadah"> Rumah Ibadah</label>
<label><input type="radio" name="mode" value="warga"> Rumah Warga</label>
<hr>
</div>
<!-- ══════════════════════════════════════
TOMBOL TOGGLE — berdiri sendiri, posisi tetap
══════════════════════════════════════ -->
<button id="toggleStatsBtn" class="toggle-btn" onclick="toggleStatistik()" >📊<span class="tooltip">Statistik Analitis</span></button>
<button id="toggleLayerBtn" class="toggle-btn" onclick="toggleLayerBox()" >🗂️<span class="tooltip">Layer Control</span></button>
<button id="toggleHeatmapBtn" class="toggle-btn" onclick="toggleHeatmapBox()" >🔥<span class="tooltip">Heatmap</span></button>
<!-- ══════════════════════════════════════
PANEL STATISTIK
══════════════════════════════════════ -->
<div class="stats-box panel-box hidden" id="statsBox">
<b>STATISTIK ANALITIS</b>
<hr>
<div id="statistikContent">Memuat statistik...</div>
</div>
<!-- ══════════════════════════════════════
PANEL LAYER CONTROL
══════════════════════════════════════ -->
<div class="layer-box panel-box hidden" id="layerBox">
<b>LAYER CONTROL</b>
<hr>
<label>
<input type="checkbox" checked id="checkAllLayer" onchange="toggleSemuaLayer(this)">
Semua Layer
</label>
<hr>
<label><input type="checkbox" checked onchange="toggleLayer(this, layerMasjid)"> Masjid</label><br>
<label><input type="checkbox" checked onchange="toggleLayer(this, layerGereja)"> Gereja</label><br>
<label><input type="checkbox" checked onchange="toggleLayer(this, layerVihara)"> Vihara</label><br>
<label><input type="checkbox" checked onchange="toggleLayer(this, layerPura)"> Pura</label>
<hr>
<b>REKOMENDASI</b><br><br>
<label><input type="checkbox" checked onchange="toggleRekomendasi(this)"> Lokasi Baru</label>
<hr>
<label><input type="checkbox" checked onchange="toggleLayer(this, layerWargaCover)"> Warga Tercover</label><br>
<label><input type="checkbox" checked onchange="toggleLayer(this, layerWargaTidakCover)"> Warga Belum Tercover</label>
</div>
<!-- ══════════════════════════════════════
PANEL HEATMAP
══════════════════════════════════════ -->
<div class="heatmap-box panel-box hidden" id="heatmapBox">
<b>HEATMAP</b>
<hr>
<label><input type="radio" name="heatmapOption" onchange="pilihHeatmap('semua')"> Semua Warga</label><br>
<label><input type="radio" name="heatmapOption" onchange="pilihHeatmap('cover')"> Warga Tercover</label><br>
<label><input type="radio" name="heatmapOption" onchange="pilihHeatmap('tidakcover')"> Warga Belum Tercover</label><br><br>
<label><input type="radio" name="heatmapOption" onchange="pilihHeatmap('none')" checked> Nonaktifkan Heatmap</label>
</div>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.heat/dist/leaflet-heat.js"></script>
<script>
// ══════════════════════════════════════
// AUTENTIKASI — cek sesi
// ══════════════════════════════════════
var ROLE = sessionStorage.getItem('sig_role');
var USERNAME = sessionStorage.getItem('sig_username');
var LOGGEDIN = sessionStorage.getItem('sig_loggedin');
var IS_ADMIN = (ROLE === 'admin');
if (LOGGEDIN !== '1') {
// Belum login → redirect ke halaman login
window.location.href = 'login.html';
}
function doLogout() {
sessionStorage.clear();
window.location.href = 'login.html';
}
// ── Tampilkan info user di topbar ──────────────────────────────
document.getElementById('usernameLabel').textContent = USERNAME || '';
var badge = document.getElementById('roleBadge');
if (IS_ADMIN) {
badge.innerHTML = '🛡️ &nbsp;Admin';
badge.classList.add('admin-badge');
} else {
badge.innerHTML = '👤 &nbsp;Pengguna';
badge.classList.add('user-badge');
}
// ── Tampilkan info banner untuk pengguna biasa ─────────────────
if (!IS_ADMIN) {
document.getElementById('infoBanner').style.display = 'flex';
}
// ── Atur posisi panel (ada/tidaknya control-box) ───────────────
if (!IS_ADMIN) {
// Sembunyikan panel Mode Input
document.getElementById('controlBox').classList.add('readonly');
document.body.classList.add('readonly-mode');
// Geser panel lain ke atas
document.getElementById('statsBox').classList.add('readonly-pos');
document.getElementById('layerBox').classList.add('readonly-pos');
document.getElementById('heatmapBox').classList.add('readonly-pos');
}
// ══════════════════════════════════════
// INISIALISASI PETA
// ══════════════════════════════════════
var map = L.map('map').setView([-0.0263, 109.3425], 13);
var ibadahLayers = [];
var dataIbadah = [];
var dataWarga = [];
var heatmapLayer;
var wargaMarkers = {};
var layerRekomendasi = L.layerGroup().addTo(map);
var layerMasjid = L.layerGroup().addTo(map);
var layerGereja = L.layerGroup().addTo(map);
var layerVihara = L.layerGroup().addTo(map);
var layerPura = L.layerGroup().addTo(map);
var layerWargaCover = L.layerGroup().addTo(map);
var layerWargaTidakCover = L.layerGroup().addTo(map);
var heatmapSemua, heatmapCover, heatmapTidakCover;
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap'
}).addTo(map);
// ══════════════════════════════════════
// HELPER FUNGSI IKON
// ══════════════════════════════════════
function getMode() {
return document.querySelector('input[name="mode"]:checked').value;
}
function getColorByJenis(jenis) {
if (jenis === 'Masjid') return 'green';
if (jenis === 'Gereja') return 'blue';
if (jenis === 'Vihara') return 'orange';
if (jenis === 'Pura') return 'yellow';
return 'gray';
}
function getIconByJenis(jenis) {
return new L.Icon({
iconUrl: `https://maps.google.com/mapfiles/ms/icons/${getColorByJenis(jenis)}-dot.png`,
iconSize: [32, 32], iconAnchor: [16, 32]
});
}
function getWargaIcon(tercover = true) {
return new L.Icon({
iconUrl: `https://maps.google.com/mapfiles/ms/icons/${tercover ? 'red' : 'purple'}-dot.png`,
iconSize: [32, 32], iconAnchor: [16, 32]
});
}
// ══════════════════════════════════════
// POPUP BUILDER — konten beda per role
// ══════════════════════════════════════
function popupIbadah(nama, jenis, leafletId, radius, dbId) {
var sliderHtml = IS_ADMIN
? `<input type="range" min="100" max="2000" value="${radius}"
oninput="updateRadius(this.value, ${leafletId}, ${dbId})"><br><br>
<button class="popup-btn edit" onclick="editIbadah(${dbId}, ${JSON.stringify(nama)}, ${JSON.stringify(jenis)}, ${radius})">✏️ Edit</button>
<button class="popup-btn hapus" onclick="hapusIbadah(${dbId})">🗑 Hapus</button>`
: `<em class="popup-readonly-note">🔒 Hanya Admin yang dapat mengubah data</em>`;
return `<b>${nama}</b><br>
Jenis: ${jenis}<br>
Radius: <span id="rVal-${leafletId}">${radius}</span> m<br>
${sliderHtml}`;
}
function popupWarga(nama, jumlah, pengurus, tercover, dbId) {
var aksiHtml = IS_ADMIN
? `<button class="popup-btn edit" onclick="editWarga(${dbId}, '${nama.replace(/'/g,"\\'")}', ${jumlah})">✏️ Edit</button>
<button class="popup-btn hapus" onclick="hapusWarga(${dbId})">🗑 Hapus</button>`
: `<em class="popup-readonly-note">🔒 Hanya Admin yang dapat mengubah data</em>`;
return `<b>Warga Kurang Mampu</b><br>
Nama KK: ${nama}<br>
Jumlah KK: ${jumlah}<br>
Pengurus: ${pengurus}
${!tercover ? "<br><b style='color:red'>⚠ Belum Tercover</b>" : ''}<br><br>
${aksiHtml}`;
}
// ══════════════════════════════════════
// KLIK PETA — hanya Admin
// ══════════════════════════════════════
map.on('click', function(e) {
if (!IS_ADMIN) return; // Pengguna biasa tidak bisa klik input
var mode = getMode();
if (mode === 'none') { alert('Pilih mode input terlebih dahulu!'); return; }
if (mode === 'ibadah') {
L.popup().setLatLng(e.latlng).setContent(`
<b>Input Rumah Ibadah</b><br>
Nama:<br><input type="text" id="namaIbadah"><br><br>
Jenis:<br>
<select id="popupJenis">
<option value="Masjid">Masjid</option>
<option value="Gereja">Gereja</option>
<option value="Vihara">Vihara</option>
<option value="Pura">Pura</option>
</select><br><br>
<button onclick="simpanIbadah(${e.latlng.lat}, ${e.latlng.lng})">Simpan</button>
`).openOn(map);
}
if (mode === 'warga') {
L.popup().setLatLng(e.latlng).setContent(`
<b>Input Data Warga</b><br>
Nama Kepala KK:<br><input type="text" id="namaKK"><br><br>
Jumlah KK:<br><input type="number" id="jumlahKK"><br><br>
<button onclick="simpanWarga(${e.latlng.lat}, ${e.latlng.lng})">Simpan</button>
`).openOn(map);
}
});
// ══════════════════════════════════════
// SIMPAN IBADAH
// ══════════════════════════════════════
function simpanIbadah(lat, lng) {
var nama = document.getElementById('namaIbadah').value;
var jenis = document.getElementById('popupJenis').value;
if (!nama.trim()) { alert('Nama wajib diisi'); return; }
var color = getColorByJenis(jenis);
var icon = getIconByJenis(jenis);
var radius = 500;
var latlng = [lat, lng];
var targetLayer = { Masjid: layerMasjid, Gereja: layerGereja, Vihara: layerVihara, Pura: layerPura }[jenis] || layerMasjid;
var circle = L.circle(latlng, { radius, color, fillColor: color, fillOpacity: 0.3 }).addTo(targetLayer);
var marker = L.marker(latlng, { icon }).addTo(targetLayer);
var lid = circle._leaflet_id;
ibadahLayers[lid] = circle;
fetch('api/simpan_ibadah.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nama, jenis, lat, lng, radius })
})
.then(r => r.json())
.then(result => {
marker.bindPopup(popupIbadah(nama, jenis, lid, radius, result.id)).openPopup();
});
dataIbadah.push({ nama, jenis, lat, lng, circle });
updateStatistik();
map.closePopup();
}
// ══════════════════════════════════════
// SIMPAN WARGA
// ══════════════════════════════════════
function simpanWarga(lat, lng) {
var nama = document.getElementById('namaKK').value;
var jumlah = document.getElementById('jumlahKK').value;
if (!nama.trim()) { alert('Nama wajib diisi'); return; }
if (!jumlah || jumlah <= 0) { alert('Jumlah KK tidak valid'); return; }
var pengurus = cariPengurus(lat, lng);
var infoPengurus = pengurus ? `${pengurus.nama} (${pengurus.jenis})` : 'Tidak tercover';
var tercover = cekTercover(lat, lng);
fetch('api/simpan_warga.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nama, jumlah, lat, lng, pengurus: infoPengurus })
})
.then(r => r.json())
.then(result => {
var dbId = result.id;
dataWarga.push({ id: dbId, nama, jumlah, lat, lng });
updateStatistik();
buatRekomendasiLokasi();
buatHeatmap();
var targetLayer = tercover ? layerWargaCover : layerWargaTidakCover;
var marker = L.marker([lat, lng], { icon: getWargaIcon(tercover) }).addTo(targetLayer);
wargaMarkers[dbId] = marker;
marker.bindPopup(popupWarga(nama, jumlah, infoPengurus, tercover, dbId));
});
map.closePopup();
}
// ══════════════════════════════════════
// EDIT & HAPUS — hanya dipanggil admin
// ══════════════════════════════════════
function editWarga(id, namaLama, jumlahLama) {
if (!IS_ADMIN) return;
var namaBaru = prompt('Nama Kepala KK:', namaLama); if (!namaBaru) return;
var jumlahBaru = prompt('Jumlah KK:', jumlahLama); if (!jumlahBaru) return;
fetch('api/update_warga.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nama: namaBaru, jumlah: jumlahBaru })
}).then(() => { alert('Data warga berhasil diupdate'); location.reload(); });
}
function editIbadah(id, namaLama, jenisLama, radiusLama) {
if (!IS_ADMIN) return;
var namaBaru = prompt('Nama Rumah Ibadah:', namaLama); if (!namaBaru) return;
var jenisBaru = prompt('Jenis (Masjid/Gereja/Vihara/Pura):', jenisLama); if (!jenisBaru) return;
var radiusBaru = prompt('Radius:', radiusLama); if (!radiusBaru) return;
fetch('api/update_ibadah.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nama: namaBaru, jenis: jenisBaru, radius: radiusBaru })
}).then(() => { alert('Data rumah ibadah berhasil diupdate'); location.reload(); });
}
function hapusIbadah(id) {
if (!IS_ADMIN) return;
if (!confirm('Hapus data ini?')) return;
fetch('api/hapus_ibadah.php?id=' + id).then(() => location.reload());
}
function hapusWarga(id) {
if (!IS_ADMIN) return;
if (!confirm('Hapus data ini?')) return;
fetch('api/hapus_warga.php?id=' + id).then(() => location.reload());
}
// ══════════════════════════════════════
// UPDATE RADIUS — hanya admin
// ══════════════════════════════════════
function updateRadius(val, leafletId, dbId) {
if (!IS_ADMIN) return;
var circle = ibadahLayers[leafletId];
if (circle) circle.setRadius(Number(val));
var text = document.getElementById('rVal-' + leafletId);
if (text) text.innerHTML = val;
fetch('api/update_radius.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: dbId, radius: Number(val) })
});
updatePengurusWarga();
updateStatistik();
}
// ══════════════════════════════════════
// LOAD DATA (baca saja — semua role)
// ══════════════════════════════════════
window.onload = function() { loadData(); };
function loadData() {
fetch('api/get_data.php')
.then(r => r.json())
.then(data => {
L.geoJSON(data, {
pointToLayer: function(feature, latlng) {
if (feature.properties.tipe === 'ibadah') {
var jenis = feature.properties.jenis;
var color = getColorByJenis(jenis);
var targetLayer = { Masjid: layerMasjid, Gereja: layerGereja, Vihara: layerVihara, Pura: layerPura }[jenis] || layerMasjid;
var marker = L.marker(latlng, { icon: getIconByJenis(jenis) }).addTo(targetLayer);
var circle = L.circle(latlng, {
radius: feature.properties.radius,
color, fillColor: color, fillOpacity: 0.3
}).addTo(targetLayer);
var lid = circle._leaflet_id;
ibadahLayers[lid] = circle;
marker.bindPopup(
popupIbadah(
feature.properties.nama,
jenis,
lid,
feature.properties.radius,
feature.properties.id
)
);
dataIbadah.push({
nama: feature.properties.nama,
jenis,
lat: latlng.lat,
lng: latlng.lng,
circle
});
return marker;
}
if (feature.properties.tipe === 'warga') {
var tercover = feature.properties.pengurus !== 'Tidak tercover';
var targetLayer = tercover ? layerWargaCover : layerWargaTidakCover;
var marker = L.marker(latlng, { icon: getWargaIcon(tercover) }).addTo(targetLayer);
wargaMarkers[feature.properties.id] = marker;
marker.bindPopup(
popupWarga(
feature.properties.nama,
feature.properties.jumlah_kk,
feature.properties.pengurus,
tercover,
feature.properties.id
)
);
dataWarga.push({
id: feature.properties.id,
nama: feature.properties.nama,
jumlah: feature.properties.jumlah_kk,
lat: latlng.lat,
lng: latlng.lng
});
return marker;
}
}
}).addTo(map);
updateStatistik();
buatRekomendasiLokasi();
});
}
// ══════════════════════════════════════
// ANALISIS SPASIAL
// ══════════════════════════════════════
function cariPengurus(lat, lng) {
var terdekat = null, jarakMin = Infinity;
dataIbadah.forEach(item => {
if (!item.circle) return;
var jarak = map.distance([lat, lng], item.circle.getLatLng());
if (jarak <= item.circle.getRadius() && jarak < jarakMin) {
jarakMin = jarak; terdekat = item;
}
});
return terdekat;
}
function cekTercover(lat, lng) {
return dataIbadah.some(ibadah => {
if (!ibadah.circle) return false;
return map.distance([lat, lng], ibadah.circle.getLatLng()) <= ibadah.circle.getRadius();
});
}
function updatePengurusWarga() {
dataWarga.forEach(warga => {
var pengurusBaru = 'Tidak tercover';
dataIbadah.forEach(ibadah => {
if (!ibadah.circle) return;
var jarak = map.distance([warga.lat, warga.lng], ibadah.circle.getLatLng());
if (jarak <= ibadah.circle.getRadius()) {
pengurusBaru = `${ibadah.nama} (${ibadah.jenis})`;
}
});
fetch('api/update_pengurus.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: warga.id, pengurus: pengurusBaru })
});
var marker = wargaMarkers[warga.id];
if (!marker) return;
var tercover = pengurusBaru !== 'Tidak tercover';
marker.setIcon(getWargaIcon(tercover));
layerWargaCover.removeLayer(marker);
layerWargaTidakCover.removeLayer(marker);
(tercover ? layerWargaCover : layerWargaTidakCover).addLayer(marker);
marker.setPopupContent(
popupWarga(warga.nama, warga.jumlah, pengurusBaru, tercover, warga.id)
);
});
buatRekomendasiLokasi();
buatHeatmap();
}
// ══════════════════════════════════════
// STATISTIK
// ══════════════════════════════════════
function updateStatistik() {
var totalWarga = dataWarga.length;
var totalKK = dataWarga.reduce((s, w) => s + Number(w.jumlah || 0), 0);
var counts = { Masjid: 0, Gereja: 0, Vihara: 0, Pura: 0 };
dataIbadah.forEach(i => { if (counts[i.jenis] !== undefined) counts[i.jenis]++; });
var ranking = dataIbadah
.filter(i => i.circle)
.map(i => ({
nama: i.nama,
jenis: i.jenis,
total: dataWarga.filter(w => map.distance([w.lat, w.lng], i.circle.getLatLng()) <= i.circle.getRadius()).length
}))
.sort((a, b) => b.total - a.total);
var belum = dataWarga
.filter(w => !cekTercover(w.lat, w.lng))
.map(w => w.nama);
var html = `
<b>Total Warga Kurang Mampu:</b><br>${totalWarga}<br><br>
<b>Total KK:</b><br>${totalKK}<br><br>
<b>Total Rumah Ibadah:</b><br>
Masjid: ${counts.Masjid}<br>
Gereja: ${counts.Gereja}<br>
Vihara: ${counts.Vihara}<br>
Pura: ${counts.Pura}<br><br>
<b>Cakupan Warga Terbanyak:</b><br>
`;
ranking.forEach((item, i) => { html += `${i + 1}. ${item.nama} (${item.total} warga)<br>`; });
html += `<br><b>Warga Belum Tercover:</b><br>`;
html += belum.length === 0
? 'Semua warga sudah tercover'
: belum.map(n => `- ${n}`).join('<br>');
document.getElementById('statistikContent').innerHTML = html;
}
// ══════════════════════════════════════
// HEATMAP
// ══════════════════════════════════════
function buatHeatmap() {
var dataSemua = [], dataCover = [], dataTidakCover = [];
dataWarga.forEach(w => {
var pt = [w.lat, w.lng, (w.jumlah || 1) * 2];
dataSemua.push(pt);
(cekTercover(w.lat, w.lng) ? dataCover : dataTidakCover).push(pt);
});
var mk = (data, grad, r = 45, b = 18, mo = 0.5) =>
L.heatLayer(data, { radius: r, blur: b, maxZoom: 18, minOpacity: mo, gradient: grad });
[heatmapSemua, heatmapCover, heatmapTidakCover].forEach(h => { if (h) map.removeLayer(h); });
heatmapSemua = mk(dataSemua, { 0.2:'#d6ecff', 0.4:'#a8d8ff', 0.6:'#73c2fb', 0.8:'#3399ff', 1:'#0066cc' });
heatmapCover = mk(dataCover, { 0.2:'#b3ffb3', 0.4:'#66ff66', 0.6:'#00cc00', 0.8:'#009900', 1:'#006600' });
heatmapTidakCover = mk(dataTidakCover, { 0.2:'#ffd6d6', 0.4:'#ff8080', 0.6:'#ff3333', 0.8:'#e60000', 1:'#800000' }, 50, 20, 0.6);
}
function pilihHeatmap(jenis) {
buatHeatmap();
[heatmapSemua, heatmapCover, heatmapTidakCover].forEach(h => { if (h) map.removeLayer(h); });
var target = { semua: heatmapSemua, cover: heatmapCover, tidakcover: heatmapTidakCover }[jenis];
if (target) map.addLayer(target);
}
// ══════════════════════════════════════
// REKOMENDASI LOKASI
// ══════════════════════════════════════
function buatRekomendasiLokasi() {
layerRekomendasi.clearLayers();
var belum = dataWarga.filter(w => !cekTercover(w.lat, w.lng));
if (belum.length === 0) return;
var sumLat = belum.reduce((s, w) => s + w.lat, 0);
var sumLng = belum.reduce((s, w) => s + w.lng, 0);
var totalKK = belum.reduce((s, w) => s + (Number(w.jumlah) || 1), 0);
var centerLat = sumLat / belum.length;
var centerLng = sumLng / belum.length;
var rekIcon = new L.Icon({
iconUrl: 'https://maps.google.com/mapfiles/kml/paddle/wht-circle.png',
iconSize: [32, 32], iconAnchor: [16, 32]
});
L.marker([centerLat, centerLng], { icon: rekIcon })
.bindPopup(`<b>Rekomendasi Lokasi Baru</b><hr>
Jumlah Warga Belum Tercover: <b>${totalKK}</b><br><br>
Titik rekomendasi ini merupakan pusat area warga yang belum tercover rumah ibadah.`)
.addTo(layerRekomendasi);
}
// ══════════════════════════════════════
// TOGGLE PANEL
// ══════════════════════════════════════
function toggleStatistik() {
var s = document.getElementById('statsBox');
var l = document.getElementById('layerBox');
var h = document.getElementById('heatmapBox');
var isOpen = !s.classList.contains('hidden');
s.classList.toggle('hidden');
if (!isOpen) { l.classList.add('hidden'); h.classList.add('hidden'); }
}
function toggleLayerBox() {
var l = document.getElementById('layerBox');
var s = document.getElementById('statsBox');
var h = document.getElementById('heatmapBox');
var isOpen = !l.classList.contains('hidden');
l.classList.toggle('hidden');
if (!isOpen) { s.classList.add('hidden'); h.classList.add('hidden'); }
}
function toggleHeatmapBox() {
var h = document.getElementById('heatmapBox');
var s = document.getElementById('statsBox');
var l = document.getElementById('layerBox');
var isOpen = !h.classList.contains('hidden');
h.classList.toggle('hidden');
if (!isOpen) { s.classList.add('hidden'); l.classList.add('hidden'); }
}
function toggleLayer(checkbox, layer) {
checkbox.checked ? map.addLayer(layer) : map.removeLayer(layer);
}
function toggleSemuaLayer(checkbox) {
document.querySelectorAll('#layerBox input[type="checkbox"]').forEach(cb => cb.checked = checkbox.checked);
[layerMasjid, layerGereja, layerVihara, layerPura,
layerWargaCover, layerWargaTidakCover, layerRekomendasi,
heatmapSemua, heatmapCover, heatmapTidakCover]
.forEach(layer => { if (!layer) return; checkbox.checked ? map.addLayer(layer) : map.removeLayer(layer); });
}
function toggleRekomendasi(checkbox) {
checkbox.checked ? map.addLayer(layerRekomendasi) : map.removeLayer(layerRekomendasi);
}
</script>
</body>
</html>