/* ========================================================= user-map.js — Read-only WebGIS Map (user mode) ========================================================= */ 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 } }; const COLORS = { spbu: '#F59E0B', spbu24: '#10B981', jalan: '#3B82F6', kavling: '#8B5CF6', kawasan: '#EF4444', rumah: '#A855F7', warga: '#EF4444', blankSpot: '#F97316' }; 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), // default (Islam) warga: makeIcon('👤', COLORS.warga), blankSpot: makeIcon('⚠️', COLORS.blankSpot, 36) }; /* ── Per-religion icon colors & emojis ── */ const AGAMA_EMOJI = { 'Islam': '🕌', 'Kristen': '⛪', 'Katolik': '⛪', 'Hindu': '🛕', 'Buddha': '☯️', 'Konghucu': '🏯' }; const AGAMA_COLOR = { 'Islam': '#6D28D9', 'Kristen': '#2563EB', 'Katolik': '#1D4ED8', 'Hindu': '#D97706', 'Buddha': '#F59E0B', 'Konghucu': '#DC2626' }; function getRumahIcon(agama) { const emoji = AGAMA_EMOJI[agama] || '🛐'; const color = AGAMA_COLOR[agama] || COLORS.rumah; return makeIcon(emoji, color); } 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); } function initMap() { state.map = L.map('user-map', { zoomControl: false }) .setView([-0.0583, 109.3448], 13); L.control.zoom({ position: 'bottomright' }).addTo(state.map); const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; const tileUrl = isDark ? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png' : 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'; const tileLayer = L.tileLayer(tileUrl, { attribution: '© OpenStreetMap, © CARTO', subdomains: 'abcd', maxZoom: 20 }).addTo(state.map); // Dynamic theme updater for tile layer const observer = new MutationObserver(() => { const dark = document.documentElement.getAttribute('data-theme') === 'dark'; tileLayer.setUrl(dark ? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png' : 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png' ); }); observer.observe(document.documentElement, { attributes: true }); } 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 }); } function bindReadOnlyPopup(layer, type, props) { let html = ``; if (type === 'spbu') { html += ``; if (props.deskripsi) html += ``; html += ``; html += ``; } else if (type === 'jalan') { html += ``; } else if (type === 'kavling') { html += ``; html += ``; } else if (type === 'rumah') { html += ``; html += ``; html += ``; html += ``; } else if (type === 'warga') { html += ``; html += ``; } layer.bindPopup(html); } // Ulasan Modal Logic window.bukaReview = function(tipe, id, nama) { let m = document.getElementById('modalReview'); if(!m) { m = document.createElement('div'); m.id = 'modalReview'; m.className = 'modal-overlay'; m.innerHTML = ` `; document.body.appendChild(m); } document.getElementById('revTipe').value = tipe; document.getElementById('revId').value = id; document.getElementById('revNama').innerText = "Mengulas: " + nama; document.getElementById('revRating').value = "5"; document.getElementById('revKomen').value = ""; m.style.display = 'flex'; }; window.submitReview = async function() { const tipe = document.getElementById('revTipe').value; const id = document.getElementById('revId').value; const rating = document.getElementById('revRating').value; const komen = document.getElementById('revKomen').value; const res = await fetch(APP_BASE + '/api/ulasan.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fasilitas_tipe: tipe, fasilitas_id: id, rating: rating, komentar: komen }) }); const j = await res.json(); if(j.status === 'success') { toast('Ulasan berhasil dikirim!'); document.getElementById('modalReview').style.display = 'none'; loadAll(); // reload data } else { toast(j.message, 'error'); } }; 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) => bindReadOnlyPopup(l, 'spbu', f.properties) }); if (document.getElementById('ly-spbu')?.checked) state.layers.spbu.addTo(state.map); } 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) => bindReadOnlyPopup(l, 'jalan', f.properties) }); if (document.getElementById('ly-jalan')?.checked) state.layers.jalan.addTo(state.map); } 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) => bindReadOnlyPopup(l, 'kavling', f.properties) }); if (document.getElementById('ly-kavling')?.checked) state.layers.kavling.addTo(state.map); } 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) => bindReadOnlyPopup(l, 'kawasan', f.properties) }); if (document.getElementById('ly-kawasan')?.checked) state.layers.kawasan.addTo(state.map); } 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 agama = props.agama || 'Islam'; const agamaColor = AGAMA_COLOR[agama] || COLORS.rumah; 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: agamaColor, fillColor: agamaColor, fillOpacity: 0.10, opacity: 0.60, weight: 2 }); const marker = L.marker(ll, { icon: getRumahIcon(agama) }); bindReadOnlyPopup(circle, 'rumah', props); bindReadOnlyPopup(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); } 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) => bindReadOnlyPopup(l, 'warga', f.properties) }); if (document.getElementById('ly-warga')?.checked) state.layers.warga.addTo(state.map); } 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) => { l.bindPopup(` `); } }); if (document.getElementById('ly-choropleth')?.checked) state.layers.choropleth.addTo(state.map); } 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); } 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); }); }); } 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 && j.data.features && j.data.features.length > 0) { const feature = j.data.features[0]; if (feature && feature.geometry && feature.geometry.coordinates) { const p = feature.properties; const coords = feature.geometry.coordinates; const jarakVal = p.jarak_km !== undefined ? p.jarak_km : (p.jarak_meter / 1000).toFixed(2); document.getElementById('spbuPanelResult').innerHTML = `
⛽ ${p.nama}
${p.buka_24_jam ? '🟢 24 Jam' : '🟡 Terbatas'}
${jarakVal} km
`; if (state.layers.spbuRoute) state.map.removeLayer(state.layers.spbuRoute); state.layers.spbuRoute = L.marker([coords[1], coords[0]], { icon: p.buka_24_jam ? ICONS.spbu24 : ICONS.spbu }).addTo(state.map) .bindPopup(`${p.nama}
${jarakVal} km dari pusat peta`).openPopup(); } else { document.getElementById('spbuPanelResult').innerHTML = `
Data geometri SPBU terdekat tidak valid.
`; } } else { document.getElementById('spbuPanelResult').innerHTML = `
${j.message || 'Tidak ada SPBU dalam jangkauan'}
`; } } catch (e) { document.getElementById('spbuPanelResult').innerHTML = `
Error: ${e.message}
`; } } 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); 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'; } } document.addEventListener('DOMContentLoaded', () => { initMap(); loadAll(); bindLayerToggles(); 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; } }); });