From 8d1d874ad7566b827a68a470b9f29740d819ae12 Mon Sep 17 00:00:00 2001 From: ekke Date: Wed, 23 Sep 2026 13:40:00 +0200 Subject: [PATCH] Shapefile export: write the format ourselves instead of shp-write Choosing Shapefile in the Export GIS modal produced no file. shp-write@0.3.2 cannot run in this build, in three separate ways: its dbf dependency assigns an undeclared variable, which was fine in the browserify bundle it was written for but is a ReferenceError under the strict mode ES modules always use; its zip.js branches on process.browser, which does not exist in a browser; and its download.js delivers the archive by navigating to a data:application/zip URL, which Chromium blocks in the top frame. - src/shapefile-write.js writes .shp/.shx/.dbf/.prj/.cpg itself: geometries split by type into up to three layers, polygon rings re-wound clockwise as the format requires, DBF column types inferred per column, names sanitised to DBF's 10 characters and de-duplicated, UTF-8 declared via .cpg. - src/zip-store.js holds the STORED-ZIP writer that was inside analysis/xlsx.js, so both writers share it. Pure-ASCII archives are byte-for-byte as before. - shp-write, and with it dbf and JSZip 2.5.0, leave the dependency tree; the build no longer warns that node's "assert" was externalised. - Service Worker v26 for the new bundle. Verified in a browser: the Export GIS modal, driven through its own controls with Web Mercator features, produced area_analysis.zip holding both layers with WGS84 coordinates, file code 9994 and the right shape types. The archive reads back through the application's own shpjs importer with geometries, numeric, text and boolean attributes intact. The zonal-statistics .xlsx export, which now shares the zip writer, still opens and reads correctly. Co-Authored-By: Claude Opus 5 --- dist/assets/analysis-modal-CXnHlVxO.js | 11 - dist/assets/analysis-modal-CXnHlVxO.js.map | 1 - dist/assets/analysis-modal-Ceula2pX.js | 12 + dist/assets/analysis-modal-Ceula2pX.js.map | 1 + ...eotiff-B9aROX49.js => geotiff-B7h926ms.js} | 4 +- ...9aROX49.js.map => geotiff-B7h926ms.js.map} | 2 +- dist/assets/index-BzlrAJOS.js | 933 ------------------ dist/assets/index-BzlrAJOS.js.map | 1 - dist/assets/index-uzWunVfg.js | 933 ++++++++++++++++++ dist/assets/index-uzWunVfg.js.map | 1 + ...ex.es-CW5N1l0t.js => index.es-MBT22Veh.js} | 4 +- ...5N1l0t.js.map => index.es-MBT22Veh.js.map} | 2 +- .../{jspdf-BsOD2LDR.js => jspdf-Dh9Z6DYg.js} | 8 +- ...-BsOD2LDR.js.map => jspdf-Dh9Z6DYg.js.map} | 2 +- dist/assets/pako-Xa-UToif.js | 2 - dist/assets/pako-Xa-UToif.js.map | 1 - dist/assets/pako-n3Pgozwg.js | 2 + dist/assets/pako-n3Pgozwg.js.map | 1 + ...ort-C0eAmANf.js => pdf-export-cmdWg-4I.js} | 4 +- ...mANf.js.map => pdf-export-cmdWg-4I.js.map} | 2 +- dist/assets/shpjs-CNrRgkgn.js | 5 + dist/assets/shpjs-CNrRgkgn.js.map | 1 + dist/assets/shpjs-Doisr2ug.js | 6 - dist/assets/shpjs-Doisr2ug.js.map | 1 - dist/assets/xlsx-D1ZOqanH.js | 30 + dist/assets/xlsx-D1ZOqanH.js.map | 1 + dist/assets/xlsx-d1LrY7GM.js | 30 - dist/assets/xlsx-d1LrY7GM.js.map | 1 - dist/index.html | 7 +- dist/sw.js | 11 +- package-lock.json | 41 - package.json | 1 - public/sw.js | 11 +- src/analysis/xlsx.js | 102 +- src/gis-export.js | 99 +- src/shapefile-write.js | 529 ++++++++++ src/zip-store.js | 112 +++ vite.config.js | 7 +- 38 files changed, 1699 insertions(+), 1223 deletions(-) delete mode 100644 dist/assets/analysis-modal-CXnHlVxO.js delete mode 100644 dist/assets/analysis-modal-CXnHlVxO.js.map create mode 100644 dist/assets/analysis-modal-Ceula2pX.js create mode 100644 dist/assets/analysis-modal-Ceula2pX.js.map rename dist/assets/{geotiff-B9aROX49.js => geotiff-B7h926ms.js} (99%) rename dist/assets/{geotiff-B9aROX49.js.map => geotiff-B7h926ms.js.map} (99%) delete mode 100644 dist/assets/index-BzlrAJOS.js delete mode 100644 dist/assets/index-BzlrAJOS.js.map create mode 100644 dist/assets/index-uzWunVfg.js create mode 100644 dist/assets/index-uzWunVfg.js.map rename dist/assets/{index.es-CW5N1l0t.js => index.es-MBT22Veh.js} (99%) rename dist/assets/{index.es-CW5N1l0t.js.map => index.es-MBT22Veh.js.map} (99%) rename dist/assets/{jspdf-BsOD2LDR.js => jspdf-Dh9Z6DYg.js} (99%) rename dist/assets/{jspdf-BsOD2LDR.js.map => jspdf-Dh9Z6DYg.js.map} (99%) delete mode 100644 dist/assets/pako-Xa-UToif.js delete mode 100644 dist/assets/pako-Xa-UToif.js.map create mode 100644 dist/assets/pako-n3Pgozwg.js create mode 100644 dist/assets/pako-n3Pgozwg.js.map rename dist/assets/{pdf-export-C0eAmANf.js => pdf-export-cmdWg-4I.js} (91%) rename dist/assets/{pdf-export-C0eAmANf.js.map => pdf-export-cmdWg-4I.js.map} (99%) create mode 100644 dist/assets/shpjs-CNrRgkgn.js create mode 100644 dist/assets/shpjs-CNrRgkgn.js.map delete mode 100644 dist/assets/shpjs-Doisr2ug.js delete mode 100644 dist/assets/shpjs-Doisr2ug.js.map create mode 100644 dist/assets/xlsx-D1ZOqanH.js create mode 100644 dist/assets/xlsx-D1ZOqanH.js.map delete mode 100644 dist/assets/xlsx-d1LrY7GM.js delete mode 100644 dist/assets/xlsx-d1LrY7GM.js.map create mode 100644 src/shapefile-write.js create mode 100644 src/zip-store.js diff --git a/dist/assets/analysis-modal-CXnHlVxO.js b/dist/assets/analysis-modal-CXnHlVxO.js deleted file mode 100644 index 0addf2e..0000000 --- a/dist/assets/analysis-modal-CXnHlVxO.js +++ /dev/null @@ -1,11 +0,0 @@ -import{_ as de}from"./geotiff-B9aROX49.js";import{M as pe}from"./bootstrap-D1-uvFxm.js";import{ax as ye}from"./openlayers-D5-VSKzq.js";import{i as X,f as j,a as fe,b as me,c as he,d as ge,e as be,g as we}from"./turf-B-0GbNlN.js";import{s as F}from"./index-BzlrAJOS.js";import"./pako-Xa-UToif.js";import"./ol-ext-DV70ljNY.js";import"./shpjs-Doisr2ug.js";const Q=4e5,ve=new ye;function $(e,n="EPSG:3857"){const o=[];for(const s of e||[]){if(!(s.getGeometry&&s.getGeometry()))continue;const a=ve.writeFeatureObject(s,{dataProjection:"EPSG:4326",featureProjection:n});a&&a.geometry&&o.push(a)}return o}function N(e){const n=e&&e.geometry;if(!n||!n.coordinates)return null;let o=1/0,s=1/0,r=-1/0,a=-1/0;const c=l=>{if(typeof l[0]=="number"){l[0]r&&(r=l[0]),l[1]a&&(a=l[1]);return}for(const i of l)c(i)};return c(n.coordinates),Number.isFinite(o)?[o,s,r,a]:null}function D(e,n){return!e||!n?!1:!(e[2]=e[0]&&n<=e[2]&&o>=e[1]&&o<=e[3]:!1}function _(e,n,o){if(e.length>0||n.length===0)return;const s=[...new Set(n.map(a=>a?.geometry?.type).filter(Boolean))],r=s.length?s.join(", "):"no geometry";throw new Error(`${o} contains no polygons (found ${r}). Overlay operations work on polygon layers only — line layers such as contours or roads cannot be used here. To summarise a line layer per zone, use Zonal statistics instead.`)}function I(e){return(e||[]).filter(n=>{const o=n?.geometry?.type;return o==="Polygon"||o==="MultiPolygon"})}function Be(e,n,o="b_"){const s={...e||{}};for(const[r,a]of Object.entries(n||{}))s[Object.prototype.hasOwnProperty.call(s,r)?`${o}${r}`:r]=a;return s}function z(e){return{type:"FeatureCollection",features:e}}function ee(e,n,o={}){const{keepBAttributes:s=!0}=o,r=$(e),a=$(n),c=I(r),l=I(a);_(c,r,"Layer A"),_(l,a,"Layer B");const i=l.map(N);let u=0;const f=[];for(const d of c){const y=N(d);for(let m=0;mQ)throw te(u);let p=null;try{p=me(j([d,l[m]]))}catch{p=null}!p||!p.geometry||(p.properties=s?Be(d.properties,l[m].properties):{...d.properties||{}},f.push(p))}}return z(f)}function Ee(e,n,o={}){return ee(e,n,{...o,keepBAttributes:!1})}function Le(e,n){const o=$(e),s=$(n),r=I(o),a=I(s);_(r,o,"Layer A"),_(a,s,"Layer B");const c=a.map(N);let l=0;const i=[];for(const u of r){const f=N(u);let d=u;for(let y=0;yQ)throw te(l);try{const m=fe(j([d,a[y]]));d=m&&m.geometry?m:null}catch{}}d&&d.geometry&&(d.properties={...u.properties||{}},i.push(d))}return z(i)}function Ae(e,n=[]){const o=$(e),s=$(n),r=I(o),a=I(s);_(r,o,"Layer A"),_(a,s,"Layer B");const c=[...r,...a];if(c.length===0)return z([]);if(c.length===1)return z([c[0]]);let l=null;try{l=X(j(c))}catch{l=c[0];for(let i=1;i{const A=w?.geometry?.type;return A==="Polygon"||A==="MultiPolygon"}),l=$(n)||[];if(c.length===0)throw new Error("The zone layer contains no polygons.");const i=[],u=new Set(l.map(w=>w?.geometry?.type).filter(Boolean)),f=u.has("Polygon")||u.has("MultiPolygon");o.includes("area")&&!f&&l.length>0&&i.push(`Total area is 0 because the features being summarised are ${[...u].join("/")} — only polygons have an area.`);const d=u.has("LineString")||u.has("MultiLineString");o.includes("mean")&&d&&!f&&s&&i.push(`Mean is the average of “${s}” across the individual line features in each zone, not an area-weighted average of the surface they describe. For contour data, Minimum and Maximum (the elevation range) are the figures you can rely on.`);const y=r==="centroid"?l.map(w=>{try{return he(w)}catch{return null}}):null,m=r==="centroid"?null:l.map(N),p=c.map(N);let L=0;const W=[];for(let w=0;wY)throw q(L);b=ge(E,A)}else{if(!D(U,m[h]))continue;if(++L>Y)throw q(L);b=be(l[h],A)}}catch{b=!1}b&&O.push(l[h])}const B=s?O.map(h=>ke(h.properties?h.properties[s]:null)).filter(h=>h!==null):[],S={};for(const h of o)switch(h){case"count":S[`${a}count`]=O.length;break;case"sum":S[`${a}sum`]=B.length?C(B.reduce((b,E)=>b+E,0)):0;break;case"mean":S[`${a}mean`]=B.length?C(B.reduce((b,E)=>b+E,0)/B.length):null;break;case"min":S[`${a}min`]=B.length?C(Math.min(...B)):null;break;case"max":S[`${a}max`]=B.length?C(Math.max(...B)):null;break;case"area":{let b=0;for(const E of O)try{b+=we(E)}catch{}S[`${a}area_m2`]=Math.round(b);break}}A.properties={...A.properties||{},...S},W.push({...Me(A),...S})}return{geojson:{type:"FeatureCollection",features:c},rows:W,warnings:i}}function q(e){return new Error(`More than ${e.toLocaleString()} features fall within the zones' bounding boxes, which is too much to summarise in the browser. Narrow the inputs using “Apply to” (current map view or selected features).`)}function C(e){return Math.round(e*1e3)/1e3}function Me(e){const n=e.properties||{};for(const o of["zone_name","name","ZONE_NAME","NAME","upn","UPN","id","ID"])if(n[o]!==void 0&&n[o]!==null&&String(n[o]).trim()!=="")return{zone:String(n[o])};return{zone:"(unnamed)"}}const Ne=9001,H="Analysis",t={};let P=null,v=null,M=[],x=null,k=null;function _e(){t.root||(t.root=document.getElementById("analysisModal"),t.mode=Array.from(document.querySelectorAll('input[name="analysis-mode"]')),t.overlayBox=document.getElementById("analysis-overlay-box"),t.zonalBox=document.getElementById("analysis-zonal-box"),t.op=document.getElementById("analysis-op"),t.opHint=document.getElementById("analysis-op-hint"),t.layerA=document.getElementById("analysis-layer-a"),t.layerB=document.getElementById("analysis-layer-b"),t.layerBWrap=document.getElementById("analysis-layer-b-wrap"),t.zoneLayer=document.getElementById("analysis-zone-layer"),t.inputLayer=document.getElementById("analysis-input-layer"),t.membership=document.getElementById("analysis-membership"),t.memberHint=document.getElementById("analysis-membership-hint"),t.field=document.getElementById("analysis-field"),t.statsBox=document.getElementById("analysis-stats-box"),t.outName=document.getElementById("analysis-output-name"),t.result=document.getElementById("analysis-result"),t.btnRun=document.getElementById("analysis-run"),t.btnExport=document.getElementById("analysis-export-table"),t.scope=document.getElementById("analysis-scope"),t.scopeHint=document.getElementById("analysis-scope-hint"),t.root.dataset.wired||(t.root.dataset.wired="1",t.mode.forEach(e=>e.addEventListener("change",oe)),t.op.addEventListener("change",re),t.membership.addEventListener("change",se),t.inputLayer.addEventListener("change",ne),t.scope.addEventListener("change",le),t.btnRun.addEventListener("click",Ce),t.btnExport.addEventListener("click",je)))}function J(e=null){return M.map((n,o)=>``).join("")}function Ie(){t.op.innerHTML=G.map(e=>``).join(""),t.membership.innerHTML=Z.map(e=>``).join(""),t.statsBox.innerHTML=R.map(e=>` -
- - -
`).join("")}function ne(){const e=M[Number(t.inputLayer.value)],n=e?e.layer.getSource().getFeatures():[],o=$e(n);t.field.innerHTML=''+o.map(s=>``).join(""),t.field.disabled=o.length===0}function V(){return(t.mode.find(e=>e.checked)||{}).value||"overlay"}function oe(){k=null,ie();const e=V()==="overlay";t.overlayBox.classList.toggle("d-none",!e),t.zonalBox.classList.toggle("d-none",e),ae()}function re(){const e=G.find(o=>o.key===t.op.value);t.opHint.textContent=e?e.hint:"";const n=e?e.needsB:!0;t.layerBWrap.querySelector("label").textContent=n?"Layer B":"Layer B (optional)",ae()}function se(){const e=Z.find(n=>n.key===t.membership.value);t.memberHint.textContent=e?e.hint:""}function ae(){if(t.outName.dataset.touched!=="1")if(V()==="overlay"){const e=t.op.value||"overlay";t.outName.value=`${e}_result`}else t.outName.value="zonal_stats"}function T(e){const n=M[Number(e.value)];if(!n)return[];const o=n.layer.getSource(),s=t.scope?t.scope.value:"all";if(s==="view"){const r=v.getMap(),a=r.getView().calculateExtent(r.getSize());return o.getFeaturesInExtent(a)}if(s==="selection"){const r=new Set(v.getSelectedFeatures());return o.getFeatures().filter(a=>r.has(a))}return s==="drawn"&&x?o.getFeatures().filter(r=>x.features.has(r)):o.getFeatures()}function le(){const e=t.scope.value;if(e==="drawn"&&x)t.scopeHint.textContent=`${x.features.size.toLocaleString()} feature(s) caught by the ${x.label}.`;else if(e==="selection"){const n=v?v.getSelectedFeatures().length:0;t.scopeHint.textContent=n===0?"Nothing selected yet — use the Digitise tool: shift-click features, or hold Ctrl/Cmd and drag a box.":`${n.toLocaleString()} feature(s) currently selected.`}else e==="view"?t.scopeHint.textContent="Only features inside the current map view are analysed — zoom to the area of interest first.":t.scopeHint.textContent="Every feature in the chosen layer. Narrow this if a large layer is slow."}function Oe(){let e=v.getLayerGroupByTitle(H);return e||(v.addLayerGroup(Ne,H,"Results of client-side spatial analysis"),e=v.getLayerGroupByTitle(H)),e}function Ce(){t.result.innerHTML="",t.btnRun.disabled=!0,setTimeout(()=>{try{const e=V(),n=(t.outName.value||"").trim()||(e==="overlay"?"overlay_result":"zonal_stats");let o,s,r=[];if(e==="overlay"){k=null;const l=t.op.value,i=T(t.layerA),u=T(t.layerB),f=(G.find(d=>d.key===l)||{}).needsB;if(i.length===0)throw new Error("Layer A has no features.");if(f&&u.length===0)throw new Error("This operation needs a second layer with features.");o=Se(l,i,u),s=`${o.features.length.toLocaleString()} feature(s) produced.`}else{const l=T(t.zoneLayer),i=T(t.inputLayer),u=Array.from(t.statsBox.querySelectorAll("input:checked")).map(p=>p.value),f=t.field.value||null;if(u.length===0)throw new Error("Select at least one statistic.");if(u.some(p=>(R.find(L=>L.key===p)||{}).needsField)&&!f)throw new Error("Sum / mean / min / max need a numeric field.");const y=ze({zoneFeatures:l,inputFeatures:i,stats:u,field:f,membership:t.membership.value});o=y.geojson,r=y.warnings||[],s=Fe(y.rows);const m=(Z.find(p=>p.key===t.membership.value)||{}).label;k={rows:y.rows,warnings:y.warnings||[],params:{Analysis:"Zonal statistics","Zone layer":K(t.zoneLayer),"Features summarised":K(t.inputLayer),"Applied to":Te(),"Zones analysed":l.length,"Features analysed":i.length,"Counted when":m||t.membership.value,Statistics:u.map(p=>(R.find(L=>L.key===p)||{}).label||p).join(", "),"Numeric field":f||"(none)","Result layer":n}}}if(!o.features.length){t.result.innerHTML='
No result features — the layers may not overlap.
';return}const a=Oe();v.addGeoJSONLayer(o,n,{strokeColor:"#d97706",strokeWidth:2,fillColor:"rgba(217,119,6,0.18)"},a),ie();const c=r.length?'
'+r.map(l=>`
${g(l)}
`).join("")+"
":"";t.result.innerHTML=`
Added layer “${g(n)}”.
`+c+(typeof s=="string"&&s.startsWith("<")?s:`
${g(s)}
`),F(`Analysis complete — “${n}” added to the Analysis group.`,"success",4e3)}catch(e){console.error("[Analysis] run failed:",e),t.result.innerHTML=`
${g(e.message||String(e))}
`}finally{t.btnRun.disabled=!1}},20)}function K(e){const n=e.options[e.selectedIndex];return n?n.textContent:"(none)"}function Te(){const e=t.scope.options[t.scope.selectedIndex];return e?e.textContent:"Whole layer"}async function je(){if(k){t.btnExport.disabled=!0;try{const{exportXlsx:e}=await de(async()=>{const{exportXlsx:i}=await import("./xlsx-d1LrY7GM.js");return{exportXlsx:i}},[]),{rows:n,params:o,warnings:s}=k,r=Object.keys(n[0]||{}).filter(i=>i!=="zone"),a=[{cells:["Zone",...r],bold:!0},...n.map(i=>["zone"in i?i.zone:"",...r.map(u=>i[u]===null||i[u]===void 0?"":i[u])])],c=[{cells:["LUPMIS2 — Zonal statistics"],bold:!0},[],{cells:["Parameter","Value"],bold:!0},...Object.entries(o).map(([i,u])=>[i,u]),["Exported",new Date().toLocaleString()],...s&&s.length?[[],{cells:["Notes"],bold:!0},...s.map(i=>["",i])]:[]],l=(o["Result layer"]||"zonal_stats").toString().trim().replace(/[^A-Za-z0-9_\-]+/g,"_")||"zonal_stats";e([{name:"Parameters",rows:c},{name:"Results",rows:a}],`${l}.xlsx`),F("Table exported to Excel.","success",3e3)}catch(e){console.error("[Analysis] table export failed:",e),F(`Could not export the table: ${e.message}`,"error",6e3)}finally{t.btnExport.disabled=!1}}}function ie(){t.btnExport&&t.btnExport.classList.toggle("d-none",!k)}function Fe(e){if(!e||!e.length)return"";const n=Object.keys(e[0]).filter(r=>r!=="zone"),o=["Zone",...n.map(r=>r.replace(/^zs_/,""))].map(r=>`${g(r)}`).join(""),s=e.map(r=>`${g(String(r.zone))}`+n.map(a=>`${r[a]===null||r[a]===void 0?"—":g(String(r[a]))}`).join("")+"").join("");return`
- - ${o} - ${s} -
`}function Xe(e,n={}){if(v=e,_e(),x=n.scopeFeatures&&n.scopeFeatures.length?{features:new Set(n.scopeFeatures),label:n.scopeLabel||"drawn area"}:null,He(),M=v.listVectorLayers({nonEmptyOnly:!0}),M.length===0){F("No vector layers with features are loaded — nothing to analyse yet.","warning",5e3);return}Ie();const o=J(0),s=J(M.length>1?1:0);t.layerA.innerHTML=o,t.layerB.innerHTML=s,t.zoneLayer.innerHTML=o,t.inputLayer.innerHTML=s,t.outName.dataset.touched="",t.outName.addEventListener("input",()=>{t.outName.dataset.touched="1"},{once:!0}),t.result.innerHTML="",k=null,t.scope.value=x?"drawn":"all",ne(),re(),se(),le(),oe(),P=P||new pe(t.root),P.show()}function He(){const e=t.scope.querySelector('option[value="drawn"]');if(!x){e&&e.remove();return}const n=`From ${x.label} (${x.features.size.toLocaleString()} features)`;if(e)e.textContent=n;else{const o=document.createElement("option");o.value="drawn",o.textContent=n,t.scope.insertBefore(o,t.scope.firstChild)}}function g(e){return String(e).replace(/[&<>"']/g,n=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[n])}export{Xe as openAnalysisModal}; -//# sourceMappingURL=analysis-modal-CXnHlVxO.js.map diff --git a/dist/assets/analysis-modal-CXnHlVxO.js.map b/dist/assets/analysis-modal-CXnHlVxO.js.map deleted file mode 100644 index 6dcd36b..0000000 --- a/dist/assets/analysis-modal-CXnHlVxO.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"mappings":"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","names":["MAX_OVERLAY_CANDIDATES","fmt","GeoJSON","toGeoJSONFeatures","features","mapProjection","out","f","gj","bboxOf","feature","geom","minX","minY","maxX","maxY","walk","c","sub","bboxOverlaps","a","b","bboxContainsPoint","box","x","y","assertPolygons","polygons","original","role","types","found","polygonsOnly","gjFeatures","t","mergeProps","propsA","propsB","prefixB","k","v","fc","intersectFeatures","aFeatures","bFeatures","opts","keepBAttributes","gjA","gjB","A","B","bBoxes","candidates","aBox","i","candidateOverflow","piece","intersect","featureCollection","clipFeatures","clipFeatures_","differenceFeatures","remainder","next","difference","unionFeatures","polyA","polyB","all","merged","union","count","OVERLAY_OPS","runOverlay","op","MAX_ZONAL_CANDIDATES","MEMBERSHIP_MODES","STATS","listNumericFields","counts","props","num","value","zonalStatistics","zoneFeatures","inputFeatures","stats","field","membership","prefix","zones","z","inputs","warnings","inputTypes","inputsHavePolygons","inputsAreLines","centroids","centroid","inputBoxes","zoneBoxes","rows","zone","zBox","members","inside","cx","cy","booleanPointInPolygon","booleanIntersects","values","result","s","round","total","m","area","labelOf","n","p","ANALYSIS_GROUP_ID","ANALYSIS_GROUP_TITLE","els","modal","mapView","layerChoices","drawnScope","lastZonal","cacheEls","r","onModeChange","onOpChange","onMembershipChange","refreshFieldOptions","onScopeChange","onRun","onExportTable","layerOptionsHtml","selectedIdx","escapeHtml","populateOps","o","choice","feats","fields","currentMode","syncExportButton","overlay","suggestName","needsB","featuresOf","select","source","scope","map","extent","selected","ensureAnalysisGroup","group","mode","name","geojson","summary","zonalWarnings","res","renderRows","membershipLabel","labelOfSelect","scopeLabel","warnHtml","w","showToast","err","opt","exportXlsx","__vitePreload","params","keys","resultRows","paramRows","base","head","h","body","openAnalysisModal","view","syncDrawnScopeOption","optsA","optsB","Modal","existing","label"],"ignoreList":[],"sources":["../../src/analysis/overlay.js","../../src/analysis/zonal.js","../../src/analysis-modal.js"],"sourcesContent":["/**\n * overlay.js — client-side vector overlay operations (Stage 1 of the\n * LUPMIS2 GIS Analytical Tools concept).\n *\n * Pure geometry helpers built on Turf.js. No DOM, no map — the caller hands\n * in OpenLayers features and gets a GeoJSON FeatureCollection back, ready for\n * MapView.addGeoJSONLayer().\n *\n * All Turf work happens in EPSG:4326 (Turf's area/length functions assume\n * WGS84), so features are reprojected out of the map's EPSG:3857 on the way\n * in and the result is handed back as WGS84 GeoJSON — the same convention\n * gis-export.js uses.\n *\n * intersectFeatures(a, b) → parts of A that fall inside B (attrs: A + B)\n * clipFeatures(a, b) → parts of A that fall inside B (attrs: A only)\n * differenceFeatures(a, b) → parts of A that fall OUTSIDE B (attrs: A)\n * unionFeatures(a[, b]) → dissolved outline of A (and B)\n */\n\nimport GeoJSON from 'ol/format/GeoJSON';\nimport intersect from '@turf/intersect';\nimport union from '@turf/union';\nimport difference from '@turf/difference';\nimport { featureCollection } from '@turf/helpers';\n\n/**\n * Guard: only *candidate* pairs (those whose bounding boxes actually overlap)\n * reach the expensive geometry test, so this ceiling is about real work, not\n * the raw n×m product. District-scale data (tens of thousands of parcels\n * against a few dozen zones) sits far below it.\n */\nexport const MAX_OVERLAY_CANDIDATES = 400000;\n\nconst fmt = new GeoJSON();\n\n/**\n * Convert OpenLayers features (map projection) to WGS84 GeoJSON features.\n * @param {Array} features\n * @param {string} [mapProjection='EPSG:3857']\n * @returns {Array} GeoJSON Features\n */\nexport function toGeoJSONFeatures(features, mapProjection = 'EPSG:3857') {\n const out = [];\n for (const f of features || []) {\n const geom = f.getGeometry && f.getGeometry();\n if (!geom) continue;\n const gj = fmt.writeFeatureObject(f, {\n dataProjection: 'EPSG:4326',\n featureProjection: mapProjection,\n });\n if (gj && gj.geometry) out.push(gj);\n }\n return out;\n}\n\n/**\n * Bounding box of a GeoJSON feature as [minX, minY, maxX, maxY].\n * Walks the coordinate arrays directly — no extra dependency, and fast enough\n * to run over tens of thousands of features.\n * @param {Object} feature GeoJSON Feature\n * @returns {number[]|null}\n */\nexport function bboxOf(feature) {\n const geom = feature && feature.geometry;\n if (!geom || !geom.coordinates) return null;\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n const walk = (c) => {\n if (typeof c[0] === 'number') {\n if (c[0] < minX) minX = c[0];\n if (c[0] > maxX) maxX = c[0];\n if (c[1] < minY) minY = c[1];\n if (c[1] > maxY) maxY = c[1];\n return;\n }\n for (const sub of c) walk(sub);\n };\n walk(geom.coordinates);\n return Number.isFinite(minX) ? [minX, minY, maxX, maxY] : null;\n}\n\n/** Do two bounding boxes overlap? Cheap pre-filter before real geometry work. */\nexport function bboxOverlaps(a, b) {\n if (!a || !b) return false;\n return !(a[2] < b[0] || b[2] < a[0] || a[3] < b[1] || b[3] < a[1]);\n}\n\n/** Is a point [x, y] inside a bounding box? */\nexport function bboxContainsPoint(box, x, y) {\n if (!box) return false;\n return x >= box[0] && x <= box[2] && y >= box[1] && y <= box[3];\n}\n\n/**\n * Reject a non-polygon input with a message that says what is actually wrong.\n *\n * Overlay operations are polygon-only, but the inputs are silently filtered by\n * polygonsOnly() — so a line layer (contours, roads) used to produce an empty\n * result and the misleading \"the layers may not overlap\". Failing loudly here\n * tells the user the real reason.\n *\n * @param {Array} polygons the surviving polygonal features\n * @param {Array} original everything that was passed in\n * @param {string} role 'Layer A' / 'Layer B'\n */\nfunction assertPolygons(polygons, original, role) {\n if (polygons.length > 0 || original.length === 0) return;\n const types = [...new Set(original.map((f) => f?.geometry?.type).filter(Boolean))];\n const found = types.length ? types.join(', ') : 'no geometry';\n throw new Error(\n `${role} contains no polygons (found ${found}). Overlay operations work on ` +\n `polygon layers only — line layers such as contours or roads cannot be used here. ` +\n `To summarise a line layer per zone, use Zonal statistics instead.`\n );\n}\n\n/** Keep only polygonal features — overlay is undefined for points/lines here. */\nfunction polygonsOnly(gjFeatures) {\n return (gjFeatures || []).filter((f) => {\n const t = f?.geometry?.type;\n return t === 'Polygon' || t === 'MultiPolygon';\n });\n}\n\n/** Merge two property bags, prefixing B's keys on collision so nothing is lost. */\nfunction mergeProps(propsA, propsB, prefixB = 'b_') {\n const out = { ...(propsA || {}) };\n for (const [k, v] of Object.entries(propsB || {})) {\n out[Object.prototype.hasOwnProperty.call(out, k) ? `${prefixB}${k}` : k] = v;\n }\n return out;\n}\n\nfunction fc(features) {\n return { type: 'FeatureCollection', features };\n}\n\n/**\n * Intersection: every piece of A that also lies inside B.\n * Result carries A's attributes merged with the matching B feature's.\n *\n * @param {Array} aFeatures OL features (layer A)\n * @param {Array} bFeatures OL features (layer B)\n * @param {Object} [opts]\n * @param {boolean} [opts.keepBAttributes=true] merge B's properties into the result\n * @returns {Object} GeoJSON FeatureCollection (WGS84)\n */\nexport function intersectFeatures(aFeatures, bFeatures, opts = {}) {\n const { keepBAttributes = true } = opts;\n const gjA = toGeoJSONFeatures(aFeatures);\n const gjB = toGeoJSONFeatures(bFeatures);\n const A = polygonsOnly(gjA);\n const B = polygonsOnly(gjB);\n assertPolygons(A, gjA, 'Layer A');\n assertPolygons(B, gjB, 'Layer B');\n\n // Pre-compute B's boxes once, then only test pairs that can possibly meet.\n const bBoxes = B.map(bboxOf);\n let candidates = 0;\n const out = [];\n for (const a of A) {\n const aBox = bboxOf(a);\n for (let i = 0; i < B.length; i++) {\n if (!bboxOverlaps(aBox, bBoxes[i])) continue;\n if (++candidates > MAX_OVERLAY_CANDIDATES) throw candidateOverflow(candidates);\n let piece = null;\n try { piece = intersect(featureCollection([a, B[i]])); } catch { piece = null; }\n if (!piece || !piece.geometry) continue;\n piece.properties = keepBAttributes\n ? mergeProps(a.properties, B[i].properties)\n : { ...(a.properties || {}) };\n out.push(piece);\n }\n }\n return fc(out);\n}\n\n/**\n * Clip: same geometry result as intersect, but the output keeps ONLY layer A's\n * attributes — the usual \"cut this layer to that boundary\" operation.\n */\nexport function clipFeatures(aFeatures, clipFeatures_, opts = {}) {\n return intersectFeatures(aFeatures, clipFeatures_, { ...opts, keepBAttributes: false });\n}\n\n/**\n * Difference: the parts of A that do NOT fall inside any feature of B.\n * Each A feature is reduced by every overlapping B feature in turn; features\n * fully covered by B drop out of the result.\n *\n * @returns {Object} GeoJSON FeatureCollection (WGS84)\n */\nexport function differenceFeatures(aFeatures, bFeatures) {\n const gjA = toGeoJSONFeatures(aFeatures);\n const gjB = toGeoJSONFeatures(bFeatures);\n const A = polygonsOnly(gjA);\n const B = polygonsOnly(gjB);\n assertPolygons(A, gjA, 'Layer A');\n assertPolygons(B, gjB, 'Layer B');\n\n const bBoxes = B.map(bboxOf);\n let candidates = 0;\n const out = [];\n for (const a of A) {\n const aBox = bboxOf(a);\n let remainder = a;\n for (let i = 0; i < B.length; i++) {\n if (!remainder) break;\n // Only subtract a B feature that can actually touch what's left.\n if (!bboxOverlaps(aBox, bBoxes[i])) continue;\n if (++candidates > MAX_OVERLAY_CANDIDATES) throw candidateOverflow(candidates);\n try {\n const next = difference(featureCollection([remainder, B[i]]));\n remainder = next && next.geometry ? next : null;\n } catch {\n // A malformed pair shouldn't kill the whole run — keep what we have.\n }\n }\n if (remainder && remainder.geometry) {\n remainder.properties = { ...(a.properties || {}) };\n out.push(remainder);\n }\n }\n return fc(out);\n}\n\n/**\n * Union / dissolve: merge everything into a single outline. Pass only `a` to\n * dissolve one layer, or both to merge two layers together.\n *\n * @returns {Object} GeoJSON FeatureCollection with 0 or 1 feature (WGS84)\n */\nexport function unionFeatures(aFeatures, bFeatures = []) {\n // Check each layer separately rather than the combined set: if only the\n // second layer had polygons, a combined check would pass and the first\n // layer's features would be dropped without the user being told.\n const gjA = toGeoJSONFeatures(aFeatures);\n const gjB = toGeoJSONFeatures(bFeatures);\n const polyA = polygonsOnly(gjA);\n const polyB = polygonsOnly(gjB);\n assertPolygons(polyA, gjA, 'Layer A');\n assertPolygons(polyB, gjB, 'Layer B'); // no-op when no second layer is given\n const all = [...polyA, ...polyB];\n if (all.length === 0) return fc([]);\n if (all.length === 1) return fc([all[0]]);\n\n let merged = null;\n try {\n merged = union(featureCollection(all));\n } catch {\n // Fall back to a pairwise fold if the bulk union chokes on bad geometry.\n merged = all[0];\n for (let i = 1; i < all.length; i++) {\n try {\n const next = union(featureCollection([merged, all[i]]));\n if (next && next.geometry) merged = next;\n } catch { /* skip the offending feature */ }\n }\n }\n if (!merged || !merged.geometry) return fc([]);\n merged.properties = { parts: all.length };\n return fc([merged]);\n}\n\n/** @private Raised only when genuinely overlapping work exceeds the budget. */\nfunction candidateOverflow(count) {\n return new Error(\n `This overlay has more than ${count.toLocaleString()} genuinely overlapping feature pairs, ` +\n `which is too much to run in the browser. Narrow the inputs using “Apply to” ` +\n `(current map view or selected features), or run it server-side.`\n );\n}\n\n/** Operation registry used by the Analysis panel. */\nexport const OVERLAY_OPS = [\n { key: 'intersect', label: 'Intersect', needsB: true,\n hint: 'Keep the parts of layer A that fall inside layer B (attributes from both).' },\n { key: 'clip', label: 'Clip', needsB: true,\n hint: 'Cut layer A to the boundary of layer B (keeps only A’s attributes).' },\n { key: 'difference', label: 'Difference', needsB: true,\n hint: 'Keep the parts of layer A that fall outside layer B.' },\n { key: 'union', label: 'Union / Dissolve', needsB: false,\n hint: 'Merge features into a single outline (optionally with a second layer).' },\n];\n\n/**\n * Run an overlay operation by key.\n * @param {string} op one of OVERLAY_OPS keys\n * @param {Array} aFeatures\n * @param {Array} bFeatures\n * @returns {Object} GeoJSON FeatureCollection (WGS84)\n */\nexport function runOverlay(op, aFeatures, bFeatures) {\n switch (op) {\n case 'intersect': return intersectFeatures(aFeatures, bFeatures);\n case 'clip': return clipFeatures(aFeatures, bFeatures);\n case 'difference': return differenceFeatures(aFeatures, bFeatures);\n case 'union': return unionFeatures(aFeatures, bFeatures);\n default: throw new Error(`Unknown overlay operation: ${op}`);\n }\n}\n","/**\n * zonal.js — vector-in-vector zonal statistics (Stage 1 of the LUPMIS2 GIS\n * Analytical Tools concept).\n *\n * Answers questions like \"how many parcels are in each collector zone?\" or\n * \"what is the mean plot size per zone?\" — the classic *summarize within*\n * operation. Raster × vector zonal statistics arrives with the raster pipeline\n * in Stage 2; this module is pure vector and runs fully offline.\n *\n * Membership rules:\n * 'centroid' — a feature belongs to the zone containing its centroid\n * (default; each feature counts exactly once)\n * 'intersects' — a feature belongs to every zone it touches\n * (features straddling a boundary are counted more than once)\n */\n\nimport booleanPointInPolygon from '@turf/boolean-point-in-polygon';\nimport booleanIntersects from '@turf/boolean-intersects';\nimport centroid from '@turf/centroid';\nimport area from '@turf/area';\nimport { toGeoJSONFeatures, bboxOf, bboxOverlaps, bboxContainsPoint } from './overlay.js';\n\n/**\n * Guard on *candidate* pairs only — a bounding-box pre-filter means a feature\n * is geometry-tested against a zone only when it could plausibly fall in it.\n * A district of 25,000 parcels across 40-odd zones produces roughly one\n * candidate per parcel, so real workloads sit far below this ceiling.\n */\nexport const MAX_ZONAL_CANDIDATES = 400000;\n\nexport const MEMBERSHIP_MODES = [\n { key: 'centroid', label: 'Centroid inside zone',\n hint: 'Each feature is counted once, in the zone containing its centre.' },\n { key: 'intersects', label: 'Any overlap with zone',\n hint: 'A feature is counted in every zone it touches (may double-count).' },\n];\n\n/** Statistics offered by the Analysis panel. */\nexport const STATS = [\n { key: 'count', label: 'Count', needsField: false },\n { key: 'sum', label: 'Sum', needsField: true },\n { key: 'mean', label: 'Mean', needsField: true },\n { key: 'min', label: 'Minimum', needsField: true },\n { key: 'max', label: 'Maximum', needsField: true },\n { key: 'area', label: 'Total area (m²)', needsField: false },\n];\n\n/**\n * List the numeric attribute names available across a set of OL features, so\n * the panel can offer only fields that can actually be summed or averaged.\n * @param {Array} features OL features\n * @returns {string[]}\n */\nexport function listNumericFields(features) {\n const counts = new Map();\n for (const f of features || []) {\n const props = f.getProperties ? f.getProperties() : {};\n for (const [k, v] of Object.entries(props)) {\n if (k === 'geometry' || k.startsWith('_')) continue;\n const n = typeof v === 'number' ? v : (v !== '' && v != null && !Number.isNaN(Number(v)) ? Number(v) : null);\n if (n === null) continue;\n counts.set(k, (counts.get(k) || 0) + 1);\n }\n }\n return Array.from(counts.keys()).sort();\n}\n\n/** Coerce a property to a finite number, or null when it isn't numeric. */\nfunction num(value) {\n if (value === null || value === undefined || value === '') return null;\n const n = typeof value === 'number' ? value : Number(value);\n return Number.isFinite(n) ? n : null;\n}\n\n/**\n * Compute zonal statistics.\n *\n * @param {Object} params\n * @param {Array} params.zoneFeatures OL features defining the zones (polygons)\n * @param {Array} params.inputFeatures OL features to summarise\n * @param {string[]} params.stats keys from STATS\n * @param {string} [params.field] numeric attribute (for sum/mean/min/max)\n * @param {string} [params.membership] 'centroid' | 'intersects'\n * @param {string} [params.prefix='zs_'] prefix for the new attribute names\n * @returns {{ geojson: Object, rows: Array }}\n * geojson — the zones with statistics attached (WGS84), ready to add as a layer\n * rows — a plain table of the same numbers, for display/export\n * warnings — figures that are valid but easy to misread (e.g. area over a\n * line layer); shown to the user rather than thrown\n */\nexport function zonalStatistics({\n zoneFeatures,\n inputFeatures,\n stats = ['count'],\n field = null,\n membership = 'centroid',\n prefix = 'zs_',\n}) {\n const zones = (toGeoJSONFeatures(zoneFeatures) || []).filter((z) => {\n const t = z?.geometry?.type;\n return t === 'Polygon' || t === 'MultiPolygon';\n });\n const inputs = toGeoJSONFeatures(inputFeatures) || [];\n\n if (zones.length === 0) throw new Error('The zone layer contains no polygons.');\n\n // Warnings are returned alongside the result rather than thrown: the run is\n // still valid and useful, but some figures would be silently meaningless\n // without an explanation.\n const warnings = [];\n const inputTypes = new Set(inputs.map((f) => f?.geometry?.type).filter(Boolean));\n const inputsHavePolygons = inputTypes.has('Polygon') || inputTypes.has('MultiPolygon');\n\n // Lines and points have no area, so \"Total area\" would report a silent 0.\n if (stats.includes('area') && !inputsHavePolygons && inputs.length > 0) {\n warnings.push(\n `Total area is 0 because the features being summarised are ` +\n `${[...inputTypes].join('/')} — only polygons have an area.`\n );\n }\n\n // Line inputs only. Averaging a field over line segments is not an\n // area-weighted average of the surface those lines describe — the classic\n // trap being \"mean elevation per parcel\" from contours, where segment count\n // has no relation to the area at each elevation. (Averaging over points is a\n // normal statistic, so it is not flagged.)\n const inputsAreLines = inputTypes.has('LineString') || inputTypes.has('MultiLineString');\n if (stats.includes('mean') && inputsAreLines && !inputsHavePolygons && field) {\n warnings.push(\n `Mean is the average of “${field}” across the individual line features in ` +\n `each zone, not an area-weighted average of the surface they describe. ` +\n `For contour data, Minimum and Maximum (the elevation range) are the ` +\n `figures you can rely on.`\n );\n }\n\n // Pre-compute once: each input's centroid (centroid mode) or bbox\n // (intersects mode), and every zone's bbox. The bbox comparisons below are\n // cheap arithmetic; only survivors reach the real geometry test.\n const centroids = membership === 'centroid'\n ? inputs.map((f) => { try { return centroid(f); } catch { return null; } })\n : null;\n const inputBoxes = membership === 'centroid' ? null : inputs.map(bboxOf);\n const zoneBoxes = zones.map(bboxOf);\n\n let candidates = 0;\n const rows = [];\n for (let z = 0; z < zones.length; z++) {\n const zone = zones[z];\n const zBox = zoneBoxes[z];\n const members = [];\n for (let i = 0; i < inputs.length; i++) {\n let inside = false;\n try {\n if (membership === 'centroid') {\n const c = centroids[i];\n if (!c) continue;\n const [cx, cy] = c.geometry.coordinates;\n if (!bboxContainsPoint(zBox, cx, cy)) continue; // cheap reject\n if (++candidates > MAX_ZONAL_CANDIDATES) throw candidateOverflow(candidates);\n inside = booleanPointInPolygon(c, zone);\n } else {\n if (!bboxOverlaps(zBox, inputBoxes[i])) continue; // cheap reject\n if (++candidates > MAX_ZONAL_CANDIDATES) throw candidateOverflow(candidates);\n inside = booleanIntersects(inputs[i], zone);\n }\n } catch { inside = false; }\n if (inside) members.push(inputs[i]);\n }\n\n const values = field\n ? members.map((f) => num(f.properties ? f.properties[field] : null)).filter((v) => v !== null)\n : [];\n\n const result = {};\n for (const s of stats) {\n switch (s) {\n case 'count':\n result[`${prefix}count`] = members.length; break;\n case 'sum':\n result[`${prefix}sum`] = values.length ? round(values.reduce((a, b) => a + b, 0)) : 0; break;\n case 'mean':\n result[`${prefix}mean`] = values.length\n ? round(values.reduce((a, b) => a + b, 0) / values.length) : null; break;\n case 'min':\n result[`${prefix}min`] = values.length ? round(Math.min(...values)) : null; break;\n case 'max':\n result[`${prefix}max`] = values.length ? round(Math.max(...values)) : null; break;\n case 'area': {\n let total = 0;\n for (const m of members) { try { total += area(m); } catch { /* skip */ } }\n result[`${prefix}area_m2`] = Math.round(total); break;\n }\n default: break;\n }\n }\n\n zone.properties = { ...(zone.properties || {}), ...result };\n rows.push({ ...labelOf(zone), ...result });\n }\n\n return { geojson: { type: 'FeatureCollection', features: zones }, rows, warnings };\n}\n\n/** @private Raised only when genuinely plausible pairs exceed the budget. */\nfunction candidateOverflow(count) {\n return new Error(\n `More than ${count.toLocaleString()} features fall within the zones' bounding boxes, ` +\n `which is too much to summarise in the browser. Narrow the inputs using “Apply to” ` +\n `(current map view or selected features).`\n );\n}\n\n/** Round to 3 decimals to keep attribute tables readable. */\nfunction round(n) {\n return Math.round(n * 1000) / 1000;\n}\n\n/**\n * Pick a human label for a zone row — first of the usual naming fields,\n * else a generic index.\n */\nfunction labelOf(zone) {\n const p = zone.properties || {};\n for (const k of ['zone_name', 'name', 'ZONE_NAME', 'NAME', 'upn', 'UPN', 'id', 'ID']) {\n if (p[k] !== undefined && p[k] !== null && String(p[k]).trim() !== '') {\n return { zone: String(p[k]) };\n }\n }\n return { zone: '(unnamed)' };\n}\n","/**\n * Analysis panel controller — Stage 1 of the GIS Analytical Tools concept.\n *\n * openAnalysisModal(mapView)\n *\n * Drives two families of client-side analysis:\n * • Overlays — intersect / clip / difference / union between layers\n * • Zonal statistics — summarise features (or a numeric field) per zone\n *\n * Both run entirely in the browser via Turf.js, so they work offline. Results\n * are added to the map as a new styled layer in an \"Analysis\" group, from\n * where they can be exported with the existing GIS export tools.\n *\n * Same Bootstrap modal pattern as src/export-gis-modal.js and src/import-modal.js.\n */\n\nimport { Modal } from 'bootstrap';\nimport { runOverlay, OVERLAY_OPS } from './analysis/overlay.js';\nimport { zonalStatistics, listNumericFields, STATS, MEMBERSHIP_MODES } from './analysis/zonal.js';\nimport { showToast } from './toast.js';\n\nconst ANALYSIS_GROUP_ID = 9001;\nconst ANALYSIS_GROUP_TITLE = 'Analysis';\n\nconst els = {};\nlet modal = null;\nlet mapView = null;\nlet layerChoices = []; // [{ layer, title, count }]\nlet drawnScope = null; // { features: Set, label } when opened from a Circle/Area popup\nlet lastZonal = null; // { rows, params } — the most recent zonal run, for export\n\n// ---------------------------------------------------------------------------\n// Element wiring\n// ---------------------------------------------------------------------------\n\nfunction cacheEls() {\n if (els.root) return;\n els.root = document.getElementById('analysisModal');\n els.mode = Array.from(document.querySelectorAll('input[name=\"analysis-mode\"]'));\n els.overlayBox = document.getElementById('analysis-overlay-box');\n els.zonalBox = document.getElementById('analysis-zonal-box');\n els.op = document.getElementById('analysis-op');\n els.opHint = document.getElementById('analysis-op-hint');\n els.layerA = document.getElementById('analysis-layer-a');\n els.layerB = document.getElementById('analysis-layer-b');\n els.layerBWrap = document.getElementById('analysis-layer-b-wrap');\n els.zoneLayer = document.getElementById('analysis-zone-layer');\n els.inputLayer = document.getElementById('analysis-input-layer');\n els.membership = document.getElementById('analysis-membership');\n els.memberHint = document.getElementById('analysis-membership-hint');\n els.field = document.getElementById('analysis-field');\n els.statsBox = document.getElementById('analysis-stats-box');\n els.outName = document.getElementById('analysis-output-name');\n els.result = document.getElementById('analysis-result');\n els.btnRun = document.getElementById('analysis-run');\n els.btnExport = document.getElementById('analysis-export-table');\n els.scope = document.getElementById('analysis-scope');\n els.scopeHint = document.getElementById('analysis-scope-hint');\n\n if (!els.root.dataset.wired) {\n els.root.dataset.wired = '1';\n els.mode.forEach((r) => r.addEventListener('change', onModeChange));\n els.op.addEventListener('change', onOpChange);\n els.membership.addEventListener('change', onMembershipChange);\n els.inputLayer.addEventListener('change', refreshFieldOptions);\n els.scope.addEventListener('change', onScopeChange);\n els.btnRun.addEventListener('click', onRun);\n els.btnExport.addEventListener('click', onExportTable);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Population\n// ---------------------------------------------------------------------------\n\nfunction layerOptionsHtml(selectedIdx = null) {\n return layerChoices.map((c, i) =>\n ``\n ).join('');\n}\n\nfunction populateOps() {\n els.op.innerHTML = OVERLAY_OPS\n .map((o) => ``).join('');\n els.membership.innerHTML = MEMBERSHIP_MODES\n .map((m) => ``).join('');\n els.statsBox.innerHTML = STATS.map((s) => `\n
\n \n \n
`).join('');\n}\n\nfunction refreshFieldOptions() {\n const choice = layerChoices[Number(els.inputLayer.value)];\n const feats = choice ? choice.layer.getSource().getFeatures() : [];\n const fields = listNumericFields(feats);\n els.field.innerHTML = '' +\n fields.map((f) => ``).join('');\n els.field.disabled = fields.length === 0;\n}\n\n// ---------------------------------------------------------------------------\n// UI reactions\n// ---------------------------------------------------------------------------\n\nfunction currentMode() {\n return (els.mode.find((r) => r.checked) || {}).value || 'overlay';\n}\n\nfunction onModeChange() {\n // A previous zonal table doesn't belong to a new overlay run.\n lastZonal = null; syncExportButton();\n const overlay = currentMode() === 'overlay';\n els.overlayBox.classList.toggle('d-none', !overlay);\n els.zonalBox.classList.toggle('d-none', overlay);\n suggestName();\n}\n\nfunction onOpChange() {\n const op = OVERLAY_OPS.find((o) => o.key === els.op.value);\n els.opHint.textContent = op ? op.hint : '';\n // Union can run on a single layer; the others need a second one.\n const needsB = op ? op.needsB : true;\n els.layerBWrap.querySelector('label').textContent =\n needsB ? 'Layer B' : 'Layer B (optional)';\n suggestName();\n}\n\nfunction onMembershipChange() {\n const m = MEMBERSHIP_MODES.find((x) => x.key === els.membership.value);\n els.memberHint.textContent = m ? m.hint : '';\n}\n\nfunction suggestName() {\n if (els.outName.dataset.touched === '1') return;\n if (currentMode() === 'overlay') {\n const op = els.op.value || 'overlay';\n els.outName.value = `${op}_result`;\n } else {\n els.outName.value = 'zonal_stats';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Run\n// ---------------------------------------------------------------------------\n\n/**\n * Features of the chosen layer, narrowed by the \"Apply to\" scope.\n * 'view' uses the source's spatial index, so it stays fast on large layers.\n */\nfunction featuresOf(select) {\n const choice = layerChoices[Number(select.value)];\n if (!choice) return [];\n const source = choice.layer.getSource();\n const scope = els.scope ? els.scope.value : 'all';\n\n if (scope === 'view') {\n const map = mapView.getMap();\n const extent = map.getView().calculateExtent(map.getSize());\n return source.getFeaturesInExtent(extent);\n }\n if (scope === 'selection') {\n const selected = new Set(mapView.getSelectedFeatures());\n return source.getFeatures().filter((f) => selected.has(f));\n }\n if (scope === 'drawn' && drawnScope) {\n return source.getFeatures().filter((f) => drawnScope.features.has(f));\n }\n return source.getFeatures();\n}\n\nfunction onScopeChange() {\n const scope = els.scope.value;\n if (scope === 'drawn' && drawnScope) {\n els.scopeHint.textContent =\n `${drawnScope.features.size.toLocaleString()} feature(s) caught by the ${drawnScope.label}.`;\n } else if (scope === 'selection') {\n const n = mapView ? mapView.getSelectedFeatures().length : 0;\n els.scopeHint.textContent = n === 0\n ? 'Nothing selected yet — use the Digitise tool: shift-click features, or hold Ctrl/Cmd and drag a box.'\n : `${n.toLocaleString()} feature(s) currently selected.`;\n } else if (scope === 'view') {\n els.scopeHint.textContent = 'Only features inside the current map view are analysed — zoom to the area of interest first.';\n } else {\n els.scopeHint.textContent = 'Every feature in the chosen layer. Narrow this if a large layer is slow.';\n }\n}\n\nfunction ensureAnalysisGroup() {\n let group = mapView.getLayerGroupByTitle(ANALYSIS_GROUP_TITLE);\n if (!group) {\n mapView.addLayerGroup(ANALYSIS_GROUP_ID, ANALYSIS_GROUP_TITLE,\n 'Results of client-side spatial analysis');\n group = mapView.getLayerGroupByTitle(ANALYSIS_GROUP_TITLE);\n }\n return group;\n}\n\nfunction onRun() {\n els.result.innerHTML = '';\n els.btnRun.disabled = true;\n // Yield a frame so the button state paints before a heavy synchronous run.\n setTimeout(() => {\n try {\n const mode = currentMode();\n const name = (els.outName.value || '').trim() ||\n (mode === 'overlay' ? 'overlay_result' : 'zonal_stats');\n\n let geojson, summary;\n let zonalWarnings = [];\n if (mode === 'overlay') {\n lastZonal = null;\n const op = els.op.value;\n const a = featuresOf(els.layerA);\n const b = featuresOf(els.layerB);\n const needsB = (OVERLAY_OPS.find((o) => o.key === op) || {}).needsB;\n if (a.length === 0) throw new Error('Layer A has no features.');\n if (needsB && b.length === 0) throw new Error('This operation needs a second layer with features.');\n geojson = runOverlay(op, a, b);\n summary = `${geojson.features.length.toLocaleString()} feature(s) produced.`;\n } else {\n const zones = featuresOf(els.zoneLayer);\n const inputs = featuresOf(els.inputLayer);\n const stats = Array.from(els.statsBox.querySelectorAll('input:checked')).map((i) => i.value);\n const field = els.field.value || null;\n if (stats.length === 0) throw new Error('Select at least one statistic.');\n const needsField = stats.some((s) => (STATS.find((x) => x.key === s) || {}).needsField);\n if (needsField && !field) throw new Error('Sum / mean / min / max need a numeric field.');\n\n const res = zonalStatistics({\n zoneFeatures: zones, inputFeatures: inputs,\n stats, field, membership: els.membership.value,\n });\n geojson = res.geojson;\n zonalWarnings = res.warnings || [];\n summary = renderRows(res.rows);\n\n // Keep the table AND the settings that produced it, so the Excel export\n // is self-describing — a table of numbers with no record of how it was\n // produced can't be checked or reproduced later.\n const membershipLabel = (MEMBERSHIP_MODES.find((m) => m.key === els.membership.value) || {}).label;\n lastZonal = {\n rows: res.rows,\n warnings: res.warnings || [],\n params: {\n 'Analysis': 'Zonal statistics',\n 'Zone layer': labelOfSelect(els.zoneLayer),\n 'Features summarised': labelOfSelect(els.inputLayer),\n 'Applied to': scopeLabel(),\n 'Zones analysed': zones.length,\n 'Features analysed': inputs.length,\n 'Counted when': membershipLabel || els.membership.value,\n 'Statistics': stats.map((k) => (STATS.find((x) => x.key === k) || {}).label || k).join(', '),\n 'Numeric field': field || '(none)',\n 'Result layer': name,\n },\n };\n }\n\n if (!geojson.features.length) {\n els.result.innerHTML =\n `
No result features — the layers may not overlap.
`;\n return;\n }\n\n const group = ensureAnalysisGroup();\n mapView.addGeoJSONLayer(geojson, name, {\n strokeColor: '#d97706', strokeWidth: 2, fillColor: 'rgba(217,119,6,0.18)',\n }, group);\n\n syncExportButton();\n const warnHtml = zonalWarnings.length\n ? `
` +\n zonalWarnings.map((w) => `
${escapeHtml(w)}
`).join('') + `
`\n : '';\n els.result.innerHTML =\n `
Added layer “${escapeHtml(name)}”.
` +\n warnHtml +\n (typeof summary === 'string' && summary.startsWith('<') ? summary : `
${escapeHtml(summary)}
`);\n showToast(`Analysis complete — “${name}” added to the Analysis group.`, 'success', 4000);\n } catch (err) {\n console.error('[Analysis] run failed:', err);\n els.result.innerHTML =\n `
${escapeHtml(err.message || String(err))}
`;\n } finally {\n els.btnRun.disabled = false;\n }\n }, 20);\n}\n\n/** Human label of a layer + + `).join("")}function ne(){const e=M[Number(t.inputLayer.value)],n=e?e.layer.getSource().getFeatures():[],o=$e(n);t.field.innerHTML=''+o.map(s=>``).join(""),t.field.disabled=o.length===0}function V(){return(t.mode.find(e=>e.checked)||{}).value||"overlay"}function oe(){k=null,ie();const e=V()==="overlay";t.overlayBox.classList.toggle("d-none",!e),t.zonalBox.classList.toggle("d-none",e),ae()}function re(){const e=G.find(o=>o.key===t.op.value);t.opHint.textContent=e?e.hint:"";const n=e?e.needsB:!0;t.layerBWrap.querySelector("label").textContent=n?"Layer B":"Layer B (optional)",ae()}function se(){const e=Z.find(n=>n.key===t.membership.value);t.memberHint.textContent=e?e.hint:""}function ae(){if(t.outName.dataset.touched!=="1")if(V()==="overlay"){const e=t.op.value||"overlay";t.outName.value=`${e}_result`}else t.outName.value="zonal_stats"}function T(e){const n=M[Number(e.value)];if(!n)return[];const o=n.layer.getSource(),s=t.scope?t.scope.value:"all";if(s==="view"){const r=v.getMap(),a=r.getView().calculateExtent(r.getSize());return o.getFeaturesInExtent(a)}if(s==="selection"){const r=new Set(v.getSelectedFeatures());return o.getFeatures().filter(a=>r.has(a))}return s==="drawn"&&x?o.getFeatures().filter(r=>x.features.has(r)):o.getFeatures()}function le(){const e=t.scope.value;if(e==="drawn"&&x)t.scopeHint.textContent=`${x.features.size.toLocaleString()} feature(s) caught by the ${x.label}.`;else if(e==="selection"){const n=v?v.getSelectedFeatures().length:0;t.scopeHint.textContent=n===0?"Nothing selected yet — use the Digitise tool: shift-click features, or hold Ctrl/Cmd and drag a box.":`${n.toLocaleString()} feature(s) currently selected.`}else e==="view"?t.scopeHint.textContent="Only features inside the current map view are analysed — zoom to the area of interest first.":t.scopeHint.textContent="Every feature in the chosen layer. Narrow this if a large layer is slow."}function Oe(){let e=v.getLayerGroupByTitle(H);return e||(v.addLayerGroup(Ne,H,"Results of client-side spatial analysis"),e=v.getLayerGroupByTitle(H)),e}function Ce(){t.result.innerHTML="",t.btnRun.disabled=!0,setTimeout(()=>{try{const e=V(),n=(t.outName.value||"").trim()||(e==="overlay"?"overlay_result":"zonal_stats");let o,s,r=[];if(e==="overlay"){k=null;const l=t.op.value,i=T(t.layerA),u=T(t.layerB),f=(G.find(d=>d.key===l)||{}).needsB;if(i.length===0)throw new Error("Layer A has no features.");if(f&&u.length===0)throw new Error("This operation needs a second layer with features.");o=Se(l,i,u),s=`${o.features.length.toLocaleString()} feature(s) produced.`}else{const l=T(t.zoneLayer),i=T(t.inputLayer),u=Array.from(t.statsBox.querySelectorAll("input:checked")).map(p=>p.value),f=t.field.value||null;if(u.length===0)throw new Error("Select at least one statistic.");if(u.some(p=>(R.find(L=>L.key===p)||{}).needsField)&&!f)throw new Error("Sum / mean / min / max need a numeric field.");const y=ze({zoneFeatures:l,inputFeatures:i,stats:u,field:f,membership:t.membership.value});o=y.geojson,r=y.warnings||[],s=Fe(y.rows);const m=(Z.find(p=>p.key===t.membership.value)||{}).label;k={rows:y.rows,warnings:y.warnings||[],params:{Analysis:"Zonal statistics","Zone layer":K(t.zoneLayer),"Features summarised":K(t.inputLayer),"Applied to":Te(),"Zones analysed":l.length,"Features analysed":i.length,"Counted when":m||t.membership.value,Statistics:u.map(p=>(R.find(L=>L.key===p)||{}).label||p).join(", "),"Numeric field":f||"(none)","Result layer":n}}}if(!o.features.length){t.result.innerHTML='
No result features — the layers may not overlap.
';return}const a=Oe();v.addGeoJSONLayer(o,n,{strokeColor:"#d97706",strokeWidth:2,fillColor:"rgba(217,119,6,0.18)"},a),ie();const c=r.length?'
'+r.map(l=>`
${g(l)}
`).join("")+"
":"";t.result.innerHTML=`
Added layer “${g(n)}”.
`+c+(typeof s=="string"&&s.startsWith("<")?s:`
${g(s)}
`),F(`Analysis complete — “${n}” added to the Analysis group.`,"success",4e3)}catch(e){console.error("[Analysis] run failed:",e),t.result.innerHTML=`
${g(e.message||String(e))}
`}finally{t.btnRun.disabled=!1}},20)}function K(e){const n=e.options[e.selectedIndex];return n?n.textContent:"(none)"}function Te(){const e=t.scope.options[t.scope.selectedIndex];return e?e.textContent:"Whole layer"}async function je(){if(k){t.btnExport.disabled=!0;try{const{exportXlsx:e}=await de(async()=>{const{exportXlsx:i}=await import("./xlsx-D1ZOqanH.js");return{exportXlsx:i}},__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10])),{rows:n,params:o,warnings:s}=k,r=Object.keys(n[0]||{}).filter(i=>i!=="zone"),a=[{cells:["Zone",...r],bold:!0},...n.map(i=>["zone"in i?i.zone:"",...r.map(u=>i[u]===null||i[u]===void 0?"":i[u])])],c=[{cells:["LUPMIS2 — Zonal statistics"],bold:!0},[],{cells:["Parameter","Value"],bold:!0},...Object.entries(o).map(([i,u])=>[i,u]),["Exported",new Date().toLocaleString()],...s&&s.length?[[],{cells:["Notes"],bold:!0},...s.map(i=>["",i])]:[]],l=(o["Result layer"]||"zonal_stats").toString().trim().replace(/[^A-Za-z0-9_\-]+/g,"_")||"zonal_stats";e([{name:"Parameters",rows:c},{name:"Results",rows:a}],`${l}.xlsx`),F("Table exported to Excel.","success",3e3)}catch(e){console.error("[Analysis] table export failed:",e),F(`Could not export the table: ${e.message}`,"error",6e3)}finally{t.btnExport.disabled=!1}}}function ie(){t.btnExport&&t.btnExport.classList.toggle("d-none",!k)}function Fe(e){if(!e||!e.length)return"";const n=Object.keys(e[0]).filter(r=>r!=="zone"),o=["Zone",...n.map(r=>r.replace(/^zs_/,""))].map(r=>`${g(r)}`).join(""),s=e.map(r=>`${g(String(r.zone))}`+n.map(a=>`${r[a]===null||r[a]===void 0?"—":g(String(r[a]))}`).join("")+"").join("");return`
+ + ${o} + ${s} +
`}function Ue(e,n={}){if(v=e,_e(),x=n.scopeFeatures&&n.scopeFeatures.length?{features:new Set(n.scopeFeatures),label:n.scopeLabel||"drawn area"}:null,He(),M=v.listVectorLayers({nonEmptyOnly:!0}),M.length===0){F("No vector layers with features are loaded — nothing to analyse yet.","warning",5e3);return}Ie();const o=J(0),s=J(M.length>1?1:0);t.layerA.innerHTML=o,t.layerB.innerHTML=s,t.zoneLayer.innerHTML=o,t.inputLayer.innerHTML=s,t.outName.dataset.touched="",t.outName.addEventListener("input",()=>{t.outName.dataset.touched="1"},{once:!0}),t.result.innerHTML="",k=null,t.scope.value=x?"drawn":"all",ne(),re(),se(),le(),oe(),P=P||new pe(t.root),P.show()}function He(){const e=t.scope.querySelector('option[value="drawn"]');if(!x){e&&e.remove();return}const n=`From ${x.label} (${x.features.size.toLocaleString()} features)`;if(e)e.textContent=n;else{const o=document.createElement("option");o.value="drawn",o.textContent=n,t.scope.insertBefore(o,t.scope.firstChild)}}function g(e){return String(e).replace(/[&<>"']/g,n=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[n])}export{Ue as openAnalysisModal}; +//# sourceMappingURL=analysis-modal-Ceula2pX.js.map diff --git a/dist/assets/analysis-modal-Ceula2pX.js.map b/dist/assets/analysis-modal-Ceula2pX.js.map new file mode 100644 index 0000000..872bc24 --- /dev/null +++ b/dist/assets/analysis-modal-Ceula2pX.js.map @@ -0,0 +1 @@ +{"version":3,"mappings":"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","names":["MAX_OVERLAY_CANDIDATES","fmt","GeoJSON","toGeoJSONFeatures","features","mapProjection","out","f","gj","bboxOf","feature","geom","minX","minY","maxX","maxY","walk","c","sub","bboxOverlaps","a","b","bboxContainsPoint","box","x","y","assertPolygons","polygons","original","role","types","found","polygonsOnly","gjFeatures","t","mergeProps","propsA","propsB","prefixB","k","v","fc","intersectFeatures","aFeatures","bFeatures","opts","keepBAttributes","gjA","gjB","A","B","bBoxes","candidates","aBox","i","candidateOverflow","piece","intersect","featureCollection","clipFeatures","clipFeatures_","differenceFeatures","remainder","next","difference","unionFeatures","polyA","polyB","all","merged","union","count","OVERLAY_OPS","runOverlay","op","MAX_ZONAL_CANDIDATES","MEMBERSHIP_MODES","STATS","listNumericFields","counts","props","num","value","zonalStatistics","zoneFeatures","inputFeatures","stats","field","membership","prefix","zones","z","inputs","warnings","inputTypes","inputsHavePolygons","inputsAreLines","centroids","centroid","inputBoxes","zoneBoxes","rows","zone","zBox","members","inside","cx","cy","booleanPointInPolygon","booleanIntersects","values","result","s","round","total","m","area","labelOf","n","p","ANALYSIS_GROUP_ID","ANALYSIS_GROUP_TITLE","els","modal","mapView","layerChoices","drawnScope","lastZonal","cacheEls","r","onModeChange","onOpChange","onMembershipChange","refreshFieldOptions","onScopeChange","onRun","onExportTable","layerOptionsHtml","selectedIdx","escapeHtml","populateOps","o","choice","feats","fields","currentMode","syncExportButton","overlay","suggestName","needsB","featuresOf","select","source","scope","map","extent","selected","ensureAnalysisGroup","group","mode","name","geojson","summary","zonalWarnings","res","renderRows","membershipLabel","labelOfSelect","scopeLabel","warnHtml","w","showToast","err","opt","exportXlsx","__vitePreload","params","keys","resultRows","paramRows","base","head","h","body","openAnalysisModal","view","syncDrawnScopeOption","optsA","optsB","Modal","existing","label"],"ignoreList":[],"sources":["../../src/analysis/overlay.js","../../src/analysis/zonal.js","../../src/analysis-modal.js"],"sourcesContent":["/**\n * overlay.js — client-side vector overlay operations (Stage 1 of the\n * LUPMIS2 GIS Analytical Tools concept).\n *\n * Pure geometry helpers built on Turf.js. No DOM, no map — the caller hands\n * in OpenLayers features and gets a GeoJSON FeatureCollection back, ready for\n * MapView.addGeoJSONLayer().\n *\n * All Turf work happens in EPSG:4326 (Turf's area/length functions assume\n * WGS84), so features are reprojected out of the map's EPSG:3857 on the way\n * in and the result is handed back as WGS84 GeoJSON — the same convention\n * gis-export.js uses.\n *\n * intersectFeatures(a, b) → parts of A that fall inside B (attrs: A + B)\n * clipFeatures(a, b) → parts of A that fall inside B (attrs: A only)\n * differenceFeatures(a, b) → parts of A that fall OUTSIDE B (attrs: A)\n * unionFeatures(a[, b]) → dissolved outline of A (and B)\n */\n\nimport GeoJSON from 'ol/format/GeoJSON';\nimport intersect from '@turf/intersect';\nimport union from '@turf/union';\nimport difference from '@turf/difference';\nimport { featureCollection } from '@turf/helpers';\n\n/**\n * Guard: only *candidate* pairs (those whose bounding boxes actually overlap)\n * reach the expensive geometry test, so this ceiling is about real work, not\n * the raw n×m product. District-scale data (tens of thousands of parcels\n * against a few dozen zones) sits far below it.\n */\nexport const MAX_OVERLAY_CANDIDATES = 400000;\n\nconst fmt = new GeoJSON();\n\n/**\n * Convert OpenLayers features (map projection) to WGS84 GeoJSON features.\n * @param {Array} features\n * @param {string} [mapProjection='EPSG:3857']\n * @returns {Array} GeoJSON Features\n */\nexport function toGeoJSONFeatures(features, mapProjection = 'EPSG:3857') {\n const out = [];\n for (const f of features || []) {\n const geom = f.getGeometry && f.getGeometry();\n if (!geom) continue;\n const gj = fmt.writeFeatureObject(f, {\n dataProjection: 'EPSG:4326',\n featureProjection: mapProjection,\n });\n if (gj && gj.geometry) out.push(gj);\n }\n return out;\n}\n\n/**\n * Bounding box of a GeoJSON feature as [minX, minY, maxX, maxY].\n * Walks the coordinate arrays directly — no extra dependency, and fast enough\n * to run over tens of thousands of features.\n * @param {Object} feature GeoJSON Feature\n * @returns {number[]|null}\n */\nexport function bboxOf(feature) {\n const geom = feature && feature.geometry;\n if (!geom || !geom.coordinates) return null;\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n const walk = (c) => {\n if (typeof c[0] === 'number') {\n if (c[0] < minX) minX = c[0];\n if (c[0] > maxX) maxX = c[0];\n if (c[1] < minY) minY = c[1];\n if (c[1] > maxY) maxY = c[1];\n return;\n }\n for (const sub of c) walk(sub);\n };\n walk(geom.coordinates);\n return Number.isFinite(minX) ? [minX, minY, maxX, maxY] : null;\n}\n\n/** Do two bounding boxes overlap? Cheap pre-filter before real geometry work. */\nexport function bboxOverlaps(a, b) {\n if (!a || !b) return false;\n return !(a[2] < b[0] || b[2] < a[0] || a[3] < b[1] || b[3] < a[1]);\n}\n\n/** Is a point [x, y] inside a bounding box? */\nexport function bboxContainsPoint(box, x, y) {\n if (!box) return false;\n return x >= box[0] && x <= box[2] && y >= box[1] && y <= box[3];\n}\n\n/**\n * Reject a non-polygon input with a message that says what is actually wrong.\n *\n * Overlay operations are polygon-only, but the inputs are silently filtered by\n * polygonsOnly() — so a line layer (contours, roads) used to produce an empty\n * result and the misleading \"the layers may not overlap\". Failing loudly here\n * tells the user the real reason.\n *\n * @param {Array} polygons the surviving polygonal features\n * @param {Array} original everything that was passed in\n * @param {string} role 'Layer A' / 'Layer B'\n */\nfunction assertPolygons(polygons, original, role) {\n if (polygons.length > 0 || original.length === 0) return;\n const types = [...new Set(original.map((f) => f?.geometry?.type).filter(Boolean))];\n const found = types.length ? types.join(', ') : 'no geometry';\n throw new Error(\n `${role} contains no polygons (found ${found}). Overlay operations work on ` +\n `polygon layers only — line layers such as contours or roads cannot be used here. ` +\n `To summarise a line layer per zone, use Zonal statistics instead.`\n );\n}\n\n/** Keep only polygonal features — overlay is undefined for points/lines here. */\nfunction polygonsOnly(gjFeatures) {\n return (gjFeatures || []).filter((f) => {\n const t = f?.geometry?.type;\n return t === 'Polygon' || t === 'MultiPolygon';\n });\n}\n\n/** Merge two property bags, prefixing B's keys on collision so nothing is lost. */\nfunction mergeProps(propsA, propsB, prefixB = 'b_') {\n const out = { ...(propsA || {}) };\n for (const [k, v] of Object.entries(propsB || {})) {\n out[Object.prototype.hasOwnProperty.call(out, k) ? `${prefixB}${k}` : k] = v;\n }\n return out;\n}\n\nfunction fc(features) {\n return { type: 'FeatureCollection', features };\n}\n\n/**\n * Intersection: every piece of A that also lies inside B.\n * Result carries A's attributes merged with the matching B feature's.\n *\n * @param {Array} aFeatures OL features (layer A)\n * @param {Array} bFeatures OL features (layer B)\n * @param {Object} [opts]\n * @param {boolean} [opts.keepBAttributes=true] merge B's properties into the result\n * @returns {Object} GeoJSON FeatureCollection (WGS84)\n */\nexport function intersectFeatures(aFeatures, bFeatures, opts = {}) {\n const { keepBAttributes = true } = opts;\n const gjA = toGeoJSONFeatures(aFeatures);\n const gjB = toGeoJSONFeatures(bFeatures);\n const A = polygonsOnly(gjA);\n const B = polygonsOnly(gjB);\n assertPolygons(A, gjA, 'Layer A');\n assertPolygons(B, gjB, 'Layer B');\n\n // Pre-compute B's boxes once, then only test pairs that can possibly meet.\n const bBoxes = B.map(bboxOf);\n let candidates = 0;\n const out = [];\n for (const a of A) {\n const aBox = bboxOf(a);\n for (let i = 0; i < B.length; i++) {\n if (!bboxOverlaps(aBox, bBoxes[i])) continue;\n if (++candidates > MAX_OVERLAY_CANDIDATES) throw candidateOverflow(candidates);\n let piece = null;\n try { piece = intersect(featureCollection([a, B[i]])); } catch { piece = null; }\n if (!piece || !piece.geometry) continue;\n piece.properties = keepBAttributes\n ? mergeProps(a.properties, B[i].properties)\n : { ...(a.properties || {}) };\n out.push(piece);\n }\n }\n return fc(out);\n}\n\n/**\n * Clip: same geometry result as intersect, but the output keeps ONLY layer A's\n * attributes — the usual \"cut this layer to that boundary\" operation.\n */\nexport function clipFeatures(aFeatures, clipFeatures_, opts = {}) {\n return intersectFeatures(aFeatures, clipFeatures_, { ...opts, keepBAttributes: false });\n}\n\n/**\n * Difference: the parts of A that do NOT fall inside any feature of B.\n * Each A feature is reduced by every overlapping B feature in turn; features\n * fully covered by B drop out of the result.\n *\n * @returns {Object} GeoJSON FeatureCollection (WGS84)\n */\nexport function differenceFeatures(aFeatures, bFeatures) {\n const gjA = toGeoJSONFeatures(aFeatures);\n const gjB = toGeoJSONFeatures(bFeatures);\n const A = polygonsOnly(gjA);\n const B = polygonsOnly(gjB);\n assertPolygons(A, gjA, 'Layer A');\n assertPolygons(B, gjB, 'Layer B');\n\n const bBoxes = B.map(bboxOf);\n let candidates = 0;\n const out = [];\n for (const a of A) {\n const aBox = bboxOf(a);\n let remainder = a;\n for (let i = 0; i < B.length; i++) {\n if (!remainder) break;\n // Only subtract a B feature that can actually touch what's left.\n if (!bboxOverlaps(aBox, bBoxes[i])) continue;\n if (++candidates > MAX_OVERLAY_CANDIDATES) throw candidateOverflow(candidates);\n try {\n const next = difference(featureCollection([remainder, B[i]]));\n remainder = next && next.geometry ? next : null;\n } catch {\n // A malformed pair shouldn't kill the whole run — keep what we have.\n }\n }\n if (remainder && remainder.geometry) {\n remainder.properties = { ...(a.properties || {}) };\n out.push(remainder);\n }\n }\n return fc(out);\n}\n\n/**\n * Union / dissolve: merge everything into a single outline. Pass only `a` to\n * dissolve one layer, or both to merge two layers together.\n *\n * @returns {Object} GeoJSON FeatureCollection with 0 or 1 feature (WGS84)\n */\nexport function unionFeatures(aFeatures, bFeatures = []) {\n // Check each layer separately rather than the combined set: if only the\n // second layer had polygons, a combined check would pass and the first\n // layer's features would be dropped without the user being told.\n const gjA = toGeoJSONFeatures(aFeatures);\n const gjB = toGeoJSONFeatures(bFeatures);\n const polyA = polygonsOnly(gjA);\n const polyB = polygonsOnly(gjB);\n assertPolygons(polyA, gjA, 'Layer A');\n assertPolygons(polyB, gjB, 'Layer B'); // no-op when no second layer is given\n const all = [...polyA, ...polyB];\n if (all.length === 0) return fc([]);\n if (all.length === 1) return fc([all[0]]);\n\n let merged = null;\n try {\n merged = union(featureCollection(all));\n } catch {\n // Fall back to a pairwise fold if the bulk union chokes on bad geometry.\n merged = all[0];\n for (let i = 1; i < all.length; i++) {\n try {\n const next = union(featureCollection([merged, all[i]]));\n if (next && next.geometry) merged = next;\n } catch { /* skip the offending feature */ }\n }\n }\n if (!merged || !merged.geometry) return fc([]);\n merged.properties = { parts: all.length };\n return fc([merged]);\n}\n\n/** @private Raised only when genuinely overlapping work exceeds the budget. */\nfunction candidateOverflow(count) {\n return new Error(\n `This overlay has more than ${count.toLocaleString()} genuinely overlapping feature pairs, ` +\n `which is too much to run in the browser. Narrow the inputs using “Apply to” ` +\n `(current map view or selected features), or run it server-side.`\n );\n}\n\n/** Operation registry used by the Analysis panel. */\nexport const OVERLAY_OPS = [\n { key: 'intersect', label: 'Intersect', needsB: true,\n hint: 'Keep the parts of layer A that fall inside layer B (attributes from both).' },\n { key: 'clip', label: 'Clip', needsB: true,\n hint: 'Cut layer A to the boundary of layer B (keeps only A’s attributes).' },\n { key: 'difference', label: 'Difference', needsB: true,\n hint: 'Keep the parts of layer A that fall outside layer B.' },\n { key: 'union', label: 'Union / Dissolve', needsB: false,\n hint: 'Merge features into a single outline (optionally with a second layer).' },\n];\n\n/**\n * Run an overlay operation by key.\n * @param {string} op one of OVERLAY_OPS keys\n * @param {Array} aFeatures\n * @param {Array} bFeatures\n * @returns {Object} GeoJSON FeatureCollection (WGS84)\n */\nexport function runOverlay(op, aFeatures, bFeatures) {\n switch (op) {\n case 'intersect': return intersectFeatures(aFeatures, bFeatures);\n case 'clip': return clipFeatures(aFeatures, bFeatures);\n case 'difference': return differenceFeatures(aFeatures, bFeatures);\n case 'union': return unionFeatures(aFeatures, bFeatures);\n default: throw new Error(`Unknown overlay operation: ${op}`);\n }\n}\n","/**\n * zonal.js — vector-in-vector zonal statistics (Stage 1 of the LUPMIS2 GIS\n * Analytical Tools concept).\n *\n * Answers questions like \"how many parcels are in each collector zone?\" or\n * \"what is the mean plot size per zone?\" — the classic *summarize within*\n * operation. Raster × vector zonal statistics arrives with the raster pipeline\n * in Stage 2; this module is pure vector and runs fully offline.\n *\n * Membership rules:\n * 'centroid' — a feature belongs to the zone containing its centroid\n * (default; each feature counts exactly once)\n * 'intersects' — a feature belongs to every zone it touches\n * (features straddling a boundary are counted more than once)\n */\n\nimport booleanPointInPolygon from '@turf/boolean-point-in-polygon';\nimport booleanIntersects from '@turf/boolean-intersects';\nimport centroid from '@turf/centroid';\nimport area from '@turf/area';\nimport { toGeoJSONFeatures, bboxOf, bboxOverlaps, bboxContainsPoint } from './overlay.js';\n\n/**\n * Guard on *candidate* pairs only — a bounding-box pre-filter means a feature\n * is geometry-tested against a zone only when it could plausibly fall in it.\n * A district of 25,000 parcels across 40-odd zones produces roughly one\n * candidate per parcel, so real workloads sit far below this ceiling.\n */\nexport const MAX_ZONAL_CANDIDATES = 400000;\n\nexport const MEMBERSHIP_MODES = [\n { key: 'centroid', label: 'Centroid inside zone',\n hint: 'Each feature is counted once, in the zone containing its centre.' },\n { key: 'intersects', label: 'Any overlap with zone',\n hint: 'A feature is counted in every zone it touches (may double-count).' },\n];\n\n/** Statistics offered by the Analysis panel. */\nexport const STATS = [\n { key: 'count', label: 'Count', needsField: false },\n { key: 'sum', label: 'Sum', needsField: true },\n { key: 'mean', label: 'Mean', needsField: true },\n { key: 'min', label: 'Minimum', needsField: true },\n { key: 'max', label: 'Maximum', needsField: true },\n { key: 'area', label: 'Total area (m²)', needsField: false },\n];\n\n/**\n * List the numeric attribute names available across a set of OL features, so\n * the panel can offer only fields that can actually be summed or averaged.\n * @param {Array} features OL features\n * @returns {string[]}\n */\nexport function listNumericFields(features) {\n const counts = new Map();\n for (const f of features || []) {\n const props = f.getProperties ? f.getProperties() : {};\n for (const [k, v] of Object.entries(props)) {\n if (k === 'geometry' || k.startsWith('_')) continue;\n const n = typeof v === 'number' ? v : (v !== '' && v != null && !Number.isNaN(Number(v)) ? Number(v) : null);\n if (n === null) continue;\n counts.set(k, (counts.get(k) || 0) + 1);\n }\n }\n return Array.from(counts.keys()).sort();\n}\n\n/** Coerce a property to a finite number, or null when it isn't numeric. */\nfunction num(value) {\n if (value === null || value === undefined || value === '') return null;\n const n = typeof value === 'number' ? value : Number(value);\n return Number.isFinite(n) ? n : null;\n}\n\n/**\n * Compute zonal statistics.\n *\n * @param {Object} params\n * @param {Array} params.zoneFeatures OL features defining the zones (polygons)\n * @param {Array} params.inputFeatures OL features to summarise\n * @param {string[]} params.stats keys from STATS\n * @param {string} [params.field] numeric attribute (for sum/mean/min/max)\n * @param {string} [params.membership] 'centroid' | 'intersects'\n * @param {string} [params.prefix='zs_'] prefix for the new attribute names\n * @returns {{ geojson: Object, rows: Array }}\n * geojson — the zones with statistics attached (WGS84), ready to add as a layer\n * rows — a plain table of the same numbers, for display/export\n * warnings — figures that are valid but easy to misread (e.g. area over a\n * line layer); shown to the user rather than thrown\n */\nexport function zonalStatistics({\n zoneFeatures,\n inputFeatures,\n stats = ['count'],\n field = null,\n membership = 'centroid',\n prefix = 'zs_',\n}) {\n const zones = (toGeoJSONFeatures(zoneFeatures) || []).filter((z) => {\n const t = z?.geometry?.type;\n return t === 'Polygon' || t === 'MultiPolygon';\n });\n const inputs = toGeoJSONFeatures(inputFeatures) || [];\n\n if (zones.length === 0) throw new Error('The zone layer contains no polygons.');\n\n // Warnings are returned alongside the result rather than thrown: the run is\n // still valid and useful, but some figures would be silently meaningless\n // without an explanation.\n const warnings = [];\n const inputTypes = new Set(inputs.map((f) => f?.geometry?.type).filter(Boolean));\n const inputsHavePolygons = inputTypes.has('Polygon') || inputTypes.has('MultiPolygon');\n\n // Lines and points have no area, so \"Total area\" would report a silent 0.\n if (stats.includes('area') && !inputsHavePolygons && inputs.length > 0) {\n warnings.push(\n `Total area is 0 because the features being summarised are ` +\n `${[...inputTypes].join('/')} — only polygons have an area.`\n );\n }\n\n // Line inputs only. Averaging a field over line segments is not an\n // area-weighted average of the surface those lines describe — the classic\n // trap being \"mean elevation per parcel\" from contours, where segment count\n // has no relation to the area at each elevation. (Averaging over points is a\n // normal statistic, so it is not flagged.)\n const inputsAreLines = inputTypes.has('LineString') || inputTypes.has('MultiLineString');\n if (stats.includes('mean') && inputsAreLines && !inputsHavePolygons && field) {\n warnings.push(\n `Mean is the average of “${field}” across the individual line features in ` +\n `each zone, not an area-weighted average of the surface they describe. ` +\n `For contour data, Minimum and Maximum (the elevation range) are the ` +\n `figures you can rely on.`\n );\n }\n\n // Pre-compute once: each input's centroid (centroid mode) or bbox\n // (intersects mode), and every zone's bbox. The bbox comparisons below are\n // cheap arithmetic; only survivors reach the real geometry test.\n const centroids = membership === 'centroid'\n ? inputs.map((f) => { try { return centroid(f); } catch { return null; } })\n : null;\n const inputBoxes = membership === 'centroid' ? null : inputs.map(bboxOf);\n const zoneBoxes = zones.map(bboxOf);\n\n let candidates = 0;\n const rows = [];\n for (let z = 0; z < zones.length; z++) {\n const zone = zones[z];\n const zBox = zoneBoxes[z];\n const members = [];\n for (let i = 0; i < inputs.length; i++) {\n let inside = false;\n try {\n if (membership === 'centroid') {\n const c = centroids[i];\n if (!c) continue;\n const [cx, cy] = c.geometry.coordinates;\n if (!bboxContainsPoint(zBox, cx, cy)) continue; // cheap reject\n if (++candidates > MAX_ZONAL_CANDIDATES) throw candidateOverflow(candidates);\n inside = booleanPointInPolygon(c, zone);\n } else {\n if (!bboxOverlaps(zBox, inputBoxes[i])) continue; // cheap reject\n if (++candidates > MAX_ZONAL_CANDIDATES) throw candidateOverflow(candidates);\n inside = booleanIntersects(inputs[i], zone);\n }\n } catch { inside = false; }\n if (inside) members.push(inputs[i]);\n }\n\n const values = field\n ? members.map((f) => num(f.properties ? f.properties[field] : null)).filter((v) => v !== null)\n : [];\n\n const result = {};\n for (const s of stats) {\n switch (s) {\n case 'count':\n result[`${prefix}count`] = members.length; break;\n case 'sum':\n result[`${prefix}sum`] = values.length ? round(values.reduce((a, b) => a + b, 0)) : 0; break;\n case 'mean':\n result[`${prefix}mean`] = values.length\n ? round(values.reduce((a, b) => a + b, 0) / values.length) : null; break;\n case 'min':\n result[`${prefix}min`] = values.length ? round(Math.min(...values)) : null; break;\n case 'max':\n result[`${prefix}max`] = values.length ? round(Math.max(...values)) : null; break;\n case 'area': {\n let total = 0;\n for (const m of members) { try { total += area(m); } catch { /* skip */ } }\n result[`${prefix}area_m2`] = Math.round(total); break;\n }\n default: break;\n }\n }\n\n zone.properties = { ...(zone.properties || {}), ...result };\n rows.push({ ...labelOf(zone), ...result });\n }\n\n return { geojson: { type: 'FeatureCollection', features: zones }, rows, warnings };\n}\n\n/** @private Raised only when genuinely plausible pairs exceed the budget. */\nfunction candidateOverflow(count) {\n return new Error(\n `More than ${count.toLocaleString()} features fall within the zones' bounding boxes, ` +\n `which is too much to summarise in the browser. Narrow the inputs using “Apply to” ` +\n `(current map view or selected features).`\n );\n}\n\n/** Round to 3 decimals to keep attribute tables readable. */\nfunction round(n) {\n return Math.round(n * 1000) / 1000;\n}\n\n/**\n * Pick a human label for a zone row — first of the usual naming fields,\n * else a generic index.\n */\nfunction labelOf(zone) {\n const p = zone.properties || {};\n for (const k of ['zone_name', 'name', 'ZONE_NAME', 'NAME', 'upn', 'UPN', 'id', 'ID']) {\n if (p[k] !== undefined && p[k] !== null && String(p[k]).trim() !== '') {\n return { zone: String(p[k]) };\n }\n }\n return { zone: '(unnamed)' };\n}\n","/**\n * Analysis panel controller — Stage 1 of the GIS Analytical Tools concept.\n *\n * openAnalysisModal(mapView)\n *\n * Drives two families of client-side analysis:\n * • Overlays — intersect / clip / difference / union between layers\n * • Zonal statistics — summarise features (or a numeric field) per zone\n *\n * Both run entirely in the browser via Turf.js, so they work offline. Results\n * are added to the map as a new styled layer in an \"Analysis\" group, from\n * where they can be exported with the existing GIS export tools.\n *\n * Same Bootstrap modal pattern as src/export-gis-modal.js and src/import-modal.js.\n */\n\nimport { Modal } from 'bootstrap';\nimport { runOverlay, OVERLAY_OPS } from './analysis/overlay.js';\nimport { zonalStatistics, listNumericFields, STATS, MEMBERSHIP_MODES } from './analysis/zonal.js';\nimport { showToast } from './toast.js';\n\nconst ANALYSIS_GROUP_ID = 9001;\nconst ANALYSIS_GROUP_TITLE = 'Analysis';\n\nconst els = {};\nlet modal = null;\nlet mapView = null;\nlet layerChoices = []; // [{ layer, title, count }]\nlet drawnScope = null; // { features: Set, label } when opened from a Circle/Area popup\nlet lastZonal = null; // { rows, params } — the most recent zonal run, for export\n\n// ---------------------------------------------------------------------------\n// Element wiring\n// ---------------------------------------------------------------------------\n\nfunction cacheEls() {\n if (els.root) return;\n els.root = document.getElementById('analysisModal');\n els.mode = Array.from(document.querySelectorAll('input[name=\"analysis-mode\"]'));\n els.overlayBox = document.getElementById('analysis-overlay-box');\n els.zonalBox = document.getElementById('analysis-zonal-box');\n els.op = document.getElementById('analysis-op');\n els.opHint = document.getElementById('analysis-op-hint');\n els.layerA = document.getElementById('analysis-layer-a');\n els.layerB = document.getElementById('analysis-layer-b');\n els.layerBWrap = document.getElementById('analysis-layer-b-wrap');\n els.zoneLayer = document.getElementById('analysis-zone-layer');\n els.inputLayer = document.getElementById('analysis-input-layer');\n els.membership = document.getElementById('analysis-membership');\n els.memberHint = document.getElementById('analysis-membership-hint');\n els.field = document.getElementById('analysis-field');\n els.statsBox = document.getElementById('analysis-stats-box');\n els.outName = document.getElementById('analysis-output-name');\n els.result = document.getElementById('analysis-result');\n els.btnRun = document.getElementById('analysis-run');\n els.btnExport = document.getElementById('analysis-export-table');\n els.scope = document.getElementById('analysis-scope');\n els.scopeHint = document.getElementById('analysis-scope-hint');\n\n if (!els.root.dataset.wired) {\n els.root.dataset.wired = '1';\n els.mode.forEach((r) => r.addEventListener('change', onModeChange));\n els.op.addEventListener('change', onOpChange);\n els.membership.addEventListener('change', onMembershipChange);\n els.inputLayer.addEventListener('change', refreshFieldOptions);\n els.scope.addEventListener('change', onScopeChange);\n els.btnRun.addEventListener('click', onRun);\n els.btnExport.addEventListener('click', onExportTable);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Population\n// ---------------------------------------------------------------------------\n\nfunction layerOptionsHtml(selectedIdx = null) {\n return layerChoices.map((c, i) =>\n ``\n ).join('');\n}\n\nfunction populateOps() {\n els.op.innerHTML = OVERLAY_OPS\n .map((o) => ``).join('');\n els.membership.innerHTML = MEMBERSHIP_MODES\n .map((m) => ``).join('');\n els.statsBox.innerHTML = STATS.map((s) => `\n
\n \n \n
`).join('');\n}\n\nfunction refreshFieldOptions() {\n const choice = layerChoices[Number(els.inputLayer.value)];\n const feats = choice ? choice.layer.getSource().getFeatures() : [];\n const fields = listNumericFields(feats);\n els.field.innerHTML = '' +\n fields.map((f) => ``).join('');\n els.field.disabled = fields.length === 0;\n}\n\n// ---------------------------------------------------------------------------\n// UI reactions\n// ---------------------------------------------------------------------------\n\nfunction currentMode() {\n return (els.mode.find((r) => r.checked) || {}).value || 'overlay';\n}\n\nfunction onModeChange() {\n // A previous zonal table doesn't belong to a new overlay run.\n lastZonal = null; syncExportButton();\n const overlay = currentMode() === 'overlay';\n els.overlayBox.classList.toggle('d-none', !overlay);\n els.zonalBox.classList.toggle('d-none', overlay);\n suggestName();\n}\n\nfunction onOpChange() {\n const op = OVERLAY_OPS.find((o) => o.key === els.op.value);\n els.opHint.textContent = op ? op.hint : '';\n // Union can run on a single layer; the others need a second one.\n const needsB = op ? op.needsB : true;\n els.layerBWrap.querySelector('label').textContent =\n needsB ? 'Layer B' : 'Layer B (optional)';\n suggestName();\n}\n\nfunction onMembershipChange() {\n const m = MEMBERSHIP_MODES.find((x) => x.key === els.membership.value);\n els.memberHint.textContent = m ? m.hint : '';\n}\n\nfunction suggestName() {\n if (els.outName.dataset.touched === '1') return;\n if (currentMode() === 'overlay') {\n const op = els.op.value || 'overlay';\n els.outName.value = `${op}_result`;\n } else {\n els.outName.value = 'zonal_stats';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Run\n// ---------------------------------------------------------------------------\n\n/**\n * Features of the chosen layer, narrowed by the \"Apply to\" scope.\n * 'view' uses the source's spatial index, so it stays fast on large layers.\n */\nfunction featuresOf(select) {\n const choice = layerChoices[Number(select.value)];\n if (!choice) return [];\n const source = choice.layer.getSource();\n const scope = els.scope ? els.scope.value : 'all';\n\n if (scope === 'view') {\n const map = mapView.getMap();\n const extent = map.getView().calculateExtent(map.getSize());\n return source.getFeaturesInExtent(extent);\n }\n if (scope === 'selection') {\n const selected = new Set(mapView.getSelectedFeatures());\n return source.getFeatures().filter((f) => selected.has(f));\n }\n if (scope === 'drawn' && drawnScope) {\n return source.getFeatures().filter((f) => drawnScope.features.has(f));\n }\n return source.getFeatures();\n}\n\nfunction onScopeChange() {\n const scope = els.scope.value;\n if (scope === 'drawn' && drawnScope) {\n els.scopeHint.textContent =\n `${drawnScope.features.size.toLocaleString()} feature(s) caught by the ${drawnScope.label}.`;\n } else if (scope === 'selection') {\n const n = mapView ? mapView.getSelectedFeatures().length : 0;\n els.scopeHint.textContent = n === 0\n ? 'Nothing selected yet — use the Digitise tool: shift-click features, or hold Ctrl/Cmd and drag a box.'\n : `${n.toLocaleString()} feature(s) currently selected.`;\n } else if (scope === 'view') {\n els.scopeHint.textContent = 'Only features inside the current map view are analysed — zoom to the area of interest first.';\n } else {\n els.scopeHint.textContent = 'Every feature in the chosen layer. Narrow this if a large layer is slow.';\n }\n}\n\nfunction ensureAnalysisGroup() {\n let group = mapView.getLayerGroupByTitle(ANALYSIS_GROUP_TITLE);\n if (!group) {\n mapView.addLayerGroup(ANALYSIS_GROUP_ID, ANALYSIS_GROUP_TITLE,\n 'Results of client-side spatial analysis');\n group = mapView.getLayerGroupByTitle(ANALYSIS_GROUP_TITLE);\n }\n return group;\n}\n\nfunction onRun() {\n els.result.innerHTML = '';\n els.btnRun.disabled = true;\n // Yield a frame so the button state paints before a heavy synchronous run.\n setTimeout(() => {\n try {\n const mode = currentMode();\n const name = (els.outName.value || '').trim() ||\n (mode === 'overlay' ? 'overlay_result' : 'zonal_stats');\n\n let geojson, summary;\n let zonalWarnings = [];\n if (mode === 'overlay') {\n lastZonal = null;\n const op = els.op.value;\n const a = featuresOf(els.layerA);\n const b = featuresOf(els.layerB);\n const needsB = (OVERLAY_OPS.find((o) => o.key === op) || {}).needsB;\n if (a.length === 0) throw new Error('Layer A has no features.');\n if (needsB && b.length === 0) throw new Error('This operation needs a second layer with features.');\n geojson = runOverlay(op, a, b);\n summary = `${geojson.features.length.toLocaleString()} feature(s) produced.`;\n } else {\n const zones = featuresOf(els.zoneLayer);\n const inputs = featuresOf(els.inputLayer);\n const stats = Array.from(els.statsBox.querySelectorAll('input:checked')).map((i) => i.value);\n const field = els.field.value || null;\n if (stats.length === 0) throw new Error('Select at least one statistic.');\n const needsField = stats.some((s) => (STATS.find((x) => x.key === s) || {}).needsField);\n if (needsField && !field) throw new Error('Sum / mean / min / max need a numeric field.');\n\n const res = zonalStatistics({\n zoneFeatures: zones, inputFeatures: inputs,\n stats, field, membership: els.membership.value,\n });\n geojson = res.geojson;\n zonalWarnings = res.warnings || [];\n summary = renderRows(res.rows);\n\n // Keep the table AND the settings that produced it, so the Excel export\n // is self-describing — a table of numbers with no record of how it was\n // produced can't be checked or reproduced later.\n const membershipLabel = (MEMBERSHIP_MODES.find((m) => m.key === els.membership.value) || {}).label;\n lastZonal = {\n rows: res.rows,\n warnings: res.warnings || [],\n params: {\n 'Analysis': 'Zonal statistics',\n 'Zone layer': labelOfSelect(els.zoneLayer),\n 'Features summarised': labelOfSelect(els.inputLayer),\n 'Applied to': scopeLabel(),\n 'Zones analysed': zones.length,\n 'Features analysed': inputs.length,\n 'Counted when': membershipLabel || els.membership.value,\n 'Statistics': stats.map((k) => (STATS.find((x) => x.key === k) || {}).label || k).join(', '),\n 'Numeric field': field || '(none)',\n 'Result layer': name,\n },\n };\n }\n\n if (!geojson.features.length) {\n els.result.innerHTML =\n `
No result features — the layers may not overlap.
`;\n return;\n }\n\n const group = ensureAnalysisGroup();\n mapView.addGeoJSONLayer(geojson, name, {\n strokeColor: '#d97706', strokeWidth: 2, fillColor: 'rgba(217,119,6,0.18)',\n }, group);\n\n syncExportButton();\n const warnHtml = zonalWarnings.length\n ? `
` +\n zonalWarnings.map((w) => `
${escapeHtml(w)}
`).join('') + `
`\n : '';\n els.result.innerHTML =\n `
Added layer “${escapeHtml(name)}”.
` +\n warnHtml +\n (typeof summary === 'string' && summary.startsWith('<') ? summary : `
${escapeHtml(summary)}
`);\n showToast(`Analysis complete — “${name}” added to the Analysis group.`, 'success', 4000);\n } catch (err) {\n console.error('[Analysis] run failed:', err);\n els.result.innerHTML =\n `
${escapeHtml(err.message || String(err))}
`;\n } finally {\n els.btnRun.disabled = false;\n }\n }, 20);\n}\n\n/** Human label of a layer - - `}const i=` -
- ✏️ Edit Parcel - -
-
- ${a} -
- - -
-
- `;this.parcelEditElement.innerHTML=i,this.parcelEditPopup.setPosition(t),this.parcelEditElement.querySelector(".parcel-edit-close").addEventListener("click",()=>{this.hideParcelEditPopup()}),this.parcelEditElement.querySelector(".parcel-edit-cancel").addEventListener("click",()=>{this.hideParcelEditPopup()});const s=this.parcelEditElement.querySelector(".parcel-edit-form");s.addEventListener("submit",l=>{l.preventDefault();const c=new FormData(s),d={};for(const[u,p]of c.entries())d[u]=p;d._layerType="parcel";for(const[u,p]of Object.entries(d))this._parcelEditFeature.set(u,p);for(const u of this._parcelEditCallbacks)u(this._parcelEditFeature,d);this.hideParcelEditPopup()})}hideParcelEditPopup(){this.parcelEditPopup.setPosition(void 0),this._parcelEditFeature=null}onParcelEdit(e){this._parcelEditCallbacks.push(e)}createMergePopup(){this.mergePopupElement=document.createElement("div"),this.mergePopupElement.className="map-merge-popup",this.mergePopupElement.style.cssText=` - position: absolute; - background: var(--card, #fff); - color: var(--card-foreground, #1e1a4b); - border-radius: 10px; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - font-family: var(--font-body, 'Exo', sans-serif); - font-size: 13px; - min-width: 280px; - max-width: 360px; - z-index: 1002; - border: 2px solid #10b981; - overflow: hidden; - display: flex; - flex-direction: column; - `,this.mergePopup=new Ee({element:this.mergePopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.mergePopup)}showMergeIdentifierPopup(e,t,r,o){const a=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"],i=h=>{for(const g of a)if(h[g]!==void 0&&h[g]!==null&&String(h[g]).trim())return{field:g,value:String(h[g])};return{field:"id",value:"Unknown"}},s=i(t),l=i(r),c=` -
- 🔗 Merged Parcel — Choose Identifier - -
-
-

- Select which parcel's attributes the merged polygon should keep: -

- - -
- - -
-
- `;this.mergePopupElement.innerHTML=c,this.mergePopup.setPosition(o);const d=()=>{this.mergePopup.setPosition(void 0)};this.mergePopupElement.querySelector(".merge-popup-close").addEventListener("click",d),this.mergePopupElement.querySelector(".merge-popup-cancel").addEventListener("click",d),this.mergePopupElement.querySelector(".merge-popup-confirm").addEventListener("click",()=>{const g=this.mergePopupElement.querySelector('input[name="merge-choice"]:checked').value==="A"?t:r,m=["geometry"];for(const[y,w]of Object.entries(g))m.includes(y)||e.set(y,w);e.set("_layerType","parcel");for(const y of this._parcelEditCallbacks)y(e,g);d()});const u=this.mergePopupElement.querySelectorAll("label"),p=this.mergePopupElement.querySelectorAll('input[name="merge-choice"]'),f=()=>{u.forEach(h=>{const g=h.querySelector("input");h.style.borderColor=g.checked?g.value==="A"?"#0ea5e9":"#f59e0b":"var(--border, #1e1a4b1f)"})};p.forEach(h=>h.addEventListener("change",f)),f()}createDividePopup(){this.dividePopupElement=document.createElement("div"),this.dividePopupElement.className="map-divide-popup",this.dividePopupElement.style.cssText=` - position: absolute; - background: var(--card, #fff); - color: var(--card-foreground, #1e1a4b); - border-radius: 10px; - box-shadow: 0 4px 20px rgba(0,0,0,0.3); - font-family: var(--font-body, 'Exo', sans-serif); - font-size: 13px; - min-width: 260px; - max-width: 320px; - z-index: 1002; - border: 2px solid #8b5cf6; - overflow: hidden; - display: flex; - flex-direction: column; - `,this.dividePopup=new Ee({element:this.dividePopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.dividePopup)}showDividePopup(e,t,r){const o=` -
- Divide Polygon - -
-
-

- Enter the number of equal pieces: -

- -
- - -
-
- `;this.dividePopupElement.innerHTML=o,this.dividePopup.setPosition(r);const a=this.dividePopupElement.querySelector(".divide-input");a.focus(),a.select();const i=()=>{this.hideDividePopup(),this._polygonDivideInteraction.cancelDivide()};this.dividePopupElement.querySelector(".divide-popup-close").addEventListener("click",i),this.dividePopupElement.querySelector(".divide-popup-cancel").addEventListener("click",i),this.dividePopupElement.querySelector(".divide-popup-confirm").addEventListener("click",()=>{const s=parseInt(a.value,10);if(!s||s<2){a.style.borderColor="#ef4444";return}this.hideDividePopup(),this._polygonDivideInteraction.performDivide(s)}),a.addEventListener("keydown",s=>{s.key==="Enter"&&(s.preventDefault(),this.dividePopupElement.querySelector(".divide-popup-confirm").click())})}hideDividePopup(){this.dividePopup.setPosition(void 0)}createDrawnPolygonPopup(){this.drawnPolygonElement=document.createElement("div"),this.drawnPolygonElement.className="map-drawn-polygon-popup",this.drawnPolygonElement.style.cssText=` - position: absolute; - background: var(--card, #fff); - border-radius: var(--radius-xl, 0.75rem); - box-shadow: 0 4px 20px rgba(0,0,0,0.2); - font-family: var(--font-body, 'Exo', sans-serif); - font-size: 13px; - min-width: 280px; - max-width: 360px; - max-height: 420px; - z-index: 1002; - border: 2px solid var(--success, #006b3f); - overflow: hidden; - display: flex; - flex-direction: column; - `,this.drawnPolygonPopup=new Ee({element:this.drawnPolygonElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.drawnPolygonPopup),this._drawnPolygonCallbacks=[],this._drawnPolygonFeature=null}getParcelAttributeKeys(){const e=["geometry","_layerType"],t=[],r=o=>{t.length>0||o.getLayers().forEach(a=>{if(!(t.length>0)){if(a instanceof Me)r(a);else if(a instanceof D){const i=a.getSource();if(!i)return;for(const s of i.getFeatures()){if(s.get("_layerType")!=="parcel")continue;const l=s.getProperties();for(const c of Object.keys(l))e.includes(c)||t.push(c);return}}}})};return r(this.overlayGroup),t}showDrawnPolygonPopup(e,t){this._drawnPolygonFeature=e;const r=this.getParcelAttributeKeys();if(r.length===0){console.warn("[MapView] No parcel attributes found — cannot build form");return}let o="";for(const d of r){const u=this.escapeHtml(d);o+=` -
- - -
- `}const a=e.getGeometry(),i=dt(a,{projection:"EPSG:3857"}),l=` -
- 📐 Polygon Attributes - -
-
- Area: ${We(i)} -
-
- ${o} -
- - -
-
- `;this.drawnPolygonElement.innerHTML=l,this.drawnPolygonPopup.setPosition(t),this.drawnPolygonElement.querySelector(".drawn-polygon-close").addEventListener("click",()=>{this.hideDrawnPolygonPopup()}),this.drawnPolygonElement.querySelector(".drawn-polygon-cancel").addEventListener("click",()=>{this.hideDrawnPolygonPopup()});const c=this.drawnPolygonElement.querySelector(".drawn-polygon-form");c.addEventListener("submit",d=>{d.preventDefault();const u=new FormData(c),p={};for(const[f,h]of u.entries())p[f]=h;for(const[f,h]of Object.entries(p))this._drawnPolygonFeature.set(f,h);this._drawnPolygonFeature.set("_layerType","parcel");for(const f of this._drawnPolygonCallbacks)f(this._drawnPolygonFeature,p);this.hideDrawnPolygonPopup()})}hideDrawnPolygonPopup(){this.drawnPolygonPopup.setPosition(void 0),this._drawnPolygonFeature=null}onDrawnPolygonSave(e){this._drawnPolygonCallbacks.push(e)}onFeatureModified(e){this._featureModifiedCallbacks||(this._featureModifiedCallbacks=[]),this._featureModifiedCallbacks.push(e)}onDrawnPoint(e){this._drawnPointCallbacks||(this._drawnPointCallbacks=[]),this._drawnPointCallbacks.push(e)}onDblClick(e){return this.dblClickCallbacks.push(e),this.dblClickCallbacks.length===1&&this.map.on("dblclick",t=>{const[r,o]=xe(t.coordinate);let a=null;this.map.forEachFeatureAtPixel(t.pixel,i=>(a=i,!0)),a&&(t.preventDefault(),t.stopPropagation());for(const i of this.dblClickCallbacks)i(r,o,a,t);if(a)return!1}),()=>{const t=this.dblClickCallbacks.indexOf(e);t>-1&&this.dblClickCallbacks.splice(t,1)}}escapeHtml(e){if(!e)return"";const t=document.createElement("div");return t.textContent=e,t.innerHTML}createAddLocationPopup(){this.addLocationPopupElement=document.createElement("div"),this.addLocationPopupElement.className="map-add-location-popup",this.addLocationPopupElement.innerHTML=` -
- ➕ Add Location - -
-
-
- - -
-
- - -
-
- - -
-
- 📍 -
- -
- `,this.addLocationPopup=new Ee({element:this.addLocationPopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.addLocationPopup),this.addLocationCoords=null,this.addLocationPopupElement.querySelector(".add-location-popup-close").addEventListener("click",()=>{this.hideAddLocationPopup()}),this.addLocationCallbacks=[]}showAddLocationPopup(e){const[t,r]=xe(e);this.addLocationCoords={lon:t,lat:r};const o=this.addLocationPopupElement.querySelector("#map-location-coords");o.textContent=`${t.toFixed(6)}, ${r.toFixed(6)}`,this.addLocationPopupElement.querySelector("#map-add-location-form").reset(),this.addLocationPopup.setPosition(e)}hideAddLocationPopup(){this.addLocationPopup.setPosition(void 0),this.addLocationCoords=null,this._discardPendingLocationSketch()}_discardPendingLocationSketch(){const e=this._pendingLocationSketch;e&&(this._pendingLocationSketch=null,this._withoutUndoRecording(()=>{try{this.drawingsSource.removeFeature(e)}catch{}}))}onAddLocation(e){if(this.addLocationCallbacks.push(e),this.addLocationCallbacks.length===1){const t=this.addLocationPopupElement.querySelector("#map-add-location-form");t.addEventListener("submit",r=>{if(r.preventDefault(),!this.addLocationCoords)return;const o=new FormData(t),a={name:o.get("name"),category:o.get("category"),description:o.get("description"),lon:this.addLocationCoords.lon,lat:this.addLocationCoords.lat};this.addLocationCallbacks.forEach(i=>i(a)),this.hideAddLocationPopup()})}}createBaseLayers(e){const t=new ae({title:"Topographic",type:"base",zIndex:-100,visible:e==="topo",source:new Ce({url:"https://{a-c}.tile.opentopomap.org/{z}/{x}/{y}.png",attributions:"Map data: © OpenTopoMap",maxZoom:17,crossOrigin:"anonymous"})});t.set("basemapKey","topo");const r=new ae({title:"Carto Light",type:"base",zIndex:-100,visible:e==="carto-light",source:new Ce({url:sn("light_all"),attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});r.set("basemapKey","carto-light");const o=new ae({title:"Carto Dark",type:"base",zIndex:-100,visible:e==="carto-dark",source:new Ce({url:sn("dark_all"),attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});o.set("basemapKey","carto-dark");const a=new ae({title:"OSM Cycle map",type:"base",zIndex:-100,visible:!1,source:new Go({url:"https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=ae1339c46dd3446b9c491e7336d38760"})});a.set("basemapKey","cycle");const i=new ae({title:"Satellite",type:"base",zIndex:-100,visible:e==="satellite",source:new Ce({url:"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",attributions:"Tiles © Esri",maxZoom:19,crossOrigin:"anonymous"})});i.set("basemapKey","satellite");const s=new ae({title:"Google Sat",type:"base",zIndex:-100,visible:e==="googlesat",source:new Ce({url:"http://mt0.google.com/vt/lyrs=y&hl=en&x={x}&y={y}&z={z}&s=Ga",attributions:"Tiles © Google",maxZoom:19,crossOrigin:"anonymous"})});s.set("basemapKey","googlesat");const l=new ae({title:"OpenStreetMap",type:"base",zIndex:-100,visible:e==="osm",source:new Go});l.set("basemapKey","osm"),this._baseMapLayers=[r,o,a,i,s,l,t];const c=new Me({title:"Base Maps",layers:[r,o,i,a,s,l,t]});return c.set("displayInLayerSwitcher",!1),c}setBaseMap(e){if(!this._baseMapLayers)return!1;if(e==="none"){for(const r of this._baseMapLayers)r.setVisible(!1);return console.log("[MapView] Base map switched off (none)"),this.map.dispatchEvent({type:"basemapchange",key:"none"}),!0}let t=!1;for(const r of this._baseMapLayers){const o=r.get("basemapKey")===e;r.setVisible(o),o&&(t=!0)}return t&&(console.log("[MapView] Base map switched to:",e),this.map.dispatchEvent({type:"basemapchange",key:e})),t}_createBaseMapPicker(){const e=[{key:"topo",label:"Topographic",grad:"linear-gradient(135deg,#e8d5b7,#a67c52)"},{key:"osm",label:"OpenStreetMap",grad:"linear-gradient(135deg,#d4e6f1,#85c1e9)"},{key:"satellite",label:"Satellite",grad:"linear-gradient(135deg,#1b4332,#40916c)"},{key:"googlesat",label:"Google Sat",grad:"linear-gradient(135deg,#2a5d3d,#4a8c5a)"},{key:"carto-light",label:"Carto Light",grad:"linear-gradient(135deg,#f5f5f5,#d4d4d4)"},{key:"carto-dark",label:"Carto Dark",grad:"linear-gradient(135deg,#1a1a2e,#0f3460)"},{key:"none",label:"None",grad:"repeating-conic-gradient(#e5e7eb 0 25%, #fff 0 50%) 50% / 12px 12px"}],t=this.map.getTargetElement();if(!t)return;const r=document.createElement("button");r.type="button",r.className="ls-basemap-toggle",r.title="Switch base map",r.setAttribute("aria-label","Switch base map"),r.innerHTML='',t.appendChild(r);const o=document.createElement("div");o.className="ls-basemap-panel",o.innerHTML='
Base Map
'+e.map(i=>` - - `).join("")+"
",t.appendChild(o),this._basemapPanel=o,this._basemapToggle=r;const a=i=>{const s=i||this._baseMapLayers?.find(l=>l.getVisible())?.get("basemapKey");o.querySelectorAll('input[name="lupmis-basemap"]').forEach(l=>{l.checked=l.value===s})};a(),r.addEventListener("click",i=>{i.stopPropagation();const s=!o.classList.contains("open");o.classList.toggle("open",s),r.classList.toggle("active",s),s&&a()}),document.addEventListener("click",i=>{o.classList.contains("open")&&(o.contains(i.target)||r.contains(i.target)||(o.classList.remove("open"),r.classList.remove("active")))}),o.addEventListener("change",i=>{const s=i.target.closest('input[type=radio][name="lupmis-basemap"]');if(!s)return;const l=s.value;this.setBaseMap(l);try{localStorage.setItem("default-basemap",l)}catch{}o.classList.remove("open"),r.classList.remove("active")}),this.map.on("basemapchange",i=>a(i.key))}_initGpsRendering(){this._gpsPositionSource=new G,this._gpsTrailSource=new G,this._gpsTrailCoords=[],this._gpsTrailLayer=new D({source:this._gpsTrailSource,zIndex:940,style:new M({stroke:new P({color:"#ff6d00",width:4,lineCap:"round",lineJoin:"round"})}),properties:{title:"GPS Trail",displayInLayerSwitcher:!1}}),this._gpsPositionLayer=new D({source:this._gpsPositionSource,zIndex:950,style:e=>e.get("_kind")==="accuracy"?new M({fill:new C({color:"rgba(0,94,184,0.12)"}),stroke:new P({color:"rgba(0,94,184,0.35)",width:1})}):new M({image:new he({radius:7,fill:new C({color:"#005eb8"}),stroke:new P({color:"#ffffff",width:2.5})})}),properties:{title:"GPS Position",displayInLayerSwitcher:!1}}),this.map.addLayer(this._gpsTrailLayer),this.map.addLayer(this._gpsPositionLayer),this._gpsCallbacks={locate:[],record:[]},this._gpsRecording=!1}onLocateMe(e){this._gpsCallbacks.locate.push(e)}onToggleRecording(e){this._gpsCallbacks.record.push(e)}showCurrentPosition(e,t,r=null){if(e==null||t==null)return;const o=ee([e,t]);if(this._gpsPositionSource.clear(),r&&r>0){const i=r/Math.cos(t*Math.PI/180),s=new fe({geometry:new ft([this._circleRing(o,i)])});s.set("_kind","accuracy"),this._gpsPositionSource.addFeature(s)}const a=new fe({geometry:new yt(o)});a.set("_kind","dot"),this._gpsPositionSource.addFeature(a)}_circleRing(e,t,r=48){const o=[],i=t/1;for(let s=0;s<=r;s++){const l=s/r*2*Math.PI;o.push([e[0]+i*Math.cos(l),e[1]+i*Math.sin(l)])}return o}centerOn(e,t,r=16){this.map.getView().animate({center:ee([e,t]),zoom:r,duration:500})}startTrailRender(){this._gpsTrailCoords=[],this._gpsTrailSource.clear()}appendTrailPoint(e,t){e==null||t==null||(this._gpsTrailCoords.push(ee([e,t])),this._gpsTrailSource.clear(),this._gpsTrailCoords.length>=2&&this._gpsTrailSource.addFeature(new fe({geometry:new de(this._gpsTrailCoords)})))}clearTrailRender(){this._gpsTrailCoords=[],this._gpsTrailSource.clear()}setRecordingState(e){this._gpsRecording=!!e,this._recordBtn&&(this._recordBtn.classList.toggle("recording",this._gpsRecording),this._recordBtn.title=this._gpsRecording?"Stop trail recording":"Record GPS trail",this._recordBtn.innerHTML=this._gpsRecording?'':''),this._locateToggle&&this._locateToggle.classList.toggle("recording",this._gpsRecording)}_createLocationControl(){const e=this.map.getTargetElement();if(!e)return;const t=document.createElement("button");t.type="button",t.className="ls-locate-toggle",t.title="My Location",t.setAttribute("aria-label","My Location"),t.innerHTML='',e.appendChild(t);const r=document.createElement("div");r.className="ls-locate-actions",r.innerHTML='',e.appendChild(r),this._locateToggle=t,this._locateActions=r,this._locateMeBtn=r.querySelector(".ls-locate-me"),this._recordBtn=r.querySelector(".ls-locate-record");const o=()=>{r.classList.remove("open"),t.classList.remove("active")},a=()=>{r.classList.add("open"),t.classList.add("active")};t.addEventListener("click",i=>{i.stopPropagation(),r.classList.contains("open")?o():a()}),document.addEventListener("click",i=>{r.classList.contains("open")&&(r.contains(i.target)||t.contains(i.target)||this._gpsRecording||o())}),this._locateMeBtn.addEventListener("click",i=>{i.stopPropagation();for(const s of this._gpsCallbacks.locate)try{s()}catch(l){console.error(l)}this._gpsRecording||o()}),this._recordBtn.addEventListener("click",i=>{i.stopPropagation();const s=!this._gpsRecording;for(const l of this._gpsCallbacks.record)try{l(s)}catch(c){console.error(c)}})}getFeatureStyle(e){const t=e.get("category")||"default",r=this.getEmoji(t);if(e===this.selectedFeature)return[new M({image:new he({radius:22,fill:new C({color:"rgba(220, 38, 38, 0.25)"}),stroke:new P({color:"#dc2626",width:3})})}),new M({text:new Ke({text:r,font:"40px sans-serif",textBaseline:"bottom",textAlign:"center",offsetY:-5})})];const o=e.get("style");return o||(this.categoryStyles[t]?this.categoryStyles[t]:this.defaultStyle)}setCategoryStyles(e){for(const[t,r]of Object.entries(e)){r.emoji&&(this.categoryEmojis[t]?(this.categoryEmojis[t].emoji=r.emoji,r.label&&(this.categoryEmojis[t].label=r.label)):this.categoryEmojis[t]={emoji:r.emoji,label:r.label||t});const o=this.getEmoji(t),a=r.fontSize||28;this.categoryStyles[t]=this.createEmojiStyle(o,a)}this.markerSource.changed()}addMarker(e,t,r={}){console.log("[MapView] Adding marker at",e,t,"with properties:",r);const o=new fe({geometry:new yt(ee([e,t])),...r});return o.set("lon",e),o.set("lat",t),this.markerSource.addFeature(o),console.log("[MapView] Marker added, total features:",this.markerSource.getFeatures().length),o}addMarkers(e){console.log("[MapView] Adding",e.length,"markers");const t=e.map(r=>new fe({geometry:new yt(ee([r.longitude,r.latitude])),id:r.id,name:r.name,description:r.description,category:r.category,lon:r.longitude,lat:r.latitude}));return this._withoutUndoRecording(()=>this.markerSource.addFeatures(t)),console.log("[MapView] Markers added, total features:",this.markerSource.getFeatures().length),t}clearMarkers(){this._withoutUndoRecording(()=>this._silentClearSource(this.markerSource)),this.selectedFeature=null}removeMarker(e){if(typeof e=="object")this.markerSource.removeFeature(e);else{const t=this.markerSource.getFeatures().find(r=>r.get("id")===e);t&&this.markerSource.removeFeature(t)}}getMarkers(){return this.markerSource.getFeatures()}findMarker(e){return this.markerSource.getFeatures().find(t=>t.get("id")===e)}showMarkers(){this.markersLayer&&!this.markersLayer.getVisible()&&(this.markersLayer.setVisible(!0),console.log("[MapView] Markers layer switched on to show a location"))}selectMarker(e){return typeof e=="object"?this.selectedFeature=e:this.selectedFeature=this.findMarker(e),this.markerSource.changed(),this.selectedFeature}clearSelection(){this.selectedFeature=null,this.markerSource.changed()}zoomTo(e,t,r=15){this.map.getView().animate({center:ee([e,t]),zoom:r,duration:500})}fitToMarkers(e=50){const t=this.markerSource.getExtent();t&&t[0]!==1/0&&this.map.getView().fit(t,{padding:[e,e,e,e],duration:500,maxZoom:16})}getCenter(){const e=this.map.getView().getCenter();return xe(e)}getZoom(){return this.map.getView().getZoom()}setCenter(e,t){this.map.getView().setCenter(ee([e,t]))}setZoom(e){this.map.getView().setZoom(e)}onClick(e){return this.clickCallbacks.push(e),this.clickCallbacks.length===1&&(this._clickTimer=null,this.map.on("dblclick",()=>{this._clickTimer&&(clearTimeout(this._clickTimer),this._clickTimer=null)}),this.map.on("click",t=>{this._clickTimer&&(clearTimeout(this._clickTimer),this._clickTimer=null),!this._editBarActive&&this._selectInteraction&&this._selectInteraction.getFeatures().clear();let r=!1,o=!1,a=null;if(this.map.forEachFeatureAtPixel(t.pixel,l=>{l.get("_layerType")==="parcel"&&(o=!0),l.get("name")&&(a=l),r=!0}),r&&!o&&!a)return;const[i,s]=xe(t.coordinate);this._clickTimer=setTimeout(()=>{this._clickTimer=null;let l=null;this.map.forEachFeatureAtPixel(t.pixel,c=>{if(c.get("name"))return l=c,!0});for(const c of this.clickCallbacks)c(i,s,l,t)},300)})),()=>{const t=this.clickCallbacks.indexOf(e);t>-1&&this.clickCallbacks.splice(t,1)}}onPointerMove(e){this.map.on("pointermove",t=>{if(t.dragging)return;const[r,o]=xe(t.coordinate);let a=null;this.map.forEachFeatureAtPixel(t.pixel,i=>{if(i.get("name"))return a=i,!0}),this.map.getTargetElement().style.cursor=a?"pointer":"",e(r,o,a,t)})}enableHoverCursor(){}addGeoJSONLayer(e,t,r={},o=null){const{strokeColor:a="#3b82f6",strokeWidth:i=2,strokeDash:s=null,fillColor:l="rgba(59,130,246,0.1)",lineCasingColor:c=null,lineCasingWidth:d=null,pointRadius:u=5,pointFillColor:p=null,pointStrokeColor:f="#ffffff",pointStrokeWidth:h=1.5}=r,g=new G({features:new le().readFeatures(e,{featureProjection:"EPSG:3857"})}),m=new C({color:l}),y=new he({radius:u,fill:new C({color:p||a}),stroke:new P({color:f,width:h})}),w=new P({color:a,width:i,...s?{lineDash:s}:{}});let b;if(c){const x=d??i+2;b=[new M({stroke:new P({color:c,width:x})}),new M({stroke:w,fill:m,image:y})]}else b=new M({stroke:w,fill:m,image:y});const S=new D({title:t,source:g,style:b,...r.declutter?{declutter:r.declutter}:{}});S.set("typeTag",r.typeTag||"VEC");const L=x=>x?x.includes("Polygon")?"Vector / Polygon":x.includes("LineString")?"Vector / Line":x.includes("Point")?"Vector / Point":"Vector":null;if(r.typeDescription)S.set("typeDescription",r.typeDescription);else{const x=g.getFeatures(),I=L(x[0]?.getGeometry?.()?.getType?.());if(I)S.set("typeDescription",I);else{const k=U=>{const z=L(U.feature.getGeometry?.()?.getType?.());z&&S.set("typeDescription",z),g.un("addfeature",k)};g.on("addfeature",k)}}return(o||this.overlayGroup).getLayers().push(S),console.log("[MapView] GeoJSON layer added:",t,"→",g.getFeatures().length,"features",o?`(in group "${o.get("title")}")`:""),S}addLayerGroup(e,t,r=""){const o=new Me({title:t.trim()});return o.set("layerId",e),o.set("description",r),this.overlayGroup.getLayers().push(o),console.log("[MapView] Layer group added:",t.trim(),"(id:",e+")"),o}addWMSLayer(e,t,r,o,a={}){const i=this.getLayerGroupByTitle(e);if(!i)return console.warn(`[MapView] Layer group "${e}" not found — cannot add WMS layer "${t}"`),null;const s={LAYERS:o,TILED:!0,WIDTH:256,HEIGHT:256};a.style!==void 0&&(s.STYLES=a.style);const l=new jo({url:r,params:s,serverType:a.serverType!==void 0?a.serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1,attributions:a.attributions}),c=new ae({title:t,visible:a.visible!==void 0?a.visible:!0,source:l,opacity:a.opacity!==void 0?a.opacity:1,zIndex:a.zIndex});if(c.set("typeTag","WMS"),c.set("typeDescription","WMS / Raster"),l.on("tileloaderror",()=>{F(`WMS layer "${t}" — tile load error. Check the URL and layer name.`,"warning",5e3)}),i.getLayers().push(c),a.legendUrl)try{this._registerLegend(c,t,a.legendUrl)}catch(d){console.warn(`[MapView] Could not register legend for "${t}":`,d)}return a.onlineOnly&&this._attachOnlineOnlyHandler(c,t),console.log(`[MapView] WMS layer added: "${t}" → group "${e}"`),c}addXYZLayer(e,t,r,o={}){const a=this.getLayerGroupByTitle(e);if(!a)return console.warn(`[MapView] Layer group "${e}" not found — cannot add XYZ layer "${t}"`),null;const i=new Ce({url:r,crossOrigin:"anonymous",maxZoom:o.maxZoom!==void 0?o.maxZoom:19,attributions:o.attributions}),s=new ae({title:t,visible:o.visible!==void 0?o.visible:!0,source:i,opacity:o.opacity!==void 0?o.opacity:1,zIndex:o.zIndex});if(s.set("typeTag","XYZ"),s.set("typeDescription","XYZ / Tile"),i.on("tileloaderror",()=>{F(`XYZ layer "${t}" — tile load error. Check the URL.`,"warning",5e3)}),a.getLayers().push(s),o.legendUrl)try{this._registerLegend(s,t,o.legendUrl)}catch(l){console.warn(`[MapView] Could not register legend for "${t}":`,l)}return o.onlineOnly&&this._attachOnlineOnlyHandler(s,t),console.log(`[MapView] XYZ layer added: "${t}" → group "${e}"`),s}async addCOGLayer(e,t,r,o={}){const a=this.getLayerGroupByTitle(e);if(!a)return console.warn(`[MapView] Layer group "${e}" not found — cannot add COG layer "${t}"`),null;const i=await this._probeCOGUrl(r);if(i)return console.warn(`[MapView] COG pre-flight failed for "${t}":`,i),F(i,"error",8e3),null;let s,l;try{[{default:s},{default:l}]=await Promise.all([Te(()=>import("./geotiff-B9aROX49.js").then(d=>d.W),__vite__mapDeps([0,1,2,3])),Te(()=>import("./geotiff-B9aROX49.js").then(d=>d.G),__vite__mapDeps([0,1,2,3]))])}catch(d){return console.error("[MapView] Could not load COG support:",d),F("Raster (COG) support could not be loaded — check your connection.","error",6e3),null}let c=null;try{const d={sources:[{url:r,...o.min!==void 0?{min:o.min}:{},...o.max!==void 0?{max:o.max}:{}}],normalize:o.normalize!==void 0?o.normalize:!0,attributions:o.attributions},u=new l(d);c=new s({title:t,source:u,visible:o.visible!==void 0?o.visible:!0,opacity:o.opacity!==void 0?o.opacity:1,zIndex:o.zIndex,...o.style?{style:o.style}:{}}),c.set("typeTag","COG"),c.set("typeDescription","Cloud-Optimized GeoTIFF"),u.on("error",p=>{console.warn(`[MapView] COG "${t}" source error:`,p),F(`Raster "${t}" could not be loaded. Check that the file is a valid COG and is CORS-enabled.`,"warning",6e3)}),a.getLayers().push(c)}catch(d){return console.error(`[MapView] Failed to add COG layer "${t}":`,d),F(`Could not add raster "${t}": ${d.message}`,"error",6e3),null}return o.onlineOnly&&this._attachOnlineOnlyHandler(c,t),console.log(`[MapView] COG layer added: "${t}" → group "${e}"`),c}async _probeCOGUrl(e){let t;try{t=await fetch(e,{headers:{Range:"bytes=0-0"}})}catch{return"Could not reach that file. It may be offline, or the server may not allow cross-origin (CORS) requests from this app."}if(t.status===404)return"No file at that URL (404). Check the address.";if(!t.ok&&t.status!==206)return`The server refused the request (HTTP ${t.status}).`;const r=(t.headers.get("content-type")||"").toLowerCase();return r.includes("text/html")?"That URL is a web page, not a GeoTIFF. Paste the direct link to the .tif file itself — a documentation or viewer page will not work.":r.startsWith("text/")||r.includes("json")?`That URL returns ${r.split(";")[0]}rather than image data. Paste the direct link to the .tif file.`:(t.status===200&&F("This server does not support byte-range requests, so the whole raster will be downloaded. It may be slow for a large file.","warning",7e3),null)}getSelectedFeatures(){return this._selectInteraction?this._selectInteraction.getFeatures().getArray().slice():[]}listVectorLayers(e={}){const{visibleOnly:t=!1,nonEmptyOnly:r=!0}=e,o=[],a=i=>{if(!i)return;if(typeof i.getLayers=="function"){i.getLayers().forEach(a);return}if(!(i instanceof D)||i===this._vertexOverlayLayer||i===this._gpsTrailLayer||i===this._gpsPositionLayer||i.get("displayInLayerSwitcher")===!1||t&&i.getVisible&&!i.getVisible())return;const s=i.getSource&&i.getSource();if(!s||typeof s.getFeatures!="function")return;const l=s.getFeatures().length;r&&l===0||o.push({layer:i,title:i.get("title")||"(untitled layer)",count:l})};return this.map.getLayers().forEach(a),o}_createAddLayerDialog(){this._addLayerDialog=document.createElement("div"),this._addLayerDialog.className="map-add-layer-dialog",this._addLayerDialog.style.cssText=` - display:none;position:absolute;top:0;left:0;right:0;bottom:0; - z-index:1100;background:rgba(0,0,0,0.4); - align-items:center;justify-content:center; - `;const e=document.createElement("div");e.style.cssText=` - background:var(--card, #fff);color:var(--card-foreground, #1e1a4b); - border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,0.35); - font-family:var(--font-body, 'Exo', sans-serif);font-size:13px; - width:340px;max-width:90vw;border:2px solid #10b981;overflow:hidden; - `,e.innerHTML=` -
- Add External Layer - -
-
-
- -
- - - - -
-
-
-
- - -
-
- - -
- WMS LAYERS parameter (e.g. workspace:layer) -
-
-
- - -
-
- - -
-
- `,this._addLayerDialog.appendChild(e),this.map.getTargetElement().appendChild(this._addLayerDialog);const t=e.querySelector(".add-layer-name-row"),r=e.querySelector(".add-layer-name-hint"),o=e.querySelector(".add-layer-url"),a=e.querySelector(".add-layer-type-hint");e.querySelectorAll('input[name="add-layer-type"]').forEach(s=>{s.addEventListener("change",()=>{const l=s.value;a.textContent=l==="cog"?"Cloud-Optimized GeoTIFF — the server must allow CORS and byte-range requests.":"",l==="cog"?(t.style.display="none",o.placeholder="https://example.com/data/elevation_cog.tif"):l==="xyz"?(t.style.display="none",o.placeholder="https://example.com/tiles/{z}/{x}/{y}.png"):(t.style.display="",o.placeholder=l==="wms"?"https://example.com/wms":"https://example.com/wfs",r.textContent=l==="wms"?"WMS LAYERS parameter (e.g. workspace:layer)":"WFS typename (e.g. workspace:layer)")})});const i=()=>this._hideAddLayerDialog();e.querySelector(".add-layer-close").addEventListener("click",i),e.querySelector(".add-layer-cancel").addEventListener("click",i),this._addLayerDialog.addEventListener("click",s=>{s.target===this._addLayerDialog&&i()}),e.querySelector(".add-layer-confirm").addEventListener("click",()=>{const s=e.querySelector('input[name="add-layer-type"]:checked').value,l=e.querySelector(".add-layer-url").value.trim(),c=e.querySelector(".add-layer-name").value.trim(),d=e.querySelector(".add-layer-title").value.trim();if(!l){e.querySelector(".add-layer-url").style.borderColor="#ef4444";return}if((s==="wms"||s==="wfs")&&!c){e.querySelector(".add-layer-name").style.borderColor="#ef4444";return}if(!d){e.querySelector(".add-layer-title").style.borderColor="#ef4444";return}this._addExternalLayer(s,l,c,d),this._hideAddLayerDialog()}),e.addEventListener("keydown",s=>{s.key==="Enter"&&(s.preventDefault(),e.querySelector(".add-layer-confirm").click()),s.key==="Escape"&&(s.preventDefault(),i())})}showAddLayerDialog(){const e=this._addLayerDialog;e.querySelector(".add-layer-url").value="",e.querySelector(".add-layer-name").value="",e.querySelector(".add-layer-title").value="",e.querySelectorAll('input[name="add-layer-type"]')[0].checked=!0,e.querySelector(".add-layer-name-row").style.display="",e.querySelector(".add-layer-url").placeholder="https://example.com/wms",e.querySelector(".add-layer-name-hint").textContent="WMS LAYERS parameter (e.g. workspace:layer)";const t=e.querySelector(".add-layer-type-hint");t&&(t.textContent=""),e.querySelectorAll('input[type="text"]').forEach(r=>{r.style.borderColor="var(--border, #1e1a4b1f)"}),e.style.display="flex",e.querySelector(".add-layer-url").focus()}_hideAddLayerDialog(){this._addLayerDialog.style.display="none"}_addExternalLayer(e,t,r,o){const a=this._externalSourceGroup;if(!a){F('Layer group "External Source" not found.',"error",4e3);return}let i;switch(e){case"wms":{const s=new jo({url:t,params:{LAYERS:r,TILED:!0,WIDTH:256,HEIGHT:256},serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1});i=new ae({title:o,visible:!0,source:s}),s.on("tileloaderror",()=>{F(`WMS "${o}" — tile load error. Check URL and layer name.`,"warning",5e3)});break}case"wfs":{const s=`${t}${t.includes("?")?"&":"?"}service=WFS&version=1.1.0&request=GetFeature&typename=${encodeURIComponent(r)}&outputFormat=application/json&srsname=EPSG:3857`,l=new G({url:s,format:new le});l.on("featuresloaderror",()=>{F(`WFS "${o}" — load error. Check URL and layer name.`,"warning",5e3)}),i=new D({title:o,visible:!0,source:l,style:new M({stroke:new P({color:"#e11d48",width:2}),fill:new C({color:"rgba(225,29,72,0.15)"})})});break}case"xyz":i=new ae({title:o,visible:!0,source:new Ce({url:t,crossOrigin:"anonymous"})}),i.getSource().on("tileloaderror",()=>{F(`XYZ "${o}" — tile load error. Check the URL template.`,"warning",5e3)});break;case"cog":{const s=a.get("title")||"External Source";this.addCOGLayer(s,o,t).then(l=>{l&&(l.set("removable",!0),F(`Layer "${o}" added to External Source.`,"success",3e3),console.log(`[MapView] External COG layer added: "${o}"`))});return}default:F(`Unknown layer type: ${e}`,"error",4e3);return}i.set("typeTag",e.toUpperCase()),i.set("typeDescription",{wms:"WMS / Raster",wfs:"WFS / Vector",xyz:"XYZ / Tile"}[e]||e.toUpperCase()),i.set("removable",!0),a.getLayers().push(i),F(`Layer "${o}" added to External Source.`,"success",3e3),console.log(`[MapView] External ${e.toUpperCase()} layer added: "${o}"`)}_decorateLayerListItem(e,t){const r=e.get("typeTag");if(r){const c=t.querySelector(":scope > .li-content > label > span");if(c&&!c.querySelector(":scope > .ls-type-tag")){const d=document.createElement("span");d.className=`ls-type-tag ls-type-tag-${String(r).toLowerCase()}`,d.textContent=String(r),d.title=`${r} layer`,c.appendChild(d)}}const o=t.querySelector(":scope > .ol-layerswitcher-buttons");if(o){const c=o.querySelector(":scope > .expend-layers, :scope > .collapse-layers");c&&!c.querySelector(":scope > svg.ls-chevron-svg")&&(c.innerHTML='')}const a=t.querySelector(":scope > .li-content"),i=()=>{if(!a)return;const c=e.get("typeDescription");let d=a.querySelector(":scope > .ls-layer-subtitle");if(!c){d&&d.remove();return}if(!d){d=document.createElement("div"),d.className="ls-layer-subtitle";const u=a.querySelector(":scope > label");u&&u.nextSibling?a.insertBefore(d,u.nextSibling):a.appendChild(d)}d.textContent=c};if(i(),e._lsSubtitleHooked||(e._lsSubtitleHooked=!0,e.on("change:typeDescription",()=>{i()})),e.get("removable")===!0&&o&&!o.querySelector(":scope > .ls-remove-btn")){const c=document.createElement("button");c.type="button",c.className="ls-remove-btn",c.title="Remove this layer",c.setAttribute("aria-label","Remove layer"),c.innerHTML='',c.addEventListener("click",d=>{d.stopPropagation(),this._removeLayer(e)}),o.appendChild(c)}const s=e.get("_externalImportId");if(s!=null){const c=t.querySelector(":scope > .li-content > label > span");let d=c?c.querySelector(":scope > .ls-import-chip"):null;const u=e.get("_externalImportStatus")||"mapped",p=e.getSource()?.getFeatures().length??0,f=e.get("_externalImportErrorCount")??0,h=(()=>{switch(u){case"mapped":return{text:`Upload ${p}`,cls:"ls-import-chip-mapped",title:"Upload this dataset to the database",clickable:!0};case"uploading":return{text:"…",cls:"ls-import-chip-uploading",title:"Uploading…",clickable:!1};case"submitted":return{text:"✓ submitted",cls:"ls-import-chip-submitted",title:"Uploaded — awaiting supervisor review",clickable:!1};case"migrated":return{text:"✓ live",cls:"ls-import-chip-migrated",title:"Approved by supervisor and live on the server",clickable:!1};case"failed":return{text:`${f} errors — fix?`,cls:"ls-import-chip-failed",title:"Some rows failed; click to review",clickable:!0};case"other":case null:case void 0:default:return null}})();if(!h)d&&d.remove();else if(c){d||(d=document.createElement("span"),d.className="ls-import-chip",c.appendChild(d)),d.className=`ls-import-chip ${h.cls}`,d.textContent=h.text,d.title=h.title,d.style.cursor=h.clickable?"pointer":"default",d.style.opacity=h.clickable?"1":"0.85";const g=d.cloneNode(!0);d.replaceWith(g),d=g,h.clickable&&d.addEventListener("click",m=>{m.preventDefault(),m.stopPropagation(),window.dispatchEvent(new CustomEvent("lupmis:import-chip-click",{detail:{importId:s,status:u,layer:e}}))})}}if((e.get("title")||"").toLowerCase().includes("external")&&(this._externalSourceGroup=e,o&&!o.querySelector(".ol-add-layer"))){const c=document.createElement("span");c.className="ol-add-layer",c.title="Add external layer",c.textContent="+",c.style.cssText=` - display:inline-flex !important;align-items:center;justify-content:center; - width:22px !important;height:22px !important;border-radius:50%; - background:#41b6a6 !important;color:#fff !important; - font-size:15px !important;font-weight:700; - cursor:pointer;line-height:1 !important; - margin:0 4px 0 0;vertical-align:middle; - transition:background 0.2s;box-sizing:border-box;border:none; - `,c.addEventListener("mouseenter",()=>{c.style.background="#329686"}),c.addEventListener("mouseleave",()=>{c.style.background="#41b6a6"}),c.addEventListener("click",d=>{d.stopPropagation(),this.showAddLayerDialog()}),o.prepend(c)}}_removeLayer(e){const t=e.get("title")||"this layer";if(!confirm(`Remove "${t}" from the map? - -This only affects the current session — built-in layers cannot be removed.`))return;const r=a=>{const i=a.getLayers();if(i.getArray().includes(e))return i.remove(e),!0;let s=!1;return i.forEach(l=>{!s&&l.getLayers&&(s=r(l))}),s};r(this.overlayGroup)?(console.log(`[MapView] Removed layer "${t}"`),F(`Removed "${t}" from the map.`,"info",3e3)):console.warn(`[MapView] Could not find layer "${t}" in any group`)}_refreshLayerSwitcherChrome(e){const t=e.element?.querySelector(".panel-container"),r=e.element?.querySelector("ul.panel");if(!t||!r)return;let o=t.querySelector(":scope > .ls-active-badge");o||(o=document.createElement("div"),o.className="ls-active-badge",o.innerHTML=` - Layers - 0 active - `,t.insertBefore(o,r));let a=t.querySelector(":scope > .ls-footer-row");a||(a=document.createElement("div"),a.className="ls-footer-row",a.innerHTML=` - — layers total - - `,t.appendChild(a),a.querySelector(".ls-footer-btn").addEventListener("click",s=>{s.stopPropagation(),this._resetAllOverlays()}));const i=this._countLayers();o.querySelector(".ls-active-badge-count").textContent=`${i.activeOverlays} active`,a.querySelector(".ls-footer-note").textContent=`${i.totalOverlays} overlay${i.totalOverlays===1?"":"s"}`}_countLayers(){let e=0,t=0;const r=new Set(["__vertex_highlight__"]),o=a=>{a.getLayers().forEach(i=>{i.get("displayInLayerSwitcher")!==!1&&(r.has(i.get("title"))||(i.getLayers?o(i):(e++,i.getVisible()&&t++)))})};return this.overlayGroup&&o(this.overlayGroup),{totalOverlays:e,activeOverlays:t}}_resetAllOverlays(){const e=new Set(["__vertex_highlight__"]),t=r=>{r.getLayers().forEach(o=>{o.get("displayInLayerSwitcher")!==!1&&(e.has(o.get("title"))||(o.getLayers?t(o):o.setVisible(!1)))})};this.overlayGroup&&t(this.overlayGroup),console.log("[MapView] Reset overlays — all hidden")}_wireLayerSwitcherVisibilityHooks(e){const t=()=>this._refreshLayerSwitcherChrome(e),r=a=>{a._lsVisHooked||(a._lsVisHooked=!0,a.on("change:visible",t))},o=a=>{a.getLayers().forEach(i=>{i.getLayers?(o(i),a._lsAddHooked||(a._lsAddHooked=!0,a.getLayers().on("add",s=>{const l=s.element;l.getLayers?o(l):r(l),t()}))):r(i)})};this.overlayGroup&&o(this.overlayGroup)}_attachOnlineOnlyHandler(e,t){e.set("onlineOnly",!0),e.on("change:visible",()=>{e.getVisible()&&!navigator.onLine&&F(`"${t}" requires an internet connection. Connect to view this layer.`,"info",5e3)})}_createLegendPanel(){this._legendPanel=document.createElement("div"),this._legendPanel.className="map-legend-panel",this._legendPanel.style.cssText=` - position:absolute;right:10px;bottom:40px;z-index:900; - display:none;flex-direction:column;gap:6px; - background:var(--card, #fff);color:var(--card-foreground, #1e1a4b); - border:1px solid var(--border, #1e1a4b1f);border-radius:8px; - box-shadow:0 4px 12px rgba(0,0,0,0.15); - font-family:var(--font-body, 'Exo', sans-serif);font-size:11px; - max-width:220px;max-height:60%;overflow-y:auto; - padding:8px 10px; - `,this.map.getTargetElement().appendChild(this._legendPanel),this._legendEntries=new Bo}_registerLegend(e,t,r){if(!this._legendPanel)return;const o=document.createElement("div");o.className="map-legend-entry",o.style.cssText="border-bottom:1px solid var(--border, #1e1a4b1f);padding-bottom:6px;",o.innerHTML=` -
- ${this._escapeHtml(t)} -
- ${this._escapeHtml(t)} legend - `,this._legendEntries.set(e,o);const a=()=>{try{this._updateLegendPanel()}catch(i){console.warn("[MapView] legend panel update failed:",i)}};e.on("change:visible",a),a()}_updateLegendPanel(){if(!this._legendPanel)return;const e=[];for(const[t,r]of this._legendEntries)t.getVisible()&&e.push(r);this._legendEntries.forEach(t=>{t.style.borderBottom="1px solid var(--border, #1e1a4b1f)",t.style.paddingBottom="6px"}),e.length>0&&(e[e.length-1].style.borderBottom="none",e[e.length-1].style.paddingBottom="0"),this._legendPanel.replaceChildren(...e),this._legendPanel.style.display=e.length>0?"flex":"none"}_escapeHtml(e){return String(e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}getLayerGroup(e){let t=null;return this.overlayGroup.getLayers().forEach(r=>{r.get("layerId")===e&&(t=r)}),t}getLayerGroupByTitle(e){let t=null;return this.overlayGroup.getLayers().forEach(r=>{r.get("title")===e&&(t=r)}),t}getOverlayGroup(){return this.overlayGroup}getMap(){return this.map}getCurrentViewExtent(){const e=this.map.getView(),t=this.map.getSize();return t?e.calculateExtent(t):null}getDistrictBoundaryExtent(){let e=null;const t=r=>{r.getLayers().forEach(o=>{if(o.getLayers)t(o);else if(o.get("title")==="District Boundary"){const a=o.getSource&&o.getSource();if(a&&typeof a.getExtent=="function"){const i=a.getExtent();i&&Number.isFinite(i[0])&&(e={extent:i,title:o.get("title")})}}})};return t(this.overlayGroup),e}getMarkerSource(){return this.markerSource}getMarkersLayer(){return this.markersLayer}updateSize(){this.map.updateSize()}onSearchSelect(e){this.searchSelectCallbacks.push(e)}navigateTo(e,t,r=14,o=500){const a=ee([e,t]);this.map.getView().animate({center:a,zoom:r,duration:o})}}class $i{constructor(e,t={}){this.map=e,this.options=t,this.measureSource=new G,this.measureLayer=new D({source:this.measureSource,style:this.getMeasureStyle(),title:"Measurements",zIndex:100,selectable:!1}),this.drawSource=new G,this.drawLayer=new D({source:this.drawSource,style:this.getDrawStyle(),title:"Draw sketches",displayInLayerSwitcher:!1,zIndex:99});const r=this.map.getLayers();let o=r.getArray().findIndex(a=>a.get("title")==="Overlays");o<0&&(o=r.getLength()),r.insertAt(o,this.drawLayer),r.insertAt(o,this.measureLayer),this.activeInteraction=null,this.measureTooltip=null,this.measureTooltipElement=null,this.onMeasureCompleteCallbacks=[],this.onDrawCompleteCallbacks=[]}getMeasureStyle(){return new M({fill:new C({color:"rgba(255, 233, 106, 0.2)"}),stroke:new P({color:"#8B008B",lineDash:[10,10],width:2}),image:new he({radius:5,stroke:new P({color:"#8B008B"}),fill:new C({color:"rgba(255, 233, 106, 0.5)"})})})}getDrawStyle(){return new M({fill:new C({color:"rgba(255, 233, 106, 0.3)"}),stroke:new P({color:"#8B008B",width:2}),image:new he({radius:6,stroke:new P({color:"#8B008B",width:2}),fill:new C({color:"#FFE96A"})})})}createMeasureTooltip(){this.measureTooltipElement&&this.measureTooltipElement.parentNode.removeChild(this.measureTooltipElement),this.measureTooltipElement=document.createElement("div"),this.measureTooltipElement.className="measure-tooltip",this.measureTooltip=new Ee({element:this.measureTooltipElement,offset:[15,0],positioning:"center-left",stopEvent:!1}),this.map.addOverlay(this.measureTooltip)}setCircleRadius(e,t){const r=e.getGeometry();if(!r||typeof r.setRadius!="function")return;const o=r.getCenter(),a=Math.max(1,Number(t)||0),i=ai(a,o);r.setRadius(i),e.set("_radius",i);const s=e.get("_radiusLine");s&&s.setGeometry(new de([o,[o[0]+i,o[1]]])),this._refreshRadiusAdjuster(e)}getCircleGroundRadius(e){const t=e.getGeometry();return Math.round(go(t.getRadius(),t.getCenter())*1e6)/1e6}_attachRadiusAdjuster(e,t,r){if(r){t.setPosition(t.getPosition()),r.classList.add("measure-tooltip-adjust"),r.innerHTML=` -
- - - -
-
`;for(const o of["pointerdown","pointerup","click","dblclick","touchstart"])r.addEventListener(o,a=>a.stopPropagation());r.querySelectorAll(".mt-step").forEach(o=>{o.addEventListener("click",()=>{const a=this.getCircleGroundRadius(e),i=ii(a),s=Number(o.dataset.dir),l=Math.round(a/i)*i,c=Math.abs(l-a)>.01&&Math.sign(l-a)===s?l:l+s*i;this.setCircleRadius(e,c)})}),r.querySelector(".mt-value").addEventListener("click",()=>{this._promptRadius(e,r)}),e.set("_adjustEl",r),this._refreshRadiusAdjuster(e)}}_promptRadius(e,t){const r=t.querySelector(".mt-value");if(!r||t.querySelector(".mt-input"))return;const o=this.getCircleGroundRadius(e),a=document.createElement("input");a.type="number",a.className="mt-input",a.inputMode="decimal",a.min="1",a.step="any",a.value=String(Math.round(o*100)/100),a.setAttribute("aria-label","Radius in metres"),r.replaceWith(a),a.focus(),a.select();let i=!1;const s=l=>{i||!a.isConnected||(i=!0,l&&this.setCircleRadius(e,parseFloat(a.value)),a.replaceWith(r),this._refreshRadiusAdjuster(e))};a.addEventListener("keydown",l=>{l.key==="Enter"&&s(!0),l.key==="Escape"&&s(!1)}),a.addEventListener("blur",()=>s(!0))}_refreshRadiusAdjuster(e){const t=e.get("_adjustEl");if(!t)return;const r=this.getCircleGroundRadius(e),o=t.querySelector(".mt-value");o&&(o.textContent=ut(r));const a=t.querySelector(".mt-area");a&&(a.textContent=We(Math.PI*r*r))}deactivate(){this.activeInteraction&&(this.map.removeInteraction(this.activeInteraction),this.activeInteraction=null),this.measureTooltip&&(this.map.removeOverlay(this.measureTooltip),this.measureTooltip=null),this.measureTooltipElement&&this.measureTooltipElement.parentNode&&(this.measureTooltipElement.parentNode.removeChild(this.measureTooltipElement),this.measureTooltipElement=null)}startCircleMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new Oe({source:this.measureSource,type:"Circle",style:new M({fill:new C({color:"rgba(255, 233, 106, 0.2)"}),stroke:new P({color:"rgba(139, 0, 139, 0.7)",lineDash:[10,10],width:2}),image:new he({radius:5,stroke:new P({color:"rgba(139, 0, 139, 0.7)"}),fill:new C({color:"rgba(255, 233, 106, 0.5)"})})})});this.activeInteraction=e,this.map.addInteraction(e);let t;return e.on("drawstart",r=>{t=r.feature.getGeometry().on("change",a=>{const i=a.target;if(i instanceof Zr){const s=go(i.getRadius(),i.getCenter()),l=ri(s),d=`${ut(s)}
${l}`;this.measureTooltipElement.innerHTML=d,this.measureTooltip.setPosition(i.getLastCoordinate())}})}),e.on("drawend",r=>{const o=r.feature,a=o.getGeometry(),i=a.getCenter(),s=a.getRadius();o.set("_layerType","measure_circle"),o.set("_radius",s),o.set("_center",i);const l=new fe({geometry:new de([i,[i[0]+s,i[1]]])});l.set("_layerType","measure_circle_radius"),this.measureSource.addFeature(l),o.set("_radiusLine",l),this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltip.setOffset([0,-7]),this._attachRadiusAdjuster(o,this.measureTooltip,this.measureTooltipElement),this.measureTooltipElement=null,this.createMeasureTooltip(),zt(t);const c={type:"circle",center:i,radius:s,area:Math.PI*s*s,feature:o};this.onMeasureCompleteCallbacks.forEach(d=>d(c))}),e}startLineMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new Oe({source:this.measureSource,type:"LineString",style:this.getMeasureStyle()});this.activeInteraction=e,this.map.addInteraction(e);let t;return e.on("drawstart",r=>{t=r.feature.getGeometry().on("change",a=>{const i=a.target,s=Pt(i),l=ut(s);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(i.getLastCoordinate())})}),e.on("drawend",r=>{const o=r.feature,a=o.getGeometry(),i=Pt(a);this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),zt(t);const s={type:"line",length:i,feature:o};this.onMeasureCompleteCallbacks.forEach(l=>l(s))}),e}startAreaMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new Oe({source:this.measureSource,type:"Polygon",style:this.getMeasureStyle()});this.activeInteraction=e,this.map.addInteraction(e);let t;return e.on("drawstart",r=>{t=r.feature.getGeometry().on("change",a=>{const i=a.target,s=dt(i),l=We(s);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(i.getInteriorPoint().getCoordinates())})}),e.on("drawend",r=>{const o=r.feature,a=o.getGeometry(),i=dt(a);o.set("_layerType","measure_area"),o.set("_area",i),this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),zt(t);const s={type:"polygon",area:i,feature:o,coordinate:a.getInteriorPoint().getCoordinates()};this.onMeasureCompleteCallbacks.forEach(l=>l(s))}),e}startDrawPoint(){this.deactivate();const e=new Oe({source:this.drawSource,type:"Point",style:this.getDrawStyle()});return this.activeInteraction=e,this.map.addInteraction(e),e.on("drawend",t=>{const r={type:"point",feature:t.feature};this.onDrawCompleteCallbacks.forEach(o=>o(r))}),e}startDrawLine(){this.deactivate();const e=new Oe({source:this.drawSource,type:"LineString",style:this.getDrawStyle()});return this.activeInteraction=e,this.map.addInteraction(e),e.on("drawend",t=>{const r={type:"line",feature:t.feature};this.onDrawCompleteCallbacks.forEach(o=>o(r))}),e}startDrawPolygon(){this.deactivate();const e=new Oe({source:this.drawSource,type:"Polygon",style:this.getDrawStyle()});return this.activeInteraction=e,this.map.addInteraction(e),e.on("drawend",t=>{const r={type:"polygon",feature:t.feature};this.onDrawCompleteCallbacks.forEach(o=>o(r))}),e}clearMeasurements(){this.measureSource.clear(),document.querySelectorAll(".measure-tooltip-static").forEach(t=>t.parentNode.removeChild(t))}clearDrawings(){this.drawSource.clear()}clearAll(){this.clearMeasurements(),this.clearDrawings()}onMeasureComplete(e){this.onMeasureCompleteCallbacks.push(e)}onDrawComplete(e){this.onDrawCompleteCallbacks.push(e)}createControlBar(e={}){e.position;const t=new uo({group:!0,className:"map-tools-bar"}),r=new uo({toggleOne:!0,group:!0}),o=new Se({html:'⭕',title:"Measure Circle (radius & area)",className:"measure-circle-btn",onToggle:l=>{l?this.startCircleMeasure():this.deactivate()}});r.addControl(o);const a=new Se({html:'📏',title:"Measure Distance",className:"measure-line-btn",onToggle:l=>{l?this.startLineMeasure():this.deactivate()}});r.addControl(a);const i=new Se({html:'⬛',title:"Measure Area",className:"measure-area-btn",onToggle:l=>{l?this.startAreaMeasure():this.deactivate()}});r.addControl(i);const s=new lt({html:'🗑️',title:"Clear Measurements",className:"clear-measure-btn",handleClick:()=>{this.clearMeasurements(),o.setActive(!1),a.setActive(!1),i.setActive(!1)}});return r.addControl(s),t.addControl(r),t}getMeasureLayer(){return this.measureLayer}getDrawLayer(){return this.drawLayer}getMeasureSource(){return this.measureSource}getDrawSource(){return this.drawSource}isActive(){return this.activeInteraction!==null}}let Re=null;async function Bi(){if(!("serviceWorker"in navigator))return console.warn("[PWA] Service Workers not supported"),null;try{return Re=await navigator.serviceWorker.register("/sw.js",{scope:"/"}),console.log("[PWA] Service Worker registered:",Re.scope),Re.addEventListener("updatefound",()=>{const n=Re.installing;n.addEventListener("statechange",()=>{n.state==="installed"&&navigator.serviceWorker.controller&&(console.log("[PWA] New version available"),qi())})}),Re}catch(n){return console.error("[PWA] Service Worker registration failed:",n),null}}let je=null,_e=null;function Gi(n="#install-btn"){if(_e=typeof n=="string"?document.querySelector(n):n,!_e){console.warn("[PWA] Install button not found:",n);return}_e.style.display="none",window.addEventListener("beforeinstallprompt",e=>{e.preventDefault(),je=e,_e.style.display="block",console.log("[PWA] Install prompt ready")}),_e.addEventListener("click",async()=>{if(!je){ji();return}je.prompt();const{outcome:e}=await je.userChoice;console.log("[PWA] Install prompt outcome:",e),je=null,_e.style.display="none"}),window.addEventListener("appinstalled",()=>{console.log("[PWA] App installed"),je=null,_e.style.display="none"}),window.matchMedia("(display-mode: standalone)").matches&&(_e.style.display="none")}function ji(){const n=/iPad|iPhone|iPod/.test(navigator.userAgent),e=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);let t=`To install this app: - -`;n?(t+=`1. Tap the Share button (square with arrow) -`,t+='2. Scroll down and tap "Add to Home Screen"'):e?(t+=`1. Click File menu -`,t+='2. Click "Add to Dock"'):(t+=`1. Click the menu button (three dots) -`,t+='2. Click "Install" or "Add to Home Screen"'),alert(t)}let yo=null;const bo=new Set;function Ui(n="#offline-indicator"){yo=typeof n=="string"?document.querySelector(n):n,Xt(!navigator.onLine),window.addEventListener("online",()=>{console.log("[PWA] Back online"),Xt(!1),ln(!1)}),window.addEventListener("offline",()=>{console.log("[PWA] Gone offline"),Xt(!0),ln(!0)})}function Xt(n){yo&&(yo.style.display=n?"block":"none"),document.body.classList.toggle("is-offline",n)}function mr(n){return bo.add(n),n(!navigator.onLine),()=>bo.delete(n)}function ln(n){for(const e of bo)try{e(n)}catch(t){console.error("[PWA] Offline listener error:",t)}}function K(){return navigator.onLine}function qi(){confirm("A new version is available. Reload now?")&&zi()}function zi(){Re?.waiting&&Re.waiting.postMessage({type:"SKIP_WAITING"}),window.location.reload()}async function Hi({timeoutMs:n=1e4}={}){if(!("serviceWorker"in navigator))throw new Error("Service Workers not supported in this browser");if(navigator.serviceWorker.controller)return navigator.serviceWorker.controller;const e=navigator.serviceWorker.ready,t=new Promise((a,i)=>setTimeout(()=>i(new Error("Service-worker readiness timeout")),n)),r=await Promise.race([e,t]),o=navigator.serviceWorker.controller||r.active;if(!o)throw new Error("No active service worker available");return o}function Wi(n){if(!("serviceWorker"in navigator))return()=>{};const e=()=>{try{n()}catch(t){console.error("[PWA] controllerchange handler error:",t)}};return navigator.serviceWorker.addEventListener("controllerchange",e),()=>navigator.serviceWorker.removeEventListener("controllerchange",e)}async function Co(n,e,t={},r=5e3,o=1e4){const a=await Hi({timeoutMs:o});return new Promise((i,s)=>{const l=new MessageChannel,c=setTimeout(()=>{l.port1.close(),s(new Error(`Service-worker reply "${e}" timed out`))},r);l.port1.onmessage=d=>{if(d.data?.type===e){clearTimeout(c),l.port1.close();const{type:u,...p}=d.data;i(p)}},a.postMessage({type:n,...t},[l.port2])})}async function Vi(){try{return(await Co("GET_TILE_STATS","TILE_STATS")).stats}catch(n){return console.warn("[PWA] getTileCacheStats failed:",n),null}}async function Ki(){try{return await Co("CLEAR_TILE_CACHES","TILE_CACHES_CLEARED"),!0}catch(n){return console.warn("[PWA] clearTileCaches failed:",n),!1}}async function Xi(n){if(!n)return!1;try{return!!(await Co("CLEAR_TILE_CACHE","TILE_CACHE_CLEARED",{cacheName:n})).deleted}catch(e){return console.warn(`[PWA] clearTileCacheForProvider(${n}) failed:`,e),!1}}async function Yi(){if(!navigator.storage?.estimate)return null;try{const{usage:n,quota:e}=await navigator.storage.estimate();return{usage:n||0,quota:e||0}}catch(n){return console.warn("[PWA] getStorageEstimate failed:",n),null}}async function Ji(n={}){const{installButton:e="#install-btn",offlineIndicator:t="#offline-indicator",autoRegisterSW:r=!0}=n;r&&await Bi(),Gi(e),Ui(t),console.log("[PWA] Initialized")}const yr={topo:{url:"https://a.tile.opentopomap.org/{z}/{x}/{y}.png",label:"Topographic",maxZoom:17,cacheKey:"tiles-topo"},osm:{url:"https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",label:"OpenStreetMap",maxZoom:19,cacheKey:"tiles-osm"}},Zi=30*1024,Rt=2*Math.PI*6378137/2;function cn(n,e){const t=n/Rt*180;let r=e/Rt*180;return r=180/Math.PI*(2*Math.atan(Math.exp(r*Math.PI/180))-Math.PI/2),[t,r]}function dn(n,e,t){const r=Math.pow(2,t),o=Math.floor((n+180)/360*r),a=e*Math.PI/180,i=Math.floor((1-Math.log(Math.tan(a)+1/Math.cos(a))/Math.PI)/2*r);return{x:o,y:i}}function br(n,e){const[t,r,o,a]=n,[i,s]=cn(t,r),[l,c]=cn(o,a),d=dn(i,c,e),u=dn(l,s,e),p=Math.pow(2,e),f=Math.max(0,Math.min(d.x,u.x)),h=Math.min(p-1,Math.max(d.x,u.x)),g=Math.max(0,Math.min(d.y,u.y)),m=Math.min(p-1,Math.max(d.y,u.y));return{z:e,minX:f,maxX:h,minY:g,maxY:m,count:(h-f+1)*(m-g+1)}}function Qi(n,e,t){let r=0;for(let o=e;o<=t;o++)r+=br(n,o).count;return r}function es(n,e,t){const r=[];for(let o=e;o<=t;o++){const a=br(n,o);for(let i=a.minX;i<=a.maxX;i++)for(let s=a.minY;s<=a.maxY;s++)r.push({z:o,x:i,y:s})}return r}function ts(n,{z:e,x:t,y:r}){return n.replace("{z}",e).replace("{x}",t).replace("{y}",r)}class os{constructor({baseMap:e,extent3857:t,minZoom:r,maxZoom:o,concurrency:a=2,interBatchDelayMs:i=50,onProgress:s=()=>{}}){const l=yr[e];if(!l)throw new Error(`Unknown base map: ${e}`);o>l.maxZoom&&(console.warn(`[OfflineTiles] ${e}: maxZoom ${o} > supported ${l.maxZoom}; clamping`),o=l.maxZoom),this.baseMap=e,this.template=l.url,this.extent=t,this.minZoom=r,this.maxZoom=o,this.concurrency=Math.max(1,Math.min(a,6)),this.interBatchDelayMs=i,this.onProgress=s,this._abortCtrl=null,this._cancelled=!1}async start(){if(this._abortCtrl)throw new Error("Downloader already started");this._abortCtrl=new AbortController,this._cancelled=!1;const e=es(this.extent,this.minZoom,this.maxZoom),t=e.length,r=Date.now();let o=0,a=0,i=0,s=0;const l=c=>{const d=Date.now()-r,u=o>0?Math.round(d/o*(t-o)):null;this.onProgress({phase:c,done:o,total:t,ok:a,failed:i,cached:s,elapsedMs:d,etaMs:u})};l("running");for(let c=0;c{if(this._cancelled)return;const p=ts(this.template,u);try{const f=await fetch(p,{signal:this._abortCtrl.signal,cache:"default"});f.ok?(a++,f.body&&f.body.cancel().catch(()=>{})):(f.status,i++)}catch(f){f.name==="AbortError"||i++}o++})),l("running"),this.interBatchDelayMs>0&&c+this.concurrencysetTimeout(u,this.interBatchDelayMs))}return l(this._cancelled?"cancelled":"done"),{phase:this._cancelled?"cancelled":"done",done:o,total:t,ok:a,failed:i,cached:s,elapsedMs:Date.now()-r}}cancel(){this._cancelled=!0,this._abortCtrl&&this._abortCtrl.abort()}}const ns=(()=>{const n=(r,o)=>{const a=r*Rt/180,i=Math.log(Math.tan((90+o)*Math.PI/360))/(Math.PI/180);return[a,i*Rt/180]},e=n(-3.3,4.5),t=n(1.2,11.2);return[e[0],e[1],t[0],t[1]]})();function rs(n){return n*Zi}const wr="https://api.lupmis4luspa.org/api",_r=`${wr}/spatial_planning`;function as(n){return n.includes("/")?`${wr}/${n}`:`${_r}/${n}`}const Yt="1",is="1c46538c712e9b5b";function ss(){try{if(typeof window>"u")return Yt;const n=window.LUPMIS_SESSION;if(!n||typeof n!="object")return Yt;const e=n.district_id;return e==null||String(e).length===0?null:String(e)}catch{}return Yt}const Ao={get district_id(){return ss()},api_token:is};function Ye(){if(typeof window<"u"&&window.LUPMIS_SESSION&&window.LUPMIS_SESSION.user_id)return window.LUPMIS_SESSION;try{const n=localStorage.getItem("dev-session");if(n){const e=JSON.parse(n);if(e&&e.user_id)return e}}catch{}return null}typeof window<"u"&&(window.lupmisDevSession=n=>{n==null?(localStorage.removeItem("dev-session"),console.log("[Dev] Session override cleared. Reload to apply.")):(localStorage.setItem("dev-session",JSON.stringify(n)),console.log("[Dev] Session override saved. Reload to apply:",n))});const ls=3e4,cs=5e3;let De=null;async function ds(n=!1){if(De!==null&&!n)return De;const e=new AbortController,t=setTimeout(()=>e.abort(),cs);try{De=(await fetch(`${_r}/get_layers.php`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(Ao),signal:e.signal})).ok}catch{De=!1}finally{clearTimeout(t)}return console.log("[RemoteDB] Server reachable:",De),De}function ke(){return De}function us(n,e=ls){const t=new AbortController,r=setTimeout(()=>t.abort(),e);return n.signal&&n.signal.addEventListener("abort",()=>t.abort()),{signal:t.signal,clear:()=>clearTimeout(r)}}async function me(n,e={},t={}){const r=as(n),o={...Ao,...e};console.log("[RemoteDB] POST",r);const a=us(t);try{const i=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(o),...t,signal:a.signal});if(!i.ok)throw new Error(`HTTP ${i.status}: ${i.statusText}`);const s=await i.json();return console.log("[RemoteDB] POST response:",n,"→",typeof s=="object"?`${Array.isArray(s)?s.length+" items":"object"}`:s),s}catch(i){throw i.name==="AbortError"?(console.error("[RemoteDB] POST timed out:",n),new Error(`Request timed out: ${n}`)):(console.error("[RemoteDB] POST failed:",n,i),i)}finally{a.clear()}}async function ps(){return me("get_district_boundary.php")}async function fs(){return me("get_layers.php")}async function hs(){return me("get_all_collector_zone_per_district.php")}async function gs(){return me("get_parcels_per_district.php")}async function ms(){return me("get_all_footprint_per_district.php")}async function ys(){const n=Ao.district_id;return me("contours/get_by_district.php",{districtid:n==null?null:Number(n)})}async function bs(){return me("get_osm_roads.php")}async function ws(){return me("get_upn_grid_per_district.php")}async function _s(n,e){const t={client_uuid:n.client_uuid,name:n.name??null,started_at:n.started_at,ended_at:n.ended_at,point_count:n.point_count??e.length,distance_m:n.distance_m??0,points:(e||[]).map(o=>({seq:o.seq,longitude:o.longitude,latitude:o.latitude,altitude:o.altitude??null,accuracy:o.accuracy??null,altitude_accuracy:o.altitude_accuracy??null,heading:o.heading??null,speed:o.speed??null,satellites:o.satellites??null,recorded_at:o.recorded_at}))},r=await me("save_gps_trail.php",t);return{remoteId:r?.id??r?.remote_id??null}}const vs=63710088e-1,Ne=Math.PI/180;function Es(n,e,t,r){const o=(r-e)*Ne,a=(t-n)*Ne,i=Math.sin(o/2)**2+Math.cos(e*Ne)*Math.cos(r*Ne)*Math.sin(a/2)**2;return 2*vs*Math.asin(Math.min(1,Math.sqrt(i)))}function un(n,e=5){return n==null||Number.isNaN(n)?"—":n.toFixed(e)}const pn=6378137,fn=1/298.257223563,hn=.9996,te=fn*(2-fn),Jt=te/(1-te);function xs(n,e){if(n==null||e==null||Number.isNaN(n)||Number.isNaN(e))return null;let t=((e+180)%360+360)%360-180;const r=Math.floor((t+180)/6)+1,o=(r-1)*6-180+3,a=n*Ne,i=t*Ne,s=o*Ne,l=Math.sin(a),c=Math.cos(a),d=Math.tan(a),u=pn/Math.sqrt(1-te*l*l),p=d*d,f=Jt*c*c,h=c*(i-s),g=pn*((1-te/4-3*te**2/64-5*te**3/256)*a-(3*te/8+3*te**2/32+45*te**3/1024)*Math.sin(2*a)+(15*te**2/256+45*te**3/1024)*Math.sin(4*a)-35*te**3/3072*Math.sin(6*a));let m=hn*u*(h+(1-p+f)*h**3/6+(5-18*p+p*p+72*f-58*Jt)*h**5/120)+5e5,y=hn*(g+u*d*(h*h/2+(5-p+9*f+4*f*f)*h**4/24+(61-58*p+p*p+600*f-330*Jt)*h**6/720));const w=n>=0?"N":"S";return w==="S"&&(y+=1e7),{zone:r,hemisphere:w,easting:Math.round(m),northing:Math.round(y)}}function Ss(n,e){const t=xs(n,e);return t?`${t.zone}${t.hemisphere} ${t.easting}E ${t.northing}N`:"—"}function Ls(n){return n==null||Number.isNaN(n)?"—":n<1e3?`${Math.round(n)} m`:`${(n/1e3).toFixed(2)} km`}function Ts(n){return n==null||Number.isNaN(n)?"—":`±${Math.round(n)} m`}function ks(n){return n==null||Number.isNaN(n)?"none":n<=10?"good":n<=30?"fair":"poor"}const Is={minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0,timeoutMs:15e3,maximumAgeMs:0};class pt{constructor(e={}){this.opts={...Is,...e},this.storage=e.storage||null,this.sync=e.sync||null,this._geo=e.geolocation||(typeof navigator<"u"?navigator.geolocation:null),this._state="idle",this._watchId=null,this._live=!1,this._recording=!1,this._activeTrailId=null,this._activeTrailUuid=null,this._lastRecorded=null,this._lastRecordedAt=0,this._distanceM=0,this._pointCount=0,this._lastFix=null,this._listeners=Object.create(null)}on(e,t){return(this._listeners[e]||(this._listeners[e]=new Set)).add(t),()=>this._listeners[e]?.delete(t)}_emit(e,t){const r=this._listeners[e];if(r)for(const o of r)try{o(t)}catch(a){console.error(`[GeoTracker] listener for "${e}" threw`,a)}}get state(){return this._state}get isRecording(){return this._recording}get lastFix(){return this._lastFix}get isSupported(){return!!this._geo}_setState(e){this._state!==e&&(this._state=e,this._emit("statechange",e))}startLive(){if(!this._geo){this._emit("error",new Error("Geolocation not supported"));return}this._live=!0,this._ensureWatch()}stopLive(){this._live=!1,this._recording||this._teardownWatch()}getCurrentPosition(){return new Promise((e,t)=>{if(!this._geo){t(new Error("Geolocation not supported"));return}this._geo.getCurrentPosition(r=>{const o=pt.normalize(r);this._lastFix=o,this._emit("position",o),e(o)},r=>{this._emit("error",r),t(r)},{enableHighAccuracy:this.opts.enableHighAccuracy,timeout:this.opts.timeoutMs,maximumAge:this.opts.maximumAgeMs})})}async startRecording(e={}){if(!this._geo)throw new Error("Geolocation not supported");if(!this.storage)throw new Error("GeoTracker: no storage adapter configured");if(this._recording)return{trailId:this._activeTrailId,uuid:this._activeTrailUuid};const t=pt.uuid(),r=new Date().toISOString(),o={uuid:t,name:e.name||null,startedAt:r,...e},a=await this.storage.createTrail(o);return this._activeTrailId=a,this._activeTrailUuid=t,this._lastRecorded=null,this._lastRecordedAt=0,this._distanceM=0,this._pointCount=0,this._recording=!0,this._ensureWatch(),this._setState("recording"),this._emit("trailstart",{trailId:a,uuid:t,startedAt:r}),{trailId:a,uuid:t}}async stopRecording(){if(!this._recording)return null;const e=this._activeTrailId,r={endedAt:new Date().toISOString(),pointCount:this._pointCount,distanceM:this._distanceM};this._recording=!1,this._live||this._teardownWatch(),this._setState(this._live?"watching":"idle");try{await this.storage.finishTrail(e,r)}catch(a){this._emit("error",a)}this._emit("trailstop",{trailId:e,...r});let o=!1;if(this.sync)try{o=await this._syncTrail(e)}catch(a){this._emit("error",a)}return this._activeTrailId=null,this._activeTrailUuid=null,{trailId:e,pointCount:r.pointCount,distanceM:r.distanceM,synced:o}}async syncPending(){if(!this.sync||!this.storage)return{pushed:0,failed:0};if(this.sync.isOnline&&!this.sync.isOnline())return{pushed:0,failed:0};let e=0,t=0;const r=await this.storage.getUnsyncedTrails();for(const o of r)try{await this._syncTrail(o.id??o.trailId,o)?e++:t++}catch(a){t++,this._emit("error",a)}return this._emit("syncstatus",{pushed:e,failed:t}),{pushed:e,failed:t}}async _syncTrail(e,t){const r=await this.storage.getTrailPoints(e),o=t||{id:e},a=await this.sync.pushTrail(o,r),i=a&&(a.remoteId??a.id??null);return await this.storage.markTrailSynced(e,i),!0}_ensureWatch(){if(this._watchId!=null||!this._geo){this._state==="idle"&&this._live&&this._setState("watching");return}this._watchId=this._geo.watchPosition(e=>this._onFix(e),e=>this._emit("error",e),{enableHighAccuracy:this.opts.enableHighAccuracy,timeout:this.opts.timeoutMs,maximumAge:this.opts.maximumAgeMs}),this._recording||this._setState("watching")}_teardownWatch(){this._watchId!=null&&this._geo&&this._geo.clearWatch(this._watchId),this._watchId=null}async _onFix(e){const t=pt.normalize(e);if(this._lastFix=t,this._emit("position",t),!this._recording)return;const{minIntervalMs:r,minDistanceM:o,heartbeatMs:a,maxAccuracyM:i}=this.opts,s=t.timestamp;if(this._lastRecordedAt&&s-this._lastRecordedAt0&&t.accuracy!=null&&t.accuracy>i&&this._lastRecorded)return;let l=!1,c=0;if(!this._lastRecorded)l=!0;else{c=Es(this._lastRecorded.lon,this._lastRecorded.lat,t.lon,t.lat);const d=s-this._lastRecordedAt;(c>=o||d>=a)&&(l=!0)}if(l){this._lastRecorded&&(this._distanceM+=c),this._pointCount+=1,this._lastRecorded={lon:t.lon,lat:t.lat,timestamp:s},this._lastRecordedAt=s;try{await this.storage.addPoint(this._activeTrailId,{...t,seq:this._pointCount}),this._emit("point",{trailId:this._activeTrailId,seq:this._pointCount,point:t,distanceM:this._distanceM,pointCount:this._pointCount})}catch(d){this._emit("error",d)}}}static normalize(e){const t=e.coords||{},r=o=>o!=null&&!Number.isNaN(o)?o:null;return{lon:t.longitude,lat:t.latitude,accuracy:r(t.accuracy),altitude:r(t.altitude),altitudeAccuracy:r(t.altitudeAccuracy),heading:r(t.heading),speed:r(t.speed),satellites:null,timestamp:e.timestamp||Date.now()}}static uuid(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{const t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}}const Ps={async createTrail(n){const e=n.districtId??Ye()?.district_id??null;return Ya({...n,districtId:e!=null?String(e):null})},addPoint:(n,e)=>Ja(n,e),finishTrail:(n,e)=>Za(n,e),getUnsyncedTrails:()=>Qa(),getTrailPoints:n=>ei(n),markTrailSynced:(n,e)=>ti(n,e)},Ms={pushTrail:(n,e)=>_s(n,e),isOnline:()=>K()},ve=new pt({storage:Ps,sync:Ms,minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0}),Cs=new Set(["set:view","set:selected","clear:selected","set:basemap"]);function As({mapView:n,embedConfig:e}){const t=n.getMap(),r=window.parent&&window.parent!==window?window.parent:null,o=new G,a=new D({source:o,zIndex:9999,style:new M({stroke:new P({color:"#f97316",width:3}),fill:new C({color:"rgba(249,115,22,0.18)"})}),properties:{title:"Permit selection",displayInLayerSwitcher:!1}});t.addLayer(a);let i=null,s=e?.upn?String(e.upn):null,l=!1;function c(m){if(!r){console.warn("[embed-bridge] No parent window — would have sent:",m);return}try{r.postMessage(m,"*")}catch(y){console.warn("[embed-bridge] postMessage failed:",y)}}function d(m,y){c({type:"error",code:m,message:y})}function u(){l||(l=!0,c({type:"ready"}))}function p(m,y,w){const b=m.getProperties();let S=y,L=w;if(S==null||L==null){const E=m.getGeometry()?.getExtent();if(E){const[x,I]=xe(Qr(E));S=x,L=I}}return{type:"parcel:select",upn:b.upn??null,parcel_id:b.id??null,lon:S??null,lat:L??null,zone_code:b.zone_code??null,zone_name:b.zone_name??null,landuse:b.landuse??null,min_height:b.min_height??null,max_height:b.max_height??null}}function f(m){if(o.clear(),m){const y=m.clone();o.addFeature(y)}}n.onClick((m,y,w,b)=>{let S=null;t.forEachFeatureAtPixel(b.pixel,L=>{if(L.get("_layerType")==="parcel")return S=L,!0}),S?(f(S),c(p(S,m,y))):(f(null),c({type:"parcel:cleared"}))}),window.addEventListener("message",m=>{const y=m.data;if(!(!y||typeof y!="object"||!Cs.has(y.type)))try{switch(y.type){case"set:view":{if(typeof y.lon=="number"&&typeof y.lat=="number"){const w=t.getView();w.setCenter(ee([y.lon,y.lat])),typeof y.zoom=="number"&&w.setZoom(y.zoom)}break}case"set:selected":y.upn&&h(String(y.upn));break;case"clear:selected":f(null),s=null;break;case"set:basemap":y.key&&typeof n.setBaseMap=="function"&&n.setBaseMap(y.key);break}}catch(w){d("COMMAND_FAILED",`Failed to handle ${y.type}: ${w.message}`)}});function h(m){if(!i){s=m;return}const w=i.getSource().getFeatures().find(S=>String(S.get("upn")??"")===m);if(!w){s=m;return}s=null,f(w);const b=w.getGeometry()?.getExtent();b&&t.getView().fit(b,{padding:[50,50,50,50],duration:400,maxZoom:17}),c(p(w,null,null))}function g(m){i=m;const y=m.getSource(),w=()=>{queueMicrotask(()=>{s&&h(s),u()})};if(y.getFeatures().length>0)w();else{let b=!1;y.on("addfeature",()=>{b||(b=!0,queueMicrotask(()=>{b=!1,s&&h(s),u()}))})}}if(e?.basemap&&typeof n.setBaseMap=="function"&&n.setBaseMap(e.basemap),typeof e?.lon=="number"&&typeof e?.lat=="number"){const m=t.getView();m.setCenter(ee([e.lon,e.lat])),m.setZoom(typeof e?.zoom=="number"?e.zoom:15)}return{attachParcelsLayer:g,emitError:d}}const Fs=[{key:"parcels",label:"Parcels",geometryFamily:"polygon"},{key:"collector_zones",label:"Collector Zones",geometryFamily:"polygon"},{key:"building_footprints",label:"Building Footprints",geometryFamily:"polygon"},{key:"osm_roads",label:"OSM Roads",geometryFamily:"line"},{key:"other",label:"Other (view only)",geometryFamily:"any"}],vr={parcels:["upn","landuse","zone_code","zone_name","sector","block","parcel_no","prop_no","st_name","prop_add","fac_name","min_height","max_height","eff_date","lp_name","locality","mmda","last_update","remarks"],collector_zones:["zone_name"],building_footprints:[],osm_roads:["osm_id","name","highway"],other:[]};function Os(n){if(!n?.features?.length)return"none";let e=0,t=0,r=0;for(const i of n.features){const s=i?.geometry?.type;s&&(s==="Polygon"||s==="MultiPolygon"?e++:s==="LineString"||s==="MultiLineString"?t++:(s==="Point"||s==="MultiPoint")&&r++)}const o=e+t+r;if(o===0)return"none";const a=Math.max(e,t,r);return ae&&(s=s.slice(0,e-1)+"…"),t[a]=s)}}return t}const T={};let wo=null,N=null;function Gs(){T.root||(T.root=document.getElementById("importMappingModal"),T.filename=document.getElementById("import-modal-filename"),T.summary=document.getElementById("import-modal-summary"),T.target=document.getElementById("import-modal-target"),T.targetHint=document.getElementById("import-modal-target-hint"),T.fieldsWrap=document.getElementById("import-modal-fields-wrap"),T.tbody=document.getElementById("import-modal-fields-tbody"),T.btnSave=document.getElementById("import-modal-save"),T.btnSaveUpload=document.getElementById("import-modal-save-upload"),T.btnCancel=document.getElementById("import-modal-cancel"),T.target&&!T.target.dataset.populated&&(T.target.innerHTML=Fs.map(n=>``).join(""),T.target.dataset.populated="1"),T.target&&!T.target.dataset.wired&&(T.target.dataset.wired="1",T.target.addEventListener("change",Sr)),T.btnSave&&!T.btnSave.dataset.wired&&(T.btnSave.dataset.wired="1",T.btnSave.addEventListener("click",()=>mn("save"))),T.btnSaveUpload&&!T.btnSaveUpload.dataset.wired&&(T.btnSaveUpload.dataset.wired="1",T.btnSaveUpload.addEventListener("click",()=>mn("upload"))),T.root&&!T.root.dataset.wired&&(T.root.dataset.wired="1",T.root.addEventListener("hidden.bs.modal",()=>{N?.onResult&&!N._resolved&&(N._resolved=!0,N.onResult({action:"cancel"})),N=null})))}function js(){const n=N.targetType,e=vr[n]||[];if(n==="other"||e.length===0){T.fieldsWrap.style.display="none";return}T.fieldsWrap.style.display="";const t=o=>{const a=N.sourceSamples[o];return a?` — [${It(a)}]`:" — [(empty)]"},r=[''].concat(N.sourceFields.map(o=>``)).join("");T.tbody.innerHTML=e.map(o=>{const a=N.mapping[o]||"",i=r.replace(`