pwaLUPMIS2/dist/assets/analysis-modal-DX_KiCnZ.js
ekke 7787f77f80 Drawing toolbar: restore the two-row wrap on phones
The toolbar still did not paint on iPhone in portrait. v17 removed the
backdrop-filter from the scrolling strip but kept the horizontal scroll, and
that was the wrong half to keep: on iOS a scroll container is the fragile part,
with or without a blur.

The strip goes back to wrapping onto two rows — the layout that shipped in v8
and worked on these phones for months, before the rail replaced it in v15. That
was a mistake of mine: I swapped a device-tested layout for a scrolling one on
the strength of desktop testing alone. It now has no overflow, no blur and an
opaque background, and pairs `left` with `width` rather than `right`, so there
is nothing exotic left for iOS to disagree with. A comment on the block says so,
because the scrolling version reads as the tidier solution and someone will be
tempted again.

Group headings become hairline dividers on the strip rather than disappearing,
so the Draw / Edit / Shape / File grouping survives the narrower layout.

Building the rail is also wrapped in a fallback now. It is presentation code and
must never be able to take the tools down with it: if it throws, our classes are
removed and ol-ext's default bar renders instead of nothing at all. The
matchMedia listener that would have been the most likely thrower (Safari only
gained MediaQueryList.addEventListener in iOS 14) falls back to addListener.

Verified: wraps to two rows in a 390px container, single row when wider, no
overflow or backdrop-filter in either, and still hides when Draw mode is off.

Service Worker v17 -> v18.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-20 16:47:59 +02:00

12 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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-BM5P5Dc7.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 As 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=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[n])}export{Xe as openAnalysisModal};
//# sourceMappingURL=analysis-modal-DX_KiCnZ.js.map