pwaLUPMIS2/dist/assets/index-DJ2WL3EC.js
ekke cfaceb3487 GPS trail recording, SSO auth, account menu, and mobile/UI refinements
Major:
- GPS trail recording: reusable, dependency-free engine in src/geotracker/
  (GeoTracker + geo-utils) with pluggable storage/sync adapters; LUPMIS
  wiring in src/geotracker-lupmis.js. Expandable My Location control
  (Locate Me + Record Trail), live navbar GPS readout, on-map trail/position
  rendering, gps_trails/gps_trail_points SQLocal tables, and store-and-forward
  sync via pushGpsTrail() -> save_gps_trail.php (server side documented, not
  yet built).
- SSO authentication: public/index.php entry point validates the LUSPA SSO
  cookie and injects window.LUPMIS_SESSION; remotedb district_id is now a
  session-resolved getter. Adds public/.htaccess (DirectoryIndex).
- Account menu offcanvas (navbar burger) with sign-in/out states.

UI / fixes:
- LayerSwitcher modernisation; base-map "None" option in picker + settings.
- Mobile drawing toolbar wraps to two rows below 576px and shows only in
  Draw mode; second row right-aligned and clears the Select option bar.
- Safari bottom-dock clipping fixed (app-container 100dvh -> 100svh).
- Rename public/icons -> app-icons to dodge Apache's default /icons/ alias.
- Service Worker bumped to v8 (network-first HTML, per-provider tile clear).

Docs: reusable-mapping and OSM-3D-buildings concept notes; ignore Office
lock files (~$*). Rebuilt dist/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 16:08:37 +02:00

708 lines
223 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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

