From 631b3161189c028722e66bc5bac3bd59813566eb Mon Sep 17 00:00:00 2001 From: ekke Date: Tue, 8 Sep 2026 17:06:14 +0200 Subject: [PATCH] Adjust a measured circle's radius from its tooltip, in ground metres MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Officers asked to enter an exact distance rather than drag for it. The tooltip that appears when the second click lands now carries the adjustment: minus, the figure itself, plus. Tapping the figure swaps in a numeric input, so an exact radius can be typed; inputmode="decimal" brings up a keypad rather than a full keyboard on a phone. Not a dial. A radius runs from a few metres to several kilometres and no dial gives usable resolution across that range on a tooltip this size. Stepping snaps to an increment that grows with the value — 1 m below 50, 5 m below 200, 10 m below 1 km, 50 m below 5 km — so one tap means something at either end, and repeated taps land on round numbers instead of carrying the fraction the drag happened to finish on. Everything writes through MapTools.setCircleRadius(). Measurements were made read-only earlier precisely because a draggable handle moved the geometry while the popup went on reporting the radius captured at draw time; a single path means the geometry, the radius line, the tooltip and the analysis popup cannot drift apart again. The figures are now ground distances. The geometry stores a Web Mercator radius, which overstates the ground by 1/cos(latitude) — a 200 m circle at Wa was 197 m on the ground. That was invisible while dragging, because the label and the shape agreed with each other, but typing 200 is a claim about the ground. The conversion lives in units.js and is applied in the live tooltip, the adjuster and the Circle Analysis popup, so all three state the same distance. Two deliberate choices worth knowing before the usability check: - The tooltip is frozen where the draw finished. It is anchored to the circumference, so re-positioning on every change would slide the buttons out from under the officer's thumb. A large adjustment therefore leaves it sitting inside the circle rather than on its edge. - No measure-complete event fires on adjustment. Consumers read that as "a new measurement was drawn", and stepping would emit a stream of false ones. The overlay is created with stopEvent:false so the map keeps working through the label; the buttons stop their own events, or a tap would also pan the map. Service Worker version deliberately unchanged, so reverting does not leave clients on a newer cache than main. Co-Authored-By: Claude Opus 4.8 --- ...BcHNiMgQ.js => analysis-modal-nyi0_snn.js} | 4 +- ....js.map => analysis-modal-nyi0_snn.js.map} | 2 +- ...eotiff-BaoeLn6q.js => geotiff-B9aROX49.js} | 4 +- ...aoeLn6q.js.map => geotiff-B9aROX49.js.map} | 2 +- .../{index-ErUYnuzo.js => index-C-By5vWJ.js} | 214 +++++++++--------- dist/assets/index-C-By5vWJ.js.map | 1 + dist/assets/index-ErUYnuzo.js.map | 1 - ...ex.es-FRV-O2Jm.js => index.es-CW5N1l0t.js} | 4 +- ...V-O2Jm.js.map => index.es-CW5N1l0t.js.map} | 2 +- .../{jspdf-FJgf4ijI.js => jspdf-BsOD2LDR.js} | 8 +- ...-FJgf4ijI.js.map => jspdf-BsOD2LDR.js.map} | 2 +- ...{ol-ext-C5frl4dY.js => ol-ext-DV70ljNY.js} | 4 +- ...C5frl4dY.js.map => ol-ext-DV70ljNY.js.map} | 2 +- ...ers-J9qS6Th1.js => openlayers-D5-VSKzq.js} | 4 +- ...6Th1.js.map => openlayers-D5-VSKzq.js.map} | 2 +- ...ort-BONdNTHD.js => pdf-export-C0eAmANf.js} | 4 +- ...NTHD.js.map => pdf-export-C0eAmANf.js.map} | 2 +- dist/index.html | 88 ++++++- index.html | 80 +++++++ src/components/MapTools.js | 160 ++++++++++++- src/components/MapView.js | 11 +- src/units.js | 49 ++++ 22 files changed, 511 insertions(+), 139 deletions(-) rename dist/assets/{analysis-modal-BcHNiMgQ.js => analysis-modal-nyi0_snn.js} (98%) rename dist/assets/{analysis-modal-BcHNiMgQ.js.map => analysis-modal-nyi0_snn.js.map} (99%) rename dist/assets/{geotiff-BaoeLn6q.js => geotiff-B9aROX49.js} (99%) rename dist/assets/{geotiff-BaoeLn6q.js.map => geotiff-B9aROX49.js.map} (99%) rename dist/assets/{index-ErUYnuzo.js => index-C-By5vWJ.js} (70%) create mode 100644 dist/assets/index-C-By5vWJ.js.map delete mode 100644 dist/assets/index-ErUYnuzo.js.map rename dist/assets/{index.es-FRV-O2Jm.js => index.es-CW5N1l0t.js} (99%) rename dist/assets/{index.es-FRV-O2Jm.js.map => index.es-CW5N1l0t.js.map} (99%) rename dist/assets/{jspdf-FJgf4ijI.js => jspdf-BsOD2LDR.js} (99%) rename dist/assets/{jspdf-FJgf4ijI.js.map => jspdf-BsOD2LDR.js.map} (99%) rename dist/assets/{ol-ext-C5frl4dY.js => ol-ext-DV70ljNY.js} (99%) rename dist/assets/{ol-ext-C5frl4dY.js.map => ol-ext-DV70ljNY.js.map} (99%) rename dist/assets/{openlayers-J9qS6Th1.js => openlayers-D5-VSKzq.js} (99%) rename dist/assets/{openlayers-J9qS6Th1.js.map => openlayers-D5-VSKzq.js.map} (99%) rename dist/assets/{pdf-export-BONdNTHD.js => pdf-export-C0eAmANf.js} (88%) rename dist/assets/{pdf-export-BONdNTHD.js.map => pdf-export-C0eAmANf.js.map} (99%) diff --git a/dist/assets/analysis-modal-BcHNiMgQ.js b/dist/assets/analysis-modal-nyi0_snn.js similarity index 98% rename from dist/assets/analysis-modal-BcHNiMgQ.js rename to dist/assets/analysis-modal-nyi0_snn.js index cb4d90c..5c923a6 100644 --- a/dist/assets/analysis-modal-BcHNiMgQ.js +++ b/dist/assets/analysis-modal-nyi0_snn.js @@ -1,4 +1,4 @@ -import{_ as de}from"./geotiff-BaoeLn6q.js";import{M as pe}from"./bootstrap-D1-uvFxm.js";import{ax as ye}from"./openlayers-J9qS6Th1.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-ErUYnuzo.js";import"./pako-Xa-UToif.js";import"./ol-ext-C5frl4dY.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-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=>`
@@ -8,4 +8,4 @@ import{_ as de}from"./geotiff-BaoeLn6q.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-BcHNiMgQ.js.map +//# sourceMappingURL=analysis-modal-nyi0_snn.js.map diff --git a/dist/assets/analysis-modal-BcHNiMgQ.js.map b/dist/assets/analysis-modal-nyi0_snn.js.map similarity index 99% rename from dist/assets/analysis-modal-BcHNiMgQ.js.map rename to dist/assets/analysis-modal-nyi0_snn.js.map index 4fa5988..e878789 100644 --- a/dist/assets/analysis-modal-BcHNiMgQ.js.map +++ b/dist/assets/analysis-modal-nyi0_snn.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 @@ -493,7 +493,7 @@ import{_ as Le}from"./geotiff-BaoeLn6q.js";import{M as ht}from"./bootstrap-D1-uv - `;this.parcelEditElement.innerHTML=i,this.parcelEditPopup.setPosition(t),this.parcelEditElement.querySelector(".parcel-edit-close").addEventListener("click",()=>{this.hideParcelEditPopup()}),this.parcelEditElement.querySelector(".parcel-edit-cancel").addEventListener("click",()=>{this.hideParcelEditPopup()});const s=this.parcelEditElement.querySelector(".parcel-edit-form");s.addEventListener("submit",l=>{l.preventDefault();const c=new FormData(s),d={};for(const[u,f]of c.entries())d[u]=f;d._layerType="parcel";for(const[u,f]of Object.entries(d))this._parcelEditFeature.set(u,f);for(const u of this._parcelEditCallbacks)u(this._parcelEditFeature,d);this.hideParcelEditPopup()})}hideParcelEditPopup(){this.parcelEditPopup.setPosition(void 0),this._parcelEditFeature=null}onParcelEdit(e){this._parcelEditCallbacks.push(e)}createMergePopup(){this.mergePopupElement=document.createElement("div"),this.mergePopupElement.className="map-merge-popup",this.mergePopupElement.style.cssText=` + `;this.parcelEditElement.innerHTML=i,this.parcelEditPopup.setPosition(t),this.parcelEditElement.querySelector(".parcel-edit-close").addEventListener("click",()=>{this.hideParcelEditPopup()}),this.parcelEditElement.querySelector(".parcel-edit-cancel").addEventListener("click",()=>{this.hideParcelEditPopup()});const s=this.parcelEditElement.querySelector(".parcel-edit-form");s.addEventListener("submit",l=>{l.preventDefault();const c=new FormData(s),d={};for(const[u,p]of c.entries())d[u]=p;d._layerType="parcel";for(const[u,p]of Object.entries(d))this._parcelEditFeature.set(u,p);for(const u of this._parcelEditCallbacks)u(this._parcelEditFeature,d);this.hideParcelEditPopup()})}hideParcelEditPopup(){this.parcelEditPopup.setPosition(void 0),this._parcelEditFeature=null}onParcelEdit(e){this._parcelEditCallbacks.push(e)}createMergePopup(){this.mergePopupElement=document.createElement("div"),this.mergePopupElement.className="map-merge-popup",this.mergePopupElement.style.cssText=` position: absolute; background: var(--card, #fff); color: var(--card-foreground, #1e1a4b); @@ -542,7 +542,7 @@ import{_ as Le}from"./geotiff-BaoeLn6q.js";import{M as ht}from"./bootstrap-D1-uv - `;this.mergePopupElement.innerHTML=c,this.mergePopup.setPosition(o);const d=()=>{this.mergePopup.setPosition(void 0)};this.mergePopupElement.querySelector(".merge-popup-close").addEventListener("click",d),this.mergePopupElement.querySelector(".merge-popup-cancel").addEventListener("click",d),this.mergePopupElement.querySelector(".merge-popup-confirm").addEventListener("click",()=>{const g=this.mergePopupElement.querySelector('input[name="merge-choice"]:checked').value==="A"?t:r,m=["geometry"];for(const[y,b]of Object.entries(g))m.includes(y)||e.set(y,b);e.set("_layerType","parcel");for(const y of this._parcelEditCallbacks)y(e,g);d()});const u=this.mergePopupElement.querySelectorAll("label"),f=this.mergePopupElement.querySelectorAll('input[name="merge-choice"]'),p=()=>{u.forEach(h=>{const g=h.querySelector("input");h.style.borderColor=g.checked?g.value==="A"?"#0ea5e9":"#f59e0b":"var(--border, #1e1a4b1f)"})};f.forEach(h=>h.addEventListener("change",p)),p()}createDividePopup(){this.dividePopupElement=document.createElement("div"),this.dividePopupElement.className="map-divide-popup",this.dividePopupElement.style.cssText=` + `;this.mergePopupElement.innerHTML=c,this.mergePopup.setPosition(o);const d=()=>{this.mergePopup.setPosition(void 0)};this.mergePopupElement.querySelector(".merge-popup-close").addEventListener("click",d),this.mergePopupElement.querySelector(".merge-popup-cancel").addEventListener("click",d),this.mergePopupElement.querySelector(".merge-popup-confirm").addEventListener("click",()=>{const g=this.mergePopupElement.querySelector('input[name="merge-choice"]:checked').value==="A"?t:r,m=["geometry"];for(const[y,w]of Object.entries(g))m.includes(y)||e.set(y,w);e.set("_layerType","parcel");for(const y of this._parcelEditCallbacks)y(e,g);d()});const u=this.mergePopupElement.querySelectorAll("label"),p=this.mergePopupElement.querySelectorAll('input[name="merge-choice"]'),f=()=>{u.forEach(h=>{const g=h.querySelector("input");h.style.borderColor=g.checked?g.value==="A"?"#0ea5e9":"#f59e0b":"var(--border, #1e1a4b1f)"})};p.forEach(h=>h.addEventListener("change",f)),f()}createDividePopup(){this.dividePopupElement=document.createElement("div"),this.dividePopupElement.className="map-divide-popup",this.dividePopupElement.style.cssText=` position: absolute; background: var(--card, #fff); color: var(--card-foreground, #1e1a4b); @@ -592,20 +592,20 @@ import{_ as Le}from"./geotiff-BaoeLn6q.js";import{M as ht}from"./bootstrap-D1-uv overflow: hidden; display: flex; flex-direction: column; - `,this.drawnPolygonPopup=new Ee({element:this.drawnPolygonElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.drawnPolygonPopup),this._drawnPolygonCallbacks=[],this._drawnPolygonFeature=null}getParcelAttributeKeys(){const e=["geometry","_layerType"],t=[],r=o=>{t.length>0||o.getLayers().forEach(a=>{if(!(t.length>0)){if(a instanceof Pe)r(a);else if(a instanceof D){const i=a.getSource();if(!i)return;for(const s of i.getFeatures()){if(s.get("_layerType")!=="parcel")continue;const l=s.getProperties();for(const c of Object.keys(l))e.includes(c)||t.push(c);return}}}})};return r(this.overlayGroup),t}showDrawnPolygonPopup(e,t){this._drawnPolygonFeature=e;const r=this.getParcelAttributeKeys();if(r.length===0){console.warn("[MapView] No parcel attributes found — cannot build form");return}let o="";for(const d of r){const u=this.escapeHtml(d);o+=` + `,this.drawnPolygonPopup=new Ee({element:this.drawnPolygonElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.drawnPolygonPopup),this._drawnPolygonCallbacks=[],this._drawnPolygonFeature=null}getParcelAttributeKeys(){const e=["geometry","_layerType"],t=[],r=o=>{t.length>0||o.getLayers().forEach(a=>{if(!(t.length>0)){if(a instanceof Me)r(a);else if(a instanceof D){const i=a.getSource();if(!i)return;for(const s of i.getFeatures()){if(s.get("_layerType")!=="parcel")continue;const l=s.getProperties();for(const c of Object.keys(l))e.includes(c)||t.push(c);return}}}})};return r(this.overlayGroup),t}showDrawnPolygonPopup(e,t){this._drawnPolygonFeature=e;const r=this.getParcelAttributeKeys();if(r.length===0){console.warn("[MapView] No parcel attributes found — cannot build form");return}let o="";for(const d of r){const u=this.escapeHtml(d);o+=`
- `}const a=e.getGeometry(),i=ct(a,{projection:"EPSG:3857"}),l=` + `}const a=e.getGeometry(),i=dt(a,{projection:"EPSG:3857"}),l=`
📐 Polygon Attributes
- Area: ${dt(i)} + Area: ${We(i)}
${o} @@ -618,7 +618,7 @@ import{_ as Le}from"./geotiff-BaoeLn6q.js";import{M as ht}from"./bootstrap-D1-uv
- `;this.drawnPolygonElement.innerHTML=l,this.drawnPolygonPopup.setPosition(t),this.drawnPolygonElement.querySelector(".drawn-polygon-close").addEventListener("click",()=>{this.hideDrawnPolygonPopup()}),this.drawnPolygonElement.querySelector(".drawn-polygon-cancel").addEventListener("click",()=>{this.hideDrawnPolygonPopup()});const c=this.drawnPolygonElement.querySelector(".drawn-polygon-form");c.addEventListener("submit",d=>{d.preventDefault();const u=new FormData(c),f={};for(const[p,h]of u.entries())f[p]=h;for(const[p,h]of Object.entries(f))this._drawnPolygonFeature.set(p,h);this._drawnPolygonFeature.set("_layerType","parcel");for(const p of this._drawnPolygonCallbacks)p(this._drawnPolygonFeature,f);this.hideDrawnPolygonPopup()})}hideDrawnPolygonPopup(){this.drawnPolygonPopup.setPosition(void 0),this._drawnPolygonFeature=null}onDrawnPolygonSave(e){this._drawnPolygonCallbacks.push(e)}onFeatureModified(e){this._featureModifiedCallbacks||(this._featureModifiedCallbacks=[]),this._featureModifiedCallbacks.push(e)}onDrawnPoint(e){this._drawnPointCallbacks||(this._drawnPointCallbacks=[]),this._drawnPointCallbacks.push(e)}onDblClick(e){return this.dblClickCallbacks.push(e),this.dblClickCallbacks.length===1&&this.map.on("dblclick",t=>{const[r,o]=Oe(t.coordinate);let a=null;this.map.forEachFeatureAtPixel(t.pixel,i=>(a=i,!0)),a&&(t.preventDefault(),t.stopPropagation());for(const i of this.dblClickCallbacks)i(r,o,a,t);if(a)return!1}),()=>{const t=this.dblClickCallbacks.indexOf(e);t>-1&&this.dblClickCallbacks.splice(t,1)}}escapeHtml(e){if(!e)return"";const t=document.createElement("div");return t.textContent=e,t.innerHTML}createAddLocationPopup(){this.addLocationPopupElement=document.createElement("div"),this.addLocationPopupElement.className="map-add-location-popup",this.addLocationPopupElement.innerHTML=` + `;this.drawnPolygonElement.innerHTML=l,this.drawnPolygonPopup.setPosition(t),this.drawnPolygonElement.querySelector(".drawn-polygon-close").addEventListener("click",()=>{this.hideDrawnPolygonPopup()}),this.drawnPolygonElement.querySelector(".drawn-polygon-cancel").addEventListener("click",()=>{this.hideDrawnPolygonPopup()});const c=this.drawnPolygonElement.querySelector(".drawn-polygon-form");c.addEventListener("submit",d=>{d.preventDefault();const u=new FormData(c),p={};for(const[f,h]of u.entries())p[f]=h;for(const[f,h]of Object.entries(p))this._drawnPolygonFeature.set(f,h);this._drawnPolygonFeature.set("_layerType","parcel");for(const f of this._drawnPolygonCallbacks)f(this._drawnPolygonFeature,p);this.hideDrawnPolygonPopup()})}hideDrawnPolygonPopup(){this.drawnPolygonPopup.setPosition(void 0),this._drawnPolygonFeature=null}onDrawnPolygonSave(e){this._drawnPolygonCallbacks.push(e)}onFeatureModified(e){this._featureModifiedCallbacks||(this._featureModifiedCallbacks=[]),this._featureModifiedCallbacks.push(e)}onDrawnPoint(e){this._drawnPointCallbacks||(this._drawnPointCallbacks=[]),this._drawnPointCallbacks.push(e)}onDblClick(e){return this.dblClickCallbacks.push(e),this.dblClickCallbacks.length===1&&this.map.on("dblclick",t=>{const[r,o]=xe(t.coordinate);let a=null;this.map.forEachFeatureAtPixel(t.pixel,i=>(a=i,!0)),a&&(t.preventDefault(),t.stopPropagation());for(const i of this.dblClickCallbacks)i(r,o,a,t);if(a)return!1}),()=>{const t=this.dblClickCallbacks.indexOf(e);t>-1&&this.dblClickCallbacks.splice(t,1)}}escapeHtml(e){if(!e)return"";const t=document.createElement("div");return t.textContent=e,t.innerHTML}createAddLocationPopup(){this.addLocationPopupElement=document.createElement("div"),this.addLocationPopupElement.className="map-add-location-popup",this.addLocationPopupElement.innerHTML=`
➕ Add Location @@ -643,7 +643,7 @@ import{_ as Le}from"./geotiff-BaoeLn6q.js";import{M as ht}from"./bootstrap-D1-uv
- `,this.addLocationPopup=new Ee({element:this.addLocationPopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.addLocationPopup),this.addLocationCoords=null,this.addLocationPopupElement.querySelector(".add-location-popup-close").addEventListener("click",()=>{this.hideAddLocationPopup()}),this.addLocationCallbacks=[]}showAddLocationPopup(e){const[t,r]=Oe(e);this.addLocationCoords={lon:t,lat:r};const o=this.addLocationPopupElement.querySelector("#map-location-coords");o.textContent=`${t.toFixed(6)}, ${r.toFixed(6)}`,this.addLocationPopupElement.querySelector("#map-add-location-form").reset(),this.addLocationPopup.setPosition(e)}hideAddLocationPopup(){this.addLocationPopup.setPosition(void 0),this.addLocationCoords=null,this._discardPendingLocationSketch()}_discardPendingLocationSketch(){const e=this._pendingLocationSketch;e&&(this._pendingLocationSketch=null,this._withoutUndoRecording(()=>{try{this.drawingsSource.removeFeature(e)}catch{}}))}onAddLocation(e){if(this.addLocationCallbacks.push(e),this.addLocationCallbacks.length===1){const t=this.addLocationPopupElement.querySelector("#map-add-location-form");t.addEventListener("submit",r=>{if(r.preventDefault(),!this.addLocationCoords)return;const o=new FormData(t),a={name:o.get("name"),category:o.get("category"),description:o.get("description"),lon:this.addLocationCoords.lon,lat:this.addLocationCoords.lat};this.addLocationCallbacks.forEach(i=>i(a)),this.hideAddLocationPopup()})}}createBaseLayers(e){const t=new ae({title:"Topographic",type:"base",zIndex:-100,visible:e==="topo",source:new Me({url:"https://{a-c}.tile.opentopomap.org/{z}/{x}/{y}.png",attributions:"Map data: © OpenTopoMap",maxZoom:17,crossOrigin:"anonymous"})});t.set("basemapKey","topo");const r=new ae({title:"Carto Light",type:"base",zIndex:-100,visible:e==="carto-light",source:new Me({url:"https://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});r.set("basemapKey","carto-light");const o=new ae({title:"Carto Dark",type:"base",zIndex:-100,visible:e==="carto-dark",source:new Me({url:"https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});o.set("basemapKey","carto-dark");const a=new ae({title:"OSM Cycle map",type:"base",zIndex:-100,visible:!1,source:new $o({url:"https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=ae1339c46dd3446b9c491e7336d38760"})});a.set("basemapKey","cycle");const i=new ae({title:"Satellite",type:"base",zIndex:-100,visible:e==="satellite",source:new Me({url:"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",attributions:"Tiles © Esri",maxZoom:19,crossOrigin:"anonymous"})});i.set("basemapKey","satellite");const s=new ae({title:"Google Sat",type:"base",zIndex:-100,visible:e==="googlesat",source:new Me({url:"http://mt0.google.com/vt/lyrs=y&hl=en&x={x}&y={y}&z={z}&s=Ga",attributions:"Tiles © Google",maxZoom:19,crossOrigin:"anonymous"})});s.set("basemapKey","googlesat");const l=new ae({title:"OpenStreetMap",type:"base",zIndex:-100,visible:e==="osm",source:new $o});l.set("basemapKey","osm"),this._baseMapLayers=[r,o,a,i,s,l,t];const c=new Pe({title:"Base Maps",layers:[r,o,i,a,s,l,t]});return c.set("displayInLayerSwitcher",!1),c}setBaseMap(e){if(!this._baseMapLayers)return!1;if(e==="none"){for(const r of this._baseMapLayers)r.setVisible(!1);return console.log("[MapView] Base map switched off (none)"),this.map.dispatchEvent({type:"basemapchange",key:"none"}),!0}let t=!1;for(const r of this._baseMapLayers){const o=r.get("basemapKey")===e;r.setVisible(o),o&&(t=!0)}return t&&(console.log("[MapView] Base map switched to:",e),this.map.dispatchEvent({type:"basemapchange",key:e})),t}_createBaseMapPicker(){const e=[{key:"topo",label:"Topographic",grad:"linear-gradient(135deg,#e8d5b7,#a67c52)"},{key:"osm",label:"OpenStreetMap",grad:"linear-gradient(135deg,#d4e6f1,#85c1e9)"},{key:"satellite",label:"Satellite",grad:"linear-gradient(135deg,#1b4332,#40916c)"},{key:"googlesat",label:"Google Sat",grad:"linear-gradient(135deg,#2a5d3d,#4a8c5a)"},{key:"carto-light",label:"Carto Light",grad:"linear-gradient(135deg,#f5f5f5,#d4d4d4)"},{key:"carto-dark",label:"Carto Dark",grad:"linear-gradient(135deg,#1a1a2e,#0f3460)"},{key:"none",label:"None",grad:"repeating-conic-gradient(#e5e7eb 0 25%, #fff 0 50%) 50% / 12px 12px"}],t=this.map.getTargetElement();if(!t)return;const r=document.createElement("button");r.type="button",r.className="ls-basemap-toggle",r.title="Switch base map",r.setAttribute("aria-label","Switch base map"),r.innerHTML='',t.appendChild(r);const o=document.createElement("div");o.className="ls-basemap-panel",o.innerHTML='
Base Map
'+e.map(i=>` + `,this.addLocationPopup=new Ee({element:this.addLocationPopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.addLocationPopup),this.addLocationCoords=null,this.addLocationPopupElement.querySelector(".add-location-popup-close").addEventListener("click",()=>{this.hideAddLocationPopup()}),this.addLocationCallbacks=[]}showAddLocationPopup(e){const[t,r]=xe(e);this.addLocationCoords={lon:t,lat:r};const o=this.addLocationPopupElement.querySelector("#map-location-coords");o.textContent=`${t.toFixed(6)}, ${r.toFixed(6)}`,this.addLocationPopupElement.querySelector("#map-add-location-form").reset(),this.addLocationPopup.setPosition(e)}hideAddLocationPopup(){this.addLocationPopup.setPosition(void 0),this.addLocationCoords=null,this._discardPendingLocationSketch()}_discardPendingLocationSketch(){const e=this._pendingLocationSketch;e&&(this._pendingLocationSketch=null,this._withoutUndoRecording(()=>{try{this.drawingsSource.removeFeature(e)}catch{}}))}onAddLocation(e){if(this.addLocationCallbacks.push(e),this.addLocationCallbacks.length===1){const t=this.addLocationPopupElement.querySelector("#map-add-location-form");t.addEventListener("submit",r=>{if(r.preventDefault(),!this.addLocationCoords)return;const o=new FormData(t),a={name:o.get("name"),category:o.get("category"),description:o.get("description"),lon:this.addLocationCoords.lon,lat:this.addLocationCoords.lat};this.addLocationCallbacks.forEach(i=>i(a)),this.hideAddLocationPopup()})}}createBaseLayers(e){const t=new ae({title:"Topographic",type:"base",zIndex:-100,visible:e==="topo",source:new Ce({url:"https://{a-c}.tile.opentopomap.org/{z}/{x}/{y}.png",attributions:"Map data: © OpenTopoMap",maxZoom:17,crossOrigin:"anonymous"})});t.set("basemapKey","topo");const r=new ae({title:"Carto Light",type:"base",zIndex:-100,visible:e==="carto-light",source:new Ce({url:"https://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});r.set("basemapKey","carto-light");const o=new ae({title:"Carto Dark",type:"base",zIndex:-100,visible:e==="carto-dark",source:new Ce({url:"https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});o.set("basemapKey","carto-dark");const a=new ae({title:"OSM Cycle map",type:"base",zIndex:-100,visible:!1,source:new Bo({url:"https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=ae1339c46dd3446b9c491e7336d38760"})});a.set("basemapKey","cycle");const i=new ae({title:"Satellite",type:"base",zIndex:-100,visible:e==="satellite",source:new Ce({url:"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",attributions:"Tiles © Esri",maxZoom:19,crossOrigin:"anonymous"})});i.set("basemapKey","satellite");const s=new ae({title:"Google Sat",type:"base",zIndex:-100,visible:e==="googlesat",source:new Ce({url:"http://mt0.google.com/vt/lyrs=y&hl=en&x={x}&y={y}&z={z}&s=Ga",attributions:"Tiles © Google",maxZoom:19,crossOrigin:"anonymous"})});s.set("basemapKey","googlesat");const l=new ae({title:"OpenStreetMap",type:"base",zIndex:-100,visible:e==="osm",source:new Bo});l.set("basemapKey","osm"),this._baseMapLayers=[r,o,a,i,s,l,t];const c=new Me({title:"Base Maps",layers:[r,o,i,a,s,l,t]});return c.set("displayInLayerSwitcher",!1),c}setBaseMap(e){if(!this._baseMapLayers)return!1;if(e==="none"){for(const r of this._baseMapLayers)r.setVisible(!1);return console.log("[MapView] Base map switched off (none)"),this.map.dispatchEvent({type:"basemapchange",key:"none"}),!0}let t=!1;for(const r of this._baseMapLayers){const o=r.get("basemapKey")===e;r.setVisible(o),o&&(t=!0)}return t&&(console.log("[MapView] Base map switched to:",e),this.map.dispatchEvent({type:"basemapchange",key:e})),t}_createBaseMapPicker(){const e=[{key:"topo",label:"Topographic",grad:"linear-gradient(135deg,#e8d5b7,#a67c52)"},{key:"osm",label:"OpenStreetMap",grad:"linear-gradient(135deg,#d4e6f1,#85c1e9)"},{key:"satellite",label:"Satellite",grad:"linear-gradient(135deg,#1b4332,#40916c)"},{key:"googlesat",label:"Google Sat",grad:"linear-gradient(135deg,#2a5d3d,#4a8c5a)"},{key:"carto-light",label:"Carto Light",grad:"linear-gradient(135deg,#f5f5f5,#d4d4d4)"},{key:"carto-dark",label:"Carto Dark",grad:"linear-gradient(135deg,#1a1a2e,#0f3460)"},{key:"none",label:"None",grad:"repeating-conic-gradient(#e5e7eb 0 25%, #fff 0 50%) 50% / 12px 12px"}],t=this.map.getTargetElement();if(!t)return;const r=document.createElement("button");r.type="button",r.className="ls-basemap-toggle",r.title="Switch base map",r.setAttribute("aria-label","Switch base map"),r.innerHTML='',t.appendChild(r);const o=document.createElement("div");o.className="ls-basemap-panel",o.innerHTML='
Base Map
'+e.map(i=>` - `).join("")+"
",t.appendChild(o),this._basemapPanel=o,this._basemapToggle=r;const a=i=>{const s=i||this._baseMapLayers?.find(l=>l.getVisible())?.get("basemapKey");o.querySelectorAll('input[name="lupmis-basemap"]').forEach(l=>{l.checked=l.value===s})};a(),r.addEventListener("click",i=>{i.stopPropagation();const s=!o.classList.contains("open");o.classList.toggle("open",s),r.classList.toggle("active",s),s&&a()}),document.addEventListener("click",i=>{o.classList.contains("open")&&(o.contains(i.target)||r.contains(i.target)||(o.classList.remove("open"),r.classList.remove("active")))}),o.addEventListener("change",i=>{const s=i.target.closest('input[type=radio][name="lupmis-basemap"]');if(!s)return;const l=s.value;this.setBaseMap(l);try{localStorage.setItem("default-basemap",l)}catch{}o.classList.remove("open"),r.classList.remove("active")}),this.map.on("basemapchange",i=>a(i.key))}_initGpsRendering(){this._gpsPositionSource=new G,this._gpsTrailSource=new G,this._gpsTrailCoords=[],this._gpsTrailLayer=new D({source:this._gpsTrailSource,zIndex:940,style:new M({stroke:new I({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 I({color:"rgba(0,94,184,0.35)",width:1})}):new M({image:new fe({radius:7,fill:new C({color:"#005eb8"}),stroke:new I({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 pe({geometry:new pt([this._circleRing(o,i)])});s.set("_kind","accuracy"),this._gpsPositionSource.addFeature(s)}const a=new pe({geometry:new mt(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 pe({geometry:new he(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 fe({radius:22,fill:new C({color:"rgba(220, 38, 38, 0.25)"}),stroke:new I({color:"#dc2626",width:3})})}),new M({text:new Ve({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 pe({geometry:new mt(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 pe({geometry:new mt(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 Oe(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]=Oe(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]=Oe(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:f=null,pointStrokeColor:p="#ffffff",pointStrokeWidth:h=1.5}=r,g=new G({features:new le().readFeatures(e,{featureProjection:"EPSG:3857"})}),m=new C({color:l}),y=new fe({radius:u,fill:new C({color:f||a}),stroke:new I({color:p,width:h})}),b=new I({color:a,width:i,...s?{lineDash:s}:{}});let _;if(c){const x=d??i+2;_=[new M({stroke:new I({color:c,width:x})}),new M({stroke:b,fill:m,image:y})]}else _=new M({stroke:b,fill:m,image:y});const S=new D({title:t,source:g,style:_,...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(),P=L(x[0]?.getGeometry?.()?.getType?.());if(P)S.set("typeDescription",P);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 Pe({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 Bo({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 Me({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([Le(()=>import("./geotiff-BaoeLn6q.js").then(d=>d.W),__vite__mapDeps([0,1,2,3])),Le(()=>import("./geotiff-BaoeLn6q.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",f=>{console.warn(`[MapView] COG "${t}" source error:`,f),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 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=` 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 Le}from"./geotiff-BaoeLn6q.js";import{M as ht}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 Bo({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 I({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 Me({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",f=e.getSource()?.getFeatures().length??0,p=e.get("_externalImportErrorCount")??0,h=(()=>{switch(u){case"mapped":return{text:`Upload ${f}`,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:`${p} 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 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=` 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,42 +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 No}_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 $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._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 Ci{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 I({color:"#8B008B",lineDash:[10,10],width:2}),image:new fe({radius:5,stroke:new I({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 I({color:"#8B008B",width:2}),image:new fe({radius:6,stroke:new I({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)}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 Fe({source:this.measureSource,type:"Circle",style:new M({fill:new C({color:"rgba(255, 233, 106, 0.2)"}),stroke:new I({color:"rgba(139, 0, 139, 0.7)",lineDash:[10,10],width:2}),image:new fe({radius:5,stroke:new I({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 Kr){const s=i.getRadius(),l=ei(s),d=`${Ot(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 pe({geometry:new he([i,[i[0]+s,i[1]]])});l.set("_layerType","measure_circle_radius"),this.measureSource.addFeature(l),this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltip.setOffset([0,-7]),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 Fe({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=kt(i),l=Ot(s);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(i.getLastCoordinate())})}),e.on("drawend",r=>{const o=r.feature,a=o.getGeometry(),i=kt(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 Fe({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=ct(i),l=dt(s);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(i.getInteriorPoint().getCoordinates())})}),e.on("drawend",r=>{const o=r.feature,a=o.getGeometry(),i=ct(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 Fe({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 Fe({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 Fe({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 xe({html:'⭕',title:"Measure Circle (radius & area)",className:"measure-circle-btn",onToggle:l=>{l?this.startCircleMeasure():this.deactivate()}});r.addControl(o);const a=new xe({html:'📏',title:"Measure Distance",className:"measure-line-btn",onToggle:l=>{l?this.startLineMeasure():this.deactivate()}});r.addControl(a);const i=new xe({html:'⬛',title:"Measure Area",className:"measure-area-btn",onToggle:l=>{l?this.startAreaMeasure():this.deactivate()}});r.addControl(i);const s=new st({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 Ai(){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"),Ri())})}),Re}catch(n){return console.error("[PWA] Service Worker registration failed:",n),null}}let je=null,_e=null;function Fi(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){Oi();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 Oi(){const n=/iPad|iPhone|iPod/.test(navigator.userAgent),e=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);let t=`To install this app: + `,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=` +
+ + + +
+
`;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: `;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 go=null;const mo=new Set;function Di(n="#offline-indicator"){go=typeof n=="string"?document.querySelector(n):n,Kt(!navigator.onLine),window.addEventListener("online",()=>{console.log("[PWA] Back online"),Kt(!1),rn(!1)}),window.addEventListener("offline",()=>{console.log("[PWA] Gone offline"),Kt(!0),rn(!0)})}function Kt(n){go&&(go.style.display=n?"block":"none"),document.body.classList.toggle("is-offline",n)}function pr(n){return mo.add(n),n(!navigator.onLine),()=>mo.delete(n)}function rn(n){for(const e of mo)try{e(n)}catch(t){console.error("[PWA] Offline listener error:",t)}}function K(){return navigator.onLine}function Ri(){confirm("A new version is available. Reload now?")&&Ni()}function Ni(){Re?.waiting&&Re.waiting.postMessage({type:"SKIP_WAITING"}),window.location.reload()}async function $i({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 Bi(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 Po(n,e,t={},r=5e3,o=1e4){const a=await $i({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,...f}=d.data;i(f)}},a.postMessage({type:n,...t},[l.port2])})}async function Gi(){try{return(await Po("GET_TILE_STATS","TILE_STATS")).stats}catch(n){return console.warn("[PWA] getTileCacheStats failed:",n),null}}async function ji(){try{return await Po("CLEAR_TILE_CACHES","TILE_CACHES_CLEARED"),!0}catch(n){return console.warn("[PWA] clearTileCaches failed:",n),!1}}async function Ui(n){if(!n)return!1;try{return!!(await Po("CLEAR_TILE_CACHE","TILE_CACHE_CLEARED",{cacheName:n})).deleted}catch(e){return console.warn(`[PWA] clearTileCacheForProvider(${n}) failed:`,e),!1}}async function qi(){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 zi(n={}){const{installButton:e="#install-btn",offlineIndicator:t="#offline-indicator",autoRegisterSW:r=!0}=n;r&&await Ai(),Fi(e),Di(t),console.log("[PWA] Initialized")}const fr={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"}},Hi=30*1024,Dt=2*Math.PI*6378137/2;function an(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 sn(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 hr(n,e){const[t,r,o,a]=n,[i,s]=an(t,r),[l,c]=an(o,a),d=sn(i,c,e),u=sn(l,s,e),f=Math.pow(2,e),p=Math.max(0,Math.min(d.x,u.x)),h=Math.min(f-1,Math.max(d.x,u.x)),g=Math.max(0,Math.min(d.y,u.y)),m=Math.min(f-1,Math.max(d.y,u.y));return{z:e,minX:p,maxX:h,minY:g,maxY:m,count:(h-p+1)*(m-g+1)}}function Wi(n,e,t){let r=0;for(let o=e;o<=t;o++)r+=hr(n,o).count;return r}function Vi(n,e,t){const r=[];for(let o=e;o<=t;o++){const a=hr(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 Ki(n,{z:e,x:t,y:r}){return n.replace("{z}",e).replace("{x}",t).replace("{y}",r)}class Xi{constructor({baseMap:e,extent3857:t,minZoom:r,maxZoom:o,concurrency:a=2,interBatchDelayMs:i=50,onProgress:s=()=>{}}){const l=fr[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=Vi(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 f=Ki(this.template,u);try{const p=await fetch(f,{signal:this._abortCtrl.signal,cache:"default"});p.ok?(a++,p.body&&p.body.cancel().catch(()=>{})):(p.status,i++)}catch(p){p.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 Yi=(()=>{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 Ji(n){return n*Hi}const gr="https://api.lupmis4luspa.org/api",mr=`${gr}/spatial_planning`;function Zi(n){return n.includes("/")?`${gr}/${n}`:`${mr}/${n}`}const Xt="1",Qi="1c46538c712e9b5b";function es(){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 Mo={get district_id(){return es()},api_token:Qi};function Xe(){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 ts=3e4,os=5e3;let De=null;async function ns(n=!1){if(De!==null&&!n)return De;const e=new AbortController,t=setTimeout(()=>e.abort(),os);try{De=(await fetch(`${mr}/get_layers.php`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(Mo),signal:e.signal})).ok}catch{De=!1}finally{clearTimeout(t)}return console.log("[RemoteDB] Server reachable:",De),De}function Te(){return De}function rs(n,e=ts){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=Zi(n),o={...Mo,...e};console.log("[RemoteDB] POST",r);const a=rs(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 as(){return me("get_district_boundary.php")}async function is(){return me("get_layers.php")}async function ss(){return me("get_all_collector_zone_per_district.php")}async function ls(){return me("get_parcels_per_district.php")}async function cs(){return me("get_all_footprint_per_district.php")}async function ds(){const n=Mo.district_id;return me("contours/get_by_district.php",{districtid:n==null?null:Number(n)})}async function us(){return me("get_osm_roads.php")}async function ps(){return me("get_upn_grid_per_district.php")}async function fs(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 hs=63710088e-1,Ne=Math.PI/180;function gs(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*hs*Math.asin(Math.min(1,Math.sqrt(i)))}function ln(n,e=5){return n==null||Number.isNaN(n)?"—":n.toFixed(e)}const cn=6378137,dn=1/298.257223563,un=.9996,te=dn*(2-dn),Yt=te/(1-te);function ms(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=cn/Math.sqrt(1-te*l*l),f=d*d,p=Yt*c*c,h=c*(i-s),g=cn*((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=un*u*(h+(1-f+p)*h**3/6+(5-18*f+f*f+72*p-58*Yt)*h**5/120)+5e5,y=un*(g+u*d*(h*h/2+(5-f+9*p+4*p*p)*h**4/24+(61-58*f+f*f+600*p-330*Yt)*h**6/720));const b=n>=0?"N":"S";return b==="S"&&(y+=1e7),{zone:r,hemisphere:b,easting:Math.round(m),northing:Math.round(y)}}function ys(n,e){const t=ms(n,e);return t?`${t.zone}${t.hemisphere} ${t.easting}E ${t.northing}N`:"—"}function bs(n){return n==null||Number.isNaN(n)?"—":n<1e3?`${Math.round(n)} m`:`${(n/1e3).toFixed(2)} km`}function ws(n){return n==null||Number.isNaN(n)?"—":`±${Math.round(n)} m`}function _s(n){return n==null||Number.isNaN(n)?"none":n<=10?"good":n<=30?"fair":"poor"}const vs={minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0,timeoutMs:15e3,maximumAgeMs:0};class ut{constructor(e={}){this.opts={...vs,...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=ut.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=ut.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=ut.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=gs(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 Es={async createTrail(n){const e=n.districtId??Xe()?.district_id??null;return Wa({...n,districtId:e!=null?String(e):null})},addPoint:(n,e)=>Va(n,e),finishTrail:(n,e)=>Ka(n,e),getUnsyncedTrails:()=>Xa(),getTrailPoints:n=>Ya(n),markTrailSynced:(n,e)=>Ja(n,e)},xs={pushTrail:(n,e)=>fs(n,e),isOnline:()=>K()},ve=new ut({storage:Es,sync:xs,minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0}),Ss=new Set(["set:view","set:selected","clear:selected","set:basemap"]);function Ls({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 I({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 f(m,y,b){const _=m.getProperties();let S=y,L=b;if(S==null||L==null){const v=m.getGeometry()?.getExtent();if(v){const[x,P]=Oe(Xr(v));S=x,L=P}}return{type:"parcel:select",upn:_.upn??null,parcel_id:_.id??null,lon:S??null,lat:L??null,zone_code:_.zone_code??null,zone_name:_.zone_name??null,landuse:_.landuse??null,min_height:_.min_height??null,max_height:_.max_height??null}}function p(m){if(o.clear(),m){const y=m.clone();o.addFeature(y)}}n.onClick((m,y,b,_)=>{let S=null;t.forEachFeatureAtPixel(_.pixel,L=>{if(L.get("_layerType")==="parcel")return S=L,!0}),S?(p(S),c(f(S,m,y))):(p(null),c({type:"parcel:cleared"}))}),window.addEventListener("message",m=>{const y=m.data;if(!(!y||typeof y!="object"||!Ss.has(y.type)))try{switch(y.type){case"set:view":{if(typeof y.lon=="number"&&typeof y.lat=="number"){const b=t.getView();b.setCenter(ee([y.lon,y.lat])),typeof y.zoom=="number"&&b.setZoom(y.zoom)}break}case"set:selected":y.upn&&h(String(y.upn));break;case"clear:selected":p(null),s=null;break;case"set:basemap":y.key&&typeof n.setBaseMap=="function"&&n.setBaseMap(y.key);break}}catch(b){d("COMMAND_FAILED",`Failed to handle ${y.type}: ${b.message}`)}});function h(m){if(!i){s=m;return}const b=i.getSource().getFeatures().find(S=>String(S.get("upn")??"")===m);if(!b){s=m;return}s=null,p(b);const _=b.getGeometry()?.getExtent();_&&t.getView().fit(_,{padding:[50,50,50,50],duration:400,maxZoom:17}),c(f(b,null,null))}function g(m){i=m;const y=m.getSource(),b=()=>{queueMicrotask(()=>{s&&h(s),u()})};if(y.getFeatures().length>0)b();else{let _=!1;y.on("addfeature",()=>{_||(_=!0,queueMicrotask(()=>{_=!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 Ts=[{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"}],yr={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 ks(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 yo=null,N=null;function Fs(){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=Ts.map(n=>``).join(""),T.target.dataset.populated="1"),T.target&&!T.target.dataset.wired&&(T.target.dataset.wired="1",T.target.addEventListener("change",_r)),T.btnSave&&!T.btnSave.dataset.wired&&(T.btnSave.dataset.wired="1",T.btnSave.addEventListener("click",()=>fn("save"))),T.btnSaveUpload&&!T.btnSaveUpload.dataset.wired&&(T.btnSaveUpload.dataset.wired="1",T.btnSaveUpload.addEventListener("click",()=>fn("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 Os(){const n=N.targetType,e=yr[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?` — [${Tt(a)}]`:" — [(empty)]"},r=[''].concat(N.sourceFields.map(o=>``)).join("");T.tbody.innerHTML=e.map(o=>{const a=N.mapping[o]||"",i=r.replace(`