menambah fitur-poverty
This commit is contained in:
@@ -0,0 +1,683 @@
|
||||
<!doctype html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Poverty Mapping Pontianak</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="" />
|
||||
<style>
|
||||
:root {
|
||||
--green: #1b4332;
|
||||
--green-2: #2d6a4f;
|
||||
--gold: #f4a261;
|
||||
--gold-soft: rgba(244, 162, 97, 0.2);
|
||||
--blue: #2563eb;
|
||||
--red: #dc2626;
|
||||
--dark-red: #7f1d1d;
|
||||
--orange: #ea580c;
|
||||
--yellow: #eab308;
|
||||
--text: #0f172a;
|
||||
--muted: #64748b;
|
||||
--panel: rgba(255, 255, 255, 0.9);
|
||||
--border: rgba(15, 23, 42, 0.12);
|
||||
--shadow: 0 18px 45px rgba(15, 23, 42, 0.18);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--text);
|
||||
background: #eef2f7;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
position: fixed;
|
||||
z-index: 900;
|
||||
top: 14px;
|
||||
left: 14px;
|
||||
right: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 420px);
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brand,
|
||||
.radius-panel,
|
||||
.stats-panel,
|
||||
.family-panel {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 20px;
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(18px);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-height: 74px;
|
||||
padding: 14px 18px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, rgba(27, 67, 50, 0.96), rgba(45, 106, 79, 0.92));
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
display: grid;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
border-radius: 16px;
|
||||
background: rgba(244, 162, 97, 0.22);
|
||||
font-size: 1.45rem;
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.brand p {
|
||||
margin: 4px 0 0;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.radius-panel {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.radius-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.radius-row strong {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
width: 100%;
|
||||
accent-color: var(--gold);
|
||||
}
|
||||
|
||||
.stats-panel {
|
||||
position: fixed;
|
||||
z-index: 850;
|
||||
top: 112px;
|
||||
left: 14px;
|
||||
width: 340px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-panel h2,
|
||||
.family-panel h2 {
|
||||
margin: 0 0 12px;
|
||||
color: var(--green);
|
||||
font-size: 0.92rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 15px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.stat-card span {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat-card strong {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: var(--text);
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.family-panel {
|
||||
position: fixed;
|
||||
z-index: 920;
|
||||
top: 112px;
|
||||
right: 14px;
|
||||
width: min(410px, calc(100vw - 28px));
|
||||
max-height: calc(100vh - 132px);
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
transform: translateX(calc(100% + 24px));
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.family-panel.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 0;
|
||||
border-radius: 11px;
|
||||
color: #fff;
|
||||
background: var(--green);
|
||||
cursor: pointer;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.family-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.family-item {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.family-item strong {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.family-item small {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
padding: 4px 8px;
|
||||
color: #fff;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.badge.sangat_miskin {
|
||||
background: var(--dark-red);
|
||||
}
|
||||
|
||||
.badge.miskin {
|
||||
background: var(--orange);
|
||||
}
|
||||
|
||||
.badge.rentan_miskin {
|
||||
color: #422006;
|
||||
background: var(--yellow);
|
||||
}
|
||||
|
||||
.popup-btn {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
margin-top: 10px;
|
||||
border: 0;
|
||||
border-radius: 11px;
|
||||
color: #fff;
|
||||
background: var(--green);
|
||||
cursor: pointer;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.worship-marker {
|
||||
display: grid;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
place-items: center;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 50% 50% 50% 12px;
|
||||
background: var(--gold);
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.24);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.worship-marker span {
|
||||
transform: rotate(45deg);
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.family-marker {
|
||||
display: grid;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
place-items: center;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.2);
|
||||
transition: background-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.family-marker.inside {
|
||||
background: var(--red);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.family-marker.outside {
|
||||
background: var(--blue);
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.leaflet-popup-content h3 {
|
||||
margin: 0 0 5px;
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.leaflet-popup-content p {
|
||||
margin: 0 0 7px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.topbar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.brand {
|
||||
min-height: unset;
|
||||
}
|
||||
|
||||
.stats-panel {
|
||||
top: 190px;
|
||||
width: calc(100vw - 28px);
|
||||
}
|
||||
|
||||
.family-panel {
|
||||
top: auto;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
width: auto;
|
||||
max-height: 55vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
|
||||
<header class="topbar">
|
||||
<section class="brand">
|
||||
<div class="brand-icon">🤝</div>
|
||||
<div>
|
||||
<h1>Poverty Mapping Pontianak</h1>
|
||||
<p>Pemetaan keluarga miskin di sekitar tempat ibadah untuk dukungan komunitas.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="radius-panel">
|
||||
<div class="radius-row">
|
||||
<span>Radius analisis</span>
|
||||
<strong id="radiusLabel">500 meter</strong>
|
||||
</div>
|
||||
<input id="radiusSlider" type="range" min="100" max="2000" step="50" value="500" />
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<section class="stats-panel">
|
||||
<h2>Dashboard Ringkas</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span>Terdampak</span>
|
||||
<strong id="totalAffected">0</strong>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span>Cakupan</span>
|
||||
<strong id="coveragePercent">0%</strong>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span>Sangat Miskin</span>
|
||||
<strong id="veryPoorCount">0</strong>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span>Miskin</span>
|
||||
<strong id="poorCount">0</strong>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span>Rentan Miskin</span>
|
||||
<strong id="vulnerableCount">0</strong>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span>Tempat Dipilih</span>
|
||||
<strong id="selectedName">-</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="family-panel" id="familyPanel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2>Daftar Keluarga</h2>
|
||||
<p class="muted" id="familyPanelSubtitle">Pilih tempat ibadah untuk melihat daftar.</p>
|
||||
</div>
|
||||
<button class="close-btn" id="closePanel" type="button">×</button>
|
||||
</div>
|
||||
<div class="family-list" id="familyList"></div>
|
||||
</aside>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||||
<script>
|
||||
const API_BASE = "/api/poverty";
|
||||
const fallbackTempatIbadah = [
|
||||
{ id: 1, nama: "Masjid Jami Sultan Syarif Abdurrahman", jenis: "masjid", alamat: "Jl. Tanjungpura", kelurahan: "Dalam Bugis", lat: -0.0226, lng: 109.3316 },
|
||||
{ id: 2, nama: "Gereja Bethel Indonesia", jenis: "gereja", alamat: "Jl. Gajahmada", kelurahan: "Sungai Bangkong", lat: -0.0334, lng: 109.3253 },
|
||||
{ id: 3, nama: "Vihara Bodhisattva", jenis: "vihara", alamat: "Jl. Diponegoro", kelurahan: "Mariana", lat: -0.0289, lng: 109.3401 }
|
||||
];
|
||||
const fallbackKeluarga = [
|
||||
{ id: 1, nama_kk: "Ahmad Fauzi", jumlah_anggota: 5, kategori: "sangat_miskin", kelurahan: "Dalam Bugis", keterangan: "Prioritas bantuan pangan.", lat: -0.0231, lng: 109.3321 },
|
||||
{ id: 2, nama_kk: "Siti Rahmah", jumlah_anggota: 3, kategori: "miskin", kelurahan: "Dalam Bugis", keterangan: "Membutuhkan dukungan pendidikan.", lat: -0.0219, lng: 109.3308 },
|
||||
{ id: 3, nama_kk: "Budi Santoso", jumlah_anggota: 6, kategori: "rentan_miskin", kelurahan: "Sungai Bangkong", keterangan: "Pendapatan tidak tetap.", lat: -0.0341, lng: 109.3261 },
|
||||
{ id: 4, nama_kk: "Maria Goretti", jumlah_anggota: 4, kategori: "miskin", kelurahan: "Mariana", keterangan: "Butuh verifikasi lanjutan.", lat: -0.0295, lng: 109.3412 },
|
||||
{ id: 5, nama_kk: "Lim Ah Kow", jumlah_anggota: 2, kategori: "sangat_miskin", kelurahan: "Mariana", keterangan: "Lansia tinggal sendiri.", lat: -0.0501, lng: 109.36 }
|
||||
];
|
||||
|
||||
const kategoriLabel = {
|
||||
sangat_miskin: "Sangat Miskin",
|
||||
miskin: "Miskin",
|
||||
rentan_miskin: "Rentan Miskin"
|
||||
};
|
||||
|
||||
const worshipIcon = {
|
||||
masjid: "🕌",
|
||||
gereja: "⛪",
|
||||
pura: "🛕",
|
||||
vihara: "☸️",
|
||||
kelenteng: "🏮",
|
||||
lainnya: "⭐"
|
||||
};
|
||||
|
||||
const map = L.map("map").setView([-0.0263, 109.3425], 13);
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
const worshipLayer = L.layerGroup().addTo(map);
|
||||
const familyLayer = L.layerGroup().addTo(map);
|
||||
let radiusCircle = null;
|
||||
let selectedIbadah = null;
|
||||
let currentRadius = 500;
|
||||
let tempatIbadah = [];
|
||||
let keluarga = [];
|
||||
const familyMarkers = new Map();
|
||||
const worshipMarkers = new Map();
|
||||
|
||||
const elements = {
|
||||
radiusSlider: document.getElementById("radiusSlider"),
|
||||
radiusLabel: document.getElementById("radiusLabel"),
|
||||
totalAffected: document.getElementById("totalAffected"),
|
||||
coveragePercent: document.getElementById("coveragePercent"),
|
||||
veryPoorCount: document.getElementById("veryPoorCount"),
|
||||
poorCount: document.getElementById("poorCount"),
|
||||
vulnerableCount: document.getElementById("vulnerableCount"),
|
||||
selectedName: document.getElementById("selectedName"),
|
||||
familyPanel: document.getElementById("familyPanel"),
|
||||
familyPanelSubtitle: document.getElementById("familyPanelSubtitle"),
|
||||
familyList: document.getElementById("familyList"),
|
||||
closePanel: document.getElementById("closePanel")
|
||||
};
|
||||
|
||||
function geoJsonToRows(payload) {
|
||||
if (payload?.type === "FeatureCollection") {
|
||||
return payload.features.map((feature) => ({
|
||||
...feature.properties,
|
||||
lng: Number(feature.geometry.coordinates[0]),
|
||||
lat: Number(feature.geometry.coordinates[1])
|
||||
}));
|
||||
}
|
||||
if (Array.isArray(payload?.data)) return payload.data;
|
||||
if (Array.isArray(payload)) return payload;
|
||||
return [];
|
||||
}
|
||||
|
||||
async function fetchRows(url, fallback) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const rows = geoJsonToRows(await response.json());
|
||||
return rows.length ? rows : fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function makeWorshipIcon(jenis) {
|
||||
return L.divIcon({
|
||||
className: "",
|
||||
html: `<div class="worship-marker"><span>${worshipIcon[jenis] || worshipIcon.lainnya}</span></div>`,
|
||||
iconSize: [42, 42],
|
||||
iconAnchor: [21, 40],
|
||||
popupAnchor: [0, -36]
|
||||
});
|
||||
}
|
||||
|
||||
function makeFamilyIcon(isInside) {
|
||||
return L.divIcon({
|
||||
className: "",
|
||||
html: `<div class="family-marker ${isInside ? "inside" : "outside"}">👥</div>`,
|
||||
iconSize: [28, 28],
|
||||
iconAnchor: [14, 14],
|
||||
popupAnchor: [0, -12]
|
||||
});
|
||||
}
|
||||
|
||||
function formatMeters(value) {
|
||||
if (value == null || Number.isNaN(value)) return "-";
|
||||
return `${Math.round(value).toLocaleString("id-ID")} m`;
|
||||
}
|
||||
|
||||
function distanceBetween(a, b) {
|
||||
return L.latLng(a.lat, a.lng).distanceTo(L.latLng(b.lat, b.lng));
|
||||
}
|
||||
|
||||
function nearestWorshipDistance(family) {
|
||||
if (!tempatIbadah.length) return null;
|
||||
return Math.min(...tempatIbadah.map((ibadah) => distanceBetween(family, ibadah)));
|
||||
}
|
||||
|
||||
function familiesInRadius() {
|
||||
if (!selectedIbadah) return [];
|
||||
return keluarga
|
||||
.map((item) => ({ ...item, distance: distanceBetween(item, selectedIbadah) }))
|
||||
.filter((item) => item.distance <= currentRadius)
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
}
|
||||
|
||||
function familyPopup(item) {
|
||||
const nearest = nearestWorshipDistance(item);
|
||||
return `
|
||||
<h3>${item.nama_kk}</h3>
|
||||
<p><strong>Anggota:</strong> ${item.jumlah_anggota} orang</p>
|
||||
<p><strong>Kategori:</strong> <span class="badge ${item.kategori}">${kategoriLabel[item.kategori]}</span></p>
|
||||
<p><strong>Catatan:</strong> ${item.keterangan || "-"}</p>
|
||||
<p><strong>Jarak tempat ibadah terdekat:</strong> ${formatMeters(nearest)}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
function worshipPopup(item) {
|
||||
const count = selectedIbadah?.id === item.id ? familiesInRadius().length : keluarga.filter((family) => distanceBetween(family, item) <= currentRadius).length;
|
||||
return `
|
||||
<h3>${item.nama}</h3>
|
||||
<p><strong>Jenis:</strong> ${item.jenis}</p>
|
||||
<p><strong>Alamat:</strong> ${item.alamat || "-"}</p>
|
||||
<p><strong>Keluarga dalam radius:</strong> ${count}</p>
|
||||
<button class="popup-btn" onclick="window.openFamilyPanel()">Lihat Daftar Keluarga</button>
|
||||
`;
|
||||
}
|
||||
|
||||
function selectIbadah(item) {
|
||||
selectedIbadah = item;
|
||||
updateCoverage();
|
||||
elements.familyPanel.classList.remove("open");
|
||||
}
|
||||
|
||||
function renderMarkers() {
|
||||
worshipLayer.clearLayers();
|
||||
familyLayer.clearLayers();
|
||||
worshipMarkers.clear();
|
||||
familyMarkers.clear();
|
||||
|
||||
tempatIbadah.forEach((item) => {
|
||||
const marker = L.marker([item.lat, item.lng], { icon: makeWorshipIcon(item.jenis), title: item.nama })
|
||||
.bindPopup(() => worshipPopup(item))
|
||||
.on("click", () => selectIbadah(item));
|
||||
marker.addTo(worshipLayer);
|
||||
worshipMarkers.set(item.id, marker);
|
||||
});
|
||||
|
||||
keluarga.forEach((item) => {
|
||||
const marker = L.marker([item.lat, item.lng], { icon: makeFamilyIcon(false), title: item.nama_kk })
|
||||
.bindPopup(() => familyPopup(item));
|
||||
marker.addTo(familyLayer);
|
||||
familyMarkers.set(item.id, marker);
|
||||
});
|
||||
}
|
||||
|
||||
function renderFamilyPanel() {
|
||||
const rows = familiesInRadius();
|
||||
elements.familyPanelSubtitle.textContent = selectedIbadah
|
||||
? `${selectedIbadah.nama} • radius ${currentRadius} meter`
|
||||
: "Pilih tempat ibadah untuk melihat daftar.";
|
||||
elements.familyList.innerHTML = rows.length
|
||||
? rows.map((item) => `
|
||||
<article class="family-item">
|
||||
<strong>${item.nama_kk}</strong>
|
||||
<span class="badge ${item.kategori}">${kategoriLabel[item.kategori]}</span>
|
||||
<p><small>Jarak ${formatMeters(item.distance)} • ${item.jumlah_anggota} anggota • ${item.kelurahan || "-"}</small></p>
|
||||
</article>
|
||||
`).join("")
|
||||
: `<p class="muted">Tidak ada keluarga dalam radius saat ini.</p>`;
|
||||
}
|
||||
|
||||
function updateStats(rows) {
|
||||
const total = rows.length;
|
||||
const counts = rows.reduce((acc, item) => {
|
||||
acc[item.kategori] = (acc[item.kategori] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
elements.totalAffected.textContent = total;
|
||||
elements.coveragePercent.textContent = keluarga.length ? `${Math.round((total / keluarga.length) * 100)}%` : "0%";
|
||||
elements.veryPoorCount.textContent = counts.sangat_miskin || 0;
|
||||
elements.poorCount.textContent = counts.miskin || 0;
|
||||
elements.vulnerableCount.textContent = counts.rentan_miskin || 0;
|
||||
elements.selectedName.textContent = selectedIbadah ? selectedIbadah.nama.split(" ").slice(0, 2).join(" ") : "-";
|
||||
}
|
||||
|
||||
function updateCoverage() {
|
||||
elements.radiusLabel.textContent = `${currentRadius} meter`;
|
||||
|
||||
if (radiusCircle) {
|
||||
radiusCircle.remove();
|
||||
radiusCircle = null;
|
||||
}
|
||||
|
||||
const rows = familiesInRadius();
|
||||
const insideIds = new Set(rows.map((item) => item.id));
|
||||
|
||||
if (selectedIbadah) {
|
||||
radiusCircle = L.circle([selectedIbadah.lat, selectedIbadah.lng], {
|
||||
radius: currentRadius,
|
||||
color: "#f4a261",
|
||||
weight: 2,
|
||||
fillColor: "#f4a261",
|
||||
fillOpacity: 0.18
|
||||
}).addTo(map);
|
||||
}
|
||||
|
||||
keluarga.forEach((item) => {
|
||||
const marker = familyMarkers.get(item.id);
|
||||
if (!marker) return;
|
||||
marker.setIcon(makeFamilyIcon(insideIds.has(item.id)));
|
||||
if (marker.isPopupOpen()) marker.setPopupContent(familyPopup(item));
|
||||
});
|
||||
|
||||
if (selectedIbadah) {
|
||||
const marker = worshipMarkers.get(selectedIbadah.id);
|
||||
if (marker?.isPopupOpen()) marker.setPopupContent(worshipPopup(selectedIbadah));
|
||||
}
|
||||
|
||||
updateStats(rows);
|
||||
renderFamilyPanel();
|
||||
}
|
||||
|
||||
window.openFamilyPanel = function () {
|
||||
renderFamilyPanel();
|
||||
elements.familyPanel.classList.add("open");
|
||||
};
|
||||
|
||||
elements.radiusSlider.addEventListener("input", (event) => {
|
||||
currentRadius = Number(event.target.value);
|
||||
updateCoverage();
|
||||
});
|
||||
|
||||
elements.closePanel.addEventListener("click", () => {
|
||||
elements.familyPanel.classList.remove("open");
|
||||
});
|
||||
|
||||
async function init() {
|
||||
[tempatIbadah, keluarga] = await Promise.all([
|
||||
fetchRows(`${API_BASE}/tempat-ibadah`, fallbackTempatIbadah),
|
||||
fetchRows(`${API_BASE}/keluarga`, fallbackKeluarga)
|
||||
]);
|
||||
renderMarkers();
|
||||
selectedIbadah = tempatIbadah[0] || null;
|
||||
updateCoverage();
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user