const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/pdf-export-vzOHm8wb.js","assets/jspdf-Dzj2Osmy.js","assets/openlayers-CvK8xBSr.js","assets/openlayers-BtPuoxOl.css"])))=>i.map(i=>d[i]);
import{_ as dt,h as M,F as I,j as k,k as re,m as Dt,b as N,V as F,L as se,D as ve,P as Ue,Q as tt,n as ne,U as be,M as qt,W as on,X as te,Y as nn,S as rn,G as sn,Z as an,o as We,O as he,$ as qe,a0 as ot,a1 as Le,A as ln,T as Z,a2 as we,a3 as jt,a4 as ae,a5 as zt,e as cn,u as bt,a6 as wo,a7 as dn}from"./openlayers-CvK8xBSr.js";import{M as Ft}from"./bootstrap-D1-uvFxm.js";import{o as un,a as pn,b as hn,c as fn,d as Tt,e as Ut,f as $e,g as gn,h as fe,i as mn,j as yn}from"./ol-ext-BR0zF6aa.js";(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))n(o);new MutationObserver(o=>{for(const i of o)if(i.type==="childList")for(const s of i.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&n(s)}).observe(document,{childList:!0,subtree:!0});function t(o){const i={};return o.integrity&&(i.integrity=o.integrity),o.referrerPolicy&&(i.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?i.credentials="include":o.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function n(o){if(o.ep)return;o.ep=!0;const i=t(o);fetch(o.href,i)}})();const Ht="function",Ie="64e10b34-2bf7-4616-9668-f99de5aa046e",bn="get",wn="has",vn="set",{isArray:Ze}=Array;let{SharedArrayBuffer:nt,window:_n}=globalThis,{notify:vo,wait:_o,waitAsync:rt}=Atomics,Eo=null;rt||(rt=r=>({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(r)})}));try{new nt(4)}catch{nt=ArrayBuffer;const e=new WeakMap;if(_n){const t=new Map,{prototype:{postMessage:n}}=Worker,o=i=>{const s=i.data?.[Ie];if(!Ze(s)){i.stopImmediatePropagation();const{id:a,sb:l}=s;t.get(a)(l)}};Eo=function(i,...s){const a=i?.[Ie];if(Ze(a)){const[l,c]=a;e.set(c,l),this.addEventListener("message",o)}return n.call(this,i,...s)},rt=i=>({value:new Promise(s=>{t.set(e.get(i),s)}).then(s=>{t.delete(e.get(i)),e.delete(i);for(let a=0;a<s.length;a++)i[a]=s[a];return"ok"})})}else{const t=(n,o)=>({[Ie]:{id:n,sb:o}});vo=n=>{postMessage(t(e.get(n),n))},addEventListener("message",n=>{const o=n.data?.[Ie];if(Ze(o)){const[i,s]=o;e.set(s,i)}})}}/*! (c) Andrea Giammarchi - ISC */const{Int32Array:kt,Map:Wt,Uint16Array:Pt}=globalThis,{BYTES_PER_ELEMENT:Vt}=kt,{BYTES_PER_ELEMENT:En}=Pt,xn=(r,e,t)=>{for(;_o(r,0,0,e)==="timed-out";)t()},Mt=new WeakSet,wt=new WeakMap,Sn={value:{then:r=>r()}};let Ln=0;const Ot=(r,{parse:e=JSON.parse,stringify:t=JSON.stringify,transform:n,interrupt:o}=JSON)=>{if(!wt.has(r)){const i=Eo||r.postMessage,s=(p,...f)=>i.call(r,{[Ie]:f},{transfer:p}),a=typeof o===Ht?o:o?.handler,l=o?.delay||42,c=new TextDecoder("utf-16"),d=(p,f)=>p?rt(f,0):(a?xn(f,l,a):_o(f,0),Sn);let u=!1;wt.set(r,new Proxy(new Wt,{[wn]:(p,f)=>typeof f=="string"&&!f.startsWith("_"),[bn]:(p,f)=>f==="then"?null:((...h)=>{const m=Ln++;let g=new kt(new nt(Vt*2)),y=[];Mt.has(h.at(-1)||y)&&Mt.delete(y=h.pop()),s(y,m,g,f,n?h.map(n):h);const b=r!==globalThis;let E=0;return u&&b&&(E=setTimeout(console.warn,1e3,`💀🔒 - Possible deadlock if proxy.${f}(...args) is awaited`)),d(b,g).value.then(()=>{clearTimeout(E);const L=g[1];if(!L)return;const x=En*L;return g=new kt(new nt(x+x%Vt)),s([],m,g),d(b,g).value.then(()=>e(c.decode(new Pt(g.buffer).slice(0,L))))})}),[vn](p,f,h){const m=typeof h;if(m!==Ht)throw new Error(`Unable to assign ${f} as ${m}`);if(!p.size){const g=new Wt;r.addEventListener("message",async y=>{const b=y.data?.[Ie];if(Ze(b)){y.stopImmediatePropagation();const[E,L,...x]=b;let w;if(x.length){const[S,P]=x;if(p.has(S)){u=!0;try{const T=await p.get(S)(...P);if(T!==void 0){const j=t(n?n(T):T);g.set(E,j),L[1]=j.length}}catch(T){w=T}finally{u=!1}}else w=new Error(`Unsupported action: ${S}`);L[0]=1}else{const S=g.get(E);g.delete(E);for(let P=new Pt(L.buffer),T=0;T<S.length;T++)P[T]=S.charCodeAt(T)}if(vo(L,0),w)throw w}})}return!!p.set(f,h)}}))}return wt.get(r)};Ot.transfer=(...r)=>(Mt.add(r),r);function Kt(){let r,e;return{lock:async()=>{for(;r;)await r;r=new Promise(o=>{e=o})},unlock:async()=>{const o=e;r=void 0,e=void 0,o?.()}}}async function xo(r,e){let t;if(r instanceof Blob?t=r.stream():t=r,t instanceof ReadableStream&&e){const o=t.getReader();switch(e){case"callback":return async()=>(await o.read()).value;case"buffer":const i=[];let s=!1;for(;!s;){const d=await o.read();d.value&&i.push(d.value),s=d.done}const a=i.reduce((d,u)=>d+u.length,0),l=new Uint8Array(a);let c=0;return i.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,n=this.getFlags(e);if(!this.sqlite3InitModule){const{default:o}=await dt(async()=>{const{default:i}=await import("./index-DTMgZTfd.js");return{default:i}},[]);this.sqlite3InitModule=o}this.sqlite3||(this.sqlite3=await this.sqlite3InitModule()),this.db&&await this.destroy(),this.db=new this.sqlite3.oo1.DB(t,n),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(n=>{const o=new Map;try{for(let i of e){let s=o.get(i.sql);if(!s){const c=n.prepare(i.sql);o.set(i.sql,c),s=c}i.params?.length&&s.bind(i.params);let a=[],l=[];for(;s.step();)a=s.getColumnNames([]),l.push(s.get([]));t.push({columns:a,rows:l}),s.reset()}}finally{o.forEach(i=>{i.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,...n)=>e.func(...n),arity:-1});break;case"aggregate":this.db.createFunction({name:e.name,xStep:(t,...n)=>e.func.step(...n),xFinal:(t,...n)=>e.func.final(...n),arity:-1});break}}async import(e){if(!this.sqlite3||!this.db||!this.config)throw new Error("Driver not initialized");const t=await xo(e,"buffer"),n=this.sqlite3.wasm.allocFromTypedArray(t);this.pointers.push(n);const o=this.sqlite3.capi.sqlite3_deserialize(this.db,"main",n,t.byteLength,t.byteLength,this.config.readOnly?this.sqlite3.capi.SQLITE_DESERIALIZE_READONLY:this.sqlite3.capi.SQLITE_DESERIALIZE_RESIZEABLE);this.db.checkRc(o)}async export(){if(!this.sqlite3||!this.db)throw new Error("Driver not initialized");return{name:"database.sqlite3",data:this.sqlite3.capi.sqlite3_js_db_export(this.db)}}async clear(){}async destroy(){this.closeDb(),this.pointers.forEach(e=>this.sqlite3?.wasm.dealloc(e)),this.pointers=[],this.writeCallbacks.clear()}getFlags(e){const{readOnly:t,verbose:n}=e;return[t===!0?"r":"cw",n===!0?"t":""].join("")}execOnDb(e,t){const n={rows:[],columns:[]},o=e.exec({sql:t.sql,bind:t.params,returnValue:"resultRows",rowMode:"array",columnNames:n.columns});switch(t.method){case"run":break;case"get":n.rows=o[0]??[];break;case"all":default:n.rows=o;break}return n}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,n,o,i,s)=>{this.writeCallbacks.forEach(a=>{a({table:i,rowid:s,operation:e[n]})})},0)}closeDb(){this.db&&(this.db.close(),this.db=void 0)}}function Tn(r,e,t){let n,o,i,s,a,l,c=0,d=!1,u=!1,p=!0;if(typeof r!="function")throw new TypeError("Expected a function");e=Number(e)||0,typeof t=="object"&&t!==null&&(d=!!t.leading,u="maxWait"in t,i=u?Math.max(Number(t.maxWait)||0,e):0,p="trailing"in t?!!t.trailing:p);function f(w){const S=n,P=o;return n=o=void 0,c=w,s=r.apply(P,S),s}function h(w){return c=w,a=setTimeout(y,e),d?f(w):s}function m(w){const S=w-(l??0),P=w-c,T=e-S;return u?Math.min(T,i-P):T}function g(w){const S=w-(l??0),P=w-c;return l===void 0||S>=e||S<0||u&&P>=i}function y(){const w=Date.now();if(g(w))return b(w);a=setTimeout(y,m(w))}function b(w){return a=void 0,p&&n?f(w):(n=o=void 0,s)}function E(){a!==void 0&&clearTimeout(a),c=0,n=l=o=a=void 0}function L(){return a===void 0?s:b(Date.now())}function x(){const w=Date.now(),S=g(w);if(n=arguments,o=this,l=w,S){if(a===void 0)return h(l);if(u)return a=setTimeout(y,e),f(l)}return a===void 0&&(a=setTimeout(y,e)),s}return x.cancel=E,x.flush=L,x}function Qe(){return crypto.randomUUID()}function So(r,e){switch(r){case"session":case":sessionStorage:":let t=sessionStorage._sqlocal_session_key;return t||(t=Qe(),sessionStorage._sqlocal_session_key=t),`session:${t}`;case"local":case":localStorage:":return"local";case":memory:":return`memory:${e}`;default:return`path:${r}`}}class Ve{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:Kt()}),Object.defineProperty(this,"transactionMutex",{enumerable:!0,configurable:!0,writable:!0,value:Kt()}),Object.defineProperty(this,"transactionKey",{enumerable:!0,configurable:!0,writable:!0,value:null}),Object.defineProperty(this,"proxy",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"dirtyTables",{enumerable:!0,configurable:!0,writable:!0,value:new Set}),Object.defineProperty(this,"effectsChannel",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"reinitChannel",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"onmessage",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"init",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{if(!(!this.config.databasePath||!this.config.clientKey)){await this.initMutex.lock();try{try{await this.driver.init(this.config)}catch{console.warn(`Persistence failed, so ${this.config.databasePath} will not be saved. For origin private file system persistence, make sure your web server is configured to use the correct HTTP response headers (See https://sqlocal.dev/guide/setup#cross-origin-isolation).`),this.config.databasePath=":memory:",this.driver=new st,await this.driver.init(this.config)}const i=So(this.config.databasePath,this.config.clientKey);this.reinitChannel=new BroadcastChannel(`_sqlocal_reinit_(${i})`),this.reinitChannel.onmessage=s=>{const a=s.data;if(this.config.clientKey!==a.clientKey)switch(a.type){case"reinit":this.init(a.reason);break;case"close":this.driver.destroy();break}},this.config.reactive&&(this.effectsChannel=new BroadcastChannel(`_sqlocal_effects_(${i})`),this.driver.onWrite(async s=>{this.dirtyTables.add(s.table),await this.transactionMutex.lock(),this.emitEffectsDebounced(),await this.transactionMutex.unlock()})),await Promise.all(Array.from(this.userFunctions.values()).map(s=>this.initUserFunction(s))),await this.execInitStatements(),this.emitMessage({type:"event",event:"connect",reason:o})}catch(i){this.emitMessage({type:"error",error:i,queryKey:null}),await this.destroy()}finally{await this.initMutex.unlock()}}}}),Object.defineProperty(this,"postMessage",{enumerable:!0,configurable:!0,writable:!0,value:async(o,i)=>{const s=o instanceof MessageEvent?o.data:o;switch(await this.initMutex.lock(),s.type){case"config":this.editConfig(s);break;case"query":case"batch":case"transaction":this.exec(s);break;case"function":this.createUserFunction(s);break;case"getinfo":this.getDatabaseInfo(s);break;case"import":this.importDb(s);break;case"export":this.exportDb(s);break;case"delete":this.deleteDb(s);break;case"destroy":this.destroy(s);break}await this.initMutex.unlock()}}),Object.defineProperty(this,"emitMessage",{enumerable:!0,configurable:!0,writable:!0,value:(o,i=[])=>{this.onmessage&&this.onmessage(o,i)}}),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:Tn(()=>this.emitEffects(),32,{maxWait:180})}),Object.defineProperty(this,"editConfig",{enumerable:!0,configurable:!0,writable:!0,value:o=>{this.config=o.config,this.init("initial")}}),Object.defineProperty(this,"exec",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{try{const i={type:"data",queryKey:o.queryKey,data:[]};switch(o.type){case"query":const s=this.transactionKey!==null&&this.transactionKey===o.transactionKey;try{s||await this.transactionMutex.lock();const a=await this.driver.exec(o);i.data.push(a)}finally{s||await this.transactionMutex.unlock()}break;case"batch":try{await this.transactionMutex.lock();const a=await this.driver.execBatch(o.statements);i.data.push(...a)}finally{await this.transactionMutex.unlock()}break;case"transaction":if(o.action==="begin"&&(await this.transactionMutex.lock(),this.transactionKey=o.transactionKey,await this.driver.exec({sql:"BEGIN"})),(o.action==="commit"||o.action==="rollback")&&this.transactionKey!==null&&this.transactionKey===o.transactionKey){const a=o.action==="commit"?"COMMIT":"ROLLBACK";await this.driver.exec({sql:a}),this.transactionKey=null,await this.transactionMutex.unlock()}break}this.emitMessage(i)}catch(i){this.emitMessage({type:"error",error:i,queryKey:o.queryKey})}}}),Object.defineProperty(this,"execInitStatements",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{if(this.config.onInitStatements)for(let o of this.config.onInitStatements)await this.driver.exec(o)}}),Object.defineProperty(this,"getDatabaseInfo",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{try{this.emitMessage({type:"info",queryKey:o.queryKey,info:{databasePath:this.config.databasePath,storageType:this.driver.storageType,databaseSizeBytes:await this.driver.getDatabaseSizeBytes(),persisted:await this.driver.isDatabasePersisted()}})}catch(i){this.emitMessage({type:"error",queryKey:o.queryKey,error:i})}}}),Object.defineProperty(this,"createUserFunction",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{functionName:i,functionType:s,queryKey:a}=o;let l;if(this.userFunctions.has(i)){this.emitMessage({type:"error",error:new Error(`A user-defined function with the name "${i}" has already been created for this SQLocal instance.`),queryKey:a});return}switch(s){case"callback":l={type:s,name:i,func:(...c)=>{this.emitMessage({type:"callback",name:i,args:c})}};break;case"scalar":l={type:s,name:i,func:this.proxy[`_sqlocal_func_${i}`]};break;case"aggregate":l={type:s,name:i,func:{step:this.proxy[`_sqlocal_func_${i}_step`],final:this.proxy[`_sqlocal_func_${i}_final`]}};break}try{await this.initUserFunction(l),this.emitMessage({type:"success",queryKey:a})}catch(c){this.emitMessage({type:"error",error:c,queryKey:a})}}}),Object.defineProperty(this,"initUserFunction",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{await this.driver.createFunction(o),this.userFunctions.set(o.name,o)}}),Object.defineProperty(this,"importDb",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{queryKey:i,database:s}=o;let a=!1;try{await this.driver.import(s),this.driver.storageType==="memory"&&await this.execInitStatements()}catch(l){this.emitMessage({type:"error",error:l,queryKey:i}),a=!0}finally{this.driver.storageType!=="memory"&&await this.init("overwrite")}a||this.emitMessage({type:"success",queryKey:i})}}),Object.defineProperty(this,"exportDb",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{queryKey:i}=o;try{const{name:s,data:a}=await this.driver.export();this.emitMessage({type:"buffer",queryKey:i,bufferName:s,buffer:a},[a])}catch(s){this.emitMessage({type:"error",error:s,queryKey:i})}}}),Object.defineProperty(this,"deleteDb",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{queryKey:i}=o;let s=!1;try{await this.driver.clear()}catch(a){this.emitMessage({type:"error",error:a,queryKey:i}),s=!0}finally{await this.init("delete")}s||this.emitMessage({type:"success",queryKey:i})}}),Object.defineProperty(this,"destroy",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{await this.driver.exec({sql:"PRAGMA optimize"}),await this.driver.destroy(),this.effectsChannel&&(this.emitEffectsDebounced.flush(),this.effectsChannel.close(),this.effectsChannel=void 0),this.reinitChannel&&(this.reinitChannel.close(),this.reinitChannel=void 0),o&&this.emitMessage({type:"success",queryKey:o.queryKey})}});const n=typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope?Ot(globalThis):globalThis;this.proxy=n,this.driver=e}}function it(r,...e){return{sql:r.join("?"),params:e}}function kn(r){return!r.some(e=>!Array.isArray(e))}function vt(r,e){let t;return kn(r)?t=r:t=[r],t.map(n=>{const o={};return e.forEach((i,s)=>{o[i]=n[s]}),o})}function Pn(r){return typeof r=="object"&&r!==null&&"getSQL"in r&&typeof r.getSQL=="function"}function Mn(r){return typeof r=="object"&&r!==null&&"sql"in r&&typeof r.sql=="string"&&"params"in r}function Yt(r){if(typeof r=="function"&&(r=r(it)),Pn(r))try{if(!("toSQL"in r&&typeof r.toSQL=="function"))throw 1;const n=r.toSQL();if(!Mn(n))throw 2;const o="all"in r&&typeof r.all=="function"?r.all:void 0;return{...n,exec:o?()=>o():void 0}}catch{throw new Error("The passed statement could not be parsed.")}const e=r.sql;let t=[];return"params"in r?t=r.params:"parameters"in r&&(t=r.parameters),{sql:e,params:t}}function Xt(r,e){let t;return typeof r=="string"?t={sql:r,params:e}:t=it(r,...e),t}async function Ke(r,e,t,n){return!e&&"locks"in navigator?navigator.locks.request(`_sqlocal_mutation_(${t.databasePath})`,{mode:r},n):n()}class Jt 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:n}=await dt(async()=>{const{default:o}=await import("./index-DTMgZTfd.js");return{default:o}},[]);this.sqlite3InitModule=n}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 Lo,To;class In{constructor(e){Object.defineProperty(this,"config",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"clientKey",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"processor",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"isDestroyed",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"bypassMutationLock",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"transactionQueryKeyQueue",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"userCallbacks",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"queriesInProgress",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"proxy",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"reinitChannel",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"effectsChannel",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"processMessageEvent",{enumerable:!0,configurable:!0,writable:!0,value:c=>{const d=c instanceof MessageEvent?c.data:c,u=this.queriesInProgress;switch(d.type){case"success":case"data":case"buffer":case"info":case"error":if(d.queryKey&&u.has(d.queryKey)){const[f,h]=u.get(d.queryKey);d.type==="error"?h(d.error):f(d),u.delete(d.queryKey)}else if(d.type==="error")throw d.error;break;case"callback":const p=this.userCallbacks.get(d.name);p&&p(...d.args??[]);break;case"event":this.config.onConnect?.(d.reason);break}}}),Object.defineProperty(this,"createQuery",{enumerable:!0,configurable:!0,writable:!0,value:async c=>Ke("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=Qe();switch(c.type){case"import":this.processor.postMessage({...c,queryKey:d},[c.database]);break;default:this.processor.postMessage({...c,queryKey:d});break}return new Promise((u,p)=>{this.queriesInProgress.set(d,[u,p])})})}),Object.defineProperty(this,"broadcast",{enumerable:!0,configurable:!0,writable:!0,value:c=>{this.reinitChannel.postMessage(c)}}),Object.defineProperty(this,"exec",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d,u="all",p)=>{const f=await this.createQuery({type:"query",transactionKey:p,sql:c,params:d,method:u}),h={rows:[],columns:[]};return f.type==="data"&&(h.rows=f.data[0]?.rows??[],h.columns=f.data[0]?.columns??[]),h}}),Object.defineProperty(this,"execBatch",{enumerable:!0,configurable:!0,writable:!0,value:async c=>{const d=await this.createQuery({type:"batch",statements:c}),u=new Array(c.length).fill({rows:[],columns:[]});return d.type==="data"&&d.data.forEach((p,f)=>{u[f]=p}),u}}),Object.defineProperty(this,"sql",{enumerable:!0,configurable:!0,writable:!0,value:async(c,...d)=>{const u=Xt(c,d),{rows:p,columns:f}=await this.exec(u.sql,u.params,"all");return vt(p,f)}}),Object.defineProperty(this,"batch",{enumerable:!0,configurable:!0,writable:!0,value:async c=>{const d=c(it);return(await this.execBatch(d)).map(({rows:p,columns:f})=>vt(p,f))}}),Object.defineProperty(this,"beginTransaction",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{const c=Qe();await this.createQuery({type:"transaction",transactionKey:c,action:"begin"});const d=async h=>{const m=Yt(h);if(m.exec)return this.transactionQueryKeyQueue.push(c),m.exec();const{rows:g,columns:y}=await this.exec(m.sql,m.params,"all",c);return vt(g,y)};return{query:d,sql:async(h,...m)=>{const g=Xt(h,m);return await d(g)},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=>Ke("exclusive",!1,this.config,async()=>{let d;this.bypassMutationLock=!0;try{d=await this.beginTransaction();const u=await c({sql:d.sql,query:d.query});return await d.commit(),u}catch(u){throw await d?.rollback(),u}finally{this.bypassMutationLock=!1}})}),Object.defineProperty(this,"reactiveQuery",{enumerable:!0,configurable:!0,writable:!0,value:c=>{let d=[],u=!1,p=!1,f=0;const h=Yt(c),m=new Set,g=new Set,y=new Set,b=async()=>{try{const L=++f;if(m.size===0){const w=await this.sql("SELECT name, wr FROM tables_used(?) WHERE type = 'table'",h.sql),S=new Set,P=new Set;if(w.forEach(T=>{typeof T.name=="string"&&(T.wr?P.add(T.name):S.add(T.name))}),S.size===0)throw new Error("The passed SQL does not read any tables.");if(Array.from(P).some(T=>S.has(T)))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(T=>m.add(T))}const x=h.exec?await h.exec():await this.sql(h.sql,...h.params);L===f&&(d=x,u=!0,g.forEach(w=>w(d)))}catch(L){y.forEach(x=>{x(L instanceof Error?L:new Error(String(L)))})}},E=L=>{L.data.tables.some(x=>m.has(x))&&b()};return{get value(){return d},subscribe:(L,x)=>{if(!this.effectsChannel)throw new Error('This SQLocal instance is not configured for reactive queries. Set the "reactive" option to enable them.');return x||(x=w=>{throw w}),g.add(L),y.add(x),p?u&&L(d):(this.effectsChannel.addEventListener("message",E),p=!0,b()),{unsubscribe:()=>{g.delete(L),y.delete(x),g.size===0&&(this.effectsChannel?.removeEventListener("message",E),p=!1)}}}}}}),Object.defineProperty(this,"createCallbackFunction",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{await this.createQuery({type:"function",functionName:c,functionType:"callback"}),this.userCallbacks.set(c,d)}}),Object.defineProperty(this,"createScalarFunction",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{const u=`_sqlocal_func_${c}`,p=()=>{this.proxy[u]=d};this.proxy===globalThis&&p(),await this.createQuery({type:"function",functionName:c,functionType:"scalar"}),this.proxy!==globalThis&&p()}}),Object.defineProperty(this,"createAggregateFunction",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{const u=`_sqlocal_func_${c}`,p=()=>{this.proxy[`${u}_step`]=d.step,this.proxy[`${u}_final`]=d.final};this.proxy===globalThis&&p(),await this.createQuery({type:"function",functionName:c,functionType:"aggregate"}),this.proxy!==globalThis&&p()}}),Object.defineProperty(this,"getDatabaseInfo",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{const c=await this.createQuery({type:"getinfo"});if(c.type==="info")return c.info;throw new Error("The database failed to return valid information.")}}),Object.defineProperty(this,"getDatabaseFile",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{const c=await this.createQuery({type:"export"});if(c.type==="buffer")return new File([c.buffer],c.bufferName,{type:"application/x-sqlite3"});throw new Error("The database failed to export.")}}),Object.defineProperty(this,"overwriteDatabaseFile",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{await Ke("exclusive",!1,this.config,async()=>{try{this.broadcast({type:"close",clientKey:this.clientKey});const u=await xo(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 Ke("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,Lo,{enumerable:!0,configurable:!0,writable:!0,value:()=>{this.destroy()}}),Object.defineProperty(this,To,{enumerable:!0,configurable:!0,writable:!0,value:async()=>{await this.destroy()}});const t=typeof e=="string"?{databasePath:e}:e,{onInit:n,onConnect:o,processor:i,...s}=t,{databasePath:a}=s;this.config=t,this.clientKey=Qe();const l=So(a,this.clientKey);if(this.reinitChannel=new BroadcastChannel(`_sqlocal_reinit_(${l})`),s.reactive&&(this.effectsChannel=new BroadcastChannel(`_sqlocal_effects_(${l})`)),typeof i<"u")this.processor=i;else if(a==="local"||a===":localStorage:"){const c=new Jt("local");this.processor=new Ve(c)}else if(a==="session"||a===":sessionStorage:"){const c=new Jt("session");this.processor=new Ve(c)}else if(typeof globalThis.Worker<"u"&&a!==":memory:")this.processor=new Worker(new URL("/assets/worker-CuIBOSaM.js",import.meta.url),{type:"module"});else{const c=new st;this.processor=new Ve(c)}this.processor instanceof Ve?(this.processor.onmessage=c=>this.processMessageEvent(c),this.proxy=globalThis):(this.processor.addEventListener("message",this.processMessageEvent),this.proxy=Ot(this.processor)),this.processor.postMessage({type:"config",config:{...s,clientKey:this.clientKey,onInitStatements:n?.(it)??[]}})}}Lo=Symbol.dispose,To=Symbol.asyncDispose;const Rt="lupmis2.db",Cn="lupmis-db-sync",ko=new In(Rt),{sql:_}=ko;console.log("[Database] SQLocal instance created for:",Rt);const Po=new BroadcastChannel(Cn);let Mo=!1,Io,Co;const Zt=new Promise((r,e)=>{Io=r,Co=e}),at=new Set;function An(r){return at.add(r),()=>at.delete(r)}Po.onmessage=r=>{const{type:e,payload:t}=r.data;if(e==="DB_CHANGE")for(const n of at)try{n(t)}catch(o){console.error("[Database] Change listener error:",o)}};function xe(r,e,t=null){Po.postMessage({type:"DB_CHANGE",payload:{table:r,action:e,id:t,timestamp:Date.now()}});for(const n of at)try{n({table:r,action:e,id:t,timestamp:Date.now(),local:!0})}catch(o){console.error("[Database] Change listener error:",o)}}async function Dn(){try{console.log("[Database] Initializing schema...");const r=await _`SELECT sqlite_version() as version`;console.log("[Database] SQLite version:",r[0]?.version),console.log("[Database] Creating locations table..."),await _`
CREATE TABLE IF NOT EXISTS locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
longitude REAL NOT NULL,
latitude REAL NOT NULL,
description TEXT,
category TEXT DEFAULT 'default',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
synced INTEGER DEFAULT 0
)
`;const e=await _`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;console.log("[Database] Locations table exists:",e.length>0),console.log("[Database] Creating sync_log table..."),await _`
CREATE TABLE IF NOT EXISTS sync_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT NOT NULL,
record_id INTEGER NOT NULL,
action TEXT NOT NULL,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
synced INTEGER DEFAULT 0
)
`,console.log("[Database] Creating remote_data table..."),await _`
CREATE TABLE IF NOT EXISTS remote_data (
key TEXT PRIMARY KEY,
data TEXT NOT NULL,
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`,console.log("[Database] Creating collector_zones table..."),await _`
CREATE TABLE IF NOT EXISTS collector_zones (
id INTEGER PRIMARY KEY,
zone_name TEXT,
geometry_wkt TEXT,
properties TEXT,
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`,console.log("[Database] Creating parcels table..."),await _`
CREATE TABLE IF NOT EXISTS parcels (
id INTEGER PRIMARY KEY,
geometry_wkt TEXT,
properties TEXT,
status TEXT DEFAULT 'verified',
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`;try{await _`SELECT status FROM parcels LIMIT 1`}catch{console.log("[Database] Adding status column to parcels table..."),await _`ALTER TABLE parcels ADD COLUMN status TEXT DEFAULT 'verified'`}console.log("[Database] Creating building_footprints table..."),await _`
CREATE TABLE IF NOT EXISTS building_footprints (
id INTEGER PRIMARY KEY,
geometry_wkt TEXT,
properties TEXT,
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`,console.log("[Database] Creating osm_roads table..."),await _`
CREATE TABLE IF NOT EXISTS osm_roads (
osm_id INTEGER PRIMARY KEY,
geometry_wkt TEXT,
properties TEXT,
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`,console.log("[Database] Creating gps_trails table..."),await _`
CREATE TABLE IF NOT EXISTS gps_trails (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_uuid TEXT UNIQUE,
name TEXT,
district_id TEXT,
started_at TEXT NOT NULL,
ended_at TEXT,
status TEXT NOT NULL DEFAULT 'recording',
point_count INTEGER NOT NULL DEFAULT 0,
distance_m REAL NOT NULL DEFAULT 0,
synced INTEGER NOT NULL DEFAULT 0,
remote_id TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`,console.log("[Database] Creating gps_trail_points table..."),await _`
CREATE TABLE IF NOT EXISTS gps_trail_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trail_id INTEGER NOT NULL,
seq INTEGER NOT NULL,
longitude REAL NOT NULL,
latitude REAL NOT NULL,
altitude REAL,
accuracy REAL,
altitude_accuracy REAL,
heading REAL,
speed REAL,
satellites INTEGER,
recorded_at TEXT NOT NULL
)
`,await _`CREATE INDEX IF NOT EXISTS idx_locations_category ON locations(category)`,await _`CREATE INDEX IF NOT EXISTS idx_locations_synced ON locations(synced)`,await _`CREATE INDEX IF NOT EXISTS idx_gps_trails_synced ON gps_trails(synced, status)`,await _`CREATE INDEX IF NOT EXISTS idx_gps_trail_points_trail ON gps_trail_points(trail_id, seq)`;const t=await _`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;console.log("[Database] All tables:",t.map(n=>n.name)),Mo=!0,Io(!0),console.log("[Database] ✓ Schema initialized")}catch(r){throw console.error("[Database] ✗ Schema init failed:",r),Co(r),r}}async function Fn(r,e,t,n={}){const{description:o=null,category:i="default"}=n;console.log("[Database] Adding location:",r,e,t,i);try{const s=await _`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;if(console.log("[Database] Table check before insert:",s),s.length===0)throw console.error("[Database] ✗ locations table does not exist!"),new Error("locations table does not exist");console.log("[Database] Executing INSERT..."),await _`
INSERT INTO locations (name, longitude, latitude, description, category)
VALUES (${r}, ${e}, ${t}, ${o}, ${i})
`,console.log("[Database] INSERT completed");const l=(await _`SELECT last_insert_rowid() as id`)[0]?.id;console.log("[Database] New ID:",l);const c=await _`SELECT * FROM locations WHERE id = ${l}`;if(console.log("[Database] Verify insert:",c),c.length===0)throw console.error("[Database] ✗ Insert verification failed - row not found!"),new Error("Insert verification failed");return await _`
INSERT INTO sync_log (table_name, record_id, action)
VALUES ('locations', ${l}, 'INSERT')
`,xe("locations","INSERT",l),console.log("[Database] ✓ Location added:",l),{id:l}}catch(s){throw console.error("[Database] ✗ Failed to add location:",s),s}}async function Ao(r={}){const{category:e=null,limit:t=1e3}=r;try{const n=await _`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;if(console.log("[Database] getLocations - table exists:",n.length>0),n.length===0)return console.warn("[Database] locations table does not exist yet"),[];let o;return e?o=await _`
SELECT * FROM locations
WHERE category = ${e}
ORDER BY created_at DESC
LIMIT ${t}
`:o=await _`
SELECT * FROM locations
ORDER BY created_at DESC
LIMIT ${t}
`,console.log("[Database] getLocations returned",o.length,"rows"),o}catch(n){return console.error("[Database] getLocations error:",n),[]}}async function On(){try{return(await _`SELECT COUNT(*) as count FROM locations`)[0]?.count??0}catch(r){return console.error("[Database] getLocationCount error:",r),0}}async function Do(r,e){try{const t=JSON.stringify(e);await _`
INSERT OR REPLACE INTO remote_data (key, data, fetched_at)
VALUES (${r}, ${t}, CURRENT_TIMESTAMP)
`,console.log("[Database] ✓ Remote data cached:",r)}catch(t){throw console.error("[Database] ✗ Failed to cache remote data:",r,t),t}}async function Fo(r){try{const e=await _`SELECT data, fetched_at FROM remote_data WHERE key = ${r}`;if(e.length===0)return null;const t=JSON.parse(e[0].data);return console.log("[Database] ✓ Remote data loaded from cache:",r,"(fetched",e[0].fetched_at+")"),t}catch(e){return console.error("[Database] ✗ Failed to read cached remote data:",r,e),null}}async function Rn(r){try{await _`DELETE FROM collector_zones`;for(const e of r){const t=JSON.stringify(e);await _`
INSERT INTO collector_zones (id, zone_name, geometry_wkt, properties, fetched_at)
VALUES (${e.colzonenr||e.id}, ${e.colzonename||e.zone_name||""}, ${e.polygon||e.boundary||""}, ${t}, CURRENT_TIMESTAMP)
`}console.log("[Database] ✓ Saved",r.length,"collector zones")}catch(e){throw console.error("[Database] ✗ Failed to save collector zones:",e),e}}async function Bn(){try{const r=await _`SELECT properties FROM collector_zones ORDER BY id`;return r.length===0?null:r.map(e=>JSON.parse(e.properties))}catch(r){return console.error("[Database] ✗ Failed to read local collector zones:",r),null}}async function Nn(r){try{await _`DELETE FROM parcels`;let e=0;for(const t of r){const n=t.id||t.parcelid||t.parcel_id||null;if(n==null)continue;const o=JSON.stringify(t),i=t.boundary||t.polygon||t.geom||t.wkt||"";await _`
INSERT OR REPLACE INTO parcels (id, geometry_wkt, properties, fetched_at)
VALUES (${n}, ${i}, ${o}, CURRENT_TIMESTAMP)
`,e++}console.log("[Database] ✓ Saved",e,"parcels (from",r.length,"rows,",r.length-e,"duplicates replaced)")}catch(e){throw console.error("[Database] ✗ Failed to save parcels:",e),e}}async function $n(){try{const r=await _`SELECT properties FROM parcels ORDER BY id`;return r.length===0?null:r.map(e=>JSON.parse(e.properties))}catch(r){return console.error("[Database] ✗ Failed to read local parcels:",r),null}}async function Gn(r,e){try{const t=JSON.stringify(e);await _`UPDATE parcels SET properties = ${t} WHERE id = ${r}`,console.log("[Database] ✓ Parcel updated:",r),xe("parcels","UPDATE",r)}catch(t){throw console.error("[Database] ✗ Failed to update parcel:",r,t),t}}async function qn(r,e){try{const t=JSON.stringify(e);await _`
INSERT INTO parcels (id, geometry_wkt, properties, status, fetched_at)
VALUES (NULL, ${r}, ${t}, 'new', CURRENT_TIMESTAMP)
`;const o=(await _`SELECT last_insert_rowid() as id`)[0]?.id;return console.log("[Database] ✓ New parcel inserted:",o,"(status: new)"),xe("parcels","INSERT",o),{id:o}}catch(t){throw console.error("[Database] ✗ Failed to insert new parcel:",t),t}}async function jn(r){try{if(r.length>0){const e=r[0],t={};for(const[n,o]of Object.entries(e))t[n]=o===null?"null":typeof o;console.log("[Database] First footprint field types:",t)}await _`DELETE FROM building_footprints`;for(const e of r){const t=JSON.stringify(e);let n=e.polygon||e.boundary||e.geom||e.wkt||e.footprint||"";const o=typeof n=="object"?JSON.stringify(n):String(n);let i=e.id||e.footprint_id||e.building_id||null;await _`
INSERT INTO building_footprints (id, geometry_wkt, properties, fetched_at)
VALUES (${i!==null&&typeof i=="object"?null:i}, ${o}, ${t}, CURRENT_TIMESTAMP)
`}console.log("[Database] ✓ Saved",r.length,"building footprints")}catch(e){throw console.error("[Database] ✗ Failed to save building footprints:",e),e}}async function zn(){try{const r=await _`SELECT properties FROM building_footprints ORDER BY id`;return r.length===0?null:r.map(e=>JSON.parse(e.properties))}catch(r){return console.error("[Database] ✗ Failed to read local building footprints:",r),null}}async function Un(r){try{if(r.length>0){const e=r[0],t={};for(const[n,o]of Object.entries(e))t[n]=o===null?"null":typeof o;console.log("[Database] First road field types:",t)}await _`DELETE FROM osm_roads`;for(const e of r){const t=JSON.stringify(e);let n=e.geom||e.geometry||e.wkt||e.road||e.line||"";const o=typeof n=="object"?JSON.stringify(n):String(n);let i=e.osm_id??e.osmid??e.id??null;await _`
INSERT OR REPLACE INTO osm_roads (osm_id, geometry_wkt, properties, fetched_at)
VALUES (${i!==null&&typeof i=="object"?null:i}, ${o}, ${t}, CURRENT_TIMESTAMP)
`}console.log("[Database] ✓ Saved",r.length,"OSM roads")}catch(e){throw console.error("[Database] ✗ Failed to save OSM roads:",e),e}}async function Hn(){try{const r=await _`SELECT properties FROM osm_roads ORDER BY osm_id`;return r.length===0?null:r.map(e=>JSON.parse(e.properties))}catch(r){return console.error("[Database] ✗ Failed to read local OSM roads:",r),null}}async function Wn(){return ko.getDatabaseFile()}async function Vn(r="lupmis-backup.sqlite3"){const e=await Wn(),t=new Blob([e],{type:"application/x-sqlite3"}),n=URL.createObjectURL(t),o=document.createElement("a");o.href=n,o.download=r,o.click(),URL.revokeObjectURL(n)}async function Kn(){return{type:"FeatureCollection",features:(await Ao()).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 Bt(){try{const r=await _`
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
ORDER BY name
`,e=await On();return{ready:Mo,databasePath:Rt,tables:r.map(t=>t.name),locationCount:e}}catch(r){return{ready:!1,error:r.message}}}const Oo=Object.freeze(["parcels","building_footprints","osm_roads","collector_zones","remote_data"]);function Ro(r){return Oo.includes(r)}async function Bo(r){if(!Ro(r))throw new Error(`Refusing to clear "${r}" — not a known cached-layer table`);const t=(await _(`SELECT COUNT(*) AS n FROM "${r}"`))[0]?.n??0;return await _(`DELETE FROM "${r}"`),console.log(`[Database] ✓ Cleared "${r}" (${t} rows)`),xe(r,"CLEAR",null),t}async function Yn(){const r=await _`
SELECT name FROM sqlite_master
WHERE type='table' AND name IN (
'parcels', 'building_footprints', 'osm_roads', 'collector_zones', 'remote_data'
)
`,e=new Set(r.map(o=>o.name)),t=[];for(const o of Oo)if(e.has(o))try{const i=await Bo(o);t.push({table:o,count:i})}catch(i){console.error(`[Database] Failed to clear ${o}:`,i),t.push({table:o,count:0,error:i.message})}const n=t.reduce((o,i)=>o+i.count,0);return console.log(`[Database] ✓ Cleared all cached layers: ${n} rows across ${t.length} tables`),t}async function Xn(){const r=await _`
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
ORDER BY name
`;if(r.length===0)return[];const e=r.map(t=>`SELECT '${t.name}' AS name, COUNT(*) AS count FROM "${t.name}"`).join(" UNION ALL ");return _(e)}async function Jn(r,e=200){if((await _`
SELECT name FROM sqlite_master
WHERE type='table' AND name = ${r}
`).length===0)throw new Error(`Table "${r}" does not exist`);const n=await _(`SELECT * FROM "${r}" LIMIT ${e}`);return{columns:n.length>0?Object.keys(n[0]):[],rows:n}}async function Zn(){console.log("=== DATABASE TEST ===");try{const r=await _`SELECT sqlite_version() as v`;console.log("1. SQLite version:",r[0].v);const e=await _`SELECT name FROM sqlite_master WHERE type='table'`;console.log("2. Tables:",e.map(o=>o.name)),console.log("3. Inserting test row..."),await _`INSERT INTO locations (name, longitude, latitude, category) VALUES ('TEST', -1.0, 7.0, 'test')`;const t=await _`SELECT * FROM locations WHERE name = 'TEST'`;console.log("4. Test row:",t);const n=await _`SELECT COUNT(*) as c FROM locations`;return console.log("5. Total rows:",n[0].c),await _`DELETE FROM locations WHERE name = 'TEST'`,console.log("6. Test row deleted"),console.log("=== TEST PASSED ==="),!0}catch(r){return console.error("=== TEST FAILED ===",r),!1}}typeof window<"u"&&(window.testDatabase=Zn,window.dbStatus=Bt);async function Qn(r){const{uuid:e,name:t=null,startedAt:n,districtId:o=null}=r;await _`
INSERT INTO gps_trails (client_uuid, name, district_id, started_at, status)
VALUES (${e}, ${t}, ${o}, ${n}, 'recording')
`;const s=(await _`SELECT last_insert_rowid() as id`)[0]?.id;return xe("gps_trails","insert",s),s}async function er(r,e){const{seq:t,lon:n,lat:o,altitude:i=null,accuracy:s=null,altitudeAccuracy:a=null,heading:l=null,speed:c=null,satellites:d=null,timestamp:u}=e,p=typeof u=="number"?new Date(u).toISOString():u||new Date().toISOString();await _`
INSERT INTO gps_trail_points
(trail_id, seq, longitude, latitude, altitude, accuracy, altitude_accuracy, heading, speed, satellites, recorded_at)
VALUES
(${r}, ${t}, ${n}, ${o}, ${i}, ${s}, ${a}, ${l}, ${c}, ${d}, ${p})
`}async function tr(r,e){const{endedAt:t,pointCount:n=0,distanceM:o=0}=e;await _`
UPDATE gps_trails
SET ended_at = ${t}, point_count = ${n}, distance_m = ${o}, status = 'completed'
WHERE id = ${r}
`,xe("gps_trails","update",r)}async function or(){return _`SELECT * FROM gps_trails WHERE synced = 0 AND status = 'completed' ORDER BY started_at ASC`}async function nr(r){return _`SELECT * FROM gps_trail_points WHERE trail_id = ${r} ORDER BY seq ASC`}async function rr(r,e=null){await _`UPDATE gps_trails SET synced = 1, remote_id = ${e} WHERE id = ${r}`,xe("gps_trails","update",r)}const No=3.28084,$o=621371e-9,Go=10.7639,qo=247105e-9,jo=3861e-10;function ut(){return localStorage.getItem("measurement-system")||"metric"}function lt(r){if(ut()==="imperial"){const e=r*No;return e>=5280?Math.round(r*$o*100)/100+" mi":Math.round(e)+" ft"}return r>1e3?Math.round(r/1e3*100)/100+" km":Math.round(r*100)/100+" m"}function sr(r){if(ut()==="imperial"){const e=r*No,t=r*$o;return e>=5280?`${t.toFixed(2)} mi (${e.toLocaleString("en",{maximumFractionDigits:0})} ft)`:`${e.toLocaleString("en",{maximumFractionDigits:1})} ft`}return r>=1e3?`${(r/1e3).toFixed(2)} km (${r.toLocaleString("en",{maximumFractionDigits:0})} m)`:`${r.toLocaleString("en",{maximumFractionDigits:1})} m`}function je(r){if(ut()==="imperial"){const e=r*qo;return e>=640?Math.round(r*jo*100)/100+" mi²":e>=1?Math.round(e*100)/100+" acres":Math.round(r*Go).toLocaleString("en")+" ft²"}return r>1e6?Math.round(r/1e6*100)/100+" km²":Math.round(r*100)/100+" m²"}function ir(r){if(ut()==="imperial"){const e=r*Go,t=r*qo,n=r*jo;return t>=640?`${n.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 r>1e6?`${(r/1e6).toFixed(2)} km² (${r.toLocaleString("en",{maximumFractionDigits:0})} m²)`:`${r.toLocaleString("en",{maximumFractionDigits:0})} m²`}function ar(r){return je(Math.PI*r*r)}function lr(r,e,t,n,o=1e-10){const i=e[0]-r[0],s=e[1]-r[1],a=n[0]-t[0],l=n[1]-t[1],c=i*l-s*a;if(Math.abs(c)<o)return null;const d=t[0]-r[0],u=t[1]-r[1],p=(d*l-u*a)/c,f=(d*s-u*i)/c;return p<-o||p>1+o||f<-o||f>1+o?null:{point:[r[0]+p*i,r[1]+p*s],t:Math.max(0,Math.min(1,p)),u:Math.max(0,Math.min(1,f))}}function zo(r){let e=0;for(let t=0,n=r.length;t<n-1;t++)e+=r[t][0]*r[t+1][1]-r[t+1][0]*r[t][1];return e/2}function It(r,e){let t=!1;for(let n=0,o=e.length-2;n<e.length-1;o=n++){const i=e[n][0],s=e[n][1],a=e[o][0],l=e[o][1];s>r[1]!=l>r[1]&&r[0]<(a-i)*(r[1]-s)/(l-s)+i&&(t=!t)}return t}function He(r,e){return(r[0]-e[0])**2+(r[1]-e[1])**2}function cr(r,e){const t=[];for(let o=0;o<e.length-1;o++)for(let i=0;i<r.length-1;i++){const s=lr(r[i],r[i+1],e[o],e[o+1],1e-10);if(!s)continue;const a=s.point;let l=!1;for(const c of t)if(He(c.point,a)<1e-6){l=!0;break}l||t.push({point:a,ringSegIdx:i,ringT:s.t,lineSegIdx:o,lineT:s.u})}return t.sort((o,i)=>o.lineSegIdx!==i.lineSegIdx?o.lineSegIdx-i.lineSegIdx:o.lineT-i.lineT),t}function dr(r,e){const t=e.map((i,s)=>({...i,origOrder:s}));t.sort((i,s)=>i.ringSegIdx!==s.ringSegIdx?i.ringSegIdx-s.ringSegIdx:i.ringT-s.ringT);const n=r.slice(),o=new Array(t.length);for(let i=t.length-1;i>=0;i--){const s=t[i],a=s.ringSegIdx+1,l=1e-6;if(He(s.point,n[s.ringSegIdx])<l){o[s.origOrder]=s.ringSegIdx;continue}if(He(s.point,n[s.ringSegIdx+1])<l){o[s.origOrder]=s.ringSegIdx+1;continue}n.splice(a,0,s.point),o[s.origOrder]=a;for(let c=i+1;c<t.length;c++)o[t[c].origOrder]>=a&&o[t[c].origOrder]++}return{ring:n,indices:o}}function Qt(r,e,t){const n=r.length-1,o=(e%n+n)%n,i=(t%n+n)%n,s=[];let a=o;for(;s.push(r[a]),a!==i;)a=(a+1)%n;return s}function eo(r,e,t){const n=[e.point],o=e.lineSegIdx,i=t.lineSegIdx;for(let s=o+1;s<=i;s++)n.push(r[s]);return He(n[n.length-1],t.point)>1e-10&&n.push(t.point),n}function to(r,e){const t=zo(r);return e&&t<0||!e&&t>0?r.slice().reverse():r}function oo(r){if(r.length<2)return r;const e=r[0],t=r[r.length-1];return He(e,t)>1e-10?[...r,e.slice()]:r}function ur(r,e){let t=1/0,n=1/0,o=-1/0,i=-1/0;for(const c of e)c[0]<t&&(t=c[0]),c[1]<n&&(n=c[1]),c[0]>o&&(o=c[0]),c[1]>i&&(i=c[1]);const s=Math.sqrt((o-t)**2+(i-n)**2)||1,a=r.slice();if(It(a[0],e)){const c=a[0],d=a[1],u=c[0]-d[0],p=c[1]-d[1],f=Math.sqrt(u*u+p*p)||1,h=s*2/f;a[0]=[c[0]+u*h,c[1]+p*h]}const l=a.length-1;if(It(a[l],e)){const c=a[l],d=a[l-1],u=c[0]-d[0],p=c[1]-d[1],f=Math.sqrt(u*u+p*p)||1,h=s*2/f;a[l]=[c[0]+u*h,c[1]+p*h]}return a}function et(r,e){const t=r[0],n=r.slice(1),o=ur(e,t),i=cr(t,o);if(i.length!==2)return console.warn(`[polygonSplit] Expected 2 intersections, got ${i.length}`),null;const[s,a]=i,{ring:l,indices:c}=dr(t,i),d=c[0],u=c[1],[p,f]=d<u?[d,u]:[u,d],h=d<u?eo(o,s,a):eo(o,a,s),m=h.slice().reverse(),g=Qt(l,p,f),y=oo([...g,...m.slice(1)]),b=Qt(l,f,p),E=oo([...b,...h.slice(1)]),L=zo(t)>0,x=to(y,L),w=to(E,L),S=[x],P=[w];for(const T of n){const j=pr(T);It(j,x)?S.push(T):P.push(T)}return[S,P]}function pr(r){let e=0,t=0;const n=r.length-1;for(let o=0;o<n;o++)e+=r[o][0],t+=r[o][1];return[e/n,t/n]}const no={success:{bg:"#10b981",icon:"✅"},error:{bg:"#ef4444",icon:"❌"},warning:{bg:"#f59e0b",icon:"⚠️"},info:{bg:"#0ea5e9",icon:""}};let Te=null;function hr(){return Te||(Te=document.createElement("div"),Te.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(Te),Te)}function O(r,e="info",t=4e3){const n=hr(),o=no[e]||no.info,i=document.createElement("div");i.style.cssText=`
background: ${o.bg};
color: #fff;
padding: 10px 18px;
border-radius: 8px;
font-family: var(--font-body, 'Exo', sans-serif);
font-size: 13px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
pointer-events: auto;
cursor: pointer;
opacity: 0;
transition: opacity 0.25s ease, transform 0.25s ease;
transform: translateY(-8px);
max-width: 420px;
text-align: center;
line-height: 1.4;
`,i.textContent=`${o.icon} ${r}`,n.appendChild(i),requestAnimationFrame(()=>{i.style.opacity="1",i.style.transform="translateY(0)"});const s=()=>{i.style.opacity="0",i.style.transform="translateY(-8px)",setTimeout(()=>i.remove(),300)};i.addEventListener("click",s),setTimeout(s,t)}const Ye=[{stroke:"#ef4444",fill:"rgba(239,68,68,0.25)"},{stroke:"#3b82f6",fill:"rgba(59,130,246,0.25)"}],fr=new M({stroke:new k({color:"#0ea5e9",width:3}),fill:new I({color:"rgba(14,165,233,0.15)"})}),gr=new M({stroke:new k({color:"#f43f5e",width:2,lineDash:[8,6]}),image:new re({radius:5,fill:new I({color:"#f43f5e"}),stroke:new k({color:"#fff",width:1.5})})});class mr extends Dt{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 N({useSpatialIndex:!1}),this._overlayLayer=new F({source:this._overlaySource,displayInLayerSwitcher:!1,style:fr})}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=n=>{n.forEach(o=>{o.getVisible()&&(o.getSource&&o.getSource()instanceof N?e.push(o.getSource()):o.getLayers&&t(o.getLayers()))})};return t(this.getMap().getLayers()),e}_handleEvent(e){if(!this.getActive())return!0;if(this._phase==="select"){if(e.type==="pointermove")return this._onSelectMove(e);if(e.type==="singleclick")return this._onSelectClick(e)}if(this._phase==="draw"&&e.type==="keydown"&&e.originalEvent?.key==="Escape")return this._cancelDraw(),!1;if(this._phase==="pick"){if(e.type==="pointermove")return this._onPickMove(e);if(e.type==="singleclick")return this._onPickClick(e);if(e.type==="keydown"&&e.originalEvent?.key==="Escape")return this._reset(),!1}return!0}_onSelectMove(e){const t=this.getMap();if(!t)return!0;this._overlaySource.clear();const n=this._closestPolygon(e);if(n){const o=n.feature.clone();this._overlaySource.addFeature(o),t.getTargetElement().style.cursor="pointer"}else t.getTargetElement().style.cursor="";return!0}_onSelectClick(e){const t=this._closestPolygon(e);if(!t)return!0;this._selectedFeature=t.feature,this._selectedSource=t.source,this._overlaySource.clear();const n=t.feature.clone();return this._overlaySource.addFeature(n),this._startDrawPhase(),!1}_closestPolygon(e){let t=null,n=this.snapDistance_+1;for(const o of this._getSources()){const i=o.getClosestFeatureToCoordinate(e.coordinate);if(!i)continue;const s=i.getGeometry();if(!s)continue;const a=s.getType();if(a!=="Polygon"&&a!=="MultiPolygon")continue;const l=s.getClosestPoint(e.coordinate),d=new se([e.coordinate,l]).getLength()/e.frameState.viewState.resolution;d<n&&(n=d,t={feature:i,source:o,coord:l})}return t}_startDrawPhase(){this._phase="draw";const e=this.getMap();e&&(e.getTargetElement().style.cursor="crosshair",this._drawInteraction=new ve({type:"LineString",style:gr}),this._drawInteraction.on("drawend",t=>{const n=t.feature.getGeometry().getCoordinates();this._performSplit(n)}),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,n=this._selectedSource,o=t.getGeometry();let i;o.getType()==="Polygon"?i=o.getCoordinates():o.getType()==="MultiPolygon"&&(i=o.getCoordinates()[0]);const s=et(i,e);if(!s){console.warn("[PolygonSplit] Split failed — line must cross the polygon boundary at exactly 2 points."),this._removeDrawInteraction(),this._startDrawPhase();return}const[a,l]=s,c=t.clone();c.setGeometry(new Ue(a)),c.setStyle(new M({stroke:new k({color:Ye[0].stroke,width:2.5}),fill:new I({color:Ye[0].fill})}));const d=t.clone();d.setGeometry(new Ue(l)),d.setStyle(new M({stroke:new k({color:Ye[1].stroke,width:2.5}),fill:new I({color:Ye[1].fill})}));const u=[c,d];if(this.dispatchEvent({type:"beforesplit",original:t,features:u}),n.dispatchEvent({type:"beforesplit",original:t,features:u}),n.removeFeature(t),n.addFeature(c),n.addFeature(d),this.dispatchEvent({type:"aftersplit",original:t,features:u}),n.dispatchEvent({type:"aftersplit",original:t,features:u}),this._removeDrawInteraction(),t.get("_layerType")==="parcel"){this._splitFeatures=u,this._phase="pick",this._overlaySource.clear();const f=this.getMap();f&&(f.getTargetElement().style.cursor=""),O("Click the polygon that should keep the original identifier.","info",5e3),this.dispatchEvent({type:"splitparcel",features:u,originalProps:t.getProperties(),source:n})}else this._reset()}_onPickMove(e){const t=this.getMap();if(!t)return!0;this._overlaySource.clear();const n=this._closestSplitPiece(e);if(n){const o=n.clone();this._overlaySource.addFeature(o),t.getTargetElement().style.cursor="pointer"}else t.getTargetElement().style.cursor="";return!0}_onPickClick(e){const t=this._closestSplitPiece(e);return t?(this.dispatchEvent({type:"splitpick",picked:t,features:this._splitFeatures}),this._reset(),!1):!0}_closestSplitPiece(e){if(!this._splitFeatures)return null;let t=null,n=this.snapDistance_+1;for(const o of this._splitFeatures){const i=o.getGeometry();if(!i)continue;const s=i.getClosestPoint(e.coordinate),l=new se([e.coordinate,s]).getLength()/e.frameState.viewState.resolution;l<n&&(n=l,t=o)}return t}_reset(){this._phase="select",this._selectedFeature=null,this._selectedSource=null,this._splitFeatures=null,this._overlaySource.clear(),this._removeDrawInteraction();const e=this.getMap();e&&(e.getTargetElement().style.cursor="")}}function Ae(r,e){return(r[0]-e[0])**2+(r[1]-e[1])**2}function Ge(r){let e=0;for(let t=0,n=r.length;t<n-1;t++)e+=r[t][0]*r[t+1][1]-r[t+1][0]*r[t][1];return e/2}function yr(r,e){let t=!1;for(let n=0,o=e.length-2;n<e.length-1;o=n++){const i=e[n][0],s=e[n][1],a=e[o][0],l=e[o][1];s>r[1]!=l>r[1]&&r[0]<(a-i)*(r[1]-s)/(l-s)+i&&(t=!t)}return t}function br(r,e){const t=Ge(r);return e&&t<0||!e&&t>0?r.slice().reverse():r}function wr(r){return r.length<2?r:Ae(r[0],r[r.length-1])>1e-10?[...r,r[0].slice()]:r}function Ce(r,e,t){const n=t[0]-e[0],o=t[1]-e[1],i=n*n+o*o;if(i<1e-20)return Ae(r,e);let s=((r[0]-e[0])*n+(r[1]-e[1])*o)/i;s=Math.max(0,Math.min(1,s));const a=e[0]+s*n,l=e[1]+s*o;return(r[0]-a)**2+(r[1]-l)**2}function ro(r,e){let t=0,n=1/0;const o=r.length-1;for(let i=0;i<o;i++){const s=Ce(e,r[i],r[(i+1)%o===0?o:i+1]);s<n&&(n=s,t=i)}return{segIdx:t,distSq:n}}function ke(r,e,t){return Ae(r,e)<t}function Xe(r,e,t){const n=e.length-1;for(let o=0;o<n;o++)if(Ce(r,e[o],e[o+1])<t)return!0;return!1}function vr(r,e,t,n,o){const i=r.length-1,s=e.length-1,a=o*o,l=r[t],c=r[(t+1)%i],d=e[n],u=e[(n+1)%s],p=Xe(l,e,a),f=Xe(c,e,a),h=Xe(d,r,a),m=Xe(u,r,a);if(!(p&&f)&&!(h&&m))return console.warn("[polygonMerge] Seed edges are not on the shared boundary"),null;let g;ke(l,u,a)&&ke(c,d,a)?g=!0:ke(l,d,a)&&ke(c,u,a)?g=!1:g=Ae(l,u)<Ae(l,d);let y=t,b=(t+1)%i,E,L;g?(E=(n+1)%s,L=n):(E=n,L=(n+1)%s);let x=i+s;for(;x-- >0;){const w=(b+1)%i,S=g?(L-1+s)%s:(L+1)%s;if(w===y||S===E)break;if(ke(r[w],e[S],a)){b=w,L=S;continue}if(Ce(r[w],e[L],e[S])<a){b=w;continue}if(Ce(e[S],r[b],r[w])<a){L=S;continue}break}for(x=i+s;x-- >0;){const w=(y-1+i)%i,S=g?(E+1)%s:(E-1+s)%s;if(w===b||S===L)break;if(ke(r[w],e[S],a)){y=w,E=S;continue}if(Ce(r[w],e[E],e[S])<a){y=w;continue}if(Ce(e[S],r[y],r[w])<a){E=S;continue}break}return{startA:y,endA:b,startB:E,endB:L,reversed:g}}function _t(r,e,t){const n=r.length-1,o=[];let i=e;for(;o.push(r[i]),!(i===t||(i=(i+1)%n,o.length>n+1)););return o}function _r(r,e,t,n,o=5){const i=r[0],s=e[0],a=r.slice(1),l=e.slice(1),c=ro(i,t),d=ro(s,n),u=vr(i,s,c.segIdx,d.segIdx,o);if(!u)return console.warn("[polygonMerge] Could not find shared boundary between polygons — seed edges are not near the other ring"),{coords:null,error:"The selected edges are not on a shared boundary. Click edges that lie on the common border between the two polygons."};const{startA:p,endA:f,startB:h,endB:m,reversed:g}=u;i.length-1,s.length-1;const y=_t(i,f,p);let b;g?b=_t(s,h,m):b=_t(s,m,h);const E=[...y,...b.slice(1)],L=o*o;E.length>2&&Ae(E[E.length-1],E[0])<L&&(E[E.length-1]=E[0].slice());const x=wr(E),w=Math.abs(Ge(i)),S=Math.abs(Ge(s)),P=Math.abs(Ge(x)),T=w+S;if(P<T*.5||P>T*1.5)return console.warn(`[polygonMerge] Area mismatch: A=${w.toFixed(1)}, B=${S.toFixed(1)}, merged=${P.toFixed(1)}, expected≈${T.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=Ge(i)>0,U=br(x,j),A=[...a,...l].filter(ee=>{const K=ee.reduce((G,ce)=>G+ce[0],0)/(ee.length-1),le=ee.reduce((G,ce)=>G+ce[1],0)/(ee.length-1);return yr([K,le],U)});return{coords:[U,...A]}}const so=new M({stroke:new k({color:"#0ea5e9",width:3}),fill:new I({color:"rgba(14,165,233,0.15)"})}),Er=new M({stroke:new k({color:"#f59e0b",width:3}),fill:new I({color:"rgba(245,158,11,0.15)"})}),xr=new M({stroke:new k({color:"#0ea5e9",width:3}),fill:new I({color:"rgba(14,165,233,0.15)"}),text:new tt({text:"A",font:"bold 22px Exo, sans-serif",fill:new I({color:"#0ea5e9"}),stroke:new k({color:"#fff",width:4}),overflow:!0})}),Sr=new M({stroke:new k({color:"#f59e0b",width:3}),fill:new I({color:"rgba(245,158,11,0.15)"}),text:new tt({text:"B",font:"bold 22px Exo, sans-serif",fill:new I({color:"#f59e0b"}),stroke:new k({color:"#fff",width:4}),overflow:!0})}),Lr=new M({stroke:new k({color:"#ec4899",width:4,lineDash:[10,6]})}),Tr=new M({stroke:new k({color:"#10b981",width:2.5}),fill:new I({color:"rgba(16,185,129,0.3)"})});class kr extends Dt{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 N({useSpatialIndex:!1}),this._highlightLayer=new F({source:this._highlightSource,displayInLayerSwitcher:!1,style:t=>t.get("_highlightStyle")||so}),this._edgeSource=new N({useSpatialIndex:!1}),this._edgeLayer=new F({source:this._edgeSource,displayInLayerSwitcher:!1,style:Lr})}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=n=>{n.forEach(o=>{o.getVisible()&&(o.getSource&&o.getSource()instanceof N?e.push(o.getSource()):o.getLayers&&t(o.getLayers()))})};return t(this.getMap().getLayers()),e}_handleEvent(e){if(!this.getActive())return!0;if(e.type==="keydown"&&e.originalEvent?.key==="Escape")return this._reset(),!1;switch(this._phase){case"select_a":if(e.type==="pointermove")return this._onSelectMove(e,null);if(e.type==="singleclick")return this._onSelectAClick(e);break;case"select_b":if(e.type==="pointermove")return this._onSelectMove(e,this._featureA);if(e.type==="singleclick")return this._onSelectBClick(e);break;case"click_edge_a":if(e.type==="pointermove")return this._onEdgeMove(e,this._featureA);if(e.type==="singleclick")return this._onEdgeAClick(e);break;case"click_edge_b":if(e.type==="pointermove")return this._onEdgeMove(e,this._featureB);if(e.type==="singleclick")return this._onEdgeBClick(e);break}return!0}_onSelectMove(e,t){const n=this.getMap();if(!n)return!0;this._highlightSource.clear(),this._edgeSource.clear(),this._rebuildHighlights();const o=this._closestPolygon(e,t);if(o){const i=this._phase==="select_a"?so:Er,s=o.feature.clone();s.set("_highlightStyle",i),this._highlightSource.addFeature(s),n.getTargetElement().style.cursor="pointer"}else n.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 n=null,o=this.snapDistance_+1;for(const i of this._getSources()){const s=i.getClosestFeatureToCoordinate(e.coordinate);if(!s||t&&s===t)continue;const a=s.getGeometry();if(!a)continue;const l=a.getType();if(l!=="Polygon"&&l!=="MultiPolygon")continue;const c=a.getClosestPoint(e.coordinate),u=new se([e.coordinate,c]).getLength()/e.frameState.viewState.resolution;u<o&&(o=u,n={feature:s,source:i,coord:c})}return n}_onEdgeMove(e,t){const n=this.getMap();if(!n)return!0;this._edgeSource.clear();const o=this._closestEdgeSegment(t,e);if(o){const i=new ne(new se([o.segStart,o.segEnd]));this._edgeSource.addFeature(i),n.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 n=e.getGeometry();let o;if(n.getType()==="Polygon")o=n.getCoordinates()[0];else if(n.getType()==="MultiPolygon")o=n.getCoordinates()[0][0];else return null;const i=t.frameState.viewState.resolution;let s=1/0,a=null;const l=o.length-1;for(let c=0;c<l;c++){const d=o[c],u=o[c+1],p=u[0]-d[0],f=u[1]-d[1],h=p*p+f*f;if(h<1e-20)continue;let m=((t.coordinate[0]-d[0])*p+(t.coordinate[1]-d[1])*f)/h;m=Math.max(0,Math.min(1,m));const g=d[0]+m*p,y=d[1]+m*f,b=Math.sqrt((t.coordinate[0]-g)**2+(t.coordinate[1]-y)**2)/i;b<s&&(s=b,a={segStart:d,segEnd:u})}return s<=this.snapDistance_?a:null}_performMerge(){const e=this._featureA,t=this._featureB,n=this._sourceA,o=this._sourceB,i=e.getGeometry(),s=t.getGeometry(),a=i.getType()==="Polygon"?i.getCoordinates():i.getCoordinates()[0],l=s.getType()==="Polygon"?s.getCoordinates():s.getCoordinates()[0],c=_r(a,l,this._edgeClickA,this._edgeClickB,this.tolerance_);if(!c.coords){O(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 Ue(c.coords)),d.setStyle(Tr);const u={type:"beforemerge",original:[e,t],merged:d};this.dispatchEvent(u),n.dispatchEvent({...u}),o!==n&&o.dispatchEvent({...u}),n.removeFeature(e),o.removeFeature(t),n.addFeature(d);const p={type:"aftermerge",original:[e,t],merged:d};this.dispatchEvent(p),n.dispatchEvent({...p}),o!==n&&o.dispatchEvent({...p});const f=e.get("_layerType")==="parcel",h=t.get("_layerType")==="parcel";f&&h?(this.dispatchEvent({type:"mergedparcel",merged:d,propsA:e.getProperties(),propsB:t.getProperties(),coordinate:this._edgeClickA}),O("Polygons merged — choose which identifier to keep.","success")):O("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",xr),t.set("_permanent",!0),this._highlightSource.addFeature(t)}if(this._featureB){const t=this._featureB.clone();t.set("_highlightStyle",Sr),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 Pr(r,e){return(r[0]-e[0])**2+(r[1]-e[1])**2}function io(r){let e=0;for(let t=0,n=r.length;t<n-1;t++)e+=r[t][0]*r[t+1][1]-r[t+1][0]*r[t][1];return e/2}function Ne(r){let e=Math.abs(io(r[0]));for(let t=1;t<r.length;t++)e-=Math.abs(io(r[t]));return e}function Mr(r){const e=r.length-1;let t=-1,n=0;for(let c=0;c<e;c++){const d=Pr(r[c],r[c+1]);d>t&&(t=d,n=c)}const o=r[n],i=r[n+1],s=Math.sqrt(t),a=[(i[0]-o[0])/s,(i[1]-o[1])/s],l=[-a[1],a[0]];return{p0:o,p1:i,along:a,perp:l}}function Et(r,e,t,n,o){const i=r[0]+n*e[0],s=r[1]+n*e[1];return[[i-o*t[0],s-o*t[1]],[i+o*t[0],s+o*t[1]]]}function Pe(r,e,t){const n=r[0],o=n.length-1;let i=0,s=0;for(let c=0;c<o;c++)i+=n[c][0],s+=n[c][1];const a=i/o-e[0],l=s/o-e[1];return a*t[0]+l*t[1]}function Ir(r,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:[r]};const n=r[0];if(Ne(r)<1e-6)return{pieces:null,error:"Polygon has no measurable area."};let i,s,a;if(t&&t.length===2){i=t[0];const g=t[1][0]-t[0][0],y=t[1][1]-t[0][1],b=Math.sqrt(g*g+y*y);if(b<1e-10)return{pieces:null,error:"Selected edge has zero length."};s=[g/b,y/b],a=[-s[1],s[0]]}else{const g=Mr(n);i=g.p0,s=g.along,a=g.perp}const l=i,c=n.length-1;for(let g=0;g<c;g++){const y=n[g][0]-l[0],b=n[g][1]-l[1];y*s[0]+b*s[1]}let d=1/0,u=-1/0;for(let g=0;g<c;g++){const y=n[g][0]-l[0],b=n[g][1]-l[1],E=y*a[0]+b*a[1];E<d&&(d=E),E>u&&(u=E)}const p=(u-d)*1.5,f=[];let h=r,m=e;for(let g=0;g<e-1;g++){const y=Ne(h),b=y/m,E=h[0],L=E.length-1;let x=1/0,w=-1/0;for(let V=0;V<L;V++){const A=E[V][0]-l[0],ee=E[V][1]-l[1],K=A*s[0]+ee*s[1];K<x&&(x=K),K>w&&(w=K)}let S=x,P=w,T=null,j=null,U=1/0;for(let V=0;V<40;V++){const A=(S+P)/2,ee=Et(l,s,a,A,p),K=et(h,ee);if(!K){const C=(P-S)*.01,q=Et(l,s,a,A+C,p),J=et(h,q);if(J){const[me,ye]=J,Fe=Pe(me,l,s),Oe=Pe(ye,l,s),Re=Fe<Oe?me:ye,mt=Fe<Oe?ye:me,yt=Ne(Re),Be=Math.abs(yt-b);Be<U&&(U=Be,T=Re,j=mt)}const gt=Et(l,s,a,A-C,p),de=et(h,gt);if(de){const[me,ye]=de,Fe=Pe(me,l,s),Oe=Pe(ye,l,s),Re=Fe<Oe?me:ye,mt=Fe<Oe?ye:me,yt=Ne(Re),Be=Math.abs(yt-b);Be<U&&(U=Be,T=Re,j=mt)}S=A;continue}const[le,G]=K,ce=Pe(le,l,s),De=Pe(G,l,s),D=ce<De?le:G,H=ce<De?G:le,z=Ne(D),X=Math.abs(z-b);if(X<U&&(U=X,T=D,j=H),X/y<.001)break;z<b?S=A:P=A}if(!T||!j)return{pieces:null,error:`Could not find a valid cut for piece ${g+1} of ${e}. The polygon shape may be too irregular for equal division.`};f.push(T),h=j,m--}return f.push(h),{pieces:f}}const Cr=new M({stroke:new k({color:"#0ea5e9",width:3}),fill:new I({color:"rgba(14,165,233,0.15)"})}),Ar=new M({stroke:new k({color:"#8b5cf6",width:4,lineDash:[10,6]})});function Dr(r){const e=[];for(let t=0;t<r;t++){const n=Math.round(t*360/r);e.push({stroke:`hsl(${n}, 70%, 45%)`,fill:`hsla(${n}, 70%, 55%, 0.25)`})}return e}class Fr extends Dt{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 N({useSpatialIndex:!1}),this._overlayLayer=new F({source:this._overlaySource,displayInLayerSwitcher:!1,style:Cr}),this._edgeSource=new N({useSpatialIndex:!1}),this._edgeLayer=new F({source:this._edgeSource,displayInLayerSwitcher:!1,style:Ar})}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=n=>{n.forEach(o=>{o.getVisible()&&(o.getSource&&o.getSource()instanceof N?e.push(o.getSource()):o.getLayers&&t(o.getLayers()))})};return t(this.getMap().getLayers()),e}_handleEvent(e){if(!this.getActive())return!0;if(e.type==="keydown"&&e.originalEvent?.key==="Escape")return this._phase==="form"?this.cancelDivide():this._reset(),!1;if(this._phase==="select"){if(e.type==="pointermove")return this._onSelectMove(e);if(e.type==="singleclick")return this._onSelectClick(e)}if(this._phase==="edge"){if(e.type==="pointermove")return this._onEdgeMove(e);if(e.type==="singleclick")return this._onEdgeClick(e)}if(this._phase==="pick"){if(e.type==="pointermove")return this._onPickMove(e);if(e.type==="singleclick")return this._onPickClick(e)}return!0}_onSelectMove(e){const t=this.getMap();if(!t)return!0;this._overlaySource.clear();const n=this._closestPolygon(e);if(n){const o=n.feature.clone();this._overlaySource.addFeature(o),t.getTargetElement().style.cursor="pointer"}else t.getTargetElement().style.cursor="";return!0}_onSelectClick(e){const t=this._closestPolygon(e);if(!t)return!0;this._selectedFeature=t.feature,this._selectedSource=t.source,this._overlaySource.clear();const n=t.feature.clone();return n.set("_permanent",!0),this._overlaySource.addFeature(n),this._phase="edge",O("Click the edge to divide along.","info",3e3),!1}_closestPolygon(e){let t=null,n=this.snapDistance_+1;for(const o of this._getSources()){const i=o.getClosestFeatureToCoordinate(e.coordinate);if(!i)continue;const s=i.getGeometry();if(!s)continue;const a=s.getType();if(a!=="Polygon"&&a!=="MultiPolygon")continue;const l=s.getClosestPoint(e.coordinate),d=new se([e.coordinate,l]).getLength()/e.frameState.viewState.resolution;d<n&&(n=d,t={feature:i,source:o})}return t}_onEdgeMove(e){const t=this.getMap();if(!t)return!0;this._edgeSource.clear();const n=this._closestEdgeSegment(this._selectedFeature,e);if(n){const o=new ne(new se([n.segStart,n.segEnd]));this._edgeSource.addFeature(o),t.getTargetElement().style.cursor="crosshair"}else t.getTargetElement().style.cursor="";return!0}_onEdgeClick(e){const t=this._closestEdgeSegment(this._selectedFeature,e);if(!t)return!0;this._selectedEdge=[t.segStart,t.segEnd],this._edgeSource.clear(),this._phase="form";const o=this._selectedFeature.getGeometry().getExtent(),i=[(o[0]+o[2])/2,(o[1]+o[3])/2];return this.dispatchEvent({type:"divideform",feature:this._selectedFeature,source:this._selectedSource,coordinate:i}),!1}_closestEdgeSegment(e,t){const n=e.getGeometry();let o;if(n.getType()==="Polygon")o=n.getCoordinates()[0];else if(n.getType()==="MultiPolygon")o=n.getCoordinates()[0][0];else return null;const i=t.frameState.viewState.resolution;let s=1/0,a=null;const l=o.length-1;for(let c=0;c<l;c++){const d=o[c],u=o[c+1],p=u[0]-d[0],f=u[1]-d[1],h=p*p+f*f;if(h<1e-20)continue;let m=((t.coordinate[0]-d[0])*p+(t.coordinate[1]-d[1])*f)/h;m=Math.max(0,Math.min(1,m));const g=d[0]+m*p,y=d[1]+m*f,b=Math.sqrt((t.coordinate[0]-g)**2+(t.coordinate[1]-y)**2)/i;b<s&&(s=b,a={segStart:d,segEnd:u})}return s<=this.snapDistance_?a:null}performDivide(e){if(this._phase!=="form"||!this._selectedFeature)return;const t=this._selectedFeature,n=this._selectedSource,o=t.getGeometry();let i;o.getType()==="Polygon"?i=o.getCoordinates():o.getType()==="MultiPolygon"&&(i=o.getCoordinates()[0]);const s=Ir(i,e,this._selectedEdge);if(!s.pieces){O(s.error||"Division failed.","error",5e3),this._reset();return}const a=Dr(e),l=s.pieces.map((p,f)=>{const h=t.clone();return h.setGeometry(new Ue(p)),h.setStyle(new M({stroke:new k({color:a[f].stroke,width:2.5}),fill:new I({color:a[f].fill})})),h}),c={type:"beforedivide",original:t,features:l};this.dispatchEvent(c),n.dispatchEvent({...c}),n.removeFeature(t);for(const p of l)n.addFeature(p);const d={type:"afterdivide",original:t,features:l};this.dispatchEvent(d),n.dispatchEvent({...d}),t.get("_layerType")==="parcel"?(this._dividedFeatures=l,this._phase="pick",O("Click the polygon that should keep the original identifier.","info",5e3),this.dispatchEvent({type:"dividedparcel",features:l,originalProps:t.getProperties(),source:n})):(O(`Polygon divided into ${e} equal pieces.`,"success"),this._reset())}_onPickMove(e){const t=this.getMap();if(!t)return!0;this._overlaySource.clear();const n=this._closestDividedPiece(e);if(n){const o=n.clone();this._overlaySource.addFeature(o),t.getTargetElement().style.cursor="pointer"}else t.getTargetElement().style.cursor="";return!0}_onPickClick(e){const t=this._closestDividedPiece(e);return t?(this.dispatchEvent({type:"dividepick",picked:t,features:this._dividedFeatures}),this._reset(),!1):!0}_closestDividedPiece(e){if(!this._dividedFeatures)return null;let t=null,n=this.snapDistance_+1;for(const o of this._dividedFeatures){const i=o.getGeometry();if(!i)continue;const s=i.getClosestPoint(e.coordinate),l=new se([e.coordinate,s]).getLength()/e.frameState.viewState.resolution;l<n&&(n=l,t=o)}return t}cancelDivide(){this.dispatchEvent({type:"dividecancel"}),this._reset()}_reset(){this._phase="select",this._selectedFeature=null,this._selectedSource=null,this._selectedEdge=null,this._dividedFeatures=null,this._overlaySource.clear(),this._edgeSource.clear();const e=this.getMap();e&&(e.getTargetElement().style.cursor="")}}class Or{constructor(e,t={}){this.options=t,this.markerSource=new N,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=a=>{const l=this.categoryEmojis[a];return l?l.emoji:"📍"},this.getCategoryOptionsHtml=()=>Object.entries(this.categoryEmojis).map(([a,{emoji:l,label:c}])=>`<option value="${a}">${l} ${c}</option>`).join(`
`),this.createEmojiStyle=(a,l=24)=>new M({text:new tt({text:a,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[a,{emoji:l}]of Object.entries(this.categoryEmojis))this.categoryStyles[a]=this.createEmojiStyle(l,32);const n=this.createBaseLayers(t.basemap||"topo");this.markersLayer=new F({title:"Markers",source:this.markerSource,style:a=>this.getFeatureStyle(a),visible:!1}),this.overlayGroup=new be({title:"Overlays"}),this.map=new qt({target:e,layers:[n,this.markersLayer,this.overlayGroup],view:new on({center:te(t.center||[0,0]),zoom:t.zoom||2,minZoom:t.minZoom||2,maxZoom:t.maxZoom||19})});const o=new un({collapsed:!0,mouseover:!0,extent:!0,trash:!1,oninfo:null});this.map.addControl(o),queueMicrotask(()=>{const a=o.element?.querySelector(":scope > button");if(a){const l="/".replace(/\/?$/,"/");a.style.backgroundImage=`url('${l}app-icons/luspa-72x72.png')`}});let i=!1;o.on("drawlist",a=>{this._decorateLayerListItem(a.layer,a.li),i||(i=!0,queueMicrotask(()=>{i=!1,this._refreshLayerSwitcherChrome(o)}))}),this.map.getLayers().on("change",()=>{this._refreshLayerSwitcherChrome(o)}),this._wireLayerSwitcherVisibilityHooks(o),this._createAddLayerDialog(),this._createLegendPanel(),this.scaleBar=new nn({bar:!0,steps:4,text:!0,minWidth:140}),this.map.addControl(this.scaleBar),this._initGpsRendering(),this._createLocationControl(),this._createBaseMapPicker();const s=new pn({placeholder:"Search location...",typing:300,minLength:3,maxItems:10,collapsed:!0});this.map.addControl(s),s.on("select",a=>{const l=a.search;if(l){const c=parseFloat(l.lon),d=parseFloat(l.lat),u=[c,d],p=te(u);this.navigateTo(c,d,14);const f={coordinate:p,lonLat:u,name:l.display_name||l.name||"Unknown",searchResult:l};this.searchSelectCallbacks.forEach(h=>h(f))}}),this.searchNominatim=s,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 N,this.drawingsLayer=new F({title:"sketches",source:this.drawingsSource,style:new M({stroke:new k({color:"#f59e0b",width:2.5}),fill:new I({color:"rgba(245,158,11,0.15)"}),image:new re({radius:6,fill:new I({color:"#f59e0b"}),stroke:new k({color:"#fff",width:1.5})})})}),this._drawingsGroup=new be({title:"Drawings",layers:[this.drawingsLayer]});const e=this.map.getLayers(),t=e.getLength()-1;e.insertAt(t,this._drawingsGroup),this._selectInteraction=new rn({condition:sn,filter:(h,m)=>!!m,layers:h=>h instanceof F}),this._selectInteraction.setActive(!1),this.map.addInteraction(this._selectInteraction),this._modifyInteraction=new hn({features:this._selectInteraction.getFeatures()}),this._modifyInteraction.setActive(!1),this._undoRedo=new fn,this.map.addInteraction(this._undoRedo),this.editBar=new Tt({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),this._setupVertexOverlay();const n=new Ut({group:!0,className:"ol-editbar-actions",controls:[new $e({html:'<i class="bi bi-arrow-counterclockwise"></i>',className:"ol-undo",title:"Undo",handleClick:()=>{this._undoRedo.hasUndo()&&this._undoRedo.undo()}}),new $e({html:'<i class="bi bi-arrow-clockwise"></i>',className:"ol-redo",title:"Redo",handleClick:()=>{this._undoRedo.hasRedo()&&this._undoRedo.redo()}}),new $e({html:'<i class="bi bi-floppy"></i>',className:"ol-save",title:"Save drawings",handleClick:()=>{this.dispatchEditEvent("save")}})]});this.editBar.addControl(n),this._lineSplitInteraction=new gn,this._polygonSplitInteraction=new mr,this.map.addInteraction(this._lineSplitInteraction),this.map.addInteraction(this._polygonSplitInteraction),this._lineSplitInteraction.setActive(!1),this._polygonSplitInteraction.setActive(!1),this._polygonSplitInteraction.on("splitpick",h=>{const m=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"];for(const g of h.features)if(g!==h.picked)for(const y of m)g.get(y)!==void 0&&g.set(y,"")}),this._polygonDivideInteraction=new Fr,this.map.addInteraction(this._polygonDivideInteraction),this._polygonDivideInteraction.setActive(!1);const o=new fe({html:'<i class="bi bi-slash-lg"></i>',className:"ol-split-line",title:"Split Lines",name:"SplitLine",interaction:this._lineSplitInteraction,autoActivate:!0}),i=new fe({html:'<i class="bi bi-scissors"></i>',className:"ol-split-polygon",title:"Split Polygons",name:"SplitPolygon",interaction:this._polygonSplitInteraction}),s=new fe({html:'<i class="bi bi-grid-3x3-gap"></i>',className:"ol-split-divide",title:"Divide Polygon",name:"DividePolygon",interaction:this._polygonDivideInteraction}),a=new Ut({toggleOne:!0,autoDeactivate:!0,controls:[o,i,s]}),l=new fe({className:"ol-split",title:"Split",name:"Split",bar:a,onToggle:h=>{h||(this._lineSplitInteraction.setActive(!1),this._polygonSplitInteraction.setActive(!1),this._polygonDivideInteraction.setActive(!1))}});this.editBar.addControl(l),this._polygonDivideInteraction.on("divideform",h=>{this.showDividePopup(h.feature,h.source,h.coordinate)}),this._polygonDivideInteraction.on("dividecancel",()=>{this.hideDividePopup()}),this._polygonDivideInteraction.on("dividepick",h=>{const m=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"];for(const g of h.features)if(g!==h.picked)for(const y of m)g.get(y)!==void 0&&g.set(y,"")}),this._polygonMergeInteraction=new kr,this.map.addInteraction(this._polygonMergeInteraction),this._polygonMergeInteraction.setActive(!1);const c=new fe({html:'<i class="bi bi-union"></i>',className:"ol-merge",title:"Merge Polygons",name:"Merge",interaction:this._polygonMergeInteraction});this.editBar.addControl(c),this._polygonMergeInteraction.on("mergedparcel",h=>{this.showMergeIdentifierPopup(h.merged,h.propsA,h.propsB,h.coordinate)});const d=this.editBar.element;if(d&&n.element&&n.element.parentNode===d){const h=document.createElement("div");h.className="ol-editbar-break",d.insertBefore(h,n.element)}this._snapGuidesEnabled=localStorage.getItem("snap-guides-enabled")==="1",this._snapGuides=new mn({pixelTolerance:10,vectorClass:an}),this.map.addInteraction(this._snapGuides);const u=["DrawPoint","DrawLine","DrawPolygon","DrawHole","DrawRegular"];for(const h of u){const m=this.editBar.getInteraction(h);m&&m.on("change:active",()=>{m.getActive()&&this._snapGuides.setDrawInteraction(m)})}this._modifyInteraction&&this._snapGuides.setModifyInteraction(this._modifyInteraction);const p=new $e({html:'<i class="bi bi-magnet"></i>',className:"ol-snap-toggle"+(this._snapGuidesEnabled?" ol-active":""),title:"Toggle Snap Guides",handleClick:()=>{this._snapGuidesEnabled=!this._snapGuidesEnabled,localStorage.setItem("snap-guides-enabled",this._snapGuidesEnabled?"1":"0"),p.element.classList.toggle("ol-active",this._snapGuidesEnabled),this._snapGuides&&this._snapGuides.setActive(this._snapGuidesEnabled&&this._editBarActive),console.log("[MapView] Snap guides:",this._snapGuidesEnabled?"ON":"OFF")}});this._snapToggleBtn=p,n.addControl(p),this.setEditMode(!1),this._drawingsGroup.on("change:visible",()=>{const h=this._drawingsGroup.getVisible();this.setEditMode(h)}),("ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0)&&(this.touchCursor=new yn({className:"ol-editbar-cursor"}),this.map.addInteraction(this.touchCursor),this.touchCursor.setActive(!1),console.log("[MapView] Touch device detected — TouchCursor added")),this.drawingsSource.on("addfeature",h=>{const m=h.feature,g=m.getGeometry();if(!g||g.getType()!=="Polygon")return;const y=g.getInteriorPoint().getCoordinates();this.showDrawnPolygonPopup(m,y)}),console.log("[MapView] EditBar initialised with Drawings group, UndoRedo and SnapGuides (default:",this._snapGuidesEnabled?"ON":"OFF",")")}dispatchEditEvent(e){if(!this._editEventListeners)return;const t=this._editEventListeners[e];t&&t.forEach(n=>n())}onEditEvent(e,t){this._editEventListeners||(this._editEventListeners={}),this._editEventListeners[e]||(this._editEventListeners[e]=[]),this._editEventListeners[e].push(t)}setEditMode(e){this._editBarActive=!!e,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._vertexOverlaySource&&this._vertexOverlaySource.clear(),console.log("[MapView] Edit mode:",this._editBarActive?"ON":"OFF")}isEditMode(){return this._editBarActive}_setupVertexOverlay(){this._vertexOverlaySource=new N,this._vertexOverlayLayer=new F({title:"__vertex_highlight__",source:this._vertexOverlaySource,zIndex:990,style:new M({image:new re({radius:4,fill:new I({color:"rgba(14,165,233,0.85)"}),stroke:new k({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())}_refreshVertexOverlay(){if(!this._vertexOverlaySource)return;if(this._vertexOverlaySource.clear(),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 n=t.getGeometry();if(!n)continue;const o=n.getType();if(!["Polygon","MultiPolygon","LineString","MultiLineString"].includes(o))continue;const i=this._collectAllVertices(n);for(const s of i)this._vertexOverlaySource.addFeature(new ne(new We(s)));t.on("change",this._onSelectedFeatureGeomChange),this._vertexTrackedFeatures.add(t)}}_collectAllVertices(e){const t=[],n=a=>Array.isArray(a)&&typeof a[0]=="number",o=(a,l)=>{const c=l&&a.length>1?a.length-1:a.length;for(let d=0;d<c;d++)t.push(a[d])},i=e.getType(),s=e.getCoordinates();switch(i){case"Polygon":for(const l of s)o(l,!0);break;case"MultiPolygon":for(const l of s)for(const c of l)o(c,!0);break;case"LineString":o(s,!1);break;case"MultiLineString":for(const l of s)o(l,!1);break;default:const a=l=>{if(n(l))t.push(l);else if(Array.isArray(l))for(const c of l)a(c)};a(s)}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 he({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 n=this.map.forEachFeatureAtPixel(t.pixel,o=>o.get("name")?o:null);n&&n!==e?(e=n,this.showPopup(n,t.coordinate)):!n&&e&&(e=null,this.hidePopup()),this.map.getTargetElement().style.cursor=n?"pointer":""}),this.map.getTargetElement().addEventListener("mouseleave",()=>{this.hidePopup(),e=null})}showPopup(e,t){const n=e.get("name")||"Unnamed",o=e.get("category")||"default",i=e.get("description"),s=e.get("lon"),a=e.get("lat");let c=`
<div style="font-weight: 600; font-size: 14px; margin-bottom: 6px;">
${this.getEmoji(o)} ${this.escapeHtml(n)}
</div>
`;const u={water:"#3b82f6",school:"#f59e0b",health:"#ef4444",market:"#8b5cf6",default:"#2d5016",other:"#6b7280"}[o]||"#6b7280";c+=`
<div style="margin-bottom: 6px;">
<span style="
background: ${u}20;
color: ${u};
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
">${o}</span>
</div>
`,i&&(c+=`
<div style="color: var(--muted-foreground, #7a7a7a); font-size: 12px; margin-bottom: 6px; line-height: 1.4;">
${this.escapeHtml(i)}
</div>
`),s!==void 0&&a!==void 0&&(c+=`
<div style="color: var(--muted-foreground, #7a7a7a); font-size: 11px; font-family: monospace;">
${Number(s).toFixed(5)}, ${Number(a).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 he({element:this.infoPopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.infoPopup)}showInfoPopup(e,t,n={}){const{title:o="Feature Info",color:i="#e11d48"}=n,s=e.getProperties(),a=e.getGeometry(),l=a.getType(),c=["geometry","_layerType"];let d="";for(const[p,f]of Object.entries(s))c.includes(p)||f===void 0||f===null||(d+=`
<tr>
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">${this.escapeHtml(p)}</td>
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${this.escapeHtml(String(f))}</td>
</tr>
`);if(l==="Polygon"||l==="MultiPolygon"){const p=qe(a,{projection:"EPSG:3857"}),f=ir(p);d+=`
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">area</td>
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${f}</td>
</tr>
`}else if(l==="LineString"||l==="MultiLineString"){const p=ot(a,{projection:"EPSG:3857"}),f=sr(p);d+=`
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">length</td>
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${f}</td>
</tr>
`}else if(l==="Point"){const p=Le(a.getCoordinates()),f=p[0].toFixed(6),h=p[1].toFixed(6);d+=`
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">longitude</td>
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${f}</td>
</tr>
<tr>
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">latitude</td>
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${h}</td>
</tr>
`}const u=`
<div style="background:${i};color:#fff;padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;border-radius:10px 10px 0 0;">
<span>${this.escapeHtml(o)}</span>
<button id="info-popup-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">&times;</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,n){const o=[];if(e.length>0&&o.push({label:"Parcels",value:String(e.length),color:"#0ea5e9"}),t.length>0){const i=t.map(s=>s.get("colzonename")||s.get("zone_name")||s.get("name")||"unnamed");o.push({label:"Zones",value:String(t.length),color:"#7c3aed"}),o.push({label:"Zone Names",value:i.map(s=>this.escapeHtml(s)).join(", "),color:"#7c3aed"})}for(const[i,s]of Object.entries(n))o.push({label:this.escapeHtml(i),value:`${s.length} feature(s)`});return o.length===0&&o.push({label:"",value:"No intersecting features found",empty:!0}),o}_buildAnalysisPopupHtml(e,t,n){let o="";for(const i of n){if(i.empty){o+=`
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
<td colspan="2" style="padding:8px;color:#999;text-align:center;font-style:italic;">${i.value}</td>
</tr>`;continue}const s=i.color||"var(--muted-foreground, #7a7a7a)",a=i._first?"":"border-top:1px solid var(--border, #1e1a4b1f);";o+=`
<tr style="${a}">
<td style="padding:4px 8px;font-weight:600;color:${s};white-space:nowrap;">${i.label}</td>
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${i.value}</td>
</tr>`}return`
<div 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;">
<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;">&times;</button>
</div>
<div style="padding:8px 4px;overflow-y:auto;flex:1 1 auto;min-height:0;">
<table style="width:100%;border-collapse:collapse;font-size:13px;">
${o}
</table>
</div>
<div style="padding:2px 8px 8px;text-align:right;flex-shrink:0;border-top:1px solid var(--border, #1e1a4b1f);">
<button id="info-popup-export-pdf"
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 PDF
</button>
</div>`}_showAnalysisPopup(e,t,n,o){this.infoPopupElement.innerHTML=this._buildAnalysisPopupHtml(e,t,n),this.infoPopup.setPosition(o),this.infoPopupElement.querySelector("#info-popup-close").addEventListener("click",()=>{this.hideInfoPopup()}),this.infoPopupElement.querySelector("#info-popup-export-pdf")?.addEventListener("click",()=>{const i=n.filter(s=>!s.empty).map(s=>({label:s.label,value:s.value.replace(/<[^>]*>/g,"")}));dt(async()=>{const{exportAnalysisPDF:s}=await import("./pdf-export-vzOHm8wb.js");return{exportAnalysisPDF:s}},__vite__mapDeps([0,1,2,3])).then(({exportAnalysisPDF:s})=>{s({title:t,rows:i})}).catch(s=>{console.error("[MapView] PDF export failed:",s)})})}showCircleIntersectionPopup(e,t){const n=e.getGeometry();if(!n||typeof n.getCenter!="function")return;const o=ln(n,64),i=o.getExtent(),s=e.get("_radius")||n.getRadius(),a=[],l=[],c={},d=g=>{const y=g.getGeometry();if(!y)return!1;const b=y.getExtent();return b[2]<i[0]||b[0]>i[2]||b[3]<i[1]||b[1]>i[3]?!1:o.intersectsExtent(b)&&this._geometriesIntersect(o,y)},u=(g,y)=>{g.getLayers().forEach(b=>{if(b instanceof be)u(b,b.get("title")||y);else if(b instanceof F&&b.getVisible()){const E=b.get("title")||y||"Unknown",L=b.getSource();if(!L)return;const x=L.getFeaturesInExtent(i);for(const w of x){const S=w.get("_layerType");S==="measure_circle"||S==="measure_circle_radius"||d(w)&&(S==="parcel"?a.push(w):S==="collector_zone"?l.push(w):(c[E]||(c[E]=[]),c[E].push(w)))}}})};u(this.overlayGroup,"Overlays");const p=lt(s),f=Math.PI*s*s,h=je(f),m=[{label:"Radius",value:p,_first:!0},{label:"Area",value:h},...this._collectIntersectionRows(a,l,c)];this._showAnalysisPopup("⭕","Circle Analysis",m,t)}showAreaIntersectionPopup(e,t){const n=e.getGeometry();if(!n)return;const o=n.getExtent(),i=qe(n,{projection:"EPSG:3857"}),s=je(i),a=ot(n,{projection:"EPSG:3857"}),l=lt(a),c=[],d=[],u={},p=m=>{const g=m.getGeometry();if(!g)return!1;const y=g.getExtent();return y[2]<o[0]||y[0]>o[2]||y[3]<o[1]||y[1]>o[3]?!1:n.intersectsExtent(y)&&this._geometriesIntersect(n,g)},f=(m,g)=>{m.getLayers().forEach(y=>{if(y instanceof be)f(y,y.get("title")||g);else if(y instanceof F&&y.getVisible()){const b=y.get("title")||g||"Unknown",E=y.getSource();if(!E)return;const L=E.getFeaturesInExtent(o);for(const x of L){const w=x.get("_layerType");w==="measure_area"||w==="measure_circle"||w==="measure_circle_radius"||p(x)&&(w==="parcel"?c.push(x):w==="collector_zone"?d.push(x):(u[b]||(u[b]=[]),u[b].push(x)))}}})};f(this.overlayGroup,"Overlays");const h=[{label:"Area",value:s,_first:!0},{label:"Perimeter",value:l},...this._collectIntersectionRows(c,d,u)];this._showAnalysisPopup("📐","Area Analysis",h,t)}_geometriesIntersect(e,t){const n=t.getType();if(n==="Polygon"||n==="MultiPolygon"){const o=t.getFlatCoordinates(),i=t.getStride();for(let l=0;l<o.length;l+=i)if(e.intersectsCoordinate([o[l],o[l+1]]))return!0;const s=e.getFlatCoordinates(),a=e.getStride();for(let l=0;l<s.length;l+=a)if(t.intersectsCoordinate([s[l],s[l+1]]))return!0;return!1}if(n==="Point")return e.intersectsCoordinate(t.getCoordinates());if(n==="LineString"||n==="MultiLineString"){const o=t.getFlatCoordinates(),i=t.getStride();for(let s=0;s<o.length;s+=i)if(e.intersectsCoordinate([o[s],o[s+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 he({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 n=e.getProperties(),o=["geometry","_layerType"];let i="";for(const[l,c]of Object.entries(n)){if(o.includes(l))continue;const d=c==null?"":String(c),u=this.escapeHtml(l),p=this.escapeHtml(d);i+=`
<div style="margin-bottom:8px;">
<label style="display:block;font-size:11px;font-weight:600;color:var(--muted-foreground, #7a7a7a);margin-bottom:2px;">${u}</label>
<input type="text" name="${u}" value="${p}"
style="width:100%;padding:6px 8px;border:1px solid var(--border, #1e1a4b1f);border-radius:4px;font-size:13px;color:var(--foreground, #1e1a4b);background:var(--muted, #f2f4f7);min-height:34px;"
/>
</div>
`}const s=`
<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;">&times;</button>
</div>
<form class="parcel-edit-form" style="padding:10px 12px;overflow-y:auto;flex:1;">
${i}
<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=s,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 a=this.parcelEditElement.querySelector(".parcel-edit-form");a.addEventListener("submit",l=>{l.preventDefault();const c=new FormData(a),d={};for(const[u,p]of c.entries())d[u]=p;d._layerType="parcel";for(const[u,p]of Object.entries(d))this._parcelEditFeature.set(u,p);for(const u of this._parcelEditCallbacks)u(this._parcelEditFeature,d);this.hideParcelEditPopup()})}hideParcelEditPopup(){this.parcelEditPopup.setPosition(void 0),this._parcelEditFeature=null}onParcelEdit(e){this._parcelEditCallbacks.push(e)}createMergePopup(){this.mergePopupElement=document.createElement("div"),this.mergePopupElement.className="map-merge-popup",this.mergePopupElement.style.cssText=`
position: absolute;
background: var(--card, #fff);
color: var(--card-foreground, #1e1a4b);
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
font-family: var(--font-body, 'Exo', sans-serif);
font-size: 13px;
min-width: 280px;
max-width: 360px;
z-index: 1002;
border: 2px solid #10b981;
overflow: hidden;
display: flex;
flex-direction: column;
`,this.mergePopup=new he({element:this.mergePopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.mergePopup)}showMergeIdentifierPopup(e,t,n,o){const i=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"],s=h=>{for(const m of i)if(h[m]!==void 0&&h[m]!==null&&String(h[m]).trim())return{field:m,value:String(h[m])};return{field:"id",value:"Unknown"}},a=s(t),l=s(n),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;">&times;</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(a.field)}: ${this.escapeHtml(a.value)}</div>
</div>
</label>
<label style="display:flex;align-items:center;padding:10px;border:2px solid var(--border, #1e1a4b1f);border-radius:8px;cursor:pointer;margin-bottom:12px;transition:border-color 0.15s;">
<input type="radio" name="merge-choice" value="B"
style="margin-right:10px;accent-color:#f59e0b;width:16px;height:16px;" />
<div>
<div style="font-weight:600;color:#f59e0b;">Parcel B</div>
<div style="font-size:12px;color:var(--muted-foreground, #7a7a7a);">${this.escapeHtml(l.field)}: ${this.escapeHtml(l.value)}</div>
</div>
</label>
<div style="display:flex;gap:8px;">
<button class="merge-popup-confirm" style="flex:1;padding:8px 12px;background:#10b981;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
✅ Confirm
</button>
<button class="merge-popup-cancel" style="flex:1;padding:8px 12px;background:var(--muted-foreground, #7a7a7a);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
Cancel
</button>
</div>
</div>
`;this.mergePopupElement.innerHTML=c,this.mergePopup.setPosition(o);const d=()=>{this.mergePopup.setPosition(void 0)};this.mergePopupElement.querySelector(".merge-popup-close").addEventListener("click",d),this.mergePopupElement.querySelector(".merge-popup-cancel").addEventListener("click",d),this.mergePopupElement.querySelector(".merge-popup-confirm").addEventListener("click",()=>{const m=this.mergePopupElement.querySelector('input[name="merge-choice"]:checked').value==="A"?t:n,g=["geometry"];for(const[y,b]of Object.entries(m))g.includes(y)||e.set(y,b);e.set("_layerType","parcel");for(const y of this._parcelEditCallbacks)y(e,m);d()});const u=this.mergePopupElement.querySelectorAll("label"),p=this.mergePopupElement.querySelectorAll('input[name="merge-choice"]'),f=()=>{u.forEach(h=>{const m=h.querySelector("input");h.style.borderColor=m.checked?m.value==="A"?"#0ea5e9":"#f59e0b":"var(--border, #1e1a4b1f)"})};p.forEach(h=>h.addEventListener("change",f)),f()}createDividePopup(){this.dividePopupElement=document.createElement("div"),this.dividePopupElement.className="map-divide-popup",this.dividePopupElement.style.cssText=`
position: absolute;
background: var(--card, #fff);
color: var(--card-foreground, #1e1a4b);
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
font-family: var(--font-body, 'Exo', sans-serif);
font-size: 13px;
min-width: 260px;
max-width: 320px;
z-index: 1002;
border: 2px solid #8b5cf6;
overflow: hidden;
display: flex;
flex-direction: column;
`,this.dividePopup=new he({element:this.dividePopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.dividePopup)}showDividePopup(e,t,n){const o=`
<div style="background:#8b5cf6;color:#fff;padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<span>Divide Polygon</span>
<button class="divide-popup-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">&times;</button>
</div>
<div style="padding:12px;">
<p style="margin:0 0 10px;color:var(--muted-foreground, #7a7a7a);font-size:12px;">
Enter the number of equal pieces:
</p>
<input type="number" class="divide-input" min="2" max="50" value="2"
style="width:100%;padding:8px 10px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:16px;font-weight:600;text-align:center;color:var(--foreground, #1e1a4b);background:var(--muted, #f2f4f7);min-height:40px;" />
<div style="display:flex;gap:8px;margin-top:12px;">
<button class="divide-popup-confirm" style="flex:1;padding:8px 12px;background:#8b5cf6;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
Divide
</button>
<button class="divide-popup-cancel" style="flex:1;padding:8px 12px;background:var(--muted-foreground, #7a7a7a);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
Cancel
</button>
</div>
</div>
`;this.dividePopupElement.innerHTML=o,this.dividePopup.setPosition(n);const i=this.dividePopupElement.querySelector(".divide-input");i.focus(),i.select();const s=()=>{this.hideDividePopup(),this._polygonDivideInteraction.cancelDivide()};this.dividePopupElement.querySelector(".divide-popup-close").addEventListener("click",s),this.dividePopupElement.querySelector(".divide-popup-cancel").addEventListener("click",s),this.dividePopupElement.querySelector(".divide-popup-confirm").addEventListener("click",()=>{const a=parseInt(i.value,10);if(!a||a<2){i.style.borderColor="#ef4444";return}this.hideDividePopup(),this._polygonDivideInteraction.performDivide(a)}),i.addEventListener("keydown",a=>{a.key==="Enter"&&(a.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 he({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=[],n=o=>{t.length>0||o.getLayers().forEach(i=>{if(!(t.length>0)){if(i instanceof be)n(i);else if(i instanceof F){const s=i.getSource();if(!s)return;for(const a of s.getFeatures()){if(a.get("_layerType")!=="parcel")continue;const l=a.getProperties();for(const c of Object.keys(l))e.includes(c)||t.push(c);return}}}})};return n(this.overlayGroup),t}showDrawnPolygonPopup(e,t){this._drawnPolygonFeature=e;const n=this.getParcelAttributeKeys();if(n.length===0){console.warn("[MapView] No parcel attributes found — cannot build form");return}let o="";for(const d of n){const u=this.escapeHtml(d);o+=`
<div style="margin-bottom:8px;">
<label style="display:block;font-size:11px;font-weight:600;color:var(--muted-foreground, #7a7a7a);margin-bottom:2px;">${u}</label>
<input type="text" name="${u}" value=""
style="width:100%;padding:6px 8px;border:1px solid var(--border, #1e1a4b1f);border-radius:4px;font-size:13px;color:var(--foreground, #1e1a4b);background:var(--muted, #f2f4f7);min-height:34px;"
/>
</div>
`}const i=e.getGeometry(),s=qe(i,{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;">&times;</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>${je(s)}</strong>
</div>
<form class="drawn-polygon-form" style="padding:10px 12px;overflow-y:auto;flex:1;">
${o}
<div style="display:flex;gap:8px;margin-top:10px;">
<button type="submit" style="flex:1;padding:8px 12px;background:var(--success, #006b3f);color:var(--success-foreground, #fff);border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
💾 Save
</button>
<button type="button" class="drawn-polygon-cancel" style="flex:1;padding:8px 12px;background:var(--muted-foreground, #7a7a7a);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
Cancel
</button>
</div>
</form>
`;this.drawnPolygonElement.innerHTML=l,this.drawnPolygonPopup.setPosition(t),this.drawnPolygonElement.querySelector(".drawn-polygon-close").addEventListener("click",()=>{this.hideDrawnPolygonPopup()}),this.drawnPolygonElement.querySelector(".drawn-polygon-cancel").addEventListener("click",()=>{this.hideDrawnPolygonPopup()});const c=this.drawnPolygonElement.querySelector(".drawn-polygon-form");c.addEventListener("submit",d=>{d.preventDefault();const u=new FormData(c),p={};for(const[f,h]of u.entries())p[f]=h;for(const[f,h]of Object.entries(p))this._drawnPolygonFeature.set(f,h);this._drawnPolygonFeature.set("_layerType","parcel");for(const f of this._drawnPolygonCallbacks)f(this._drawnPolygonFeature,p);this.hideDrawnPolygonPopup()})}hideDrawnPolygonPopup(){this.drawnPolygonPopup.setPosition(void 0),this._drawnPolygonFeature=null}onDrawnPolygonSave(e){this._drawnPolygonCallbacks.push(e)}onDblClick(e){return this.dblClickCallbacks.push(e),this.dblClickCallbacks.length===1&&this.map.on("dblclick",t=>{const[n,o]=Le(t.coordinate);let i=null;this.map.forEachFeatureAtPixel(t.pixel,s=>(i=s,!0)),i&&(t.preventDefault(),t.stopPropagation());for(const s of this.dblClickCallbacks)s(n,o,i,t);if(i)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">&times;</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 he({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,n]=Le(e);this.addLocationCoords={lon:t,lat:n};const o=this.addLocationPopupElement.querySelector("#map-location-coords");o.textContent=`${t.toFixed(6)}, ${n.toFixed(6)}`,this.addLocationPopupElement.querySelector("#map-add-location-form").reset(),this.addLocationPopup.setPosition(e)}hideAddLocationPopup(){this.addLocationPopup.setPosition(void 0),this.addLocationCoords=null}onAddLocation(e){if(this.addLocationCallbacks.push(e),this.addLocationCallbacks.length===1){const t=this.addLocationPopupElement.querySelector("#map-add-location-form");t.addEventListener("submit",n=>{if(n.preventDefault(),!this.addLocationCoords)return;const o=new FormData(t),i={name:o.get("name"),category:o.get("category"),description:o.get("description"),lon:this.addLocationCoords.lon,lat:this.addLocationCoords.lat};this.addLocationCallbacks.forEach(s=>s(i)),this.hideAddLocationPopup()})}}createBaseLayers(e){const t=new Z({title:"Topographic",type:"base",zIndex:-100,visible:e==="topo",source:new we({url:"https://{a-c}.tile.opentopomap.org/{z}/{x}/{y}.png",attributions:"Map data: © OpenTopoMap",maxZoom:17,crossOrigin:"anonymous"})});t.set("basemapKey","topo");const n=new Z({title:"Carto Light",type:"base",zIndex:-100,visible:e==="carto-light",source:new we({url:"https://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});n.set("basemapKey","carto-light");const o=new Z({title:"Carto Dark",type:"base",zIndex:-100,visible:e==="carto-dark",source:new we({url:"https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});o.set("basemapKey","carto-dark");const i=new Z({title:"OSM Cycle map",type:"base",zIndex:-100,visible:!1,source:new jt({url:"https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=ae1339c46dd3446b9c491e7336d38760"})});i.set("basemapKey","cycle");const s=new Z({title:"Satellite",type:"base",zIndex:-100,visible:e==="satellite",source:new we({url:"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",attributions:"Tiles © Esri",maxZoom:19,crossOrigin:"anonymous"})});s.set("basemapKey","satellite");const a=new Z({title:"Google Sat",type:"base",zIndex:-100,visible:e==="googlesat",source:new we({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"})});a.set("basemapKey","googlesat");const l=new Z({title:"OpenStreetMap",type:"base",zIndex:-100,visible:e==="osm",source:new jt});l.set("basemapKey","osm"),this._baseMapLayers=[n,o,i,s,a,l,t];const c=new be({title:"Base Maps",layers:[n,o,s,i,a,l,t]});return c.set("displayInLayerSwitcher",!1),c}setBaseMap(e){if(!this._baseMapLayers)return!1;if(e==="none"){for(const n of this._baseMapLayers)n.setVisible(!1);return console.log("[MapView] Base map switched off (none)"),this.map.dispatchEvent({type:"basemapchange",key:"none"}),!0}let t=!1;for(const n of this._baseMapLayers){const o=n.get("basemapKey")===e;n.setVisible(o),o&&(t=!0)}return t&&(console.log("[MapView] Base map switched to:",e),this.map.dispatchEvent({type:"basemapchange",key:e})),t}_createBaseMapPicker(){const e=[{key:"topo",label:"Topographic",grad:"linear-gradient(135deg,#e8d5b7,#a67c52)"},{key:"osm",label:"OpenStreetMap",grad:"linear-gradient(135deg,#d4e6f1,#85c1e9)"},{key:"satellite",label:"Satellite",grad:"linear-gradient(135deg,#1b4332,#40916c)"},{key:"googlesat",label:"Google Sat",grad:"linear-gradient(135deg,#2a5d3d,#4a8c5a)"},{key:"carto-light",label:"Carto Light",grad:"linear-gradient(135deg,#f5f5f5,#d4d4d4)"},{key:"carto-dark",label:"Carto Dark",grad:"linear-gradient(135deg,#1a1a2e,#0f3460)"},{key:"none",label:"None",grad:"repeating-conic-gradient(#e5e7eb 0 25%, #fff 0 50%) 50% / 12px 12px"}],t=this.map.getTargetElement();if(!t)return;const n=document.createElement("button");n.type="button",n.className="ls-basemap-toggle",n.title="Switch base map",n.setAttribute("aria-label","Switch base map"),n.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(n);const o=document.createElement("div");o.className="ls-basemap-panel",o.innerHTML='<div class="ls-basemap-header">Base Map</div><div class="ls-basemap-grid">'+e.map(s=>`
<label class="ls-bm-chip">
<input type="radio" name="lupmis-basemap" value="${s.key}">
<div class="ls-bm-label">
<div class="ls-bm-thumb" style="background:${s.grad};"></div>
<div class="ls-bm-name">${s.label}</div>
</div>
</label>
`).join("")+"</div>",t.appendChild(o),this._basemapPanel=o,this._basemapToggle=n;const i=s=>{const a=s||this._baseMapLayers?.find(l=>l.getVisible())?.get("basemapKey");o.querySelectorAll('input[name="lupmis-basemap"]').forEach(l=>{l.checked=l.value===a})};i(),n.addEventListener("click",s=>{s.stopPropagation();const a=!o.classList.contains("open");o.classList.toggle("open",a),n.classList.toggle("active",a),a&&i()}),document.addEventListener("click",s=>{o.classList.contains("open")&&(o.contains(s.target)||n.contains(s.target)||(o.classList.remove("open"),n.classList.remove("active")))}),o.addEventListener("change",s=>{const a=s.target.closest('input[type=radio][name="lupmis-basemap"]');if(!a)return;const l=a.value;this.setBaseMap(l);try{localStorage.setItem("default-basemap",l)}catch{}o.classList.remove("open"),n.classList.remove("active")}),this.map.on("basemapchange",s=>i(s.key))}_initGpsRendering(){this._gpsPositionSource=new N,this._gpsTrailSource=new N,this._gpsTrailCoords=[],this._gpsTrailLayer=new F({source:this._gpsTrailSource,zIndex:940,style:new M({stroke:new k({color:"#ff6d00",width:4,lineCap:"round",lineJoin:"round"})}),properties:{title:"GPS Trail",displayInLayerSwitcher:!1}}),this._gpsPositionLayer=new F({source:this._gpsPositionSource,zIndex:950,style:e=>e.get("_kind")==="accuracy"?new M({fill:new I({color:"rgba(0,94,184,0.12)"}),stroke:new k({color:"rgba(0,94,184,0.35)",width:1})}):new M({image:new re({radius:7,fill:new I({color:"#005eb8"}),stroke:new k({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,n=null){if(e==null||t==null)return;const o=te([e,t]);if(this._gpsPositionSource.clear(),n&&n>0){const s=n/Math.cos(t*Math.PI/180),a=new ne({geometry:new Ue([this._circleRing(o,s)])});a.set("_kind","accuracy"),this._gpsPositionSource.addFeature(a)}const i=new ne({geometry:new We(o)});i.set("_kind","dot"),this._gpsPositionSource.addFeature(i)}_circleRing(e,t,n=48){const o=[],s=t/1;for(let a=0;a<=n;a++){const l=a/n*2*Math.PI;o.push([e[0]+s*Math.cos(l),e[1]+s*Math.sin(l)])}return o}centerOn(e,t,n=16){this.map.getView().animate({center:te([e,t]),zoom:n,duration:500})}startTrailRender(){this._gpsTrailCoords=[],this._gpsTrailSource.clear()}appendTrailPoint(e,t){e==null||t==null||(this._gpsTrailCoords.push(te([e,t])),this._gpsTrailSource.clear(),this._gpsTrailCoords.length>=2&&this._gpsTrailSource.addFeature(new ne({geometry:new se(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 n=document.createElement("div");n.className="ls-locate-actions",n.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(n),this._locateToggle=t,this._locateActions=n,this._locateMeBtn=n.querySelector(".ls-locate-me"),this._recordBtn=n.querySelector(".ls-locate-record");const o=()=>{n.classList.remove("open"),t.classList.remove("active")},i=()=>{n.classList.add("open"),t.classList.add("active")};t.addEventListener("click",s=>{s.stopPropagation(),n.classList.contains("open")?o():i()}),document.addEventListener("click",s=>{n.classList.contains("open")&&(n.contains(s.target)||t.contains(s.target)||this._gpsRecording||o())}),this._locateMeBtn.addEventListener("click",s=>{s.stopPropagation();for(const a of this._gpsCallbacks.locate)try{a()}catch(l){console.error(l)}this._gpsRecording||o()}),this._recordBtn.addEventListener("click",s=>{s.stopPropagation();const a=!this._gpsRecording;for(const l of this._gpsCallbacks.record)try{l(a)}catch(c){console.error(c)}})}getFeatureStyle(e){const t=e.get("category")||"default",n=this.getEmoji(t);if(e===this.selectedFeature)return[new M({image:new re({radius:22,fill:new I({color:"rgba(220, 38, 38, 0.25)"}),stroke:new k({color:"#dc2626",width:3})})}),new M({text:new tt({text:n,font:"40px sans-serif",textBaseline:"bottom",textAlign:"center",offsetY:-5})})];const o=e.get("style");return o||(this.categoryStyles[t]?this.categoryStyles[t]:this.defaultStyle)}setCategoryStyles(e){for(const[t,n]of Object.entries(e)){n.emoji&&(this.categoryEmojis[t]?(this.categoryEmojis[t].emoji=n.emoji,n.label&&(this.categoryEmojis[t].label=n.label)):this.categoryEmojis[t]={emoji:n.emoji,label:n.label||t});const o=this.getEmoji(t),i=n.fontSize||28;this.categoryStyles[t]=this.createEmojiStyle(o,i)}this.markerSource.changed()}addMarker(e,t,n={}){console.log("[MapView] Adding marker at",e,t,"with properties:",n);const o=new ne({geometry:new We(te([e,t])),...n});return o.set("lon",e),o.set("lat",t),this.markerSource.addFeature(o),console.log("[MapView] Marker added, total features:",this.markerSource.getFeatures().length),o}addMarkers(e){console.log("[MapView] Adding",e.length,"markers");const t=e.map(n=>new ne({geometry:new We(te([n.longitude,n.latitude])),id:n.id,name:n.name,description:n.description,category:n.category,lon:n.longitude,lat:n.latitude}));return this.markerSource.addFeatures(t),console.log("[MapView] Markers added, total features:",this.markerSource.getFeatures().length),t}clearMarkers(){this.markerSource.clear(),this.selectedFeature=null}removeMarker(e){if(typeof e=="object")this.markerSource.removeFeature(e);else{const t=this.markerSource.getFeatures().find(n=>n.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)}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,n=15){this.map.getView().animate({center:te([e,t]),zoom:n,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 Le(e)}getZoom(){return this.map.getView().getZoom()}setCenter(e,t){this.map.getView().setCenter(te([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 n=!1,o=!1,i=null;if(this.map.forEachFeatureAtPixel(t.pixel,l=>{l.get("_layerType")==="parcel"&&(o=!0),l.get("name")&&(i=l),n=!0}),n&&!o&&!i)return;const[s,a]=Le(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(s,a,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[n,o]=Le(t.coordinate);let i=null;this.map.forEachFeatureAtPixel(t.pixel,s=>{if(s.get("name"))return i=s,!0}),this.map.getTargetElement().style.cursor=i?"pointer":"",e(n,o,i,t)})}enableHoverCursor(){}addGeoJSONLayer(e,t,n={},o=null){const{strokeColor:i="#3b82f6",strokeWidth:s=2,fillColor:a="rgba(59,130,246,0.1)",lineCasingColor:l=null,lineCasingWidth:c=null,pointRadius:d=5,pointFillColor:u=null,pointStrokeColor:p="#ffffff",pointStrokeWidth:f=1.5}=n,h=new N({features:new ae().readFeatures(e,{featureProjection:"EPSG:3857"})}),m=new I({color:a}),g=new re({radius:d,fill:new I({color:u||i}),stroke:new k({color:p,width:f})});let y;if(l){const x=c??s+2;y=[new M({stroke:new k({color:l,width:x})}),new M({stroke:new k({color:i,width:s}),fill:m,image:g})]}else y=new M({stroke:new k({color:i,width:s}),fill:m,image:g});const b=new F({title:t,source:h,style:y});b.set("typeTag",n.typeTag||"VEC");const E=x=>x?x.includes("Polygon")?"Vector / Polygon":x.includes("LineString")?"Vector / Line":x.includes("Point")?"Vector / Point":"Vector":null;if(n.typeDescription)b.set("typeDescription",n.typeDescription);else{const x=h.getFeatures(),w=E(x[0]?.getGeometry?.()?.getType?.());if(w)b.set("typeDescription",w);else{const S=P=>{const T=E(P.feature.getGeometry?.()?.getType?.());T&&b.set("typeDescription",T),h.un("addfeature",S)};h.on("addfeature",S)}}return(o||this.overlayGroup).getLayers().push(b),console.log("[MapView] GeoJSON layer added:",t,"→",h.getFeatures().length,"features",o?`(in group "${o.get("title")}")`:""),b}addLayerGroup(e,t,n=""){const o=new be({title:t.trim()});return o.set("layerId",e),o.set("description",n),this.overlayGroup.getLayers().push(o),console.log("[MapView] Layer group added:",t.trim(),"(id:",e+")"),o}addWMSLayer(e,t,n,o,i={}){const s=this.getLayerGroupByTitle(e);if(!s)return console.warn(`[MapView] Layer group "${e}" not found — cannot add WMS layer "${t}"`),null;const a={LAYERS:o,TILED:!0,WIDTH:256,HEIGHT:256};i.style!==void 0&&(a.STYLES=i.style);const l=new zt({url:n,params:a,serverType:i.serverType!==void 0?i.serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1,attributions:i.attributions}),c=new Z({title:t,visible:i.visible!==void 0?i.visible:!0,source:l,opacity:i.opacity!==void 0?i.opacity:1,zIndex:i.zIndex});if(c.set("typeTag","WMS"),c.set("typeDescription","WMS / Raster"),l.on("tileloaderror",()=>{O(`WMS layer "${t}" — tile load error. Check the URL and layer name.`,"warning",5e3)}),s.getLayers().push(c),i.legendUrl)try{this._registerLegend(c,t,i.legendUrl)}catch(d){console.warn(`[MapView] Could not register legend for "${t}":`,d)}return i.onlineOnly&&this._attachOnlineOnlyHandler(c,t),console.log(`[MapView] WMS layer added: "${t}" → group "${e}"`),c}addXYZLayer(e,t,n,o={}){const i=this.getLayerGroupByTitle(e);if(!i)return console.warn(`[MapView] Layer group "${e}" not found — cannot add XYZ layer "${t}"`),null;const s=new we({url:n,crossOrigin:"anonymous",maxZoom:o.maxZoom!==void 0?o.maxZoom:19,attributions:o.attributions}),a=new Z({title:t,visible:o.visible!==void 0?o.visible:!0,source:s,opacity:o.opacity!==void 0?o.opacity:1,zIndex:o.zIndex});if(a.set("typeTag","XYZ"),a.set("typeDescription","XYZ / Tile"),s.on("tileloaderror",()=>{O(`XYZ layer "${t}" — tile load error. Check the URL.`,"warning",5e3)}),i.getLayers().push(a),o.legendUrl)try{this._registerLegend(a,t,o.legendUrl)}catch(l){console.warn(`[MapView] Could not register legend for "${t}":`,l)}return o.onlineOnly&&this._attachOnlineOnlyHandler(a,t),console.log(`[MapView] XYZ layer added: "${t}" → group "${e}"`),a}_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;">&times;</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>
</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"),n=e.querySelector(".add-layer-name-hint"),o=e.querySelector(".add-layer-url");e.querySelectorAll('input[name="add-layer-type"]').forEach(s=>{s.addEventListener("change",()=>{const a=s.value;a==="xyz"?(t.style.display="none",o.placeholder="https://example.com/tiles/{z}/{x}/{y}.png"):(t.style.display="",o.placeholder=a==="wms"?"https://example.com/wms":"https://example.com/wfs",n.textContent=a==="wms"?"WMS LAYERS parameter (e.g. workspace:layer)":"WFS typename (e.g. workspace:layer)")})});const i=()=>this._hideAddLayerDialog();e.querySelector(".add-layer-close").addEventListener("click",i),e.querySelector(".add-layer-cancel").addEventListener("click",i),this._addLayerDialog.addEventListener("click",s=>{s.target===this._addLayerDialog&&i()}),e.querySelector(".add-layer-confirm").addEventListener("click",()=>{const s=e.querySelector('input[name="add-layer-type"]:checked').value,a=e.querySelector(".add-layer-url").value.trim(),l=e.querySelector(".add-layer-name").value.trim(),c=e.querySelector(".add-layer-title").value.trim();if(!a){e.querySelector(".add-layer-url").style.borderColor="#ef4444";return}if((s==="wms"||s==="wfs")&&!l){e.querySelector(".add-layer-name").style.borderColor="#ef4444";return}if(!c){e.querySelector(".add-layer-title").style.borderColor="#ef4444";return}this._addExternalLayer(s,a,l,c),this._hideAddLayerDialog()}),e.addEventListener("keydown",s=>{s.key==="Enter"&&(s.preventDefault(),e.querySelector(".add-layer-confirm").click()),s.key==="Escape"&&(s.preventDefault(),i())})}showAddLayerDialog(){const e=this._addLayerDialog;e.querySelector(".add-layer-url").value="",e.querySelector(".add-layer-name").value="",e.querySelector(".add-layer-title").value="",e.querySelectorAll('input[name="add-layer-type"]')[0].checked=!0,e.querySelector(".add-layer-name-row").style.display="",e.querySelector(".add-layer-url").placeholder="https://example.com/wms",e.querySelector(".add-layer-name-hint").textContent="WMS LAYERS parameter (e.g. workspace:layer)",e.querySelectorAll('input[type="text"]').forEach(t=>{t.style.borderColor="var(--border, #1e1a4b1f)"}),e.style.display="flex",e.querySelector(".add-layer-url").focus()}_hideAddLayerDialog(){this._addLayerDialog.style.display="none"}_addExternalLayer(e,t,n,o){const i=this._externalSourceGroup;if(!i){O('Layer group "External Source" not found.',"error",4e3);return}let s;switch(e){case"wms":{const a=new zt({url:t,params:{LAYERS:n,TILED:!0,WIDTH:256,HEIGHT:256},serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1});s=new Z({title:o,visible:!0,source:a}),a.on("tileloaderror",()=>{O(`WMS "${o}" — tile load error. Check URL and layer name.`,"warning",5e3)});break}case"wfs":{const a=`${t}${t.includes("?")?"&":"?"}service=WFS&version=1.1.0&request=GetFeature&typename=${encodeURIComponent(n)}&outputFormat=application/json&srsname=EPSG:3857`,l=new N({url:a,format:new ae});l.on("featuresloaderror",()=>{O(`WFS "${o}" — load error. Check URL and layer name.`,"warning",5e3)}),s=new F({title:o,visible:!0,source:l,style:new M({stroke:new k({color:"#e11d48",width:2}),fill:new I({color:"rgba(225,29,72,0.15)"})})});break}case"xyz":s=new Z({title:o,visible:!0,source:new we({url:t,crossOrigin:"anonymous"})}),s.getSource().on("tileloaderror",()=>{O(`XYZ "${o}" — tile load error. Check the URL template.`,"warning",5e3)});break;default:O(`Unknown layer type: ${e}`,"error",4e3);return}s.set("typeTag",e.toUpperCase()),s.set("typeDescription",{wms:"WMS / Raster",wfs:"WFS / Vector",xyz:"XYZ / Tile"}[e]||e.toUpperCase()),s.set("removable",!0),i.getLayers().push(s),O(`Layer "${o}" added to External Source.`,"success",3e3),console.log(`[MapView] External ${e.toUpperCase()} layer added: "${o}"`)}_decorateLayerListItem(e,t){const n=e.get("typeTag");if(n){const l=t.querySelector(":scope > .li-content > label > span");if(l&&!l.querySelector(":scope > .ls-type-tag")){const c=document.createElement("span");c.className=`ls-type-tag ls-type-tag-${String(n).toLowerCase()}`,c.textContent=String(n),c.title=`${n} layer`,l.appendChild(c)}}const o=t.querySelector(":scope > .ol-layerswitcher-buttons");if(o){const l=o.querySelector(":scope > .expend-layers, :scope > .collapse-layers");l&&!l.querySelector(":scope > svg.ls-chevron-svg")&&(l.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 i=t.querySelector(":scope > .li-content"),s=()=>{if(!i)return;const l=e.get("typeDescription");let c=i.querySelector(":scope > .ls-layer-subtitle");if(!l){c&&c.remove();return}if(!c){c=document.createElement("div"),c.className="ls-layer-subtitle";const d=i.querySelector(":scope > label");d&&d.nextSibling?i.insertBefore(c,d.nextSibling):i.appendChild(c)}c.textContent=l};if(s(),e._lsSubtitleHooked||(e._lsSubtitleHooked=!0,e.on("change:typeDescription",()=>{s()})),e.get("removable")===!0&&o&&!o.querySelector(":scope > .ls-remove-btn")){const l=document.createElement("button");l.type="button",l.className="ls-remove-btn",l.title="Remove this layer",l.setAttribute("aria-label","Remove layer"),l.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>',l.addEventListener("click",c=>{c.stopPropagation(),this._removeLayer(e)}),o.appendChild(l)}if((e.get("title")||"").toLowerCase().includes("external")&&(this._externalSourceGroup=e,o&&!o.querySelector(".ol-add-layer"))){const l=document.createElement("span");l.className="ol-add-layer",l.title="Add external layer",l.textContent="+",l.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;
`,l.addEventListener("mouseenter",()=>{l.style.background="#329686"}),l.addEventListener("mouseleave",()=>{l.style.background="#41b6a6"}),l.addEventListener("click",c=>{c.stopPropagation(),this.showAddLayerDialog()}),o.prepend(l)}}_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 n=i=>{const s=i.getLayers();if(s.getArray().includes(e))return s.remove(e),!0;let a=!1;return s.forEach(l=>{!a&&l.getLayers&&(a=n(l))}),a};n(this.overlayGroup)?(console.log(`[MapView] Removed layer "${t}"`),O(`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"),n=e.element?.querySelector("ul.panel");if(!t||!n)return;let o=t.querySelector(":scope > .ls-active-badge");o||(o=document.createElement("div"),o.className="ls-active-badge",o.innerHTML=`
<span class="ls-active-badge-title">Layers</span>
<span class="ls-active-badge-count">0 active</span>
`,t.insertBefore(o,n));let i=t.querySelector(":scope > .ls-footer-row");i||(i=document.createElement("div"),i.className="ls-footer-row",i.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(i),i.querySelector(".ls-footer-btn").addEventListener("click",a=>{a.stopPropagation(),this._resetAllOverlays()}));const s=this._countLayers();o.querySelector(".ls-active-badge-count").textContent=`${s.activeOverlays} active`,i.querySelector(".ls-footer-note").textContent=`${s.totalOverlays} overlay${s.totalOverlays===1?"":"s"}`}_countLayers(){let e=0,t=0;const n=new Set(["__vertex_highlight__"]),o=i=>{i.getLayers().forEach(s=>{s.get("displayInLayerSwitcher")!==!1&&(n.has(s.get("title"))||(s.getLayers?o(s):(e++,s.getVisible()&&t++)))})};return this.overlayGroup&&o(this.overlayGroup),{totalOverlays:e,activeOverlays:t}}_resetAllOverlays(){const e=new Set(["__vertex_highlight__"]),t=n=>{n.getLayers().forEach(o=>{o.get("displayInLayerSwitcher")!==!1&&(e.has(o.get("title"))||(o.getLayers?t(o):o.setVisible(!1)))})};this.overlayGroup&&t(this.overlayGroup),console.log("[MapView] Reset overlays — all hidden")}_wireLayerSwitcherVisibilityHooks(e){const t=()=>this._refreshLayerSwitcherChrome(e),n=i=>{i._lsVisHooked||(i._lsVisHooked=!0,i.on("change:visible",t))},o=i=>{i.getLayers().forEach(s=>{s.getLayers?(o(s),i._lsAddHooked||(i._lsAddHooked=!0,i.getLayers().on("add",a=>{const l=a.element;l.getLayers?o(l):n(l),t()}))):n(s)})};this.overlayGroup&&o(this.overlayGroup)}_attachOnlineOnlyHandler(e,t){e.set("onlineOnly",!0),e.on("change:visible",()=>{e.getVisible()&&!navigator.onLine&&O(`"${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 qt}_registerLegend(e,t,n){if(!this._legendPanel)return;const o=document.createElement("div");o.className="map-legend-entry",o.style.cssText="border-bottom:1px solid var(--border, #1e1a4b1f);padding-bottom:6px;",o.innerHTML=`
<div style="font-weight:600;font-size:11px;margin-bottom:4px;line-height:1.3;">
${this._escapeHtml(t)}
</div>
<img src="${n}" alt="${this._escapeHtml(t)} legend"
style="display:block;max-width:100%;height:auto;border-radius:3px;"
onerror="this.style.display='none'" />
`,this._legendEntries.set(e,o);const i=()=>{try{this._updateLegendPanel()}catch(s){console.warn("[MapView] legend panel update failed:",s)}};e.on("change:visible",i),i()}_updateLegendPanel(){if(!this._legendPanel)return;const e=[];for(const[t,n]of this._legendEntries)t.getVisible()&&e.push(n);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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}getLayerGroup(e){let t=null;return this.overlayGroup.getLayers().forEach(n=>{n.get("layerId")===e&&(t=n)}),t}getLayerGroupByTitle(e){let t=null;return this.overlayGroup.getLayers().forEach(n=>{n.get("title")===e&&(t=n)}),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=n=>{n.getLayers().forEach(o=>{if(o.getLayers)t(o);else if(o.get("title")==="District Boundary"){const i=o.getSource&&o.getSource();if(i&&typeof i.getExtent=="function"){const s=i.getExtent();s&&Number.isFinite(s[0])&&(e={extent:s,title:o.get("title")})}}})};return t(this.overlayGroup),e}getMarkerSource(){return this.markerSource}getMarkersLayer(){return this.markersLayer}updateSize(){this.map.updateSize()}onSearchSelect(e){this.searchSelectCallbacks.push(e)}navigateTo(e,t,n=14,o=500){const i=te([e,t]);this.map.getView().animate({center:i,zoom:n,duration:o})}}class Rr{constructor(e,t={}){this.map=e,this.options=t,this.measureSource=new N,this.measureLayer=new F({source:this.measureSource,style:this.getMeasureStyle(),title:"Measurements",zIndex:100}),this.drawSource=new N,this.drawLayer=new F({source:this.drawSource,style:this.getDrawStyle(),title:"Draw sketches",displayInLayerSwitcher:!1,zIndex:99});const n=this.map.getLayers(),o=n.getLength()-1;n.insertAt(o,this.drawLayer),n.insertAt(o,this.measureLayer),this.activeInteraction=null,this.measureTooltip=null,this.measureTooltipElement=null,this.onMeasureCompleteCallbacks=[],this.onDrawCompleteCallbacks=[]}getMeasureStyle(){return new M({fill:new I({color:"rgba(255, 233, 106, 0.2)"}),stroke:new k({color:"#8B008B",lineDash:[10,10],width:2}),image:new re({radius:5,stroke:new k({color:"#8B008B"}),fill:new I({color:"rgba(255, 233, 106, 0.5)"})})})}getDrawStyle(){return new M({fill:new I({color:"rgba(255, 233, 106, 0.3)"}),stroke:new k({color:"#8B008B",width:2}),image:new re({radius:6,stroke:new k({color:"#8B008B",width:2}),fill:new I({color:"#FFE96A"})})})}createMeasureTooltip(){this.measureTooltipElement&&this.measureTooltipElement.parentNode.removeChild(this.measureTooltipElement),this.measureTooltipElement=document.createElement("div"),this.measureTooltipElement.className="measure-tooltip",this.measureTooltip=new he({element:this.measureTooltipElement,offset:[15,0],positioning:"center-left",stopEvent:!1}),this.map.addOverlay(this.measureTooltip)}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 ve({source:this.measureSource,type:"Circle",style:new M({fill:new I({color:"rgba(255, 233, 106, 0.2)"}),stroke:new k({color:"rgba(139, 0, 139, 0.7)",lineDash:[10,10],width:2}),image:new re({radius:5,stroke:new k({color:"rgba(139, 0, 139, 0.7)"}),fill:new I({color:"rgba(255, 233, 106, 0.5)"})})})});this.activeInteraction=e,this.map.addInteraction(e);let t;return e.on("drawstart",n=>{t=n.feature.getGeometry().on("change",i=>{const s=i.target;if(s instanceof cn){const a=s.getRadius(),l=ar(a),d=`<strong>${lt(a)}</strong><br><small>${l}</small>`;this.measureTooltipElement.innerHTML=d,this.measureTooltip.setPosition(s.getLastCoordinate())}})}),e.on("drawend",n=>{const o=n.feature,i=o.getGeometry(),s=i.getCenter(),a=i.getRadius();o.set("_layerType","measure_circle"),o.set("_radius",a),o.set("_center",s);const l=new ne({geometry:new se([s,[s[0]+a,s[1]]])});l.set("_layerType","measure_circle_radius"),this.measureSource.addFeature(l),this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltip.setOffset([0,-7]),this.measureTooltipElement=null,this.createMeasureTooltip(),bt(t);const c={type:"circle",center:s,radius:a,area:Math.PI*a*a,feature:o};this.onMeasureCompleteCallbacks.forEach(d=>d(c))}),e}startLineMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new ve({source:this.measureSource,type:"LineString",style:this.getMeasureStyle()});this.activeInteraction=e,this.map.addInteraction(e);let t;return e.on("drawstart",n=>{t=n.feature.getGeometry().on("change",i=>{const s=i.target,a=ot(s),l=lt(a);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(s.getLastCoordinate())})}),e.on("drawend",n=>{const o=n.feature,i=o.getGeometry(),s=ot(i);this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),bt(t);const a={type:"line",length:s,feature:o};this.onMeasureCompleteCallbacks.forEach(l=>l(a))}),e}startAreaMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new ve({source:this.measureSource,type:"Polygon",style:this.getMeasureStyle()});this.activeInteraction=e,this.map.addInteraction(e);let t;return e.on("drawstart",n=>{t=n.feature.getGeometry().on("change",i=>{const s=i.target,a=qe(s),l=je(a);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(s.getInteriorPoint().getCoordinates())})}),e.on("drawend",n=>{const o=n.feature,i=o.getGeometry(),s=qe(i);o.set("_layerType","measure_area"),o.set("_area",s),this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),bt(t);const a={type:"polygon",area:s,feature:o,coordinate:i.getInteriorPoint().getCoordinates()};this.onMeasureCompleteCallbacks.forEach(l=>l(a))}),e}startDrawPoint(){this.deactivate();const e=new ve({source:this.drawSource,type:"Point",style:this.getDrawStyle()});return this.activeInteraction=e,this.map.addInteraction(e),e.on("drawend",t=>{const n={type:"point",feature:t.feature};this.onDrawCompleteCallbacks.forEach(o=>o(n))}),e}startDrawLine(){this.deactivate();const e=new ve({source:this.drawSource,type:"LineString",style:this.getDrawStyle()});return this.activeInteraction=e,this.map.addInteraction(e),e.on("drawend",t=>{const n={type:"line",feature:t.feature};this.onDrawCompleteCallbacks.forEach(o=>o(n))}),e}startDrawPolygon(){this.deactivate();const e=new ve({source:this.drawSource,type:"Polygon",style:this.getDrawStyle()});return this.activeInteraction=e,this.map.addInteraction(e),e.on("drawend",t=>{const n={type:"polygon",feature:t.feature};this.onDrawCompleteCallbacks.forEach(o=>o(n))}),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 Tt({group:!0,className:"map-tools-bar"}),n=new Tt({toggleOne:!0,group:!0}),o=new fe({html:'<span class="tool-icon">⭕</span>',title:"Measure Circle (radius & area)",className:"measure-circle-btn",onToggle:l=>{l?this.startCircleMeasure():this.deactivate()}});n.addControl(o);const i=new fe({html:'<span class="tool-icon">📏</span>',title:"Measure Distance",className:"measure-line-btn",onToggle:l=>{l?this.startLineMeasure():this.deactivate()}});n.addControl(i);const s=new fe({html:'<span class="tool-icon">⬛</span>',title:"Measure Area",className:"measure-area-btn",onToggle:l=>{l?this.startAreaMeasure():this.deactivate()}});n.addControl(s);const a=new $e({html:'<span class="tool-icon">🗑️</span>',title:"Clear Measurements",className:"clear-measure-btn",handleClick:()=>{this.clearMeasurements(),o.setActive(!1),i.setActive(!1),s.setActive(!1)}});return n.addControl(a),t.addControl(n),t}getMeasureLayer(){return this.measureLayer}getDrawLayer(){return this.drawLayer}getMeasureSource(){return this.measureSource}getDrawSource(){return this.drawSource}isActive(){return this.activeInteraction!==null}}let Ee=null;async function Br(){if(!("serviceWorker"in navigator))return console.warn("[PWA] Service Workers not supported"),null;try{return Ee=await navigator.serviceWorker.register("/sw.js",{scope:"/"}),console.log("[PWA] Service Worker registered:",Ee.scope),Ee.addEventListener("updatefound",()=>{const r=Ee.installing;r.addEventListener("statechange",()=>{r.state==="installed"&&navigator.serviceWorker.controller&&(console.log("[PWA] New version available"),qr())})}),Ee}catch(r){return console.error("[PWA] Service Worker registration failed:",r),null}}let Me=null,ue=null;function Nr(r="#install-btn"){if(ue=typeof r=="string"?document.querySelector(r):r,!ue){console.warn("[PWA] Install button not found:",r);return}ue.style.display="none",window.addEventListener("beforeinstallprompt",e=>{e.preventDefault(),Me=e,ue.style.display="block",console.log("[PWA] Install prompt ready")}),ue.addEventListener("click",async()=>{if(!Me){$r();return}Me.prompt();const{outcome:e}=await Me.userChoice;console.log("[PWA] Install prompt outcome:",e),Me=null,ue.style.display="none"}),window.addEventListener("appinstalled",()=>{console.log("[PWA] App installed"),Me=null,ue.style.display="none"}),window.matchMedia("(display-mode: standalone)").matches&&(ue.style.display="none")}function $r(){const r=/iPad|iPhone|iPod/.test(navigator.userAgent),e=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);let t=`To install this app:
`;r?(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 Ct=null;const At=new Set;function Gr(r="#offline-indicator"){Ct=typeof r=="string"?document.querySelector(r):r,xt(!navigator.onLine),window.addEventListener("online",()=>{console.log("[PWA] Back online"),xt(!1),ao(!1)}),window.addEventListener("offline",()=>{console.log("[PWA] Gone offline"),xt(!0),ao(!0)})}function xt(r){Ct&&(Ct.style.display=r?"block":"none"),document.body.classList.toggle("is-offline",r)}function Uo(r){return At.add(r),r(!navigator.onLine),()=>At.delete(r)}function ao(r){for(const e of At)try{e(r)}catch(t){console.error("[PWA] Offline listener error:",t)}}function W(){return navigator.onLine}function qr(){confirm("A new version is available. Reload now?")&&jr()}function jr(){Ee?.waiting&&Ee.waiting.postMessage({type:"SKIP_WAITING"}),window.location.reload()}async function zr({timeoutMs:r=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((i,s)=>setTimeout(()=>s(new Error("Service-worker readiness timeout")),r)),n=await Promise.race([e,t]),o=navigator.serviceWorker.controller||n.active;if(!o)throw new Error("No active service worker available");return o}function Ur(r){if(!("serviceWorker"in navigator))return()=>{};const e=()=>{try{r()}catch(t){console.error("[PWA] controllerchange handler error:",t)}};return navigator.serviceWorker.addEventListener("controllerchange",e),()=>navigator.serviceWorker.removeEventListener("controllerchange",e)}async function Nt(r,e,t={},n=5e3,o=1e4){const i=await zr({timeoutMs:o});return new Promise((s,a)=>{const l=new MessageChannel,c=setTimeout(()=>{l.port1.close(),a(new Error(`Service-worker reply "${e}" timed out`))},n);l.port1.onmessage=d=>{if(d.data?.type===e){clearTimeout(c),l.port1.close();const{type:u,...p}=d.data;s(p)}},i.postMessage({type:r,...t},[l.port2])})}async function Hr(){try{return(await Nt("GET_TILE_STATS","TILE_STATS")).stats}catch(r){return console.warn("[PWA] getTileCacheStats failed:",r),null}}async function Wr(){try{return await Nt("CLEAR_TILE_CACHES","TILE_CACHES_CLEARED"),!0}catch(r){return console.warn("[PWA] clearTileCaches failed:",r),!1}}async function Vr(r){if(!r)return!1;try{return!!(await Nt("CLEAR_TILE_CACHE","TILE_CACHE_CLEARED",{cacheName:r})).deleted}catch(e){return console.warn(`[PWA] clearTileCacheForProvider(${r}) failed:`,e),!1}}async function Kr(){if(!navigator.storage?.estimate)return null;try{const{usage:r,quota:e}=await navigator.storage.estimate();return{usage:r||0,quota:e||0}}catch(r){return console.warn("[PWA] getStorageEstimate failed:",r),null}}async function Yr(r={}){const{installButton:e="#install-btn",offlineIndicator:t="#offline-indicator",autoRegisterSW:n=!0}=r;n&&await Br(),Nr(e),Gr(t),console.log("[PWA] Initialized")}const Ho={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"}},Xr=30*1024,ct=2*Math.PI*6378137/2;function lo(r,e){const t=r/ct*180;let n=e/ct*180;return n=180/Math.PI*(2*Math.atan(Math.exp(n*Math.PI/180))-Math.PI/2),[t,n]}function co(r,e,t){const n=Math.pow(2,t),o=Math.floor((r+180)/360*n),i=e*Math.PI/180,s=Math.floor((1-Math.log(Math.tan(i)+1/Math.cos(i))/Math.PI)/2*n);return{x:o,y:s}}function Wo(r,e){const[t,n,o,i]=r,[s,a]=lo(t,n),[l,c]=lo(o,i),d=co(s,c,e),u=co(l,a,e),p=Math.pow(2,e),f=Math.max(0,Math.min(d.x,u.x)),h=Math.min(p-1,Math.max(d.x,u.x)),m=Math.max(0,Math.min(d.y,u.y)),g=Math.min(p-1,Math.max(d.y,u.y));return{z:e,minX:f,maxX:h,minY:m,maxY:g,count:(h-f+1)*(g-m+1)}}function Jr(r,e,t){let n=0;for(let o=e;o<=t;o++)n+=Wo(r,o).count;return n}function Zr(r,e,t){const n=[];for(let o=e;o<=t;o++){const i=Wo(r,o);for(let s=i.minX;s<=i.maxX;s++)for(let a=i.minY;a<=i.maxY;a++)n.push({z:o,x:s,y:a})}return n}function Qr(r,{z:e,x:t,y:n}){return r.replace("{z}",e).replace("{x}",t).replace("{y}",n)}class es{constructor({baseMap:e,extent3857:t,minZoom:n,maxZoom:o,concurrency:i=2,interBatchDelayMs:s=50,onProgress:a=()=>{}}){const l=Ho[e];if(!l)throw new Error(`Unknown base map: ${e}`);o>l.maxZoom&&(console.warn(`[OfflineTiles] ${e}: maxZoom ${o} > supported ${l.maxZoom}; clamping`),o=l.maxZoom),this.baseMap=e,this.template=l.url,this.extent=t,this.minZoom=n,this.maxZoom=o,this.concurrency=Math.max(1,Math.min(i,6)),this.interBatchDelayMs=s,this.onProgress=a,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=Zr(this.extent,this.minZoom,this.maxZoom),t=e.length,n=Date.now();let o=0,i=0,s=0,a=0;const l=c=>{const d=Date.now()-n,u=o>0?Math.round(d/o*(t-o)):null;this.onProgress({phase:c,done:o,total:t,ok:i,failed:s,cached:a,elapsedMs:d,etaMs:u})};l("running");for(let c=0;c<e.length&&!this._cancelled;c+=this.concurrency){const d=e.slice(c,c+this.concurrency);await Promise.all(d.map(async u=>{if(this._cancelled)return;const p=Qr(this.template,u);try{const f=await fetch(p,{signal:this._abortCtrl.signal,cache:"default"});f.ok?(i++,f.body&&f.body.cancel().catch(()=>{})):(f.status,s++)}catch(f){f.name==="AbortError"||s++}o++})),l("running"),this.interBatchDelayMs>0&&c+this.concurrency<e.length&&await new Promise(u=>setTimeout(u,this.interBatchDelayMs))}return l(this._cancelled?"cancelled":"done"),{phase:this._cancelled?"cancelled":"done",done:o,total:t,ok:i,failed:s,cached:a,elapsedMs:Date.now()-n}}cancel(){this._cancelled=!0,this._abortCtrl&&this._abortCtrl.abort()}}const ts=(()=>{const r=(n,o)=>{const i=n*ct/180,s=Math.log(Math.tan((90+o)*Math.PI/360))/(Math.PI/180);return[i,s*ct/180]},e=r(-3.3,4.5),t=r(1.2,11.2);return[e[0],e[1],t[0],t[1]]})();function os(r){return r*Xr}const Vo="https://api.lupmis4luspa.org/api/spatial_planning",ns="1",rs="1c46538c712e9b5b";function ss(){try{const r=typeof window<"u"&&window.LUPMIS_SESSION?.district_id;if(r!=null&&String(r).length>0)return String(r)}catch{}return ns}const Ko={get district_id(){return ss()},api_token:rs};function Yo(){if(typeof window<"u"&&window.LUPMIS_SESSION&&window.LUPMIS_SESSION.user_id)return window.LUPMIS_SESSION;try{const r=localStorage.getItem("dev-session");if(r){const e=JSON.parse(r);if(e&&e.user_id)return e}}catch{}return null}typeof window<"u"&&(window.lupmisDevSession=r=>{r==null?(localStorage.removeItem("dev-session"),console.log("[Dev] Session override cleared. Reload to apply.")):(localStorage.setItem("dev-session",JSON.stringify(r)),console.log("[Dev] Session override saved. Reload to apply:",r))});const is=3e4,as=5e3;let _e=null;async function ls(r=!1){if(_e!==null&&!r)return _e;const e=new AbortController,t=setTimeout(()=>e.abort(),as);try{_e=(await fetch(`${Vo}/get_layers.php`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(Ko),signal:e.signal})).ok}catch{_e=!1}finally{clearTimeout(t)}return console.log("[RemoteDB] Server reachable:",_e),_e}function Se(){return _e}function cs(r,e=is){const t=new AbortController,n=setTimeout(()=>t.abort(),e);return r.signal&&r.signal.addEventListener("abort",()=>t.abort()),{signal:t.signal,clear:()=>clearTimeout(n)}}async function ge(r,e={},t={}){const n=`${Vo}/${r}`,o={...Ko,...e};console.log("[RemoteDB] POST",n);const i=cs(t);try{const s=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(o),...t,signal:i.signal});if(!s.ok)throw new Error(`HTTP ${s.status}: ${s.statusText}`);const a=await s.json();return console.log("[RemoteDB] POST response:",r,"→",typeof a=="object"?`${Array.isArray(a)?a.length+" items":"object"}`:a),a}catch(s){throw s.name==="AbortError"?(console.error("[RemoteDB] POST timed out:",r),new Error(`Request timed out: ${r}`)):(console.error("[RemoteDB] POST failed:",r,s),s)}finally{i.clear()}}async function ds(){return ge("get_district_boundary.php")}async function us(){return ge("get_layers.php")}async function ps(){return ge("get_all_collector_zone_per_district.php")}async function hs(){return ge("get_parcels_per_district.php")}async function fs(){return ge("get_all_footprint_per_district.php")}async function gs(){return ge("get_contours_hillshade.php")}async function ms(){return ge("get_osm_roads.php")}async function ys(r,e){const t={client_uuid:r.client_uuid,name:r.name??null,started_at:r.started_at,ended_at:r.ended_at,point_count:r.point_count??e.length,distance_m:r.distance_m??0,points:(e||[]).map(o=>({seq:o.seq,longitude:o.longitude,latitude:o.latitude,altitude:o.altitude??null,accuracy:o.accuracy??null,altitude_accuracy:o.altitude_accuracy??null,heading:o.heading??null,speed:o.speed??null,satellites:o.satellites??null,recorded_at:o.recorded_at}))},n=await ge("save_gps_trail.php",t);return{remoteId:n?.id??n?.remote_id??null}}const bs=63710088e-1,Je=Math.PI/180;function ws(r,e,t,n){const o=(n-e)*Je,i=(t-r)*Je,s=Math.sin(o/2)**2+Math.cos(e*Je)*Math.cos(n*Je)*Math.sin(i/2)**2;return 2*bs*Math.asin(Math.min(1,Math.sqrt(s)))}function uo(r,e=5){return r==null||Number.isNaN(r)?"—":r.toFixed(e)}function vs(r){return r==null||Number.isNaN(r)?"—":r<1e3?`${Math.round(r)} m`:`${(r/1e3).toFixed(2)} km`}function _s(r){return r==null||Number.isNaN(r)?"—":`±${Math.round(r)} m`}function Es(r){return r==null||Number.isNaN(r)?"none":r<=10?"good":r<=30?"fair":"poor"}const xs={minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0,timeoutMs:15e3,maximumAgeMs:0};class ze{constructor(e={}){this.opts={...xs,...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 n=this._listeners[e];if(n)for(const o of n)try{o(t)}catch(i){console.error(`[GeoTracker] listener for "${e}" threw`,i)}}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(n=>{const o=ze.normalize(n);this._lastFix=o,this._emit("position",o),e(o)},n=>{this._emit("error",n),t(n)},{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=ze.uuid(),n=new Date().toISOString(),o={uuid:t,name:e.name||null,startedAt:n,...e},i=await this.storage.createTrail(o);return this._activeTrailId=i,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:i,uuid:t,startedAt:n}),{trailId:i,uuid:t}}async stopRecording(){if(!this._recording)return null;const e=this._activeTrailId,n={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,n)}catch(i){this._emit("error",i)}this._emit("trailstop",{trailId:e,...n});let o=!1;if(this.sync)try{o=await this._syncTrail(e)}catch(i){this._emit("error",i)}return this._activeTrailId=null,this._activeTrailUuid=null,{trailId:e,pointCount:n.pointCount,distanceM:n.distanceM,synced:o}}async syncPending(){if(!this.sync||!this.storage)return{pushed:0,failed:0};if(this.sync.isOnline&&!this.sync.isOnline())return{pushed:0,failed:0};let e=0,t=0;const n=await this.storage.getUnsyncedTrails();for(const o of n)try{await this._syncTrail(o.id??o.trailId,o)?e++:t++}catch(i){t++,this._emit("error",i)}return this._emit("syncstatus",{pushed:e,failed:t}),{pushed:e,failed:t}}async _syncTrail(e,t){const n=await this.storage.getTrailPoints(e),o=t||{id:e},i=await this.sync.pushTrail(o,n),s=i&&(i.remoteId??i.id??null);return await this.storage.markTrailSynced(e,s),!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=ze.normalize(e);if(this._lastFix=t,this._emit("position",t),!this._recording)return;const{minIntervalMs:n,minDistanceM:o,heartbeatMs:i,maxAccuracyM:s}=this.opts,a=t.timestamp;if(this._lastRecordedAt&&a-this._lastRecordedAt<n||s>0&&t.accuracy!=null&&t.accuracy>s&&this._lastRecorded)return;let l=!1,c=0;if(!this._lastRecorded)l=!0;else{c=ws(this._lastRecorded.lon,this._lastRecorded.lat,t.lon,t.lat);const d=a-this._lastRecordedAt;(c>=o||d>=i)&&(l=!0)}if(l){this._lastRecorded&&(this._distanceM+=c),this._pointCount+=1,this._lastRecorded={lon:t.lon,lat:t.lat,timestamp:a},this._lastRecordedAt=a;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||{},n=o=>o!=null&&!Number.isNaN(o)?o:null;return{lon:t.longitude,lat:t.latitude,accuracy:n(t.accuracy),altitude:n(t.altitude),altitudeAccuracy:n(t.altitudeAccuracy),heading:n(t.heading),speed:n(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 Ss={async createTrail(r){const e=r.districtId??Yo()?.district_id??null;return Qn({...r,districtId:e!=null?String(e):null})},addPoint:(r,e)=>er(r,e),finishTrail:(r,e)=>tr(r,e),getUnsyncedTrails:()=>or(),getTrailPoints:r=>nr(r),markTrailSynced:(r,e)=>rr(r,e)},Ls={pushTrail:(r,e)=>ys(r,e),isOnline:()=>W()},pe=new ze({storage:Ss,sync:Ls,minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0});let St=null;async function po(){if(!St){const r=await dt(()=>import("./shpjs-CNrRgkgn.js"),[]);St=r.default||r}return St}let v=null,Q=null,B="addLocation";async function ho(){console.log("[App] Initializing..."),await Yr({installButton:"#install-btn",offlineIndicator:"#offline-indicator",autoRegisterSW:!0});const r=localStorage.getItem("default-basemap")||"topo";v=new Or("map",{center:[-1.5,7.5],zoom:7,basemap:r}),Q=new Rr(v.getMap()),Zs(),Q.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)}),v.onClick((t,n,o,i)=>{if(console.log("[MapClick] Clicked at:",t.toFixed(4),n.toFixed(4)),console.log("[MapClick] currentMode =",B),B==="draw"||B.startsWith("measure"))return;let s=null;if(v.getMap().forEachFeatureAtPixel(i.pixel,a=>{if(a.get("_layerType")==="parcel")return s=a,!0}),s){console.log("[MapClick] Clicked on parcel → Edit Attributes"),v.showParcelEditPopup(s,i.coordinate);return}B==="addLocation"&&(o?(console.log("[MapClick] Clicked on marker:",o.getId()),v.selectMarker(o),ks(o)):(console.log("[MapClick] Empty space → Add Location popup"),v.clearSelection(),v.showAddLocationPopup(i.coordinate)))}),v.onDblClick((t,n,o,i)=>{if(!o)return;const s=o.get("_layerType");if(console.log("[App] Double-click on feature, _layerType:",s||"none"),s==="measure_circle")v.showCircleIntersectionPopup(o,i.coordinate);else{if(s==="measure_circle_radius")return;s==="measure_area"?v.showAreaIntersectionPopup(o,i.coordinate):s==="collector_zone"?v.showInfoPopup(o,i.coordinate,{title:"Zone Info",color:"#7c3aed"}):s==="parcel"?v.showInfoPopup(o,i.coordinate,{title:"Parcel Info",color:"#0ea5e9"}):v.showInfoPopup(o,i.coordinate,{title:"Feature Info",color:"#e11d48"})}}),v.onAddLocation(async t=>{console.log("[App] Add location from map popup:",t);try{const n=await Fn(t.name,t.lon,t.lat,{description:t.description||null,category:t.category||"default"});console.log("[App] Location added:",t.name,"id:",n.id),await Lt(),v?.zoomTo(t.lon,t.lat,14),n.id&&v?.selectMarker(n.id),ie("Location added successfully")}catch(n){console.error("[App] Failed to add location:",n),R("Failed to add location: "+n.message)}}),v.onParcelEdit(async(t,n)=>{const o=n.id||n.parcelid||n.parcel_id;if(console.log("[App] Parcel edit saved:",o,n),!o){console.warn("[App] No parcel ID found in updated properties — skipping local save");return}try{await Gn(o,n),ie("Parcel updated locally")}catch(i){console.error("[App] Failed to save parcel update:",i),R("Failed to save parcel: "+i.message)}});const e=new wo;v.onDrawnPolygonSave(async(t,n)=>{console.log("[App] Drawn polygon attributes saved:",n);try{const o=e.writeGeometry(t.getGeometry(),{dataProjection:"EPSG:4326",featureProjection:"EPSG:3857"}),i=await qn(o,n);console.log("[App] New parcel inserted with id:",i.id),ie("New parcel saved (pending verification)")}catch(o){console.error("[App] Failed to save new parcel:",o),R("Failed to save parcel: "+o.message)}});try{console.log("[App] Initializing database..."),await Dn(),console.log("[App] Database ready");const t=await Bt();console.log("[App] Database status:",t),W()&&(await ls()||(console.warn("[App] API server unreachable — using local data only"),Qs("Server not responding — loading cached data."))),await Us(),v?.initEditBar(),Bs(),Ns(),$s(),Gs(),qs(),js(),zs()}catch(t){console.error("[App] Database initialization failed:",t),R("Failed to initialize database. Please refresh the page.");return}Ts(),await Lt(),An(t=>{if(console.log("[App] Database change:",t),t.table==="locations"&&!t.local&&Lt(),t.table==="parcels"){const n=document.getElementById("local-data-stats");n&&!n.classList.contains("d-none")&&pt()}}),Uo(t=>{t?console.log("[App] Working offline - data will sync when back online"):(console.log("[App] Back online - syncing data..."),Hs())}),ei(),oi(),ti(),ni(),ri(),si(),ii(),console.log("[App] Initialized successfully")}function Ts(){console.log("[initUI] Starting UI initialization..."),Js();const r=document.getElementById("export-btn");r&&r.addEventListener("click",Cs);const e=document.getElementById("local-data-btn");e&&e.addEventListener("click",()=>pt());const t=document.getElementById("import-shp-btn"),n=document.getElementById("shp-file-input");t&&n&&(t.addEventListener("click",()=>n.click()),n.addEventListener("change",Qo));const o=document.getElementById("import-geojson-btn"),i=document.getElementById("geojson-file-input");o&&i&&(o.addEventListener("click",()=>i.click()),i.addEventListener("change",en));const s=document.getElementById("import-kml-btn"),a=document.getElementById("kml-file-input");s&&a&&(s.addEventListener("click",()=>a.click()),a.addEventListener("change",tn)),Ys();const l=document.getElementById("exportGeoJSON-btn");l&&l.addEventListener("click",As);const c=document.getElementById("status-btn");c&&c.addEventListener("click",Ds);const d=document.getElementById("fit-btn");d&&d.addEventListener("click",()=>v?.fitToMarkers());const u=document.getElementById("dock-btn-add-location"),p=document.getElementById("dock-btn-measure-circle"),f=document.getElementById("dock-btn-measure-line"),h=document.getElementById("dock-btn-measure-area"),m=document.getElementById("dock-btn-draw"),g=document.getElementById("dock-btn-clear");console.log("[initUI] Buttons found:",{addLocation:!!u,measureCircle:!!p,measureLine:!!f,measureArea:!!h,draw:!!m,clear:!!g});const y=[u,p,f,h,m],b=(E,L)=>{switch(console.log("[setMode] Changing mode from",B,"to",E),B=E,console.log("[setMode] currentMode is now:",B),y.forEach(x=>{x&&x.classList.toggle("active",x===L)}),Q?.deactivate(),E!=="draw"&&v?.setEditMode(!1),E!=="addLocation"&&v?.hideAddLocationPopup(),E){case"measureCircle":Q?.startCircleMeasure();break;case"measureLine":Q?.startLineMeasure();break;case"measureArea":Q?.startAreaMeasure();break;case"draw":v?.setEditMode(!0);break}};u&&u.addEventListener("click",()=>{console.log("[Button] Add Location clicked"),b("addLocation",u)}),p&&p.addEventListener("click",()=>{console.log("[Button] Circle clicked, currentMode is:",B),B==="measureCircle"?b("addLocation",u):b("measureCircle",p)}),f&&f.addEventListener("click",()=>{console.log("[Button] Line clicked, currentMode is:",B),B==="measureLine"?b("addLocation",u):b("measureLine",f)}),h&&h.addEventListener("click",()=>{console.log("[Button] Area clicked, currentMode is:",B),B==="measureArea"?b("addLocation",u):b("measureArea",h)}),m&&m.addEventListener("click",()=>{console.log("[Button] Draw clicked, currentMode is:",B),B==="draw"?b("addLocation",u):b("draw",m)}),g&&g.addEventListener("click",()=>{if(Q?.clearMeasurements(),B.startsWith("measure"))switch(Q?.deactivate(),B){case"measureCircle":Q?.startCircleMeasure();break;case"measureLine":Q?.startLineMeasure();break;case"measureArea":Q?.startAreaMeasure();break}})}async function Lt(){try{console.log("[App] Loading locations...");const r=await Ao();console.log("[App] Locations loaded:",r),Ps(r),v&&(v.clearMarkers(),r.length>0&&(v.addMarkers(r),console.log("[App] Added",r.length,"markers to map")));const e=document.getElementById("location-count");e&&(e.textContent=r.length)}catch(r){console.error("[App] Failed to load locations:",r)}}function ks(r){const e=r.get("name"),t=r.get("description"),n=r.get("category"),o=r.get("lon")||r.get("longitude"),i=r.get("lat")||r.get("latitude");console.log("[App] Selected location:",{name:e,description:t,category:n,lon:o,lat:i})}function Ps(r){const e=document.getElementById("locations-list");if(!e)return;const t=document.getElementById("location-count-mobile");if(t&&(t.textContent=r.length),r.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 n={water:"💧",school:"🏫",health:"🏥",market:"🏪",default:"📍",other:"📌"};e.innerHTML=r.map(o=>{const i=n[o.category]||"📍";return`
<a href="#" class="list-group-item list-group-item-action location-item py-2"
data-id="${o.id}" data-lon="${o.longitude}" data-lat="${o.latitude}">
<div class="d-flex w-100 justify-content-between align-items-start">
<div>
<h6 class="mb-1">${i} ${$(o.name)}</h6>
<small class="text-muted font-monospace">${o.latitude.toFixed(5)}, ${o.longitude.toFixed(5)}</small>
</div>
<span class="badge badge-${o.category}">${o.category}</span>
</div>
${o.description?`<small class="text-secondary d-block mt-1">${$(o.description)}</small>`:""}
</a>
`}).join(""),e.querySelectorAll(".location-item").forEach(o=>{o.addEventListener("click",i=>{i.preventDefault();const s=parseFloat(o.dataset.lon),a=parseFloat(o.dataset.lat),l=parseInt(o.dataset.id);v?.zoomTo(s,a,14),v?.selectMarker(l)})})}async function pt(){const r=document.getElementById("local-data-stats"),e=document.getElementById("local-data-tbody"),t=document.getElementById("clear-all-cached-btn");if(!(!r||!e)){try{const n=await Xn();e.innerHTML=n.map(o=>{const s=Ro(o.name)?`<button type="button" class="btn btn-sm btn-link text-danger p-0 table-clear-btn"
data-table="${$(o.name)}"
title="Clear local cache (will re-download from server)">
<i class="bi bi-trash3"></i>
</button>`:"";return`
<tr>
<td class="ps-3">
<a href="#" class="table-name-link" data-table="${$(o.name)}">${$(o.name)}</a>
</td>
<td class="text-end"><span class="badge bg-secondary">${o.count}</span></td>
<td class="text-end pe-3">${s}</td>
</tr>
`}).join(""),r.classList.remove("d-none"),e.querySelectorAll(".table-name-link").forEach(o=>{o.addEventListener("click",i=>{i.preventDefault(),Is(o.dataset.table)})}),e.querySelectorAll(".table-clear-btn").forEach(o=>{o.addEventListener("click",async i=>{i.preventDefault();const s=o.dataset.table;if(confirm(`Clear local cache for "${s}"?
The data will be re-downloaded from the server on the next app start.`))try{const a=await Bo(s);ie(`Cleared ${a} row${a===1?"":"s"} from "${s}". It will re-download on next start.`),await pt()}catch(a){console.error("[App] Per-table clear failed:",a),R(`Could not clear "${s}": ${a.message}`)}})})}catch(n){console.error("[App] Failed to load table stats:",n),e.innerHTML='<tr><td colspan="3" class="text-danger ps-3">Failed to load</td></tr>',r.classList.remove("d-none")}t&&!t._wired&&(t._wired=!0,t.addEventListener("click",Ms))}}async function Ms(){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 r=await Yn(),e=r.reduce((t,n)=>t+n.count,0);ie(`Cleared ${e} row${e===1?"":"s"} across ${r.length} table${r.length===1?"":"s"}.`),await pt(),confirm("Reload the app now to re-download the layers fresh from the server?")&&window.location.reload()}catch(r){console.error("[App] Clear-all failed:",r),R("Failed to clear cached layers: "+r.message)}}async function Is(r){const e=document.getElementById("tableContentModalLabel"),t=document.getElementById("table-content-body"),n=document.getElementById("table-content-info");e.textContent=`Table: ${r}`,t.innerHTML=`
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
`,n.textContent="",new Ft(document.getElementById("tableContentModal")).show();try{const{columns:i,rows:s}=await Jn(r);if(s.length===0){t.innerHTML='<div class="text-center text-muted py-4">Table is empty</div>',n.textContent="0 rows";return}const a=i.map(c=>`<th class="text-nowrap">${$(c)}</th>`).join(""),l=s.map(c=>`<tr>${i.map(u=>{let p=c[u];if(p==null)return'<td class="text-muted fst-italic">NULL</td>';p=String(p);const f=p.length>120?p.substring(0,120)+"...":p;return`<td>${$(f)}</td>`}).join("")}</tr>`).join("");t.innerHTML=`
<div class="table-responsive">
<table class="table table-sm table-striped table-hover mb-0" style="font-size:12px;">
<thead class="table-light">
<tr>${a}</tr>
</thead>
<tbody>${l}</tbody>
</table>
</div>
`,n.textContent=`${s.length}${s.length>=200?"+":""} row(s), ${i.length} column(s)`}catch(i){console.error("[App] Failed to load table content:",i),t.innerHTML=`<div class="text-danger text-center py-4">Failed to load: ${$(i.message)}</div>`}}async function Cs(){try{await Vn("lupmis-backup.sqlite3"),ie("Database exported successfully")}catch(r){console.error("[App] Export failed:",r),R("Export failed: "+r.message)}}async function As(){try{const r=await Kn(),e=new Blob([JSON.stringify(r,null,2)],{type:"application/json"}),t=URL.createObjectURL(e),n=document.createElement("a");n.href=t,n.download="locations.geojson",n.click(),URL.revokeObjectURL(t),ie(`Exported ${r.features.length} location(s)`)}catch(r){console.error("[App] GeoJSON Export failed:",r),R("GeoJSON Export failed: "+r.message)}}async function Ds(){try{const r=await Bt(),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 ${r.ready?"bg-success":"bg-danger"}">${r.ready?"Yes":"No"}</span></td>
</tr>
<tr>
<td class="fw-semibold">Online:</td>
<td><span class="badge ${W()?"bg-success":"bg-warning"}">${W()?"Yes":"Offline"}</span></td>
</tr>
<tr>
<td class="fw-semibold">Database:</td>
<td><code>${r.databasePath||"N/A"}</code></td>
</tr>
<tr>
<td class="fw-semibold">Tables:</td>
<td>${r.tables.map(n=>`<span class="badge bg-secondary me-1">${n}</span>`).join("")}</td>
</tr>
<tr>
<td class="fw-semibold">Locations:</td>
<td><span class="badge bg-primary">${r.locationCount}</span></td>
</tr>
</tbody>
</table>
`),new Ft(document.getElementById("statusModal")).show()}catch(r){console.error("[App] Failed to get status:",r),R("Failed to get status")}}function Xo(r){return r.replace(/^\(+/,"").replace(/\)+$/,"").split(",").map(e=>{const[t,n]=e.trim().split(/\s+/).map(Number);return[t,n]})}function Fs(r){return{type:"Polygon",coordinates:r.trim().replace(/^POLYGON\s*\(\s*/i,"").replace(/\s*\)$/,"").split("),(").map(Xo)}}function Os(r){return{type:"MultiPolygon",coordinates:r.trim().replace(/^MULTIPOLYGON\s*\(\s*/i,"").replace(/\s*\)$/,"").split(")),((").map(o=>o.replace(/^\(+/,"").replace(/\)+$/,"").split("),(").map(Xo))}}function ht(r){if(!r)return null;const e=r.trim().toUpperCase();return e.startsWith("MULTIPOLYGON")?Os(r):e.startsWith("POLYGON")?Fs(r):(console.warn("[App] Unsupported WKT type:",e.substring(0,30)),null)}function Rs(r){if(!r?.success||!r?.data?.boundary)return console.warn("[App] API response missing success or boundary data"),null;const{boundary:e,districtid:t,district_name:n}=r.data,o=ht(e);return{type:"FeatureCollection",features:[{type:"Feature",properties:{districtid:t,district_name:n},geometry:o}]}}function fo(r){if(!Array.isArray(r)||r.length===0)return null;const e=[];for(const t of r){const n=t.polygon||t.boundary,o=ht(n);if(!o)continue;const i={_layerType:"collector_zone"};for(const[s,a]of Object.entries(t))s==="polygon"||s==="boundary"||(i[s]=a);e.push({type:"Feature",properties:i,geometry:o})}return e.length===0?null:{type:"FeatureCollection",features:e}}async function Bs(){const r="district_boundary",t={strokeColor:"#e11d48",strokeWidth:2.5,fillColor:"rgba(225,29,72,0.08)",typeDescription:"Vector / Polygon"},n=v?.getLayerGroup(1)||null;function o(s){if(!s)return;const a=s.getLayers(),l=[];a.forEach(c=>{c.get("title")==="District Boundary"&&l.push(c)}),l.forEach(c=>a.remove(c))}function i(s){if(!s||!v)return;const a=s.getSource().getExtent();a&&a[0]!==1/0&&v.getMap().getView().fit(a,{padding:[40,40,40,40],duration:600})}try{const s=await Fo(r);if(s){console.log("[App] District boundary loaded from local cache");const a=v?.addGeoJSONLayer(s,"District Boundary",t,n);i(a)}if(W()&&Se()){console.log("[App] Fetching district boundary from API...");const a=await ds(),l=Rs(a);if(!l){console.warn("[App] Could not convert API response to GeoJSON");return}console.log("[App] District boundary:",l.features[0]?.properties?.district_name,"→",l.features[0]?.geometry?.coordinates?.length,"polygon(s)"),await Do(r,l),s&&o(n||v?.getOverlayGroup());const c=v?.addGeoJSONLayer(l,"District Boundary",t,n);i(c),console.log("[App] District boundary loaded from API")}else s||console.log("[App] District boundary not available — offline and no local cache")}catch(s){console.error("[App] Failed to load district boundary:",s)}}async function Ns(){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 n={type:"FeatureCollection",features:[]},o=v?.addGeoJSONLayer(n,"Zones",e,t);if(!o){console.warn("[App] Could not create Zones layer");return}o.setVisible(!1),o.on("change:visible",()=>{o.getVisible()&&o.getSource().getFeatures().length===0&&R("No collector zones available locally. Connect to the internet to download zone data.")});function i(s){const a=new ae().readFeatures(s,{featureProjection:"EPSG:3857"});o.getSource().clear(),o.getSource().addFeatures(a)}try{const s=await Bn();if(s){const a=fo(s);a&&(console.log("[App] Collector zones loaded from local cache:",a.features.length,"zones"),i(a))}if(W()&&Se()){console.log("[App] Fetching collector zones from API...");const a=await ps();if(!a?.success||!Array.isArray(a?.data)){console.warn("[App] getCollectorZones API response invalid:",a);return}const l=a.data;console.log("[App] Collector zones from API:",l.length,"entries"),await Rn(l);const c=fo(l);if(!c){console.warn("[App] Could not convert zones to GeoJSON");return}i(c),console.log("[App] Collector zones updated from API:",c.features.length,"zones")}else s||console.log("[App] Collector zones not available — offline and no local cache")}catch(s){console.error("[App] Failed to load collector zones:",s)}}function go(r){if(!Array.isArray(r)||r.length===0)return null;const e=new Set,t=[];for(const n of r){const o=n.id||n.parcelid||n.parcel_id;if(o!=null){if(e.has(o))continue;e.add(o)}let i=null;if(n.sp_boundary&&n.sp_boundary.type&&n.sp_boundary.coordinates)i={type:n.sp_boundary.type,coordinates:n.sp_boundary.coordinates};else{const l=n.boundary||n.polygon||n.geom||n.wkt;i=ht(l)}if(!i)continue;const s=new Set(["polygon","boundary","geom","wkt","textboundary","sp_boundary"]),a={_layerType:"parcel"};for(const[l,c]of Object.entries(n))s.has(l)||(a[l]=c);t.push({type:"Feature",properties:a,geometry:i})}return t.length===0?null:{type:"FeatureCollection",features:t}}async function $s(){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 n={type:"FeatureCollection",features:[]},o=v?.addGeoJSONLayer(n,"Parcels",e,t);if(!o){console.warn("[App] Could not create Parcels layer");return}o.setVisible(!1),o.on("change:visible",()=>{o.getVisible()&&o.getSource().getFeatures().length===0&&R("No parcels available locally. Connect to the internet to download parcel data.")});function i(s){const a=new ae().readFeatures(s,{featureProjection:"EPSG:3857"});o.getSource().clear(),o.getSource().addFeatures(a)}try{const s=await $n();if(s){const a=go(s);a&&(console.log("[App] Parcels loaded from local cache:",a.features.length,"parcels"),i(a))}if(W()&&Se()){console.log("[App] Fetching parcels from API...");const a=await hs();if(!a?.success||!Array.isArray(a?.data)){console.warn("[App] getDistrictParcels API response invalid:",a);return}const l=a.data;console.log("[App] Parcels from API:",l.length,"entries"),l.length>0&&console.log("[App] First parcel keys:",Object.keys(l[0])),await Nn(l);const c=go(l);if(!c){console.warn("[App] Could not convert parcels to GeoJSON");return}i(c),console.log("[App] Parcels updated from API:",c.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 mo(r){if(!Array.isArray(r)||r.length===0)return null;const e=["polygon","boundary","geom","wkt","footprint"],t=[];for(const n of r){const o=n.polygon||n.boundary||n.geom||n.wkt||n.footprint;let i;if(typeof o=="object"&&o!==null&&o.type?i=o:i=ht(o),!i)continue;const s={_layerType:"building_footprint"};for(const[a,l]of Object.entries(n))e.includes(a)||typeof l=="object"&&l!==null||(s[a]=l);t.push({type:"Feature",properties:s,geometry:i})}return t.length===0?null:{type:"FeatureCollection",features:t}}async function Gs(){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 n={type:"FeatureCollection",features:[]},o=v?.addGeoJSONLayer(n,"Building footprints",e,t);if(!o){console.warn("[App] Could not create Building footprints layer");return}o.setVisible(!1),o.on("change:visible",()=>{o.getVisible()&&o.getSource().getFeatures().length===0&&R("No building footprints available locally. Connect to the internet to download footprint data.")});function i(s){const a=new ae().readFeatures(s,{featureProjection:"EPSG:3857"});o.getSource().clear(),o.getSource().addFeatures(a)}try{const s=await zn();if(s){const a=mo(s);a&&(console.log("[App] Building footprints loaded from local cache:",a.features.length,"footprints"),i(a))}if(W()&&Se()){console.log("[App] Fetching building footprints from API...");const a=await fs();if(!a?.success||!Array.isArray(a?.data)){console.warn("[App] getBuildingFootprints API response invalid:",a);return}const l=a.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 jn(l);const c=mo(l);if(!c){console.warn("[App] Could not convert building footprints to GeoJSON");return}i(c),console.log("[App] Building footprints updated from API:",c.features.length,"footprints")}else s||console.log("[App] Building footprints not available — offline and no local cache")}catch(s){console.error("[App] Failed to load building footprints:",s)}}function Jo(r,e){if(!Array.isArray(r)||r.length===0)return null;const t=new wo,n=new ae,o=["geom","geometry","wkt","polygon","boundary","road","line"],i=[];for(const s of r){const a=s.geom||s.geometry||s.wkt||s.polygon||s.boundary||s.road||s.line;if(!a)continue;let l;try{if(typeof a=="object"&&a!==null&&a.type){i.push({type:"Feature",properties:yo(s,o,e),geometry:a});continue}l=t.readGeometry(a)}catch(d){console.warn(`[App] Could not parse WKT for ${e}:`,d,a?.toString().slice(0,60));continue}const c=JSON.parse(n.writeGeometry(l));i.push({type:"Feature",properties:yo(s,o,e),geometry:c})}return i.length===0?null:{type:"FeatureCollection",features:i}}function yo(r,e,t){const n={_layerType:t};for(const[o,i]of Object.entries(r))e.includes(o)||typeof i=="object"&&i!==null||(n[o]=i);return n}async function qs(){const r={strokeColor:"#78716c",strokeWidth:.8,typeDescription:"Vector / Line",fillColor:"rgba(0,0,0,0)"},e=v?.getLayerGroupByTitle("Biophysical Environment");console.log("[App] loadContoursHillshade — group:",e?e.get("title"):"null");const t={type:"FeatureCollection",features:[]},n=v?.addGeoJSONLayer(t,"Contours hillshade",r,e);if(!n){console.warn("[App] Could not create Contours hillshade layer");return}if(n.setVisible(!1),n.on("change:visible",()=>{n.getVisible()&&n.getSource().getFeatures().length===0&&R("No Contours hillshade data available. Connect to the internet to download it.")}),!W()||!Se()){console.log("[App] Contours hillshade not available — offline or server unreachable");return}try{console.log("[App] Fetching contours_hillshade from API...");const o=await gs();if(!o?.success||!Array.isArray(o?.data)){console.warn("[App] getContoursHillshade API response invalid:",o);return}const i=o.data;console.log("[App] Contours hillshade from API:",i.length,"rows"),i.length>0&&console.log("[App] First row keys:",Object.keys(i[0]));const s=Jo(i,"contours_hillshade");if(!s){console.warn("[App] Could not convert contours to GeoJSON");return}const a=new ae().readFeatures(s,{featureProjection:"EPSG:3857"});n.getSource().clear(),n.getSource().addFeatures(a),console.log("[App] Contours hillshade loaded:",a.length,"features")}catch(o){console.error("[App] Failed to load contours_hillshade:",o)}}async function js(){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 n={type:"FeatureCollection",features:[]},o=v?.addGeoJSONLayer(n,"OSM_roads",e,t);if(!o){console.warn("[App] Could not create OSM_roads layer");return}o.setVisible(!1),o.on("change:visible",()=>{o.getVisible()&&o.getSource().getFeatures().length===0&&R("No OSM roads available locally. Connect to the internet to download them.")});function i(s){const a=Jo(s,"osm_road");if(!a)return console.warn("[App] Could not convert OSM roads to GeoJSON"),0;const l=new ae().readFeatures(a,{featureProjection:"EPSG:3857"});return o.getSource().clear(),o.getSource().addFeatures(l),l.length}try{const s=await Hn();if(s){const a=i(s);console.log("[App] OSM_roads loaded from local cache:",a,"features")}if(W()&&Se()){console.log("[App] Fetching OSM_roads from API...");const a=await ms();if(!a?.success||!Array.isArray(a?.data)){console.warn("[App] getOSMRoads API response invalid:",a);return}const l=a.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 Un(l);const c=i(l);console.log("[App] OSM_roads updated from API:",c,"features")}else s||console.log("[App] OSM_roads not available — offline and no local cache")}catch(s){console.error("[App] Failed to load OSM_roads:",s)}}function zs(){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:'&copy; <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 Us(){const r="layer_categories";function e(t){const n=[...t].sort((o,i)=>i.id-o.id);for(const o of n)v?.addLayerGroup(o.id,o.name,o.description||"");console.log("[App] Created",t.length,"layer groups on map")}try{const t=await Fo(r);if(t&&(console.log("[App] Layer categories loaded from local cache:",t.length,"entries"),e(t)),W()&&Se()){console.log("[App] Fetching layer categories from API...");const n=await us();if(!n?.success||!Array.isArray(n?.data)){console.warn("[App] getLayers API response invalid:",n);return}const o=n.data;if(console.log("[App] Layer categories from API:",o.length,"entries"),await Do(r,o),t){const i=v?.getOverlayGroup()?.getLayers();if(i){const s=[];i.forEach(a=>{a.get("layerId")!==void 0&&s.push(a)}),s.forEach(a=>i.remove(a))}}e(o),console.log("[App] Layer categories refreshed from API")}else t||console.log("[App] Layer categories not available — offline and no local cache")}catch(t){console.error("[App] Failed to load layer categories:",t)}}async function Hs(){if(!W()){console.log("[App] Cannot sync - offline");return}console.log("[App] Sync placeholder - implement based on your backend")}const oe=[],Ws={strokeColor:"#e11d48",strokeWidth:2,fillColor:"rgba(225,29,72,0.12)"};function Y(r){ft("error",r);const e=document.getElementById("file-import-alert");e&&(e.querySelector(".message-text").textContent=r,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),8e3))}function $t(r,e,t){const n=Array.isArray(r)?r:[r];let o=0;for(const s of n){if(!s||s.type!=="FeatureCollection"||!s.features?.length)continue;const a=s.fileName?s.fileName.replace(/\.[^/.]+$/,""):e,l=v?.addGeoJSONLayer(s,a,Ws);l&&(l.set("removable",!0),l.set("typeTag","GEO"),oe.push(l),o+=s.features.length)}if(o===0){Y("No features found in the file.");return}console.log(`[${t}] Added ${o} feature(s) from ${n.length} layer(s)`);const i=oe[oe.length-1];if(i){const s=i.getSource().getExtent();v?.getMap().getView().fit(s,{padding:[50,50,50,50],maxZoom:18})}Gt()}function Gt(){const r=document.getElementById("imported-layers-info");if(!r)return;if(oe.length===0){r.innerHTML="",r.classList.add("d-none");return}r.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=r.querySelector("#imported-layers-list");oe.forEach((t,n)=>{const o=document.createElement("li");o.className="list-group-item d-flex justify-content-between align-items-center py-2",o.innerHTML=`<small class="text-truncate me-2">${$(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="${n}" title="Remove layer">
<i class="bi bi-x-lg" style="font-size:.75rem;"></i>
</button>
</span>`,e.appendChild(o)}),r.classList.remove("d-none"),r.querySelectorAll("[data-remove-idx]").forEach(t=>{t.addEventListener("click",()=>{Vs(Number(t.dataset.removeIdx))})}),r.querySelector("#remove-imported-layers")?.addEventListener("click",()=>{Ks()})}function Vs(r){if(r<0||r>=oe.length)return;const e=oe[r],t=v?.getOverlayGroup();t&&t.getLayers().remove(e),oe.splice(r,1),Gt(),console.log("[FileImport] Removed layer:",e.get("title"))}function Ks(){const r=v?.getOverlayGroup();if(r)for(const e of oe)r.getLayers().remove(e);oe.length=0,Gt(),console.log("[FileImport] All imported layers removed")}function Zo(r){const e={};for(const t of r){const n=t.name.split(".").pop().toLowerCase();e[n]=t}return e}async function Qo(r){const e=r.target.files;if(!e||e.length===0)return;const t=200*1024*1024,n=Array.from(e).reduce((o,i)=>o+i.size,0);if(n>t){const o=(n/1048576).toFixed(0);Y(`Files too large (${o} MB total). Maximum supported size is 200 MB.`),r.target.value="";return}try{let o,i;const s=Zo(e);if(s.zip){const a=s.zip;i=a.name.replace(/\.zip$/i,""),console.log("[ShpImport] Parsing zip",a.name,"("+(a.size/1024).toFixed(1)+" KB)"),o=await(await po())(await a.arrayBuffer())}else if(s.shp){i=s.shp.name.replace(/\.shp$/i,"");const l=["dbf","shx","prj"].filter(u=>!s[u]);if(l.length>0){Y("Missing required file(s): "+l.map(u=>"."+u).join(", ")+". Please select .shp, .dbf, .shx and .prj together."),r.target.value="";return}const c={};c.shp=await s.shp.arrayBuffer(),c.dbf=await s.dbf.arrayBuffer(),c.prj=await new Response(s.prj).text(),s.cpg&&(c.cpg=await new Response(s.cpg).text()),console.log("[ShpImport] Parsing loose files:",Object.keys(s).map(u=>"."+u).join(", "),"("+(s.shp.size/1024).toFixed(1)+" KB .shp)"),o=await(await po())(c)}else{Y("Please select a .zip or at least a .shp file."),r.target.value="";return}$t(o,i,"ShpImport")}catch(o){console.error("[ShpImport] Failed:",o),Y("Failed to parse shapefile: "+o.message)}r.target.value=""}async function en(r){const e=r.target.files?.[0];if(!e)return;const t=200*1024*1024;if(e.size>t){const n=(e.size/1048576).toFixed(0);Y(`File too large (${n} MB). Maximum supported size is 200 MB. Consider splitting the file into smaller tiles with ogr2ogr or QGIS.`),r.target.value="";return}try{const n=await e.text();console.log("[GeoJSONImport] Parsing",e.name,"("+(e.size/1024).toFixed(1)+" KB)");const o=JSON.parse(n);let i;if(o.type==="FeatureCollection")i=o;else if(o.type==="Feature")i={type:"FeatureCollection",features:[o]};else if(o.type&&o.coordinates)i={type:"FeatureCollection",features:[{type:"Feature",geometry:o,properties:{}}]};else{Y("The file does not contain valid GeoJSON."),r.target.value="";return}const s=e.name.replace(/\.(geo)?json$/i,"");$t(i,s,"GeoJSONImport")}catch(n){console.error("[GeoJSONImport] Failed:",n);const o=(e.size/(1024*1024)).toFixed(1);Y(`Failed to import "${e.name}" (${o} MB): ${n.message}`)}r.target.value=""}async function tn(r){const e=r.target.files?.[0];if(!e)return;const t=200*1024*1024;if(e.size>t){const n=(e.size/1048576).toFixed(0);Y(`File too large (${n} MB). Maximum supported size is 200 MB.`),r.target.value="";return}try{const n=await e.text();console.log("[KMLImport] Parsing",e.name,"("+(e.size/1024).toFixed(1)+" KB)");const i=new dn({extractStyles:!1}).readFeatures(n,{featureProjection:"EPSG:3857"});if(!i||i.length===0){Y("No features found in the KML file."),r.target.value="";return}const s=new ae,a=JSON.parse(s.writeFeatures(i,{featureProjection:"EPSG:3857",dataProjection:"EPSG:4326"})),l=e.name.replace(/\.kml$/i,"");$t(a,l,"KMLImport")}catch(n){console.error("[KMLImport] Failed:",n);const o=(e.size/(1024*1024)).toFixed(1);Y(`Failed to import "${e.name}" (${o} MB): ${n.message}`)}r.target.value=""}function Ys(){const r=document.querySelector(".map-container");if(!r)return;let e=0;r.addEventListener("dragenter",t=>{t.preventDefault(),e++,r.classList.add("drag-over")}),r.addEventListener("dragover",t=>{t.preventDefault()}),r.addEventListener("dragleave",t=>{t.preventDefault(),e--,e<=0&&(e=0,r.classList.remove("drag-over"))}),r.addEventListener("drop",t=>{t.preventDefault(),e=0,r.classList.remove("drag-over");const n=t.dataTransfer?.files;if(!n||n.length===0)return;const o=Zo(n),i=Object.keys(o);if(o.zip||o.shp){const s={target:{files:n,value:""}};Object.defineProperty(s.target,"value",{writable:!0}),Qo(s)}else if(o.geojson||o.json){const a={target:{files:[o.geojson||o.json],value:""}};Object.defineProperty(a.target,"value",{writable:!0}),en(a)}else if(o.kml){const s={target:{files:[o.kml],value:""}};Object.defineProperty(s.target,"value",{writable:!0}),tn(s)}else Y("Unsupported file type(s): "+i.map(s=>"."+s).join(", ")+". Drop .zip, .shp, .geojson, .json, or .kml files.")}),console.log("[FileImport] Map drop zone initialised")}function $(r){const e=document.createElement("div");return e.textContent=r,e.innerHTML}const Xs=50,bo={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(r,e){const t=bo[r]||bo.info;(r==="error"?console.error:r==="warning"?console.warn:console.log)("[App]",e);const o=document.getElementById("message-log");if(!o)return;const i=o.querySelector(".text-muted");i&&i.remove();const s=document.createElement("div");s.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(s.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>${$(e)}</small></div><small class="text-muted flex-shrink-0 ms-1">${l}</small></div>`,o.prepend(s);o.children.length>Xs;)o.lastElementChild.remove()}function Js(){const r=document.getElementById("clear-message-log");r&&r.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>')})}function Zs(){const r=document.getElementById("gps-readout"),e=document.getElementById("gps-coords"),t=document.getElementById("gps-accuracy"),n=document.getElementById("gps-sats");if(!pe.isSupported){e&&(e.textContent="No GPS");return}pe.on("position",i=>{e&&(e.textContent=`${uo(i.lat)}, ${uo(i.lon)}`),t&&(t.textContent=_s(i.accuracy)),n&&(n.textContent=`${i.satellites!=null?i.satellites:"—"} sat`),r&&(r.classList.add("active"),r.classList.remove("quality-good","quality-fair","quality-poor"),r.classList.add("quality-"+Es(i.accuracy))),v?.showCurrentPosition(i.lon,i.lat,i.accuracy)}),pe.on("point",i=>{v?.appendTrailPoint(i.point.lon,i.point.lat)}),pe.on("error",i=>{console.warn("[GPS]",i?.message||i),i&&i.code===1&&R("Location permission denied. Enable location access to use GPS.")}),v.onLocateMe(async()=>{try{const i=await pe.getCurrentPosition();v.centerOn(i.lon,i.lat,16)}catch(i){R("Could not get your location: "+(i?.message||i))}}),v.onToggleRecording(async i=>{if(i)try{await Zt,v.startTrailRender(),v.setRecordingState(!0),r?.classList.add("recording"),await pe.startRecording({name:`Trail ${new Date().toLocaleString()}`}),ie("GPS trail recording started")}catch(s){v.setRecordingState(!1),r?.classList.remove("recording"),R("Could not start recording: "+(s?.message||s))}else try{const s=await pe.stopRecording();if(v.setRecordingState(!1),r?.classList.remove("recording"),s){const a=`Trail saved: ${s.pointCount} points, ${vs(s.distanceM)}`+(s.synced?" — synced":" — will sync when online");ie(a)}}catch(s){R("Error stopping recording: "+(s?.message||s))}});const o=async()=>{if(W())try{await Zt;const i=await pe.syncPending();i.pushed&&console.log(`[GPS] Synced ${i.pushed} pending trail(s)`)}catch(i){console.warn("[GPS] pending-sync error",i)}};o(),Uo(i=>{i||o()})}function R(r){ft("error",r);const e=document.getElementById("error-message");e&&(e.querySelector(".message-text").textContent=r,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),5e3))}function ie(r){ft("success",r);const e=document.getElementById("success-message");e&&(e.querySelector(".message-text").textContent=r,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),3e3))}function Qs(r){ft("warning",r);const e=document.getElementById("warning-message");e&&(e.querySelector(".message-text").textContent=r,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),5e3))}function ei(){const r=document.getElementById("fieldwork-mode-toggle");if(!r)return;localStorage.getItem("fieldwork-mode")==="true"&&(document.documentElement.classList.add("fieldwork-mode"),r.checked=!0),r.addEventListener("change",()=>{document.documentElement.classList.toggle("fieldwork-mode",r.checked),localStorage.setItem("fieldwork-mode",r.checked),console.log("[Settings] Fieldwork mode",r.checked?"ON":"OFF")})}function ti(){const r=document.getElementById("dark-mode-toggle");if(!r)return;function e(n){document.documentElement.classList.toggle("dark-mode",n),document.documentElement.setAttribute("data-bs-theme",n?"dark":"light")}localStorage.getItem("dark-mode")==="true"&&(r.checked=!0,e(!0)),r.addEventListener("change",()=>{e(r.checked),localStorage.setItem("dark-mode",r.checked),console.log("[Settings] Dark mode",r.checked?"ON":"OFF")})}function oi(){const r=document.getElementById("measurement-system-toggle"),e=document.getElementById("measurement-system-label");if(!r)return;function t(){e&&(e.textContent=r.checked?"Imperial":"Metric")}const n=localStorage.getItem("measurement-system");n==="imperial"&&(r.checked=!0),t(),v?.setScaleBarUnits(n||"metric"),r.addEventListener("change",()=>{const o=r.checked?"imperial":"metric";localStorage.setItem("measurement-system",o),t(),v?.setScaleBarUnits(o),console.log("[Settings] Measurement system:",o)})}function ni(){const r=document.getElementById("default-basemap-select");if(!r)return;const e=localStorage.getItem("default-basemap")||"topo";r.value=e,r.addEventListener("change",()=>{const t=r.value;localStorage.setItem("default-basemap",t),v?.setBaseMap(t),console.log("[Settings] Default base map:",t)}),v?.getMap()?.on("basemapchange",t=>{if(t?.key&&r.value!==t.key){r.value=t.key;try{localStorage.setItem("default-basemap",t.key)}catch{}}})}function ri(){const r=document.getElementById("tile-cache-stats"),e=document.getElementById("clear-tiles-btn"),t=document.getElementById("offcanvasBottom");if(!r||!e||!t)return;function n(s){return s?s<1024*1024?(s/1024).toFixed(0)+" KB":s<1024*1024*1024?(s/(1024*1024)).toFixed(1)+" MB":(s/(1024*1024*1024)).toFixed(2)+" GB":"0 KB"}let o=null;async function i(){if(o)return o;const s=!!navigator.serviceWorker?.controller;return r.innerHTML=s?'<div class="text-muted fst-italic">Loading…</div>':'<div class="text-muted fst-italic">Initialising service worker…</div>',o=(async()=>{try{const a=await Hr();if(!a){r.innerHTML=`
<div class="text-muted fst-italic">
Tile cache stats unavailable. Try reloading the page if this persists.
</div>`;return}const l=a.totals,c=a.byProvider.filter(p=>p.count>0).map(p=>`
<tr>
<td>${$(p.label)}</td>
<td class="text-end">${p.count.toLocaleString()} / ${p.limit.toLocaleString()}</td>
<td class="text-end">${n(p.estBytes)}</td>
<td class="text-end pe-0" style="width:2.2rem;">
<button type="button" class="btn btn-sm btn-link text-danger p-0 provider-clear-btn"
data-cache="${$(p.key)}" data-label="${$(p.label)}"
title="Clear ${$(p.label)} tiles only">
<i class="bi bi-trash3"></i>
</button>
</td>
</tr>`).join("");let d="";const u=await Kr();if(u&&u.quota>0){const p=(u.usage/u.quota*100).toFixed(1);d=`
<div class="mt-2 text-muted">
Total app storage: ${n(u.usage)} of ${n(u.quota)} available (${p}%)
</div>`}if(l.count===0){r.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}r.innerHTML=`
<div class="mb-1">
<strong>${l.count.toLocaleString()}</strong> tiles cached, ~${n(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,r.querySelectorAll(".provider-clear-btn").forEach(p=>{p.addEventListener("click",async f=>{f.preventDefault();const h=p.dataset.cache,m=p.dataset.label||h;if(!confirm(`Clear cached "${m}" tiles?
Other providers are not affected. The tiles will re-download as you browse online.`))return;p.disabled=!0,await Vr(h)?console.log(`[Settings] Cleared tile cache for ${m}`):console.warn(`[Settings] Could not clear tile cache for ${m}`),await i()})})}finally{o=null}})(),o}e.addEventListener("click",async()=>{if(!confirm("Clear all cached map tiles from this device? You will need to be online to view them again."))return;e.disabled=!0,await Wr()?console.log("[Settings] Tile caches cleared"):console.warn("[Settings] Tile-cache clear failed"),await i()}),t.addEventListener("show.bs.offcanvas",i),Ur(()=>{console.log("[Settings] SW controller changed → refreshing tile-cache stats"),i()}),i()}function si(){const r=document.getElementById("download-tiles-btn"),e=document.getElementById("offline-download-modal");if(!r||!e)return;const t=Ft.getOrCreateInstance(e),n=document.getElementById("offline-download-form-view"),o=document.getElementById("offline-download-progress-view"),i=document.getElementById("offline-download-done-view"),s=document.getElementById("offline-download-cancel-btn"),a=document.getElementById("offline-download-start-btn"),l=document.getElementById("offline-download-close-done-btn"),c=document.getElementById("offline-download-close-btn"),d=document.getElementById("offline-basemap-select"),u=document.getElementById("offline-min-zoom"),p=document.getElementById("offline-max-zoom"),f=document.getElementById("offline-ack-check"),h=document.getElementById("offline-estimate-detail"),m=document.getElementById("offline-estimate"),g=document.getElementById("offline-area-view"),y=document.getElementById("offline-area-district"),b=document.getElementById("offline-area-ghana"),E=document.getElementById("offline-area-view-info"),L=document.getElementById("offline-area-district-info"),x=document.getElementById("offline-progress-bar"),w=document.getElementById("offline-progress-percent"),S=document.getElementById("offline-progress-counts"),P=document.getElementById("offline-progress-ok"),T=document.getElementById("offline-progress-failed"),j=document.getElementById("offline-progress-eta"),U=document.getElementById("offline-done-title"),V=document.getElementById("offline-done-detail");let A=null;function ee(D){return D?D<1024*1024?(D/1024).toFixed(0)+" KB":D<1024*1024*1024?(D/(1024*1024)).toFixed(1)+" MB":(D/(1024*1024*1024)).toFixed(2)+" GB":"0 KB"}function K(D){if(!D||D<1e3)return"< 1 s";const H=Math.round(D/1e3);if(H<60)return H+" s";const z=Math.floor(H/60),X=H%60;return z<60?`${z} min ${X} s`:`${Math.floor(z/60)} h ${z%60} min`}function le(){return g.checked?v?.getCurrentViewExtent()||null:y.checked?v?.getDistrictBoundaryExtent()?.extent||null:b.checked?ts:null}function G(){const D=d.value,H=parseInt(u.value,10),z=parseInt(p.value,10);if(Number.isNaN(H)||Number.isNaN(z)||H>z){h.textContent="Invalid zoom range",m.classList.replace("alert-info","alert-warning"),a.disabled=!0;return}const X=le();if(!X){h.textContent="Selected area is not available.",m.classList.replace("alert-info","alert-warning"),a.disabled=!0;return}const C=Ho[D]?.maxZoom??19,q=Math.min(z,C),J=Jr(X,H,q),gt=os(J);let de="";q<z&&(de=`<br><span class="text-warning">Zoom ${z} is above this provider's max (${C}); will clamp to ${C}.</span>`),J>8e3&&(de+='<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>${J.toLocaleString()}</strong> tiles · ~${ee(gt)}`+de,m.classList.toggle("alert-warning",!!de),m.classList.toggle("alert-info",!de),a.disabled=!f.checked||J===0}function ce(){v?.getCurrentViewExtent()?E.textContent=" · ready":E.textContent="",v?.getDistrictBoundaryExtent()?(L.textContent="",y.disabled=!1):(L.textContent=" (not loaded — connect online to fetch)",y.disabled=!0,y.checked&&(g.checked=!0))}function De(){n.classList.remove("d-none"),o.classList.add("d-none"),i.classList.add("d-none"),a.classList.remove("d-none"),s.classList.remove("d-none"),s.textContent="Cancel",l.classList.add("d-none"),c.disabled=!1,f.checked=!1,a.disabled=!0,A=null}r.addEventListener("click",()=>{De(),ce(),G(),t.show()}),d.addEventListener("change",G),u.addEventListener("input",G),p.addEventListener("input",G),g.addEventListener("change",G),y.addEventListener("change",G),b.addEventListener("change",G),f.addEventListener("change",G),a.addEventListener("click",async()=>{const D=d.value,H=parseInt(u.value,10),z=parseInt(p.value,10),X=le();if(!X)return;n.classList.add("d-none"),o.classList.remove("d-none"),a.classList.add("d-none"),s.textContent="Cancel download",c.disabled=!0,x.style.width="0%",x.setAttribute("aria-valuenow","0"),w.textContent="0%",S.textContent="0 of 0 tiles",P.textContent="0",T.textContent="0",j.textContent="—",A=new es({baseMap:D,extent3857:X,minZoom:H,maxZoom:z,onProgress:q=>{if(q.total>0){const J=Math.min(100,Math.round(q.done/q.total*100));x.style.width=J+"%",x.setAttribute("aria-valuenow",String(J)),w.textContent=J+"%",S.textContent=`${q.done.toLocaleString()} of ${q.total.toLocaleString()} tiles`}P.textContent=q.ok.toLocaleString(),T.textContent=q.failed.toLocaleString(),j.textContent=q.etaMs!=null?K(q.etaMs):"—"}});let C;try{C=await A.start()}catch(q){console.error("[OfflineDownload] failed:",q),C={phase:"error",done:0,total:0,ok:0,failed:0}}o.classList.add("d-none"),i.classList.remove("d-none"),s.classList.add("d-none"),l.classList.remove("d-none"),c.disabled=!1,C.phase==="cancelled"?(U.textContent="Download cancelled",V.innerHTML=`Stopped after <strong>${C.done.toLocaleString()}</strong> of ${C.total.toLocaleString()} tiles.<br>${C.ok.toLocaleString()} fetched · ${C.failed.toLocaleString()} failed.`):C.phase==="error"?(U.textContent="Download failed",V.textContent="See console for details."):(U.textContent="Download complete",V.innerHTML=`<strong>${C.ok.toLocaleString()}</strong> tiles cached`+(C.failed>0?`, ${C.failed.toLocaleString()} failed`:"")+`.<br>Took ${K(C.elapsedMs)}.`)}),s.addEventListener("click",()=>{A&&A.cancel()}),e.addEventListener("hidden.bs.modal",()=>{A&&A.cancel(),De()})}function ii(){const r=Yo(),e=document.getElementById("menu-btn"),t=document.getElementById("menu-user-avatar"),n=document.getElementById("menu-user-name"),o=document.getElementById("menu-user-email"),i=document.getElementById("menu-user-detail"),s=document.getElementById("menu-signout-btn"),a=document.getElementById("menu-signin-link"),l=document.getElementById("menu-no-session-note");if(!e||!t||!n||!o||!i||!s){console.warn("[AccountMenu] One or more elements missing — shell may be stale. Hard-refresh.");return}if(!!r&&!!r.user_id){const d=[r.title,r.full_name].filter(Boolean).join(" ").trim()||r.username||"Authenticated user",u=(r.full_name||r.username||"?").trim().charAt(0).toUpperCase();t.textContent=u,t.style.background="var(--brand-navy, #1e1a4b)",n.textContent=d,o.textContent=r.email||"";const p=[];r.district_id!=null&&p.push(`District ${$(String(r.district_id))}`),r.region_id!=null&&p.push(`Region ${$(String(r.region_id))}`),r.ua_position&&p.push($(r.ua_position)),i.innerHTML=p.join(" · ")||"No district info",s.classList.remove("d-none"),s.addEventListener("click",()=>ai(r),{once:!1}),a?.classList.add("d-none"),l?.classList.add("d-none"),e.removeAttribute("data-state"),e.setAttribute("title",`Menu — ${d}`)}else typeof window.LUPMIS_SESSION>"u"?(t.innerHTML='<i class="bi bi-exclamation"></i>',t.style.background="var(--brand-orange-warm, #ff9e1b)",n.textContent="No session injected",o.textContent="",i.textContent="",s.classList.add("d-none"),a?.classList.add("d-none"),l?.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)",n.textContent="Not signed in",o.textContent="",i.textContent="",s.classList.add("d-none"),a?.classList.remove("d-none"),l?.classList.add("d-none"),e.dataset.state="unauthenticated",e.setAttribute("title","Menu (not signed in)"))}async function ai(r){if(!confirm(`Return to Landing Page, ${r?.full_name||r?.username||"user"}?`))return;const e=document.cookie.split(";").map(n=>n.trim()).find(n=>n.startsWith("sso_auth_token="))?.split("=")[1];if(e)try{await fetch("https://lupmis4luspa.org/sso/logout?token="+encodeURIComponent(e),{method:"GET",mode:"no-cors",credentials:"include",cache:"no-store"})}catch(n){console.warn("[Signout] Best-effort SSO logout call failed:",n)}const t="Thu, 01 Jan 1970 00:00:00 GMT";document.cookie=`sso_auth_token=; expires=${t}; path=/; domain=.lupmis4luspa.org`,document.cookie=`sso_auth_token=; expires=${t}; path=/; domain=lupmis4luspa.org`,document.cookie=`sso_auth_token=; expires=${t}; path=/`,window.location.href="https://lupmis4luspa.org/"}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",ho):ho();
//# sourceMappingURL=index-DJ2WL3EC.js.map