The popup covers the thing it is reporting on — a circle's own parcels sit directly underneath it — so it has to be movable to be read against the map. Pressing the title bar and dragging now moves it. The drag changes the overlay's offset, not its position. The popup stays tied to the shape it describes, so panning and zooming carry it along and it goes on pointing at the right circle; only where it rests relative to that anchor changes. Setting a new position instead would cut it loose and re-anchor it to whatever ground it happened to be dropped on. The resting offset is now a named constant shared with the overlay, so the two cannot drift apart, and each showing starts from it — a popup opened for a new shape appears where it belongs rather than where the last one was left. A press on the close button is not a drag, and the overlay already has stopEvent:true, so the map does not pan underneath. This covers the Area Analysis popup too: both are built by the same _buildAnalysisPopupHtml, so both get the handle. Separate commit from the radius work so it can be cherry-picked onto main if the radius entry does not survive the usability check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
934 lines
287 KiB
JavaScript
934 lines
287 KiB
JavaScript
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/geotiff-B9aROX49.js","assets/openlayers-D5-VSKzq.js","assets/openlayers-BtPuoxOl.css","assets/pako-Xa-UToif.js","assets/pdf-export-C0eAmANf.js","assets/jspdf-BsOD2LDR.js","assets/shpjs-Doisr2ug.js","assets/analysis-modal-C3OTjBir.js","assets/bootstrap-D1-uvFxm.js","assets/bootstrap-BtmJYOxZ.css","assets/turf-B-0GbNlN.js","assets/ol-ext-DV70ljNY.js","assets/ol-ext-BgKrOIxx.css"])))=>i.map(i=>d[i]);
|
||
import{_ as Te}from"./geotiff-B9aROX49.js";import{M as gt}from"./bootstrap-D1-uvFxm.js";import{ak as xe,h as M,F as C,j as P,k as he,m as Lo,b as G,V as D,L as de,D as Oe,P as ft,al as Ke,n as fe,am as Me,M as Bo,an as Ur,ao as ee,ap as qr,S as zr,J as Hr,G as Wr,aq as Vr,ar as Kr,as as Xr,o as yt,O as Ee,at as dt,au as Pt,A as Yr,T as ae,av as Ce,aw as Go,ax as le,ay as jo,e as Jr,u as zt,s as Zr,az as Un,aA as To}from"./openlayers-D5-VSKzq.js";import{o as Qr,a as ea,b as ta,c as oa,d as uo,e as Uo,f as lt,g as na,h as Se,i as ra,j as qo}from"./ol-ext-DV70ljNY.js";import{r as aa}from"./shpjs-Doisr2ug.js";import"./pako-Xa-UToif.js";(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const a of o)if(a.type==="childList")for(const i of a.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function t(o){const a={};return o.integrity&&(a.integrity=o.integrity),o.referrerPolicy&&(a.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?a.credentials="include":o.crossOrigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function r(o){if(o.ep)return;o.ep=!0;const a=t(o);fetch(o.href,a)}})();const zo="function",ze="64e10b34-2bf7-4616-9668-f99de5aa046e",ia="get",sa="has",la="set",{isArray:Lt}=Array;let{SharedArrayBuffer:Mt,window:ca}=globalThis,{notify:qn,wait:zn,waitAsync:Ct}=Atomics,Hn=null;Ct||(Ct=n=>({value:new Promise(e=>{let t=new Worker("data:application/javascript,onmessage%3D(%7Bdata%3Ab%7D)%3D%3E(Atomics.wait(b%2C0)%2CpostMessage(0))");t.onmessage=e,t.postMessage(n)})}));try{new Mt(4)}catch{Mt=ArrayBuffer;const e=new WeakMap;if(ca){const t=new Map,{prototype:{postMessage:r}}=Worker,o=a=>{const i=a.data?.[ze];if(!Lt(i)){a.stopImmediatePropagation();const{id:s,sb:l}=i;t.get(s)(l)}};Hn=function(a,...i){const s=a?.[ze];if(Lt(s)){const[l,c]=s;e.set(c,l),this.addEventListener("message",o)}return r.call(this,a,...i)},Ct=a=>({value:new Promise(i=>{t.set(e.get(a),i)}).then(i=>{t.delete(e.get(a)),e.delete(a);for(let s=0;s<i.length;s++)a[s]=i[s];return"ok"})})}else{const t=(r,o)=>({[ze]:{id:r,sb:o}});qn=r=>{postMessage(t(e.get(r),r))},addEventListener("message",r=>{const o=r.data?.[ze];if(Lt(o)){const[a,i]=o;e.set(i,a)}})}}/*! (c) Andrea Giammarchi - ISC */const{Int32Array:po,Map:Ho,Uint16Array:fo}=globalThis,{BYTES_PER_ELEMENT:Wo}=po,{BYTES_PER_ELEMENT:da}=fo,ua=(n,e,t)=>{for(;zn(n,0,0,e)==="timed-out";)t()},ho=new WeakSet,Ht=new WeakMap,pa={value:{then:n=>n()}};let fa=0;const ko=(n,{parse:e=JSON.parse,stringify:t=JSON.stringify,transform:r,interrupt:o}=JSON)=>{if(!Ht.has(n)){const a=Hn||n.postMessage,i=(p,...f)=>a.call(n,{[ze]:f},{transfer:p}),s=typeof o===zo?o:o?.handler,l=o?.delay||42,c=new TextDecoder("utf-16"),d=(p,f)=>p?Ct(f,0):(s?ua(f,l,s):zn(f,0),pa);let u=!1;Ht.set(n,new Proxy(new Ho,{[sa]:(p,f)=>typeof f=="string"&&!f.startsWith("_"),[ia]:(p,f)=>f==="then"?null:((...h)=>{const g=fa++;let m=new po(new Mt(Wo*2)),y=[];ho.has(h.at(-1)||y)&&ho.delete(y=h.pop()),i(y,g,m,f,r?h.map(r):h);const w=n!==globalThis;let b=0;return u&&w&&(b=setTimeout(console.warn,1e3,`💀🔒 - Possible deadlock if proxy.${f}(...args) is awaited`)),d(w,m).value.then(()=>{clearTimeout(b);const S=m[1];if(!S)return;const L=da*S;return m=new po(new Mt(L+L%Wo)),i([],g,m),d(w,m).value.then(()=>e(c.decode(new fo(m.buffer).slice(0,S))))})}),[la](p,f,h){const g=typeof h;if(g!==zo)throw new Error(`Unable to assign ${f} as ${g}`);if(!p.size){const m=new Ho;n.addEventListener("message",async y=>{const w=y.data?.[ze];if(Lt(w)){y.stopImmediatePropagation();const[b,S,...L]=w;let E;if(L.length){const[x,I]=L;if(p.has(x)){u=!0;try{const k=await p.get(x)(...I);if(k!==void 0){const U=t(r?r(k):k);m.set(b,U),S[1]=U.length}}catch(k){E=k}finally{u=!1}}else E=new Error(`Unsupported action: ${x}`);S[0]=1}else{const x=m.get(b);m.delete(b);for(let I=new fo(S.buffer),k=0;k<x.length;k++)I[k]=x.charCodeAt(k)}if(qn(S,0),E)throw E}})}return!!p.set(f,h)}}))}return Ht.get(n)};ko.transfer=(...n)=>(ho.add(n),n);function Vo(){let n,e;return{lock:async()=>{for(;n;)await n;n=new Promise(o=>{e=o})},unlock:async()=>{const o=e;n=void 0,e=void 0,o?.()}}}async function Wn(n,e){let t;if(n instanceof Blob?t=n.stream():t=n,t instanceof ReadableStream&&e){const o=t.getReader();switch(e){case"callback":return async()=>(await o.read()).value;case"buffer":const a=[];let i=!1;for(;!i;){const d=await o.read();d.value&&a.push(d.value),i=d.done}const s=a.reduce((d,u)=>d+u.length,0),l=new Uint8Array(s);let c=0;return a.forEach(d=>{l.set(d,c),c+=d.length}),l.buffer}}else return t}class At{constructor(e){Object.defineProperty(this,"sqlite3InitModule",{enumerable:!0,configurable:!0,writable:!0,value:e}),Object.defineProperty(this,"sqlite3",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"db",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"config",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"pointers",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"writeCallbacks",{enumerable:!0,configurable:!0,writable:!0,value:new Set}),Object.defineProperty(this,"storageType",{enumerable:!0,configurable:!0,writable:!0,value:"memory"})}async init(e){const{databasePath:t}=e,r=this.getFlags(e);if(!this.sqlite3InitModule){const{default:o}=await Te(async()=>{const{default:a}=await import("./index-DTMgZTfd.js");return{default:a}},[]);this.sqlite3InitModule=o}this.sqlite3||(this.sqlite3=await this.sqlite3InitModule()),this.db&&await this.destroy(),this.db=new this.sqlite3.oo1.DB(t,r),this.config=e,this.initWriteHook()}onWrite(e){return this.writeCallbacks.add(e),()=>{this.writeCallbacks.delete(e)}}async exec(e){if(!this.db)throw new Error("Driver not initialized");return this.execOnDb(this.db,e)}async execBatch(e){if(!this.db)throw new Error("Driver not initialized");const t=[];return this.db.transaction(r=>{const o=new Map;try{for(let a of e){let i=o.get(a.sql);if(!i){const c=r.prepare(a.sql);o.set(a.sql,c),i=c}a.params?.length&&i.bind(a.params);let s=[],l=[];for(;i.step();)s=i.getColumnNames([]),l.push(i.get([]));t.push({columns:s,rows:l}),i.reset()}}finally{o.forEach(a=>{a.finalize()})}}),t}async isDatabasePersisted(){return!1}async getDatabaseSizeBytes(){const t=(await this.exec({sql:`SELECT page_count * page_size AS size
|
||
FROM pragma_page_count(), pragma_page_size()`,method:"get"}))?.rows?.[0];if(typeof t!="number")throw new Error("Failed to query database size");return t}async createFunction(e){if(!this.db)throw new Error("Driver not initialized");switch(e.type){case"callback":case"scalar":this.db.createFunction({name:e.name,xFunc:(t,...r)=>e.func(...r),arity:-1});break;case"aggregate":this.db.createFunction({name:e.name,xStep:(t,...r)=>e.func.step(...r),xFinal:(t,...r)=>e.func.final(...r),arity:-1});break}}async import(e){if(!this.sqlite3||!this.db||!this.config)throw new Error("Driver not initialized");const t=await Wn(e,"buffer"),r=this.sqlite3.wasm.allocFromTypedArray(t);this.pointers.push(r);const o=this.sqlite3.capi.sqlite3_deserialize(this.db,"main",r,t.byteLength,t.byteLength,this.config.readOnly?this.sqlite3.capi.SQLITE_DESERIALIZE_READONLY:this.sqlite3.capi.SQLITE_DESERIALIZE_RESIZEABLE);this.db.checkRc(o)}async export(){if(!this.sqlite3||!this.db)throw new Error("Driver not initialized");return{name:"database.sqlite3",data:this.sqlite3.capi.sqlite3_js_db_export(this.db)}}async clear(){}async destroy(){this.closeDb(),this.pointers.forEach(e=>this.sqlite3?.wasm.dealloc(e)),this.pointers=[],this.writeCallbacks.clear()}getFlags(e){const{readOnly:t,verbose:r}=e;return[t===!0?"r":"cw",r===!0?"t":""].join("")}execOnDb(e,t){const r={rows:[],columns:[]},o=e.exec({sql:t.sql,bind:t.params,returnValue:"resultRows",rowMode:"array",columnNames:r.columns});switch(t.method){case"run":break;case"get":r.rows=o[0]??[];break;case"all":default:r.rows=o;break}return r}initWriteHook(){if(!this.config?.reactive)return;if(!this.sqlite3||!this.db)throw new Error("Driver not initialized");const e={[this.sqlite3.capi.SQLITE_INSERT]:"insert",[this.sqlite3.capi.SQLITE_UPDATE]:"update",[this.sqlite3.capi.SQLITE_DELETE]:"delete"};this.sqlite3.capi.sqlite3_update_hook(this.db,(t,r,o,a,i)=>{this.writeCallbacks.forEach(s=>{s({table:a,rowid:i,operation:e[r]})})},0)}closeDb(){this.db&&(this.db.close(),this.db=void 0)}}function ha(n,e,t){let r,o,a,i,s,l,c=0,d=!1,u=!1,p=!0;if(typeof n!="function")throw new TypeError("Expected a function");e=Number(e)||0,typeof t=="object"&&t!==null&&(d=!!t.leading,u="maxWait"in t,a=u?Math.max(Number(t.maxWait)||0,e):0,p="trailing"in t?!!t.trailing:p);function f(E){const x=r,I=o;return r=o=void 0,c=E,i=n.apply(I,x),i}function h(E){return c=E,s=setTimeout(y,e),d?f(E):i}function g(E){const x=E-(l??0),I=E-c,k=e-x;return u?Math.min(k,a-I):k}function m(E){const x=E-(l??0),I=E-c;return l===void 0||x>=e||x<0||u&&I>=a}function y(){const E=Date.now();if(m(E))return w(E);s=setTimeout(y,g(E))}function w(E){return s=void 0,p&&r?f(E):(r=o=void 0,i)}function b(){s!==void 0&&clearTimeout(s),c=0,r=l=o=s=void 0}function S(){return s===void 0?i:w(Date.now())}function L(){const E=Date.now(),x=m(E);if(r=arguments,o=this,l=E,x){if(s===void 0)return h(l);if(u)return s=setTimeout(y,e),f(l)}return s===void 0&&(s=setTimeout(y,e)),i}return L.cancel=b,L.flush=S,L}function Tt(){return crypto.randomUUID()}function Vn(n,e){switch(n){case"session":case":sessionStorage:":let t=sessionStorage._sqlocal_session_key;return t||(t=Tt(),sessionStorage._sqlocal_session_key=t),`session:${t}`;case"local":case":localStorage:":return"local";case":memory:":return`memory:${e}`;default:return`path:${n}`}}class bt{constructor(e){Object.defineProperty(this,"driver",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"config",{enumerable:!0,configurable:!0,writable:!0,value:{}}),Object.defineProperty(this,"userFunctions",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"initMutex",{enumerable:!0,configurable:!0,writable:!0,value:Vo()}),Object.defineProperty(this,"transactionMutex",{enumerable:!0,configurable:!0,writable:!0,value:Vo()}),Object.defineProperty(this,"transactionKey",{enumerable:!0,configurable:!0,writable:!0,value:null}),Object.defineProperty(this,"proxy",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"dirtyTables",{enumerable:!0,configurable:!0,writable:!0,value:new Set}),Object.defineProperty(this,"effectsChannel",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"reinitChannel",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"onmessage",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"init",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{if(!(!this.config.databasePath||!this.config.clientKey)){await this.initMutex.lock();try{try{await this.driver.init(this.config)}catch{console.warn(`Persistence failed, so ${this.config.databasePath} will not be saved. For origin private file system persistence, make sure your web server is configured to use the correct HTTP response headers (See https://sqlocal.dev/guide/setup#cross-origin-isolation).`),this.config.databasePath=":memory:",this.driver=new At,await this.driver.init(this.config)}const a=Vn(this.config.databasePath,this.config.clientKey);this.reinitChannel=new BroadcastChannel(`_sqlocal_reinit_(${a})`),this.reinitChannel.onmessage=i=>{const s=i.data;if(this.config.clientKey!==s.clientKey)switch(s.type){case"reinit":this.init(s.reason);break;case"close":this.driver.destroy();break}},this.config.reactive&&(this.effectsChannel=new BroadcastChannel(`_sqlocal_effects_(${a})`),this.driver.onWrite(async i=>{this.dirtyTables.add(i.table),await this.transactionMutex.lock(),this.emitEffectsDebounced(),await this.transactionMutex.unlock()})),await Promise.all(Array.from(this.userFunctions.values()).map(i=>this.initUserFunction(i))),await this.execInitStatements(),this.emitMessage({type:"event",event:"connect",reason:o})}catch(a){this.emitMessage({type:"error",error:a,queryKey:null}),await this.destroy()}finally{await this.initMutex.unlock()}}}}),Object.defineProperty(this,"postMessage",{enumerable:!0,configurable:!0,writable:!0,value:async(o,a)=>{const i=o instanceof MessageEvent?o.data:o;switch(await this.initMutex.lock(),i.type){case"config":this.editConfig(i);break;case"query":case"batch":case"transaction":this.exec(i);break;case"function":this.createUserFunction(i);break;case"getinfo":this.getDatabaseInfo(i);break;case"import":this.importDb(i);break;case"export":this.exportDb(i);break;case"delete":this.deleteDb(i);break;case"destroy":this.destroy(i);break}await this.initMutex.unlock()}}),Object.defineProperty(this,"emitMessage",{enumerable:!0,configurable:!0,writable:!0,value:(o,a=[])=>{this.onmessage&&this.onmessage(o,a)}}),Object.defineProperty(this,"emitEffects",{enumerable:!0,configurable:!0,writable:!0,value:()=>{!this.effectsChannel||this.dirtyTables.size===0||(this.effectsChannel.postMessage({type:"effects",tables:[...this.dirtyTables]}),this.dirtyTables.clear())}}),Object.defineProperty(this,"emitEffectsDebounced",{enumerable:!0,configurable:!0,writable:!0,value:ha(()=>this.emitEffects(),32,{maxWait:180})}),Object.defineProperty(this,"editConfig",{enumerable:!0,configurable:!0,writable:!0,value:o=>{this.config=o.config,this.init("initial")}}),Object.defineProperty(this,"exec",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{try{const a={type:"data",queryKey:o.queryKey,data:[]};switch(o.type){case"query":const i=this.transactionKey!==null&&this.transactionKey===o.transactionKey;try{i||await this.transactionMutex.lock();const s=await this.driver.exec(o);a.data.push(s)}finally{i||await this.transactionMutex.unlock()}break;case"batch":try{await this.transactionMutex.lock();const s=await this.driver.execBatch(o.statements);a.data.push(...s)}finally{await this.transactionMutex.unlock()}break;case"transaction":if(o.action==="begin"&&(await this.transactionMutex.lock(),this.transactionKey=o.transactionKey,await this.driver.exec({sql:"BEGIN"})),(o.action==="commit"||o.action==="rollback")&&this.transactionKey!==null&&this.transactionKey===o.transactionKey){const s=o.action==="commit"?"COMMIT":"ROLLBACK";await this.driver.exec({sql:s}),this.transactionKey=null,await this.transactionMutex.unlock()}break}this.emitMessage(a)}catch(a){this.emitMessage({type:"error",error:a,queryKey:o.queryKey})}}}),Object.defineProperty(this,"execInitStatements",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{if(this.config.onInitStatements)for(let o of this.config.onInitStatements)await this.driver.exec(o)}}),Object.defineProperty(this,"getDatabaseInfo",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{try{this.emitMessage({type:"info",queryKey:o.queryKey,info:{databasePath:this.config.databasePath,storageType:this.driver.storageType,databaseSizeBytes:await this.driver.getDatabaseSizeBytes(),persisted:await this.driver.isDatabasePersisted()}})}catch(a){this.emitMessage({type:"error",queryKey:o.queryKey,error:a})}}}),Object.defineProperty(this,"createUserFunction",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{functionName:a,functionType:i,queryKey:s}=o;let l;if(this.userFunctions.has(a)){this.emitMessage({type:"error",error:new Error(`A user-defined function with the name "${a}" has already been created for this SQLocal instance.`),queryKey:s});return}switch(i){case"callback":l={type:i,name:a,func:(...c)=>{this.emitMessage({type:"callback",name:a,args:c})}};break;case"scalar":l={type:i,name:a,func:this.proxy[`_sqlocal_func_${a}`]};break;case"aggregate":l={type:i,name:a,func:{step:this.proxy[`_sqlocal_func_${a}_step`],final:this.proxy[`_sqlocal_func_${a}_final`]}};break}try{await this.initUserFunction(l),this.emitMessage({type:"success",queryKey:s})}catch(c){this.emitMessage({type:"error",error:c,queryKey:s})}}}),Object.defineProperty(this,"initUserFunction",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{await this.driver.createFunction(o),this.userFunctions.set(o.name,o)}}),Object.defineProperty(this,"importDb",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{queryKey:a,database:i}=o;let s=!1;try{await this.driver.import(i),this.driver.storageType==="memory"&&await this.execInitStatements()}catch(l){this.emitMessage({type:"error",error:l,queryKey:a}),s=!0}finally{this.driver.storageType!=="memory"&&await this.init("overwrite")}s||this.emitMessage({type:"success",queryKey:a})}}),Object.defineProperty(this,"exportDb",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{queryKey:a}=o;try{const{name:i,data:s}=await this.driver.export();this.emitMessage({type:"buffer",queryKey:a,bufferName:i,buffer:s},[s])}catch(i){this.emitMessage({type:"error",error:i,queryKey:a})}}}),Object.defineProperty(this,"deleteDb",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{queryKey:a}=o;let i=!1;try{await this.driver.clear()}catch(s){this.emitMessage({type:"error",error:s,queryKey:a}),i=!0}finally{await this.init("delete")}i||this.emitMessage({type:"success",queryKey:a})}}),Object.defineProperty(this,"destroy",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{await this.driver.exec({sql:"PRAGMA optimize"}),await this.driver.destroy(),this.effectsChannel&&(this.emitEffectsDebounced.flush(),this.effectsChannel.close(),this.effectsChannel=void 0),this.reinitChannel&&(this.reinitChannel.close(),this.reinitChannel=void 0),o&&this.emitMessage({type:"success",queryKey:o.queryKey})}});const r=typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope?ko(globalThis):globalThis;this.proxy=r,this.driver=e}}function Ft(n,...e){return{sql:n.join("?"),params:e}}function ga(n){return!n.some(e=>!Array.isArray(e))}function Wt(n,e){let t;return ga(n)?t=n:t=[n],t.map(r=>{const o={};return e.forEach((a,i)=>{o[a]=r[i]}),o})}function ma(n){return typeof n=="object"&&n!==null&&"getSQL"in n&&typeof n.getSQL=="function"}function ya(n){return typeof n=="object"&&n!==null&&"sql"in n&&typeof n.sql=="string"&&"params"in n}function Ko(n){if(typeof n=="function"&&(n=n(Ft)),ma(n))try{if(!("toSQL"in n&&typeof n.toSQL=="function"))throw 1;const r=n.toSQL();if(!ya(r))throw 2;const o="all"in n&&typeof n.all=="function"?n.all:void 0;return{...r,exec:o?()=>o():void 0}}catch{throw new Error("The passed statement could not be parsed.")}const e=n.sql;let t=[];return"params"in n?t=n.params:"parameters"in n&&(t=n.parameters),{sql:e,params:t}}function Xo(n,e){let t;return typeof n=="string"?t={sql:n,params:e}:t=Ft(n,...e),t}async function wt(n,e,t,r){return!e&&"locks"in navigator?navigator.locks.request(`_sqlocal_mutation_(${t.databasePath})`,{mode:n},r):r()}class Yo extends At{constructor(e,t){super(t),Object.defineProperty(this,"storageType",{enumerable:!0,configurable:!0,writable:!0,value:e})}async init(e){const t=this.getFlags(e);if(e.readOnly)throw new Error(`SQLite storage type "${this.storageType}" does not support read-only mode.`);if(!this.sqlite3InitModule){const{default:r}=await Te(async()=>{const{default:o}=await import("./index-DTMgZTfd.js");return{default:o}},[]);this.sqlite3InitModule=r}this.sqlite3||(this.sqlite3=await this.sqlite3InitModule()),this.db&&await this.destroy(),this.db=new this.sqlite3.oo1.JsStorageDb({filename:this.storageType,flags:t}),this.config=e,this.initWriteHook()}async isDatabasePersisted(){return navigator.storage?.persisted()}async getDatabaseSizeBytes(){if(!this.db)throw new Error("Driver not initialized");return this.db.storageSize()}async import(e){const t=new At;await t.init({}),await t.import(e),await this.clear(),await t.exec({sql:`VACUUM INTO 'file:${this.storageType}?vfs=kvvfs'`}),await t.destroy()}async clear(){if(!this.db)throw new Error("Driver not initialized");this.db.clearStorage()}async destroy(){this.closeDb(),this.writeCallbacks.clear()}}var Kn,Xn;class ba{constructor(e){Object.defineProperty(this,"config",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"clientKey",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"processor",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"isDestroyed",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"bypassMutationLock",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"transactionQueryKeyQueue",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"userCallbacks",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"queriesInProgress",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"proxy",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"reinitChannel",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"effectsChannel",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"processMessageEvent",{enumerable:!0,configurable:!0,writable:!0,value:c=>{const d=c instanceof MessageEvent?c.data:c,u=this.queriesInProgress;switch(d.type){case"success":case"data":case"buffer":case"info":case"error":if(d.queryKey&&u.has(d.queryKey)){const[f,h]=u.get(d.queryKey);d.type==="error"?h(d.error):f(d),u.delete(d.queryKey)}else if(d.type==="error")throw d.error;break;case"callback":const p=this.userCallbacks.get(d.name);p&&p(...d.args??[]);break;case"event":this.config.onConnect?.(d.reason);break}}}),Object.defineProperty(this,"createQuery",{enumerable:!0,configurable:!0,writable:!0,value:async c=>wt("shared",this.bypassMutationLock||c.type==="import"||c.type==="delete",this.config,async()=>{if(this.isDestroyed===!0)throw new Error("This SQLocal client has been destroyed. You will need to initialize a new client in order to make further queries.");const d=Tt();switch(c.type){case"import":this.processor.postMessage({...c,queryKey:d},[c.database]);break;default:this.processor.postMessage({...c,queryKey:d});break}return new Promise((u,p)=>{this.queriesInProgress.set(d,[u,p])})})}),Object.defineProperty(this,"broadcast",{enumerable:!0,configurable:!0,writable:!0,value:c=>{this.reinitChannel.postMessage(c)}}),Object.defineProperty(this,"exec",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d,u="all",p)=>{const f=await this.createQuery({type:"query",transactionKey:p,sql:c,params:d,method:u}),h={rows:[],columns:[]};return f.type==="data"&&(h.rows=f.data[0]?.rows??[],h.columns=f.data[0]?.columns??[]),h}}),Object.defineProperty(this,"execBatch",{enumerable:!0,configurable:!0,writable:!0,value:async c=>{const d=await this.createQuery({type:"batch",statements:c}),u=new Array(c.length).fill({rows:[],columns:[]});return d.type==="data"&&d.data.forEach((p,f)=>{u[f]=p}),u}}),Object.defineProperty(this,"sql",{enumerable:!0,configurable:!0,writable:!0,value:async(c,...d)=>{const u=Xo(c,d),{rows:p,columns:f}=await this.exec(u.sql,u.params,"all");return Wt(p,f)}}),Object.defineProperty(this,"batch",{enumerable:!0,configurable:!0,writable:!0,value:async c=>{const d=c(Ft);return(await this.execBatch(d)).map(({rows:p,columns:f})=>Wt(p,f))}}),Object.defineProperty(this,"beginTransaction",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{const c=Tt();await this.createQuery({type:"transaction",transactionKey:c,action:"begin"});const d=async h=>{const g=Ko(h);if(g.exec)return this.transactionQueryKeyQueue.push(c),g.exec();const{rows:m,columns:y}=await this.exec(g.sql,g.params,"all",c);return Wt(m,y)};return{query:d,sql:async(h,...g)=>{const m=Xo(h,g);return await d(m)},commit:async()=>{await this.createQuery({type:"transaction",transactionKey:c,action:"commit"})},rollback:async()=>{await this.createQuery({type:"transaction",transactionKey:c,action:"rollback"})}}}}),Object.defineProperty(this,"transaction",{enumerable:!0,configurable:!0,writable:!0,value:async c=>wt("exclusive",!1,this.config,async()=>{let d;this.bypassMutationLock=!0;try{d=await this.beginTransaction();const u=await c({sql:d.sql,query:d.query});return await d.commit(),u}catch(u){throw await d?.rollback(),u}finally{this.bypassMutationLock=!1}})}),Object.defineProperty(this,"reactiveQuery",{enumerable:!0,configurable:!0,writable:!0,value:c=>{let d=[],u=!1,p=!1,f=0;const h=Ko(c),g=new Set,m=new Set,y=new Set,w=async()=>{try{const S=++f;if(g.size===0){const E=await this.sql("SELECT name, wr FROM tables_used(?) WHERE type = 'table'",h.sql),x=new Set,I=new Set;if(E.forEach(k=>{typeof k.name=="string"&&(k.wr?I.add(k.name):x.add(k.name))}),x.size===0)throw new Error("The passed SQL does not read any tables.");if(Array.from(I).some(k=>x.has(k)))throw new Error("The passed SQL would mutate one or more of the tables that it reads. Doing this in a reactive query would create an infinite loop.");x.forEach(k=>g.add(k))}const L=h.exec?await h.exec():await this.sql(h.sql,...h.params);S===f&&(d=L,u=!0,m.forEach(E=>E(d)))}catch(S){y.forEach(L=>{L(S instanceof Error?S:new Error(String(S)))})}},b=S=>{S.data.tables.some(L=>g.has(L))&&w()};return{get value(){return d},subscribe:(S,L)=>{if(!this.effectsChannel)throw new Error('This SQLocal instance is not configured for reactive queries. Set the "reactive" option to enable them.');return L||(L=E=>{throw E}),m.add(S),y.add(L),p?u&&S(d):(this.effectsChannel.addEventListener("message",b),p=!0,w()),{unsubscribe:()=>{m.delete(S),y.delete(L),m.size===0&&(this.effectsChannel?.removeEventListener("message",b),p=!1)}}}}}}),Object.defineProperty(this,"createCallbackFunction",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{await this.createQuery({type:"function",functionName:c,functionType:"callback"}),this.userCallbacks.set(c,d)}}),Object.defineProperty(this,"createScalarFunction",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{const u=`_sqlocal_func_${c}`,p=()=>{this.proxy[u]=d};this.proxy===globalThis&&p(),await this.createQuery({type:"function",functionName:c,functionType:"scalar"}),this.proxy!==globalThis&&p()}}),Object.defineProperty(this,"createAggregateFunction",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{const u=`_sqlocal_func_${c}`,p=()=>{this.proxy[`${u}_step`]=d.step,this.proxy[`${u}_final`]=d.final};this.proxy===globalThis&&p(),await this.createQuery({type:"function",functionName:c,functionType:"aggregate"}),this.proxy!==globalThis&&p()}}),Object.defineProperty(this,"getDatabaseInfo",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{const c=await this.createQuery({type:"getinfo"});if(c.type==="info")return c.info;throw new Error("The database failed to return valid information.")}}),Object.defineProperty(this,"getDatabaseFile",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{const c=await this.createQuery({type:"export"});if(c.type==="buffer")return new File([c.buffer],c.bufferName,{type:"application/x-sqlite3"});throw new Error("The database failed to export.")}}),Object.defineProperty(this,"overwriteDatabaseFile",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{await wt("exclusive",!1,this.config,async()=>{try{this.broadcast({type:"close",clientKey:this.clientKey});const u=await Wn(c,"buffer");await this.createQuery({type:"import",database:u}),typeof d=="function"&&(this.bypassMutationLock=!0,await d()),this.broadcast({type:"reinit",clientKey:this.clientKey,reason:"overwrite"})}finally{this.bypassMutationLock=!1}})}}),Object.defineProperty(this,"deleteDatabaseFile",{enumerable:!0,configurable:!0,writable:!0,value:async c=>{await wt("exclusive",!1,this.config,async()=>{try{this.broadcast({type:"close",clientKey:this.clientKey}),await this.createQuery({type:"delete"}),typeof c=="function"&&(this.bypassMutationLock=!0,await c()),this.broadcast({type:"reinit",clientKey:this.clientKey,reason:"delete"})}finally{this.bypassMutationLock=!1}})}}),Object.defineProperty(this,"destroy",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{await this.createQuery({type:"destroy"}),typeof globalThis.Worker<"u"&&this.processor instanceof Worker&&(this.processor.removeEventListener("message",this.processMessageEvent),this.processor.terminate()),this.queriesInProgress.clear(),this.userCallbacks.clear(),this.reinitChannel.close(),this.effectsChannel?.close(),this.isDestroyed=!0}}),Object.defineProperty(this,Kn,{enumerable:!0,configurable:!0,writable:!0,value:()=>{this.destroy()}}),Object.defineProperty(this,Xn,{enumerable:!0,configurable:!0,writable:!0,value:async()=>{await this.destroy()}});const t=typeof e=="string"?{databasePath:e}:e,{onInit:r,onConnect:o,processor:a,...i}=t,{databasePath:s}=i;this.config=t,this.clientKey=Tt();const l=Vn(s,this.clientKey);if(this.reinitChannel=new BroadcastChannel(`_sqlocal_reinit_(${l})`),i.reactive&&(this.effectsChannel=new BroadcastChannel(`_sqlocal_effects_(${l})`)),typeof a<"u")this.processor=a;else if(s==="local"||s===":localStorage:"){const c=new Yo("local");this.processor=new bt(c)}else if(s==="session"||s===":sessionStorage:"){const c=new Yo("session");this.processor=new bt(c)}else if(typeof globalThis.Worker<"u"&&s!==":memory:")this.processor=new Worker(new URL("/assets/worker-CuIBOSaM.js",import.meta.url),{type:"module"});else{const c=new At;this.processor=new bt(c)}this.processor instanceof bt?(this.processor.onmessage=c=>this.processMessageEvent(c),this.proxy=globalThis):(this.processor.addEventListener("message",this.processMessageEvent),this.proxy=ko(this.processor)),this.processor.postMessage({type:"config",config:{...i,clientKey:this.clientKey,onInitStatements:r?.(Ft)??[]}})}}Kn=Symbol.dispose,Xn=Symbol.asyncDispose;const Io="lupmis2.db",wa="lupmis-db-sync",Yn=new ba(Io),{sql:_}=Yn;console.log("[Database] SQLocal instance created for:",Io);const Jn=new BroadcastChannel(wa);let Zn=!1,Qn,er;const Jo=new Promise((n,e)=>{Qn=n,er=e}),Ot=new Set;function _a(n){return Ot.add(n),()=>Ot.delete(n)}Jn.onmessage=n=>{const{type:e,payload:t}=n.data;if(e==="DB_CHANGE")for(const r of Ot)try{r(t)}catch(o){console.error("[Database] Change listener error:",o)}};function Y(n,e,t=null){Jn.postMessage({type:"DB_CHANGE",payload:{table:n,action:e,id:t,timestamp:Date.now()}});for(const r of Ot)try{r({table:n,action:e,id:t,timestamp:Date.now(),local:!0})}catch(o){console.error("[Database] Change listener error:",o)}}async function va(){try{console.log("[Database] Initializing schema...");const n=await _`SELECT sqlite_version() as version`;console.log("[Database] SQLite version:",n[0]?.version),console.log("[Database] Creating locations table..."),await _`
|
||
CREATE TABLE IF NOT EXISTS locations (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT NOT NULL,
|
||
longitude REAL NOT NULL,
|
||
latitude REAL NOT NULL,
|
||
description TEXT,
|
||
category TEXT DEFAULT 'default',
|
||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||
synced INTEGER DEFAULT 0
|
||
)
|
||
`;const e=await _`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;console.log("[Database] Locations table exists:",e.length>0),console.log("[Database] Creating sync_log table..."),await _`
|
||
CREATE TABLE IF NOT EXISTS sync_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
table_name TEXT NOT NULL,
|
||
record_id INTEGER NOT NULL,
|
||
action TEXT NOT NULL,
|
||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
|
||
synced INTEGER DEFAULT 0
|
||
)
|
||
`,console.log("[Database] Creating remote_data table..."),await _`
|
||
CREATE TABLE IF NOT EXISTS remote_data (
|
||
key TEXT PRIMARY KEY,
|
||
data TEXT NOT NULL,
|
||
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
`,console.log("[Database] Creating collector_zones table..."),await _`
|
||
CREATE TABLE IF NOT EXISTS collector_zones (
|
||
id INTEGER PRIMARY KEY,
|
||
zone_name TEXT,
|
||
geometry_wkt TEXT,
|
||
properties TEXT,
|
||
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
`,console.log("[Database] Creating parcels table...");try{const r=await _`PRAGMA table_info(parcels)`;r.length>0&&!r.some(o=>o.name==="upn")&&(console.log("[Database] Migrating parcels table to lu_parcels structure (dropping old cache)..."),await _`DROP TABLE parcels`)}catch(r){console.warn("[Database] parcels migration check failed:",r)}await _`
|
||
CREATE TABLE IF NOT EXISTS parcels (
|
||
id INTEGER PRIMARY KEY,
|
||
upn TEXT,
|
||
style INTEGER,
|
||
landuse TEXT,
|
||
zone_code TEXT,
|
||
zone_name TEXT,
|
||
sector TEXT,
|
||
block TEXT,
|
||
parcel_no TEXT,
|
||
prop_no TEXT,
|
||
st_name TEXT,
|
||
prop_add TEXT,
|
||
fac_name TEXT,
|
||
min_height INTEGER,
|
||
max_height INTEGER,
|
||
eff_date TEXT,
|
||
lp_name TEXT,
|
||
locality TEXT,
|
||
mmda TEXT,
|
||
last_update TEXT,
|
||
remarks TEXT,
|
||
geometry_wkt TEXT,
|
||
created_at TEXT,
|
||
updated_at TEXT,
|
||
districtid INTEGER,
|
||
status TEXT DEFAULT 'verified',
|
||
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
`,console.log("[Database] Creating building_footprints table..."),await _`
|
||
CREATE TABLE IF NOT EXISTS building_footprints (
|
||
id INTEGER PRIMARY KEY,
|
||
geometry_wkt TEXT,
|
||
properties TEXT,
|
||
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
`,console.log("[Database] Creating osm_roads table..."),await _`
|
||
CREATE TABLE IF NOT EXISTS osm_roads (
|
||
osm_id INTEGER PRIMARY KEY,
|
||
geometry_wkt TEXT,
|
||
properties TEXT,
|
||
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
`,console.log("[Database] Creating upn_grid table..."),await _`
|
||
CREATE TABLE IF NOT EXISTS upn_grid (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
districtid INTEGER,
|
||
upn_prefix TEXT,
|
||
geometry_wkt TEXT,
|
||
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
`,console.log("[Database] Creating gps_trails table..."),await _`
|
||
CREATE TABLE IF NOT EXISTS gps_trails (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
client_uuid TEXT UNIQUE,
|
||
name TEXT,
|
||
district_id TEXT,
|
||
started_at TEXT NOT NULL,
|
||
ended_at TEXT,
|
||
status TEXT NOT NULL DEFAULT 'recording',
|
||
point_count INTEGER NOT NULL DEFAULT 0,
|
||
distance_m REAL NOT NULL DEFAULT 0,
|
||
synced INTEGER NOT NULL DEFAULT 0,
|
||
remote_id TEXT,
|
||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
`,console.log("[Database] Creating gps_trail_points table..."),await _`
|
||
CREATE TABLE IF NOT EXISTS gps_trail_points (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
trail_id INTEGER NOT NULL,
|
||
seq INTEGER NOT NULL,
|
||
longitude REAL NOT NULL,
|
||
latitude REAL NOT NULL,
|
||
altitude REAL,
|
||
accuracy REAL,
|
||
altitude_accuracy REAL,
|
||
heading REAL,
|
||
speed REAL,
|
||
satellites INTEGER,
|
||
recorded_at TEXT NOT NULL
|
||
)
|
||
`,console.log("[Database] Creating external_imports table..."),await _`
|
||
CREATE TABLE IF NOT EXISTS external_imports (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
filename TEXT NOT NULL,
|
||
target_type TEXT NOT NULL DEFAULT 'other',
|
||
mapping_json TEXT,
|
||
status TEXT NOT NULL DEFAULT 'imported',
|
||
feature_count INTEGER NOT NULL DEFAULT 0,
|
||
error_count INTEGER NOT NULL DEFAULT 0,
|
||
client_import_id TEXT UNIQUE,
|
||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||
last_uploaded_at TEXT
|
||
)
|
||
`,console.log("[Database] Creating external_import_features table..."),await _`
|
||
CREATE TABLE IF NOT EXISTS external_import_features (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
import_id INTEGER NOT NULL REFERENCES external_imports(id) ON DELETE CASCADE,
|
||
client_uuid TEXT UNIQUE,
|
||
geometry_wkt TEXT NOT NULL,
|
||
properties_json TEXT,
|
||
upload_status TEXT NOT NULL DEFAULT 'pending',
|
||
server_id INTEGER,
|
||
error_message TEXT
|
||
)
|
||
`,await _`CREATE INDEX IF NOT EXISTS idx_locations_category ON locations(category)`,await _`CREATE INDEX IF NOT EXISTS idx_locations_synced ON locations(synced)`,await _`CREATE INDEX IF NOT EXISTS idx_gps_trails_synced ON gps_trails(synced, status)`,await _`CREATE INDEX IF NOT EXISTS idx_gps_trail_points_trail ON gps_trail_points(trail_id, seq)`,await _`CREATE INDEX IF NOT EXISTS idx_external_imports_status ON external_imports(status)`,await _`CREATE INDEX IF NOT EXISTS idx_external_import_features_import ON external_import_features(import_id, upload_status)`;const t=await _`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;console.log("[Database] All tables:",t.map(r=>r.name)),Zn=!0,Qn(!0),console.log("[Database] ✓ Schema initialized")}catch(n){throw console.error("[Database] ✗ Schema init failed:",n),er(n),n}}async function Ea(n,e,t,r={}){const{description:o=null,category:a="default"}=r;console.log("[Database] Adding location:",n,e,t,a);try{const i=await _`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;if(console.log("[Database] Table check before insert:",i),i.length===0)throw console.error("[Database] ✗ locations table does not exist!"),new Error("locations table does not exist");console.log("[Database] Executing INSERT..."),await _`
|
||
INSERT INTO locations (name, longitude, latitude, description, category)
|
||
VALUES (${n}, ${e}, ${t}, ${o}, ${a})
|
||
`,console.log("[Database] INSERT completed");const l=(await _`SELECT last_insert_rowid() as id`)[0]?.id;console.log("[Database] New ID:",l);const c=await _`SELECT * FROM locations WHERE id = ${l}`;if(console.log("[Database] Verify insert:",c),c.length===0)throw console.error("[Database] ✗ Insert verification failed - row not found!"),new Error("Insert verification failed");return await _`
|
||
INSERT INTO sync_log (table_name, record_id, action)
|
||
VALUES ('locations', ${l}, 'INSERT')
|
||
`,Y("locations","INSERT",l),console.log("[Database] ✓ Location added:",l),{id:l}}catch(i){throw console.error("[Database] ✗ Failed to add location:",i),i}}async function tr(n={}){const{category:e=null,limit:t=1e3}=n;try{const r=await _`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;if(console.log("[Database] getLocations - table exists:",r.length>0),r.length===0)return console.warn("[Database] locations table does not exist yet"),[];let o;return e?o=await _`
|
||
SELECT * FROM locations
|
||
WHERE category = ${e}
|
||
ORDER BY created_at DESC
|
||
LIMIT ${t}
|
||
`:o=await _`
|
||
SELECT * FROM locations
|
||
ORDER BY created_at DESC
|
||
LIMIT ${t}
|
||
`,console.log("[Database] getLocations returned",o.length,"rows"),o}catch(r){return console.error("[Database] getLocations error:",r),[]}}async function xa(){try{return(await _`SELECT COUNT(*) as count FROM locations`)[0]?.count??0}catch(n){return console.error("[Database] getLocationCount error:",n),0}}async function or(n,e){try{const t=JSON.stringify(e);await _`
|
||
INSERT OR REPLACE INTO remote_data (key, data, fetched_at)
|
||
VALUES (${n}, ${t}, CURRENT_TIMESTAMP)
|
||
`,console.log("[Database] ✓ Remote data cached:",n)}catch(t){throw console.error("[Database] ✗ Failed to cache remote data:",n,t),t}}async function nr(n){try{const e=await _`SELECT data, fetched_at FROM remote_data WHERE key = ${n}`;if(e.length===0)return null;const t=JSON.parse(e[0].data);return console.log("[Database] ✓ Remote data loaded from cache:",n,"(fetched",e[0].fetched_at+")"),t}catch(e){return console.error("[Database] ✗ Failed to read cached remote data:",n,e),null}}async function Sa(n){try{await _`DELETE FROM collector_zones`;for(const e of n){const t=JSON.stringify(e);await _`
|
||
INSERT INTO collector_zones (id, zone_name, geometry_wkt, properties, fetched_at)
|
||
VALUES (${e.colzonenr||e.id}, ${e.colzonename||e.zone_name||""}, ${e.polygon||e.boundary||""}, ${t}, CURRENT_TIMESTAMP)
|
||
`}console.log("[Database] ✓ Saved",n.length,"collector zones")}catch(e){throw console.error("[Database] ✗ Failed to save collector zones:",e),e}}async function La(){try{const n=await _`SELECT properties FROM collector_zones ORDER BY id`;return n.length===0?null:n.map(e=>JSON.parse(e.properties))}catch(n){return console.error("[Database] ✗ Failed to read local collector zones:",n),null}}async function Ta(n,e){try{await _`BEGIN`,await _`DELETE FROM upn_grid`;let t=0;for(const r of n){const o=r.polygon||r.geometry_wkt||r.geom||"";await _`
|
||
INSERT INTO upn_grid (districtid, upn_prefix, geometry_wkt, fetched_at)
|
||
VALUES (${X(e)}, ${r.upn_prefix??null}, ${o}, CURRENT_TIMESTAMP)
|
||
`,t++}await _`COMMIT`,console.log("[Database] ✓ Saved",t,"UPN-grid cells (district",e,")")}catch(t){try{await _`ROLLBACK`}catch{}throw console.error("[Database] ✗ Failed to save UPN grid:",t),t}}async function ka(n){try{const e=await _`
|
||
SELECT id, districtid, upn_prefix, geometry_wkt
|
||
FROM upn_grid
|
||
WHERE districtid = ${X(n)}
|
||
ORDER BY id
|
||
`;return e.length===0?null:e}catch(e){return console.error("[Database] ✗ Failed to read UPN grid:",e),null}}function rr(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,n=>{const e=Math.random()*16|0;return(n==="x"?e:e&3|8).toString(16)})}async function Ia(n){const{filename:e,targetType:t="other",featureCount:r=0}=n,o=rr();try{await _`
|
||
INSERT INTO external_imports
|
||
(filename, target_type, status, feature_count, client_import_id)
|
||
VALUES
|
||
(${e}, ${t},
|
||
${t==="other"?"other":"imported"},
|
||
${r}, ${o})
|
||
`;const i=(await _`SELECT last_insert_rowid() AS id`)[0]?.id;return Y("external_imports","INSERT",i),{id:i,client_import_id:o}}catch(a){throw console.error("[Database] ✗ Failed to create external import:",a),a}}async function Pa(n,e){if(!Array.isArray(e)||e.length===0)return 0;try{await _`BEGIN`;let t=0;for(const r of e){const o=r.geometry_wkt||"";if(!o)continue;const a=JSON.stringify(r.properties??{}),i=r.client_uuid||rr();await _`
|
||
INSERT INTO external_import_features
|
||
(import_id, client_uuid, geometry_wkt, properties_json, upload_status)
|
||
VALUES
|
||
(${n}, ${i}, ${o}, ${a}, 'pending')
|
||
`,t++}return await _`COMMIT`,Y("external_import_features","INSERT",n),t}catch(t){try{await _`ROLLBACK`}catch{}throw console.error("[Database] ✗ Failed to add import features:",t),t}}async function Dt(n,e={}){try{const t=await _`SELECT * FROM external_imports WHERE id = ${n}`;if(t.length===0)throw new Error(`Import ${n} not found`);const r=t[0],o=e.targetType??r.target_type,a=e.mapping!==void 0?e.mapping?JSON.stringify(e.mapping):null:r.mapping_json,i=e.status??r.status,s=e.errorCount??r.error_count,l=e.lastUploadedAt??r.last_uploaded_at;await _`
|
||
UPDATE external_imports SET
|
||
target_type = ${o},
|
||
mapping_json = ${a},
|
||
status = ${i},
|
||
error_count = ${s},
|
||
last_uploaded_at = ${l}
|
||
WHERE id = ${n}
|
||
`,Y("external_imports","UPDATE",n)}catch(t){throw console.error("[Database] ✗ Failed to update external import:",t),t}}async function Ma(n){try{const e=await _`SELECT * FROM external_imports WHERE id = ${n}`;if(e.length===0)return null;const t=e[0];return{...t,mapping:t.mapping_json?JSON.parse(t.mapping_json):null}}catch(e){return console.error("[Database] ✗ Failed to read external import:",e),null}}async function Ca(n){try{return(await _`
|
||
SELECT id, client_uuid, geometry_wkt, properties_json,
|
||
upload_status, server_id, error_message
|
||
FROM external_import_features
|
||
WHERE import_id = ${n}
|
||
ORDER BY id
|
||
`).map(t=>({...t,properties:t.properties_json?JSON.parse(t.properties_json):{}}))}catch(e){return console.error("[Database] ✗ Failed to read import features:",e),[]}}async function Aa(n,e){try{const t=await _`
|
||
SELECT id, properties_json
|
||
FROM external_import_features
|
||
WHERE import_id = ${n}
|
||
`;if(t.length===0)return 0;await _`BEGIN`;let r=0;for(const o of t){const a=o.properties_json?JSON.parse(o.properties_json):{},i=e(a)??{};await _`
|
||
UPDATE external_import_features
|
||
SET properties_json = ${JSON.stringify(i)}
|
||
WHERE id = ${o.id}
|
||
`,r++}return await _`COMMIT`,Y("external_import_features","UPDATE",n),r}catch(t){try{await _`ROLLBACK`}catch{}throw console.error("[Database] ✗ Failed to remap import features:",t),t}}async function Fa(n,e,t){try{if(t!==void 0){const r=JSON.stringify(t);await _`
|
||
UPDATE external_import_features
|
||
SET geometry_wkt = ${e}, properties_json = ${r}
|
||
WHERE client_uuid = ${n}
|
||
`}else await _`
|
||
UPDATE external_import_features
|
||
SET geometry_wkt = ${e}
|
||
WHERE client_uuid = ${n}
|
||
`;Y("external_import_features","UPDATE",n)}catch(r){throw console.error("[Database] ✗ Failed to update import feature geometry:",r),r}}async function Oa(n){try{const e=await _`
|
||
SELECT import_id FROM external_import_features WHERE client_uuid = ${n}
|
||
`;if(e.length===0)return;const t=e[0].import_id;await _`BEGIN`;try{await _`DELETE FROM external_import_features WHERE client_uuid = ${n}`,await _`
|
||
UPDATE external_imports
|
||
SET feature_count = MAX(0, feature_count - 1)
|
||
WHERE id = ${t}
|
||
`,await _`COMMIT`}catch(r){try{await _`ROLLBACK`}catch{}throw r}Y("external_import_features","DELETE",n),Y("external_imports","UPDATE",t)}catch(e){throw console.error("[Database] ✗ Failed to delete import feature:",e),e}}function X(n){if(n===""||n===null||n===void 0)return null;const e=Number(n);return Number.isNaN(e)?null:e}async function Da(n){try{await _`BEGIN`,await _`DELETE FROM parcels`;let e=0;for(const t of n){const r=t.id??t.parcelid??t.parcel_id??null;if(r==null)continue;const o=t.boundary||t.geometry_wkt||t.polygon||t.wkt||(typeof t.geom=="string"?t.geom:"");await _`
|
||
INSERT OR REPLACE INTO parcels (
|
||
id, upn, style, 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, geometry_wkt,
|
||
created_at, updated_at, districtid, status, fetched_at
|
||
) VALUES (
|
||
${r}, ${t.upn??null}, ${X(t.style)}, ${t.landuse??null},
|
||
${t.zone_code??null}, ${t.zone_name??null}, ${t.sector??null},
|
||
${t.block??null}, ${t.parcel_no??null}, ${t.prop_no??null},
|
||
${t.st_name??null}, ${t.prop_add??null}, ${t.fac_name??null},
|
||
${X(t.min_height)}, ${X(t.max_height)}, ${t.eff_date??null},
|
||
${t.lp_name??null}, ${t.locality??null}, ${t.mmda??null},
|
||
${t.last_update??null}, ${t.remarks??null}, ${o},
|
||
${t.created_at??null}, ${t.updated_at??null}, ${X(t.districtid)},
|
||
'verified', CURRENT_TIMESTAMP
|
||
)
|
||
`,e++}await _`COMMIT`,console.log("[Database] ✓ Saved",e,"parcels (from",n.length,"rows,",n.length-e,"skipped/replaced)")}catch(e){try{await _`ROLLBACK`}catch{}throw console.error("[Database] ✗ Failed to save parcels:",e),e}}async function Ra(){try{const n=await _`SELECT * FROM parcels ORDER BY id`;return n.length===0?null:n}catch(n){return console.error("[Database] ✗ Failed to read local parcels:",n),null}}async function Na(n,e){try{await _`
|
||
UPDATE parcels SET
|
||
upn = ${e.upn??null},
|
||
style = ${X(e.style)},
|
||
landuse = ${e.landuse??null},
|
||
zone_code = ${e.zone_code??null},
|
||
zone_name = ${e.zone_name??null},
|
||
sector = ${e.sector??null},
|
||
block = ${e.block??null},
|
||
parcel_no = ${e.parcel_no??null},
|
||
prop_no = ${e.prop_no??null},
|
||
st_name = ${e.st_name??null},
|
||
prop_add = ${e.prop_add??null},
|
||
fac_name = ${e.fac_name??null},
|
||
min_height = ${X(e.min_height)},
|
||
max_height = ${X(e.max_height)},
|
||
eff_date = ${e.eff_date??null},
|
||
lp_name = ${e.lp_name??null},
|
||
locality = ${e.locality??null},
|
||
mmda = ${e.mmda??null},
|
||
last_update = ${e.last_update??null},
|
||
remarks = ${e.remarks??null},
|
||
districtid = ${X(e.districtid)},
|
||
updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = ${n}
|
||
`,console.log("[Database] ✓ Parcel updated:",n),Y("parcels","UPDATE",n)}catch(t){throw console.error("[Database] ✗ Failed to update parcel:",n,t),t}}async function $a(n,e={}){try{await _`
|
||
INSERT INTO parcels (
|
||
id, upn, style, 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, geometry_wkt,
|
||
created_at, updated_at, districtid, status, fetched_at
|
||
) VALUES (
|
||
NULL, ${e.upn??null}, ${X(e.style)}, ${e.landuse??null},
|
||
${e.zone_code??null}, ${e.zone_name??null}, ${e.sector??null},
|
||
${e.block??null}, ${e.parcel_no??null}, ${e.prop_no??null},
|
||
${e.st_name??null}, ${e.prop_add??null}, ${e.fac_name??null},
|
||
${X(e.min_height)}, ${X(e.max_height)}, ${e.eff_date??null},
|
||
${e.lp_name??null}, ${e.locality??null}, ${e.mmda??null},
|
||
${e.last_update??null}, ${e.remarks??null}, ${n},
|
||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ${X(e.districtid)},
|
||
'new', CURRENT_TIMESTAMP
|
||
)
|
||
`;const r=(await _`SELECT last_insert_rowid() as id`)[0]?.id;return console.log("[Database] ✓ New parcel inserted:",r,"(status: new)"),Y("parcels","INSERT",r),{id:r}}catch(t){throw console.error("[Database] ✗ Failed to insert new parcel:",t),t}}async function Ba(n){try{if(n.length>0){const e=n[0],t={};for(const[r,o]of Object.entries(e))t[r]=o===null?"null":typeof o;console.log("[Database] First footprint field types:",t)}await _`DELETE FROM building_footprints`;for(const e of n){const t=JSON.stringify(e);let r=e.polygon||e.boundary||e.geom||e.wkt||e.footprint||"";const o=typeof r=="object"?JSON.stringify(r):String(r);let a=e.id||e.footprint_id||e.building_id||null;await _`
|
||
INSERT INTO building_footprints (id, geometry_wkt, properties, fetched_at)
|
||
VALUES (${a!==null&&typeof a=="object"?null:a}, ${o}, ${t}, CURRENT_TIMESTAMP)
|
||
`}console.log("[Database] ✓ Saved",n.length,"building footprints")}catch(e){throw console.error("[Database] ✗ Failed to save building footprints:",e),e}}async function Ga(){try{const n=await _`SELECT properties FROM building_footprints ORDER BY id`;return n.length===0?null:n.map(e=>JSON.parse(e.properties))}catch(n){return console.error("[Database] ✗ Failed to read local building footprints:",n),null}}async function ja(n){try{if(n.length>0){const e=n[0],t={};for(const[r,o]of Object.entries(e))t[r]=o===null?"null":typeof o;console.log("[Database] First road field types:",t)}await _`DELETE FROM osm_roads`;for(const e of n){const t=JSON.stringify(e);let r=e.geom||e.geometry||e.wkt||e.road||e.line||"";const o=typeof r=="object"?JSON.stringify(r):String(r);let a=e.osm_id??e.osmid??e.id??null;await _`
|
||
INSERT OR REPLACE INTO osm_roads (osm_id, geometry_wkt, properties, fetched_at)
|
||
VALUES (${a!==null&&typeof a=="object"?null:a}, ${o}, ${t}, CURRENT_TIMESTAMP)
|
||
`}console.log("[Database] ✓ Saved",n.length,"OSM roads")}catch(e){throw console.error("[Database] ✗ Failed to save OSM roads:",e),e}}async function Ua(){try{const n=await _`SELECT properties FROM osm_roads ORDER BY osm_id`;return n.length===0?null:n.map(e=>JSON.parse(e.properties))}catch(n){return console.error("[Database] ✗ Failed to read local OSM roads:",n),null}}async function qa(){return Yn.getDatabaseFile()}async function za(n="lupmis-backup.sqlite3"){const e=await qa(),t=new Blob([e],{type:"application/x-sqlite3"}),r=URL.createObjectURL(t),o=document.createElement("a");o.href=r,o.download=n,o.click(),URL.revokeObjectURL(r)}async function Ha(){return{type:"FeatureCollection",features:(await tr()).map(e=>({type:"Feature",properties:{id:e.id,name:e.name,category:e.category,notes:e.notes,created_at:e.created_at},geometry:{type:"Point",coordinates:[e.lon,e.lat]}}))}}async function Po(){try{const n=await _`
|
||
SELECT name FROM sqlite_master
|
||
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
||
ORDER BY name
|
||
`,e=await xa();return{ready:Zn,databasePath:Io,tables:n.map(t=>t.name),locationCount:e}}catch(n){return{ready:!1,error:n.message}}}const ar=Object.freeze(["parcels","building_footprints","osm_roads","collector_zones","upn_grid","remote_data"]);function ir(n){return ar.includes(n)}async function sr(n){if(!ir(n))throw new Error(`Refusing to clear "${n}" — not a known cached-layer table`);const t=(await _(`SELECT COUNT(*) AS n FROM "${n}"`))[0]?.n??0;return await _(`DELETE FROM "${n}"`),console.log(`[Database] ✓ Cleared "${n}" (${t} rows)`),Y(n,"CLEAR",null),t}async function Mo(){const n=await _`
|
||
SELECT name FROM sqlite_master
|
||
WHERE type='table' AND name IN (
|
||
'parcels', 'building_footprints', 'osm_roads', 'collector_zones', 'upn_grid', 'remote_data'
|
||
)
|
||
`,e=new Set(n.map(o=>o.name)),t=[];for(const o of ar)if(e.has(o))try{const a=await sr(o);t.push({table:o,count:a})}catch(a){console.error(`[Database] Failed to clear ${o}:`,a),t.push({table:o,count:0,error:a.message})}const r=t.reduce((o,a)=>o+a.count,0);return console.log(`[Database] ✓ Cleared all cached layers: ${r} rows across ${t.length} tables`),t}async function Wa(){const n=await _`
|
||
SELECT name FROM sqlite_master
|
||
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
||
ORDER BY name
|
||
`;if(n.length===0)return[];const e=n.map(t=>`SELECT '${t.name}' AS name, COUNT(*) AS count FROM "${t.name}"`).join(" UNION ALL ");return _(e)}async function Va(n,e=200){if((await _`
|
||
SELECT name FROM sqlite_master
|
||
WHERE type='table' AND name = ${n}
|
||
`).length===0)throw new Error(`Table "${n}" does not exist`);const r=await _(`SELECT * FROM "${n}" LIMIT ${e}`);return{columns:r.length>0?Object.keys(r[0]):[],rows:r}}async function Ka(){console.log("=== DATABASE TEST ===");try{const n=await _`SELECT sqlite_version() as v`;console.log("1. SQLite version:",n[0].v);const e=await _`SELECT name FROM sqlite_master WHERE type='table'`;console.log("2. Tables:",e.map(o=>o.name)),console.log("3. Inserting test row..."),await _`INSERT INTO locations (name, longitude, latitude, category) VALUES ('TEST', -1.0, 7.0, 'test')`;const t=await _`SELECT * FROM locations WHERE name = 'TEST'`;console.log("4. Test row:",t);const r=await _`SELECT COUNT(*) as c FROM locations`;return console.log("5. Total rows:",r[0].c),await _`DELETE FROM locations WHERE name = 'TEST'`,console.log("6. Test row deleted"),console.log("=== TEST PASSED ==="),!0}catch(n){return console.error("=== TEST FAILED ===",n),!1}}typeof window<"u"&&(window.testDatabase=Ka,window.dbStatus=Po);async function Xa(n){const{uuid:e,name:t=null,startedAt:r,districtId:o=null}=n;await _`
|
||
INSERT INTO gps_trails (client_uuid, name, district_id, started_at, status)
|
||
VALUES (${e}, ${t}, ${o}, ${r}, 'recording')
|
||
`;const i=(await _`SELECT last_insert_rowid() as id`)[0]?.id;return Y("gps_trails","insert",i),i}async function Ya(n,e){const{seq:t,lon:r,lat:o,altitude:a=null,accuracy:i=null,altitudeAccuracy:s=null,heading:l=null,speed:c=null,satellites:d=null,timestamp:u}=e,p=typeof u=="number"?new Date(u).toISOString():u||new Date().toISOString();await _`
|
||
INSERT INTO gps_trail_points
|
||
(trail_id, seq, longitude, latitude, altitude, accuracy, altitude_accuracy, heading, speed, satellites, recorded_at)
|
||
VALUES
|
||
(${n}, ${t}, ${r}, ${o}, ${a}, ${i}, ${s}, ${l}, ${c}, ${d}, ${p})
|
||
`}async function Ja(n,e){const{endedAt:t,pointCount:r=0,distanceM:o=0}=e;await _`
|
||
UPDATE gps_trails
|
||
SET ended_at = ${t}, point_count = ${r}, distance_m = ${o}, status = 'completed'
|
||
WHERE id = ${n}
|
||
`,Y("gps_trails","update",n)}async function Za(){return _`SELECT * FROM gps_trails WHERE synced = 0 AND status = 'completed' ORDER BY started_at ASC`}async function Qa(n){return _`SELECT * FROM gps_trail_points WHERE trail_id = ${n} ORDER BY seq ASC`}async function ei(n,e=null){await _`UPDATE gps_trails SET synced = 1, remote_id = ${e} WHERE id = ${n}`,Y("gps_trails","update",n)}const lr=3.28084,cr=621371e-9,dr=10.7639,ur=247105e-9,pr=3861e-10;function $t(){return localStorage.getItem("measurement-system")||"metric"}function ut(n){if($t()==="imperial"){const e=n*lr;return e>=5280?Math.round(n*cr*100)/100+" mi":Math.round(e)+" ft"}return n>1e3?Math.round(n/1e3*100)/100+" km":Math.round(n*100)/100+" m"}function ti(n){if($t()==="imperial"){const e=n*lr,t=n*cr;return e>=5280?`${t.toFixed(2)} mi (${e.toLocaleString("en",{maximumFractionDigits:0})} ft)`:`${e.toLocaleString("en",{maximumFractionDigits:1})} ft`}return n>=1e3?`${(n/1e3).toFixed(2)} km (${n.toLocaleString("en",{maximumFractionDigits:0})} m)`:`${n.toLocaleString("en",{maximumFractionDigits:1})} m`}function We(n){if($t()==="imperial"){const e=n*ur;return e>=640?Math.round(n*pr*100)/100+" mi²":e>=1?Math.round(e*100)/100+" acres":Math.round(n*dr).toLocaleString("en")+" ft²"}return n>1e6?Math.round(n/1e6*100)/100+" km²":Math.round(n*100)/100+" m²"}function oi(n){if($t()==="imperial"){const e=n*dr,t=n*ur,r=n*pr;return t>=640?`${r.toFixed(2)} mi² (${t.toLocaleString("en",{maximumFractionDigits:0})} acres)`:t>=1?`${t.toLocaleString("en",{maximumFractionDigits:1})} acres (${e.toLocaleString("en",{maximumFractionDigits:0})} ft²)`:`${e.toLocaleString("en",{maximumFractionDigits:0})} ft²`}return n>1e6?`${(n/1e6).toFixed(2)} km² (${n.toLocaleString("en",{maximumFractionDigits:0})} m²)`:`${n.toLocaleString("en",{maximumFractionDigits:0})} m²`}function ni(n){return We(Math.PI*n*n)}function fr(n){const e=xe(n)[1];return Math.cos(e*Math.PI/180)}function go(n,e){return n*fr(e)}function ri(n,e){const t=fr(e);return t===0?n:n/t}function ai(n){return n<50?1:n<200?5:n<1e3?10:n<5e3?50:100}function ii(n,e,t,r,o=1e-10){const a=e[0]-n[0],i=e[1]-n[1],s=r[0]-t[0],l=r[1]-t[1],c=a*l-i*s;if(Math.abs(c)<o)return null;const d=t[0]-n[0],u=t[1]-n[1],p=(d*l-u*s)/c,f=(d*i-u*a)/c;return p<-o||p>1+o||f<-o||f>1+o?null:{point:[n[0]+p*a,n[1]+p*i],t:Math.max(0,Math.min(1,p)),u:Math.max(0,Math.min(1,f))}}function hr(n){let e=0;for(let t=0,r=n.length;t<r-1;t++)e+=n[t][0]*n[t+1][1]-n[t+1][0]*n[t][1];return e/2}function mo(n,e){let t=!1;for(let r=0,o=e.length-2;r<e.length-1;o=r++){const a=e[r][0],i=e[r][1],s=e[o][0],l=e[o][1];i>n[1]!=l>n[1]&&n[0]<(s-a)*(n[1]-i)/(l-i)+a&&(t=!t)}return t}function ht(n,e){return(n[0]-e[0])**2+(n[1]-e[1])**2}function si(n,e){const t=[];for(let o=0;o<e.length-1;o++)for(let a=0;a<n.length-1;a++){const i=ii(n[a],n[a+1],e[o],e[o+1],1e-10);if(!i)continue;const s=i.point;let l=!1;for(const c of t)if(ht(c.point,s)<1e-6){l=!0;break}l||t.push({point:s,ringSegIdx:a,ringT:i.t,lineSegIdx:o,lineT:i.u})}return t.sort((o,a)=>o.lineSegIdx!==a.lineSegIdx?o.lineSegIdx-a.lineSegIdx:o.lineT-a.lineT),t}function li(n,e){const t=e.map((a,i)=>({...a,origOrder:i}));t.sort((a,i)=>a.ringSegIdx!==i.ringSegIdx?a.ringSegIdx-i.ringSegIdx:a.ringT-i.ringT);const r=n.slice(),o=new Array(t.length);for(let a=t.length-1;a>=0;a--){const i=t[a],s=i.ringSegIdx+1,l=1e-6;if(ht(i.point,r[i.ringSegIdx])<l){o[i.origOrder]=i.ringSegIdx;continue}if(ht(i.point,r[i.ringSegIdx+1])<l){o[i.origOrder]=i.ringSegIdx+1;continue}r.splice(s,0,i.point),o[i.origOrder]=s;for(let c=a+1;c<t.length;c++)o[t[c].origOrder]>=s&&o[t[c].origOrder]++}return{ring:r,indices:o}}function Zo(n,e,t){const r=n.length-1,o=(e%r+r)%r,a=(t%r+r)%r,i=[];let s=o;for(;i.push(n[s]),s!==a;)s=(s+1)%r;return i}function Qo(n,e,t){const r=[e.point],o=e.lineSegIdx,a=t.lineSegIdx;for(let i=o+1;i<=a;i++)r.push(n[i]);return ht(r[r.length-1],t.point)>1e-10&&r.push(t.point),r}function en(n,e){const t=hr(n);return e&&t<0||!e&&t>0?n.slice().reverse():n}function tn(n){if(n.length<2)return n;const e=n[0],t=n[n.length-1];return ht(e,t)>1e-10?[...n,e.slice()]:n}function ci(n,e){let t=1/0,r=1/0,o=-1/0,a=-1/0;for(const c of e)c[0]<t&&(t=c[0]),c[1]<r&&(r=c[1]),c[0]>o&&(o=c[0]),c[1]>a&&(a=c[1]);const i=Math.sqrt((o-t)**2+(a-r)**2)||1,s=n.slice();if(mo(s[0],e)){const c=s[0],d=s[1],u=c[0]-d[0],p=c[1]-d[1],f=Math.sqrt(u*u+p*p)||1,h=i*2/f;s[0]=[c[0]+u*h,c[1]+p*h]}const l=s.length-1;if(mo(s[l],e)){const c=s[l],d=s[l-1],u=c[0]-d[0],p=c[1]-d[1],f=Math.sqrt(u*u+p*p)||1,h=i*2/f;s[l]=[c[0]+u*h,c[1]+p*h]}return s}function kt(n,e){const t=n[0],r=n.slice(1),o=ci(e,t),a=si(t,o);if(a.length!==2)return console.warn(`[polygonSplit] Expected 2 intersections, got ${a.length}`),null;const[i,s]=a,{ring:l,indices:c}=li(t,a),d=c[0],u=c[1],[p,f]=d<u?[d,u]:[u,d],h=d<u?Qo(o,i,s):Qo(o,s,i),g=h.slice().reverse(),m=Zo(l,p,f),y=tn([...m,...g.slice(1)]),w=Zo(l,f,p),b=tn([...w,...h.slice(1)]),S=hr(t)>0,L=en(y,S),E=en(b,S),x=[L],I=[E];for(const k of r){const U=di(k);mo(U,L)?x.push(k):I.push(k)}return[x,I]}function di(n){let e=0,t=0;const r=n.length-1;for(let o=0;o<r;o++)e+=n[o][0],t+=n[o][1];return[e/r,t/r]}const on={success:{bg:"#10b981",icon:"✅"},error:{bg:"#ef4444",icon:"❌"},warning:{bg:"#f59e0b",icon:"⚠️"},info:{bg:"#0ea5e9",icon:"ℹ️"}};let $e=null;function ui(){return $e||($e=document.createElement("div"),$e.style.cssText=`
|
||
position: fixed;
|
||
top: 16px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
z-index: 10000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
pointer-events: none;
|
||
`,document.body.appendChild($e),$e)}function F(n,e="info",t=4e3){const r=ui(),o=on[e]||on.info,a=document.createElement("div");a.style.cssText=`
|
||
background: ${o.bg};
|
||
color: #fff;
|
||
padding: 10px 18px;
|
||
border-radius: 8px;
|
||
font-family: var(--font-body, 'Exo', sans-serif);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
|
||
pointer-events: auto;
|
||
cursor: pointer;
|
||
opacity: 0;
|
||
transition: opacity 0.25s ease, transform 0.25s ease;
|
||
transform: translateY(-8px);
|
||
max-width: 420px;
|
||
text-align: center;
|
||
line-height: 1.4;
|
||
`,a.textContent=`${o.icon} ${n}`,r.appendChild(a),requestAnimationFrame(()=>{a.style.opacity="1",a.style.transform="translateY(0)"});const i=()=>{a.style.opacity="0",a.style.transform="translateY(-8px)",setTimeout(()=>a.remove(),300)};a.addEventListener("click",i),setTimeout(i,t)}const _t=[{stroke:"#ef4444",fill:"rgba(239,68,68,0.25)"},{stroke:"#3b82f6",fill:"rgba(59,130,246,0.25)"}],pi=new M({stroke:new P({color:"#0ea5e9",width:3}),fill:new C({color:"rgba(14,165,233,0.15)"})}),fi=new M({stroke:new P({color:"#f43f5e",width:2,lineDash:[8,6]}),image:new he({radius:5,fill:new C({color:"#f43f5e"}),stroke:new P({color:"#fff",width:1.5})})});class hi extends Lo{constructor(e={}){super({handleEvent:t=>this._handleEvent(t)}),this.snapDistance_=e.snapDistance||25,this._sources=e.sources?Array.isArray(e.sources)?e.sources:[e.sources]:null,this._phase="select",this._selectedFeature=null,this._selectedSource=null,this._drawInteraction=null,this._splitFeatures=null,this._overlaySource=new G({useSpatialIndex:!1}),this._overlayLayer=new D({source:this._overlaySource,displayInLayerSwitcher:!1,style:pi})}setMap(e){this.getMap()&&(this.getMap().removeLayer(this._overlayLayer),this._removeDrawInteraction()),super.setMap(e),e&&this._overlayLayer.setMap(e)}setActive(e){super.setActive(e),e||this._reset()}_getSources(){if(this._sources)return this._sources;if(!this.getMap())return[];const e=[],t=r=>{r.forEach(o=>{o.getVisible()&&(o.getSource&&o.getSource()instanceof G?e.push(o.getSource()):o.getLayers&&t(o.getLayers()))})};return t(this.getMap().getLayers()),e}_handleEvent(e){if(!this.getActive())return!0;if(this._phase==="select"){if(e.type==="pointermove")return this._onSelectMove(e);if(e.type==="singleclick")return this._onSelectClick(e)}if(this._phase==="draw"&&e.type==="keydown"&&e.originalEvent?.key==="Escape")return this._cancelDraw(),!1;if(this._phase==="pick"){if(e.type==="pointermove")return this._onPickMove(e);if(e.type==="singleclick")return this._onPickClick(e);if(e.type==="keydown"&&e.originalEvent?.key==="Escape")return this._reset(),!1}return!0}_onSelectMove(e){const t=this.getMap();if(!t)return!0;this._overlaySource.clear();const r=this._closestPolygon(e);if(r){const o=r.feature.clone();this._overlaySource.addFeature(o),t.getTargetElement().style.cursor="pointer"}else t.getTargetElement().style.cursor="";return!0}_onSelectClick(e){const t=this._closestPolygon(e);if(!t)return!0;this._selectedFeature=t.feature,this._selectedSource=t.source,this._overlaySource.clear();const r=t.feature.clone();return this._overlaySource.addFeature(r),this._startDrawPhase(),!1}_closestPolygon(e){let t=null,r=this.snapDistance_+1;for(const o of this._getSources()){const a=o.getClosestFeatureToCoordinate(e.coordinate);if(!a)continue;const i=a.getGeometry();if(!i)continue;const s=i.getType();if(s!=="Polygon"&&s!=="MultiPolygon")continue;const l=i.getClosestPoint(e.coordinate),d=new de([e.coordinate,l]).getLength()/e.frameState.viewState.resolution;d<r&&(r=d,t={feature:a,source:o,coord:l})}return t}_startDrawPhase(){this._phase="draw";const e=this.getMap();e&&(e.getTargetElement().style.cursor="crosshair",this._drawInteraction=new Oe({type:"LineString",style:fi}),this._drawInteraction.on("drawend",t=>{const r=t.feature.getGeometry().getCoordinates();this._performSplit(r)}),e.addInteraction(this._drawInteraction))}_removeDrawInteraction(){this._drawInteraction&&this.getMap()&&this.getMap().removeInteraction(this._drawInteraction),this._drawInteraction=null}_cancelDraw(){this._removeDrawInteraction(),this._reset()}_performSplit(e){const t=this._selectedFeature,r=this._selectedSource,o=t.getGeometry();let a;o.getType()==="Polygon"?a=o.getCoordinates():o.getType()==="MultiPolygon"&&(a=o.getCoordinates()[0]);const i=kt(a,e);if(!i){console.warn("[PolygonSplit] Split failed — line must cross the polygon boundary at exactly 2 points."),this._removeDrawInteraction(),this._startDrawPhase();return}const[s,l]=i,c=t.clone();c.setGeometry(new ft(s)),c.setStyle(new M({stroke:new P({color:_t[0].stroke,width:2.5}),fill:new C({color:_t[0].fill})}));const d=t.clone();d.setGeometry(new ft(l)),d.setStyle(new M({stroke:new P({color:_t[1].stroke,width:2.5}),fill:new C({color:_t[1].fill})}));const u=[c,d];this.dispatchEvent({type:"beforesplit",original:t,features:u}),r.dispatchEvent({type:"beforesplit",original:t,features:u});const p=this.getMap();if(p?.dispatchEvent("undoblockstart"),r.removeFeature(t),r.addFeature(c),r.addFeature(d),p?.dispatchEvent("undoblockend"),this.dispatchEvent({type:"aftersplit",original:t,features:u}),r.dispatchEvent({type:"aftersplit",original:t,features:u}),this._removeDrawInteraction(),t.get("_layerType")==="parcel"){this._splitFeatures=u,this._phase="pick",this._overlaySource.clear();const h=this.getMap();h&&(h.getTargetElement().style.cursor=""),F("Click the polygon that should keep the original identifier.","info",5e3),this.dispatchEvent({type:"splitparcel",features:u,originalProps:t.getProperties(),source:r})}else this._reset()}_onPickMove(e){const t=this.getMap();if(!t)return!0;this._overlaySource.clear();const r=this._closestSplitPiece(e);if(r){const o=r.clone();this._overlaySource.addFeature(o),t.getTargetElement().style.cursor="pointer"}else t.getTargetElement().style.cursor="";return!0}_onPickClick(e){const t=this._closestSplitPiece(e);return t?(this.dispatchEvent({type:"splitpick",picked:t,features:this._splitFeatures}),this._reset(),!1):!0}_closestSplitPiece(e){if(!this._splitFeatures)return null;let t=null,r=this.snapDistance_+1;for(const o of this._splitFeatures){const a=o.getGeometry();if(!a)continue;const i=a.getClosestPoint(e.coordinate),l=new de([e.coordinate,i]).getLength()/e.frameState.viewState.resolution;l<r&&(r=l,t=o)}return t}_reset(){this._phase="select",this._selectedFeature=null,this._selectedSource=null,this._splitFeatures=null,this._overlaySource.clear(),this._removeDrawInteraction();const e=this.getMap();e&&(e.getTargetElement().style.cursor="")}}function Xe(n,e){return(n[0]-e[0])**2+(n[1]-e[1])**2}function ct(n){let e=0;for(let t=0,r=n.length;t<r-1;t++)e+=n[t][0]*n[t+1][1]-n[t+1][0]*n[t][1];return e/2}function gi(n,e){let t=!1;for(let r=0,o=e.length-2;r<e.length-1;o=r++){const a=e[r][0],i=e[r][1],s=e[o][0],l=e[o][1];i>n[1]!=l>n[1]&&n[0]<(s-a)*(n[1]-i)/(l-i)+a&&(t=!t)}return t}function mi(n,e){const t=ct(n);return e&&t<0||!e&&t>0?n.slice().reverse():n}function yi(n){return n.length<2?n:Xe(n[0],n[n.length-1])>1e-10?[...n,n[0].slice()]:n}function He(n,e,t){const r=t[0]-e[0],o=t[1]-e[1],a=r*r+o*o;if(a<1e-20)return Xe(n,e);let i=((n[0]-e[0])*r+(n[1]-e[1])*o)/a;i=Math.max(0,Math.min(1,i));const s=e[0]+i*r,l=e[1]+i*o;return(n[0]-s)**2+(n[1]-l)**2}function nn(n,e){let t=0,r=1/0;const o=n.length-1;for(let a=0;a<o;a++){const i=He(e,n[a],n[(a+1)%o===0?o:a+1]);i<r&&(r=i,t=a)}return{segIdx:t,distSq:r}}function Be(n,e,t){return Xe(n,e)<t}function vt(n,e,t){const r=e.length-1;for(let o=0;o<r;o++)if(He(n,e[o],e[o+1])<t)return!0;return!1}function bi(n,e,t,r,o){const a=n.length-1,i=e.length-1,s=o*o,l=n[t],c=n[(t+1)%a],d=e[r],u=e[(r+1)%i],p=vt(l,e,s),f=vt(c,e,s),h=vt(d,n,s),g=vt(u,n,s);if(!(p&&f)&&!(h&&g))return console.warn("[polygonMerge] Seed edges are not on the shared boundary"),null;let m;Be(l,u,s)&&Be(c,d,s)?m=!0:Be(l,d,s)&&Be(c,u,s)?m=!1:m=Xe(l,u)<Xe(l,d);let y=t,w=(t+1)%a,b,S;m?(b=(r+1)%i,S=r):(b=r,S=(r+1)%i);let L=a+i;for(;L-- >0;){const E=(w+1)%a,x=m?(S-1+i)%i:(S+1)%i;if(E===y||x===b)break;if(Be(n[E],e[x],s)){w=E,S=x;continue}if(He(n[E],e[S],e[x])<s){w=E;continue}if(He(e[x],n[w],n[E])<s){S=x;continue}break}for(L=a+i;L-- >0;){const E=(y-1+a)%a,x=m?(b+1)%i:(b-1+i)%i;if(E===w||x===S)break;if(Be(n[E],e[x],s)){y=E,b=x;continue}if(He(n[E],e[b],e[x])<s){y=E;continue}if(He(e[x],n[y],n[E])<s){b=x;continue}break}return{startA:y,endA:w,startB:b,endB:S,reversed:m}}function Vt(n,e,t){const r=n.length-1,o=[];let a=e;for(;o.push(n[a]),!(a===t||(a=(a+1)%r,o.length>r+1)););return o}function wi(n,e,t,r,o=5){const a=n[0],i=e[0],s=n.slice(1),l=e.slice(1),c=nn(a,t),d=nn(i,r),u=bi(a,i,c.segIdx,d.segIdx,o);if(!u)return console.warn("[polygonMerge] Could not find shared boundary between polygons — seed edges are not near the other ring"),{coords:null,error:"The selected edges are not on a shared boundary. Click edges that lie on the common border between the two polygons."};const{startA:p,endA:f,startB:h,endB:g,reversed:m}=u;a.length-1,i.length-1;const y=Vt(a,f,p);let w;m?w=Vt(i,h,g):w=Vt(i,g,h);const b=[...y,...w.slice(1)],S=o*o;b.length>2&&Xe(b[b.length-1],b[0])<S&&(b[b.length-1]=b[0].slice());const L=yi(b),E=Math.abs(ct(a)),x=Math.abs(ct(i)),I=Math.abs(ct(L)),k=E+x;if(I<k*.5||I>k*1.5)return console.warn(`[polygonMerge] Area mismatch: A=${E.toFixed(1)}, B=${x.toFixed(1)}, merged=${I.toFixed(1)}, expected≈${k.toFixed(1)}`),{coords:null,error:"Merge produced an invalid polygon (area mismatch). The polygons may not be truly adjacent — try clicking closer to the shared boundary."};const U=ct(a)>0,z=mi(L,U),$=[...s,...l].filter(ce=>{const Q=ce.reduce((H,be)=>H+be[0],0)/(ce.length-1),ye=ce.reduce((H,be)=>H+be[1],0)/(ce.length-1);return gi([Q,ye],z)});return{coords:[z,...$]}}const rn=new M({stroke:new P({color:"#0ea5e9",width:3}),fill:new C({color:"rgba(14,165,233,0.15)"})}),_i=new M({stroke:new P({color:"#f59e0b",width:3}),fill:new C({color:"rgba(245,158,11,0.15)"})}),vi=new M({stroke:new P({color:"#0ea5e9",width:3}),fill:new C({color:"rgba(14,165,233,0.15)"}),text:new Ke({text:"A",font:"bold 22px Exo, sans-serif",fill:new C({color:"#0ea5e9"}),stroke:new P({color:"#fff",width:4}),overflow:!0})}),Ei=new M({stroke:new P({color:"#f59e0b",width:3}),fill:new C({color:"rgba(245,158,11,0.15)"}),text:new Ke({text:"B",font:"bold 22px Exo, sans-serif",fill:new C({color:"#f59e0b"}),stroke:new P({color:"#fff",width:4}),overflow:!0})}),xi=new M({stroke:new P({color:"#ec4899",width:4,lineDash:[10,6]})}),Si=new M({stroke:new P({color:"#10b981",width:2.5}),fill:new C({color:"rgba(16,185,129,0.3)"})});class Li extends Lo{constructor(e={}){super({handleEvent:t=>this._handleEvent(t)}),this.snapDistance_=e.snapDistance||25,this.tolerance_=e.tolerance||5,this._phase="select_a",this._featureA=null,this._sourceA=null,this._featureB=null,this._sourceB=null,this._edgeClickA=null,this._edgeClickB=null,this._highlightSource=new G({useSpatialIndex:!1}),this._highlightLayer=new D({source:this._highlightSource,displayInLayerSwitcher:!1,style:t=>t.get("_highlightStyle")||rn}),this._edgeSource=new G({useSpatialIndex:!1}),this._edgeLayer=new D({source:this._edgeSource,displayInLayerSwitcher:!1,style:xi})}setMap(e){this.getMap()&&(this.getMap().removeLayer(this._highlightLayer),this.getMap().removeLayer(this._edgeLayer)),super.setMap(e),e&&(this._highlightLayer.setMap(e),this._edgeLayer.setMap(e))}setActive(e){super.setActive(e),e||this._reset()}_getSources(){if(!this.getMap())return[];const e=[],t=r=>{r.forEach(o=>{o.getVisible()&&(o.getSource&&o.getSource()instanceof G?e.push(o.getSource()):o.getLayers&&t(o.getLayers()))})};return t(this.getMap().getLayers()),e}_handleEvent(e){if(!this.getActive())return!0;if(e.type==="keydown"&&e.originalEvent?.key==="Escape")return this._reset(),!1;switch(this._phase){case"select_a":if(e.type==="pointermove")return this._onSelectMove(e,null);if(e.type==="singleclick")return this._onSelectAClick(e);break;case"select_b":if(e.type==="pointermove")return this._onSelectMove(e,this._featureA);if(e.type==="singleclick")return this._onSelectBClick(e);break;case"click_edge_a":if(e.type==="pointermove")return this._onEdgeMove(e,this._featureA);if(e.type==="singleclick")return this._onEdgeAClick(e);break;case"click_edge_b":if(e.type==="pointermove")return this._onEdgeMove(e,this._featureB);if(e.type==="singleclick")return this._onEdgeBClick(e);break}return!0}_onSelectMove(e,t){const r=this.getMap();if(!r)return!0;this._highlightSource.clear(),this._edgeSource.clear(),this._rebuildHighlights();const o=this._closestPolygon(e,t);if(o){const a=this._phase==="select_a"?rn:_i,i=o.feature.clone();i.set("_highlightStyle",a),this._highlightSource.addFeature(i),r.getTargetElement().style.cursor="pointer"}else r.getTargetElement().style.cursor="";return!0}_onSelectAClick(e){const t=this._closestPolygon(e,null);return t?(this._featureA=t.feature,this._sourceA=t.source,this._phase="select_b",this._rebuildHighlights(),!1):!0}_onSelectBClick(e){const t=this._closestPolygon(e,this._featureA);return t?(this._featureB=t.feature,this._sourceB=t.source,this._phase="click_edge_a",this._rebuildHighlights(),this.getMap().getTargetElement().style.cursor="crosshair",!1):!0}_closestPolygon(e,t){let r=null,o=this.snapDistance_+1;for(const a of this._getSources()){const i=a.getClosestFeatureToCoordinate(e.coordinate);if(!i||t&&i===t)continue;const s=i.getGeometry();if(!s)continue;const l=s.getType();if(l!=="Polygon"&&l!=="MultiPolygon")continue;const c=s.getClosestPoint(e.coordinate),u=new de([e.coordinate,c]).getLength()/e.frameState.viewState.resolution;u<o&&(o=u,r={feature:i,source:a,coord:c})}return r}_onEdgeMove(e,t){const r=this.getMap();if(!r)return!0;this._edgeSource.clear();const o=this._closestEdgeSegment(t,e);if(o){const a=new fe(new de([o.segStart,o.segEnd]));this._edgeSource.addFeature(a),r.getTargetElement().style.cursor="crosshair"}return!0}_onEdgeAClick(e){return this._edgeClickA=e.coordinate,this._phase="click_edge_b",this._edgeSource.clear(),!1}_onEdgeBClick(e){return this._edgeClickB=e.coordinate,this._performMerge(),!1}_closestEdgeSegment(e,t){const r=e.getGeometry();let o;if(r.getType()==="Polygon")o=r.getCoordinates()[0];else if(r.getType()==="MultiPolygon")o=r.getCoordinates()[0][0];else return null;const a=t.frameState.viewState.resolution;let i=1/0,s=null;const l=o.length-1;for(let c=0;c<l;c++){const d=o[c],u=o[c+1],p=u[0]-d[0],f=u[1]-d[1],h=p*p+f*f;if(h<1e-20)continue;let g=((t.coordinate[0]-d[0])*p+(t.coordinate[1]-d[1])*f)/h;g=Math.max(0,Math.min(1,g));const m=d[0]+g*p,y=d[1]+g*f,w=Math.sqrt((t.coordinate[0]-m)**2+(t.coordinate[1]-y)**2)/a;w<i&&(i=w,s={segStart:d,segEnd:u})}return i<=this.snapDistance_?s:null}_performMerge(){const e=this._featureA,t=this._featureB,r=this._sourceA,o=this._sourceB,a=e.getGeometry(),i=t.getGeometry(),s=a.getType()==="Polygon"?a.getCoordinates():a.getCoordinates()[0],l=i.getType()==="Polygon"?i.getCoordinates():i.getCoordinates()[0],c=wi(s,l,this._edgeClickA,this._edgeClickB,this.tolerance_);if(!c.coords){F(c.error||"Merge failed — try clicking on the shared boundary.","error",5e3),this._edgeClickA=null,this._edgeClickB=null,this._phase="click_edge_a",this._edgeSource.clear();return}const d=e.clone();d.setGeometry(new ft(c.coords)),d.setStyle(Si);const u={type:"beforemerge",original:[e,t],merged:d};this.dispatchEvent(u),r.dispatchEvent({...u}),o!==r&&o.dispatchEvent({...u});const p=this.getMap();p?.dispatchEvent("undoblockstart"),r.removeFeature(e),o.removeFeature(t),r.addFeature(d),p?.dispatchEvent("undoblockend");const f={type:"aftermerge",original:[e,t],merged:d};this.dispatchEvent(f),r.dispatchEvent({...f}),o!==r&&o.dispatchEvent({...f});const h=e.get("_layerType")==="parcel",g=t.get("_layerType")==="parcel";h&&g?(this.dispatchEvent({type:"mergedparcel",merged:d,propsA:e.getProperties(),propsB:t.getProperties(),coordinate:this._edgeClickA}),F("Polygons merged — choose which identifier to keep.","success")):F("Polygons merged successfully.","success"),this._reset()}_rebuildHighlights(){const e=[];if(this._highlightSource.getFeatures().forEach(t=>{t.get("_permanent")&&e.push(t)}),e.forEach(t=>this._highlightSource.removeFeature(t)),this._featureA){const t=this._featureA.clone();t.set("_highlightStyle",vi),t.set("_permanent",!0),this._highlightSource.addFeature(t)}if(this._featureB){const t=this._featureB.clone();t.set("_highlightStyle",Ei),t.set("_permanent",!0),this._highlightSource.addFeature(t)}}_reset(){this._phase="select_a",this._featureA=null,this._sourceA=null,this._featureB=null,this._sourceB=null,this._edgeClickA=null,this._edgeClickB=null,this._highlightSource.clear(),this._edgeSource.clear();const e=this.getMap();e&&(e.getTargetElement().style.cursor="")}}function Ti(n,e){return(n[0]-e[0])**2+(n[1]-e[1])**2}function an(n){let e=0;for(let t=0,r=n.length;t<r-1;t++)e+=n[t][0]*n[t+1][1]-n[t+1][0]*n[t][1];return e/2}function ot(n){let e=Math.abs(an(n[0]));for(let t=1;t<n.length;t++)e-=Math.abs(an(n[t]));return e}function ki(n){const e=n.length-1;let t=-1,r=0;for(let c=0;c<e;c++){const d=Ti(n[c],n[c+1]);d>t&&(t=d,r=c)}const o=n[r],a=n[r+1],i=Math.sqrt(t),s=[(a[0]-o[0])/i,(a[1]-o[1])/i],l=[-s[1],s[0]];return{p0:o,p1:a,along:s,perp:l}}function Kt(n,e,t,r,o){const a=n[0]+r*e[0],i=n[1]+r*e[1];return[[a-o*t[0],i-o*t[1]],[a+o*t[0],i+o*t[1]]]}function Ge(n,e,t){const r=n[0],o=r.length-1;let a=0,i=0;for(let c=0;c<o;c++)a+=r[c][0],i+=r[c][1];const s=a/o-e[0],l=i/o-e[1];return s*t[0]+l*t[1]}function Ii(n,e,t){if(!Number.isInteger(e)||e<1)return{pieces:null,error:"Number of divisions must be a positive integer."};if(e===1)return{pieces:[n]};const r=n[0];if(ot(n)<1e-6)return{pieces:null,error:"Polygon has no measurable area."};let a,i,s;if(t&&t.length===2){a=t[0];const m=t[1][0]-t[0][0],y=t[1][1]-t[0][1],w=Math.sqrt(m*m+y*y);if(w<1e-10)return{pieces:null,error:"Selected edge has zero length."};i=[m/w,y/w],s=[-i[1],i[0]]}else{const m=ki(r);a=m.p0,i=m.along,s=m.perp}const l=a,c=r.length-1;for(let m=0;m<c;m++){const y=r[m][0]-l[0],w=r[m][1]-l[1];y*i[0]+w*i[1]}let d=1/0,u=-1/0;for(let m=0;m<c;m++){const y=r[m][0]-l[0],w=r[m][1]-l[1],b=y*s[0]+w*s[1];b<d&&(d=b),b>u&&(u=b)}const p=(u-d)*1.5,f=[];let h=n,g=e;for(let m=0;m<e-1;m++){const y=ot(h),w=y/g,b=h[0],S=b.length-1;let L=1/0,E=-1/0;for(let Z=0;Z<S;Z++){const $=b[Z][0]-l[0],ce=b[Z][1]-l[1],Q=$*i[0]+ce*i[1];Q<L&&(L=Q),Q>E&&(E=Q)}let x=L,I=E,k=null,U=null,z=1/0;for(let Z=0;Z<40;Z++){const $=(x+I)/2,ce=Kt(l,i,s,$,p),Q=kt(h,ce);if(!Q){const R=(I-x)*.01,W=Kt(l,i,s,$+R,p),re=kt(h,W);if(re){const[Ie,Pe]=re,Ze=Ge(Ie,l,i),Qe=Ge(Pe,l,i),et=Ze<Qe?Ie:Pe,Ut=Ze<Qe?Pe:Ie,qt=ot(et),tt=Math.abs(qt-w);tt<z&&(z=tt,k=et,U=Ut)}const jt=Kt(l,i,s,$-R,p),we=kt(h,jt);if(we){const[Ie,Pe]=we,Ze=Ge(Ie,l,i),Qe=Ge(Pe,l,i),et=Ze<Qe?Ie:Pe,Ut=Ze<Qe?Pe:Ie,qt=ot(et),tt=Math.abs(qt-w);tt<z&&(z=tt,k=et,U=Ut)}x=$;continue}const[ye,H]=Q,be=Ge(ye,l,i),Je=Ge(H,l,i),B=be<Je?ye:H,J=be<Je?H:ye,V=ot(B),ne=Math.abs(V-w);if(ne<z&&(z=ne,k=B,U=J),ne/y<.001)break;V<w?x=$:I=$}if(!k||!U)return{pieces:null,error:`Could not find a valid cut for piece ${m+1} of ${e}. The polygon shape may be too irregular for equal division.`};f.push(k),h=U,g--}return f.push(h),{pieces:f}}const Pi=new M({stroke:new P({color:"#0ea5e9",width:3}),fill:new C({color:"rgba(14,165,233,0.15)"})}),Mi=new M({stroke:new P({color:"#8b5cf6",width:4,lineDash:[10,6]})});function Ci(n){const e=[];for(let t=0;t<n;t++){const r=Math.round(t*360/n);e.push({stroke:`hsl(${r}, 70%, 45%)`,fill:`hsla(${r}, 70%, 55%, 0.25)`})}return e}class Ai extends Lo{constructor(e={}){super({handleEvent:t=>this._handleEvent(t)}),this.snapDistance_=e.snapDistance||25,this._sources=e.sources?Array.isArray(e.sources)?e.sources:[e.sources]:null,this._phase="select",this._selectedFeature=null,this._selectedSource=null,this._selectedEdge=null,this._dividedFeatures=null,this._overlaySource=new G({useSpatialIndex:!1}),this._overlayLayer=new D({source:this._overlaySource,displayInLayerSwitcher:!1,style:Pi}),this._edgeSource=new G({useSpatialIndex:!1}),this._edgeLayer=new D({source:this._edgeSource,displayInLayerSwitcher:!1,style:Mi})}setMap(e){this.getMap()&&(this.getMap().removeLayer(this._overlayLayer),this.getMap().removeLayer(this._edgeLayer)),super.setMap(e),e&&(this._overlayLayer.setMap(e),this._edgeLayer.setMap(e))}setActive(e){super.setActive(e),e||this._reset()}_getSources(){if(this._sources)return this._sources;if(!this.getMap())return[];const e=[],t=r=>{r.forEach(o=>{o.getVisible()&&(o.getSource&&o.getSource()instanceof G?e.push(o.getSource()):o.getLayers&&t(o.getLayers()))})};return t(this.getMap().getLayers()),e}_handleEvent(e){if(!this.getActive())return!0;if(e.type==="keydown"&&e.originalEvent?.key==="Escape")return this._phase==="form"?this.cancelDivide():this._reset(),!1;if(this._phase==="select"){if(e.type==="pointermove")return this._onSelectMove(e);if(e.type==="singleclick")return this._onSelectClick(e)}if(this._phase==="edge"){if(e.type==="pointermove")return this._onEdgeMove(e);if(e.type==="singleclick")return this._onEdgeClick(e)}if(this._phase==="pick"){if(e.type==="pointermove")return this._onPickMove(e);if(e.type==="singleclick")return this._onPickClick(e)}return!0}_onSelectMove(e){const t=this.getMap();if(!t)return!0;this._overlaySource.clear();const r=this._closestPolygon(e);if(r){const o=r.feature.clone();this._overlaySource.addFeature(o),t.getTargetElement().style.cursor="pointer"}else t.getTargetElement().style.cursor="";return!0}_onSelectClick(e){const t=this._closestPolygon(e);if(!t)return!0;this._selectedFeature=t.feature,this._selectedSource=t.source,this._overlaySource.clear();const r=t.feature.clone();return r.set("_permanent",!0),this._overlaySource.addFeature(r),this._phase="edge",F("Click the edge to divide along.","info",3e3),!1}_closestPolygon(e){let t=null,r=this.snapDistance_+1;for(const o of this._getSources()){const a=o.getClosestFeatureToCoordinate(e.coordinate);if(!a)continue;const i=a.getGeometry();if(!i)continue;const s=i.getType();if(s!=="Polygon"&&s!=="MultiPolygon")continue;const l=i.getClosestPoint(e.coordinate),d=new de([e.coordinate,l]).getLength()/e.frameState.viewState.resolution;d<r&&(r=d,t={feature:a,source:o})}return t}_onEdgeMove(e){const t=this.getMap();if(!t)return!0;this._edgeSource.clear();const r=this._closestEdgeSegment(this._selectedFeature,e);if(r){const o=new fe(new de([r.segStart,r.segEnd]));this._edgeSource.addFeature(o),t.getTargetElement().style.cursor="crosshair"}else t.getTargetElement().style.cursor="";return!0}_onEdgeClick(e){const t=this._closestEdgeSegment(this._selectedFeature,e);if(!t)return!0;this._selectedEdge=[t.segStart,t.segEnd],this._edgeSource.clear(),this._phase="form";const o=this._selectedFeature.getGeometry().getExtent(),a=[(o[0]+o[2])/2,(o[1]+o[3])/2];return this.dispatchEvent({type:"divideform",feature:this._selectedFeature,source:this._selectedSource,coordinate:a}),!1}_closestEdgeSegment(e,t){const r=e.getGeometry();let o;if(r.getType()==="Polygon")o=r.getCoordinates()[0];else if(r.getType()==="MultiPolygon")o=r.getCoordinates()[0][0];else return null;const a=t.frameState.viewState.resolution;let i=1/0,s=null;const l=o.length-1;for(let c=0;c<l;c++){const d=o[c],u=o[c+1],p=u[0]-d[0],f=u[1]-d[1],h=p*p+f*f;if(h<1e-20)continue;let g=((t.coordinate[0]-d[0])*p+(t.coordinate[1]-d[1])*f)/h;g=Math.max(0,Math.min(1,g));const m=d[0]+g*p,y=d[1]+g*f,w=Math.sqrt((t.coordinate[0]-m)**2+(t.coordinate[1]-y)**2)/a;w<i&&(i=w,s={segStart:d,segEnd:u})}return i<=this.snapDistance_?s:null}performDivide(e){if(this._phase!=="form"||!this._selectedFeature)return;const t=this._selectedFeature,r=this._selectedSource,o=t.getGeometry();let a;o.getType()==="Polygon"?a=o.getCoordinates():o.getType()==="MultiPolygon"&&(a=o.getCoordinates()[0]);const i=Ii(a,e,this._selectedEdge);if(!i.pieces){F(i.error||"Division failed.","error",5e3),this._reset();return}const s=Ci(e),l=i.pieces.map((f,h)=>{const g=t.clone();return g.setGeometry(new ft(f)),g.setStyle(new M({stroke:new P({color:s[h].stroke,width:2.5}),fill:new C({color:s[h].fill})})),g}),c={type:"beforedivide",original:t,features:l};this.dispatchEvent(c),r.dispatchEvent({...c});const d=this.getMap();d?.dispatchEvent("undoblockstart"),r.removeFeature(t);for(const f of l)r.addFeature(f);d?.dispatchEvent("undoblockend");const u={type:"afterdivide",original:t,features:l};this.dispatchEvent(u),r.dispatchEvent({...u}),t.get("_layerType")==="parcel"?(this._dividedFeatures=l,this._phase="pick",F("Click the polygon that should keep the original identifier.","info",5e3),this.dispatchEvent({type:"dividedparcel",features:l,originalProps:t.getProperties(),source:r})):(F(`Polygon divided into ${e} equal pieces.`,"success"),this._reset())}_onPickMove(e){const t=this.getMap();if(!t)return!0;this._overlaySource.clear();const r=this._closestDividedPiece(e);if(r){const o=r.clone();this._overlaySource.addFeature(o),t.getTargetElement().style.cursor="pointer"}else t.getTargetElement().style.cursor="";return!0}_onPickClick(e){const t=this._closestDividedPiece(e);return t?(this.dispatchEvent({type:"dividepick",picked:t,features:this._dividedFeatures}),this._reset(),!1):!0}_closestDividedPiece(e){if(!this._dividedFeatures)return null;let t=null,r=this.snapDistance_+1;for(const o of this._dividedFeatures){const a=o.getGeometry();if(!a)continue;const i=a.getClosestPoint(e.coordinate),l=new de([e.coordinate,i]).getLength()/e.frameState.viewState.resolution;l<r&&(r=l,t=o)}return t}cancelDivide(){this.dispatchEvent({type:"dividecancel"}),this._reset()}_reset(){this._phase="select",this._selectedFeature=null,this._selectedSource=null,this._selectedEdge=null,this._dividedFeatures=null,this._overlaySource.clear(),this._edgeSource.clear();const e=this.getMap();e&&(e.getTargetElement().style.cursor="")}}const Et=[0,-10],Fi={"ol-selection":"bi-cursor","ol-drawpoint":"bi-geo-alt","ol-drawline":"bi-vector-pen","ol-drawpolygon":"bi-pentagon","ol-drawregular":"bi-square","ol-drawhole":"bi-record-circle","ol-transform":"bi-arrows-move","ol-offset":"bi-arrows-angle-expand","ol-delete":"bi-trash3","ol-info":"bi-info-circle","ol-split":"bi-signpost-split","ol-split-line":"bi-slash-lg","ol-split-polygon":"bi-scissors","ol-split-divide":"bi-grid-3x3-gap","ol-merge":"bi-union","ol-undo":"bi-arrow-counterclockwise","ol-redo":"bi-arrow-clockwise","ol-save":"bi-floppy","ol-snap-toggle":"bi-magnet"},Oi=[{label:"Draw",controls:["ol-drawpoint","ol-drawline","ol-drawpolygon","ol-drawregular","ol-drawhole"]},{label:"Edit",controls:["ol-selection","ol-transform"]},{label:"Shape",controls:["ol-split","ol-merge","ol-offset"]},{label:"File",controls:["ol-editbar-actions"]}];class Di{constructor(e,t={}){this.options=t,this.markerSource=new G,this.clickCallbacks=[],this.categoryEmojis={default:{emoji:"📍",label:"Default"},water:{emoji:"💧",label:"Water Point"},school:{emoji:"🏫",label:"School"},health:{emoji:"🏥",label:"Health Facility"},market:{emoji:"🏪",label:"Market"},other:{emoji:"📌",label:"Other"}},this.getEmoji=s=>{const l=this.categoryEmojis[s];return l?l.emoji:"📍"},this.getCategoryOptionsHtml=()=>Object.entries(this.categoryEmojis).map(([s,{emoji:l,label:c}])=>`<option value="${s}">${l} ${c}</option>`).join(`
|
||
`),this.createEmojiStyle=(s,l=24)=>new M({text:new Ke({text:s,font:`${l}px sans-serif`,textBaseline:"bottom",textAlign:"center",offsetY:-5})}),this.defaultStyle=this.createEmojiStyle("📍",32),this.selectedStyle=this.createEmojiStyle("📍",42),this.categoryStyles={};for(const[s,{emoji:l}]of Object.entries(this.categoryEmojis))this.categoryStyles[s]=this.createEmojiStyle(l,32);const r=this.createBaseLayers(t.basemap||"topo");this.markersLayer=new D({title:"Markers",source:this.markerSource,style:s=>this.getFeatureStyle(s),visible:!1}),this.overlayGroup=new Me({title:"Overlays"}),this.map=new Bo({target:e,layers:[r,this.markersLayer,this.overlayGroup],view:new Ur({center:ee(t.center||[0,0]),zoom:t.zoom||2,minZoom:t.minZoom||2,maxZoom:t.maxZoom||19})});const o=new Qr({collapsed:!0,mouseover:!0,extent:!0,trash:!1,oninfo:null});this.map.addControl(o),queueMicrotask(()=>{const s=o.element?.querySelector(":scope > button");if(s){const l="/".replace(/\/?$/,"/");s.style.backgroundImage=`url('${l}app-icons/luspa-72x72.png')`}});let a=!1;o.on("drawlist",s=>{this._decorateLayerListItem(s.layer,s.li),a||(a=!0,queueMicrotask(()=>{a=!1,this._refreshLayerSwitcherChrome(o)}))}),this.map.getLayers().on("change",()=>{this._refreshLayerSwitcherChrome(o)}),this._wireLayerSwitcherVisibilityHooks(o),this._createAddLayerDialog(),this._createLegendPanel(),this.scaleBar=new qr({bar:!0,steps:4,text:!0,minWidth:140}),this.map.addControl(this.scaleBar),this._initGpsRendering(),this._createLocationControl(),this._createBaseMapPicker();const i=new ea({placeholder:"Search location...",typing:300,minLength:3,maxItems:10,collapsed:!0});this.map.addControl(i),i.on("select",s=>{const l=s.search;if(l){const c=parseFloat(l.lon),d=parseFloat(l.lat),u=[c,d],p=ee(u);this.navigateTo(c,d,14);const f={coordinate:p,lonLat:u,name:l.display_name||l.name||"Unknown",searchResult:l};this.searchSelectCallbacks.forEach(h=>h(f))}}),this.searchNominatim=i,this.searchSelectCallbacks=[],this.selectedFeature=null,this.createPopup(),this.createInfoPopup(),this.createAddLocationPopup(),this.createParcelEditPopup(),this.createDrawnPolygonPopup(),this.createMergePopup(),this.createDividePopup(),this.dblClickCallbacks=[],this.editBar=null,this.drawingsSource=null,this.drawingsLayer=null,this.touchCursor=null,this._editBarActive=!1}initEditBar(){this.drawingsSource=new G,this.drawingsLayer=new D({title:"sketches",source:this.drawingsSource,style:new M({stroke:new P({color:"#f59e0b",width:2.5}),fill:new C({color:"rgba(245,158,11,0.15)"}),image:new he({radius:6,fill:new C({color:"#f59e0b"}),stroke:new P({color:"#fff",width:1.5})})})}),this._drawingsGroup=new Me({title:"Drawings",layers:[this.drawingsLayer]});const e=this.map.getLayers(),t=e.getArray().indexOf(this.overlayGroup);e.insertAt(t>=0?t:e.getLength(),this._drawingsGroup),this._selectInteraction=new zr({condition:Wr,toggleCondition:Hr,filter:(f,h)=>!!h,layers:f=>f instanceof D&&f.get("selectable")!==!1}),this._selectInteraction.setActive(!1),this.map.addInteraction(this._selectInteraction),this._dragBoxSelect=new Vr({condition:Kr}),this._dragBoxSelect.setActive(!1),this.map.addInteraction(this._dragBoxSelect),this._dragBoxSelect.on("boxend",()=>{const f=this._dragBoxSelect.getGeometry().getExtent(),h=this._selectInteraction.getFeatures();this.map.getLayers().forEach(g=>this._boxAddFromLayer(g,f,h)),this._refreshVertexOverlay?.()}),this._selectInteraction.on("change:active",()=>{this._dragBoxSelect.setActive(this._selectInteraction.getActive())}),this._modifyInteraction=new ta({features:this._selectInteraction.getFeatures()}),this._modifyInteraction.setActive(!1),this._modifyInteraction.on("modifyend",f=>{if(!this._featureModifiedCallbacks?.length)return;const h=f.features?.getArray?.()||[];for(const g of h)for(const m of this._featureModifiedCallbacks)try{m(g)}catch(y){console.warn("[MapView] onFeatureModified callback failed:",y)}}),this._undoRedo=new oa,this.map.addInteraction(this._undoRedo),this.editBar=new uo({source:this.drawingsSource,interactions:{Select:this._selectInteraction,ModifySelect:this._modifyInteraction,DrawPoint:!0,DrawLine:!0,DrawPolygon:!0,DrawRegular:!0,DrawHole:!0,Delete:!0,Info:!0,Transform:!0,Split:!1}}),this.map.addControl(this.editBar);const r=this.editBar.getInteraction("DrawPoint");r&&r.on("drawend",f=>{const h=f.feature?.getGeometry?.()?.getCoordinates?.();if(!h)return;this._discardPendingLocationSketch(),this._pendingLocationSketch=f.feature;const g=this._undoRedo,m=g?._record;g&&(g._record=!1),setTimeout(()=>{g&&(g._record=m)},0);for(const y of this._drawnPointCallbacks||[])try{y(h)}catch(w){console.warn("[MapView] onDrawnPoint callback failed:",w)}}),this._setupVertexOverlay();const o=new Uo({group:!0,className:"ol-editbar-actions",controls:[new lt({html:'<i class="bi bi-arrow-counterclockwise"></i>',className:"ol-undo",title:"Undo",handleClick:()=>{this._undoRedo.hasUndo()&&this._undoRedo.undo()}}),new lt({html:'<i class="bi bi-arrow-clockwise"></i>',className:"ol-redo",title:"Redo",handleClick:()=>{this._undoRedo.hasRedo()&&this._undoRedo.redo()}}),new lt({html:'<i class="bi bi-floppy"></i>',className:"ol-save",title:"Save drawings",handleClick:()=>{this.dispatchEditEvent("save")}})]});this.editBar.addControl(o),this._lineSplitInteraction=new na,this._polygonSplitInteraction=new hi,this.map.addInteraction(this._lineSplitInteraction),this.map.addInteraction(this._polygonSplitInteraction),this._lineSplitInteraction.setActive(!1),this._polygonSplitInteraction.setActive(!1),this._polygonSplitInteraction.on("splitpick",f=>{const h=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"];for(const g of f.features)if(g!==f.picked)for(const m of h)g.get(m)!==void 0&&g.set(m,"")}),this._polygonDivideInteraction=new Ai,this.map.addInteraction(this._polygonDivideInteraction),this._polygonDivideInteraction.setActive(!1);const a=new Se({html:'<i class="bi bi-slash-lg"></i>',className:"ol-split-line",title:"Split Lines",name:"SplitLine",interaction:this._lineSplitInteraction,autoActivate:!0}),i=new Se({html:'<i class="bi bi-scissors"></i>',className:"ol-split-polygon",title:"Split Polygons",name:"SplitPolygon",interaction:this._polygonSplitInteraction}),s=new Se({html:'<i class="bi bi-grid-3x3-gap"></i>',className:"ol-split-divide",title:"Divide Polygon",name:"DividePolygon",interaction:this._polygonDivideInteraction}),l=new Uo({toggleOne:!0,autoDeactivate:!0,controls:[a,i,s]}),c=new Se({className:"ol-split",title:"Split",name:"Split",bar:l,onToggle:f=>{f||(this._lineSplitInteraction.setActive(!1),this._polygonSplitInteraction.setActive(!1),this._polygonDivideInteraction.setActive(!1))}});this.editBar.addControl(c),this._polygonDivideInteraction.on("divideform",f=>{this.showDividePopup(f.feature,f.source,f.coordinate)}),this._polygonDivideInteraction.on("dividecancel",()=>{this.hideDividePopup()}),this._polygonDivideInteraction.on("dividepick",f=>{const h=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"];for(const g of f.features)if(g!==f.picked)for(const m of h)g.get(m)!==void 0&&g.set(m,"")}),this._polygonMergeInteraction=new Li,this.map.addInteraction(this._polygonMergeInteraction),this._polygonMergeInteraction.setActive(!1);const d=new Se({html:'<i class="bi bi-union"></i>',className:"ol-merge",title:"Merge Polygons",name:"Merge",interaction:this._polygonMergeInteraction});this.editBar.addControl(d),this._polygonMergeInteraction.on("mergedparcel",f=>{this.showMergeIdentifierPopup(f.merged,f.propsA,f.propsB,f.coordinate)}),this._snapGuidesEnabled=localStorage.getItem("snap-guides-enabled")==="1",this._snapGuides=new ra({pixelTolerance:10,vectorClass:Xr}),this.map.addInteraction(this._snapGuides);const u=["DrawPoint","DrawLine","DrawPolygon","DrawHole","DrawRegular"];for(const f of u){const h=this.editBar.getInteraction(f);h&&h.on("change:active",()=>{h.getActive()&&this._snapGuides.setDrawInteraction(h)})}this._modifyInteraction&&this._snapGuides.setModifyInteraction(this._modifyInteraction);const p=new lt({html:'<i class="bi bi-magnet"></i>',className:"ol-snap-toggle"+(this._snapGuidesEnabled?" ol-active":""),title:"Toggle Snap Guides",handleClick:()=>{this._snapGuidesEnabled=!this._snapGuidesEnabled,localStorage.setItem("snap-guides-enabled",this._snapGuidesEnabled?"1":"0"),p.element.classList.toggle("ol-active",this._snapGuidesEnabled),this._snapGuides&&this._snapGuides.setActive(this._snapGuidesEnabled&&this._editBarActive),console.log("[MapView] Snap guides:",this._snapGuidesEnabled?"ON":"OFF")}});this._snapToggleBtn=p,o.addControl(p),this._buildToolbarRail(),this.setEditMode(!1),this._drawingsGroup.on("change:visible",()=>{const f=this._drawingsGroup.getVisible();this.setEditMode(f)}),this._isTouchOnlyDevice()?(this.touchCursor=new qo({className:"ol-editbar-cursor"}),this.map.addInteraction(this.touchCursor),this.touchCursor.setActive(!1),console.log("[MapView] Touch-only device detected — TouchCursor added")):console.log("[MapView] Fine pointer available — TouchCursor skipped"),this._installPointerCapabilityWatcher(),this.drawingsSource.on("addfeature",f=>{const h=f.feature,g=h.getGeometry();if(!g||g.getType()!=="Polygon")return;const m=g.getInteriorPoint().getCoordinates();this.showDrawnPolygonPopup(h,m)}),this._editKeyHandler=f=>{if(f.key!=="Delete"&&f.key!=="Backspace"||!this._editBarActive)return;const h=f.target,g=h&&h.tagName;h?.isContentEditable||g==="INPUT"||g==="TEXTAREA"||g==="SELECT"||this._deleteSelectedFeatures()&&f.preventDefault()},document.addEventListener("keydown",this._editKeyHandler),console.log("[MapView] EditBar initialised with Drawings group, UndoRedo and SnapGuides (default:",this._snapGuidesEnabled?"ON":"OFF",")")}_deleteSelectedFeatures(){if(!this._editBarActive)return!1;const e=this.editBar?.getInteraction("Delete"),t=this._selectInteraction?.getFeatures();return!e||!t||t.getLength()===0?!1:(e.delete(t),t.clear(),this._refreshVertexOverlay?.(),!0)}_boxAddFromLayer(e,t,r){if(!e)return;if(typeof e.getLayers=="function"){e.getLayers().forEach(a=>this._boxAddFromLayer(a,t,r));return}if(!(e instanceof D)||e.getVisible&&!e.getVisible()||e===this._vertexOverlayLayer||e===this._gpsTrailLayer||e===this._gpsPositionLayer||e.get("selectable")===!1||e.get("displayInLayerSwitcher")===!1)return;const o=e.getSource&&e.getSource();!o||typeof o.forEachFeatureIntersectingExtent!="function"||o.forEachFeatureIntersectingExtent(t,a=>{r.getArray().includes(a)||r.push(a)})}dispatchEditEvent(e){if(!this._editEventListeners)return;const t=this._editEventListeners[e];t&&t.forEach(r=>r())}onEditEvent(e,t){this._editEventListeners||(this._editEventListeners={}),this._editEventListeners[e]||(this._editEventListeners[e]=[]),this._editEventListeners[e].push(t)}setEditMode(e){const t=this._editBarActive;if(this._editBarActive=!!e,this._editBarActive&&!t&&this._undoRedo)try{this._undoRedo._watchSources(),this._undoRedo.clear()}catch(r){console.warn("[MapView] UndoRedo re-watch/clear failed:",r)}this.editBar&&(this.editBar.setVisible(this._editBarActive),this._editBarActive||this.editBar.deactivateControls()),this._selectInteraction&&(this._editBarActive||this._selectInteraction.getFeatures().clear(),this._selectInteraction.setActive(this._editBarActive)),this._modifyInteraction&&this._modifyInteraction.setActive(this._editBarActive),this._snapGuides&&this._snapGuides.setActive(this._snapGuidesEnabled&&this._editBarActive),this.touchCursor&&this.touchCursor.setActive(this._editBarActive),this._editBarActive||this._discardPendingLocationSketch(),!this._editBarActive&&this._vertexOverlaySource&&this._withoutUndoRecording(()=>this._silentClearSource(this._vertexOverlaySource)),console.log("[MapView] Edit mode:",this._editBarActive?"ON":"OFF")}isEditMode(){return this._editBarActive}_isTouchOnlyDevice(){if(!("ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0))return!1;if(typeof window.matchMedia!="function")return!0;const t=window.matchMedia("(any-pointer: fine)").matches,r=window.matchMedia("(any-hover: hover)").matches;return!t&&!r}_refreshTouchCursor(){const e=this._isTouchOnlyDevice();e&&!this.touchCursor?(this.touchCursor=new qo({className:"ol-editbar-cursor"}),this.map.addInteraction(this.touchCursor),this.touchCursor.setActive(this._editBarActive),console.log("[MapView] Pointer change → TouchCursor added")):!e&&this.touchCursor&&(this.map.removeInteraction(this.touchCursor),this.touchCursor=null,console.log("[MapView] Pointer change → TouchCursor removed (fine pointer present)"))}_installPointerCapabilityWatcher(){if(typeof window.matchMedia!="function"||this._pointerMediaQueries)return;const e=["(any-pointer: fine)","(any-hover: hover)"].map(r=>window.matchMedia(r)),t=()=>this._refreshTouchCursor();for(const r of e)typeof r.addEventListener=="function"?r.addEventListener("change",t):typeof r.addListener=="function"&&r.addListener(t);this._pointerMediaQueries=e,this._pointerMediaListener=t}_buildToolbarRail(){const e=this.editBar?.element;if(e)try{this._applyToolbarRail(e)}catch(t){console.warn("[MapView] Could not build the toolbar rail; falling back to the default bar:",t),e.classList.remove("ol-left","ol-editbar-rail")}}_applyToolbarRail(e){e.classList.add("ol-left","ol-editbar-rail");for(const[o,a]of Object.entries(Fi))for(const i of e.querySelectorAll(`.${o}`)){const s=i.matches("button")?i:i.querySelector(":scope > button");s&&(s.innerHTML=`<span class="bi ${a}" aria-hidden="true"></span>`)}const t=document.createDocumentFragment();for(const o of Oi){const a=o.controls.map(s=>e.querySelector(`:scope > .${s}`)).filter(Boolean);if(!a.length)continue;const i=document.createElement("div");i.className="ol-rail-label",i.textContent=o.label,i.setAttribute("aria-hidden","true"),t.appendChild(i);for(const s of a)t.appendChild(s)}e.appendChild(t);const r=document.createElement("div");r.className="ol-rail-grip",r.title="Drag to move the toolbar — double-click to reset",r.setAttribute("role","button"),r.setAttribute("aria-label","Move the drawing toolbar"),e.insertBefore(r,e.firstChild),this._makeToolbarDraggable(e,r)}_makeToolbarDraggable(e,t){const r="editbar-position",o=window.matchMedia("(max-width: 576px), (max-height: 560px)"),a=(g,m,y)=>Math.min(Math.max(g,m),y),i=(g,m)=>{const y=e.offsetParent||this.map.getTargetElement();if(!y)return;const w=Math.max(0,y.clientWidth-e.offsetWidth-4),b=Math.max(0,y.clientHeight-e.offsetHeight-4);e.style.left=`${a(g,4,w)}px`,e.style.top=`${a(m,4,b)}px`,e.style.transform="none",e.style.bottom="auto"},s=()=>{e.style.left="",e.style.top="",e.style.transform="",e.style.bottom=""},l=()=>{if(o.matches){s();return}try{const g=JSON.parse(localStorage.getItem(r)||"null");g&&Number.isFinite(g.left)&&Number.isFinite(g.top)&&i(g.left,g.top)}catch{}};let c=0,d=0,u=0,p=0;const f=g=>{i(u+(g.clientX-c),p+(g.clientY-d))},h=g=>{t.releasePointerCapture?.(g.pointerId),window.removeEventListener("pointermove",f),window.removeEventListener("pointerup",h),e.classList.remove("is-dragging");try{localStorage.setItem(r,JSON.stringify({left:parseFloat(e.style.left)||0,top:parseFloat(e.style.top)||0}))}catch{}};t.addEventListener("pointerdown",g=>{if(o.matches||g.button!==0)return;g.preventDefault();const m=e.getBoundingClientRect(),y=(e.offsetParent||this.map.getTargetElement()).getBoundingClientRect();c=g.clientX,d=g.clientY,u=m.left-y.left,p=m.top-y.top,e.classList.add("is-dragging"),t.setPointerCapture?.(g.pointerId),window.addEventListener("pointermove",f),window.addEventListener("pointerup",h)}),t.addEventListener("dblclick",()=>{try{localStorage.removeItem(r)}catch{}s()}),typeof o.addEventListener=="function"?o.addEventListener("change",l):typeof o.addListener=="function"&&o.addListener(l),l()}_setupVertexOverlay(){this._vertexOverlaySource=new G,this._vertexOverlayLayer=new D({title:"__vertex_highlight__",source:this._vertexOverlaySource,zIndex:990,style:new M({image:new he({radius:4,fill:new C({color:"rgba(14,165,233,0.85)"}),stroke:new P({color:"#fff",width:1.2})})})}),this._vertexOverlayLayer.set("displayInLayerSwitcher",!1),this.map.addLayer(this._vertexOverlayLayer),this._onSelectedFeatureGeomChange=()=>this._refreshVertexOverlay(),this._vertexTrackedFeatures=new Set,this._selectInteraction.on("select",()=>this._refreshVertexOverlay())}_withoutUndoRecording(e){const t=this._undoRedo;if(!t)return e();const r=t._record;t._record=!1;try{return e()}finally{t._record=r}}_silentClearSource(e){if(!e)return;const t=e.getFeatures();for(let r=t.length-1;r>=0;r--)e.removeFeature(t[r])}_refreshVertexOverlay(){this._vertexOverlaySource&&this._withoutUndoRecording(()=>{if(this._silentClearSource(this._vertexOverlaySource),this._vertexTrackedFeatures){for(const t of this._vertexTrackedFeatures)t.un("change",this._onSelectedFeatureGeomChange);this._vertexTrackedFeatures.clear()}if(!this._editBarActive||!this._selectInteraction)return;const e=this._selectInteraction.getFeatures().getArray();for(const t of e){const r=t.getGeometry();if(!r)continue;const o=r.getType();if(!["Polygon","MultiPolygon","LineString","MultiLineString"].includes(o))continue;const a=this._collectAllVertices(r);for(const i of a)this._vertexOverlaySource.addFeature(new fe(new yt(i)));t.on("change",this._onSelectedFeatureGeomChange),this._vertexTrackedFeatures.add(t)}})}_collectAllVertices(e){const t=[],r=s=>Array.isArray(s)&&typeof s[0]=="number",o=(s,l)=>{const c=l&&s.length>1?s.length-1:s.length;for(let d=0;d<c;d++)t.push(s[d])},a=e.getType(),i=e.getCoordinates();switch(a){case"Polygon":for(const l of i)o(l,!0);break;case"MultiPolygon":for(const l of i)for(const c of l)o(c,!0);break;case"LineString":o(i,!1);break;case"MultiLineString":for(const l of i)o(l,!1);break;default:const s=l=>{if(r(l))t.push(l);else if(Array.isArray(l))for(const c of l)s(c)};s(i)}return t}getDrawingsLayer(){return this.drawingsLayer}getDrawingsSource(){return this.drawingsSource}getEditBar(){return this.editBar}setScaleBarUnits(e){this.scaleBar&&this.scaleBar.setUnits(e==="imperial"?"imperial":"metric")}createPopup(){this.popupElement=document.createElement("div"),this.popupElement.className="map-popup",this.popupElement.style.cssText=`
|
||
position: absolute;
|
||
background: var(--card, #fff);
|
||
color: var(--card-foreground, #1e1a4b);
|
||
border-radius: 8px;
|
||
padding: 10px 14px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
|
||
font-family: var(--font-body, 'Exo', sans-serif);
|
||
font-size: 13px;
|
||
min-width: 150px;
|
||
max-width: 280px;
|
||
pointer-events: none;
|
||
z-index: 1000;
|
||
border: 1px solid var(--border, #1e1a4b1f);
|
||
`,this.popup=new Ee({element:this.popupElement,positioning:"bottom-center",offset:[0,-15],stopEvent:!1}),this.map.addOverlay(this.popup),this.setupHoverPopup()}setupHoverPopup(){let e=null;this.map.on("pointermove",t=>{if(t.dragging){this.hidePopup();return}const r=this.map.forEachFeatureAtPixel(t.pixel,o=>o.get("name")?o:null);r&&r!==e?(e=r,this.showPopup(r,t.coordinate)):!r&&e&&(e=null,this.hidePopup()),this.map.getTargetElement().style.cursor=r?"pointer":""}),this.map.getTargetElement().addEventListener("mouseleave",()=>{this.hidePopup(),e=null})}showPopup(e,t){const r=e.get("name")||"Unnamed",o=e.get("category")||"default",a=e.get("description"),i=e.get("lon"),s=e.get("lat");let c=`
|
||
<div style="font-weight: 600; font-size: 14px; margin-bottom: 6px;">
|
||
${this.getEmoji(o)} ${this.escapeHtml(r)}
|
||
</div>
|
||
`;const u={water:"#3b82f6",school:"#f59e0b",health:"#ef4444",market:"#8b5cf6",default:"#2d5016",other:"#6b7280"}[o]||"#6b7280";c+=`
|
||
<div style="margin-bottom: 6px;">
|
||
<span style="
|
||
background: ${u}20;
|
||
color: ${u};
|
||
padding: 2px 8px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
">${o}</span>
|
||
</div>
|
||
`,a&&(c+=`
|
||
<div style="color: var(--muted-foreground, #7a7a7a); font-size: 12px; margin-bottom: 6px; line-height: 1.4;">
|
||
${this.escapeHtml(a)}
|
||
</div>
|
||
`),i!==void 0&&s!==void 0&&(c+=`
|
||
<div style="color: var(--muted-foreground, #7a7a7a); font-size: 11px; font-family: monospace;">
|
||
${Number(i).toFixed(5)}, ${Number(s).toFixed(5)}
|
||
</div>
|
||
`),this.popupElement.innerHTML=c,this.popup.setPosition(t)}hidePopup(){this.popup.setPosition(void 0)}createInfoPopup(){this.infoPopupElement=document.createElement("div"),this.infoPopupElement.className="map-info-popup",this.infoPopupElement.style.cssText=`
|
||
position: absolute;
|
||
background: var(--card, #fff);
|
||
color: var(--card-foreground, #1e1a4b);
|
||
border-radius: 10px;
|
||
padding: 0;
|
||
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
||
font-family: var(--font-body, 'Exo', sans-serif);
|
||
font-size: 13px;
|
||
min-width: 220px;
|
||
max-width: 320px;
|
||
max-height: 70vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
z-index: 1001;
|
||
border: 1px solid var(--border, #1e1a4b1f);
|
||
overflow: hidden;
|
||
`,this.infoPopup=new Ee({element:this.infoPopupElement,positioning:"bottom-center",offset:Et.slice(),stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.infoPopup)}showInfoPopup(e,t,r={}){const{title:o="Feature Info",color:a="#e11d48"}=r,i=e.getProperties(),s=e.getGeometry(),l=s.getType(),c=["geometry","_layerType"];let d="";for(const[p,f]of Object.entries(i))c.includes(p)||f===void 0||f===null||(d+=`
|
||
<tr>
|
||
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">${this.escapeHtml(p)}</td>
|
||
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${this.escapeHtml(String(f))}</td>
|
||
</tr>
|
||
`);if(l==="Polygon"||l==="MultiPolygon"){const p=dt(s,{projection:"EPSG:3857"}),f=oi(p);d+=`
|
||
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
|
||
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">area</td>
|
||
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${f}</td>
|
||
</tr>
|
||
`}else if(l==="LineString"||l==="MultiLineString"){const p=Pt(s,{projection:"EPSG:3857"}),f=ti(p);d+=`
|
||
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
|
||
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">length</td>
|
||
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${f}</td>
|
||
</tr>
|
||
`}else if(l==="Point"){const p=xe(s.getCoordinates()),f=p[0].toFixed(6),h=p[1].toFixed(6);d+=`
|
||
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
|
||
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">longitude</td>
|
||
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${f}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">latitude</td>
|
||
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${h}</td>
|
||
</tr>
|
||
`}const u=`
|
||
<div style="background:${a};color:#fff;padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;border-radius:10px 10px 0 0;">
|
||
<span>${this.escapeHtml(o)}</span>
|
||
<button id="info-popup-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">×</button>
|
||
</div>
|
||
<div style="padding:8px 4px;overflow-y:auto;flex:1 1 auto;min-height:0;">
|
||
<table style="width:100%;border-collapse:collapse;font-size:13px;">
|
||
${d}
|
||
</table>
|
||
</div>
|
||
`;this.infoPopupElement.innerHTML=u,this.infoPopup.setPosition(t),this.infoPopupElement.querySelector("#info-popup-close").addEventListener("click",()=>{this.hideInfoPopup()})}hideInfoPopup(){this.infoPopup.setPosition(void 0)}_collectIntersectionRows(e,t,r){const o=[];if(e.length>0&&o.push({label:"Parcels",value:String(e.length),color:"#0ea5e9"}),t.length>0){const a=t.map(i=>i.get("colzonename")||i.get("zone_name")||i.get("name")||"unnamed");o.push({label:"Zones",value:String(t.length),color:"#7c3aed"}),o.push({label:"Zone Names",value:a.map(i=>this.escapeHtml(i)).join(", "),color:"#7c3aed"})}for(const[a,i]of Object.entries(r))o.push({label:this.escapeHtml(a),value:`${i.length} feature(s)`});return o.length===0&&o.push({label:"",value:"No intersecting features found",empty:!0}),o}_makePopupDraggable(){const e=this.infoPopupElement.querySelector("#info-popup-header");if(!e)return;this.infoPopup.setOffset(Et.slice());let t=0,r=0,o=Et.slice();const a=s=>{this.infoPopup.setOffset([o[0]+(s.clientX-t),o[1]+(s.clientY-r)])},i=s=>{e.releasePointerCapture?.(s.pointerId),e.removeEventListener("pointermove",a),e.removeEventListener("pointerup",i),e.removeEventListener("pointercancel",i),e.style.cursor="grab"};e.addEventListener("pointerdown",s=>{s.target.closest("#info-popup-close")||s.button!==0||(s.preventDefault(),t=s.clientX,r=s.clientY,o=(this.infoPopup.getOffset()||Et).slice(),e.style.cursor="grabbing",e.setPointerCapture?.(s.pointerId),e.addEventListener("pointermove",a),e.addEventListener("pointerup",i),e.addEventListener("pointercancel",i))})}_buildAnalysisPopupHtml(e,t,r){let o="";for(const a of r){if(a.empty){o+=`
|
||
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
|
||
<td colspan="2" style="padding:8px;color:#999;text-align:center;font-style:italic;">${a.value}</td>
|
||
</tr>`;continue}const i=a.color||"var(--muted-foreground, #7a7a7a)",s=a._first?"":"border-top:1px solid var(--border, #1e1a4b1f);";o+=`
|
||
<tr style="${s}">
|
||
<td style="padding:4px 8px;font-weight:600;color:${i};white-space:nowrap;">${a.label}</td>
|
||
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${a.value}</td>
|
||
</tr>`}return`
|
||
<div id="info-popup-header" style="background:var(--brand-navy, #1e1a4b);color:#fff;padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;border-radius:10px 10px 0 0;cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none;">
|
||
<span>${e} ${t}</span>
|
||
<button id="info-popup-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">×</button>
|
||
</div>
|
||
<div style="padding:8px 4px;overflow-y:auto;flex:1 1 auto;min-height:0;">
|
||
<table style="width:100%;border-collapse:collapse;font-size:13px;">
|
||
${o}
|
||
</table>
|
||
</div>
|
||
<div style="padding:2px 8px 8px;display:flex;justify-content:flex-end;gap:6px;flex-shrink:0;border-top:1px solid var(--border, #1e1a4b1f);">
|
||
<button id="info-popup-analyse"
|
||
style="background:#fff;color:var(--brand-navy,#1e1a4b);border:1px solid var(--brand-navy,#1e1a4b);border-radius:6px;padding:5px 12px;font-size:12px;cursor:pointer;font-family:inherit;">
|
||
🧮 Analyse
|
||
</button>
|
||
<button id="info-popup-export-gis"
|
||
style="background:var(--brand-navy,#1e1a4b);color:#fff;border:none;border-radius:6px;padding:5px 12px;font-size:12px;cursor:pointer;font-family:inherit;">
|
||
⬇️ Export
|
||
</button>
|
||
</div>`}_showAnalysisPopup(e,t,r,o,a=null){this.infoPopupElement.innerHTML=this._buildAnalysisPopupHtml(e,t,r),this.infoPopup.setPosition(o),this.infoPopupElement.querySelector("#info-popup-close").addEventListener("click",()=>{this.hideInfoPopup()}),this._makePopupDraggable();const i=r.filter(c=>!c.empty).map(c=>({label:c.label,value:String(c.value).replace(/<[^>]*>/g,"")})),s=this.infoPopupElement.querySelector("#info-popup-analyse");if(s){const c=a?a.parcelFeatures.length+a.zoneFeatures.length+Object.values(a.otherByLayer).reduce((d,u)=>d+u.length,0):0;!a||c===0?(s.disabled=!0,s.style.opacity="0.5",s.style.cursor="not-allowed",s.title="No intersecting features to analyse"):s.addEventListener("click",()=>{window.dispatchEvent(new CustomEvent("lupmis:analyse-features",{detail:{title:t,...a}}))})}const l=this.infoPopupElement.querySelector("#info-popup-export-gis");if(l){const c=a?a.parcelFeatures.length+a.zoneFeatures.length+Object.values(a.otherByLayer).reduce((d,u)=>d+u.length,0):0;!a||c===0?(l.disabled=!0,l.style.opacity="0.5",l.style.cursor="not-allowed",l.title="No intersecting features to export"):l.addEventListener("click",()=>{window.dispatchEvent(new CustomEvent("lupmis:export-gis",{detail:{title:t,pdfRows:i,...a}}))})}}showCircleIntersectionPopup(e,t){const r=e.getGeometry();if(!r||typeof r.getCenter!="function")return;const o=Yr(r,64),a=o.getExtent(),i=r.getRadius(),s=[],l=[],c={},d=y=>{const w=y.getGeometry();if(!w)return!1;const b=w.getExtent();return b[2]<a[0]||b[0]>a[2]||b[3]<a[1]||b[1]>a[3]?!1:o.intersectsExtent(b)&&this._geometriesIntersect(o,w)},u=(y,w)=>{y.getLayers().forEach(b=>{if(b instanceof Me)u(b,b.get("title")||w);else if(b instanceof D&&b.getVisible()){const S=b.get("title")||w||"Unknown",L=b.getSource();if(!L)return;const E=L.getFeaturesInExtent(a);for(const x of E){const I=x.get("_layerType");I==="measure_circle"||I==="measure_circle_radius"||d(x)&&(I==="parcel"?s.push(x):I==="collector_zone"?l.push(x):(c[S]||(c[S]=[]),c[S].push(x)))}}})};u(this.overlayGroup,"Overlays");const p=go(i,r.getCenter()),f=ut(p),h=Math.PI*p*p,g=We(h),m=[{label:"Radius",value:f,_first:!0},{label:"Area",value:g},...this._collectIntersectionRows(s,l,c)];this._showAnalysisPopup("⭕","Circle Analysis",m,t,{kind:"circle",clipGeometry:o,parcelFeatures:s,zoneFeatures:l,otherByLayer:c})}showAreaIntersectionPopup(e,t){const r=e.getGeometry();if(!r)return;const o=r.getExtent(),a=dt(r,{projection:"EPSG:3857"}),i=We(a),s=Pt(r,{projection:"EPSG:3857"}),l=ut(s),c=[],d=[],u={},p=g=>{const m=g.getGeometry();if(!m)return!1;const y=m.getExtent();return y[2]<o[0]||y[0]>o[2]||y[3]<o[1]||y[1]>o[3]?!1:r.intersectsExtent(y)&&this._geometriesIntersect(r,m)},f=(g,m)=>{g.getLayers().forEach(y=>{if(y instanceof Me)f(y,y.get("title")||m);else if(y instanceof D&&y.getVisible()){const w=y.get("title")||m||"Unknown",b=y.getSource();if(!b)return;const S=b.getFeaturesInExtent(o);for(const L of S){const E=L.get("_layerType");E==="measure_area"||E==="measure_circle"||E==="measure_circle_radius"||p(L)&&(E==="parcel"?c.push(L):E==="collector_zone"?d.push(L):(u[w]||(u[w]=[]),u[w].push(L)))}}})};f(this.overlayGroup,"Overlays");const h=[{label:"Area",value:i,_first:!0},{label:"Perimeter",value:l},...this._collectIntersectionRows(c,d,u)];this._showAnalysisPopup("📐","Area Analysis",h,t,{kind:"area",clipGeometry:r,parcelFeatures:c,zoneFeatures:d,otherByLayer:u})}_geometriesIntersect(e,t){const r=t.getType();if(r==="Polygon"||r==="MultiPolygon"){const o=t.getFlatCoordinates(),a=t.getStride();for(let l=0;l<o.length;l+=a)if(e.intersectsCoordinate([o[l],o[l+1]]))return!0;const i=e.getFlatCoordinates(),s=e.getStride();for(let l=0;l<i.length;l+=s)if(t.intersectsCoordinate([i[l],i[l+1]]))return!0;return!1}if(r==="Point")return e.intersectsCoordinate(t.getCoordinates());if(r==="LineString"||r==="MultiLineString"){const o=t.getFlatCoordinates(),a=t.getStride();for(let i=0;i<o.length;i+=a)if(e.intersectsCoordinate([o[i],o[i+1]]))return!0;return!1}return!0}createParcelEditPopup(){this.parcelEditElement=document.createElement("div"),this.parcelEditElement.className="map-parcel-edit-popup",this.parcelEditElement.style.cssText=`
|
||
position: absolute;
|
||
background: var(--card, #fff);
|
||
color: var(--card-foreground, #1e1a4b);
|
||
border-radius: 10px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
font-family: var(--font-body, 'Exo', sans-serif);
|
||
font-size: 13px;
|
||
min-width: 280px;
|
||
max-width: 360px;
|
||
max-height: 420px;
|
||
z-index: 1002;
|
||
border: 2px solid var(--primary, #005eb8);
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
`,this.parcelEditPopup=new Ee({element:this.parcelEditElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.parcelEditPopup),this._parcelEditCallbacks=[],this._parcelEditFeature=null}showParcelEditPopup(e,t){this._parcelEditFeature=e;const r=e.getProperties(),o=["geometry","_layerType"];let a="";for(const[l,c]of Object.entries(r)){if(o.includes(l))continue;const d=c==null?"":String(c),u=this.escapeHtml(l),p=this.escapeHtml(d);a+=`
|
||
<div style="margin-bottom:8px;">
|
||
<label style="display:block;font-size:11px;font-weight:600;color:var(--muted-foreground, #7a7a7a);margin-bottom:2px;">${u}</label>
|
||
<input type="text" name="${u}" value="${p}"
|
||
style="width:100%;padding:6px 8px;border:1px solid var(--border, #1e1a4b1f);border-radius:4px;font-size:13px;color:var(--foreground, #1e1a4b);background:var(--muted, #f2f4f7);min-height:34px;"
|
||
/>
|
||
</div>
|
||
`}const i=`
|
||
<div style="background:var(--primary, #005eb8);color:#fff;padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
|
||
<span>✏️ Edit Parcel</span>
|
||
<button class="parcel-edit-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">×</button>
|
||
</div>
|
||
<form class="parcel-edit-form" style="padding:10px 12px;overflow-y:auto;flex:1;">
|
||
${a}
|
||
<div style="display:flex;gap:8px;margin-top:10px;">
|
||
<button type="submit" style="flex:1;padding:8px 12px;background:var(--primary, #005eb8);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
|
||
💾 Save
|
||
</button>
|
||
<button type="button" class="parcel-edit-cancel" style="flex:1;padding:8px 12px;background:var(--muted-foreground, #7a7a7a);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</form>
|
||
`;this.parcelEditElement.innerHTML=i,this.parcelEditPopup.setPosition(t),this.parcelEditElement.querySelector(".parcel-edit-close").addEventListener("click",()=>{this.hideParcelEditPopup()}),this.parcelEditElement.querySelector(".parcel-edit-cancel").addEventListener("click",()=>{this.hideParcelEditPopup()});const s=this.parcelEditElement.querySelector(".parcel-edit-form");s.addEventListener("submit",l=>{l.preventDefault();const c=new FormData(s),d={};for(const[u,p]of c.entries())d[u]=p;d._layerType="parcel";for(const[u,p]of Object.entries(d))this._parcelEditFeature.set(u,p);for(const u of this._parcelEditCallbacks)u(this._parcelEditFeature,d);this.hideParcelEditPopup()})}hideParcelEditPopup(){this.parcelEditPopup.setPosition(void 0),this._parcelEditFeature=null}onParcelEdit(e){this._parcelEditCallbacks.push(e)}createMergePopup(){this.mergePopupElement=document.createElement("div"),this.mergePopupElement.className="map-merge-popup",this.mergePopupElement.style.cssText=`
|
||
position: absolute;
|
||
background: var(--card, #fff);
|
||
color: var(--card-foreground, #1e1a4b);
|
||
border-radius: 10px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
font-family: var(--font-body, 'Exo', sans-serif);
|
||
font-size: 13px;
|
||
min-width: 280px;
|
||
max-width: 360px;
|
||
z-index: 1002;
|
||
border: 2px solid #10b981;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
`,this.mergePopup=new Ee({element:this.mergePopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.mergePopup)}showMergeIdentifierPopup(e,t,r,o){const a=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"],i=h=>{for(const g of a)if(h[g]!==void 0&&h[g]!==null&&String(h[g]).trim())return{field:g,value:String(h[g])};return{field:"id",value:"Unknown"}},s=i(t),l=i(r),c=`
|
||
<div style="background:#10b981;color:#fff;padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
|
||
<span>🔗 Merged Parcel — Choose Identifier</span>
|
||
<button class="merge-popup-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">×</button>
|
||
</div>
|
||
<div style="padding:12px;">
|
||
<p style="margin:0 0 10px;color:var(--muted-foreground, #7a7a7a);font-size:12px;">
|
||
Select which parcel's attributes the merged polygon should keep:
|
||
</p>
|
||
<label style="display:flex;align-items:center;padding:10px;border:2px solid var(--border, #1e1a4b1f);border-radius:8px;cursor:pointer;margin-bottom:8px;transition:border-color 0.15s;">
|
||
<input type="radio" name="merge-choice" value="A" checked
|
||
style="margin-right:10px;accent-color:#0ea5e9;width:16px;height:16px;" />
|
||
<div>
|
||
<div style="font-weight:600;color:#0ea5e9;">Parcel A</div>
|
||
<div style="font-size:12px;color:var(--muted-foreground, #7a7a7a);">${this.escapeHtml(s.field)}: ${this.escapeHtml(s.value)}</div>
|
||
</div>
|
||
</label>
|
||
<label style="display:flex;align-items:center;padding:10px;border:2px solid var(--border, #1e1a4b1f);border-radius:8px;cursor:pointer;margin-bottom:12px;transition:border-color 0.15s;">
|
||
<input type="radio" name="merge-choice" value="B"
|
||
style="margin-right:10px;accent-color:#f59e0b;width:16px;height:16px;" />
|
||
<div>
|
||
<div style="font-weight:600;color:#f59e0b;">Parcel B</div>
|
||
<div style="font-size:12px;color:var(--muted-foreground, #7a7a7a);">${this.escapeHtml(l.field)}: ${this.escapeHtml(l.value)}</div>
|
||
</div>
|
||
</label>
|
||
<div style="display:flex;gap:8px;">
|
||
<button class="merge-popup-confirm" style="flex:1;padding:8px 12px;background:#10b981;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
|
||
✅ Confirm
|
||
</button>
|
||
<button class="merge-popup-cancel" style="flex:1;padding:8px 12px;background:var(--muted-foreground, #7a7a7a);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;this.mergePopupElement.innerHTML=c,this.mergePopup.setPosition(o);const d=()=>{this.mergePopup.setPosition(void 0)};this.mergePopupElement.querySelector(".merge-popup-close").addEventListener("click",d),this.mergePopupElement.querySelector(".merge-popup-cancel").addEventListener("click",d),this.mergePopupElement.querySelector(".merge-popup-confirm").addEventListener("click",()=>{const g=this.mergePopupElement.querySelector('input[name="merge-choice"]:checked').value==="A"?t:r,m=["geometry"];for(const[y,w]of Object.entries(g))m.includes(y)||e.set(y,w);e.set("_layerType","parcel");for(const y of this._parcelEditCallbacks)y(e,g);d()});const u=this.mergePopupElement.querySelectorAll("label"),p=this.mergePopupElement.querySelectorAll('input[name="merge-choice"]'),f=()=>{u.forEach(h=>{const g=h.querySelector("input");h.style.borderColor=g.checked?g.value==="A"?"#0ea5e9":"#f59e0b":"var(--border, #1e1a4b1f)"})};p.forEach(h=>h.addEventListener("change",f)),f()}createDividePopup(){this.dividePopupElement=document.createElement("div"),this.dividePopupElement.className="map-divide-popup",this.dividePopupElement.style.cssText=`
|
||
position: absolute;
|
||
background: var(--card, #fff);
|
||
color: var(--card-foreground, #1e1a4b);
|
||
border-radius: 10px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
font-family: var(--font-body, 'Exo', sans-serif);
|
||
font-size: 13px;
|
||
min-width: 260px;
|
||
max-width: 320px;
|
||
z-index: 1002;
|
||
border: 2px solid #8b5cf6;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
`,this.dividePopup=new Ee({element:this.dividePopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.dividePopup)}showDividePopup(e,t,r){const o=`
|
||
<div style="background:#8b5cf6;color:#fff;padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
|
||
<span>Divide Polygon</span>
|
||
<button class="divide-popup-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">×</button>
|
||
</div>
|
||
<div style="padding:12px;">
|
||
<p style="margin:0 0 10px;color:var(--muted-foreground, #7a7a7a);font-size:12px;">
|
||
Enter the number of equal pieces:
|
||
</p>
|
||
<input type="number" class="divide-input" min="2" max="50" value="2"
|
||
style="width:100%;padding:8px 10px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:16px;font-weight:600;text-align:center;color:var(--foreground, #1e1a4b);background:var(--muted, #f2f4f7);min-height:40px;" />
|
||
<div style="display:flex;gap:8px;margin-top:12px;">
|
||
<button class="divide-popup-confirm" style="flex:1;padding:8px 12px;background:#8b5cf6;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
|
||
Divide
|
||
</button>
|
||
<button class="divide-popup-cancel" style="flex:1;padding:8px 12px;background:var(--muted-foreground, #7a7a7a);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;this.dividePopupElement.innerHTML=o,this.dividePopup.setPosition(r);const a=this.dividePopupElement.querySelector(".divide-input");a.focus(),a.select();const i=()=>{this.hideDividePopup(),this._polygonDivideInteraction.cancelDivide()};this.dividePopupElement.querySelector(".divide-popup-close").addEventListener("click",i),this.dividePopupElement.querySelector(".divide-popup-cancel").addEventListener("click",i),this.dividePopupElement.querySelector(".divide-popup-confirm").addEventListener("click",()=>{const s=parseInt(a.value,10);if(!s||s<2){a.style.borderColor="#ef4444";return}this.hideDividePopup(),this._polygonDivideInteraction.performDivide(s)}),a.addEventListener("keydown",s=>{s.key==="Enter"&&(s.preventDefault(),this.dividePopupElement.querySelector(".divide-popup-confirm").click())})}hideDividePopup(){this.dividePopup.setPosition(void 0)}createDrawnPolygonPopup(){this.drawnPolygonElement=document.createElement("div"),this.drawnPolygonElement.className="map-drawn-polygon-popup",this.drawnPolygonElement.style.cssText=`
|
||
position: absolute;
|
||
background: var(--card, #fff);
|
||
border-radius: var(--radius-xl, 0.75rem);
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
|
||
font-family: var(--font-body, 'Exo', sans-serif);
|
||
font-size: 13px;
|
||
min-width: 280px;
|
||
max-width: 360px;
|
||
max-height: 420px;
|
||
z-index: 1002;
|
||
border: 2px solid var(--success, #006b3f);
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
`,this.drawnPolygonPopup=new Ee({element:this.drawnPolygonElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.drawnPolygonPopup),this._drawnPolygonCallbacks=[],this._drawnPolygonFeature=null}getParcelAttributeKeys(){const e=["geometry","_layerType"],t=[],r=o=>{t.length>0||o.getLayers().forEach(a=>{if(!(t.length>0)){if(a instanceof Me)r(a);else if(a instanceof D){const i=a.getSource();if(!i)return;for(const s of i.getFeatures()){if(s.get("_layerType")!=="parcel")continue;const l=s.getProperties();for(const c of Object.keys(l))e.includes(c)||t.push(c);return}}}})};return r(this.overlayGroup),t}showDrawnPolygonPopup(e,t){this._drawnPolygonFeature=e;const r=this.getParcelAttributeKeys();if(r.length===0){console.warn("[MapView] No parcel attributes found — cannot build form");return}let o="";for(const d of r){const u=this.escapeHtml(d);o+=`
|
||
<div style="margin-bottom:8px;">
|
||
<label style="display:block;font-size:11px;font-weight:600;color:var(--muted-foreground, #7a7a7a);margin-bottom:2px;">${u}</label>
|
||
<input type="text" name="${u}" value=""
|
||
style="width:100%;padding:6px 8px;border:1px solid var(--border, #1e1a4b1f);border-radius:4px;font-size:13px;color:var(--foreground, #1e1a4b);background:var(--muted, #f2f4f7);min-height:34px;"
|
||
/>
|
||
</div>
|
||
`}const a=e.getGeometry(),i=dt(a,{projection:"EPSG:3857"}),l=`
|
||
<div style="background:var(--success, #006b3f);color:var(--success-foreground, #fff);padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
|
||
<span>📐 Polygon Attributes</span>
|
||
<button class="drawn-polygon-close" style="background:none;border:none;color:var(--success-foreground, #fff);font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">×</button>
|
||
</div>
|
||
<div style="padding:8px 12px;background:var(--muted, #f2f4f7);border-bottom:1px solid var(--border, #1e1a4b1f);font-size:12px;color:var(--muted-foreground, #7a7a7a);flex-shrink:0;">
|
||
Area: <strong>${We(i)}</strong>
|
||
</div>
|
||
<form class="drawn-polygon-form" style="padding:10px 12px;overflow-y:auto;flex:1;">
|
||
${o}
|
||
<div style="display:flex;gap:8px;margin-top:10px;">
|
||
<button type="submit" style="flex:1;padding:8px 12px;background:var(--success, #006b3f);color:var(--success-foreground, #fff);border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
|
||
💾 Save
|
||
</button>
|
||
<button type="button" class="drawn-polygon-cancel" style="flex:1;padding:8px 12px;background:var(--muted-foreground, #7a7a7a);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</form>
|
||
`;this.drawnPolygonElement.innerHTML=l,this.drawnPolygonPopup.setPosition(t),this.drawnPolygonElement.querySelector(".drawn-polygon-close").addEventListener("click",()=>{this.hideDrawnPolygonPopup()}),this.drawnPolygonElement.querySelector(".drawn-polygon-cancel").addEventListener("click",()=>{this.hideDrawnPolygonPopup()});const c=this.drawnPolygonElement.querySelector(".drawn-polygon-form");c.addEventListener("submit",d=>{d.preventDefault();const u=new FormData(c),p={};for(const[f,h]of u.entries())p[f]=h;for(const[f,h]of Object.entries(p))this._drawnPolygonFeature.set(f,h);this._drawnPolygonFeature.set("_layerType","parcel");for(const f of this._drawnPolygonCallbacks)f(this._drawnPolygonFeature,p);this.hideDrawnPolygonPopup()})}hideDrawnPolygonPopup(){this.drawnPolygonPopup.setPosition(void 0),this._drawnPolygonFeature=null}onDrawnPolygonSave(e){this._drawnPolygonCallbacks.push(e)}onFeatureModified(e){this._featureModifiedCallbacks||(this._featureModifiedCallbacks=[]),this._featureModifiedCallbacks.push(e)}onDrawnPoint(e){this._drawnPointCallbacks||(this._drawnPointCallbacks=[]),this._drawnPointCallbacks.push(e)}onDblClick(e){return this.dblClickCallbacks.push(e),this.dblClickCallbacks.length===1&&this.map.on("dblclick",t=>{const[r,o]=xe(t.coordinate);let a=null;this.map.forEachFeatureAtPixel(t.pixel,i=>(a=i,!0)),a&&(t.preventDefault(),t.stopPropagation());for(const i of this.dblClickCallbacks)i(r,o,a,t);if(a)return!1}),()=>{const t=this.dblClickCallbacks.indexOf(e);t>-1&&this.dblClickCallbacks.splice(t,1)}}escapeHtml(e){if(!e)return"";const t=document.createElement("div");return t.textContent=e,t.innerHTML}createAddLocationPopup(){this.addLocationPopupElement=document.createElement("div"),this.addLocationPopupElement.className="map-add-location-popup",this.addLocationPopupElement.innerHTML=`
|
||
<div class="add-location-popup-header">
|
||
<span>➕ Add Location</span>
|
||
<button type="button" class="add-location-popup-close" aria-label="Close">×</button>
|
||
</div>
|
||
<form id="map-add-location-form">
|
||
<div class="add-location-popup-field">
|
||
<label for="map-location-name">Name <span class="text-danger">*</span></label>
|
||
<input type="text" id="map-location-name" name="name" required placeholder="e.g., Water Point A">
|
||
</div>
|
||
<div class="add-location-popup-field">
|
||
<label for="map-location-category">Category</label>
|
||
<select id="map-location-category" name="category">
|
||
${this.getCategoryOptionsHtml()}
|
||
</select>
|
||
</div>
|
||
<div class="add-location-popup-field">
|
||
<label for="map-location-description">Description</label>
|
||
<textarea id="map-location-description" name="description" rows="2" placeholder="Optional notes..."></textarea>
|
||
</div>
|
||
<div class="add-location-popup-coords">
|
||
<small>📍 <span id="map-location-coords"></span></small>
|
||
</div>
|
||
<button type="submit" class="add-location-popup-submit">➕ Add Location</button>
|
||
</form>
|
||
`,this.addLocationPopup=new Ee({element:this.addLocationPopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.addLocationPopup),this.addLocationCoords=null,this.addLocationPopupElement.querySelector(".add-location-popup-close").addEventListener("click",()=>{this.hideAddLocationPopup()}),this.addLocationCallbacks=[]}showAddLocationPopup(e){const[t,r]=xe(e);this.addLocationCoords={lon:t,lat:r};const o=this.addLocationPopupElement.querySelector("#map-location-coords");o.textContent=`${t.toFixed(6)}, ${r.toFixed(6)}`,this.addLocationPopupElement.querySelector("#map-add-location-form").reset(),this.addLocationPopup.setPosition(e)}hideAddLocationPopup(){this.addLocationPopup.setPosition(void 0),this.addLocationCoords=null,this._discardPendingLocationSketch()}_discardPendingLocationSketch(){const e=this._pendingLocationSketch;e&&(this._pendingLocationSketch=null,this._withoutUndoRecording(()=>{try{this.drawingsSource.removeFeature(e)}catch{}}))}onAddLocation(e){if(this.addLocationCallbacks.push(e),this.addLocationCallbacks.length===1){const t=this.addLocationPopupElement.querySelector("#map-add-location-form");t.addEventListener("submit",r=>{if(r.preventDefault(),!this.addLocationCoords)return;const o=new FormData(t),a={name:o.get("name"),category:o.get("category"),description:o.get("description"),lon:this.addLocationCoords.lon,lat:this.addLocationCoords.lat};this.addLocationCallbacks.forEach(i=>i(a)),this.hideAddLocationPopup()})}}createBaseLayers(e){const t=new ae({title:"Topographic",type:"base",zIndex:-100,visible:e==="topo",source:new Ce({url:"https://{a-c}.tile.opentopomap.org/{z}/{x}/{y}.png",attributions:"Map data: © OpenTopoMap",maxZoom:17,crossOrigin:"anonymous"})});t.set("basemapKey","topo");const r=new ae({title:"Carto Light",type:"base",zIndex:-100,visible:e==="carto-light",source:new Ce({url:"https://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});r.set("basemapKey","carto-light");const o=new ae({title:"Carto Dark",type:"base",zIndex:-100,visible:e==="carto-dark",source:new Ce({url:"https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});o.set("basemapKey","carto-dark");const a=new ae({title:"OSM Cycle map",type:"base",zIndex:-100,visible:!1,source:new Go({url:"https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=ae1339c46dd3446b9c491e7336d38760"})});a.set("basemapKey","cycle");const i=new ae({title:"Satellite",type:"base",zIndex:-100,visible:e==="satellite",source:new Ce({url:"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",attributions:"Tiles © Esri",maxZoom:19,crossOrigin:"anonymous"})});i.set("basemapKey","satellite");const s=new ae({title:"Google Sat",type:"base",zIndex:-100,visible:e==="googlesat",source:new Ce({url:"http://mt0.google.com/vt/lyrs=y&hl=en&x={x}&y={y}&z={z}&s=Ga",attributions:"Tiles © Google",maxZoom:19,crossOrigin:"anonymous"})});s.set("basemapKey","googlesat");const l=new ae({title:"OpenStreetMap",type:"base",zIndex:-100,visible:e==="osm",source:new Go});l.set("basemapKey","osm"),this._baseMapLayers=[r,o,a,i,s,l,t];const c=new Me({title:"Base Maps",layers:[r,o,i,a,s,l,t]});return c.set("displayInLayerSwitcher",!1),c}setBaseMap(e){if(!this._baseMapLayers)return!1;if(e==="none"){for(const r of this._baseMapLayers)r.setVisible(!1);return console.log("[MapView] Base map switched off (none)"),this.map.dispatchEvent({type:"basemapchange",key:"none"}),!0}let t=!1;for(const r of this._baseMapLayers){const o=r.get("basemapKey")===e;r.setVisible(o),o&&(t=!0)}return t&&(console.log("[MapView] Base map switched to:",e),this.map.dispatchEvent({type:"basemapchange",key:e})),t}_createBaseMapPicker(){const e=[{key:"topo",label:"Topographic",grad:"linear-gradient(135deg,#e8d5b7,#a67c52)"},{key:"osm",label:"OpenStreetMap",grad:"linear-gradient(135deg,#d4e6f1,#85c1e9)"},{key:"satellite",label:"Satellite",grad:"linear-gradient(135deg,#1b4332,#40916c)"},{key:"googlesat",label:"Google Sat",grad:"linear-gradient(135deg,#2a5d3d,#4a8c5a)"},{key:"carto-light",label:"Carto Light",grad:"linear-gradient(135deg,#f5f5f5,#d4d4d4)"},{key:"carto-dark",label:"Carto Dark",grad:"linear-gradient(135deg,#1a1a2e,#0f3460)"},{key:"none",label:"None",grad:"repeating-conic-gradient(#e5e7eb 0 25%, #fff 0 50%) 50% / 12px 12px"}],t=this.map.getTargetElement();if(!t)return;const r=document.createElement("button");r.type="button",r.className="ls-basemap-toggle",r.title="Switch base map",r.setAttribute("aria-label","Switch base map"),r.innerHTML='<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true"><path d="M9 2L16 5.8L9 9.6L2 5.8L9 2Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M2 9.2L9 13L16 9.2" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M2 12.4L9 16.2L16 12.4" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" stroke-opacity=".4"/></svg>',t.appendChild(r);const o=document.createElement("div");o.className="ls-basemap-panel",o.innerHTML='<div class="ls-basemap-header">Base Map</div><div class="ls-basemap-grid">'+e.map(i=>`
|
||
<label class="ls-bm-chip">
|
||
<input type="radio" name="lupmis-basemap" value="${i.key}">
|
||
<div class="ls-bm-label">
|
||
<div class="ls-bm-thumb" style="background:${i.grad};"></div>
|
||
<div class="ls-bm-name">${i.label}</div>
|
||
</div>
|
||
</label>
|
||
`).join("")+"</div>",t.appendChild(o),this._basemapPanel=o,this._basemapToggle=r;const a=i=>{const s=i||this._baseMapLayers?.find(l=>l.getVisible())?.get("basemapKey");o.querySelectorAll('input[name="lupmis-basemap"]').forEach(l=>{l.checked=l.value===s})};a(),r.addEventListener("click",i=>{i.stopPropagation();const s=!o.classList.contains("open");o.classList.toggle("open",s),r.classList.toggle("active",s),s&&a()}),document.addEventListener("click",i=>{o.classList.contains("open")&&(o.contains(i.target)||r.contains(i.target)||(o.classList.remove("open"),r.classList.remove("active")))}),o.addEventListener("change",i=>{const s=i.target.closest('input[type=radio][name="lupmis-basemap"]');if(!s)return;const l=s.value;this.setBaseMap(l);try{localStorage.setItem("default-basemap",l)}catch{}o.classList.remove("open"),r.classList.remove("active")}),this.map.on("basemapchange",i=>a(i.key))}_initGpsRendering(){this._gpsPositionSource=new G,this._gpsTrailSource=new G,this._gpsTrailCoords=[],this._gpsTrailLayer=new D({source:this._gpsTrailSource,zIndex:940,style:new M({stroke:new P({color:"#ff6d00",width:4,lineCap:"round",lineJoin:"round"})}),properties:{title:"GPS Trail",displayInLayerSwitcher:!1}}),this._gpsPositionLayer=new D({source:this._gpsPositionSource,zIndex:950,style:e=>e.get("_kind")==="accuracy"?new M({fill:new C({color:"rgba(0,94,184,0.12)"}),stroke:new P({color:"rgba(0,94,184,0.35)",width:1})}):new M({image:new he({radius:7,fill:new C({color:"#005eb8"}),stroke:new P({color:"#ffffff",width:2.5})})}),properties:{title:"GPS Position",displayInLayerSwitcher:!1}}),this.map.addLayer(this._gpsTrailLayer),this.map.addLayer(this._gpsPositionLayer),this._gpsCallbacks={locate:[],record:[]},this._gpsRecording=!1}onLocateMe(e){this._gpsCallbacks.locate.push(e)}onToggleRecording(e){this._gpsCallbacks.record.push(e)}showCurrentPosition(e,t,r=null){if(e==null||t==null)return;const o=ee([e,t]);if(this._gpsPositionSource.clear(),r&&r>0){const i=r/Math.cos(t*Math.PI/180),s=new fe({geometry:new ft([this._circleRing(o,i)])});s.set("_kind","accuracy"),this._gpsPositionSource.addFeature(s)}const a=new fe({geometry:new yt(o)});a.set("_kind","dot"),this._gpsPositionSource.addFeature(a)}_circleRing(e,t,r=48){const o=[],i=t/1;for(let s=0;s<=r;s++){const l=s/r*2*Math.PI;o.push([e[0]+i*Math.cos(l),e[1]+i*Math.sin(l)])}return o}centerOn(e,t,r=16){this.map.getView().animate({center:ee([e,t]),zoom:r,duration:500})}startTrailRender(){this._gpsTrailCoords=[],this._gpsTrailSource.clear()}appendTrailPoint(e,t){e==null||t==null||(this._gpsTrailCoords.push(ee([e,t])),this._gpsTrailSource.clear(),this._gpsTrailCoords.length>=2&&this._gpsTrailSource.addFeature(new fe({geometry:new de(this._gpsTrailCoords)})))}clearTrailRender(){this._gpsTrailCoords=[],this._gpsTrailSource.clear()}setRecordingState(e){this._gpsRecording=!!e,this._recordBtn&&(this._recordBtn.classList.toggle("recording",this._gpsRecording),this._recordBtn.title=this._gpsRecording?"Stop trail recording":"Record GPS trail",this._recordBtn.innerHTML=this._gpsRecording?'<i class="bi bi-stop-fill"></i>':'<i class="bi bi-record-circle"></i>'),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='<i class="bi bi-geo-alt-fill"></i>',e.appendChild(t);const r=document.createElement("div");r.className="ls-locate-actions",r.innerHTML='<button type="button" class="ls-locate-btn ls-locate-me" title="Locate me"><i class="bi bi-crosshair"></i></button><button type="button" class="ls-locate-btn ls-locate-record" title="Record GPS trail"><i class="bi bi-record-circle"></i></button>',e.appendChild(r),this._locateToggle=t,this._locateActions=r,this._locateMeBtn=r.querySelector(".ls-locate-me"),this._recordBtn=r.querySelector(".ls-locate-record");const o=()=>{r.classList.remove("open"),t.classList.remove("active")},a=()=>{r.classList.add("open"),t.classList.add("active")};t.addEventListener("click",i=>{i.stopPropagation(),r.classList.contains("open")?o():a()}),document.addEventListener("click",i=>{r.classList.contains("open")&&(r.contains(i.target)||t.contains(i.target)||this._gpsRecording||o())}),this._locateMeBtn.addEventListener("click",i=>{i.stopPropagation();for(const s of this._gpsCallbacks.locate)try{s()}catch(l){console.error(l)}this._gpsRecording||o()}),this._recordBtn.addEventListener("click",i=>{i.stopPropagation();const s=!this._gpsRecording;for(const l of this._gpsCallbacks.record)try{l(s)}catch(c){console.error(c)}})}getFeatureStyle(e){const t=e.get("category")||"default",r=this.getEmoji(t);if(e===this.selectedFeature)return[new M({image:new he({radius:22,fill:new C({color:"rgba(220, 38, 38, 0.25)"}),stroke:new P({color:"#dc2626",width:3})})}),new M({text:new Ke({text:r,font:"40px sans-serif",textBaseline:"bottom",textAlign:"center",offsetY:-5})})];const o=e.get("style");return o||(this.categoryStyles[t]?this.categoryStyles[t]:this.defaultStyle)}setCategoryStyles(e){for(const[t,r]of Object.entries(e)){r.emoji&&(this.categoryEmojis[t]?(this.categoryEmojis[t].emoji=r.emoji,r.label&&(this.categoryEmojis[t].label=r.label)):this.categoryEmojis[t]={emoji:r.emoji,label:r.label||t});const o=this.getEmoji(t),a=r.fontSize||28;this.categoryStyles[t]=this.createEmojiStyle(o,a)}this.markerSource.changed()}addMarker(e,t,r={}){console.log("[MapView] Adding marker at",e,t,"with properties:",r);const o=new fe({geometry:new yt(ee([e,t])),...r});return o.set("lon",e),o.set("lat",t),this.markerSource.addFeature(o),console.log("[MapView] Marker added, total features:",this.markerSource.getFeatures().length),o}addMarkers(e){console.log("[MapView] Adding",e.length,"markers");const t=e.map(r=>new fe({geometry:new yt(ee([r.longitude,r.latitude])),id:r.id,name:r.name,description:r.description,category:r.category,lon:r.longitude,lat:r.latitude}));return this._withoutUndoRecording(()=>this.markerSource.addFeatures(t)),console.log("[MapView] Markers added, total features:",this.markerSource.getFeatures().length),t}clearMarkers(){this._withoutUndoRecording(()=>this._silentClearSource(this.markerSource)),this.selectedFeature=null}removeMarker(e){if(typeof e=="object")this.markerSource.removeFeature(e);else{const t=this.markerSource.getFeatures().find(r=>r.get("id")===e);t&&this.markerSource.removeFeature(t)}}getMarkers(){return this.markerSource.getFeatures()}findMarker(e){return this.markerSource.getFeatures().find(t=>t.get("id")===e)}showMarkers(){this.markersLayer&&!this.markersLayer.getVisible()&&(this.markersLayer.setVisible(!0),console.log("[MapView] Markers layer switched on to show a location"))}selectMarker(e){return typeof e=="object"?this.selectedFeature=e:this.selectedFeature=this.findMarker(e),this.markerSource.changed(),this.selectedFeature}clearSelection(){this.selectedFeature=null,this.markerSource.changed()}zoomTo(e,t,r=15){this.map.getView().animate({center:ee([e,t]),zoom:r,duration:500})}fitToMarkers(e=50){const t=this.markerSource.getExtent();t&&t[0]!==1/0&&this.map.getView().fit(t,{padding:[e,e,e,e],duration:500,maxZoom:16})}getCenter(){const e=this.map.getView().getCenter();return xe(e)}getZoom(){return this.map.getView().getZoom()}setCenter(e,t){this.map.getView().setCenter(ee([e,t]))}setZoom(e){this.map.getView().setZoom(e)}onClick(e){return this.clickCallbacks.push(e),this.clickCallbacks.length===1&&(this._clickTimer=null,this.map.on("dblclick",()=>{this._clickTimer&&(clearTimeout(this._clickTimer),this._clickTimer=null)}),this.map.on("click",t=>{this._clickTimer&&(clearTimeout(this._clickTimer),this._clickTimer=null),!this._editBarActive&&this._selectInteraction&&this._selectInteraction.getFeatures().clear();let r=!1,o=!1,a=null;if(this.map.forEachFeatureAtPixel(t.pixel,l=>{l.get("_layerType")==="parcel"&&(o=!0),l.get("name")&&(a=l),r=!0}),r&&!o&&!a)return;const[i,s]=xe(t.coordinate);this._clickTimer=setTimeout(()=>{this._clickTimer=null;let l=null;this.map.forEachFeatureAtPixel(t.pixel,c=>{if(c.get("name"))return l=c,!0});for(const c of this.clickCallbacks)c(i,s,l,t)},300)})),()=>{const t=this.clickCallbacks.indexOf(e);t>-1&&this.clickCallbacks.splice(t,1)}}onPointerMove(e){this.map.on("pointermove",t=>{if(t.dragging)return;const[r,o]=xe(t.coordinate);let a=null;this.map.forEachFeatureAtPixel(t.pixel,i=>{if(i.get("name"))return a=i,!0}),this.map.getTargetElement().style.cursor=a?"pointer":"",e(r,o,a,t)})}enableHoverCursor(){}addGeoJSONLayer(e,t,r={},o=null){const{strokeColor:a="#3b82f6",strokeWidth:i=2,strokeDash:s=null,fillColor:l="rgba(59,130,246,0.1)",lineCasingColor:c=null,lineCasingWidth:d=null,pointRadius:u=5,pointFillColor:p=null,pointStrokeColor:f="#ffffff",pointStrokeWidth:h=1.5}=r,g=new G({features:new le().readFeatures(e,{featureProjection:"EPSG:3857"})}),m=new C({color:l}),y=new he({radius:u,fill:new C({color:p||a}),stroke:new P({color:f,width:h})}),w=new P({color:a,width:i,...s?{lineDash:s}:{}});let b;if(c){const x=d??i+2;b=[new M({stroke:new P({color:c,width:x})}),new M({stroke:w,fill:m,image:y})]}else b=new M({stroke:w,fill:m,image:y});const S=new D({title:t,source:g,style:b,...r.declutter?{declutter:r.declutter}:{}});S.set("typeTag",r.typeTag||"VEC");const L=x=>x?x.includes("Polygon")?"Vector / Polygon":x.includes("LineString")?"Vector / Line":x.includes("Point")?"Vector / Point":"Vector":null;if(r.typeDescription)S.set("typeDescription",r.typeDescription);else{const x=g.getFeatures(),I=L(x[0]?.getGeometry?.()?.getType?.());if(I)S.set("typeDescription",I);else{const k=U=>{const z=L(U.feature.getGeometry?.()?.getType?.());z&&S.set("typeDescription",z),g.un("addfeature",k)};g.on("addfeature",k)}}return(o||this.overlayGroup).getLayers().push(S),console.log("[MapView] GeoJSON layer added:",t,"→",g.getFeatures().length,"features",o?`(in group "${o.get("title")}")`:""),S}addLayerGroup(e,t,r=""){const o=new Me({title:t.trim()});return o.set("layerId",e),o.set("description",r),this.overlayGroup.getLayers().push(o),console.log("[MapView] Layer group added:",t.trim(),"(id:",e+")"),o}addWMSLayer(e,t,r,o,a={}){const i=this.getLayerGroupByTitle(e);if(!i)return console.warn(`[MapView] Layer group "${e}" not found — cannot add WMS layer "${t}"`),null;const s={LAYERS:o,TILED:!0,WIDTH:256,HEIGHT:256};a.style!==void 0&&(s.STYLES=a.style);const l=new jo({url:r,params:s,serverType:a.serverType!==void 0?a.serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1,attributions:a.attributions}),c=new ae({title:t,visible:a.visible!==void 0?a.visible:!0,source:l,opacity:a.opacity!==void 0?a.opacity:1,zIndex:a.zIndex});if(c.set("typeTag","WMS"),c.set("typeDescription","WMS / Raster"),l.on("tileloaderror",()=>{F(`WMS layer "${t}" — tile load error. Check the URL and layer name.`,"warning",5e3)}),i.getLayers().push(c),a.legendUrl)try{this._registerLegend(c,t,a.legendUrl)}catch(d){console.warn(`[MapView] Could not register legend for "${t}":`,d)}return a.onlineOnly&&this._attachOnlineOnlyHandler(c,t),console.log(`[MapView] WMS layer added: "${t}" → group "${e}"`),c}addXYZLayer(e,t,r,o={}){const a=this.getLayerGroupByTitle(e);if(!a)return console.warn(`[MapView] Layer group "${e}" not found — cannot add XYZ layer "${t}"`),null;const i=new Ce({url:r,crossOrigin:"anonymous",maxZoom:o.maxZoom!==void 0?o.maxZoom:19,attributions:o.attributions}),s=new ae({title:t,visible:o.visible!==void 0?o.visible:!0,source:i,opacity:o.opacity!==void 0?o.opacity:1,zIndex:o.zIndex});if(s.set("typeTag","XYZ"),s.set("typeDescription","XYZ / Tile"),i.on("tileloaderror",()=>{F(`XYZ layer "${t}" — tile load error. Check the URL.`,"warning",5e3)}),a.getLayers().push(s),o.legendUrl)try{this._registerLegend(s,t,o.legendUrl)}catch(l){console.warn(`[MapView] Could not register legend for "${t}":`,l)}return o.onlineOnly&&this._attachOnlineOnlyHandler(s,t),console.log(`[MapView] XYZ layer added: "${t}" → group "${e}"`),s}async addCOGLayer(e,t,r,o={}){const a=this.getLayerGroupByTitle(e);if(!a)return console.warn(`[MapView] Layer group "${e}" not found — cannot add COG layer "${t}"`),null;const i=await this._probeCOGUrl(r);if(i)return console.warn(`[MapView] COG pre-flight failed for "${t}":`,i),F(i,"error",8e3),null;let s,l;try{[{default:s},{default:l}]=await Promise.all([Te(()=>import("./geotiff-B9aROX49.js").then(d=>d.W),__vite__mapDeps([0,1,2,3])),Te(()=>import("./geotiff-B9aROX49.js").then(d=>d.G),__vite__mapDeps([0,1,2,3]))])}catch(d){return console.error("[MapView] Could not load COG support:",d),F("Raster (COG) support could not be loaded — check your connection.","error",6e3),null}let c=null;try{const d={sources:[{url:r,...o.min!==void 0?{min:o.min}:{},...o.max!==void 0?{max:o.max}:{}}],normalize:o.normalize!==void 0?o.normalize:!0,attributions:o.attributions},u=new l(d);c=new s({title:t,source:u,visible:o.visible!==void 0?o.visible:!0,opacity:o.opacity!==void 0?o.opacity:1,zIndex:o.zIndex,...o.style?{style:o.style}:{}}),c.set("typeTag","COG"),c.set("typeDescription","Cloud-Optimized GeoTIFF"),u.on("error",p=>{console.warn(`[MapView] COG "${t}" source error:`,p),F(`Raster "${t}" could not be loaded. Check that the file is a valid COG and is CORS-enabled.`,"warning",6e3)}),a.getLayers().push(c)}catch(d){return console.error(`[MapView] Failed to add COG layer "${t}":`,d),F(`Could not add raster "${t}": ${d.message}`,"error",6e3),null}return o.onlineOnly&&this._attachOnlineOnlyHandler(c,t),console.log(`[MapView] COG layer added: "${t}" → group "${e}"`),c}async _probeCOGUrl(e){let t;try{t=await fetch(e,{headers:{Range:"bytes=0-0"}})}catch{return"Could not reach that file. It may be offline, or the server may not allow cross-origin (CORS) requests from this app."}if(t.status===404)return"No file at that URL (404). Check the address.";if(!t.ok&&t.status!==206)return`The server refused the request (HTTP ${t.status}).`;const r=(t.headers.get("content-type")||"").toLowerCase();return r.includes("text/html")?"That URL is a web page, not a GeoTIFF. Paste the direct link to the .tif file itself — a documentation or viewer page will not work.":r.startsWith("text/")||r.includes("json")?`That URL returns ${r.split(";")[0]}rather than image data. Paste the direct link to the .tif file.`:(t.status===200&&F("This server does not support byte-range requests, so the whole raster will be downloaded. It may be slow for a large file.","warning",7e3),null)}getSelectedFeatures(){return this._selectInteraction?this._selectInteraction.getFeatures().getArray().slice():[]}listVectorLayers(e={}){const{visibleOnly:t=!1,nonEmptyOnly:r=!0}=e,o=[],a=i=>{if(!i)return;if(typeof i.getLayers=="function"){i.getLayers().forEach(a);return}if(!(i instanceof D)||i===this._vertexOverlayLayer||i===this._gpsTrailLayer||i===this._gpsPositionLayer||i.get("displayInLayerSwitcher")===!1||t&&i.getVisible&&!i.getVisible())return;const s=i.getSource&&i.getSource();if(!s||typeof s.getFeatures!="function")return;const l=s.getFeatures().length;r&&l===0||o.push({layer:i,title:i.get("title")||"(untitled layer)",count:l})};return this.map.getLayers().forEach(a),o}_createAddLayerDialog(){this._addLayerDialog=document.createElement("div"),this._addLayerDialog.className="map-add-layer-dialog",this._addLayerDialog.style.cssText=`
|
||
display:none;position:absolute;top:0;left:0;right:0;bottom:0;
|
||
z-index:1100;background:rgba(0,0,0,0.4);
|
||
align-items:center;justify-content:center;
|
||
`;const e=document.createElement("div");e.style.cssText=`
|
||
background:var(--card, #fff);color:var(--card-foreground, #1e1a4b);
|
||
border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,0.35);
|
||
font-family:var(--font-body, 'Exo', sans-serif);font-size:13px;
|
||
width:340px;max-width:90vw;border:2px solid #10b981;overflow:hidden;
|
||
`,e.innerHTML=`
|
||
<div style="background:#10b981;color:#fff;padding:10px 14px;font-weight:600;display:flex;justify-content:space-between;align-items:center;">
|
||
<span>Add External Layer</span>
|
||
<button class="add-layer-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">×</button>
|
||
</div>
|
||
<div style="padding:14px;display:flex;flex-direction:column;gap:10px;">
|
||
<div>
|
||
<label style="font-weight:600;font-size:12px;display:block;margin-bottom:4px;">Layer Type</label>
|
||
<div class="add-layer-types" style="display:flex;gap:6px;">
|
||
<label style="flex:1;display:flex;align-items:center;gap:4px;cursor:pointer;padding:6px 8px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:12px;font-weight:600;">
|
||
<input type="radio" name="add-layer-type" value="wms" checked style="accent-color:#10b981;"> WMS
|
||
</label>
|
||
<label style="flex:1;display:flex;align-items:center;gap:4px;cursor:pointer;padding:6px 8px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:12px;font-weight:600;">
|
||
<input type="radio" name="add-layer-type" value="wfs" style="accent-color:#10b981;"> WFS
|
||
</label>
|
||
<label style="flex:1;display:flex;align-items:center;gap:4px;cursor:pointer;padding:6px 8px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:12px;font-weight:600;">
|
||
<input type="radio" name="add-layer-type" value="xyz" style="accent-color:#10b981;"> XYZ
|
||
</label>
|
||
<label style="flex:1;display:flex;align-items:center;gap:4px;cursor:pointer;padding:6px 8px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:12px;font-weight:600;">
|
||
<input type="radio" name="add-layer-type" value="cog" style="accent-color:#10b981;"> COG
|
||
</label>
|
||
</div>
|
||
<div style="font-size:11px;color:var(--muted-foreground, #7a7a7a);margin-top:4px;" class="add-layer-type-hint"></div>
|
||
</div>
|
||
<div>
|
||
<label style="font-weight:600;font-size:12px;display:block;margin-bottom:4px;">Server URL</label>
|
||
<input type="text" class="add-layer-url" placeholder="https://example.com/wms"
|
||
style="width:100%;padding:8px 10px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:13px;background:var(--muted, #f2f4f7);color:var(--foreground, #1e1a4b);box-sizing:border-box;" />
|
||
</div>
|
||
<div class="add-layer-name-row">
|
||
<label style="font-weight:600;font-size:12px;display:block;margin-bottom:4px;">Layer Name</label>
|
||
<input type="text" class="add-layer-name" placeholder="workspace:layer_name"
|
||
style="width:100%;padding:8px 10px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:13px;background:var(--muted, #f2f4f7);color:var(--foreground, #1e1a4b);box-sizing:border-box;" />
|
||
<div style="font-size:11px;color:var(--muted-foreground, #7a7a7a);margin-top:2px;" class="add-layer-name-hint">
|
||
WMS LAYERS parameter (e.g. workspace:layer)
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label style="font-weight:600;font-size:12px;display:block;margin-bottom:4px;">Display Title</label>
|
||
<input type="text" class="add-layer-title" placeholder="My Layer"
|
||
style="width:100%;padding:8px 10px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:13px;background:var(--muted, #f2f4f7);color:var(--foreground, #1e1a4b);box-sizing:border-box;" />
|
||
</div>
|
||
<div style="display:flex;gap:8px;margin-top:4px;">
|
||
<button class="add-layer-confirm" style="flex:1;padding:8px 12px;background:#10b981;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
|
||
Add Layer
|
||
</button>
|
||
<button class="add-layer-cancel" style="flex:1;padding:8px 12px;background:var(--muted-foreground, #7a7a7a);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`,this._addLayerDialog.appendChild(e),this.map.getTargetElement().appendChild(this._addLayerDialog);const t=e.querySelector(".add-layer-name-row"),r=e.querySelector(".add-layer-name-hint"),o=e.querySelector(".add-layer-url"),a=e.querySelector(".add-layer-type-hint");e.querySelectorAll('input[name="add-layer-type"]').forEach(s=>{s.addEventListener("change",()=>{const l=s.value;a.textContent=l==="cog"?"Cloud-Optimized GeoTIFF — the server must allow CORS and byte-range requests.":"",l==="cog"?(t.style.display="none",o.placeholder="https://example.com/data/elevation_cog.tif"):l==="xyz"?(t.style.display="none",o.placeholder="https://example.com/tiles/{z}/{x}/{y}.png"):(t.style.display="",o.placeholder=l==="wms"?"https://example.com/wms":"https://example.com/wfs",r.textContent=l==="wms"?"WMS LAYERS parameter (e.g. workspace:layer)":"WFS typename (e.g. workspace:layer)")})});const i=()=>this._hideAddLayerDialog();e.querySelector(".add-layer-close").addEventListener("click",i),e.querySelector(".add-layer-cancel").addEventListener("click",i),this._addLayerDialog.addEventListener("click",s=>{s.target===this._addLayerDialog&&i()}),e.querySelector(".add-layer-confirm").addEventListener("click",()=>{const s=e.querySelector('input[name="add-layer-type"]:checked').value,l=e.querySelector(".add-layer-url").value.trim(),c=e.querySelector(".add-layer-name").value.trim(),d=e.querySelector(".add-layer-title").value.trim();if(!l){e.querySelector(".add-layer-url").style.borderColor="#ef4444";return}if((s==="wms"||s==="wfs")&&!c){e.querySelector(".add-layer-name").style.borderColor="#ef4444";return}if(!d){e.querySelector(".add-layer-title").style.borderColor="#ef4444";return}this._addExternalLayer(s,l,c,d),this._hideAddLayerDialog()}),e.addEventListener("keydown",s=>{s.key==="Enter"&&(s.preventDefault(),e.querySelector(".add-layer-confirm").click()),s.key==="Escape"&&(s.preventDefault(),i())})}showAddLayerDialog(){const e=this._addLayerDialog;e.querySelector(".add-layer-url").value="",e.querySelector(".add-layer-name").value="",e.querySelector(".add-layer-title").value="",e.querySelectorAll('input[name="add-layer-type"]')[0].checked=!0,e.querySelector(".add-layer-name-row").style.display="",e.querySelector(".add-layer-url").placeholder="https://example.com/wms",e.querySelector(".add-layer-name-hint").textContent="WMS LAYERS parameter (e.g. workspace:layer)";const t=e.querySelector(".add-layer-type-hint");t&&(t.textContent=""),e.querySelectorAll('input[type="text"]').forEach(r=>{r.style.borderColor="var(--border, #1e1a4b1f)"}),e.style.display="flex",e.querySelector(".add-layer-url").focus()}_hideAddLayerDialog(){this._addLayerDialog.style.display="none"}_addExternalLayer(e,t,r,o){const a=this._externalSourceGroup;if(!a){F('Layer group "External Source" not found.',"error",4e3);return}let i;switch(e){case"wms":{const s=new jo({url:t,params:{LAYERS:r,TILED:!0,WIDTH:256,HEIGHT:256},serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1});i=new ae({title:o,visible:!0,source:s}),s.on("tileloaderror",()=>{F(`WMS "${o}" — tile load error. Check URL and layer name.`,"warning",5e3)});break}case"wfs":{const s=`${t}${t.includes("?")?"&":"?"}service=WFS&version=1.1.0&request=GetFeature&typename=${encodeURIComponent(r)}&outputFormat=application/json&srsname=EPSG:3857`,l=new G({url:s,format:new le});l.on("featuresloaderror",()=>{F(`WFS "${o}" — load error. Check URL and layer name.`,"warning",5e3)}),i=new D({title:o,visible:!0,source:l,style:new M({stroke:new P({color:"#e11d48",width:2}),fill:new C({color:"rgba(225,29,72,0.15)"})})});break}case"xyz":i=new ae({title:o,visible:!0,source:new Ce({url:t,crossOrigin:"anonymous"})}),i.getSource().on("tileloaderror",()=>{F(`XYZ "${o}" — tile load error. Check the URL template.`,"warning",5e3)});break;case"cog":{const s=a.get("title")||"External Source";this.addCOGLayer(s,o,t).then(l=>{l&&(l.set("removable",!0),F(`Layer "${o}" added to External Source.`,"success",3e3),console.log(`[MapView] External COG layer added: "${o}"`))});return}default:F(`Unknown layer type: ${e}`,"error",4e3);return}i.set("typeTag",e.toUpperCase()),i.set("typeDescription",{wms:"WMS / Raster",wfs:"WFS / Vector",xyz:"XYZ / Tile"}[e]||e.toUpperCase()),i.set("removable",!0),a.getLayers().push(i),F(`Layer "${o}" added to External Source.`,"success",3e3),console.log(`[MapView] External ${e.toUpperCase()} layer added: "${o}"`)}_decorateLayerListItem(e,t){const r=e.get("typeTag");if(r){const c=t.querySelector(":scope > .li-content > label > span");if(c&&!c.querySelector(":scope > .ls-type-tag")){const d=document.createElement("span");d.className=`ls-type-tag ls-type-tag-${String(r).toLowerCase()}`,d.textContent=String(r),d.title=`${r} layer`,c.appendChild(d)}}const o=t.querySelector(":scope > .ol-layerswitcher-buttons");if(o){const c=o.querySelector(":scope > .expend-layers, :scope > .collapse-layers");c&&!c.querySelector(":scope > svg.ls-chevron-svg")&&(c.innerHTML='<svg class="ls-chevron-svg" width="11" height="11" viewBox="0 0 11 11" fill="none" aria-hidden="true"><path d="M3 2L7 5.5L3 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path></svg>')}const a=t.querySelector(":scope > .li-content"),i=()=>{if(!a)return;const c=e.get("typeDescription");let d=a.querySelector(":scope > .ls-layer-subtitle");if(!c){d&&d.remove();return}if(!d){d=document.createElement("div"),d.className="ls-layer-subtitle";const u=a.querySelector(":scope > label");u&&u.nextSibling?a.insertBefore(d,u.nextSibling):a.appendChild(d)}d.textContent=c};if(i(),e._lsSubtitleHooked||(e._lsSubtitleHooked=!0,e.on("change:typeDescription",()=>{i()})),e.get("removable")===!0&&o&&!o.querySelector(":scope > .ls-remove-btn")){const c=document.createElement("button");c.type="button",c.className="ls-remove-btn",c.title="Remove this layer",c.setAttribute("aria-label","Remove layer"),c.innerHTML='<svg width="11" height="11" viewBox="0 0 11 11" fill="none" aria-hidden="true"><path d="M2 2l7 7M9 2L2 9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"></path></svg>',c.addEventListener("click",d=>{d.stopPropagation(),this._removeLayer(e)}),o.appendChild(c)}const s=e.get("_externalImportId");if(s!=null){const c=t.querySelector(":scope > .li-content > label > span");let d=c?c.querySelector(":scope > .ls-import-chip"):null;const u=e.get("_externalImportStatus")||"mapped",p=e.getSource()?.getFeatures().length??0,f=e.get("_externalImportErrorCount")??0,h=(()=>{switch(u){case"mapped":return{text:`Upload ${p}`,cls:"ls-import-chip-mapped",title:"Upload this dataset to the database",clickable:!0};case"uploading":return{text:"…",cls:"ls-import-chip-uploading",title:"Uploading…",clickable:!1};case"submitted":return{text:"✓ submitted",cls:"ls-import-chip-submitted",title:"Uploaded — awaiting supervisor review",clickable:!1};case"migrated":return{text:"✓ live",cls:"ls-import-chip-migrated",title:"Approved by supervisor and live on the server",clickable:!1};case"failed":return{text:`${f} errors — fix?`,cls:"ls-import-chip-failed",title:"Some rows failed; click to review",clickable:!0};case"other":case null:case void 0:default:return null}})();if(!h)d&&d.remove();else if(c){d||(d=document.createElement("span"),d.className="ls-import-chip",c.appendChild(d)),d.className=`ls-import-chip ${h.cls}`,d.textContent=h.text,d.title=h.title,d.style.cursor=h.clickable?"pointer":"default",d.style.opacity=h.clickable?"1":"0.85";const g=d.cloneNode(!0);d.replaceWith(g),d=g,h.clickable&&d.addEventListener("click",m=>{m.preventDefault(),m.stopPropagation(),window.dispatchEvent(new CustomEvent("lupmis:import-chip-click",{detail:{importId:s,status:u,layer:e}}))})}}if((e.get("title")||"").toLowerCase().includes("external")&&(this._externalSourceGroup=e,o&&!o.querySelector(".ol-add-layer"))){const c=document.createElement("span");c.className="ol-add-layer",c.title="Add external layer",c.textContent="+",c.style.cssText=`
|
||
display:inline-flex !important;align-items:center;justify-content:center;
|
||
width:22px !important;height:22px !important;border-radius:50%;
|
||
background:#41b6a6 !important;color:#fff !important;
|
||
font-size:15px !important;font-weight:700;
|
||
cursor:pointer;line-height:1 !important;
|
||
margin:0 4px 0 0;vertical-align:middle;
|
||
transition:background 0.2s;box-sizing:border-box;border:none;
|
||
`,c.addEventListener("mouseenter",()=>{c.style.background="#329686"}),c.addEventListener("mouseleave",()=>{c.style.background="#41b6a6"}),c.addEventListener("click",d=>{d.stopPropagation(),this.showAddLayerDialog()}),o.prepend(c)}}_removeLayer(e){const t=e.get("title")||"this layer";if(!confirm(`Remove "${t}" from the map?
|
||
|
||
This only affects the current session — built-in layers cannot be removed.`))return;const r=a=>{const i=a.getLayers();if(i.getArray().includes(e))return i.remove(e),!0;let s=!1;return i.forEach(l=>{!s&&l.getLayers&&(s=r(l))}),s};r(this.overlayGroup)?(console.log(`[MapView] Removed layer "${t}"`),F(`Removed "${t}" from the map.`,"info",3e3)):console.warn(`[MapView] Could not find layer "${t}" in any group`)}_refreshLayerSwitcherChrome(e){const t=e.element?.querySelector(".panel-container"),r=e.element?.querySelector("ul.panel");if(!t||!r)return;let o=t.querySelector(":scope > .ls-active-badge");o||(o=document.createElement("div"),o.className="ls-active-badge",o.innerHTML=`
|
||
<span class="ls-active-badge-title">Layers</span>
|
||
<span class="ls-active-badge-count">0 active</span>
|
||
`,t.insertBefore(o,r));let a=t.querySelector(":scope > .ls-footer-row");a||(a=document.createElement("div"),a.className="ls-footer-row",a.innerHTML=`
|
||
<span class="ls-footer-note">— layers total</span>
|
||
<button type="button" class="ls-footer-btn"
|
||
title="Hide every overlay (base map stays on)">
|
||
Reset
|
||
</button>
|
||
`,t.appendChild(a),a.querySelector(".ls-footer-btn").addEventListener("click",s=>{s.stopPropagation(),this._resetAllOverlays()}));const i=this._countLayers();o.querySelector(".ls-active-badge-count").textContent=`${i.activeOverlays} active`,a.querySelector(".ls-footer-note").textContent=`${i.totalOverlays} overlay${i.totalOverlays===1?"":"s"}`}_countLayers(){let e=0,t=0;const r=new Set(["__vertex_highlight__"]),o=a=>{a.getLayers().forEach(i=>{i.get("displayInLayerSwitcher")!==!1&&(r.has(i.get("title"))||(i.getLayers?o(i):(e++,i.getVisible()&&t++)))})};return this.overlayGroup&&o(this.overlayGroup),{totalOverlays:e,activeOverlays:t}}_resetAllOverlays(){const e=new Set(["__vertex_highlight__"]),t=r=>{r.getLayers().forEach(o=>{o.get("displayInLayerSwitcher")!==!1&&(e.has(o.get("title"))||(o.getLayers?t(o):o.setVisible(!1)))})};this.overlayGroup&&t(this.overlayGroup),console.log("[MapView] Reset overlays — all hidden")}_wireLayerSwitcherVisibilityHooks(e){const t=()=>this._refreshLayerSwitcherChrome(e),r=a=>{a._lsVisHooked||(a._lsVisHooked=!0,a.on("change:visible",t))},o=a=>{a.getLayers().forEach(i=>{i.getLayers?(o(i),a._lsAddHooked||(a._lsAddHooked=!0,a.getLayers().on("add",s=>{const l=s.element;l.getLayers?o(l):r(l),t()}))):r(i)})};this.overlayGroup&&o(this.overlayGroup)}_attachOnlineOnlyHandler(e,t){e.set("onlineOnly",!0),e.on("change:visible",()=>{e.getVisible()&&!navigator.onLine&&F(`"${t}" requires an internet connection. Connect to view this layer.`,"info",5e3)})}_createLegendPanel(){this._legendPanel=document.createElement("div"),this._legendPanel.className="map-legend-panel",this._legendPanel.style.cssText=`
|
||
position:absolute;right:10px;bottom:40px;z-index:900;
|
||
display:none;flex-direction:column;gap:6px;
|
||
background:var(--card, #fff);color:var(--card-foreground, #1e1a4b);
|
||
border:1px solid var(--border, #1e1a4b1f);border-radius:8px;
|
||
box-shadow:0 4px 12px rgba(0,0,0,0.15);
|
||
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 Bo}_registerLegend(e,t,r){if(!this._legendPanel)return;const o=document.createElement("div");o.className="map-legend-entry",o.style.cssText="border-bottom:1px solid var(--border, #1e1a4b1f);padding-bottom:6px;",o.innerHTML=`
|
||
<div style="font-weight:600;font-size:11px;margin-bottom:4px;line-height:1.3;">
|
||
${this._escapeHtml(t)}
|
||
</div>
|
||
<img src="${r}" alt="${this._escapeHtml(t)} legend"
|
||
style="display:block;max-width:100%;height:auto;border-radius:3px;"
|
||
onerror="this.style.display='none'" />
|
||
`,this._legendEntries.set(e,o);const a=()=>{try{this._updateLegendPanel()}catch(i){console.warn("[MapView] legend panel update failed:",i)}};e.on("change:visible",a),a()}_updateLegendPanel(){if(!this._legendPanel)return;const e=[];for(const[t,r]of this._legendEntries)t.getVisible()&&e.push(r);this._legendEntries.forEach(t=>{t.style.borderBottom="1px solid var(--border, #1e1a4b1f)",t.style.paddingBottom="6px"}),e.length>0&&(e[e.length-1].style.borderBottom="none",e[e.length-1].style.paddingBottom="0"),this._legendPanel.replaceChildren(...e),this._legendPanel.style.display=e.length>0?"flex":"none"}_escapeHtml(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}getLayerGroup(e){let t=null;return this.overlayGroup.getLayers().forEach(r=>{r.get("layerId")===e&&(t=r)}),t}getLayerGroupByTitle(e){let t=null;return this.overlayGroup.getLayers().forEach(r=>{r.get("title")===e&&(t=r)}),t}getOverlayGroup(){return this.overlayGroup}getMap(){return this.map}getCurrentViewExtent(){const e=this.map.getView(),t=this.map.getSize();return t?e.calculateExtent(t):null}getDistrictBoundaryExtent(){let e=null;const t=r=>{r.getLayers().forEach(o=>{if(o.getLayers)t(o);else if(o.get("title")==="District Boundary"){const a=o.getSource&&o.getSource();if(a&&typeof a.getExtent=="function"){const i=a.getExtent();i&&Number.isFinite(i[0])&&(e={extent:i,title:o.get("title")})}}})};return t(this.overlayGroup),e}getMarkerSource(){return this.markerSource}getMarkersLayer(){return this.markersLayer}updateSize(){this.map.updateSize()}onSearchSelect(e){this.searchSelectCallbacks.push(e)}navigateTo(e,t,r=14,o=500){const a=ee([e,t]);this.map.getView().animate({center:a,zoom:r,duration:o})}}class Ri{constructor(e,t={}){this.map=e,this.options=t,this.measureSource=new G,this.measureLayer=new D({source:this.measureSource,style:this.getMeasureStyle(),title:"Measurements",zIndex:100,selectable:!1}),this.drawSource=new G,this.drawLayer=new D({source:this.drawSource,style:this.getDrawStyle(),title:"Draw sketches",displayInLayerSwitcher:!1,zIndex:99});const r=this.map.getLayers();let o=r.getArray().findIndex(a=>a.get("title")==="Overlays");o<0&&(o=r.getLength()),r.insertAt(o,this.drawLayer),r.insertAt(o,this.measureLayer),this.activeInteraction=null,this.measureTooltip=null,this.measureTooltipElement=null,this.onMeasureCompleteCallbacks=[],this.onDrawCompleteCallbacks=[]}getMeasureStyle(){return new M({fill:new C({color:"rgba(255, 233, 106, 0.2)"}),stroke:new P({color:"#8B008B",lineDash:[10,10],width:2}),image:new he({radius:5,stroke:new P({color:"#8B008B"}),fill:new C({color:"rgba(255, 233, 106, 0.5)"})})})}getDrawStyle(){return new M({fill:new C({color:"rgba(255, 233, 106, 0.3)"}),stroke:new P({color:"#8B008B",width:2}),image:new he({radius:6,stroke:new P({color:"#8B008B",width:2}),fill:new C({color:"#FFE96A"})})})}createMeasureTooltip(){this.measureTooltipElement&&this.measureTooltipElement.parentNode.removeChild(this.measureTooltipElement),this.measureTooltipElement=document.createElement("div"),this.measureTooltipElement.className="measure-tooltip",this.measureTooltip=new Ee({element:this.measureTooltipElement,offset:[15,0],positioning:"center-left",stopEvent:!1}),this.map.addOverlay(this.measureTooltip)}setCircleRadius(e,t){const r=e.getGeometry();if(!r||typeof r.setRadius!="function")return;const o=r.getCenter(),a=Math.max(1,Number(t)||0),i=ri(a,o);r.setRadius(i),e.set("_radius",i);const s=e.get("_radiusLine");s&&s.setGeometry(new de([o,[o[0]+i,o[1]]])),this._refreshRadiusAdjuster(e)}getCircleGroundRadius(e){const t=e.getGeometry();return go(t.getRadius(),t.getCenter())}_attachRadiusAdjuster(e,t,r){if(r){t.setPosition(t.getPosition()),r.classList.add("measure-tooltip-adjust"),r.innerHTML=`
|
||
<div class="mt-row">
|
||
<button type="button" class="mt-step" data-dir="-1" aria-label="Smaller radius">−</button>
|
||
<button type="button" class="mt-value" aria-label="Type an exact radius"></button>
|
||
<button type="button" class="mt-step" data-dir="1" aria-label="Larger radius">+</button>
|
||
</div>
|
||
<div class="mt-area"></div>`;for(const o of["pointerdown","pointerup","click","dblclick","touchstart"])r.addEventListener(o,a=>a.stopPropagation());r.querySelectorAll(".mt-step").forEach(o=>{o.addEventListener("click",()=>{const a=this.getCircleGroundRadius(e),i=ai(a),s=Number(o.dataset.dir),l=Math.round(a/i)*i,c=Math.abs(l-a)>.01&&Math.sign(l-a)===s?l:l+s*i;this.setCircleRadius(e,c)})}),r.querySelector(".mt-value").addEventListener("click",()=>{this._promptRadius(e,r)}),e.set("_adjustEl",r),this._refreshRadiusAdjuster(e)}}_promptRadius(e,t){const r=t.querySelector(".mt-value");if(!r||t.querySelector(".mt-input"))return;const o=this.getCircleGroundRadius(e),a=document.createElement("input");a.type="number",a.className="mt-input",a.inputMode="decimal",a.min="1",a.step="any",a.value=String(Math.round(o*100)/100),a.setAttribute("aria-label","Radius in metres"),r.replaceWith(a),a.focus(),a.select();const i=s=>{a.isConnected&&(s&&this.setCircleRadius(e,parseFloat(a.value)),a.replaceWith(r),this._refreshRadiusAdjuster(e))};a.addEventListener("keydown",s=>{s.key==="Enter"&&i(!0),s.key==="Escape"&&i(!1)}),a.addEventListener("blur",()=>i(!0))}_refreshRadiusAdjuster(e){const t=e.get("_adjustEl");if(!t)return;const r=this.getCircleGroundRadius(e),o=t.querySelector(".mt-value");o&&(o.textContent=ut(r));const a=t.querySelector(".mt-area");a&&(a.textContent=We(Math.PI*r*r))}deactivate(){this.activeInteraction&&(this.map.removeInteraction(this.activeInteraction),this.activeInteraction=null),this.measureTooltip&&(this.map.removeOverlay(this.measureTooltip),this.measureTooltip=null),this.measureTooltipElement&&this.measureTooltipElement.parentNode&&(this.measureTooltipElement.parentNode.removeChild(this.measureTooltipElement),this.measureTooltipElement=null)}startCircleMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new Oe({source:this.measureSource,type:"Circle",style:new M({fill:new C({color:"rgba(255, 233, 106, 0.2)"}),stroke:new P({color:"rgba(139, 0, 139, 0.7)",lineDash:[10,10],width:2}),image:new he({radius:5,stroke:new P({color:"rgba(139, 0, 139, 0.7)"}),fill:new C({color:"rgba(255, 233, 106, 0.5)"})})})});this.activeInteraction=e,this.map.addInteraction(e);let t;return e.on("drawstart",r=>{t=r.feature.getGeometry().on("change",a=>{const i=a.target;if(i instanceof Jr){const s=go(i.getRadius(),i.getCenter()),l=ni(s),d=`<strong>${ut(s)}</strong><br><small>${l}</small>`;this.measureTooltipElement.innerHTML=d,this.measureTooltip.setPosition(i.getLastCoordinate())}})}),e.on("drawend",r=>{const o=r.feature,a=o.getGeometry(),i=a.getCenter(),s=a.getRadius();o.set("_layerType","measure_circle"),o.set("_radius",s),o.set("_center",i);const l=new fe({geometry:new de([i,[i[0]+s,i[1]]])});l.set("_layerType","measure_circle_radius"),this.measureSource.addFeature(l),o.set("_radiusLine",l),this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltip.setOffset([0,-7]),this._attachRadiusAdjuster(o,this.measureTooltip,this.measureTooltipElement),this.measureTooltipElement=null,this.createMeasureTooltip(),zt(t);const c={type:"circle",center:i,radius:s,area:Math.PI*s*s,feature:o};this.onMeasureCompleteCallbacks.forEach(d=>d(c))}),e}startLineMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new Oe({source:this.measureSource,type:"LineString",style:this.getMeasureStyle()});this.activeInteraction=e,this.map.addInteraction(e);let t;return e.on("drawstart",r=>{t=r.feature.getGeometry().on("change",a=>{const i=a.target,s=Pt(i),l=ut(s);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(i.getLastCoordinate())})}),e.on("drawend",r=>{const o=r.feature,a=o.getGeometry(),i=Pt(a);this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),zt(t);const s={type:"line",length:i,feature:o};this.onMeasureCompleteCallbacks.forEach(l=>l(s))}),e}startAreaMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new Oe({source:this.measureSource,type:"Polygon",style:this.getMeasureStyle()});this.activeInteraction=e,this.map.addInteraction(e);let t;return e.on("drawstart",r=>{t=r.feature.getGeometry().on("change",a=>{const i=a.target,s=dt(i),l=We(s);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(i.getInteriorPoint().getCoordinates())})}),e.on("drawend",r=>{const o=r.feature,a=o.getGeometry(),i=dt(a);o.set("_layerType","measure_area"),o.set("_area",i),this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),zt(t);const s={type:"polygon",area:i,feature:o,coordinate:a.getInteriorPoint().getCoordinates()};this.onMeasureCompleteCallbacks.forEach(l=>l(s))}),e}startDrawPoint(){this.deactivate();const e=new Oe({source:this.drawSource,type:"Point",style:this.getDrawStyle()});return this.activeInteraction=e,this.map.addInteraction(e),e.on("drawend",t=>{const r={type:"point",feature:t.feature};this.onDrawCompleteCallbacks.forEach(o=>o(r))}),e}startDrawLine(){this.deactivate();const e=new Oe({source:this.drawSource,type:"LineString",style:this.getDrawStyle()});return this.activeInteraction=e,this.map.addInteraction(e),e.on("drawend",t=>{const r={type:"line",feature:t.feature};this.onDrawCompleteCallbacks.forEach(o=>o(r))}),e}startDrawPolygon(){this.deactivate();const e=new Oe({source:this.drawSource,type:"Polygon",style:this.getDrawStyle()});return this.activeInteraction=e,this.map.addInteraction(e),e.on("drawend",t=>{const r={type:"polygon",feature:t.feature};this.onDrawCompleteCallbacks.forEach(o=>o(r))}),e}clearMeasurements(){this.measureSource.clear(),document.querySelectorAll(".measure-tooltip-static").forEach(t=>t.parentNode.removeChild(t))}clearDrawings(){this.drawSource.clear()}clearAll(){this.clearMeasurements(),this.clearDrawings()}onMeasureComplete(e){this.onMeasureCompleteCallbacks.push(e)}onDrawComplete(e){this.onDrawCompleteCallbacks.push(e)}createControlBar(e={}){e.position;const t=new uo({group:!0,className:"map-tools-bar"}),r=new uo({toggleOne:!0,group:!0}),o=new Se({html:'<span class="tool-icon">⭕</span>',title:"Measure Circle (radius & area)",className:"measure-circle-btn",onToggle:l=>{l?this.startCircleMeasure():this.deactivate()}});r.addControl(o);const a=new Se({html:'<span class="tool-icon">📏</span>',title:"Measure Distance",className:"measure-line-btn",onToggle:l=>{l?this.startLineMeasure():this.deactivate()}});r.addControl(a);const i=new Se({html:'<span class="tool-icon">⬛</span>',title:"Measure Area",className:"measure-area-btn",onToggle:l=>{l?this.startAreaMeasure():this.deactivate()}});r.addControl(i);const s=new lt({html:'<span class="tool-icon">🗑️</span>',title:"Clear Measurements",className:"clear-measure-btn",handleClick:()=>{this.clearMeasurements(),o.setActive(!1),a.setActive(!1),i.setActive(!1)}});return r.addControl(s),t.addControl(r),t}getMeasureLayer(){return this.measureLayer}getDrawLayer(){return this.drawLayer}getMeasureSource(){return this.measureSource}getDrawSource(){return this.drawSource}isActive(){return this.activeInteraction!==null}}let Re=null;async function Ni(){if(!("serviceWorker"in navigator))return console.warn("[PWA] Service Workers not supported"),null;try{return Re=await navigator.serviceWorker.register("/sw.js",{scope:"/"}),console.log("[PWA] Service Worker registered:",Re.scope),Re.addEventListener("updatefound",()=>{const n=Re.installing;n.addEventListener("statechange",()=>{n.state==="installed"&&navigator.serviceWorker.controller&&(console.log("[PWA] New version available"),ji())})}),Re}catch(n){return console.error("[PWA] Service Worker registration failed:",n),null}}let je=null,_e=null;function $i(n="#install-btn"){if(_e=typeof n=="string"?document.querySelector(n):n,!_e){console.warn("[PWA] Install button not found:",n);return}_e.style.display="none",window.addEventListener("beforeinstallprompt",e=>{e.preventDefault(),je=e,_e.style.display="block",console.log("[PWA] Install prompt ready")}),_e.addEventListener("click",async()=>{if(!je){Bi();return}je.prompt();const{outcome:e}=await je.userChoice;console.log("[PWA] Install prompt outcome:",e),je=null,_e.style.display="none"}),window.addEventListener("appinstalled",()=>{console.log("[PWA] App installed"),je=null,_e.style.display="none"}),window.matchMedia("(display-mode: standalone)").matches&&(_e.style.display="none")}function Bi(){const n=/iPad|iPhone|iPod/.test(navigator.userAgent),e=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);let t=`To install this app:
|
||
|
||
`;n?(t+=`1. Tap the Share button (square with arrow)
|
||
`,t+='2. Scroll down and tap "Add to Home Screen"'):e?(t+=`1. Click File menu
|
||
`,t+='2. Click "Add to Dock"'):(t+=`1. Click the menu button (three dots)
|
||
`,t+='2. Click "Install" or "Add to Home Screen"'),alert(t)}let yo=null;const bo=new Set;function Gi(n="#offline-indicator"){yo=typeof n=="string"?document.querySelector(n):n,Xt(!navigator.onLine),window.addEventListener("online",()=>{console.log("[PWA] Back online"),Xt(!1),sn(!1)}),window.addEventListener("offline",()=>{console.log("[PWA] Gone offline"),Xt(!0),sn(!0)})}function Xt(n){yo&&(yo.style.display=n?"block":"none"),document.body.classList.toggle("is-offline",n)}function gr(n){return bo.add(n),n(!navigator.onLine),()=>bo.delete(n)}function sn(n){for(const e of bo)try{e(n)}catch(t){console.error("[PWA] Offline listener error:",t)}}function K(){return navigator.onLine}function ji(){confirm("A new version is available. Reload now?")&&Ui()}function Ui(){Re?.waiting&&Re.waiting.postMessage({type:"SKIP_WAITING"}),window.location.reload()}async function qi({timeoutMs:n=1e4}={}){if(!("serviceWorker"in navigator))throw new Error("Service Workers not supported in this browser");if(navigator.serviceWorker.controller)return navigator.serviceWorker.controller;const e=navigator.serviceWorker.ready,t=new Promise((a,i)=>setTimeout(()=>i(new Error("Service-worker readiness timeout")),n)),r=await Promise.race([e,t]),o=navigator.serviceWorker.controller||r.active;if(!o)throw new Error("No active service worker available");return o}function zi(n){if(!("serviceWorker"in navigator))return()=>{};const e=()=>{try{n()}catch(t){console.error("[PWA] controllerchange handler error:",t)}};return navigator.serviceWorker.addEventListener("controllerchange",e),()=>navigator.serviceWorker.removeEventListener("controllerchange",e)}async function Co(n,e,t={},r=5e3,o=1e4){const a=await qi({timeoutMs:o});return new Promise((i,s)=>{const l=new MessageChannel,c=setTimeout(()=>{l.port1.close(),s(new Error(`Service-worker reply "${e}" timed out`))},r);l.port1.onmessage=d=>{if(d.data?.type===e){clearTimeout(c),l.port1.close();const{type:u,...p}=d.data;i(p)}},a.postMessage({type:n,...t},[l.port2])})}async function Hi(){try{return(await Co("GET_TILE_STATS","TILE_STATS")).stats}catch(n){return console.warn("[PWA] getTileCacheStats failed:",n),null}}async function Wi(){try{return await Co("CLEAR_TILE_CACHES","TILE_CACHES_CLEARED"),!0}catch(n){return console.warn("[PWA] clearTileCaches failed:",n),!1}}async function Vi(n){if(!n)return!1;try{return!!(await Co("CLEAR_TILE_CACHE","TILE_CACHE_CLEARED",{cacheName:n})).deleted}catch(e){return console.warn(`[PWA] clearTileCacheForProvider(${n}) failed:`,e),!1}}async function Ki(){if(!navigator.storage?.estimate)return null;try{const{usage:n,quota:e}=await navigator.storage.estimate();return{usage:n||0,quota:e||0}}catch(n){return console.warn("[PWA] getStorageEstimate failed:",n),null}}async function Xi(n={}){const{installButton:e="#install-btn",offlineIndicator:t="#offline-indicator",autoRegisterSW:r=!0}=n;r&&await Ni(),$i(e),Gi(t),console.log("[PWA] Initialized")}const mr={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"}},Yi=30*1024,Rt=2*Math.PI*6378137/2;function ln(n,e){const t=n/Rt*180;let r=e/Rt*180;return r=180/Math.PI*(2*Math.atan(Math.exp(r*Math.PI/180))-Math.PI/2),[t,r]}function cn(n,e,t){const r=Math.pow(2,t),o=Math.floor((n+180)/360*r),a=e*Math.PI/180,i=Math.floor((1-Math.log(Math.tan(a)+1/Math.cos(a))/Math.PI)/2*r);return{x:o,y:i}}function yr(n,e){const[t,r,o,a]=n,[i,s]=ln(t,r),[l,c]=ln(o,a),d=cn(i,c,e),u=cn(l,s,e),p=Math.pow(2,e),f=Math.max(0,Math.min(d.x,u.x)),h=Math.min(p-1,Math.max(d.x,u.x)),g=Math.max(0,Math.min(d.y,u.y)),m=Math.min(p-1,Math.max(d.y,u.y));return{z:e,minX:f,maxX:h,minY:g,maxY:m,count:(h-f+1)*(m-g+1)}}function Ji(n,e,t){let r=0;for(let o=e;o<=t;o++)r+=yr(n,o).count;return r}function Zi(n,e,t){const r=[];for(let o=e;o<=t;o++){const a=yr(n,o);for(let i=a.minX;i<=a.maxX;i++)for(let s=a.minY;s<=a.maxY;s++)r.push({z:o,x:i,y:s})}return r}function Qi(n,{z:e,x:t,y:r}){return n.replace("{z}",e).replace("{x}",t).replace("{y}",r)}class es{constructor({baseMap:e,extent3857:t,minZoom:r,maxZoom:o,concurrency:a=2,interBatchDelayMs:i=50,onProgress:s=()=>{}}){const l=mr[e];if(!l)throw new Error(`Unknown base map: ${e}`);o>l.maxZoom&&(console.warn(`[OfflineTiles] ${e}: maxZoom ${o} > supported ${l.maxZoom}; clamping`),o=l.maxZoom),this.baseMap=e,this.template=l.url,this.extent=t,this.minZoom=r,this.maxZoom=o,this.concurrency=Math.max(1,Math.min(a,6)),this.interBatchDelayMs=i,this.onProgress=s,this._abortCtrl=null,this._cancelled=!1}async start(){if(this._abortCtrl)throw new Error("Downloader already started");this._abortCtrl=new AbortController,this._cancelled=!1;const e=Zi(this.extent,this.minZoom,this.maxZoom),t=e.length,r=Date.now();let o=0,a=0,i=0,s=0;const l=c=>{const d=Date.now()-r,u=o>0?Math.round(d/o*(t-o)):null;this.onProgress({phase:c,done:o,total:t,ok:a,failed:i,cached:s,elapsedMs:d,etaMs:u})};l("running");for(let c=0;c<e.length&&!this._cancelled;c+=this.concurrency){const d=e.slice(c,c+this.concurrency);await Promise.all(d.map(async u=>{if(this._cancelled)return;const p=Qi(this.template,u);try{const f=await fetch(p,{signal:this._abortCtrl.signal,cache:"default"});f.ok?(a++,f.body&&f.body.cancel().catch(()=>{})):(f.status,i++)}catch(f){f.name==="AbortError"||i++}o++})),l("running"),this.interBatchDelayMs>0&&c+this.concurrency<e.length&&await new Promise(u=>setTimeout(u,this.interBatchDelayMs))}return l(this._cancelled?"cancelled":"done"),{phase:this._cancelled?"cancelled":"done",done:o,total:t,ok:a,failed:i,cached:s,elapsedMs:Date.now()-r}}cancel(){this._cancelled=!0,this._abortCtrl&&this._abortCtrl.abort()}}const ts=(()=>{const n=(r,o)=>{const a=r*Rt/180,i=Math.log(Math.tan((90+o)*Math.PI/360))/(Math.PI/180);return[a,i*Rt/180]},e=n(-3.3,4.5),t=n(1.2,11.2);return[e[0],e[1],t[0],t[1]]})();function os(n){return n*Yi}const br="https://api.lupmis4luspa.org/api",wr=`${br}/spatial_planning`;function ns(n){return n.includes("/")?`${br}/${n}`:`${wr}/${n}`}const Yt="1",rs="1c46538c712e9b5b";function as(){try{if(typeof window>"u")return Yt;const n=window.LUPMIS_SESSION;if(!n||typeof n!="object")return Yt;const e=n.district_id;return e==null||String(e).length===0?null:String(e)}catch{}return Yt}const Ao={get district_id(){return as()},api_token:rs};function Ye(){if(typeof window<"u"&&window.LUPMIS_SESSION&&window.LUPMIS_SESSION.user_id)return window.LUPMIS_SESSION;try{const n=localStorage.getItem("dev-session");if(n){const e=JSON.parse(n);if(e&&e.user_id)return e}}catch{}return null}typeof window<"u"&&(window.lupmisDevSession=n=>{n==null?(localStorage.removeItem("dev-session"),console.log("[Dev] Session override cleared. Reload to apply.")):(localStorage.setItem("dev-session",JSON.stringify(n)),console.log("[Dev] Session override saved. Reload to apply:",n))});const is=3e4,ss=5e3;let De=null;async function ls(n=!1){if(De!==null&&!n)return De;const e=new AbortController,t=setTimeout(()=>e.abort(),ss);try{De=(await fetch(`${wr}/get_layers.php`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(Ao),signal:e.signal})).ok}catch{De=!1}finally{clearTimeout(t)}return console.log("[RemoteDB] Server reachable:",De),De}function ke(){return De}function cs(n,e=is){const t=new AbortController,r=setTimeout(()=>t.abort(),e);return n.signal&&n.signal.addEventListener("abort",()=>t.abort()),{signal:t.signal,clear:()=>clearTimeout(r)}}async function me(n,e={},t={}){const r=ns(n),o={...Ao,...e};console.log("[RemoteDB] POST",r);const a=cs(t);try{const i=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(o),...t,signal:a.signal});if(!i.ok)throw new Error(`HTTP ${i.status}: ${i.statusText}`);const s=await i.json();return console.log("[RemoteDB] POST response:",n,"→",typeof s=="object"?`${Array.isArray(s)?s.length+" items":"object"}`:s),s}catch(i){throw i.name==="AbortError"?(console.error("[RemoteDB] POST timed out:",n),new Error(`Request timed out: ${n}`)):(console.error("[RemoteDB] POST failed:",n,i),i)}finally{a.clear()}}async function ds(){return me("get_district_boundary.php")}async function us(){return me("get_layers.php")}async function ps(){return me("get_all_collector_zone_per_district.php")}async function fs(){return me("get_parcels_per_district.php")}async function hs(){return me("get_all_footprint_per_district.php")}async function gs(){const n=Ao.district_id;return me("contours/get_by_district.php",{districtid:n==null?null:Number(n)})}async function ms(){return me("get_osm_roads.php")}async function ys(){return me("get_upn_grid_per_district.php")}async function bs(n,e){const t={client_uuid:n.client_uuid,name:n.name??null,started_at:n.started_at,ended_at:n.ended_at,point_count:n.point_count??e.length,distance_m:n.distance_m??0,points:(e||[]).map(o=>({seq:o.seq,longitude:o.longitude,latitude:o.latitude,altitude:o.altitude??null,accuracy:o.accuracy??null,altitude_accuracy:o.altitude_accuracy??null,heading:o.heading??null,speed:o.speed??null,satellites:o.satellites??null,recorded_at:o.recorded_at}))},r=await me("save_gps_trail.php",t);return{remoteId:r?.id??r?.remote_id??null}}const ws=63710088e-1,Ne=Math.PI/180;function _s(n,e,t,r){const o=(r-e)*Ne,a=(t-n)*Ne,i=Math.sin(o/2)**2+Math.cos(e*Ne)*Math.cos(r*Ne)*Math.sin(a/2)**2;return 2*ws*Math.asin(Math.min(1,Math.sqrt(i)))}function dn(n,e=5){return n==null||Number.isNaN(n)?"—":n.toFixed(e)}const un=6378137,pn=1/298.257223563,fn=.9996,te=pn*(2-pn),Jt=te/(1-te);function vs(n,e){if(n==null||e==null||Number.isNaN(n)||Number.isNaN(e))return null;let t=((e+180)%360+360)%360-180;const r=Math.floor((t+180)/6)+1,o=(r-1)*6-180+3,a=n*Ne,i=t*Ne,s=o*Ne,l=Math.sin(a),c=Math.cos(a),d=Math.tan(a),u=un/Math.sqrt(1-te*l*l),p=d*d,f=Jt*c*c,h=c*(i-s),g=un*((1-te/4-3*te**2/64-5*te**3/256)*a-(3*te/8+3*te**2/32+45*te**3/1024)*Math.sin(2*a)+(15*te**2/256+45*te**3/1024)*Math.sin(4*a)-35*te**3/3072*Math.sin(6*a));let m=fn*u*(h+(1-p+f)*h**3/6+(5-18*p+p*p+72*f-58*Jt)*h**5/120)+5e5,y=fn*(g+u*d*(h*h/2+(5-p+9*f+4*f*f)*h**4/24+(61-58*p+p*p+600*f-330*Jt)*h**6/720));const w=n>=0?"N":"S";return w==="S"&&(y+=1e7),{zone:r,hemisphere:w,easting:Math.round(m),northing:Math.round(y)}}function Es(n,e){const t=vs(n,e);return t?`${t.zone}${t.hemisphere} ${t.easting}E ${t.northing}N`:"—"}function xs(n){return n==null||Number.isNaN(n)?"—":n<1e3?`${Math.round(n)} m`:`${(n/1e3).toFixed(2)} km`}function Ss(n){return n==null||Number.isNaN(n)?"—":`±${Math.round(n)} m`}function Ls(n){return n==null||Number.isNaN(n)?"none":n<=10?"good":n<=30?"fair":"poor"}const Ts={minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0,timeoutMs:15e3,maximumAgeMs:0};class pt{constructor(e={}){this.opts={...Ts,...e},this.storage=e.storage||null,this.sync=e.sync||null,this._geo=e.geolocation||(typeof navigator<"u"?navigator.geolocation:null),this._state="idle",this._watchId=null,this._live=!1,this._recording=!1,this._activeTrailId=null,this._activeTrailUuid=null,this._lastRecorded=null,this._lastRecordedAt=0,this._distanceM=0,this._pointCount=0,this._lastFix=null,this._listeners=Object.create(null)}on(e,t){return(this._listeners[e]||(this._listeners[e]=new Set)).add(t),()=>this._listeners[e]?.delete(t)}_emit(e,t){const r=this._listeners[e];if(r)for(const o of r)try{o(t)}catch(a){console.error(`[GeoTracker] listener for "${e}" threw`,a)}}get state(){return this._state}get isRecording(){return this._recording}get lastFix(){return this._lastFix}get isSupported(){return!!this._geo}_setState(e){this._state!==e&&(this._state=e,this._emit("statechange",e))}startLive(){if(!this._geo){this._emit("error",new Error("Geolocation not supported"));return}this._live=!0,this._ensureWatch()}stopLive(){this._live=!1,this._recording||this._teardownWatch()}getCurrentPosition(){return new Promise((e,t)=>{if(!this._geo){t(new Error("Geolocation not supported"));return}this._geo.getCurrentPosition(r=>{const o=pt.normalize(r);this._lastFix=o,this._emit("position",o),e(o)},r=>{this._emit("error",r),t(r)},{enableHighAccuracy:this.opts.enableHighAccuracy,timeout:this.opts.timeoutMs,maximumAge:this.opts.maximumAgeMs})})}async startRecording(e={}){if(!this._geo)throw new Error("Geolocation not supported");if(!this.storage)throw new Error("GeoTracker: no storage adapter configured");if(this._recording)return{trailId:this._activeTrailId,uuid:this._activeTrailUuid};const t=pt.uuid(),r=new Date().toISOString(),o={uuid:t,name:e.name||null,startedAt:r,...e},a=await this.storage.createTrail(o);return this._activeTrailId=a,this._activeTrailUuid=t,this._lastRecorded=null,this._lastRecordedAt=0,this._distanceM=0,this._pointCount=0,this._recording=!0,this._ensureWatch(),this._setState("recording"),this._emit("trailstart",{trailId:a,uuid:t,startedAt:r}),{trailId:a,uuid:t}}async stopRecording(){if(!this._recording)return null;const e=this._activeTrailId,r={endedAt:new Date().toISOString(),pointCount:this._pointCount,distanceM:this._distanceM};this._recording=!1,this._live||this._teardownWatch(),this._setState(this._live?"watching":"idle");try{await this.storage.finishTrail(e,r)}catch(a){this._emit("error",a)}this._emit("trailstop",{trailId:e,...r});let o=!1;if(this.sync)try{o=await this._syncTrail(e)}catch(a){this._emit("error",a)}return this._activeTrailId=null,this._activeTrailUuid=null,{trailId:e,pointCount:r.pointCount,distanceM:r.distanceM,synced:o}}async syncPending(){if(!this.sync||!this.storage)return{pushed:0,failed:0};if(this.sync.isOnline&&!this.sync.isOnline())return{pushed:0,failed:0};let e=0,t=0;const r=await this.storage.getUnsyncedTrails();for(const o of r)try{await this._syncTrail(o.id??o.trailId,o)?e++:t++}catch(a){t++,this._emit("error",a)}return this._emit("syncstatus",{pushed:e,failed:t}),{pushed:e,failed:t}}async _syncTrail(e,t){const r=await this.storage.getTrailPoints(e),o=t||{id:e},a=await this.sync.pushTrail(o,r),i=a&&(a.remoteId??a.id??null);return await this.storage.markTrailSynced(e,i),!0}_ensureWatch(){if(this._watchId!=null||!this._geo){this._state==="idle"&&this._live&&this._setState("watching");return}this._watchId=this._geo.watchPosition(e=>this._onFix(e),e=>this._emit("error",e),{enableHighAccuracy:this.opts.enableHighAccuracy,timeout:this.opts.timeoutMs,maximumAge:this.opts.maximumAgeMs}),this._recording||this._setState("watching")}_teardownWatch(){this._watchId!=null&&this._geo&&this._geo.clearWatch(this._watchId),this._watchId=null}async _onFix(e){const t=pt.normalize(e);if(this._lastFix=t,this._emit("position",t),!this._recording)return;const{minIntervalMs:r,minDistanceM:o,heartbeatMs:a,maxAccuracyM:i}=this.opts,s=t.timestamp;if(this._lastRecordedAt&&s-this._lastRecordedAt<r||i>0&&t.accuracy!=null&&t.accuracy>i&&this._lastRecorded)return;let l=!1,c=0;if(!this._lastRecorded)l=!0;else{c=_s(this._lastRecorded.lon,this._lastRecorded.lat,t.lon,t.lat);const d=s-this._lastRecordedAt;(c>=o||d>=a)&&(l=!0)}if(l){this._lastRecorded&&(this._distanceM+=c),this._pointCount+=1,this._lastRecorded={lon:t.lon,lat:t.lat,timestamp:s},this._lastRecordedAt=s;try{await this.storage.addPoint(this._activeTrailId,{...t,seq:this._pointCount}),this._emit("point",{trailId:this._activeTrailId,seq:this._pointCount,point:t,distanceM:this._distanceM,pointCount:this._pointCount})}catch(d){this._emit("error",d)}}}static normalize(e){const t=e.coords||{},r=o=>o!=null&&!Number.isNaN(o)?o:null;return{lon:t.longitude,lat:t.latitude,accuracy:r(t.accuracy),altitude:r(t.altitude),altitudeAccuracy:r(t.altitudeAccuracy),heading:r(t.heading),speed:r(t.speed),satellites:null,timestamp:e.timestamp||Date.now()}}static uuid(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{const t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}}const ks={async createTrail(n){const e=n.districtId??Ye()?.district_id??null;return Xa({...n,districtId:e!=null?String(e):null})},addPoint:(n,e)=>Ya(n,e),finishTrail:(n,e)=>Ja(n,e),getUnsyncedTrails:()=>Za(),getTrailPoints:n=>Qa(n),markTrailSynced:(n,e)=>ei(n,e)},Is={pushTrail:(n,e)=>bs(n,e),isOnline:()=>K()},ve=new pt({storage:ks,sync:Is,minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0}),Ps=new Set(["set:view","set:selected","clear:selected","set:basemap"]);function Ms({mapView:n,embedConfig:e}){const t=n.getMap(),r=window.parent&&window.parent!==window?window.parent:null,o=new G,a=new D({source:o,zIndex:9999,style:new M({stroke:new P({color:"#f97316",width:3}),fill:new C({color:"rgba(249,115,22,0.18)"})}),properties:{title:"Permit selection",displayInLayerSwitcher:!1}});t.addLayer(a);let i=null,s=e?.upn?String(e.upn):null,l=!1;function c(m){if(!r){console.warn("[embed-bridge] No parent window — would have sent:",m);return}try{r.postMessage(m,"*")}catch(y){console.warn("[embed-bridge] postMessage failed:",y)}}function d(m,y){c({type:"error",code:m,message:y})}function u(){l||(l=!0,c({type:"ready"}))}function p(m,y,w){const b=m.getProperties();let S=y,L=w;if(S==null||L==null){const E=m.getGeometry()?.getExtent();if(E){const[x,I]=xe(Zr(E));S=x,L=I}}return{type:"parcel:select",upn:b.upn??null,parcel_id:b.id??null,lon:S??null,lat:L??null,zone_code:b.zone_code??null,zone_name:b.zone_name??null,landuse:b.landuse??null,min_height:b.min_height??null,max_height:b.max_height??null}}function f(m){if(o.clear(),m){const y=m.clone();o.addFeature(y)}}n.onClick((m,y,w,b)=>{let S=null;t.forEachFeatureAtPixel(b.pixel,L=>{if(L.get("_layerType")==="parcel")return S=L,!0}),S?(f(S),c(p(S,m,y))):(f(null),c({type:"parcel:cleared"}))}),window.addEventListener("message",m=>{const y=m.data;if(!(!y||typeof y!="object"||!Ps.has(y.type)))try{switch(y.type){case"set:view":{if(typeof y.lon=="number"&&typeof y.lat=="number"){const w=t.getView();w.setCenter(ee([y.lon,y.lat])),typeof y.zoom=="number"&&w.setZoom(y.zoom)}break}case"set:selected":y.upn&&h(String(y.upn));break;case"clear:selected":f(null),s=null;break;case"set:basemap":y.key&&typeof n.setBaseMap=="function"&&n.setBaseMap(y.key);break}}catch(w){d("COMMAND_FAILED",`Failed to handle ${y.type}: ${w.message}`)}});function h(m){if(!i){s=m;return}const w=i.getSource().getFeatures().find(S=>String(S.get("upn")??"")===m);if(!w){s=m;return}s=null,f(w);const b=w.getGeometry()?.getExtent();b&&t.getView().fit(b,{padding:[50,50,50,50],duration:400,maxZoom:17}),c(p(w,null,null))}function g(m){i=m;const y=m.getSource(),w=()=>{queueMicrotask(()=>{s&&h(s),u()})};if(y.getFeatures().length>0)w();else{let b=!1;y.on("addfeature",()=>{b||(b=!0,queueMicrotask(()=>{b=!1,s&&h(s),u()}))})}}if(e?.basemap&&typeof n.setBaseMap=="function"&&n.setBaseMap(e.basemap),typeof e?.lon=="number"&&typeof e?.lat=="number"){const m=t.getView();m.setCenter(ee([e.lon,e.lat])),m.setZoom(typeof e?.zoom=="number"?e.zoom:15)}return{attachParcelsLayer:g,emitError:d}}const Cs=[{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"}],_r={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 As(n){if(!n?.features?.length)return"none";let e=0,t=0,r=0;for(const i of n.features){const s=i?.geometry?.type;s&&(s==="Polygon"||s==="MultiPolygon"?e++:s==="LineString"||s==="MultiLineString"?t++:(s==="Point"||s==="MultiPoint")&&r++)}const o=e+t+r;if(o===0)return"none";const a=Math.max(e,t,r);return a<o*.85?"mixed":e===a?"polygon":t===a?"line":"point"}function vr(n,e=50){const t=new Set,r=Math.min(e,n.features.length);for(let o=0;o<r;o++){const a=n.features[o]?.properties;if(!(!a||typeof a!="object"))for(const i of Object.keys(a))t.add(String(i).toLowerCase())}return t}function Ue(n,...e){for(const t of e)if(n.has(t))return!0;return!1}function Fs(n){const e=As(n);if(e==="none"||e==="mixed")return"other";const t=vr(n);return e==="line"?Ue(t,"osm_id","highway")?"osm_roads":"other":e==="polygon"?Ue(t,"upn","parcel_no","landuse","lu_code","zone_code")&&Ue(t,"upn","parcel_no","landuse","lu_code")?"parcels":Ue(t,"zone_name","colzonename","colzonenr")?"collector_zones":Ue(t,"building","building:levels","building_levels","height","min_height","max_height")&&!Ue(t,"upn","parcel_no")?"building_footprints":"other":"other"}const Os={upn:["upn","unique_parcel_no","parcel_id","pid"],landuse:["landuse","land_use","lu","lu_code"],zone_code:["zone_code","zonecode","zone"],zone_name:["zone_name","zonename","colzonename"],sector:["sector","sec"],block:["block","blk"],parcel_no:["parcel_no","parcelno","plot_no","plotno"],prop_no:["prop_no","propertyno","property_no"],st_name:["st_name","street","street_name","road"],prop_add:["prop_add","address","addr"],fac_name:["fac_name","facility","facilityname"],min_height:["min_height","minheight","h_min"],max_height:["max_height","maxheight","h_max","height"],eff_date:["eff_date","effectivedate","effdate"],lp_name:["lp_name","lpname","localplan"],locality:["locality","town","settlement"],mmda:["mmda","district","assembly"],last_update:["last_update","lastupdate","updated"],remarks:["remarks","notes","comments"],osm_id:["osm_id","osmid","id"],name:["name","street_name","st_name"],highway:["highway","road_class","class"]};function hn(n){return String(n).toLowerCase().replace(/[\s_\-]+/g,"")}function Er(n,e){const t=_r[e]||[];if(t.length===0)return{};const r=vr(n,50),o=new Map;for(const i of r)o.set(hn(i),i);const a={};for(const i of t){const s=Os[i]||[i];let l=null;for(const c of s){const d=hn(c);if(o.has(d)){l=o.get(d);break}}a[i]=l}return a}function Ds(n,e){const t={};for(const[r,o]of Object.entries(e||{}))o!=null&&n&&Object.prototype.hasOwnProperty.call(n,o)&&(t[r]=n[o]);return t}function Rs(n){const e=new Map;for(const t of n.features||[]){const r=t?.properties;if(!(!r||typeof r!="object"))for(const o of Object.keys(r))e.has(o)||e.set(o,!0)}return Array.from(e.keys())}function Ns(n,e=35){const t={};for(const r of n.features||[]){const o=r?.properties;if(!(!o||typeof o!="object"))for(const[a,i]of Object.entries(o)){if(t[a]!==void 0||i==null||i==="")continue;let s;if(typeof i=="object")try{s=JSON.stringify(i)}catch{s=String(i)}else s=String(i);s=s.replace(/\s+/g," ").trim(),s&&(s.length>e&&(s=s.slice(0,e-1)+"…"),t[a]=s)}}return t}const T={};let wo=null,N=null;function $s(){T.root||(T.root=document.getElementById("importMappingModal"),T.filename=document.getElementById("import-modal-filename"),T.summary=document.getElementById("import-modal-summary"),T.target=document.getElementById("import-modal-target"),T.targetHint=document.getElementById("import-modal-target-hint"),T.fieldsWrap=document.getElementById("import-modal-fields-wrap"),T.tbody=document.getElementById("import-modal-fields-tbody"),T.btnSave=document.getElementById("import-modal-save"),T.btnSaveUpload=document.getElementById("import-modal-save-upload"),T.btnCancel=document.getElementById("import-modal-cancel"),T.target&&!T.target.dataset.populated&&(T.target.innerHTML=Cs.map(n=>`<option value="${n.key}">${n.label}</option>`).join(""),T.target.dataset.populated="1"),T.target&&!T.target.dataset.wired&&(T.target.dataset.wired="1",T.target.addEventListener("change",xr)),T.btnSave&&!T.btnSave.dataset.wired&&(T.btnSave.dataset.wired="1",T.btnSave.addEventListener("click",()=>gn("save"))),T.btnSaveUpload&&!T.btnSaveUpload.dataset.wired&&(T.btnSaveUpload.dataset.wired="1",T.btnSaveUpload.addEventListener("click",()=>gn("upload"))),T.root&&!T.root.dataset.wired&&(T.root.dataset.wired="1",T.root.addEventListener("hidden.bs.modal",()=>{N?.onResult&&!N._resolved&&(N._resolved=!0,N.onResult({action:"cancel"})),N=null})))}function Bs(){const n=N.targetType,e=_r[n]||[];if(n==="other"||e.length===0){T.fieldsWrap.style.display="none";return}T.fieldsWrap.style.display="";const t=o=>{const a=N.sourceSamples[o];return a?` — [${It(a)}]`:" — [(empty)]"},r=['<option value="">(none)</option>'].concat(N.sourceFields.map(o=>`<option value="${xt(o)}">${It(js(o))}${t(o)}</option>`)).join("");T.tbody.innerHTML=e.map(o=>{const a=N.mapping[o]||"",i=r.replace(`<option value="${xt(a)}">`,`<option value="${xt(a)}" selected>`);return`
|
||
<tr>
|
||
<td><code>${It(o)}</code></td>
|
||
<td>
|
||
<select class="form-select form-select-sm import-field-map"
|
||
data-col="${xt(o)}">
|
||
${i}
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
`}).join(""),T.tbody.querySelectorAll(".import-field-map").forEach(o=>{o.addEventListener("change",a=>{const i=a.target.dataset.col;N.mapping[i]=a.target.value||null})})}function xr(){const n=T.target.value;N.targetType=n,N.mapping=Er(N.fc,n),n==="other"?(T.targetHint.innerHTML="<em>This dataset will be visible on the map but cannot be uploaded to the database. You can change the type later.</em>",T.btnSave.disabled=!1,T.btnSaveUpload.disabled=!0):(T.targetHint.innerHTML="Each LUPMIS2 column is matched to a source field where possible. You can override any choice below.",T.btnSave.disabled=!1,T.btnSaveUpload.disabled=!1),Bs()}function gn(n){if(!N||N._resolved)return;N._resolved=!0;const{targetType:e,mapping:t,onResult:r}=N;wo.hide(),r&&r({action:n,targetType:e,mapping:e==="other"?null:{...t}}),N=null}function Gs(n){if($s(),!T.root){console.warn("[ImportModal] Modal element missing — calling onResult with cancel"),n.onResult?.({action:"cancel"});return}const e=n.fc,t=e?.features?.length??0,r=Fs(e);N={importId:n.importId,filename:n.filename,fc:e,sourceFields:Rs(e),sourceSamples:Ns(e),targetType:r,mapping:Er(e,r),onResult:n.onResult,_resolved:!1},T.filename.textContent=n.filename||"imported dataset",T.summary.textContent=`— ${t} feature${t===1?"":"s"}`,T.target.value=N.targetType,xr(),wo=gt.getOrCreateInstance(T.root),wo.show()}function It(n){return String(n).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function xt(n){return It(n)}function js(n){let e="";for(const t of String(n)){const r=t.codePointAt(0);r>=97&&r<=122?e+=String.fromCodePoint(r-97+119834):r>=65&&r<=90?e+=String.fromCodePoint(r-65+119808):r>=48&&r<=57?e+=String.fromCodePoint(r-48+120782):e+=t}return e}var nt={},Zt={},mn;function Fo(){return mn||(mn=1,Zt.geometries={NULL:0,POINT:1,POLYLINE:3,POLYGON:5,MULTIPOINT:8,POINTZ:11,POLYLINEZ:13,POLYGONZ:15,MULTIPOINTZ:18,POINTM:21,POLYLINEM:23,POLYGONM:25,MULTIPOINTM:28,MULTIPATCH:31}),Zt}var Qt={},eo,yn;function Sr(){return yn||(yn=1,eo={C:254,L:1,D:8,N:18,M:18,F:18,B:8}),eo}var rt={},bn;function Us(){return bn||(bn=1,rt.lpad=function(e,t,r){for(;e.length<t;)e=r+e;return e},rt.rpad=function(e,t,r){for(;e.length<t;)e=e+r;return e},rt.writeField=function(e,t,r,o){for(var a=0;a<t;a++)e.setUint8(o,r.charCodeAt(a)),o++;return o}),rt}var at={},wn;function qs(){if(wn)return at;wn=1;var n=Sr(),e={string:"C",number:"N",boolean:"L",null:"C"};at.multi=t,at.bytesPer=a,at.obj=o;function t(i){var s={};i.forEach(l);function l(c){r(s,c)}return o(s)}function r(i,s){for(var l in s){var c=typeof s[l]<"u"&&s[l]!==null;(typeof i[l]>"u"||c)&&(i[l]=s[l])}return i}function o(i){var s={},l=[];for(var c in i)s[c]=i[c]===null?"null":typeof i[c];for(var d in s){var u=e[s[d]];u&&l.push({name:d,type:u,size:n[u]})}return l}function a(i){return i.reduce(function(s,l){return s+l.size},1)}return at}var to,_n;function zs(){if(_n)return to;_n=1,Sr();var n=Us(),e=qs();return to=function(r,o){var a=o||e.multi(r),i=32*a.length+1,s=e.bytesPer(a),l=new ArrayBuffer(i+32+s*r.length+1),c=new Date,d=new DataView(l);d.setUint8(0,3),d.setUint8(1,c.getFullYear()-1900),d.setUint8(2,c.getMonth()),d.setUint8(3,c.getDate()),d.setUint32(4,r.length,!0);var u=i+32;return d.setUint16(8,u,!0),d.setUint16(10,s,!0),d.setInt8(32+i-1,13),a.forEach(function(p,f){p.name.split("").slice(0,8).forEach(function(h,g){d.setInt8(32+f*32+g,h.charCodeAt(0))}),d.setInt8(32+f*32+11,p.type.charCodeAt(0)),d.setInt8(32+f*32+16,p.size),p.type=="N"&&d.setInt8(32+f*32+17,3)}),offset=i+32,r.forEach(function(p,f){d.setUint8(offset,32),offset++,a.forEach(function(h){var g=p[h.name];switch((g===null||typeof g>"u")&&(g=""),h.type){case"L":d.setUint8(offset,g?84:70),offset++;break;case"D":offset=n.writeField(d,8,n.lpad(g.toString(),8," "),offset);break;case"N":offset=n.writeField(d,h.size,n.lpad(g.toString(),h.size," ").substr(0,18),offset);break;case"C":offset=n.writeField(d,h.size,n.rpad(g.toString(),h.size," "),offset);break;default:throw new Error("Unknown field type")}})}),d.setUint8(offset,26),d},to}var vn;function Hs(){return vn||(vn=1,Qt.structure=zs()),Qt}var oo,En;function Lr(){return En||(En=1,oo='GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]'),oo}var it={},xn;function Oo(){return xn||(xn=1,it.enlarge=function(e,t){return t[0]<e.xmin&&(e.xmin=t[0]),t[0]>e.xmax&&(e.xmax=t[0]),t[1]<e.ymin&&(e.ymin=t[1]),t[1]>e.ymax&&(e.ymax=t[1]),e},it.enlargeExtent=function(e,t){return t.xmax>e.xmax&&(e.xmax=t.xmax),t.xmin<e.xmin&&(e.xmin=t.xmin),t.ymax>e.ymax&&(e.ymax=t.ymax),t.ymin<e.ymin&&(e.ymin=t.ymin),e},it.blank=function(){return{xmin:Number.MAX_VALUE,ymin:Number.MAX_VALUE,xmax:-Number.MAX_VALUE,ymax:-Number.MAX_VALUE}}),it}var St={},Sn;function Ws(){if(Sn)return St;Sn=1;var n=Fo().jstypes;St.geojson=e,St.obj=r;function e(o){var a={};o.forEach(i);function i(s){t(a,s.properties)}return r(a)}function t(o,a){for(var i in a)o[i]=a[i];return o}function r(o){var a={},i=[];for(var s in o)a[s]=typeof o[s];for(var l in a)i.push({name:l,type:n[a[l]]});return i}return St}var Ae={},Ln;function Vs(){if(Ln)return Ae;Ln=1;var n=Oo();return Ae.write=function(t,r,o,a){var i=28,s=100,l=0,c=0;t.forEach(function(u,p){o.setInt32(l,p),o.setInt32(l+4,10),o.setInt32(l+8,1,!0),o.setFloat64(l+12,u[0],!0),o.setFloat64(l+20,u[1],!0),a.setInt32(c,s/2),a.setInt32(c+4,10),c+=8,l+=i,s+=i})},Ae.extent=function(e){return e.reduce(function(t,r){return n.enlarge(t,r)},n.blank())},Ae.parts=function(t,r){return t.length},Ae.shxLength=function(e){return e.length*8},Ae.shpLength=function(e){return e.length*28},Ae}var Fe={},Tn;function Ks(){if(Tn)return Fe;Tn=1;var n=Oo(),e=Fo();Fe.write=function(a,i,s,l,c){var d=0,u=0,p=100;a.forEach(f);function f(h,g){var m=r(h),y=t([h],c),w=m.length*16+48+(y-1)*4,b=m.reduce(function(E,x){return n.enlarge(E,x)},n.blank());l.setInt32(u,p/2),l.setInt32(u+4,w/2),u+=8,p+=w+8,s.setInt32(d,g+1),s.setInt32(d+4,w/2),s.setInt32(d+8,c,!0),s.setFloat64(d+12,b.xmin,!0),s.setFloat64(d+20,b.ymin,!0),s.setFloat64(d+28,b.xmax,!0),s.setFloat64(d+36,b.ymax,!0),s.setInt32(d+44,y,!0),s.setInt32(d+48,m.length,!0),s.setInt32(d+52,0,!0);for(var S=h.reduce(function(E,x){return Array.isArray(x[0][0])?E=E.concat(x):E.push(x),E},[]),L=1;L<y;L++)s.setInt32(d+52+L*4,S.reduce(function(E,x,I){return I<L?E+x.length:E},0),!0);m.forEach(function(x,I){s.setFloat64(d+56+I*16+(y-1)*4,x[0],!0),s.setFloat64(d+56+I*16+(y-1)*4+8,x[1],!0)}),d+=w+8}},Fe.shpLength=function(o){return o.length*56+r(o).length*16},Fe.shxLength=function(o){return o.length*8},Fe.extent=function(o){return r(o).reduce(function(a,i){return n.enlarge(a,i)},n.blank())};function t(o,a){var i=1;return(a===e.geometries.POLYGON||a===e.geometries.POLYLINE)&&(i=o.reduce(function(s,l){return s+=l.length,Array.isArray(l[0][0][0])&&(s+=l.reduce(function(c,d){return c+d.length-1},0)),s},0)),i}Fe.parts=t;function r(o,a){return a===void 0&&(a=[]),typeof o[0][0]=="object"?o.reduce(function(i,s){return i.concat(r(s))},a):o}return Fe}var no,kn;function Tr(){if(kn)return no;kn=1;var n=Fo(),e=Hs(),t=Lr();Oo(),Ws();var r=Vs(),o=Ks(),a={1:r,5:o,3:o};no=i;function i(c,d,u,p){var f=n.geometries[d],h=a[f],g=h.parts(u,f),m=100+(g-u.length)*4+h.shpLength(u),y=100+h.shxLength(u),w=new ArrayBuffer(m),b=new DataView(w),S=new ArrayBuffer(y),L=new DataView(S),E=h.extent(u);s(b,f),s(L,f),l(E,b),l(E,L),h.write(u,E,new DataView(w,100),new DataView(S,100),f),b.setInt32(24,m/2),L.setInt32(24,50+u.length*4);var x=e.structure(c);p(null,{shp:b,shx:L,dbf:x,prj:t})}function s(c,d){c.setInt32(0,9994),c.setInt32(28,1e3,!0),c.setInt32(32,d,!0)}function l(c,d){d.setFloat64(36,c.xmin,!0),d.setFloat64(44,c.ymin,!0),d.setFloat64(52,c.xmax,!0),d.setFloat64(60,c.ymax,!0)}return no}var st={},In;function Xs(){if(In)return st;In=1,st.point=n("Point","POINT"),st.line=n("LineString","POLYLINE"),st.polygon=n("Polygon","POLYGON");function n(o,a){return function(i){var s=i.features.filter(r(o));return{geometries:a==="POLYGON"||a==="POLYLINE"?[s.map(e)]:s.map(e),properties:s.map(t),type:a}}}function e(o){return o.geometry.coordinates[0]!==void 0&&o.geometry.coordinates[0][0]!==void 0&&o.geometry.coordinates[0][0][0]!==void 0?o.geometry.coordinates[0]:o.geometry.coordinates}function t(o){return o.properties}function r(o){return function(a){return a.geometry.type===o}}return st}var ro,Pn;function kr(){if(Pn)return ro;Pn=1;var n=Tr(),e=Xs(),t=Lr(),r=aa();return ro=function(o,a){var i=new r,s=i.folder(a&&a.folder?a.folder:"layers");[e.point(o),e.line(o),e.polygon(o)].forEach(function(c){c.geometries.length&&c.geometries[0].length&&n(c.properties,c.type,c.geometries,function(d,u){var p=a&&a.types[c.type.toLowerCase()]?a.types[c.type.toLowerCase()]:c.type;s.file(p+".shp",u.shp.buffer,{binary:!0}),s.file(p+".shx",u.shx.buffer,{binary:!0}),s.file(p+".dbf",u.dbf.buffer,{binary:!0}),s.file(p+".prj",t)})});var l={compression:"STORE"};return process.browser||(l.type="nodebuffer"),i.generate(l)},ro}var ao,Mn;function Ys(){if(Mn)return ao;Mn=1;var n=kr();return ao=function(e,t){var r=n(e,t);location.href="data:application/zip;base64,"+r},ao}var Cn;function Js(){return Cn||(Cn=1,nt.download=Ys(),nt.write=Tr(),nt.zip=kr()),nt}var Zs=Js();const Ir="EPSG:3857",Pr="EPSG:4326";async function Qs({features:n,rename:e,format:t,filenameBase:r="export"}){if(!Array.isArray(n)||n.length===0)throw new Error("No features to export");const o=el(n,e);switch(t){case"geojson":return An(new Blob([JSON.stringify(o,null,2)],{type:"application/geo+json"}),`${r}.geojson`);case"kml":return An(new Blob([tl(n,e)],{type:"application/vnd.google-earth.kml+xml"}),`${r}.kml`);case"shp":return ol(o,r);default:throw new Error(`Unknown export format: ${t}`)}}function el(n,e){const t=new le,r={type:"FeatureCollection",features:[]};for(const o of n){if(!o.getGeometry())continue;const a=t.writeGeometryObject(o.getGeometry(),{dataProjection:Pr,featureProjection:Ir}),i=Mr(o.getProperties()),s={};for(const[l,c]of Object.entries(e||{}))c&&Object.prototype.hasOwnProperty.call(i,l)&&(s[c]=i[l]);r.features.push({type:"Feature",geometry:a,properties:s})}return r}function Mr(n){const e={};for(const[t,r]of Object.entries(n||{}))t!=="geometry"&&(e[t]=r);return e}function tl(n,e){const t=n.filter(r=>r.getGeometry()).map(r=>{const o=r.clone(),a=Mr(r.getProperties()),i={};for(const[l,c]of Object.entries(e||{}))c&&Object.prototype.hasOwnProperty.call(a,l)&&(i[c]=a[l]);o.setProperties(i,!0);const s=Object.values(e||{}).find(l=>l&&i[l]!=null&&i[l]!=="");return s&&o.set("name",String(i[s])),o});return new Un({extractStyles:!1}).writeFeatures(t,{dataProjection:Pr,featureProjection:Ir})}async function ol(n,e){const t=nl(n);return new Promise((r,o)=>{try{Zs.download(t,{folder:e,outputType:"blob",compression:"DEFLATE",types:{point:`${e}_point`,polygon:`${e}_polygon`,polyline:`${e}_line`}}),r()}catch(a){o(a)}})}function nl(n){const e=new Set;for(const o of n.features)for(const a of Object.keys(o.properties||{}))e.add(a);const t=new Set,r={};for(const o of e){let a=String(o).replace(/[^A-Za-z0-9_]+/g,"_").slice(0,10)||"field",i=a,s=1;for(;t.has(i);){const l=String(s++);i=a.slice(0,Math.max(1,10-l.length))+l}t.add(i),r[o]=i}return{type:"FeatureCollection",features:n.features.map(o=>{const a={};for(const[i,s]of Object.entries(o.properties||{}))a[r[i]]=s;return{type:"Feature",geometry:o.geometry,properties:a}})}}function An(n,e){const t=URL.createObjectURL(n),r=document.createElement("a");r.href=t,r.download=e,document.body.appendChild(r),r.click(),document.body.removeChild(r),setTimeout(()=>URL.revokeObjectURL(t),1e3)}const A={};let _o=null,Le=null;function rl(){A.root||(A.root=document.getElementById("exportGisModal"),A.summary=document.getElementById("export-gis-summary"),A.filename=document.getElementById("export-gis-filename"),A.tbody=document.getElementById("export-gis-fields-tbody"),A.fmtHint=document.getElementById("export-gis-format-hint"),A.fieldsWrap=document.getElementById("export-gis-fields-wrap"),A.btnGo=document.getElementById("export-gis-go"),A.fmtInputs=Array.from(document.querySelectorAll('input[name="export-gis-format"]')),A.root.dataset.wired||(A.root.dataset.wired="1",A.fmtInputs.forEach(n=>n.addEventListener("change",Ar)),A.btnGo.addEventListener("click",sl)))}function al(n){const e=[];for(const t of n.parcelFeatures||[])e.push(io(t,"Parcels"));for(const t of n.zoneFeatures||[])e.push(io(t,"Zones"));for(const[t,r]of Object.entries(n.otherByLayer||{}))for(const o of r)e.push(io(o,t));return e}function io(n,e){const t=n.clone();return t.set("_source",e),t}function il(n){const e=new Set(["geometry","_layerType"]),t=new Map;for(const o of n)for(const a of Object.keys(o.getProperties()||{}))e.has(a)||t.has(a)||t.set(a,!0);t.delete("_source");const r=Array.from(t.keys());return r.push("_source"),r}function Cr(){const n=Nt();A.tbody.innerHTML=Le.keys.map(e=>{const t=Le.rename[e]??e,o=n==="shp"&&t.length>10?`<div class="form-text text-danger mt-1">
|
||
${vo(t.length)} characters — Shapefile will
|
||
truncate / rename.
|
||
</div>`:"";return`
|
||
<tr>
|
||
<td><code>${vo(e)}</code></td>
|
||
<td>
|
||
<input type="text" class="form-control form-control-sm export-field-rename"
|
||
data-src="${Fn(e)}"
|
||
value="${Fn(t)}">
|
||
${o}
|
||
</td>
|
||
</tr>
|
||
`}).join(""),A.tbody.querySelectorAll(".export-field-rename").forEach(e=>{e.addEventListener("input",t=>{const r=t.target.dataset.src;Le.rename[r]=t.target.value,Nt()==="shp"&&Cr()})})}function Ar(){const n=Nt();A.fmtHint.innerHTML={geojson:"GeoJSON keeps all attributes as-is and is the safest default.",shp:"Shapefile attribute names are limited to <strong>10 characters</strong> and alphanumeric/underscore only. Over-length names will be truncated (collisions are auto-numbered). One file per geometry type is written, all zipped into one download.",kml:"KML preserves attribute names; the first non-empty renamed field is used as each feature's <name> in Google Earth.",pdf:"A one-page <strong>summary report</strong> of the analysis figures — not the feature geometry. Field renaming does not apply."}[n];const e=n==="pdf";A.fieldsWrap&&A.fieldsWrap.classList.toggle("d-none",e),Cr()}function Nt(){return A.fmtInputs.find(n=>n.checked)?.value||"geojson"}async function sl(){const n=Nt(),e=(A.filename.value||"export").replace(/[^A-Za-z0-9_\-]+/g,"_");A.btnGo.disabled=!0;try{if(n==="pdf"){const t=Le.pdfRows||[];if(t.length===0)throw new Error("No summary figures available for a PDF report.");const{exportAnalysisPDF:r}=await Te(async()=>{const{exportAnalysisPDF:o}=await import("./pdf-export-C0eAmANf.js");return{exportAnalysisPDF:o}},__vite__mapDeps([4,5,3,0,1,2]));await r({title:Le.title||"Analysis",rows:t})}else await Qs({features:Le.features,rename:Le.rename,format:n,filenameBase:e});_o.hide()}catch(t){console.error("[ExportGIS] failed:",t),alert("Export failed: "+t.message)}finally{A.btnGo.disabled=!1}}function ll(n){if(rl(),!A.root){console.warn("[ExportGIS] Modal missing from DOM");return}const e=al(n);if(e.length===0){alert("No intersecting features to export.");return}const t=il(e),r=Object.fromEntries(t.map(a=>[a,a]));Le={features:e,keys:t,rename:r,pdfRows:n.pdfRows||[],title:n.title},A.summary.textContent=`${e.length} feature${e.length===1?"":"s"} intersecting the ${n.kind==="circle"?"circle":"area"}`,A.filename.value=n.kind==="circle"?"circle_analysis":"area_analysis";const o=document.getElementById("export-gis-fmt-geojson");o&&(o.checked=!0),Ar(),_o=gt.getOrCreateInstance(A.root),_o.show()}function vo(n){return String(n).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function Fn(n){return vo(n)}let so=null;async function On(){if(!so){const n=await Te(()=>import("./shpjs-Doisr2ug.js").then(e=>e.i),__vite__mapDeps([6,3]));so=n.default||n}return so}function Do(n){const e=document.getElementById("import-spinner-overlay"),t=document.getElementById("import-spinner-filename");e&&(t&&(t.textContent=n||""),e.classList.remove("d-none"),e.classList.add("d-flex"))}function ue(){const n=document.getElementById("import-spinner-overlay");n&&(n.classList.add("d-none"),n.classList.remove("d-flex"))}let v=null,ie=null,se=null,lo=null;const Eo=typeof window<"u"&&window.LUPMIS_EMBED||null,qe=!!(Eo&&Eo.mode==="permit");let q=qe?"embed-permit":"none";function cl(){const n=typeof window<"u"?window.LUPMIS_SESSION:null;if(!n||typeof n!="object")return!1;const e=n.district_id;if(e!=null&&String(e).length>0)return!1;console.warn("[App] Authenticated user has no district assigned; halting init.");const t=document.createElement("div");t.id="no-district-overlay",t.setAttribute("role","alertdialog"),t.setAttribute("aria-modal","true"),t.style.cssText="position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.98);padding:24px;";const r=n.full_name||n.username||"You";return t.innerHTML=`
|
||
<div style="max-width:480px;text-align:center;border:1px solid #e5e7eb;
|
||
border-radius:12px;padding:28px 24px;box-shadow:0 8px 24px rgba(0,0,0,0.08);
|
||
background:#fff;font-family:var(--font-body,sans-serif);">
|
||
<div style="font-size:42px;line-height:1;margin-bottom:12px;">🛑</div>
|
||
<h2 style="margin:0 0 12px;color:var(--primary,#005eb8);font-size:1.35rem;">
|
||
No district assigned
|
||
</h2>
|
||
<p style="margin:0 0 10px;color:#333;">
|
||
${j(r)}, your user profile is not associated with any
|
||
district. LUPMIS2 cannot load the relevant map data without one.
|
||
</p>
|
||
<p style="margin:0 0 20px;color:#6b7280;font-size:0.95rem;">
|
||
Please contact the system administrator to have a district assigned
|
||
to your account.
|
||
</p>
|
||
<button type="button" id="no-district-portal-btn"
|
||
style="background:var(--primary,#005eb8);color:#fff;border:0;
|
||
border-radius:8px;padding:10px 18px;font-weight:600;cursor:pointer;">
|
||
Return to LUSPA portal
|
||
</button>
|
||
</div>`,document.body.appendChild(t),t.querySelector("#no-district-portal-btn")?.addEventListener("click",()=>{window.location.href="https://lupmis4luspa.org/"}),!0}async function Dn(){if(console.log("[App] Initializing..."),cl())return;await Xi({installButton:"#install-btn",offlineIndicator:"#offline-indicator",autoRegisterSW:!0});const n=localStorage.getItem("default-basemap")||"topo";v=new Di("map",{center:[-1.5,7.5],zoom:7,basemap:n}),ie=new Ri(v.getMap()),Hl(),ie.onMeasureComplete(t=>{console.log("[MapTools] Measurement complete:",t),t.type==="polygon"&&t.coordinate&&t.feature?.get("_layerType")!=="measure_area"&&v?.showDrawnPolygonPopup(t.feature,t.coordinate)}),qe&&(lo=Ms({mapView:v,embedConfig:Eo})),v.onClick((t,r,o,a)=>{if(qe||(console.log("[MapClick] Clicked at:",t.toFixed(4),r.toFixed(4)),console.log("[MapClick] currentMode =",q),q==="draw"||q.startsWith("measure")))return;let i=null;if(v.getMap().forEachFeatureAtPixel(a.pixel,l=>{if(l.get("_layerType")==="parcel")return i=l,!0}),i){console.log("[MapClick] Clicked on parcel → Edit Attributes"),v.showParcelEditPopup(i,a.coordinate);return}let s=null;if(v.getMap().forEachFeatureAtPixel(a.pixel,l=>{if(l.get("_layerType")==="upn_grid")return s=l,!0}),s){console.log("[MapClick] Clicked on UPN-grid cell → Info popup"),v.showInfoPopup(s,a.coordinate,{title:"UPN Grid Cell",color:"#7c3aed"});return}q==="none"&&(o?(console.log("[MapClick] Clicked on marker:",o.getId()),v.selectMarker(o),ul(o)):v.clearSelection())}),v.onDblClick((t,r,o,a)=>{if(qe||!o)return;const i=o.get("_layerType");if(console.log("[App] Double-click on feature, _layerType:",i||"none"),i==="measure_circle")v.showCircleIntersectionPopup(o,a.coordinate);else{if(i==="measure_circle_radius")return;i==="measure_area"?v.showAreaIntersectionPopup(o,a.coordinate):i==="collector_zone"?v.showInfoPopup(o,a.coordinate,{title:"Zone Info",color:"#7c3aed"}):i==="parcel"?v.showInfoPopup(o,a.coordinate,{title:"Parcel Info",color:"#0ea5e9"}):v.showInfoPopup(o,a.coordinate,{title:"Feature Info",color:"#e11d48"})}}),v.onDrawnPoint(t=>{qe||(console.log("[App] Point drawn → Add Location popup"),v.clearSelection(),v.showAddLocationPopup(t))}),v.onAddLocation(async t=>{console.log("[App] Add location from map popup:",t);try{const r=await Ea(t.name,t.lon,t.lat,{description:t.description||null,category:t.category||"default"});console.log("[App] Location added:",t.name,"id:",r.id),await co(),v?.zoomTo(t.lon,t.lat,14),r.id&&v?.selectMarker(r.id),ge("Location added successfully")}catch(r){console.error("[App] Failed to add location:",r),O("Failed to add location: "+r.message)}}),v.onParcelEdit(async(t,r)=>{const o=r.id||r.parcelid||r.parcel_id;if(console.log("[App] Parcel edit saved:",o,r),!o){console.warn("[App] No parcel ID found in updated properties — skipping local save");return}try{await Na(o,r),ge("Parcel updated locally")}catch(a){console.error("[App] Failed to save parcel update:",a),O("Failed to save parcel: "+a.message)}});const e=new To;v.onDrawnPolygonSave(async(t,r)=>{console.log("[App] Drawn polygon attributes saved:",r);try{const o=e.writeGeometry(t.getGeometry(),{dataProjection:"EPSG:4326",featureProjection:"EPSG:3857"}),a=await $a(o,r);console.log("[App] New parcel inserted with id:",a.id),ge("New parcel saved (pending verification)")}catch(o){console.error("[App] Failed to save new parcel:",o),O("Failed to save parcel: "+o.message)}}),v.onFeatureModified(async t=>{const r=t.get("_externalImportId"),o=t.get("_clientUuid");if(!(r==null||!o))try{const a=e.writeGeometry(t.getGeometry(),{dataProjection:"EPSG:4326",featureProjection:"EPSG:3857"});await Fa(o,a),console.log("[App] Imported feature geometry updated in staging:",o)}catch(a){console.warn("[App] Failed to persist imported-feature edit:",a),O("Could not save the edit locally: "+a.message)}});try{console.log("[App] Initializing database..."),await va(),console.log("[App] Database ready"),await El();const t=await Po();console.log("[App] Database status:",t),K()&&(await ls()||(console.warn("[App] API server unreachable — using local data only"),$o("Server not responding — loading cached data."))),await Cl(),v?.initEditBar(),xl(),Sl(),Ll(),Tl(),qe&&lo&&se&&(se.setVisible(!0),lo.attachParcelsLayer(se)),kl(),Il(),Pl(),Ml()}catch(t){console.error("[App] Database initialization failed:",t),O("Failed to initialize database. Please refresh the page.");return}dl(),await co(),_a(t=>{if(console.log("[App] Database change:",t),t.table==="locations"&&!t.local&&co(),t.table==="parcels"){const r=document.getElementById("local-data-stats");r&&!r.classList.contains("d-none")&&Bt()}}),gr(t=>{t?console.log("[App] Working offline - data will sync when back online"):(console.log("[App] Back online - syncing data..."),Al())}),Wl(),Kl(),Xl(),Vl(),Yl(),Jl(),Zl(),Ql(),console.log("[App] Initialized successfully")}function dl(){console.log("[initUI] Starting UI initialization..."),zl();const n=document.getElementById("export-btn");n&&n.addEventListener("click",gl);const e=document.getElementById("local-data-btn");e&&e.addEventListener("click",()=>Bt());const t=document.getElementById("import-shp-btn"),r=document.getElementById("shp-file-input");t&&r&&(t.addEventListener("click",()=>r.click()),r.addEventListener("change",Nr));const o=document.getElementById("import-geojson-btn"),a=document.getElementById("geojson-file-input");o&&a&&(o.addEventListener("click",()=>a.click()),a.addEventListener("change",$r));const i=document.getElementById("import-kml-btn"),s=document.getElementById("kml-file-input");i&&s&&(i.addEventListener("click",()=>s.click()),s.addEventListener("change",Br)),Ul();const l=document.getElementById("exportGeoJSON-btn");l&&l.addEventListener("click",ml);const c=document.getElementById("status-btn");c&&c.addEventListener("click",yl);const d=document.getElementById("fit-btn");d&&d.addEventListener("click",()=>v?.fitToMarkers());const u=document.getElementById("dock-btn-measure-circle"),p=document.getElementById("dock-btn-measure-line"),f=document.getElementById("dock-btn-measure-area"),h=document.getElementById("dock-btn-draw"),g=document.getElementById("dock-btn-clear"),m=document.getElementById("dock-btn-analysis");m&&m.addEventListener("click",async()=>{if(v)try{const{openAnalysisModal:b}=await Te(async()=>{const{openAnalysisModal:S}=await import("./analysis-modal-C3OTjBir.js");return{openAnalysisModal:S}},__vite__mapDeps([7,0,1,2,3,8,9,10,11,12,6]));b(v)}catch(b){console.error("[App] Could not load the Analysis panel:",b),O("Analysis tools could not be loaded — check your connection and try again.")}}),console.log("[initUI] Buttons found:",{measureCircle:!!u,measureLine:!!p,measureArea:!!f,draw:!!h,clear:!!g});const y=[u,p,f,h],w=(b,S)=>{switch(console.log("[setMode] Changing mode from",q,"to",b),q=b,console.log("[setMode] currentMode is now:",q),y.forEach(L=>{L&&L.classList.toggle("active",L===S)}),ie?.deactivate(),b!=="draw"&&v?.setEditMode(!1),b!=="draw"&&v?.hideAddLocationPopup(),b){case"measureCircle":ie?.startCircleMeasure();break;case"measureLine":ie?.startLineMeasure();break;case"measureArea":ie?.startAreaMeasure();break;case"draw":v?.setEditMode(!0);break}};u&&u.addEventListener("click",()=>{console.log("[Button] Circle clicked, currentMode is:",q),q==="measureCircle"?w("none",null):w("measureCircle",u)}),p&&p.addEventListener("click",()=>{console.log("[Button] Line clicked, currentMode is:",q),q==="measureLine"?w("none",null):w("measureLine",p)}),f&&f.addEventListener("click",()=>{console.log("[Button] Area clicked, currentMode is:",q),q==="measureArea"?w("none",null):w("measureArea",f)}),h&&h.addEventListener("click",()=>{console.log("[Button] Draw clicked, currentMode is:",q),q==="draw"?w("none",null):w("draw",h)}),g&&g.addEventListener("click",()=>{if(ie?.clearMeasurements(),q.startsWith("measure"))switch(ie?.deactivate(),q){case"measureCircle":ie?.startCircleMeasure();break;case"measureLine":ie?.startLineMeasure();break;case"measureArea":ie?.startAreaMeasure();break}})}async function co(){try{console.log("[App] Loading locations...");const n=await tr();console.log("[App] Locations loaded:",n),pl(n),v&&(v.clearMarkers(),n.length>0&&(v.addMarkers(n),v.showMarkers(),console.log("[App] Added",n.length,"markers to map")));const e=document.getElementById("location-count");e&&(e.textContent=n.length)}catch(n){console.error("[App] Failed to load locations:",n)}}function ul(n){const e=n.get("name"),t=n.get("description"),r=n.get("category"),o=n.get("lon")||n.get("longitude"),a=n.get("lat")||n.get("latitude");console.log("[App] Selected location:",{name:e,description:t,category:r,lon:o,lat:a})}function pl(n){const e=document.getElementById("locations-list");if(!e)return;const t=document.getElementById("location-count-mobile");if(t&&(t.textContent=n.length),n.length===0){e.innerHTML=`
|
||
<div class="text-center text-muted py-4">
|
||
<p class="mb-0">No locations yet.</p>
|
||
<small>Click the map or fill the form above!</small>
|
||
</div>
|
||
`;return}const r={water:"💧",school:"🏫",health:"🏥",market:"🏪",default:"📍",other:"📌"};e.innerHTML=n.map(o=>{const a=r[o.category]||"📍";return`
|
||
<a href="#" class="list-group-item list-group-item-action location-item py-2"
|
||
data-id="${o.id}" data-lon="${o.longitude}" data-lat="${o.latitude}">
|
||
<div class="d-flex w-100 justify-content-between align-items-start">
|
||
<div>
|
||
<h6 class="mb-1">${a} ${j(o.name)}</h6>
|
||
<small class="text-muted font-monospace">${o.latitude.toFixed(5)}, ${o.longitude.toFixed(5)}</small>
|
||
</div>
|
||
<span class="badge badge-${o.category}">${o.category}</span>
|
||
</div>
|
||
${o.description?`<small class="text-secondary d-block mt-1">${j(o.description)}</small>`:""}
|
||
</a>
|
||
`}).join(""),e.querySelectorAll(".location-item").forEach(o=>{o.addEventListener("click",a=>{a.preventDefault();const i=parseFloat(o.dataset.lon),s=parseFloat(o.dataset.lat),l=parseInt(o.dataset.id);v?.showMarkers(),v?.zoomTo(i,s,14),v?.selectMarker(l)})})}async function Bt(){const n=document.getElementById("local-data-stats"),e=document.getElementById("local-data-tbody"),t=document.getElementById("clear-all-cached-btn");if(!(!n||!e)){try{const r=await Wa();e.innerHTML=r.map(o=>{const i=ir(o.name)?`<button type="button" class="btn btn-sm btn-link text-danger p-0 table-clear-btn"
|
||
data-table="${j(o.name)}"
|
||
title="Clear local cache (will re-download from server)">
|
||
<i class="bi bi-trash3"></i>
|
||
</button>`:"";return`
|
||
<tr>
|
||
<td class="ps-3">
|
||
<a href="#" class="table-name-link" data-table="${j(o.name)}">${j(o.name)}</a>
|
||
</td>
|
||
<td class="text-end"><span class="badge bg-secondary">${o.count}</span></td>
|
||
<td class="text-end pe-3">${i}</td>
|
||
</tr>
|
||
`}).join(""),n.classList.remove("d-none"),e.querySelectorAll(".table-name-link").forEach(o=>{o.addEventListener("click",a=>{a.preventDefault(),hl(o.dataset.table)})}),e.querySelectorAll(".table-clear-btn").forEach(o=>{o.addEventListener("click",async a=>{a.preventDefault();const i=o.dataset.table;if(confirm(`Clear local cache for "${i}"?
|
||
|
||
The data will be re-downloaded from the server on the next app start.`))try{const s=await sr(i);ge(`Cleared ${s} row${s===1?"":"s"} from "${i}". It will re-download on next start.`),await Bt()}catch(s){console.error("[App] Per-table clear failed:",s),O(`Could not clear "${i}": ${s.message}`)}})})}catch(r){console.error("[App] Failed to load table stats:",r),e.innerHTML='<tr><td colspan="3" class="text-danger ps-3">Failed to load</td></tr>',n.classList.remove("d-none")}t&&!t._wired&&(t._wired=!0,t.addEventListener("click",fl))}}async function fl(){if(confirm(`Delete all cached map layers from this device?
|
||
|
||
The next time the app starts (or after a reload), every layer will be re-downloaded from the server. Your locally drawn data is not affected.`))try{const n=await Mo(),e=n.reduce((t,r)=>t+r.count,0);ge(`Cleared ${e} row${e===1?"":"s"} across ${n.length} table${n.length===1?"":"s"}.`),await Bt(),confirm("Reload the app now to re-download the layers fresh from the server?")&&window.location.reload()}catch(n){console.error("[App] Clear-all failed:",n),O("Failed to clear cached layers: "+n.message)}}async function hl(n){const e=document.getElementById("tableContentModalLabel"),t=document.getElementById("table-content-body"),r=document.getElementById("table-content-info");e.textContent=`Table: ${n}`,t.innerHTML=`
|
||
<div class="text-center py-4">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">Loading...</span>
|
||
</div>
|
||
</div>
|
||
`,r.textContent="",new gt(document.getElementById("tableContentModal")).show();try{const{columns:a,rows:i}=await Va(n);if(i.length===0){t.innerHTML='<div class="text-center text-muted py-4">Table is empty</div>',r.textContent="0 rows";return}const s=a.map(c=>`<th class="text-nowrap">${j(c)}</th>`).join(""),l=i.map(c=>`<tr>${a.map(u=>{let p=c[u];if(p==null)return'<td class="text-muted fst-italic">NULL</td>';p=String(p);const f=p.length>120?p.substring(0,120)+"...":p;return`<td>${j(f)}</td>`}).join("")}</tr>`).join("");t.innerHTML=`
|
||
<div class="table-responsive">
|
||
<table class="table table-sm table-striped table-hover mb-0" style="font-size:12px;">
|
||
<thead class="table-light">
|
||
<tr>${s}</tr>
|
||
</thead>
|
||
<tbody>${l}</tbody>
|
||
</table>
|
||
</div>
|
||
`,r.textContent=`${i.length}${i.length>=200?"+":""} row(s), ${a.length} column(s)`}catch(a){console.error("[App] Failed to load table content:",a),t.innerHTML=`<div class="text-danger text-center py-4">Failed to load: ${j(a.message)}</div>`}}async function gl(){try{await za("lupmis-backup.sqlite3"),ge("Database exported successfully")}catch(n){console.error("[App] Export failed:",n),O("Export failed: "+n.message)}}async function ml(){try{const n=await Ha(),e=new Blob([JSON.stringify(n,null,2)],{type:"application/json"}),t=URL.createObjectURL(e),r=document.createElement("a");r.href=t,r.download="locations.geojson",r.click(),URL.revokeObjectURL(t),ge(`Exported ${n.features.length} location(s)`)}catch(n){console.error("[App] GeoJSON Export failed:",n),O("GeoJSON Export failed: "+n.message)}}async function yl(){try{const n=await Po(),e=document.getElementById("status-content");e&&(e.innerHTML=`
|
||
<table class="table table-sm table-borderless mb-0">
|
||
<tbody>
|
||
<tr>
|
||
<td class="fw-semibold">Ready:</td>
|
||
<td><span class="badge ${n.ready?"bg-success":"bg-danger"}">${n.ready?"Yes":"No"}</span></td>
|
||
</tr>
|
||
<tr>
|
||
<td class="fw-semibold">Online:</td>
|
||
<td><span class="badge ${K()?"bg-success":"bg-warning"}">${K()?"Yes":"Offline"}</span></td>
|
||
</tr>
|
||
<tr>
|
||
<td class="fw-semibold">Database:</td>
|
||
<td><code>${n.databasePath||"N/A"}</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td class="fw-semibold">Tables:</td>
|
||
<td>${n.tables.map(r=>`<span class="badge bg-secondary me-1">${r}</span>`).join("")}</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="fw-semibold">Locations:</td>
|
||
<td><span class="badge bg-primary">${n.locationCount}</span></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
`),new gt(document.getElementById("statusModal")).show()}catch(n){console.error("[App] Failed to get status:",n),O("Failed to get status")}}function Fr(n){return n.replace(/^\(+/,"").replace(/\)+$/,"").split(",").map(e=>{const[t,r]=e.trim().split(/\s+/).map(Number);return[t,r]})}function bl(n){return{type:"Polygon",coordinates:n.trim().replace(/^POLYGON\s*\(\s*/i,"").replace(/\s*\)$/,"").split("),(").map(Fr)}}function wl(n){return{type:"MultiPolygon",coordinates:n.trim().replace(/^MULTIPOLYGON\s*\(\s*/i,"").replace(/\s*\)$/,"").split(")),((").map(o=>o.replace(/^\(+/,"").replace(/\)+$/,"").split("),(").map(Fr))}}function mt(n){if(!n)return null;const e=n.trim().toUpperCase();return e.startsWith("MULTIPOLYGON")?wl(n):e.startsWith("POLYGON")?bl(n):(console.warn("[App] Unsupported WKT type:",e.substring(0,30)),null)}function _l(n){if(!n?.success||!n?.data?.boundary)return console.warn("[App] API response missing success or boundary data"),null;const{boundary:e,districtid:t,district_name:r}=n.data,o=mt(e);return{type:"FeatureCollection",features:[{type:"Feature",properties:{districtid:t,district_name:r},geometry:o}]}}function Rn(n){if(!Array.isArray(n)||n.length===0)return null;const e=[];for(const t of n){const r=t.polygon||t.boundary,o=mt(r);if(!o)continue;const a={_layerType:"collector_zone"};for(const[i,s]of Object.entries(t))i==="polygon"||i==="boundary"||(a[i]=s);e.push({type:"Feature",properties:a,geometry:o})}return e.length===0?null:{type:"FeatureCollection",features:e}}const xo="lupmis-last-district";function vl(n){return n==null||String(n).length===0?"district_boundary":`district_boundary_${n}`}async function El(){const e=Ye()?.district_id;if(e==null||String(e).length===0)return;const t=String(e);let r=null;try{r=localStorage.getItem(xo)}catch{}if(r!==null&&r!==t){console.warn(`[App] District changed ${r} → ${t}; clearing cached layers.`);try{await Mo(),$o("Your district was updated — refreshing map data.")}catch(o){console.error("[App] Failed to clear caches on district change:",o)}}try{localStorage.setItem(xo,t)}catch{}}async function xl(){const n=Ye()?.district_id??null,e=vl(n),t=1,r={strokeColor:"#e11d48",strokeWidth:2.5,fillColor:"rgba(225,29,72,0.08)",typeDescription:"Vector / Polygon"},o=v?.getLayerGroup(t)||null;function a(s){if(!s)return;const l=s.getLayers(),c=[];l.forEach(d=>{d.get("title")==="District Boundary"&&c.push(d)}),c.forEach(d=>l.remove(d))}function i(s){if(!s||!v)return;const l=s.getSource().getExtent();l&&l[0]!==1/0&&v.getMap().getView().fit(l,{padding:[40,40,40,40],duration:600})}try{const s=K()&&ke(),l=await nr(e);if(l){console.log("[App] District boundary loaded from local cache");const c=v?.addGeoJSONLayer(l,"District Boundary",r,o);s||i(c)}if(s){console.log("[App] Fetching district boundary from API...");const c=await ds(),d=_l(c);if(!d){console.warn("[App] Could not convert API response to GeoJSON");return}console.log("[App] District boundary:",d.features[0]?.properties?.district_name,"→",d.features[0]?.geometry?.coordinates?.length,"polygon(s)"),await or(e,d),l&&a(o||v?.getOverlayGroup());const u=v?.addGeoJSONLayer(d,"District Boundary",r,o);i(u),console.log("[App] District boundary loaded from API")}else l||console.log("[App] District boundary not available — offline and no local cache")}catch(s){console.error("[App] Failed to load district boundary:",s)}}function Nn(n){if(!Array.isArray(n)||n.length===0)return null;const e=[];for(const t of n){const r=t.polygon||t.geometry_wkt||t.geom,o=mt(r);o&&e.push({type:"Feature",properties:{_layerType:"upn_grid",upn_prefix:t.upn_prefix??null},geometry:o})}return e.length===0?null:{type:"FeatureCollection",features:e}}async function Sl(){const e={strokeColor:"#5b21b6",strokeWidth:1.5,fillColor:"rgba(124,58,237,0.04)",typeDescription:"Vector / Polygon"},t=v?.getLayerGroup(1)||null,r={type:"FeatureCollection",features:[]},o=v?.addGeoJSONLayer(r,"UPN Grid",e,t);if(!o){console.warn("[App] Could not create UPN Grid layer");return}o.setVisible(!1);const a=7;o.setStyle((s,l)=>{const c=[new M({stroke:new P({color:"rgba(255,255,255,0.95)",width:3.5})}),new M({stroke:new P({color:"#5b21b6",width:1.5,lineDash:[7,4]}),fill:new C({color:"rgba(124,58,237,0.04)"})})];if(l<=a){const d=s.get("upn_prefix");d!=null&&String(d).length>0&&c.push(new M({text:new Ke({text:String(d),font:"600 12px Arial, sans-serif",fill:new C({color:"#3b0764"}),stroke:new P({color:"rgba(255,255,255,0.95)",width:3}),overflow:!0})}))}return c}),o.on("change:visible",()=>{o.getVisible()&&o.getSource().getFeatures().length===0&&O("No UPN grid available locally. Connect to the internet to download it.")});function i(s){const l=new le().readFeatures(s,{featureProjection:"EPSG:3857"});o.getSource().clear(),o.getSource().addFeatures(l)}try{const l=Ye()?.district_id??null,c=await ka(l);if(c){const f=Nn(c);f&&i(f),console.log("[App] UPN grid from cache:",c.length,"cells (district",l,")");return}if(!K()||!ke()){console.log("[App] UPN grid not available — offline and no cache for district",l);return}console.log("[App] Fetching UPN grid from API (district",l,")...");const d=await ys();if(!d?.success||!Array.isArray(d?.data)){console.warn("[App] getUpnGrid invalid response:",d);return}const u=d.data;console.log("[App] UPN grid from API:",u.length,"cells"),await Ta(u,l);const p=Nn(u);p&&i(p),console.log("[App] UPN grid loaded:",p?.features.length??0,"cells rendered")}catch(s){console.error("[App] Failed to load UPN grid:",s)}}async function Ll(){const e={strokeColor:"#7c3aed",strokeWidth:1.5,fillColor:"rgba(124,58,237,0.12)",typeDescription:"Vector / Polygon"},t=v?.getLayerGroup(1)||null;console.log("[App] loadCollectorZones — adminGroup:",t?t.get("title"):"null");const r={type:"FeatureCollection",features:[]},o=v?.addGeoJSONLayer(r,"Zones",e,t);if(!o){console.warn("[App] Could not create Zones layer");return}o.setVisible(!1),o.on("change:visible",()=>{o.getVisible()&&o.getSource().getFeatures().length===0&&O("No collector zones available locally. Connect to the internet to download zone data.")});function a(i){const s=new le().readFeatures(i,{featureProjection:"EPSG:3857"});o.getSource().clear(),o.getSource().addFeatures(s)}try{const i=await La();if(i){const s=Rn(i);s&&(console.log("[App] Collector zones loaded from local cache:",s.features.length,"zones"),a(s))}if(K()&&ke()){console.log("[App] Fetching collector zones from API...");const s=await ps();if(!s?.success||!Array.isArray(s?.data)){console.warn("[App] getCollectorZones API response invalid:",s);return}const l=s.data;console.log("[App] Collector zones from API:",l.length,"entries"),await Sa(l);const c=Rn(l);if(!c){console.warn("[App] Could not convert zones to GeoJSON");return}a(c),console.log("[App] Collector zones updated from API:",c.features.length,"zones")}else i||console.log("[App] Collector zones not available — offline and no local cache")}catch(i){console.error("[App] Failed to load collector zones:",i)}}function $n(n){if(!Array.isArray(n)||n.length===0)return null;const e=new Set,t=[];for(const r of n){const o=r.id||r.parcelid||r.parcel_id;if(o!=null){if(e.has(o))continue;e.add(o)}let a=null;if(r.geom&&r.geom.type&&r.geom.coordinates)a={type:r.geom.type,coordinates:r.geom.coordinates};else if(r.sp_boundary&&r.sp_boundary.type&&r.sp_boundary.coordinates)a={type:r.sp_boundary.type,coordinates:r.sp_boundary.coordinates};else{const l=r.boundary||r.geometry_wkt||r.polygon||r.wkt;a=mt(l)}if(!a)continue;const i=new Set(["polygon","boundary","geom","geometry_wkt","wkt","textboundary","sp_boundary","fetched_at"]),s={_layerType:"parcel"};for(const[l,c]of Object.entries(r))i.has(l)||(s[l]=c);t.push({type:"Feature",properties:s,geometry:a})}return t.length===0?null:{type:"FeatureCollection",features:t}}async function Tl(){const e={strokeColor:"#0ea5e9",strokeWidth:1.5,fillColor:"rgba(14,165,233,0.12)",typeDescription:"Vector / Polygon"},t=v?.getLayerGroup(4)||null;console.log("[App] loadParcels — landUseGroup:",t?t.get("title"):"null");const r={type:"FeatureCollection",features:[]};if(se=v?.addGeoJSONLayer(r,"Parcels",e,t),!se){console.warn("[App] Could not create Parcels layer");return}se.setVisible(!1),se.on("change:visible",()=>{se.getVisible()&&se.getSource().getFeatures().length===0&&O("No parcels available locally. Connect to the internet to download parcel data.")});function o(a){const i=new le().readFeatures(a,{featureProjection:"EPSG:3857"});se.getSource().clear(),se.getSource().addFeatures(i)}try{const a=await Ra();if(a){const i=$n(a);i&&(console.log("[App] Parcels loaded from local cache:",i.features.length,"parcels"),o(i))}if(K()&&ke()){console.log("[App] Fetching parcels from API...");const i=await fs();if(!i?.success||!Array.isArray(i?.data)){console.warn("[App] getDistrictParcels API response invalid:",i);return}const s=i.data;console.log("[App] Parcels from API:",s.length,"entries"),s.length>0&&console.log("[App] First parcel keys:",Object.keys(s[0])),await Da(s);const l=$n(s);if(!l){console.warn("[App] Could not convert parcels to GeoJSON");return}o(l),console.log("[App] Parcels updated from API:",l.features.length,"parcels")}else a||console.log("[App] Parcels not available — offline and no local cache")}catch(a){console.error("[App] Failed to load parcels:",a)}}function Bn(n){if(!Array.isArray(n)||n.length===0)return null;const e=["polygon","boundary","geom","wkt","footprint"],t=[];for(const r of n){const o=r.polygon||r.boundary||r.geom||r.wkt||r.footprint;let a;if(typeof o=="object"&&o!==null&&o.type?a=o:a=mt(o),!a)continue;const i={_layerType:"building_footprint"};for(const[s,l]of Object.entries(r))e.includes(s)||typeof l=="object"&&l!==null||(i[s]=l);t.push({type:"Feature",properties:i,geometry:a})}return t.length===0?null:{type:"FeatureCollection",features:t}}async function kl(){const e={strokeColor:"#8b6f47",strokeWidth:1,fillColor:"rgba(139,111,71,0.18)",typeDescription:"Vector / Polygon"},t=v?.getLayerGroup(5)||null;console.log("[App] loadBuildingFootprints — physInfraGroup:",t?t.get("title"):"null");const r={type:"FeatureCollection",features:[]},o=v?.addGeoJSONLayer(r,"Building footprints",e,t);if(!o){console.warn("[App] Could not create Building footprints layer");return}o.setVisible(!1),o.on("change:visible",()=>{o.getVisible()&&o.getSource().getFeatures().length===0&&O("No building footprints available locally. Connect to the internet to download footprint data.")});function a(i){const s=new le().readFeatures(i,{featureProjection:"EPSG:3857"});o.getSource().clear(),o.getSource().addFeatures(s)}try{const i=await Ga();if(i){const s=Bn(i);s&&(console.log("[App] Building footprints loaded from local cache:",s.features.length,"footprints"),a(s))}if(K()&&ke()){console.log("[App] Fetching building footprints from API...");const s=await hs();if(!s?.success||!Array.isArray(s?.data)){console.warn("[App] getBuildingFootprints API response invalid:",s);return}const l=s.data;console.log("[App] Building footprints from API:",l.length,"entries"),l.length>0&&console.log("[App] First footprint keys:",Object.keys(l[0])),await Ba(l);const c=Bn(l);if(!c){console.warn("[App] Could not convert building footprints to GeoJSON");return}a(c),console.log("[App] Building footprints updated from API:",c.features.length,"footprints")}else i||console.log("[App] Building footprints not available — offline and no local cache")}catch(i){console.error("[App] Failed to load building footprints:",i)}}function Or(n,e){if(!Array.isArray(n)||n.length===0)return null;const t=new To,r=new le,o=["geom","geometry","wkt","polygon","boundary","road","line"],a=[];for(const i of n){const s=i.geom||i.geometry||i.wkt||i.polygon||i.boundary||i.road||i.line;if(!s)continue;let l;try{if(typeof s=="object"&&s!==null&&s.type){a.push({type:"Feature",properties:Gn(i,o,e),geometry:s});continue}l=t.readGeometry(s)}catch(d){console.warn(`[App] Could not parse WKT for ${e}:`,d,s?.toString().slice(0,60));continue}const c=JSON.parse(r.writeGeometry(l));a.push({type:"Feature",properties:Gn(i,o,e),geometry:c})}return a.length===0?null:{type:"FeatureCollection",features:a}}function Gn(n,e,t){const r={_layerType:t};for(const[o,a]of Object.entries(n))e.includes(o)||typeof a=="object"&&a!==null||(r[o]=a);return r}async function Il(){const n={strokeColor:"#78716c",strokeWidth:.8,typeDescription:"Vector / Line",fillColor:"rgba(0,0,0,0)",declutter:!0},e=v?.getLayerGroupByTitle("Biophysical Environment");console.log("[App] loadContoursHillshade — group:",e?e.get("title"):"null");const t={type:"FeatureCollection",features:[]},r=v?.addGeoJSONLayer(t,"Contours hillshade",n,e);if(!r){console.warn("[App] Could not create Contours hillshade layer");return}r.setVisible(!1);const o=10;if(r.setStyle((a,i)=>{const s=[new M({stroke:new P({color:"#78716c",width:.8})})];if(i<=o){const l=a.get("elev");l!=null&&l!==""&&s.push(new M({text:new Ke({text:String(l),font:"600 11px Arial, sans-serif",fill:new C({color:"#57534e"}),stroke:new P({color:"rgba(255,255,255,0.95)",width:3}),overflow:!0})}))}return s}),r.on("change:visible",()=>{r.getVisible()&&r.getSource().getFeatures().length===0&&O("No Contours hillshade data available. Connect to the internet to download it.")}),!K()||!ke()){console.log("[App] Contours hillshade not available — offline or server unreachable");return}try{console.log("[App] Fetching contours_hillshade from API...");const a=await gs(),i=Array.isArray(a)?a:Array.isArray(a?.data)?a.data:null;if(!i){console.warn("[App] getContoursHillshade API response invalid:",a);return}console.log("[App] Contours hillshade from API:",i.length,"rows"),i.length>0&&console.log("[App] First row keys:",Object.keys(i[0]));const s=Or(i,"contours_hillshade");if(!s){console.warn("[App] Could not convert contours to GeoJSON");return}const l=new le().readFeatures(s,{featureProjection:"EPSG:3857"});r.getSource().clear(),r.getSource().addFeatures(l),console.log("[App] Contours hillshade loaded:",l.length,"features")}catch(a){console.error("[App] Failed to load contours_hillshade:",a)}}async function Pl(){const e={strokeColor:"#F0F1F0",strokeWidth:1.5,lineCasingColor:"#000000",lineCasingWidth:3.5,fillColor:"rgba(0,0,0,0)",typeDescription:"Vector / Line"},t=v?.getLayerGroup(5)||null;console.log("[App] loadOSMRoads — group:",t?t.get("title"):"null");const r={type:"FeatureCollection",features:[]},o=v?.addGeoJSONLayer(r,"OSM_roads",e,t);if(!o){console.warn("[App] Could not create OSM_roads layer");return}o.setVisible(!1),o.on("change:visible",()=>{o.getVisible()&&o.getSource().getFeatures().length===0&&O("No OSM roads available locally. Connect to the internet to download them.")});function a(i){const s=Or(i,"osm_road");if(!s)return console.warn("[App] Could not convert OSM roads to GeoJSON"),0;const l=new le().readFeatures(s,{featureProjection:"EPSG:3857"});return o.getSource().clear(),o.getSource().addFeatures(l),l.length}try{const i=await Ua();if(i){const s=a(i);console.log("[App] OSM_roads loaded from local cache:",s,"features")}if(K()&&ke()){console.log("[App] Fetching OSM_roads from API...");const s=await ms();if(!s?.success||!Array.isArray(s?.data)){console.warn("[App] getOSMRoads API response invalid:",s);return}const l=s.data;console.log("[App] OSM_roads from API:",l.length,"rows"),l.length>0&&console.log("[App] First row keys:",Object.keys(l[0])),await ja(l);const c=a(l);console.log("[App] OSM_roads updated from API:",c,"features")}else i||console.log("[App] OSM_roads not available — offline and no local cache")}catch(i){console.error("[App] Failed to load OSM_roads:",i)}}function Ml(){v?.addWMSLayer("Biophysical Environment","DEAfrica Coastlines v0.4","https://geoserver.digitalearth.africa/geoserver/wms","coastlines:DEAfrica_Coastlines",{serverType:"geoserver",visible:!1,onlineOnly:!0}),v?.addWMSLayer("Biophysical Environment","DEAfrica Slope (SRTM 30m)","https://ows.digitalearth.africa/wms","srtm_deriv",{serverType:null,style:"style_slope",visible:!1,opacity:.5,zIndex:-50,onlineOnly:!0,attributions:'© <a href="https://www.digitalearthafrica.org/">Digital Earth Africa</a> — SRTM-derived Slope',legendUrl:"https://ows.digitalearth.africa/legend/srtm_deriv/style_slope/legend.png"})}async function Cl(){const n="layer_categories";function e(t){const r=[...t].sort((o,a)=>a.id-o.id);for(const o of r)v?.addLayerGroup(o.id,o.name,o.description||"");console.log("[App] Created",t.length,"layer groups on map")}try{const t=await nr(n);if(t&&(console.log("[App] Layer categories loaded from local cache:",t.length,"entries"),e(t)),K()&&ke()){console.log("[App] Fetching layer categories from API...");const r=await us();if(!r?.success||!Array.isArray(r?.data)){console.warn("[App] getLayers API response invalid:",r);return}const o=r.data;if(console.log("[App] Layer categories from API:",o.length,"entries"),await or(n,o),t){const a=v?.getOverlayGroup()?.getLayers();if(a){const i=[];a.forEach(s=>{s.get("layerId")!==void 0&&i.push(s)}),i.forEach(s=>a.remove(s))}}e(o),console.log("[App] Layer categories refreshed from API")}else t||console.log("[App] Layer categories not available — offline and no local cache")}catch(t){console.error("[App] Failed to load layer categories:",t)}}async function Al(){if(!K()){console.log("[App] Cannot sync - offline");return}console.log("[App] Sync placeholder - implement based on your backend")}const pe=[],Fl={strokeColor:"#e11d48",strokeWidth:2,fillColor:"rgba(225,29,72,0.12)"};function oe(n){Gt("error",n);const e=document.getElementById("file-import-alert");e&&(e.querySelector(".message-text").textContent=n,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),8e3))}function Ro(n,e,t){const r=Array.isArray(n)?n:[n];let o=0;for(const i of r){if(!i||i.type!=="FeatureCollection"||!i.features?.length)continue;const s=i.fileName?i.fileName.replace(/\.[^/.]+$/,""):e,l=v?.addGeoJSONLayer(i,s,Fl);l&&(l.set("removable",!0),l.set("typeTag","GEO"),pe.push(l),o+=i.features.length,Rl(i,s,l).catch(c=>console.warn("[FileImport] Staging failed (layer remains view-only):",c)))}if(o===0){ue(),oe("No features found in the file.");return}console.log(`[${t}] Added ${o} feature(s) from ${r.length} layer(s)`);const a=pe[pe.length-1];if(a){const i=a.getSource().getExtent();v?.getMap().getView().fit(i,{padding:[50,50,50,50],maxZoom:18})}No()}const Ol=new To;function Dl(n){return Ol.writeGeometry(n,{dataProjection:"EPSG:4326",featureProjection:"EPSG:3857"})}async function Rl(n,e,t){const r=n?.features?.length??0;if(r===0){ue();return}const{id:o}=await Ia({filename:e||"imported dataset",targetType:"other",featureCount:r});t.set("_externalImportId",o);const i=t.getSource().getFeatures().map(s=>{const l=s.getGeometry(),c=Nl();return s.set("_externalImportId",o),s.set("_clientUuid",c),{client_uuid:c,geometry_wkt:l?Dl(l):"",properties:$l(s.getProperties())}});await Pa(o,i),t.getSource().on("removefeature",async s=>{const l=s.feature,c=l?.get("_clientUuid"),d=l?.get("_externalImportId");if(!(!c||d==null))try{await Oa(c),console.log("[FileImport] Removed feature from staging:",c)}catch(u){console.warn("[FileImport] Failed to remove staging row:",u)}}),ue(),Gs({importId:o,filename:e,fc:n,onResult:async s=>{try{await Bl(o,t,s)}catch(l){console.error("[FileImport] Failed to apply mapping result:",l),O("Could not save the import mapping: "+l.message)}}})}function Nl(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,n=>{const e=Math.random()*16|0;return(n==="x"?e:e&3|8).toString(16)})}function $l(n){const e={};for(const[t,r]of Object.entries(n||{}))t!=="geometry"&&(e[t]=r);return e}async function Bl(n,e,t){if(!t||t.action==="cancel"){e?.set("_externalImportStatus","other"),Ve(e);return}const{action:r,targetType:o,mapping:a}=t;if(!o||o==="other"){await Dt(n,{targetType:"other",mapping:null,status:"other"}),e?.set("_externalImportStatus","other"),Ve(e);return}await Aa(n,i=>Ds(i,a)),await Dt(n,{targetType:o,mapping:a,status:"mapped"}),e?.set("_externalImportStatus","mapped"),e?.set("_externalImportTargetType",o),Ve(e),r==="upload"&&await Dr(n,e)}async function Dr(n,e){e?.set("_externalImportStatus","uploading"),Ve(e);try{await Dt(n,{status:"uploading"});const t=await Ma(n),r=await Ca(n),a={user_id_upload:Ye()?.user_id??null,import:{client_import_id:t.client_import_id,filename:t.filename,feature_count:r.length},features:r.map(i=>({client_uuid:i.client_uuid,geom:i.geometry_wkt,props:i.properties}))};console.log("[Upload]",{endpoint:`upload_${t.target_type}.php (not yet available on the server)`,target_type:t.target_type,body:a}),await Dt(n,{status:"mapped",lastUploadedAt:new Date().toISOString()}),e?.set("_externalImportStatus","mapped"),Ve(e),$o("The server upload endpoint is not yet available. The data stays staged locally — you can upload again later.")}catch(t){console.error("[Upload] Stub failed:",t),e?.set("_externalImportStatus","mapped"),Ve(e),O("Upload preparation failed: "+t.message)}}window.addEventListener("lupmis:export-gis",n=>{ll(n.detail||{})});window.addEventListener("lupmis:analyse-features",async n=>{if(!v)return;const e=n.detail||{},t=[...e.parcelFeatures||[],...e.zoneFeatures||[],...Object.values(e.otherByLayer||{}).flat()];try{const{openAnalysisModal:r}=await Te(async()=>{const{openAnalysisModal:o}=await import("./analysis-modal-C3OTjBir.js");return{openAnalysisModal:o}},__vite__mapDeps([7,0,1,2,3,8,9,10,11,12,6]));r(v,{scopeFeatures:t,scopeLabel:e.title||"drawn area"})}catch(r){console.error("[App] Could not load the Analysis panel:",r),O("Analysis tools could not be loaded — check your connection and try again.")}});window.addEventListener("lupmis:import-chip-click",n=>{const{importId:e,status:t,layer:r}=n.detail||{};t==="mapped"&&Dr(e,r).catch(o=>console.error("[FileImport] runUpload failed:",o))});function Ve(n){if(!n||!v)return;const e=v.getMap()?.getControls()?.getArray()?.find(t=>t?.constructor?.name==="LayerSwitcher"||t?.element?.classList?.contains("ol-layerswitcher"));e&&typeof e.drawPanel=="function"&&e.drawPanel()}function No(){const n=document.getElementById("imported-layers-info");if(!n)return;if(pe.length===0){n.innerHTML="",n.classList.add("d-none");return}n.innerHTML=`
|
||
<div class="card">
|
||
<div class="card-header bg-primary py-2 d-flex justify-content-between align-items-center">
|
||
<h6 class="mb-0"><i class="bi bi-layers me-2"></i>Imported Layers</h6>
|
||
<button type="button" class="btn btn-sm btn-outline-light" id="remove-imported-layers" title="Remove all imported layers">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</div>
|
||
<ul class="list-group list-group-flush" id="imported-layers-list"></ul>
|
||
</div>`;const e=n.querySelector("#imported-layers-list");pe.forEach((t,r)=>{const o=document.createElement("li");o.className="list-group-item d-flex justify-content-between align-items-center py-2",o.innerHTML=`<small class="text-truncate me-2">${j(t.get("title"))}</small>
|
||
<span class="d-flex align-items-center gap-2 flex-shrink-0">
|
||
<span class="badge" style="background-color:var(--primary);color:var(--primary-foreground);">${t.getSource().getFeatures().length}</span>
|
||
<button type="button" class="btn btn-sm btn-outline-danger border-0 p-0 lh-1" data-remove-idx="${r}" title="Remove layer">
|
||
<i class="bi bi-x-lg" style="font-size:.75rem;"></i>
|
||
</button>
|
||
</span>`,e.appendChild(o)}),n.classList.remove("d-none"),n.querySelectorAll("[data-remove-idx]").forEach(t=>{t.addEventListener("click",()=>{Gl(Number(t.dataset.removeIdx))})}),n.querySelector("#remove-imported-layers")?.addEventListener("click",()=>{jl()})}function Gl(n){if(n<0||n>=pe.length)return;const e=pe[n],t=v?.getOverlayGroup();t&&t.getLayers().remove(e),pe.splice(n,1),No(),console.log("[FileImport] Removed layer:",e.get("title"))}function jl(){const n=v?.getOverlayGroup();if(n)for(const e of pe)n.getLayers().remove(e);pe.length=0,No(),console.log("[FileImport] All imported layers removed")}function Rr(n){const e={};for(const t of n){const r=t.name.split(".").pop().toLowerCase();e[r]=t}return e}async function Nr(n){const e=n.target.files;if(!e||e.length===0)return;const t=200*1024*1024,r=Array.from(e).reduce((o,a)=>o+a.size,0);if(r>t){const o=(r/1048576).toFixed(0);oe(`Files too large (${o} MB total). Maximum supported size is 200 MB.`),n.target.value="";return}Do(e[0]?.name||"shapefile");try{let o,a;const i=Rr(e);if(i.zip){const s=i.zip;a=s.name.replace(/\.zip$/i,""),console.log("[ShpImport] Parsing zip",s.name,"("+(s.size/1024).toFixed(1)+" KB)"),o=await(await On())(await s.arrayBuffer())}else if(i.shp){a=i.shp.name.replace(/\.shp$/i,"");const l=["dbf","shx","prj"].filter(u=>!i[u]);if(l.length>0){ue(),oe("Missing required file(s): "+l.map(u=>"."+u).join(", ")+". Please select .shp, .dbf, .shx and .prj together."),n.target.value="";return}const c={};c.shp=await i.shp.arrayBuffer(),c.dbf=await i.dbf.arrayBuffer(),c.prj=await new Response(i.prj).text(),i.cpg&&(c.cpg=await new Response(i.cpg).text()),console.log("[ShpImport] Parsing loose files:",Object.keys(i).map(u=>"."+u).join(", "),"("+(i.shp.size/1024).toFixed(1)+" KB .shp)"),o=await(await On())(c)}else{ue(),oe("Please select a .zip or at least a .shp file."),n.target.value="";return}Ro(o,a,"ShpImport")}catch(o){ue(),console.error("[ShpImport] Failed:",o),oe("Failed to parse shapefile: "+o.message)}n.target.value=""}async function $r(n){const e=n.target.files?.[0];if(!e)return;const t=200*1024*1024;if(e.size>t){const r=(e.size/1048576).toFixed(0);oe(`File too large (${r} MB). Maximum supported size is 200 MB. Consider splitting the file into smaller tiles with ogr2ogr or QGIS.`),n.target.value="";return}Do(e.name);try{const r=await e.text();console.log("[GeoJSONImport] Parsing",e.name,"("+(e.size/1024).toFixed(1)+" KB)");const o=JSON.parse(r);let a;if(o.type==="FeatureCollection")a=o;else if(o.type==="Feature")a={type:"FeatureCollection",features:[o]};else if(o.type&&o.coordinates)a={type:"FeatureCollection",features:[{type:"Feature",geometry:o,properties:{}}]};else{ue(),oe("The file does not contain valid GeoJSON."),n.target.value="";return}const i=e.name.replace(/\.(geo)?json$/i,"");Ro(a,i,"GeoJSONImport")}catch(r){ue(),console.error("[GeoJSONImport] Failed:",r);const o=(e.size/(1024*1024)).toFixed(1);oe(`Failed to import "${e.name}" (${o} MB): ${r.message}`)}n.target.value=""}async function Br(n){const e=n.target.files?.[0];if(!e)return;const t=200*1024*1024;if(e.size>t){const r=(e.size/1048576).toFixed(0);oe(`File too large (${r} MB). Maximum supported size is 200 MB.`),n.target.value="";return}Do(e.name);try{const r=await e.text();console.log("[KMLImport] Parsing",e.name,"("+(e.size/1024).toFixed(1)+" KB)");const a=new Un({extractStyles:!1}).readFeatures(r,{featureProjection:"EPSG:3857"});if(!a||a.length===0){ue(),oe("No features found in the KML file."),n.target.value="";return}const i=new le,s=JSON.parse(i.writeFeatures(a,{featureProjection:"EPSG:3857",dataProjection:"EPSG:4326"})),l=e.name.replace(/\.kml$/i,"");Ro(s,l,"KMLImport")}catch(r){ue(),console.error("[KMLImport] Failed:",r);const o=(e.size/(1024*1024)).toFixed(1);oe(`Failed to import "${e.name}" (${o} MB): ${r.message}`)}n.target.value=""}function Ul(){const n=document.querySelector(".map-container");if(!n)return;let e=0;n.addEventListener("dragenter",t=>{t.preventDefault(),e++,n.classList.add("drag-over")}),n.addEventListener("dragover",t=>{t.preventDefault()}),n.addEventListener("dragleave",t=>{t.preventDefault(),e--,e<=0&&(e=0,n.classList.remove("drag-over"))}),n.addEventListener("drop",t=>{t.preventDefault(),e=0,n.classList.remove("drag-over");const r=t.dataTransfer?.files;if(!r||r.length===0)return;const o=Rr(r),a=Object.keys(o);if(o.zip||o.shp){const i={target:{files:r,value:""}};Object.defineProperty(i.target,"value",{writable:!0}),Nr(i)}else if(o.geojson||o.json){const s={target:{files:[o.geojson||o.json],value:""}};Object.defineProperty(s.target,"value",{writable:!0}),$r(s)}else if(o.kml){const i={target:{files:[o.kml],value:""}};Object.defineProperty(i.target,"value",{writable:!0}),Br(i)}else oe("Unsupported file type(s): "+a.map(i=>"."+i).join(", ")+". Drop .zip, .shp, .geojson, .json, or .kml files.")}),console.log("[FileImport] Map drop zone initialised")}function j(n){const e=document.createElement("div");return e.textContent=n,e.innerHTML}const ql=50,jn={error:{icon:"bi-x-circle-fill",color:"var(--destructive, #dc3545)"},warning:{icon:"bi-exclamation-triangle-fill",color:"var(--warning, #ffc107)"},success:{icon:"bi-check-circle-fill",color:"var(--success, #198754)"},info:{icon:"bi-info-circle-fill",color:"var(--primary, #0d6efd)"}};function Gt(n,e){const t=jn[n]||jn.info;(n==="error"?console.error:n==="warning"?console.warn:console.log)("[App]",e);const o=document.getElementById("message-log");if(!o)return;const a=o.querySelector(".text-muted");a&&a.remove();const i=document.createElement("div");i.className="list-group-item message-log-entry py-2 px-3";const l=new Date().toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"});for(i.innerHTML=`<div class="d-flex align-items-start gap-2"><i class="bi ${t.icon} flex-shrink-0 mt-1" style="color:${t.color}"></i><div class="flex-grow-1 text-break"><small>${j(e)}</small></div><small class="text-muted flex-shrink-0 ms-1">${l}</small></div>`,o.prepend(i);o.children.length>ql;)o.lastElementChild.remove()}function zl(){const n=document.getElementById("clear-message-log");n&&n.addEventListener("click",()=>{const e=document.getElementById("message-log");e&&(e.innerHTML='<div class="text-center text-muted py-3"><small>No messages yet.</small></div>')})}let So=null;function Gr(){const n=localStorage.getItem("coord-format");return n==="utm"||n==="both"?n:"latlon"}function jr(){const n=document.getElementById("gps-coords"),e=document.getElementById("gps-utm");if(!n||!So)return;const{lat:t,lon:r}=So,o=`${dn(t)}, ${dn(r)}`,a=Es(t,r),i=Gr();i==="utm"?(n.textContent=a,e&&e.classList.add("d-none")):i==="both"?(n.textContent=o,e&&(e.textContent=a,e.classList.remove("d-none"))):(n.textContent=o,e&&e.classList.add("d-none"));const s=document.getElementById("gps-readout");s&&(s.title=`Lat/Lon: ${o}
|
||
UTM: ${a}`)}function Hl(){const n=document.getElementById("gps-readout"),e=document.getElementById("gps-coords"),t=document.getElementById("gps-accuracy"),r=document.getElementById("gps-sats");if(!ve.isSupported){e&&(e.textContent="No GPS");return}ve.on("position",a=>{So=a,jr(),t&&(t.textContent=Ss(a.accuracy)),r&&(r.textContent=`${a.satellites!=null?a.satellites:"—"} sat`),n&&(n.classList.add("active"),n.classList.remove("quality-good","quality-fair","quality-poor"),n.classList.add("quality-"+Ls(a.accuracy))),v?.showCurrentPosition(a.lon,a.lat,a.accuracy)}),ve.on("point",a=>{v?.appendTrailPoint(a.point.lon,a.point.lat)}),ve.on("error",a=>{console.warn("[GPS]",a?.message||a),a&&a.code===1&&O("Location permission denied. Enable location access to use GPS.")}),v.onLocateMe(async()=>{try{const a=await ve.getCurrentPosition();v.centerOn(a.lon,a.lat,16)}catch(a){O("Could not get your location: "+(a?.message||a))}}),v.onToggleRecording(async a=>{if(a)try{await Jo,v.startTrailRender(),v.setRecordingState(!0),n?.classList.add("recording"),await ve.startRecording({name:`Trail ${new Date().toLocaleString()}`}),ge("GPS trail recording started")}catch(i){v.setRecordingState(!1),n?.classList.remove("recording"),O("Could not start recording: "+(i?.message||i))}else try{const i=await ve.stopRecording();if(v.setRecordingState(!1),n?.classList.remove("recording"),i){const s=`Trail saved: ${i.pointCount} points, ${xs(i.distanceM)}`+(i.synced?" — synced":" — will sync when online");ge(s)}}catch(i){O("Error stopping recording: "+(i?.message||i))}});const o=async()=>{if(K())try{await Jo;const a=await ve.syncPending();a.pushed&&console.log(`[GPS] Synced ${a.pushed} pending trail(s)`)}catch(a){console.warn("[GPS] pending-sync error",a)}};o(),gr(a=>{a||o()})}function O(n){Gt("error",n);const e=document.getElementById("error-message");e&&(e.querySelector(".message-text").textContent=n,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),5e3))}function ge(n){Gt("success",n);const e=document.getElementById("success-message");e&&(e.querySelector(".message-text").textContent=n,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),3e3))}function $o(n){Gt("warning",n);const e=document.getElementById("warning-message");e&&(e.querySelector(".message-text").textContent=n,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),5e3))}function Wl(){const n=document.getElementById("fieldwork-mode-toggle");if(!n)return;localStorage.getItem("fieldwork-mode")==="true"&&(document.documentElement.classList.add("fieldwork-mode"),n.checked=!0),n.addEventListener("change",()=>{document.documentElement.classList.toggle("fieldwork-mode",n.checked),localStorage.setItem("fieldwork-mode",n.checked),console.log("[Settings] Fieldwork mode",n.checked?"ON":"OFF")})}function Vl(){const n=document.getElementById("dark-mode-toggle");if(!n)return;function e(r){document.documentElement.classList.toggle("dark-mode",r),document.documentElement.setAttribute("data-bs-theme",r?"dark":"light")}localStorage.getItem("dark-mode")==="true"&&(n.checked=!0,e(!0)),n.addEventListener("change",()=>{e(n.checked),localStorage.setItem("dark-mode",n.checked),console.log("[Settings] Dark mode",n.checked?"ON":"OFF")})}function Kl(){const n=document.getElementById("measurement-system-toggle"),e=document.getElementById("measurement-system-label");if(!n)return;function t(){e&&(e.textContent=n.checked?"Imperial":"Metric")}const r=localStorage.getItem("measurement-system");r==="imperial"&&(n.checked=!0),t(),v?.setScaleBarUnits(r||"metric"),n.addEventListener("change",()=>{const o=n.checked?"imperial":"metric";localStorage.setItem("measurement-system",o),t(),v?.setScaleBarUnits(o),console.log("[Settings] Measurement system:",o)})}function Xl(){const n=document.getElementById("coord-format-select");n&&(n.value=Gr(),n.addEventListener("change",()=>{const e=n.value==="utm"||n.value==="both"?n.value:"latlon";localStorage.setItem("coord-format",e),jr(),console.log("[Settings] GPS coordinate format:",e)}))}function Yl(){const n=document.getElementById("default-basemap-select");if(!n)return;const e=localStorage.getItem("default-basemap")||"topo";n.value=e,n.addEventListener("change",()=>{const t=n.value;localStorage.setItem("default-basemap",t),v?.setBaseMap(t),console.log("[Settings] Default base map:",t)}),v?.getMap()?.on("basemapchange",t=>{if(t?.key&&n.value!==t.key){n.value=t.key;try{localStorage.setItem("default-basemap",t.key)}catch{}}})}function Jl(){const n=document.getElementById("tile-cache-stats"),e=document.getElementById("clear-tiles-btn"),t=document.getElementById("offcanvasBottom");if(!n||!e||!t)return;function r(i){return i?i<1024*1024?(i/1024).toFixed(0)+" KB":i<1024*1024*1024?(i/(1024*1024)).toFixed(1)+" MB":(i/(1024*1024*1024)).toFixed(2)+" GB":"0 KB"}let o=null;async function a(){if(o)return o;const i=!!navigator.serviceWorker?.controller;return n.innerHTML=i?'<div class="text-muted fst-italic">Loading…</div>':'<div class="text-muted fst-italic">Initialising service worker…</div>',o=(async()=>{try{const s=await Hi();if(!s){n.innerHTML=`
|
||
<div class="text-muted fst-italic">
|
||
Tile cache stats unavailable. Try reloading the page if this persists.
|
||
</div>`;return}const l=s.totals,c=s.byProvider.filter(p=>p.count>0).map(p=>`
|
||
<tr>
|
||
<td>${j(p.label)}</td>
|
||
<td class="text-end">${p.count.toLocaleString()} / ${p.limit.toLocaleString()}</td>
|
||
<td class="text-end">${r(p.estBytes)}</td>
|
||
<td class="text-end pe-0" style="width:2.2rem;">
|
||
<button type="button" class="btn btn-sm btn-link text-danger p-0 provider-clear-btn"
|
||
data-cache="${j(p.key)}" data-label="${j(p.label)}"
|
||
title="Clear ${j(p.label)} tiles only">
|
||
<i class="bi bi-trash3"></i>
|
||
</button>
|
||
</td>
|
||
</tr>`).join("");let d="";const u=await Ki();if(u&&u.quota>0){const p=(u.usage/u.quota*100).toFixed(1);d=`
|
||
<div class="mt-2 text-muted">
|
||
Total app storage: ${r(u.usage)} of ${r(u.quota)} available (${p}%)
|
||
</div>`}if(l.count===0){n.innerHTML=`
|
||
<div class="text-muted">
|
||
No tiles cached yet. Pan and zoom the map to start caching tiles automatically.
|
||
</div>${d}`,e.disabled=!0;return}n.innerHTML=`
|
||
<div class="mb-1">
|
||
<strong>${l.count.toLocaleString()}</strong> tiles cached, ~${r(l.estBytes)} on this device
|
||
</div>
|
||
<table class="table table-sm mb-0" style="font-size:0.85em;">
|
||
<thead><tr>
|
||
<th>Base map</th>
|
||
<th class="text-end">Cached / limit</th>
|
||
<th class="text-end">Approx. size</th>
|
||
<th class="text-end pe-0" style="width:2.2rem;"></th>
|
||
</tr></thead>
|
||
<tbody>${c}</tbody>
|
||
</table>${d}`,e.disabled=!1,n.querySelectorAll(".provider-clear-btn").forEach(p=>{p.addEventListener("click",async f=>{f.preventDefault();const h=p.dataset.cache,g=p.dataset.label||h;if(!confirm(`Clear cached "${g}" tiles?
|
||
|
||
Other providers are not affected. The tiles will re-download as you browse online.`))return;p.disabled=!0,await Vi(h)?console.log(`[Settings] Cleared tile cache for ${g}`):console.warn(`[Settings] Could not clear tile cache for ${g}`),await a()})})}finally{o=null}})(),o}e.addEventListener("click",async()=>{if(!confirm("Clear all cached map tiles from this device? You will need to be online to view them again."))return;e.disabled=!0,await Wi()?console.log("[Settings] Tile caches cleared"):console.warn("[Settings] Tile-cache clear failed"),await a()}),t.addEventListener("show.bs.offcanvas",a),zi(()=>{console.log("[Settings] SW controller changed → refreshing tile-cache stats"),a()}),a()}function Zl(){const n=document.getElementById("download-tiles-btn"),e=document.getElementById("offline-download-modal");if(!n||!e)return;const t=gt.getOrCreateInstance(e),r=document.getElementById("offline-download-form-view"),o=document.getElementById("offline-download-progress-view"),a=document.getElementById("offline-download-done-view"),i=document.getElementById("offline-download-cancel-btn"),s=document.getElementById("offline-download-start-btn"),l=document.getElementById("offline-download-close-done-btn"),c=document.getElementById("offline-download-close-btn"),d=document.getElementById("offline-basemap-select"),u=document.getElementById("offline-min-zoom"),p=document.getElementById("offline-max-zoom"),f=document.getElementById("offline-ack-check"),h=document.getElementById("offline-estimate-detail"),g=document.getElementById("offline-estimate"),m=document.getElementById("offline-area-view"),y=document.getElementById("offline-area-district"),w=document.getElementById("offline-area-ghana"),b=document.getElementById("offline-area-view-info"),S=document.getElementById("offline-area-district-info"),L=document.getElementById("offline-progress-bar"),E=document.getElementById("offline-progress-percent"),x=document.getElementById("offline-progress-counts"),I=document.getElementById("offline-progress-ok"),k=document.getElementById("offline-progress-failed"),U=document.getElementById("offline-progress-eta"),z=document.getElementById("offline-done-title"),Z=document.getElementById("offline-done-detail");let $=null;function ce(B){return B?B<1024*1024?(B/1024).toFixed(0)+" KB":B<1024*1024*1024?(B/(1024*1024)).toFixed(1)+" MB":(B/(1024*1024*1024)).toFixed(2)+" GB":"0 KB"}function Q(B){if(!B||B<1e3)return"< 1 s";const J=Math.round(B/1e3);if(J<60)return J+" s";const V=Math.floor(J/60),ne=J%60;return V<60?`${V} min ${ne} s`:`${Math.floor(V/60)} h ${V%60} min`}function ye(){return m.checked?v?.getCurrentViewExtent()||null:y.checked?v?.getDistrictBoundaryExtent()?.extent||null:w.checked?ts:null}function H(){const B=d.value,J=parseInt(u.value,10),V=parseInt(p.value,10);if(Number.isNaN(J)||Number.isNaN(V)||J>V){h.textContent="Invalid zoom range",g.classList.replace("alert-info","alert-warning"),s.disabled=!0;return}const ne=ye();if(!ne){h.textContent="Selected area is not available.",g.classList.replace("alert-info","alert-warning"),s.disabled=!0;return}const R=mr[B]?.maxZoom??19,W=Math.min(V,R),re=Ji(ne,J,W),jt=os(re);let we="";W<V&&(we=`<br><span class="text-warning">Zoom ${V} is above this provider's max (${R}); will clamp to ${R}.</span>`),re>8e3&&(we+='<br><span class="text-warning">More than 8 000 tiles — exceeds the per-provider cache limit. Earlier tiles will be evicted as new ones arrive.</span>'),h.innerHTML=`<strong>${re.toLocaleString()}</strong> tiles · ~${ce(jt)}`+we,g.classList.toggle("alert-warning",!!we),g.classList.toggle("alert-info",!we),s.disabled=!f.checked||re===0}function be(){v?.getCurrentViewExtent()?b.textContent=" · ready":b.textContent="",v?.getDistrictBoundaryExtent()?(S.textContent="",y.disabled=!1):(S.textContent=" (not loaded — connect online to fetch)",y.disabled=!0,y.checked&&(m.checked=!0))}function Je(){r.classList.remove("d-none"),o.classList.add("d-none"),a.classList.add("d-none"),s.classList.remove("d-none"),i.classList.remove("d-none"),i.textContent="Cancel",l.classList.add("d-none"),c.disabled=!1,f.checked=!1,s.disabled=!0,$=null}n.addEventListener("click",()=>{Je(),be(),H(),t.show()}),d.addEventListener("change",H),u.addEventListener("input",H),p.addEventListener("input",H),m.addEventListener("change",H),y.addEventListener("change",H),w.addEventListener("change",H),f.addEventListener("change",H),s.addEventListener("click",async()=>{const B=d.value,J=parseInt(u.value,10),V=parseInt(p.value,10),ne=ye();if(!ne)return;r.classList.add("d-none"),o.classList.remove("d-none"),s.classList.add("d-none"),i.textContent="Cancel download",c.disabled=!0,L.style.width="0%",L.setAttribute("aria-valuenow","0"),E.textContent="0%",x.textContent="0 of 0 tiles",I.textContent="0",k.textContent="0",U.textContent="—",$=new es({baseMap:B,extent3857:ne,minZoom:J,maxZoom:V,onProgress:W=>{if(W.total>0){const re=Math.min(100,Math.round(W.done/W.total*100));L.style.width=re+"%",L.setAttribute("aria-valuenow",String(re)),E.textContent=re+"%",x.textContent=`${W.done.toLocaleString()} of ${W.total.toLocaleString()} tiles`}I.textContent=W.ok.toLocaleString(),k.textContent=W.failed.toLocaleString(),U.textContent=W.etaMs!=null?Q(W.etaMs):"—"}});let R;try{R=await $.start()}catch(W){console.error("[OfflineDownload] failed:",W),R={phase:"error",done:0,total:0,ok:0,failed:0}}o.classList.add("d-none"),a.classList.remove("d-none"),i.classList.add("d-none"),l.classList.remove("d-none"),c.disabled=!1,R.phase==="cancelled"?(z.textContent="Download cancelled",Z.innerHTML=`Stopped after <strong>${R.done.toLocaleString()}</strong> of ${R.total.toLocaleString()} tiles.<br>${R.ok.toLocaleString()} fetched · ${R.failed.toLocaleString()} failed.`):R.phase==="error"?(z.textContent="Download failed",Z.textContent="See console for details."):(z.textContent="Download complete",Z.innerHTML=`<strong>${R.ok.toLocaleString()}</strong> tiles cached`+(R.failed>0?`, ${R.failed.toLocaleString()} failed`:"")+`.<br>Took ${Q(R.elapsedMs)}.`)}),i.addEventListener("click",()=>{$&&$.cancel()}),e.addEventListener("hidden.bs.modal",()=>{$&&$.cancel(),Je()})}function Ql(){const n=Ye(),e=document.getElementById("menu-btn"),t=document.getElementById("menu-user-avatar"),r=document.getElementById("menu-user-name"),o=document.getElementById("menu-user-email"),a=document.getElementById("menu-user-detail"),i=document.getElementById("menu-signout-btn"),s=document.getElementById("menu-landing-link"),l=document.getElementById("menu-signin-link"),c=document.getElementById("menu-no-session-note");if(!e||!t||!r||!o||!a||!i){console.warn("[AccountMenu] One or more elements missing — shell may be stale. Hard-refresh.");return}if(!!n&&!!n.user_id){const u=[n.title,n.full_name].filter(Boolean).join(" ").trim()||n.username||"Authenticated user",p=(n.full_name||n.username||"?").trim().charAt(0).toUpperCase();t.textContent=p,t.style.background="var(--brand-navy, #1e1a4b)",r.textContent=u,o.textContent=n.email||"";const f=[];n.district_id!=null&&f.push(`District ${j(String(n.district_id))}`),n.region_id!=null&&f.push(`Region ${j(String(n.region_id))}`),n.ua_position&&f.push(j(n.ua_position)),a.innerHTML=f.join(" · ")||"No district info",i.classList.remove("d-none"),i.addEventListener("click",()=>ec(n),{once:!1}),s?.classList.remove("d-none"),l?.classList.add("d-none"),c?.classList.add("d-none"),e.removeAttribute("data-state"),e.setAttribute("title",`Menu — ${u}`)}else typeof window.LUPMIS_SESSION>"u"?(t.innerHTML='<i class="bi bi-exclamation"></i>',t.style.background="var(--brand-orange-warm, #ff9e1b)",r.textContent="No session injected",o.textContent="",a.textContent="",i.classList.add("d-none"),s?.classList.add("d-none"),l?.classList.add("d-none"),c?.classList.remove("d-none"),e.dataset.state="no-session",e.setAttribute("title","Menu (no session — dev mode)")):(t.innerHTML='<i class="bi bi-person-fill"></i>',t.style.background="var(--brand-gray-medium, #7a7a7a)",r.textContent="Not signed in",o.textContent="",a.textContent="",i.classList.add("d-none"),s?.classList.add("d-none"),l?.classList.remove("d-none"),c?.classList.add("d-none"),e.dataset.state="unauthenticated",e.setAttribute("title","Menu (not signed in)"))}async function ec(n){if(!K()){alert(`You appear to be offline.
|
||
|
||
Logging out needs a connection to the sign-in server, and you would not be able to sign back in until you are online again.
|
||
|
||
Please try again once you have a connection. You can keep working — the app stays signed in while offline.`);return}if(confirm(`Log out, ${n?.full_name||n?.username||"user"}?`)){try{await Mo(),localStorage.removeItem(xo)}catch(e){console.warn("[Logout] Cache clear failed (continuing):",e)}window.location.href="/?logout=1"}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",Dn):Dn();export{F as s};
|
||
//# sourceMappingURL=index-By1A2oQD.js.map
|