From 90a5a01b05ae2772d6dd4a7a5e57cf1c7d61cbad Mon Sep 17 00:00:00 2001 From: ekke Date: Tue, 8 Sep 2026 17:26:30 +0200 Subject: [PATCH] Drag the analysis popup by its title bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The popup covers the thing it is reporting on — a circle's own parcels sit directly underneath it — so it has to be movable to be read against the map. Pressing the title bar and dragging now moves it. The drag changes the overlay's offset, not its position. The popup stays tied to the shape it describes, so panning and zooming carry it along and it goes on pointing at the right circle; only where it rests relative to that anchor changes. Setting a new position instead would cut it loose and re-anchor it to whatever ground it happened to be dropped on. The resting offset is now a named constant shared with the overlay, so the two cannot drift apart, and each showing starts from it — a popup opened for a new shape appears where it belongs rather than where the last one was left. A press on the close button is not a drag, and the overlay already has stopEvent:true, so the map does not pan underneath. This covers the Area Analysis popup too: both are built by the same _buildAnalysisPopupHtml, so both get the handle. Separate commit from the radius work so it can be cherry-picked onto main if the radius entry does not survive the usability check. Co-Authored-By: Claude Opus 4.8 --- ...nyi0_snn.js => analysis-modal-C3OTjBir.js} | 4 +- ....js.map => analysis-modal-C3OTjBir.js.map} | 2 +- .../{index-C-By5vWJ.js => index-By1A2oQD.js} | 134 +++++++++--------- dist/assets/index-By1A2oQD.js.map | 1 + dist/assets/index-C-By5vWJ.js.map | 1 - dist/index.html | 2 +- src/components/MapView.js | 67 ++++++++- 7 files changed, 137 insertions(+), 74 deletions(-) rename dist/assets/{analysis-modal-nyi0_snn.js => analysis-modal-C3OTjBir.js} (99%) rename dist/assets/{analysis-modal-nyi0_snn.js.map => analysis-modal-C3OTjBir.js.map} (99%) rename dist/assets/{index-C-By5vWJ.js => index-By1A2oQD.js} (82%) create mode 100644 dist/assets/index-By1A2oQD.js.map delete mode 100644 dist/assets/index-C-By5vWJ.js.map diff --git a/dist/assets/analysis-modal-nyi0_snn.js b/dist/assets/analysis-modal-C3OTjBir.js similarity index 99% rename from dist/assets/analysis-modal-nyi0_snn.js rename to dist/assets/analysis-modal-C3OTjBir.js index 5c923a6..43a24ce 100644 --- a/dist/assets/analysis-modal-nyi0_snn.js +++ b/dist/assets/analysis-modal-C3OTjBir.js @@ -1,4 +1,4 @@ -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-C-By5vWJ.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=>` +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-By1A2oQD.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=>`
@@ -8,4 +8,4 @@ import{_ as de}from"./geotiff-B9aROX49.js";import{M as pe}from"./bootstrap-D1-uv ${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-nyi0_snn.js.map +//# sourceMappingURL=analysis-modal-C3OTjBir.js.map diff --git a/dist/assets/analysis-modal-nyi0_snn.js.map b/dist/assets/analysis-modal-C3OTjBir.js.map similarity index 99% rename from dist/assets/analysis-modal-nyi0_snn.js.map rename to dist/assets/analysis-modal-C3OTjBir.js.map index e878789..c79847e 100644 --- a/dist/assets/analysis-modal-nyi0_snn.js.map +++ b/dist/assets/analysis-modal-C3OTjBir.js.map @@ -1 +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 \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
@@ -651,7 +651,7 @@ import{_ as Te}from"./geotiff-B9aROX49.js";import{M as gt}from"./bootstrap-D1-uv
${i.label}
- `).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 Go({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=` + `).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; @@ -711,7 +711,7 @@ import{_ as Te}from"./geotiff-B9aROX49.js";import{M as gt}from"./bootstrap-D1-uv - `,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 Go({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=` + `,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; @@ -739,48 +739,48 @@ This only affects the current session — built-in layers cannot be removed.`))r 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 $o}_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.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 Di{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=ni(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 ho(t.getRadius(),t.getCenter())}_attachRadiusAdjuster(e,t,r){if(r){t.setPosition(t.getPosition()),r.classList.add("measure-tooltip-adjust"),r.innerHTML=` + `,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 Ri{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=ri(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 go(t.getRadius(),t.getCenter())}_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=ri(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();const i=s=>{a.isConnected&&(s&&this.setCircleRadius(e,parseFloat(a.value)),a.replaceWith(r),this._refreshRadiusAdjuster(e))};a.addEventListener("keydown",s=>{s.key==="Enter"&&i(!0),s.key==="Escape"&&i(!1)}),a.addEventListener("blur",()=>i(!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 Yr){const s=ho(i.getRadius(),i.getCenter()),l=oi(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(),qt(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=It(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=It(a);this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),qt(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(),qt(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 co({group:!0,className:"map-tools-bar"}),r=new co({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 Ri(){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"),Gi())})}),Re}catch(n){return console.error("[PWA] Service Worker registration failed:",n),null}}let je=null,_e=null;function Ni(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){$i();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 $i(){const n=/iPad|iPhone|iPod/.test(navigator.userAgent),e=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);let t=`To install this app: +
`;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=ai(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();const i=s=>{a.isConnected&&(s&&this.setCircleRadius(e,parseFloat(a.value)),a.replaceWith(r),this._refreshRadiusAdjuster(e))};a.addEventListener("keydown",s=>{s.key==="Enter"&&i(!0),s.key==="Escape"&&i(!1)}),a.addEventListener("blur",()=>i(!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 Jr){const s=go(i.getRadius(),i.getCenter()),l=ni(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 Ni(){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"),ji())})}),Re}catch(n){return console.error("[PWA] Service Worker registration failed:",n),null}}let je=null,_e=null;function $i(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){Bi();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 Bi(){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 mo=null;const yo=new Set;function Bi(n="#offline-indicator"){mo=typeof n=="string"?document.querySelector(n):n,Kt(!navigator.onLine),window.addEventListener("online",()=>{console.log("[PWA] Back online"),Kt(!1),an(!1)}),window.addEventListener("offline",()=>{console.log("[PWA] Gone offline"),Kt(!0),an(!0)})}function Kt(n){mo&&(mo.style.display=n?"block":"none"),document.body.classList.toggle("is-offline",n)}function hr(n){return yo.add(n),n(!navigator.onLine),()=>yo.delete(n)}function an(n){for(const e of yo)try{e(n)}catch(t){console.error("[PWA] Offline listener error:",t)}}function K(){return navigator.onLine}function Gi(){confirm("A new version is available. Reload now?")&&ji()}function ji(){Re?.waiting&&Re.waiting.postMessage({type:"SKIP_WAITING"}),window.location.reload()}async function Ui({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 qi(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 Mo(n,e,t={},r=5e3,o=1e4){const a=await Ui({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 zi(){try{return(await Mo("GET_TILE_STATS","TILE_STATS")).stats}catch(n){return console.warn("[PWA] getTileCacheStats failed:",n),null}}async function Hi(){try{return await Mo("CLEAR_TILE_CACHES","TILE_CACHES_CLEARED"),!0}catch(n){return console.warn("[PWA] clearTileCaches failed:",n),!1}}async function Wi(n){if(!n)return!1;try{return!!(await Mo("CLEAR_TILE_CACHE","TILE_CACHE_CLEARED",{cacheName:n})).deleted}catch(e){return console.warn(`[PWA] clearTileCacheForProvider(${n}) failed:`,e),!1}}async function Vi(){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 Ki(n={}){const{installButton:e="#install-btn",offlineIndicator:t="#offline-indicator",autoRegisterSW:r=!0}=n;r&&await Ri(),Ni(e),Bi(t),console.log("[PWA] Initialized")}const gr={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"}},Xi=30*1024,Dt=2*Math.PI*6378137/2;function sn(n,e){const t=n/Dt*180;let r=e/Dt*180;return r=180/Math.PI*(2*Math.atan(Math.exp(r*Math.PI/180))-Math.PI/2),[t,r]}function ln(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 mr(n,e){const[t,r,o,a]=n,[i,s]=sn(t,r),[l,c]=sn(o,a),d=ln(i,c,e),u=ln(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 Yi(n,e,t){let r=0;for(let o=e;o<=t;o++)r+=mr(n,o).count;return r}function Ji(n,e,t){const r=[];for(let o=e;o<=t;o++){const a=mr(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 Zi(n,{z:e,x:t,y:r}){return n.replace("{z}",e).replace("{x}",t).replace("{y}",r)}class Qi{constructor({baseMap:e,extent3857:t,minZoom:r,maxZoom:o,concurrency:a=2,interBatchDelayMs:i=50,onProgress:s=()=>{}}){const l=gr[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=Ji(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=Zi(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 es=(()=>{const n=(r,o)=>{const a=r*Dt/180,i=Math.log(Math.tan((90+o)*Math.PI/360))/(Math.PI/180);return[a,i*Dt/180]},e=n(-3.3,4.5),t=n(1.2,11.2);return[e[0],e[1],t[0],t[1]]})();function ts(n){return n*Xi}const yr="https://api.lupmis4luspa.org/api",br=`${yr}/spatial_planning`;function os(n){return n.includes("/")?`${yr}/${n}`:`${br}/${n}`}const Xt="1",ns="1c46538c712e9b5b";function rs(){try{if(typeof window>"u")return Xt;const n=window.LUPMIS_SESSION;if(!n||typeof n!="object")return Xt;const e=n.district_id;return e==null||String(e).length===0?null:String(e)}catch{}return Xt}const Co={get district_id(){return rs()},api_token:ns};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 as=3e4,is=5e3;let De=null;async function ss(n=!1){if(De!==null&&!n)return De;const e=new AbortController,t=setTimeout(()=>e.abort(),is);try{De=(await fetch(`${br}/get_layers.php`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(Co),signal:e.signal})).ok}catch{De=!1}finally{clearTimeout(t)}return console.log("[RemoteDB] Server reachable:",De),De}function ke(){return De}function ls(n,e=as){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=os(n),o={...Co,...e};console.log("[RemoteDB] POST",r);const a=ls(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 cs(){return me("get_district_boundary.php")}async function ds(){return me("get_layers.php")}async function us(){return me("get_all_collector_zone_per_district.php")}async function ps(){return me("get_parcels_per_district.php")}async function fs(){return me("get_all_footprint_per_district.php")}async function hs(){const n=Co.district_id;return me("contours/get_by_district.php",{districtid:n==null?null:Number(n)})}async function gs(){return me("get_osm_roads.php")}async function ms(){return me("get_upn_grid_per_district.php")}async function ys(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 bs=63710088e-1,Ne=Math.PI/180;function ws(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*bs*Math.asin(Math.min(1,Math.sqrt(i)))}function cn(n,e=5){return n==null||Number.isNaN(n)?"—":n.toFixed(e)}const dn=6378137,un=1/298.257223563,pn=.9996,te=un*(2-un),Yt=te/(1-te);function _s(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=dn/Math.sqrt(1-te*l*l),p=d*d,f=Yt*c*c,h=c*(i-s),g=dn*((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=pn*u*(h+(1-p+f)*h**3/6+(5-18*p+p*p+72*f-58*Yt)*h**5/120)+5e5,y=pn*(g+u*d*(h*h/2+(5-p+9*f+4*f*f)*h**4/24+(61-58*p+p*p+600*f-330*Yt)*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 vs(n,e){const t=_s(n,e);return t?`${t.zone}${t.hemisphere} ${t.easting}E ${t.northing}N`:"—"}function Es(n){return n==null||Number.isNaN(n)?"—":n<1e3?`${Math.round(n)} m`:`${(n/1e3).toFixed(2)} km`}function xs(n){return n==null||Number.isNaN(n)?"—":`±${Math.round(n)} m`}function Ss(n){return n==null||Number.isNaN(n)?"none":n<=10?"good":n<=30?"fair":"poor"}const Ls={minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0,timeoutMs:15e3,maximumAgeMs:0};class pt{constructor(e={}){this.opts={...Ls,...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=ws(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 Ts={async createTrail(n){const e=n.districtId??Ye()?.district_id??null;return Ka({...n,districtId:e!=null?String(e):null})},addPoint:(n,e)=>Xa(n,e),finishTrail:(n,e)=>Ya(n,e),getUnsyncedTrails:()=>Ja(),getTrailPoints:n=>Za(n),markTrailSynced:(n,e)=>Qa(n,e)},ks={pushTrail:(n,e)=>ys(n,e),isOnline:()=>K()},ve=new pt({storage:Ts,sync:ks,minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0}),Is=new Set(["set:view","set:selected","clear:selected","set:basemap"]);function Ps({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(Jr(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"||!Is.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 Ms=[{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"}],wr={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 Cs(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 bo=null,N=null;function Ns(){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=Ms.map(n=>``).join(""),T.target.dataset.populated="1"),T.target&&!T.target.dataset.wired&&(T.target.dataset.wired="1",T.target.addEventListener("change",Er)),T.btnSave&&!T.btnSave.dataset.wired&&(T.btnSave.dataset.wired="1",T.btnSave.addEventListener("click",()=>hn("save"))),T.btnSaveUpload&&!T.btnSaveUpload.dataset.wired&&(T.btnSaveUpload.dataset.wired="1",T.btnSaveUpload.addEventListener("click",()=>hn("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 $s(){const n=N.targetType,e=wr[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?` — [${kt(a)}]`:" — [(empty)]"},r=[''].concat(N.sourceFields.map(o=>``)).join("");T.tbody.innerHTML=e.map(o=>{const a=N.mapping[o]||"",i=r.replace(`