const state = { adm1: "", adm2: "", adm3: "", }; const elements = { adm1: document.getElementById("adm1Select"), adm2: document.getElementById("adm2Select"), adm3: document.getElementById("adm3Select"), tableBody: document.getElementById("featureTable"), totalAreaRai: document.getElementById("totalAreaRai"), itemCount: document.getElementById("itemCount"), averageAreaRai: document.getElementById("averageAreaRai"), loadingOverlay: document.getElementById("loadingOverlay"), clearButton: document.getElementById("clearFilters"), }; declareLoadingRow("Loading data..."); setLoading(true); let allFeatures = []; let map; let layerControl; let adminLayer; let palmLayer; let layerLookup = new Map(); let activeFeatureId = null; let activeRowId = null; initMap(); loadData(); attachEventListeners(); function declareLoadingRow(message) { elements.tableBody.innerHTML = `${message}`; } function initMap() { map = L.map("map", { preferCanvas: true, zoomControl: true, }); const osm = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "© OpenStreetMap contributors", }).addTo(map); const googleHybrid = L.tileLayer( "https://{s}.google.com/vt/lyrs=y&x={x}&y={y}&z={z}", { maxZoom: 21, subdomains: ["mt0", "mt1", "mt2", "mt3"], attribution: "© Google", } ); layerControl = L.control.layers( { "OpenStreetMap": osm, "Google Hybrid": googleHybrid, }, {}, { collapsed: false } ).addTo(map); if (L.control && typeof L.control.measure === "function") { L.control .measure({ position: "topleft", primaryLengthUnit: "kilometers", secondaryLengthUnit: "meters", primaryAreaUnit: "hectares", secondaryAreaUnit: "sqmeters", }) .addTo(map); } else { console.warn("Leaflet measure plugin is unavailable; measure control disabled."); } window.addEventListener("resize", () => map.invalidateSize()); map.createPane("adminPane"); map.createPane("palmPane"); map.getPane("adminPane").style.zIndex = 410; map.getPane("palmPane").style.zIndex = 420; adminLayer = L.geoJSON(null, { pane: "adminPane", style: { color: "#ffbf00ff", weight: 0.8, fillOpacity: 0, }, }).addTo(map); palmLayer = L.geoJSON(null, { pane: "palmPane", style: palmDefaultStyle, onEachFeature: onPalmFeature, }).addTo(map); layerControl.addOverlay(adminLayer, "Administrative Boundaries"); layerControl.addOverlay(palmLayer, "Palm Predictions"); } async function loadData() { try { setLoading(true); const [palmResponse, adminResponse] = await Promise.all([ fetch("palm_pred_all_admin.geojson"), fetch("admin_bnd.geojson"), ]); if (!palmResponse.ok || !adminResponse.ok) { throw new Error("Unable to load GeoJSON files."); } const [palmData, adminData] = await Promise.all([ palmResponse.json(), adminResponse.json(), ]); allFeatures = palmData.features; adminLayer.addData(adminData); initializeSelects(); updateDashboard({ fitBounds: true }); setLoading(false); } catch (error) { console.error(error); declareLoadingRow("Failed to load data. Check console for details."); setLoading(false); } } function attachEventListeners() { elements.adm1.addEventListener("change", () => { state.adm1 = elements.adm1.value; state.adm2 = ""; state.adm3 = ""; const baseForAdm2 = state.adm1 ? allFeatures.filter((feature) => feature.properties.ADM1_TH === state.adm1) : allFeatures; const baseForAdm3 = state.adm1 ? baseForAdm2 : allFeatures; populateSelect(elements.adm2, baseForAdm2, "ADM2_TH", Boolean(baseForAdm2.length)); populateSelect(elements.adm3, baseForAdm3, "ADM3_TH", Boolean(baseForAdm3.length)); updateDashboard({ fitBounds: true }); }); elements.adm2.addEventListener("change", () => { state.adm2 = elements.adm2.value; state.adm3 = ""; const baseForAdm3 = allFeatures.filter((feature) => { if (state.adm1 && feature.properties.ADM1_TH !== state.adm1) return false; if (state.adm2 && feature.properties.ADM2_TH !== state.adm2) return false; return true; }); populateSelect(elements.adm3, baseForAdm3, "ADM3_TH", Boolean(baseForAdm3.length)); updateDashboard({ fitBounds: true }); }); elements.adm3.addEventListener("change", () => { state.adm3 = elements.adm3.value; updateDashboard({ fitBounds: true }); }); if (elements.clearButton) { elements.clearButton.addEventListener("click", () => { state.adm1 = ""; state.adm2 = ""; state.adm3 = ""; elements.adm1.value = ""; elements.adm2.value = ""; elements.adm3.value = ""; elements.adm2.disabled = elements.adm2.options.length <= 1; elements.adm3.disabled = elements.adm3.options.length <= 1; updateDashboard({ fitBounds: true }); }); } } function initializeSelects() { populateSelect(elements.adm1, allFeatures, "ADM1_TH", Boolean(allFeatures.length)); populateSelect(elements.adm2, allFeatures, "ADM2_TH", Boolean(allFeatures.length)); populateSelect(elements.adm3, allFeatures, "ADM3_TH", Boolean(allFeatures.length)); } function populateSelect(select, features, propertyKey, enable) { const selectedValue = select.value; const values = getUniqueValues(features, propertyKey); select.innerHTML = ""; const defaultOption = document.createElement("option"); defaultOption.value = ""; defaultOption.textContent = "ทั้งหมด"; select.appendChild(defaultOption); values .sort((a, b) => a.localeCompare(b, "th", { sensitivity: "base" })) .forEach((value) => { const option = document.createElement("option"); option.value = value; option.textContent = value; select.appendChild(option); }); select.disabled = !enable || values.length === 0; if (!select.disabled && values.includes(selectedValue)) { select.value = selectedValue; } else { select.value = ""; } } function getUniqueValues(features, propertyKey) { const set = new Set(); features.forEach((feature) => { const value = feature.properties[propertyKey]; if (value || value === 0) { set.add(value); } }); return Array.from(set); } function applyFilters(features) { return features.filter((feature) => { const { ADM1_TH, ADM2_TH, ADM3_TH } = feature.properties; if (state.adm1 && ADM1_TH !== state.adm1) return false; if (state.adm2 && ADM2_TH !== state.adm2) return false; if (state.adm3 && ADM3_TH !== state.adm3) return false; return true; }); } function updateDashboard({ fitBounds } = { fitBounds: false }) { if (!allFeatures.length) return; const filtered = applyFilters(allFeatures); updateSummary(filtered); updatePalmLayer(filtered, { fitBounds }); renderTable(filtered); } function updateSummary(features) { const totals = features.reduce( (acc, feature) => { const { area_rai } = feature.properties; acc.area_rai += Number(area_rai) || 0; return acc; }, { area_rai: 0 } ); const totalAreaRai = totals.area_rai; const count = features.length; const averageAreaRai = count ? totalAreaRai / count : 0; elements.totalAreaRai.textContent = formatNumber(totalAreaRai, 2); elements.itemCount.textContent = formatNumber(count, 0); elements.averageAreaRai.textContent = formatNumber(averageAreaRai, 2); } function renderTable(features) { elements.tableBody.innerHTML = ""; activeRowId = null; if (!features.length) { declareLoadingRow("No features match the selected filters."); return; } const fragment = document.createDocumentFragment(); features.forEach((feature) => { const { id, ADM1_TH, ADM2_TH, ADM3_TH, area_m2, area_rai } = feature.properties; const row = document.createElement("tr"); row.dataset.featureId = id; row.innerHTML = ` ${id} ${ADM1_TH || "-"} ${ADM2_TH || "-"} ${ADM3_TH || "-"} ${formatNumber(area_m2, 2)} ${formatNumber(area_rai, 2)} `; row.addEventListener("click", () => { setActiveRow(id); highlightMapFeature(id, { adjustView: true, openPopup: true }); }); fragment.appendChild(row); }); elements.tableBody.appendChild(fragment); // Highlight the first feature by default for quick context. const firstId = features[0].properties.id; setActiveRow(firstId); highlightMapFeature(firstId, { adjustView: false, openPopup: false }); } function palmDefaultStyle() { return { color: "#ffffffff", weight: 0.6, fillColor: "#649e66ff", fillOpacity: 0.7, }; } function palmHighlightStyle() { return { color: "#ff8800", weight: 1.2, fillColor: "#ffe0b2", fillOpacity: 0.65, }; } function onPalmFeature(feature, layer) { const { id } = feature.properties; layerLookup.set(id, layer); const popupHtml = buildPopupContent(feature.properties); layer.bindPopup(popupHtml); layer.on("click", () => { setActiveRow(id); highlightMapFeature(id, { adjustView: false, openPopup: false }); layer.openPopup(); }); } function updatePalmLayer(features, { fitBounds }) { layerLookup.clear(); palmLayer.clearLayers(); activeFeatureId = null; if (!features.length) { return; } palmLayer.addData(features); if (fitBounds) { const bounds = palmLayer.getBounds(); if (bounds.isValid()) { map.fitBounds(bounds, { padding: [20, 20] }); } } } function highlightMapFeature(id, { adjustView = false, openPopup = false } = {}) { if (activeFeatureId && layerLookup.has(activeFeatureId)) { const previousLayer = layerLookup.get(activeFeatureId); palmLayer.resetStyle(previousLayer); previousLayer.closePopup(); } activeFeatureId = null; const layer = layerLookup.get(id); if (!layer) return; activeFeatureId = id; layer.setStyle(palmHighlightStyle()); layer.bringToFront(); if (adjustView) { const bounds = layer.getBounds(); if (bounds.isValid()) { map.fitBounds(bounds, { maxZoom: 16, padding: [20, 20] }); } } if (openPopup) { layer.openPopup(); } } function setActiveRow(id) { if (activeRowId !== null) { const previousRow = elements.tableBody.querySelector( `tr[data-feature-id="${activeRowId}"]` ); if (previousRow) { previousRow.classList.remove("active"); } } const row = elements.tableBody.querySelector(`tr[data-feature-id="${id}"]`); if (!row) { activeRowId = null; return; } activeRowId = id; row.classList.add("active"); row.scrollIntoView({ block: "nearest" }); } function buildPopupContent(properties) { const rows = [ { label: "ADM1_TH", value: properties.ADM1_TH }, { label: "ADM2_TH", value: properties.ADM2_TH }, { label: "ADM3_TH", value: properties.ADM3_TH }, { label: "Area (m2)", value: formatNumber(properties.area_m2, 2) }, { label: "Area (Rai)", value: formatNumber(properties.area_rai, 2) }, ]; const details = rows .map(({ label, value }) => `
${label}: ${value ?? "-"}
`) .join(""); return ``; } function formatNumber(value, fractionDigits = 0) { if (value === null || value === undefined || Number.isNaN(Number(value))) { return "-"; } return new Intl.NumberFormat("en-US", { minimumFractionDigits: 0, maximumFractionDigits: fractionDigits, }).format(Number(value)); } function setLoading(isLoading) { document.body.classList.toggle("is-loading", Boolean(isLoading)); if (elements.loadingOverlay) { elements.loadingOverlay.setAttribute("aria-hidden", isLoading ? "false" : "true"); } }