feat: add animated custom markers for SPBU and implement centralized application logic in app.js
This commit is contained in:
+29
@@ -1099,6 +1099,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 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.spbu { background: rgba(34, 197, 94, 0.5); }
|
||||
.marker-halo.spbu-off { background: rgba(245, 158, 11, 0.5); }
|
||||
.marker-halo.ibadah { background: rgba(14, 165, 233, 0.5); }
|
||||
.marker-halo.miskin { background: rgba(239, 68, 68, 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; }
|
||||
}
|
||||
|
||||
/* ---- Floating Panels ---- */
|
||||
.search-panel {
|
||||
position: absolute; top: 16px; left: 50%; transform: translateX(-50%);
|
||||
|
||||
@@ -989,15 +989,16 @@ function renderMapLayers() {
|
||||
|
||||
// Render SPBU
|
||||
window.appState.data.spbu.forEach(item => {
|
||||
const color = item.status === '24jam' ? '#22c55e' : '#f59e0b';
|
||||
const marker = L.circleMarker([item.latitude, item.longitude], {
|
||||
radius: 6,
|
||||
fillColor: color,
|
||||
color: '#fff',
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.8
|
||||
}).bindPopup(`<strong>${item.nama}</strong><br>Status: ${item.status === '24jam' ? '24 Jam' : 'Tidak 24 Jam'}`);
|
||||
const is24 = item.status === '24jam';
|
||||
const haloClass = is24 ? 'spbu' : 'spbu-off';
|
||||
const customIcon = L.divIcon({
|
||||
className: 'custom-marker',
|
||||
html: `<div class="marker-halo ${haloClass}"></div><div class="marker-icon-inner">⛽</div>`,
|
||||
iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14]
|
||||
});
|
||||
|
||||
const marker = L.marker([item.latitude, item.longitude], { icon: customIcon })
|
||||
.bindPopup(`<strong>${item.nama}</strong><br>Status: ${is24 ? '24 Jam' : 'Tidak 24 Jam'}`);
|
||||
|
||||
const layer = item.status === '24jam' ? window.appState.layers.spbu24 : window.appState.layers.spbuTidak;
|
||||
marker.addTo(layer);
|
||||
@@ -1036,27 +1037,25 @@ function renderMapLayers() {
|
||||
|
||||
// Render Ibadah
|
||||
window.appState.data.ibadah.forEach(item => {
|
||||
const marker = L.circleMarker([item.latitude, item.longitude], {
|
||||
radius: 5,
|
||||
fillColor: '#8b5cf6',
|
||||
color: '#fff',
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.8
|
||||
}).bindPopup(`<strong>${item.nama}</strong><br>${item.alamat}`);
|
||||
const ibadahIcon = 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 marker = L.marker([item.latitude, item.longitude], { icon: ibadahIcon })
|
||||
.bindPopup(`<strong>${item.nama}</strong><br>${item.alamat}`);
|
||||
marker.addTo(window.appState.layers.ibadah);
|
||||
});
|
||||
|
||||
// Render Miskin
|
||||
window.appState.data.miskin.forEach(item => {
|
||||
const marker = L.circleMarker([item.latitude, item.longitude], {
|
||||
radius: 5,
|
||||
fillColor: '#000000',
|
||||
color: '#fff',
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
fillOpacity: 0.8
|
||||
}).bindPopup(`<strong>${item.nama_kk}</strong><br>${item.keterangan || ''}`);
|
||||
const miskinIcon = L.divIcon({
|
||||
className: 'custom-marker',
|
||||
html: `<div class="marker-halo miskin"></div><div class="marker-icon-inner">🏠</div>`,
|
||||
iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14]
|
||||
});
|
||||
const marker = L.marker([item.latitude, item.longitude], { icon: miskinIcon })
|
||||
.bindPopup(`<strong>${item.nama_kk}</strong><br>${item.keterangan || ''}`);
|
||||
marker.addTo(window.appState.layers.miskin);
|
||||
});
|
||||
}
|
||||
|
||||
+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]);
|
||||
});
|
||||
|
||||
@@ -147,6 +147,34 @@
|
||||
.legend-panel h3 { font-size: 11px; font-weight: 700; color: var(--clr-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
|
||||
.legend-row { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; color: var(--clr-text); font-size: 12px; }
|
||||
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
||||
|
||||
/* ---- 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.spbu { background: rgba(34, 197, 94, 0.5); }
|
||||
.marker-halo.spbu-off { background: rgba(245, 158, 11, 0.5); }
|
||||
.marker-halo.user { background: rgba(59, 130, 246, 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>
|
||||
@@ -275,9 +303,19 @@
|
||||
if (filterText && !spbu.nama.toLowerCase().includes(filterText.toLowerCase()) && !spbu.no_spbu.toLowerCase().includes(filterText.toLowerCase())) return;
|
||||
const lat = parseFloat(spbu.latitude), lng = parseFloat(spbu.longitude);
|
||||
if (isNaN(lat) || isNaN(lng)) return;
|
||||
const color = spbu.status === '24jam' ? '#22c55e' : '#f59e0b';
|
||||
const marker = L.circleMarker([lat, lng], { radius: 7, fillColor: color, color: '#fff', weight: 2, opacity: 1, fillOpacity: 0.9 })
|
||||
.bindPopup('<strong>' + spbu.nama + '</strong><br>No: ' + spbu.no_spbu + '<br>Status: ' + (spbu.status === '24jam' ? '✅ 24 Jam' : '⏰ Tidak 24 Jam'))
|
||||
|
||||
const is24 = spbu.status === '24jam';
|
||||
const haloClass = is24 ? 'spbu' : 'spbu-off';
|
||||
const customIcon = L.divIcon({
|
||||
className: 'custom-marker',
|
||||
html: `<div class="marker-halo ${haloClass}"></div><div class="marker-icon-inner">⛽</div>`,
|
||||
iconSize: [28, 28],
|
||||
iconAnchor: [14, 14],
|
||||
popupAnchor: [0, -14]
|
||||
});
|
||||
|
||||
const marker = L.marker([lat, lng], { icon: customIcon })
|
||||
.bindPopup('<strong>' + spbu.nama + '</strong><br>No: ' + spbu.no_spbu + '<br>Status: ' + (is24 ? '✅ 24 Jam' : '⏰ Tidak 24 Jam'))
|
||||
.addTo(spbuLayer);
|
||||
bounds.push([lat, lng]);
|
||||
});
|
||||
@@ -335,10 +373,11 @@
|
||||
renderSPBU(spbu.nama.toLowerCase(), true);
|
||||
};
|
||||
|
||||
const userIcon = 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]
|
||||
const userIcon = L.divIcon({
|
||||
className: 'custom-marker',
|
||||
html: `<div class="marker-halo user"></div><div class="marker-icon-inner">📍</div>`,
|
||||
iconSize: [28, 28],
|
||||
iconAnchor: [14, 14]
|
||||
});
|
||||
|
||||
document.getElementById('btn-location').addEventListener('click', () => {
|
||||
|
||||
Reference in New Issue
Block a user