Two branches from parallel sessions, merged in that order because the first removes shp-write and with it JSZip, which the second was working around. Conflicts resolved by hand in vite.config.js: the JSZip chunk rule the second branch added is dropped, since nothing reaches JSZip any more — shapefiles are written by src/shapefile-write.js and shpjs 6 unzips with but-unzip. Vite's preload-helper rule stays, which is what keeps the geotiff chunk out of the startup preload. dist/ was rebuilt from scratch rather than merged, its hashed file names being a conflict on every file. Service Worker v26 now records both changes. Verified on the merged tree: a production build preloads only the entry, vite-helpers, bootstrap, openlayers and ol-ext — geotiff, shpjs and pako are gone from startup, and Analyse still fetches its chunk and turf on first use. Shapefile export, driven through the Export GIS modal, writes a zip holding the point and polygon sets with their .prj and .cpg. Co-Authored-By: Claude Opus 5 <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-B_Wfowsp.js","assets/openlayers-D5-VSKzq.js","assets/openlayers-BtPuoxOl.css","assets/vite-helpers-BlTxHScW.js","assets/pako-n3Pgozwg.js","assets/pdf-export-DJBZ6C0H.js","assets/jspdf-HFui8oS7.js","assets/analysis-modal-CwVHFT_a.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 Le}from"./vite-helpers-BlTxHScW.js";import{M as lt}from"./bootstrap-D1-uvFxm.js";import{ak as xe,h as M,F as A,j as I,k as he,m as fo,b as G,V as D,L as de,D as Ce,P as at,al as We,n as fe,am as Me,M as To,an as gr,ao as ee,ap as mr,S as yr,J as br,G as wr,aq as _r,ar as vr,as as Er,o as dt,O as Ee,at as nt,au as vt,A as xr,T as se,av as Ae,aw as Lo,ax as le,ay as ko,e as Sr,u as Nt,s as Tr,az as gn,aA as ho}from"./openlayers-D5-VSKzq.js";import{o as Lr,a as kr,b as Ir,c as Pr,d as Xt,e as Io,f as tt,g as Mr,h as Se,i as Ar,j as Po}from"./ol-ext-DV70ljNY.js";(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))r(n);new MutationObserver(n=>{for(const s of n)if(s.type==="childList")for(const a of s.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&r(a)}).observe(document,{childList:!0,subtree:!0});function t(n){const s={};return n.integrity&&(s.integrity=n.integrity),n.referrerPolicy&&(s.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?s.credentials="include":n.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function r(n){if(n.ep)return;n.ep=!0;const s=t(n);fetch(n.href,s)}})();const Mo="function",je="64e10b34-2bf7-4616-9668-f99de5aa046e",Cr="get",Fr="has",Or="set",{isArray:yt}=Array;let{SharedArrayBuffer:Et,window:Dr}=globalThis,{notify:mn,wait:yn,waitAsync:xt}=Atomics,bn=null;xt||(xt=o=>({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(o)})}));try{new Et(4)}catch{Et=ArrayBuffer;const e=new WeakMap;if(Dr){const t=new Map,{prototype:{postMessage:r}}=Worker,n=s=>{const a=s.data?.[je];if(!yt(a)){s.stopImmediatePropagation();const{id:i,sb:l}=a;t.get(i)(l)}};bn=function(s,...a){const i=s?.[je];if(yt(i)){const[l,c]=i;e.set(c,l),this.addEventListener("message",n)}return r.call(this,s,...a)},xt=s=>({value:new Promise(a=>{t.set(e.get(s),a)}).then(a=>{t.delete(e.get(s)),e.delete(s);for(let i=0;i<a.length;i++)s[i]=a[i];return"ok"})})}else{const t=(r,n)=>({[je]:{id:r,sb:n}});mn=r=>{postMessage(t(e.get(r),r))},addEventListener("message",r=>{const n=r.data?.[je];if(yt(n)){const[s,a]=n;e.set(a,s)}})}}/*! (c) Andrea Giammarchi - ISC */const{Int32Array:Yt,Map:Ao,Uint16Array:Jt}=globalThis,{BYTES_PER_ELEMENT:Co}=Yt,{BYTES_PER_ELEMENT:Rr}=Jt,Nr=(o,e,t)=>{for(;yn(o,0,0,e)==="timed-out";)t()},Zt=new WeakSet,$t=new WeakMap,$r={value:{then:o=>o()}};let Br=0;const go=(o,{parse:e=JSON.parse,stringify:t=JSON.stringify,transform:r,interrupt:n}=JSON)=>{if(!$t.has(o)){const s=bn||o.postMessage,a=(f,...p)=>s.call(o,{[je]:p},{transfer:f}),i=typeof n===Mo?n:n?.handler,l=n?.delay||42,c=new TextDecoder("utf-16"),d=(f,p)=>f?xt(p,0):(i?Nr(p,l,i):yn(p,0),$r);let u=!1;$t.set(o,new Proxy(new Ao,{[Fr]:(f,p)=>typeof p=="string"&&!p.startsWith("_"),[Cr]:(f,p)=>p==="then"?null:((...h)=>{const g=Br++;let m=new Yt(new Et(Co*2)),y=[];Zt.has(h.at(-1)||y)&&Zt.delete(y=h.pop()),a(y,g,m,p,r?h.map(r):h);const w=o!==globalThis;let _=0;return u&&w&&(_=setTimeout(console.warn,1e3,`💀🔒 - Possible deadlock if proxy.${p}(...args) is awaited`)),d(w,m).value.then(()=>{clearTimeout(_);const E=m[1];if(!E)return;const T=Rr*E;return m=new Yt(new Et(T+T%Co)),a([],g,m),d(w,m).value.then(()=>e(c.decode(new Jt(m.buffer).slice(0,E))))})}),[Or](f,p,h){const g=typeof h;if(g!==Mo)throw new Error(`Unable to assign ${p} as ${g}`);if(!f.size){const m=new Ao;o.addEventListener("message",async y=>{const w=y.data?.[je];if(yt(w)){y.stopImmediatePropagation();const[_,E,...T]=w;let x;if(T.length){const[S,P]=T;if(f.has(S)){u=!0;try{const k=await f.get(S)(...P);if(k!==void 0){const j=t(r?r(k):k);m.set(_,j),E[1]=j.length}}catch(k){x=k}finally{u=!1}}else x=new Error(`Unsupported action: ${S}`);E[0]=1}else{const S=m.get(_);m.delete(_);for(let P=new Jt(E.buffer),k=0;k<S.length;k++)P[k]=S.charCodeAt(k)}if(mn(E,0),x)throw x}})}return!!f.set(p,h)}}))}return $t.get(o)};go.transfer=(...o)=>(Zt.add(o),o);function Fo(){let o,e;return{lock:async()=>{for(;o;)await o;o=new Promise(n=>{e=n})},unlock:async()=>{const n=e;o=void 0,e=void 0,n?.()}}}async function wn(o,e){let t;if(o instanceof Blob?t=o.stream():t=o,t instanceof ReadableStream&&e){const n=t.getReader();switch(e){case"callback":return async()=>(await n.read()).value;case"buffer":const s=[];let a=!1;for(;!a;){const d=await n.read();d.value&&s.push(d.value),a=d.done}const i=s.reduce((d,u)=>d+u.length,0),l=new Uint8Array(i);let c=0;return s.forEach(d=>{l.set(d,c),c+=d.length}),l.buffer}}else return t}class St{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:n}=await Le(async()=>{const{default:s}=await import("./index-DTMgZTfd.js");return{default:s}},[]);this.sqlite3InitModule=n}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 n=new Map;try{for(let s of e){let a=n.get(s.sql);if(!a){const c=r.prepare(s.sql);n.set(s.sql,c),a=c}s.params?.length&&a.bind(s.params);let i=[],l=[];for(;a.step();)i=a.getColumnNames([]),l.push(a.get([]));t.push({columns:i,rows:l}),a.reset()}}finally{n.forEach(s=>{s.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 n=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(n)}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:[]},n=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=n[0]??[];break;case"all":default:r.rows=n;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,n,s,a)=>{this.writeCallbacks.forEach(i=>{i({table:s,rowid:a,operation:e[r]})})},0)}closeDb(){this.db&&(this.db.close(),this.db=void 0)}}function Gr(o,e,t){let r,n,s,a,i,l,c=0,d=!1,u=!1,f=!0;if(typeof o!="function")throw new TypeError("Expected a function");e=Number(e)||0,typeof t=="object"&&t!==null&&(d=!!t.leading,u="maxWait"in t,s=u?Math.max(Number(t.maxWait)||0,e):0,f="trailing"in t?!!t.trailing:f);function p(x){const S=r,P=n;return r=n=void 0,c=x,a=o.apply(P,S),a}function h(x){return c=x,i=setTimeout(y,e),d?p(x):a}function g(x){const S=x-(l??0),P=x-c,k=e-S;return u?Math.min(k,s-P):k}function m(x){const S=x-(l??0),P=x-c;return l===void 0||S>=e||S<0||u&&P>=s}function y(){const x=Date.now();if(m(x))return w(x);i=setTimeout(y,g(x))}function w(x){return i=void 0,f&&r?p(x):(r=n=void 0,a)}function _(){i!==void 0&&clearTimeout(i),c=0,r=l=n=i=void 0}function E(){return i===void 0?a:w(Date.now())}function T(){const x=Date.now(),S=m(x);if(r=arguments,n=this,l=x,S){if(i===void 0)return h(l);if(u)return i=setTimeout(y,e),p(l)}return i===void 0&&(i=setTimeout(y,e)),a}return T.cancel=_,T.flush=E,T}function bt(){return crypto.randomUUID()}function _n(o,e){switch(o){case"session":case":sessionStorage:":let t=sessionStorage._sqlocal_session_key;return t||(t=bt(),sessionStorage._sqlocal_session_key=t),`session:${t}`;case"local":case":localStorage:":return"local";case":memory:":return`memory:${e}`;default:return`path:${o}`}}class ut{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:Fo()}),Object.defineProperty(this,"transactionMutex",{enumerable:!0,configurable:!0,writable:!0,value:Fo()}),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 n=>{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 St,await this.driver.init(this.config)}const s=_n(this.config.databasePath,this.config.clientKey);this.reinitChannel=new BroadcastChannel(`_sqlocal_reinit_(${s})`),this.reinitChannel.onmessage=a=>{const i=a.data;if(this.config.clientKey!==i.clientKey)switch(i.type){case"reinit":this.init(i.reason);break;case"close":this.driver.destroy();break}},this.config.reactive&&(this.effectsChannel=new BroadcastChannel(`_sqlocal_effects_(${s})`),this.driver.onWrite(async a=>{this.dirtyTables.add(a.table),await this.transactionMutex.lock(),this.emitEffectsDebounced(),await this.transactionMutex.unlock()})),await Promise.all(Array.from(this.userFunctions.values()).map(a=>this.initUserFunction(a))),await this.execInitStatements(),this.emitMessage({type:"event",event:"connect",reason:n})}catch(s){this.emitMessage({type:"error",error:s,queryKey:null}),await this.destroy()}finally{await this.initMutex.unlock()}}}}),Object.defineProperty(this,"postMessage",{enumerable:!0,configurable:!0,writable:!0,value:async(n,s)=>{const a=n instanceof MessageEvent?n.data:n;switch(await this.initMutex.lock(),a.type){case"config":this.editConfig(a);break;case"query":case"batch":case"transaction":this.exec(a);break;case"function":this.createUserFunction(a);break;case"getinfo":this.getDatabaseInfo(a);break;case"import":this.importDb(a);break;case"export":this.exportDb(a);break;case"delete":this.deleteDb(a);break;case"destroy":this.destroy(a);break}await this.initMutex.unlock()}}),Object.defineProperty(this,"emitMessage",{enumerable:!0,configurable:!0,writable:!0,value:(n,s=[])=>{this.onmessage&&this.onmessage(n,s)}}),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:Gr(()=>this.emitEffects(),32,{maxWait:180})}),Object.defineProperty(this,"editConfig",{enumerable:!0,configurable:!0,writable:!0,value:n=>{this.config=n.config,this.init("initial")}}),Object.defineProperty(this,"exec",{enumerable:!0,configurable:!0,writable:!0,value:async n=>{try{const s={type:"data",queryKey:n.queryKey,data:[]};switch(n.type){case"query":const a=this.transactionKey!==null&&this.transactionKey===n.transactionKey;try{a||await this.transactionMutex.lock();const i=await this.driver.exec(n);s.data.push(i)}finally{a||await this.transactionMutex.unlock()}break;case"batch":try{await this.transactionMutex.lock();const i=await this.driver.execBatch(n.statements);s.data.push(...i)}finally{await this.transactionMutex.unlock()}break;case"transaction":if(n.action==="begin"&&(await this.transactionMutex.lock(),this.transactionKey=n.transactionKey,await this.driver.exec({sql:"BEGIN"})),(n.action==="commit"||n.action==="rollback")&&this.transactionKey!==null&&this.transactionKey===n.transactionKey){const i=n.action==="commit"?"COMMIT":"ROLLBACK";await this.driver.exec({sql:i}),this.transactionKey=null,await this.transactionMutex.unlock()}break}this.emitMessage(s)}catch(s){this.emitMessage({type:"error",error:s,queryKey:n.queryKey})}}}),Object.defineProperty(this,"execInitStatements",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{if(this.config.onInitStatements)for(let n of this.config.onInitStatements)await this.driver.exec(n)}}),Object.defineProperty(this,"getDatabaseInfo",{enumerable:!0,configurable:!0,writable:!0,value:async n=>{try{this.emitMessage({type:"info",queryKey:n.queryKey,info:{databasePath:this.config.databasePath,storageType:this.driver.storageType,databaseSizeBytes:await this.driver.getDatabaseSizeBytes(),persisted:await this.driver.isDatabasePersisted()}})}catch(s){this.emitMessage({type:"error",queryKey:n.queryKey,error:s})}}}),Object.defineProperty(this,"createUserFunction",{enumerable:!0,configurable:!0,writable:!0,value:async n=>{const{functionName:s,functionType:a,queryKey:i}=n;let l;if(this.userFunctions.has(s)){this.emitMessage({type:"error",error:new Error(`A user-defined function with the name "${s}" has already been created for this SQLocal instance.`),queryKey:i});return}switch(a){case"callback":l={type:a,name:s,func:(...c)=>{this.emitMessage({type:"callback",name:s,args:c})}};break;case"scalar":l={type:a,name:s,func:this.proxy[`_sqlocal_func_${s}`]};break;case"aggregate":l={type:a,name:s,func:{step:this.proxy[`_sqlocal_func_${s}_step`],final:this.proxy[`_sqlocal_func_${s}_final`]}};break}try{await this.initUserFunction(l),this.emitMessage({type:"success",queryKey:i})}catch(c){this.emitMessage({type:"error",error:c,queryKey:i})}}}),Object.defineProperty(this,"initUserFunction",{enumerable:!0,configurable:!0,writable:!0,value:async n=>{await this.driver.createFunction(n),this.userFunctions.set(n.name,n)}}),Object.defineProperty(this,"importDb",{enumerable:!0,configurable:!0,writable:!0,value:async n=>{const{queryKey:s,database:a}=n;let i=!1;try{await this.driver.import(a),this.driver.storageType==="memory"&&await this.execInitStatements()}catch(l){this.emitMessage({type:"error",error:l,queryKey:s}),i=!0}finally{this.driver.storageType!=="memory"&&await this.init("overwrite")}i||this.emitMessage({type:"success",queryKey:s})}}),Object.defineProperty(this,"exportDb",{enumerable:!0,configurable:!0,writable:!0,value:async n=>{const{queryKey:s}=n;try{const{name:a,data:i}=await this.driver.export();this.emitMessage({type:"buffer",queryKey:s,bufferName:a,buffer:i},[i])}catch(a){this.emitMessage({type:"error",error:a,queryKey:s})}}}),Object.defineProperty(this,"deleteDb",{enumerable:!0,configurable:!0,writable:!0,value:async n=>{const{queryKey:s}=n;let a=!1;try{await this.driver.clear()}catch(i){this.emitMessage({type:"error",error:i,queryKey:s}),a=!0}finally{await this.init("delete")}a||this.emitMessage({type:"success",queryKey:s})}}),Object.defineProperty(this,"destroy",{enumerable:!0,configurable:!0,writable:!0,value:async n=>{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),n&&this.emitMessage({type:"success",queryKey:n.queryKey})}});const r=typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope?go(globalThis):globalThis;this.proxy=r,this.driver=e}}function Tt(o,...e){return{sql:o.join("?"),params:e}}function Ur(o){return!o.some(e=>!Array.isArray(e))}function Bt(o,e){let t;return Ur(o)?t=o:t=[o],t.map(r=>{const n={};return e.forEach((s,a)=>{n[s]=r[a]}),n})}function jr(o){return typeof o=="object"&&o!==null&&"getSQL"in o&&typeof o.getSQL=="function"}function zr(o){return typeof o=="object"&&o!==null&&"sql"in o&&typeof o.sql=="string"&&"params"in o}function Oo(o){if(typeof o=="function"&&(o=o(Tt)),jr(o))try{if(!("toSQL"in o&&typeof o.toSQL=="function"))throw 1;const r=o.toSQL();if(!zr(r))throw 2;const n="all"in o&&typeof o.all=="function"?o.all:void 0;return{...r,exec:n?()=>n():void 0}}catch{throw new Error("The passed statement could not be parsed.")}const e=o.sql;let t=[];return"params"in o?t=o.params:"parameters"in o&&(t=o.parameters),{sql:e,params:t}}function Do(o,e){let t;return typeof o=="string"?t={sql:o,params:e}:t=Tt(o,...e),t}async function pt(o,e,t,r){return!e&&"locks"in navigator?navigator.locks.request(`_sqlocal_mutation_(${t.databasePath})`,{mode:o},r):r()}class Ro extends St{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 Le(async()=>{const{default:n}=await import("./index-DTMgZTfd.js");return{default:n}},[]);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 St;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 vn,En;class qr{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[p,h]=u.get(d.queryKey);d.type==="error"?h(d.error):p(d),u.delete(d.queryKey)}else if(d.type==="error")throw d.error;break;case"callback":const f=this.userCallbacks.get(d.name);f&&f(...d.args??[]);break;case"event":this.config.onConnect?.(d.reason);break}}}),Object.defineProperty(this,"createQuery",{enumerable:!0,configurable:!0,writable:!0,value:async c=>pt("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=bt();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,f)=>{this.queriesInProgress.set(d,[u,f])})})}),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",f)=>{const p=await this.createQuery({type:"query",transactionKey:f,sql:c,params:d,method:u}),h={rows:[],columns:[]};return p.type==="data"&&(h.rows=p.data[0]?.rows??[],h.columns=p.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((f,p)=>{u[p]=f}),u}}),Object.defineProperty(this,"sql",{enumerable:!0,configurable:!0,writable:!0,value:async(c,...d)=>{const u=Do(c,d),{rows:f,columns:p}=await this.exec(u.sql,u.params,"all");return Bt(f,p)}}),Object.defineProperty(this,"batch",{enumerable:!0,configurable:!0,writable:!0,value:async c=>{const d=c(Tt);return(await this.execBatch(d)).map(({rows:f,columns:p})=>Bt(f,p))}}),Object.defineProperty(this,"beginTransaction",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{const c=bt();await this.createQuery({type:"transaction",transactionKey:c,action:"begin"});const d=async h=>{const g=Oo(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 Bt(m,y)};return{query:d,sql:async(h,...g)=>{const m=Do(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=>pt("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,f=!1,p=0;const h=Oo(c),g=new Set,m=new Set,y=new Set,w=async()=>{try{const E=++p;if(g.size===0){const x=await this.sql("SELECT name, wr FROM tables_used(?) WHERE type = 'table'",h.sql),S=new Set,P=new Set;if(x.forEach(k=>{typeof k.name=="string"&&(k.wr?P.add(k.name):S.add(k.name))}),S.size===0)throw new Error("The passed SQL does not read any tables.");if(Array.from(P).some(k=>S.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.");S.forEach(k=>g.add(k))}const T=h.exec?await h.exec():await this.sql(h.sql,...h.params);E===p&&(d=T,u=!0,m.forEach(x=>x(d)))}catch(E){y.forEach(T=>{T(E instanceof Error?E:new Error(String(E)))})}},_=E=>{E.data.tables.some(T=>g.has(T))&&w()};return{get value(){return d},subscribe:(E,T)=>{if(!this.effectsChannel)throw new Error('This SQLocal instance is not configured for reactive queries. Set the "reactive" option to enable them.');return T||(T=x=>{throw x}),m.add(E),y.add(T),f?u&&E(d):(this.effectsChannel.addEventListener("message",_),f=!0,w()),{unsubscribe:()=>{m.delete(E),y.delete(T),m.size===0&&(this.effectsChannel?.removeEventListener("message",_),f=!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}`,f=()=>{this.proxy[u]=d};this.proxy===globalThis&&f(),await this.createQuery({type:"function",functionName:c,functionType:"scalar"}),this.proxy!==globalThis&&f()}}),Object.defineProperty(this,"createAggregateFunction",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{const u=`_sqlocal_func_${c}`,f=()=>{this.proxy[`${u}_step`]=d.step,this.proxy[`${u}_final`]=d.final};this.proxy===globalThis&&f(),await this.createQuery({type:"function",functionName:c,functionType:"aggregate"}),this.proxy!==globalThis&&f()}}),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 pt("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 pt("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,vn,{enumerable:!0,configurable:!0,writable:!0,value:()=>{this.destroy()}}),Object.defineProperty(this,En,{enumerable:!0,configurable:!0,writable:!0,value:async()=>{await this.destroy()}});const t=typeof e=="string"?{databasePath:e}:e,{onInit:r,onConnect:n,processor:s,...a}=t,{databasePath:i}=a;this.config=t,this.clientKey=bt();const l=_n(i,this.clientKey);if(this.reinitChannel=new BroadcastChannel(`_sqlocal_reinit_(${l})`),a.reactive&&(this.effectsChannel=new BroadcastChannel(`_sqlocal_effects_(${l})`)),typeof s<"u")this.processor=s;else if(i==="local"||i===":localStorage:"){const c=new Ro("local");this.processor=new ut(c)}else if(i==="session"||i===":sessionStorage:"){const c=new Ro("session");this.processor=new ut(c)}else if(typeof globalThis.Worker<"u"&&i!==":memory:")this.processor=new Worker(new URL("/assets/worker-CuIBOSaM.js",import.meta.url),{type:"module"});else{const c=new St;this.processor=new ut(c)}this.processor instanceof ut?(this.processor.onmessage=c=>this.processMessageEvent(c),this.proxy=globalThis):(this.processor.addEventListener("message",this.processMessageEvent),this.proxy=go(this.processor)),this.processor.postMessage({type:"config",config:{...a,clientKey:this.clientKey,onInitStatements:r?.(Tt)??[]}})}}vn=Symbol.dispose,En=Symbol.asyncDispose;const mo="lupmis2.db",Hr="lupmis-db-sync",xn=new qr(mo),{sql:b}=xn;console.log("[Database] SQLocal instance created for:",mo);const Sn=new BroadcastChannel(Hr);let Tn=!1,Ln,kn;const No=new Promise((o,e)=>{Ln=o,kn=e}),Lt=new Set;function Wr(o){return Lt.add(o),()=>Lt.delete(o)}Sn.onmessage=o=>{const{type:e,payload:t}=o.data;if(e==="DB_CHANGE")for(const r of Lt)try{r(t)}catch(n){console.error("[Database] Change listener error:",n)}};function Y(o,e,t=null){Sn.postMessage({type:"DB_CHANGE",payload:{table:o,action:e,id:t,timestamp:Date.now()}});for(const r of Lt)try{r({table:o,action:e,id:t,timestamp:Date.now(),local:!0})}catch(n){console.error("[Database] Change listener error:",n)}}async function Vr(){try{console.log("[Database] Initializing schema...");const o=await b`SELECT sqlite_version() as version`;console.log("[Database] SQLite version:",o[0]?.version),console.log("[Database] Creating locations table..."),await b`
|
||
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 b`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 b`
|
||
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 b`
|
||
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 b`
|
||
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 b`PRAGMA table_info(parcels)`;r.length>0&&!r.some(n=>n.name==="upn")&&(console.log("[Database] Migrating parcels table to lu_parcels structure (dropping old cache)..."),await b`DROP TABLE parcels`)}catch(r){console.warn("[Database] parcels migration check failed:",r)}await b`
|
||
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 b`
|
||
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 b`
|
||
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 b`
|
||
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 b`
|
||
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 b`
|
||
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 b`
|
||
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 b`
|
||
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 b`CREATE INDEX IF NOT EXISTS idx_locations_category ON locations(category)`,await b`CREATE INDEX IF NOT EXISTS idx_locations_synced ON locations(synced)`,await b`CREATE INDEX IF NOT EXISTS idx_gps_trails_synced ON gps_trails(synced, status)`,await b`CREATE INDEX IF NOT EXISTS idx_gps_trail_points_trail ON gps_trail_points(trail_id, seq)`,await b`CREATE INDEX IF NOT EXISTS idx_external_imports_status ON external_imports(status)`,await b`CREATE INDEX IF NOT EXISTS idx_external_import_features_import ON external_import_features(import_id, upload_status)`;const t=await b`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;console.log("[Database] All tables:",t.map(r=>r.name)),Tn=!0,Ln(!0),console.log("[Database] ✓ Schema initialized")}catch(o){throw console.error("[Database] ✗ Schema init failed:",o),kn(o),o}}async function Kr(o,e,t,r={}){const{description:n=null,category:s="default"}=r;console.log("[Database] Adding location:",o,e,t,s);try{const a=await b`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;if(console.log("[Database] Table check before insert:",a),a.length===0)throw console.error("[Database] ✗ locations table does not exist!"),new Error("locations table does not exist");console.log("[Database] Executing INSERT..."),await b`
|
||
INSERT INTO locations (name, longitude, latitude, description, category)
|
||
VALUES (${o}, ${e}, ${t}, ${n}, ${s})
|
||
`,console.log("[Database] INSERT completed");const l=(await b`SELECT last_insert_rowid() as id`)[0]?.id;console.log("[Database] New ID:",l);const c=await b`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 b`
|
||
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(a){throw console.error("[Database] ✗ Failed to add location:",a),a}}async function In(o={}){const{category:e=null,limit:t=1e3}=o;try{const r=await b`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 n;return e?n=await b`
|
||
SELECT * FROM locations
|
||
WHERE category = ${e}
|
||
ORDER BY created_at DESC
|
||
LIMIT ${t}
|
||
`:n=await b`
|
||
SELECT * FROM locations
|
||
ORDER BY created_at DESC
|
||
LIMIT ${t}
|
||
`,console.log("[Database] getLocations returned",n.length,"rows"),n}catch(r){return console.error("[Database] getLocations error:",r),[]}}async function Xr(){try{return(await b`SELECT COUNT(*) as count FROM locations`)[0]?.count??0}catch(o){return console.error("[Database] getLocationCount error:",o),0}}async function Pn(o,e){try{const t=JSON.stringify(e);await b`
|
||
INSERT OR REPLACE INTO remote_data (key, data, fetched_at)
|
||
VALUES (${o}, ${t}, CURRENT_TIMESTAMP)
|
||
`,console.log("[Database] ✓ Remote data cached:",o)}catch(t){throw console.error("[Database] ✗ Failed to cache remote data:",o,t),t}}async function Mn(o){try{const e=await b`SELECT data, fetched_at FROM remote_data WHERE key = ${o}`;if(e.length===0)return null;const t=JSON.parse(e[0].data);return console.log("[Database] ✓ Remote data loaded from cache:",o,"(fetched",e[0].fetched_at+")"),t}catch(e){return console.error("[Database] ✗ Failed to read cached remote data:",o,e),null}}async function Yr(o){try{await b`DELETE FROM collector_zones`;for(const e of o){const t=JSON.stringify(e);await b`
|
||
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",o.length,"collector zones")}catch(e){throw console.error("[Database] ✗ Failed to save collector zones:",e),e}}async function Jr(){try{const o=await b`SELECT properties FROM collector_zones ORDER BY id`;return o.length===0?null:o.map(e=>JSON.parse(e.properties))}catch(o){return console.error("[Database] ✗ Failed to read local collector zones:",o),null}}async function Zr(o,e){try{await b`BEGIN`,await b`DELETE FROM upn_grid`;let t=0;for(const r of o){const n=r.polygon||r.geometry_wkt||r.geom||"";await b`
|
||
INSERT INTO upn_grid (districtid, upn_prefix, geometry_wkt, fetched_at)
|
||
VALUES (${X(e)}, ${r.upn_prefix??null}, ${n}, CURRENT_TIMESTAMP)
|
||
`,t++}await b`COMMIT`,console.log("[Database] ✓ Saved",t,"UPN-grid cells (district",e,")")}catch(t){try{await b`ROLLBACK`}catch{}throw console.error("[Database] ✗ Failed to save UPN grid:",t),t}}async function Qr(o){try{const e=await b`
|
||
SELECT id, districtid, upn_prefix, geometry_wkt
|
||
FROM upn_grid
|
||
WHERE districtid = ${X(o)}
|
||
ORDER BY id
|
||
`;return e.length===0?null:e}catch(e){return console.error("[Database] ✗ Failed to read UPN grid:",e),null}}function An(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,o=>{const e=Math.random()*16|0;return(o==="x"?e:e&3|8).toString(16)})}async function es(o){const{filename:e,targetType:t="other",featureCount:r=0}=o,n=An();try{await b`
|
||
INSERT INTO external_imports
|
||
(filename, target_type, status, feature_count, client_import_id)
|
||
VALUES
|
||
(${e}, ${t},
|
||
${t==="other"?"other":"imported"},
|
||
${r}, ${n})
|
||
`;const a=(await b`SELECT last_insert_rowid() AS id`)[0]?.id;return Y("external_imports","INSERT",a),{id:a,client_import_id:n}}catch(s){throw console.error("[Database] ✗ Failed to create external import:",s),s}}async function ts(o,e){if(!Array.isArray(e)||e.length===0)return 0;try{await b`BEGIN`;let t=0;for(const r of e){const n=r.geometry_wkt||"";if(!n)continue;const s=JSON.stringify(r.properties??{}),a=r.client_uuid||An();await b`
|
||
INSERT INTO external_import_features
|
||
(import_id, client_uuid, geometry_wkt, properties_json, upload_status)
|
||
VALUES
|
||
(${o}, ${a}, ${n}, ${s}, 'pending')
|
||
`,t++}return await b`COMMIT`,Y("external_import_features","INSERT",o),t}catch(t){try{await b`ROLLBACK`}catch{}throw console.error("[Database] ✗ Failed to add import features:",t),t}}async function kt(o,e={}){try{const t=await b`SELECT * FROM external_imports WHERE id = ${o}`;if(t.length===0)throw new Error(`Import ${o} not found`);const r=t[0],n=e.targetType??r.target_type,s=e.mapping!==void 0?e.mapping?JSON.stringify(e.mapping):null:r.mapping_json,a=e.status??r.status,i=e.errorCount??r.error_count,l=e.lastUploadedAt??r.last_uploaded_at;await b`
|
||
UPDATE external_imports SET
|
||
target_type = ${n},
|
||
mapping_json = ${s},
|
||
status = ${a},
|
||
error_count = ${i},
|
||
last_uploaded_at = ${l}
|
||
WHERE id = ${o}
|
||
`,Y("external_imports","UPDATE",o)}catch(t){throw console.error("[Database] ✗ Failed to update external import:",t),t}}async function os(o){try{const e=await b`SELECT * FROM external_imports WHERE id = ${o}`;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 ns(o){try{return(await b`
|
||
SELECT id, client_uuid, geometry_wkt, properties_json,
|
||
upload_status, server_id, error_message
|
||
FROM external_import_features
|
||
WHERE import_id = ${o}
|
||
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 rs(o,e){try{const t=await b`
|
||
SELECT id, properties_json
|
||
FROM external_import_features
|
||
WHERE import_id = ${o}
|
||
`;if(t.length===0)return 0;await b`BEGIN`;let r=0;for(const n of t){const s=n.properties_json?JSON.parse(n.properties_json):{},a=e(s)??{};await b`
|
||
UPDATE external_import_features
|
||
SET properties_json = ${JSON.stringify(a)}
|
||
WHERE id = ${n.id}
|
||
`,r++}return await b`COMMIT`,Y("external_import_features","UPDATE",o),r}catch(t){try{await b`ROLLBACK`}catch{}throw console.error("[Database] ✗ Failed to remap import features:",t),t}}async function ss(o,e,t){try{if(t!==void 0){const r=JSON.stringify(t);await b`
|
||
UPDATE external_import_features
|
||
SET geometry_wkt = ${e}, properties_json = ${r}
|
||
WHERE client_uuid = ${o}
|
||
`}else await b`
|
||
UPDATE external_import_features
|
||
SET geometry_wkt = ${e}
|
||
WHERE client_uuid = ${o}
|
||
`;Y("external_import_features","UPDATE",o)}catch(r){throw console.error("[Database] ✗ Failed to update import feature geometry:",r),r}}async function as(o){try{const e=await b`
|
||
SELECT import_id FROM external_import_features WHERE client_uuid = ${o}
|
||
`;if(e.length===0)return;const t=e[0].import_id;await b`BEGIN`;try{await b`DELETE FROM external_import_features WHERE client_uuid = ${o}`,await b`
|
||
UPDATE external_imports
|
||
SET feature_count = MAX(0, feature_count - 1)
|
||
WHERE id = ${t}
|
||
`,await b`COMMIT`}catch(r){try{await b`ROLLBACK`}catch{}throw r}Y("external_import_features","DELETE",o),Y("external_imports","UPDATE",t)}catch(e){throw console.error("[Database] ✗ Failed to delete import feature:",e),e}}function X(o){if(o===""||o===null||o===void 0)return null;const e=Number(o);return Number.isNaN(e)?null:e}async function is(o){try{await b`BEGIN`,await b`DELETE FROM parcels`;let e=0;for(const t of o){const r=t.id??t.parcelid??t.parcel_id??null;if(r==null)continue;const n=t.boundary||t.geometry_wkt||t.polygon||t.wkt||(typeof t.geom=="string"?t.geom:"");await b`
|
||
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}, ${n},
|
||
${t.created_at??null}, ${t.updated_at??null}, ${X(t.districtid)},
|
||
'verified', CURRENT_TIMESTAMP
|
||
)
|
||
`,e++}await b`COMMIT`,console.log("[Database] ✓ Saved",e,"parcels (from",o.length,"rows,",o.length-e,"skipped/replaced)")}catch(e){try{await b`ROLLBACK`}catch{}throw console.error("[Database] ✗ Failed to save parcels:",e),e}}async function ls(){try{const o=await b`SELECT * FROM parcels ORDER BY id`;return o.length===0?null:o}catch(o){return console.error("[Database] ✗ Failed to read local parcels:",o),null}}async function cs(o,e){try{await b`
|
||
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 = ${o}
|
||
`,console.log("[Database] ✓ Parcel updated:",o),Y("parcels","UPDATE",o)}catch(t){throw console.error("[Database] ✗ Failed to update parcel:",o,t),t}}async function ds(o,e={}){try{await b`
|
||
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}, ${o},
|
||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ${X(e.districtid)},
|
||
'new', CURRENT_TIMESTAMP
|
||
)
|
||
`;const r=(await b`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 us(o){try{if(o.length>0){const e=o[0],t={};for(const[r,n]of Object.entries(e))t[r]=n===null?"null":typeof n;console.log("[Database] First footprint field types:",t)}await b`DELETE FROM building_footprints`;for(const e of o){const t=JSON.stringify(e);let r=e.polygon||e.boundary||e.geom||e.wkt||e.footprint||"";const n=typeof r=="object"?JSON.stringify(r):String(r);let s=e.id||e.footprint_id||e.building_id||null;await b`
|
||
INSERT INTO building_footprints (id, geometry_wkt, properties, fetched_at)
|
||
VALUES (${s!==null&&typeof s=="object"?null:s}, ${n}, ${t}, CURRENT_TIMESTAMP)
|
||
`}console.log("[Database] ✓ Saved",o.length,"building footprints")}catch(e){throw console.error("[Database] ✗ Failed to save building footprints:",e),e}}async function ps(){try{const o=await b`SELECT properties FROM building_footprints ORDER BY id`;return o.length===0?null:o.map(e=>JSON.parse(e.properties))}catch(o){return console.error("[Database] ✗ Failed to read local building footprints:",o),null}}async function fs(o){try{if(o.length>0){const e=o[0],t={};for(const[r,n]of Object.entries(e))t[r]=n===null?"null":typeof n;console.log("[Database] First road field types:",t)}await b`DELETE FROM osm_roads`;for(const e of o){const t=JSON.stringify(e);let r=e.geom||e.geometry||e.wkt||e.road||e.line||"";const n=typeof r=="object"?JSON.stringify(r):String(r);let s=e.osm_id??e.osmid??e.id??null;await b`
|
||
INSERT OR REPLACE INTO osm_roads (osm_id, geometry_wkt, properties, fetched_at)
|
||
VALUES (${s!==null&&typeof s=="object"?null:s}, ${n}, ${t}, CURRENT_TIMESTAMP)
|
||
`}console.log("[Database] ✓ Saved",o.length,"OSM roads")}catch(e){throw console.error("[Database] ✗ Failed to save OSM roads:",e),e}}async function hs(){try{const o=await b`SELECT properties FROM osm_roads ORDER BY osm_id`;return o.length===0?null:o.map(e=>JSON.parse(e.properties))}catch(o){return console.error("[Database] ✗ Failed to read local OSM roads:",o),null}}async function gs(){return xn.getDatabaseFile()}async function ms(o="lupmis-backup.sqlite3"){const e=await gs(),t=new Blob([e],{type:"application/x-sqlite3"}),r=URL.createObjectURL(t),n=document.createElement("a");n.href=r,n.download=o,n.click(),URL.revokeObjectURL(r)}async function ys(){return{type:"FeatureCollection",features:(await In()).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 yo(){try{const o=await b`
|
||
SELECT name FROM sqlite_master
|
||
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
||
ORDER BY name
|
||
`,e=await Xr();return{ready:Tn,databasePath:mo,tables:o.map(t=>t.name),locationCount:e}}catch(o){return{ready:!1,error:o.message}}}const Cn=Object.freeze(["parcels","building_footprints","osm_roads","collector_zones","upn_grid","remote_data"]);function Fn(o){return Cn.includes(o)}async function On(o){if(!Fn(o))throw new Error(`Refusing to clear "${o}" — not a known cached-layer table`);const t=(await b(`SELECT COUNT(*) AS n FROM "${o}"`))[0]?.n??0;return await b(`DELETE FROM "${o}"`),console.log(`[Database] ✓ Cleared "${o}" (${t} rows)`),Y(o,"CLEAR",null),t}async function bo(){const o=await b`
|
||
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(o.map(n=>n.name)),t=[];for(const n of Cn)if(e.has(n))try{const s=await On(n);t.push({table:n,count:s})}catch(s){console.error(`[Database] Failed to clear ${n}:`,s),t.push({table:n,count:0,error:s.message})}const r=t.reduce((n,s)=>n+s.count,0);return console.log(`[Database] ✓ Cleared all cached layers: ${r} rows across ${t.length} tables`),t}async function bs(){const o=await b`
|
||
SELECT name FROM sqlite_master
|
||
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
||
ORDER BY name
|
||
`;if(o.length===0)return[];const e=o.map(t=>`SELECT '${t.name}' AS name, COUNT(*) AS count FROM "${t.name}"`).join(" UNION ALL ");return b(e)}async function ws(o,e=200){if((await b`
|
||
SELECT name FROM sqlite_master
|
||
WHERE type='table' AND name = ${o}
|
||
`).length===0)throw new Error(`Table "${o}" does not exist`);const r=await b(`SELECT * FROM "${o}" LIMIT ${e}`);return{columns:r.length>0?Object.keys(r[0]):[],rows:r}}async function _s(){console.log("=== DATABASE TEST ===");try{const o=await b`SELECT sqlite_version() as v`;console.log("1. SQLite version:",o[0].v);const e=await b`SELECT name FROM sqlite_master WHERE type='table'`;console.log("2. Tables:",e.map(n=>n.name)),console.log("3. Inserting test row..."),await b`INSERT INTO locations (name, longitude, latitude, category) VALUES ('TEST', -1.0, 7.0, 'test')`;const t=await b`SELECT * FROM locations WHERE name = 'TEST'`;console.log("4. Test row:",t);const r=await b`SELECT COUNT(*) as c FROM locations`;return console.log("5. Total rows:",r[0].c),await b`DELETE FROM locations WHERE name = 'TEST'`,console.log("6. Test row deleted"),console.log("=== TEST PASSED ==="),!0}catch(o){return console.error("=== TEST FAILED ===",o),!1}}typeof window<"u"&&(window.testDatabase=_s,window.dbStatus=yo);async function vs(o){const{uuid:e,name:t=null,startedAt:r,districtId:n=null}=o;await b`
|
||
INSERT INTO gps_trails (client_uuid, name, district_id, started_at, status)
|
||
VALUES (${e}, ${t}, ${n}, ${r}, 'recording')
|
||
`;const a=(await b`SELECT last_insert_rowid() as id`)[0]?.id;return Y("gps_trails","insert",a),a}async function Es(o,e){const{seq:t,lon:r,lat:n,altitude:s=null,accuracy:a=null,altitudeAccuracy:i=null,heading:l=null,speed:c=null,satellites:d=null,timestamp:u}=e,f=typeof u=="number"?new Date(u).toISOString():u||new Date().toISOString();await b`
|
||
INSERT INTO gps_trail_points
|
||
(trail_id, seq, longitude, latitude, altitude, accuracy, altitude_accuracy, heading, speed, satellites, recorded_at)
|
||
VALUES
|
||
(${o}, ${t}, ${r}, ${n}, ${s}, ${a}, ${i}, ${l}, ${c}, ${d}, ${f})
|
||
`}async function xs(o,e){const{endedAt:t,pointCount:r=0,distanceM:n=0}=e;await b`
|
||
UPDATE gps_trails
|
||
SET ended_at = ${t}, point_count = ${r}, distance_m = ${n}, status = 'completed'
|
||
WHERE id = ${o}
|
||
`,Y("gps_trails","update",o)}async function Ss(){return b`SELECT * FROM gps_trails WHERE synced = 0 AND status = 'completed' ORDER BY started_at ASC`}async function Ts(o){return b`SELECT * FROM gps_trail_points WHERE trail_id = ${o} ORDER BY seq ASC`}async function Ls(o,e=null){await b`UPDATE gps_trails SET synced = 1, remote_id = ${e} WHERE id = ${o}`,Y("gps_trails","update",o)}const Dn=3.28084,Rn=621371e-9,Nn=10.7639,$n=247105e-9,Bn=3861e-10;function At(){return localStorage.getItem("measurement-system")||"metric"}function rt(o){if(At()==="imperial"){const e=o*Dn;return e>=5280?Math.round(o*Rn*100)/100+" mi":Math.round(e)+" ft"}return o>1e3?Math.round(o/1e3*100)/100+" km":Math.round(o*100)/100+" m"}function ks(o){if(At()==="imperial"){const e=o*Dn,t=o*Rn;return e>=5280?`${t.toFixed(2)} mi (${e.toLocaleString("en",{maximumFractionDigits:0})} ft)`:`${e.toLocaleString("en",{maximumFractionDigits:1})} ft`}return o>=1e3?`${(o/1e3).toFixed(2)} km (${o.toLocaleString("en",{maximumFractionDigits:0})} m)`:`${o.toLocaleString("en",{maximumFractionDigits:1})} m`}function qe(o){if(At()==="imperial"){const e=o*$n;return e>=640?Math.round(o*Bn*100)/100+" mi²":e>=1?Math.round(e*100)/100+" acres":Math.round(o*Nn).toLocaleString("en")+" ft²"}return o>1e6?Math.round(o/1e6*100)/100+" km²":Math.round(o*100)/100+" m²"}function Is(o){if(At()==="imperial"){const e=o*Nn,t=o*$n,r=o*Bn;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 o>1e6?`${(o/1e6).toFixed(2)} km² (${o.toLocaleString("en",{maximumFractionDigits:0})} m²)`:`${o.toLocaleString("en",{maximumFractionDigits:0})} m²`}function Ps(o){return qe(Math.PI*o*o)}function Gn(o){const e=xe(o)[1];return Math.cos(e*Math.PI/180)}function Qt(o,e){return o*Gn(e)}function Ms(o,e){const t=Gn(e);return t===0?o:o/t}function As(o){return o<50?1:o<200?5:o<1e3?10:o<5e3?50:100}function Cs(o,e,t,r,n=1e-10){const s=e[0]-o[0],a=e[1]-o[1],i=r[0]-t[0],l=r[1]-t[1],c=s*l-a*i;if(Math.abs(c)<n)return null;const d=t[0]-o[0],u=t[1]-o[1],f=(d*l-u*i)/c,p=(d*a-u*s)/c;return f<-n||f>1+n||p<-n||p>1+n?null:{point:[o[0]+f*s,o[1]+f*a],t:Math.max(0,Math.min(1,f)),u:Math.max(0,Math.min(1,p))}}function Un(o){let e=0;for(let t=0,r=o.length;t<r-1;t++)e+=o[t][0]*o[t+1][1]-o[t+1][0]*o[t][1];return e/2}function eo(o,e){let t=!1;for(let r=0,n=e.length-2;r<e.length-1;n=r++){const s=e[r][0],a=e[r][1],i=e[n][0],l=e[n][1];a>o[1]!=l>o[1]&&o[0]<(i-s)*(o[1]-a)/(l-a)+s&&(t=!t)}return t}function it(o,e){return(o[0]-e[0])**2+(o[1]-e[1])**2}function Fs(o,e){const t=[];for(let n=0;n<e.length-1;n++)for(let s=0;s<o.length-1;s++){const a=Cs(o[s],o[s+1],e[n],e[n+1],1e-10);if(!a)continue;const i=a.point;let l=!1;for(const c of t)if(it(c.point,i)<1e-6){l=!0;break}l||t.push({point:i,ringSegIdx:s,ringT:a.t,lineSegIdx:n,lineT:a.u})}return t.sort((n,s)=>n.lineSegIdx!==s.lineSegIdx?n.lineSegIdx-s.lineSegIdx:n.lineT-s.lineT),t}function Os(o,e){const t=e.map((s,a)=>({...s,origOrder:a}));t.sort((s,a)=>s.ringSegIdx!==a.ringSegIdx?s.ringSegIdx-a.ringSegIdx:s.ringT-a.ringT);const r=o.slice(),n=new Array(t.length);for(let s=t.length-1;s>=0;s--){const a=t[s],i=a.ringSegIdx+1,l=1e-6;if(it(a.point,r[a.ringSegIdx])<l){n[a.origOrder]=a.ringSegIdx;continue}if(it(a.point,r[a.ringSegIdx+1])<l){n[a.origOrder]=a.ringSegIdx+1;continue}r.splice(i,0,a.point),n[a.origOrder]=i;for(let c=s+1;c<t.length;c++)n[t[c].origOrder]>=i&&n[t[c].origOrder]++}return{ring:r,indices:n}}function $o(o,e,t){const r=o.length-1,n=(e%r+r)%r,s=(t%r+r)%r,a=[];let i=n;for(;a.push(o[i]),i!==s;)i=(i+1)%r;return a}function Bo(o,e,t){const r=[e.point],n=e.lineSegIdx,s=t.lineSegIdx;for(let a=n+1;a<=s;a++)r.push(o[a]);return it(r[r.length-1],t.point)>1e-10&&r.push(t.point),r}function Go(o,e){const t=Un(o);return e&&t<0||!e&&t>0?o.slice().reverse():o}function Uo(o){if(o.length<2)return o;const e=o[0],t=o[o.length-1];return it(e,t)>1e-10?[...o,e.slice()]:o}function Ds(o,e){let t=1/0,r=1/0,n=-1/0,s=-1/0;for(const c of e)c[0]<t&&(t=c[0]),c[1]<r&&(r=c[1]),c[0]>n&&(n=c[0]),c[1]>s&&(s=c[1]);const a=Math.sqrt((n-t)**2+(s-r)**2)||1,i=o.slice();if(eo(i[0],e)){const c=i[0],d=i[1],u=c[0]-d[0],f=c[1]-d[1],p=Math.sqrt(u*u+f*f)||1,h=a*2/p;i[0]=[c[0]+u*h,c[1]+f*h]}const l=i.length-1;if(eo(i[l],e)){const c=i[l],d=i[l-1],u=c[0]-d[0],f=c[1]-d[1],p=Math.sqrt(u*u+f*f)||1,h=a*2/p;i[l]=[c[0]+u*h,c[1]+f*h]}return i}function wt(o,e){const t=o[0],r=o.slice(1),n=Ds(e,t),s=Fs(t,n);if(s.length!==2)return console.warn(`[polygonSplit] Expected 2 intersections, got ${s.length}`),null;const[a,i]=s,{ring:l,indices:c}=Os(t,s),d=c[0],u=c[1],[f,p]=d<u?[d,u]:[u,d],h=d<u?Bo(n,a,i):Bo(n,i,a),g=h.slice().reverse(),m=$o(l,f,p),y=Uo([...m,...g.slice(1)]),w=$o(l,p,f),_=Uo([...w,...h.slice(1)]),E=Un(t)>0,T=Go(y,E),x=Go(_,E),S=[T],P=[x];for(const k of r){const j=Rs(k);eo(j,T)?S.push(k):P.push(k)}return[S,P]}function Rs(o){let e=0,t=0;const r=o.length-1;for(let n=0;n<r;n++)e+=o[n][0],t+=o[n][1];return[e/r,t/r]}const jo={success:{bg:"#10b981",icon:"✅"},error:{bg:"#ef4444",icon:"❌"},warning:{bg:"#f59e0b",icon:"⚠️"},info:{bg:"#0ea5e9",icon:"ℹ️"}};let Re=null;function Ns(){return Re||(Re=document.createElement("div"),Re.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(Re),Re)}function F(o,e="info",t=4e3){const r=Ns(),n=jo[e]||jo.info,s=document.createElement("div");s.style.cssText=`
|
||
background: ${n.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;
|
||
`,s.textContent=`${n.icon} ${o}`,r.appendChild(s),requestAnimationFrame(()=>{s.style.opacity="1",s.style.transform="translateY(0)"});const a=()=>{s.style.opacity="0",s.style.transform="translateY(-8px)",setTimeout(()=>s.remove(),300)};s.addEventListener("click",a),setTimeout(a,t)}const ft=[{stroke:"#ef4444",fill:"rgba(239,68,68,0.25)"},{stroke:"#3b82f6",fill:"rgba(59,130,246,0.25)"}],$s=new M({stroke:new I({color:"#0ea5e9",width:3}),fill:new A({color:"rgba(14,165,233,0.15)"})}),Bs=new M({stroke:new I({color:"#f43f5e",width:2,lineDash:[8,6]}),image:new he({radius:5,fill:new A({color:"#f43f5e"}),stroke:new I({color:"#fff",width:1.5})})});class Gs extends fo{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:$s})}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(n=>{n.getVisible()&&(n.getSource&&n.getSource()instanceof G?e.push(n.getSource()):n.getLayers&&t(n.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 n=r.feature.clone();this._overlaySource.addFeature(n),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 n of this._getSources()){const s=n.getClosestFeatureToCoordinate(e.coordinate);if(!s)continue;const a=s.getGeometry();if(!a)continue;const i=a.getType();if(i!=="Polygon"&&i!=="MultiPolygon")continue;const l=a.getClosestPoint(e.coordinate),d=new de([e.coordinate,l]).getLength()/e.frameState.viewState.resolution;d<r&&(r=d,t={feature:s,source:n,coord:l})}return t}_startDrawPhase(){this._phase="draw";const e=this.getMap();e&&(e.getTargetElement().style.cursor="crosshair",this._drawInteraction=new Ce({type:"LineString",style:Bs}),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,n=t.getGeometry();let s;n.getType()==="Polygon"?s=n.getCoordinates():n.getType()==="MultiPolygon"&&(s=n.getCoordinates()[0]);const a=wt(s,e);if(!a){console.warn("[PolygonSplit] Split failed — line must cross the polygon boundary at exactly 2 points."),this._removeDrawInteraction(),this._startDrawPhase();return}const[i,l]=a,c=t.clone();c.setGeometry(new at(i)),c.setStyle(new M({stroke:new I({color:ft[0].stroke,width:2.5}),fill:new A({color:ft[0].fill})}));const d=t.clone();d.setGeometry(new at(l)),d.setStyle(new M({stroke:new I({color:ft[1].stroke,width:2.5}),fill:new A({color:ft[1].fill})}));const u=[c,d];this.dispatchEvent({type:"beforesplit",original:t,features:u}),r.dispatchEvent({type:"beforesplit",original:t,features:u});const f=this.getMap();if(f?.dispatchEvent("undoblockstart"),r.removeFeature(t),r.addFeature(c),r.addFeature(d),f?.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 n=r.clone();this._overlaySource.addFeature(n),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 n of this._splitFeatures){const s=n.getGeometry();if(!s)continue;const a=s.getClosestPoint(e.coordinate),l=new de([e.coordinate,a]).getLength()/e.frameState.viewState.resolution;l<r&&(r=l,t=n)}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 Ve(o,e){return(o[0]-e[0])**2+(o[1]-e[1])**2}function ot(o){let e=0;for(let t=0,r=o.length;t<r-1;t++)e+=o[t][0]*o[t+1][1]-o[t+1][0]*o[t][1];return e/2}function Us(o,e){let t=!1;for(let r=0,n=e.length-2;r<e.length-1;n=r++){const s=e[r][0],a=e[r][1],i=e[n][0],l=e[n][1];a>o[1]!=l>o[1]&&o[0]<(i-s)*(o[1]-a)/(l-a)+s&&(t=!t)}return t}function js(o,e){const t=ot(o);return e&&t<0||!e&&t>0?o.slice().reverse():o}function zs(o){return o.length<2?o:Ve(o[0],o[o.length-1])>1e-10?[...o,o[0].slice()]:o}function ze(o,e,t){const r=t[0]-e[0],n=t[1]-e[1],s=r*r+n*n;if(s<1e-20)return Ve(o,e);let a=((o[0]-e[0])*r+(o[1]-e[1])*n)/s;a=Math.max(0,Math.min(1,a));const i=e[0]+a*r,l=e[1]+a*n;return(o[0]-i)**2+(o[1]-l)**2}function zo(o,e){let t=0,r=1/0;const n=o.length-1;for(let s=0;s<n;s++){const a=ze(e,o[s],o[(s+1)%n===0?n:s+1]);a<r&&(r=a,t=s)}return{segIdx:t,distSq:r}}function Ne(o,e,t){return Ve(o,e)<t}function ht(o,e,t){const r=e.length-1;for(let n=0;n<r;n++)if(ze(o,e[n],e[n+1])<t)return!0;return!1}function qs(o,e,t,r,n){const s=o.length-1,a=e.length-1,i=n*n,l=o[t],c=o[(t+1)%s],d=e[r],u=e[(r+1)%a],f=ht(l,e,i),p=ht(c,e,i),h=ht(d,o,i),g=ht(u,o,i);if(!(f&&p)&&!(h&&g))return console.warn("[polygonMerge] Seed edges are not on the shared boundary"),null;let m;Ne(l,u,i)&&Ne(c,d,i)?m=!0:Ne(l,d,i)&&Ne(c,u,i)?m=!1:m=Ve(l,u)<Ve(l,d);let y=t,w=(t+1)%s,_,E;m?(_=(r+1)%a,E=r):(_=r,E=(r+1)%a);let T=s+a;for(;T-- >0;){const x=(w+1)%s,S=m?(E-1+a)%a:(E+1)%a;if(x===y||S===_)break;if(Ne(o[x],e[S],i)){w=x,E=S;continue}if(ze(o[x],e[E],e[S])<i){w=x;continue}if(ze(e[S],o[w],o[x])<i){E=S;continue}break}for(T=s+a;T-- >0;){const x=(y-1+s)%s,S=m?(_+1)%a:(_-1+a)%a;if(x===w||S===E)break;if(Ne(o[x],e[S],i)){y=x,_=S;continue}if(ze(o[x],e[_],e[S])<i){y=x;continue}if(ze(e[S],o[y],o[x])<i){_=S;continue}break}return{startA:y,endA:w,startB:_,endB:E,reversed:m}}function Gt(o,e,t){const r=o.length-1,n=[];let s=e;for(;n.push(o[s]),!(s===t||(s=(s+1)%r,n.length>r+1)););return n}function Hs(o,e,t,r,n=5){const s=o[0],a=e[0],i=o.slice(1),l=e.slice(1),c=zo(s,t),d=zo(a,r),u=qs(s,a,c.segIdx,d.segIdx,n);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:f,endA:p,startB:h,endB:g,reversed:m}=u;s.length-1,a.length-1;const y=Gt(s,p,f);let w;m?w=Gt(a,h,g):w=Gt(a,g,h);const _=[...y,...w.slice(1)],E=n*n;_.length>2&&Ve(_[_.length-1],_[0])<E&&(_[_.length-1]=_[0].slice());const T=zs(_),x=Math.abs(ot(s)),S=Math.abs(ot(a)),P=Math.abs(ot(T)),k=x+S;if(P<k*.5||P>k*1.5)return console.warn(`[polygonMerge] Area mismatch: A=${x.toFixed(1)}, B=${S.toFixed(1)}, merged=${P.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 j=ot(s)>0,q=js(T,j),$=[...i,...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 Us([Q,ye],q)});return{coords:[q,...$]}}const qo=new M({stroke:new I({color:"#0ea5e9",width:3}),fill:new A({color:"rgba(14,165,233,0.15)"})}),Ws=new M({stroke:new I({color:"#f59e0b",width:3}),fill:new A({color:"rgba(245,158,11,0.15)"})}),Vs=new M({stroke:new I({color:"#0ea5e9",width:3}),fill:new A({color:"rgba(14,165,233,0.15)"}),text:new We({text:"A",font:"bold 22px Exo, sans-serif",fill:new A({color:"#0ea5e9"}),stroke:new I({color:"#fff",width:4}),overflow:!0})}),Ks=new M({stroke:new I({color:"#f59e0b",width:3}),fill:new A({color:"rgba(245,158,11,0.15)"}),text:new We({text:"B",font:"bold 22px Exo, sans-serif",fill:new A({color:"#f59e0b"}),stroke:new I({color:"#fff",width:4}),overflow:!0})}),Xs=new M({stroke:new I({color:"#ec4899",width:4,lineDash:[10,6]})}),Ys=new M({stroke:new I({color:"#10b981",width:2.5}),fill:new A({color:"rgba(16,185,129,0.3)"})});class Js extends fo{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")||qo}),this._edgeSource=new G({useSpatialIndex:!1}),this._edgeLayer=new D({source:this._edgeSource,displayInLayerSwitcher:!1,style:Xs})}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(n=>{n.getVisible()&&(n.getSource&&n.getSource()instanceof G?e.push(n.getSource()):n.getLayers&&t(n.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 n=this._closestPolygon(e,t);if(n){const s=this._phase==="select_a"?qo:Ws,a=n.feature.clone();a.set("_highlightStyle",s),this._highlightSource.addFeature(a),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,n=this.snapDistance_+1;for(const s of this._getSources()){const a=s.getClosestFeatureToCoordinate(e.coordinate);if(!a||t&&a===t)continue;const i=a.getGeometry();if(!i)continue;const l=i.getType();if(l!=="Polygon"&&l!=="MultiPolygon")continue;const c=i.getClosestPoint(e.coordinate),u=new de([e.coordinate,c]).getLength()/e.frameState.viewState.resolution;u<n&&(n=u,r={feature:a,source:s,coord:c})}return r}_onEdgeMove(e,t){const r=this.getMap();if(!r)return!0;this._edgeSource.clear();const n=this._closestEdgeSegment(t,e);if(n){const s=new fe(new de([n.segStart,n.segEnd]));this._edgeSource.addFeature(s),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 n;if(r.getType()==="Polygon")n=r.getCoordinates()[0];else if(r.getType()==="MultiPolygon")n=r.getCoordinates()[0][0];else return null;const s=t.frameState.viewState.resolution;let a=1/0,i=null;const l=n.length-1;for(let c=0;c<l;c++){const d=n[c],u=n[c+1],f=u[0]-d[0],p=u[1]-d[1],h=f*f+p*p;if(h<1e-20)continue;let g=((t.coordinate[0]-d[0])*f+(t.coordinate[1]-d[1])*p)/h;g=Math.max(0,Math.min(1,g));const m=d[0]+g*f,y=d[1]+g*p,w=Math.sqrt((t.coordinate[0]-m)**2+(t.coordinate[1]-y)**2)/s;w<a&&(a=w,i={segStart:d,segEnd:u})}return a<=this.snapDistance_?i:null}_performMerge(){const e=this._featureA,t=this._featureB,r=this._sourceA,n=this._sourceB,s=e.getGeometry(),a=t.getGeometry(),i=s.getType()==="Polygon"?s.getCoordinates():s.getCoordinates()[0],l=a.getType()==="Polygon"?a.getCoordinates():a.getCoordinates()[0],c=Hs(i,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 at(c.coords)),d.setStyle(Ys);const u={type:"beforemerge",original:[e,t],merged:d};this.dispatchEvent(u),r.dispatchEvent({...u}),n!==r&&n.dispatchEvent({...u});const f=this.getMap();f?.dispatchEvent("undoblockstart"),r.removeFeature(e),n.removeFeature(t),r.addFeature(d),f?.dispatchEvent("undoblockend");const p={type:"aftermerge",original:[e,t],merged:d};this.dispatchEvent(p),r.dispatchEvent({...p}),n!==r&&n.dispatchEvent({...p});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",Vs),t.set("_permanent",!0),this._highlightSource.addFeature(t)}if(this._featureB){const t=this._featureB.clone();t.set("_highlightStyle",Ks),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 Zs(o,e){return(o[0]-e[0])**2+(o[1]-e[1])**2}function Ho(o){let e=0;for(let t=0,r=o.length;t<r-1;t++)e+=o[t][0]*o[t+1][1]-o[t+1][0]*o[t][1];return e/2}function et(o){let e=Math.abs(Ho(o[0]));for(let t=1;t<o.length;t++)e-=Math.abs(Ho(o[t]));return e}function Qs(o){const e=o.length-1;let t=-1,r=0;for(let c=0;c<e;c++){const d=Zs(o[c],o[c+1]);d>t&&(t=d,r=c)}const n=o[r],s=o[r+1],a=Math.sqrt(t),i=[(s[0]-n[0])/a,(s[1]-n[1])/a],l=[-i[1],i[0]];return{p0:n,p1:s,along:i,perp:l}}function Ut(o,e,t,r,n){const s=o[0]+r*e[0],a=o[1]+r*e[1];return[[s-n*t[0],a-n*t[1]],[s+n*t[0],a+n*t[1]]]}function $e(o,e,t){const r=o[0],n=r.length-1;let s=0,a=0;for(let c=0;c<n;c++)s+=r[c][0],a+=r[c][1];const i=s/n-e[0],l=a/n-e[1];return i*t[0]+l*t[1]}function ea(o,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:[o]};const r=o[0];if(et(o)<1e-6)return{pieces:null,error:"Polygon has no measurable area."};let s,a,i;if(t&&t.length===2){s=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."};a=[m/w,y/w],i=[-a[1],a[0]]}else{const m=Qs(r);s=m.p0,a=m.along,i=m.perp}const l=s,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*a[0]+w*a[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],_=y*i[0]+w*i[1];_<d&&(d=_),_>u&&(u=_)}const f=(u-d)*1.5,p=[];let h=o,g=e;for(let m=0;m<e-1;m++){const y=et(h),w=y/g,_=h[0],E=_.length-1;let T=1/0,x=-1/0;for(let Z=0;Z<E;Z++){const $=_[Z][0]-l[0],ce=_[Z][1]-l[1],Q=$*a[0]+ce*a[1];Q<T&&(T=Q),Q>x&&(x=Q)}let S=T,P=x,k=null,j=null,q=1/0;for(let Z=0;Z<40;Z++){const $=(S+P)/2,ce=Ut(l,a,i,$,f),Q=wt(h,ce);if(!Q){const R=(P-S)*.01,W=Ut(l,a,i,$+R,f),re=wt(h,W);if(re){const[Ie,Pe]=re,Ye=$e(Ie,l,a),Je=$e(Pe,l,a),Ze=Ye<Je?Ie:Pe,Dt=Ye<Je?Pe:Ie,Rt=et(Ze),Qe=Math.abs(Rt-w);Qe<q&&(q=Qe,k=Ze,j=Dt)}const Ot=Ut(l,a,i,$-R,f),we=wt(h,Ot);if(we){const[Ie,Pe]=we,Ye=$e(Ie,l,a),Je=$e(Pe,l,a),Ze=Ye<Je?Ie:Pe,Dt=Ye<Je?Pe:Ie,Rt=et(Ze),Qe=Math.abs(Rt-w);Qe<q&&(q=Qe,k=Ze,j=Dt)}S=$;continue}const[ye,H]=Q,be=$e(ye,l,a),Xe=$e(H,l,a),B=be<Xe?ye:H,J=be<Xe?H:ye,V=et(B),ne=Math.abs(V-w);if(ne<q&&(q=ne,k=B,j=J),ne/y<.001)break;V<w?S=$:P=$}if(!k||!j)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.`};p.push(k),h=j,g--}return p.push(h),{pieces:p}}const ta=new M({stroke:new I({color:"#0ea5e9",width:3}),fill:new A({color:"rgba(14,165,233,0.15)"})}),oa=new M({stroke:new I({color:"#8b5cf6",width:4,lineDash:[10,6]})});function na(o){const e=[];for(let t=0;t<o;t++){const r=Math.round(t*360/o);e.push({stroke:`hsl(${r}, 70%, 45%)`,fill:`hsla(${r}, 70%, 55%, 0.25)`})}return e}class ra extends fo{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:ta}),this._edgeSource=new G({useSpatialIndex:!1}),this._edgeLayer=new D({source:this._edgeSource,displayInLayerSwitcher:!1,style:oa})}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(n=>{n.getVisible()&&(n.getSource&&n.getSource()instanceof G?e.push(n.getSource()):n.getLayers&&t(n.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 n=r.feature.clone();this._overlaySource.addFeature(n),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 n of this._getSources()){const s=n.getClosestFeatureToCoordinate(e.coordinate);if(!s)continue;const a=s.getGeometry();if(!a)continue;const i=a.getType();if(i!=="Polygon"&&i!=="MultiPolygon")continue;const l=a.getClosestPoint(e.coordinate),d=new de([e.coordinate,l]).getLength()/e.frameState.viewState.resolution;d<r&&(r=d,t={feature:s,source:n})}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 n=new fe(new de([r.segStart,r.segEnd]));this._edgeSource.addFeature(n),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 n=this._selectedFeature.getGeometry().getExtent(),s=[(n[0]+n[2])/2,(n[1]+n[3])/2];return this.dispatchEvent({type:"divideform",feature:this._selectedFeature,source:this._selectedSource,coordinate:s}),!1}_closestEdgeSegment(e,t){const r=e.getGeometry();let n;if(r.getType()==="Polygon")n=r.getCoordinates()[0];else if(r.getType()==="MultiPolygon")n=r.getCoordinates()[0][0];else return null;const s=t.frameState.viewState.resolution;let a=1/0,i=null;const l=n.length-1;for(let c=0;c<l;c++){const d=n[c],u=n[c+1],f=u[0]-d[0],p=u[1]-d[1],h=f*f+p*p;if(h<1e-20)continue;let g=((t.coordinate[0]-d[0])*f+(t.coordinate[1]-d[1])*p)/h;g=Math.max(0,Math.min(1,g));const m=d[0]+g*f,y=d[1]+g*p,w=Math.sqrt((t.coordinate[0]-m)**2+(t.coordinate[1]-y)**2)/s;w<a&&(a=w,i={segStart:d,segEnd:u})}return a<=this.snapDistance_?i:null}performDivide(e){if(this._phase!=="form"||!this._selectedFeature)return;const t=this._selectedFeature,r=this._selectedSource,n=t.getGeometry();let s;n.getType()==="Polygon"?s=n.getCoordinates():n.getType()==="MultiPolygon"&&(s=n.getCoordinates()[0]);const a=ea(s,e,this._selectedEdge);if(!a.pieces){F(a.error||"Division failed.","error",5e3),this._reset();return}const i=na(e),l=a.pieces.map((p,h)=>{const g=t.clone();return g.setGeometry(new at(p)),g.setStyle(new M({stroke:new I({color:i[h].stroke,width:2.5}),fill:new A({color:i[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 p of l)r.addFeature(p);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 n=r.clone();this._overlaySource.addFeature(n),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 n of this._dividedFeatures){const s=n.getGeometry();if(!s)continue;const a=s.getClosestPoint(e.coordinate),l=new de([e.coordinate,a]).getLength()/e.frameState.viewState.resolution;l<r&&(r=l,t=n)}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 sa="cb1_3oda_1_7e628f491723b99347ed6dca";function Wo(o){return`${`https://{a-c}.basemaps.cartocdn.com/${o}/{z}/{x}/{y}.png`}?key=${encodeURIComponent(sa)}`}const gt=[0,-10],aa={"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"},ia=[{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 la{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=i=>{const l=this.categoryEmojis[i];return l?l.emoji:"📍"},this.getCategoryOptionsHtml=()=>Object.entries(this.categoryEmojis).map(([i,{emoji:l,label:c}])=>`<option value="${i}">${l} ${c}</option>`).join(`
|
||
`),this.createEmojiStyle=(i,l=24)=>new M({text:new We({text:i,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[i,{emoji:l}]of Object.entries(this.categoryEmojis))this.categoryStyles[i]=this.createEmojiStyle(l,32);const r=this.createBaseLayers(t.basemap||"topo");this.markersLayer=new D({title:"Markers",source:this.markerSource,style:i=>this.getFeatureStyle(i),visible:!1}),this.overlayGroup=new Me({title:"Overlays"}),this.map=new To({target:e,layers:[r,this.markersLayer,this.overlayGroup],view:new gr({center:ee(t.center||[0,0]),zoom:t.zoom||2,minZoom:t.minZoom||2,maxZoom:t.maxZoom||19})});const n=new Lr({collapsed:!0,mouseover:!0,extent:!0,trash:!1,oninfo:null});this.map.addControl(n),queueMicrotask(()=>{const i=n.element?.querySelector(":scope > button");if(i){const l="/".replace(/\/?$/,"/");i.style.backgroundImage=`url('${l}app-icons/luspa-72x72.png')`}});let s=!1;n.on("drawlist",i=>{this._decorateLayerListItem(i.layer,i.li),s||(s=!0,queueMicrotask(()=>{s=!1,this._refreshLayerSwitcherChrome(n)}))}),this.map.getLayers().on("change",()=>{this._refreshLayerSwitcherChrome(n)}),this._wireLayerSwitcherVisibilityHooks(n),this._createAddLayerDialog(),this._createLegendPanel(),this.scaleBar=new mr({bar:!0,steps:4,text:!0,minWidth:140}),this.map.addControl(this.scaleBar),this._initGpsRendering(),this._createLocationControl(),this._createBaseMapPicker();const a=new kr({placeholder:"Search location...",typing:300,minLength:3,maxItems:10,collapsed:!0});this.map.addControl(a),a.on("select",i=>{const l=i.search;if(l){const c=parseFloat(l.lon),d=parseFloat(l.lat),u=[c,d],f=ee(u);this.navigateTo(c,d,14);const p={coordinate:f,lonLat:u,name:l.display_name||l.name||"Unknown",searchResult:l};this.searchSelectCallbacks.forEach(h=>h(p))}}),this.searchNominatim=a,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 I({color:"#f59e0b",width:2.5}),fill:new A({color:"rgba(245,158,11,0.15)"}),image:new he({radius:6,fill:new A({color:"#f59e0b"}),stroke:new I({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 yr({condition:wr,toggleCondition:br,filter:(p,h)=>!!h,layers:p=>p instanceof D&&p.get("selectable")!==!1}),this._selectInteraction.setActive(!1),this.map.addInteraction(this._selectInteraction),this._dragBoxSelect=new _r({condition:vr}),this._dragBoxSelect.setActive(!1),this.map.addInteraction(this._dragBoxSelect),this._dragBoxSelect.on("boxend",()=>{const p=this._dragBoxSelect.getGeometry().getExtent(),h=this._selectInteraction.getFeatures();this.map.getLayers().forEach(g=>this._boxAddFromLayer(g,p,h)),this._refreshVertexOverlay?.()}),this._selectInteraction.on("change:active",()=>{this._dragBoxSelect.setActive(this._selectInteraction.getActive())}),this._modifyInteraction=new Ir({features:this._selectInteraction.getFeatures()}),this._modifyInteraction.setActive(!1),this._modifyInteraction.on("modifyend",p=>{if(!this._featureModifiedCallbacks?.length)return;const h=p.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 Pr,this.map.addInteraction(this._undoRedo),this.editBar=new Xt({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",p=>{const h=p.feature?.getGeometry?.()?.getCoordinates?.();if(!h)return;this._discardPendingLocationSketch(),this._pendingLocationSketch=p.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 n=new Io({group:!0,className:"ol-editbar-actions",controls:[new tt({html:'<i class="bi bi-arrow-counterclockwise"></i>',className:"ol-undo",title:"Undo",handleClick:()=>{this._undoRedo.hasUndo()&&this._undoRedo.undo()}}),new tt({html:'<i class="bi bi-arrow-clockwise"></i>',className:"ol-redo",title:"Redo",handleClick:()=>{this._undoRedo.hasRedo()&&this._undoRedo.redo()}}),new tt({html:'<i class="bi bi-floppy"></i>',className:"ol-save",title:"Save drawings",handleClick:()=>{this.dispatchEditEvent("save")}})]});this.editBar.addControl(n),this._lineSplitInteraction=new Mr,this._polygonSplitInteraction=new Gs,this.map.addInteraction(this._lineSplitInteraction),this.map.addInteraction(this._polygonSplitInteraction),this._lineSplitInteraction.setActive(!1),this._polygonSplitInteraction.setActive(!1),this._polygonSplitInteraction.on("splitpick",p=>{const h=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"];for(const g of p.features)if(g!==p.picked)for(const m of h)g.get(m)!==void 0&&g.set(m,"")}),this._polygonDivideInteraction=new ra,this.map.addInteraction(this._polygonDivideInteraction),this._polygonDivideInteraction.setActive(!1);const s=new Se({html:'<i class="bi bi-slash-lg"></i>',className:"ol-split-line",title:"Split Lines",name:"SplitLine",interaction:this._lineSplitInteraction,autoActivate:!0}),a=new Se({html:'<i class="bi bi-scissors"></i>',className:"ol-split-polygon",title:"Split Polygons",name:"SplitPolygon",interaction:this._polygonSplitInteraction}),i=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 Io({toggleOne:!0,autoDeactivate:!0,controls:[s,a,i]}),c=new Se({className:"ol-split",title:"Split",name:"Split",bar:l,onToggle:p=>{p||(this._lineSplitInteraction.setActive(!1),this._polygonSplitInteraction.setActive(!1),this._polygonDivideInteraction.setActive(!1))}});this.editBar.addControl(c),this._polygonDivideInteraction.on("divideform",p=>{this.showDividePopup(p.feature,p.source,p.coordinate)}),this._polygonDivideInteraction.on("dividecancel",()=>{this.hideDividePopup()}),this._polygonDivideInteraction.on("dividepick",p=>{const h=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"];for(const g of p.features)if(g!==p.picked)for(const m of h)g.get(m)!==void 0&&g.set(m,"")}),this._polygonMergeInteraction=new Js,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",p=>{this.showMergeIdentifierPopup(p.merged,p.propsA,p.propsB,p.coordinate)}),this._snapGuidesEnabled=localStorage.getItem("snap-guides-enabled")==="1",this._snapGuides=new Ar({pixelTolerance:10,vectorClass:Er}),this.map.addInteraction(this._snapGuides);const u=["DrawPoint","DrawLine","DrawPolygon","DrawHole","DrawRegular"];for(const p of u){const h=this.editBar.getInteraction(p);h&&h.on("change:active",()=>{h.getActive()&&this._snapGuides.setDrawInteraction(h)})}this._modifyInteraction&&this._snapGuides.setModifyInteraction(this._modifyInteraction);const f=new tt({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"),f.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=f,n.addControl(f),this._buildToolbarRail(),this.setEditMode(!1),this._drawingsGroup.on("change:visible",()=>{const p=this._drawingsGroup.getVisible();this.setEditMode(p)}),this._isTouchOnlyDevice()?(this.touchCursor=new Po({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",p=>{const h=p.feature,g=h.getGeometry();if(!g||g.getType()!=="Polygon")return;const m=g.getInteriorPoint().getCoordinates();this.showDrawnPolygonPopup(h,m)}),this._editKeyHandler=p=>{if(p.key!=="Delete"&&p.key!=="Backspace"||!this._editBarActive)return;const h=p.target,g=h&&h.tagName;h?.isContentEditable||g==="INPUT"||g==="TEXTAREA"||g==="SELECT"||this._deleteSelectedFeatures()&&p.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(s=>this._boxAddFromLayer(s,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 n=e.getSource&&e.getSource();!n||typeof n.forEachFeatureIntersectingExtent!="function"||n.forEachFeatureIntersectingExtent(t,s=>{r.getArray().includes(s)||r.push(s)})}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 Po({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[n,s]of Object.entries(aa))for(const a of e.querySelectorAll(`.${n}`)){const i=a.matches("button")?a:a.querySelector(":scope > button");i&&(i.innerHTML=`<span class="bi ${s}" aria-hidden="true"></span>`)}const t=document.createDocumentFragment();for(const n of ia){const s=n.controls.map(i=>e.querySelector(`:scope > .${i}`)).filter(Boolean);if(!s.length)continue;const a=document.createElement("div");a.className="ol-rail-label",a.textContent=n.label,a.setAttribute("aria-hidden","true"),t.appendChild(a);for(const i of s)t.appendChild(i)}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",n=window.matchMedia("(max-width: 576px), (max-height: 560px)"),s=(g,m,y)=>Math.min(Math.max(g,m),y),a=(g,m)=>{const y=e.offsetParent||this.map.getTargetElement();if(!y)return;const w=Math.max(0,y.clientWidth-e.offsetWidth-4),_=Math.max(0,y.clientHeight-e.offsetHeight-4);e.style.left=`${s(g,4,w)}px`,e.style.top=`${s(m,4,_)}px`,e.style.transform="none",e.style.bottom="auto"},i=()=>{e.style.left="",e.style.top="",e.style.transform="",e.style.bottom=""},l=()=>{if(n.matches){i();return}try{const g=JSON.parse(localStorage.getItem(r)||"null");g&&Number.isFinite(g.left)&&Number.isFinite(g.top)&&a(g.left,g.top)}catch{}};let c=0,d=0,u=0,f=0;const p=g=>{a(u+(g.clientX-c),f+(g.clientY-d))},h=g=>{t.releasePointerCapture?.(g.pointerId),window.removeEventListener("pointermove",p),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(n.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,f=m.top-y.top,e.classList.add("is-dragging"),t.setPointerCapture?.(g.pointerId),window.addEventListener("pointermove",p),window.addEventListener("pointerup",h)}),t.addEventListener("dblclick",()=>{try{localStorage.removeItem(r)}catch{}i()}),typeof n.addEventListener=="function"?n.addEventListener("change",l):typeof n.addListener=="function"&&n.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 A({color:"rgba(14,165,233,0.85)"}),stroke:new I({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 n=r.getType();if(!["Polygon","MultiPolygon","LineString","MultiLineString"].includes(n))continue;const s=this._collectAllVertices(r);for(const a of s)this._vertexOverlaySource.addFeature(new fe(new dt(a)));t.on("change",this._onSelectedFeatureGeomChange),this._vertexTrackedFeatures.add(t)}})}_collectAllVertices(e){const t=[],r=i=>Array.isArray(i)&&typeof i[0]=="number",n=(i,l)=>{const c=l&&i.length>1?i.length-1:i.length;for(let d=0;d<c;d++)t.push(i[d])},s=e.getType(),a=e.getCoordinates();switch(s){case"Polygon":for(const l of a)n(l,!0);break;case"MultiPolygon":for(const l of a)for(const c of l)n(c,!0);break;case"LineString":n(a,!1);break;case"MultiLineString":for(const l of a)n(l,!1);break;default:const i=l=>{if(r(l))t.push(l);else if(Array.isArray(l))for(const c of l)i(c)};i(a)}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,n=>n.get("name")?n: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",n=e.get("category")||"default",s=e.get("description"),a=e.get("lon"),i=e.get("lat");let c=`
|
||
<div style="font-weight: 600; font-size: 14px; margin-bottom: 6px;">
|
||
${this.getEmoji(n)} ${this.escapeHtml(r)}
|
||
</div>
|
||
`;const u={water:"#3b82f6",school:"#f59e0b",health:"#ef4444",market:"#8b5cf6",default:"#2d5016",other:"#6b7280"}[n]||"#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;
|
||
">${n}</span>
|
||
</div>
|
||
`,s&&(c+=`
|
||
<div style="color: var(--muted-foreground, #7a7a7a); font-size: 12px; margin-bottom: 6px; line-height: 1.4;">
|
||
${this.escapeHtml(s)}
|
||
</div>
|
||
`),a!==void 0&&i!==void 0&&(c+=`
|
||
<div style="color: var(--muted-foreground, #7a7a7a); font-size: 11px; font-family: monospace;">
|
||
${Number(a).toFixed(5)}, ${Number(i).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:gt.slice(),stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.infoPopup)}showInfoPopup(e,t,r={}){const{title:n="Feature Info",color:s="#e11d48"}=r,a=e.getProperties(),i=e.getGeometry(),l=i.getType(),c=["geometry","_layerType"];let d="";for(const[f,p]of Object.entries(a))c.includes(f)||p===void 0||p===null||(d+=`
|
||
<tr>
|
||
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">${this.escapeHtml(f)}</td>
|
||
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${this.escapeHtml(String(p))}</td>
|
||
</tr>
|
||
`);if(l==="Polygon"||l==="MultiPolygon"){const f=nt(i,{projection:"EPSG:3857"}),p=Is(f);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);">${p}</td>
|
||
</tr>
|
||
`}else if(l==="LineString"||l==="MultiLineString"){const f=vt(i,{projection:"EPSG:3857"}),p=ks(f);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);">${p}</td>
|
||
</tr>
|
||
`}else if(l==="Point"){const f=xe(i.getCoordinates()),p=f[0].toFixed(6),h=f[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);">${p}</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:${s};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(n)}</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 n=[];if(e.length>0&&n.push({label:"Parcels",value:String(e.length),color:"#0ea5e9"}),t.length>0){const s=t.map(a=>a.get("colzonename")||a.get("zone_name")||a.get("name")||"unnamed");n.push({label:"Zones",value:String(t.length),color:"#7c3aed"}),n.push({label:"Zone Names",value:s.map(a=>this.escapeHtml(a)).join(", "),color:"#7c3aed"})}for(const[s,a]of Object.entries(r))n.push({label:this.escapeHtml(s),value:`${a.length} feature(s)`});return n.length===0&&n.push({label:"",value:"No intersecting features found",empty:!0}),n}_makePopupDraggable(){const e=this.infoPopupElement.querySelector("#info-popup-header");if(!e)return;this.infoPopup.setOffset(gt.slice());let t=0,r=0,n=gt.slice();const s=i=>{this.infoPopup.setOffset([n[0]+(i.clientX-t),n[1]+(i.clientY-r)])},a=i=>{e.releasePointerCapture?.(i.pointerId),e.removeEventListener("pointermove",s),e.removeEventListener("pointerup",a),e.removeEventListener("pointercancel",a),e.style.cursor="grab"};e.addEventListener("pointerdown",i=>{i.target.closest("#info-popup-close")||i.button!==0||(i.preventDefault(),t=i.clientX,r=i.clientY,n=(this.infoPopup.getOffset()||gt).slice(),e.style.cursor="grabbing",e.setPointerCapture?.(i.pointerId),e.addEventListener("pointermove",s),e.addEventListener("pointerup",a),e.addEventListener("pointercancel",a))})}_buildAnalysisPopupHtml(e,t,r){let n="";for(const s of r){if(s.empty){n+=`
|
||
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
|
||
<td colspan="2" style="padding:8px;color:#999;text-align:center;font-style:italic;">${s.value}</td>
|
||
</tr>`;continue}const a=s.color||"var(--muted-foreground, #7a7a7a)",i=s._first?"":"border-top:1px solid var(--border, #1e1a4b1f);";n+=`
|
||
<tr style="${i}">
|
||
<td style="padding:4px 8px;font-weight:600;color:${a};white-space:nowrap;">${s.label}</td>
|
||
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${s.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;">
|
||
${n}
|
||
</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,n,s=null){this.infoPopupElement.innerHTML=this._buildAnalysisPopupHtml(e,t,r),this.infoPopup.setPosition(n),this.infoPopupElement.querySelector("#info-popup-close").addEventListener("click",()=>{this.hideInfoPopup()}),this._makePopupDraggable();const a=r.filter(c=>!c.empty).map(c=>({label:c.label,value:String(c.value).replace(/<[^>]*>/g,"")})),i=this.infoPopupElement.querySelector("#info-popup-analyse");if(i){const c=s?s.parcelFeatures.length+s.zoneFeatures.length+Object.values(s.otherByLayer).reduce((d,u)=>d+u.length,0):0;!s||c===0?(i.disabled=!0,i.style.opacity="0.5",i.style.cursor="not-allowed",i.title="No intersecting features to analyse"):i.addEventListener("click",()=>{window.dispatchEvent(new CustomEvent("lupmis:analyse-features",{detail:{title:t,...s}}))})}const l=this.infoPopupElement.querySelector("#info-popup-export-gis");if(l){const c=s?s.parcelFeatures.length+s.zoneFeatures.length+Object.values(s.otherByLayer).reduce((d,u)=>d+u.length,0):0;!s||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:a,...s}}))})}}showCircleIntersectionPopup(e,t){const r=e.getGeometry();if(!r||typeof r.getCenter!="function")return;const n=xr(r,64),s=n.getExtent(),a=r.getRadius(),i=[],l=[],c={},d=y=>{const w=y.getGeometry();if(!w)return!1;const _=w.getExtent();return _[2]<s[0]||_[0]>s[2]||_[3]<s[1]||_[1]>s[3]?!1:n.intersectsExtent(_)&&this._geometriesIntersect(n,w)},u=(y,w)=>{y.getLayers().forEach(_=>{if(_ instanceof Me)u(_,_.get("title")||w);else if(_ instanceof D&&_.getVisible()){const E=_.get("title")||w||"Unknown",T=_.getSource();if(!T)return;const x=T.getFeaturesInExtent(s);for(const S of x){const P=S.get("_layerType");P==="measure_circle"||P==="measure_circle_radius"||d(S)&&(P==="parcel"?i.push(S):P==="collector_zone"?l.push(S):(c[E]||(c[E]=[]),c[E].push(S)))}}})};u(this.overlayGroup,"Overlays");const f=Qt(a,r.getCenter()),p=rt(f),h=Math.PI*f*f,g=qe(h),m=[{label:"Radius",value:p,_first:!0},{label:"Area",value:g},...this._collectIntersectionRows(i,l,c)];this._showAnalysisPopup("⭕","Circle Analysis",m,t,{kind:"circle",clipGeometry:n,parcelFeatures:i,zoneFeatures:l,otherByLayer:c})}showAreaIntersectionPopup(e,t){const r=e.getGeometry();if(!r)return;const n=r.getExtent(),s=nt(r,{projection:"EPSG:3857"}),a=qe(s),i=vt(r,{projection:"EPSG:3857"}),l=rt(i),c=[],d=[],u={},f=g=>{const m=g.getGeometry();if(!m)return!1;const y=m.getExtent();return y[2]<n[0]||y[0]>n[2]||y[3]<n[1]||y[1]>n[3]?!1:r.intersectsExtent(y)&&this._geometriesIntersect(r,m)},p=(g,m)=>{g.getLayers().forEach(y=>{if(y instanceof Me)p(y,y.get("title")||m);else if(y instanceof D&&y.getVisible()){const w=y.get("title")||m||"Unknown",_=y.getSource();if(!_)return;const E=_.getFeaturesInExtent(n);for(const T of E){const x=T.get("_layerType");x==="measure_area"||x==="measure_circle"||x==="measure_circle_radius"||f(T)&&(x==="parcel"?c.push(T):x==="collector_zone"?d.push(T):(u[w]||(u[w]=[]),u[w].push(T)))}}})};p(this.overlayGroup,"Overlays");const h=[{label:"Area",value:a,_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 n=t.getFlatCoordinates(),s=t.getStride();for(let l=0;l<n.length;l+=s)if(e.intersectsCoordinate([n[l],n[l+1]]))return!0;const a=e.getFlatCoordinates(),i=e.getStride();for(let l=0;l<a.length;l+=i)if(t.intersectsCoordinate([a[l],a[l+1]]))return!0;return!1}if(r==="Point")return e.intersectsCoordinate(t.getCoordinates());if(r==="LineString"||r==="MultiLineString"){const n=t.getFlatCoordinates(),s=t.getStride();for(let a=0;a<n.length;a+=s)if(e.intersectsCoordinate([n[a],n[a+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(),n=["geometry","_layerType"];let s="";for(const[l,c]of Object.entries(r)){if(n.includes(l))continue;const d=c==null?"":String(c),u=this.escapeHtml(l),f=this.escapeHtml(d);s+=`
|
||
<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="${f}"
|
||
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=`
|
||
<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;">
|
||
${s}
|
||
<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=a,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 i=this.parcelEditElement.querySelector(".parcel-edit-form");i.addEventListener("submit",l=>{l.preventDefault();const c=new FormData(i),d={};for(const[u,f]of c.entries())d[u]=f;d._layerType="parcel";for(const[u,f]of Object.entries(d))this._parcelEditFeature.set(u,f);for(const u of this._parcelEditCallbacks)u(this._parcelEditFeature,d);this.hideParcelEditPopup()})}hideParcelEditPopup(){this.parcelEditPopup.setPosition(void 0),this._parcelEditFeature=null}onParcelEdit(e){this._parcelEditCallbacks.push(e)}createMergePopup(){this.mergePopupElement=document.createElement("div"),this.mergePopupElement.className="map-merge-popup",this.mergePopupElement.style.cssText=`
|
||
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,n){const s=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"],a=h=>{for(const g of s)if(h[g]!==void 0&&h[g]!==null&&String(h[g]).trim())return{field:g,value:String(h[g])};return{field:"id",value:"Unknown"}},i=a(t),l=a(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(i.field)}: ${this.escapeHtml(i.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(n);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"),f=this.mergePopupElement.querySelectorAll('input[name="merge-choice"]'),p=()=>{u.forEach(h=>{const g=h.querySelector("input");h.style.borderColor=g.checked?g.value==="A"?"#0ea5e9":"#f59e0b":"var(--border, #1e1a4b1f)"})};f.forEach(h=>h.addEventListener("change",p)),p()}createDividePopup(){this.dividePopupElement=document.createElement("div"),this.dividePopupElement.className="map-divide-popup",this.dividePopupElement.style.cssText=`
|
||
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 n=`
|
||
<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=n,this.dividePopup.setPosition(r);const s=this.dividePopupElement.querySelector(".divide-input");s.focus(),s.select();const a=()=>{this.hideDividePopup(),this._polygonDivideInteraction.cancelDivide()};this.dividePopupElement.querySelector(".divide-popup-close").addEventListener("click",a),this.dividePopupElement.querySelector(".divide-popup-cancel").addEventListener("click",a),this.dividePopupElement.querySelector(".divide-popup-confirm").addEventListener("click",()=>{const i=parseInt(s.value,10);if(!i||i<2){s.style.borderColor="#ef4444";return}this.hideDividePopup(),this._polygonDivideInteraction.performDivide(i)}),s.addEventListener("keydown",i=>{i.key==="Enter"&&(i.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=n=>{t.length>0||n.getLayers().forEach(s=>{if(!(t.length>0)){if(s instanceof Me)r(s);else if(s instanceof D){const a=s.getSource();if(!a)return;for(const i of a.getFeatures()){if(i.get("_layerType")!=="parcel")continue;const l=i.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 n="";for(const d of r){const u=this.escapeHtml(d);n+=`
|
||
<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 s=e.getGeometry(),a=nt(s,{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>${qe(a)}</strong>
|
||
</div>
|
||
<form class="drawn-polygon-form" style="padding:10px 12px;overflow-y:auto;flex:1;">
|
||
${n}
|
||
<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),f={};for(const[p,h]of u.entries())f[p]=h;for(const[p,h]of Object.entries(f))this._drawnPolygonFeature.set(p,h);this._drawnPolygonFeature.set("_layerType","parcel");for(const p of this._drawnPolygonCallbacks)p(this._drawnPolygonFeature,f);this.hideDrawnPolygonPopup()})}hideDrawnPolygonPopup(){this.drawnPolygonPopup.setPosition(void 0),this._drawnPolygonFeature=null}onDrawnPolygonSave(e){this._drawnPolygonCallbacks.push(e)}onFeatureModified(e){this._featureModifiedCallbacks||(this._featureModifiedCallbacks=[]),this._featureModifiedCallbacks.push(e)}onDrawnPoint(e){this._drawnPointCallbacks||(this._drawnPointCallbacks=[]),this._drawnPointCallbacks.push(e)}onDblClick(e){return this.dblClickCallbacks.push(e),this.dblClickCallbacks.length===1&&this.map.on("dblclick",t=>{const[r,n]=xe(t.coordinate);let s=null;this.map.forEachFeatureAtPixel(t.pixel,a=>(s=a,!0)),s&&(t.preventDefault(),t.stopPropagation());for(const a of this.dblClickCallbacks)a(r,n,s,t);if(s)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 n=this.addLocationPopupElement.querySelector("#map-location-coords");n.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 n=new FormData(t),s={name:n.get("name"),category:n.get("category"),description:n.get("description"),lon:this.addLocationCoords.lon,lat:this.addLocationCoords.lat};this.addLocationCallbacks.forEach(a=>a(s)),this.hideAddLocationPopup()})}}createBaseLayers(e){const t=new se({title:"Topographic",type:"base",zIndex:-100,visible:e==="topo",source:new Ae({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 se({title:"Carto Light",type:"base",zIndex:-100,visible:e==="carto-light",source:new Ae({url:Wo("light_all"),attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});r.set("basemapKey","carto-light");const n=new se({title:"Carto Dark",type:"base",zIndex:-100,visible:e==="carto-dark",source:new Ae({url:Wo("dark_all"),attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});n.set("basemapKey","carto-dark");const s=new se({title:"OSM Cycle map",type:"base",zIndex:-100,visible:!1,source:new Lo({url:"https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=ae1339c46dd3446b9c491e7336d38760"})});s.set("basemapKey","cycle");const a=new se({title:"Satellite",type:"base",zIndex:-100,visible:e==="satellite",source:new Ae({url:"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",attributions:"Tiles © Esri",maxZoom:19,crossOrigin:"anonymous"})});a.set("basemapKey","satellite");const i=new se({title:"Google Sat",type:"base",zIndex:-100,visible:e==="googlesat",source:new Ae({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"})});i.set("basemapKey","googlesat");const l=new se({title:"OpenStreetMap",type:"base",zIndex:-100,visible:e==="osm",source:new Lo});l.set("basemapKey","osm"),this._baseMapLayers=[r,n,s,a,i,l,t];const c=new Me({title:"Base Maps",layers:[r,n,a,s,i,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 n=r.get("basemapKey")===e;r.setVisible(n),n&&(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 n=document.createElement("div");n.className="ls-basemap-panel",n.innerHTML='<div class="ls-basemap-header">Base Map</div><div class="ls-basemap-grid">'+e.map(a=>`
|
||
<label class="ls-bm-chip">
|
||
<input type="radio" name="lupmis-basemap" value="${a.key}">
|
||
<div class="ls-bm-label">
|
||
<div class="ls-bm-thumb" style="background:${a.grad};"></div>
|
||
<div class="ls-bm-name">${a.label}</div>
|
||
</div>
|
||
</label>
|
||
`).join("")+"</div>",t.appendChild(n),this._basemapPanel=n,this._basemapToggle=r;const s=a=>{const i=a||this._baseMapLayers?.find(l=>l.getVisible())?.get("basemapKey");n.querySelectorAll('input[name="lupmis-basemap"]').forEach(l=>{l.checked=l.value===i})};s(),r.addEventListener("click",a=>{a.stopPropagation();const i=!n.classList.contains("open");n.classList.toggle("open",i),r.classList.toggle("active",i),i&&s()}),document.addEventListener("click",a=>{n.classList.contains("open")&&(n.contains(a.target)||r.contains(a.target)||(n.classList.remove("open"),r.classList.remove("active")))}),n.addEventListener("change",a=>{const i=a.target.closest('input[type=radio][name="lupmis-basemap"]');if(!i)return;const l=i.value;this.setBaseMap(l);try{localStorage.setItem("default-basemap",l)}catch{}n.classList.remove("open"),r.classList.remove("active")}),this.map.on("basemapchange",a=>s(a.key))}_initGpsRendering(){this._gpsPositionSource=new G,this._gpsTrailSource=new G,this._gpsTrailCoords=[],this._gpsTrailLayer=new D({source:this._gpsTrailSource,zIndex:940,style:new M({stroke:new I({color:"#ff6d00",width:4,lineCap:"round",lineJoin:"round"})}),properties:{title:"GPS Trail",displayInLayerSwitcher:!1}}),this._gpsPositionLayer=new D({source:this._gpsPositionSource,zIndex:950,style:e=>e.get("_kind")==="accuracy"?new M({fill:new A({color:"rgba(0,94,184,0.12)"}),stroke:new I({color:"rgba(0,94,184,0.35)",width:1})}):new M({image:new he({radius:7,fill:new A({color:"#005eb8"}),stroke:new I({color:"#ffffff",width:2.5})})}),properties:{title:"GPS Position",displayInLayerSwitcher:!1}}),this.map.addLayer(this._gpsTrailLayer),this.map.addLayer(this._gpsPositionLayer),this._gpsCallbacks={locate:[],record:[]},this._gpsRecording=!1}onLocateMe(e){this._gpsCallbacks.locate.push(e)}onToggleRecording(e){this._gpsCallbacks.record.push(e)}showCurrentPosition(e,t,r=null){if(e==null||t==null)return;const n=ee([e,t]);if(this._gpsPositionSource.clear(),r&&r>0){const a=r/Math.cos(t*Math.PI/180),i=new fe({geometry:new at([this._circleRing(n,a)])});i.set("_kind","accuracy"),this._gpsPositionSource.addFeature(i)}const s=new fe({geometry:new dt(n)});s.set("_kind","dot"),this._gpsPositionSource.addFeature(s)}_circleRing(e,t,r=48){const n=[],a=t/1;for(let i=0;i<=r;i++){const l=i/r*2*Math.PI;n.push([e[0]+a*Math.cos(l),e[1]+a*Math.sin(l)])}return n}centerOn(e,t,r=16){this.map.getView().animate({center:ee([e,t]),zoom:r,duration:500})}startTrailRender(){this._gpsTrailCoords=[],this._gpsTrailSource.clear()}appendTrailPoint(e,t){e==null||t==null||(this._gpsTrailCoords.push(ee([e,t])),this._gpsTrailSource.clear(),this._gpsTrailCoords.length>=2&&this._gpsTrailSource.addFeature(new fe({geometry:new de(this._gpsTrailCoords)})))}clearTrailRender(){this._gpsTrailCoords=[],this._gpsTrailSource.clear()}setRecordingState(e){this._gpsRecording=!!e,this._recordBtn&&(this._recordBtn.classList.toggle("recording",this._gpsRecording),this._recordBtn.title=this._gpsRecording?"Stop trail recording":"Record GPS trail",this._recordBtn.innerHTML=this._gpsRecording?'<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 n=()=>{r.classList.remove("open"),t.classList.remove("active")},s=()=>{r.classList.add("open"),t.classList.add("active")};t.addEventListener("click",a=>{a.stopPropagation(),r.classList.contains("open")?n():s()}),document.addEventListener("click",a=>{r.classList.contains("open")&&(r.contains(a.target)||t.contains(a.target)||this._gpsRecording||n())}),this._locateMeBtn.addEventListener("click",a=>{a.stopPropagation();for(const i of this._gpsCallbacks.locate)try{i()}catch(l){console.error(l)}this._gpsRecording||n()}),this._recordBtn.addEventListener("click",a=>{a.stopPropagation();const i=!this._gpsRecording;for(const l of this._gpsCallbacks.record)try{l(i)}catch(c){console.error(c)}})}getFeatureStyle(e){const t=e.get("category")||"default",r=this.getEmoji(t);if(e===this.selectedFeature)return[new M({image:new he({radius:22,fill:new A({color:"rgba(220, 38, 38, 0.25)"}),stroke:new I({color:"#dc2626",width:3})})}),new M({text:new We({text:r,font:"40px sans-serif",textBaseline:"bottom",textAlign:"center",offsetY:-5})})];const n=e.get("style");return n||(this.categoryStyles[t]?this.categoryStyles[t]:this.defaultStyle)}setCategoryStyles(e){for(const[t,r]of Object.entries(e)){r.emoji&&(this.categoryEmojis[t]?(this.categoryEmojis[t].emoji=r.emoji,r.label&&(this.categoryEmojis[t].label=r.label)):this.categoryEmojis[t]={emoji:r.emoji,label:r.label||t});const n=this.getEmoji(t),s=r.fontSize||28;this.categoryStyles[t]=this.createEmojiStyle(n,s)}this.markerSource.changed()}addMarker(e,t,r={}){console.log("[MapView] Adding marker at",e,t,"with properties:",r);const n=new fe({geometry:new dt(ee([e,t])),...r});return n.set("lon",e),n.set("lat",t),this.markerSource.addFeature(n),console.log("[MapView] Marker added, total features:",this.markerSource.getFeatures().length),n}addMarkers(e){console.log("[MapView] Adding",e.length,"markers");const t=e.map(r=>new fe({geometry:new dt(ee([r.longitude,r.latitude])),id:r.id,name:r.name,description:r.description,category:r.category,lon:r.longitude,lat:r.latitude}));return this._withoutUndoRecording(()=>this.markerSource.addFeatures(t)),console.log("[MapView] Markers added, total features:",this.markerSource.getFeatures().length),t}clearMarkers(){this._withoutUndoRecording(()=>this._silentClearSource(this.markerSource)),this.selectedFeature=null}removeMarker(e){if(typeof e=="object")this.markerSource.removeFeature(e);else{const t=this.markerSource.getFeatures().find(r=>r.get("id")===e);t&&this.markerSource.removeFeature(t)}}getMarkers(){return this.markerSource.getFeatures()}findMarker(e){return this.markerSource.getFeatures().find(t=>t.get("id")===e)}showMarkers(){this.markersLayer&&!this.markersLayer.getVisible()&&(this.markersLayer.setVisible(!0),console.log("[MapView] Markers layer switched on to show a location"))}selectMarker(e){return typeof e=="object"?this.selectedFeature=e:this.selectedFeature=this.findMarker(e),this.markerSource.changed(),this.selectedFeature}clearSelection(){this.selectedFeature=null,this.markerSource.changed()}zoomTo(e,t,r=15){this.map.getView().animate({center:ee([e,t]),zoom:r,duration:500})}fitToMarkers(e=50){const t=this.markerSource.getExtent();t&&t[0]!==1/0&&this.map.getView().fit(t,{padding:[e,e,e,e],duration:500,maxZoom:16})}getCenter(){const e=this.map.getView().getCenter();return xe(e)}getZoom(){return this.map.getView().getZoom()}setCenter(e,t){this.map.getView().setCenter(ee([e,t]))}setZoom(e){this.map.getView().setZoom(e)}onClick(e){return this.clickCallbacks.push(e),this.clickCallbacks.length===1&&(this._clickTimer=null,this.map.on("dblclick",()=>{this._clickTimer&&(clearTimeout(this._clickTimer),this._clickTimer=null)}),this.map.on("click",t=>{this._clickTimer&&(clearTimeout(this._clickTimer),this._clickTimer=null),!this._editBarActive&&this._selectInteraction&&this._selectInteraction.getFeatures().clear();let r=!1,n=!1,s=null;if(this.map.forEachFeatureAtPixel(t.pixel,l=>{l.get("_layerType")==="parcel"&&(n=!0),l.get("name")&&(s=l),r=!0}),r&&!n&&!s)return;const[a,i]=xe(t.coordinate);this._clickTimer=setTimeout(()=>{this._clickTimer=null;let l=null;this.map.forEachFeatureAtPixel(t.pixel,c=>{if(c.get("name"))return l=c,!0});for(const c of this.clickCallbacks)c(a,i,l,t)},300)})),()=>{const t=this.clickCallbacks.indexOf(e);t>-1&&this.clickCallbacks.splice(t,1)}}onPointerMove(e){this.map.on("pointermove",t=>{if(t.dragging)return;const[r,n]=xe(t.coordinate);let s=null;this.map.forEachFeatureAtPixel(t.pixel,a=>{if(a.get("name"))return s=a,!0}),this.map.getTargetElement().style.cursor=s?"pointer":"",e(r,n,s,t)})}enableHoverCursor(){}addGeoJSONLayer(e,t,r={},n=null){const{strokeColor:s="#3b82f6",strokeWidth:a=2,strokeDash:i=null,fillColor:l="rgba(59,130,246,0.1)",lineCasingColor:c=null,lineCasingWidth:d=null,pointRadius:u=5,pointFillColor:f=null,pointStrokeColor:p="#ffffff",pointStrokeWidth:h=1.5}=r,g=new G({features:new le().readFeatures(e,{featureProjection:"EPSG:3857"})}),m=new A({color:l}),y=new he({radius:u,fill:new A({color:f||s}),stroke:new I({color:p,width:h})}),w=new I({color:s,width:a,...i?{lineDash:i}:{}});let _;if(c){const S=d??a+2;_=[new M({stroke:new I({color:c,width:S})}),new M({stroke:w,fill:m,image:y})]}else _=new M({stroke:w,fill:m,image:y});const E=new D({title:t,source:g,style:_,...r.declutter?{declutter:r.declutter}:{}});E.set("typeTag",r.typeTag||"VEC");const T=S=>S?S.includes("Polygon")?"Vector / Polygon":S.includes("LineString")?"Vector / Line":S.includes("Point")?"Vector / Point":"Vector":null;if(r.typeDescription)E.set("typeDescription",r.typeDescription);else{const S=g.getFeatures(),P=T(S[0]?.getGeometry?.()?.getType?.());if(P)E.set("typeDescription",P);else{const k=j=>{const q=T(j.feature.getGeometry?.()?.getType?.());q&&E.set("typeDescription",q),g.un("addfeature",k)};g.on("addfeature",k)}}return(n||this.overlayGroup).getLayers().push(E),console.log("[MapView] GeoJSON layer added:",t,"→",g.getFeatures().length,"features",n?`(in group "${n.get("title")}")`:""),E}addLayerGroup(e,t,r=""){const n=new Me({title:t.trim()});return n.set("layerId",e),n.set("description",r),this.overlayGroup.getLayers().push(n),console.log("[MapView] Layer group added:",t.trim(),"(id:",e+")"),n}addWMSLayer(e,t,r,n,s={}){const a=this.getLayerGroupByTitle(e);if(!a)return console.warn(`[MapView] Layer group "${e}" not found — cannot add WMS layer "${t}"`),null;const i={LAYERS:n,TILED:!0,WIDTH:256,HEIGHT:256};s.style!==void 0&&(i.STYLES=s.style);const l=new ko({url:r,params:i,serverType:s.serverType!==void 0?s.serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1,attributions:s.attributions}),c=new se({title:t,visible:s.visible!==void 0?s.visible:!0,source:l,opacity:s.opacity!==void 0?s.opacity:1,zIndex:s.zIndex});if(c.set("typeTag","WMS"),c.set("typeDescription","WMS / Raster"),l.on("tileloaderror",()=>{F(`WMS layer "${t}" — tile load error. Check the URL and layer name.`,"warning",5e3)}),a.getLayers().push(c),s.legendUrl)try{this._registerLegend(c,t,s.legendUrl)}catch(d){console.warn(`[MapView] Could not register legend for "${t}":`,d)}return s.onlineOnly&&this._attachOnlineOnlyHandler(c,t),console.log(`[MapView] WMS layer added: "${t}" → group "${e}"`),c}addXYZLayer(e,t,r,n={}){const s=this.getLayerGroupByTitle(e);if(!s)return console.warn(`[MapView] Layer group "${e}" not found — cannot add XYZ layer "${t}"`),null;const a=new Ae({url:r,crossOrigin:"anonymous",maxZoom:n.maxZoom!==void 0?n.maxZoom:19,attributions:n.attributions}),i=new se({title:t,visible:n.visible!==void 0?n.visible:!0,source:a,opacity:n.opacity!==void 0?n.opacity:1,zIndex:n.zIndex});if(i.set("typeTag","XYZ"),i.set("typeDescription","XYZ / Tile"),a.on("tileloaderror",()=>{F(`XYZ layer "${t}" — tile load error. Check the URL.`,"warning",5e3)}),s.getLayers().push(i),n.legendUrl)try{this._registerLegend(i,t,n.legendUrl)}catch(l){console.warn(`[MapView] Could not register legend for "${t}":`,l)}return n.onlineOnly&&this._attachOnlineOnlyHandler(i,t),console.log(`[MapView] XYZ layer added: "${t}" → group "${e}"`),i}async addCOGLayer(e,t,r,n={}){const s=this.getLayerGroupByTitle(e);if(!s)return console.warn(`[MapView] Layer group "${e}" not found — cannot add COG layer "${t}"`),null;const a=await this._probeCOGUrl(r);if(a)return console.warn(`[MapView] COG pre-flight failed for "${t}":`,a),F(a,"error",8e3),null;let i,l;try{[{default:i},{default:l}]=await Promise.all([Le(()=>import("./geotiff-B_Wfowsp.js").then(d=>d.W),__vite__mapDeps([0,1,2,3,4])),Le(()=>import("./geotiff-B_Wfowsp.js").then(d=>d.G),__vite__mapDeps([0,1,2,3,4]))])}catch(d){return console.error("[MapView] Could not load COG support:",d),F("Raster (COG) support could not be loaded — check your connection.","error",6e3),null}let c=null;try{const d={sources:[{url:r,...n.min!==void 0?{min:n.min}:{},...n.max!==void 0?{max:n.max}:{}}],normalize:n.normalize!==void 0?n.normalize:!0,attributions:n.attributions},u=new l(d);c=new i({title:t,source:u,visible:n.visible!==void 0?n.visible:!0,opacity:n.opacity!==void 0?n.opacity:1,zIndex:n.zIndex,...n.style?{style:n.style}:{}}),c.set("typeTag","COG"),c.set("typeDescription","Cloud-Optimized GeoTIFF"),u.on("error",f=>{console.warn(`[MapView] COG "${t}" source error:`,f),F(`Raster "${t}" could not be loaded. Check that the file is a valid COG and is CORS-enabled.`,"warning",6e3)}),s.getLayers().push(c)}catch(d){return console.error(`[MapView] Failed to add COG layer "${t}":`,d),F(`Could not add raster "${t}": ${d.message}`,"error",6e3),null}return n.onlineOnly&&this._attachOnlineOnlyHandler(c,t),console.log(`[MapView] COG layer added: "${t}" → group "${e}"`),c}async _probeCOGUrl(e){let t;try{t=await fetch(e,{headers:{Range:"bytes=0-0"}})}catch{return"Could not reach that file. It may be offline, or the server may not allow cross-origin (CORS) requests from this app."}if(t.status===404)return"No file at that URL (404). Check the address.";if(!t.ok&&t.status!==206)return`The server refused the request (HTTP ${t.status}).`;const r=(t.headers.get("content-type")||"").toLowerCase();return r.includes("text/html")?"That URL is a web page, not a GeoTIFF. Paste the direct link to the .tif file itself — a documentation or viewer page will not work.":r.startsWith("text/")||r.includes("json")?`That URL returns ${r.split(";")[0]}rather than image data. Paste the direct link to the .tif file.`:(t.status===200&&F("This server does not support byte-range requests, so the whole raster will be downloaded. It may be slow for a large file.","warning",7e3),null)}getSelectedFeatures(){return this._selectInteraction?this._selectInteraction.getFeatures().getArray().slice():[]}listVectorLayers(e={}){const{visibleOnly:t=!1,nonEmptyOnly:r=!0}=e,n=[],s=a=>{if(!a)return;if(typeof a.getLayers=="function"){a.getLayers().forEach(s);return}if(!(a instanceof D)||a===this._vertexOverlayLayer||a===this._gpsTrailLayer||a===this._gpsPositionLayer||a.get("displayInLayerSwitcher")===!1||t&&a.getVisible&&!a.getVisible())return;const i=a.getSource&&a.getSource();if(!i||typeof i.getFeatures!="function")return;const l=i.getFeatures().length;r&&l===0||n.push({layer:a,title:a.get("title")||"(untitled layer)",count:l})};return this.map.getLayers().forEach(s),n}_createAddLayerDialog(){this._addLayerDialog=document.createElement("div"),this._addLayerDialog.className="map-add-layer-dialog",this._addLayerDialog.style.cssText=`
|
||
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"),n=e.querySelector(".add-layer-url"),s=e.querySelector(".add-layer-type-hint");e.querySelectorAll('input[name="add-layer-type"]').forEach(i=>{i.addEventListener("change",()=>{const l=i.value;s.textContent=l==="cog"?"Cloud-Optimized GeoTIFF — the server must allow CORS and byte-range requests.":"",l==="cog"?(t.style.display="none",n.placeholder="https://example.com/data/elevation_cog.tif"):l==="xyz"?(t.style.display="none",n.placeholder="https://example.com/tiles/{z}/{x}/{y}.png"):(t.style.display="",n.placeholder=l==="wms"?"https://example.com/wms":"https://example.com/wfs",r.textContent=l==="wms"?"WMS LAYERS parameter (e.g. workspace:layer)":"WFS typename (e.g. workspace:layer)")})});const a=()=>this._hideAddLayerDialog();e.querySelector(".add-layer-close").addEventListener("click",a),e.querySelector(".add-layer-cancel").addEventListener("click",a),this._addLayerDialog.addEventListener("click",i=>{i.target===this._addLayerDialog&&a()}),e.querySelector(".add-layer-confirm").addEventListener("click",()=>{const i=e.querySelector('input[name="add-layer-type"]:checked').value,l=e.querySelector(".add-layer-url").value.trim(),c=e.querySelector(".add-layer-name").value.trim(),d=e.querySelector(".add-layer-title").value.trim();if(!l){e.querySelector(".add-layer-url").style.borderColor="#ef4444";return}if((i==="wms"||i==="wfs")&&!c){e.querySelector(".add-layer-name").style.borderColor="#ef4444";return}if(!d){e.querySelector(".add-layer-title").style.borderColor="#ef4444";return}this._addExternalLayer(i,l,c,d),this._hideAddLayerDialog()}),e.addEventListener("keydown",i=>{i.key==="Enter"&&(i.preventDefault(),e.querySelector(".add-layer-confirm").click()),i.key==="Escape"&&(i.preventDefault(),a())})}showAddLayerDialog(){const e=this._addLayerDialog;e.querySelector(".add-layer-url").value="",e.querySelector(".add-layer-name").value="",e.querySelector(".add-layer-title").value="",e.querySelectorAll('input[name="add-layer-type"]')[0].checked=!0,e.querySelector(".add-layer-name-row").style.display="",e.querySelector(".add-layer-url").placeholder="https://example.com/wms",e.querySelector(".add-layer-name-hint").textContent="WMS LAYERS parameter (e.g. workspace:layer)";const t=e.querySelector(".add-layer-type-hint");t&&(t.textContent=""),e.querySelectorAll('input[type="text"]').forEach(r=>{r.style.borderColor="var(--border, #1e1a4b1f)"}),e.style.display="flex",e.querySelector(".add-layer-url").focus()}_hideAddLayerDialog(){this._addLayerDialog.style.display="none"}_addExternalLayer(e,t,r,n){const s=this._externalSourceGroup;if(!s){F('Layer group "External Source" not found.',"error",4e3);return}let a;switch(e){case"wms":{const i=new ko({url:t,params:{LAYERS:r,TILED:!0,WIDTH:256,HEIGHT:256},serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1});a=new se({title:n,visible:!0,source:i}),i.on("tileloaderror",()=>{F(`WMS "${n}" — tile load error. Check URL and layer name.`,"warning",5e3)});break}case"wfs":{const i=`${t}${t.includes("?")?"&":"?"}service=WFS&version=1.1.0&request=GetFeature&typename=${encodeURIComponent(r)}&outputFormat=application/json&srsname=EPSG:3857`,l=new G({url:i,format:new le});l.on("featuresloaderror",()=>{F(`WFS "${n}" — load error. Check URL and layer name.`,"warning",5e3)}),a=new D({title:n,visible:!0,source:l,style:new M({stroke:new I({color:"#e11d48",width:2}),fill:new A({color:"rgba(225,29,72,0.15)"})})});break}case"xyz":a=new se({title:n,visible:!0,source:new Ae({url:t,crossOrigin:"anonymous"})}),a.getSource().on("tileloaderror",()=>{F(`XYZ "${n}" — tile load error. Check the URL template.`,"warning",5e3)});break;case"cog":{const i=s.get("title")||"External Source";this.addCOGLayer(i,n,t).then(l=>{l&&(l.set("removable",!0),F(`Layer "${n}" added to External Source.`,"success",3e3),console.log(`[MapView] External COG layer added: "${n}"`))});return}default:F(`Unknown layer type: ${e}`,"error",4e3);return}a.set("typeTag",e.toUpperCase()),a.set("typeDescription",{wms:"WMS / Raster",wfs:"WFS / Vector",xyz:"XYZ / Tile"}[e]||e.toUpperCase()),a.set("removable",!0),s.getLayers().push(a),F(`Layer "${n}" added to External Source.`,"success",3e3),console.log(`[MapView] External ${e.toUpperCase()} layer added: "${n}"`)}_decorateLayerListItem(e,t){const r=e.get("typeTag");if(r){const c=t.querySelector(":scope > .li-content > label > span");if(c&&!c.querySelector(":scope > .ls-type-tag")){const d=document.createElement("span");d.className=`ls-type-tag ls-type-tag-${String(r).toLowerCase()}`,d.textContent=String(r),d.title=`${r} layer`,c.appendChild(d)}}const n=t.querySelector(":scope > .ol-layerswitcher-buttons");if(n){const c=n.querySelector(":scope > .expend-layers, :scope > .collapse-layers");c&&!c.querySelector(":scope > svg.ls-chevron-svg")&&(c.innerHTML='<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 s=t.querySelector(":scope > .li-content"),a=()=>{if(!s)return;const c=e.get("typeDescription");let d=s.querySelector(":scope > .ls-layer-subtitle");if(!c){d&&d.remove();return}if(!d){d=document.createElement("div"),d.className="ls-layer-subtitle";const u=s.querySelector(":scope > label");u&&u.nextSibling?s.insertBefore(d,u.nextSibling):s.appendChild(d)}d.textContent=c};if(a(),e._lsSubtitleHooked||(e._lsSubtitleHooked=!0,e.on("change:typeDescription",()=>{a()})),e.get("removable")===!0&&n&&!n.querySelector(":scope > .ls-remove-btn")){const c=document.createElement("button");c.type="button",c.className="ls-remove-btn",c.title="Remove this layer",c.setAttribute("aria-label","Remove layer"),c.innerHTML='<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)}),n.appendChild(c)}const i=e.get("_externalImportId");if(i!=null){const c=t.querySelector(":scope > .li-content > label > span");let d=c?c.querySelector(":scope > .ls-import-chip"):null;const u=e.get("_externalImportStatus")||"mapped",f=e.getSource()?.getFeatures().length??0,p=e.get("_externalImportErrorCount")??0,h=(()=>{switch(u){case"mapped":return{text:`Upload ${f}`,cls:"ls-import-chip-mapped",title:"Upload this dataset to the database",clickable:!0};case"uploading":return{text:"…",cls:"ls-import-chip-uploading",title:"Uploading…",clickable:!1};case"submitted":return{text:"✓ submitted",cls:"ls-import-chip-submitted",title:"Uploaded — awaiting supervisor review",clickable:!1};case"migrated":return{text:"✓ live",cls:"ls-import-chip-migrated",title:"Approved by supervisor and live on the server",clickable:!1};case"failed":return{text:`${p} errors — fix?`,cls:"ls-import-chip-failed",title:"Some rows failed; click to review",clickable:!0};case"other":case null:case void 0:default:return null}})();if(!h)d&&d.remove();else if(c){d||(d=document.createElement("span"),d.className="ls-import-chip",c.appendChild(d)),d.className=`ls-import-chip ${h.cls}`,d.textContent=h.text,d.title=h.title,d.style.cursor=h.clickable?"pointer":"default",d.style.opacity=h.clickable?"1":"0.85";const g=d.cloneNode(!0);d.replaceWith(g),d=g,h.clickable&&d.addEventListener("click",m=>{m.preventDefault(),m.stopPropagation(),window.dispatchEvent(new CustomEvent("lupmis:import-chip-click",{detail:{importId:i,status:u,layer:e}}))})}}if((e.get("title")||"").toLowerCase().includes("external")&&(this._externalSourceGroup=e,n&&!n.querySelector(".ol-add-layer"))){const c=document.createElement("span");c.className="ol-add-layer",c.title="Add external layer",c.textContent="+",c.style.cssText=`
|
||
display:inline-flex !important;align-items:center;justify-content:center;
|
||
width:22px !important;height:22px !important;border-radius:50%;
|
||
background:#41b6a6 !important;color:#fff !important;
|
||
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()}),n.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=s=>{const a=s.getLayers();if(a.getArray().includes(e))return a.remove(e),!0;let i=!1;return a.forEach(l=>{!i&&l.getLayers&&(i=r(l))}),i};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 n=t.querySelector(":scope > .ls-active-badge");n||(n=document.createElement("div"),n.className="ls-active-badge",n.innerHTML=`
|
||
<span class="ls-active-badge-title">Layers</span>
|
||
<span class="ls-active-badge-count">0 active</span>
|
||
`,t.insertBefore(n,r));let s=t.querySelector(":scope > .ls-footer-row");s||(s=document.createElement("div"),s.className="ls-footer-row",s.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(s),s.querySelector(".ls-footer-btn").addEventListener("click",i=>{i.stopPropagation(),this._resetAllOverlays()}));const a=this._countLayers();n.querySelector(".ls-active-badge-count").textContent=`${a.activeOverlays} active`,s.querySelector(".ls-footer-note").textContent=`${a.totalOverlays} overlay${a.totalOverlays===1?"":"s"}`}_countLayers(){let e=0,t=0;const r=new Set(["__vertex_highlight__"]),n=s=>{s.getLayers().forEach(a=>{a.get("displayInLayerSwitcher")!==!1&&(r.has(a.get("title"))||(a.getLayers?n(a):(e++,a.getVisible()&&t++)))})};return this.overlayGroup&&n(this.overlayGroup),{totalOverlays:e,activeOverlays:t}}_resetAllOverlays(){const e=new Set(["__vertex_highlight__"]),t=r=>{r.getLayers().forEach(n=>{n.get("displayInLayerSwitcher")!==!1&&(e.has(n.get("title"))||(n.getLayers?t(n):n.setVisible(!1)))})};this.overlayGroup&&t(this.overlayGroup),console.log("[MapView] Reset overlays — all hidden")}_wireLayerSwitcherVisibilityHooks(e){const t=()=>this._refreshLayerSwitcherChrome(e),r=s=>{s._lsVisHooked||(s._lsVisHooked=!0,s.on("change:visible",t))},n=s=>{s.getLayers().forEach(a=>{a.getLayers?(n(a),s._lsAddHooked||(s._lsAddHooked=!0,s.getLayers().on("add",i=>{const l=i.element;l.getLayers?n(l):r(l),t()}))):r(a)})};this.overlayGroup&&n(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 To}_registerLegend(e,t,r){if(!this._legendPanel)return;const n=document.createElement("div");n.className="map-legend-entry",n.style.cssText="border-bottom:1px solid var(--border, #1e1a4b1f);padding-bottom:6px;",n.innerHTML=`
|
||
<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,n);const s=()=>{try{this._updateLegendPanel()}catch(a){console.warn("[MapView] legend panel update failed:",a)}};e.on("change:visible",s),s()}_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(n=>{if(n.getLayers)t(n);else if(n.get("title")==="District Boundary"){const s=n.getSource&&n.getSource();if(s&&typeof s.getExtent=="function"){const a=s.getExtent();a&&Number.isFinite(a[0])&&(e={extent:a,title:n.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,n=500){const s=ee([e,t]);this.map.getView().animate({center:s,zoom:r,duration:n})}}class ca{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 n=r.getArray().findIndex(s=>s.get("title")==="Overlays");n<0&&(n=r.getLength()),r.insertAt(n,this.drawLayer),r.insertAt(n,this.measureLayer),this.activeInteraction=null,this.measureTooltip=null,this.measureTooltipElement=null,this.onMeasureCompleteCallbacks=[],this.onDrawCompleteCallbacks=[]}getMeasureStyle(){return new M({fill:new A({color:"rgba(255, 233, 106, 0.2)"}),stroke:new I({color:"#8B008B",lineDash:[10,10],width:2}),image:new he({radius:5,stroke:new I({color:"#8B008B"}),fill:new A({color:"rgba(255, 233, 106, 0.5)"})})})}getDrawStyle(){return new M({fill:new A({color:"rgba(255, 233, 106, 0.3)"}),stroke:new I({color:"#8B008B",width:2}),image:new he({radius:6,stroke:new I({color:"#8B008B",width:2}),fill:new A({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 n=r.getCenter(),s=Math.max(1,Number(t)||0),a=Ms(s,n);r.setRadius(a),e.set("_radius",a);const i=e.get("_radiusLine");i&&i.setGeometry(new de([n,[n[0]+a,n[1]]])),this._refreshRadiusAdjuster(e)}getCircleGroundRadius(e){const t=e.getGeometry();return Math.round(Qt(t.getRadius(),t.getCenter())*1e6)/1e6}_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 n of["pointerdown","pointerup","click","dblclick","touchstart"])r.addEventListener(n,s=>s.stopPropagation());r.querySelectorAll(".mt-step").forEach(n=>{n.addEventListener("click",()=>{const s=this.getCircleGroundRadius(e),a=As(s),i=Number(n.dataset.dir),l=Math.round(s/a)*a,c=Math.abs(l-s)>.01&&Math.sign(l-s)===i?l:l+i*a;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 n=this.getCircleGroundRadius(e),s=document.createElement("input");s.type="number",s.className="mt-input",s.inputMode="decimal",s.min="1",s.step="any",s.value=String(Math.round(n*100)/100),s.setAttribute("aria-label","Radius in metres"),r.replaceWith(s),s.focus(),s.select();let a=!1;const i=l=>{a||!s.isConnected||(a=!0,l&&this.setCircleRadius(e,parseFloat(s.value)),s.replaceWith(r),this._refreshRadiusAdjuster(e))};s.addEventListener("keydown",l=>{l.key==="Enter"&&i(!0),l.key==="Escape"&&i(!1)}),s.addEventListener("blur",()=>i(!0))}_refreshRadiusAdjuster(e){const t=e.get("_adjustEl");if(!t)return;const r=this.getCircleGroundRadius(e),n=t.querySelector(".mt-value");n&&(n.textContent=rt(r));const s=t.querySelector(".mt-area");s&&(s.textContent=qe(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 Ce({source:this.measureSource,type:"Circle",style:new M({fill:new A({color:"rgba(255, 233, 106, 0.2)"}),stroke:new I({color:"rgba(139, 0, 139, 0.7)",lineDash:[10,10],width:2}),image:new he({radius:5,stroke:new I({color:"rgba(139, 0, 139, 0.7)"}),fill:new A({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",s=>{const a=s.target;if(a instanceof Sr){const i=Qt(a.getRadius(),a.getCenter()),l=Ps(i),d=`<strong>${rt(i)}</strong><br><small>${l}</small>`;this.measureTooltipElement.innerHTML=d,this.measureTooltip.setPosition(a.getLastCoordinate())}})}),e.on("drawend",r=>{const n=r.feature,s=n.getGeometry(),a=s.getCenter(),i=s.getRadius();n.set("_layerType","measure_circle"),n.set("_radius",i),n.set("_center",a);const l=new fe({geometry:new de([a,[a[0]+i,a[1]]])});l.set("_layerType","measure_circle_radius"),this.measureSource.addFeature(l),n.set("_radiusLine",l),this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltip.setOffset([0,-7]),this._attachRadiusAdjuster(n,this.measureTooltip,this.measureTooltipElement),this.measureTooltipElement=null,this.createMeasureTooltip(),Nt(t);const c={type:"circle",center:a,radius:i,area:Math.PI*i*i,feature:n};this.onMeasureCompleteCallbacks.forEach(d=>d(c))}),e}startLineMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new Ce({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",s=>{const a=s.target,i=vt(a),l=rt(i);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(a.getLastCoordinate())})}),e.on("drawend",r=>{const n=r.feature,s=n.getGeometry(),a=vt(s);this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),Nt(t);const i={type:"line",length:a,feature:n};this.onMeasureCompleteCallbacks.forEach(l=>l(i))}),e}startAreaMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new Ce({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",s=>{const a=s.target,i=nt(a),l=qe(i);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(a.getInteriorPoint().getCoordinates())})}),e.on("drawend",r=>{const n=r.feature,s=n.getGeometry(),a=nt(s);n.set("_layerType","measure_area"),n.set("_area",a),this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),Nt(t);const i={type:"polygon",area:a,feature:n,coordinate:s.getInteriorPoint().getCoordinates()};this.onMeasureCompleteCallbacks.forEach(l=>l(i))}),e}startDrawPoint(){this.deactivate();const e=new Ce({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(n=>n(r))}),e}startDrawLine(){this.deactivate();const e=new Ce({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(n=>n(r))}),e}startDrawPolygon(){this.deactivate();const e=new Ce({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(n=>n(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 Xt({group:!0,className:"map-tools-bar"}),r=new Xt({toggleOne:!0,group:!0}),n=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(n);const s=new Se({html:'<span class="tool-icon">📏</span>',title:"Measure Distance",className:"measure-line-btn",onToggle:l=>{l?this.startLineMeasure():this.deactivate()}});r.addControl(s);const a=new Se({html:'<span class="tool-icon">⬛</span>',title:"Measure Area",className:"measure-area-btn",onToggle:l=>{l?this.startAreaMeasure():this.deactivate()}});r.addControl(a);const i=new tt({html:'<span class="tool-icon">🗑️</span>',title:"Clear Measurements",className:"clear-measure-btn",handleClick:()=>{this.clearMeasurements(),n.setActive(!1),s.setActive(!1),a.setActive(!1)}});return r.addControl(i),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 Oe=null;async function da(){if(!("serviceWorker"in navigator))return console.warn("[PWA] Service Workers not supported"),null;try{return Oe=await navigator.serviceWorker.register("/sw.js",{scope:"/"}),console.log("[PWA] Service Worker registered:",Oe.scope),Oe.addEventListener("updatefound",()=>{const o=Oe.installing;o.addEventListener("statechange",()=>{o.state==="installed"&&navigator.serviceWorker.controller&&(console.log("[PWA] New version available"),ha())})}),Oe}catch(o){return console.error("[PWA] Service Worker registration failed:",o),null}}let Be=null,_e=null;function ua(o="#install-btn"){if(_e=typeof o=="string"?document.querySelector(o):o,!_e){console.warn("[PWA] Install button not found:",o);return}_e.style.display="none",window.addEventListener("beforeinstallprompt",e=>{e.preventDefault(),Be=e,_e.style.display="block",console.log("[PWA] Install prompt ready")}),_e.addEventListener("click",async()=>{if(!Be){pa();return}Be.prompt();const{outcome:e}=await Be.userChoice;console.log("[PWA] Install prompt outcome:",e),Be=null,_e.style.display="none"}),window.addEventListener("appinstalled",()=>{console.log("[PWA] App installed"),Be=null,_e.style.display="none"}),window.matchMedia("(display-mode: standalone)").matches&&(_e.style.display="none")}function pa(){const o=/iPad|iPhone|iPod/.test(navigator.userAgent),e=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);let t=`To install this app:
|
||
|
||
`;o?(t+=`1. Tap the Share button (square with arrow)
|
||
`,t+='2. Scroll down and tap "Add to Home Screen"'):e?(t+=`1. Click File menu
|
||
`,t+='2. Click "Add to Dock"'):(t+=`1. Click the menu button (three dots)
|
||
`,t+='2. Click "Install" or "Add to Home Screen"'),alert(t)}let to=null;const oo=new Set;function fa(o="#offline-indicator"){to=typeof o=="string"?document.querySelector(o):o,jt(!navigator.onLine),window.addEventListener("online",()=>{console.log("[PWA] Back online"),jt(!1),Vo(!1)}),window.addEventListener("offline",()=>{console.log("[PWA] Gone offline"),jt(!0),Vo(!0)})}function jt(o){to&&(to.style.display=o?"block":"none"),document.body.classList.toggle("is-offline",o)}function jn(o){return oo.add(o),o(!navigator.onLine),()=>oo.delete(o)}function Vo(o){for(const e of oo)try{e(o)}catch(t){console.error("[PWA] Offline listener error:",t)}}function K(){return navigator.onLine}function ha(){confirm("A new version is available. Reload now?")&&ga()}function ga(){Oe?.waiting&&Oe.waiting.postMessage({type:"SKIP_WAITING"}),window.location.reload()}async function ma({timeoutMs:o=1e4}={}){if(!("serviceWorker"in navigator))throw new Error("Service Workers not supported in this browser");if(navigator.serviceWorker.controller)return navigator.serviceWorker.controller;const e=navigator.serviceWorker.ready,t=new Promise((s,a)=>setTimeout(()=>a(new Error("Service-worker readiness timeout")),o)),r=await Promise.race([e,t]),n=navigator.serviceWorker.controller||r.active;if(!n)throw new Error("No active service worker available");return n}function ya(o){if(!("serviceWorker"in navigator))return()=>{};const e=()=>{try{o()}catch(t){console.error("[PWA] controllerchange handler error:",t)}};return navigator.serviceWorker.addEventListener("controllerchange",e),()=>navigator.serviceWorker.removeEventListener("controllerchange",e)}async function wo(o,e,t={},r=5e3,n=1e4){const s=await ma({timeoutMs:n});return new Promise((a,i)=>{const l=new MessageChannel,c=setTimeout(()=>{l.port1.close(),i(new Error(`Service-worker reply "${e}" timed out`))},r);l.port1.onmessage=d=>{if(d.data?.type===e){clearTimeout(c),l.port1.close();const{type:u,...f}=d.data;a(f)}},s.postMessage({type:o,...t},[l.port2])})}async function ba(){try{return(await wo("GET_TILE_STATS","TILE_STATS")).stats}catch(o){return console.warn("[PWA] getTileCacheStats failed:",o),null}}async function wa(){try{return await wo("CLEAR_TILE_CACHES","TILE_CACHES_CLEARED"),!0}catch(o){return console.warn("[PWA] clearTileCaches failed:",o),!1}}async function _a(o){if(!o)return!1;try{return!!(await wo("CLEAR_TILE_CACHE","TILE_CACHE_CLEARED",{cacheName:o})).deleted}catch(e){return console.warn(`[PWA] clearTileCacheForProvider(${o}) failed:`,e),!1}}async function va(){if(!navigator.storage?.estimate)return null;try{const{usage:o,quota:e}=await navigator.storage.estimate();return{usage:o||0,quota:e||0}}catch(o){return console.warn("[PWA] getStorageEstimate failed:",o),null}}async function Ea(o={}){const{installButton:e="#install-btn",offlineIndicator:t="#offline-indicator",autoRegisterSW:r=!0}=o;r&&await da(),ua(e),fa(t),console.log("[PWA] Initialized")}const zn={topo:{url:"https://a.tile.opentopomap.org/{z}/{x}/{y}.png",label:"Topographic",maxZoom:17,cacheKey:"tiles-topo"},osm:{url:"https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",label:"OpenStreetMap",maxZoom:19,cacheKey:"tiles-osm"}},xa=30*1024,It=2*Math.PI*6378137/2;function Ko(o,e){const t=o/It*180;let r=e/It*180;return r=180/Math.PI*(2*Math.atan(Math.exp(r*Math.PI/180))-Math.PI/2),[t,r]}function Xo(o,e,t){const r=Math.pow(2,t),n=Math.floor((o+180)/360*r),s=e*Math.PI/180,a=Math.floor((1-Math.log(Math.tan(s)+1/Math.cos(s))/Math.PI)/2*r);return{x:n,y:a}}function qn(o,e){const[t,r,n,s]=o,[a,i]=Ko(t,r),[l,c]=Ko(n,s),d=Xo(a,c,e),u=Xo(l,i,e),f=Math.pow(2,e),p=Math.max(0,Math.min(d.x,u.x)),h=Math.min(f-1,Math.max(d.x,u.x)),g=Math.max(0,Math.min(d.y,u.y)),m=Math.min(f-1,Math.max(d.y,u.y));return{z:e,minX:p,maxX:h,minY:g,maxY:m,count:(h-p+1)*(m-g+1)}}function Sa(o,e,t){let r=0;for(let n=e;n<=t;n++)r+=qn(o,n).count;return r}function Ta(o,e,t){const r=[];for(let n=e;n<=t;n++){const s=qn(o,n);for(let a=s.minX;a<=s.maxX;a++)for(let i=s.minY;i<=s.maxY;i++)r.push({z:n,x:a,y:i})}return r}function La(o,{z:e,x:t,y:r}){return o.replace("{z}",e).replace("{x}",t).replace("{y}",r)}class ka{constructor({baseMap:e,extent3857:t,minZoom:r,maxZoom:n,concurrency:s=2,interBatchDelayMs:a=50,onProgress:i=()=>{}}){const l=zn[e];if(!l)throw new Error(`Unknown base map: ${e}`);n>l.maxZoom&&(console.warn(`[OfflineTiles] ${e}: maxZoom ${n} > supported ${l.maxZoom}; clamping`),n=l.maxZoom),this.baseMap=e,this.template=l.url,this.extent=t,this.minZoom=r,this.maxZoom=n,this.concurrency=Math.max(1,Math.min(s,6)),this.interBatchDelayMs=a,this.onProgress=i,this._abortCtrl=null,this._cancelled=!1}async start(){if(this._abortCtrl)throw new Error("Downloader already started");this._abortCtrl=new AbortController,this._cancelled=!1;const e=Ta(this.extent,this.minZoom,this.maxZoom),t=e.length,r=Date.now();let n=0,s=0,a=0,i=0;const l=c=>{const d=Date.now()-r,u=n>0?Math.round(d/n*(t-n)):null;this.onProgress({phase:c,done:n,total:t,ok:s,failed:a,cached:i,elapsedMs:d,etaMs:u})};l("running");for(let c=0;c<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 f=La(this.template,u);try{const p=await fetch(f,{signal:this._abortCtrl.signal,cache:"default"});p.ok?(s++,p.body&&p.body.cancel().catch(()=>{})):(p.status,a++)}catch(p){p.name==="AbortError"||a++}n++})),l("running"),this.interBatchDelayMs>0&&c+this.concurrency<e.length&&await new Promise(u=>setTimeout(u,this.interBatchDelayMs))}return l(this._cancelled?"cancelled":"done"),{phase:this._cancelled?"cancelled":"done",done:n,total:t,ok:s,failed:a,cached:i,elapsedMs:Date.now()-r}}cancel(){this._cancelled=!0,this._abortCtrl&&this._abortCtrl.abort()}}const Ia=(()=>{const o=(r,n)=>{const s=r*It/180,a=Math.log(Math.tan((90+n)*Math.PI/360))/(Math.PI/180);return[s,a*It/180]},e=o(-3.3,4.5),t=o(1.2,11.2);return[e[0],e[1],t[0],t[1]]})();function Pa(o){return o*xa}const Hn="https://api.lupmis4luspa.org/api",Wn=`${Hn}/spatial_planning`;function Ma(o){return o.includes("/")?`${Hn}/${o}`:`${Wn}/${o}`}const zt="1",Aa="1c46538c712e9b5b";function Ca(){try{if(typeof window>"u")return zt;const o=window.LUPMIS_SESSION;if(!o||typeof o!="object")return zt;const e=o.district_id;return e==null||String(e).length===0?null:String(e)}catch{}return zt}const _o={get district_id(){return Ca()},api_token:Aa};function Ke(){if(typeof window<"u"&&window.LUPMIS_SESSION&&window.LUPMIS_SESSION.user_id)return window.LUPMIS_SESSION;try{const o=localStorage.getItem("dev-session");if(o){const e=JSON.parse(o);if(e&&e.user_id)return e}}catch{}return null}typeof window<"u"&&(window.lupmisDevSession=o=>{o==null?(localStorage.removeItem("dev-session"),console.log("[Dev] Session override cleared. Reload to apply.")):(localStorage.setItem("dev-session",JSON.stringify(o)),console.log("[Dev] Session override saved. Reload to apply:",o))});const Fa=3e4,Oa=5e3;let Fe=null;async function Da(o=!1){if(Fe!==null&&!o)return Fe;const e=new AbortController,t=setTimeout(()=>e.abort(),Oa);try{Fe=(await fetch(`${Wn}/get_layers.php`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(_o),signal:e.signal})).ok}catch{Fe=!1}finally{clearTimeout(t)}return console.log("[RemoteDB] Server reachable:",Fe),Fe}function ke(){return Fe}function Ra(o,e=Fa){const t=new AbortController,r=setTimeout(()=>t.abort(),e);return o.signal&&o.signal.addEventListener("abort",()=>t.abort()),{signal:t.signal,clear:()=>clearTimeout(r)}}async function me(o,e={},t={}){const r=Ma(o),n={..._o,...e};console.log("[RemoteDB] POST",r);const s=Ra(t);try{const a=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(n),...t,signal:s.signal});if(!a.ok)throw new Error(`HTTP ${a.status}: ${a.statusText}`);const i=await a.json();return console.log("[RemoteDB] POST response:",o,"→",typeof i=="object"?`${Array.isArray(i)?i.length+" items":"object"}`:i),i}catch(a){throw a.name==="AbortError"?(console.error("[RemoteDB] POST timed out:",o),new Error(`Request timed out: ${o}`)):(console.error("[RemoteDB] POST failed:",o,a),a)}finally{s.clear()}}async function Na(){return me("get_district_boundary.php")}async function $a(){return me("get_layers.php")}async function Ba(){return me("get_all_collector_zone_per_district.php")}async function Ga(){return me("get_parcels_per_district.php")}async function Ua(){return me("get_all_footprint_per_district.php")}async function ja(){const o=_o.district_id;return me("contours/get_by_district.php",{districtid:o==null?null:Number(o)})}async function za(){return me("get_osm_roads.php")}async function qa(){return me("get_upn_grid_per_district.php")}async function Ha(o,e){const t={client_uuid:o.client_uuid,name:o.name??null,started_at:o.started_at,ended_at:o.ended_at,point_count:o.point_count??e.length,distance_m:o.distance_m??0,points:(e||[]).map(n=>({seq:n.seq,longitude:n.longitude,latitude:n.latitude,altitude:n.altitude??null,accuracy:n.accuracy??null,altitude_accuracy:n.altitude_accuracy??null,heading:n.heading??null,speed:n.speed??null,satellites:n.satellites??null,recorded_at:n.recorded_at}))},r=await me("save_gps_trail.php",t);return{remoteId:r?.id??r?.remote_id??null}}const Wa=63710088e-1,De=Math.PI/180;function Va(o,e,t,r){const n=(r-e)*De,s=(t-o)*De,a=Math.sin(n/2)**2+Math.cos(e*De)*Math.cos(r*De)*Math.sin(s/2)**2;return 2*Wa*Math.asin(Math.min(1,Math.sqrt(a)))}function Yo(o,e=5){return o==null||Number.isNaN(o)?"—":o.toFixed(e)}const Jo=6378137,Zo=1/298.257223563,Qo=.9996,te=Zo*(2-Zo),qt=te/(1-te);function Ka(o,e){if(o==null||e==null||Number.isNaN(o)||Number.isNaN(e))return null;let t=((e+180)%360+360)%360-180;const r=Math.floor((t+180)/6)+1,n=(r-1)*6-180+3,s=o*De,a=t*De,i=n*De,l=Math.sin(s),c=Math.cos(s),d=Math.tan(s),u=Jo/Math.sqrt(1-te*l*l),f=d*d,p=qt*c*c,h=c*(a-i),g=Jo*((1-te/4-3*te**2/64-5*te**3/256)*s-(3*te/8+3*te**2/32+45*te**3/1024)*Math.sin(2*s)+(15*te**2/256+45*te**3/1024)*Math.sin(4*s)-35*te**3/3072*Math.sin(6*s));let m=Qo*u*(h+(1-f+p)*h**3/6+(5-18*f+f*f+72*p-58*qt)*h**5/120)+5e5,y=Qo*(g+u*d*(h*h/2+(5-f+9*p+4*p*p)*h**4/24+(61-58*f+f*f+600*p-330*qt)*h**6/720));const w=o>=0?"N":"S";return w==="S"&&(y+=1e7),{zone:r,hemisphere:w,easting:Math.round(m),northing:Math.round(y)}}function Xa(o,e){const t=Ka(o,e);return t?`${t.zone}${t.hemisphere} ${t.easting}E ${t.northing}N`:"—"}function Ya(o){return o==null||Number.isNaN(o)?"—":o<1e3?`${Math.round(o)} m`:`${(o/1e3).toFixed(2)} km`}function Ja(o){return o==null||Number.isNaN(o)?"—":`±${Math.round(o)} m`}function Za(o){return o==null||Number.isNaN(o)?"none":o<=10?"good":o<=30?"fair":"poor"}const Qa={minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0,timeoutMs:15e3,maximumAgeMs:0};class st{constructor(e={}){this.opts={...Qa,...e},this.storage=e.storage||null,this.sync=e.sync||null,this._geo=e.geolocation||(typeof navigator<"u"?navigator.geolocation:null),this._state="idle",this._watchId=null,this._live=!1,this._recording=!1,this._activeTrailId=null,this._activeTrailUuid=null,this._lastRecorded=null,this._lastRecordedAt=0,this._distanceM=0,this._pointCount=0,this._lastFix=null,this._listeners=Object.create(null)}on(e,t){return(this._listeners[e]||(this._listeners[e]=new Set)).add(t),()=>this._listeners[e]?.delete(t)}_emit(e,t){const r=this._listeners[e];if(r)for(const n of r)try{n(t)}catch(s){console.error(`[GeoTracker] listener for "${e}" threw`,s)}}get state(){return this._state}get isRecording(){return this._recording}get lastFix(){return this._lastFix}get isSupported(){return!!this._geo}_setState(e){this._state!==e&&(this._state=e,this._emit("statechange",e))}startLive(){if(!this._geo){this._emit("error",new Error("Geolocation not supported"));return}this._live=!0,this._ensureWatch()}stopLive(){this._live=!1,this._recording||this._teardownWatch()}getCurrentPosition(){return new Promise((e,t)=>{if(!this._geo){t(new Error("Geolocation not supported"));return}this._geo.getCurrentPosition(r=>{const n=st.normalize(r);this._lastFix=n,this._emit("position",n),e(n)},r=>{this._emit("error",r),t(r)},{enableHighAccuracy:this.opts.enableHighAccuracy,timeout:this.opts.timeoutMs,maximumAge:this.opts.maximumAgeMs})})}async startRecording(e={}){if(!this._geo)throw new Error("Geolocation not supported");if(!this.storage)throw new Error("GeoTracker: no storage adapter configured");if(this._recording)return{trailId:this._activeTrailId,uuid:this._activeTrailUuid};const t=st.uuid(),r=new Date().toISOString(),n={uuid:t,name:e.name||null,startedAt:r,...e},s=await this.storage.createTrail(n);return this._activeTrailId=s,this._activeTrailUuid=t,this._lastRecorded=null,this._lastRecordedAt=0,this._distanceM=0,this._pointCount=0,this._recording=!0,this._ensureWatch(),this._setState("recording"),this._emit("trailstart",{trailId:s,uuid:t,startedAt:r}),{trailId:s,uuid:t}}async stopRecording(){if(!this._recording)return null;const e=this._activeTrailId,r={endedAt:new Date().toISOString(),pointCount:this._pointCount,distanceM:this._distanceM};this._recording=!1,this._live||this._teardownWatch(),this._setState(this._live?"watching":"idle");try{await this.storage.finishTrail(e,r)}catch(s){this._emit("error",s)}this._emit("trailstop",{trailId:e,...r});let n=!1;if(this.sync)try{n=await this._syncTrail(e)}catch(s){this._emit("error",s)}return this._activeTrailId=null,this._activeTrailUuid=null,{trailId:e,pointCount:r.pointCount,distanceM:r.distanceM,synced:n}}async syncPending(){if(!this.sync||!this.storage)return{pushed:0,failed:0};if(this.sync.isOnline&&!this.sync.isOnline())return{pushed:0,failed:0};let e=0,t=0;const r=await this.storage.getUnsyncedTrails();for(const n of r)try{await this._syncTrail(n.id??n.trailId,n)?e++:t++}catch(s){t++,this._emit("error",s)}return this._emit("syncstatus",{pushed:e,failed:t}),{pushed:e,failed:t}}async _syncTrail(e,t){const r=await this.storage.getTrailPoints(e),n=t||{id:e},s=await this.sync.pushTrail(n,r),a=s&&(s.remoteId??s.id??null);return await this.storage.markTrailSynced(e,a),!0}_ensureWatch(){if(this._watchId!=null||!this._geo){this._state==="idle"&&this._live&&this._setState("watching");return}this._watchId=this._geo.watchPosition(e=>this._onFix(e),e=>this._emit("error",e),{enableHighAccuracy:this.opts.enableHighAccuracy,timeout:this.opts.timeoutMs,maximumAge:this.opts.maximumAgeMs}),this._recording||this._setState("watching")}_teardownWatch(){this._watchId!=null&&this._geo&&this._geo.clearWatch(this._watchId),this._watchId=null}async _onFix(e){const t=st.normalize(e);if(this._lastFix=t,this._emit("position",t),!this._recording)return;const{minIntervalMs:r,minDistanceM:n,heartbeatMs:s,maxAccuracyM:a}=this.opts,i=t.timestamp;if(this._lastRecordedAt&&i-this._lastRecordedAt<r||a>0&&t.accuracy!=null&&t.accuracy>a&&this._lastRecorded)return;let l=!1,c=0;if(!this._lastRecorded)l=!0;else{c=Va(this._lastRecorded.lon,this._lastRecorded.lat,t.lon,t.lat);const d=i-this._lastRecordedAt;(c>=n||d>=s)&&(l=!0)}if(l){this._lastRecorded&&(this._distanceM+=c),this._pointCount+=1,this._lastRecorded={lon:t.lon,lat:t.lat,timestamp:i},this._lastRecordedAt=i;try{await this.storage.addPoint(this._activeTrailId,{...t,seq:this._pointCount}),this._emit("point",{trailId:this._activeTrailId,seq:this._pointCount,point:t,distanceM:this._distanceM,pointCount:this._pointCount})}catch(d){this._emit("error",d)}}}static normalize(e){const t=e.coords||{},r=n=>n!=null&&!Number.isNaN(n)?n:null;return{lon:t.longitude,lat:t.latitude,accuracy:r(t.accuracy),altitude:r(t.altitude),altitudeAccuracy:r(t.altitudeAccuracy),heading:r(t.heading),speed:r(t.speed),satellites:null,timestamp:e.timestamp||Date.now()}}static uuid(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{const t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}}const ei={async createTrail(o){const e=o.districtId??Ke()?.district_id??null;return vs({...o,districtId:e!=null?String(e):null})},addPoint:(o,e)=>Es(o,e),finishTrail:(o,e)=>xs(o,e),getUnsyncedTrails:()=>Ss(),getTrailPoints:o=>Ts(o),markTrailSynced:(o,e)=>Ls(o,e)},ti={pushTrail:(o,e)=>Ha(o,e),isOnline:()=>K()},ve=new st({storage:ei,sync:ti,minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0}),oi=new Set(["set:view","set:selected","clear:selected","set:basemap"]);function ni({mapView:o,embedConfig:e}){const t=o.getMap(),r=window.parent&&window.parent!==window?window.parent:null,n=new G,s=new D({source:n,zIndex:9999,style:new M({stroke:new I({color:"#f97316",width:3}),fill:new A({color:"rgba(249,115,22,0.18)"})}),properties:{title:"Permit selection",displayInLayerSwitcher:!1}});t.addLayer(s);let a=null,i=e?.upn?String(e.upn):null,l=!1;function c(m){if(!r){console.warn("[embed-bridge] No parent window — would have sent:",m);return}try{r.postMessage(m,"*")}catch(y){console.warn("[embed-bridge] postMessage failed:",y)}}function d(m,y){c({type:"error",code:m,message:y})}function u(){l||(l=!0,c({type:"ready"}))}function f(m,y,w){const _=m.getProperties();let E=y,T=w;if(E==null||T==null){const x=m.getGeometry()?.getExtent();if(x){const[S,P]=xe(Tr(x));E=S,T=P}}return{type:"parcel:select",upn:_.upn??null,parcel_id:_.id??null,lon:E??null,lat:T??null,zone_code:_.zone_code??null,zone_name:_.zone_name??null,landuse:_.landuse??null,min_height:_.min_height??null,max_height:_.max_height??null}}function p(m){if(n.clear(),m){const y=m.clone();n.addFeature(y)}}o.onClick((m,y,w,_)=>{let E=null;t.forEachFeatureAtPixel(_.pixel,T=>{if(T.get("_layerType")==="parcel")return E=T,!0}),E?(p(E),c(f(E,m,y))):(p(null),c({type:"parcel:cleared"}))}),window.addEventListener("message",m=>{const y=m.data;if(!(!y||typeof y!="object"||!oi.has(y.type)))try{switch(y.type){case"set:view":{if(typeof y.lon=="number"&&typeof y.lat=="number"){const w=t.getView();w.setCenter(ee([y.lon,y.lat])),typeof y.zoom=="number"&&w.setZoom(y.zoom)}break}case"set:selected":y.upn&&h(String(y.upn));break;case"clear:selected":p(null),i=null;break;case"set:basemap":y.key&&typeof o.setBaseMap=="function"&&o.setBaseMap(y.key);break}}catch(w){d("COMMAND_FAILED",`Failed to handle ${y.type}: ${w.message}`)}});function h(m){if(!a){i=m;return}const w=a.getSource().getFeatures().find(E=>String(E.get("upn")??"")===m);if(!w){i=m;return}i=null,p(w);const _=w.getGeometry()?.getExtent();_&&t.getView().fit(_,{padding:[50,50,50,50],duration:400,maxZoom:17}),c(f(w,null,null))}function g(m){a=m;const y=m.getSource(),w=()=>{queueMicrotask(()=>{i&&h(i),u()})};if(y.getFeatures().length>0)w();else{let _=!1;y.on("addfeature",()=>{_||(_=!0,queueMicrotask(()=>{_=!1,i&&h(i),u()}))})}}if(e?.basemap&&typeof o.setBaseMap=="function"&&o.setBaseMap(e.basemap),typeof e?.lon=="number"&&typeof e?.lat=="number"){const m=t.getView();m.setCenter(ee([e.lon,e.lat])),m.setZoom(typeof e?.zoom=="number"?e.zoom:15)}return{attachParcelsLayer:g,emitError:d}}const ri=[{key:"parcels",label:"Parcels",geometryFamily:"polygon"},{key:"collector_zones",label:"Collector Zones",geometryFamily:"polygon"},{key:"building_footprints",label:"Building Footprints",geometryFamily:"polygon"},{key:"osm_roads",label:"OSM Roads",geometryFamily:"line"},{key:"other",label:"Other (view only)",geometryFamily:"any"}],Vn={parcels:["upn","landuse","zone_code","zone_name","sector","block","parcel_no","prop_no","st_name","prop_add","fac_name","min_height","max_height","eff_date","lp_name","locality","mmda","last_update","remarks"],collector_zones:["zone_name"],building_footprints:[],osm_roads:["osm_id","name","highway"],other:[]};function si(o){if(!o?.features?.length)return"none";let e=0,t=0,r=0;for(const a of o.features){const i=a?.geometry?.type;i&&(i==="Polygon"||i==="MultiPolygon"?e++:i==="LineString"||i==="MultiLineString"?t++:(i==="Point"||i==="MultiPoint")&&r++)}const n=e+t+r;if(n===0)return"none";const s=Math.max(e,t,r);return s<n*.85?"mixed":e===s?"polygon":t===s?"line":"point"}function Kn(o,e=50){const t=new Set,r=Math.min(e,o.features.length);for(let n=0;n<r;n++){const s=o.features[n]?.properties;if(!(!s||typeof s!="object"))for(const a of Object.keys(s))t.add(String(a).toLowerCase())}return t}function Ge(o,...e){for(const t of e)if(o.has(t))return!0;return!1}function ai(o){const e=si(o);if(e==="none"||e==="mixed")return"other";const t=Kn(o);return e==="line"?Ge(t,"osm_id","highway")?"osm_roads":"other":e==="polygon"?Ge(t,"upn","parcel_no","landuse","lu_code","zone_code")&&Ge(t,"upn","parcel_no","landuse","lu_code")?"parcels":Ge(t,"zone_name","colzonename","colzonenr")?"collector_zones":Ge(t,"building","building:levels","building_levels","height","min_height","max_height")&&!Ge(t,"upn","parcel_no")?"building_footprints":"other":"other"}const ii={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 en(o){return String(o).toLowerCase().replace(/[\s_\-]+/g,"")}function Xn(o,e){const t=Vn[e]||[];if(t.length===0)return{};const r=Kn(o,50),n=new Map;for(const a of r)n.set(en(a),a);const s={};for(const a of t){const i=ii[a]||[a];let l=null;for(const c of i){const d=en(c);if(n.has(d)){l=n.get(d);break}}s[a]=l}return s}function li(o,e){const t={};for(const[r,n]of Object.entries(e||{}))n!=null&&o&&Object.prototype.hasOwnProperty.call(o,n)&&(t[r]=o[n]);return t}function ci(o){const e=new Map;for(const t of o.features||[]){const r=t?.properties;if(!(!r||typeof r!="object"))for(const n of Object.keys(r))e.has(n)||e.set(n,!0)}return Array.from(e.keys())}function di(o,e=35){const t={};for(const r of o.features||[]){const n=r?.properties;if(!(!n||typeof n!="object"))for(const[s,a]of Object.entries(n)){if(t[s]!==void 0||a==null||a==="")continue;let i;if(typeof a=="object")try{i=JSON.stringify(a)}catch{i=String(a)}else i=String(a);i=i.replace(/\s+/g," ").trim(),i&&(i.length>e&&(i=i.slice(0,e-1)+"…"),t[s]=i)}}return t}const L={};let no=null,N=null;function ui(){L.root||(L.root=document.getElementById("importMappingModal"),L.filename=document.getElementById("import-modal-filename"),L.summary=document.getElementById("import-modal-summary"),L.target=document.getElementById("import-modal-target"),L.targetHint=document.getElementById("import-modal-target-hint"),L.fieldsWrap=document.getElementById("import-modal-fields-wrap"),L.tbody=document.getElementById("import-modal-fields-tbody"),L.btnSave=document.getElementById("import-modal-save"),L.btnSaveUpload=document.getElementById("import-modal-save-upload"),L.btnCancel=document.getElementById("import-modal-cancel"),L.target&&!L.target.dataset.populated&&(L.target.innerHTML=ri.map(o=>`<option value="${o.key}">${o.label}</option>`).join(""),L.target.dataset.populated="1"),L.target&&!L.target.dataset.wired&&(L.target.dataset.wired="1",L.target.addEventListener("change",Yn)),L.btnSave&&!L.btnSave.dataset.wired&&(L.btnSave.dataset.wired="1",L.btnSave.addEventListener("click",()=>tn("save"))),L.btnSaveUpload&&!L.btnSaveUpload.dataset.wired&&(L.btnSaveUpload.dataset.wired="1",L.btnSaveUpload.addEventListener("click",()=>tn("upload"))),L.root&&!L.root.dataset.wired&&(L.root.dataset.wired="1",L.root.addEventListener("hidden.bs.modal",()=>{N?.onResult&&!N._resolved&&(N._resolved=!0,N.onResult({action:"cancel"})),N=null})))}function pi(){const o=N.targetType,e=Vn[o]||[];if(o==="other"||e.length===0){L.fieldsWrap.style.display="none";return}L.fieldsWrap.style.display="";const t=n=>{const s=N.sourceSamples[n];return s?` — [${_t(s)}]`:" — [(empty)]"},r=['<option value="">(none)</option>'].concat(N.sourceFields.map(n=>`<option value="${mt(n)}">${_t(hi(n))}${t(n)}</option>`)).join("");L.tbody.innerHTML=e.map(n=>{const s=N.mapping[n]||"",a=r.replace(`<option value="${mt(s)}">`,`<option value="${mt(s)}" selected>`);return`
|
||
<tr>
|
||
<td><code>${_t(n)}</code></td>
|
||
<td>
|
||
<select class="form-select form-select-sm import-field-map"
|
||
data-col="${mt(n)}">
|
||
${a}
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
`}).join(""),L.tbody.querySelectorAll(".import-field-map").forEach(n=>{n.addEventListener("change",s=>{const a=s.target.dataset.col;N.mapping[a]=s.target.value||null})})}function Yn(){const o=L.target.value;N.targetType=o,N.mapping=Xn(N.fc,o),o==="other"?(L.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>",L.btnSave.disabled=!1,L.btnSaveUpload.disabled=!0):(L.targetHint.innerHTML="Each LUPMIS2 column is matched to a source field where possible. You can override any choice below.",L.btnSave.disabled=!1,L.btnSaveUpload.disabled=!1),pi()}function tn(o){if(!N||N._resolved)return;N._resolved=!0;const{targetType:e,mapping:t,onResult:r}=N;no.hide(),r&&r({action:o,targetType:e,mapping:e==="other"?null:{...t}}),N=null}function fi(o){if(ui(),!L.root){console.warn("[ImportModal] Modal element missing — calling onResult with cancel"),o.onResult?.({action:"cancel"});return}const e=o.fc,t=e?.features?.length??0,r=ai(e);N={importId:o.importId,filename:o.filename,fc:e,sourceFields:ci(e),sourceSamples:di(e),targetType:r,mapping:Xn(e,r),onResult:o.onResult,_resolved:!1},L.filename.textContent=o.filename||"imported dataset",L.summary.textContent=`— ${t} feature${t===1?"":"s"}`,L.target.value=N.targetType,Yn(),no=lt.getOrCreateInstance(L.root),no.show()}function _t(o){return String(o).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function mt(o){return _t(o)}function hi(o){let e="";for(const t of String(o)){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}const gi=(()=>{const o=new Uint32Array(256);for(let e=0;e<256;e++){let t=e;for(let r=0;r<8;r++)t=t&1?3988292384^t>>>1:t>>>1;o[e]=t>>>0}return o})();function mi(o){let e=4294967295;for(let t=0;t<o.length;t++)e=gi[(e^o[t])&255]^e>>>8;return(e^4294967295)>>>0}function yi(o,e){return e instanceof Uint8Array?e:e instanceof ArrayBuffer?new Uint8Array(e):ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):o.encode(String(e))}function bi(o,e="application/zip"){const t=new TextEncoder,r=Object.entries(o).map(([h,g])=>{const m=t.encode(h);return{nameBytes:m,data:yi(t,g),flags:m.length===h.length?0:2048}}),n=0,s=20513,a=[],i=[];let l=0;const c=h=>[h&255,h>>>8&255],d=h=>[h&255,h>>>8&255,h>>>16&255,h>>>24&255];for(const h of r){const g=mi(h.data),m=h.data.length,y=[...d(67324752),...c(20),...c(h.flags),...c(0),...c(n),...c(s),...d(g),...d(m),...d(m),...c(h.nameBytes.length),...c(0)];a.push(new Uint8Array(y),h.nameBytes,h.data),i.push([...d(33639248),...c(20),...c(20),...c(h.flags),...c(0),...c(n),...c(s),...d(g),...d(m),...d(m),...c(h.nameBytes.length),...c(0),...c(0),...c(0),...c(0),...d(0),...d(l)]),i.push(h.nameBytes),l+=y.length+h.nameBytes.length+m}const u=[];let f=0;for(const h of i){const g=h instanceof Uint8Array?h:new Uint8Array(h);u.push(g),f+=g.length}const p=new Uint8Array([...d(101010256),...c(0),...c(0),...c(r.length),...c(r.length),...d(f),...d(l),...c(0)]);return new Blob([...a,...u,p],{type:e})}function ro(o,e){const t=URL.createObjectURL(o),r=document.createElement("a");r.href=t,r.download=e,document.body.appendChild(r),r.click(),r.remove(),setTimeout(()=>URL.revokeObjectURL(t),1e3)}const Pt=1,wi=3,_i=5,vi='GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]';function Ei(o,e={}){const t=Si(o),r=Object.keys(t).filter(a=>t[a].length>0);if(r.length===0)throw new Error("No point, line or polygon geometries to export.");const n=e.folder?`${String(e.folder).replace(/\/+$/,"")}/`:"",s={};for(const a of r){const i=t[a],l=e.types&&e.types[a]||a,{shp:c,shx:d}=ki(i,xi[a]);s[`${n}${l}.shp`]=c,s[`${n}${l}.shx`]=d,s[`${n}${l}.dbf`]=Ai(i.map(u=>u.properties)),s[`${n}${l}.prj`]=vi,s[`${n}${l}.cpg`]="UTF-8"}return bi(s,"application/zip")}const xi={point:Pt,polyline:wi,polygon:_i};function Si(o){const e={point:[],polyline:[],polygon:[]},t=o&&o.features||[];for(const r of t){const n=r&&r.properties||{};for(const s of Jn(r&&r.geometry))switch(s.type){case"Point":so(s.coordinates)&&e.point.push({parts:[[s.coordinates]],properties:n});break;case"MultiPoint":for(const a of s.coordinates||[])so(a)&&e.point.push({parts:[[a]],properties:n});break;case"LineString":{const a=[ao(s.coordinates)].filter(i=>i.length>=2);a.length&&e.polyline.push({parts:a,properties:n});break}case"MultiLineString":{const a=(s.coordinates||[]).map(ao).filter(i=>i.length>=2);a.length&&e.polyline.push({parts:a,properties:n});break}case"Polygon":{const a=on(s.coordinates);a.length&&e.polygon.push({parts:a,properties:n});break}case"MultiPolygon":{const a=[];for(const i of s.coordinates||[])a.push(...on(i));a.length&&e.polygon.push({parts:a,properties:n});break}}}return e}function*Jn(o){if(!(!o||!o.type))if(o.type==="GeometryCollection")for(const e of o.geometries||[])yield*Jn(e);else yield o}function so(o){return Array.isArray(o)&&Number.isFinite(o[0])&&Number.isFinite(o[1])}function ao(o){const e=[];for(const t of o||[]){if(!so(t))continue;const r=e[e.length-1];r&&r[0]===t[0]&&r[1]===t[1]||e.push([t[0],t[1]])}return e}function on(o){const e=[];return(o||[]).forEach((t,r)=>{const n=Ti(ao(t));if(n.length<4)return;const s=r===0;Li(n)>0===s&&n.reverse(),e.push(n)}),e}function Ti(o){if(o.length<3)return o;const e=o[0],t=o[o.length-1];return(e[0]!==t[0]||e[1]!==t[1])&&o.push([e[0],e[1]]),o}function Li(o){let e=0;for(let t=0,r=o.length-1;t<o.length;r=t++)e+=o[r][0]*o[t][1]-o[t][0]*o[r][1];return e/2}function ki(o,e){const t=e===Pt,r=o.map(d=>t?20:44+4*d.parts.length+16*Zn(d.parts)),n=r.reduce((d,u)=>d+8+u,100),s=100+8*o.length,a=new DataView(new ArrayBuffer(n)),i=new DataView(new ArrayBuffer(s)),l=Qn(o);nn(a,e,n,l),nn(i,e,s,l);let c=100;return o.forEach((d,u)=>{const f=r[u];a.setInt32(c,u+1),a.setInt32(c+4,f/2),i.setInt32(100+u*8,c/2),i.setInt32(100+u*8+4,f/2),Ii(a,c+8,d,e),c+=8+f}),{shp:new Uint8Array(a.buffer),shx:new Uint8Array(i.buffer)}}function nn(o,e,t,r){o.setInt32(0,9994),o.setInt32(24,t/2),o.setInt32(28,1e3,!0),o.setInt32(32,e,!0),o.setFloat64(36,r.xmin,!0),o.setFloat64(44,r.ymin,!0),o.setFloat64(52,r.xmax,!0),o.setFloat64(60,r.ymax,!0)}function Ii(o,e,t,r){if(r===Pt){const[c,d]=t.parts[0][0];o.setInt32(e,Pt,!0),o.setFloat64(e+4,c,!0),o.setFloat64(e+12,d,!0);return}const n=Qn([t]),s=Zn(t.parts);o.setInt32(e,r,!0),o.setFloat64(e+4,n.xmin,!0),o.setFloat64(e+12,n.ymin,!0),o.setFloat64(e+20,n.xmax,!0),o.setFloat64(e+28,n.ymax,!0),o.setInt32(e+36,t.parts.length,!0),o.setInt32(e+40,s,!0);let a=e+44,i=0;for(const c of t.parts)o.setInt32(a,i,!0),a+=4,i+=c.length;let l=a;for(const c of t.parts)for(const[d,u]of c)o.setFloat64(l,d,!0),o.setFloat64(l+8,u,!0),l+=16}function Zn(o){return o.reduce((e,t)=>e+t.length,0)}function Qn(o){let e=1/0,t=1/0,r=-1/0,n=-1/0;for(const s of o)for(const a of s.parts)for(const[i,l]of a)i<e&&(e=i),l<t&&(t=l),i>r&&(r=i),l>n&&(n=l);return Number.isFinite(e)?{xmin:e,ymin:t,xmax:r,ymax:n}:{xmin:0,ymin:0,xmax:0,ymax:0}}const Pi=254,Mi=18,rn=10;function Ai(o){const e=Ci(o),t=new TextEncoder,r=32+32*e.length+1,n=e.reduce((u,f)=>u+f.size,1),s=r+o.length*n+1,a=new ArrayBuffer(s),i=new DataView(a),l=new Uint8Array(a),c=new Date;i.setUint8(0,3),i.setUint8(1,c.getFullYear()-1900),i.setUint8(2,c.getMonth()+1),i.setUint8(3,c.getDate()),i.setUint32(4,o.length,!0),i.setUint16(8,r,!0),i.setUint16(10,n,!0),e.forEach((u,f)=>{const p=32+f*32,h=t.encode(u.name).subarray(0,10);l.set(h,p),i.setUint8(p+11,u.type.charCodeAt(0)),i.setUint8(p+16,u.size),i.setUint8(p+17,u.decimals)}),i.setUint8(32+e.length*32,13);let d=r;return o.forEach((u,f)=>{i.setUint8(d,32),d+=1;for(const p of e){const h=p.sequential?f+1:u?u[p.source]:void 0;l.set(Ri(t,p,h),d),d+=p.size}}),i.setUint8(d,26),l}function Ci(o){const e=[];for(const r of o)for(const n of Object.keys(r||{}))e.includes(n)||e.push(n);if(e.length===0)return[{name:"FID",source:null,type:"N",size:9,decimals:0,sequential:!0}];const t=new Set;return e.map(r=>{const n=Oi(o.map(s=>s?s[r]:void 0));return n.source=r,n.name=Fi(r,t),n})}function Fi(o,e){const t=String(o).replace(/[^A-Za-z0-9_]+/g,"_").slice(0,10)||"field";let r=t,n=1;for(;e.has(r.toUpperCase());){const s=String(n++);r=t.slice(0,Math.max(1,10-s.length))+s}return e.add(r.toUpperCase()),r}function Oi(o){const e=new TextEncoder;let t=!1,r=!0,n=!0,s=0,a=1,i=1;for(const l of o){if(l==null||l==="")continue;if(t=!0,typeof l!="boolean"&&(r=!1),typeof l=="number"&&Number.isFinite(l)){const d=Di(l);d>s&&(s=d)}else n=!1;const c=l instanceof Date?l.toISOString():String(l);i=Math.max(i,e.encode(c).length)}if(!t)return{type:"C",size:1,decimals:0};if(r)return{type:"L",size:1,decimals:0};if(n){let l=!0;for(const c of o){if(typeof c!="number"||!Number.isFinite(c))continue;const d=er(c,s);if(d===null){l=!1;break}a=Math.max(a,d.length)}if(l&&a<=Mi)return{type:"N",size:a,decimals:s}}return{type:"C",size:Math.min(i,Pi),decimals:0}}function er(o,e){const t=o.toFixed(e);return t.includes("e")||t.includes("E")?null:t}function Di(o){if(Number.isInteger(o))return 0;for(let e=1;e<rn;e++)if(Number(o.toFixed(e))===o)return e;return rn}function Ri(o,e,t){const r=new Uint8Array(e.size).fill(32);if(e.type==="L")return r[0]=t==null||t===""?63:t?84:70,r;if(t==null||t==="")return r;if(e.type==="N"){const s=typeof t=="number"&&Number.isFinite(t)?er(t,e.decimals):null,a=s===null?null:o.encode(s);return a&&a.length&&a.length<=e.size&&r.set(a,e.size-a.length),r}const n=t instanceof Date?t.toISOString():String(t);return r.set(Ni(o,n,e.size),0),r}function Ni(o,e,t){const r=o.encode(e);if(r.length<=t)return r;let n=t;for(;n>0&&(r[n]&192)===128;)n--;return r.subarray(0,n)}const tr="EPSG:3857",or="EPSG:4326";async function $i({features:o,rename:e,format:t,filenameBase:r="export"}){if(!Array.isArray(o)||o.length===0)throw new Error("No features to export");const n=Bi(o,e);switch(t){case"geojson":return ro(new Blob([JSON.stringify(n,null,2)],{type:"application/geo+json"}),`${r}.geojson`);case"kml":return ro(new Blob([Gi(o,e)],{type:"application/vnd.google-earth.kml+xml"}),`${r}.kml`);case"shp":return Ui(n,r);default:throw new Error(`Unknown export format: ${t}`)}}function Bi(o,e){const t=new le,r={type:"FeatureCollection",features:[]};for(const n of o){if(!n.getGeometry())continue;const s=t.writeGeometryObject(n.getGeometry(),{dataProjection:or,featureProjection:tr}),a=nr(n.getProperties()),i={};for(const[l,c]of Object.entries(e||{}))c&&Object.prototype.hasOwnProperty.call(a,l)&&(i[c]=a[l]);r.features.push({type:"Feature",geometry:s,properties:i})}return r}function nr(o){const e={};for(const[t,r]of Object.entries(o||{}))t!=="geometry"&&(e[t]=r);return e}function Gi(o,e){const t=o.filter(r=>r.getGeometry()).map(r=>{const n=r.clone(),s=nr(r.getProperties()),a={};for(const[l,c]of Object.entries(e||{}))c&&Object.prototype.hasOwnProperty.call(s,l)&&(a[c]=s[l]);n.setProperties(a,!0);const i=Object.values(e||{}).find(l=>l&&a[l]!=null&&a[l]!=="");return i&&n.set("name",String(a[i])),n});return new gn({extractStyles:!1}).writeFeatures(t,{dataProjection:or,featureProjection:tr})}function Ui(o,e){const t=Ei(o,{folder:e,types:{point:`${e}_point`,polygon:`${e}_polygon`,polyline:`${e}_line`}});ro(t,`${e}.zip`)}const C={};let io=null,Te=null;function ji(){C.root||(C.root=document.getElementById("exportGisModal"),C.summary=document.getElementById("export-gis-summary"),C.filename=document.getElementById("export-gis-filename"),C.tbody=document.getElementById("export-gis-fields-tbody"),C.fmtHint=document.getElementById("export-gis-format-hint"),C.fieldsWrap=document.getElementById("export-gis-fields-wrap"),C.btnGo=document.getElementById("export-gis-go"),C.fmtInputs=Array.from(document.querySelectorAll('input[name="export-gis-format"]')),C.root.dataset.wired||(C.root.dataset.wired="1",C.fmtInputs.forEach(o=>o.addEventListener("change",sr)),C.btnGo.addEventListener("click",Hi)))}function zi(o){const e=[];for(const t of o.parcelFeatures||[])e.push(Ht(t,"Parcels"));for(const t of o.zoneFeatures||[])e.push(Ht(t,"Zones"));for(const[t,r]of Object.entries(o.otherByLayer||{}))for(const n of r)e.push(Ht(n,t));return e}function Ht(o,e){const t=o.clone();return t.set("_source",e),t}function qi(o){const e=new Set(["geometry","_layerType"]),t=new Map;for(const n of o)for(const s of Object.keys(n.getProperties()||{}))e.has(s)||t.has(s)||t.set(s,!0);t.delete("_source");const r=Array.from(t.keys());return r.push("_source"),r}function rr(){const o=Mt();C.tbody.innerHTML=Te.keys.map(e=>{const t=Te.rename[e]??e,n=o==="shp"&&t.length>10?`<div class="form-text text-danger mt-1">
|
||
${lo(t.length)} characters — Shapefile will
|
||
truncate / rename.
|
||
</div>`:"";return`
|
||
<tr>
|
||
<td><code>${lo(e)}</code></td>
|
||
<td>
|
||
<input type="text" class="form-control form-control-sm export-field-rename"
|
||
data-src="${sn(e)}"
|
||
value="${sn(t)}">
|
||
${n}
|
||
</td>
|
||
</tr>
|
||
`}).join(""),C.tbody.querySelectorAll(".export-field-rename").forEach(e=>{e.addEventListener("input",t=>{const r=t.target.dataset.src;Te.rename[r]=t.target.value,Mt()==="shp"&&rr()})})}function sr(){const o=Mt();C.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."}[o];const e=o==="pdf";C.fieldsWrap&&C.fieldsWrap.classList.toggle("d-none",e),rr()}function Mt(){return C.fmtInputs.find(o=>o.checked)?.value||"geojson"}async function Hi(){const o=Mt(),e=(C.filename.value||"export").replace(/[^A-Za-z0-9_\-]+/g,"_");C.btnGo.disabled=!0;try{if(o==="pdf"){const t=Te.pdfRows||[];if(t.length===0)throw new Error("No summary figures available for a PDF report.");const{exportAnalysisPDF:r}=await Le(async()=>{const{exportAnalysisPDF:n}=await import("./pdf-export-DJBZ6C0H.js");return{exportAnalysisPDF:n}},__vite__mapDeps([5,6,4,3]));await r({title:Te.title||"Analysis",rows:t})}else await $i({features:Te.features,rename:Te.rename,format:o,filenameBase:e});io.hide()}catch(t){console.error("[ExportGIS] failed:",t),alert("Export failed: "+t.message)}finally{C.btnGo.disabled=!1}}function Wi(o){if(ji(),!C.root){console.warn("[ExportGIS] Modal missing from DOM");return}const e=zi(o);if(e.length===0){alert("No intersecting features to export.");return}const t=qi(e),r=Object.fromEntries(t.map(s=>[s,s]));Te={features:e,keys:t,rename:r,pdfRows:o.pdfRows||[],title:o.title},C.summary.textContent=`${e.length} feature${e.length===1?"":"s"} intersecting the ${o.kind==="circle"?"circle":"area"}`,C.filename.value=o.kind==="circle"?"circle_analysis":"area_analysis";const n=document.getElementById("export-gis-fmt-geojson");n&&(n.checked=!0),sr(),io=lt.getOrCreateInstance(C.root),io.show()}function lo(o){return String(o).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function sn(o){return lo(o)}let Wt=null;async function an(){if(!Wt){const o=await Le(()=>import("./shpjs-CNrRgkgn.js"),[]);Wt=o.default||o}return Wt}function vo(o){const e=document.getElementById("import-spinner-overlay"),t=document.getElementById("import-spinner-filename");e&&(t&&(t.textContent=o||""),e.classList.remove("d-none"),e.classList.add("d-flex"))}function ue(){const o=document.getElementById("import-spinner-overlay");o&&(o.classList.add("d-none"),o.classList.remove("d-flex"))}let v=null,ae=null,ie=null,Vt=null;const co=typeof window<"u"&&window.LUPMIS_EMBED||null,Ue=!!(co&&co.mode==="permit");let z=Ue?"embed-permit":"none";function Vi(){const o=typeof window<"u"?window.LUPMIS_SESSION:null;if(!o||typeof o!="object")return!1;const e=o.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=o.full_name||o.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;">
|
||
${U(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 ln(){if(console.log("[App] Initializing..."),Vi())return;await Ea({installButton:"#install-btn",offlineIndicator:"#offline-indicator",autoRegisterSW:!0});const o=localStorage.getItem("default-basemap")||"topo";v=new la("map",{center:[-1.5,7.5],zoom:7,basemap:o}),ae=new ca(v.getMap()),Pl(),ae.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)}),Ue&&(Vt=ni({mapView:v,embedConfig:co})),v.onClick((t,r,n,s)=>{if(Ue||(console.log("[MapClick] Clicked at:",t.toFixed(4),r.toFixed(4)),console.log("[MapClick] currentMode =",z),z==="draw"||z.startsWith("measure")))return;let a=null;if(v.getMap().forEachFeatureAtPixel(s.pixel,l=>{if(l.get("_layerType")==="parcel")return a=l,!0}),a){console.log("[MapClick] Clicked on parcel → Edit Attributes"),v.showParcelEditPopup(a,s.coordinate);return}let i=null;if(v.getMap().forEachFeatureAtPixel(s.pixel,l=>{if(l.get("_layerType")==="upn_grid")return i=l,!0}),i){console.log("[MapClick] Clicked on UPN-grid cell → Info popup"),v.showInfoPopup(i,s.coordinate,{title:"UPN Grid Cell",color:"#7c3aed"});return}z==="none"&&(n?(console.log("[MapClick] Clicked on marker:",n.getId()),v.selectMarker(n),Xi(n)):v.clearSelection())}),v.onDblClick((t,r,n,s)=>{if(Ue||!n)return;const a=n.get("_layerType");if(console.log("[App] Double-click on feature, _layerType:",a||"none"),a==="measure_circle")v.showCircleIntersectionPopup(n,s.coordinate);else{if(a==="measure_circle_radius")return;a==="measure_area"?v.showAreaIntersectionPopup(n,s.coordinate):a==="collector_zone"?v.showInfoPopup(n,s.coordinate,{title:"Zone Info",color:"#7c3aed"}):a==="parcel"?v.showInfoPopup(n,s.coordinate,{title:"Parcel Info",color:"#0ea5e9"}):v.showInfoPopup(n,s.coordinate,{title:"Feature Info",color:"#e11d48"})}}),v.onDrawnPoint(t=>{Ue||(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 Kr(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 Kt(),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 n=r.id||r.parcelid||r.parcel_id;if(console.log("[App] Parcel edit saved:",n,r),!n){console.warn("[App] No parcel ID found in updated properties — skipping local save");return}try{await cs(n,r),ge("Parcel updated locally")}catch(s){console.error("[App] Failed to save parcel update:",s),O("Failed to save parcel: "+s.message)}});const e=new ho;v.onDrawnPolygonSave(async(t,r)=>{console.log("[App] Drawn polygon attributes saved:",r);try{const n=e.writeGeometry(t.getGeometry(),{dataProjection:"EPSG:4326",featureProjection:"EPSG:3857"}),s=await ds(n,r);console.log("[App] New parcel inserted with id:",s.id),ge("New parcel saved (pending verification)")}catch(n){console.error("[App] Failed to save new parcel:",n),O("Failed to save parcel: "+n.message)}}),v.onFeatureModified(async t=>{const r=t.get("_externalImportId"),n=t.get("_clientUuid");if(!(r==null||!n))try{const s=e.writeGeometry(t.getGeometry(),{dataProjection:"EPSG:4326",featureProjection:"EPSG:3857"});await ss(n,s),console.log("[App] Imported feature geometry updated in staging:",n)}catch(s){console.warn("[App] Failed to persist imported-feature edit:",s),O("Could not save the edit locally: "+s.message)}});try{console.log("[App] Initializing database..."),await Vr(),console.log("[App] Database ready"),await al();const t=await yo();console.log("[App] Database status:",t),K()&&(await Da()||(console.warn("[App] API server unreachable — using local data only"),So("Server not responding — loading cached data."))),await gl(),v?.initEditBar(),il(),ll(),cl(),dl(),Ue&&Vt&&ie&&(ie.setVisible(!0),Vt.attachParcelsLayer(ie)),ul(),pl(),fl(),hl()}catch(t){console.error("[App] Database initialization failed:",t),O("Failed to initialize database. Please refresh the page.");return}Ki(),await Kt(),Wr(t=>{if(console.log("[App] Database change:",t),t.table==="locations"&&!t.local&&Kt(),t.table==="parcels"){const r=document.getElementById("local-data-stats");r&&!r.classList.contains("d-none")&&Ct()}}),jn(t=>{t?console.log("[App] Working offline - data will sync when back online"):(console.log("[App] Back online - syncing data..."),ml())}),Ml(),Cl(),Fl(),Al(),Ol(),Dl(),Rl(),Nl(),console.log("[App] Initialized successfully")}function Ki(){console.log("[initUI] Starting UI initialization..."),Il();const o=document.getElementById("export-btn");o&&o.addEventListener("click",Qi);const e=document.getElementById("local-data-btn");e&&e.addEventListener("click",()=>Ct());const t=document.getElementById("import-shp-btn"),r=document.getElementById("shp-file-input");t&&r&&(t.addEventListener("click",()=>r.click()),r.addEventListener("change",dr));const n=document.getElementById("import-geojson-btn"),s=document.getElementById("geojson-file-input");n&&s&&(n.addEventListener("click",()=>s.click()),s.addEventListener("change",ur));const a=document.getElementById("import-kml-btn"),i=document.getElementById("kml-file-input");a&&i&&(a.addEventListener("click",()=>i.click()),i.addEventListener("change",pr)),Ll();const l=document.getElementById("exportGeoJSON-btn");l&&l.addEventListener("click",el);const c=document.getElementById("status-btn");c&&c.addEventListener("click",tl);const d=document.getElementById("fit-btn");d&&d.addEventListener("click",()=>v?.fitToMarkers());const u=document.getElementById("dock-btn-measure-circle"),f=document.getElementById("dock-btn-measure-line"),p=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:_}=await Le(async()=>{const{openAnalysisModal:E}=await import("./analysis-modal-CwVHFT_a.js");return{openAnalysisModal:E}},__vite__mapDeps([7,3,8,9,1,2,10,11,12]));_(v)}catch(_){console.error("[App] Could not load the Analysis panel:",_),O("Analysis tools could not be loaded — check your connection and try again.")}}),console.log("[initUI] Buttons found:",{measureCircle:!!u,measureLine:!!f,measureArea:!!p,draw:!!h,clear:!!g});const y=[u,f,p,h],w=(_,E)=>{switch(console.log("[setMode] Changing mode from",z,"to",_),z=_,console.log("[setMode] currentMode is now:",z),y.forEach(T=>{T&&T.classList.toggle("active",T===E)}),ae?.deactivate(),_!=="draw"&&v?.setEditMode(!1),_!=="draw"&&v?.hideAddLocationPopup(),_){case"measureCircle":ae?.startCircleMeasure();break;case"measureLine":ae?.startLineMeasure();break;case"measureArea":ae?.startAreaMeasure();break;case"draw":v?.setEditMode(!0);break}};u&&u.addEventListener("click",()=>{console.log("[Button] Circle clicked, currentMode is:",z),z==="measureCircle"?w("none",null):w("measureCircle",u)}),f&&f.addEventListener("click",()=>{console.log("[Button] Line clicked, currentMode is:",z),z==="measureLine"?w("none",null):w("measureLine",f)}),p&&p.addEventListener("click",()=>{console.log("[Button] Area clicked, currentMode is:",z),z==="measureArea"?w("none",null):w("measureArea",p)}),h&&h.addEventListener("click",()=>{console.log("[Button] Draw clicked, currentMode is:",z),z==="draw"?w("none",null):w("draw",h)}),g&&g.addEventListener("click",()=>{if(ae?.clearMeasurements(),z.startsWith("measure"))switch(ae?.deactivate(),z){case"measureCircle":ae?.startCircleMeasure();break;case"measureLine":ae?.startLineMeasure();break;case"measureArea":ae?.startAreaMeasure();break}})}async function Kt(){try{console.log("[App] Loading locations...");const o=await In();console.log("[App] Locations loaded:",o),Yi(o),v&&(v.clearMarkers(),o.length>0&&(v.addMarkers(o),v.showMarkers(),console.log("[App] Added",o.length,"markers to map")));const e=document.getElementById("location-count");e&&(e.textContent=o.length)}catch(o){console.error("[App] Failed to load locations:",o)}}function Xi(o){const e=o.get("name"),t=o.get("description"),r=o.get("category"),n=o.get("lon")||o.get("longitude"),s=o.get("lat")||o.get("latitude");console.log("[App] Selected location:",{name:e,description:t,category:r,lon:n,lat:s})}function Yi(o){const e=document.getElementById("locations-list");if(!e)return;const t=document.getElementById("location-count-mobile");if(t&&(t.textContent=o.length),o.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=o.map(n=>{const s=r[n.category]||"📍";return`
|
||
<a href="#" class="list-group-item list-group-item-action location-item py-2"
|
||
data-id="${n.id}" data-lon="${n.longitude}" data-lat="${n.latitude}">
|
||
<div class="d-flex w-100 justify-content-between align-items-start">
|
||
<div>
|
||
<h6 class="mb-1">${s} ${U(n.name)}</h6>
|
||
<small class="text-muted font-monospace">${n.latitude.toFixed(5)}, ${n.longitude.toFixed(5)}</small>
|
||
</div>
|
||
<span class="badge badge-${n.category}">${n.category}</span>
|
||
</div>
|
||
${n.description?`<small class="text-secondary d-block mt-1">${U(n.description)}</small>`:""}
|
||
</a>
|
||
`}).join(""),e.querySelectorAll(".location-item").forEach(n=>{n.addEventListener("click",s=>{s.preventDefault();const a=parseFloat(n.dataset.lon),i=parseFloat(n.dataset.lat),l=parseInt(n.dataset.id);v?.showMarkers(),v?.zoomTo(a,i,14),v?.selectMarker(l)})})}async function Ct(){const o=document.getElementById("local-data-stats"),e=document.getElementById("local-data-tbody"),t=document.getElementById("clear-all-cached-btn");if(!(!o||!e)){try{const r=await bs();e.innerHTML=r.map(n=>{const a=Fn(n.name)?`<button type="button" class="btn btn-sm btn-link text-danger p-0 table-clear-btn"
|
||
data-table="${U(n.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="${U(n.name)}">${U(n.name)}</a>
|
||
</td>
|
||
<td class="text-end"><span class="badge bg-secondary">${n.count}</span></td>
|
||
<td class="text-end pe-3">${a}</td>
|
||
</tr>
|
||
`}).join(""),o.classList.remove("d-none"),e.querySelectorAll(".table-name-link").forEach(n=>{n.addEventListener("click",s=>{s.preventDefault(),Zi(n.dataset.table)})}),e.querySelectorAll(".table-clear-btn").forEach(n=>{n.addEventListener("click",async s=>{s.preventDefault();const a=n.dataset.table;if(confirm(`Clear local cache for "${a}"?
|
||
|
||
The data will be re-downloaded from the server on the next app start.`))try{const i=await On(a);ge(`Cleared ${i} row${i===1?"":"s"} from "${a}". It will re-download on next start.`),await Ct()}catch(i){console.error("[App] Per-table clear failed:",i),O(`Could not clear "${a}": ${i.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>',o.classList.remove("d-none")}t&&!t._wired&&(t._wired=!0,t.addEventListener("click",Ji))}}async function Ji(){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 o=await bo(),e=o.reduce((t,r)=>t+r.count,0);ge(`Cleared ${e} row${e===1?"":"s"} across ${o.length} table${o.length===1?"":"s"}.`),await Ct(),confirm("Reload the app now to re-download the layers fresh from the server?")&&window.location.reload()}catch(o){console.error("[App] Clear-all failed:",o),O("Failed to clear cached layers: "+o.message)}}async function Zi(o){const e=document.getElementById("tableContentModalLabel"),t=document.getElementById("table-content-body"),r=document.getElementById("table-content-info");e.textContent=`Table: ${o}`,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 lt(document.getElementById("tableContentModal")).show();try{const{columns:s,rows:a}=await ws(o);if(a.length===0){t.innerHTML='<div class="text-center text-muted py-4">Table is empty</div>',r.textContent="0 rows";return}const i=s.map(c=>`<th class="text-nowrap">${U(c)}</th>`).join(""),l=a.map(c=>`<tr>${s.map(u=>{let f=c[u];if(f==null)return'<td class="text-muted fst-italic">NULL</td>';f=String(f);const p=f.length>120?f.substring(0,120)+"...":f;return`<td>${U(p)}</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>${i}</tr>
|
||
</thead>
|
||
<tbody>${l}</tbody>
|
||
</table>
|
||
</div>
|
||
`,r.textContent=`${a.length}${a.length>=200?"+":""} row(s), ${s.length} column(s)`}catch(s){console.error("[App] Failed to load table content:",s),t.innerHTML=`<div class="text-danger text-center py-4">Failed to load: ${U(s.message)}</div>`}}async function Qi(){try{await ms("lupmis-backup.sqlite3"),ge("Database exported successfully")}catch(o){console.error("[App] Export failed:",o),O("Export failed: "+o.message)}}async function el(){try{const o=await ys(),e=new Blob([JSON.stringify(o,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 ${o.features.length} location(s)`)}catch(o){console.error("[App] GeoJSON Export failed:",o),O("GeoJSON Export failed: "+o.message)}}async function tl(){try{const o=await yo(),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 ${o.ready?"bg-success":"bg-danger"}">${o.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>${o.databasePath||"N/A"}</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td class="fw-semibold">Tables:</td>
|
||
<td>${o.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">${o.locationCount}</span></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
`),new lt(document.getElementById("statusModal")).show()}catch(o){console.error("[App] Failed to get status:",o),O("Failed to get status")}}function ar(o){return o.replace(/^\(+/,"").replace(/\)+$/,"").split(",").map(e=>{const[t,r]=e.trim().split(/\s+/).map(Number);return[t,r]})}function ol(o){return{type:"Polygon",coordinates:o.trim().replace(/^POLYGON\s*\(\s*/i,"").replace(/\s*\)$/,"").split("),(").map(ar)}}function nl(o){return{type:"MultiPolygon",coordinates:o.trim().replace(/^MULTIPOLYGON\s*\(\s*/i,"").replace(/\s*\)$/,"").split(")),((").map(n=>n.replace(/^\(+/,"").replace(/\)+$/,"").split("),(").map(ar))}}function ct(o){if(!o)return null;const e=o.trim().toUpperCase();return e.startsWith("MULTIPOLYGON")?nl(o):e.startsWith("POLYGON")?ol(o):(console.warn("[App] Unsupported WKT type:",e.substring(0,30)),null)}function rl(o){if(!o?.success||!o?.data?.boundary)return console.warn("[App] API response missing success or boundary data"),null;const{boundary:e,districtid:t,district_name:r}=o.data,n=ct(e);return{type:"FeatureCollection",features:[{type:"Feature",properties:{districtid:t,district_name:r},geometry:n}]}}function cn(o){if(!Array.isArray(o)||o.length===0)return null;const e=[];for(const t of o){const r=t.polygon||t.boundary,n=ct(r);if(!n)continue;const s={_layerType:"collector_zone"};for(const[a,i]of Object.entries(t))a==="polygon"||a==="boundary"||(s[a]=i);e.push({type:"Feature",properties:s,geometry:n})}return e.length===0?null:{type:"FeatureCollection",features:e}}const uo="lupmis-last-district";function sl(o){return o==null||String(o).length===0?"district_boundary":`district_boundary_${o}`}async function al(){const e=Ke()?.district_id;if(e==null||String(e).length===0)return;const t=String(e);let r=null;try{r=localStorage.getItem(uo)}catch{}if(r!==null&&r!==t){console.warn(`[App] District changed ${r} → ${t}; clearing cached layers.`);try{await bo(),So("Your district was updated — refreshing map data.")}catch(n){console.error("[App] Failed to clear caches on district change:",n)}}try{localStorage.setItem(uo,t)}catch{}}async function il(){const o=Ke()?.district_id??null,e=sl(o),t=1,r={strokeColor:"#e11d48",strokeWidth:2.5,fillColor:"rgba(225,29,72,0.08)",typeDescription:"Vector / Polygon"},n=v?.getLayerGroup(t)||null;function s(i){if(!i)return;const l=i.getLayers(),c=[];l.forEach(d=>{d.get("title")==="District Boundary"&&c.push(d)}),c.forEach(d=>l.remove(d))}function a(i){if(!i||!v)return;const l=i.getSource().getExtent();l&&l[0]!==1/0&&v.getMap().getView().fit(l,{padding:[40,40,40,40],duration:600})}try{const i=K()&&ke(),l=await Mn(e);if(l){console.log("[App] District boundary loaded from local cache");const c=v?.addGeoJSONLayer(l,"District Boundary",r,n);i||a(c)}if(i){console.log("[App] Fetching district boundary from API...");const c=await Na(),d=rl(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 Pn(e,d),l&&s(n||v?.getOverlayGroup());const u=v?.addGeoJSONLayer(d,"District Boundary",r,n);a(u),console.log("[App] District boundary loaded from API")}else l||console.log("[App] District boundary not available — offline and no local cache")}catch(i){console.error("[App] Failed to load district boundary:",i)}}function dn(o){if(!Array.isArray(o)||o.length===0)return null;const e=[];for(const t of o){const r=t.polygon||t.geometry_wkt||t.geom,n=ct(r);n&&e.push({type:"Feature",properties:{_layerType:"upn_grid",upn_prefix:t.upn_prefix??null},geometry:n})}return e.length===0?null:{type:"FeatureCollection",features:e}}async function ll(){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:[]},n=v?.addGeoJSONLayer(r,"UPN Grid",e,t);if(!n){console.warn("[App] Could not create UPN Grid layer");return}n.setVisible(!1);const s=7;n.setStyle((i,l)=>{const c=[new M({stroke:new I({color:"rgba(255,255,255,0.95)",width:3.5})}),new M({stroke:new I({color:"#5b21b6",width:1.5,lineDash:[7,4]}),fill:new A({color:"rgba(124,58,237,0.04)"})})];if(l<=s){const d=i.get("upn_prefix");d!=null&&String(d).length>0&&c.push(new M({text:new We({text:String(d),font:"600 12px Arial, sans-serif",fill:new A({color:"#3b0764"}),stroke:new I({color:"rgba(255,255,255,0.95)",width:3}),overflow:!0})}))}return c}),n.on("change:visible",()=>{n.getVisible()&&n.getSource().getFeatures().length===0&&O("No UPN grid available locally. Connect to the internet to download it.")});function a(i){const l=new le().readFeatures(i,{featureProjection:"EPSG:3857"});n.getSource().clear(),n.getSource().addFeatures(l)}try{const l=Ke()?.district_id??null,c=await Qr(l);if(c){const p=dn(c);p&&a(p),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 qa();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 Zr(u,l);const f=dn(u);f&&a(f),console.log("[App] UPN grid loaded:",f?.features.length??0,"cells rendered")}catch(i){console.error("[App] Failed to load UPN grid:",i)}}async function cl(){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:[]},n=v?.addGeoJSONLayer(r,"Zones",e,t);if(!n){console.warn("[App] Could not create Zones layer");return}n.setVisible(!1),n.on("change:visible",()=>{n.getVisible()&&n.getSource().getFeatures().length===0&&O("No collector zones available locally. Connect to the internet to download zone data.")});function s(a){const i=new le().readFeatures(a,{featureProjection:"EPSG:3857"});n.getSource().clear(),n.getSource().addFeatures(i)}try{const a=await Jr();if(a){const i=cn(a);i&&(console.log("[App] Collector zones loaded from local cache:",i.features.length,"zones"),s(i))}if(K()&&ke()){console.log("[App] Fetching collector zones from API...");const i=await Ba();if(!i?.success||!Array.isArray(i?.data)){console.warn("[App] getCollectorZones API response invalid:",i);return}const l=i.data;console.log("[App] Collector zones from API:",l.length,"entries"),await Yr(l);const c=cn(l);if(!c){console.warn("[App] Could not convert zones to GeoJSON");return}s(c),console.log("[App] Collector zones updated from API:",c.features.length,"zones")}else a||console.log("[App] Collector zones not available — offline and no local cache")}catch(a){console.error("[App] Failed to load collector zones:",a)}}function un(o){if(!Array.isArray(o)||o.length===0)return null;const e=new Set,t=[];for(const r of o){const n=r.id||r.parcelid||r.parcel_id;if(n!=null){if(e.has(n))continue;e.add(n)}let s=null;if(r.geom&&r.geom.type&&r.geom.coordinates)s={type:r.geom.type,coordinates:r.geom.coordinates};else if(r.sp_boundary&&r.sp_boundary.type&&r.sp_boundary.coordinates)s={type:r.sp_boundary.type,coordinates:r.sp_boundary.coordinates};else{const l=r.boundary||r.geometry_wkt||r.polygon||r.wkt;s=ct(l)}if(!s)continue;const a=new Set(["polygon","boundary","geom","geometry_wkt","wkt","textboundary","sp_boundary","fetched_at"]),i={_layerType:"parcel"};for(const[l,c]of Object.entries(r))a.has(l)||(i[l]=c);t.push({type:"Feature",properties:i,geometry:s})}return t.length===0?null:{type:"FeatureCollection",features:t}}async function dl(){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(ie=v?.addGeoJSONLayer(r,"Parcels",e,t),!ie){console.warn("[App] Could not create Parcels layer");return}ie.setVisible(!1),ie.on("change:visible",()=>{ie.getVisible()&&ie.getSource().getFeatures().length===0&&O("No parcels available locally. Connect to the internet to download parcel data.")});function n(s){const a=new le().readFeatures(s,{featureProjection:"EPSG:3857"});ie.getSource().clear(),ie.getSource().addFeatures(a)}try{const s=await ls();if(s){const a=un(s);a&&(console.log("[App] Parcels loaded from local cache:",a.features.length,"parcels"),n(a))}if(K()&&ke()){console.log("[App] Fetching parcels from API...");const a=await Ga();if(!a?.success||!Array.isArray(a?.data)){console.warn("[App] getDistrictParcels API response invalid:",a);return}const i=a.data;console.log("[App] Parcels from API:",i.length,"entries"),i.length>0&&console.log("[App] First parcel keys:",Object.keys(i[0])),await is(i);const l=un(i);if(!l){console.warn("[App] Could not convert parcels to GeoJSON");return}n(l),console.log("[App] Parcels updated from API:",l.features.length,"parcels")}else s||console.log("[App] Parcels not available — offline and no local cache")}catch(s){console.error("[App] Failed to load parcels:",s)}}function pn(o){if(!Array.isArray(o)||o.length===0)return null;const e=["polygon","boundary","geom","wkt","footprint"],t=[];for(const r of o){const n=r.polygon||r.boundary||r.geom||r.wkt||r.footprint;let s;if(typeof n=="object"&&n!==null&&n.type?s=n:s=ct(n),!s)continue;const a={_layerType:"building_footprint"};for(const[i,l]of Object.entries(r))e.includes(i)||typeof l=="object"&&l!==null||(a[i]=l);t.push({type:"Feature",properties:a,geometry:s})}return t.length===0?null:{type:"FeatureCollection",features:t}}async function ul(){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:[]},n=v?.addGeoJSONLayer(r,"Building footprints",e,t);if(!n){console.warn("[App] Could not create Building footprints layer");return}n.setVisible(!1),n.on("change:visible",()=>{n.getVisible()&&n.getSource().getFeatures().length===0&&O("No building footprints available locally. Connect to the internet to download footprint data.")});function s(a){const i=new le().readFeatures(a,{featureProjection:"EPSG:3857"});n.getSource().clear(),n.getSource().addFeatures(i)}try{const a=await ps();if(a){const i=pn(a);i&&(console.log("[App] Building footprints loaded from local cache:",i.features.length,"footprints"),s(i))}if(K()&&ke()){console.log("[App] Fetching building footprints from API...");const i=await Ua();if(!i?.success||!Array.isArray(i?.data)){console.warn("[App] getBuildingFootprints API response invalid:",i);return}const l=i.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 us(l);const c=pn(l);if(!c){console.warn("[App] Could not convert building footprints to GeoJSON");return}s(c),console.log("[App] Building footprints updated from API:",c.features.length,"footprints")}else a||console.log("[App] Building footprints not available — offline and no local cache")}catch(a){console.error("[App] Failed to load building footprints:",a)}}function ir(o,e){if(!Array.isArray(o)||o.length===0)return null;const t=new ho,r=new le,n=["geom","geometry","wkt","polygon","boundary","road","line"],s=[];for(const a of o){const i=a.geom||a.geometry||a.wkt||a.polygon||a.boundary||a.road||a.line;if(!i)continue;let l;try{if(typeof i=="object"&&i!==null&&i.type){s.push({type:"Feature",properties:fn(a,n,e),geometry:i});continue}l=t.readGeometry(i)}catch(d){console.warn(`[App] Could not parse WKT for ${e}:`,d,i?.toString().slice(0,60));continue}const c=JSON.parse(r.writeGeometry(l));s.push({type:"Feature",properties:fn(a,n,e),geometry:c})}return s.length===0?null:{type:"FeatureCollection",features:s}}function fn(o,e,t){const r={_layerType:t};for(const[n,s]of Object.entries(o))e.includes(n)||typeof s=="object"&&s!==null||(r[n]=s);return r}async function pl(){const o={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",o,e);if(!r){console.warn("[App] Could not create Contours hillshade layer");return}r.setVisible(!1);const n=10;if(r.setStyle((s,a)=>{const i=[new M({stroke:new I({color:"#78716c",width:.8})})];if(a<=n){const l=s.get("elev");l!=null&&l!==""&&i.push(new M({text:new We({text:String(l),font:"600 11px Arial, sans-serif",fill:new A({color:"#57534e"}),stroke:new I({color:"rgba(255,255,255,0.95)",width:3}),overflow:!0})}))}return i}),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 s=await ja(),a=Array.isArray(s)?s:Array.isArray(s?.data)?s.data:null;if(!a){console.warn("[App] getContoursHillshade API response invalid:",s);return}console.log("[App] Contours hillshade from API:",a.length,"rows"),a.length>0&&console.log("[App] First row keys:",Object.keys(a[0]));const i=ir(a,"contours_hillshade");if(!i){console.warn("[App] Could not convert contours to GeoJSON");return}const l=new le().readFeatures(i,{featureProjection:"EPSG:3857"});r.getSource().clear(),r.getSource().addFeatures(l),console.log("[App] Contours hillshade loaded:",l.length,"features")}catch(s){console.error("[App] Failed to load contours_hillshade:",s)}}async function fl(){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:[]},n=v?.addGeoJSONLayer(r,"OSM_roads",e,t);if(!n){console.warn("[App] Could not create OSM_roads layer");return}n.setVisible(!1),n.on("change:visible",()=>{n.getVisible()&&n.getSource().getFeatures().length===0&&O("No OSM roads available locally. Connect to the internet to download them.")});function s(a){const i=ir(a,"osm_road");if(!i)return console.warn("[App] Could not convert OSM roads to GeoJSON"),0;const l=new le().readFeatures(i,{featureProjection:"EPSG:3857"});return n.getSource().clear(),n.getSource().addFeatures(l),l.length}try{const a=await hs();if(a){const i=s(a);console.log("[App] OSM_roads loaded from local cache:",i,"features")}if(K()&&ke()){console.log("[App] Fetching OSM_roads from API...");const i=await za();if(!i?.success||!Array.isArray(i?.data)){console.warn("[App] getOSMRoads API response invalid:",i);return}const l=i.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 fs(l);const c=s(l);console.log("[App] OSM_roads updated from API:",c,"features")}else a||console.log("[App] OSM_roads not available — offline and no local cache")}catch(a){console.error("[App] Failed to load OSM_roads:",a)}}function hl(){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 gl(){const o="layer_categories";function e(t){const r=[...t].sort((n,s)=>s.id-n.id);for(const n of r)v?.addLayerGroup(n.id,n.name,n.description||"");console.log("[App] Created",t.length,"layer groups on map")}try{const t=await Mn(o);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 $a();if(!r?.success||!Array.isArray(r?.data)){console.warn("[App] getLayers API response invalid:",r);return}const n=r.data;if(console.log("[App] Layer categories from API:",n.length,"entries"),await Pn(o,n),t){const s=v?.getOverlayGroup()?.getLayers();if(s){const a=[];s.forEach(i=>{i.get("layerId")!==void 0&&a.push(i)}),a.forEach(i=>s.remove(i))}}e(n),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 ml(){if(!K()){console.log("[App] Cannot sync - offline");return}console.log("[App] Sync placeholder - implement based on your backend")}const pe=[],yl={strokeColor:"#e11d48",strokeWidth:2,fillColor:"rgba(225,29,72,0.12)"};function oe(o){Ft("error",o);const e=document.getElementById("file-import-alert");e&&(e.querySelector(".message-text").textContent=o,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),8e3))}function Eo(o,e,t){const r=Array.isArray(o)?o:[o];let n=0;for(const a of r){if(!a||a.type!=="FeatureCollection"||!a.features?.length)continue;const i=a.fileName?a.fileName.replace(/\.[^/.]+$/,""):e,l=v?.addGeoJSONLayer(a,i,yl);l&&(l.set("removable",!0),l.set("typeTag","GEO"),pe.push(l),n+=a.features.length,_l(a,i,l).catch(c=>console.warn("[FileImport] Staging failed (layer remains view-only):",c)))}if(n===0){ue(),oe("No features found in the file.");return}console.log(`[${t}] Added ${n} feature(s) from ${r.length} layer(s)`);const s=pe[pe.length-1];if(s){const a=s.getSource().getExtent();v?.getMap().getView().fit(a,{padding:[50,50,50,50],maxZoom:18})}xo()}const bl=new ho;function wl(o){return bl.writeGeometry(o,{dataProjection:"EPSG:4326",featureProjection:"EPSG:3857"})}async function _l(o,e,t){const r=o?.features?.length??0;if(r===0){ue();return}const{id:n}=await es({filename:e||"imported dataset",targetType:"other",featureCount:r});t.set("_externalImportId",n);const a=t.getSource().getFeatures().map(i=>{const l=i.getGeometry(),c=vl();return i.set("_externalImportId",n),i.set("_clientUuid",c),{client_uuid:c,geometry_wkt:l?wl(l):"",properties:El(i.getProperties())}});await ts(n,a),t.getSource().on("removefeature",async i=>{const l=i.feature,c=l?.get("_clientUuid"),d=l?.get("_externalImportId");if(!(!c||d==null))try{await as(c),console.log("[FileImport] Removed feature from staging:",c)}catch(u){console.warn("[FileImport] Failed to remove staging row:",u)}}),ue(),fi({importId:n,filename:e,fc:o,onResult:async i=>{try{await xl(n,t,i)}catch(l){console.error("[FileImport] Failed to apply mapping result:",l),O("Could not save the import mapping: "+l.message)}}})}function vl(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,o=>{const e=Math.random()*16|0;return(o==="x"?e:e&3|8).toString(16)})}function El(o){const e={};for(const[t,r]of Object.entries(o||{}))t!=="geometry"&&(e[t]=r);return e}async function xl(o,e,t){if(!t||t.action==="cancel"){e?.set("_externalImportStatus","other"),He(e);return}const{action:r,targetType:n,mapping:s}=t;if(!n||n==="other"){await kt(o,{targetType:"other",mapping:null,status:"other"}),e?.set("_externalImportStatus","other"),He(e);return}await rs(o,a=>li(a,s)),await kt(o,{targetType:n,mapping:s,status:"mapped"}),e?.set("_externalImportStatus","mapped"),e?.set("_externalImportTargetType",n),He(e),r==="upload"&&await lr(o,e)}async function lr(o,e){e?.set("_externalImportStatus","uploading"),He(e);try{await kt(o,{status:"uploading"});const t=await os(o),r=await ns(o),s={user_id_upload:Ke()?.user_id??null,import:{client_import_id:t.client_import_id,filename:t.filename,feature_count:r.length},features:r.map(a=>({client_uuid:a.client_uuid,geom:a.geometry_wkt,props:a.properties}))};console.log("[Upload]",{endpoint:`upload_${t.target_type}.php (not yet available on the server)`,target_type:t.target_type,body:s}),await kt(o,{status:"mapped",lastUploadedAt:new Date().toISOString()}),e?.set("_externalImportStatus","mapped"),He(e),So("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"),He(e),O("Upload preparation failed: "+t.message)}}window.addEventListener("lupmis:export-gis",o=>{Wi(o.detail||{})});window.addEventListener("lupmis:analyse-features",async o=>{if(!v)return;const e=o.detail||{},t=[...e.parcelFeatures||[],...e.zoneFeatures||[],...Object.values(e.otherByLayer||{}).flat()];try{const{openAnalysisModal:r}=await Le(async()=>{const{openAnalysisModal:n}=await import("./analysis-modal-CwVHFT_a.js");return{openAnalysisModal:n}},__vite__mapDeps([7,3,8,9,1,2,10,11,12]));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",o=>{const{importId:e,status:t,layer:r}=o.detail||{};t==="mapped"&&lr(e,r).catch(n=>console.error("[FileImport] runUpload failed:",n))});function He(o){if(!o||!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 xo(){const o=document.getElementById("imported-layers-info");if(!o)return;if(pe.length===0){o.innerHTML="",o.classList.add("d-none");return}o.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=o.querySelector("#imported-layers-list");pe.forEach((t,r)=>{const n=document.createElement("li");n.className="list-group-item d-flex justify-content-between align-items-center py-2",n.innerHTML=`<small class="text-truncate me-2">${U(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(n)}),o.classList.remove("d-none"),o.querySelectorAll("[data-remove-idx]").forEach(t=>{t.addEventListener("click",()=>{Sl(Number(t.dataset.removeIdx))})}),o.querySelector("#remove-imported-layers")?.addEventListener("click",()=>{Tl()})}function Sl(o){if(o<0||o>=pe.length)return;const e=pe[o],t=v?.getOverlayGroup();t&&t.getLayers().remove(e),pe.splice(o,1),xo(),console.log("[FileImport] Removed layer:",e.get("title"))}function Tl(){const o=v?.getOverlayGroup();if(o)for(const e of pe)o.getLayers().remove(e);pe.length=0,xo(),console.log("[FileImport] All imported layers removed")}function cr(o){const e={};for(const t of o){const r=t.name.split(".").pop().toLowerCase();e[r]=t}return e}async function dr(o){const e=o.target.files;if(!e||e.length===0)return;const t=200*1024*1024,r=Array.from(e).reduce((n,s)=>n+s.size,0);if(r>t){const n=(r/1048576).toFixed(0);oe(`Files too large (${n} MB total). Maximum supported size is 200 MB.`),o.target.value="";return}vo(e[0]?.name||"shapefile");try{let n,s;const a=cr(e);if(a.zip){const i=a.zip;s=i.name.replace(/\.zip$/i,""),console.log("[ShpImport] Parsing zip",i.name,"("+(i.size/1024).toFixed(1)+" KB)"),n=await(await an())(await i.arrayBuffer())}else if(a.shp){s=a.shp.name.replace(/\.shp$/i,"");const l=["dbf","shx","prj"].filter(u=>!a[u]);if(l.length>0){ue(),oe("Missing required file(s): "+l.map(u=>"."+u).join(", ")+". Please select .shp, .dbf, .shx and .prj together."),o.target.value="";return}const c={};c.shp=await a.shp.arrayBuffer(),c.dbf=await a.dbf.arrayBuffer(),c.prj=await new Response(a.prj).text(),a.cpg&&(c.cpg=await new Response(a.cpg).text()),console.log("[ShpImport] Parsing loose files:",Object.keys(a).map(u=>"."+u).join(", "),"("+(a.shp.size/1024).toFixed(1)+" KB .shp)"),n=await(await an())(c)}else{ue(),oe("Please select a .zip or at least a .shp file."),o.target.value="";return}Eo(n,s,"ShpImport")}catch(n){ue(),console.error("[ShpImport] Failed:",n),oe("Failed to parse shapefile: "+n.message)}o.target.value=""}async function ur(o){const e=o.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.`),o.target.value="";return}vo(e.name);try{const r=await e.text();console.log("[GeoJSONImport] Parsing",e.name,"("+(e.size/1024).toFixed(1)+" KB)");const n=JSON.parse(r);let s;if(n.type==="FeatureCollection")s=n;else if(n.type==="Feature")s={type:"FeatureCollection",features:[n]};else if(n.type&&n.coordinates)s={type:"FeatureCollection",features:[{type:"Feature",geometry:n,properties:{}}]};else{ue(),oe("The file does not contain valid GeoJSON."),o.target.value="";return}const a=e.name.replace(/\.(geo)?json$/i,"");Eo(s,a,"GeoJSONImport")}catch(r){ue(),console.error("[GeoJSONImport] Failed:",r);const n=(e.size/(1024*1024)).toFixed(1);oe(`Failed to import "${e.name}" (${n} MB): ${r.message}`)}o.target.value=""}async function pr(o){const e=o.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.`),o.target.value="";return}vo(e.name);try{const r=await e.text();console.log("[KMLImport] Parsing",e.name,"("+(e.size/1024).toFixed(1)+" KB)");const s=new gn({extractStyles:!1}).readFeatures(r,{featureProjection:"EPSG:3857"});if(!s||s.length===0){ue(),oe("No features found in the KML file."),o.target.value="";return}const a=new le,i=JSON.parse(a.writeFeatures(s,{featureProjection:"EPSG:3857",dataProjection:"EPSG:4326"})),l=e.name.replace(/\.kml$/i,"");Eo(i,l,"KMLImport")}catch(r){ue(),console.error("[KMLImport] Failed:",r);const n=(e.size/(1024*1024)).toFixed(1);oe(`Failed to import "${e.name}" (${n} MB): ${r.message}`)}o.target.value=""}function Ll(){const o=document.querySelector(".map-container");if(!o)return;let e=0;o.addEventListener("dragenter",t=>{t.preventDefault(),e++,o.classList.add("drag-over")}),o.addEventListener("dragover",t=>{t.preventDefault()}),o.addEventListener("dragleave",t=>{t.preventDefault(),e--,e<=0&&(e=0,o.classList.remove("drag-over"))}),o.addEventListener("drop",t=>{t.preventDefault(),e=0,o.classList.remove("drag-over");const r=t.dataTransfer?.files;if(!r||r.length===0)return;const n=cr(r),s=Object.keys(n);if(n.zip||n.shp){const a={target:{files:r,value:""}};Object.defineProperty(a.target,"value",{writable:!0}),dr(a)}else if(n.geojson||n.json){const i={target:{files:[n.geojson||n.json],value:""}};Object.defineProperty(i.target,"value",{writable:!0}),ur(i)}else if(n.kml){const a={target:{files:[n.kml],value:""}};Object.defineProperty(a.target,"value",{writable:!0}),pr(a)}else oe("Unsupported file type(s): "+s.map(a=>"."+a).join(", ")+". Drop .zip, .shp, .geojson, .json, or .kml files.")}),console.log("[FileImport] Map drop zone initialised")}function U(o){const e=document.createElement("div");return e.textContent=o,e.innerHTML}const kl=50,hn={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 Ft(o,e){const t=hn[o]||hn.info;(o==="error"?console.error:o==="warning"?console.warn:console.log)("[App]",e);const n=document.getElementById("message-log");if(!n)return;const s=n.querySelector(".text-muted");s&&s.remove();const a=document.createElement("div");a.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(a.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>${U(e)}</small></div><small class="text-muted flex-shrink-0 ms-1">${l}</small></div>`,n.prepend(a);n.children.length>kl;)n.lastElementChild.remove()}function Il(){const o=document.getElementById("clear-message-log");o&&o.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 po=null;function fr(){const o=localStorage.getItem("coord-format");return o==="utm"||o==="both"?o:"latlon"}function hr(){const o=document.getElementById("gps-coords"),e=document.getElementById("gps-utm");if(!o||!po)return;const{lat:t,lon:r}=po,n=`${Yo(t)}, ${Yo(r)}`,s=Xa(t,r),a=fr();a==="utm"?(o.textContent=s,e&&e.classList.add("d-none")):a==="both"?(o.textContent=n,e&&(e.textContent=s,e.classList.remove("d-none"))):(o.textContent=n,e&&e.classList.add("d-none"));const i=document.getElementById("gps-readout");i&&(i.title=`Lat/Lon: ${n}
|
||
UTM: ${s}`)}function Pl(){const o=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",s=>{po=s,hr(),t&&(t.textContent=Ja(s.accuracy)),r&&(r.textContent=`${s.satellites!=null?s.satellites:"—"} sat`),o&&(o.classList.add("active"),o.classList.remove("quality-good","quality-fair","quality-poor"),o.classList.add("quality-"+Za(s.accuracy))),v?.showCurrentPosition(s.lon,s.lat,s.accuracy)}),ve.on("point",s=>{v?.appendTrailPoint(s.point.lon,s.point.lat)}),ve.on("error",s=>{console.warn("[GPS]",s?.message||s),s&&s.code===1&&O("Location permission denied. Enable location access to use GPS.")}),v.onLocateMe(async()=>{try{const s=await ve.getCurrentPosition();v.centerOn(s.lon,s.lat,16)}catch(s){O("Could not get your location: "+(s?.message||s))}}),v.onToggleRecording(async s=>{if(s)try{await No,v.startTrailRender(),v.setRecordingState(!0),o?.classList.add("recording"),await ve.startRecording({name:`Trail ${new Date().toLocaleString()}`}),ge("GPS trail recording started")}catch(a){v.setRecordingState(!1),o?.classList.remove("recording"),O("Could not start recording: "+(a?.message||a))}else try{const a=await ve.stopRecording();if(v.setRecordingState(!1),o?.classList.remove("recording"),a){const i=`Trail saved: ${a.pointCount} points, ${Ya(a.distanceM)}`+(a.synced?" — synced":" — will sync when online");ge(i)}}catch(a){O("Error stopping recording: "+(a?.message||a))}});const n=async()=>{if(K())try{await No;const s=await ve.syncPending();s.pushed&&console.log(`[GPS] Synced ${s.pushed} pending trail(s)`)}catch(s){console.warn("[GPS] pending-sync error",s)}};n(),jn(s=>{s||n()})}function O(o){Ft("error",o);const e=document.getElementById("error-message");e&&(e.querySelector(".message-text").textContent=o,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),5e3))}function ge(o){Ft("success",o);const e=document.getElementById("success-message");e&&(e.querySelector(".message-text").textContent=o,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),3e3))}function So(o){Ft("warning",o);const e=document.getElementById("warning-message");e&&(e.querySelector(".message-text").textContent=o,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),5e3))}function Ml(){const o=document.getElementById("fieldwork-mode-toggle");if(!o)return;localStorage.getItem("fieldwork-mode")==="true"&&(document.documentElement.classList.add("fieldwork-mode"),o.checked=!0),o.addEventListener("change",()=>{document.documentElement.classList.toggle("fieldwork-mode",o.checked),localStorage.setItem("fieldwork-mode",o.checked),console.log("[Settings] Fieldwork mode",o.checked?"ON":"OFF")})}function Al(){const o=document.getElementById("dark-mode-toggle");if(!o)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"&&(o.checked=!0,e(!0)),o.addEventListener("change",()=>{e(o.checked),localStorage.setItem("dark-mode",o.checked),console.log("[Settings] Dark mode",o.checked?"ON":"OFF")})}function Cl(){const o=document.getElementById("measurement-system-toggle"),e=document.getElementById("measurement-system-label");if(!o)return;function t(){e&&(e.textContent=o.checked?"Imperial":"Metric")}const r=localStorage.getItem("measurement-system");r==="imperial"&&(o.checked=!0),t(),v?.setScaleBarUnits(r||"metric"),o.addEventListener("change",()=>{const n=o.checked?"imperial":"metric";localStorage.setItem("measurement-system",n),t(),v?.setScaleBarUnits(n),console.log("[Settings] Measurement system:",n)})}function Fl(){const o=document.getElementById("coord-format-select");o&&(o.value=fr(),o.addEventListener("change",()=>{const e=o.value==="utm"||o.value==="both"?o.value:"latlon";localStorage.setItem("coord-format",e),hr(),console.log("[Settings] GPS coordinate format:",e)}))}function Ol(){const o=document.getElementById("default-basemap-select");if(!o)return;const e=localStorage.getItem("default-basemap")||"topo";o.value=e,o.addEventListener("change",()=>{const t=o.value;localStorage.setItem("default-basemap",t),v?.setBaseMap(t),console.log("[Settings] Default base map:",t)}),v?.getMap()?.on("basemapchange",t=>{if(t?.key&&o.value!==t.key){o.value=t.key;try{localStorage.setItem("default-basemap",t.key)}catch{}}})}function Dl(){const o=document.getElementById("tile-cache-stats"),e=document.getElementById("clear-tiles-btn"),t=document.getElementById("offcanvasBottom");if(!o||!e||!t)return;function r(a){return a?a<1024*1024?(a/1024).toFixed(0)+" KB":a<1024*1024*1024?(a/(1024*1024)).toFixed(1)+" MB":(a/(1024*1024*1024)).toFixed(2)+" GB":"0 KB"}let n=null;async function s(){if(n)return n;const a=!!navigator.serviceWorker?.controller;return o.innerHTML=a?'<div class="text-muted fst-italic">Loading…</div>':'<div class="text-muted fst-italic">Initialising service worker…</div>',n=(async()=>{try{const i=await ba();if(!i){o.innerHTML=`
|
||
<div class="text-muted fst-italic">
|
||
Tile cache stats unavailable. Try reloading the page if this persists.
|
||
</div>`;return}const l=i.totals,c=i.byProvider.filter(f=>f.count>0).map(f=>`
|
||
<tr>
|
||
<td>${U(f.label)}</td>
|
||
<td class="text-end">${f.count.toLocaleString()} / ${f.limit.toLocaleString()}</td>
|
||
<td class="text-end">${r(f.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="${U(f.key)}" data-label="${U(f.label)}"
|
||
title="Clear ${U(f.label)} tiles only">
|
||
<i class="bi bi-trash3"></i>
|
||
</button>
|
||
</td>
|
||
</tr>`).join("");let d="";const u=await va();if(u&&u.quota>0){const f=(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 (${f}%)
|
||
</div>`}if(l.count===0){o.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}o.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,o.querySelectorAll(".provider-clear-btn").forEach(f=>{f.addEventListener("click",async p=>{p.preventDefault();const h=f.dataset.cache,g=f.dataset.label||h;if(!confirm(`Clear cached "${g}" tiles?
|
||
|
||
Other providers are not affected. The tiles will re-download as you browse online.`))return;f.disabled=!0,await _a(h)?console.log(`[Settings] Cleared tile cache for ${g}`):console.warn(`[Settings] Could not clear tile cache for ${g}`),await s()})})}finally{n=null}})(),n}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 wa()?console.log("[Settings] Tile caches cleared"):console.warn("[Settings] Tile-cache clear failed"),await s()}),t.addEventListener("show.bs.offcanvas",s),ya(()=>{console.log("[Settings] SW controller changed → refreshing tile-cache stats"),s()}),s()}function Rl(){const o=document.getElementById("download-tiles-btn"),e=document.getElementById("offline-download-modal");if(!o||!e)return;const t=lt.getOrCreateInstance(e),r=document.getElementById("offline-download-form-view"),n=document.getElementById("offline-download-progress-view"),s=document.getElementById("offline-download-done-view"),a=document.getElementById("offline-download-cancel-btn"),i=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"),f=document.getElementById("offline-max-zoom"),p=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"),_=document.getElementById("offline-area-view-info"),E=document.getElementById("offline-area-district-info"),T=document.getElementById("offline-progress-bar"),x=document.getElementById("offline-progress-percent"),S=document.getElementById("offline-progress-counts"),P=document.getElementById("offline-progress-ok"),k=document.getElementById("offline-progress-failed"),j=document.getElementById("offline-progress-eta"),q=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?Ia:null}function H(){const B=d.value,J=parseInt(u.value,10),V=parseInt(f.value,10);if(Number.isNaN(J)||Number.isNaN(V)||J>V){h.textContent="Invalid zoom range",g.classList.replace("alert-info","alert-warning"),i.disabled=!0;return}const ne=ye();if(!ne){h.textContent="Selected area is not available.",g.classList.replace("alert-info","alert-warning"),i.disabled=!0;return}const R=zn[B]?.maxZoom??19,W=Math.min(V,R),re=Sa(ne,J,W),Ot=Pa(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(Ot)}`+we,g.classList.toggle("alert-warning",!!we),g.classList.toggle("alert-info",!we),i.disabled=!p.checked||re===0}function be(){v?.getCurrentViewExtent()?_.textContent=" · ready":_.textContent="",v?.getDistrictBoundaryExtent()?(E.textContent="",y.disabled=!1):(E.textContent=" (not loaded — connect online to fetch)",y.disabled=!0,y.checked&&(m.checked=!0))}function Xe(){r.classList.remove("d-none"),n.classList.add("d-none"),s.classList.add("d-none"),i.classList.remove("d-none"),a.classList.remove("d-none"),a.textContent="Cancel",l.classList.add("d-none"),c.disabled=!1,p.checked=!1,i.disabled=!0,$=null}o.addEventListener("click",()=>{Xe(),be(),H(),t.show()}),d.addEventListener("change",H),u.addEventListener("input",H),f.addEventListener("input",H),m.addEventListener("change",H),y.addEventListener("change",H),w.addEventListener("change",H),p.addEventListener("change",H),i.addEventListener("click",async()=>{const B=d.value,J=parseInt(u.value,10),V=parseInt(f.value,10),ne=ye();if(!ne)return;r.classList.add("d-none"),n.classList.remove("d-none"),i.classList.add("d-none"),a.textContent="Cancel download",c.disabled=!0,T.style.width="0%",T.setAttribute("aria-valuenow","0"),x.textContent="0%",S.textContent="0 of 0 tiles",P.textContent="0",k.textContent="0",j.textContent="—",$=new ka({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));T.style.width=re+"%",T.setAttribute("aria-valuenow",String(re)),x.textContent=re+"%",S.textContent=`${W.done.toLocaleString()} of ${W.total.toLocaleString()} tiles`}P.textContent=W.ok.toLocaleString(),k.textContent=W.failed.toLocaleString(),j.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}}n.classList.add("d-none"),s.classList.remove("d-none"),a.classList.add("d-none"),l.classList.remove("d-none"),c.disabled=!1,R.phase==="cancelled"?(q.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"?(q.textContent="Download failed",Z.textContent="See console for details."):(q.textContent="Download complete",Z.innerHTML=`<strong>${R.ok.toLocaleString()}</strong> tiles cached`+(R.failed>0?`, ${R.failed.toLocaleString()} failed`:"")+`.<br>Took ${Q(R.elapsedMs)}.`)}),a.addEventListener("click",()=>{$&&$.cancel()}),e.addEventListener("hidden.bs.modal",()=>{$&&$.cancel(),Xe()})}function Nl(){const o=Ke(),e=document.getElementById("menu-btn"),t=document.getElementById("menu-user-avatar"),r=document.getElementById("menu-user-name"),n=document.getElementById("menu-user-email"),s=document.getElementById("menu-user-detail"),a=document.getElementById("menu-signout-btn"),i=document.getElementById("menu-landing-link"),l=document.getElementById("menu-signin-link"),c=document.getElementById("menu-no-session-note");if(!e||!t||!r||!n||!s||!a){console.warn("[AccountMenu] One or more elements missing — shell may be stale. Hard-refresh.");return}if(!!o&&!!o.user_id){const u=[o.title,o.full_name].filter(Boolean).join(" ").trim()||o.username||"Authenticated user",f=(o.full_name||o.username||"?").trim().charAt(0).toUpperCase();t.textContent=f,t.style.background="var(--brand-navy, #1e1a4b)",r.textContent=u,n.textContent=o.email||"";const p=[];o.district_id!=null&&p.push(`District ${U(String(o.district_id))}`),o.region_id!=null&&p.push(`Region ${U(String(o.region_id))}`),o.ua_position&&p.push(U(o.ua_position)),s.innerHTML=p.join(" · ")||"No district info",a.classList.remove("d-none"),a.addEventListener("click",()=>$l(o),{once:!1}),i?.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",n.textContent="",s.textContent="",a.classList.add("d-none"),i?.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",n.textContent="",s.textContent="",a.classList.add("d-none"),i?.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 $l(o){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, ${o?.full_name||o?.username||"user"}?`)){try{await bo(),localStorage.removeItem(uo)}catch(e){console.warn("[Logout] Cache clear failed (continuing):",e)}window.location.href="/?logout=1"}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",ln):ln();export{ro as d,F as s,bi as z};
|
||
//# sourceMappingURL=index-Ddi_Zl4K.js.map
|