Files
WebGIS-Poverty-MAP/app.js
T
2026-06-12 11:56:45 +07:00

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>
&nbsp;·&nbsp; ${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');
};