diff --git a/dist/assets/analysis-modal-BJxejC9Q.js b/dist/assets/analysis-modal-CWqTGfac.js similarity index 98% rename from dist/assets/analysis-modal-BJxejC9Q.js rename to dist/assets/analysis-modal-CWqTGfac.js index 20ef959..a70c716 100644 --- a/dist/assets/analysis-modal-BJxejC9Q.js +++ b/dist/assets/analysis-modal-CWqTGfac.js @@ -1,12 +1,12 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/xlsx-x-C7b9wf.js","assets/index-ybgDODsx.js","assets/vite-helpers-BlTxHScW.js","assets/bootstrap-D1-uvFxm.js","assets/bootstrap-BtmJYOxZ.css","assets/openlayers-D5-VSKzq.js","assets/openlayers-BtPuoxOl.css","assets/ol-ext-DV70ljNY.js","assets/ol-ext-BgKrOIxx.css","assets/index-Dp-9_Fz_.css"])))=>i.map(i=>d[i]); -import{_ as de}from"./vite-helpers-BlTxHScW.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-ybgDODsx.js";import"./ol-ext-DV70ljNY.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=>` +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/xlsx-q0F9FUtJ.js","assets/index-BZOjrkLV.js","assets/vite-helpers-BlTxHScW.js","assets/bootstrap-D1-uvFxm.js","assets/bootstrap-BtmJYOxZ.css","assets/openlayers-D5-VSKzq.js","assets/openlayers-BtPuoxOl.css","assets/ol-ext-DV70ljNY.js","assets/ol-ext-BgKrOIxx.css","assets/index-Dp-9_Fz_.css"])))=>i.map(i=>d[i]); +import{_ as de}from"./vite-helpers-BlTxHScW.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-BZOjrkLV.js";import"./ol-ext-DV70ljNY.js";const Q=4e5,ve=new ye;function $(e,n="EPSG:3857"){const o=[];for(const s of e||[]){if(!(s.getGeometry&&s.getGeometry()))continue;const a=ve.writeFeatureObject(s,{dataProjection:"EPSG:4326",featureProjection:n});a&&a.geometry&&o.push(a)}return o}function N(e){const n=e&&e.geometry;if(!n||!n.coordinates)return null;let o=1/0,s=1/0,r=-1/0,a=-1/0;const c=l=>{if(typeof l[0]=="number"){l[0]r&&(r=l[0]),l[1]a&&(a=l[1]);return}for(const i of l)c(i)};return c(n.coordinates),Number.isFinite(o)?[o,s,r,a]:null}function D(e,n){return!e||!n?!1:!(e[2]=e[0]&&n<=e[2]&&o>=e[1]&&o<=e[3]:!1}function _(e,n,o){if(e.length>0||n.length===0)return;const s=[...new Set(n.map(a=>a?.geometry?.type).filter(Boolean))],r=s.length?s.join(", "):"no geometry";throw new Error(`${o} contains no polygons (found ${r}). Overlay operations work on polygon layers only — line layers such as contours or roads cannot be used here. To summarise a line layer per zone, use Zonal statistics instead.`)}function I(e){return(e||[]).filter(n=>{const o=n?.geometry?.type;return o==="Polygon"||o==="MultiPolygon"})}function Be(e,n,o="b_"){const s={...e||{}};for(const[r,a]of Object.entries(n||{}))s[Object.prototype.hasOwnProperty.call(s,r)?`${o}${r}`:r]=a;return s}function z(e){return{type:"FeatureCollection",features:e}}function ee(e,n,o={}){const{keepBAttributes:s=!0}=o,r=$(e),a=$(n),c=I(r),l=I(a);_(c,r,"Layer A"),_(l,a,"Layer B");const i=l.map(N);let u=0;const f=[];for(const d of c){const y=N(d);for(let m=0;mQ)throw te(u);let p=null;try{p=me(j([d,l[m]]))}catch{p=null}!p||!p.geometry||(p.properties=s?Be(d.properties,l[m].properties):{...d.properties||{}},f.push(p))}}return z(f)}function Ee(e,n,o={}){return ee(e,n,{...o,keepBAttributes:!1})}function Le(e,n){const o=$(e),s=$(n),r=I(o),a=I(s);_(r,o,"Layer A"),_(a,s,"Layer B");const c=a.map(N);let l=0;const i=[];for(const u of r){const f=N(u);let d=u;for(let y=0;yQ)throw te(l);try{const m=fe(j([d,a[y]]));d=m&&m.geometry?m:null}catch{}}d&&d.geometry&&(d.properties={...u.properties||{}},i.push(d))}return z(i)}function Ae(e,n=[]){const o=$(e),s=$(n),r=I(o),a=I(s);_(r,o,"Layer A"),_(a,s,"Layer B");const c=[...r,...a];if(c.length===0)return z([]);if(c.length===1)return z([c[0]]);let l=null;try{l=X(j(c))}catch{l=c[0];for(let i=1;i{const A=w?.geometry?.type;return A==="Polygon"||A==="MultiPolygon"}),l=$(n)||[];if(c.length===0)throw new Error("The zone layer contains no polygons.");const i=[],u=new Set(l.map(w=>w?.geometry?.type).filter(Boolean)),f=u.has("Polygon")||u.has("MultiPolygon");o.includes("area")&&!f&&l.length>0&&i.push(`Total area is 0 because the features being summarised are ${[...u].join("/")} — only polygons have an area.`);const d=u.has("LineString")||u.has("MultiLineString");o.includes("mean")&&d&&!f&&s&&i.push(`Mean is the average of “${s}” across the individual line features in each zone, not an area-weighted average of the surface they describe. For contour data, Minimum and Maximum (the elevation range) are the figures you can rely on.`);const y=r==="centroid"?l.map(w=>{try{return he(w)}catch{return null}}):null,m=r==="centroid"?null:l.map(N),p=c.map(N);let L=0;const W=[];for(let w=0;wY)throw q(L);b=ge(E,A)}else{if(!D(U,m[h]))continue;if(++L>Y)throw q(L);b=be(l[h],A)}}catch{b=!1}b&&O.push(l[h])}const B=s?O.map(h=>ke(h.properties?h.properties[s]:null)).filter(h=>h!==null):[],S={};for(const h of o)switch(h){case"count":S[`${a}count`]=O.length;break;case"sum":S[`${a}sum`]=B.length?C(B.reduce((b,E)=>b+E,0)):0;break;case"mean":S[`${a}mean`]=B.length?C(B.reduce((b,E)=>b+E,0)/B.length):null;break;case"min":S[`${a}min`]=B.length?C(Math.min(...B)):null;break;case"max":S[`${a}max`]=B.length?C(Math.max(...B)):null;break;case"area":{let b=0;for(const E of O)try{b+=we(E)}catch{}S[`${a}area_m2`]=Math.round(b);break}}A.properties={...A.properties||{},...S},W.push({...Me(A),...S})}return{geojson:{type:"FeatureCollection",features:c},rows:W,warnings:i}}function q(e){return new Error(`More than ${e.toLocaleString()} features fall within the zones' bounding boxes, which is too much to summarise in the browser. Narrow the inputs using “Apply to” (current map view or selected features).`)}function C(e){return Math.round(e*1e3)/1e3}function Me(e){const n=e.properties||{};for(const o of["zone_name","name","ZONE_NAME","NAME","upn","UPN","id","ID"])if(n[o]!==void 0&&n[o]!==null&&String(n[o]).trim()!=="")return{zone:String(n[o])};return{zone:"(unnamed)"}}const Ne=9001,H="Analysis",t={};let P=null,v=null,M=[],x=null,k=null;function _e(){t.root||(t.root=document.getElementById("analysisModal"),t.mode=Array.from(document.querySelectorAll('input[name="analysis-mode"]')),t.overlayBox=document.getElementById("analysis-overlay-box"),t.zonalBox=document.getElementById("analysis-zonal-box"),t.op=document.getElementById("analysis-op"),t.opHint=document.getElementById("analysis-op-hint"),t.layerA=document.getElementById("analysis-layer-a"),t.layerB=document.getElementById("analysis-layer-b"),t.layerBWrap=document.getElementById("analysis-layer-b-wrap"),t.zoneLayer=document.getElementById("analysis-zone-layer"),t.inputLayer=document.getElementById("analysis-input-layer"),t.membership=document.getElementById("analysis-membership"),t.memberHint=document.getElementById("analysis-membership-hint"),t.field=document.getElementById("analysis-field"),t.statsBox=document.getElementById("analysis-stats-box"),t.outName=document.getElementById("analysis-output-name"),t.result=document.getElementById("analysis-result"),t.btnRun=document.getElementById("analysis-run"),t.btnExport=document.getElementById("analysis-export-table"),t.scope=document.getElementById("analysis-scope"),t.scopeHint=document.getElementById("analysis-scope-hint"),t.root.dataset.wired||(t.root.dataset.wired="1",t.mode.forEach(e=>e.addEventListener("change",oe)),t.op.addEventListener("change",re),t.membership.addEventListener("change",se),t.inputLayer.addEventListener("change",ne),t.scope.addEventListener("change",le),t.btnRun.addEventListener("click",Ce),t.btnExport.addEventListener("click",je)))}function J(e=null){return M.map((n,o)=>``).join("")}function Ie(){t.op.innerHTML=G.map(e=>``).join(""),t.membership.innerHTML=Z.map(e=>``).join(""),t.statsBox.innerHTML=R.map(e=>`
-
`).join("")}function ne(){const e=M[Number(t.inputLayer.value)],n=e?e.layer.getSource().getFeatures():[],o=$e(n);t.field.innerHTML=''+o.map(s=>``).join(""),t.field.disabled=o.length===0}function V(){return(t.mode.find(e=>e.checked)||{}).value||"overlay"}function oe(){k=null,ie();const e=V()==="overlay";t.overlayBox.classList.toggle("d-none",!e),t.zonalBox.classList.toggle("d-none",e),ae()}function re(){const e=G.find(o=>o.key===t.op.value);t.opHint.textContent=e?e.hint:"";const n=e?e.needsB:!0;t.layerBWrap.querySelector("label").textContent=n?"Layer B":"Layer B (optional)",ae()}function se(){const e=Z.find(n=>n.key===t.membership.value);t.memberHint.textContent=e?e.hint:""}function ae(){if(t.outName.dataset.touched!=="1")if(V()==="overlay"){const e=t.op.value||"overlay";t.outName.value=`${e}_result`}else t.outName.value="zonal_stats"}function T(e){const n=M[Number(e.value)];if(!n)return[];const o=n.layer.getSource(),s=t.scope?t.scope.value:"all";if(s==="view"){const r=v.getMap(),a=r.getView().calculateExtent(r.getSize());return o.getFeaturesInExtent(a)}if(s==="selection"){const r=new Set(v.getSelectedFeatures());return o.getFeatures().filter(a=>r.has(a))}return s==="drawn"&&x?o.getFeatures().filter(r=>x.features.has(r)):o.getFeatures()}function le(){const e=t.scope.value;if(e==="drawn"&&x)t.scopeHint.textContent=`${x.features.size.toLocaleString()} feature(s) caught by the ${x.label}.`;else if(e==="selection"){const n=v?v.getSelectedFeatures().length:0;t.scopeHint.textContent=n===0?"Nothing selected yet — use the Digitise tool: shift-click features, or hold Ctrl/Cmd and drag a box.":`${n.toLocaleString()} feature(s) currently selected.`}else e==="view"?t.scopeHint.textContent="Only features inside the current map view are analysed — zoom to the area of interest first.":t.scopeHint.textContent="Every feature in the chosen layer. Narrow this if a large layer is slow."}function Oe(){let e=v.getLayerGroupByTitle(H);return e||(v.addLayerGroup(Ne,H,"Results of client-side spatial analysis"),e=v.getLayerGroupByTitle(H)),e}function Ce(){t.result.innerHTML="",t.btnRun.disabled=!0,setTimeout(()=>{try{const e=V(),n=(t.outName.value||"").trim()||(e==="overlay"?"overlay_result":"zonal_stats");let o,s,r=[];if(e==="overlay"){k=null;const l=t.op.value,i=T(t.layerA),u=T(t.layerB),f=(G.find(d=>d.key===l)||{}).needsB;if(i.length===0)throw new Error("Layer A has no features.");if(f&&u.length===0)throw new Error("This operation needs a second layer with features.");o=Se(l,i,u),s=`${o.features.length.toLocaleString()} feature(s) produced.`}else{const l=T(t.zoneLayer),i=T(t.inputLayer),u=Array.from(t.statsBox.querySelectorAll("input:checked")).map(p=>p.value),f=t.field.value||null;if(u.length===0)throw new Error("Select at least one statistic.");if(u.some(p=>(R.find(L=>L.key===p)||{}).needsField)&&!f)throw new Error("Sum / mean / min / max need a numeric field.");const y=ze({zoneFeatures:l,inputFeatures:i,stats:u,field:f,membership:t.membership.value});o=y.geojson,r=y.warnings||[],s=Fe(y.rows);const m=(Z.find(p=>p.key===t.membership.value)||{}).label;k={rows:y.rows,warnings:y.warnings||[],params:{Analysis:"Zonal statistics","Zone layer":K(t.zoneLayer),"Features summarised":K(t.inputLayer),"Applied to":Te(),"Zones analysed":l.length,"Features analysed":i.length,"Counted when":m||t.membership.value,Statistics:u.map(p=>(R.find(L=>L.key===p)||{}).label||p).join(", "),"Numeric field":f||"(none)","Result layer":n}}}if(!o.features.length){t.result.innerHTML='
No result features — the layers may not overlap.
';return}const a=Oe();v.addGeoJSONLayer(o,n,{strokeColor:"#d97706",strokeWidth:2,fillColor:"rgba(217,119,6,0.18)"},a),ie();const c=r.length?'
'+r.map(l=>`
${g(l)}
`).join("")+"
":"";t.result.innerHTML=`
Added layer “${g(n)}”.
`+c+(typeof s=="string"&&s.startsWith("<")?s:`
${g(s)}
`),F(`Analysis complete — “${n}” added to the Analysis group.`,"success",4e3)}catch(e){console.error("[Analysis] run failed:",e),t.result.innerHTML=`
${g(e.message||String(e))}
`}finally{t.btnRun.disabled=!1}},20)}function K(e){const n=e.options[e.selectedIndex];return n?n.textContent:"(none)"}function Te(){const e=t.scope.options[t.scope.selectedIndex];return e?e.textContent:"Whole layer"}async function je(){if(k){t.btnExport.disabled=!0;try{const{exportXlsx:e}=await de(async()=>{const{exportXlsx:i}=await import("./xlsx-x-C7b9wf.js");return{exportXlsx:i}},__vite__mapDeps([0,1,2,3,4,5,6,7,8,9])),{rows:n,params:o,warnings:s}=k,r=Object.keys(n[0]||{}).filter(i=>i!=="zone"),a=[{cells:["Zone",...r],bold:!0},...n.map(i=>["zone"in i?i.zone:"",...r.map(u=>i[u]===null||i[u]===void 0?"":i[u])])],c=[{cells:["LUPMIS2 — Zonal statistics"],bold:!0},[],{cells:["Parameter","Value"],bold:!0},...Object.entries(o).map(([i,u])=>[i,u]),["Exported",new Date().toLocaleString()],...s&&s.length?[[],{cells:["Notes"],bold:!0},...s.map(i=>["",i])]:[]],l=(o["Result layer"]||"zonal_stats").toString().trim().replace(/[^A-Za-z0-9_\-]+/g,"_")||"zonal_stats";e([{name:"Parameters",rows:c},{name:"Results",rows:a}],`${l}.xlsx`),F("Table exported to Excel.","success",3e3)}catch(e){console.error("[Analysis] table export failed:",e),F(`Could not export the table: ${e.message}`,"error",6e3)}finally{t.btnExport.disabled=!1}}}function ie(){t.btnExport&&t.btnExport.classList.toggle("d-none",!k)}function Fe(e){if(!e||!e.length)return"";const n=Object.keys(e[0]).filter(r=>r!=="zone"),o=["Zone",...n.map(r=>r.replace(/^zs_/,""))].map(r=>`${g(r)}`).join(""),s=e.map(r=>`${g(String(r.zone))}`+n.map(a=>`${r[a]===null||r[a]===void 0?"—":g(String(r[a]))}`).join("")+"").join("");return`
+
`).join("")}function ne(){const e=M[Number(t.inputLayer.value)],n=e?e.layer.getSource().getFeatures():[],o=$e(n);t.field.innerHTML=''+o.map(s=>``).join(""),t.field.disabled=o.length===0}function V(){return(t.mode.find(e=>e.checked)||{}).value||"overlay"}function oe(){k=null,ie();const e=V()==="overlay";t.overlayBox.classList.toggle("d-none",!e),t.zonalBox.classList.toggle("d-none",e),ae()}function re(){const e=G.find(o=>o.key===t.op.value);t.opHint.textContent=e?e.hint:"";const n=e?e.needsB:!0;t.layerBWrap.querySelector("label").textContent=n?"Layer B":"Layer B (optional)",ae()}function se(){const e=Z.find(n=>n.key===t.membership.value);t.memberHint.textContent=e?e.hint:""}function ae(){if(t.outName.dataset.touched!=="1")if(V()==="overlay"){const e=t.op.value||"overlay";t.outName.value=`${e}_result`}else t.outName.value="zonal_stats"}function T(e){const n=M[Number(e.value)];if(!n)return[];const o=n.layer.getSource(),s=t.scope?t.scope.value:"all";if(s==="view"){const r=v.getMap(),a=r.getView().calculateExtent(r.getSize());return o.getFeaturesInExtent(a)}if(s==="selection"){const r=new Set(v.getSelectedFeatures());return o.getFeatures().filter(a=>r.has(a))}return s==="drawn"&&x?o.getFeatures().filter(r=>x.features.has(r)):o.getFeatures()}function le(){const e=t.scope.value;if(e==="drawn"&&x)t.scopeHint.textContent=`${x.features.size.toLocaleString()} feature(s) caught by the ${x.label}.`;else if(e==="selection"){const n=v?v.getSelectedFeatures().length:0;t.scopeHint.textContent=n===0?"Nothing selected yet — use the Digitise tool: shift-click features, or hold Ctrl/Cmd and drag a box.":`${n.toLocaleString()} feature(s) currently selected.`}else e==="view"?t.scopeHint.textContent="Only features inside the current map view are analysed — zoom to the area of interest first.":t.scopeHint.textContent="Every feature in the chosen layer. Narrow this if a large layer is slow."}function Oe(){let e=v.getLayerGroupByTitle(H);return e||(v.addLayerGroup(Ne,H,"Results of client-side spatial analysis"),e=v.getLayerGroupByTitle(H)),e}function Ce(){t.result.innerHTML="",t.btnRun.disabled=!0,setTimeout(()=>{try{const e=V(),n=(t.outName.value||"").trim()||(e==="overlay"?"overlay_result":"zonal_stats");let o,s,r=[];if(e==="overlay"){k=null;const l=t.op.value,i=T(t.layerA),u=T(t.layerB),f=(G.find(d=>d.key===l)||{}).needsB;if(i.length===0)throw new Error("Layer A has no features.");if(f&&u.length===0)throw new Error("This operation needs a second layer with features.");o=Se(l,i,u),s=`${o.features.length.toLocaleString()} feature(s) produced.`}else{const l=T(t.zoneLayer),i=T(t.inputLayer),u=Array.from(t.statsBox.querySelectorAll("input:checked")).map(p=>p.value),f=t.field.value||null;if(u.length===0)throw new Error("Select at least one statistic.");if(u.some(p=>(R.find(L=>L.key===p)||{}).needsField)&&!f)throw new Error("Sum / mean / min / max need a numeric field.");const y=ze({zoneFeatures:l,inputFeatures:i,stats:u,field:f,membership:t.membership.value});o=y.geojson,r=y.warnings||[],s=Fe(y.rows);const m=(Z.find(p=>p.key===t.membership.value)||{}).label;k={rows:y.rows,warnings:y.warnings||[],params:{Analysis:"Zonal statistics","Zone layer":K(t.zoneLayer),"Features summarised":K(t.inputLayer),"Applied to":Te(),"Zones analysed":l.length,"Features analysed":i.length,"Counted when":m||t.membership.value,Statistics:u.map(p=>(R.find(L=>L.key===p)||{}).label||p).join(", "),"Numeric field":f||"(none)","Result layer":n}}}if(!o.features.length){t.result.innerHTML='
No result features — the layers may not overlap.
';return}const a=Oe();v.addGeoJSONLayer(o,n,{strokeColor:"#d97706",strokeWidth:2,fillColor:"rgba(217,119,6,0.18)"},a),ie();const c=r.length?'
'+r.map(l=>`
${g(l)}
`).join("")+"
":"";t.result.innerHTML=`
Added layer “${g(n)}”.
`+c+(typeof s=="string"&&s.startsWith("<")?s:`
${g(s)}
`),F(`Analysis complete — “${n}” added to the Analysis group.`,"success",4e3)}catch(e){console.error("[Analysis] run failed:",e),t.result.innerHTML=`
${g(e.message||String(e))}
`}finally{t.btnRun.disabled=!1}},20)}function K(e){const n=e.options[e.selectedIndex];return n?n.textContent:"(none)"}function Te(){const e=t.scope.options[t.scope.selectedIndex];return e?e.textContent:"Whole layer"}async function je(){if(k){t.btnExport.disabled=!0;try{const{exportXlsx:e}=await de(async()=>{const{exportXlsx:i}=await import("./xlsx-q0F9FUtJ.js");return{exportXlsx:i}},__vite__mapDeps([0,1,2,3,4,5,6,7,8,9])),{rows:n,params:o,warnings:s}=k,r=Object.keys(n[0]||{}).filter(i=>i!=="zone"),a=[{cells:["Zone",...r],bold:!0},...n.map(i=>["zone"in i?i.zone:"",...r.map(u=>i[u]===null||i[u]===void 0?"":i[u])])],c=[{cells:["LUPMIS2 — Zonal statistics"],bold:!0},[],{cells:["Parameter","Value"],bold:!0},...Object.entries(o).map(([i,u])=>[i,u]),["Exported",new Date().toLocaleString()],...s&&s.length?[[],{cells:["Notes"],bold:!0},...s.map(i=>["",i])]:[]],l=(o["Result layer"]||"zonal_stats").toString().trim().replace(/[^A-Za-z0-9_\-]+/g,"_")||"zonal_stats";e([{name:"Parameters",rows:c},{name:"Results",rows:a}],`${l}.xlsx`),F("Table exported to Excel.","success",3e3)}catch(e){console.error("[Analysis] table export failed:",e),F(`Could not export the table: ${e.message}`,"error",6e3)}finally{t.btnExport.disabled=!1}}}function ie(){t.btnExport&&t.btnExport.classList.toggle("d-none",!k)}function Fe(e){if(!e||!e.length)return"";const n=Object.keys(e[0]).filter(r=>r!=="zone"),o=["Zone",...n.map(r=>r.replace(/^zs_/,""))].map(r=>`${g(r)}`).join(""),s=e.map(r=>`${g(String(r.zone))}`+n.map(a=>`${r[a]===null||r[a]===void 0?"—":g(String(r[a]))}`).join("")+"").join("");return`
${o}${s}
`}function We(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{We as openAnalysisModal}; -//# sourceMappingURL=analysis-modal-BJxejC9Q.js.map +//# sourceMappingURL=analysis-modal-CWqTGfac.js.map diff --git a/dist/assets/analysis-modal-BJxejC9Q.js.map b/dist/assets/analysis-modal-CWqTGfac.js.map similarity index 99% rename from dist/assets/analysis-modal-BJxejC9Q.js.map rename to dist/assets/analysis-modal-CWqTGfac.js.map index d8edb3b..9146a56 100644 --- a/dist/assets/analysis-modal-BJxejC9Q.js.map +++ b/dist/assets/analysis-modal-CWqTGfac.js.map @@ -1 +1 @@ -{"version":3,"mappings":"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","names":["MAX_OVERLAY_CANDIDATES","fmt","GeoJSON","toGeoJSONFeatures","features","mapProjection","out","f","gj","bboxOf","feature","geom","minX","minY","maxX","maxY","walk","c","sub","bboxOverlaps","a","b","bboxContainsPoint","box","x","y","assertPolygons","polygons","original","role","types","found","polygonsOnly","gjFeatures","t","mergeProps","propsA","propsB","prefixB","k","v","fc","intersectFeatures","aFeatures","bFeatures","opts","keepBAttributes","gjA","gjB","A","B","bBoxes","candidates","aBox","i","candidateOverflow","piece","intersect","featureCollection","clipFeatures","clipFeatures_","differenceFeatures","remainder","next","difference","unionFeatures","polyA","polyB","all","merged","union","count","OVERLAY_OPS","runOverlay","op","MAX_ZONAL_CANDIDATES","MEMBERSHIP_MODES","STATS","listNumericFields","counts","props","num","value","zonalStatistics","zoneFeatures","inputFeatures","stats","field","membership","prefix","zones","z","inputs","warnings","inputTypes","inputsHavePolygons","inputsAreLines","centroids","centroid","inputBoxes","zoneBoxes","rows","zone","zBox","members","inside","cx","cy","booleanPointInPolygon","booleanIntersects","values","result","s","round","total","m","area","labelOf","n","p","ANALYSIS_GROUP_ID","ANALYSIS_GROUP_TITLE","els","modal","mapView","layerChoices","drawnScope","lastZonal","cacheEls","r","onModeChange","onOpChange","onMembershipChange","refreshFieldOptions","onScopeChange","onRun","onExportTable","layerOptionsHtml","selectedIdx","escapeHtml","populateOps","o","choice","feats","fields","currentMode","syncExportButton","overlay","suggestName","needsB","featuresOf","select","source","scope","map","extent","selected","ensureAnalysisGroup","group","mode","name","geojson","summary","zonalWarnings","res","renderRows","membershipLabel","labelOfSelect","scopeLabel","warnHtml","w","showToast","err","opt","exportXlsx","__vitePreload","params","keys","resultRows","paramRows","base","head","h","body","openAnalysisModal","view","syncDrawnScopeOption","optsA","optsB","Modal","existing","label"],"ignoreList":[],"sources":["../../src/analysis/overlay.js","../../src/analysis/zonal.js","../../src/analysis-modal.js"],"sourcesContent":["/**\n * overlay.js — client-side vector overlay operations (Stage 1 of the\n * LUPMIS2 GIS Analytical Tools concept).\n *\n * Pure geometry helpers built on Turf.js. No DOM, no map — the caller hands\n * in OpenLayers features and gets a GeoJSON FeatureCollection back, ready for\n * MapView.addGeoJSONLayer().\n *\n * All Turf work happens in EPSG:4326 (Turf's area/length functions assume\n * WGS84), so features are reprojected out of the map's EPSG:3857 on the way\n * in and the result is handed back as WGS84 GeoJSON — the same convention\n * gis-export.js uses.\n *\n * intersectFeatures(a, b) → parts of A that fall inside B (attrs: A + B)\n * clipFeatures(a, b) → parts of A that fall inside B (attrs: A only)\n * differenceFeatures(a, b) → parts of A that fall OUTSIDE B (attrs: A)\n * unionFeatures(a[, b]) → dissolved outline of A (and B)\n */\n\nimport GeoJSON from 'ol/format/GeoJSON';\nimport intersect from '@turf/intersect';\nimport union from '@turf/union';\nimport difference from '@turf/difference';\nimport { featureCollection } from '@turf/helpers';\n\n/**\n * Guard: only *candidate* pairs (those whose bounding boxes actually overlap)\n * reach the expensive geometry test, so this ceiling is about real work, not\n * the raw n×m product. District-scale data (tens of thousands of parcels\n * against a few dozen zones) sits far below it.\n */\nexport const MAX_OVERLAY_CANDIDATES = 400000;\n\nconst fmt = new GeoJSON();\n\n/**\n * Convert OpenLayers features (map projection) to WGS84 GeoJSON features.\n * @param {Array} features\n * @param {string} [mapProjection='EPSG:3857']\n * @returns {Array} GeoJSON Features\n */\nexport function toGeoJSONFeatures(features, mapProjection = 'EPSG:3857') {\n const out = [];\n for (const f of features || []) {\n const geom = f.getGeometry && f.getGeometry();\n if (!geom) continue;\n const gj = fmt.writeFeatureObject(f, {\n dataProjection: 'EPSG:4326',\n featureProjection: mapProjection,\n });\n if (gj && gj.geometry) out.push(gj);\n }\n return out;\n}\n\n/**\n * Bounding box of a GeoJSON feature as [minX, minY, maxX, maxY].\n * Walks the coordinate arrays directly — no extra dependency, and fast enough\n * to run over tens of thousands of features.\n * @param {Object} feature GeoJSON Feature\n * @returns {number[]|null}\n */\nexport function bboxOf(feature) {\n const geom = feature && feature.geometry;\n if (!geom || !geom.coordinates) return null;\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n const walk = (c) => {\n if (typeof c[0] === 'number') {\n if (c[0] < minX) minX = c[0];\n if (c[0] > maxX) maxX = c[0];\n if (c[1] < minY) minY = c[1];\n if (c[1] > maxY) maxY = c[1];\n return;\n }\n for (const sub of c) walk(sub);\n };\n walk(geom.coordinates);\n return Number.isFinite(minX) ? [minX, minY, maxX, maxY] : null;\n}\n\n/** Do two bounding boxes overlap? Cheap pre-filter before real geometry work. */\nexport function bboxOverlaps(a, b) {\n if (!a || !b) return false;\n return !(a[2] < b[0] || b[2] < a[0] || a[3] < b[1] || b[3] < a[1]);\n}\n\n/** Is a point [x, y] inside a bounding box? */\nexport function bboxContainsPoint(box, x, y) {\n if (!box) return false;\n return x >= box[0] && x <= box[2] && y >= box[1] && y <= box[3];\n}\n\n/**\n * Reject a non-polygon input with a message that says what is actually wrong.\n *\n * Overlay operations are polygon-only, but the inputs are silently filtered by\n * polygonsOnly() — so a line layer (contours, roads) used to produce an empty\n * result and the misleading \"the layers may not overlap\". Failing loudly here\n * tells the user the real reason.\n *\n * @param {Array} polygons the surviving polygonal features\n * @param {Array} original everything that was passed in\n * @param {string} role 'Layer A' / 'Layer B'\n */\nfunction assertPolygons(polygons, original, role) {\n if (polygons.length > 0 || original.length === 0) return;\n const types = [...new Set(original.map((f) => f?.geometry?.type).filter(Boolean))];\n const found = types.length ? types.join(', ') : 'no geometry';\n throw new Error(\n `${role} contains no polygons (found ${found}). Overlay operations work on ` +\n `polygon layers only — line layers such as contours or roads cannot be used here. ` +\n `To summarise a line layer per zone, use Zonal statistics instead.`\n );\n}\n\n/** Keep only polygonal features — overlay is undefined for points/lines here. */\nfunction polygonsOnly(gjFeatures) {\n return (gjFeatures || []).filter((f) => {\n const t = f?.geometry?.type;\n return t === 'Polygon' || t === 'MultiPolygon';\n });\n}\n\n/** Merge two property bags, prefixing B's keys on collision so nothing is lost. */\nfunction mergeProps(propsA, propsB, prefixB = 'b_') {\n const out = { ...(propsA || {}) };\n for (const [k, v] of Object.entries(propsB || {})) {\n out[Object.prototype.hasOwnProperty.call(out, k) ? `${prefixB}${k}` : k] = v;\n }\n return out;\n}\n\nfunction fc(features) {\n return { type: 'FeatureCollection', features };\n}\n\n/**\n * Intersection: every piece of A that also lies inside B.\n * Result carries A's attributes merged with the matching B feature's.\n *\n * @param {Array} aFeatures OL features (layer A)\n * @param {Array} bFeatures OL features (layer B)\n * @param {Object} [opts]\n * @param {boolean} [opts.keepBAttributes=true] merge B's properties into the result\n * @returns {Object} GeoJSON FeatureCollection (WGS84)\n */\nexport function intersectFeatures(aFeatures, bFeatures, opts = {}) {\n const { keepBAttributes = true } = opts;\n const gjA = toGeoJSONFeatures(aFeatures);\n const gjB = toGeoJSONFeatures(bFeatures);\n const A = polygonsOnly(gjA);\n const B = polygonsOnly(gjB);\n assertPolygons(A, gjA, 'Layer A');\n assertPolygons(B, gjB, 'Layer B');\n\n // Pre-compute B's boxes once, then only test pairs that can possibly meet.\n const bBoxes = B.map(bboxOf);\n let candidates = 0;\n const out = [];\n for (const a of A) {\n const aBox = bboxOf(a);\n for (let i = 0; i < B.length; i++) {\n if (!bboxOverlaps(aBox, bBoxes[i])) continue;\n if (++candidates > MAX_OVERLAY_CANDIDATES) throw candidateOverflow(candidates);\n let piece = null;\n try { piece = intersect(featureCollection([a, B[i]])); } catch { piece = null; }\n if (!piece || !piece.geometry) continue;\n piece.properties = keepBAttributes\n ? mergeProps(a.properties, B[i].properties)\n : { ...(a.properties || {}) };\n out.push(piece);\n }\n }\n return fc(out);\n}\n\n/**\n * Clip: same geometry result as intersect, but the output keeps ONLY layer A's\n * attributes — the usual \"cut this layer to that boundary\" operation.\n */\nexport function clipFeatures(aFeatures, clipFeatures_, opts = {}) {\n return intersectFeatures(aFeatures, clipFeatures_, { ...opts, keepBAttributes: false });\n}\n\n/**\n * Difference: the parts of A that do NOT fall inside any feature of B.\n * Each A feature is reduced by every overlapping B feature in turn; features\n * fully covered by B drop out of the result.\n *\n * @returns {Object} GeoJSON FeatureCollection (WGS84)\n */\nexport function differenceFeatures(aFeatures, bFeatures) {\n const gjA = toGeoJSONFeatures(aFeatures);\n const gjB = toGeoJSONFeatures(bFeatures);\n const A = polygonsOnly(gjA);\n const B = polygonsOnly(gjB);\n assertPolygons(A, gjA, 'Layer A');\n assertPolygons(B, gjB, 'Layer B');\n\n const bBoxes = B.map(bboxOf);\n let candidates = 0;\n const out = [];\n for (const a of A) {\n const aBox = bboxOf(a);\n let remainder = a;\n for (let i = 0; i < B.length; i++) {\n if (!remainder) break;\n // Only subtract a B feature that can actually touch what's left.\n if (!bboxOverlaps(aBox, bBoxes[i])) continue;\n if (++candidates > MAX_OVERLAY_CANDIDATES) throw candidateOverflow(candidates);\n try {\n const next = difference(featureCollection([remainder, B[i]]));\n remainder = next && next.geometry ? next : null;\n } catch {\n // A malformed pair shouldn't kill the whole run — keep what we have.\n }\n }\n if (remainder && remainder.geometry) {\n remainder.properties = { ...(a.properties || {}) };\n out.push(remainder);\n }\n }\n return fc(out);\n}\n\n/**\n * Union / dissolve: merge everything into a single outline. Pass only `a` to\n * dissolve one layer, or both to merge two layers together.\n *\n * @returns {Object} GeoJSON FeatureCollection with 0 or 1 feature (WGS84)\n */\nexport function unionFeatures(aFeatures, bFeatures = []) {\n // Check each layer separately rather than the combined set: if only the\n // second layer had polygons, a combined check would pass and the first\n // layer's features would be dropped without the user being told.\n const gjA = toGeoJSONFeatures(aFeatures);\n const gjB = toGeoJSONFeatures(bFeatures);\n const polyA = polygonsOnly(gjA);\n const polyB = polygonsOnly(gjB);\n assertPolygons(polyA, gjA, 'Layer A');\n assertPolygons(polyB, gjB, 'Layer B'); // no-op when no second layer is given\n const all = [...polyA, ...polyB];\n if (all.length === 0) return fc([]);\n if (all.length === 1) return fc([all[0]]);\n\n let merged = null;\n try {\n merged = union(featureCollection(all));\n } catch {\n // Fall back to a pairwise fold if the bulk union chokes on bad geometry.\n merged = all[0];\n for (let i = 1; i < all.length; i++) {\n try {\n const next = union(featureCollection([merged, all[i]]));\n if (next && next.geometry) merged = next;\n } catch { /* skip the offending feature */ }\n }\n }\n if (!merged || !merged.geometry) return fc([]);\n merged.properties = { parts: all.length };\n return fc([merged]);\n}\n\n/** @private Raised only when genuinely overlapping work exceeds the budget. */\nfunction candidateOverflow(count) {\n return new Error(\n `This overlay has more than ${count.toLocaleString()} genuinely overlapping feature pairs, ` +\n `which is too much to run in the browser. Narrow the inputs using “Apply to” ` +\n `(current map view or selected features), or run it server-side.`\n );\n}\n\n/** Operation registry used by the Analysis panel. */\nexport const OVERLAY_OPS = [\n { key: 'intersect', label: 'Intersect', needsB: true,\n hint: 'Keep the parts of layer A that fall inside layer B (attributes from both).' },\n { key: 'clip', label: 'Clip', needsB: true,\n hint: 'Cut layer A to the boundary of layer B (keeps only A’s attributes).' },\n { key: 'difference', label: 'Difference', needsB: true,\n hint: 'Keep the parts of layer A that fall outside layer B.' },\n { key: 'union', label: 'Union / Dissolve', needsB: false,\n hint: 'Merge features into a single outline (optionally with a second layer).' },\n];\n\n/**\n * Run an overlay operation by key.\n * @param {string} op one of OVERLAY_OPS keys\n * @param {Array} aFeatures\n * @param {Array} bFeatures\n * @returns {Object} GeoJSON FeatureCollection (WGS84)\n */\nexport function runOverlay(op, aFeatures, bFeatures) {\n switch (op) {\n case 'intersect': return intersectFeatures(aFeatures, bFeatures);\n case 'clip': return clipFeatures(aFeatures, bFeatures);\n case 'difference': return differenceFeatures(aFeatures, bFeatures);\n case 'union': return unionFeatures(aFeatures, bFeatures);\n default: throw new Error(`Unknown overlay operation: ${op}`);\n }\n}\n","/**\n * zonal.js — vector-in-vector zonal statistics (Stage 1 of the LUPMIS2 GIS\n * Analytical Tools concept).\n *\n * Answers questions like \"how many parcels are in each collector zone?\" or\n * \"what is the mean plot size per zone?\" — the classic *summarize within*\n * operation. Raster × vector zonal statistics arrives with the raster pipeline\n * in Stage 2; this module is pure vector and runs fully offline.\n *\n * Membership rules:\n * 'centroid' — a feature belongs to the zone containing its centroid\n * (default; each feature counts exactly once)\n * 'intersects' — a feature belongs to every zone it touches\n * (features straddling a boundary are counted more than once)\n */\n\nimport booleanPointInPolygon from '@turf/boolean-point-in-polygon';\nimport booleanIntersects from '@turf/boolean-intersects';\nimport centroid from '@turf/centroid';\nimport area from '@turf/area';\nimport { toGeoJSONFeatures, bboxOf, bboxOverlaps, bboxContainsPoint } from './overlay.js';\n\n/**\n * Guard on *candidate* pairs only — a bounding-box pre-filter means a feature\n * is geometry-tested against a zone only when it could plausibly fall in it.\n * A district of 25,000 parcels across 40-odd zones produces roughly one\n * candidate per parcel, so real workloads sit far below this ceiling.\n */\nexport const MAX_ZONAL_CANDIDATES = 400000;\n\nexport const MEMBERSHIP_MODES = [\n { key: 'centroid', label: 'Centroid inside zone',\n hint: 'Each feature is counted once, in the zone containing its centre.' },\n { key: 'intersects', label: 'Any overlap with zone',\n hint: 'A feature is counted in every zone it touches (may double-count).' },\n];\n\n/** Statistics offered by the Analysis panel. */\nexport const STATS = [\n { key: 'count', label: 'Count', needsField: false },\n { key: 'sum', label: 'Sum', needsField: true },\n { key: 'mean', label: 'Mean', needsField: true },\n { key: 'min', label: 'Minimum', needsField: true },\n { key: 'max', label: 'Maximum', needsField: true },\n { key: 'area', label: 'Total area (m²)', needsField: false },\n];\n\n/**\n * List the numeric attribute names available across a set of OL features, so\n * the panel can offer only fields that can actually be summed or averaged.\n * @param {Array} features OL features\n * @returns {string[]}\n */\nexport function listNumericFields(features) {\n const counts = new Map();\n for (const f of features || []) {\n const props = f.getProperties ? f.getProperties() : {};\n for (const [k, v] of Object.entries(props)) {\n if (k === 'geometry' || k.startsWith('_')) continue;\n const n = typeof v === 'number' ? v : (v !== '' && v != null && !Number.isNaN(Number(v)) ? Number(v) : null);\n if (n === null) continue;\n counts.set(k, (counts.get(k) || 0) + 1);\n }\n }\n return Array.from(counts.keys()).sort();\n}\n\n/** Coerce a property to a finite number, or null when it isn't numeric. */\nfunction num(value) {\n if (value === null || value === undefined || value === '') return null;\n const n = typeof value === 'number' ? value : Number(value);\n return Number.isFinite(n) ? n : null;\n}\n\n/**\n * Compute zonal statistics.\n *\n * @param {Object} params\n * @param {Array} params.zoneFeatures OL features defining the zones (polygons)\n * @param {Array} params.inputFeatures OL features to summarise\n * @param {string[]} params.stats keys from STATS\n * @param {string} [params.field] numeric attribute (for sum/mean/min/max)\n * @param {string} [params.membership] 'centroid' | 'intersects'\n * @param {string} [params.prefix='zs_'] prefix for the new attribute names\n * @returns {{ geojson: Object, rows: Array }}\n * geojson — the zones with statistics attached (WGS84), ready to add as a layer\n * rows — a plain table of the same numbers, for display/export\n * warnings — figures that are valid but easy to misread (e.g. area over a\n * line layer); shown to the user rather than thrown\n */\nexport function zonalStatistics({\n zoneFeatures,\n inputFeatures,\n stats = ['count'],\n field = null,\n membership = 'centroid',\n prefix = 'zs_',\n}) {\n const zones = (toGeoJSONFeatures(zoneFeatures) || []).filter((z) => {\n const t = z?.geometry?.type;\n return t === 'Polygon' || t === 'MultiPolygon';\n });\n const inputs = toGeoJSONFeatures(inputFeatures) || [];\n\n if (zones.length === 0) throw new Error('The zone layer contains no polygons.');\n\n // Warnings are returned alongside the result rather than thrown: the run is\n // still valid and useful, but some figures would be silently meaningless\n // without an explanation.\n const warnings = [];\n const inputTypes = new Set(inputs.map((f) => f?.geometry?.type).filter(Boolean));\n const inputsHavePolygons = inputTypes.has('Polygon') || inputTypes.has('MultiPolygon');\n\n // Lines and points have no area, so \"Total area\" would report a silent 0.\n if (stats.includes('area') && !inputsHavePolygons && inputs.length > 0) {\n warnings.push(\n `Total area is 0 because the features being summarised are ` +\n `${[...inputTypes].join('/')} — only polygons have an area.`\n );\n }\n\n // Line inputs only. Averaging a field over line segments is not an\n // area-weighted average of the surface those lines describe — the classic\n // trap being \"mean elevation per parcel\" from contours, where segment count\n // has no relation to the area at each elevation. (Averaging over points is a\n // normal statistic, so it is not flagged.)\n const inputsAreLines = inputTypes.has('LineString') || inputTypes.has('MultiLineString');\n if (stats.includes('mean') && inputsAreLines && !inputsHavePolygons && field) {\n warnings.push(\n `Mean is the average of “${field}” across the individual line features in ` +\n `each zone, not an area-weighted average of the surface they describe. ` +\n `For contour data, Minimum and Maximum (the elevation range) are the ` +\n `figures you can rely on.`\n );\n }\n\n // Pre-compute once: each input's centroid (centroid mode) or bbox\n // (intersects mode), and every zone's bbox. The bbox comparisons below are\n // cheap arithmetic; only survivors reach the real geometry test.\n const centroids = membership === 'centroid'\n ? inputs.map((f) => { try { return centroid(f); } catch { return null; } })\n : null;\n const inputBoxes = membership === 'centroid' ? null : inputs.map(bboxOf);\n const zoneBoxes = zones.map(bboxOf);\n\n let candidates = 0;\n const rows = [];\n for (let z = 0; z < zones.length; z++) {\n const zone = zones[z];\n const zBox = zoneBoxes[z];\n const members = [];\n for (let i = 0; i < inputs.length; i++) {\n let inside = false;\n try {\n if (membership === 'centroid') {\n const c = centroids[i];\n if (!c) continue;\n const [cx, cy] = c.geometry.coordinates;\n if (!bboxContainsPoint(zBox, cx, cy)) continue; // cheap reject\n if (++candidates > MAX_ZONAL_CANDIDATES) throw candidateOverflow(candidates);\n inside = booleanPointInPolygon(c, zone);\n } else {\n if (!bboxOverlaps(zBox, inputBoxes[i])) continue; // cheap reject\n if (++candidates > MAX_ZONAL_CANDIDATES) throw candidateOverflow(candidates);\n inside = booleanIntersects(inputs[i], zone);\n }\n } catch { inside = false; }\n if (inside) members.push(inputs[i]);\n }\n\n const values = field\n ? members.map((f) => num(f.properties ? f.properties[field] : null)).filter((v) => v !== null)\n : [];\n\n const result = {};\n for (const s of stats) {\n switch (s) {\n case 'count':\n result[`${prefix}count`] = members.length; break;\n case 'sum':\n result[`${prefix}sum`] = values.length ? round(values.reduce((a, b) => a + b, 0)) : 0; break;\n case 'mean':\n result[`${prefix}mean`] = values.length\n ? round(values.reduce((a, b) => a + b, 0) / values.length) : null; break;\n case 'min':\n result[`${prefix}min`] = values.length ? round(Math.min(...values)) : null; break;\n case 'max':\n result[`${prefix}max`] = values.length ? round(Math.max(...values)) : null; break;\n case 'area': {\n let total = 0;\n for (const m of members) { try { total += area(m); } catch { /* skip */ } }\n result[`${prefix}area_m2`] = Math.round(total); break;\n }\n default: break;\n }\n }\n\n zone.properties = { ...(zone.properties || {}), ...result };\n rows.push({ ...labelOf(zone), ...result });\n }\n\n return { geojson: { type: 'FeatureCollection', features: zones }, rows, warnings };\n}\n\n/** @private Raised only when genuinely plausible pairs exceed the budget. */\nfunction candidateOverflow(count) {\n return new Error(\n `More than ${count.toLocaleString()} features fall within the zones' bounding boxes, ` +\n `which is too much to summarise in the browser. Narrow the inputs using “Apply to” ` +\n `(current map view or selected features).`\n );\n}\n\n/** Round to 3 decimals to keep attribute tables readable. */\nfunction round(n) {\n return Math.round(n * 1000) / 1000;\n}\n\n/**\n * Pick a human label for a zone row — first of the usual naming fields,\n * else a generic index.\n */\nfunction labelOf(zone) {\n const p = zone.properties || {};\n for (const k of ['zone_name', 'name', 'ZONE_NAME', 'NAME', 'upn', 'UPN', 'id', 'ID']) {\n if (p[k] !== undefined && p[k] !== null && String(p[k]).trim() !== '') {\n return { zone: String(p[k]) };\n }\n }\n return { zone: '(unnamed)' };\n}\n","/**\n * Analysis panel controller — Stage 1 of the GIS Analytical Tools concept.\n *\n * openAnalysisModal(mapView)\n *\n * Drives two families of client-side analysis:\n * • Overlays — intersect / clip / difference / union between layers\n * • Zonal statistics — summarise features (or a numeric field) per zone\n *\n * Both run entirely in the browser via Turf.js, so they work offline. Results\n * are added to the map as a new styled layer in an \"Analysis\" group, from\n * where they can be exported with the existing GIS export tools.\n *\n * Same Bootstrap modal pattern as src/export-gis-modal.js and src/import-modal.js.\n */\n\nimport { Modal } from 'bootstrap';\nimport { runOverlay, OVERLAY_OPS } from './analysis/overlay.js';\nimport { zonalStatistics, listNumericFields, STATS, MEMBERSHIP_MODES } from './analysis/zonal.js';\nimport { showToast } from './toast.js';\n\nconst ANALYSIS_GROUP_ID = 9001;\nconst ANALYSIS_GROUP_TITLE = 'Analysis';\n\nconst els = {};\nlet modal = null;\nlet mapView = null;\nlet layerChoices = []; // [{ layer, title, count }]\nlet drawnScope = null; // { features: Set, label } when opened from a Circle/Area popup\nlet lastZonal = null; // { rows, params } — the most recent zonal run, for export\n\n// ---------------------------------------------------------------------------\n// Element wiring\n// ---------------------------------------------------------------------------\n\nfunction cacheEls() {\n if (els.root) return;\n els.root = document.getElementById('analysisModal');\n els.mode = Array.from(document.querySelectorAll('input[name=\"analysis-mode\"]'));\n els.overlayBox = document.getElementById('analysis-overlay-box');\n els.zonalBox = document.getElementById('analysis-zonal-box');\n els.op = document.getElementById('analysis-op');\n els.opHint = document.getElementById('analysis-op-hint');\n els.layerA = document.getElementById('analysis-layer-a');\n els.layerB = document.getElementById('analysis-layer-b');\n els.layerBWrap = document.getElementById('analysis-layer-b-wrap');\n els.zoneLayer = document.getElementById('analysis-zone-layer');\n els.inputLayer = document.getElementById('analysis-input-layer');\n els.membership = document.getElementById('analysis-membership');\n els.memberHint = document.getElementById('analysis-membership-hint');\n els.field = document.getElementById('analysis-field');\n els.statsBox = document.getElementById('analysis-stats-box');\n els.outName = document.getElementById('analysis-output-name');\n els.result = document.getElementById('analysis-result');\n els.btnRun = document.getElementById('analysis-run');\n els.btnExport = document.getElementById('analysis-export-table');\n els.scope = document.getElementById('analysis-scope');\n els.scopeHint = document.getElementById('analysis-scope-hint');\n\n if (!els.root.dataset.wired) {\n els.root.dataset.wired = '1';\n els.mode.forEach((r) => r.addEventListener('change', onModeChange));\n els.op.addEventListener('change', onOpChange);\n els.membership.addEventListener('change', onMembershipChange);\n els.inputLayer.addEventListener('change', refreshFieldOptions);\n els.scope.addEventListener('change', onScopeChange);\n els.btnRun.addEventListener('click', onRun);\n els.btnExport.addEventListener('click', onExportTable);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Population\n// ---------------------------------------------------------------------------\n\nfunction layerOptionsHtml(selectedIdx = null) {\n return layerChoices.map((c, i) =>\n ``\n ).join('');\n}\n\nfunction populateOps() {\n els.op.innerHTML = OVERLAY_OPS\n .map((o) => ``).join('');\n els.membership.innerHTML = MEMBERSHIP_MODES\n .map((m) => ``).join('');\n els.statsBox.innerHTML = STATS.map((s) => `\n
\n \n \n
`).join('');\n}\n\nfunction refreshFieldOptions() {\n const choice = layerChoices[Number(els.inputLayer.value)];\n const feats = choice ? choice.layer.getSource().getFeatures() : [];\n const fields = listNumericFields(feats);\n els.field.innerHTML = '' +\n fields.map((f) => ``).join('');\n els.field.disabled = fields.length === 0;\n}\n\n// ---------------------------------------------------------------------------\n// UI reactions\n// ---------------------------------------------------------------------------\n\nfunction currentMode() {\n return (els.mode.find((r) => r.checked) || {}).value || 'overlay';\n}\n\nfunction onModeChange() {\n // A previous zonal table doesn't belong to a new overlay run.\n lastZonal = null; syncExportButton();\n const overlay = currentMode() === 'overlay';\n els.overlayBox.classList.toggle('d-none', !overlay);\n els.zonalBox.classList.toggle('d-none', overlay);\n suggestName();\n}\n\nfunction onOpChange() {\n const op = OVERLAY_OPS.find((o) => o.key === els.op.value);\n els.opHint.textContent = op ? op.hint : '';\n // Union can run on a single layer; the others need a second one.\n const needsB = op ? op.needsB : true;\n els.layerBWrap.querySelector('label').textContent =\n needsB ? 'Layer B' : 'Layer B (optional)';\n suggestName();\n}\n\nfunction onMembershipChange() {\n const m = MEMBERSHIP_MODES.find((x) => x.key === els.membership.value);\n els.memberHint.textContent = m ? m.hint : '';\n}\n\nfunction suggestName() {\n if (els.outName.dataset.touched === '1') return;\n if (currentMode() === 'overlay') {\n const op = els.op.value || 'overlay';\n els.outName.value = `${op}_result`;\n } else {\n els.outName.value = 'zonal_stats';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Run\n// ---------------------------------------------------------------------------\n\n/**\n * Features of the chosen layer, narrowed by the \"Apply to\" scope.\n * 'view' uses the source's spatial index, so it stays fast on large layers.\n */\nfunction featuresOf(select) {\n const choice = layerChoices[Number(select.value)];\n if (!choice) return [];\n const source = choice.layer.getSource();\n const scope = els.scope ? els.scope.value : 'all';\n\n if (scope === 'view') {\n const map = mapView.getMap();\n const extent = map.getView().calculateExtent(map.getSize());\n return source.getFeaturesInExtent(extent);\n }\n if (scope === 'selection') {\n const selected = new Set(mapView.getSelectedFeatures());\n return source.getFeatures().filter((f) => selected.has(f));\n }\n if (scope === 'drawn' && drawnScope) {\n return source.getFeatures().filter((f) => drawnScope.features.has(f));\n }\n return source.getFeatures();\n}\n\nfunction onScopeChange() {\n const scope = els.scope.value;\n if (scope === 'drawn' && drawnScope) {\n els.scopeHint.textContent =\n `${drawnScope.features.size.toLocaleString()} feature(s) caught by the ${drawnScope.label}.`;\n } else if (scope === 'selection') {\n const n = mapView ? mapView.getSelectedFeatures().length : 0;\n els.scopeHint.textContent = n === 0\n ? 'Nothing selected yet — use the Digitise tool: shift-click features, or hold Ctrl/Cmd and drag a box.'\n : `${n.toLocaleString()} feature(s) currently selected.`;\n } else if (scope === 'view') {\n els.scopeHint.textContent = 'Only features inside the current map view are analysed — zoom to the area of interest first.';\n } else {\n els.scopeHint.textContent = 'Every feature in the chosen layer. Narrow this if a large layer is slow.';\n }\n}\n\nfunction ensureAnalysisGroup() {\n let group = mapView.getLayerGroupByTitle(ANALYSIS_GROUP_TITLE);\n if (!group) {\n mapView.addLayerGroup(ANALYSIS_GROUP_ID, ANALYSIS_GROUP_TITLE,\n 'Results of client-side spatial analysis');\n group = mapView.getLayerGroupByTitle(ANALYSIS_GROUP_TITLE);\n }\n return group;\n}\n\nfunction onRun() {\n els.result.innerHTML = '';\n els.btnRun.disabled = true;\n // Yield a frame so the button state paints before a heavy synchronous run.\n setTimeout(() => {\n try {\n const mode = currentMode();\n const name = (els.outName.value || '').trim() ||\n (mode === 'overlay' ? 'overlay_result' : 'zonal_stats');\n\n let geojson, summary;\n let zonalWarnings = [];\n if (mode === 'overlay') {\n lastZonal = null;\n const op = els.op.value;\n const a = featuresOf(els.layerA);\n const b = featuresOf(els.layerB);\n const needsB = (OVERLAY_OPS.find((o) => o.key === op) || {}).needsB;\n if (a.length === 0) throw new Error('Layer A has no features.');\n if (needsB && b.length === 0) throw new Error('This operation needs a second layer with features.');\n geojson = runOverlay(op, a, b);\n summary = `${geojson.features.length.toLocaleString()} feature(s) produced.`;\n } else {\n const zones = featuresOf(els.zoneLayer);\n const inputs = featuresOf(els.inputLayer);\n const stats = Array.from(els.statsBox.querySelectorAll('input:checked')).map((i) => i.value);\n const field = els.field.value || null;\n if (stats.length === 0) throw new Error('Select at least one statistic.');\n const needsField = stats.some((s) => (STATS.find((x) => x.key === s) || {}).needsField);\n if (needsField && !field) throw new Error('Sum / mean / min / max need a numeric field.');\n\n const res = zonalStatistics({\n zoneFeatures: zones, inputFeatures: inputs,\n stats, field, membership: els.membership.value,\n });\n geojson = res.geojson;\n zonalWarnings = res.warnings || [];\n summary = renderRows(res.rows);\n\n // Keep the table AND the settings that produced it, so the Excel export\n // is self-describing — a table of numbers with no record of how it was\n // produced can't be checked or reproduced later.\n const membershipLabel = (MEMBERSHIP_MODES.find((m) => m.key === els.membership.value) || {}).label;\n lastZonal = {\n rows: res.rows,\n warnings: res.warnings || [],\n params: {\n 'Analysis': 'Zonal statistics',\n 'Zone layer': labelOfSelect(els.zoneLayer),\n 'Features summarised': labelOfSelect(els.inputLayer),\n 'Applied to': scopeLabel(),\n 'Zones analysed': zones.length,\n 'Features analysed': inputs.length,\n 'Counted when': membershipLabel || els.membership.value,\n 'Statistics': stats.map((k) => (STATS.find((x) => x.key === k) || {}).label || k).join(', '),\n 'Numeric field': field || '(none)',\n 'Result layer': name,\n },\n };\n }\n\n if (!geojson.features.length) {\n els.result.innerHTML =\n `
No result features — the layers may not overlap.
`;\n return;\n }\n\n const group = ensureAnalysisGroup();\n mapView.addGeoJSONLayer(geojson, name, {\n strokeColor: '#d97706', strokeWidth: 2, fillColor: 'rgba(217,119,6,0.18)',\n }, group);\n\n syncExportButton();\n const warnHtml = zonalWarnings.length\n ? `
` +\n zonalWarnings.map((w) => `
${escapeHtml(w)}
`).join('') + `
`\n : '';\n els.result.innerHTML =\n `
Added layer “${escapeHtml(name)}”.
` +\n warnHtml +\n (typeof summary === 'string' && summary.startsWith('<') ? summary : `
${escapeHtml(summary)}
`);\n showToast(`Analysis complete — “${name}” added to the Analysis group.`, 'success', 4000);\n } catch (err) {\n console.error('[Analysis] run failed:', err);\n els.result.innerHTML =\n `
${escapeHtml(err.message || String(err))}
`;\n } finally {\n els.btnRun.disabled = false;\n }\n }, 20);\n}\n\n/** Human label of a layer \n \n `).join('');\n}\n\nfunction refreshFieldOptions() {\n const choice = layerChoices[Number(els.inputLayer.value)];\n const feats = choice ? choice.layer.getSource().getFeatures() : [];\n const fields = listNumericFields(feats);\n els.field.innerHTML = '' +\n fields.map((f) => ``).join('');\n els.field.disabled = fields.length === 0;\n}\n\n// ---------------------------------------------------------------------------\n// UI reactions\n// ---------------------------------------------------------------------------\n\nfunction currentMode() {\n return (els.mode.find((r) => r.checked) || {}).value || 'overlay';\n}\n\nfunction onModeChange() {\n // A previous zonal table doesn't belong to a new overlay run.\n lastZonal = null; syncExportButton();\n const overlay = currentMode() === 'overlay';\n els.overlayBox.classList.toggle('d-none', !overlay);\n els.zonalBox.classList.toggle('d-none', overlay);\n suggestName();\n}\n\nfunction onOpChange() {\n const op = OVERLAY_OPS.find((o) => o.key === els.op.value);\n els.opHint.textContent = op ? op.hint : '';\n // Union can run on a single layer; the others need a second one.\n const needsB = op ? op.needsB : true;\n els.layerBWrap.querySelector('label').textContent =\n needsB ? 'Layer B' : 'Layer B (optional)';\n suggestName();\n}\n\nfunction onMembershipChange() {\n const m = MEMBERSHIP_MODES.find((x) => x.key === els.membership.value);\n els.memberHint.textContent = m ? m.hint : '';\n}\n\nfunction suggestName() {\n if (els.outName.dataset.touched === '1') return;\n if (currentMode() === 'overlay') {\n const op = els.op.value || 'overlay';\n els.outName.value = `${op}_result`;\n } else {\n els.outName.value = 'zonal_stats';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Run\n// ---------------------------------------------------------------------------\n\n/**\n * Features of the chosen layer, narrowed by the \"Apply to\" scope.\n * 'view' uses the source's spatial index, so it stays fast on large layers.\n */\nfunction featuresOf(select) {\n const choice = layerChoices[Number(select.value)];\n if (!choice) return [];\n const source = choice.layer.getSource();\n const scope = els.scope ? els.scope.value : 'all';\n\n if (scope === 'view') {\n const map = mapView.getMap();\n const extent = map.getView().calculateExtent(map.getSize());\n return source.getFeaturesInExtent(extent);\n }\n if (scope === 'selection') {\n const selected = new Set(mapView.getSelectedFeatures());\n return source.getFeatures().filter((f) => selected.has(f));\n }\n if (scope === 'drawn' && drawnScope) {\n return source.getFeatures().filter((f) => drawnScope.features.has(f));\n }\n return source.getFeatures();\n}\n\nfunction onScopeChange() {\n const scope = els.scope.value;\n if (scope === 'drawn' && drawnScope) {\n els.scopeHint.textContent =\n `${drawnScope.features.size.toLocaleString()} feature(s) caught by the ${drawnScope.label}.`;\n } else if (scope === 'selection') {\n const n = mapView ? mapView.getSelectedFeatures().length : 0;\n els.scopeHint.textContent = n === 0\n ? 'Nothing selected yet — use the Digitise tool: shift-click features, or hold Ctrl/Cmd and drag a box.'\n : `${n.toLocaleString()} feature(s) currently selected.`;\n } else if (scope === 'view') {\n els.scopeHint.textContent = 'Only features inside the current map view are analysed — zoom to the area of interest first.';\n } else {\n els.scopeHint.textContent = 'Every feature in the chosen layer. Narrow this if a large layer is slow.';\n }\n}\n\nfunction ensureAnalysisGroup() {\n let group = mapView.getLayerGroupByTitle(ANALYSIS_GROUP_TITLE);\n if (!group) {\n mapView.addLayerGroup(ANALYSIS_GROUP_ID, ANALYSIS_GROUP_TITLE,\n 'Results of client-side spatial analysis');\n group = mapView.getLayerGroupByTitle(ANALYSIS_GROUP_TITLE);\n }\n return group;\n}\n\nfunction onRun() {\n els.result.innerHTML = '';\n els.btnRun.disabled = true;\n // Yield a frame so the button state paints before a heavy synchronous run.\n setTimeout(() => {\n try {\n const mode = currentMode();\n const name = (els.outName.value || '').trim() ||\n (mode === 'overlay' ? 'overlay_result' : 'zonal_stats');\n\n let geojson, summary;\n let zonalWarnings = [];\n if (mode === 'overlay') {\n lastZonal = null;\n const op = els.op.value;\n const a = featuresOf(els.layerA);\n const b = featuresOf(els.layerB);\n const needsB = (OVERLAY_OPS.find((o) => o.key === op) || {}).needsB;\n if (a.length === 0) throw new Error('Layer A has no features.');\n if (needsB && b.length === 0) throw new Error('This operation needs a second layer with features.');\n geojson = runOverlay(op, a, b);\n summary = `${geojson.features.length.toLocaleString()} feature(s) produced.`;\n } else {\n const zones = featuresOf(els.zoneLayer);\n const inputs = featuresOf(els.inputLayer);\n const stats = Array.from(els.statsBox.querySelectorAll('input:checked')).map((i) => i.value);\n const field = els.field.value || null;\n if (stats.length === 0) throw new Error('Select at least one statistic.');\n const needsField = stats.some((s) => (STATS.find((x) => x.key === s) || {}).needsField);\n if (needsField && !field) throw new Error('Sum / mean / min / max need a numeric field.');\n\n const res = zonalStatistics({\n zoneFeatures: zones, inputFeatures: inputs,\n stats, field, membership: els.membership.value,\n });\n geojson = res.geojson;\n zonalWarnings = res.warnings || [];\n summary = renderRows(res.rows);\n\n // Keep the table AND the settings that produced it, so the Excel export\n // is self-describing — a table of numbers with no record of how it was\n // produced can't be checked or reproduced later.\n const membershipLabel = (MEMBERSHIP_MODES.find((m) => m.key === els.membership.value) || {}).label;\n lastZonal = {\n rows: res.rows,\n warnings: res.warnings || [],\n params: {\n 'Analysis': 'Zonal statistics',\n 'Zone layer': labelOfSelect(els.zoneLayer),\n 'Features summarised': labelOfSelect(els.inputLayer),\n 'Applied to': scopeLabel(),\n 'Zones analysed': zones.length,\n 'Features analysed': inputs.length,\n 'Counted when': membershipLabel || els.membership.value,\n 'Statistics': stats.map((k) => (STATS.find((x) => x.key === k) || {}).label || k).join(', '),\n 'Numeric field': field || '(none)',\n 'Result layer': name,\n },\n };\n }\n\n if (!geojson.features.length) {\n els.result.innerHTML =\n `
No result features — the layers may not overlap.
`;\n return;\n }\n\n const group = ensureAnalysisGroup();\n mapView.addGeoJSONLayer(geojson, name, {\n strokeColor: '#d97706', strokeWidth: 2, fillColor: 'rgba(217,119,6,0.18)',\n }, group);\n\n syncExportButton();\n const warnHtml = zonalWarnings.length\n ? `
` +\n zonalWarnings.map((w) => `
${escapeHtml(w)}
`).join('') + `
`\n : '';\n els.result.innerHTML =\n `
Added layer “${escapeHtml(name)}”.
` +\n warnHtml +\n (typeof summary === 'string' && summary.startsWith('<') ? summary : `
${escapeHtml(summary)}
`);\n showToast(`Analysis complete — “${name}” added to the Analysis group.`, 'success', 4000);\n } catch (err) {\n console.error('[Analysis] run failed:', err);\n els.result.innerHTML =\n `
${escapeHtml(err.message || String(err))}
`;\n } finally {\n els.btnRun.disabled = false;\n }\n }, 20);\n}\n\n/** Human label of a layer
@@ -651,7 +651,7 @@ import{_ as Le}from"./vite-helpers-BlTxHScW.js";import{M as lt}from"./bootstrap-
${a.label}
- `).join("")+"",t.appendChild(n),this._basemapPanel=n,this._basemapToggle=r;const s=a=>{const i=a||this._baseMapLayers?.find(l=>l.getVisible())?.get("basemapKey");n.querySelectorAll('input[name="lupmis-basemap"]').forEach(l=>{l.checked=l.value===i})};s(),r.addEventListener("click",a=>{a.stopPropagation();const i=!n.classList.contains("open");n.classList.toggle("open",i),r.classList.toggle("active",i),i&&s()}),document.addEventListener("click",a=>{n.classList.contains("open")&&(n.contains(a.target)||r.contains(a.target)||(n.classList.remove("open"),r.classList.remove("active")))}),n.addEventListener("change",a=>{const i=a.target.closest('input[type=radio][name="lupmis-basemap"]');if(!i)return;const l=i.value;this.setBaseMap(l);try{localStorage.setItem("default-basemap",l)}catch{}n.classList.remove("open"),r.classList.remove("active")}),this.map.on("basemapchange",a=>s(a.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 A({color:"rgba(0,94,184,0.12)"}),stroke:new I({color:"rgba(0,94,184,0.35)",width:1})}):new M({image:new he({radius:7,fill:new A({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 n=ee([e,t]);if(this._gpsPositionSource.clear(),r&&r>0){const a=r/Math.cos(t*Math.PI/180),i=new fe({geometry:new at([this._circleRing(n,a)])});i.set("_kind","accuracy"),this._gpsPositionSource.addFeature(i)}const s=new fe({geometry:new dt(n)});s.set("_kind","dot"),this._gpsPositionSource.addFeature(s)}_circleRing(e,t,r=48){const n=[],a=t/1;for(let i=0;i<=r;i++){const l=i/r*2*Math.PI;n.push([e[0]+a*Math.cos(l),e[1]+a*Math.sin(l)])}return n}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 n=()=>{r.classList.remove("open"),t.classList.remove("active")},s=()=>{r.classList.add("open"),t.classList.add("active")};t.addEventListener("click",a=>{a.stopPropagation(),r.classList.contains("open")?n():s()}),document.addEventListener("click",a=>{r.classList.contains("open")&&(r.contains(a.target)||t.contains(a.target)||this._gpsRecording||n())}),this._locateMeBtn.addEventListener("click",a=>{a.stopPropagation();for(const i of this._gpsCallbacks.locate)try{i()}catch(l){console.error(l)}this._gpsRecording||n()}),this._recordBtn.addEventListener("click",a=>{a.stopPropagation();const i=!this._gpsRecording;for(const l of this._gpsCallbacks.record)try{l(i)}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 A({color:"rgba(220, 38, 38, 0.25)"}),stroke:new I({color:"#dc2626",width:3})})}),new M({text:new We({text:r,font:"40px sans-serif",textBaseline:"bottom",textAlign:"center",offsetY:-5})})];const n=e.get("style");return n||(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 n=this.getEmoji(t),s=r.fontSize||28;this.categoryStyles[t]=this.createEmojiStyle(n,s)}this.markerSource.changed()}addMarker(e,t,r={}){console.log("[MapView] Adding marker at",e,t,"with properties:",r);const n=new fe({geometry:new dt(ee([e,t])),...r});return n.set("lon",e),n.set("lat",t),this.markerSource.addFeature(n),console.log("[MapView] Marker added, total features:",this.markerSource.getFeatures().length),n}addMarkers(e){console.log("[MapView] Adding",e.length,"markers");const t=e.map(r=>new fe({geometry:new dt(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,n=!1,s=null;if(this.map.forEachFeatureAtPixel(t.pixel,l=>{l.get("_layerType")==="parcel"&&(n=!0),l.get("name")&&(s=l),r=!0}),r&&!n&&!s)return;const[a,i]=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(a,i,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,n]=xe(t.coordinate);let s=null;this.map.forEachFeatureAtPixel(t.pixel,a=>{if(a.get("name"))return s=a,!0}),this.map.getTargetElement().style.cursor=s?"pointer":"",e(r,n,s,t)})}enableHoverCursor(){}addGeoJSONLayer(e,t,r={},n=null){const{strokeColor:s="#3b82f6",strokeWidth:a=2,strokeDash:i=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 A({color:l}),y=new he({radius:u,fill:new A({color:f||s}),stroke:new I({color:p,width:h})}),w=new I({color:s,width:a,...i?{lineDash:i}:{}});let _;if(c){const S=d??a+2;_=[new M({stroke:new I({color:c,width:S})}),new M({stroke:w,fill:m,image:y})]}else _=new M({stroke:w,fill:m,image:y});const E=new D({title:t,source:g,style:_,...r.declutter?{declutter:r.declutter}:{}});E.set("typeTag",r.typeTag||"VEC");const T=S=>S?S.includes("Polygon")?"Vector / Polygon":S.includes("LineString")?"Vector / Line":S.includes("Point")?"Vector / Point":"Vector":null;if(r.typeDescription)E.set("typeDescription",r.typeDescription);else{const S=g.getFeatures(),P=T(S[0]?.getGeometry?.()?.getType?.());if(P)E.set("typeDescription",P);else{const k=j=>{const q=T(j.feature.getGeometry?.()?.getType?.());q&&E.set("typeDescription",q),g.un("addfeature",k)};g.on("addfeature",k)}}return(n||this.overlayGroup).getLayers().push(E),console.log("[MapView] GeoJSON layer added:",t,"→",g.getFeatures().length,"features",n?`(in group "${n.get("title")}")`:""),E}addLayerGroup(e,t,r=""){const n=new Me({title:t.trim()});return n.set("layerId",e),n.set("description",r),this.overlayGroup.getLayers().push(n),console.log("[MapView] Layer group added:",t.trim(),"(id:",e+")"),n}addWMSLayer(e,t,r,n,s={}){const a=this.getLayerGroupByTitle(e);if(!a)return console.warn(`[MapView] Layer group "${e}" not found — cannot add WMS layer "${t}"`),null;const i={LAYERS:n,TILED:!0,WIDTH:256,HEIGHT:256};s.style!==void 0&&(i.STYLES=s.style);const l=new ko({url:r,params:i,serverType:s.serverType!==void 0?s.serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1,attributions:s.attributions}),c=new se({title:t,visible:s.visible!==void 0?s.visible:!0,source:l,opacity:s.opacity!==void 0?s.opacity:1,zIndex:s.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)}),a.getLayers().push(c),s.legendUrl)try{this._registerLegend(c,t,s.legendUrl)}catch(d){console.warn(`[MapView] Could not register legend for "${t}":`,d)}return s.onlineOnly&&this._attachOnlineOnlyHandler(c,t),console.log(`[MapView] WMS layer added: "${t}" → group "${e}"`),c}addXYZLayer(e,t,r,n={}){const s=this.getLayerGroupByTitle(e);if(!s)return console.warn(`[MapView] Layer group "${e}" not found — cannot add XYZ layer "${t}"`),null;const a=new Ae({url:r,crossOrigin:"anonymous",maxZoom:n.maxZoom!==void 0?n.maxZoom:19,attributions:n.attributions}),i=new se({title:t,visible:n.visible!==void 0?n.visible:!0,source:a,opacity:n.opacity!==void 0?n.opacity:1,zIndex:n.zIndex});if(i.set("typeTag","XYZ"),i.set("typeDescription","XYZ / Tile"),a.on("tileloaderror",()=>{F(`XYZ layer "${t}" — tile load error. Check the URL.`,"warning",5e3)}),s.getLayers().push(i),n.legendUrl)try{this._registerLegend(i,t,n.legendUrl)}catch(l){console.warn(`[MapView] Could not register legend for "${t}":`,l)}return n.onlineOnly&&this._attachOnlineOnlyHandler(i,t),console.log(`[MapView] XYZ layer added: "${t}" → group "${e}"`),i}async addCOGLayer(e,t,r,n={}){const s=this.getLayerGroupByTitle(e);if(!s)return console.warn(`[MapView] Layer group "${e}" not found — cannot add COG layer "${t}"`),null;const a=await this._probeCOGUrl(r);if(a)return console.warn(`[MapView] COG pre-flight failed for "${t}":`,a),F(a,"error",8e3),null;let i,l;try{[{default:i},{default:l}]=await Promise.all([Le(()=>import("./geotiff-B_Wfowsp.js").then(d=>d.W),__vite__mapDeps([0,1,2,3,4])),Le(()=>import("./geotiff-B_Wfowsp.js").then(d=>d.G),__vite__mapDeps([0,1,2,3,4]))])}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,...n.min!==void 0?{min:n.min}:{},...n.max!==void 0?{max:n.max}:{}}],normalize:n.normalize!==void 0?n.normalize:!0,attributions:n.attributions},u=new l(d);c=new i({title:t,source:u,visible:n.visible!==void 0?n.visible:!0,opacity:n.opacity!==void 0?n.opacity:1,zIndex:n.zIndex,...n.style?{style:n.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)}),s.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 n.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,n=[],s=a=>{if(!a)return;if(typeof a.getLayers=="function"){a.getLayers().forEach(s);return}if(!(a instanceof D)||a===this._vertexOverlayLayer||a===this._gpsTrailLayer||a===this._gpsPositionLayer||a.get("displayInLayerSwitcher")===!1||t&&a.getVisible&&!a.getVisible())return;const i=a.getSource&&a.getSource();if(!i||typeof i.getFeatures!="function")return;const l=i.getFeatures().length;r&&l===0||n.push({layer:a,title:a.get("title")||"(untitled layer)",count:l})};return this.map.getLayers().forEach(s),n}_createAddLayerDialog(){this._addLayerDialog=document.createElement("div"),this._addLayerDialog.className="map-add-layer-dialog",this._addLayerDialog.style.cssText=` + `).join("")+"",t.appendChild(n),this._basemapPanel=n,this._basemapToggle=r;const s=a=>{const i=a||this._baseMapLayers?.find(l=>l.getVisible())?.get("basemapKey");n.querySelectorAll('input[name="lupmis-basemap"]').forEach(l=>{l.checked=l.value===i})};s(),r.addEventListener("click",a=>{a.stopPropagation();const i=!n.classList.contains("open");n.classList.toggle("open",i),r.classList.toggle("active",i),i&&s()}),document.addEventListener("click",a=>{n.classList.contains("open")&&(n.contains(a.target)||r.contains(a.target)||(n.classList.remove("open"),r.classList.remove("active")))}),n.addEventListener("change",a=>{const i=a.target.closest('input[type=radio][name="lupmis-basemap"]');if(!i)return;const l=i.value;this.setBaseMap(l);try{localStorage.setItem("default-basemap",l)}catch{}n.classList.remove("open"),r.classList.remove("active")}),this.map.on("basemapchange",a=>s(a.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 A({color:"rgba(0,94,184,0.12)"}),stroke:new I({color:"rgba(0,94,184,0.35)",width:1})}):new M({image:new he({radius:7,fill:new A({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 n=ee([e,t]);if(this._gpsPositionSource.clear(),r&&r>0){const a=r/Math.cos(t*Math.PI/180),i=new fe({geometry:new at([this._circleRing(n,a)])});i.set("_kind","accuracy"),this._gpsPositionSource.addFeature(i)}const s=new fe({geometry:new dt(n)});s.set("_kind","dot"),this._gpsPositionSource.addFeature(s)}_circleRing(e,t,r=48){const n=[],a=t/1;for(let i=0;i<=r;i++){const l=i/r*2*Math.PI;n.push([e[0]+a*Math.cos(l),e[1]+a*Math.sin(l)])}return n}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 n=()=>{r.classList.remove("open"),t.classList.remove("active")},s=()=>{r.classList.add("open"),t.classList.add("active")};t.addEventListener("click",a=>{a.stopPropagation(),r.classList.contains("open")?n():s()}),document.addEventListener("click",a=>{r.classList.contains("open")&&(r.contains(a.target)||t.contains(a.target)||this._gpsRecording||n())}),this._locateMeBtn.addEventListener("click",a=>{a.stopPropagation();for(const i of this._gpsCallbacks.locate)try{i()}catch(l){console.error(l)}this._gpsRecording||n()}),this._recordBtn.addEventListener("click",a=>{a.stopPropagation();const i=!this._gpsRecording;for(const l of this._gpsCallbacks.record)try{l(i)}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 A({color:"rgba(220, 38, 38, 0.25)"}),stroke:new I({color:"#dc2626",width:3})})}),new M({text:new We({text:r,font:"40px sans-serif",textBaseline:"bottom",textAlign:"center",offsetY:-5})})];const n=e.get("style");return n||(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 n=this.getEmoji(t),s=r.fontSize||28;this.categoryStyles[t]=this.createEmojiStyle(n,s)}this.markerSource.changed()}addMarker(e,t,r={}){console.log("[MapView] Adding marker at",e,t,"with properties:",r);const n=new fe({geometry:new dt(ee([e,t])),...r});return n.set("lon",e),n.set("lat",t),this.markerSource.addFeature(n),console.log("[MapView] Marker added, total features:",this.markerSource.getFeatures().length),n}addMarkers(e){console.log("[MapView] Adding",e.length,"markers");const t=e.map(r=>new fe({geometry:new dt(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,n=!1,s=null;if(this.map.forEachFeatureAtPixel(t.pixel,l=>{l.get("_layerType")==="parcel"&&(n=!0),l.get("name")&&(s=l),r=!0}),r&&!n&&!s)return;const[a,i]=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(a,i,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,n]=xe(t.coordinate);let s=null;this.map.forEachFeatureAtPixel(t.pixel,a=>{if(a.get("name"))return s=a,!0}),this.map.getTargetElement().style.cursor=s?"pointer":"",e(r,n,s,t)})}enableHoverCursor(){}addGeoJSONLayer(e,t,r={},n=null){const{strokeColor:s="#3b82f6",strokeWidth:a=2,strokeDash:i=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 A({color:l}),y=new he({radius:u,fill:new A({color:f||s}),stroke:new I({color:p,width:h})}),w=new I({color:s,width:a,...i?{lineDash:i}:{}});let _;if(c){const S=d??a+2;_=[new M({stroke:new I({color:c,width:S})}),new M({stroke:w,fill:m,image:y})]}else _=new M({stroke:w,fill:m,image:y});const E=new D({title:t,source:g,style:_,...r.declutter?{declutter:r.declutter}:{}});E.set("typeTag",r.typeTag||"VEC");const T=S=>S?S.includes("Polygon")?"Vector / Polygon":S.includes("LineString")?"Vector / Line":S.includes("Point")?"Vector / Point":"Vector":null;if(r.typeDescription)E.set("typeDescription",r.typeDescription);else{const S=g.getFeatures(),P=T(S[0]?.getGeometry?.()?.getType?.());if(P)E.set("typeDescription",P);else{const k=j=>{const q=T(j.feature.getGeometry?.()?.getType?.());q&&E.set("typeDescription",q),g.un("addfeature",k)};g.on("addfeature",k)}}return(n||this.overlayGroup).getLayers().push(E),console.log("[MapView] GeoJSON layer added:",t,"→",g.getFeatures().length,"features",n?`(in group "${n.get("title")}")`:""),E}addLayerGroup(e,t,r=""){const n=new Me({title:t.trim()});return n.set("layerId",e),n.set("description",r),this.overlayGroup.getLayers().push(n),console.log("[MapView] Layer group added:",t.trim(),"(id:",e+")"),n}addWMSLayer(e,t,r,n,s={}){const a=this.getLayerGroupByTitle(e);if(!a)return console.warn(`[MapView] Layer group "${e}" not found — cannot add WMS layer "${t}"`),null;const i={LAYERS:n,TILED:!0,WIDTH:256,HEIGHT:256};s.style!==void 0&&(i.STYLES=s.style);const l=new Lo({url:r,params:i,serverType:s.serverType!==void 0?s.serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1,attributions:s.attributions}),c=new se({title:t,visible:s.visible!==void 0?s.visible:!0,source:l,opacity:s.opacity!==void 0?s.opacity:1,zIndex:s.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)}),a.getLayers().push(c),s.legendUrl)try{this._registerLegend(c,t,s.legendUrl)}catch(d){console.warn(`[MapView] Could not register legend for "${t}":`,d)}return s.onlineOnly&&this._attachOnlineOnlyHandler(c,t),console.log(`[MapView] WMS layer added: "${t}" → group "${e}"`),c}addXYZLayer(e,t,r,n={}){const s=this.getLayerGroupByTitle(e);if(!s)return console.warn(`[MapView] Layer group "${e}" not found — cannot add XYZ layer "${t}"`),null;const a=new Ae({url:r,crossOrigin:"anonymous",maxZoom:n.maxZoom!==void 0?n.maxZoom:19,attributions:n.attributions}),i=new se({title:t,visible:n.visible!==void 0?n.visible:!0,source:a,opacity:n.opacity!==void 0?n.opacity:1,zIndex:n.zIndex});if(i.set("typeTag","XYZ"),i.set("typeDescription","XYZ / Tile"),a.on("tileloaderror",()=>{F(`XYZ layer "${t}" — tile load error. Check the URL.`,"warning",5e3)}),s.getLayers().push(i),n.legendUrl)try{this._registerLegend(i,t,n.legendUrl)}catch(l){console.warn(`[MapView] Could not register legend for "${t}":`,l)}return n.onlineOnly&&this._attachOnlineOnlyHandler(i,t),console.log(`[MapView] XYZ layer added: "${t}" → group "${e}"`),i}async addCOGLayer(e,t,r,n={}){const s=this.getLayerGroupByTitle(e);if(!s)return console.warn(`[MapView] Layer group "${e}" not found — cannot add COG layer "${t}"`),null;const a=await this._probeCOGUrl(r);if(a)return console.warn(`[MapView] COG pre-flight failed for "${t}":`,a),F(a,"error",8e3),null;let i,l;try{[{default:i},{default:l}]=await Promise.all([Le(()=>import("./geotiff-B_Wfowsp.js").then(d=>d.W),__vite__mapDeps([0,1,2,3,4])),Le(()=>import("./geotiff-B_Wfowsp.js").then(d=>d.G),__vite__mapDeps([0,1,2,3,4]))])}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,...n.min!==void 0?{min:n.min}:{},...n.max!==void 0?{max:n.max}:{}}],normalize:n.normalize!==void 0?n.normalize:!0,attributions:n.attributions},u=new l(d);c=new i({title:t,source:u,visible:n.visible!==void 0?n.visible:!0,opacity:n.opacity!==void 0?n.opacity:1,zIndex:n.zIndex,...n.style?{style:n.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)}),s.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 n.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,n=[],s=a=>{if(!a)return;if(typeof a.getLayers=="function"){a.getLayers().forEach(s);return}if(!(a instanceof D)||a===this._vertexOverlayLayer||a===this._gpsTrailLayer||a===this._gpsPositionLayer||a.get("displayInLayerSwitcher")===!1||t&&a.getVisible&&!a.getVisible())return;const i=a.getSource&&a.getSource();if(!i||typeof i.getFeatures!="function")return;const l=i.getFeatures().length;r&&l===0||n.push({layer:a,title:a.get("title")||"(untitled layer)",count:l})};return this.map.getLayers().forEach(s),n}_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"./vite-helpers-BlTxHScW.js";import{M as lt}from"./bootstrap- - `,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"),n=e.querySelector(".add-layer-url"),s=e.querySelector(".add-layer-type-hint");e.querySelectorAll('input[name="add-layer-type"]').forEach(i=>{i.addEventListener("change",()=>{const l=i.value;s.textContent=l==="cog"?"Cloud-Optimized GeoTIFF — the server must allow CORS and byte-range requests.":"",l==="cog"?(t.style.display="none",n.placeholder="https://example.com/data/elevation_cog.tif"):l==="xyz"?(t.style.display="none",n.placeholder="https://example.com/tiles/{z}/{x}/{y}.png"):(t.style.display="",n.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 a=()=>this._hideAddLayerDialog();e.querySelector(".add-layer-close").addEventListener("click",a),e.querySelector(".add-layer-cancel").addEventListener("click",a),this._addLayerDialog.addEventListener("click",i=>{i.target===this._addLayerDialog&&a()}),e.querySelector(".add-layer-confirm").addEventListener("click",()=>{const i=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((i==="wms"||i==="wfs")&&!c){e.querySelector(".add-layer-name").style.borderColor="#ef4444";return}if(!d){e.querySelector(".add-layer-title").style.borderColor="#ef4444";return}this._addExternalLayer(i,l,c,d),this._hideAddLayerDialog()}),e.addEventListener("keydown",i=>{i.key==="Enter"&&(i.preventDefault(),e.querySelector(".add-layer-confirm").click()),i.key==="Escape"&&(i.preventDefault(),a())})}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,n){const s=this._externalSourceGroup;if(!s){F('Layer group "External Source" not found.',"error",4e3);return}let a;switch(e){case"wms":{const i=new ko({url:t,params:{LAYERS:r,TILED:!0,WIDTH:256,HEIGHT:256},serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1});a=new se({title:n,visible:!0,source:i}),i.on("tileloaderror",()=>{F(`WMS "${n}" — tile load error. Check URL and layer name.`,"warning",5e3)});break}case"wfs":{const i=`${t}${t.includes("?")?"&":"?"}service=WFS&version=1.1.0&request=GetFeature&typename=${encodeURIComponent(r)}&outputFormat=application/json&srsname=EPSG:3857`,l=new G({url:i,format:new le});l.on("featuresloaderror",()=>{F(`WFS "${n}" — load error. Check URL and layer name.`,"warning",5e3)}),a=new D({title:n,visible:!0,source:l,style:new M({stroke:new I({color:"#e11d48",width:2}),fill:new A({color:"rgba(225,29,72,0.15)"})})});break}case"xyz":a=new se({title:n,visible:!0,source:new Ae({url:t,crossOrigin:"anonymous"})}),a.getSource().on("tileloaderror",()=>{F(`XYZ "${n}" — tile load error. Check the URL template.`,"warning",5e3)});break;case"cog":{const i=s.get("title")||"External Source";this.addCOGLayer(i,n,t).then(l=>{l&&(l.set("removable",!0),F(`Layer "${n}" added to External Source.`,"success",3e3),console.log(`[MapView] External COG layer added: "${n}"`))});return}default:F(`Unknown layer type: ${e}`,"error",4e3);return}a.set("typeTag",e.toUpperCase()),a.set("typeDescription",{wms:"WMS / Raster",wfs:"WFS / Vector",xyz:"XYZ / Tile"}[e]||e.toUpperCase()),a.set("removable",!0),s.getLayers().push(a),F(`Layer "${n}" added to External Source.`,"success",3e3),console.log(`[MapView] External ${e.toUpperCase()} layer added: "${n}"`)}_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 n=t.querySelector(":scope > .ol-layerswitcher-buttons");if(n){const c=n.querySelector(":scope > .expend-layers, :scope > .collapse-layers");c&&!c.querySelector(":scope > svg.ls-chevron-svg")&&(c.innerHTML='')}const s=t.querySelector(":scope > .li-content"),a=()=>{if(!s)return;const c=e.get("typeDescription");let d=s.querySelector(":scope > .ls-layer-subtitle");if(!c){d&&d.remove();return}if(!d){d=document.createElement("div"),d.className="ls-layer-subtitle";const u=s.querySelector(":scope > label");u&&u.nextSibling?s.insertBefore(d,u.nextSibling):s.appendChild(d)}d.textContent=c};if(a(),e._lsSubtitleHooked||(e._lsSubtitleHooked=!0,e.on("change:typeDescription",()=>{a()})),e.get("removable")===!0&&n&&!n.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)}),n.appendChild(c)}const i=e.get("_externalImportId");if(i!=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:i,status:u,layer:e}}))})}}if((e.get("title")||"").toLowerCase().includes("external")&&(this._externalSourceGroup=e,n&&!n.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"),n=e.querySelector(".add-layer-url"),s=e.querySelector(".add-layer-type-hint");e.querySelectorAll('input[name="add-layer-type"]').forEach(i=>{i.addEventListener("change",()=>{const l=i.value;s.textContent=l==="cog"?"Cloud-Optimized GeoTIFF — the server must allow CORS and byte-range requests.":"",l==="cog"?(t.style.display="none",n.placeholder="https://example.com/data/elevation_cog.tif"):l==="xyz"?(t.style.display="none",n.placeholder="https://example.com/tiles/{z}/{x}/{y}.png"):(t.style.display="",n.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 a=()=>this._hideAddLayerDialog();e.querySelector(".add-layer-close").addEventListener("click",a),e.querySelector(".add-layer-cancel").addEventListener("click",a),this._addLayerDialog.addEventListener("click",i=>{i.target===this._addLayerDialog&&a()}),e.querySelector(".add-layer-confirm").addEventListener("click",()=>{const i=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((i==="wms"||i==="wfs")&&!c){e.querySelector(".add-layer-name").style.borderColor="#ef4444";return}if(!d){e.querySelector(".add-layer-title").style.borderColor="#ef4444";return}this._addExternalLayer(i,l,c,d),this._hideAddLayerDialog()}),e.addEventListener("keydown",i=>{i.key==="Enter"&&(i.preventDefault(),e.querySelector(".add-layer-confirm").click()),i.key==="Escape"&&(i.preventDefault(),a())})}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,n){const s=this._externalSourceGroup;if(!s){F('Layer group "External Source" not found.',"error",4e3);return}let a;switch(e){case"wms":{const i=new Lo({url:t,params:{LAYERS:r,TILED:!0,WIDTH:256,HEIGHT:256},serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1});a=new se({title:n,visible:!0,source:i}),i.on("tileloaderror",()=>{F(`WMS "${n}" — tile load error. Check URL and layer name.`,"warning",5e3)});break}case"wfs":{const i=`${t}${t.includes("?")?"&":"?"}service=WFS&version=1.1.0&request=GetFeature&typename=${encodeURIComponent(r)}&outputFormat=application/json&srsname=EPSG:3857`,l=new G({url:i,format:new le});l.on("featuresloaderror",()=>{F(`WFS "${n}" — load error. Check URL and layer name.`,"warning",5e3)}),a=new D({title:n,visible:!0,source:l,style:new M({stroke:new I({color:"#e11d48",width:2}),fill:new A({color:"rgba(225,29,72,0.15)"})})});break}case"xyz":a=new se({title:n,visible:!0,source:new Ae({url:t,crossOrigin:"anonymous"})}),a.getSource().on("tileloaderror",()=>{F(`XYZ "${n}" — tile load error. Check the URL template.`,"warning",5e3)});break;case"cog":{const i=s.get("title")||"External Source";this.addCOGLayer(i,n,t).then(l=>{l&&(l.set("removable",!0),F(`Layer "${n}" added to External Source.`,"success",3e3),console.log(`[MapView] External COG layer added: "${n}"`))});return}default:F(`Unknown layer type: ${e}`,"error",4e3);return}a.set("typeTag",e.toUpperCase()),a.set("typeDescription",{wms:"WMS / Raster",wfs:"WFS / Vector",xyz:"XYZ / Tile"}[e]||e.toUpperCase()),a.set("removable",!0),s.getLayers().push(a),F(`Layer "${n}" added to External Source.`,"success",3e3),console.log(`[MapView] External ${e.toUpperCase()} layer added: "${n}"`)}_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 n=t.querySelector(":scope > .ol-layerswitcher-buttons");if(n){const c=n.querySelector(":scope > .expend-layers, :scope > .collapse-layers");c&&!c.querySelector(":scope > svg.ls-chevron-svg")&&(c.innerHTML='')}const s=t.querySelector(":scope > .li-content"),a=()=>{if(!s)return;const c=e.get("typeDescription");let d=s.querySelector(":scope > .ls-layer-subtitle");if(!c){d&&d.remove();return}if(!d){d=document.createElement("div"),d.className="ls-layer-subtitle";const u=s.querySelector(":scope > label");u&&u.nextSibling?s.insertBefore(d,u.nextSibling):s.appendChild(d)}d.textContent=c};if(a(),e._lsSubtitleHooked||(e._lsSubtitleHooked=!0,e.on("change:typeDescription",()=>{a()})),e.get("removable")===!0&&n&&!n.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)}),n.appendChild(c)}const i=e.get("_externalImportId");if(i!=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:i,status:u,layer:e}}))})}}if((e.get("title")||"").toLowerCase().includes("external")&&(this._externalSourceGroup=e,n&&!n.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,7 +739,7 @@ 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 To}_registerLegend(e,t,r){if(!this._legendPanel)return;const n=document.createElement("div");n.className="map-legend-entry",n.style.cssText="border-bottom:1px solid var(--border, #1e1a4b1f);padding-bottom:6px;",n.innerHTML=` + `,this.map.getTargetElement().appendChild(this._legendPanel),this._legendEntries=new Map}_registerLegend(e,t,r){if(!this._legendPanel)return;const n=document.createElement("div");n.className="map-legend-entry",n.style.cssText="border-bottom:1px solid var(--border, #1e1a4b1f);padding-bottom:6px;",n.innerHTML=`
${this._escapeHtml(t)}
@@ -757,7 +757,7 @@ This only affects the current session — built-in layers cannot be removed.`))r `;o?(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 to=null;const oo=new Set;function ha(o="#offline-indicator"){to=typeof o=="string"?document.querySelector(o):o,jt(!navigator.onLine),window.addEventListener("online",()=>{console.log("[PWA] Back online"),jt(!1),Vo(!1)}),window.addEventListener("offline",()=>{console.log("[PWA] Gone offline"),jt(!0),Vo(!0)})}function jt(o){to&&(to.style.display=o?"block":"none"),document.body.classList.toggle("is-offline",o)}function jn(o){return oo.add(o),o(!navigator.onLine),()=>oo.delete(o)}function Vo(o){for(const e of oo)try{e(o)}catch(t){console.error("[PWA] Offline listener error:",t)}}function K(){return navigator.onLine}function ga(){confirm("A new version is available. Reload now?")&&ma()}function ma(){Oe?.waiting&&Oe.waiting.postMessage({type:"SKIP_WAITING"}),window.location.reload()}async function ya({timeoutMs:o=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((s,a)=>setTimeout(()=>a(new Error("Service-worker readiness timeout")),o)),r=await Promise.race([e,t]),n=navigator.serviceWorker.controller||r.active;if(!n)throw new Error("No active service worker available");return n}function ba(o){if(!("serviceWorker"in navigator))return()=>{};const e=()=>{try{o()}catch(t){console.error("[PWA] controllerchange handler error:",t)}};return navigator.serviceWorker.addEventListener("controllerchange",e),()=>navigator.serviceWorker.removeEventListener("controllerchange",e)}async function wo(o,e,t={},r=5e3,n=1e4){const s=await ya({timeoutMs:n});return new Promise((a,i)=>{const l=new MessageChannel,c=setTimeout(()=>{l.port1.close(),i(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;a(f)}},s.postMessage({type:o,...t},[l.port2])})}async function wa(){try{return(await wo("GET_TILE_STATS","TILE_STATS")).stats}catch(o){return console.warn("[PWA] getTileCacheStats failed:",o),null}}async function _a(){try{return await wo("CLEAR_TILE_CACHES","TILE_CACHES_CLEARED"),!0}catch(o){return console.warn("[PWA] clearTileCaches failed:",o),!1}}async function va(o){if(!o)return!1;try{return!!(await wo("CLEAR_TILE_CACHE","TILE_CACHE_CLEARED",{cacheName:o})).deleted}catch(e){return console.warn(`[PWA] clearTileCacheForProvider(${o}) failed:`,e),!1}}async function Ea(){if(!navigator.storage?.estimate)return null;try{const{usage:o,quota:e}=await navigator.storage.estimate();return{usage:o||0,quota:e||0}}catch(o){return console.warn("[PWA] getStorageEstimate failed:",o),null}}async function xa(o={}){const{installButton:e="#install-btn",offlineIndicator:t="#offline-indicator",autoRegisterSW:r=!0}=o;r?await da():await ua(),pa(e),ha(t),console.log("[PWA] Initialized")}const zn={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"}},Sa=30*1024,It=2*Math.PI*6378137/2;function Ko(o,e){const t=o/It*180;let r=e/It*180;return r=180/Math.PI*(2*Math.atan(Math.exp(r*Math.PI/180))-Math.PI/2),[t,r]}function Xo(o,e,t){const r=Math.pow(2,t),n=Math.floor((o+180)/360*r),s=e*Math.PI/180,a=Math.floor((1-Math.log(Math.tan(s)+1/Math.cos(s))/Math.PI)/2*r);return{x:n,y:a}}function qn(o,e){const[t,r,n,s]=o,[a,i]=Ko(t,r),[l,c]=Ko(n,s),d=Xo(a,c,e),u=Xo(l,i,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 Ta(o,e,t){let r=0;for(let n=e;n<=t;n++)r+=qn(o,n).count;return r}function La(o,e,t){const r=[];for(let n=e;n<=t;n++){const s=qn(o,n);for(let a=s.minX;a<=s.maxX;a++)for(let i=s.minY;i<=s.maxY;i++)r.push({z:n,x:a,y:i})}return r}function ka(o,{z:e,x:t,y:r}){return o.replace("{z}",e).replace("{x}",t).replace("{y}",r)}class Ia{constructor({baseMap:e,extent3857:t,minZoom:r,maxZoom:n,concurrency:s=2,interBatchDelayMs:a=50,onProgress:i=()=>{}}){const l=zn[e];if(!l)throw new Error(`Unknown base map: ${e}`);n>l.maxZoom&&(console.warn(`[OfflineTiles] ${e}: maxZoom ${n} > supported ${l.maxZoom}; clamping`),n=l.maxZoom),this.baseMap=e,this.template=l.url,this.extent=t,this.minZoom=r,this.maxZoom=n,this.concurrency=Math.max(1,Math.min(s,6)),this.interBatchDelayMs=a,this.onProgress=i,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=La(this.extent,this.minZoom,this.maxZoom),t=e.length,r=Date.now();let n=0,s=0,a=0,i=0;const l=c=>{const d=Date.now()-r,u=n>0?Math.round(d/n*(t-n)):null;this.onProgress({phase:c,done:n,total:t,ok:s,failed:a,cached:i,elapsedMs:d,etaMs:u})};l("running");for(let c=0;c{if(this._cancelled)return;const f=ka(this.template,u);try{const p=await fetch(f,{signal:this._abortCtrl.signal,cache:"default"});p.ok?(s++,p.body&&p.body.cancel().catch(()=>{})):(p.status,a++)}catch(p){p.name==="AbortError"||a++}n++})),l("running"),this.interBatchDelayMs>0&&c+this.concurrencysetTimeout(u,this.interBatchDelayMs))}return l(this._cancelled?"cancelled":"done"),{phase:this._cancelled?"cancelled":"done",done:n,total:t,ok:s,failed:a,cached:i,elapsedMs:Date.now()-r}}cancel(){this._cancelled=!0,this._abortCtrl&&this._abortCtrl.abort()}}const Pa=(()=>{const o=(r,n)=>{const s=r*It/180,a=Math.log(Math.tan((90+n)*Math.PI/360))/(Math.PI/180);return[s,a*It/180]},e=o(-3.3,4.5),t=o(1.2,11.2);return[e[0],e[1],t[0],t[1]]})();function Ma(o){return o*Sa}const Hn="/api",Wn=`${Hn}/spatial_planning`;function Aa(o){return o.includes("/")?`${Hn}/${o}`:`${Wn}/${o}`}const zt="1",Ca="1c46538c712e9b5b";function Fa(){try{if(typeof window>"u")return zt;const o=window.LUPMIS_SESSION;if(!o||typeof o!="object")return zt;const e=o.district_id;return e==null||String(e).length===0?null:String(e)}catch{}return zt}const _o={get district_id(){return Fa()},api_token:Ca};function Ke(){if(typeof window<"u"&&window.LUPMIS_SESSION&&window.LUPMIS_SESSION.user_id)return window.LUPMIS_SESSION;try{const o=localStorage.getItem("dev-session");if(o){const e=JSON.parse(o);if(e&&e.user_id)return e}}catch{}return null}typeof window<"u"&&(window.lupmisDevSession=o=>{o==null?(localStorage.removeItem("dev-session"),console.log("[Dev] Session override cleared. Reload to apply.")):(localStorage.setItem("dev-session",JSON.stringify(o)),console.log("[Dev] Session override saved. Reload to apply:",o))});const Oa=3e4,Da=5e3;let Fe=null;async function Ra(o=!1){if(Fe!==null&&!o)return Fe;const e=new AbortController,t=setTimeout(()=>e.abort(),Da);try{Fe=(await fetch(`${Wn}/get_layers.php`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(_o),signal:e.signal})).ok}catch{Fe=!1}finally{clearTimeout(t)}return console.log("[RemoteDB] Server reachable:",Fe),Fe}function ke(){return Fe}function Na(o,e=Oa){const t=new AbortController,r=setTimeout(()=>t.abort(),e);return o.signal&&o.signal.addEventListener("abort",()=>t.abort()),{signal:t.signal,clear:()=>clearTimeout(r)}}async function me(o,e={},t={}){const r=Aa(o),n={..._o,...e};console.log("[RemoteDB] POST",r);const s=Na(t);try{const a=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(n),...t,signal:s.signal});if(!a.ok)throw new Error(`HTTP ${a.status}: ${a.statusText}`);const i=await a.json();return console.log("[RemoteDB] POST response:",o,"→",typeof i=="object"?`${Array.isArray(i)?i.length+" items":"object"}`:i),i}catch(a){throw a.name==="AbortError"?(console.error("[RemoteDB] POST timed out:",o),new Error(`Request timed out: ${o}`)):(console.error("[RemoteDB] POST failed:",o,a),a)}finally{s.clear()}}async function $a(){return me("get_district_boundary.php")}async function Ba(){return me("get_layers.php")}async function Ga(){return me("get_all_collector_zone_per_district.php")}async function Ua(){return me("get_parcels_per_district.php")}async function ja(){return me("get_all_footprint_per_district.php")}async function za(){const o=_o.district_id;return me("contours/get_by_district.php",{districtid:o==null?null:Number(o)})}async function qa(){return me("get_osm_roads.php")}async function Ha(){return me("get_upn_grid_per_district.php")}async function Wa(o,e){const t={client_uuid:o.client_uuid,name:o.name??null,started_at:o.started_at,ended_at:o.ended_at,point_count:o.point_count??e.length,distance_m:o.distance_m??0,points:(e||[]).map(n=>({seq:n.seq,longitude:n.longitude,latitude:n.latitude,altitude:n.altitude??null,accuracy:n.accuracy??null,altitude_accuracy:n.altitude_accuracy??null,heading:n.heading??null,speed:n.speed??null,satellites:n.satellites??null,recorded_at:n.recorded_at}))},r=await me("save_gps_trail.php",t);return{remoteId:r?.id??r?.remote_id??null}}const Va=63710088e-1,De=Math.PI/180;function Ka(o,e,t,r){const n=(r-e)*De,s=(t-o)*De,a=Math.sin(n/2)**2+Math.cos(e*De)*Math.cos(r*De)*Math.sin(s/2)**2;return 2*Va*Math.asin(Math.min(1,Math.sqrt(a)))}function Yo(o,e=5){return o==null||Number.isNaN(o)?"—":o.toFixed(e)}const Jo=6378137,Zo=1/298.257223563,Qo=.9996,te=Zo*(2-Zo),qt=te/(1-te);function Xa(o,e){if(o==null||e==null||Number.isNaN(o)||Number.isNaN(e))return null;let t=((e+180)%360+360)%360-180;const r=Math.floor((t+180)/6)+1,n=(r-1)*6-180+3,s=o*De,a=t*De,i=n*De,l=Math.sin(s),c=Math.cos(s),d=Math.tan(s),u=Jo/Math.sqrt(1-te*l*l),f=d*d,p=qt*c*c,h=c*(a-i),g=Jo*((1-te/4-3*te**2/64-5*te**3/256)*s-(3*te/8+3*te**2/32+45*te**3/1024)*Math.sin(2*s)+(15*te**2/256+45*te**3/1024)*Math.sin(4*s)-35*te**3/3072*Math.sin(6*s));let m=Qo*u*(h+(1-f+p)*h**3/6+(5-18*f+f*f+72*p-58*qt)*h**5/120)+5e5,y=Qo*(g+u*d*(h*h/2+(5-f+9*p+4*p*p)*h**4/24+(61-58*f+f*f+600*p-330*qt)*h**6/720));const w=o>=0?"N":"S";return w==="S"&&(y+=1e7),{zone:r,hemisphere:w,easting:Math.round(m),northing:Math.round(y)}}function Ya(o,e){const t=Xa(o,e);return t?`${t.zone}${t.hemisphere} ${t.easting}E ${t.northing}N`:"—"}function Ja(o){return o==null||Number.isNaN(o)?"—":o<1e3?`${Math.round(o)} m`:`${(o/1e3).toFixed(2)} km`}function Za(o){return o==null||Number.isNaN(o)?"—":`±${Math.round(o)} m`}function Qa(o){return o==null||Number.isNaN(o)?"none":o<=10?"good":o<=30?"fair":"poor"}const ei={minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0,timeoutMs:15e3,maximumAgeMs:0};class st{constructor(e={}){this.opts={...ei,...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 n of r)try{n(t)}catch(s){console.error(`[GeoTracker] listener for "${e}" threw`,s)}}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 n=st.normalize(r);this._lastFix=n,this._emit("position",n),e(n)},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=st.uuid(),r=new Date().toISOString(),n={uuid:t,name:e.name||null,startedAt:r,...e},s=await this.storage.createTrail(n);return this._activeTrailId=s,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:s,uuid:t,startedAt:r}),{trailId:s,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(s){this._emit("error",s)}this._emit("trailstop",{trailId:e,...r});let n=!1;if(this.sync)try{n=await this._syncTrail(e)}catch(s){this._emit("error",s)}return this._activeTrailId=null,this._activeTrailUuid=null,{trailId:e,pointCount:r.pointCount,distanceM:r.distanceM,synced:n}}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 n of r)try{await this._syncTrail(n.id??n.trailId,n)?e++:t++}catch(s){t++,this._emit("error",s)}return this._emit("syncstatus",{pushed:e,failed:t}),{pushed:e,failed:t}}async _syncTrail(e,t){const r=await this.storage.getTrailPoints(e),n=t||{id:e},s=await this.sync.pushTrail(n,r),a=s&&(s.remoteId??s.id??null);return await this.storage.markTrailSynced(e,a),!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=st.normalize(e);if(this._lastFix=t,this._emit("position",t),!this._recording)return;const{minIntervalMs:r,minDistanceM:n,heartbeatMs:s,maxAccuracyM:a}=this.opts,i=t.timestamp;if(this._lastRecordedAt&&i-this._lastRecordedAt0&&t.accuracy!=null&&t.accuracy>a&&this._lastRecorded)return;let l=!1,c=0;if(!this._lastRecorded)l=!0;else{c=Ka(this._lastRecorded.lon,this._lastRecorded.lat,t.lon,t.lat);const d=i-this._lastRecordedAt;(c>=n||d>=s)&&(l=!0)}if(l){this._lastRecorded&&(this._distanceM+=c),this._pointCount+=1,this._lastRecorded={lon:t.lon,lat:t.lat,timestamp:i},this._lastRecordedAt=i;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=n=>n!=null&&!Number.isNaN(n)?n: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 ti={async createTrail(o){const e=o.districtId??Ke()?.district_id??null;return vs({...o,districtId:e!=null?String(e):null})},addPoint:(o,e)=>Es(o,e),finishTrail:(o,e)=>xs(o,e),getUnsyncedTrails:()=>Ss(),getTrailPoints:o=>Ts(o),markTrailSynced:(o,e)=>Ls(o,e)},oi={pushTrail:(o,e)=>Wa(o,e),isOnline:()=>K()},ve=new st({storage:ti,sync:oi,minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0}),ni=new Set(["set:view","set:selected","clear:selected","set:basemap"]);function ri({mapView:o,embedConfig:e}){const t=o.getMap(),r=window.parent&&window.parent!==window?window.parent:null,n=new G,s=new D({source:n,zIndex:9999,style:new M({stroke:new I({color:"#f97316",width:3}),fill:new A({color:"rgba(249,115,22,0.18)"})}),properties:{title:"Permit selection",displayInLayerSwitcher:!1}});t.addLayer(s);let a=null,i=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,w){const _=m.getProperties();let E=y,T=w;if(E==null||T==null){const x=m.getGeometry()?.getExtent();if(x){const[S,P]=xe(Tr(x));E=S,T=P}}return{type:"parcel:select",upn:_.upn??null,parcel_id:_.id??null,lon:E??null,lat:T??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(n.clear(),m){const y=m.clone();n.addFeature(y)}}o.onClick((m,y,w,_)=>{let E=null;t.forEachFeatureAtPixel(_.pixel,T=>{if(T.get("_layerType")==="parcel")return E=T,!0}),E?(p(E),c(f(E,m,y))):(p(null),c({type:"parcel:cleared"}))}),window.addEventListener("message",m=>{const y=m.data;if(!(!y||typeof y!="object"||!ni.has(y.type)))try{switch(y.type){case"set:view":{if(typeof y.lon=="number"&&typeof y.lat=="number"){const w=t.getView();w.setCenter(ee([y.lon,y.lat])),typeof y.zoom=="number"&&w.setZoom(y.zoom)}break}case"set:selected":y.upn&&h(String(y.upn));break;case"clear:selected":p(null),i=null;break;case"set:basemap":y.key&&typeof o.setBaseMap=="function"&&o.setBaseMap(y.key);break}}catch(w){d("COMMAND_FAILED",`Failed to handle ${y.type}: ${w.message}`)}});function h(m){if(!a){i=m;return}const w=a.getSource().getFeatures().find(E=>String(E.get("upn")??"")===m);if(!w){i=m;return}i=null,p(w);const _=w.getGeometry()?.getExtent();_&&t.getView().fit(_,{padding:[50,50,50,50],duration:400,maxZoom:17}),c(f(w,null,null))}function g(m){a=m;const y=m.getSource(),w=()=>{queueMicrotask(()=>{i&&h(i),u()})};if(y.getFeatures().length>0)w();else{let _=!1;y.on("addfeature",()=>{_||(_=!0,queueMicrotask(()=>{_=!1,i&&h(i),u()}))})}}if(e?.basemap&&typeof o.setBaseMap=="function"&&o.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 si=[{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"}],Vn={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 ai(o){if(!o?.features?.length)return"none";let e=0,t=0,r=0;for(const a of o.features){const i=a?.geometry?.type;i&&(i==="Polygon"||i==="MultiPolygon"?e++:i==="LineString"||i==="MultiLineString"?t++:(i==="Point"||i==="MultiPoint")&&r++)}const n=e+t+r;if(n===0)return"none";const s=Math.max(e,t,r);return se&&(i=i.slice(0,e-1)+"…"),t[s]=i)}}return t}const L={};let no=null,N=null;function pi(){L.root||(L.root=document.getElementById("importMappingModal"),L.filename=document.getElementById("import-modal-filename"),L.summary=document.getElementById("import-modal-summary"),L.target=document.getElementById("import-modal-target"),L.targetHint=document.getElementById("import-modal-target-hint"),L.fieldsWrap=document.getElementById("import-modal-fields-wrap"),L.tbody=document.getElementById("import-modal-fields-tbody"),L.btnSave=document.getElementById("import-modal-save"),L.btnSaveUpload=document.getElementById("import-modal-save-upload"),L.btnCancel=document.getElementById("import-modal-cancel"),L.target&&!L.target.dataset.populated&&(L.target.innerHTML=si.map(o=>``).join(""),L.target.dataset.populated="1"),L.target&&!L.target.dataset.wired&&(L.target.dataset.wired="1",L.target.addEventListener("change",Yn)),L.btnSave&&!L.btnSave.dataset.wired&&(L.btnSave.dataset.wired="1",L.btnSave.addEventListener("click",()=>tn("save"))),L.btnSaveUpload&&!L.btnSaveUpload.dataset.wired&&(L.btnSaveUpload.dataset.wired="1",L.btnSaveUpload.addEventListener("click",()=>tn("upload"))),L.root&&!L.root.dataset.wired&&(L.root.dataset.wired="1",L.root.addEventListener("hidden.bs.modal",()=>{N?.onResult&&!N._resolved&&(N._resolved=!0,N.onResult({action:"cancel"})),N=null})))}function fi(){const o=N.targetType,e=Vn[o]||[];if(o==="other"||e.length===0){L.fieldsWrap.style.display="none";return}L.fieldsWrap.style.display="";const t=n=>{const s=N.sourceSamples[n];return s?` — [${_t(s)}]`:" — [(empty)]"},r=[''].concat(N.sourceFields.map(n=>``)).join("");L.tbody.innerHTML=e.map(n=>{const s=N.mapping[n]||"",a=r.replace(`