feat: add animated custom markers for SPBU and implement centralized application logic in app.js
This commit is contained in:
+76
-26
@@ -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]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user