Switching the layer on only at the moment a location was saved was too narrow. loadLocations() runs at startup and after every change, and it left the layer hidden — so markers were visible right after saving and gone again on the next reload, which is what made it look as though closing Digitise had removed them. The call moves into loadLocations(), guarded on there being at least one location. That covers startup, saving, and any later refresh from one place, and the two calls in the add paths that duplicated it are gone. An empty district still opens with the layer off, which is what it was created hidden for. One trade-off, since it was asked about: an officer who deliberately unchecks Markers in the layer switcher will find it back on after the next reload or saved location. Respecting that choice would mean persisting it, which is worth doing only if anyone actually wants the layer off while holding locations. Verified both cases: with locations the layer goes visible on the startup load and stays visible through entering and leaving Digitise and through a reload; with none it stays hidden throughout. Service Worker v22 -> v23. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
12 lines
17 KiB
JavaScript
12 lines
17 KiB
JavaScript
import{_ as de}from"./geotiff-BaoeLn6q.js";import{M as pe}from"./bootstrap-D1-uvFxm.js";import{ax as ye}from"./openlayers-J9qS6Th1.js";import{i as X,f as j,a as fe,b as me,c as he,d as ge,e as be,g as we}from"./turf-B-0GbNlN.js";import{s as F}from"./index-ErUYnuzo.js";import"./pako-Xa-UToif.js";import"./ol-ext-C5frl4dY.js";import"./shpjs-Doisr2ug.js";const Q=4e5,ve=new ye;function $(e,n="EPSG:3857"){const o=[];for(const s of e||[]){if(!(s.getGeometry&&s.getGeometry()))continue;const a=ve.writeFeatureObject(s,{dataProjection:"EPSG:4326",featureProjection:n});a&&a.geometry&&o.push(a)}return o}function N(e){const n=e&&e.geometry;if(!n||!n.coordinates)return null;let o=1/0,s=1/0,r=-1/0,a=-1/0;const c=l=>{if(typeof l[0]=="number"){l[0]<o&&(o=l[0]),l[0]>r&&(r=l[0]),l[1]<s&&(s=l[1]),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]<n[0]||n[2]<e[0]||e[3]<n[1]||n[3]<e[1])}function xe(e,n,o){return e?n>=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;m<l.length;m++){if(!D(y,i[m]))continue;if(++u>Q)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;y<a.length&&d;y++)if(D(f,c[y])){if(++l>Q)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<c.length;i++)try{const u=X(j([l,c[i]]));u&&u.geometry&&(l=u)}catch{}}return!l||!l.geometry?z([]):(l.properties={parts:c.length},z([l]))}function te(e){return new Error(`This overlay has more than ${e.toLocaleString()} genuinely overlapping feature pairs, which is too much to run in the browser. Narrow the inputs using “Apply to” (current map view or selected features), or run it server-side.`)}const G=[{key:"intersect",label:"Intersect",needsB:!0,hint:"Keep the parts of layer A that fall inside layer B (attributes from both)."},{key:"clip",label:"Clip",needsB:!0,hint:"Cut layer A to the boundary of layer B (keeps only A’s attributes)."},{key:"difference",label:"Difference",needsB:!0,hint:"Keep the parts of layer A that fall outside layer B."},{key:"union",label:"Union / Dissolve",needsB:!1,hint:"Merge features into a single outline (optionally with a second layer)."}];function Se(e,n,o){switch(e){case"intersect":return ee(n,o);case"clip":return Ee(n,o);case"difference":return Le(n,o);case"union":return Ae(n,o);default:throw new Error(`Unknown overlay operation: ${e}`)}}const Y=4e5,Z=[{key:"centroid",label:"Centroid inside zone",hint:"Each feature is counted once, in the zone containing its centre."},{key:"intersects",label:"Any overlap with zone",hint:"A feature is counted in every zone it touches (may double-count)."}],R=[{key:"count",label:"Count",needsField:!1},{key:"sum",label:"Sum",needsField:!0},{key:"mean",label:"Mean",needsField:!0},{key:"min",label:"Minimum",needsField:!0},{key:"max",label:"Maximum",needsField:!0},{key:"area",label:"Total area (m²)",needsField:!1}];function $e(e){const n=new Map;for(const o of e||[]){const s=o.getProperties?o.getProperties():{};for(const[r,a]of Object.entries(s))r==="geometry"||r.startsWith("_")||(typeof a=="number"?a:a!==""&&a!=null&&!Number.isNaN(Number(a))?Number(a):null)===null||n.set(r,(n.get(r)||0)+1)}return Array.from(n.keys()).sort()}function ke(e){if(e==null||e==="")return null;const n=typeof e=="number"?e:Number(e);return Number.isFinite(n)?n:null}function ze({zoneFeatures:e,inputFeatures:n,stats:o=["count"],field:s=null,membership:r="centroid",prefix:a="zs_"}){const c=($(e)||[]).filter(w=>{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;w<c.length;w++){const A=c[w],U=p[w],O=[];for(let h=0;h<l.length;h++){let b=!1;try{if(r==="centroid"){const E=y[h];if(!E)continue;const[ue,ce]=E.geometry.coordinates;if(!xe(U,ue,ce))continue;if(++L>Y)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)=>`<option value="${o}"${o===e?" selected":""}>${g(n.title)} (${n.count.toLocaleString()})</option>`).join("")}function Ie(){t.op.innerHTML=G.map(e=>`<option value="${e.key}">${g(e.label)}</option>`).join(""),t.membership.innerHTML=Z.map(e=>`<option value="${e.key}">${g(e.label)}</option>`).join(""),t.statsBox.innerHTML=R.map(e=>`
|
||
<div class="form-check form-check-inline">
|
||
<input class="form-check-input" type="checkbox" value="${e.key}"
|
||
id="analysis-stat-${e.key}"${e.key==="count"?" checked":""}>
|
||
<label class="form-check-label" for="analysis-stat-${e.key}">${g(e.label)}</label>
|
||
</div>`).join("")}function ne(){const e=M[Number(t.inputLayer.value)],n=e?e.layer.getSource().getFeatures():[],o=$e(n);t.field.innerHTML='<option value="">(none — count/area only)</option>'+o.map(s=>`<option value="${g(s)}">${g(s)}</option>`).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='<div class="alert alert-warning py-2 px-3 mb-0">No result features — the layers may not overlap.</div>';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?'<div class="alert alert-warning py-2 px-3 mb-2" style="font-size:0.85rem;">'+r.map(l=>`<div>${g(l)}</div>`).join("")+"</div>":"";t.result.innerHTML=`<div class="alert alert-success py-2 px-3 mb-2">Added layer “${g(n)}”.</div>`+c+(typeof s=="string"&&s.startsWith("<")?s:`<div class="small text-muted">${g(s)}</div>`),F(`Analysis complete — “${n}” added to the Analysis group.`,"success",4e3)}catch(e){console.error("[Analysis] run failed:",e),t.result.innerHTML=`<div class="alert alert-danger py-2 px-3 mb-0">${g(e.message||String(e))}</div>`}finally{t.btnRun.disabled=!1}},20)}function K(e){const n=e.options[e.selectedIndex];return n?n.textContent:"(none)"}function Te(){const e=t.scope.options[t.scope.selectedIndex];return e?e.textContent:"Whole layer"}async function je(){if(k){t.btnExport.disabled=!0;try{const{exportXlsx:e}=await de(async()=>{const{exportXlsx:i}=await import("./xlsx-d1LrY7GM.js");return{exportXlsx:i}},[]),{rows:n,params:o,warnings:s}=k,r=Object.keys(n[0]||{}).filter(i=>i!=="zone"),a=[{cells:["Zone",...r],bold:!0},...n.map(i=>["zone"in i?i.zone:"",...r.map(u=>i[u]===null||i[u]===void 0?"":i[u])])],c=[{cells:["LUPMIS2 — Zonal statistics"],bold:!0},[],{cells:["Parameter","Value"],bold:!0},...Object.entries(o).map(([i,u])=>[i,u]),["Exported",new Date().toLocaleString()],...s&&s.length?[[],{cells:["Notes"],bold:!0},...s.map(i=>["",i])]:[]],l=(o["Result layer"]||"zonal_stats").toString().trim().replace(/[^A-Za-z0-9_\-]+/g,"_")||"zonal_stats";e([{name:"Parameters",rows:c},{name:"Results",rows:a}],`${l}.xlsx`),F("Table exported to Excel.","success",3e3)}catch(e){console.error("[Analysis] table export failed:",e),F(`Could not export the table: ${e.message}`,"error",6e3)}finally{t.btnExport.disabled=!1}}}function ie(){t.btnExport&&t.btnExport.classList.toggle("d-none",!k)}function Fe(e){if(!e||!e.length)return"";const n=Object.keys(e[0]).filter(r=>r!=="zone"),o=["Zone",...n.map(r=>r.replace(/^zs_/,""))].map(r=>`<th class="text-nowrap">${g(r)}</th>`).join(""),s=e.map(r=>`<tr><td class="text-nowrap">${g(String(r.zone))}</td>`+n.map(a=>`<td>${r[a]===null||r[a]===void 0?"—":g(String(r[a]))}</td>`).join("")+"</tr>").join("");return`<div class="table-responsive" style="max-height:220px;overflow:auto;">
|
||
<table class="table table-sm table-striped mb-0" style="font-size:0.85rem;">
|
||
<thead class="table-light" style="position:sticky;top:0;"><tr>${o}</tr></thead>
|
||
<tbody>${s}</tbody>
|
||
</table></div>`}function Xe(e,n={}){if(v=e,_e(),x=n.scopeFeatures&&n.scopeFeatures.length?{features:new Set(n.scopeFeatures),label:n.scopeLabel||"drawn area"}:null,He(),M=v.listVectorLayers({nonEmptyOnly:!0}),M.length===0){F("No vector layers with features are loaded — nothing to analyse yet.","warning",5e3);return}Ie();const o=J(0),s=J(M.length>1?1:0);t.layerA.innerHTML=o,t.layerB.innerHTML=s,t.zoneLayer.innerHTML=o,t.inputLayer.innerHTML=s,t.outName.dataset.touched="",t.outName.addEventListener("input",()=>{t.outName.dataset.touched="1"},{once:!0}),t.result.innerHTML="",k=null,t.scope.value=x?"drawn":"all",ne(),re(),se(),le(),oe(),P=P||new pe(t.root),P.show()}function He(){const e=t.scope.querySelector('option[value="drawn"]');if(!x){e&&e.remove();return}const n=`From ${x.label} (${x.features.size.toLocaleString()} features)`;if(e)e.textContent=n;else{const o=document.createElement("option");o.value="drawn",o.textContent=n,t.scope.insertBefore(o,t.scope.firstChild)}}function g(e){return String(e).replace(/[&<>"']/g,n=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[n])}export{Xe as openAnalysisModal};
|
||
//# sourceMappingURL=analysis-modal-BcHNiMgQ.js.map
|