feat: add animated custom markers for SPBU and implement centralized application logic in app.js

This commit is contained in:
z0rayy
2026-06-08 21:59:05 +07:00
parent a090cc7934
commit d3c9de3a34
4 changed files with 175 additions and 58 deletions
+76 -26
View File
@@ -145,6 +145,33 @@
#loader { position: fixed; inset: 0; background: var(--clr-bg); color: var(--clr-text); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 9999; gap: 12px; }
.loader-spinner { width: 36px; height: 36px; border: 3px solid var(--clr-border); border-top-color: var(--clr-danger); border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* ---- Custom Marker ---- */
.custom-marker {
display: flex; justify-content: center; align-items: center;
}
.marker-halo {
position: absolute;
width: 40px; height: 40px;
border-radius: 50%;
animation: pulse 2s infinite ease-out;
}
.marker-halo.ibadah { background: rgba(34, 197, 94, 0.5); }
.marker-halo.miskin-in { background: rgba(239, 68, 68, 0.5); }
.marker-halo.miskin-out { background: rgba(30, 41, 59, 0.5); }
.marker-icon-inner {
position: relative; z-index: 2;
width: 28px; height: 28px;
background: white; border-radius: 50%;
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
display: flex; justify-content: center; align-items: center;
font-size: 14px;
}
@keyframes pulse {
0% { transform: scale(0.5); opacity: 1; }
100% { transform: scale(1.5); opacity: 0; }
}
</style>
</head>
<body>
@@ -274,9 +301,23 @@
ibadahLayer = L.layerGroup().addTo(map);
miskinLayer = L.layerGroup().addTo(map);
ibadahIcon = L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25,41], iconAnchor: [12,41], popupAnchor: [1,-34], shadowSize: [41,41] });
miskinRedIcon = L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25,41], iconAnchor: [12,41], popupAnchor: [1,-34], shadowSize: [41,41] });
miskinBlackIcon = L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-black.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25,41], iconAnchor: [12,41], popupAnchor: [1,-34], shadowSize: [41,41] });
const ibadahCustomIcon = L.divIcon({
className: 'custom-marker',
html: `<div class="marker-halo ibadah"></div><div class="marker-icon-inner">🕌</div>`,
iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14]
});
const miskinInIcon = L.divIcon({
className: 'custom-marker',
html: `<div class="marker-halo miskin-in"></div><div class="marker-icon-inner">🏠</div>`,
iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14]
});
const miskinOutIcon = L.divIcon({
className: 'custom-marker',
html: `<div class="marker-halo miskin-out"></div><div class="marker-icon-inner" style="filter: grayscale(1);">🏠</div>`,
iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14]
});
try {
const [ir, mr] = await Promise.all([fetch('backend/read_ibadah.php'), fetch('backend/read_miskin.php')]);
@@ -289,8 +330,6 @@
window.updateRadius = function(id, val) {
ibadahRadiuses[id] = parseInt(val);
const el = document.getElementById('radius-val-' + id);
if (el) el.textContent = val;
renderBuffers();
renderMiskin();
};
@@ -314,18 +353,23 @@
ibadahLayer.clearLayers(); ibadahPoints = [];
const bounds = [];
const f = document.getElementById('search-ibadah').value.toLowerCase();
ibadahData.forEach(item => {
if (f && !item.nama.toLowerCase().includes(f)) return;
const lat = parseFloat(item.latitude), lng = parseFloat(item.longitude);
ibadahData.forEach(i => {
if (f && !i.nama.toLowerCase().includes(f)) return;
const lat = parseFloat(i.latitude), lng = parseFloat(i.longitude);
if (isNaN(lat) || isNaN(lng)) return;
ibadahPoints.push({ id: item.id, pt: turf.point([lng, lat]) });
const r = ibadahRadiuses[item.id] || 0;
const popup = '<strong style="color:#166534;">' + item.nama + '</strong><br><small>' + item.alamat + '</small>'
+ '<div class="popup-slider-wrap">'
+ '<span class="popup-slider-label">Radius: <b><span id="radius-val-' + item.id + '">' + r + '</span> m</b></span>'
+ '<input type="range" class="popup-slider" min="0" max="5000" step="100" value="' + r + '" oninput="window.updateRadius(' + item.id + ', this.value)">'
+ '</div>';
L.marker([lat, lng], { icon: ibadahIcon }).bindPopup(popup, { minWidth: 220 }).addTo(ibadahLayer);
const rVal = ibadahRadiuses[i.id] || 0;
ibadahPoints.push({ id: i.id, geom: [lng, lat], rad: rVal });
const marker = L.marker([lat, lng], { icon: L.divIcon({ className: 'custom-marker', html: `<div class="marker-halo ibadah"></div><div class="marker-icon-inner">🕌</div>`, iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14] }) })
.bindPopup(`
<strong>${i.nama}</strong><br>${i.alamat}<br><br>
<label style="font-size:11px;font-weight:600;color:#64748b;">Atur Radius Analisis (meter)</label>
<input type="range" min="0" max="5000" step="100" value="${rVal}" style="width:100%"
oninput="document.getElementById('rval-${i.id}').innerText=this.value"
onchange="updateRadius(${i.id}, this.value)">
<div style="font-size:11px;color:#94a3b8;margin-top:2px;" id="rval-${i.id}">${rVal}</div>
`)
.addTo(ibadahLayer);
bounds.push([lat, lng]);
});
@@ -339,18 +383,24 @@
miskinLayer.clearLayers();
const bounds = [];
const f = document.getElementById('search-miskin').value.toLowerCase();
miskinData.forEach(item => {
if (f && !item.nama_kk.toLowerCase().includes(f)) return;
const lat = parseFloat(item.latitude), lng = parseFloat(item.longitude);
miskinData.forEach(m => {
if (f && !m.nama_kk.toLowerCase().includes(f)) return;
const lat = parseFloat(m.latitude), lng = parseFloat(m.longitude);
if (isNaN(lat) || isNaN(lng)) return;
const pt = turf.point([lng, lat]);
const pt = [lng, lat];
let inRadius = false;
for (const ib of ibadahPoints) {
const km = ibadahRadiuses[ib.id] / 1000;
if (km > 0 && turf.distance(pt, ib.pt, { units: 'kilometers' }) <= km) { inRadius = true; break; }
}
L.marker([lat, lng], { icon: inRadius ? miskinRedIcon : miskinBlackIcon })
.bindPopup('<strong>KK: ' + item.nama_kk + '</strong><br>' + (item.keterangan || '-') + '<br>Status: ' + (inRadius ? '🔴 Dalam Radius' : '⚫ Luar Radius'))
ibadahPoints.forEach(ib => {
if (ib.rad > 0 && turf.distance(turf.point(ib.geom), turf.point(pt), { units: 'meters' }) <= ib.rad) inRadius = true;
});
const icon = L.divIcon({
className: 'custom-marker',
html: `<div class="marker-halo ${inRadius ? 'miskin-in' : 'miskin-out'}"></div><div class="marker-icon-inner" ${inRadius ? '' : 'style="filter: grayscale(1);"'}>🏠</div>`,
iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14]
});
L.marker([lat, lng], { icon: icon })
.bindPopup('<strong>KK: ' + m.nama_kk + '</strong><br>' + (m.keterangan || '-') + '<br>Status: ' + (inRadius ? '🔴 Dalam Radius' : '⚫ Luar Radius'))
.addTo(miskinLayer);
bounds.push([lat, lng]);
});