742 lines
35 KiB
JavaScript
742 lines
35 KiB
JavaScript
// ═══════════════════════════════════════════════════════════════
|
|
// MAP SETUP
|
|
// ═══════════════════════════════════════════════════════════════
|
|
const map = L.map('map', { preferCanvas: false }).setView([-0.0263, 109.3425], 13);
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright" style="color:#5b7fff">OpenStreetMap</a> contributors',
|
|
maxZoom: 19
|
|
}).addTo(map);
|
|
setTimeout(() => map.invalidateSize(), 150);
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// STATE
|
|
// ═══════════════════════════════════════════════════════════════
|
|
let mode = null; // 'ibadah' | 'miskin' | null
|
|
let editMode = null; // { type:'ibadah'|'miskin', id } — saat edit
|
|
let ibadahList = [];
|
|
let miskinList = [];
|
|
let importedGeoJSON = [];
|
|
let myMarker = null;
|
|
let pendingLatLng = null;
|
|
let pendingAddr = null;
|
|
let tempMarker = null;
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// TOAST
|
|
// ═══════════════════════════════════════════════════════════════
|
|
function showToast(msg, color = '#5b7fff') {
|
|
const t = document.getElementById('toast');
|
|
t.innerHTML = msg;
|
|
t.style.display = 'block';
|
|
t.style.borderLeft = `3px solid ${color}`;
|
|
clearTimeout(t._timer);
|
|
t._timer = setTimeout(() => t.style.display = 'none', 3500);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// TOGGLE MODE + HIGHLIGHT
|
|
// ═══════════════════════════════════════════════════════════════
|
|
function setMode(m) {
|
|
if (mode === m) {
|
|
mode = null; clearHighlights(); updateToolbar(); resetMapCursor(); return;
|
|
}
|
|
mode = m; updateToolbar(); resetMapCursor();
|
|
if (m === 'ibadah') {
|
|
document.getElementById('map').classList.add('adding-ibadah');
|
|
highlightAll('ibadah');
|
|
showToast('🏛️ Klik peta untuk menentukan lokasi Rumah Ibadah', '#5b7fff');
|
|
} else {
|
|
document.getElementById('map').classList.add('adding-miskin');
|
|
highlightAll('miskin');
|
|
showToast('⛽ Klik peta untuk menentukan lokasi SPBU / Pangkalan BBM', '#f59e0b');
|
|
}
|
|
}
|
|
|
|
function resetMapCursor() {
|
|
document.getElementById('map').classList.remove('adding-ibadah', 'adding-miskin');
|
|
}
|
|
|
|
function updateToolbar() {
|
|
document.getElementById('toggle-ibadah').className = 'toggle-btn' + (mode === 'ibadah' ? ' active-ibadah' : '');
|
|
document.getElementById('toggle-miskin').className = 'toggle-btn' + (mode === 'miskin' ? ' active-miskin' : '');
|
|
}
|
|
|
|
function highlightAll(type) {
|
|
clearHighlights();
|
|
if (type === 'ibadah') {
|
|
ibadahList.forEach(ib => {
|
|
if (ib.circle) ib.circle.setStyle({ fillOpacity: 0.18, weight: 3, opacity: 1 });
|
|
pulseMarker(ib.marker);
|
|
const c = document.getElementById('card-ibadah-' + ib.id);
|
|
if (c) c.classList.add('highlighted');
|
|
});
|
|
} else {
|
|
miskinList.forEach(m => {
|
|
pulseMarker(m.marker);
|
|
const c = document.getElementById('card-miskin-' + m.id);
|
|
if (c) c.classList.add('highlighted');
|
|
});
|
|
}
|
|
}
|
|
|
|
function clearHighlights() {
|
|
ibadahList.forEach(ib => {
|
|
if (ib.circle) ib.circle.setStyle({ fillOpacity: 0.08, weight: 2, opacity: 0.8 });
|
|
const c = document.getElementById('card-ibadah-' + ib.id);
|
|
if (c) c.classList.remove('highlighted');
|
|
});
|
|
miskinList.forEach(m => {
|
|
const c = document.getElementById('card-miskin-' + m.id);
|
|
if (c) c.classList.remove('highlighted');
|
|
});
|
|
}
|
|
|
|
function pulseMarker(marker) {
|
|
if (!marker) return;
|
|
const el = marker.getElement(); if (!el) return;
|
|
el.classList.remove('leaflet-marker-pulsing');
|
|
void el.offsetWidth;
|
|
el.classList.add('leaflet-marker-pulsing');
|
|
setTimeout(() => el.classList.remove('leaflet-marker-pulsing'), 3000);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// REVERSE GEOCODING
|
|
// ═══════════════════════════════════════════════════════════════
|
|
async function reverseGeocode(lat, lng) {
|
|
try {
|
|
const r = await fetch(
|
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`,
|
|
{ headers: { 'Accept-Language': 'id' } }
|
|
);
|
|
const d = await r.json();
|
|
return d.display_name || `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
|
|
} catch { return `${lat.toFixed(5)}, ${lng.toFixed(5)}`; }
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// RADIUS: cari rumah ibadah TERDEKAT yang masih dalam radius
|
|
// ═══════════════════════════════════════════════════════════════
|
|
function getNearestIbadah(lat, lng) {
|
|
let nearest = null, minDist = Infinity;
|
|
for (const ib of ibadahList) {
|
|
const dist = map.distance([lat, lng], [ib.lat, ib.lng]);
|
|
if (dist <= ib.radius && dist < minDist) {
|
|
minDist = dist; nearest = ib;
|
|
}
|
|
}
|
|
return nearest; // null = di luar semua radius
|
|
}
|
|
|
|
function updateAllMiskinColors() {
|
|
miskinList.forEach(m => {
|
|
const nearest = getNearestIbadah(m.lat, m.lng);
|
|
m.inRadius = nearest !== null;
|
|
m.nearestIbadah = nearest ? nearest.id : null;
|
|
if (m.marker) m.marker.setIcon(createMiskinIcon(m.inRadius));
|
|
if (m.marker) m.marker.setPopupContent(buildMiskinPopup(m));
|
|
});
|
|
updateStats();
|
|
renderMiskinList();
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// ICONS
|
|
// ═══════════════════════════════════════════════════════════════
|
|
function createIbadahIcon(emoji) {
|
|
return L.divIcon({
|
|
html: `<div style="background:linear-gradient(135deg,#5b7fff,#7c5bff);width:36px;height:36px;
|
|
border-radius:50% 50% 50% 0;transform:rotate(-45deg);border:3px solid #fff;
|
|
box-shadow:0 3px 10px rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;">
|
|
<span style="transform:rotate(45deg);font-size:16px;">${emoji}</span></div>`,
|
|
iconSize: [36,36], iconAnchor: [18,36], popupAnchor: [0,-40], className: ''
|
|
});
|
|
}
|
|
|
|
function createMiskinIcon(inRadius) {
|
|
// Icon SPBU — fuel pump style
|
|
const c = inRadius ? '#f59e0b' : '#ef4444';
|
|
const s = inRadius ? 'rgba(245,158,11,0.5)' : 'rgba(239,68,68,0.5)';
|
|
return L.divIcon({
|
|
html: `<div style="background:${c};width:22px;height:22px;border-radius:4px;
|
|
border:3px solid #fff;box-shadow:0 2px 8px ${s};
|
|
display:flex;align-items:center;justify-content:center;font-size:10px;">⛽</div>`,
|
|
iconSize: [22,22], iconAnchor: [11,11], popupAnchor: [0,-14], className: ''
|
|
});
|
|
}
|
|
|
|
function createTempIcon(type) {
|
|
if (type === 'ibadah') return L.divIcon({
|
|
html: `<div style="background:linear-gradient(135deg,#5b7fff,#7c5bff);opacity:0.55;
|
|
width:36px;height:36px;border-radius:50% 50% 50% 0;transform:rotate(-45deg);
|
|
border:3px dashed #fff;display:flex;align-items:center;justify-content:center;">
|
|
<span style="transform:rotate(45deg);font-size:16px;">❓</span></div>`,
|
|
iconSize: [36,36], iconAnchor: [18,36], className: ''
|
|
});
|
|
return L.divIcon({
|
|
html: `<div style="background:#8890b0;width:20px;height:20px;border-radius:50%;
|
|
border:3px dashed #fff;opacity:0.55;"></div>`,
|
|
iconSize: [20,20], iconAnchor: [10,10], className: ''
|
|
});
|
|
}
|
|
|
|
function createMyIcon() {
|
|
return L.divIcon({
|
|
html: `<div style="background:#f59e0b;width:24px;height:24px;border-radius:50%;
|
|
border:3px solid #fff;box-shadow:0 2px 12px rgba(245,158,11,0.6);animation:mypulse 1.5s infinite;"></div>
|
|
<style>@keyframes mypulse{0%,100%{box-shadow:0 0 0 0 rgba(245,158,11,0.6)}50%{box-shadow:0 0 0 8px rgba(245,158,11,0)}}</style>`,
|
|
iconSize: [24,24], iconAnchor: [12,12], popupAnchor: [0,-14], className: ''
|
|
});
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// MAP CLICK → FORM POPUP
|
|
// ═══════════════════════════════════════════════════════════════
|
|
map.on('click', async (e) => {
|
|
if (!mode) return;
|
|
const { lat, lng } = e.latlng;
|
|
pendingLatLng = { lat, lng };
|
|
pendingAddr = null;
|
|
editMode = null; // ini bukan edit
|
|
|
|
if (tempMarker) map.removeLayer(tempMarker);
|
|
tempMarker = L.marker([lat, lng], { icon: createTempIcon(mode) }).addTo(map);
|
|
|
|
openFormOverlay(mode, lat, lng, null); // null = mode tambah baru
|
|
|
|
reverseGeocode(lat, lng).then(addr => {
|
|
pendingAddr = addr;
|
|
const el = document.getElementById(mode === 'ibadah' ? 'f-ibadah-addr' : 'f-miskin-addr');
|
|
if (el) el.innerHTML = `📍 ${addr}`;
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// FORM OVERLAY — bisa untuk tambah BARU atau EDIT
|
|
// ═══════════════════════════════════════════════════════════════
|
|
function openFormOverlay(type, lat, lng, existingData) {
|
|
const isEdit = existingData !== null;
|
|
editMode = isEdit ? { type, id: existingData.id } : null;
|
|
|
|
const header = document.getElementById('form-header');
|
|
const title = document.getElementById('form-title');
|
|
const fI = document.getElementById('form-ibadah-fields');
|
|
const fM = document.getElementById('form-miskin-fields');
|
|
const saveBtn = document.getElementById('btn-form-save');
|
|
|
|
const coordText = `Lat: ${lat.toFixed(6)} Lng: ${lng.toFixed(6)}`;
|
|
|
|
if (type === 'ibadah') {
|
|
title.textContent = isEdit ? '✏️ Edit Rumah Ibadah' : '🏛️ Tambah Rumah Ibadah';
|
|
header.className = 'form-header ibadah-header';
|
|
fI.style.display = 'block';
|
|
fM.style.display = 'none';
|
|
|
|
document.getElementById('f-ibadah-coord').textContent = coordText;
|
|
document.getElementById('f-ibadah-name').value = isEdit ? existingData.name : '';
|
|
document.getElementById('f-ibadah-type').value = isEdit ? existingData.type : '🕌';
|
|
|
|
const rad = isEdit ? existingData.radius : 500;
|
|
document.getElementById('f-radius-slider').value = rad;
|
|
document.getElementById('f-radius-val').textContent = fmtRadius(rad);
|
|
|
|
if (isEdit) {
|
|
document.getElementById('f-ibadah-addr').innerHTML = `📍 ${existingData.addr}`;
|
|
pendingAddr = existingData.addr;
|
|
} else {
|
|
document.getElementById('f-ibadah-addr').innerHTML = '<span class="addr-loading">🔄 Memuat alamat...</span>';
|
|
}
|
|
|
|
saveBtn.onclick = isEdit ? applyEditIbadah : saveIbadah;
|
|
|
|
} else {
|
|
title.textContent = isEdit ? '✏️ Edit Penduduk Miskin' : '👤 Tambah Penduduk Miskin';
|
|
header.className = 'form-header miskin-header';
|
|
fI.style.display = 'none';
|
|
fM.style.display = 'block';
|
|
|
|
document.getElementById('f-miskin-coord').textContent = coordText;
|
|
document.getElementById('f-miskin-name').value = isEdit ? existingData.name : '';
|
|
|
|
if (isEdit) {
|
|
document.getElementById('f-miskin-addr').innerHTML = `📍 ${existingData.addr}`;
|
|
pendingAddr = existingData.addr;
|
|
} else {
|
|
document.getElementById('f-miskin-addr').innerHTML = '<span class="addr-loading">🔄 Memuat alamat...</span>';
|
|
}
|
|
|
|
saveBtn.onclick = isEdit ? applyEditMiskin : saveMiskin;
|
|
}
|
|
|
|
document.getElementById('form-overlay').classList.remove('hidden');
|
|
setTimeout(() => {
|
|
const inp = document.getElementById(type === 'ibadah' ? 'f-ibadah-name' : 'f-miskin-name');
|
|
if (inp) inp.focus();
|
|
}, 100);
|
|
}
|
|
|
|
window.closeFormOverlay = function () {
|
|
document.getElementById('form-overlay').classList.add('hidden');
|
|
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
|
|
pendingLatLng = null; pendingAddr = null; editMode = null;
|
|
};
|
|
|
|
// radius slider in form
|
|
document.getElementById('f-radius-slider').addEventListener('input', function () {
|
|
document.getElementById('f-radius-val').textContent = fmtRadius(parseInt(this.value));
|
|
});
|
|
// SPBU radius slider
|
|
document.getElementById('f-radius-slider-spbu').addEventListener('input', function () {
|
|
document.getElementById('f-radius-val-spbu').textContent = fmtRadius(parseInt(this.value));
|
|
});
|
|
|
|
function fmtRadius(v) { return v >= 1000 ? (v/1000).toFixed(1)+' km' : v+' m'; }
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// SAVE — TAMBAH IBADAH BARU
|
|
// ═══════════════════════════════════════════════════════════════
|
|
function saveIbadah() {
|
|
if (!pendingLatLng) return;
|
|
const { lat, lng } = pendingLatLng;
|
|
const name = document.getElementById('f-ibadah-name').value.trim() || 'Rumah Ibadah';
|
|
const type = document.getElementById('f-ibadah-type').value;
|
|
const radius = parseInt(document.getElementById('f-radius-slider').value);
|
|
const addr = pendingAddr || `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
|
|
|
|
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
|
|
|
|
// also get spbu radius slider for form that was open
|
|
const id = Date.now();
|
|
const marker = L.marker([lat, lng], { icon: createIbadahIcon(type) }).addTo(map);
|
|
const circle = L.circle([lat, lng], {
|
|
radius, color: '#5b7fff', fillColor: '#5b7fff',
|
|
fillOpacity: 0.08, weight: 2, dashArray: '6,4', opacity: 0.8
|
|
}).addTo(map);
|
|
|
|
const obj = { id, name, type, lat, lng, radius, addr, marker, circle };
|
|
marker.bindPopup(() => buildIbadahPopup(obj));
|
|
ibadahList.push(obj);
|
|
|
|
closeFormOverlay();
|
|
updateAllMiskinColors();
|
|
renderIbadahList();
|
|
updateStats();
|
|
|
|
setTimeout(() => {
|
|
pulseMarker(marker);
|
|
const c = document.getElementById('card-ibadah-' + id);
|
|
if (c) { c.classList.add('highlighted'); c.scrollIntoView({ behavior:'smooth', block:'nearest' }); }
|
|
}, 100);
|
|
|
|
showToast(`✅ ${type} ${name} ditambahkan`, '#5b7fff');
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// EDIT — TERAPKAN EDIT IBADAH
|
|
// ═══════════════════════════════════════════════════════════════
|
|
function applyEditIbadah() {
|
|
if (!editMode) return;
|
|
const ib = ibadahList.find(x => x.id === editMode.id);
|
|
if (!ib) return;
|
|
|
|
ib.name = document.getElementById('f-ibadah-name').value.trim() || ib.name;
|
|
ib.type = document.getElementById('f-ibadah-type').value;
|
|
ib.radius = parseInt(document.getElementById('f-radius-slider').value);
|
|
// koordinat & addr tidak berubah saat edit (hanya metadata)
|
|
|
|
// Update marker icon & circle
|
|
ib.marker.setIcon(createIbadahIcon(ib.type));
|
|
ib.circle.setRadius(ib.radius);
|
|
ib.marker.setPopupContent(buildIbadahPopup(ib));
|
|
|
|
closeFormOverlay();
|
|
updateAllMiskinColors();
|
|
renderIbadahList();
|
|
updateStats();
|
|
showToast(`✏️ ${ib.type} ${ib.name} diperbarui`, '#5b7fff');
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// SAVE — TAMBAH MISKIN BARU
|
|
// ═══════════════════════════════════════════════════════════════
|
|
function saveMiskin() {
|
|
if (!pendingLatLng) return;
|
|
const { lat, lng } = pendingLatLng;
|
|
const name = document.getElementById('f-miskin-name').value.trim() || 'SPBU';
|
|
const spbuType = document.getElementById('f-spbu-type') ? document.getElementById('f-spbu-type').value : '⛽';
|
|
const radius = parseInt(document.getElementById('f-radius-slider-spbu').value) || 1000;
|
|
const addr = pendingAddr || `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
|
|
const nearest = getNearestIbadah(lat, lng);
|
|
|
|
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
|
|
|
|
const id = Date.now();
|
|
const inRadius = nearest !== null;
|
|
const marker = L.marker([lat, lng], { icon: createMiskinIcon(true) }).addTo(map);
|
|
const circle = L.circle([lat, lng], {
|
|
radius, color: '#f59e0b', fillColor: '#f59e0b',
|
|
fillOpacity: 0.06, weight: 1.5, dashArray: '4,4', opacity: 0.7
|
|
}).addTo(map);
|
|
const obj = { id, name, type: spbuType, lat, lng, inRadius, nearestIbadah: nearest ? nearest.id : null, addr, marker, circle, radius };
|
|
marker.bindPopup(() => buildMiskinPopup(obj));
|
|
miskinList.push(obj);
|
|
|
|
closeFormOverlay();
|
|
updateStats();
|
|
renderMiskinList();
|
|
|
|
setTimeout(() => {
|
|
pulseMarker(marker);
|
|
const c = document.getElementById('card-miskin-' + id);
|
|
if (c) { c.classList.add('highlighted'); c.scrollIntoView({ behavior:'smooth', block:'nearest' }); }
|
|
}, 100);
|
|
|
|
showToast(`⛽ ${spbuType} ${name} ditambahkan`, '#f59e0b');
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// EDIT — TERAPKAN EDIT MISKIN
|
|
// ═══════════════════════════════════════════════════════════════
|
|
function applyEditMiskin() {
|
|
if (!editMode) return;
|
|
const m = miskinList.find(x => x.id === editMode.id);
|
|
if (!m) return;
|
|
|
|
m.name = document.getElementById('f-miskin-name').value.trim() || m.name;
|
|
m.marker.setPopupContent(buildMiskinPopup(m));
|
|
|
|
closeFormOverlay();
|
|
updateStats();
|
|
renderMiskinList();
|
|
showToast(`✏️ Data ${m.name} diperbarui`, '#22c55e');
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// POPUP BUILDERS
|
|
// ═══════════════════════════════════════════════════════════════
|
|
function buildIbadahPopup(ib) {
|
|
return `<b>${ib.type} ${ib.name}</b>
|
|
<div class="popup-addr">📍 ${ib.addr}</div>
|
|
<div style="margin-top:6px;font-size:11px;color:#8890b0;">
|
|
Radius: <b style="color:#5b7fff">${fmtRadius(ib.radius)}</b>
|
|
· ${ib.lat.toFixed(5)}, ${ib.lng.toFixed(5)}
|
|
</div>
|
|
<div style="margin-top:8px;">
|
|
<span onclick="openEditIbadah(${ib.id})" style="cursor:pointer;font-size:11px;color:#5b7fff;
|
|
background:rgba(91,127,255,0.15);padding:3px 9px;border-radius:5px;font-weight:600;">
|
|
✏️ Edit
|
|
</span>
|
|
</div>`;
|
|
}
|
|
|
|
function buildMiskinPopup(m) {
|
|
const nearest = ibadahList.find(x => x.id === m.nearestIbadah);
|
|
const nearestLabel = nearest
|
|
? `<div style="margin-top:4px;font-size:11px;color:#5b7fff;">
|
|
📌 Terdekat: <b>${nearest.type} ${nearest.name}</b>
|
|
(${Math.round(map.distance([m.lat,m.lng],[nearest.lat,nearest.lng]))}m)
|
|
</div>`
|
|
: '';
|
|
return `<b>⛽ ${m.type || ''} ${m.name}</b>
|
|
<div style="margin-top:4px;">
|
|
<span style="background:rgba(245,158,11,0.2);color:#f59e0b;
|
|
padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;">
|
|
⛽ SPBU / Pangkalan BBM
|
|
</span>
|
|
</div>
|
|
${nearestLabel}
|
|
<div class="popup-addr">📍 ${m.addr}</div>
|
|
<div style="margin-top:5px;font-size:11px;color:#8890b0;">${m.lat.toFixed(5)}, ${m.lng.toFixed(5)}</div>
|
|
<div style="margin-top:8px;">
|
|
<span onclick="openEditMiskin(${m.id})" style="cursor:pointer;font-size:11px;color:#f59e0b;
|
|
background:rgba(245,158,11,0.15);padding:3px 9px;border-radius:5px;font-weight:600;">
|
|
✏️ Edit
|
|
</span>
|
|
</div>`;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// OPEN EDIT (dipanggil dari popup peta ATAU tombol edit di sidebar)
|
|
// ═══════════════════════════════════════════════════════════════
|
|
window.openEditIbadah = (id) => {
|
|
const ib = ibadahList.find(x => x.id === id); if (!ib) return;
|
|
map.closePopup();
|
|
pendingLatLng = { lat: ib.lat, lng: ib.lng };
|
|
openFormOverlay('ibadah', ib.lat, ib.lng, ib);
|
|
};
|
|
|
|
window.openEditMiskin = (id) => {
|
|
const m = miskinList.find(x => x.id === id); if (!m) return;
|
|
map.closePopup();
|
|
pendingLatLng = { lat: m.lat, lng: m.lng };
|
|
openFormOverlay('miskin', m.lat, m.lng, m);
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// RENDER SIDEBAR
|
|
// ═══════════════════════════════════════════════════════════════
|
|
function renderIbadahList() {
|
|
const el = document.getElementById('ibadah-list');
|
|
document.getElementById('count-ibadah').textContent = ibadahList.length;
|
|
if (!ibadahList.length) { el.innerHTML = '<div class="empty-state">Belum ada rumah ibadah</div>'; return; }
|
|
|
|
el.innerHTML = ibadahList.map(ib => `
|
|
<div class="item-card" id="card-ibadah-${ib.id}" onclick="focusIbadah(${ib.id})">
|
|
<div class="item-icon">${ib.type}</div>
|
|
<div class="item-info">
|
|
<div class="item-name">${ib.name}</div>
|
|
<div class="item-addr">${truncate(ib.addr, 52)}</div>
|
|
<div class="item-coord">${ib.lat.toFixed(4)}, ${ib.lng.toFixed(4)}</div>
|
|
<!-- Live radius slider -->
|
|
<div class="item-radius-row" onclick="event.stopPropagation()">
|
|
<input type="range" min="100" max="5000" step="100" value="${ib.radius}"
|
|
oninput="liveRadius(${ib.id}, this.value)"
|
|
title="Geser untuk ubah radius">
|
|
<span class="radius-display" id="rdisplay-${ib.id}">${fmtRadius(ib.radius)}</span>
|
|
</div>
|
|
</div>
|
|
<div class="item-actions">
|
|
<button class="btn-edit" onclick="event.stopPropagation();openEditIbadah(${ib.id})" title="Edit">✏️</button>
|
|
<button class="btn-remove" onclick="event.stopPropagation();removeIbadah(${ib.id})" title="Hapus">✕</button>
|
|
</div>
|
|
</div>`
|
|
).join('');
|
|
}
|
|
|
|
function renderMiskinList() {
|
|
const el = document.getElementById('miskin-list');
|
|
document.getElementById('count-miskin').textContent = miskinList.length;
|
|
if (!miskinList.length) { el.innerHTML = '<div class="empty-state">Belum ada data SPBU</div>'; return; }
|
|
|
|
el.innerHTML = miskinList.map(m => {
|
|
const nearest = ibadahList.find(x => x.id === m.nearestIbadah);
|
|
const nearestHtml = nearest
|
|
? `<div class="nearest-tag">📌 ${nearest.type} ${truncate(nearest.name, 20)}</div>`
|
|
: '';
|
|
return `
|
|
<div class="item-card" id="card-miskin-${m.id}" onclick="focusMiskin(${m.id})">
|
|
<div class="item-icon">${m.type || '⛽'}</div>
|
|
<div class="item-info">
|
|
<div class="item-name">${m.name}</div>
|
|
<div class="item-addr">${truncate(m.addr, 52)}</div>
|
|
<div class="item-coord">${m.lat.toFixed(4)}, ${m.lng.toFixed(4)}</div>
|
|
${nearestHtml}
|
|
</div>
|
|
<div class="item-actions">
|
|
<span class="badge badge-yellow">⛽</span>
|
|
<button class="btn-edit" onclick="event.stopPropagation();openEditMiskin(${m.id})" title="Edit">✏️</button>
|
|
<button class="btn-remove" onclick="event.stopPropagation();removeMiskin(${m.id})" title="Hapus">✕</button>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
// ─── Live radius update dari slider sidebar ───────────────────
|
|
window.liveRadius = (id, val) => {
|
|
const ib = ibadahList.find(x => x.id === id); if (!ib) return;
|
|
ib.radius = parseInt(val);
|
|
if (ib.circle) ib.circle.setRadius(ib.radius);
|
|
const disp = document.getElementById('rdisplay-' + id);
|
|
if (disp) disp.textContent = fmtRadius(ib.radius);
|
|
ib.marker.setPopupContent(buildIbadahPopup(ib));
|
|
updateAllMiskinColors(); // recalculate nearest
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// FOCUS (klik card → pan + popup)
|
|
// ═══════════════════════════════════════════════════════════════
|
|
window.focusIbadah = (id) => {
|
|
const ib = ibadahList.find(x => x.id === id); if (!ib) return;
|
|
map.setView([ib.lat, ib.lng], Math.max(map.getZoom(), 15));
|
|
ib.marker.openPopup(); pulseMarker(ib.marker);
|
|
};
|
|
window.focusMiskin = (id) => {
|
|
const m = miskinList.find(x => x.id === id); if (!m) return;
|
|
map.setView([m.lat, m.lng], Math.max(map.getZoom(), 16));
|
|
m.marker.openPopup(); pulseMarker(m.marker);
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// STATS
|
|
// ═══════════════════════════════════════════════════════════════
|
|
function updateStats() {
|
|
document.getElementById('stat-ibadah').textContent = ibadahList.length;
|
|
document.getElementById('stat-in').textContent = miskinList.length;
|
|
document.getElementById('stat-out').textContent = 0; // not used for SPBU
|
|
}
|
|
function truncate(s, n) { return s.length > n ? s.slice(0,n)+'…' : s; }
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// REMOVE
|
|
// ═══════════════════════════════════════════════════════════════
|
|
window.removeIbadah = (id) => {
|
|
const i = ibadahList.findIndex(x => x.id === id); if (i < 0) return;
|
|
map.removeLayer(ibadahList[i].marker);
|
|
map.removeLayer(ibadahList[i].circle);
|
|
ibadahList.splice(i, 1);
|
|
updateAllMiskinColors(); renderIbadahList(); updateStats();
|
|
};
|
|
window.removeMiskin = (id) => {
|
|
const i = miskinList.findIndex(x => x.id === id); if (i < 0) return;
|
|
map.removeLayer(miskinList[i].marker);
|
|
if (miskinList[i].circle) map.removeLayer(miskinList[i].circle);
|
|
miskinList.splice(i, 1);
|
|
updateStats(); renderMiskinList();
|
|
};
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// RESET
|
|
// ═══════════════════════════════════════════════════════════════
|
|
document.getElementById('btn-reset').addEventListener('click', () => {
|
|
ibadahList.forEach(ib => { map.removeLayer(ib.marker); map.removeLayer(ib.circle); });
|
|
miskinList.forEach(m => { map.removeLayer(m.marker); if (m.circle) map.removeLayer(m.circle); });
|
|
importedGeoJSON.forEach(g => { map.removeLayer(g.layer); });
|
|
if (myMarker) { map.removeLayer(myMarker); myMarker = null; }
|
|
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
|
|
ibadahList = []; miskinList = []; importedGeoJSON = [];
|
|
mode = null; editMode = null; updateToolbar(); resetMapCursor();
|
|
document.getElementById('form-overlay').classList.add('hidden');
|
|
document.getElementById('my-location-info').style.display = 'none';
|
|
document.getElementById('loc-bar').style.display = 'none';
|
|
renderIbadahList(); renderMiskinList(); if(typeof renderGeoJSONList === 'function') renderGeoJSONList(); updateStats();
|
|
showToast('🗑️ Semua data dihapus', '#ef4444');
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// GEOLOCATION
|
|
// ═══════════════════════════════════════════════════════════════
|
|
function doLocate() {
|
|
if (!navigator.geolocation) { showToast('❌ Browser tidak mendukung Geolocation', '#ef4444'); return; }
|
|
showToast('📡 Mendeteksi lokasi...', '#f59e0b');
|
|
navigator.geolocation.getCurrentPosition(async (pos) => {
|
|
const lat = pos.coords.latitude, lng = pos.coords.longitude;
|
|
if (myMarker) map.removeLayer(myMarker);
|
|
myMarker = L.marker([lat, lng], { icon: createMyIcon() }).addTo(map);
|
|
const addr = await reverseGeocode(lat, lng);
|
|
myMarker.bindPopup(`<b>💻 Lokasi Saya</b>
|
|
<div class="popup-addr">📍 ${addr}</div>
|
|
<div style="margin-top:5px;font-size:11px;color:#8890b0;">
|
|
±${Math.round(pos.coords.accuracy)}m · ${lat.toFixed(6)}, ${lng.toFixed(6)}
|
|
</div>`).openPopup();
|
|
map.setView([lat, lng], 15);
|
|
document.getElementById('my-location-info').style.display = 'block';
|
|
document.getElementById('my-addr').textContent = truncate(addr, 80);
|
|
document.getElementById('my-coord').textContent = `${lat.toFixed(6)}, ${lng.toFixed(6)} · ±${Math.round(pos.coords.accuracy)}m`;
|
|
const lb = document.getElementById('loc-bar');
|
|
lb.style.display = 'flex';
|
|
document.getElementById('loc-bar-addr').textContent = truncate(addr, 60);
|
|
document.getElementById('loc-bar-coord').textContent = `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
|
|
showToast('✅ Lokasi berhasil ditemukan!', '#f59e0b');
|
|
}, (err) => {
|
|
const msgs = { 1:'Izin lokasi ditolak', 2:'Posisi tidak tersedia', 3:'Timeout' };
|
|
showToast('❌ ' + (msgs[err.code] || 'Gagal mendapatkan lokasi'), '#ef4444');
|
|
}, { enableHighAccuracy: false, timeout: 10000 });
|
|
}
|
|
document.getElementById('btn-locate').addEventListener('click', doLocate);
|
|
document.getElementById('btn-locate-map').addEventListener('click', doLocate);
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// KEYBOARD
|
|
// ═══════════════════════════════════════════════════════════════
|
|
document.addEventListener('keydown', e => {
|
|
if (e.key === 'Escape') {
|
|
if (!document.getElementById('form-overlay').classList.contains('hidden')) {
|
|
closeFormOverlay();
|
|
} else {
|
|
mode = null; updateToolbar(); resetMapCursor(); clearHighlights();
|
|
}
|
|
}
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// GEOJSON IMPORT
|
|
// ═══════════════════════════════════════════════════════════════
|
|
const fImportGeoJSON = document.getElementById('f-import-geojson');
|
|
if (fImportGeoJSON) {
|
|
fImportGeoJSON.addEventListener('change', function(e) {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = function(evt) {
|
|
try {
|
|
const geojsonData = JSON.parse(evt.target.result);
|
|
|
|
// Buat layer GeoJSON
|
|
const layer = L.geoJSON(geojsonData, {
|
|
style: function(feature) {
|
|
return {
|
|
color: '#f59e0b',
|
|
weight: 3,
|
|
opacity: 0.8,
|
|
fillColor: '#f59e0b',
|
|
fillOpacity: 0.2
|
|
};
|
|
},
|
|
onEachFeature: function(feature, layer) {
|
|
let popupContent = '<b>GeoJSON Feature</b><br>';
|
|
if (feature.properties) {
|
|
for (const key in feature.properties) {
|
|
popupContent += `<i>${key}</i>: ${feature.properties[key]}<br>`;
|
|
}
|
|
}
|
|
layer.bindPopup(popupContent);
|
|
}
|
|
}).addTo(map);
|
|
|
|
// Zoom ke layer yang baru diimpor
|
|
map.fitBounds(layer.getBounds());
|
|
|
|
const id = Date.now();
|
|
importedGeoJSON.push({
|
|
id: id,
|
|
name: file.name,
|
|
layer: layer
|
|
});
|
|
|
|
renderGeoJSONList();
|
|
showToast(`📂 File ${file.name} berhasil diimpor`, '#22c55e');
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
showToast('❌ Gagal memparsing file GeoJSON', '#ef4444');
|
|
}
|
|
|
|
// Reset input agar file yang sama bisa dipilih lagi
|
|
fImportGeoJSON.value = '';
|
|
};
|
|
reader.readAsText(file);
|
|
});
|
|
}
|
|
|
|
window.renderGeoJSONList = function() {
|
|
const el = document.getElementById('geojson-list');
|
|
if (!el) return;
|
|
|
|
if (!importedGeoJSON.length) {
|
|
el.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
el.innerHTML = importedGeoJSON.map(g => `
|
|
<div class="item-card" style="margin-bottom: 5px;">
|
|
<div class="item-icon" style="background: rgba(245, 158, 11, 0.2); color: #f59e0b; border: 1px solid rgba(245, 158, 11, 0.3);">🗺️</div>
|
|
<div class="item-info">
|
|
<div class="item-name">${truncate(g.name, 25)}</div>
|
|
<div class="item-addr">GeoJSON Layer</div>
|
|
</div>
|
|
<div class="item-actions">
|
|
<button class="btn-remove" onclick="removeGeoJSON(${g.id})" title="Hapus Layer">✕</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
};
|
|
|
|
window.removeGeoJSON = function(id) {
|
|
const idx = importedGeoJSON.findIndex(g => g.id === id);
|
|
if (idx < 0) return;
|
|
map.removeLayer(importedGeoJSON[idx].layer);
|
|
importedGeoJSON.splice(idx, 1);
|
|
renderGeoJSONList();
|
|
showToast('🗑️ Layer dihapus', '#f59e0b');
|
|
}; |