/* ========================================================= admin-map.js — Full WebGIS Map (admin mode: edit & draw) ========================================================= */ const API = APP_BASE + '/api'; const STAT_API = APP_BASE + '/api/statistik.php'; const state = { map: null, layers: { spbu: null, jalan: null, kavling: null, kawasan: null, rumah: null, warga: null, choropleth: null, blankSpot: null, spbuRoute: null }, activeDraw: null, // 'spbu' | 'jalan' | 'kavling' | 'kawasan' | 'rumah' | 'warga' drawHandlers: {}, pendingGeometry: null }; const COLORS = { spbu: '#F59E0B', spbu24: '#10B981', jalan: '#3B82F6', kavling: '#8B5CF6', kawasan: '#EF4444', rumah: '#A855F7', warga: '#EF4444', blankSpot: '#F97316' }; /* ── Icon factory ── */ function makeIcon(emoji, bgColor, size = 34) { return L.divIcon({ className: 'div-icon-wrap', html: `
${emoji}
`, iconSize: [size, size], iconAnchor: [size / 2, size], popupAnchor: [0, -size] }); } const ICONS = { spbu24: makeIcon('⛽', COLORS.spbu24), spbu: makeIcon('⛽', COLORS.spbu), rumah: makeIcon('🕌', COLORS.rumah), warga: makeIcon('👤', COLORS.warga), blankSpot: makeIcon('⚠️', COLORS.blankSpot, 36) }; /* ── Toast ── */ function toast(msg, type = 'success') { const c = document.getElementById('toastContainer'); const t = document.createElement('div'); t.className = `toast ${type}`; t.innerHTML = `${msg}`; c.appendChild(t); setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 400); }, 3000); } /* ── Initialize map ── */ function initMap() { state.map = L.map('admin-map', { zoomControl: false }) .setView([-0.0583, 109.3448], 13); L.control.zoom({ position: 'bottomright' }).addTo(state.map); L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap, © CARTO', subdomains: 'abcd', maxZoom: 20 }).addTo(state.map); } /* ── Fetch GeoJSON ── */ async function fetchLayer(path) { const r = await fetch(`${API}/${path}`); const j = await r.json(); return j.status === 'success' ? j.data : { type: 'FeatureCollection', features: [] }; } function formatKm(value) { return Number(value || 0).toLocaleString('id-ID', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); } /* ── Render: SPBU (Point) ── */ function renderSpbu(data) { if (state.layers.spbu) state.map.removeLayer(state.layers.spbu); state.layers.spbu = L.geoJSON(data, { pointToLayer: (f, ll) => L.marker(ll, { icon: f.properties.buka_24_jam ? ICONS.spbu24 : ICONS.spbu }), onEachFeature: (f, l) => bindPopup(l, 'spbu', f.properties) }); if (document.getElementById('ly-spbu')?.checked) state.layers.spbu.addTo(state.map); } /* ── Render: Jalan (LineString) ── */ function renderJalan(data) { if (state.layers.jalan) state.map.removeLayer(state.layers.jalan); state.layers.jalan = L.geoJSON(data, { style: { color: COLORS.jalan, weight: 5, opacity: 0.85 }, onEachFeature: (f, l) => bindPopup(l, 'jalan', f.properties) }); if (document.getElementById('ly-jalan')?.checked) state.layers.jalan.addTo(state.map); } /* ── Render: Kavling (Polygon) ── */ function renderKavling(data) { if (state.layers.kavling) state.map.removeLayer(state.layers.kavling); state.layers.kavling = L.geoJSON(data, { style: { color: COLORS.kavling, fillColor: COLORS.kavling, fillOpacity: 0.3, weight: 2 }, onEachFeature: (f, l) => bindPopup(l, 'kavling', f.properties) }); if (document.getElementById('ly-kavling')?.checked) state.layers.kavling.addTo(state.map); } /* ── Render: Kawasan Kumuh (Polygon) ── */ function renderKawasan(data) { if (state.layers.kawasan) state.map.removeLayer(state.layers.kawasan); state.layers.kawasan = L.geoJSON(data, { style: { color: COLORS.kawasan, fillColor: COLORS.kawasan, fillOpacity: 0.25, weight: 2, dashArray: '6 4' }, onEachFeature: (f, l) => bindPopup(l, 'kawasan', f.properties) }); if (document.getElementById('ly-kawasan')?.checked) state.layers.kawasan.addTo(state.map); } /* ── Render: Rumah Ibadah (Point) ── */ function renderRumah(data) { if (state.layers.rumah) state.map.removeLayer(state.layers.rumah); state.layers.rumah = L.featureGroup(); (data.features || []).forEach((f) => { if (!f.geometry || f.geometry.type !== 'Point') return; const props = f.properties || {}; const ll = L.latLng(f.geometry.coordinates[1], f.geometry.coordinates[0]); const circle = L.circle(ll, { radius: Number(props.radius_bantuan_meter || 1000), color: COLORS.rumah, fillColor: COLORS.rumah, fillOpacity: 0.08, opacity: 0.55, weight: 2 }); const marker = L.marker(ll, { icon: ICONS.rumah }); bindPopup(circle, 'rumah', props); bindPopup(marker, 'rumah', props); state.layers.rumah.addLayer(circle); state.layers.rumah.addLayer(marker); }); if (document.getElementById('ly-rumah')?.checked) state.layers.rumah.addTo(state.map); } /* ── Render: Warga Miskin (Point) ── */ function renderWarga(data) { if (state.layers.warga) state.map.removeLayer(state.layers.warga); state.layers.warga = L.geoJSON(data, { pointToLayer: (f, ll) => L.marker(ll, { icon: ICONS.warga }), onEachFeature: (f, l) => bindPopup(l, 'warga', f.properties) }); if (document.getElementById('ly-warga')?.checked) state.layers.warga.addTo(state.map); } /* ── Render: Choropleth (kawasan + count warga) ── */ function renderChoropleth(data) { if (state.layers.choropleth) state.map.removeLayer(state.layers.choropleth); state.layers.choropleth = L.geoJSON(data, { style: (f) => { const c = f.properties.jumlah_warga; let color = '#10B981'; if (c > 0) color = '#F59E0B'; if (c > 3) color = '#EF4444'; return { color, fillColor: color, fillOpacity: 0.35, weight: 3 }; }, onEachFeature: (f, l) => { const c = f.properties.jumlah_warga; l.bindPopup(` `); } }); if (document.getElementById('ly-choropleth')?.checked) state.layers.choropleth.addTo(state.map); } /* ── Render: Blank Spot (warga yang jauh dari rumah ibadah) ── */ function renderBlankSpot(data) { if (state.layers.blankSpot) state.map.removeLayer(state.layers.blankSpot); state.layers.blankSpot = L.geoJSON(data, { pointToLayer: (f, ll) => L.marker(ll, { icon: ICONS.blankSpot }), onEachFeature: (f, l) => { l.bindPopup(` `); } }); if (document.getElementById('ly-blankspot')?.checked) state.layers.blankSpot.addTo(state.map); } /* ── Popup binding helper ── */ function bindPopup(layer, type, props) { let html = ``; if (type === 'spbu') { html += ``; if (props.deskripsi) html += ``; } else if (type === 'jalan') { html += ``; } else if (type === 'kavling') { html += ``; html += ``; } else if (type === 'rumah') { html += ``; html += ``; } else if (type === 'warga') { html += ``; html += ``; } html += ``; layer.bindPopup(html); } /* ── Delete from map ── */ window.deleteFeature = async function (type, id) { if (!confirm('Yakin hapus data ini?')) return; const ep = { spbu: 'spbu.php', jalan: 'jalan.php', kavling: 'kavling.php', kawasan: 'kawasan_kumuh.php', rumah: 'rumah_ibadah.php', warga: 'warga_miskin.php' }[type]; if (!ep) return; const r = await fetch(`${API}/${ep}?id=${id}`, { method: 'DELETE' }); const j = await r.json(); if (j.status === 'success') { toast('Data dihapus!'); loadAll(); } else toast(j.message, 'error'); }; /* ── Layer toggling ── */ function bindLayerToggles() { const map = { 'ly-spbu': 'spbu', 'ly-jalan': 'jalan', 'ly-kavling': 'kavling', 'ly-kawasan': 'kawasan', 'ly-rumah': 'rumah', 'ly-warga': 'warga', 'ly-choropleth': 'choropleth', 'ly-blankspot': 'blankSpot' }; Object.entries(map).forEach(([id, key]) => { document.getElementById(id)?.addEventListener('change', (e) => { const layer = state.layers[key]; if (!layer) return; if (e.target.checked) state.map.addLayer(layer); else state.map.removeLayer(layer); }); }); } /* ── Drawing tool activation ── */ function activateDraw(type) { if (state.activeDraw) deactivateDraw(); state.activeDraw = type; // Update UI button state document.querySelectorAll('[data-draw]').forEach(b => b.classList.remove('active')); document.querySelector(`[data-draw="${type}"]`)?.classList.add('active'); // Create appropriate L.Draw handler let handler = null; if (type === 'spbu' || type === 'rumah' || type === 'warga') { handler = new L.Draw.Marker(state.map); } else if (type === 'jalan') { handler = new L.Draw.Polyline(state.map, { shapeOptions: { color: COLORS.jalan, weight: 5 } }); } else if (type === 'kavling' || type === 'kawasan') { handler = new L.Draw.Polygon(state.map, { shapeOptions: { color: type === 'kavling' ? COLORS.kavling : COLORS.kawasan } }); } state.drawHandlers[type] = handler; handler.enable(); } /* ── Deactivate drawing ── */ function deactivateDraw() { if (state.activeDraw && state.drawHandlers[state.activeDraw]) { state.drawHandlers[state.activeDraw].disable(); } state.activeDraw = null; document.querySelectorAll('[data-draw]').forEach(b => b.classList.remove('active')); } /* ── Draw event listener ── */ function bindDrawEvent() { state.map.on(L.Draw.Event.CREATED, (e) => { const geom = e.layer.toGeoJSON().geometry; state.pendingGeometry = geom; openFormModal(state.activeDraw, geom); }); state.map.on(L.Draw.Event.DRAWSTOP, () => { // User cancelled drawing document.querySelectorAll('[data-draw]').forEach(b => b.classList.remove('active')); state.activeDraw = null; }); } /* ── Form modal untuk input data baru ── */ function openFormModal(type, geometry) { const modal = document.getElementById('formModal'); const title = document.getElementById('formModalTitle'); const body = document.getElementById('formModalBody'); const titles = { spbu: '⛽ Tambah SPBU', jalan: '🛣️ Tambah Jalan', kavling: '🏘️ Tambah Kavling', kawasan: '⚠️ Tambah Kawasan Kumuh', rumah: '🕌 Tambah Rumah Ibadah', warga: '👤 Tambah Warga Miskin' }; title.textContent = titles[type] || 'Tambah Data'; let formHtml = ''; if (type === 'spbu') { formHtml = `
`; } else if (type === 'jalan') { formHtml = `
`; } else if (type === 'kavling') { formHtml = `
`; } else if (type === 'kawasan') { formHtml = `
`; } else if (type === 'rumah') { formHtml = `
`; } else if (type === 'warga') { formHtml = `
`; } body.innerHTML = formHtml + ` `; modal.classList.add('active'); } window.closeFormModal = function () { document.getElementById('formModal').classList.remove('active'); state.pendingGeometry = null; deactivateDraw(); }; /* ── Save feature to backend ── */ window.saveFeature = async function (type) { const ep = { spbu: 'spbu.php', jalan: 'jalan.php', kavling: 'kavling.php', kawasan: 'kawasan_kumuh.php', rumah: 'rumah_ibadah.php', warga: 'warga_miskin.php' }[type]; if (!ep) return; let payload = { geometry: state.pendingGeometry }; const v = (id) => document.getElementById(id)?.value; if (type === 'spbu') { if (!v('f-nama')) return toast('Nama wajib!', 'error'); payload = { ...payload, nama: v('f-nama'), deskripsi: v('f-deskripsi') || '', buka_24_jam: document.getElementById('f-24').checked ? 1 : 0 }; } else if (type === 'jalan') { if (!v('f-nama')) return toast('Nama wajib!', 'error'); payload = { ...payload, nama: v('f-nama'), jenis_jalan: v('f-jenis') }; } else if (type === 'kavling') { if (!v('f-nama')) return toast('Nama wajib!', 'error'); payload = { ...payload, nama_pemilik: v('f-nama'), status_kepemilikan: v('f-status'), luas: parseFloat(v('f-luas')) || 0 }; } else if (type === 'kawasan') { if (!v('f-nama')) return toast('Nama wajib!', 'error'); payload = { ...payload, nama_kawasan: v('f-nama') }; } else if (type === 'rumah') { if (!v('f-nama')) return toast('Nama wajib!', 'error'); payload = { ...payload, nama: v('f-nama'), agama: v('f-agama'), radius_bantuan_meter: Math.max(100, Math.min(10000, parseInt(v('f-radius'), 10) || 1000)) }; } else if (type === 'warga') { if (!v('f-nama')) return toast('Nama wajib!', 'error'); payload = { ...payload, nama_kk: v('f-nama'), penghasilan: parseFloat(v('f-penghasilan')) || 0, jumlah_tanggungan: parseInt(v('f-tanggungan')) || 0 }; } try { const r = await fetch(`${API}/${ep}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const j = await r.json(); if (j.status === 'success') { toast('Data berhasil disimpan!'); closeFormModal(); loadAll(); } else toast(j.message || 'Gagal menyimpan', 'error'); } catch (e) { toast('Error: ' + e.message, 'error'); } }; /* ── Analysis: SPBU Terdekat ── */ async function findNearestSpbu() { const center = state.map.getCenter(); document.getElementById('spbuPanelResult').innerHTML = '
Mencari...'; document.getElementById('spbuPanel').style.display = 'block'; try { const r = await fetch(`${API}/spbu_terdekat.php?lat=${center.lat}&lng=${center.lng}`); const j = await r.json(); if (j.status === 'success' && j.data) { const p = j.data.properties; const coords = j.data.geometry.coordinates; const html = `
⛽ ${p.nama}
${p.buka_24_jam ? '🟢 24 Jam' : '🟡 Terbatas'}
${p.jarak_km} km
`; document.getElementById('spbuPanelResult').innerHTML = html; // Show marker if (state.layers.spbuRoute) state.map.removeLayer(state.layers.spbuRoute); state.layers.spbuRoute = L.marker([coords[1], coords[0]], { icon: ICONS.spbu }).addTo(state.map) .bindPopup(`${p.nama}
${p.jarak_km} km dari pusat peta`).openPopup(); } else { document.getElementById('spbuPanelResult').innerHTML = `
${j.message || 'Tidak ada SPBU'}
`; } } catch (e) { document.getElementById('spbuPanelResult').innerHTML = `
Error: ${e.message}
`; } } /* ── Load all layers ── */ async function loadAll() { document.getElementById('loadingOverlay').style.display = 'flex'; try { const [spbu, jalan, kavling, kawasan, rumah, warga, stat, blank] = await Promise.all([ fetchLayer('spbu.php'), fetchLayer('jalan.php'), fetchLayer('kavling.php'), fetchLayer('kawasan_kumuh.php'), fetchLayer('rumah_ibadah.php'), fetchLayer('warga_miskin.php'), fetch(STAT_API).then(r => r.json()).then(j => j.data).catch(() => null), fetchLayer('blank_spot.php') ]); renderSpbu(spbu); renderJalan(jalan); renderKavling(kavling); renderKawasan(kawasan); renderRumah(rumah); renderWarga(warga); if (stat && stat.choropleth) renderChoropleth(stat.choropleth); if (blank) renderBlankSpot(blank); // Update quick stats if (stat && stat.counts) { document.getElementById('qs-spbu').textContent = stat.counts.total_spbu; document.getElementById('qs-jalan').textContent = stat.counts.total_jalan; document.getElementById('qs-kavling').textContent = stat.counts.total_kavling; document.getElementById('qs-warga').textContent = stat.counts.total_warga_miskin; } } catch (e) { toast('Gagal load data: ' + e.message, 'error'); } finally { document.getElementById('loadingOverlay').style.display = 'none'; } } /* ── Init ── */ document.addEventListener('DOMContentLoaded', () => { initMap(); loadAll(); bindLayerToggles(); bindDrawEvent(); document.querySelectorAll('[data-draw]').forEach(btn => { btn.addEventListener('click', () => { const type = btn.dataset.draw; if (state.activeDraw === type) deactivateDraw(); else activateDraw(type); }); }); document.getElementById('btn-find-spbu')?.addEventListener('click', findNearestSpbu); document.getElementById('btn-close-spbu')?.addEventListener('click', () => { document.getElementById('spbuPanel').style.display = 'none'; if (state.layers.spbuRoute) { state.map.removeLayer(state.layers.spbuRoute); state.layers.spbuRoute = null; } }); });