378 lines
14 KiB
JavaScript
378 lines
14 KiB
JavaScript
/* ============================================================
|
||
map.js – WebGIS Pemetaan Kemiskinan & Masjid (SRS v1.0)
|
||
============================================================ */
|
||
|
||
window.appState = {
|
||
mode: "view",
|
||
activeType: "kemiskinan",
|
||
layers: {},
|
||
layerVisibility: {
|
||
kemiskinan: true,
|
||
masjid: true
|
||
},
|
||
isHeatmap: false,
|
||
isRouting: false,
|
||
routingWaypoints: []
|
||
};
|
||
|
||
/* ── Utility: escape HTML ── */
|
||
window.escapeHtml = function (value) {
|
||
return String(value ?? "")
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
};
|
||
|
||
/* ── Utility: SVG pin marker ── */
|
||
window.createPinIcon = function (bgColor, svgSymbol) {
|
||
var svg = [
|
||
'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="42" viewBox="0 0 32 42">',
|
||
'<defs>',
|
||
'<filter id="shadow" x="-30%" y="-20%" width="160%" height="160%">',
|
||
'<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="rgba(0,0,0,0.35)"/>',
|
||
'</filter>',
|
||
'</defs>',
|
||
/* Pin body */
|
||
'<path d="M16 2C9.37 2 4 7.37 4 14c0 9 12 26 12 26S28 23 28 14C28 7.37 22.63 2 16 2z"',
|
||
'fill="' + bgColor + '" filter="url(#shadow)"/>',
|
||
/* White inner circle */
|
||
'<circle cx="16" cy="14" r="8" fill="rgba(255,255,255,0.25)"/>',
|
||
/* Symbol */
|
||
svgSymbol,
|
||
'</svg>'
|
||
].join('');
|
||
return L.divIcon({
|
||
className: 'custom-pin-icon',
|
||
html: svg,
|
||
iconSize: [32, 42],
|
||
iconAnchor: [16, 42],
|
||
popupAnchor: [0, -44]
|
||
});
|
||
};
|
||
|
||
/* Kemiskinan – merah (tidak terjangkau) */
|
||
window.redIcon = window.createPinIcon('#dc2626',
|
||
'<text x="16" y="19" text-anchor="middle" font-size="11" fill="white" font-family="Arial">👪</text>'
|
||
);
|
||
/* Kemiskinan – hijau (terjangkau) */
|
||
window.greenIcon = window.createPinIcon('#16a34a',
|
||
'<text x="16" y="19" text-anchor="middle" font-size="11" fill="white" font-family="Arial">👪</text>'
|
||
);
|
||
/* Masjid – biru */
|
||
window.blueIcon = window.createPinIcon('#1d4ed8',
|
||
'<text x="16" y="19" text-anchor="middle" font-size="11" fill="white" font-family="Arial">🕌</text>'
|
||
);
|
||
/* Fallback orange (tidak digunakan, tapi tetap didefinisikan) */
|
||
window.orangeIcon = window.createPinIcon('#f97316',
|
||
'<text x="16" y="19" text-anchor="middle" font-size="11" fill="white" font-family="Arial">👪</text>'
|
||
);
|
||
|
||
/* ── Utility: POST form-encoded ── */
|
||
window.postForm = async function (url, payload) {
|
||
var response = await fetch(url, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||
body: new URLSearchParams(payload)
|
||
});
|
||
var text = (await response.text()).trim();
|
||
if (!response.ok || text !== "success") {
|
||
throw new Error(text || "Permintaan gagal.");
|
||
}
|
||
};
|
||
|
||
/* ── Navbar hint ── */
|
||
window.updateMapHint = function () {
|
||
var hint = document.getElementById("mapHint");
|
||
if (!hint) { return; }
|
||
|
||
if (appState.mode === "view") {
|
||
hint.textContent = "Mode Lihat aktif — data keluarga prasejahtera & rumah ibadah ditampilkan. Klik kartu untuk navigasi peta.";
|
||
return;
|
||
}
|
||
if (appState.activeType === "kemiskinan") {
|
||
hint.textContent = "Mode Edit aktif — klik titik di peta untuk menambahkan data keluarga prasejahtera.";
|
||
return;
|
||
}
|
||
hint.textContent = "Mode Edit aktif — klik titik di peta untuk menambahkan data rumah ibadah baru.";
|
||
};
|
||
|
||
/* ── Toolbar state ── */
|
||
window.updateToolbarState = function () {
|
||
var states = {
|
||
modeViewBtn: appState.mode === "view",
|
||
modeEditBtn: appState.mode === "edit",
|
||
typeKemiskinanBtn: appState.activeType === "kemiskinan",
|
||
typeMasjidBtn: appState.activeType === "masjid"
|
||
};
|
||
Object.keys(states).forEach(function (id) {
|
||
var btn = document.getElementById(id);
|
||
if (!btn) { return; }
|
||
btn.classList.toggle("active", states[id]);
|
||
/* Mode buttons get a different active colour */
|
||
if (id === "modeEditBtn" && states[id]) {
|
||
btn.classList.add("mode-active");
|
||
} else {
|
||
btn.classList.remove("mode-active");
|
||
}
|
||
});
|
||
|
||
var viewBtn = document.getElementById("luminaModeView");
|
||
var editBtn = document.getElementById("luminaModeEdit");
|
||
if (viewBtn && editBtn) {
|
||
viewBtn.style.opacity = appState.mode === "view" ? "1" : "0.5";
|
||
viewBtn.style.boxShadow = appState.mode === "view" ? "0 4px 12px rgba(99,102,241,0.25)" : "none";
|
||
|
||
editBtn.style.opacity = appState.mode === "edit" ? "1" : "0.5";
|
||
editBtn.style.boxShadow = appState.mode === "edit" ? "0 4px 12px rgba(20,184,166,0.25)" : "none";
|
||
}
|
||
};
|
||
|
||
window.setActiveMenu = function(element) {
|
||
var items = document.querySelectorAll('.lumina-menu-item');
|
||
items.forEach(function(item) {
|
||
item.classList.remove('active');
|
||
});
|
||
element.classList.add('active');
|
||
};
|
||
|
||
/* ── Layer visibility ── */
|
||
window.setLayerVisibility = function (key, isVisible) {
|
||
appState.layerVisibility[key] = !!isVisible;
|
||
var layer = appState.layers[key];
|
||
if (!layer || !window.map) { return; }
|
||
if (isVisible) {
|
||
if (!map.hasLayer(layer)) { map.addLayer(layer); }
|
||
} else {
|
||
if (map.hasLayer(layer)) { map.removeLayer(layer); }
|
||
}
|
||
};
|
||
|
||
/* ── Mode & type switches ── */
|
||
window.setMode = function (mode) {
|
||
appState.mode = mode;
|
||
window.updateToolbarState();
|
||
window.updateMapHint();
|
||
|
||
var mapEl = document.getElementById("map");
|
||
if (mapEl) {
|
||
if (mode === "edit") {
|
||
mapEl.classList.add("edit-mode");
|
||
} else {
|
||
mapEl.classList.remove("edit-mode");
|
||
}
|
||
}
|
||
|
||
if (typeof window.loadAllData === "function") {
|
||
window.loadAllData();
|
||
}
|
||
};
|
||
|
||
window.setDataType = function (type) {
|
||
appState.activeType = type;
|
||
window.updateToolbarState();
|
||
window.updateMapHint();
|
||
if (typeof window.filterSidebar === 'function') {
|
||
window.filterSidebar(type);
|
||
}
|
||
};
|
||
|
||
window.filterSidebar = function(type) {
|
||
var secKem = document.getElementById('sec-kemiskinan');
|
||
var secMsj = document.getElementById('sec-masjid');
|
||
|
||
if (secKem) secKem.style.display = (type === 'semua' || type === 'kemiskinan') ? 'block' : 'none';
|
||
if (secMsj) secMsj.style.display = (type === 'semua' || type === 'masjid') ? 'block' : 'none';
|
||
|
||
var btnSemua = document.getElementById('filter-btn-semua');
|
||
var btnKem = document.getElementById('filter-btn-kemiskinan');
|
||
var btnMsj = document.getElementById('filter-btn-masjid');
|
||
|
||
if (btnSemua) btnSemua.classList.toggle('active', type === 'semua');
|
||
if (btnKem) btnKem.classList.toggle('active', type === 'kemiskinan');
|
||
if (btnMsj) btnMsj.classList.toggle('active', type === 'masjid');
|
||
|
||
var sidebarSub = document.getElementById('sidebar-sub');
|
||
if (sidebarSub) {
|
||
if (type === 'semua') sidebarSub.textContent = 'Menampilkan semua data';
|
||
else if (type === 'kemiskinan') sidebarSub.textContent = 'Menampilkan list Data Keluarga';
|
||
else sidebarSub.textContent = 'Menampilkan list Rumah Ibadah';
|
||
}
|
||
|
||
var sidebar = document.getElementById('sidebar');
|
||
if (sidebar && sidebar.style.display === 'none') {
|
||
sidebar.style.display = 'flex';
|
||
var pes = document.getElementById('panel-pesan');
|
||
if (pes) pes.style.display = 'none';
|
||
var lap = document.getElementById('panel-laporan');
|
||
if (lap) lap.style.display = 'none';
|
||
}
|
||
};
|
||
|
||
/* ── Format helpers ── */
|
||
window.formatMeters = function (value) {
|
||
return Number(value).toLocaleString("id-ID", { maximumFractionDigits: 0 }) + " m";
|
||
};
|
||
|
||
window.isValidCoordinate = function (lat, lng) {
|
||
return Number.isFinite(Number(lat)) && Number.isFinite(Number(lng));
|
||
};
|
||
|
||
window.isValidRadius = function (radius) {
|
||
var r = Number(radius);
|
||
return Number.isFinite(r) && r >= 100 && r <= 5000;
|
||
};
|
||
|
||
/* ─────────────────────────────────────────────
|
||
DOMContentLoaded – inisialisasi peta (KF-01)
|
||
───────────────────────────────────────────── */
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
|
||
/* Inisialisasi peta terpusat di Pontianak */
|
||
window.map = L.map("map").setView([-0.0263, 109.3425], 13);
|
||
|
||
// --- Base maps ---
|
||
window.osmBase = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
attribution: '© OpenStreetMap contributors'
|
||
});
|
||
window.darkBase = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||
attribution: '© OpenStreetMap contributors © CARTO'
|
||
});
|
||
|
||
// Add default basemap
|
||
window.osmBase.addTo(map);
|
||
|
||
/* ── Layers ── */
|
||
appState.layers.kemiskinan = L.layerGroup().addTo(map);
|
||
appState.layers.masjid = L.layerGroup().addTo(map);
|
||
appState.layers.heatmap = L.layerGroup(); // Do not add yet
|
||
|
||
/* Klik peta – hanya aktif di Mode Edit (KF-03, KF-07) */
|
||
map.on("click", function (event) {
|
||
if (appState.mode !== "edit") { return; }
|
||
|
||
var lat = event.latlng.lat;
|
||
var lng = event.latlng.lng;
|
||
|
||
if (appState.activeType === "kemiskinan") {
|
||
if (typeof window.openKemiskinanModal === "function") {
|
||
window.openKemiskinanModal(lat, lng);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (appState.activeType === "masjid") {
|
||
if (typeof window.openMasjidModal === "function") {
|
||
window.openMasjidModal(lat, lng);
|
||
}
|
||
}
|
||
});
|
||
|
||
/* Init state awal */
|
||
window.setMode("view");
|
||
window.setDataType("kemiskinan");
|
||
|
||
setTimeout(function () { map.invalidateSize(); }, 300);
|
||
|
||
/* Cek session login, lalu load data */
|
||
if (typeof window.checkAuthState === "function") {
|
||
window.checkAuthState().then(function () {
|
||
if (typeof window.loadAllData === "function") { window.loadAllData(); }
|
||
});
|
||
} else if (typeof window.loadAllData === "function") {
|
||
window.loadAllData();
|
||
}
|
||
});
|
||
|
||
/* ── Heatmap & Routing Features ── */
|
||
window.toggleHeatmap = function(btnEl) {
|
||
if(!window.appState) return;
|
||
window.appState.isHeatmap = !window.appState.isHeatmap;
|
||
btnEl.classList.toggle('active', window.appState.isHeatmap);
|
||
|
||
if(window.appState.isHeatmap) {
|
||
document.getElementById('desc-heatmap').style.display = 'block';
|
||
// Hide kemiskinan markers
|
||
window.appState.layers.kemiskinan.eachLayer(function(l) { l.setOpacity(0); });
|
||
// Show heatmap
|
||
if(!window.map.hasLayer(window.appState.layers.heatmap)) {
|
||
window.appState.layers.heatmap.addTo(window.map);
|
||
}
|
||
// Switch to Dark mode for better visual
|
||
window.map.removeLayer(window.osmBase);
|
||
window.darkBase.addTo(window.map);
|
||
} else {
|
||
document.getElementById('desc-heatmap').style.display = 'none';
|
||
// Show kemiskinan markers
|
||
window.appState.layers.kemiskinan.eachLayer(function(l) { l.setOpacity(1); });
|
||
// Hide heatmap
|
||
if(window.map.hasLayer(window.appState.layers.heatmap)) {
|
||
window.map.removeLayer(window.appState.layers.heatmap);
|
||
}
|
||
// Restore map base
|
||
window.map.removeLayer(window.darkBase);
|
||
window.osmBase.addTo(window.map);
|
||
}
|
||
};
|
||
|
||
window.toggleRoutingMode = function(btnEl) {
|
||
if(!window.appState) return;
|
||
window.appState.isRouting = true;
|
||
window.appState.routingWaypoints = [];
|
||
btnEl.classList.add('active');
|
||
document.getElementById('desc-routing').style.display = 'block';
|
||
|
||
// Show instruction panel
|
||
document.getElementById('routing-instruction-panel').style.display = 'block';
|
||
document.getElementById('routing-instruction-text').innerHTML = 'Silakan klik <b>Masjid</b> atau <b>Rumah Warga</b> di peta sebagai titik awal rute.';
|
||
};
|
||
|
||
window.cancelRouting = function() {
|
||
if(!window.appState) return;
|
||
window.appState.isRouting = false;
|
||
window.appState.routingWaypoints = [];
|
||
document.getElementById('btn-toggle-routing').classList.remove('active');
|
||
document.getElementById('desc-routing').style.display = 'none';
|
||
document.getElementById('routing-instruction-panel').style.display = 'none';
|
||
|
||
// Remove routing control if exists
|
||
if(window.routingControl) {
|
||
window.map.removeControl(window.routingControl);
|
||
window.routingControl = null;
|
||
}
|
||
};
|
||
|
||
window.handleRoutingClick = function(lat, lng, name) {
|
||
window.appState.routingWaypoints.push(L.latLng(lat, lng));
|
||
var pts = window.appState.routingWaypoints;
|
||
var inst = document.getElementById('routing-instruction-text');
|
||
|
||
if(pts.length === 1) {
|
||
inst.innerHTML = '<div style="margin-bottom:6px; color:#16a34a;"><i class="fa-solid fa-check"></i> Titik Awal: <b>' + window.escapeHtml(name) + '</b></div>' +
|
||
'<div style="margin-bottom:6px;"><b>Langkah 2:</b> Pilih Tujuan</div>' +
|
||
'<span style="color:var(--lumina-text-muted);">Klik rumah/masjid lain di peta sebagai tujuan akhir.</span>';
|
||
} else if(pts.length === 2) {
|
||
inst.innerHTML = '<div style="margin-bottom:6px; color:#16a34a;"><i class="fa-solid fa-check"></i> Rute Berhasil Dibuat</div>' +
|
||
'<span style="color:var(--lumina-text-muted);">Detail arah jalan (navigasi) muncul di layar sebelah kanan peta.</span>';
|
||
|
||
// Remove existing control
|
||
if(window.routingControl) {
|
||
window.map.removeControl(window.routingControl);
|
||
}
|
||
|
||
window.routingControl = L.Routing.control({
|
||
waypoints: pts,
|
||
routeWhileDragging: false,
|
||
addWaypoints: false,
|
||
show: true,
|
||
lineOptions: {
|
||
styles: [{color: '#2563eb', opacity: 0.8, weight: 6}]
|
||
}
|
||
}).addTo(window.map);
|
||
|
||
window.appState.isRouting = false; // Turn off click interception after 2 points
|
||
document.getElementById('btn-toggle-routing').classList.remove('active');
|
||
}
|
||
};
|