From 933bfcf4c07c774e423a9718994c6edece9b3a1a Mon Sep 17 00:00:00 2001 From: ekke Date: Mon, 1 Jun 2026 16:20:15 +0200 Subject: [PATCH] Permit-map iframe embed, lu_parcels schema, security guards, LayerSwitcher fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iframe embed for the Permitting app (LUPMIS2_Reusable_Mapping_Concept §3.2): - public/embed.php — SSO + production gate + frame-ancestors CSP + whitelisted URL params (mode, lon/lat/zoom, upn, basemap, application_code); injects window.LUPMIS_SESSION + window.LUPMIS_EMBED. - public/.htaccess — clean /embed URL (rewrite before the SPA fallback). - src/embed-bridge.js — postMessage protocol: out ready / parcel:select / parcel:cleared / error; in set:view / set:selected / clear:selected / set:basemap. Visual highlight via a dedicated VectorLayer; pending-UPN queue resolved as parcels stream in. - main.js — reads window.LUPMIS_EMBED, gates the normal click/dblclick handlers in permit mode, exposes parcelsLayer to module scope, makes it visible and hands it to the bridge after loadParcels(). - index.html — CSS for body.embed-mode-permit hides navbar/dock/offcanvas and lets the map fill the iframe. - LUPMIS2_Permit_Map_Integration.docx — integration instructions for the Permitting team (contract, show.blade.php changes, phasing). Local lu_parcels structural refactor: - src/database.js — parcels table now mirrors spatial.lu_parcels with explicit columns (upn, style, landuse, zone_code/name, sector, block, parcel_no, prop_no, st_name, prop_add, fac_name, min/max_height, eff_date, lp_name, locality, mmda, last_update, remarks, geom→geometry_wkt, created_at, updated_at, districtid) plus local-only status/fetched_at. Drop-and-recreate migration off `upn` presence. saveParcels wraps the ~25k inserts in a transaction; numeric coercion via numOrNull. updateParcel/insertNewParcel write individual columns. - main.js parcelsToGeoJSON — handles GeoJSON `geom` object (API) and `geometry_wkt` string (local cache); skips housekeeping fields. Production access guard + no-district overlay: - public/index.php — on *.lupmis4luspa.org, redirect to the SSO portal if no session. - src/remotedb.js resolveDistrictId — no silent fallback to '1' for an authenticated user; dev mode (no session at all) keeps the fallback. - main.js — blocking overlay if the session lacks district_id; init aborts so no API call is made with the wrong scope. LayerSwitcher ordering fix: - MapView.initEditBar + MapTools — find the Overlays group by reference / title instead of assuming it's the last layer (the GPS layers add-layered on top in the constructor broke that assumption). Service Worker v8 → v9 to evict stale shell/module caches on deploy. Co-Authored-By: Claude Opus 4.7 (1M context) --- LUPMIS2_Permit_Map_Integration.docx | Bin 0 -> 18503 bytes dist/.htaccess | 12 +- dist/assets/index-DJ2WL3EC.js | 707 ------------------------ dist/assets/index-DJ2WL3EC.js.map | 1 - dist/assets/index-YjHYbDyk.js | 803 ++++++++++++++++++++++++++++ dist/assets/index-YjHYbDyk.js.map | 1 + dist/embed.php | 157 ++++++ dist/index.html | 29 +- dist/index.php | 15 + dist/sw.js | 6 +- index.html | 27 + main.js | 117 +++- public/.htaccess | 12 +- public/embed.php | 157 ++++++ public/index.php | 15 + public/sw.js | 6 +- src/components/MapTools.js | 9 +- src/components/MapView.js | 11 +- src/database.js | 166 ++++-- src/embed-bridge.js | 262 +++++++++ src/remotedb.js | 25 +- 21 files changed, 1766 insertions(+), 772 deletions(-) create mode 100644 LUPMIS2_Permit_Map_Integration.docx delete mode 100644 dist/assets/index-DJ2WL3EC.js delete mode 100644 dist/assets/index-DJ2WL3EC.js.map create mode 100644 dist/assets/index-YjHYbDyk.js create mode 100644 dist/assets/index-YjHYbDyk.js.map create mode 100644 dist/embed.php create mode 100644 public/embed.php create mode 100644 src/embed-bridge.js diff --git a/LUPMIS2_Permit_Map_Integration.docx b/LUPMIS2_Permit_Map_Integration.docx new file mode 100644 index 0000000000000000000000000000000000000000..92824538d9864a91aa9ab1860b7b705e91565b78 GIT binary patch literal 18503 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 -# Common single-page-app behaviour: if a route doesn't map to a real file or -# directory, send the request to index.php so the PWA can handle it client-side. -# Comment out the next block if hash-based routing is preferred (no rewrites). RewriteEngine On + + # Clean URL for the iframe embed endpoint: /embed → embed.php + # Must come BEFORE the SPA fallback so /embed doesn't get routed to + # index.php. Query strings (?mode=permit&...) pass through automatically. + RewriteRule ^embed/?$ embed.php [L] + + # Common single-page-app behaviour: if a route doesn't map to a real file + # or directory, send the request to index.php so the PWA can handle it + # client-side. Comment out this block if hash-based routing is preferred. RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^ index.php [L] diff --git a/dist/assets/index-DJ2WL3EC.js b/dist/assets/index-DJ2WL3EC.js deleted file mode 100644 index b39ca63..0000000 --- a/dist/assets/index-DJ2WL3EC.js +++ /dev/null @@ -1,707 +0,0 @@ -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({[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(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)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;tr[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;oo.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])=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]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]=d0,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{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{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;lr[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;i0;){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])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])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])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{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;tt&&(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;cu&&(u=E)}const p=(u-d)*1.5,f=[];let h=r,m=e;for(let g=0;gw&&(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=Fethis._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{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{const l=this.categoryEmojis[a];return l?l.emoji:"📍"},this.getCategoryOptionsHtml=()=>Object.entries(this.categoryEmojis).map(([a,{emoji:l,label:c}])=>``).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:'',className:"ol-undo",title:"Undo",handleClick:()=>{this._undoRedo.hasUndo()&&this._undoRedo.undo()}}),new $e({html:'',className:"ol-redo",title:"Redo",handleClick:()=>{this._undoRedo.hasRedo()&&this._undoRedo.redo()}}),new $e({html:'',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:'',className:"ol-split-line",title:"Split Lines",name:"SplitLine",interaction:this._lineSplitInteraction,autoActivate:!0}),i=new fe({html:'',className:"ol-split-polygon",title:"Split Polygons",name:"SplitPolygon",interaction:this._polygonSplitInteraction}),s=new fe({html:'',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:'',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:'',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{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=` -
- ${this.getEmoji(o)} ${this.escapeHtml(n)} -
- `;const u={water:"#3b82f6",school:"#f59e0b",health:"#ef4444",market:"#8b5cf6",default:"#2d5016",other:"#6b7280"}[o]||"#6b7280";c+=` -
- ${o} -
- `,i&&(c+=` -
- ${this.escapeHtml(i)} -
- `),s!==void 0&&a!==void 0&&(c+=` -
- ${Number(s).toFixed(5)}, ${Number(a).toFixed(5)} -
- `),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+=` - - ${this.escapeHtml(p)} - ${this.escapeHtml(String(f))} - - `);if(l==="Polygon"||l==="MultiPolygon"){const p=qe(a,{projection:"EPSG:3857"}),f=ir(p);d+=` - - area - ${f} - - `}else if(l==="LineString"||l==="MultiLineString"){const p=ot(a,{projection:"EPSG:3857"}),f=sr(p);d+=` - - length - ${f} - - `}else if(l==="Point"){const p=Le(a.getCoordinates()),f=p[0].toFixed(6),h=p[1].toFixed(6);d+=` - - longitude - ${f} - - - latitude - ${h} - - `}const u=` -
- ${this.escapeHtml(o)} - -
-
- - ${d} -
-
- `;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+=` - - ${i.value} - `;continue}const s=i.color||"var(--muted-foreground, #7a7a7a)",a=i._first?"":"border-top:1px solid var(--border, #1e1a4b1f);";o+=` - - ${i.label} - ${i.value} - `}return` -
- ${e} ${t} - -
-
- - ${o} -
-
-
- -
`}_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[2]||b[3]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[2]||y[3]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 - - - - `}const s=` -
- ✏️ Edit Parcel - -
-
- ${i} -
- - -
-
- `;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=` -
- 🔗 Merged Parcel — Choose Identifier - -
-
-

- Select which parcel's attributes the merged polygon should keep: -

- - -
- - -
-
- `;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=` -
- Divide Polygon - -
-
-

- Enter the number of equal pieces: -

- -
- - -
-
- `;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+=` -
- - -
- `}const i=e.getGeometry(),s=qe(i,{projection:"EPSG:3857"}),l=` -
- 📐 Polygon Attributes - -
-
- Area: ${je(s)} -
-
- ${o} -
- - -
-
- `;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=` -
- ➕ Add Location - -
-
-
- - -
-
- - -
-
- - -
-
- 📍 -
- -
- `,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='',t.appendChild(n);const o=document.createElement("div");o.className="ls-basemap-panel",o.innerHTML='
Base Map
'+e.map(s=>` - - `).join("")+"
",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?'':''),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='',e.appendChild(t);const n=document.createElement("div");n.className="ls-locate-actions",n.innerHTML='',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=` -
- Add External Layer - -
-
-
- -
- - - -
-
-
- - -
-
- - -
- WMS LAYERS parameter (e.g. workspace:layer) -
-
-
- - -
-
- - -
-
- `,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='')}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='',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=` - Layers - 0 active - `,t.insertBefore(o,n));let i=t.querySelector(":scope > .ls-footer-row");i||(i=document.createElement("div"),i.className="ls-footer-row",i.innerHTML=` - — layers total - - `,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=` -
- ${this._escapeHtml(t)} -
- ${this._escapeHtml(t)} legend - `,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,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}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=`${lt(a)}
${l}`;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:'',title:"Measure Circle (radius & area)",className:"measure-circle-btn",onToggle:l=>{l?this.startCircleMeasure():this.deactivate()}});n.addControl(o);const i=new fe({html:'📏',title:"Measure Distance",className:"measure-line-btn",onToggle:l=>{l?this.startLineMeasure():this.deactivate()}});n.addControl(i);const s=new fe({html:'',title:"Measure Area",className:"measure-area-btn",onToggle:l=>{l?this.startAreaMeasure():this.deactivate()}});n.addControl(s);const a=new $e({html:'🗑️',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{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.concurrencysetTimeout(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._lastRecordedAt0&&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=` -
-

No locations yet.

- Click the map or fill the form above! -
- `;return}const n={water:"💧",school:"🏫",health:"🏥",market:"🏪",default:"📍",other:"📌"};e.innerHTML=r.map(o=>{const i=n[o.category]||"📍";return` - -
-
-
${i} ${$(o.name)}
- ${o.latitude.toFixed(5)}, ${o.longitude.toFixed(5)} -
- ${o.category} -
- ${o.description?`${$(o.description)}`:""} -
- `}).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)?``:"";return` - - - ${$(o.name)} - - ${o.count} - ${s} - - `}).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='Failed to load',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=` -
-
- Loading... -
-
- `,n.textContent="",new Ft(document.getElementById("tableContentModal")).show();try{const{columns:i,rows:s}=await Jn(r);if(s.length===0){t.innerHTML='
Table is empty
',n.textContent="0 rows";return}const a=i.map(c=>`${$(c)}`).join(""),l=s.map(c=>`${i.map(u=>{let p=c[u];if(p==null)return'NULL';p=String(p);const f=p.length>120?p.substring(0,120)+"...":p;return`${$(f)}`}).join("")}`).join("");t.innerHTML=` -
- - - ${a} - - ${l} -
-
- `,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=`
Failed to load: ${$(i.message)}
`}}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=` - - - - - - - - - - - - - - - - - - - - - - - -
Ready:${r.ready?"Yes":"No"}
Online:${W()?"Yes":"Offline"}
Database:${r.databasePath||"N/A"}
Tables:${r.tables.map(n=>`${n}`).join("")}
Locations:${r.locationCount}
- `),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:'© Digital Earth Africa — 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=` -
-
-
Imported Layers
- -
-
    -
    `;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=`${$(t.get("title"))} - - ${t.getSource().getFeatures().length} - - `,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=`
    ${$(e)}
    ${l}
    `,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='
    No messages yet.
    ')})}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?'
    Loading…
    ':'
    Initialising service worker…
    ',o=(async()=>{try{const a=await Hr();if(!a){r.innerHTML=` -
    - Tile cache stats unavailable. Try reloading the page if this persists. -
    `;return}const l=a.totals,c=a.byProvider.filter(p=>p.count>0).map(p=>` - - ${$(p.label)} - ${p.count.toLocaleString()} / ${p.limit.toLocaleString()} - ${n(p.estBytes)} - - - - `).join("");let d="";const u=await Kr();if(u&&u.quota>0){const p=(u.usage/u.quota*100).toFixed(1);d=` -
    - Total app storage: ${n(u.usage)} of ${n(u.quota)} available (${p}%) -
    `}if(l.count===0){r.innerHTML=` -
    - No tiles cached yet. Pan and zoom the map to start caching tiles automatically. -
    ${d}`,e.disabled=!0;return}r.innerHTML=` -
    - ${l.count.toLocaleString()} tiles cached, ~${n(l.estBytes)} on this device -
    - - - - - - - - ${c} -
    Base mapCached / limitApprox. size
    ${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="";qZoom ${z} is above this provider's max (${C}); will clamp to ${C}.`),J>8e3&&(de+='
    More than 8 000 tiles — exceeds the per-provider cache limit. Earlier tiles will be evicted as new ones arrive.'),h.innerHTML=`${J.toLocaleString()} 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 ${C.done.toLocaleString()} of ${C.total.toLocaleString()} tiles.
    ${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=`${C.ok.toLocaleString()} tiles cached`+(C.failed>0?`, ${C.failed.toLocaleString()} failed`:"")+`.
    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='',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='',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 diff --git a/dist/assets/index-DJ2WL3EC.js.map b/dist/assets/index-DJ2WL3EC.js.map deleted file mode 100644 index 3a35254..0000000 --- a/dist/assets/index-DJ2WL3EC.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"mappings":"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","names":["FUNCTION","CHANNEL","GET","HAS","SET","isArray","SharedArrayBuffer","window","notify","wait","waitAsync","postPatched","buffer","onmessage","w","ids","resolvers","postMessage","listener","event","details","id","sb","data","rest","resolve","buff","i","as","Int32Array","Map","Uint16Array","I32_BYTES","UI16_BYTES","waitInterrupt","delay","handler","buffers","context","syncResult","fn","uid","coincident","self","parse","stringify","transform","interrupt","sendMessage","post","transfer","args","decoder","waitFor","isAsync","seppuku","_","action","deadlock","length","bytes","actions","callback","type","results","error","result","serialized","ui16a","createMutex","promise","res","normalizeDatabaseFile","dbFile","convertStreamTo","bufferOrStream","reader","chunks","streamDone","chunk","arrayLength","offset","SQLiteMemoryDriver","sqlite3InitModule","config","databasePath","flags","__vitePreload","statement","statements","tx","prepared","stmt","newStmt","columns","rows","size","database","dataPointer","resultCode","pointer","readOnly","verbose","db","statementData","opMap","_ctx","opId","_db","table","rowid","cb","debounce","func","options","lastArgs","lastThis","maxWait","timerId","lastCallTime","lastInvokeTime","leading","maxing","trailing","invokeFunc","time","thisArg","leadingEdge","timerExpired","remainingWait","timeSinceLastCall","timeSinceLastInvoke","timeWaiting","shouldInvoke","trailingEdge","cancel","flush","debounced","isInvoking","getQueryKey","getDatabaseKey","clientKey","sessionKey","SQLocalProcessor","driver","reason","dbKey","message","change","_transfer","response","partOfTransaction","sql","name","queryKey","errored","proxy","sqlTag","queryTemplate","params","isArrayOfArrays","row","convertRowsToObjects","checkedRows","rowObj","column","columnIndex","isDrizzleStatement","isStatement","normalizeStatement","drizzleStatement","exec","normalizeSql","maybeQueryTemplate","mutationLock","mode","bypass","mutation","SQLiteKvvfsDriver","storageType","memdb","_a","_b","SQLocal","queries","reject","userCallback","method","transactionKey","resultIndex","passStatements","query","passStatement","transaction","err","value","gotFirstValue","isListening","updateCount","watchedTables","subObservers","errObservers","runStatement","updateOrder","usedTables","readTables","writtenTables","observer","onEffect","onData","onError","funcName","key","attachFunction","databaseFile","beforeUnlock","clientConfig","onInit","onConnect","processor","commonConfig","DATABASE_PATH","BROADCAST_CHANNEL","channel","isReady","readyResolve","readyReject","dbReady","changeListeners","onDatabaseChange","payload","e","broadcastChange","initSchema","testResult","tablesAfterLocations","allTables","t","addLocation","longitude","latitude","description","category","tableCheck","newId","verifyResult","getLocations","limit","getLocationCount","saveRemoteData","json","getRemoteData","parsed","saveCollectorZones","zones","z","props","getLocalCollectorZones","r","saveParcels","parcels","saved","p","wkt","getLocalParcels","updateParcel","parcelId","updatedProps","insertNewParcel","geometryWkt","properties","saveBuildingFootprints","footprints","first","types","k","v","f","rawWkt","rawId","getLocalBuildingFootprints","saveOSMRoads","roads","getLocalOSMRoads","exportDatabase","downloadDatabase","filename","blob","url","a","exportToGeoJSON","loc","getDatabaseStatus","tables","locationCount","CACHED_LAYER_TABLES","isCachedLayerTable","tableName","clearTable","count","clearAllCachedLayers","existing","existingNames","total","s","getTableStats","getTableContent","testDatabase","version","createGpsTrail","meta","uuid","startedAt","districtId","addGpsTrailPoint","trailId","point","seq","lon","lat","altitude","accuracy","altitudeAccuracy","heading","speed","satellites","timestamp","recordedAt","finishGpsTrail","summary","endedAt","pointCount","distanceM","getUnsyncedGpsTrails","getGpsTrailPoints","markGpsTrailSynced","remoteId","M_TO_FT","M_TO_MI","SQM_TO_SQFT","SQM_TO_ACRE","SQM_TO_SQMI","getSystem","formatLength","metres","ft","formatLengthFull","mi","formatArea","sqMetres","acres","formatAreaFull","sqft","sqmi","formatCircleExtent","radiusMetres","segmentIntersection","p1","p2","p3","p4","eps","dx1","dy1","dx2","dy2","denom","dx3","dy3","u","signedArea","ring","area","pointInRing","pt","inside","j","xi","yi","xj","yj","dist2","b","findIntersections","line","hits","li","ri","ix","isDup","h","insertPointsIntoRing","sorted","expanded","indices","insertIdx","snapDist","ringSlice","i0","i1","start","end","idx","cuttingLineSlice","hit0","hit1","startSeg","endSeg","ensureWinding","ccw","closeRing","coords","last","extendLineOutsideRing","minX","minY","maxX","maxY","diag","p0","dx","dy","len","scale","pN","pN1","splitPolygonByLine","polygonCoords","lineCoords","exteriorRing","holes","extendedLine","expandedRing","idx0","idx1","iA","iB","cutForward","cutReverse","sliceAB","ringA","sliceBA","ringB","originalCCW","finalA","finalB","polyA","polyB","hole","centroid","holeCentroid","cx","cy","THEMES","container","ensureContainer","showToast","duration","parent","theme","el","dismiss","SPLIT_COLORS","HIGHLIGHT_STYLE","Style","Stroke","Fill","SKETCH_STYLE","CircleStyle","PolygonSplitInteraction","ol_interaction_Interaction","VectorSource","VectorLayer","map","active","sources","collect","layers","layer","hit","clone","best","bestDist","source","feat","geom","closest","distPx","LineString","ol_interaction_Draw","evt","cuttingLine","cuttingLineCoords","feature","coordsA","coordsB","featureA","PolygonGeom","featureB","splitFeatures","distToSegmentSq","segA","segB","lenSq","projX","projY","findClosestEdge","clickCoord","bestIdx","n","d","coordsEqual","tolSq","isVertexNearRing","findSharedBoundary","seedIdxA","seedIdxB","tolerance","nA","nB","a0","a1","b0","b1","a0NearB","a1NearB","b0NearA","b1NearA","reversed","startA","endA","startB","endB","safety","nextA","nextB","prevA","prevB","walkRing","fromIdx","toIdx","mergePolygons","polygonCoordsA","polygonCoordsB","clickCoordA","clickCoordB","holesA","holesB","seedA","seedB","shared","partA","partB","merged","mergedRing","areaA","areaB","areaMerged","expectedArea","finalRing","validHoles","HIGHLIGHT_A","HIGHLIGHT_B","LABEL_A","Text","LABEL_B","EDGE_STYLE","MERGE_STYLE","PolygonMergeInteraction","skipFeature","style","edge","edgeFeat","Feature","resolution","bestSeg","sourceA","sourceB","geomA","geomB","mergedFeature","evtData","afterEvt","isParcelA","isParcelB","toRemove","cloneA","cloneB","polygonArea","longestEdge","bestLen","bestI","along","perp","makeCuttingLine","origin","extent","centroidT","sx","sy","dividePolygon","edgeCoords","nVerts","perpMin","perpMax","pieces","remaining","remainingCount","remainingArea","targetArea","remRing","remN","rMin","rMax","lo","hi","bestPiece","bestRemaining","bestError","iter","mid","nudge","lineA","resultA","halfA","halfB","tA","tB","nearPiece","farPiece","nearArea","lineB","resultB","pieceColors","colors","hue","PolygonDivideInteraction","ext","center","newFeatures","MapView","targetId","cat","emoji","label","fontSize","baseLayers","LayerGroup","View","fromLonLat","layerSwitcher","LayerSwitcher","btn","baseUrl","_lsChromeScheduled","ScaleLine","searchNominatim","SearchNominatim","searchResult","lonLat","coordinate","Circle","mapLayers","overlayIdx","Select","clickCondition","ModifyFeature","UndoRedo","EditBar","extraBar","Bar","Button","Split","idFields","field","splitLineToggle","Toggle","splitPolyToggle","splitDivideToggle","splitSubBar","splitParentToggle","mergeToggle","editbarEl","breakEl","SnapGuides","VectorImageLayer","drawToolNames","interaction","snapToggleBtn","visible","TouchCursor","listeners","selected","c","Point","out","isCoord","visitRing","isPolygonRing","poly","walk","sub","system","Overlay","currentFeature","html","catColor","title","color","geometry","geomType","skipKeys","areaSqm","getArea","areaFormatted","lengthM","getLength","lengthFormatted","toLonLat","parcelFeatures","zoneFeatures","otherByLayer","dataRows","names","features","tableRows","labelColor","border","pdfRows","exportAnalysisPDF","circleFeature","circleGeom","circlePoly","fromCircle","circleExtent","radius","intersectsCircle","fExtent","scanGroup","group","groupTitle","layerTitle","candidates","fType","radiusFormatted","polygonFeature","polyGeom","polyExtent","perimeterM","perimeterFormatted","intersectsPoly","typeB","flatB","stride","flatA","strideA","fieldsHtml","displayVal","escapedKey","escapedVal","form","formData","propsA","propsB","getLabel","labelA","labelB","close","chosenProps","labels","radios","updateHighlight","lbl","radio","input","keys","attributeKeys","clickedFeature","text","div","coordsEl","defaultBasemap","topoLayer","TileLayer","XYZ","cartoLightLayer","cartoDarkLayer","osmCycleLayer","OSM","satelliteLayer","googleLayer","osmLayer","baseGroup","matched","on","OPTIONS","target","panel","opt","syncSelection","open","resAtLat","halo","dot","radiusMeters","segments","zoom","toggle","next","customStyle","styles","locations","featureOrId","padding","hasOverlayFeature","hasParcelFeature","markerFeature","index","hoveredFeature","geojson","styleOptions","targetGroup","strokeColor","strokeWidth","fillColor","lineCasingColor","lineCasingWidth","pointRadius","pointFillColor","pointStrokeColor","pointStrokeWidth","GeoJSON","fillStyle","pointStyle","layerStyle","casingW","describeFromGeom","feats","initial","once","ev","desc","wmsSource","TileWMS","wmsLayer","xyzSource","xyzLayer","card","nameRow","nameHint","urlInput","layerName","dlg","inp","wmsSrc","wfsUrl","wfsSource","tag","labelSpan","chip","btnBar","chevronEl","content","ensureSubtitle","removeBtn","addBtn","visit","removed","child","panelContainer","ul","badge","footer","counts","totalOverlays","activeOverlays","HIDDEN_INTERNAL","refresh","hookLayer","added","legendUrl","wrapper","update","children","str","found","view","src","ex","MapTools","drawCircle","Draw","output","radiusLine","unByKey","drawLine","drawPolygon","drawPoint","mainBar","measureBar","circleBtn","lineBtn","areaBtn","clearBtn","swRegistration","registerServiceWorker","newWorker","showUpdateNotification","deferredPrompt","installButton","initInstallPrompt","buttonSelector","showManualInstallInstructions","outcome","isIOS","isSafari","offlineIndicator","offlineListeners","initOfflineDetection","indicatorSelector","updateOfflineUI","notifyOfflineListeners","isOffline","onOfflineChange","isOnline","applyUpdate","getActiveServiceWorker","timeoutMs","ready","timeout","registration","sw","onServiceWorkerControllerChange","requestFromServiceWorker","requestType","responseType","extra","readyTimeoutMs","timer","getTileCacheStats","clearTileCaches","clearTileCacheForProvider","cacheName","getStorageEstimate","usage","quota","initPWA","autoRegisterSW","BASEMAP_TEMPLATES","AVG_TILE_BYTES","ORIGIN_SHIFT","metersToLonLat","x","y","lonLatToTile","latRad","tileRangeForExtent","extent3857","minLon","minLat","maxLon","maxLat","tl","br","minTileX","maxTileX","minTileY","maxTileY","countTiles","minZ","maxZ","enumerateTiles","formatTileUrl","template","OfflineTileDownloader","baseMap","minZoom","maxZoom","concurrency","interBatchDelayMs","onProgress","tpl","tiles","done","ok","failed","cached","emit","phase","elapsedMs","etaMs","batch","GHANA_EXTENT_3857","lonLatToMeters","ne","estimatedSizeBytes","tileCount","API_BASE","FALLBACK_DISTRICT_ID","API_TOKEN","resolveDistrictId","API_CREDENTIALS","getSession","raw","REQUEST_TIMEOUT","PING_TIMEOUT","_serverReachable","checkServerReachable","force","controller","isServerReachable","withTimeout","ms","remotePost","endpoint","body","getDistrictBoundary","getLayers","getCollectorZones","getDistrictParcels","getBuildingFootprints","getContoursHillshade","getOSMRoads","pushGpsTrail","trail","points","EARTH_RADIUS_M","DEG2RAD","haversineMeters","lon1","lat1","lon2","lat2","dLat","dLon","formatCoord","decimals","formatDistance","meters","formatAccuracy","accuracyQuality","DEFAULTS","GeoTracker","set","pos","fix","trailMeta","synced","pushed","trails","trailRow","minIntervalMs","minDistanceM","heartbeatMs","maxAccuracyM","now","keep","stepM","elapsed","num","ch","sqlocalStorage","remote","remoteSync","geoTracker","_shpModule","getShp","mod","mapView","mapTools","currentMode","initApp","savedBasemap","initGpsTracking","parcelFeature","showLocationDetails","layerType","loadLocations","showSuccess","showError","wktFormat","WKT","wktString","status","showWarning","loadLayers","loadDistrictBoundary","loadCollectorZones","loadParcels","loadBuildingFootprints","loadContoursHillshade","loadOSMRoads","loadExternalWMSLayers","initUI","statsContainer","refreshLocalDataStats","offline","syncData","initFieldworkMode","initMeasurementSystem","initDarkMode","initDefaultBasemap","initOfflineTileCache","initOfflineDownloadDialog","initAccountCard","initMessageLog","exportBtn","handleExport","localDataBtn","importShpBtn","shpFileInput","handleShapefileImport","importGeoJSONBtn","geojsonFileInput","handleGeoJSONImport","importKMLBtn","kmlFileInput","handleKMLImport","initMapDropZone","exportGeoJSONBtn","handleExportGeoJSON","statusBtn","handleShowStatus","fitBtn","addLocationBtn","measureCircleBtn","measureLineBtn","measureAreaBtn","drawBtn","modeButtons","setMode","activeBtn","renderLocations","countEl","mobileCount","categoryEmojis","escapeHtml","item","tbody","clearAllBtn","stats","link","showTableContent","handleClearAllCachedLayers","modalTitle","modalBody","modalInfo","Modal","headerCells","bodyRows","val","display","statusContent","parseCoordRing","ringStr","pair","parseWKTPolygon","parseWKTMultiPolygon","polyStr","parseWKT","trimmed","apiResponseToGeoJSON","apiResponse","boundary","districtid","district_name","zonesToGeoJSON","zone","CACHE_KEY","boundaryStyle","adminGroup","removeBoundaryLayer","zoomToBoundary","zoneStyle","emptyGeoJSON","zonesLayer","setZoneFeatures","parcelsToGeoJSON","seen","parcel","parcelStyle","landUseGroup","parcelsLayer","setParcelFeatures","footprintsToGeoJSON","geomKeys","fp","footprintStyle","physInfraGroup","footprintsLayer","setFootprintFeatures","wktRowsToGeoJSON","geojsonFormat","olGeom","flattenProps","contoursStyle","biophysGroup","contoursLayer","roadsStyle","roadsLayer","setRoadFeatures","createLayerGroupsOnMap","overlayLayers","importedFileLayers","IMPORT_STYLE","showFileImportError","logMessage","addImportedGeoJSON","geojsonInput","fallbackName","collections","totalFeatures","fc","lastLayer","refreshImportedLayersCard","infoEl","listEl","l","removeImportedLayer","removeImportedLayers","overlayGroup","indexFilesByExtension","files","MAX_FILE_SIZE","totalSize","sizeMB","displayName","byExt","file","missing","shpObj","KML","dragCounter","exts","fakeEvt","MESSAGE_LOG_MAX","MSG_CONFIG","cfg","log","placeholder","entry","readout","accEl","satsEl","msg","trySync","applyDark","updateLabel","select","statsEl","offcanvas","fmtBytes","refreshInFlight","swActive","storageNote","est","pct","triggerBtn","modalEl","modal","formView","progressView","doneView","cancelBtn","startBtn","closeDoneBtn","headerCloseBtn","basemapSelect","minZoomInput","maxZoomInput","ackCheck","estimateEl","estimateBox","areaViewRadio","areaDistrictRadio","areaGhanaRadio","areaViewInfo","areaDistrictInfo","progressBar","progressPercent","progressCounts","progressOk","progressFailed","progressEta","doneTitle","doneDetail","currentDownloader","fmtDuration","m","getSelectedExtent","updateEstimate","tplMaxZoom","effMaxZ","warningHTML","updateAreaInfos","resetModal","session","menuBtn","avatarEl","nameEl","emailEl","detailEl","signoutBtn","signinLink","noSessNote","bits","handleSignOut","cookieToken","past"],"ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18],"sources":["../../node_modules/proxy-target/esm/types.js","../../node_modules/coincident/esm/channel.js","../../node_modules/proxy-target/esm/traps.js","../../node_modules/coincident/esm/bridge.js","../../node_modules/coincident/esm/index.js","../../node_modules/sqlocal/dist/lib/create-mutex.js","../../node_modules/sqlocal/dist/lib/normalize-database-file.js","../../node_modules/sqlocal/dist/drivers/sqlite-memory-driver.js","../../node_modules/sqlocal/dist/lib/debounce.js","../../node_modules/sqlocal/dist/lib/get-query-key.js","../../node_modules/sqlocal/dist/lib/get-database-key.js","../../node_modules/sqlocal/dist/processor.js","../../node_modules/sqlocal/dist/lib/sql-tag.js","../../node_modules/sqlocal/dist/lib/convert-rows-to-objects.js","../../node_modules/sqlocal/dist/lib/normalize-statement.js","../../node_modules/sqlocal/dist/lib/normalize-sql.js","../../node_modules/sqlocal/dist/lib/mutation-lock.js","../../node_modules/sqlocal/dist/drivers/sqlite-kvvfs-driver.js","../../node_modules/sqlocal/dist/client.js","../../src/database.js","../../src/units.js","../../src/geom/polygonSplit.js","../../src/toast.js","../../src/interactions/PolygonSplitInteraction.js","../../src/geom/polygonMerge.js","../../src/interactions/PolygonMergeInteraction.js","../../src/geom/polygonDivide.js","../../src/interactions/PolygonDivideInteraction.js","../../src/components/MapView.js","../../src/components/MapTools.js","../../src/pwa.js","../../src/offlineTiles.js","../../src/remotedb.js","../../src/geotracker/geo-utils.js","../../src/geotracker/GeoTracker.js","../../src/geotracker-lupmis.js","../../main.js"],"sourcesContent":["export const ARRAY = 'array';\nexport const BIGINT = 'bigint';\nexport const BOOLEAN = 'boolean';\nexport const FUNCTION = 'function';\nexport const NULL = 'null';\nexport const NUMBER = 'number';\nexport const OBJECT = 'object';\nexport const STRING = 'string';\nexport const SYMBOL = 'symbol';\nexport const UNDEFINED = 'undefined';\n","// ⚠️ AUTOMATICALLY GENERATED - DO NOT CHANGE\nexport const CHANNEL = '64e10b34-2bf7-4616-9668-f99de5aa046e';\n\nexport const MAIN = 'M' + CHANNEL;\nexport const THREAD = 'T' + CHANNEL;\n","export const APPLY = 'apply';\nexport const CONSTRUCT = 'construct';\nexport const DEFINE_PROPERTY = 'defineProperty';\nexport const DELETE_PROPERTY = 'deleteProperty';\nexport const GET = 'get';\nexport const GET_OWN_PROPERTY_DESCRIPTOR = 'getOwnPropertyDescriptor';\nexport const GET_PROTOTYPE_OF = 'getPrototypeOf';\nexport const HAS = 'has';\nexport const IS_EXTENSIBLE = 'isExtensible';\nexport const OWN_KEYS = 'ownKeys';\nexport const PREVENT_EXTENSION = 'preventExtensions';\nexport const SET = 'set';\nexport const SET_PROTOTYPE_OF = 'setPrototypeOf';\n","// The goal of this file is to normalize SAB\n// at least in main -> worker() use cases.\n// This still cannot possibly solve the sync\n// worker -> main() use case if SharedArrayBuffer\n// is not available or usable.\n\nimport {CHANNEL} from './channel.js';\n\nconst {isArray} = Array;\n\nlet {SharedArrayBuffer, window} = globalThis;\nlet {notify, wait, waitAsync} = Atomics;\nlet postPatched = null;\n\n// This is needed for some version of Firefox\nif (!waitAsync) {\n waitAsync = buffer => ({\n value: new Promise(onmessage => {\n // encodeURIComponent('onmessage=({data:b})=>(Atomics.wait(b,0),postMessage(0))')\n let w = new Worker('data:application/javascript,onmessage%3D(%7Bdata%3Ab%7D)%3D%3E(Atomics.wait(b%2C0)%2CpostMessage(0))');\n w.onmessage = onmessage;\n w.postMessage(buffer);\n })\n });\n}\n\n// Monkey-patch SharedArrayBuffer if needed\ntry {\n new SharedArrayBuffer(4);\n}\ncatch (_) {\n SharedArrayBuffer = ArrayBuffer;\n\n const ids = new WeakMap;\n // patch only main -> worker():async use case\n if (window) {\n const resolvers = new Map;\n const {prototype: {postMessage}} = Worker;\n\n const listener = event => {\n const details = event.data?.[CHANNEL];\n if (!isArray(details)) {\n event.stopImmediatePropagation();\n const { id, sb } = details;\n resolvers.get(id)(sb);\n }\n };\n\n postPatched = function (data, ...rest) {\n const details = data?.[CHANNEL];\n if (isArray(details)) {\n const [id, sb] = details;\n ids.set(sb, id);\n this.addEventListener('message', listener);\n }\n return postMessage.call(this, data, ...rest);\n };\n\n waitAsync = sb => ({\n value: new Promise(resolve => {\n resolvers.set(ids.get(sb), resolve);\n }).then(buff => {\n resolvers.delete(ids.get(sb));\n ids.delete(sb);\n for (let i = 0; i < buff.length; i++) sb[i] = buff[i];\n return 'ok';\n })\n });\n }\n else {\n const as = (id, sb) => ({[CHANNEL]: { id, sb }});\n\n notify = sb => {\n postMessage(as(ids.get(sb), sb));\n };\n\n addEventListener('message', event => {\n const details = event.data?.[CHANNEL];\n if (isArray(details)) {\n const [id, sb] = details;\n ids.set(sb, id);\n }\n });\n }\n}\n\nexport {SharedArrayBuffer, isArray, notify, postPatched, wait, waitAsync};\n","/*! (c) Andrea Giammarchi - ISC */\n\nimport {FUNCTION} from 'proxy-target/types';\n\nimport {CHANNEL} from './channel.js';\nimport {GET, HAS, SET} from './shared/traps.js';\n\nimport {SharedArrayBuffer, isArray, notify, postPatched, wait, waitAsync} from './bridge.js';\n\n// just minifier friendly for Blob Workers' cases\nconst {Int32Array, Map, Uint16Array} = globalThis;\n\n// common constants / utilities for repeated operations\nconst {BYTES_PER_ELEMENT: I32_BYTES} = Int32Array;\nconst {BYTES_PER_ELEMENT: UI16_BYTES} = Uint16Array;\n\nconst waitInterrupt = (sb, delay, handler) => {\n while (wait(sb, 0, 0, delay) === 'timed-out')\n handler();\n};\n\n// retain buffers to transfer\nconst buffers = new WeakSet;\n\n// retain either main threads or workers global context\nconst context = new WeakMap;\n\nconst syncResult = {value: {then: fn => fn()}};\n\n// used to generate a unique `id` per each worker `postMessage` \"transaction\"\nlet uid = 0;\n\n/**\n * @typedef {Object} Interrupt used to sanity-check interrupts while waiting synchronously.\n * @prop {function} [handler] a callback invoked every `delay` milliseconds.\n * @prop {number} [delay=42] define `handler` invokes in terms of milliseconds.\n */\n\n/**\n * Create once a `Proxy` able to orchestrate synchronous `postMessage` out of the box.\n * @param {globalThis | Worker} self the context in which code should run\n * @param {{parse: (serialized: string) => any, stringify: (serializable: any) => string, transform?: (value:any) => any, interrupt?: () => void | Interrupt}} [JSON] an optional `JSON` like interface to `parse` or `stringify` content with extra `transform` ability.\n * @returns {ProxyHandler | ProxyHandler}\n */\nconst coincident = (self, {parse = JSON.parse, stringify = JSON.stringify, transform, interrupt} = JSON) => {\n // create a Proxy once for the given context (globalThis or Worker instance)\n if (!context.has(self)) {\n // ensure no SAB gets a chance to pass through this call\n const sendMessage = postPatched || self.postMessage;\n // ensure the CHANNEL and data are posted correctly\n const post = (transfer, ...args) => sendMessage.call(self, {[CHANNEL]: args}, {transfer});\n\n const handler = typeof interrupt === FUNCTION ? interrupt : interrupt?.handler;\n const delay = interrupt?.delay || 42;\n const decoder = new TextDecoder('utf-16');\n\n // automatically uses sync wait (worker -> main)\n // or fallback to async wait (main -> worker)\n const waitFor = (isAsync, sb) => isAsync ?\n waitAsync(sb, 0) :\n ((handler ? waitInterrupt(sb, delay, handler) : wait(sb, 0)), syncResult);\n\n // prevent Harakiri https://github.com/WebReflection/coincident/issues/18\n let seppuku = false;\n\n context.set(self, new Proxy(new Map, {\n // there is very little point in checking prop in proxy for this very specific case\n // and I don't want to orchestrate a whole roundtrip neither, as stuff would fail\n // regardless if from Worker we access non existent Main callback, and vice-versa.\n // This is here mostly to guarantee that if such check is performed, at least the\n // get trap goes through and then it's up to developers guarantee they are accessing\n // stuff that actually exists elsewhere.\n [HAS]: (_, action) => typeof action === 'string' && !action.startsWith('_'),\n\n // worker related: get any utility that should be available on the main thread\n [GET]: (_, action) => action === 'then' ? null : ((...args) => {\n // transaction id\n const id = uid++;\n\n // first contact: just ask for how big the buffer should be\n // the value would be stored at index [1] while [0] is just control\n let sb = new Int32Array(new SharedArrayBuffer(I32_BYTES * 2));\n\n // if a transfer list has been passed, drop it from args\n let transfer = [];\n if (buffers.has(args.at(-1) || transfer))\n buffers.delete(transfer = args.pop());\n\n // ask for invoke with arguments and wait for it\n post(transfer, id, sb, action, transform ? args.map(transform) : args);\n\n // helps deciding how to wait for results\n const isAsync = self !== globalThis;\n\n // warn users about possible deadlock still allowing them\n // to explicitly `proxy.invoke().then(...)` without blocking\n let deadlock = 0;\n if (seppuku && isAsync)\n deadlock = setTimeout(console.warn, 1000, `💀🔒 - Possible deadlock if proxy.${action}(...args) is awaited`);\n\n return waitFor(isAsync, sb).value.then(() => {\n clearTimeout(deadlock);\n\n // commit transaction using the returned / needed buffer length\n const length = sb[1];\n\n // filter undefined results\n if (!length) return;\n\n // calculate the needed ui16 bytes length to store the result string\n const bytes = UI16_BYTES * length;\n\n // round up to the next amount of bytes divided by 4 to allow i32 operations\n sb = new Int32Array(new SharedArrayBuffer(bytes + (bytes % I32_BYTES)));\n\n // ask for results and wait for it\n post([], id, sb);\n return waitFor(isAsync, sb).value.then(() => parse(\n decoder.decode(new Uint16Array(sb.buffer).slice(0, length)))\n );\n });\n }),\n\n // main thread related: react to any utility a worker is asking for\n [SET](actions, action, callback) {\n const type = typeof callback;\n if (type !== FUNCTION)\n throw new Error(`Unable to assign ${action} as ${type}`);\n // lazy event listener and logic handling, triggered once by setters actions\n if (!actions.size) {\n // maps results by `id` as they are asked for\n const results = new Map;\n // add the event listener once (first defined setter, all others work the same)\n self.addEventListener('message', async (event) => {\n // grub the very same library CHANNEL; ignore otherwise\n const details = event.data?.[CHANNEL];\n if (isArray(details)) {\n // if early enough, avoid leaking data to other listeners\n event.stopImmediatePropagation();\n const [id, sb, ...rest] = details;\n let error;\n // action available: it must be defined/known on the main thread\n if (rest.length) {\n const [action, args] = rest;\n if (actions.has(action)) {\n seppuku = true;\n try {\n // await for result either sync or async and serialize it\n const result = await actions.get(action)(...args);\n if (result !== void 0) {\n const serialized = stringify(transform ? transform(result) : result);\n // store the result for \"the very next\" event listener call\n results.set(id, serialized);\n // communicate the required SharedArrayBuffer length out of the\n // resulting serialized string\n sb[1] = serialized.length;\n }\n }\n catch (_) {\n error = _;\n }\n finally {\n seppuku = false;\n }\n }\n // unknown action should be notified as missing on the main thread\n else {\n error = new Error(`Unsupported action: ${action}`);\n }\n // unlock the wait lock later on\n sb[0] = 1;\n }\n // no action means: get results out of the well known `id`\n // wait lock automatically unlocked here as no `0` value would\n // possibly ever land at index `0`\n else {\n const result = results.get(id);\n results.delete(id);\n // populate the SharedArrayBuffer with utf-16 chars code\n for (let ui16a = new Uint16Array(sb.buffer), i = 0; i < result.length; i++)\n ui16a[i] = result.charCodeAt(i);\n }\n // release te worker waiting either the length or the result\n notify(sb, 0);\n if (error) throw error;\n }\n });\n }\n // store this action callback allowing the setter in the process\n return !!actions.set(action, callback);\n }\n }));\n }\n return context.get(self);\n};\n\ncoincident.transfer = (...args) => (buffers.add(args), args);\n\nexport default coincident;\n","export function createMutex() {\n let promise;\n let resolve;\n const lock = async () => {\n while (promise) {\n await promise;\n }\n promise = new Promise((res) => {\n resolve = res;\n });\n };\n const unlock = async () => {\n const res = resolve;\n promise = undefined;\n resolve = undefined;\n res?.();\n };\n return { lock, unlock };\n}\n//# sourceMappingURL=create-mutex.js.map","export async function normalizeDatabaseFile(dbFile, convertStreamTo) {\n let bufferOrStream;\n if (dbFile instanceof Blob) {\n bufferOrStream = dbFile.stream();\n }\n else {\n bufferOrStream = dbFile;\n }\n if (bufferOrStream instanceof ReadableStream && convertStreamTo) {\n const stream = bufferOrStream;\n const reader = stream.getReader();\n switch (convertStreamTo) {\n case 'callback':\n return async () => {\n const chunk = await reader.read();\n return chunk.value;\n };\n case 'buffer':\n const chunks = [];\n let streamDone = false;\n while (!streamDone) {\n const chunk = await reader.read();\n if (chunk.value)\n chunks.push(chunk.value);\n streamDone = chunk.done;\n }\n const arrayLength = chunks.reduce((length, chunk) => {\n return length + chunk.length;\n }, 0);\n const buffer = new Uint8Array(arrayLength);\n let offset = 0;\n chunks.forEach((chunk) => {\n buffer.set(chunk, offset);\n offset += chunk.length;\n });\n return buffer.buffer;\n }\n }\n else {\n return bufferOrStream;\n }\n}\n//# sourceMappingURL=normalize-database-file.js.map","import { normalizeDatabaseFile } from '../lib/normalize-database-file.js';\nexport class SQLiteMemoryDriver {\n constructor(sqlite3InitModule) {\n Object.defineProperty(this, \"sqlite3InitModule\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: sqlite3InitModule\n });\n Object.defineProperty(this, \"sqlite3\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"db\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"config\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"pointers\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: []\n });\n Object.defineProperty(this, \"writeCallbacks\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: new Set()\n });\n Object.defineProperty(this, \"storageType\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: 'memory'\n });\n }\n async init(config) {\n const { databasePath } = config;\n const flags = this.getFlags(config);\n if (!this.sqlite3InitModule) {\n const { default: sqlite3InitModule } = await import('@sqlite.org/sqlite-wasm');\n this.sqlite3InitModule = sqlite3InitModule;\n }\n if (!this.sqlite3) {\n this.sqlite3 = await this.sqlite3InitModule();\n }\n if (this.db) {\n await this.destroy();\n }\n this.db = new this.sqlite3.oo1.DB(databasePath, flags);\n this.config = config;\n this.initWriteHook();\n }\n onWrite(callback) {\n this.writeCallbacks.add(callback);\n return () => {\n this.writeCallbacks.delete(callback);\n };\n }\n async exec(statement) {\n if (!this.db)\n throw new Error('Driver not initialized');\n return this.execOnDb(this.db, statement);\n }\n async execBatch(statements) {\n if (!this.db)\n throw new Error('Driver not initialized');\n const results = [];\n this.db.transaction((tx) => {\n const prepared = new Map();\n try {\n for (let statement of statements) {\n let stmt = prepared.get(statement.sql);\n if (!stmt) {\n const newStmt = tx.prepare(statement.sql);\n prepared.set(statement.sql, newStmt);\n stmt = newStmt;\n }\n if (statement.params?.length) {\n stmt.bind(statement.params);\n }\n let columns = [];\n let rows = [];\n while (stmt.step()) {\n columns = stmt.getColumnNames([]);\n rows.push(stmt.get([]));\n }\n results.push({ columns, rows });\n stmt.reset();\n }\n }\n finally {\n prepared.forEach((stmt) => {\n stmt.finalize();\n });\n }\n });\n return results;\n }\n async isDatabasePersisted() {\n return false;\n }\n async getDatabaseSizeBytes() {\n const sizeResult = await this.exec({\n sql: `SELECT page_count * page_size AS size \n\t\t\t\tFROM pragma_page_count(), pragma_page_size()`,\n method: 'get',\n });\n const size = sizeResult?.rows?.[0];\n if (typeof size !== 'number') {\n throw new Error('Failed to query database size');\n }\n return size;\n }\n async createFunction(fn) {\n if (!this.db)\n throw new Error('Driver not initialized');\n switch (fn.type) {\n case 'callback':\n case 'scalar':\n this.db.createFunction({\n name: fn.name,\n xFunc: (_, ...args) => fn.func(...args),\n arity: -1,\n });\n break;\n case 'aggregate':\n this.db.createFunction({\n name: fn.name,\n xStep: (_, ...args) => fn.func.step(...args),\n xFinal: (_, ...args) => fn.func.final(...args),\n arity: -1,\n });\n break;\n }\n }\n async import(database) {\n if (!this.sqlite3 || !this.db || !this.config) {\n throw new Error('Driver not initialized');\n }\n const data = await normalizeDatabaseFile(database, 'buffer');\n const dataPointer = this.sqlite3.wasm.allocFromTypedArray(data);\n this.pointers.push(dataPointer);\n const resultCode = this.sqlite3.capi.sqlite3_deserialize(this.db, 'main', dataPointer, data.byteLength, data.byteLength, this.config.readOnly\n ? this.sqlite3.capi.SQLITE_DESERIALIZE_READONLY\n : this.sqlite3.capi.SQLITE_DESERIALIZE_RESIZEABLE);\n this.db.checkRc(resultCode);\n }\n async export() {\n if (!this.sqlite3 || !this.db) {\n throw new Error('Driver not initialized');\n }\n return {\n name: 'database.sqlite3',\n data: this.sqlite3.capi.sqlite3_js_db_export(this.db),\n };\n }\n async clear() { }\n async destroy() {\n this.closeDb();\n this.pointers.forEach((pointer) => this.sqlite3?.wasm.dealloc(pointer));\n this.pointers = [];\n this.writeCallbacks.clear();\n }\n getFlags(config) {\n const { readOnly, verbose } = config;\n const parts = [readOnly === true ? 'r' : 'cw', verbose === true ? 't' : ''];\n return parts.join('');\n }\n execOnDb(db, statement) {\n const statementData = {\n rows: [],\n columns: [],\n };\n const rows = db.exec({\n sql: statement.sql,\n bind: statement.params,\n returnValue: 'resultRows',\n rowMode: 'array',\n columnNames: statementData.columns,\n });\n switch (statement.method) {\n case 'run':\n break;\n case 'get':\n statementData.rows = rows[0] ?? [];\n break;\n case 'all':\n default:\n statementData.rows = rows;\n break;\n }\n return statementData;\n }\n initWriteHook() {\n if (!this.config?.reactive)\n return;\n if (!this.sqlite3 || !this.db) {\n throw new Error('Driver not initialized');\n }\n const opMap = {\n [this.sqlite3.capi.SQLITE_INSERT]: 'insert',\n [this.sqlite3.capi.SQLITE_UPDATE]: 'update',\n [this.sqlite3.capi.SQLITE_DELETE]: 'delete',\n };\n this.sqlite3.capi.sqlite3_update_hook(this.db, (_ctx, opId, _db, table, rowid) => {\n this.writeCallbacks.forEach((cb) => {\n cb({ table, rowid, operation: opMap[opId] });\n });\n }, 0);\n }\n closeDb() {\n if (this.db) {\n this.db.close();\n this.db = undefined;\n }\n }\n}\n//# sourceMappingURL=sqlite-memory-driver.js.map","/**\n * Lodash (Custom Build) \n * Build: `lodash modularize exports=\"es\" include=\"debounce\" -p -o ./`\n * Copyright JS Foundation and other contributors \n * Released under MIT license \n * Based on Underscore.js 1.8.3 \n * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n */\nexport function debounce(func, wait, options) {\n let lastArgs;\n let lastThis;\n let maxWait;\n let result;\n let timerId;\n let lastCallTime;\n let lastInvokeTime = 0;\n let leading = false;\n let maxing = false;\n let trailing = true;\n if (typeof func !== 'function') {\n throw new TypeError('Expected a function');\n }\n wait = Number(wait) || 0;\n if (typeof options === 'object' && options !== null) {\n leading = !!options.leading;\n maxing = 'maxWait' in options;\n maxWait = maxing ? Math.max(Number(options.maxWait) || 0, wait) : 0;\n trailing = 'trailing' in options ? !!options.trailing : trailing;\n }\n function invokeFunc(time) {\n const args = lastArgs;\n const thisArg = lastThis;\n lastArgs = lastThis = undefined;\n lastInvokeTime = time;\n result = func.apply(thisArg, args);\n return result;\n }\n function leadingEdge(time) {\n lastInvokeTime = time;\n timerId = setTimeout(timerExpired, wait);\n return leading ? invokeFunc(time) : result;\n }\n function remainingWait(time) {\n const timeSinceLastCall = time - (lastCallTime ?? 0);\n const timeSinceLastInvoke = time - lastInvokeTime;\n const timeWaiting = wait - timeSinceLastCall;\n return maxing\n ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)\n : timeWaiting;\n }\n function shouldInvoke(time) {\n const timeSinceLastCall = time - (lastCallTime ?? 0);\n const timeSinceLastInvoke = time - lastInvokeTime;\n return (lastCallTime === undefined ||\n timeSinceLastCall >= wait ||\n timeSinceLastCall < 0 ||\n (maxing && timeSinceLastInvoke >= maxWait));\n }\n function timerExpired() {\n const time = Date.now();\n if (shouldInvoke(time)) {\n return trailingEdge(time);\n }\n timerId = setTimeout(timerExpired, remainingWait(time));\n }\n function trailingEdge(time) {\n timerId = undefined;\n if (trailing && lastArgs) {\n return invokeFunc(time);\n }\n lastArgs = lastThis = undefined;\n return result;\n }\n function cancel() {\n if (timerId !== undefined) {\n clearTimeout(timerId);\n }\n lastInvokeTime = 0;\n lastArgs = lastCallTime = lastThis = timerId = undefined;\n }\n function flush() {\n return timerId === undefined ? result : trailingEdge(Date.now());\n }\n function debounced() {\n const time = Date.now();\n const isInvoking = shouldInvoke(time);\n // @ts-ignore\n lastArgs = arguments;\n // @ts-ignore\n lastThis = this;\n lastCallTime = time;\n if (isInvoking) {\n if (timerId === undefined) {\n return leadingEdge(lastCallTime);\n }\n if (maxing) {\n timerId = setTimeout(timerExpired, wait);\n return invokeFunc(lastCallTime);\n }\n }\n if (timerId === undefined) {\n timerId = setTimeout(timerExpired, wait);\n }\n return result;\n }\n debounced.cancel = cancel;\n debounced.flush = flush;\n return debounced;\n}\n//# sourceMappingURL=debounce.js.map","export function getQueryKey() {\n return crypto.randomUUID();\n}\n//# sourceMappingURL=get-query-key.js.map","import { getQueryKey } from './get-query-key.js';\nexport function getDatabaseKey(databasePath, clientKey) {\n switch (databasePath) {\n case 'session':\n case ':sessionStorage:':\n // The sessionStorage DB can be shared between clients in the same tab\n let sessionKey = sessionStorage._sqlocal_session_key;\n if (!sessionKey) {\n sessionKey = getQueryKey();\n sessionStorage._sqlocal_session_key = sessionKey;\n }\n return `session:${sessionKey}`;\n case 'local':\n case ':localStorage:':\n // There's only one localStorage DB per origin\n return 'local';\n case ':memory:':\n // Each memory DB is unique to a client\n return `memory:${clientKey}`;\n default:\n // OPFS DBs are shared by path across same-origin tabs\n return `path:${databasePath}`;\n }\n}\n//# sourceMappingURL=get-database-key.js.map","import coincident from 'coincident';\nimport { createMutex } from './lib/create-mutex.js';\nimport { SQLiteMemoryDriver } from './drivers/sqlite-memory-driver.js';\nimport { debounce } from './lib/debounce.js';\nimport { getDatabaseKey } from './lib/get-database-key.js';\nexport class SQLocalProcessor {\n constructor(driver) {\n Object.defineProperty(this, \"driver\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"config\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: {}\n });\n Object.defineProperty(this, \"userFunctions\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: new Map()\n });\n Object.defineProperty(this, \"initMutex\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: createMutex()\n });\n Object.defineProperty(this, \"transactionMutex\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: createMutex()\n });\n Object.defineProperty(this, \"transactionKey\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: null\n });\n Object.defineProperty(this, \"proxy\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"dirtyTables\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: new Set()\n });\n Object.defineProperty(this, \"effectsChannel\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"reinitChannel\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"onmessage\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"init\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (reason) => {\n if (!this.config.databasePath || !this.config.clientKey)\n return;\n await this.initMutex.lock();\n try {\n try {\n await this.driver.init(this.config);\n }\n catch {\n 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).`);\n this.config.databasePath = ':memory:';\n this.driver = new SQLiteMemoryDriver();\n await this.driver.init(this.config);\n }\n const dbKey = getDatabaseKey(this.config.databasePath, this.config.clientKey);\n this.reinitChannel = new BroadcastChannel(`_sqlocal_reinit_(${dbKey})`);\n this.reinitChannel.onmessage = (event) => {\n const message = event.data;\n if (this.config.clientKey === message.clientKey)\n return;\n switch (message.type) {\n case 'reinit':\n this.init(message.reason);\n break;\n case 'close':\n this.driver.destroy();\n break;\n }\n };\n if (this.config.reactive) {\n this.effectsChannel = new BroadcastChannel(`_sqlocal_effects_(${dbKey})`);\n this.driver.onWrite(async (change) => {\n this.dirtyTables.add(change.table);\n await this.transactionMutex.lock();\n this.emitEffectsDebounced();\n await this.transactionMutex.unlock();\n });\n }\n await Promise.all(Array.from(this.userFunctions.values()).map((fn) => {\n return this.initUserFunction(fn);\n }));\n await this.execInitStatements();\n this.emitMessage({ type: 'event', event: 'connect', reason });\n }\n catch (error) {\n this.emitMessage({\n type: 'error',\n error,\n queryKey: null,\n });\n await this.destroy();\n }\n finally {\n await this.initMutex.unlock();\n }\n }\n });\n Object.defineProperty(this, \"postMessage\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (event, _transfer) => {\n const message = event instanceof MessageEvent ? event.data : event;\n await this.initMutex.lock();\n switch (message.type) {\n case 'config':\n this.editConfig(message);\n break;\n case 'query':\n case 'batch':\n case 'transaction':\n this.exec(message);\n break;\n case 'function':\n this.createUserFunction(message);\n break;\n case 'getinfo':\n this.getDatabaseInfo(message);\n break;\n case 'import':\n this.importDb(message);\n break;\n case 'export':\n this.exportDb(message);\n break;\n case 'delete':\n this.deleteDb(message);\n break;\n case 'destroy':\n this.destroy(message);\n break;\n }\n await this.initMutex.unlock();\n }\n });\n Object.defineProperty(this, \"emitMessage\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: (message, transfer = []) => {\n if (this.onmessage) {\n this.onmessage(message, transfer);\n }\n }\n });\n Object.defineProperty(this, \"emitEffects\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: () => {\n if (!this.effectsChannel || this.dirtyTables.size === 0)\n return;\n this.effectsChannel.postMessage({\n type: 'effects',\n tables: [...this.dirtyTables],\n });\n this.dirtyTables.clear();\n }\n });\n Object.defineProperty(this, \"emitEffectsDebounced\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: debounce(() => this.emitEffects(), 32, {\n maxWait: 180,\n })\n });\n Object.defineProperty(this, \"editConfig\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: (message) => {\n this.config = message.config;\n this.init('initial');\n }\n });\n Object.defineProperty(this, \"exec\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n try {\n const response = {\n type: 'data',\n queryKey: message.queryKey,\n data: [],\n };\n switch (message.type) {\n case 'query':\n const partOfTransaction = this.transactionKey !== null &&\n this.transactionKey === message.transactionKey;\n try {\n if (!partOfTransaction) {\n await this.transactionMutex.lock();\n }\n const statementData = await this.driver.exec(message);\n response.data.push(statementData);\n }\n finally {\n if (!partOfTransaction) {\n await this.transactionMutex.unlock();\n }\n }\n break;\n case 'batch':\n try {\n await this.transactionMutex.lock();\n const results = await this.driver.execBatch(message.statements);\n response.data.push(...results);\n }\n finally {\n await this.transactionMutex.unlock();\n }\n break;\n case 'transaction':\n if (message.action === 'begin') {\n await this.transactionMutex.lock();\n this.transactionKey = message.transactionKey;\n await this.driver.exec({ sql: 'BEGIN' });\n }\n if ((message.action === 'commit' || message.action === 'rollback') &&\n this.transactionKey !== null &&\n this.transactionKey === message.transactionKey) {\n const sql = message.action === 'commit' ? 'COMMIT' : 'ROLLBACK';\n await this.driver.exec({ sql });\n this.transactionKey = null;\n await this.transactionMutex.unlock();\n }\n break;\n }\n this.emitMessage(response);\n }\n catch (error) {\n this.emitMessage({\n type: 'error',\n error,\n queryKey: message.queryKey,\n });\n }\n }\n });\n Object.defineProperty(this, \"execInitStatements\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async () => {\n if (this.config.onInitStatements) {\n for (let statement of this.config.onInitStatements) {\n await this.driver.exec(statement);\n }\n }\n }\n });\n Object.defineProperty(this, \"getDatabaseInfo\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n try {\n this.emitMessage({\n type: 'info',\n queryKey: message.queryKey,\n info: {\n databasePath: this.config.databasePath,\n storageType: this.driver.storageType,\n databaseSizeBytes: await this.driver.getDatabaseSizeBytes(),\n persisted: await this.driver.isDatabasePersisted(),\n },\n });\n }\n catch (error) {\n this.emitMessage({\n type: 'error',\n queryKey: message.queryKey,\n error,\n });\n }\n }\n });\n Object.defineProperty(this, \"createUserFunction\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n const { functionName: name, functionType: type, queryKey } = message;\n let fn;\n if (this.userFunctions.has(name)) {\n this.emitMessage({\n type: 'error',\n error: new Error(`A user-defined function with the name \"${name}\" has already been created for this SQLocal instance.`),\n queryKey,\n });\n return;\n }\n switch (type) {\n case 'callback':\n fn = {\n type,\n name,\n func: (...args) => {\n this.emitMessage({ type: 'callback', name, args });\n },\n };\n break;\n case 'scalar':\n fn = {\n type,\n name,\n func: this.proxy[`_sqlocal_func_${name}`],\n };\n break;\n case 'aggregate':\n fn = {\n type,\n name,\n func: {\n step: this.proxy[`_sqlocal_func_${name}_step`],\n final: this.proxy[`_sqlocal_func_${name}_final`],\n },\n };\n break;\n }\n try {\n await this.initUserFunction(fn);\n this.emitMessage({\n type: 'success',\n queryKey,\n });\n }\n catch (error) {\n this.emitMessage({\n type: 'error',\n error,\n queryKey,\n });\n }\n }\n });\n Object.defineProperty(this, \"initUserFunction\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (fn) => {\n await this.driver.createFunction(fn);\n this.userFunctions.set(fn.name, fn);\n }\n });\n Object.defineProperty(this, \"importDb\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n const { queryKey, database } = message;\n let errored = false;\n try {\n await this.driver.import(database);\n if (this.driver.storageType === 'memory') {\n await this.execInitStatements();\n }\n }\n catch (error) {\n this.emitMessage({\n type: 'error',\n error,\n queryKey,\n });\n errored = true;\n }\n finally {\n if (this.driver.storageType !== 'memory') {\n await this.init('overwrite');\n }\n }\n if (!errored) {\n this.emitMessage({\n type: 'success',\n queryKey,\n });\n }\n }\n });\n Object.defineProperty(this, \"exportDb\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n const { queryKey } = message;\n try {\n const { name, data } = await this.driver.export();\n this.emitMessage({\n type: 'buffer',\n queryKey,\n bufferName: name,\n buffer: data,\n }, [data]);\n }\n catch (error) {\n this.emitMessage({\n type: 'error',\n error,\n queryKey,\n });\n }\n }\n });\n Object.defineProperty(this, \"deleteDb\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n const { queryKey } = message;\n let errored = false;\n try {\n await this.driver.clear();\n }\n catch (error) {\n this.emitMessage({\n type: 'error',\n error,\n queryKey,\n });\n errored = true;\n }\n finally {\n await this.init('delete');\n }\n if (!errored) {\n this.emitMessage({\n type: 'success',\n queryKey,\n });\n }\n }\n });\n Object.defineProperty(this, \"destroy\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n await this.driver.exec({ sql: 'PRAGMA optimize' });\n await this.driver.destroy();\n if (this.effectsChannel) {\n this.emitEffectsDebounced.flush();\n this.effectsChannel.close();\n this.effectsChannel = undefined;\n }\n if (this.reinitChannel) {\n this.reinitChannel.close();\n this.reinitChannel = undefined;\n }\n if (message) {\n this.emitMessage({\n type: 'success',\n queryKey: message.queryKey,\n });\n }\n }\n });\n const isInWorker = typeof WorkerGlobalScope !== 'undefined' &&\n globalThis instanceof WorkerGlobalScope;\n const proxy = isInWorker ? coincident(globalThis) : globalThis;\n this.proxy = proxy;\n this.driver = driver;\n }\n}\n//# sourceMappingURL=processor.js.map","export function sqlTag(queryTemplate, ...params) {\n return {\n sql: queryTemplate.join('?'),\n params,\n };\n}\n//# sourceMappingURL=sql-tag.js.map","function isArrayOfArrays(rows) {\n return !rows.some((row) => !Array.isArray(row));\n}\nexport function convertRowsToObjects(rows, columns) {\n let checkedRows;\n if (isArrayOfArrays(rows)) {\n checkedRows = rows;\n }\n else {\n checkedRows = [rows];\n }\n return checkedRows.map((row) => {\n const rowObj = {};\n columns.forEach((column, columnIndex) => {\n rowObj[column] = row[columnIndex];\n });\n return rowObj;\n });\n}\n//# sourceMappingURL=convert-rows-to-objects.js.map","import { sqlTag } from './sql-tag.js';\nfunction isDrizzleStatement(statement) {\n return (typeof statement === 'object' &&\n statement !== null &&\n 'getSQL' in statement &&\n typeof statement.getSQL === 'function');\n}\nfunction isStatement(statement) {\n return (typeof statement === 'object' &&\n statement !== null &&\n 'sql' in statement === true &&\n typeof statement.sql === 'string' &&\n 'params' in statement === true);\n}\nexport function normalizeStatement(statement) {\n if (typeof statement === 'function') {\n statement = statement(sqlTag);\n }\n if (isDrizzleStatement(statement)) {\n try {\n if (!('toSQL' in statement && typeof statement.toSQL === 'function')) {\n throw 1;\n }\n const drizzleStatement = statement.toSQL();\n if (!isStatement(drizzleStatement)) {\n throw 2;\n }\n const exec = 'all' in statement && typeof statement.all === 'function'\n ? statement.all\n : undefined;\n return {\n ...drizzleStatement,\n exec: exec ? () => exec() : undefined,\n };\n }\n catch {\n throw new Error('The passed statement could not be parsed.');\n }\n }\n const sql = statement.sql;\n let params = [];\n if ('params' in statement) {\n params = statement.params;\n }\n else if ('parameters' in statement) {\n params = statement.parameters;\n }\n return { sql, params };\n}\n//# sourceMappingURL=normalize-statement.js.map","import { sqlTag } from './sql-tag.js';\nexport function normalizeSql(maybeQueryTemplate, params) {\n let statement;\n if (typeof maybeQueryTemplate === 'string') {\n statement = { sql: maybeQueryTemplate, params };\n }\n else {\n statement = sqlTag(maybeQueryTemplate, ...params);\n }\n return statement;\n}\n//# sourceMappingURL=normalize-sql.js.map","export async function mutationLock(mode, bypass, config, mutation) {\n if (!bypass && 'locks' in navigator) {\n return navigator.locks.request(`_sqlocal_mutation_(${config.databasePath})`, { mode }, mutation);\n }\n else {\n return mutation();\n }\n}\n//# sourceMappingURL=mutation-lock.js.map","import { SQLiteMemoryDriver } from './sqlite-memory-driver.js';\nexport class SQLiteKvvfsDriver extends SQLiteMemoryDriver {\n constructor(storageType, sqlite3InitModule) {\n super(sqlite3InitModule);\n Object.defineProperty(this, \"storageType\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: storageType\n });\n }\n async init(config) {\n const flags = this.getFlags(config);\n if (config.readOnly) {\n throw new Error(`SQLite storage type \"${this.storageType}\" does not support read-only mode.`);\n }\n if (!this.sqlite3InitModule) {\n const { default: sqlite3InitModule } = await import('@sqlite.org/sqlite-wasm');\n this.sqlite3InitModule = sqlite3InitModule;\n }\n if (!this.sqlite3) {\n this.sqlite3 = await this.sqlite3InitModule();\n }\n if (this.db) {\n await this.destroy();\n }\n this.db = new this.sqlite3.oo1.JsStorageDb({\n filename: this.storageType,\n flags,\n });\n this.config = config;\n this.initWriteHook();\n }\n async isDatabasePersisted() {\n return navigator.storage?.persisted();\n }\n async getDatabaseSizeBytes() {\n if (!this.db)\n throw new Error('Driver not initialized');\n return this.db.storageSize();\n }\n async import(database) {\n const memdb = new SQLiteMemoryDriver();\n await memdb.init({});\n await memdb.import(database);\n await this.clear();\n await memdb.exec({\n sql: `VACUUM INTO 'file:${this.storageType}?vfs=kvvfs'`,\n });\n await memdb.destroy();\n }\n async clear() {\n if (!this.db)\n throw new Error('Driver not initialized');\n this.db.clearStorage();\n }\n async destroy() {\n this.closeDb();\n this.writeCallbacks.clear();\n }\n}\n//# sourceMappingURL=sqlite-kvvfs-driver.js.map","var _a, _b;\nimport coincident from 'coincident';\nimport { SQLocalProcessor } from './processor.js';\nimport { sqlTag } from './lib/sql-tag.js';\nimport { convertRowsToObjects } from './lib/convert-rows-to-objects.js';\nimport { normalizeStatement } from './lib/normalize-statement.js';\nimport { getQueryKey } from './lib/get-query-key.js';\nimport { normalizeSql } from './lib/normalize-sql.js';\nimport { mutationLock } from './lib/mutation-lock.js';\nimport { normalizeDatabaseFile } from './lib/normalize-database-file.js';\nimport { SQLiteMemoryDriver } from './drivers/sqlite-memory-driver.js';\nimport { SQLiteKvvfsDriver } from './drivers/sqlite-kvvfs-driver.js';\nimport { getDatabaseKey } from './lib/get-database-key.js';\nexport class SQLocal {\n constructor(config) {\n Object.defineProperty(this, \"config\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"clientKey\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"processor\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"isDestroyed\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: false\n });\n Object.defineProperty(this, \"bypassMutationLock\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: false\n });\n Object.defineProperty(this, \"transactionQueryKeyQueue\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: []\n });\n Object.defineProperty(this, \"userCallbacks\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: new Map()\n });\n Object.defineProperty(this, \"queriesInProgress\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: new Map()\n });\n Object.defineProperty(this, \"proxy\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"reinitChannel\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"effectsChannel\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"processMessageEvent\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: (event) => {\n const message = event instanceof MessageEvent ? event.data : event;\n const queries = this.queriesInProgress;\n switch (message.type) {\n case 'success':\n case 'data':\n case 'buffer':\n case 'info':\n case 'error':\n if (message.queryKey && queries.has(message.queryKey)) {\n const [resolve, reject] = queries.get(message.queryKey);\n if (message.type === 'error') {\n reject(message.error);\n }\n else {\n resolve(message);\n }\n queries.delete(message.queryKey);\n }\n else if (message.type === 'error') {\n throw message.error;\n }\n break;\n case 'callback':\n const userCallback = this.userCallbacks.get(message.name);\n if (userCallback) {\n userCallback(...(message.args ?? []));\n }\n break;\n case 'event':\n this.config.onConnect?.(message.reason);\n break;\n }\n }\n });\n Object.defineProperty(this, \"createQuery\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n return mutationLock('shared', this.bypassMutationLock ||\n message.type === 'import' ||\n message.type === 'delete', this.config, async () => {\n if (this.isDestroyed === true) {\n throw new Error('This SQLocal client has been destroyed. You will need to initialize a new client in order to make further queries.');\n }\n const queryKey = getQueryKey();\n switch (message.type) {\n case 'import':\n this.processor.postMessage({\n ...message,\n queryKey,\n }, [message.database]);\n break;\n default:\n this.processor.postMessage({\n ...message,\n queryKey,\n });\n break;\n }\n return new Promise((resolve, reject) => {\n this.queriesInProgress.set(queryKey, [resolve, reject]);\n });\n });\n }\n });\n Object.defineProperty(this, \"broadcast\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: (message) => {\n this.reinitChannel.postMessage(message);\n }\n });\n Object.defineProperty(this, \"exec\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (sql, params, method = 'all', transactionKey) => {\n const message = await this.createQuery({\n type: 'query',\n transactionKey,\n sql,\n params,\n method,\n });\n const data = {\n rows: [],\n columns: [],\n };\n if (message.type === 'data') {\n data.rows = message.data[0]?.rows ?? [];\n data.columns = message.data[0]?.columns ?? [];\n }\n return data;\n }\n });\n Object.defineProperty(this, \"execBatch\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (statements) => {\n const message = await this.createQuery({\n type: 'batch',\n statements,\n });\n const data = new Array(statements.length).fill({\n rows: [],\n columns: [],\n });\n if (message.type === 'data') {\n message.data.forEach((result, resultIndex) => {\n data[resultIndex] = result;\n });\n }\n return data;\n }\n });\n Object.defineProperty(this, \"sql\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (queryTemplate, ...params) => {\n const statement = normalizeSql(queryTemplate, params);\n const { rows, columns } = await this.exec(statement.sql, statement.params, 'all');\n const resultRecords = convertRowsToObjects(rows, columns);\n return resultRecords;\n }\n });\n Object.defineProperty(this, \"batch\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (passStatements) => {\n const statements = passStatements(sqlTag);\n const data = await this.execBatch(statements);\n return data.map(({ rows, columns }) => {\n const resultRecords = convertRowsToObjects(rows, columns);\n return resultRecords;\n });\n }\n });\n Object.defineProperty(this, \"beginTransaction\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async () => {\n const transactionKey = getQueryKey();\n await this.createQuery({\n type: 'transaction',\n transactionKey,\n action: 'begin',\n });\n const query = async (passStatement) => {\n const statement = normalizeStatement(passStatement);\n if (statement.exec) {\n this.transactionQueryKeyQueue.push(transactionKey);\n return statement.exec();\n }\n const { rows, columns } = await this.exec(statement.sql, statement.params, 'all', transactionKey);\n const resultRecords = convertRowsToObjects(rows, columns);\n return resultRecords;\n };\n const sql = async (queryTemplate, ...params) => {\n const statement = normalizeSql(queryTemplate, params);\n const resultRecords = await query(statement);\n return resultRecords;\n };\n const commit = async () => {\n await this.createQuery({\n type: 'transaction',\n transactionKey,\n action: 'commit',\n });\n };\n const rollback = async () => {\n await this.createQuery({\n type: 'transaction',\n transactionKey,\n action: 'rollback',\n });\n };\n return {\n query,\n sql,\n commit,\n rollback,\n };\n }\n });\n Object.defineProperty(this, \"transaction\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (transaction) => {\n return mutationLock('exclusive', false, this.config, async () => {\n let tx;\n this.bypassMutationLock = true;\n try {\n tx = await this.beginTransaction();\n const result = await transaction({\n sql: tx.sql,\n query: tx.query,\n });\n await tx.commit();\n return result;\n }\n catch (err) {\n await tx?.rollback();\n throw err;\n }\n finally {\n this.bypassMutationLock = false;\n }\n });\n }\n });\n Object.defineProperty(this, \"reactiveQuery\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: (passStatement) => {\n let value = [];\n let gotFirstValue = false;\n let isListening = false;\n let updateCount = 0;\n const statement = normalizeStatement(passStatement);\n const watchedTables = new Set();\n const subObservers = new Set();\n const errObservers = new Set();\n const runStatement = async () => {\n try {\n const updateOrder = ++updateCount;\n if (watchedTables.size === 0) {\n const usedTables = await this.sql(\"SELECT name, wr FROM tables_used(?) WHERE type = 'table'\", statement.sql);\n const readTables = new Set();\n const writtenTables = new Set();\n usedTables.forEach((table) => {\n if (typeof table.name !== 'string')\n return;\n table.wr\n ? writtenTables.add(table.name)\n : readTables.add(table.name);\n });\n if (readTables.size === 0) {\n throw new Error('The passed SQL does not read any tables.');\n }\n if (Array.from(writtenTables).some((table) => readTables.has(table))) {\n 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.');\n }\n readTables.forEach((name) => watchedTables.add(name));\n }\n const results = statement.exec\n ? await statement.exec()\n : await this.sql(statement.sql, ...statement.params);\n if (updateOrder === updateCount) {\n value = results;\n gotFirstValue = true;\n subObservers.forEach((observer) => observer(value));\n }\n }\n catch (err) {\n errObservers.forEach((observer) => {\n observer(err instanceof Error ? err : new Error(String(err)));\n });\n }\n };\n const onEffect = (message) => {\n if (message.data.tables.some((table) => watchedTables.has(table))) {\n runStatement();\n }\n };\n return {\n get value() {\n return value;\n },\n subscribe: (onData, onError) => {\n if (!this.effectsChannel) {\n throw new Error('This SQLocal instance is not configured for reactive queries. Set the \"reactive\" option to enable them.');\n }\n if (!onError) {\n onError = (err) => {\n throw err;\n };\n }\n subObservers.add(onData);\n errObservers.add(onError);\n if (!isListening) {\n this.effectsChannel.addEventListener('message', onEffect);\n isListening = true;\n runStatement();\n }\n else if (gotFirstValue) {\n onData(value);\n }\n return {\n unsubscribe: () => {\n subObservers.delete(onData);\n errObservers.delete(onError);\n if (subObservers.size !== 0)\n return;\n this.effectsChannel?.removeEventListener('message', onEffect);\n isListening = false;\n },\n };\n },\n };\n }\n });\n Object.defineProperty(this, \"createCallbackFunction\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (funcName, func) => {\n await this.createQuery({\n type: 'function',\n functionName: funcName,\n functionType: 'callback',\n });\n this.userCallbacks.set(funcName, func);\n }\n });\n Object.defineProperty(this, \"createScalarFunction\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (funcName, func) => {\n const key = `_sqlocal_func_${funcName}`;\n const attachFunction = () => {\n this.proxy[key] = func;\n };\n if (this.proxy === globalThis) {\n attachFunction();\n }\n await this.createQuery({\n type: 'function',\n functionName: funcName,\n functionType: 'scalar',\n });\n if (this.proxy !== globalThis) {\n attachFunction();\n }\n }\n });\n Object.defineProperty(this, \"createAggregateFunction\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (funcName, func) => {\n const key = `_sqlocal_func_${funcName}`;\n const attachFunction = () => {\n this.proxy[`${key}_step`] = func.step;\n this.proxy[`${key}_final`] = func.final;\n };\n if (this.proxy === globalThis) {\n attachFunction();\n }\n await this.createQuery({\n type: 'function',\n functionName: funcName,\n functionType: 'aggregate',\n });\n if (this.proxy !== globalThis) {\n attachFunction();\n }\n }\n });\n Object.defineProperty(this, \"getDatabaseInfo\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async () => {\n const message = await this.createQuery({ type: 'getinfo' });\n if (message.type === 'info') {\n return message.info;\n }\n else {\n throw new Error('The database failed to return valid information.');\n }\n }\n });\n Object.defineProperty(this, \"getDatabaseFile\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async () => {\n const message = await this.createQuery({ type: 'export' });\n if (message.type === 'buffer') {\n return new File([message.buffer], message.bufferName, {\n type: 'application/x-sqlite3',\n });\n }\n else {\n throw new Error('The database failed to export.');\n }\n }\n });\n Object.defineProperty(this, \"overwriteDatabaseFile\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (databaseFile, beforeUnlock) => {\n await mutationLock('exclusive', false, this.config, async () => {\n try {\n this.broadcast({\n type: 'close',\n clientKey: this.clientKey,\n });\n const database = await normalizeDatabaseFile(databaseFile, 'buffer');\n await this.createQuery({\n type: 'import',\n database,\n });\n if (typeof beforeUnlock === 'function') {\n this.bypassMutationLock = true;\n await beforeUnlock();\n }\n this.broadcast({\n type: 'reinit',\n clientKey: this.clientKey,\n reason: 'overwrite',\n });\n }\n finally {\n this.bypassMutationLock = false;\n }\n });\n }\n });\n Object.defineProperty(this, \"deleteDatabaseFile\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (beforeUnlock) => {\n await mutationLock('exclusive', false, this.config, async () => {\n try {\n this.broadcast({\n type: 'close',\n clientKey: this.clientKey,\n });\n await this.createQuery({\n type: 'delete',\n });\n if (typeof beforeUnlock === 'function') {\n this.bypassMutationLock = true;\n await beforeUnlock();\n }\n this.broadcast({\n type: 'reinit',\n clientKey: this.clientKey,\n reason: 'delete',\n });\n }\n finally {\n this.bypassMutationLock = false;\n }\n });\n }\n });\n Object.defineProperty(this, \"destroy\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async () => {\n await this.createQuery({ type: 'destroy' });\n if (typeof globalThis.Worker !== 'undefined' &&\n this.processor instanceof Worker) {\n this.processor.removeEventListener('message', this.processMessageEvent);\n this.processor.terminate();\n }\n this.queriesInProgress.clear();\n this.userCallbacks.clear();\n this.reinitChannel.close();\n this.effectsChannel?.close();\n this.isDestroyed = true;\n }\n });\n Object.defineProperty(this, _a, {\n enumerable: true,\n configurable: true,\n writable: true,\n value: () => {\n this.destroy();\n }\n });\n Object.defineProperty(this, _b, {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async () => {\n await this.destroy();\n }\n });\n const clientConfig = typeof config === 'string' ? { databasePath: config } : config;\n const { onInit, onConnect, processor, ...commonConfig } = clientConfig;\n const { databasePath } = commonConfig;\n this.config = clientConfig;\n this.clientKey = getQueryKey();\n const dbKey = getDatabaseKey(databasePath, this.clientKey);\n this.reinitChannel = new BroadcastChannel(`_sqlocal_reinit_(${dbKey})`);\n if (commonConfig.reactive) {\n this.effectsChannel = new BroadcastChannel(`_sqlocal_effects_(${dbKey})`);\n }\n if (typeof processor !== 'undefined') {\n this.processor = processor;\n }\n else if (databasePath === 'local' || databasePath === ':localStorage:') {\n const driver = new SQLiteKvvfsDriver('local');\n this.processor = new SQLocalProcessor(driver);\n }\n else if (databasePath === 'session' ||\n databasePath === ':sessionStorage:') {\n const driver = new SQLiteKvvfsDriver('session');\n this.processor = new SQLocalProcessor(driver);\n }\n else if (typeof globalThis.Worker !== 'undefined' &&\n databasePath !== ':memory:') {\n this.processor = new Worker(new URL('./worker', import.meta.url), {\n type: 'module',\n });\n }\n else {\n const driver = new SQLiteMemoryDriver();\n this.processor = new SQLocalProcessor(driver);\n }\n if (this.processor instanceof SQLocalProcessor) {\n this.processor.onmessage = (message) => this.processMessageEvent(message);\n this.proxy = globalThis;\n }\n else {\n this.processor.addEventListener('message', this.processMessageEvent);\n this.proxy = coincident(this.processor);\n }\n this.processor.postMessage({\n type: 'config',\n config: {\n ...commonConfig,\n clientKey: this.clientKey,\n onInitStatements: onInit?.(sqlTag) ?? [],\n },\n });\n }\n}\n_a = Symbol.dispose, _b = Symbol.asyncDispose;\n//# sourceMappingURL=client.js.map","/**\n * Database Module\n *\n * Uses SQLocal directly with BroadcastChannel for cross-tab coordination.\n *\n * Why this approach instead of SharedWorker?\n * - SQLocal already uses its own internal worker for OPFS access\n * - Wrapping it in another SharedWorker adds complexity and causes issues\n * - BroadcastChannel provides simple cross-tab communication\n * - Each tab has its own SQLocal instance but they share the same OPFS database file\n *\n * Usage:\n * import { sql, dbReady, addLocation, getLocations } from './database.js';\n *\n * await dbReady;\n * await addLocation('Point A', -1.5, 7.5);\n * const locations = await getLocations();\n */\n\nimport { SQLocal } from 'sqlocal';\n\n// Database configuration\nconst DATABASE_PATH = 'lupmis2.db';\nconst BROADCAST_CHANNEL = 'lupmis-db-sync';\n\n// Create SQLocal instance\nconst db = new SQLocal(DATABASE_PATH);\n\n// Get the sql tagged template function\nconst { sql } = db;\n\nconsole.log('[Database] SQLocal instance created for:', DATABASE_PATH);\n\n// Export sql for direct queries\nexport { sql };\n\n// Create broadcast channel for cross-tab coordination\nconst channel = new BroadcastChannel(BROADCAST_CHANNEL);\n\n// Track if database is ready\nlet isReady = false;\nlet readyResolve;\nlet readyReject;\n\nexport const dbReady = new Promise((resolve, reject) => {\n readyResolve = resolve;\n readyReject = reject;\n});\n\n// Database change listeners\nconst changeListeners = new Set();\n\n/**\n * Subscribe to database changes (from any tab)\n * @param {Function} listener - Called with { table, action, id }\n * @returns {Function} Unsubscribe function\n */\nexport function onDatabaseChange(listener) {\n changeListeners.add(listener);\n return () => changeListeners.delete(listener);\n}\n\n// Handle messages from other tabs\nchannel.onmessage = (event) => {\n const { type, payload } = event.data;\n if (type === 'DB_CHANGE') {\n // Notify local listeners about changes from other tabs\n for (const listener of changeListeners) {\n try {\n listener(payload);\n } catch (e) {\n console.error('[Database] Change listener error:', e);\n }\n }\n }\n};\n\n/**\n * Broadcast a database change to other tabs\n */\nfunction broadcastChange(table, action, id = null) {\n channel.postMessage({\n type: 'DB_CHANGE',\n payload: { table, action, id, timestamp: Date.now() }\n });\n\n // Also notify local listeners\n for (const listener of changeListeners) {\n try {\n listener({ table, action, id, timestamp: Date.now(), local: true });\n } catch (e) {\n console.error('[Database] Change listener error:', e);\n }\n }\n}\n\n// ============================================================================\n// Database Initialization\n// ============================================================================\n\n/**\n * Initialize the database schema\n */\nexport async function initSchema() {\n try {\n console.log('[Database] Initializing schema...');\n\n // Test connection\n const testResult = await sql`SELECT sqlite_version() as version`;\n console.log('[Database] SQLite version:', testResult[0]?.version);\n\n // Create locations table\n console.log('[Database] Creating locations table...');\n await sql`\n CREATE TABLE IF NOT EXISTS locations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n longitude REAL NOT NULL,\n latitude REAL NOT NULL,\n description TEXT,\n category TEXT DEFAULT 'default',\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP,\n synced INTEGER DEFAULT 0\n )\n `;\n\n // Verify table exists\n const tablesAfterLocations = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;\n console.log('[Database] Locations table exists:', tablesAfterLocations.length > 0);\n\n // Create sync_log table\n console.log('[Database] Creating sync_log table...');\n await sql`\n CREATE TABLE IF NOT EXISTS sync_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n table_name TEXT NOT NULL,\n record_id INTEGER NOT NULL,\n action TEXT NOT NULL,\n timestamp TEXT DEFAULT CURRENT_TIMESTAMP,\n synced INTEGER DEFAULT 0\n )\n `;\n\n // Create remote_data cache table\n console.log('[Database] Creating remote_data table...');\n await sql`\n CREATE TABLE IF NOT EXISTS remote_data (\n key TEXT PRIMARY KEY,\n data TEXT NOT NULL,\n fetched_at TEXT DEFAULT CURRENT_TIMESTAMP\n )\n `;\n\n // Create collector_zones table for caching zone data\n console.log('[Database] Creating collector_zones table...');\n await sql`\n CREATE TABLE IF NOT EXISTS collector_zones (\n id INTEGER PRIMARY KEY,\n zone_name TEXT,\n geometry_wkt TEXT,\n properties TEXT,\n fetched_at TEXT DEFAULT CURRENT_TIMESTAMP\n )\n `;\n\n // Create parcels table for caching parcel data\n // status: 'verified' = from API, 'new' = drawn locally, needs verification\n console.log('[Database] Creating parcels table...');\n await sql`\n CREATE TABLE IF NOT EXISTS parcels (\n id INTEGER PRIMARY KEY,\n geometry_wkt TEXT,\n properties TEXT,\n status TEXT DEFAULT 'verified',\n fetched_at TEXT DEFAULT CURRENT_TIMESTAMP\n )\n `;\n\n // Migrate: add status column if it doesn't exist (for existing databases)\n try {\n await sql`SELECT status FROM parcels LIMIT 1`;\n } catch {\n console.log('[Database] Adding status column to parcels table...');\n await sql`ALTER TABLE parcels ADD COLUMN status TEXT DEFAULT 'verified'`;\n }\n\n // Create building_footprints table for caching footprint data\n console.log('[Database] Creating building_footprints table...');\n await sql`\n CREATE TABLE IF NOT EXISTS building_footprints (\n id INTEGER PRIMARY KEY,\n geometry_wkt TEXT,\n properties TEXT,\n fetched_at TEXT DEFAULT CURRENT_TIMESTAMP\n )\n `;\n\n // Create osm_roads table for caching the OSM road network\n console.log('[Database] Creating osm_roads table...');\n await sql`\n CREATE TABLE IF NOT EXISTS osm_roads (\n osm_id INTEGER PRIMARY KEY,\n geometry_wkt TEXT,\n properties TEXT,\n fetched_at TEXT DEFAULT CURRENT_TIMESTAMP\n )\n `;\n\n // ── GPS trails ──────────────────────────────────────────────────────\n // Recorded field-movement tracks. These are the local store for the\n // reusable GeoTracker module (src/geotracker/). `client_uuid` lets the\n // server de-duplicate re-synced trails; `satellites` is nullable because\n // the web Geolocation API does not expose it (only native builds do).\n console.log('[Database] Creating gps_trails table...');\n await sql`\n CREATE TABLE IF NOT EXISTS gps_trails (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n client_uuid TEXT UNIQUE,\n name TEXT,\n district_id TEXT,\n started_at TEXT NOT NULL,\n ended_at TEXT,\n status TEXT NOT NULL DEFAULT 'recording',\n point_count INTEGER NOT NULL DEFAULT 0,\n distance_m REAL NOT NULL DEFAULT 0,\n synced INTEGER NOT NULL DEFAULT 0,\n remote_id TEXT,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP\n )\n `;\n\n console.log('[Database] Creating gps_trail_points table...');\n await sql`\n CREATE TABLE IF NOT EXISTS gps_trail_points (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n trail_id INTEGER NOT NULL,\n seq INTEGER NOT NULL,\n longitude REAL NOT NULL,\n latitude REAL NOT NULL,\n altitude REAL,\n accuracy REAL,\n altitude_accuracy REAL,\n heading REAL,\n speed REAL,\n satellites INTEGER,\n recorded_at TEXT NOT NULL\n )\n `;\n\n // Create indexes\n await sql`CREATE INDEX IF NOT EXISTS idx_locations_category ON locations(category)`;\n await sql`CREATE INDEX IF NOT EXISTS idx_locations_synced ON locations(synced)`;\n await sql`CREATE INDEX IF NOT EXISTS idx_gps_trails_synced ON gps_trails(synced, status)`;\n await sql`CREATE INDEX IF NOT EXISTS idx_gps_trail_points_trail ON gps_trail_points(trail_id, seq)`;\n\n // Final verification\n const allTables = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;\n console.log('[Database] All tables:', allTables.map(t => t.name));\n\n isReady = true;\n readyResolve(true);\n console.log('[Database] ✓ Schema initialized');\n\n } catch (error) {\n console.error('[Database] ✗ Schema init failed:', error);\n readyReject(error);\n throw error;\n }\n}\n\n// ============================================================================\n// Location Operations\n// ============================================================================\n\n/**\n * Add a new location\n */\nexport async function addLocation(name, longitude, latitude, options = {}) {\n const { description = null, category = 'default' } = options;\n\n console.log('[Database] Adding location:', name, longitude, latitude, category);\n\n try {\n // Check table exists first\n const tableCheck = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;\n console.log('[Database] Table check before insert:', tableCheck);\n\n if (tableCheck.length === 0) {\n console.error('[Database] ✗ locations table does not exist!');\n throw new Error('locations table does not exist');\n }\n\n // Insert - using explicit values\n console.log('[Database] Executing INSERT...');\n await sql`\n INSERT INTO locations (name, longitude, latitude, description, category)\n VALUES (${name}, ${longitude}, ${latitude}, ${description}, ${category})\n `;\n console.log('[Database] INSERT completed');\n\n // Get the ID\n const idResult = await sql`SELECT last_insert_rowid() as id`;\n const newId = idResult[0]?.id;\n console.log('[Database] New ID:', newId);\n\n // Verify it was actually inserted\n const verifyResult = await sql`SELECT * FROM locations WHERE id = ${newId}`;\n console.log('[Database] Verify insert:', verifyResult);\n\n if (verifyResult.length === 0) {\n console.error('[Database] ✗ Insert verification failed - row not found!');\n throw new Error('Insert verification failed');\n }\n\n // Log for sync\n await sql`\n INSERT INTO sync_log (table_name, record_id, action)\n VALUES ('locations', ${newId}, 'INSERT')\n `;\n\n // Broadcast to other tabs\n broadcastChange('locations', 'INSERT', newId);\n\n console.log('[Database] ✓ Location added:', newId);\n return { id: newId };\n\n } catch (error) {\n console.error('[Database] ✗ Failed to add location:', error);\n throw error;\n}\n}\n\n/**\n * Get all locations\n */\nexport async function getLocations(options = {}) {\n const { category = null, limit = 1000 } = options;\n\n try {\n // First check if table exists\n const tableCheck = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;\n console.log('[Database] getLocations - table exists:', tableCheck.length > 0);\n\n if (tableCheck.length === 0) {\n console.warn('[Database] locations table does not exist yet');\n return [];\n }\n\n let results;\n if (category) {\n results = await sql`\n SELECT * FROM locations\n WHERE category = ${category}\n ORDER BY created_at DESC\n LIMIT ${limit}\n `;\n } else {\n results = await sql`\n SELECT * FROM locations\n ORDER BY created_at DESC\n LIMIT ${limit}\n `;\n}\n\n console.log('[Database] getLocations returned', results.length, 'rows');\n return results;\n\n } catch (error) {\n console.error('[Database] getLocations error:', error);\n return [];\n }\n}\n\nexport async function getLocation(id) {\n try {\n const results = await sql`SELECT * FROM locations WHERE id = ${id}`;\n return results[0] || null;\n } catch (error) {\n console.error('[Database] getLocation error:', error);\n return null;\n}\n}\n\n/**\n * Update a location\n */\nexport async function updateLocation(id, updates) {\n const { name, longitude, latitude, description, category } = updates;\n\n try {\n const location = await getLocation(id);\n if (!location) {\n throw new Error(`Location ${id} not found`);\n }\n\n await sql`\n UPDATE locations\n SET\n name = ${name ?? location.name},\n longitude = ${longitude ?? location.longitude},\n latitude = ${latitude ?? location.latitude},\n description = ${description ?? location.description},\n category = ${category ?? location.category},\n updated_at = CURRENT_TIMESTAMP,\n synced = 0\n WHERE id = ${id}\n `;\n\n // Log for sync\n await sql`\n INSERT INTO sync_log (table_name, record_id, action)\n VALUES ('locations', ${id}, 'UPDATE')\n `;\n\n // Broadcast to other tabs\n broadcastChange('locations', 'UPDATE', id);\n console.log('[Database] ✓ Location updated:', id);\n\n } catch (error) {\n console.error('[Database] ✗ updateLocation error:', error);\n throw error;\n}\n}\n\n/**\n * Delete a location\n */\nexport async function deleteLocation(id) {\n try {\n await sql`\n INSERT INTO sync_log (table_name, record_id, action)\n VALUES ('locations', ${id}, 'DELETE')\n `;\n\n await sql`DELETE FROM locations WHERE id = ${id}`;\n\n // Broadcast to other tabs\n broadcastChange('locations', 'DELETE', id);\n console.log('[Database] ✓ Location deleted:', id);\n\n } catch (error) {\n console.error('[Database] ✗ deleteLocation error:', error);\n throw error;\n}\n}\n\n/**\n * Get location count\n */\nexport async function getLocationCount() {\n try {\n const result = await sql`SELECT COUNT(*) as count FROM locations`;\n return result[0]?.count ?? 0;\n } catch (error) {\n console.error('[Database] getLocationCount error:', error);\n return 0;\n}\n}\n\n// ============================================================================\n// Sync Operations\n// ============================================================================\n\n/**\n * Get unsynced changes\n */\nexport async function getUnsyncedChanges() {\n return sql`SELECT * FROM sync_log WHERE synced = 0 ORDER BY timestamp ASC`;\n}\n\n/**\n * Mark changes as synced\n */\nexport async function markSynced(syncLogIds) {\n if (!syncLogIds.length) return;\n for (const id of syncLogIds) {\n await sql`UPDATE sync_log SET synced = 1 WHERE id = ${id}`;\n}\n}\n\n/**\n * Get locations that need syncing\n */\nexport async function getUnsyncedLocations() {\n return sql`SELECT * FROM locations WHERE synced = 0`;\n}\n\n/**\n * Mark locations as synced\n */\nexport async function markLocationsSynced(ids) {\n if (!ids.length) return;\n for (const id of ids) {\n await sql`UPDATE locations SET synced = 1 WHERE id = ${id}`;\n}\n}\n\n// ============================================================================\n// Remote Data Cache\n// ============================================================================\n\n/**\n * Save remote API data to the local cache.\n * Uses INSERT OR REPLACE so the same key is always overwritten.\n *\n * @param {string} key - Unique identifier (e.g. 'district_boundary')\n * @param {Object|Array} data - JSON-serialisable data to cache\n */\nexport async function saveRemoteData(key, data) {\n try {\n const json = JSON.stringify(data);\n await sql`\n INSERT OR REPLACE INTO remote_data (key, data, fetched_at)\n VALUES (${key}, ${json}, CURRENT_TIMESTAMP)\n `;\n console.log('[Database] ✓ Remote data cached:', key);\n } catch (error) {\n console.error('[Database] ✗ Failed to cache remote data:', key, error);\n throw error;\n }\n}\n\n/**\n * Retrieve cached remote data by key.\n *\n * @param {string} key - Unique identifier (e.g. 'district_boundary')\n * @returns {Promise} Parsed data, or null if not cached\n */\nexport async function getRemoteData(key) {\n try {\n const rows = await sql`SELECT data, fetched_at FROM remote_data WHERE key = ${key}`;\n if (rows.length === 0) return null;\n const parsed = JSON.parse(rows[0].data);\n console.log('[Database] ✓ Remote data loaded from cache:', key, '(fetched', rows[0].fetched_at + ')');\n return parsed;\n } catch (error) {\n console.error('[Database] ✗ Failed to read cached remote data:', key, error);\n return null;\n }\n}\n\n// ============================================================================\n// Collector Zones\n// ============================================================================\n\n/**\n * Save collector zones to the local table.\n * Replaces all existing rows.\n *\n * @param {Array} zones - Array of zone objects from the API\n */\nexport async function saveCollectorZones(zones) {\n try {\n await sql`DELETE FROM collector_zones`;\n for (const z of zones) {\n const props = JSON.stringify(z);\n await sql`\n INSERT INTO collector_zones (id, zone_name, geometry_wkt, properties, fetched_at)\n VALUES (${z.colzonenr || z.id}, ${z.colzonename || z.zone_name || ''}, ${z.polygon || z.boundary || ''}, ${props}, CURRENT_TIMESTAMP)\n `;\n }\n console.log('[Database] ✓ Saved', zones.length, 'collector zones');\n } catch (error) {\n console.error('[Database] ✗ Failed to save collector zones:', error);\n throw error;\n }\n}\n\n/**\n * Load all cached collector zones from the local table.\n * @returns {Promise} Array of zone objects, or null if empty\n */\nexport async function getLocalCollectorZones() {\n try {\n const rows = await sql`SELECT properties FROM collector_zones ORDER BY id`;\n if (rows.length === 0) return null;\n return rows.map(r => JSON.parse(r.properties));\n } catch (error) {\n console.error('[Database] ✗ Failed to read local collector zones:', error);\n return null;\n }\n}\n\n// ============================================================================\n// Parcels\n// ============================================================================\n\n/**\n * Save parcels to the local table.\n * Replaces all existing rows.\n *\n * @param {Array} parcels - Array of parcel objects from the API\n */\nexport async function saveParcels(parcels) {\n try {\n await sql`DELETE FROM parcels`;\n let saved = 0;\n for (const p of parcels) {\n const id = p.id || p.parcelid || p.parcel_id || null;\n if (id == null) continue; // skip rows without a usable ID\n const props = JSON.stringify(p);\n // API field names: 'boundary' (WKT), 'polygon', 'geom', 'wkt'\n const wkt = p.boundary || p.polygon || p.geom || p.wkt || '';\n await sql`\n INSERT OR REPLACE INTO parcels (id, geometry_wkt, properties, fetched_at)\n VALUES (${id}, ${wkt}, ${props}, CURRENT_TIMESTAMP)\n `;\n saved++;\n }\n console.log('[Database] ✓ Saved', saved, 'parcels (from', parcels.length, 'rows,', parcels.length - saved, 'duplicates replaced)');\n } catch (error) {\n console.error('[Database] ✗ Failed to save parcels:', error);\n throw error;\n }\n}\n\n/**\n * Load all cached parcels from the local table.\n * @returns {Promise} Array of parcel objects, or null if empty\n */\nexport async function getLocalParcels() {\n try {\n const rows = await sql`SELECT properties FROM parcels ORDER BY id`;\n if (rows.length === 0) return null;\n return rows.map(r => JSON.parse(r.properties));\n } catch (error) {\n console.error('[Database] ✗ Failed to read local parcels:', error);\n return null;\n }\n}\n\n/**\n * Update a single parcel's properties in the local table.\n * Only the properties JSON blob is updated; geometry stays unchanged.\n *\n * @param {number|string} parcelId - The parcel id\n * @param {Object} updatedProps - Full updated properties object\n */\nexport async function updateParcel(parcelId, updatedProps) {\n try {\n const props = JSON.stringify(updatedProps);\n await sql`UPDATE parcels SET properties = ${props} WHERE id = ${parcelId}`;\n console.log('[Database] ✓ Parcel updated:', parcelId);\n broadcastChange('parcels', 'UPDATE', parcelId);\n } catch (error) {\n console.error('[Database] ✗ Failed to update parcel:', parcelId, error);\n throw error;\n }\n}\n\n/**\n * Insert a newly drawn parcel into the local table.\n * The parcel is tagged with status='new' to indicate it needs verification.\n *\n * @param {string} geometryWkt - WKT geometry string (EPSG:4326)\n * @param {Object} properties - Attribute properties from the form\n * @returns {Promise<{id: number}>} The new row id\n */\nexport async function insertNewParcel(geometryWkt, properties) {\n try {\n const props = JSON.stringify(properties);\n await sql`\n INSERT INTO parcels (id, geometry_wkt, properties, status, fetched_at)\n VALUES (NULL, ${geometryWkt}, ${props}, 'new', CURRENT_TIMESTAMP)\n `;\n const idResult = await sql`SELECT last_insert_rowid() as id`;\n const newId = idResult[0]?.id;\n console.log('[Database] ✓ New parcel inserted:', newId, '(status: new)');\n broadcastChange('parcels', 'INSERT', newId);\n return { id: newId };\n } catch (error) {\n console.error('[Database] ✗ Failed to insert new parcel:', error);\n throw error;\n }\n}\n\n// ============================================================================\n// Building Footprints\n// ============================================================================\n\n/**\n * Save building footprints to the local table.\n * Replaces all existing rows.\n *\n * @param {Array} footprints - Array of footprint objects from the API\n */\nexport async function saveBuildingFootprints(footprints) {\n try {\n // Log first entry's keys and value types to help debug field names\n if (footprints.length > 0) {\n const first = footprints[0];\n const types = {};\n for (const [k, v] of Object.entries(first)) {\n types[k] = v === null ? 'null' : typeof v;\n }\n console.log('[Database] First footprint field types:', types);\n }\n\n await sql`DELETE FROM building_footprints`;\n for (const f of footprints) {\n const props = JSON.stringify(f);\n\n // Geometry may arrive as a string (WKT) or an object (GeoJSON) —\n // coerce to a string so SQLocal can bind it.\n let rawWkt = f.polygon || f.boundary || f.geom || f.wkt || f.footprint || '';\n const wkt = typeof rawWkt === 'object' ? JSON.stringify(rawWkt) : String(rawWkt);\n\n // ID must be a primitive (number or null)\n let rawId = f.id || f.footprint_id || f.building_id || null;\n const id = (rawId !== null && typeof rawId === 'object') ? null : rawId;\n\n await sql`\n INSERT INTO building_footprints (id, geometry_wkt, properties, fetched_at)\n VALUES (${id}, ${wkt}, ${props}, CURRENT_TIMESTAMP)\n `;\n }\n console.log('[Database] ✓ Saved', footprints.length, 'building footprints');\n } catch (error) {\n console.error('[Database] ✗ Failed to save building footprints:', error);\n throw error;\n }\n}\n\n/**\n * Load all cached building footprints from the local table.\n * @returns {Promise} Array of footprint objects, or null if empty\n */\nexport async function getLocalBuildingFootprints() {\n try {\n const rows = await sql`SELECT properties FROM building_footprints ORDER BY id`;\n if (rows.length === 0) return null;\n return rows.map(r => JSON.parse(r.properties));\n } catch (error) {\n console.error('[Database] ✗ Failed to read local building footprints:', error);\n return null;\n }\n}\n\n/**\n * Save OSM roads to the local SQLite table.\n * Replaces all existing rows.\n *\n * @param {Array} roads - Array of road objects from the API\n */\nexport async function saveOSMRoads(roads) {\n try {\n if (roads.length > 0) {\n const first = roads[0];\n const types = {};\n for (const [k, v] of Object.entries(first)) {\n types[k] = v === null ? 'null' : typeof v;\n }\n console.log('[Database] First road field types:', types);\n }\n\n await sql`DELETE FROM osm_roads`;\n for (const r of roads) {\n const props = JSON.stringify(r);\n\n // Geometry — may arrive as WKT string or GeoJSON object\n let rawWkt = r.geom || r.geometry || r.wkt || r.road || r.line || '';\n const wkt = typeof rawWkt === 'object' ? JSON.stringify(rawWkt) : String(rawWkt);\n\n // osm_id must be a primitive — fall back to null if missing or malformed\n let rawId = r.osm_id ?? r.osmid ?? r.id ?? null;\n const osmId = (rawId !== null && typeof rawId === 'object') ? null : rawId;\n\n await sql`\n INSERT OR REPLACE INTO osm_roads (osm_id, geometry_wkt, properties, fetched_at)\n VALUES (${osmId}, ${wkt}, ${props}, CURRENT_TIMESTAMP)\n `;\n }\n console.log('[Database] ✓ Saved', roads.length, 'OSM roads');\n } catch (error) {\n console.error('[Database] ✗ Failed to save OSM roads:', error);\n throw error;\n }\n}\n\n/**\n * Load all cached OSM roads from the local table.\n * @returns {Promise} Array of road objects, or null if empty\n */\nexport async function getLocalOSMRoads() {\n try {\n const rows = await sql`SELECT properties FROM osm_roads ORDER BY osm_id`;\n if (rows.length === 0) return null;\n return rows.map(r => JSON.parse(r.properties));\n } catch (error) {\n console.error('[Database] ✗ Failed to read local OSM roads:', error);\n return null;\n }\n}\n\n// ============================================================================\n// Export / Import\n// ============================================================================\n\n/**\n * Export database for backup\n */\nexport async function exportDatabase() {\n return db.getDatabaseFile();\n}\n\n/**\n * Import database from backup\n */\nexport async function importDatabase(data) {\n await db.overwriteDatabaseFile(data);\n broadcastChange('*', 'IMPORT', null);\n}\n\n/**\n * Download database as file\n */\nexport async function downloadDatabase(filename = 'lupmis-backup.sqlite3') {\n const data = await exportDatabase();\n const blob = new Blob([data], { type: 'application/x-sqlite3' });\n const url = URL.createObjectURL(blob);\n\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n a.click();\n\n URL.revokeObjectURL(url);\n}\n\n// Export to GeoJSON\nexport async function exportToGeoJSON() {\n const locations = await getLocations();\n\n return {\n type: 'FeatureCollection',\n features: locations.map((loc) => ({\n type: 'Feature',\n properties: {\n id: loc.id,\n name: loc.name,\n category: loc.category,\n notes: loc.notes,\n created_at: loc.created_at,\n },\n geometry: {\n type: 'Point',\n coordinates: [loc.lon, loc.lat],\n },\n })),\n };\n}\n\n// ============================================================================\n// Utility & Debug\n// ============================================================================\n\n/**\n * Get database status\n */\nexport async function getDatabaseStatus() {\n try {\n const tables = await sql`\n SELECT name FROM sqlite_master\n WHERE type='table' AND name NOT LIKE 'sqlite_%'\n ORDER BY name\n `;\n\n const locationCount = await getLocationCount();\n\n return {\n ready: isReady,\n databasePath: DATABASE_PATH,\n tables: tables.map(t => t.name),\n locationCount\n };\n } catch (error) {\n return {\n ready: false,\n error: error.message\n };\n}\n}\n\n// ============================================================================\n// Cached-Layer Management\n// ============================================================================\n\n/**\n * Tables that hold data fetched from the server.\n *\n * Clearing these is safe — the corresponding loaders (loadParcels,\n * loadBuildingFootprints, loadOSMRoads, loadCollectorZones, …) will re-fetch\n * the data from the API on the next app start.\n *\n * NOT included: user-created tables (`locations`, `pending_changes`) — those\n * hold local work that must not be auto-deleted.\n */\nexport const CACHED_LAYER_TABLES = Object.freeze([\n 'parcels',\n 'building_footprints',\n 'osm_roads',\n 'collector_zones',\n 'remote_data',\n]);\n\n/**\n * Check whether a table name is in the cleared-layer allow-list.\n * @param {string} tableName\n * @returns {boolean}\n */\nexport function isCachedLayerTable(tableName) {\n return CACHED_LAYER_TABLES.includes(tableName);\n}\n\n/**\n * Delete all rows from a single cached-layer table.\n * Rejects unknown table names so this can't be abused to drop user data.\n *\n * @param {string} tableName - One of CACHED_LAYER_TABLES\n * @returns {Promise} Number of rows that were in the table before deletion\n */\nexport async function clearTable(tableName) {\n if (!isCachedLayerTable(tableName)) {\n throw new Error(`Refusing to clear \"${tableName}\" — not a known cached-layer table`);\n }\n\n const before = await sql(`SELECT COUNT(*) AS n FROM \"${tableName}\"`);\n const count = before[0]?.n ?? 0;\n\n await sql(`DELETE FROM \"${tableName}\"`);\n console.log(`[Database] ✓ Cleared \"${tableName}\" (${count} rows)`);\n broadcastChange(tableName, 'CLEAR', null);\n return count;\n}\n\n/**\n * Clear every cached-layer table (whatever exists in this database).\n * Tables that don't exist yet are skipped silently.\n *\n * @returns {Promise<{ table: string, count: number }[]>} per-table report\n */\nexport async function clearAllCachedLayers() {\n const existing = await sql`\n SELECT name FROM sqlite_master\n WHERE type='table' AND name IN (\n 'parcels', 'building_footprints', 'osm_roads', 'collector_zones', 'remote_data'\n )\n `;\n const existingNames = new Set(existing.map((r) => r.name));\n\n const results = [];\n for (const tableName of CACHED_LAYER_TABLES) {\n if (!existingNames.has(tableName)) continue;\n try {\n const count = await clearTable(tableName);\n results.push({ table: tableName, count });\n } catch (err) {\n console.error(`[Database] Failed to clear ${tableName}:`, err);\n results.push({ table: tableName, count: 0, error: err.message });\n }\n }\n\n const total = results.reduce((s, r) => s + r.count, 0);\n console.log(`[Database] ✓ Cleared all cached layers: ${total} rows across ${results.length} tables`);\n return results;\n}\n\n/**\n * Get a list of all tables with their row counts.\n * @returns {Promise>}\n */\nexport async function getTableStats() {\n const tables = await sql`\n SELECT name FROM sqlite_master\n WHERE type='table' AND name NOT LIKE 'sqlite_%'\n ORDER BY name\n `;\n\n if (tables.length === 0) return [];\n\n // Build a UNION ALL query — table names come from sqlite_master so safe to embed.\n // sql() accepts a plain string as first argument (not only tagged template).\n const query = tables\n .map(t => `SELECT '${t.name}' AS name, COUNT(*) AS count FROM \"${t.name}\"`)\n .join(' UNION ALL ');\n\n return sql(query);\n}\n\n/**\n * Get all rows from a given table (max 200 rows for safety).\n * Table name is validated against sqlite_master before querying.\n * @param {string} tableName - Name of the table to read\n * @param {number} [limit=200] - Max rows to return\n * @returns {Promise<{columns: string[], rows: Object[]}>}\n */\nexport async function getTableContent(tableName, limit = 200) {\n // Validate that the table actually exists (prevent injection)\n const valid = await sql`\n SELECT name FROM sqlite_master\n WHERE type='table' AND name = ${tableName}\n `;\n if (valid.length === 0) {\n throw new Error(`Table \"${tableName}\" does not exist`);\n }\n\n const rows = await sql(`SELECT * FROM \"${tableName}\" LIMIT ${limit}`);\n\n // Extract column names from the first row (or return empty)\n const columns = rows.length > 0 ? Object.keys(rows[0]) : [];\n\n return { columns, rows };\n}\n\n// Debug function - call from console to test\nexport async function testDatabase() {\n console.log('=== DATABASE TEST ===');\n\n try {\n // 1. Check connection\n const version = await sql`SELECT sqlite_version() as v`;\n console.log('1. SQLite version:', version[0].v);\n\n // 2. Check tables\n const tables = await sql`SELECT name FROM sqlite_master WHERE type='table'`;\n console.log('2. Tables:', tables.map(t => t.name));\n\n // 3. Try to insert a test row\n console.log('3. Inserting test row...');\n await sql`INSERT INTO locations (name, longitude, latitude, category) VALUES ('TEST', -1.0, 7.0, 'test')`;\n\n // 4. Read it back\n const rows = await sql`SELECT * FROM locations WHERE name = 'TEST'`;\n console.log('4. Test row:', rows);\n\n // 5. Count all rows\n const count = await sql`SELECT COUNT(*) as c FROM locations`;\n console.log('5. Total rows:', count[0].c);\n\n // 6. Delete test row\n await sql`DELETE FROM locations WHERE name = 'TEST'`;\n console.log('6. Test row deleted');\n\n console.log('=== TEST PASSED ===');\n return true;\n } catch (error) {\n console.error('=== TEST FAILED ===', error);\n return false;\n}\n}\n\n// Expose to window for debugging\nif (typeof window !== 'undefined') {\n window.testDatabase = testDatabase;\n window.dbStatus = getDatabaseStatus;\n}\n\nexport async function closeDatabase() {\n channel.close();\n if (db.destroy) {\n await db.destroy();\n}\n}\n\n// ============================================================================\n// GPS Trails (storage adapter for the reusable GeoTracker module)\n// ============================================================================\n//\n// These functions implement the GeoTracker \"storage adapter\" contract using\n// SQLocal. They are intentionally generic (no map / UI coupling) so the same\n// schema + helpers can be lifted into another app alongside src/geotracker/.\n\n/**\n * Create a new trail row (status='recording') and return its local id.\n * @param {object} meta { uuid, name, startedAt, districtId }\n * @returns {Promise} local trail id\n */\nexport async function createGpsTrail(meta) {\n const { uuid, name = null, startedAt, districtId = null } = meta;\n await sql`\n INSERT INTO gps_trails (client_uuid, name, district_id, started_at, status)\n VALUES (${uuid}, ${name}, ${districtId}, ${startedAt}, 'recording')\n `;\n const idResult = await sql`SELECT last_insert_rowid() as id`;\n const id = idResult[0]?.id;\n broadcastChange('gps_trails', 'insert', id);\n return id;\n}\n\n/**\n * Append one recorded point to a trail.\n * @param {number} trailId\n * @param {object} point normalized fix + { seq }\n */\nexport async function addGpsTrailPoint(trailId, point) {\n const {\n seq, lon, lat,\n altitude = null, accuracy = null, altitudeAccuracy = null,\n heading = null, speed = null, satellites = null, timestamp,\n } = point;\n const recordedAt = typeof timestamp === 'number' ? new Date(timestamp).toISOString() : (timestamp || new Date().toISOString());\n await sql`\n INSERT INTO gps_trail_points\n (trail_id, seq, longitude, latitude, altitude, accuracy, altitude_accuracy, heading, speed, satellites, recorded_at)\n VALUES\n (${trailId}, ${seq}, ${lon}, ${lat}, ${altitude}, ${accuracy}, ${altitudeAccuracy}, ${heading}, ${speed}, ${satellites}, ${recordedAt})\n `;\n}\n\n/**\n * Finalise a trail: mark completed and store the summary.\n * @param {number} trailId\n * @param {object} summary { endedAt, pointCount, distanceM }\n */\nexport async function finishGpsTrail(trailId, summary) {\n const { endedAt, pointCount = 0, distanceM = 0 } = summary;\n await sql`\n UPDATE gps_trails\n SET ended_at = ${endedAt}, point_count = ${pointCount}, distance_m = ${distanceM}, status = 'completed'\n WHERE id = ${trailId}\n `;\n broadcastChange('gps_trails', 'update', trailId);\n}\n\n/**\n * Trails that are completed but not yet pushed to the server.\n * @returns {Promise}\n */\nexport async function getUnsyncedGpsTrails() {\n return sql`SELECT * FROM gps_trails WHERE synced = 0 AND status = 'completed' ORDER BY started_at ASC`;\n}\n\n/**\n * All points of a trail, in recorded order.\n * @param {number} trailId\n * @returns {Promise}\n */\nexport async function getGpsTrailPoints(trailId) {\n return sql`SELECT * FROM gps_trail_points WHERE trail_id = ${trailId} ORDER BY seq ASC`;\n}\n\n/**\n * Mark a trail as synced and record the server-assigned id.\n * @param {number} trailId\n * @param {string|number|null} remoteId\n */\nexport async function markGpsTrailSynced(trailId, remoteId = null) {\n await sql`UPDATE gps_trails SET synced = 1, remote_id = ${remoteId} WHERE id = ${trailId}`;\n broadcastChange('gps_trails', 'update', trailId);\n}\n\n/**\n * List trails (most recent first) for a history/list UI.\n * @returns {Promise}\n */\nexport async function getGpsTrails() {\n return sql`SELECT * FROM gps_trails ORDER BY started_at DESC`;\n}\n\n/**\n * Delete a trail and all its points.\n * @param {number} trailId\n */\nexport async function deleteGpsTrail(trailId) {\n await sql`DELETE FROM gps_trail_points WHERE trail_id = ${trailId}`;\n await sql`DELETE FROM gps_trails WHERE id = ${trailId}`;\n broadcastChange('gps_trails', 'delete', trailId);\n}\n\nexport default {\n sql,\n dbReady,\n initSchema,\n addLocation,\n getLocations,\n getLocation,\n updateLocation,\n deleteLocation,\n getLocationCount,\n getUnsyncedChanges,\n getUnsyncedLocations,\n markSynced,\n markLocationsSynced,\n saveRemoteData,\n getRemoteData,\n saveCollectorZones,\n getLocalCollectorZones,\n saveParcels,\n getLocalParcels,\n updateParcel,\n insertNewParcel,\n saveBuildingFootprints,\n getLocalBuildingFootprints,\n saveOSMRoads,\n getLocalOSMRoads,\n createGpsTrail,\n addGpsTrailPoint,\n finishGpsTrail,\n getUnsyncedGpsTrails,\n getGpsTrailPoints,\n markGpsTrailSynced,\n getGpsTrails,\n deleteGpsTrail,\n CACHED_LAYER_TABLES,\n isCachedLayerTable,\n clearTable,\n clearAllCachedLayers,\n exportDatabase,\n exportToGeoJSON,\n importDatabase,\n downloadDatabase,\n getDatabaseStatus,\n getTableStats,\n getTableContent,\n testDatabase,\n onDatabaseChange,\n closeDatabase\n};\n","/**\n * Measurement unit formatting — Metric / Imperial.\n *\n * The active system is persisted in localStorage('measurement-system').\n * Every formatter reads the current setting so the UI updates immediately\n * after the user flips the toggle.\n *\n * All input values are in metres (length) or square metres (area).\n */\n\n// ── Conversion constants ────────────────────────────────────────────────────\nconst M_TO_FT = 3.28084;\nconst M_TO_MI = 0.000621371;\nconst SQM_TO_SQFT = 10.7639;\nconst SQM_TO_ACRE = 0.000247105;\nconst SQM_TO_SQMI = 3.861e-7;\n\n// ── System accessor ─────────────────────────────────────────────────────────\n\n/** @returns {'metric'|'imperial'} */\nexport function getSystem() {\n return localStorage.getItem('measurement-system') || 'metric';\n}\n\n// ── Length / distance ───────────────────────────────────────────────────────\n\n/**\n * Format a length value (in metres) for display.\n * Metric: m / km\n * Imperial: ft / mi\n */\nexport function formatLength(metres) {\n if (getSystem() === 'imperial') {\n const ft = metres * M_TO_FT;\n if (ft >= 5280) {\n return (Math.round(metres * M_TO_MI * 100) / 100) + ' mi';\n }\n return Math.round(ft) + ' ft';\n }\n // metric\n if (metres > 1000) {\n return (Math.round(metres / 1000 * 100) / 100) + ' km';\n }\n return (Math.round(metres * 100) / 100) + ' m';\n}\n\n/**\n * Format a length with both large and small units (for info popups).\n * Metric: \"1.23 km (1,230 m)\" or \"456 m\"\n * Imperial: \"1.23 mi (6,494 ft)\" or \"456 ft\"\n */\nexport function formatLengthFull(metres) {\n if (getSystem() === 'imperial') {\n const ft = metres * M_TO_FT;\n const mi = metres * M_TO_MI;\n if (ft >= 5280) {\n return `${mi.toFixed(2)} mi (${ft.toLocaleString('en', { maximumFractionDigits: 0 })} ft)`;\n }\n return `${ft.toLocaleString('en', { maximumFractionDigits: 1 })} ft`;\n }\n if (metres >= 1000) {\n return `${(metres / 1000).toFixed(2)} km (${metres.toLocaleString('en', { maximumFractionDigits: 0 })} m)`;\n }\n return `${metres.toLocaleString('en', { maximumFractionDigits: 1 })} m`;\n}\n\n// ── Area ────────────────────────────────────────────────────────────────────\n\n/**\n * Format an area value (in square metres) for display.\n * Metric: m² / km²\n * Imperial: ft² / acres / mi²\n */\nexport function formatArea(sqMetres) {\n if (getSystem() === 'imperial') {\n const acres = sqMetres * SQM_TO_ACRE;\n if (acres >= 640) {\n return (Math.round(sqMetres * SQM_TO_SQMI * 100) / 100) + ' mi²';\n }\n if (acres >= 1) {\n return (Math.round(acres * 100) / 100) + ' acres';\n }\n return Math.round(sqMetres * SQM_TO_SQFT).toLocaleString('en') + ' ft²';\n }\n // metric\n if (sqMetres > 1000000) {\n return (Math.round(sqMetres / 1000000 * 100) / 100) + ' km²';\n }\n return (Math.round(sqMetres * 100) / 100) + ' m²';\n}\n\n/**\n * Format an area with both large and small units (for info popups).\n * Metric: \"1.23 km² (1,230,000 m²)\" or \"456 m²\"\n * Imperial: \"1.23 mi² (787 acres)\" or \"2.5 acres\" or \"456 ft²\"\n */\nexport function formatAreaFull(sqMetres) {\n if (getSystem() === 'imperial') {\n const sqft = sqMetres * SQM_TO_SQFT;\n const acres = sqMetres * SQM_TO_ACRE;\n const sqmi = sqMetres * SQM_TO_SQMI;\n if (acres >= 640) {\n return `${sqmi.toFixed(2)} mi² (${acres.toLocaleString('en', { maximumFractionDigits: 0 })} acres)`;\n }\n if (acres >= 1) {\n return `${acres.toLocaleString('en', { maximumFractionDigits: 1 })} acres (${sqft.toLocaleString('en', { maximumFractionDigits: 0 })} ft²)`;\n }\n return `${sqft.toLocaleString('en', { maximumFractionDigits: 0 })} ft²`;\n }\n if (sqMetres > 1000000) {\n return `${(sqMetres / 1000000).toFixed(2)} km² (${sqMetres.toLocaleString('en', { maximumFractionDigits: 0 })} m²)`;\n }\n return `${sqMetres.toLocaleString('en', { maximumFractionDigits: 0 })} m²`;\n}\n\n// ── Circle helper ───────────────────────────────────────────────────────────\n\n/**\n * Format the area of a circle given its radius (in metres).\n */\nexport function formatCircleExtent(radiusMetres) {\n return formatArea(Math.PI * radiusMetres * radiusMetres);\n}\n","/**\n * Pure geometry functions for splitting a polygon by a line.\n *\n * No OpenLayers dependency — operates on raw coordinate arrays.\n */\n\n/**\n * Compute the intersection point of two 2D line segments.\n * Segment A: p1→p2, Segment B: p3→p4.\n *\n * @param {number[]} p1\n * @param {number[]} p2\n * @param {number[]} p3\n * @param {number[]} p4\n * @param {number} [eps=1e-10] tolerance for parallel check\n * @returns {{ point: number[], t: number, u: number } | null}\n * t = parametric position on segment A (0–1),\n * u = parametric position on segment B (0–1)\n */\nfunction segmentIntersection(p1, p2, p3, p4, eps = 1e-10) {\n const dx1 = p2[0] - p1[0];\n const dy1 = p2[1] - p1[1];\n const dx2 = p4[0] - p3[0];\n const dy2 = p4[1] - p3[1];\n\n const denom = dx1 * dy2 - dy1 * dx2;\n if (Math.abs(denom) < eps) return null; // parallel / collinear\n\n const dx3 = p3[0] - p1[0];\n const dy3 = p3[1] - p1[1];\n\n const t = (dx3 * dy2 - dy3 * dx2) / denom;\n const u = (dx3 * dy1 - dy3 * dx1) / denom;\n\n if (t < -eps || t > 1 + eps || u < -eps || u > 1 + eps) return null;\n\n return {\n point: [p1[0] + t * dx1, p1[1] + t * dy1],\n t: Math.max(0, Math.min(1, t)),\n u: Math.max(0, Math.min(1, u)),\n };\n}\n\n/**\n * Signed area of a ring (shoelace formula).\n * Positive = counter-clockwise, negative = clockwise.\n */\nfunction signedArea(ring) {\n let area = 0;\n for (let i = 0, n = ring.length; i < n - 1; i++) {\n area += (ring[i][0] * ring[i + 1][1]) - (ring[i + 1][0] * ring[i][1]);\n }\n return area / 2;\n}\n\n/**\n * Test whether a point is inside a ring (ray-casting algorithm).\n */\nfunction pointInRing(pt, ring) {\n let inside = false;\n for (let i = 0, j = ring.length - 2; i < ring.length - 1; j = i++) {\n const xi = ring[i][0], yi = ring[i][1];\n const xj = ring[j][0], yj = ring[j][1];\n if (((yi > pt[1]) !== (yj > pt[1])) &&\n (pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi)) {\n inside = !inside;\n }\n }\n return inside;\n}\n\n/**\n * Squared distance between two points.\n */\nfunction dist2(a, b) {\n return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;\n}\n\n/**\n * Find all intersection points between a cutting line and a polygon ring.\n *\n * @param {number[][]} ring Closed ring coordinates (first === last)\n * @param {number[][]} line LineString coordinates (2+ points)\n * @returns {Array<{ point: number[], ringSegIdx: number, ringT: number, lineSegIdx: number, lineT: number }>}\n */\nfunction findIntersections(ring, line) {\n const hits = [];\n const eps = 1e-10;\n\n for (let li = 0; li < line.length - 1; li++) {\n for (let ri = 0; ri < ring.length - 1; ri++) {\n const ix = segmentIntersection(ring[ri], ring[ri + 1], line[li], line[li + 1], eps);\n if (!ix) continue;\n\n // Skip if intersection is at the very start of the ring segment\n // but was already caught as the end of the previous segment\n const pt = ix.point;\n\n // Avoid duplicate hits at shared vertices\n let isDup = false;\n for (const h of hits) {\n if (dist2(h.point, pt) < 1e-6) {\n isDup = true;\n break;\n }\n }\n if (isDup) continue;\n\n hits.push({\n point: pt,\n ringSegIdx: ri,\n ringT: ix.t,\n lineSegIdx: li,\n lineT: ix.u,\n });\n }\n }\n\n // Sort by position along the cutting line\n hits.sort((a, b) => {\n if (a.lineSegIdx !== b.lineSegIdx) return a.lineSegIdx - b.lineSegIdx;\n return a.lineT - b.lineT;\n });\n\n return hits;\n}\n\n/**\n * Insert intersection points into a ring, returning the expanded ring\n * and the new indices of the inserted points.\n *\n * @param {number[][]} ring Closed ring (first === last)\n * @param {Array<{ point: number[], ringSegIdx: number, ringT: number }>} hits\n * Sorted by ringSegIdx then ringT.\n * @returns {{ ring: number[][], indices: number[] }}\n */\nfunction insertPointsIntoRing(ring, hits) {\n // Sort hits by ring position (segment index, then parametric t) so\n // we can insert from back to front without shifting earlier indices.\n const sorted = hits.map((h, i) => ({ ...h, origOrder: i }));\n sorted.sort((a, b) => {\n if (a.ringSegIdx !== b.ringSegIdx) return a.ringSegIdx - b.ringSegIdx;\n return a.ringT - b.ringT;\n });\n\n const expanded = ring.slice(); // copy\n const indices = new Array(sorted.length);\n\n // Insert from the end so that earlier insertions don't shift later indices.\n for (let k = sorted.length - 1; k >= 0; k--) {\n const h = sorted[k];\n const insertIdx = h.ringSegIdx + 1;\n\n // Check if this point is essentially identical to an existing vertex\n const snapDist = 1e-6;\n if (dist2(h.point, expanded[h.ringSegIdx]) < snapDist) {\n indices[h.origOrder] = h.ringSegIdx;\n continue;\n }\n if (dist2(h.point, expanded[h.ringSegIdx + 1]) < snapDist) {\n indices[h.origOrder] = h.ringSegIdx + 1;\n continue;\n }\n\n // Insert the new point\n expanded.splice(insertIdx, 0, h.point);\n indices[h.origOrder] = insertIdx;\n\n // Adjust indices for all previously recorded insertions\n // that reference a position >= insertIdx\n for (let j = k + 1; j < sorted.length; j++) {\n if (indices[sorted[j].origOrder] >= insertIdx) {\n indices[sorted[j].origOrder]++;\n }\n }\n }\n\n return { ring: expanded, indices };\n}\n\n/**\n * Extract a slice of a ring from index i0 to i1 (going forward, wrapping).\n * Both endpoints are included.\n *\n * @param {number[][]} ring Closed ring (first === last); length includes closing vertex\n * @param {number} i0 Start index (inclusive)\n * @param {number} i1 End index (inclusive)\n * @returns {number[][]}\n */\nfunction ringSlice(ring, i0, i1) {\n const n = ring.length - 1; // number of unique vertices (ring is closed)\n // Normalise indices into the [0, n-1] range\n const start = ((i0 % n) + n) % n;\n const end = ((i1 % n) + n) % n;\n const result = [];\n let idx = start;\n while (true) {\n result.push(ring[idx]);\n if (idx === end) break;\n idx = (idx + 1) % n;\n }\n return result;\n}\n\n/**\n * Extract the cutting line segment between two intersection points.\n *\n * @param {number[][]} line Full cutting line coordinates\n * @param {{ point: number[], lineSegIdx: number, lineT: number }} hit0\n * @param {{ point: number[], lineSegIdx: number, lineT: number }} hit1\n * @returns {number[][]} Coordinates from hit0.point to hit1.point along the line\n */\nfunction cuttingLineSlice(line, hit0, hit1) {\n const result = [hit0.point];\n\n // Include all intermediate line vertices between the two hit segments\n const startSeg = hit0.lineSegIdx;\n const endSeg = hit1.lineSegIdx;\n\n for (let i = startSeg + 1; i <= endSeg; i++) {\n result.push(line[i]);\n }\n\n // Add the end intersection point if it's not the same as the last vertex\n if (dist2(result[result.length - 1], hit1.point) > 1e-10) {\n result.push(hit1.point);\n }\n\n return result;\n}\n\n/**\n * Ensure a ring has the desired winding order.\n * @param {number[][]} ring Closed ring\n * @param {boolean} ccw true for counter-clockwise\n * @returns {number[][]}\n */\nfunction ensureWinding(ring, ccw) {\n const area = signedArea(ring);\n if ((ccw && area < 0) || (!ccw && area > 0)) {\n return ring.slice().reverse();\n }\n return ring;\n}\n\n/**\n * Close a ring (ensure first === last).\n */\nfunction closeRing(coords) {\n if (coords.length < 2) return coords;\n const first = coords[0];\n const last = coords[coords.length - 1];\n if (dist2(first, last) > 1e-10) {\n return [...coords, first.slice()];\n }\n return coords;\n}\n\n/**\n * Extend a cutting line so that both endpoints lie outside the polygon ring.\n * If an endpoint is inside, we extend the first/last segment outward past the\n * bounding box diagonal so it definitely exits.\n *\n * @param {number[][]} line Cutting line coordinates\n * @param {number[][]} ring Closed polygon ring\n * @returns {number[][]} Extended line (may be the original if already outside)\n */\nfunction extendLineOutsideRing(line, ring) {\n // Compute bounding-box diagonal for a generous extension distance\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const pt of ring) {\n if (pt[0] < minX) minX = pt[0];\n if (pt[1] < minY) minY = pt[1];\n if (pt[0] > maxX) maxX = pt[0];\n if (pt[1] > maxY) maxY = pt[1];\n }\n const diag = Math.sqrt((maxX - minX) ** 2 + (maxY - minY) ** 2) || 1;\n\n const result = line.slice();\n\n // Extend start if inside\n if (pointInRing(result[0], ring)) {\n const p0 = result[0];\n const p1 = result[1];\n const dx = p0[0] - p1[0];\n const dy = p0[1] - p1[1];\n const len = Math.sqrt(dx * dx + dy * dy) || 1;\n const scale = diag * 2 / len;\n result[0] = [p0[0] + dx * scale, p0[1] + dy * scale];\n }\n\n // Extend end if inside\n const last = result.length - 1;\n if (pointInRing(result[last], ring)) {\n const pN = result[last];\n const pN1 = result[last - 1];\n const dx = pN[0] - pN1[0];\n const dy = pN[1] - pN1[1];\n const len = Math.sqrt(dx * dx + dy * dy) || 1;\n const scale = diag * 2 / len;\n result[last] = [pN[0] + dx * scale, pN[1] + dy * scale];\n }\n\n return result;\n}\n\n/**\n * Split a polygon by a cutting line.\n *\n * The cutting line can start or end inside the polygon — the algorithm will\n * automatically extend it outward so it crosses the boundary at exactly 2\n * points. Multi-vertex cutting lines (with corners or approximated arcs)\n * are fully supported.\n *\n * @param {number[][][]} polygonCoords Polygon coordinates:\n * [exteriorRing, ...holeRings] where each ring is closed (first === last)\n * @param {number[][]} lineCoords Cutting line coordinates (2+ points)\n * @returns {number[][][][] | null} Two polygon coordinate arrays, or null if split failed\n */\nexport function splitPolygonByLine(polygonCoords, lineCoords) {\n const exteriorRing = polygonCoords[0];\n const holes = polygonCoords.slice(1);\n\n // Extend the cutting line if its endpoints are inside the polygon\n const extendedLine = extendLineOutsideRing(lineCoords, exteriorRing);\n\n // 1. Find intersections between cutting line and exterior ring\n const hits = findIntersections(exteriorRing, extendedLine);\n\n // We need exactly 2 intersection points for a simple split\n if (hits.length !== 2) {\n console.warn(`[polygonSplit] Expected 2 intersections, got ${hits.length}`);\n return null;\n }\n\n const [hit0, hit1] = hits;\n\n // 2. Insert intersection points into the ring\n const { ring: expandedRing, indices } = insertPointsIntoRing(exteriorRing, hits);\n const idx0 = indices[0];\n const idx1 = indices[1];\n\n // Ensure idx0 < idx1 for consistent traversal\n const [iA, iB] = idx0 < idx1 ? [idx0, idx1] : [idx1, idx0];\n const [hitA, hitB] = idx0 < idx1 ? [hit0, hit1] : [hit1, hit0];\n\n // 3. Get the cutting line segment between the two intersection points\n const cutForward = idx0 < idx1\n ? cuttingLineSlice(extendedLine, hit0, hit1)\n : cuttingLineSlice(extendedLine, hit1, hit0);\n const cutReverse = cutForward.slice().reverse();\n\n // 4. Build two polygon rings\n // Ring A: walk ring from iA to iB (forward), then cutting line reversed back to iA\n const sliceAB = ringSlice(expandedRing, iA, iB);\n const ringA = closeRing([...sliceAB, ...cutReverse.slice(1)]);\n\n // Ring B: walk ring from iB to iA (wrapping), then cutting line forward back to iB\n const sliceBA = ringSlice(expandedRing, iB, iA);\n const ringB = closeRing([...sliceBA, ...cutForward.slice(1)]);\n\n // 5. Match winding order to original\n const originalCCW = signedArea(exteriorRing) > 0;\n const finalA = ensureWinding(ringA, originalCCW);\n const finalB = ensureWinding(ringB, originalCCW);\n\n // 6. Build polygon coordinate arrays, assigning holes to the correct piece\n const polyA = [finalA];\n const polyB = [finalB];\n\n for (const hole of holes) {\n // Use the centroid of the hole to determine containment\n const centroid = holeCentroid(hole);\n if (pointInRing(centroid, finalA)) {\n polyA.push(hole);\n } else {\n polyB.push(hole);\n }\n }\n\n return [polyA, polyB];\n}\n\n/**\n * Compute the centroid of a closed ring.\n */\nfunction holeCentroid(ring) {\n let cx = 0, cy = 0;\n const n = ring.length - 1; // exclude closing vertex\n for (let i = 0; i < n; i++) {\n cx += ring[i][0];\n cy += ring[i][1];\n }\n return [cx / n, cy / n];\n}\n","/**\n * Lightweight toast notification system.\n *\n * Usage:\n * import { showToast } from '../toast.js';\n *\n * showToast('Something went wrong', 'error');\n * showToast('Merge successful!', 'success');\n * showToast('Select two adjacent polygons', 'info');\n */\n\n// ── Palette ──────────────────────────────────────────────────────────────────\n\nconst THEMES = {\n success: { bg: '#10b981', icon: '\\u2705' }, // green\n error: { bg: '#ef4444', icon: '\\u274c' }, // red\n warning: { bg: '#f59e0b', icon: '\\u26a0\\ufe0f' }, // amber\n info: { bg: '#0ea5e9', icon: '\\u2139\\ufe0f' }, // cyan\n};\n\n// ── Container (created once, appended to ) ────────────────────────────\n\nlet container = null;\n\nfunction ensureContainer() {\n if (container) return container;\n container = document.createElement('div');\n container.style.cssText = `\n position: fixed;\n top: 16px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 10000;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n pointer-events: none;\n `;\n document.body.appendChild(container);\n return container;\n}\n\n// ── Public API ──────────────────────────────────────────────────────────────\n\n/**\n * Display a toast notification.\n *\n * @param {string} message Plain-text message to show.\n * @param {'success'|'error'|'warning'|'info'} [type='info']\n * @param {number} [duration=4000] Auto-dismiss time in ms.\n */\nexport function showToast(message, type = 'info', duration = 4000) {\n const parent = ensureContainer();\n const theme = THEMES[type] || THEMES.info;\n\n const el = document.createElement('div');\n el.style.cssText = `\n background: ${theme.bg};\n color: #fff;\n padding: 10px 18px;\n border-radius: 8px;\n font-family: var(--font-body, 'Exo', sans-serif);\n font-size: 13px;\n font-weight: 600;\n box-shadow: 0 4px 12px rgba(0,0,0,0.25);\n pointer-events: auto;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.25s ease, transform 0.25s ease;\n transform: translateY(-8px);\n max-width: 420px;\n text-align: center;\n line-height: 1.4;\n `;\n el.textContent = `${theme.icon} ${message}`;\n\n parent.appendChild(el);\n\n // Animate in\n requestAnimationFrame(() => {\n el.style.opacity = '1';\n el.style.transform = 'translateY(0)';\n });\n\n // Dismiss helper\n const dismiss = () => {\n el.style.opacity = '0';\n el.style.transform = 'translateY(-8px)';\n setTimeout(() => el.remove(), 300);\n };\n\n // Click to dismiss early\n el.addEventListener('click', dismiss);\n\n // Auto-dismiss\n setTimeout(dismiss, duration);\n}\n","/**\n * PolygonSplitInteraction\n *\n * A two-phase OpenLayers interaction for splitting polygons:\n * Phase 1 – SELECT: hover to highlight, click to select a polygon\n * Phase 2 – DRAW: draw a cutting line, double-click to finish\n *\n * After a successful split the original feature is removed and two new\n * coloured features are added. The interaction fires `beforesplit` and\n * `aftersplit` events compatible with ol-ext's UndoRedo.\n */\n\nimport ol_interaction_Interaction from 'ol/interaction/Interaction';\nimport ol_interaction_Draw from 'ol/interaction/Draw';\nimport VectorSource from 'ol/source/Vector';\nimport VectorLayer from 'ol/layer/Vector';\nimport Feature from 'ol/Feature';\nimport { Style, Stroke, Fill, Circle as CircleStyle } from 'ol/style';\nimport { LineString } from 'ol/geom';\nimport { Polygon as PolygonGeom } from 'ol/geom';\nimport { splitPolygonByLine } from '../geom/polygonSplit.js';\nimport { showToast } from '../toast.js';\n\n// Marker colours for the two split pieces\nconst SPLIT_COLORS = [\n { stroke: '#ef4444', fill: 'rgba(239,68,68,0.25)' }, // red\n { stroke: '#3b82f6', fill: 'rgba(59,130,246,0.25)' }, // blue\n];\n\n// Highlight style for the selected polygon (phase 1)\nconst HIGHLIGHT_STYLE = new Style({\n stroke: new Stroke({ color: '#0ea5e9', width: 3 }),\n fill: new Fill({ color: 'rgba(14,165,233,0.15)' }),\n});\n\n// Style for the cutting-line sketch (phase 2)\nconst SKETCH_STYLE = new Style({\n stroke: new Stroke({ color: '#f43f5e', width: 2, lineDash: [8, 6] }),\n image: new CircleStyle({\n radius: 5,\n fill: new Fill({ color: '#f43f5e' }),\n stroke: new Stroke({ color: '#fff', width: 1.5 }),\n }),\n});\n\nexport class PolygonSplitInteraction extends ol_interaction_Interaction {\n /**\n * @param {Object} options\n * @param {VectorSource|VectorSource[]} [options.sources] Sources containing\n * polygons to split. If omitted the interaction searches all visible\n * vector layers on the map.\n * @param {number} [options.snapDistance=25] Pixel distance for hover highlight.\n */\n constructor(options = {}) {\n super({\n handleEvent: (e) => this._handleEvent(e),\n });\n\n this.snapDistance_ = options.snapDistance || 25;\n this._sources = options.sources\n ? (Array.isArray(options.sources) ? options.sources : [options.sources])\n : null;\n\n // Phase: 'select' | 'draw' | 'pick'\n this._phase = 'select';\n this._selectedFeature = null;\n this._selectedSource = null;\n this._drawInteraction = null;\n this._splitFeatures = null; // the two pieces (for pick phase)\n\n // Overlay layer for highlighting the polygon under the cursor / selected\n this._overlaySource = new VectorSource({ useSpatialIndex: false });\n this._overlayLayer = new VectorLayer({\n source: this._overlaySource,\n displayInLayerSwitcher: false,\n style: HIGHLIGHT_STYLE,\n });\n }\n\n /* ------------------------------------------------------------------ */\n /* Map lifecycle */\n /* ------------------------------------------------------------------ */\n\n setMap(map) {\n if (this.getMap()) {\n this.getMap().removeLayer(this._overlayLayer);\n this._removeDrawInteraction();\n }\n super.setMap(map);\n if (map) {\n this._overlayLayer.setMap(map);\n }\n }\n\n setActive(active) {\n super.setActive(active);\n if (!active) {\n this._reset();\n }\n }\n\n /* ------------------------------------------------------------------ */\n /* Source helpers */\n /* ------------------------------------------------------------------ */\n\n _getSources() {\n if (this._sources) return this._sources;\n if (!this.getMap()) return [];\n const sources = [];\n const collect = (layers) => {\n layers.forEach((layer) => {\n if (layer.getVisible()) {\n if (layer.getSource && layer.getSource() instanceof VectorSource) {\n sources.push(layer.getSource());\n } else if (layer.getLayers) {\n collect(layer.getLayers());\n }\n }\n });\n };\n collect(this.getMap().getLayers());\n return sources;\n }\n\n /* ------------------------------------------------------------------ */\n /* Event router */\n /* ------------------------------------------------------------------ */\n\n _handleEvent(e) {\n if (!this.getActive()) return true;\n\n if (this._phase === 'select') {\n if (e.type === 'pointermove') return this._onSelectMove(e);\n if (e.type === 'singleclick') return this._onSelectClick(e);\n }\n // In 'draw' phase the Draw interaction handles events directly;\n // we only intercept Escape to cancel.\n if (this._phase === 'draw') {\n if (e.type === 'keydown' && e.originalEvent?.key === 'Escape') {\n this._cancelDraw();\n return false;\n }\n }\n\n // In 'pick' phase the user selects which split piece keeps the UPN\n if (this._phase === 'pick') {\n if (e.type === 'pointermove') return this._onPickMove(e);\n if (e.type === 'singleclick') return this._onPickClick(e);\n if (e.type === 'keydown' && e.originalEvent?.key === 'Escape') {\n this._reset();\n return false;\n }\n }\n\n return true;\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 1: SELECT */\n /* ------------------------------------------------------------------ */\n\n _onSelectMove(e) {\n const map = this.getMap();\n if (!map) return true;\n\n this._overlaySource.clear();\n\n const hit = this._closestPolygon(e);\n if (hit) {\n // Show highlight copy\n const clone = hit.feature.clone();\n this._overlaySource.addFeature(clone);\n map.getTargetElement().style.cursor = 'pointer';\n } else {\n map.getTargetElement().style.cursor = '';\n }\n return true;\n }\n\n _onSelectClick(e) {\n const hit = this._closestPolygon(e);\n if (!hit) return true;\n\n this._selectedFeature = hit.feature;\n this._selectedSource = hit.source;\n\n // Keep highlight visible during draw phase\n this._overlaySource.clear();\n const clone = hit.feature.clone();\n this._overlaySource.addFeature(clone);\n\n this._startDrawPhase();\n return false; // consume the click\n }\n\n /**\n * Find the closest polygon feature within snap distance.\n */\n _closestPolygon(e) {\n let best = null;\n let bestDist = this.snapDistance_ + 1;\n\n for (const source of this._getSources()) {\n const feat = source.getClosestFeatureToCoordinate(e.coordinate);\n if (!feat) continue;\n const geom = feat.getGeometry();\n if (!geom) continue;\n const type = geom.getType();\n if (type !== 'Polygon' && type !== 'MultiPolygon') continue;\n\n const closest = geom.getClosestPoint(e.coordinate);\n const line = new LineString([e.coordinate, closest]);\n const distPx = line.getLength() / e.frameState.viewState.resolution;\n\n if (distPx < bestDist) {\n bestDist = distPx;\n best = { feature: feat, source, coord: closest };\n }\n }\n return best;\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 2: DRAW cutting line */\n /* ------------------------------------------------------------------ */\n\n _startDrawPhase() {\n this._phase = 'draw';\n const map = this.getMap();\n if (!map) return;\n\n map.getTargetElement().style.cursor = 'crosshair';\n\n this._drawInteraction = new ol_interaction_Draw({\n type: 'LineString',\n style: SKETCH_STYLE,\n });\n\n this._drawInteraction.on('drawend', (evt) => {\n const cuttingLine = evt.feature.getGeometry().getCoordinates();\n this._performSplit(cuttingLine);\n });\n\n map.addInteraction(this._drawInteraction);\n }\n\n _removeDrawInteraction() {\n if (this._drawInteraction && this.getMap()) {\n this.getMap().removeInteraction(this._drawInteraction);\n }\n this._drawInteraction = null;\n }\n\n _cancelDraw() {\n this._removeDrawInteraction();\n this._reset();\n }\n\n /* ------------------------------------------------------------------ */\n /* Split logic */\n /* ------------------------------------------------------------------ */\n\n _performSplit(cuttingLineCoords) {\n const feature = this._selectedFeature;\n const source = this._selectedSource;\n const geom = feature.getGeometry();\n\n let polygonCoords;\n if (geom.getType() === 'Polygon') {\n polygonCoords = geom.getCoordinates();\n } else if (geom.getType() === 'MultiPolygon') {\n // For MultiPolygon, try to split each sub-polygon and use the\n // first one that produces a valid result.\n // For now, use the first polygon ring.\n polygonCoords = geom.getCoordinates()[0];\n }\n\n const result = splitPolygonByLine(polygonCoords, cuttingLineCoords);\n\n if (!result) {\n console.warn('[PolygonSplit] Split failed — line must cross the polygon boundary at exactly 2 points.');\n // Stay in draw phase so user can retry\n this._removeDrawInteraction();\n this._startDrawPhase();\n return;\n }\n\n const [coordsA, coordsB] = result;\n\n // Create two new features from the split result\n const featureA = feature.clone();\n featureA.setGeometry(new PolygonGeom(coordsA));\n featureA.setStyle(new Style({\n stroke: new Stroke({ color: SPLIT_COLORS[0].stroke, width: 2.5 }),\n fill: new Fill({ color: SPLIT_COLORS[0].fill }),\n }));\n\n const featureB = feature.clone();\n featureB.setGeometry(new PolygonGeom(coordsB));\n featureB.setStyle(new Style({\n stroke: new Stroke({ color: SPLIT_COLORS[1].stroke, width: 2.5 }),\n fill: new Fill({ color: SPLIT_COLORS[1].fill }),\n }));\n\n // Dispatch beforesplit (compatible with ol-ext UndoRedo)\n const splitFeatures = [featureA, featureB];\n this.dispatchEvent({\n type: 'beforesplit',\n original: feature,\n features: splitFeatures,\n });\n source.dispatchEvent({\n type: 'beforesplit',\n original: feature,\n features: splitFeatures,\n });\n\n // Replace the original feature\n source.removeFeature(feature);\n source.addFeature(featureA);\n source.addFeature(featureB);\n\n // Dispatch aftersplit\n this.dispatchEvent({\n type: 'aftersplit',\n original: feature,\n features: splitFeatures,\n });\n source.dispatchEvent({\n type: 'aftersplit',\n original: feature,\n features: splitFeatures,\n });\n\n // Clean up draw interaction\n this._removeDrawInteraction();\n\n // If the original was a parcel, enter pick phase for UPN assignment\n const isParcel = feature.get('_layerType') === 'parcel';\n if (isParcel) {\n this._splitFeatures = splitFeatures;\n this._phase = 'pick';\n this._overlaySource.clear();\n const map = this.getMap();\n if (map) map.getTargetElement().style.cursor = '';\n showToast('Click the polygon that should keep the original identifier.', 'info', 5000);\n\n this.dispatchEvent({\n type: 'splitparcel',\n features: splitFeatures,\n originalProps: feature.getProperties(),\n source,\n });\n } else {\n this._reset();\n }\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 3: PICK — select which split piece keeps the UPN */\n /* ------------------------------------------------------------------ */\n\n _onPickMove(e) {\n const map = this.getMap();\n if (!map) return true;\n\n this._overlaySource.clear();\n\n const hit = this._closestSplitPiece(e);\n if (hit) {\n const clone = hit.clone();\n this._overlaySource.addFeature(clone);\n map.getTargetElement().style.cursor = 'pointer';\n } else {\n map.getTargetElement().style.cursor = '';\n }\n return true;\n }\n\n _onPickClick(e) {\n const hit = this._closestSplitPiece(e);\n if (!hit) return true;\n\n this.dispatchEvent({\n type: 'splitpick',\n picked: hit,\n features: this._splitFeatures,\n });\n\n this._reset();\n return false;\n }\n\n /**\n * Find the closest split piece to the cursor.\n */\n _closestSplitPiece(e) {\n if (!this._splitFeatures) return null;\n let best = null;\n let bestDist = this.snapDistance_ + 1;\n\n for (const feat of this._splitFeatures) {\n const geom = feat.getGeometry();\n if (!geom) continue;\n const closest = geom.getClosestPoint(e.coordinate);\n const line = new LineString([e.coordinate, closest]);\n const distPx = line.getLength() / e.frameState.viewState.resolution;\n if (distPx < bestDist) {\n bestDist = distPx;\n best = feat;\n }\n }\n return best;\n }\n\n /* ------------------------------------------------------------------ */\n /* Reset */\n /* ------------------------------------------------------------------ */\n\n _reset() {\n this._phase = 'select';\n this._selectedFeature = null;\n this._selectedSource = null;\n this._splitFeatures = null;\n this._overlaySource.clear();\n this._removeDrawInteraction();\n\n const map = this.getMap();\n if (map) {\n map.getTargetElement().style.cursor = '';\n }\n }\n}\n","/**\n * Pure geometry functions for merging two adjacent polygons.\n *\n * No OpenLayers dependency — operates on raw coordinate arrays.\n */\n\n/**\n * Squared distance between two points.\n */\nfunction dist2(a, b) {\n return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;\n}\n\n/**\n * Signed area of a ring (shoelace formula).\n * Positive = counter-clockwise, negative = clockwise.\n */\nfunction signedArea(ring) {\n let area = 0;\n for (let i = 0, n = ring.length; i < n - 1; i++) {\n area += (ring[i][0] * ring[i + 1][1]) - (ring[i + 1][0] * ring[i][1]);\n }\n return area / 2;\n}\n\n/**\n * Test whether a point is inside a ring (ray-casting algorithm).\n */\nfunction pointInRing(pt, ring) {\n let inside = false;\n for (let i = 0, j = ring.length - 2; i < ring.length - 1; j = i++) {\n const xi = ring[i][0], yi = ring[i][1];\n const xj = ring[j][0], yj = ring[j][1];\n if (((yi > pt[1]) !== (yj > pt[1])) &&\n (pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi)) {\n inside = !inside;\n }\n }\n return inside;\n}\n\n/**\n * Ensure a ring has the desired winding order.\n */\nfunction ensureWinding(ring, ccw) {\n const area = signedArea(ring);\n if ((ccw && area < 0) || (!ccw && area > 0)) {\n return ring.slice().reverse();\n }\n return ring;\n}\n\n/**\n * Close a ring (ensure first === last).\n */\nfunction closeRing(coords) {\n if (coords.length < 2) return coords;\n if (dist2(coords[0], coords[coords.length - 1]) > 1e-10) {\n return [...coords, coords[0].slice()];\n }\n return coords;\n}\n\n/**\n * Perpendicular distance from a point to a segment.\n *\n * @param {number[]} pt\n * @param {number[]} segA Segment start\n * @param {number[]} segB Segment end\n * @returns {number} Squared distance\n */\nfunction distToSegmentSq(pt, segA, segB) {\n const dx = segB[0] - segA[0];\n const dy = segB[1] - segA[1];\n const lenSq = dx * dx + dy * dy;\n\n if (lenSq < 1e-20) return dist2(pt, segA); // degenerate segment\n\n // Parametric position of the projection\n let t = ((pt[0] - segA[0]) * dx + (pt[1] - segA[1]) * dy) / lenSq;\n t = Math.max(0, Math.min(1, t));\n\n const projX = segA[0] + t * dx;\n const projY = segA[1] + t * dy;\n return (pt[0] - projX) ** 2 + (pt[1] - projY) ** 2;\n}\n\n/**\n * Find the ring edge closest to a click coordinate.\n *\n * @param {number[][]} ring Closed ring\n * @param {number[]} clickCoord [x, y]\n * @returns {{ segIdx: number, distSq: number }}\n */\nfunction findClosestEdge(ring, clickCoord) {\n let bestIdx = 0;\n let bestDist = Infinity;\n const n = ring.length - 1; // unique vertices\n\n for (let i = 0; i < n; i++) {\n const d = distToSegmentSq(clickCoord, ring[i], ring[(i + 1) % n === 0 ? n : i + 1]);\n if (d < bestDist) {\n bestDist = d;\n bestIdx = i;\n }\n }\n return { segIdx: bestIdx, distSq: bestDist };\n}\n\n/**\n * Check if two coordinates are equal within tolerance.\n */\nfunction coordsEqual(a, b, tolSq) {\n return dist2(a, b) < tolSq;\n}\n\n/**\n * Test whether a point lies within tolerance of any edge of a ring.\n *\n * Unlike coordsEqual (vertex-to-vertex), this checks whether the point is\n * close to the ring's *boundary* — it projects onto segments, so it works\n * even when the two polygons have different vertex density along the shared\n * edge or when vertices are slightly offset from separate digitisation.\n *\n * @param {number[]} pt Point to test\n * @param {number[][]} ring Closed ring\n * @param {number} tolSq Squared distance tolerance\n * @returns {boolean}\n */\nfunction isVertexNearRing(pt, ring, tolSq) {\n const n = ring.length - 1;\n for (let i = 0; i < n; i++) {\n if (distToSegmentSq(pt, ring[i], ring[i + 1]) < tolSq) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Find the shared boundary between two polygon rings.\n *\n * Adjacent polygons share edges in reverse winding direction:\n * if A walks P→Q along the shared boundary, B walks Q→P.\n *\n * The algorithm has two stages:\n *\n * 1. **Seed validation** — uses `isVertexNearRing` (vertex-to-edge proximity)\n * to confirm the user-clicked edges actually lie on a common boundary.\n * This is the forgiving check that handles offset vertices from separate\n * digitisation.\n *\n * 2. **Lockstep extension** — walks both rings together (A forward, B in\n * the opposite direction) and extends the shared boundary one vertex at\n * a time. Three cases are tried at each step:\n * a) Both rings advance: vertex-to-vertex match (classic case).\n * b) Only A advances: A has an extra vertex that projects onto B's\n * frontier edge (different vertex density).\n * c) Only B advances: vice-versa.\n * Because extension is coupled to the *frontier edge* of the other ring\n * (not the entire ring), it cannot overshoot into non-shared territory,\n * even for small or closely-spaced polygons.\n *\n * @param {number[][]} ringA Closed ring\n * @param {number[][]} ringB Closed ring\n * @param {number} seedIdxA Seed edge index on ring A\n * @param {number} seedIdxB Seed edge index on ring B\n * @param {number} tolerance Distance tolerance (in map units)\n * @returns {{ startA: number, endA: number, startB: number, endB: number, reversed: boolean } | null}\n */\nfunction findSharedBoundary(ringA, ringB, seedIdxA, seedIdxB, tolerance) {\n const nA = ringA.length - 1; // unique vertices\n const nB = ringB.length - 1;\n const tolSq = tolerance * tolerance;\n\n // ── Validate seed edges ─────────────────────────────────────────────\n // Both vertices of the seed edge on A must be near ring B's boundary,\n // or both vertices of the seed edge on B must be near ring A's boundary.\n // This uses vertex-to-edge proximity so it handles offset digitisation.\n const a0 = ringA[seedIdxA];\n const a1 = ringA[(seedIdxA + 1) % nA];\n const b0 = ringB[seedIdxB];\n const b1 = ringB[(seedIdxB + 1) % nB];\n\n const a0NearB = isVertexNearRing(a0, ringB, tolSq);\n const a1NearB = isVertexNearRing(a1, ringB, tolSq);\n const b0NearA = isVertexNearRing(b0, ringA, tolSq);\n const b1NearA = isVertexNearRing(b1, ringA, tolSq);\n\n if (!(a0NearB && a1NearB) && !(b0NearA && b1NearA)) {\n console.warn('[polygonMerge] Seed edges are not on the shared boundary');\n return null;\n }\n\n // ── Determine winding direction ─────────────────────────────────────\n // Reversed (the normal case): A's a0 ≈ B's b1 and A's a1 ≈ B's b0.\n let reversed;\n if (coordsEqual(a0, b1, tolSq) && coordsEqual(a1, b0, tolSq)) {\n reversed = true;\n } else if (coordsEqual(a0, b0, tolSq) && coordsEqual(a1, b1, tolSq)) {\n reversed = false;\n } else {\n // Vertices don't match exactly — use proximity to decide direction\n reversed = dist2(a0, b1) < dist2(a0, b0);\n }\n\n // ── Initialise shared boundary ──────────────────────────────────────\n let startA = seedIdxA;\n let endA = (seedIdxA + 1) % nA;\n let startB, endB;\n\n if (reversed) {\n // A walks startA → endA, B walks startB ← endB (reversed ring order)\n startB = (seedIdxB + 1) % nB;\n endB = seedIdxB;\n } else {\n startB = seedIdxB;\n endB = (seedIdxB + 1) % nB;\n }\n\n // ── Extend forward (endA++, endB-- if reversed) ─────────────────────\n // Walk both rings in lockstep. At each step try three strategies:\n // 1. Both advance — vertices match (vertex-to-vertex).\n // 2. Only A advances — A's next vertex projects onto B's frontier edge.\n // 3. Only B advances — B's next vertex projects onto A's frontier edge.\n let safety = nA + nB;\n while (safety-- > 0) {\n const nextA = (endA + 1) % nA;\n const nextB = reversed ? (endB - 1 + nB) % nB : (endB + 1) % nB;\n if (nextA === startA || nextB === startB) break; // wrapped around\n\n // Case 1: vertex-to-vertex match\n if (coordsEqual(ringA[nextA], ringB[nextB], tolSq)) {\n endA = nextA;\n endB = nextB;\n continue;\n }\n\n // Case 2: A has extra vertex — project onto B's frontier edge\n if (distToSegmentSq(ringA[nextA], ringB[endB], ringB[nextB]) < tolSq) {\n endA = nextA;\n continue;\n }\n\n // Case 3: B has extra vertex — project onto A's frontier edge\n if (distToSegmentSq(ringB[nextB], ringA[endA], ringA[nextA]) < tolSq) {\n endB = nextB;\n continue;\n }\n\n break; // no match — end of shared boundary\n }\n\n // ── Extend backward (startA--, startB++ if reversed) ────────────────\n safety = nA + nB;\n while (safety-- > 0) {\n const prevA = (startA - 1 + nA) % nA;\n const prevB = reversed ? (startB + 1) % nB : (startB - 1 + nB) % nB;\n if (prevA === endA || prevB === endB) break;\n\n // Case 1: vertex-to-vertex match\n if (coordsEqual(ringA[prevA], ringB[prevB], tolSq)) {\n startA = prevA;\n startB = prevB;\n continue;\n }\n\n // Case 2: A has extra vertex — project onto B's frontier edge\n if (distToSegmentSq(ringA[prevA], ringB[startB], ringB[prevB]) < tolSq) {\n startA = prevA;\n continue;\n }\n\n // Case 3: B has extra vertex — project onto A's frontier edge\n if (distToSegmentSq(ringB[prevB], ringA[startA], ringA[prevA]) < tolSq) {\n startB = prevB;\n continue;\n }\n\n break;\n }\n\n return { startA, endA, startB, endB, reversed };\n}\n\n/**\n * Walk a ring from startIdx to endIdx (exclusive), going forward and wrapping.\n * Skips startIdx and stops before reaching endIdx.\n * Returns the vertices of the non-shared portion.\n *\n * @param {number[][]} ring Closed ring\n * @param {number} fromIdx Start walking from this index (inclusive)\n * @param {number} toIdx Stop at this index (inclusive)\n * @returns {number[][]}\n */\nfunction walkRing(ring, fromIdx, toIdx) {\n const n = ring.length - 1;\n const result = [];\n let idx = fromIdx;\n while (true) {\n result.push(ring[idx]);\n if (idx === toIdx) break;\n idx = (idx + 1) % n;\n // Safety: prevent infinite loops\n if (result.length > n + 1) break;\n }\n return result;\n}\n\n/**\n * Merge two adjacent polygons along their shared boundary.\n *\n * @param {number[][][]} polygonCoordsA Polygon A coordinates [exteriorRing, ...holes]\n * @param {number[][][]} polygonCoordsB Polygon B coordinates [exteriorRing, ...holes]\n * @param {number[]} clickCoordA Click coordinate on the shared edge of polygon A\n * @param {number[]} clickCoordB Click coordinate on the shared edge of polygon B\n * @param {number} [tolerance=5] Distance tolerance in map units (default 5 metres in EPSG:3857).\n * A larger tolerance handles polygons that were digitised separately and\n * whose shared vertices don't coincide exactly.\n * @returns {{ coords: number[][][], error?: undefined } | { coords: null, error: string }}\n * On success: `{ coords: [...] }`. On failure: `{ coords: null, error: 'reason' }`.\n */\nexport function mergePolygons(polygonCoordsA, polygonCoordsB, clickCoordA, clickCoordB, tolerance = 5) {\n const ringA = polygonCoordsA[0];\n const ringB = polygonCoordsB[0];\n const holesA = polygonCoordsA.slice(1);\n const holesB = polygonCoordsB.slice(1);\n\n // 1. Find seed edges (closest to user clicks)\n const seedA = findClosestEdge(ringA, clickCoordA);\n const seedB = findClosestEdge(ringB, clickCoordB);\n\n // 2. Find shared boundary\n const shared = findSharedBoundary(ringA, ringB, seedA.segIdx, seedB.segIdx, tolerance);\n if (!shared) {\n console.warn('[polygonMerge] Could not find shared boundary between polygons — seed edges are not near the other ring');\n return { coords: null, error: 'The selected edges are not on a shared boundary. Click edges that lie on the common border between the two polygons.' };\n }\n\n const { startA, endA, startB, endB, reversed } = shared;\n const nA = ringA.length - 1;\n const nB = ringB.length - 1;\n\n // 3. Stitch the non-shared portions together\n // A's shared goes from startA → endA. Non-shared: endA → startA (forward, wrapping).\n // B's shared (reversed case) goes from startB backward to endB.\n // Non-shared: startB → endB (forward).\n // B's shared (same-dir case) goes from startB → endB.\n // Non-shared: endB → startB (forward, wrapping).\n //\n // The last vertex of partA (startA) must coincide with the first vertex\n // of partB for a clean join.\n\n const partA = walkRing(ringA, endA, startA);\n\n let partB;\n if (reversed) {\n // B's non-shared goes from startB forward to endB.\n // startB vertex ≈ startA vertex (they meet at one end of the shared boundary).\n partB = walkRing(ringB, startB, endB);\n } else {\n // B's non-shared goes from endB forward to startB.\n partB = walkRing(ringB, endB, startB);\n }\n\n // partA ends at startA, partB starts at a vertex that should coincide.\n // Skip the first vertex of partB to avoid the duplicate junction point.\n const merged = [...partA, ...partB.slice(1)];\n\n // Snap the closing junction. With non-coincident vertices (separate\n // digitisation) the last vertex of partB may be a few metres from the\n // first vertex of partA (ringA[endA]). Replace it to avoid a tiny\n // sliver edge that closeRing would otherwise create.\n const tolSq = tolerance * tolerance;\n if (merged.length > 2 && dist2(merged[merged.length - 1], merged[0]) < tolSq) {\n merged[merged.length - 1] = merged[0].slice();\n }\n\n const mergedRing = closeRing(merged);\n\n // 4. Validate: the merged ring should have a reasonable area\n const areaA = Math.abs(signedArea(ringA));\n const areaB = Math.abs(signedArea(ringB));\n const areaMerged = Math.abs(signedArea(mergedRing));\n const expectedArea = areaA + areaB;\n\n // Allow 10% tolerance for area mismatch (shared edges can cause slight differences)\n if (areaMerged < expectedArea * 0.5 || areaMerged > expectedArea * 1.5) {\n console.warn(`[polygonMerge] Area mismatch: A=${areaA.toFixed(1)}, B=${areaB.toFixed(1)}, merged=${areaMerged.toFixed(1)}, expected≈${expectedArea.toFixed(1)}`);\n return { coords: null, error: 'Merge produced an invalid polygon (area mismatch). The polygons may not be truly adjacent — try clicking closer to the shared boundary.' };\n }\n\n // 5. Match winding order to original\n const originalCCW = signedArea(ringA) > 0;\n const finalRing = ensureWinding(mergedRing, originalCCW);\n\n // 6. Collect holes from both polygons\n const allHoles = [...holesA, ...holesB];\n // Filter: only include holes that actually fall inside the merged ring\n const validHoles = allHoles.filter(hole => {\n const cx = hole.reduce((s, p) => s + p[0], 0) / (hole.length - 1);\n const cy = hole.reduce((s, p) => s + p[1], 0) / (hole.length - 1);\n return pointInRing([cx, cy], finalRing);\n });\n\n return { coords: [finalRing, ...validHoles] };\n}\n","/**\n * PolygonMergeInteraction\n *\n * A four-phase OpenLayers interaction for merging two adjacent polygons:\n * Phase 1 – SELECT_A: hover to highlight, click to select polygon A\n * Phase 2 – SELECT_B: hover to highlight, click to select polygon B\n * Phase 3 – CLICK_EDGE_A: hover highlights edge, click to pick shared edge on A\n * Phase 4 – CLICK_EDGE_B: hover highlights edge, click to pick shared edge on B → merge\n *\n * After a successful merge the two original features are removed and one\n * merged feature (coloured green) is added. If both originals were parcels,\n * a `mergedparcel` event is fired so external code can present a UPN chooser.\n */\n\nimport ol_interaction_Interaction from 'ol/interaction/Interaction';\nimport VectorSource from 'ol/source/Vector';\nimport VectorLayer from 'ol/layer/Vector';\nimport Feature from 'ol/Feature';\nimport { Style, Stroke, Fill, Text } from 'ol/style';\nimport { LineString, Polygon as PolygonGeom } from 'ol/geom';\nimport { mergePolygons } from '../geom/polygonMerge.js';\nimport { showToast } from '../toast.js';\n\n// ── Styles ───────────────────────────────────────────────────────────────────\n\nconst HIGHLIGHT_A = new Style({\n stroke: new Stroke({ color: '#0ea5e9', width: 3 }),\n fill: new Fill({ color: 'rgba(14,165,233,0.15)' }),\n});\n\nconst HIGHLIGHT_B = new Style({\n stroke: new Stroke({ color: '#f59e0b', width: 3 }),\n fill: new Fill({ color: 'rgba(245,158,11,0.15)' }),\n});\n\n// Labelled versions for permanent highlights (shown after selection)\nconst LABEL_A = new Style({\n stroke: new Stroke({ color: '#0ea5e9', width: 3 }),\n fill: new Fill({ color: 'rgba(14,165,233,0.15)' }),\n text: new Text({\n text: 'A',\n font: 'bold 22px Exo, sans-serif',\n fill: new Fill({ color: '#0ea5e9' }),\n stroke: new Stroke({ color: '#fff', width: 4 }),\n overflow: true,\n }),\n});\n\nconst LABEL_B = new Style({\n stroke: new Stroke({ color: '#f59e0b', width: 3 }),\n fill: new Fill({ color: 'rgba(245,158,11,0.15)' }),\n text: new Text({\n text: 'B',\n font: 'bold 22px Exo, sans-serif',\n fill: new Fill({ color: '#f59e0b' }),\n stroke: new Stroke({ color: '#fff', width: 4 }),\n overflow: true,\n }),\n});\n\nconst EDGE_STYLE = new Style({\n stroke: new Stroke({ color: '#ec4899', width: 4, lineDash: [10, 6] }),\n});\n\nconst MERGE_STYLE = new Style({\n stroke: new Stroke({ color: '#10b981', width: 2.5 }),\n fill: new Fill({ color: 'rgba(16,185,129,0.3)' }),\n});\n\n// ── Interaction ──────────────────────────────────────────────────────────────\n\nexport class PolygonMergeInteraction extends ol_interaction_Interaction {\n /**\n * @param {Object} [options]\n * @param {number} [options.snapDistance=25] Pixel distance for hover detection.\n * @param {number} [options.tolerance=5] Map-unit tolerance for shared-edge matching.\n */\n constructor(options = {}) {\n super({\n handleEvent: (e) => this._handleEvent(e),\n });\n\n this.snapDistance_ = options.snapDistance || 25;\n this.tolerance_ = options.tolerance || 5;\n\n // Phase: 'select_a' | 'select_b' | 'click_edge_a' | 'click_edge_b'\n this._phase = 'select_a';\n\n // Selected features and their sources\n this._featureA = null;\n this._sourceA = null;\n this._featureB = null;\n this._sourceB = null;\n\n // Clicked edge coordinates (map coords)\n this._edgeClickA = null;\n this._edgeClickB = null;\n\n // Overlay for polygon highlights\n this._highlightSource = new VectorSource({ useSpatialIndex: false });\n this._highlightLayer = new VectorLayer({\n source: this._highlightSource,\n displayInLayerSwitcher: false,\n style: (f) => f.get('_highlightStyle') || HIGHLIGHT_A,\n });\n\n // Overlay for edge highlights\n this._edgeSource = new VectorSource({ useSpatialIndex: false });\n this._edgeLayer = new VectorLayer({\n source: this._edgeSource,\n displayInLayerSwitcher: false,\n style: EDGE_STYLE,\n });\n }\n\n /* ------------------------------------------------------------------ */\n /* Map lifecycle */\n /* ------------------------------------------------------------------ */\n\n setMap(map) {\n if (this.getMap()) {\n this.getMap().removeLayer(this._highlightLayer);\n this.getMap().removeLayer(this._edgeLayer);\n }\n super.setMap(map);\n if (map) {\n this._highlightLayer.setMap(map);\n this._edgeLayer.setMap(map);\n }\n }\n\n setActive(active) {\n super.setActive(active);\n if (!active) this._reset();\n }\n\n /* ------------------------------------------------------------------ */\n /* Source helpers */\n /* ------------------------------------------------------------------ */\n\n _getSources() {\n if (!this.getMap()) return [];\n const sources = [];\n const collect = (layers) => {\n layers.forEach((layer) => {\n if (layer.getVisible()) {\n if (layer.getSource && layer.getSource() instanceof VectorSource) {\n sources.push(layer.getSource());\n } else if (layer.getLayers) {\n collect(layer.getLayers());\n }\n }\n });\n };\n collect(this.getMap().getLayers());\n return sources;\n }\n\n /* ------------------------------------------------------------------ */\n /* Event router */\n /* ------------------------------------------------------------------ */\n\n _handleEvent(e) {\n if (!this.getActive()) return true;\n\n // Escape cancels at any phase\n if (e.type === 'keydown' && e.originalEvent?.key === 'Escape') {\n this._reset();\n return false;\n }\n\n switch (this._phase) {\n case 'select_a':\n if (e.type === 'pointermove') return this._onSelectMove(e, null);\n if (e.type === 'singleclick') return this._onSelectAClick(e);\n break;\n case 'select_b':\n if (e.type === 'pointermove') return this._onSelectMove(e, this._featureA);\n if (e.type === 'singleclick') return this._onSelectBClick(e);\n break;\n case 'click_edge_a':\n if (e.type === 'pointermove') return this._onEdgeMove(e, this._featureA);\n if (e.type === 'singleclick') return this._onEdgeAClick(e);\n break;\n case 'click_edge_b':\n if (e.type === 'pointermove') return this._onEdgeMove(e, this._featureB);\n if (e.type === 'singleclick') return this._onEdgeBClick(e);\n break;\n }\n return true;\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 1 & 2: SELECT polygons */\n /* ------------------------------------------------------------------ */\n\n _onSelectMove(e, skipFeature) {\n const map = this.getMap();\n if (!map) return true;\n\n // Keep existing highlights for already-selected polygons\n this._highlightSource.clear();\n this._edgeSource.clear();\n this._rebuildHighlights();\n\n const hit = this._closestPolygon(e, skipFeature);\n if (hit) {\n const style = this._phase === 'select_a' ? HIGHLIGHT_A : HIGHLIGHT_B;\n const clone = hit.feature.clone();\n clone.set('_highlightStyle', style);\n this._highlightSource.addFeature(clone);\n map.getTargetElement().style.cursor = 'pointer';\n } else {\n map.getTargetElement().style.cursor = '';\n }\n return true;\n }\n\n _onSelectAClick(e) {\n const hit = this._closestPolygon(e, null);\n if (!hit) return true;\n\n this._featureA = hit.feature;\n this._sourceA = hit.source;\n this._phase = 'select_b';\n\n this._rebuildHighlights();\n return false;\n }\n\n _onSelectBClick(e) {\n const hit = this._closestPolygon(e, this._featureA);\n if (!hit) return true;\n\n this._featureB = hit.feature;\n this._sourceB = hit.source;\n this._phase = 'click_edge_a';\n\n this._rebuildHighlights();\n this.getMap().getTargetElement().style.cursor = 'crosshair';\n return false;\n }\n\n /**\n * Find the closest polygon feature within snap distance.\n * Optionally skip a feature (used in phase 2 to avoid re-selecting A).\n */\n _closestPolygon(e, skipFeature) {\n let best = null;\n let bestDist = this.snapDistance_ + 1;\n\n for (const source of this._getSources()) {\n const feat = source.getClosestFeatureToCoordinate(e.coordinate);\n if (!feat) continue;\n if (skipFeature && feat === skipFeature) continue;\n const geom = feat.getGeometry();\n if (!geom) continue;\n const type = geom.getType();\n if (type !== 'Polygon' && type !== 'MultiPolygon') continue;\n\n const closest = geom.getClosestPoint(e.coordinate);\n const line = new LineString([e.coordinate, closest]);\n const distPx = line.getLength() / e.frameState.viewState.resolution;\n\n if (distPx < bestDist) {\n bestDist = distPx;\n best = { feature: feat, source, coord: closest };\n }\n }\n return best;\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 3 & 4: CLICK edges */\n /* ------------------------------------------------------------------ */\n\n _onEdgeMove(e, feature) {\n const map = this.getMap();\n if (!map) return true;\n\n this._edgeSource.clear();\n\n const edge = this._closestEdgeSegment(feature, e);\n if (edge) {\n const edgeFeat = new Feature(new LineString([edge.segStart, edge.segEnd]));\n this._edgeSource.addFeature(edgeFeat);\n map.getTargetElement().style.cursor = 'crosshair';\n }\n return true;\n }\n\n _onEdgeAClick(e) {\n this._edgeClickA = e.coordinate;\n this._phase = 'click_edge_b';\n this._edgeSource.clear();\n return false;\n }\n\n _onEdgeBClick(e) {\n this._edgeClickB = e.coordinate;\n this._performMerge();\n return false;\n }\n\n /**\n * Find the closest edge segment of a polygon feature to the cursor.\n */\n _closestEdgeSegment(feature, e) {\n const geom = feature.getGeometry();\n let ring;\n if (geom.getType() === 'Polygon') {\n ring = geom.getCoordinates()[0];\n } else if (geom.getType() === 'MultiPolygon') {\n ring = geom.getCoordinates()[0][0];\n } else {\n return null;\n }\n\n const resolution = e.frameState.viewState.resolution;\n let bestDist = Infinity;\n let bestSeg = null;\n const n = ring.length - 1;\n\n for (let i = 0; i < n; i++) {\n const a = ring[i];\n const b = ring[i + 1];\n const dx = b[0] - a[0], dy = b[1] - a[1];\n const lenSq = dx * dx + dy * dy;\n if (lenSq < 1e-20) continue;\n\n let t = ((e.coordinate[0] - a[0]) * dx + (e.coordinate[1] - a[1]) * dy) / lenSq;\n t = Math.max(0, Math.min(1, t));\n const projX = a[0] + t * dx, projY = a[1] + t * dy;\n const distPx = Math.sqrt((e.coordinate[0] - projX) ** 2 + (e.coordinate[1] - projY) ** 2) / resolution;\n\n if (distPx < bestDist) {\n bestDist = distPx;\n bestSeg = { segStart: a, segEnd: b };\n }\n }\n return bestDist <= this.snapDistance_ ? bestSeg : null;\n }\n\n /* ------------------------------------------------------------------ */\n /* Merge logic */\n /* ------------------------------------------------------------------ */\n\n _performMerge() {\n const featureA = this._featureA;\n const featureB = this._featureB;\n const sourceA = this._sourceA;\n const sourceB = this._sourceB;\n\n // Extract polygon coordinates\n const geomA = featureA.getGeometry();\n const geomB = featureB.getGeometry();\n const coordsA = geomA.getType() === 'Polygon' ? geomA.getCoordinates() : geomA.getCoordinates()[0];\n const coordsB = geomB.getType() === 'Polygon' ? geomB.getCoordinates() : geomB.getCoordinates()[0];\n\n const result = mergePolygons(coordsA, coordsB, this._edgeClickA, this._edgeClickB, this.tolerance_);\n\n if (!result.coords) {\n showToast(result.error || 'Merge failed — try clicking on the shared boundary.', 'error', 5000);\n // Return to edge click phase for retry\n this._edgeClickA = null;\n this._edgeClickB = null;\n this._phase = 'click_edge_a';\n this._edgeSource.clear();\n return;\n }\n\n // Create merged feature (clone A for default properties)\n const mergedFeature = featureA.clone();\n mergedFeature.setGeometry(new PolygonGeom(result.coords));\n mergedFeature.setStyle(MERGE_STYLE);\n\n // Dispatch beforemerge events\n const evtData = {\n type: 'beforemerge',\n original: [featureA, featureB],\n merged: mergedFeature,\n };\n this.dispatchEvent(evtData);\n sourceA.dispatchEvent({ ...evtData });\n if (sourceB !== sourceA) {\n sourceB.dispatchEvent({ ...evtData });\n }\n\n // Replace originals with merged\n sourceA.removeFeature(featureA);\n sourceB.removeFeature(featureB);\n sourceA.addFeature(mergedFeature);\n\n // Dispatch aftermerge events\n const afterEvt = {\n type: 'aftermerge',\n original: [featureA, featureB],\n merged: mergedFeature,\n };\n this.dispatchEvent(afterEvt);\n sourceA.dispatchEvent({ ...afterEvt });\n if (sourceB !== sourceA) {\n sourceB.dispatchEvent({ ...afterEvt });\n }\n\n // If both features were parcels, fire mergedparcel so MapView can show the UPN chooser\n const isParcelA = featureA.get('_layerType') === 'parcel';\n const isParcelB = featureB.get('_layerType') === 'parcel';\n if (isParcelA && isParcelB) {\n this.dispatchEvent({\n type: 'mergedparcel',\n merged: mergedFeature,\n propsA: featureA.getProperties(),\n propsB: featureB.getProperties(),\n coordinate: this._edgeClickA,\n });\n showToast('Polygons merged — choose which identifier to keep.', 'success');\n } else {\n showToast('Polygons merged successfully.', 'success');\n }\n\n // Clean up\n this._reset();\n }\n\n /* ------------------------------------------------------------------ */\n /* Highlight management */\n /* ------------------------------------------------------------------ */\n\n /**\n * Rebuild the permanent highlights for already-selected polygons.\n */\n _rebuildHighlights() {\n // Remove previous non-hover highlights\n const toRemove = [];\n this._highlightSource.getFeatures().forEach((f) => {\n if (f.get('_permanent')) toRemove.push(f);\n });\n toRemove.forEach((f) => this._highlightSource.removeFeature(f));\n\n if (this._featureA) {\n const cloneA = this._featureA.clone();\n cloneA.set('_highlightStyle', LABEL_A);\n cloneA.set('_permanent', true);\n this._highlightSource.addFeature(cloneA);\n }\n if (this._featureB) {\n const cloneB = this._featureB.clone();\n cloneB.set('_highlightStyle', LABEL_B);\n cloneB.set('_permanent', true);\n this._highlightSource.addFeature(cloneB);\n }\n }\n\n /* ------------------------------------------------------------------ */\n /* Reset */\n /* ------------------------------------------------------------------ */\n\n _reset() {\n this._phase = 'select_a';\n this._featureA = null;\n this._sourceA = null;\n this._featureB = null;\n this._sourceB = null;\n this._edgeClickA = null;\n this._edgeClickB = null;\n this._highlightSource.clear();\n this._edgeSource.clear();\n\n const map = this.getMap();\n if (map) {\n map.getTargetElement().style.cursor = '';\n }\n }\n}\n","/**\n * Pure geometry functions for dividing a polygon into N equal-area pieces.\n *\n * No OpenLayers dependency — operates on raw coordinate arrays.\n *\n * The algorithm finds the polygon's longest edge, then places N-1 cutting\n * lines perpendicular to that edge. Each cutting-line position is found\n * via binary search so that the piece it cuts off has exactly 1/N of the\n * remaining area. The actual cut is delegated to `splitPolygonByLine()`.\n */\n\nimport { splitPolygonByLine } from './polygonSplit.js';\n\n// ── Utility helpers (self-contained) ─────────────────────────────────────────\n\nfunction dist2(a, b) {\n return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;\n}\n\n/**\n * Signed area of a ring (shoelace formula).\n */\nfunction signedArea(ring) {\n let area = 0;\n for (let i = 0, n = ring.length; i < n - 1; i++) {\n area += ring[i][0] * ring[i + 1][1] - ring[i + 1][0] * ring[i][1];\n }\n return area / 2;\n}\n\n/**\n * Absolute polygon area, accounting for holes.\n */\nfunction polygonArea(coords) {\n let area = Math.abs(signedArea(coords[0]));\n for (let i = 1; i < coords.length; i++) {\n area -= Math.abs(signedArea(coords[i]));\n }\n return area;\n}\n\n/**\n * Find the longest edge of a ring and return direction vectors.\n *\n * @param {number[][]} ring Closed ring\n * @returns {{ p0: number[], p1: number[], along: number[], perp: number[] }}\n * `along` = unit vector along the longest edge,\n * `perp` = unit vector perpendicular to `along` (rotated 90° CCW)\n */\nfunction longestEdge(ring) {\n const n = ring.length - 1; // unique vertices\n let bestLen = -1;\n let bestI = 0;\n\n for (let i = 0; i < n; i++) {\n const d = dist2(ring[i], ring[i + 1]);\n if (d > bestLen) {\n bestLen = d;\n bestI = i;\n }\n }\n\n const p0 = ring[bestI];\n const p1 = ring[bestI + 1];\n const len = Math.sqrt(bestLen);\n const along = [(p1[0] - p0[0]) / len, (p1[1] - p0[1]) / len];\n // Perpendicular: rotate 90° CCW\n const perp = [-along[1], along[0]];\n\n return { p0, p1, along, perp };\n}\n\n/**\n * Build a cutting line perpendicular to `along` at parameter `t`.\n *\n * The line passes through `origin + t * along` and extends `extent` units\n * in both `perp` directions — long enough to fully cross the polygon.\n */\nfunction makeCuttingLine(origin, along, perp, t, extent) {\n const cx = origin[0] + t * along[0];\n const cy = origin[1] + t * along[1];\n return [\n [cx - extent * perp[0], cy - extent * perp[1]],\n [cx + extent * perp[0], cy + extent * perp[1]],\n ];\n}\n\n/**\n * Project the centroid of a polygon's exterior ring onto the `along` axis.\n * Returns the scalar parameter `t` relative to `origin`.\n */\nfunction centroidT(coords, origin, along) {\n const ring = coords[0];\n const n = ring.length - 1;\n let sx = 0, sy = 0;\n for (let i = 0; i < n; i++) {\n sx += ring[i][0];\n sy += ring[i][1];\n }\n const cx = sx / n - origin[0];\n const cy = sy / n - origin[1];\n return cx * along[0] + cy * along[1];\n}\n\n// ── Main export ──────────────────────────────────────────────────────────────\n\n/**\n * Divide a polygon into N equal-area pieces by parallel cuts perpendicular\n * to a user-selected edge.\n *\n * @param {number[][][]} polygonCoords Polygon coordinates [ring, ...holes]\n * @param {number} n Number of pieces (must be >= 1)\n * @param {number[][]} edgeCoords The selected edge `[p0, p1]` — cuts will be\n * perpendicular to this edge direction.\n * @returns {{ pieces: number[][][][], error?: undefined } | { pieces: null, error: string }}\n */\nexport function dividePolygon(polygonCoords, n, edgeCoords) {\n if (!Number.isInteger(n) || n < 1) {\n return { pieces: null, error: 'Number of divisions must be a positive integer.' };\n }\n if (n === 1) {\n return { pieces: [polygonCoords] };\n }\n\n const ring = polygonCoords[0];\n const totalArea = polygonArea(polygonCoords);\n\n if (totalArea < 1e-6) {\n return { pieces: null, error: 'Polygon has no measurable area.' };\n }\n\n // 1. Determine cutting direction from the selected edge\n let p0, along, perp;\n if (edgeCoords && edgeCoords.length === 2) {\n p0 = edgeCoords[0];\n const dx = edgeCoords[1][0] - edgeCoords[0][0];\n const dy = edgeCoords[1][1] - edgeCoords[0][1];\n const len = Math.sqrt(dx * dx + dy * dy);\n if (len < 1e-10) {\n return { pieces: null, error: 'Selected edge has zero length.' };\n }\n along = [dx / len, dy / len];\n perp = [-along[1], along[0]];\n } else {\n // Fallback: use longest edge\n const edge = longestEdge(ring);\n p0 = edge.p0;\n along = edge.along;\n perp = edge.perp;\n }\n const origin = p0;\n\n // 2. Project all vertices onto the `along` axis to find extent\n const nVerts = ring.length - 1;\n let tMin = Infinity, tMax = -Infinity;\n for (let i = 0; i < nVerts; i++) {\n const dx = ring[i][0] - origin[0];\n const dy = ring[i][1] - origin[1];\n const t = dx * along[0] + dy * along[1];\n if (t < tMin) tMin = t;\n if (t > tMax) tMax = t;\n }\n\n // Cutting line extent: enough to cross the polygon in the `perp` direction\n let perpMin = Infinity, perpMax = -Infinity;\n for (let i = 0; i < nVerts; i++) {\n const dx = ring[i][0] - origin[0];\n const dy = ring[i][1] - origin[1];\n const p = dx * perp[0] + dy * perp[1];\n if (p < perpMin) perpMin = p;\n if (p > perpMax) perpMax = p;\n }\n const extent = (perpMax - perpMin) * 1.5; // generous overshoot\n\n // 3. Iteratively cut pieces\n const pieces = [];\n let remaining = polygonCoords;\n let remainingCount = n;\n\n for (let i = 0; i < n - 1; i++) {\n const remainingArea = polygonArea(remaining);\n const targetArea = remainingArea / remainingCount;\n\n // Re-project the remaining polygon to get its current t-range\n const remRing = remaining[0];\n const remN = remRing.length - 1;\n let rMin = Infinity, rMax = -Infinity;\n for (let j = 0; j < remN; j++) {\n const dx = remRing[j][0] - origin[0];\n const dy = remRing[j][1] - origin[1];\n const t = dx * along[0] + dy * along[1];\n if (t < rMin) rMin = t;\n if (t > rMax) rMax = t;\n }\n\n // Binary search for the cutting position\n let lo = rMin;\n let hi = rMax;\n let bestT = (lo + hi) / 2;\n let bestPiece = null;\n let bestRemaining = null;\n let bestError = Infinity;\n\n for (let iter = 0; iter < 40; iter++) {\n const mid = (lo + hi) / 2;\n const line = makeCuttingLine(origin, along, perp, mid, extent);\n const result = splitPolygonByLine(remaining, line);\n\n if (!result) {\n // Cutting line didn't produce a valid split — nudge and retry\n // Try slightly shifted positions\n const nudge = (hi - lo) * 0.01;\n const lineA = makeCuttingLine(origin, along, perp, mid + nudge, extent);\n const resultA = splitPolygonByLine(remaining, lineA);\n if (resultA) {\n const [halfA, halfB] = resultA;\n const tA = centroidT(halfA, origin, along);\n const tB = centroidT(halfB, origin, along);\n const nearPiece = tA < tB ? halfA : halfB;\n const farPiece = tA < tB ? halfB : halfA;\n const nearArea = polygonArea(nearPiece);\n const err = Math.abs(nearArea - targetArea);\n if (err < bestError) {\n bestError = err;\n bestT = mid + nudge;\n bestPiece = nearPiece;\n bestRemaining = farPiece;\n }\n }\n // Try the other direction\n const lineB = makeCuttingLine(origin, along, perp, mid - nudge, extent);\n const resultB = splitPolygonByLine(remaining, lineB);\n if (resultB) {\n const [halfA, halfB] = resultB;\n const tA = centroidT(halfA, origin, along);\n const tB = centroidT(halfB, origin, along);\n const nearPiece = tA < tB ? halfA : halfB;\n const farPiece = tA < tB ? halfB : halfA;\n const nearArea = polygonArea(nearPiece);\n const err = Math.abs(nearArea - targetArea);\n if (err < bestError) {\n bestError = err;\n bestT = mid - nudge;\n bestPiece = nearPiece;\n bestRemaining = farPiece;\n }\n }\n // Bisect anyway to keep converging\n lo = mid;\n continue;\n }\n\n const [halfA, halfB] = result;\n const tA = centroidT(halfA, origin, along);\n const tB = centroidT(halfB, origin, along);\n const nearPiece = tA < tB ? halfA : halfB;\n const farPiece = tA < tB ? halfB : halfA;\n const nearArea = polygonArea(nearPiece);\n\n const err = Math.abs(nearArea - targetArea);\n if (err < bestError) {\n bestError = err;\n bestT = mid;\n bestPiece = nearPiece;\n bestRemaining = farPiece;\n }\n\n // Converged?\n if (err / remainingArea < 0.001) break;\n\n // Adjust search range\n if (nearArea < targetArea) {\n lo = mid; // need to cut farther out\n } else {\n hi = mid; // need to cut closer\n }\n }\n\n if (!bestPiece || !bestRemaining) {\n return {\n pieces: null,\n error: `Could not find a valid cut for piece ${i + 1} of ${n}. The polygon shape may be too irregular for equal division.`,\n };\n }\n\n pieces.push(bestPiece);\n remaining = bestRemaining;\n remainingCount--;\n }\n\n // The last remaining piece is the Nth piece\n pieces.push(remaining);\n\n return { pieces };\n}\n","/**\n * PolygonDivideInteraction\n *\n * A three-phase OpenLayers interaction for dividing a polygon into N\n * equal-area pieces:\n * Phase 1 – SELECT: hover to highlight, click to select a polygon\n * Phase 2 – EDGE: hover to highlight edges, click to pick the divide\n * direction (cuts will be perpendicular to this edge)\n * Phase 3 – FORM: wait for the popup form to call performDivide(n)\n *\n * After a successful divide the original feature is removed and N new\n * coloured features are added. The interaction fires `beforedivide` and\n * `afterdivide` events compatible with ol-ext's UndoRedo.\n */\n\nimport ol_interaction_Interaction from 'ol/interaction/Interaction';\nimport VectorSource from 'ol/source/Vector';\nimport VectorLayer from 'ol/layer/Vector';\nimport Feature from 'ol/Feature';\nimport { Style, Stroke, Fill } from 'ol/style';\nimport { LineString, Polygon as PolygonGeom } from 'ol/geom';\nimport { dividePolygon } from '../geom/polygonDivide.js';\nimport { showToast } from '../toast.js';\n\n// Highlight style for the selected polygon (phase 1)\nconst HIGHLIGHT_STYLE = new Style({\n stroke: new Stroke({ color: '#0ea5e9', width: 3 }),\n fill: new Fill({ color: 'rgba(14,165,233,0.15)' }),\n});\n\n// Style for the hovered edge (phase 2)\nconst EDGE_STYLE = new Style({\n stroke: new Stroke({ color: '#8b5cf6', width: 4, lineDash: [10, 6] }),\n});\n\n/**\n * Generate N visually distinct colours using evenly-spaced HSL hues.\n */\nfunction pieceColors(n) {\n const colors = [];\n for (let i = 0; i < n; i++) {\n const hue = Math.round((i * 360) / n);\n colors.push({\n stroke: `hsl(${hue}, 70%, 45%)`,\n fill: `hsla(${hue}, 70%, 55%, 0.25)`,\n });\n }\n return colors;\n}\n\nexport class PolygonDivideInteraction extends ol_interaction_Interaction {\n /**\n * @param {Object} options\n * @param {VectorSource|VectorSource[]} [options.sources] Specific sources\n * to search. If omitted the interaction searches all visible vector layers.\n * @param {number} [options.snapDistance=25] Pixel distance for hover.\n */\n constructor(options = {}) {\n super({\n handleEvent: (e) => this._handleEvent(e),\n });\n\n this.snapDistance_ = options.snapDistance || 25;\n this._sources = options.sources\n ? (Array.isArray(options.sources) ? options.sources : [options.sources])\n : null;\n\n // Phase: 'select' | 'edge' | 'form' | 'pick'\n this._phase = 'select';\n this._selectedFeature = null;\n this._selectedSource = null;\n this._selectedEdge = null; // [p0, p1] — the edge the user clicked\n this._dividedFeatures = null; // features created after divide (for pick phase)\n\n // Overlay layer for polygon highlight\n this._overlaySource = new VectorSource({ useSpatialIndex: false });\n this._overlayLayer = new VectorLayer({\n source: this._overlaySource,\n displayInLayerSwitcher: false,\n style: HIGHLIGHT_STYLE,\n });\n\n // Overlay layer for edge highlight\n this._edgeSource = new VectorSource({ useSpatialIndex: false });\n this._edgeLayer = new VectorLayer({\n source: this._edgeSource,\n displayInLayerSwitcher: false,\n style: EDGE_STYLE,\n });\n }\n\n /* ------------------------------------------------------------------ */\n /* Map lifecycle */\n /* ------------------------------------------------------------------ */\n\n setMap(map) {\n if (this.getMap()) {\n this.getMap().removeLayer(this._overlayLayer);\n this.getMap().removeLayer(this._edgeLayer);\n }\n super.setMap(map);\n if (map) {\n this._overlayLayer.setMap(map);\n this._edgeLayer.setMap(map);\n }\n }\n\n setActive(active) {\n super.setActive(active);\n if (!active) {\n this._reset();\n }\n }\n\n /* ------------------------------------------------------------------ */\n /* Source helpers */\n /* ------------------------------------------------------------------ */\n\n _getSources() {\n if (this._sources) return this._sources;\n if (!this.getMap()) return [];\n const sources = [];\n const collect = (layers) => {\n layers.forEach((layer) => {\n if (layer.getVisible()) {\n if (layer.getSource && layer.getSource() instanceof VectorSource) {\n sources.push(layer.getSource());\n } else if (layer.getLayers) {\n collect(layer.getLayers());\n }\n }\n });\n };\n collect(this.getMap().getLayers());\n return sources;\n }\n\n /* ------------------------------------------------------------------ */\n /* Event router */\n /* ------------------------------------------------------------------ */\n\n _handleEvent(e) {\n if (!this.getActive()) return true;\n\n // Escape cancels at any phase\n if (e.type === 'keydown' && e.originalEvent?.key === 'Escape') {\n if (this._phase === 'form') {\n this.cancelDivide();\n } else {\n this._reset();\n }\n return false;\n }\n\n if (this._phase === 'select') {\n if (e.type === 'pointermove') return this._onSelectMove(e);\n if (e.type === 'singleclick') return this._onSelectClick(e);\n }\n\n if (this._phase === 'edge') {\n if (e.type === 'pointermove') return this._onEdgeMove(e);\n if (e.type === 'singleclick') return this._onEdgeClick(e);\n }\n\n if (this._phase === 'pick') {\n if (e.type === 'pointermove') return this._onPickMove(e);\n if (e.type === 'singleclick') return this._onPickClick(e);\n }\n\n return true;\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 1: SELECT polygon */\n /* ------------------------------------------------------------------ */\n\n _onSelectMove(e) {\n const map = this.getMap();\n if (!map) return true;\n\n this._overlaySource.clear();\n\n const hit = this._closestPolygon(e);\n if (hit) {\n const clone = hit.feature.clone();\n this._overlaySource.addFeature(clone);\n map.getTargetElement().style.cursor = 'pointer';\n } else {\n map.getTargetElement().style.cursor = '';\n }\n return true;\n }\n\n _onSelectClick(e) {\n const hit = this._closestPolygon(e);\n if (!hit) return true;\n\n this._selectedFeature = hit.feature;\n this._selectedSource = hit.source;\n\n // Keep polygon highlight visible during edge phase\n this._overlaySource.clear();\n const clone = hit.feature.clone();\n clone.set('_permanent', true);\n this._overlaySource.addFeature(clone);\n\n this._phase = 'edge';\n showToast('Click the edge to divide along.', 'info', 3000);\n return false;\n }\n\n _closestPolygon(e) {\n let best = null;\n let bestDist = this.snapDistance_ + 1;\n\n for (const source of this._getSources()) {\n const feat = source.getClosestFeatureToCoordinate(e.coordinate);\n if (!feat) continue;\n const geom = feat.getGeometry();\n if (!geom) continue;\n const type = geom.getType();\n if (type !== 'Polygon' && type !== 'MultiPolygon') continue;\n\n const closest = geom.getClosestPoint(e.coordinate);\n const line = new LineString([e.coordinate, closest]);\n const distPx = line.getLength() / e.frameState.viewState.resolution;\n\n if (distPx < bestDist) {\n bestDist = distPx;\n best = { feature: feat, source };\n }\n }\n return best;\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 2: EDGE selection */\n /* ------------------------------------------------------------------ */\n\n _onEdgeMove(e) {\n const map = this.getMap();\n if (!map) return true;\n\n this._edgeSource.clear();\n\n const edge = this._closestEdgeSegment(this._selectedFeature, e);\n if (edge) {\n const edgeFeat = new Feature(new LineString([edge.segStart, edge.segEnd]));\n this._edgeSource.addFeature(edgeFeat);\n map.getTargetElement().style.cursor = 'crosshair';\n } else {\n map.getTargetElement().style.cursor = '';\n }\n return true;\n }\n\n _onEdgeClick(e) {\n const edge = this._closestEdgeSegment(this._selectedFeature, e);\n if (!edge) return true;\n\n this._selectedEdge = [edge.segStart, edge.segEnd];\n this._edgeSource.clear();\n\n this._phase = 'form';\n\n // Dispatch divideform so MapView can show the popup\n const geom = this._selectedFeature.getGeometry();\n const ext = geom.getExtent();\n const center = [(ext[0] + ext[2]) / 2, (ext[1] + ext[3]) / 2];\n\n this.dispatchEvent({\n type: 'divideform',\n feature: this._selectedFeature,\n source: this._selectedSource,\n coordinate: center,\n });\n\n return false;\n }\n\n /**\n * Find the closest edge segment of a polygon feature to the cursor.\n */\n _closestEdgeSegment(feature, e) {\n const geom = feature.getGeometry();\n let ring;\n if (geom.getType() === 'Polygon') {\n ring = geom.getCoordinates()[0];\n } else if (geom.getType() === 'MultiPolygon') {\n ring = geom.getCoordinates()[0][0];\n } else {\n return null;\n }\n\n const resolution = e.frameState.viewState.resolution;\n let bestDist = Infinity;\n let bestSeg = null;\n const n = ring.length - 1;\n\n for (let i = 0; i < n; i++) {\n const a = ring[i];\n const b = ring[i + 1];\n const dx = b[0] - a[0], dy = b[1] - a[1];\n const lenSq = dx * dx + dy * dy;\n if (lenSq < 1e-20) continue;\n\n let t = ((e.coordinate[0] - a[0]) * dx + (e.coordinate[1] - a[1]) * dy) / lenSq;\n t = Math.max(0, Math.min(1, t));\n const projX = a[0] + t * dx, projY = a[1] + t * dy;\n const distPx = Math.sqrt((e.coordinate[0] - projX) ** 2 + (e.coordinate[1] - projY) ** 2) / resolution;\n\n if (distPx < bestDist) {\n bestDist = distPx;\n bestSeg = { segStart: a, segEnd: b };\n }\n }\n return bestDist <= this.snapDistance_ ? bestSeg : null;\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 3: FORM — called externally by the popup */\n /* ------------------------------------------------------------------ */\n\n /**\n * Divide the selected polygon into `n` equal-area pieces.\n * Called by the MapView popup's Confirm handler.\n *\n * @param {number} n Number of pieces (>= 2)\n */\n performDivide(n) {\n if (this._phase !== 'form' || !this._selectedFeature) return;\n\n const feature = this._selectedFeature;\n const source = this._selectedSource;\n const geom = feature.getGeometry();\n\n let polygonCoords;\n if (geom.getType() === 'Polygon') {\n polygonCoords = geom.getCoordinates();\n } else if (geom.getType() === 'MultiPolygon') {\n polygonCoords = geom.getCoordinates()[0];\n }\n\n const result = dividePolygon(polygonCoords, n, this._selectedEdge);\n\n if (!result.pieces) {\n showToast(result.error || 'Division failed.', 'error', 5000);\n this._reset();\n return;\n }\n\n // Create N new coloured features\n const colors = pieceColors(n);\n const newFeatures = result.pieces.map((coords, i) => {\n const f = feature.clone();\n f.setGeometry(new PolygonGeom(coords));\n f.setStyle(new Style({\n stroke: new Stroke({ color: colors[i].stroke, width: 2.5 }),\n fill: new Fill({ color: colors[i].fill }),\n }));\n return f;\n });\n\n // Dispatch beforedivide (UndoRedo compatible)\n const evtData = {\n type: 'beforedivide',\n original: feature,\n features: newFeatures,\n };\n this.dispatchEvent(evtData);\n source.dispatchEvent({ ...evtData });\n\n // Replace original with pieces\n source.removeFeature(feature);\n for (const f of newFeatures) {\n source.addFeature(f);\n }\n\n // Dispatch afterdivide\n const afterEvt = {\n type: 'afterdivide',\n original: feature,\n features: newFeatures,\n };\n this.dispatchEvent(afterEvt);\n source.dispatchEvent({ ...afterEvt });\n\n // If original was a parcel, enter pick phase for UPN assignment\n const isParcel = feature.get('_layerType') === 'parcel';\n if (isParcel) {\n this._dividedFeatures = newFeatures;\n this._phase = 'pick';\n showToast('Click the polygon that should keep the original identifier.', 'info', 5000);\n\n this.dispatchEvent({\n type: 'dividedparcel',\n features: newFeatures,\n originalProps: feature.getProperties(),\n source,\n });\n } else {\n showToast(`Polygon divided into ${n} equal pieces.`, 'success');\n this._reset();\n }\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 4: PICK — select which piece keeps the UPN */\n /* ------------------------------------------------------------------ */\n\n _onPickMove(e) {\n const map = this.getMap();\n if (!map) return true;\n\n this._overlaySource.clear();\n\n // Highlight whichever divided piece is under the cursor\n const hit = this._closestDividedPiece(e);\n if (hit) {\n const clone = hit.clone();\n this._overlaySource.addFeature(clone);\n map.getTargetElement().style.cursor = 'pointer';\n } else {\n map.getTargetElement().style.cursor = '';\n }\n return true;\n }\n\n _onPickClick(e) {\n const hit = this._closestDividedPiece(e);\n if (!hit) return true;\n\n this.dispatchEvent({\n type: 'dividepick',\n picked: hit,\n features: this._dividedFeatures,\n });\n\n this._reset();\n return false;\n }\n\n /**\n * Find the closest divided piece to the cursor.\n */\n _closestDividedPiece(e) {\n if (!this._dividedFeatures) return null;\n let best = null;\n let bestDist = this.snapDistance_ + 1;\n\n for (const feat of this._dividedFeatures) {\n const geom = feat.getGeometry();\n if (!geom) continue;\n const closest = geom.getClosestPoint(e.coordinate);\n const line = new LineString([e.coordinate, closest]);\n const distPx = line.getLength() / e.frameState.viewState.resolution;\n if (distPx < bestDist) {\n bestDist = distPx;\n best = feat;\n }\n }\n return best;\n }\n\n /**\n * Cancel the divide operation and return to select phase.\n * Called by the MapView popup's Cancel handler.\n */\n cancelDivide() {\n this.dispatchEvent({ type: 'dividecancel' });\n this._reset();\n }\n\n /* ------------------------------------------------------------------ */\n /* Reset */\n /* ------------------------------------------------------------------ */\n\n _reset() {\n this._phase = 'select';\n this._selectedFeature = null;\n this._selectedSource = null;\n this._selectedEdge = null;\n this._dividedFeatures = null;\n this._overlaySource.clear();\n this._edgeSource.clear();\n\n const map = this.getMap();\n if (map) {\n map.getTargetElement().style.cursor = '';\n }\n }\n}\n","/**\n * MapView Component\n *\n * OpenLayers map with ol-ext LayerSwitcher for base map selection.\n *\n * Usage:\n * import { MapView } from './components/MapView.js';\n *\n * const map = new MapView('map', {\n * center: [-1.5, 7.5], // Ghana\n * zoom: 7,\n * basemap: 'osm'\n * });\n *\n * map.onClick((lon, lat) => console.log('Clicked:', lon, lat));\n * map.addMarker(lon, lat, { name: 'Point A' });\n */\n\nimport Map from 'ol/Map';\nimport View from 'ol/View';\nimport Overlay from 'ol/Overlay';\nimport TileLayer from 'ol/layer/Tile';\nimport ImageLayer from 'ol/layer/Image';\nimport LayerGroup from 'ol/layer/Group';\nimport VectorLayer from 'ol/layer/Vector';\nimport VectorImageLayer from 'ol/layer/VectorImage';\nimport VectorSource from 'ol/source/Vector';\nimport ImageWMS from 'ol/source/ImageWMS';\nimport TileWMS from 'ol/source/TileWMS';\nimport OSM from 'ol/source/OSM';\nimport XYZ from 'ol/source/XYZ';\nimport { fromLonLat, toLonLat } from 'ol/proj';\nimport { Point, LineString, Polygon as PolygonGeom } from 'ol/geom';\nimport Feature from 'ol/Feature';\nimport { Style, Circle, Fill, Stroke, Text } from 'ol/style';\nimport GeoJSON from 'ol/format/GeoJSON';\nimport { getArea, getLength } from 'ol/sphere';\nimport { fromCircle } from 'ol/geom/Polygon';\nimport ScaleLine from 'ol/control/ScaleLine';\nimport { formatLength, formatLengthFull, formatArea, formatAreaFull } from '../units.js';\n\n// ol-ext LayerSwitcher\nimport LayerSwitcher from 'ol-ext/control/LayerSwitcher';\n\n// ol-ext SearchNominatim\nimport SearchNominatim from 'ol-ext/control/SearchNominatim';\n\n// ol-ext EditBar for drawing/editing features\nimport EditBar from 'ol-ext/control/EditBar';\nimport Bar from 'ol-ext/control/Bar';\nimport Button from 'ol-ext/control/Button';\n\n// ol-ext TouchCursor for touch-enabled devices\nimport TouchCursor from 'ol-ext/interaction/TouchCursor';\n\n// ol-ext ModifyFeature for cross-layer modification\nimport ModifyFeature from 'ol-ext/interaction/ModifyFeature';\n\n// ol-ext UndoRedo interaction\nimport UndoRedo from 'ol-ext/interaction/UndoRedo';\n\n// ol-ext SnapGuides — snaps drawing vertices to alignment guides\nimport SnapGuides from 'ol-ext/interaction/SnapGuides';\n\n// ol Select interaction (for custom multi-layer Select)\nimport Select from 'ol/interaction/Select';\nimport { click as clickCondition } from 'ol/events/condition';\n\n// ol-ext Split interaction (for line splitting) and Toggle control\nimport Split from 'ol-ext/interaction/Split';\nimport Toggle from 'ol-ext/control/Toggle';\nimport TextButton from 'ol-ext/control/TextButton';\n\n// Custom polygon split interaction\nimport { PolygonSplitInteraction } from '../interactions/PolygonSplitInteraction.js';\n\n// Custom polygon merge interaction\nimport { PolygonMergeInteraction } from '../interactions/PolygonMergeInteraction.js';\n\n// Custom polygon divide interaction\nimport { PolygonDivideInteraction } from '../interactions/PolygonDivideInteraction.js';\n\n// Toast notifications\nimport { showToast } from '../toast.js';\n\n// CSS imports\nimport 'ol/ol.css';\nimport 'ol-ext/dist/ol-ext.css';\nimport '../styles/layerswitcher.css';\n\nexport class MapView {\n constructor(targetId, options = {}) {\n this.options = options;\n this.markerSource = new VectorSource();\n this.clickCallbacks = [];\n\n // Category emoji and label mapping\n // Add new categories here - they will automatically appear in the dropdown\n this.categoryEmojis = {\n 'default': { emoji: '📍', label: 'Default' },\n 'water': { emoji: '💧', label: 'Water Point' },\n 'school': { emoji: '🏫', label: 'School' },\n 'health': { emoji: '🏥', label: 'Health Facility' },\n 'market': { emoji: '🏪', label: 'Market' },\n 'other': { emoji: '📌', label: 'Other' }\n };\n\n // Helper to get emoji for a category\n this.getEmoji = (category) => {\n const cat = this.categoryEmojis[category];\n return cat ? cat.emoji : '📍';\n };\n\n // Helper to generate category options HTML for select dropdowns\n this.getCategoryOptionsHtml = () => {\n return Object.entries(this.categoryEmojis)\n .map(([key, { emoji, label }]) =>\n ``\n )\n .join('\\n ');\n };\n\n // Create emoji style helper\n this.createEmojiStyle = (emoji, fontSize = 24) => {\n return new Style({\n text: new Text({\n text: emoji,\n font: `${fontSize}px sans-serif`,\n textBaseline: 'bottom',\n textAlign: 'center',\n offsetY: -5,\n }),\n });\n };\n\n // Default marker style (pin emoji)\n this.defaultStyle = this.createEmojiStyle('📍', 32);\n\n // Selected marker style (larger)\n this.selectedStyle = this.createEmojiStyle('📍', 42);\n\n // Initialize category styles with emojis\n this.categoryStyles = {};\n for (const [category, { emoji }] of Object.entries(this.categoryEmojis)) {\n this.categoryStyles[category] = this.createEmojiStyle(emoji, 32);\n }\n\n // Create base layers group\n const baseLayers = this.createBaseLayers(options.basemap || 'topo');\n\n // Markers layer — hidden at startup; the user enables it from the\n // LayerSwitcher when they want to see location markers / category pins.\n this.markersLayer = new VectorLayer({\n title: 'Markers',\n source: this.markerSource,\n style: (feature) => this.getFeatureStyle(feature),\n visible: false,\n });\n\n // Overlay layers group (for remote data like boundaries)\n this.overlayGroup = new LayerGroup({\n title: 'Overlays',\n });\n\n // Create map\n // Layer order (bottom → top): Base Maps, Markers, Overlays\n // MapTools will insert Measurements and Drawings between Markers and Overlays.\n // initEditBar() will insert its Drawings group above those.\n // Final LayerSwitcher order (top → bottom):\n // Overlays, Drawings, Measurements, Markers, Base Maps\n this.map = new Map({\n target: targetId,\n layers: [\n baseLayers,\n this.markersLayer,\n this.overlayGroup,\n ],\n view: new View({\n center: fromLonLat(options.center || [0, 0]),\n zoom: options.zoom || 2,\n minZoom: options.minZoom || 2,\n maxZoom: options.maxZoom || 19,\n })\n });\n\n // Add LayerSwitcher control\n const layerSwitcher = new LayerSwitcher({\n collapsed: true,\n mouseover: true,\n extent: true,\n trash: false,\n oninfo: null,\n });\n this.map.addControl(layerSwitcher);\n\n // Apply the LUSPA branded icon to the LayerSwitcher's collapse button.\n // Done in JS so the URL respects Vite's BASE_URL — survives deployment\n // under any sub-path.\n // NOTE: folder name is `app-icons`, NOT `icons` — Apache aliases `/icons/`\n // by default to its own directory-listing thumbnails, which would\n // intercept this request server-side.\n queueMicrotask(() => {\n const btn = layerSwitcher.element?.querySelector(':scope > button');\n if (btn) {\n const baseUrl = (import.meta.env?.BASE_URL || '/').replace(/\\/?$/, '/');\n btn.style.backgroundImage = `url('${baseUrl}app-icons/luspa-72x72.png')`;\n }\n });\n\n // ------------------------------------------------------------------\n // Decorate each layer's
  • as it's rendered:\n // • inject a type-tag chip (WMS / XYZ / VEC / …) next to the label\n // • add a green \"+\" button to the \"External Source\" group header\n // After each draw cycle, refresh the panel chrome (active count badge\n // + footer reset button). Schedule once per cycle via a microtask.\n // ------------------------------------------------------------------\n let _lsChromeScheduled = false;\n layerSwitcher.on('drawlist', (evt) => {\n this._decorateLayerListItem(evt.layer, evt.li);\n\n if (!_lsChromeScheduled) {\n _lsChromeScheduled = true;\n queueMicrotask(() => {\n _lsChromeScheduled = false;\n this._refreshLayerSwitcherChrome(layerSwitcher);\n });\n }\n });\n\n // Re-render the chrome whenever any layer's visibility changes (so the\n // active-count badge updates even when the user toggles via the panel).\n this.map.getLayers().on('change', () => {\n this._refreshLayerSwitcherChrome(layerSwitcher);\n });\n // Hook visibility events on every layer (recursive into groups).\n this._wireLayerSwitcherVisibilityHooks(layerSwitcher);\n\n // Create the add-layer dialog (hidden by default)\n this._createAddLayerDialog();\n\n // Create the legend panel (shows legends for visible layers that have one)\n this._createLegendPanel();\n\n // Add ScaleBar control\n this.scaleBar = new ScaleLine({\n bar: true,\n steps: 4,\n text: true,\n minWidth: 140,\n });\n this.map.addControl(this.scaleBar);\n\n // GPS rendering layers (current position + recorded trail) and the\n // expandable \"My Location\" control (Locate Me + Record Trail sub-buttons).\n this._initGpsRendering();\n this._createLocationControl();\n\n // Dedicated base-map picker — sits above the My Location button\n this._createBaseMapPicker();\n\n // Add SearchNominatim control\n const searchNominatim = new SearchNominatim({\n placeholder: 'Search location...',\n typing: 300, // Delay before search (ms)\n minLength: 3, // Minimum characters to start search\n maxItems: 10, // Maximum results to show\n collapsed: true, // Start collapsed\n // Limit search to improve relevance (can be adjusted)\n // countrycodes: 'gh', // Uncomment to limit to Ghana\n });\n this.map.addControl(searchNominatim);\n\n // Handle search result selection\n searchNominatim.on('select', (event) => {\n const searchResult = event.search;\n if (searchResult) {\n // SearchNominatim returns a plain object with lon/lat properties (as strings)\n const lon = parseFloat(searchResult.lon);\n const lat = parseFloat(searchResult.lat);\n const lonLat = [lon, lat];\n const coordinate = fromLonLat(lonLat);\n\n // Navigate to the selected location\n this.navigateTo(lon, lat, 14);\n\n // Trigger search select callbacks\n const result = {\n coordinate: coordinate,\n lonLat: lonLat,\n name: searchResult.display_name || searchResult.name || 'Unknown',\n searchResult: searchResult,\n };\n this.searchSelectCallbacks.forEach(cb => cb(result));\n }\n });\n\n // Store reference for external access\n this.searchNominatim = searchNominatim;\n this.searchSelectCallbacks = [];\n\n // Track selected feature\n this.selectedFeature = null;\n\n // Create popup overlay for hover\n this.createPopup();\n\n // Create info popup for double-click feature details\n this.createInfoPopup();\n\n // Create Add Location popup form\n this.createAddLocationPopup();\n\n // Create editable parcel form popup\n this.createParcelEditPopup();\n\n // Create drawn polygon attribute popup\n this.createDrawnPolygonPopup();\n\n // Create merge identifier (UPN) chooser popup\n this.createMergePopup();\n\n // Create divide polygon popup (number input)\n this.createDividePopup();\n\n // Double-click callbacks\n this.dblClickCallbacks = [];\n\n // EditBar is set up lazily via initEditBar() once the Drawings\n // layer/group is available (called from main.js after loadLayers).\n this.editBar = null;\n this.drawingsSource = null;\n this.drawingsLayer = null;\n this.touchCursor = null;\n this._editBarActive = false;\n }\n\n // ============================================================================\n // EditBar + Drawings Layer + TouchCursor\n // ============================================================================\n\n /**\n * Initialise the EditBar with a dedicated \"Drawings\" LayerGroup.\n *\n * A \"Drawings\" LayerGroup is created at the top of the overlay stack\n * containing a \"sketches\" VectorLayer for storing drawn features.\n * The EditBar, Select and Modify interactions are only active while\n * edit mode is on; in all other cases normal click / double-click\n * behaviour is preserved.\n *\n * Call this once from main.js after the layer groups have been created.\n */\n initEditBar() {\n // 1. Create a \"Drawings\" LayerGroup with a \"sketches\" VectorLayer inside\n this.drawingsSource = new VectorSource();\n this.drawingsLayer = new VectorLayer({\n title: 'sketches',\n source: this.drawingsSource,\n style: new Style({\n stroke: new Stroke({ color: '#f59e0b', width: 2.5 }),\n fill: new Fill({ color: 'rgba(245,158,11,0.15)' }),\n image: new Circle({\n radius: 6,\n fill: new Fill({ color: '#f59e0b' }),\n stroke: new Stroke({ color: '#fff', width: 1.5 }),\n }),\n }),\n });\n\n this._drawingsGroup = new LayerGroup({\n title: 'Drawings',\n layers: [this.drawingsLayer],\n });\n // Insert as a top-level map layer just before the Overlays group\n // so the LayerSwitcher order is: Overlays > Drawings > Measurements > Markers > Base Maps\n const mapLayers = this.map.getLayers();\n const overlayIdx = mapLayers.getLength() - 1; // Overlays is the last layer\n mapLayers.insertAt(overlayIdx, this._drawingsGroup);\n\n // 2. Create a Select interaction that works on ALL vector layers.\n // It starts INACTIVE so it doesn't steal clicks from normal handlers.\n this._selectInteraction = new Select({\n condition: clickCondition,\n filter: (feature, layer) => !!layer,\n layers: (layer) => layer instanceof VectorLayer,\n });\n this._selectInteraction.setActive(false);\n this.map.addInteraction(this._selectInteraction);\n\n // 3. Create a ModifyFeature interaction bound to the selection.\n // Also starts inactive.\n this._modifyInteraction = new ModifyFeature({\n features: this._selectInteraction.getFeatures(),\n });\n this._modifyInteraction.setActive(false);\n\n // 4. UndoRedo interaction — watches the drawings source\n this._undoRedo = new UndoRedo();\n this.map.addInteraction(this._undoRedo);\n\n // 5. Build the EditBar — all interactions enabled.\n this.editBar = new EditBar({\n source: this.drawingsSource,\n interactions: {\n Select: this._selectInteraction,\n ModifySelect: this._modifyInteraction,\n DrawPoint: true,\n DrawLine: true,\n DrawPolygon: true,\n DrawRegular: true,\n DrawHole: true,\n Delete: true,\n Info: true,\n Transform: true,\n Split: false,\n },\n });\n this.map.addControl(this.editBar);\n\n // 5b. Persistent vertex overlay — when edit mode is active and the user\n // selects a polygon (or line) for modification, render a small dot\n // at every vertex so the user can see all editable nodes at a glance.\n // ol-ext's ModifyFeature only renders the closest vertex on hover; this\n // overlay complements that without subclassing the interaction.\n this._setupVertexOverlay();\n\n // 6. Add extra buttons (Undo, Redo, Save) as a sub-bar\n // inside the EditBar so they appear inline.\n const extraBar = new Bar({\n group: true,\n // Stable class so CSS can move this group (undo/redo/save/snap) to a\n // second row on small screens — see `.ol-editbar-actions` media query.\n className: 'ol-editbar-actions',\n controls: [\n new Button({\n html: '',\n className: 'ol-undo',\n title: 'Undo',\n handleClick: () => {\n if (this._undoRedo.hasUndo()) this._undoRedo.undo();\n },\n }),\n new Button({\n html: '',\n className: 'ol-redo',\n title: 'Redo',\n handleClick: () => {\n if (this._undoRedo.hasRedo()) this._undoRedo.redo();\n },\n }),\n new Button({\n html: '',\n className: 'ol-save',\n title: 'Save drawings',\n handleClick: () => {\n this.dispatchEditEvent('save');\n },\n }),\n ],\n });\n this.editBar.addControl(extraBar);\n\n // 6a-split. Custom Split tool with Lines / Polygons sub-categories.\n // The default ol-ext Split only handles LineString. We add a parent\n // Toggle with a sub-bar containing two sub-toggles: \"Lines\" (ol-ext\n // Split) and \"Polygons\" (our PolygonSplitInteraction).\n // No explicit sources → both interactions search ALL visible vector layers,\n // so they work on drawn features, parcels, zones, and any other polygon layer.\n this._lineSplitInteraction = new Split();\n this._polygonSplitInteraction = new PolygonSplitInteraction();\n this.map.addInteraction(this._lineSplitInteraction);\n this.map.addInteraction(this._polygonSplitInteraction);\n this._lineSplitInteraction.setActive(false);\n this._polygonSplitInteraction.setActive(false);\n\n // When a parcel is split, the user picks which piece keeps the UPN.\n this._polygonSplitInteraction.on('splitpick', (evt) => {\n const idFields = ['UPN', 'upn', 'id', 'parcelid', 'parcel_id', 'PARCELID', 'PARCEL_ID', 'ID'];\n for (const feat of evt.features) {\n if (feat === evt.picked) continue;\n for (const field of idFields) {\n if (feat.get(field) !== undefined) {\n feat.set(field, '');\n }\n }\n }\n });\n\n // Polygon Divide interaction (parameter-driven equal-area division)\n this._polygonDivideInteraction = new PolygonDivideInteraction();\n this.map.addInteraction(this._polygonDivideInteraction);\n this._polygonDivideInteraction.setActive(false);\n\n const splitLineToggle = new Toggle({\n html: '',\n className: 'ol-split-line',\n title: 'Split Lines',\n name: 'SplitLine',\n interaction: this._lineSplitInteraction,\n autoActivate: true,\n });\n const splitPolyToggle = new Toggle({\n html: '',\n className: 'ol-split-polygon',\n title: 'Split Polygons',\n name: 'SplitPolygon',\n interaction: this._polygonSplitInteraction,\n });\n const splitDivideToggle = new Toggle({\n html: '',\n className: 'ol-split-divide',\n title: 'Divide Polygon',\n name: 'DividePolygon',\n interaction: this._polygonDivideInteraction,\n });\n\n const splitSubBar = new Bar({\n toggleOne: true,\n autoDeactivate: true,\n controls: [splitLineToggle, splitPolyToggle, splitDivideToggle],\n });\n\n const splitParentToggle = new Toggle({\n className: 'ol-split',\n title: 'Split',\n name: 'Split',\n bar: splitSubBar,\n onToggle: (active) => {\n if (!active) {\n this._lineSplitInteraction.setActive(false);\n this._polygonSplitInteraction.setActive(false);\n this._polygonDivideInteraction.setActive(false);\n }\n },\n });\n this.editBar.addControl(splitParentToggle);\n\n // Listen for divide form request → show divide popup\n this._polygonDivideInteraction.on('divideform', (evt) => {\n this.showDividePopup(evt.feature, evt.source, evt.coordinate);\n });\n this._polygonDivideInteraction.on('dividecancel', () => {\n this.hideDividePopup();\n });\n\n // When a parcel is divided, the user picks which piece keeps the UPN.\n // The picked piece gets the original properties; all others get UPN cleared.\n this._polygonDivideInteraction.on('dividepick', (evt) => {\n const idFields = ['UPN', 'upn', 'id', 'parcelid', 'parcel_id', 'PARCELID', 'PARCEL_ID', 'ID'];\n for (const feat of evt.features) {\n if (feat === evt.picked) continue;\n // Clear identifier fields on the non-picked pieces\n for (const field of idFields) {\n if (feat.get(field) !== undefined) {\n feat.set(field, '');\n }\n }\n }\n });\n\n // 6a-merge. Polygon Merge tool — select two adjacent polygons, click shared\n // edges, and merge them into one. For parcels, a UPN chooser popup appears.\n this._polygonMergeInteraction = new PolygonMergeInteraction();\n this.map.addInteraction(this._polygonMergeInteraction);\n this._polygonMergeInteraction.setActive(false);\n\n const mergeToggle = new Toggle({\n html: '',\n className: 'ol-merge',\n title: 'Merge Polygons',\n name: 'Merge',\n interaction: this._polygonMergeInteraction,\n });\n this.editBar.addControl(mergeToggle);\n\n // Listen for merged-parcel event → show UPN chooser\n this._polygonMergeInteraction.on('mergedparcel', (evt) => {\n this.showMergeIdentifierPopup(evt.merged, evt.propsA, evt.propsB, evt.coordinate);\n });\n\n // Small-screen layout: insert a zero-height, full-width flex line-break\n // immediately BEFORE the action group. On phones (see the .ol-editbar\n // media query) this forces the wrap to happen here, so the action group\n // (undo/redo/save/snap) together with the Split and Merge toggles all land\n // on a single second row instead of Split/Merge spilling onto a third row.\n // The break is display:none on wider screens, so desktop layout is unchanged.\n const editbarEl = this.editBar.element;\n if (editbarEl && extraBar.element && extraBar.element.parentNode === editbarEl) {\n const breakEl = document.createElement('div');\n breakEl.className = 'ol-editbar-break';\n editbarEl.insertBefore(breakEl, extraBar.element);\n }\n\n // 6b. SnapGuides — shows alignment guides while drawing.\n // Uses VectorImageLayer for GPU-friendly canvas rendering instead of\n // re-creating individual SVG elements on every guide update.\n this._snapGuidesEnabled = localStorage.getItem('snap-guides-enabled') === '1';\n this._snapGuides = new SnapGuides({\n pixelTolerance: 10,\n vectorClass: VectorImageLayer,\n });\n this.map.addInteraction(this._snapGuides);\n\n // Connect SnapGuides to whichever draw interaction becomes active.\n // setDrawInteraction() only tracks one at a time, so we re-bind\n // whenever a draw tool is activated.\n const drawToolNames = ['DrawPoint', 'DrawLine', 'DrawPolygon', 'DrawHole', 'DrawRegular'];\n for (const name of drawToolNames) {\n const interaction = this.editBar.getInteraction(name);\n if (interaction) {\n interaction.on('change:active', () => {\n if (interaction.getActive()) {\n this._snapGuides.setDrawInteraction(interaction);\n }\n });\n }\n }\n\n // Also connect SnapGuides to the Modify interaction for vertex editing\n if (this._modifyInteraction) {\n this._snapGuides.setModifyInteraction(this._modifyInteraction);\n }\n\n // 6c. Snap-guides toggle button (magnet icon) — persisted in localStorage\n const snapToggleBtn = new Button({\n html: '',\n className: 'ol-snap-toggle' + (this._snapGuidesEnabled ? ' ol-active' : ''),\n title: 'Toggle Snap Guides',\n handleClick: () => {\n this._snapGuidesEnabled = !this._snapGuidesEnabled;\n localStorage.setItem('snap-guides-enabled', this._snapGuidesEnabled ? '1' : '0');\n // Update visual state\n snapToggleBtn.element.classList.toggle('ol-active', this._snapGuidesEnabled);\n // Activate or deactivate the interaction\n if (this._snapGuides) {\n this._snapGuides.setActive(this._snapGuidesEnabled && this._editBarActive);\n }\n console.log('[MapView] Snap guides:', this._snapGuidesEnabled ? 'ON' : 'OFF');\n },\n });\n this._snapToggleBtn = snapToggleBtn;\n extraBar.addControl(snapToggleBtn);\n\n // Start hidden — use the full setEditMode(false) so the Select +\n // Modify interactions are deactivated (the EditBar constructor may\n // have re-activated them).\n this.setEditMode(false);\n\n // 7. Link EditBar visibility to the Drawings group's visibility.\n this._drawingsGroup.on('change:visible', () => {\n const visible = this._drawingsGroup.getVisible();\n this.setEditMode(visible);\n });\n\n // 8. Touch-device detection & TouchCursor setup\n const isTouchDevice = ('ontouchstart' in window) ||\n (navigator.maxTouchPoints > 0) ||\n (navigator.msMaxTouchPoints > 0);\n\n if (isTouchDevice) {\n this.touchCursor = new TouchCursor({\n className: 'ol-editbar-cursor',\n });\n this.map.addInteraction(this.touchCursor);\n this.touchCursor.setActive(false);\n console.log('[MapView] Touch device detected — TouchCursor added');\n }\n\n // 9. Listen for polygon features drawn via EditBar's DrawPolygon tool.\n // When a Polygon is added to the drawings source, show the attribute popup.\n this.drawingsSource.on('addfeature', (evt) => {\n const feature = evt.feature;\n const geom = feature.getGeometry();\n if (!geom || geom.getType() !== 'Polygon') return;\n\n const coordinate = geom.getInteriorPoint().getCoordinates();\n this.showDrawnPolygonPopup(feature, coordinate);\n });\n\n console.log('[MapView] EditBar initialised with Drawings group, UndoRedo and SnapGuides (default:', this._snapGuidesEnabled ? 'ON' : 'OFF', ')');\n }\n\n /**\n * Dispatch a custom edit event (e.g. 'save').\n * External code can listen via mapView.onEditEvent('save', callback).\n * @param {string} type\n */\n dispatchEditEvent(type) {\n if (!this._editEventListeners) return;\n const listeners = this._editEventListeners[type];\n if (listeners) {\n listeners.forEach((fn) => fn());\n }\n }\n\n /**\n * Listen for custom edit events (e.g. 'save').\n * @param {string} type - Event name\n * @param {Function} callback\n */\n onEditEvent(type, callback) {\n if (!this._editEventListeners) this._editEventListeners = {};\n if (!this._editEventListeners[type]) this._editEventListeners[type] = [];\n this._editEventListeners[type].push(callback);\n }\n\n /**\n * Toggle edit mode on or off.\n *\n * When ON: EditBar is visible, Select + Modify interactions are active.\n * When OFF: EditBar is hidden, Select + Modify are deactivated, any\n * current selection is cleared so normal click / double-click\n * events work without interference.\n *\n * @param {boolean} active\n */\n setEditMode(active) {\n this._editBarActive = !!active;\n\n if (this.editBar) {\n this.editBar.setVisible(this._editBarActive);\n\n if (!this._editBarActive) {\n // Deactivate all EditBar controls (DrawPoint, DrawLine, etc.)\n // so no draw interaction stays active in the background.\n this.editBar.deactivateControls();\n }\n }\n\n // Activate / deactivate Select + Modify\n if (this._selectInteraction) {\n if (!this._editBarActive) {\n // Clear any current selection first\n this._selectInteraction.getFeatures().clear();\n }\n this._selectInteraction.setActive(this._editBarActive);\n }\n if (this._modifyInteraction) {\n this._modifyInteraction.setActive(this._editBarActive);\n }\n\n // Toggle SnapGuides — only active when both edit mode AND the user toggle are on\n if (this._snapGuides) {\n this._snapGuides.setActive(this._snapGuidesEnabled && this._editBarActive);\n }\n\n // Toggle TouchCursor\n if (this.touchCursor) {\n this.touchCursor.setActive(this._editBarActive);\n }\n\n // Clear persistent vertex highlights when leaving edit mode\n if (!this._editBarActive && this._vertexOverlaySource) {\n this._vertexOverlaySource.clear();\n }\n\n console.log('[MapView] Edit mode:', this._editBarActive ? 'ON' : 'OFF');\n }\n\n /**\n * Check whether edit mode (select / modify) is currently active.\n * @returns {boolean}\n */\n isEditMode() {\n return this._editBarActive;\n }\n\n // ============================================================================\n // Persistent Vertex Highlight Overlay\n // ============================================================================\n\n /**\n * Create a vector layer that renders a small dot at every vertex of any\n * currently-selected feature (polygon, multipolygon, line, multiline).\n * Only active while edit mode is on.\n *\n * Hooks:\n * - `select` event from the Select interaction → rebuild dots for the new selection\n * - `change` event on the selected feature → reposition dots when a vertex is dragged\n */\n _setupVertexOverlay() {\n this._vertexOverlaySource = new VectorSource();\n this._vertexOverlayLayer = new VectorLayer({\n title: '__vertex_highlight__',\n source: this._vertexOverlaySource,\n // Render above all other overlays but below ModifyFeature's hover indicator\n zIndex: 990,\n style: new Style({\n image: new Circle({\n radius: 4,\n fill: new Fill({ color: 'rgba(14,165,233,0.85)' }), // brand blue\n stroke: new Stroke({ color: '#fff', width: 1.2 }),\n }),\n }),\n });\n // Hide from LayerSwitcher — purely visual, not user-toggleable\n this._vertexOverlayLayer.set('displayInLayerSwitcher', false);\n this.map.addLayer(this._vertexOverlayLayer);\n\n // Bound handler so we can attach/detach by reference\n this._onSelectedFeatureGeomChange = () => this._refreshVertexOverlay();\n\n // Track which feature(s) we're listening on, so we can unhook cleanly\n this._vertexTrackedFeatures = new Set();\n\n // When the selection changes, swap which features we listen to and rebuild dots\n this._selectInteraction.on('select', () => this._refreshVertexOverlay());\n }\n\n /**\n * Rebuild the vertex overlay from the current Select interaction's features.\n * No-ops when not in edit mode.\n */\n _refreshVertexOverlay() {\n if (!this._vertexOverlaySource) return;\n this._vertexOverlaySource.clear();\n\n // Detach change listeners from previously-tracked features\n if (this._vertexTrackedFeatures) {\n for (const f of this._vertexTrackedFeatures) {\n f.un('change', this._onSelectedFeatureGeomChange);\n }\n this._vertexTrackedFeatures.clear();\n }\n\n if (!this._editBarActive || !this._selectInteraction) return;\n\n const selected = this._selectInteraction.getFeatures().getArray();\n for (const feat of selected) {\n const geom = feat.getGeometry();\n if (!geom) continue;\n const type = geom.getType();\n if (!['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'].includes(type)) {\n continue;\n }\n const coords = this._collectAllVertices(geom);\n for (const c of coords) {\n this._vertexOverlaySource.addFeature(new Feature(new Point(c)));\n }\n // Listen for vertex moves on this feature\n feat.on('change', this._onSelectedFeatureGeomChange);\n this._vertexTrackedFeatures.add(feat);\n }\n }\n\n /**\n * Walk a (Multi)Polygon or (Multi)LineString geometry and return the flat\n * list of vertex coordinates. Polygon rings have a duplicate closing vertex\n * (last == first) which is dropped here so we don't render two dots on top\n * of each other.\n *\n * @param {Geometry} geom\n * @returns {Array>}\n */\n _collectAllVertices(geom) {\n const out = [];\n const isCoord = (v) => Array.isArray(v) && typeof v[0] === 'number';\n\n const visitRing = (ring, isPolygonRing) => {\n const len = isPolygonRing && ring.length > 1 ? ring.length - 1 : ring.length;\n for (let i = 0; i < len; i++) out.push(ring[i]);\n };\n\n const type = geom.getType();\n const coords = geom.getCoordinates();\n\n switch (type) {\n case 'Polygon':\n // coords = [outerRing, hole1, hole2, …]\n for (const ring of coords) visitRing(ring, true);\n break;\n case 'MultiPolygon':\n // coords = [poly1, poly2, …]; each poly = [outerRing, hole1, …]\n for (const poly of coords) for (const ring of poly) visitRing(ring, true);\n break;\n case 'LineString':\n visitRing(coords, false);\n break;\n case 'MultiLineString':\n for (const line of coords) visitRing(line, false);\n break;\n default:\n // Fallback: deep walk to find arrays of [x, y]\n const walk = (v) => {\n if (isCoord(v)) out.push(v);\n else if (Array.isArray(v)) for (const sub of v) walk(sub);\n };\n walk(coords);\n }\n return out;\n }\n\n /**\n * Get the Drawings layer for external access.\n * @returns {VectorLayer}\n */\n getDrawingsLayer() {\n return this.drawingsLayer;\n }\n\n /**\n * Get the Drawings source for external access.\n * @returns {VectorSource}\n */\n getDrawingsSource() {\n return this.drawingsSource;\n }\n\n /**\n * Get the EditBar control for external access.\n * @returns {EditBar}\n */\n getEditBar() {\n return this.editBar;\n }\n\n /**\n * Update the ScaleBar units ('metric' or 'imperial').\n * @param {'metric'|'imperial'} system\n */\n setScaleBarUnits(system) {\n if (this.scaleBar) {\n this.scaleBar.setUnits(system === 'imperial' ? 'imperial' : 'metric');\n }\n }\n\n /**\n * Create the popup overlay element and add to map\n */\n createPopup() {\n // Create popup container element\n this.popupElement = document.createElement('div');\n this.popupElement.className = 'map-popup';\n this.popupElement.style.cssText = `\n position: absolute;\n background: var(--card, #fff);\n color: var(--card-foreground, #1e1a4b);\n border-radius: 8px;\n padding: 10px 14px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.25);\n font-family: var(--font-body, 'Exo', sans-serif);\n font-size: 13px;\n min-width: 150px;\n max-width: 280px;\n pointer-events: none;\n z-index: 1000;\n border: 1px solid var(--border, #1e1a4b1f);\n `;\n\n // Create the overlay\n this.popup = new Overlay({\n element: this.popupElement,\n positioning: 'bottom-center',\n offset: [0, -15],\n stopEvent: false,\n });\n\n this.map.addOverlay(this.popup);\n\n // Set up hover handler\n this.setupHoverPopup();\n }\n\n /**\n * Set up the hover popup behavior\n */\n setupHoverPopup() {\n let currentFeature = null;\n\n this.map.on('pointermove', (evt) => {\n if (evt.dragging) {\n this.hidePopup();\n return;\n }\n\n // Only find features that are location markers (have 'name' property)\n const feature = this.map.forEachFeatureAtPixel(evt.pixel, (f) => {\n // Only return features that have a 'name' property (location markers)\n if (f.get('name')) {\n return f;\n }\n return null;\n });\n\n if (feature && feature !== currentFeature) {\n currentFeature = feature;\n this.showPopup(feature, evt.coordinate);\n } else if (!feature && currentFeature) {\n currentFeature = null;\n this.hidePopup();\n }\n\n // Update cursor - only show pointer for location markers\n this.map.getTargetElement().style.cursor = feature ? 'pointer' : '';\n });\n\n // Hide popup when mouse leaves the map\n this.map.getTargetElement().addEventListener('mouseleave', () => {\n this.hidePopup();\n currentFeature = null;\n });\n }\n\n /**\n * Show popup with feature attributes\n */\n showPopup(feature, coordinate) {\n const name = feature.get('name') || 'Unnamed';\n const category = feature.get('category') || 'default';\n const description = feature.get('description');\n const lon = feature.get('lon');\n const lat = feature.get('lat');\n const emoji = this.getEmoji(category);\n\n // Build popup content\n let html = `\n
    \n ${emoji} ${this.escapeHtml(name)}\n
    \n `;\n\n // Category badge\n const categoryColors = {\n 'water': '#3b82f6',\n 'school': '#f59e0b',\n 'health': '#ef4444',\n 'market': '#8b5cf6',\n 'default': '#2d5016',\n 'other': '#6b7280'\n };\n const catColor = categoryColors[category] || '#6b7280';\n html += `\n
    \n ${category}\n
    \n `;\n\n // Description if available\n if (description) {\n html += `\n
    \n ${this.escapeHtml(description)}\n
    \n `;\n }\n\n // Coordinates\n if (lon !== undefined && lat !== undefined) {\n html += `\n
    \n ${Number(lon).toFixed(5)}, ${Number(lat).toFixed(5)}\n
    \n `;\n }\n\n this.popupElement.innerHTML = html;\n this.popup.setPosition(coordinate);\n }\n\n /**\n * Hide the popup\n */\n hidePopup() {\n this.popup.setPosition(undefined);\n }\n\n /**\n * Create the info popup overlay for double-click feature details\n */\n createInfoPopup() {\n this.infoPopupElement = document.createElement('div');\n this.infoPopupElement.className = 'map-info-popup';\n this.infoPopupElement.style.cssText = `\n position: absolute;\n background: var(--card, #fff);\n color: var(--card-foreground, #1e1a4b);\n border-radius: 10px;\n padding: 0;\n box-shadow: 0 4px 16px rgba(0,0,0,0.3);\n font-family: var(--font-body, 'Exo', sans-serif);\n font-size: 13px;\n min-width: 220px;\n max-width: 320px;\n max-height: 70vh;\n display: flex;\n flex-direction: column;\n z-index: 1001;\n border: 1px solid var(--border, #1e1a4b1f);\n overflow: hidden;\n `;\n\n this.infoPopup = new Overlay({\n element: this.infoPopupElement,\n positioning: 'bottom-center',\n offset: [0, -10],\n stopEvent: true,\n autoPan: true,\n autoPanAnimation: { duration: 250 },\n });\n\n this.map.addOverlay(this.infoPopup);\n }\n\n /**\n * Show the info popup with feature attributes and area\n * @param {Feature} feature - OpenLayers feature\n * @param {Array} coordinate - Map coordinate [x, y]\n * @param {Object} [options] - Display options\n * @param {string} [options.title='Feature Info'] - Popup header title\n * @param {string} [options.color='#e11d48'] - Header background colour\n */\n showInfoPopup(feature, coordinate, options = {}) {\n const { title = 'Feature Info', color = '#e11d48' } = options;\n const properties = feature.getProperties();\n const geometry = feature.getGeometry();\n const geomType = geometry.getType();\n\n // Build attributes table rows (skip geometry and internal keys)\n const skipKeys = ['geometry', '_layerType'];\n let rows = '';\n for (const [key, value] of Object.entries(properties)) {\n if (skipKeys.includes(key) || value === undefined || value === null) continue;\n rows += `\n \n ${this.escapeHtml(key)}\n ${this.escapeHtml(String(value))}\n \n `;\n }\n\n // Add measurement row based on geometry type\n if (geomType === 'Polygon' || geomType === 'MultiPolygon') {\n // Area for polygons\n const areaSqm = getArea(geometry, { projection: 'EPSG:3857' });\n const areaFormatted = formatAreaFull(areaSqm);\n rows += `\n \n area\n ${areaFormatted}\n \n `;\n } else if (geomType === 'LineString' || geomType === 'MultiLineString') {\n // Length for lines\n const lengthM = getLength(geometry, { projection: 'EPSG:3857' });\n const lengthFormatted = formatLengthFull(lengthM);\n rows += `\n \n length\n ${lengthFormatted}\n \n `;\n } else if (geomType === 'Point') {\n // Coordinates for points\n const coords = toLonLat(geometry.getCoordinates());\n const lon = coords[0].toFixed(6);\n const lat = coords[1].toFixed(6);\n rows += `\n \n longitude\n ${lon}\n \n \n latitude\n ${lat}\n \n `;\n }\n\n const html = `\n
    \n ${this.escapeHtml(title)}\n \n
    \n
    \n \n ${rows}\n
    \n
    \n `;\n\n this.infoPopupElement.innerHTML = html;\n this.infoPopup.setPosition(coordinate);\n\n // Close button handler\n this.infoPopupElement.querySelector('#info-popup-close').addEventListener('click', () => {\n this.hideInfoPopup();\n });\n }\n\n /**\n * Hide the info popup\n */\n hideInfoPopup() {\n this.infoPopup.setPosition(undefined);\n }\n\n // ============================================================================\n // Circle Intersection Analysis\n // ============================================================================\n\n /**\n * Analyse which features from overlay layers intersect a measurement circle\n * and show the results in the info popup.\n *\n * @param {Feature} circleFeature - The measurement circle feature (Circle geometry)\n * @param {Array} coordinate - Map coordinate for popup placement [x, y]\n */\n /**\n * Collect intersection results (parcels, zones, other) into a\n * structured { label, value } array for both HTML and PDF rendering.\n */\n _collectIntersectionRows(parcelFeatures, zoneFeatures, otherByLayer) {\n const dataRows = [];\n\n if (parcelFeatures.length > 0) {\n dataRows.push({ label: 'Parcels', value: String(parcelFeatures.length), color: '#0ea5e9' });\n }\n\n if (zoneFeatures.length > 0) {\n const names = zoneFeatures.map(f =>\n f.get('colzonename') || f.get('zone_name') || f.get('name') || 'unnamed'\n );\n dataRows.push({ label: 'Zones', value: String(zoneFeatures.length), color: '#7c3aed' });\n dataRows.push({ label: 'Zone Names', value: names.map(n => this.escapeHtml(n)).join(', '), color: '#7c3aed' });\n }\n\n for (const [title, features] of Object.entries(otherByLayer)) {\n dataRows.push({ label: this.escapeHtml(title), value: `${features.length} feature(s)` });\n }\n\n if (dataRows.length === 0) {\n dataRows.push({ label: '', value: 'No intersecting features found', empty: true });\n }\n\n return dataRows;\n }\n\n /**\n * Build the full popup HTML for an analysis popup (circle or area).\n *\n * @param {string} emoji - Header emoji\n * @param {string} title - e.g. \"Circle Analysis\"\n * @param {Array<{label:string, value:string, color?:string, empty?:boolean}>} dataRows\n * @returns {string} HTML\n */\n _buildAnalysisPopupHtml(emoji, title, dataRows) {\n let tableRows = '';\n for (const row of dataRows) {\n if (row.empty) {\n tableRows += `\n \n ${row.value}\n `;\n continue;\n }\n const labelColor = row.color || 'var(--muted-foreground, #7a7a7a)';\n const border = row._first ? '' : 'border-top:1px solid var(--border, #1e1a4b1f);';\n tableRows += `\n \n ${row.label}\n ${row.value}\n `;\n }\n\n return `\n
    \n ${emoji} ${title}\n \n
    \n
    \n \n ${tableRows}\n
    \n
    \n
    \n \n
    `;\n }\n\n /**\n * Show the analysis popup, attach close + PDF export handlers.\n */\n _showAnalysisPopup(emoji, title, dataRows, coordinate) {\n this.infoPopupElement.innerHTML = this._buildAnalysisPopupHtml(emoji, title, dataRows);\n this.infoPopup.setPosition(coordinate);\n\n this.infoPopupElement.querySelector('#info-popup-close').addEventListener('click', () => {\n this.hideInfoPopup();\n });\n\n // PDF export — dynamic import so jspdf is only loaded on demand\n this.infoPopupElement.querySelector('#info-popup-export-pdf')?.addEventListener('click', () => {\n // Strip HTML from values and remove the color/empty keys for the PDF\n const pdfRows = dataRows\n .filter(r => !r.empty)\n .map(r => ({ label: r.label, value: r.value.replace(/<[^>]*>/g, '') }));\n\n import('../pdf-export.js').then(({ exportAnalysisPDF }) => {\n exportAnalysisPDF({ title, rows: pdfRows });\n }).catch(err => {\n console.error('[MapView] PDF export failed:', err);\n });\n });\n }\n\n showCircleIntersectionPopup(circleFeature, coordinate) {\n const circleGeom = circleFeature.getGeometry();\n if (!circleGeom || typeof circleGeom.getCenter !== 'function') return;\n\n // Convert the OL Circle to a polygon (64 sides) for intersection testing\n const circlePoly = fromCircle(circleGeom, 64);\n const circleExtent = circlePoly.getExtent();\n\n const radius = circleFeature.get('_radius') || circleGeom.getRadius();\n\n // Collect intersecting features grouped by layer type\n const parcelFeatures = [];\n const zoneFeatures = [];\n const otherByLayer = {};\n\n const intersectsCircle = (feature) => {\n const geom = feature.getGeometry();\n if (!geom) return false;\n const fExtent = geom.getExtent();\n if (\n fExtent[2] < circleExtent[0] ||\n fExtent[0] > circleExtent[2] ||\n fExtent[3] < circleExtent[1] ||\n fExtent[1] > circleExtent[3]\n ) {\n return false;\n }\n return circlePoly.intersectsExtent(fExtent) && this._geometriesIntersect(circlePoly, geom);\n };\n\n const scanGroup = (group, groupTitle) => {\n group.getLayers().forEach((layer) => {\n if (layer instanceof LayerGroup) {\n scanGroup(layer, layer.get('title') || groupTitle);\n } else if (layer instanceof VectorLayer && layer.getVisible()) {\n const layerTitle = layer.get('title') || groupTitle || 'Unknown';\n const source = layer.getSource();\n if (!source) return;\n\n const candidates = source.getFeaturesInExtent(circleExtent);\n for (const f of candidates) {\n const fType = f.get('_layerType');\n if (fType === 'measure_circle' || fType === 'measure_circle_radius') continue;\n\n if (!intersectsCircle(f)) continue;\n\n if (fType === 'parcel') {\n parcelFeatures.push(f);\n } else if (fType === 'collector_zone') {\n zoneFeatures.push(f);\n } else {\n if (!otherByLayer[layerTitle]) otherByLayer[layerTitle] = [];\n otherByLayer[layerTitle].push(f);\n }\n }\n }\n });\n };\n\n scanGroup(this.overlayGroup, 'Overlays');\n\n // Build structured data rows\n const radiusFormatted = formatLength(radius);\n const areaSqm = Math.PI * radius * radius;\n const areaFormatted = formatArea(areaSqm);\n\n const dataRows = [\n { label: 'Radius', value: radiusFormatted, _first: true },\n { label: 'Area', value: areaFormatted },\n ...this._collectIntersectionRows(parcelFeatures, zoneFeatures, otherByLayer),\n ];\n\n this._showAnalysisPopup('⭕', 'Circle Analysis', dataRows, coordinate);\n }\n\n /**\n * Show an intersection-analysis popup for a measured area polygon.\n * Same logic as showCircleIntersectionPopup but works with an\n * arbitrary Polygon geometry instead of a circle.\n *\n * @param {Feature} polygonFeature - The measure_area feature\n * @param {number[]} coordinate - Map coordinate for the popup anchor\n */\n showAreaIntersectionPopup(polygonFeature, coordinate) {\n const polyGeom = polygonFeature.getGeometry();\n if (!polyGeom) return;\n\n const polyExtent = polyGeom.getExtent();\n\n // Compute area via ol/sphere for geodesic accuracy\n const areaSqm = getArea(polyGeom, { projection: 'EPSG:3857' });\n const areaFormatted = formatArea(areaSqm);\n\n // Compute perimeter\n const perimeterM = getLength(polyGeom, { projection: 'EPSG:3857' });\n const perimeterFormatted = formatLength(perimeterM);\n\n // Collect intersecting features grouped by layer type\n const parcelFeatures = [];\n const zoneFeatures = [];\n const otherByLayer = {};\n\n const intersectsPoly = (feature) => {\n const geom = feature.getGeometry();\n if (!geom) return false;\n const fExtent = geom.getExtent();\n if (\n fExtent[2] < polyExtent[0] ||\n fExtent[0] > polyExtent[2] ||\n fExtent[3] < polyExtent[1] ||\n fExtent[1] > polyExtent[3]\n ) {\n return false;\n }\n return polyGeom.intersectsExtent(fExtent) && this._geometriesIntersect(polyGeom, geom);\n };\n\n const scanGroup = (group, groupTitle) => {\n group.getLayers().forEach((layer) => {\n if (layer instanceof LayerGroup) {\n scanGroup(layer, layer.get('title') || groupTitle);\n } else if (layer instanceof VectorLayer && layer.getVisible()) {\n const layerTitle = layer.get('title') || groupTitle || 'Unknown';\n const source = layer.getSource();\n if (!source) return;\n\n const candidates = source.getFeaturesInExtent(polyExtent);\n for (const f of candidates) {\n const fType = f.get('_layerType');\n if (fType === 'measure_area' || fType === 'measure_circle' || fType === 'measure_circle_radius') continue;\n\n if (!intersectsPoly(f)) continue;\n\n if (fType === 'parcel') {\n parcelFeatures.push(f);\n } else if (fType === 'collector_zone') {\n zoneFeatures.push(f);\n } else {\n if (!otherByLayer[layerTitle]) otherByLayer[layerTitle] = [];\n otherByLayer[layerTitle].push(f);\n }\n }\n }\n });\n };\n\n scanGroup(this.overlayGroup, 'Overlays');\n\n // Build structured data rows\n const dataRows = [\n { label: 'Area', value: areaFormatted, _first: true },\n { label: 'Perimeter', value: perimeterFormatted },\n ...this._collectIntersectionRows(parcelFeatures, zoneFeatures, otherByLayer),\n ];\n\n this._showAnalysisPopup('📐', 'Area Analysis', dataRows, coordinate);\n }\n\n /**\n * Test whether two geometries truly intersect (beyond just extent overlap).\n * Works for Polygon/MultiPolygon against any geometry type.\n *\n * @param {Geometry} geomA - First geometry (usually the circle polygon)\n * @param {Geometry} geomB - Second geometry\n * @returns {boolean}\n * @private\n */\n _geometriesIntersect(geomA, geomB) {\n const typeB = geomB.getType();\n\n // For polygons / multi-polygons: check if any coordinate of B is inside A,\n // or if any coordinate of A is inside B (covers overlap & containment).\n if (typeB === 'Polygon' || typeB === 'MultiPolygon') {\n // Check if any vertex of B lies inside A (use flatCoordinates for efficiency)\n const flatB = geomB.getFlatCoordinates();\n const stride = geomB.getStride();\n for (let i = 0; i < flatB.length; i += stride) {\n if (geomA.intersectsCoordinate([flatB[i], flatB[i + 1]])) return true;\n }\n // Check if any vertex of A lies inside B\n const flatA = geomA.getFlatCoordinates();\n const strideA = geomA.getStride();\n for (let i = 0; i < flatA.length; i += strideA) {\n if (geomB.intersectsCoordinate([flatA[i], flatA[i + 1]])) return true;\n }\n return false;\n }\n\n if (typeB === 'Point') {\n return geomA.intersectsCoordinate(geomB.getCoordinates());\n }\n\n if (typeB === 'LineString' || typeB === 'MultiLineString') {\n const flatB = geomB.getFlatCoordinates();\n const stride = geomB.getStride();\n for (let i = 0; i < flatB.length; i += stride) {\n if (geomA.intersectsCoordinate([flatB[i], flatB[i + 1]])) return true;\n }\n return false;\n }\n\n // Fallback: extent overlap is good enough\n return true;\n }\n\n // ============================================================================\n // Parcel Edit Popup (single-click editable form)\n // ============================================================================\n\n /**\n * Create the parcel edit popup overlay with a dynamic form.\n */\n createParcelEditPopup() {\n this.parcelEditElement = document.createElement('div');\n this.parcelEditElement.className = 'map-parcel-edit-popup';\n this.parcelEditElement.style.cssText = `\n position: absolute;\n background: var(--card, #fff);\n color: var(--card-foreground, #1e1a4b);\n border-radius: 10px;\n box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n font-family: var(--font-body, 'Exo', sans-serif);\n font-size: 13px;\n min-width: 280px;\n max-width: 360px;\n max-height: 420px;\n z-index: 1002;\n border: 2px solid var(--primary, #005eb8);\n overflow: hidden;\n display: flex;\n flex-direction: column;\n `;\n\n this.parcelEditPopup = new Overlay({\n element: this.parcelEditElement,\n positioning: 'bottom-center',\n offset: [0, -10],\n stopEvent: true,\n autoPan: true,\n autoPanAnimation: { duration: 250 },\n });\n\n this.map.addOverlay(this.parcelEditPopup);\n\n // Callbacks for save events\n this._parcelEditCallbacks = [];\n // Track the current feature being edited\n this._parcelEditFeature = null;\n }\n\n /**\n * Show the parcel edit popup with an editable form for all feature attributes.\n * Internal keys (_layerType, geometry) are excluded from the form.\n *\n * @param {Feature} feature - The OL feature to edit\n * @param {Array} coordinate - Map coordinate [x, y]\n */\n showParcelEditPopup(feature, coordinate) {\n this._parcelEditFeature = feature;\n const properties = feature.getProperties();\n\n // Keys to skip in the form\n const skipKeys = ['geometry', '_layerType'];\n\n // Build form fields from feature properties\n let fieldsHtml = '';\n for (const [key, value] of Object.entries(properties)) {\n if (skipKeys.includes(key)) continue;\n const displayVal = (value === null || value === undefined) ? '' : String(value);\n const escapedKey = this.escapeHtml(key);\n const escapedVal = this.escapeHtml(displayVal);\n fieldsHtml += `\n
    \n \n \n
    \n `;\n }\n\n const html = `\n
    \n ✏️ Edit Parcel\n \n
    \n
    \n ${fieldsHtml}\n
    \n \n \n
    \n
    \n `;\n\n this.parcelEditElement.innerHTML = html;\n this.parcelEditPopup.setPosition(coordinate);\n\n // Close / Cancel handlers\n this.parcelEditElement.querySelector('.parcel-edit-close').addEventListener('click', () => {\n this.hideParcelEditPopup();\n });\n this.parcelEditElement.querySelector('.parcel-edit-cancel').addEventListener('click', () => {\n this.hideParcelEditPopup();\n });\n\n // Form submit handler\n const form = this.parcelEditElement.querySelector('.parcel-edit-form');\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n\n // Collect all edited values\n const formData = new FormData(form);\n const updatedProps = {};\n for (const [key, value] of formData.entries()) {\n updatedProps[key] = value;\n }\n\n // Restore internal properties that were excluded from the form\n updatedProps._layerType = 'parcel';\n\n // Update the feature's properties in-place\n for (const [key, value] of Object.entries(updatedProps)) {\n this._parcelEditFeature.set(key, value);\n }\n\n // Notify external listeners\n for (const cb of this._parcelEditCallbacks) {\n cb(this._parcelEditFeature, updatedProps);\n }\n\n this.hideParcelEditPopup();\n });\n }\n\n /**\n * Hide the parcel edit popup.\n */\n hideParcelEditPopup() {\n this.parcelEditPopup.setPosition(undefined);\n this._parcelEditFeature = null;\n }\n\n /**\n * Register a callback for when a parcel edit is saved.\n * Callback receives (feature, updatedProperties).\n *\n * @param {Function} callback\n */\n onParcelEdit(callback) {\n this._parcelEditCallbacks.push(callback);\n }\n\n // ============================================================================\n // Merge Identifier (UPN) Chooser Popup\n // ============================================================================\n\n /**\n * Create the merge identifier popup overlay.\n * Shown after two parcels are merged so the user can choose which UPN to keep.\n */\n createMergePopup() {\n this.mergePopupElement = document.createElement('div');\n this.mergePopupElement.className = 'map-merge-popup';\n this.mergePopupElement.style.cssText = `\n position: absolute;\n background: var(--card, #fff);\n color: var(--card-foreground, #1e1a4b);\n border-radius: 10px;\n box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n font-family: var(--font-body, 'Exo', sans-serif);\n font-size: 13px;\n min-width: 280px;\n max-width: 360px;\n z-index: 1002;\n border: 2px solid #10b981;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n `;\n\n this.mergePopup = new Overlay({\n element: this.mergePopupElement,\n positioning: 'bottom-center',\n offset: [0, -10],\n stopEvent: true,\n autoPan: true,\n autoPanAnimation: { duration: 250 },\n });\n\n this.map.addOverlay(this.mergePopup);\n }\n\n /**\n * Show the merge identifier popup so the user can pick which parcel's\n * attributes (including UPN) the merged polygon should inherit.\n *\n * @param {Feature} mergedFeature The newly created merged feature\n * @param {Object} propsA Properties from original parcel A\n * @param {Object} propsB Properties from original parcel B\n * @param {Array} coordinate Map coordinate [x, y] for popup placement\n */\n showMergeIdentifierPopup(mergedFeature, propsA, propsB, coordinate) {\n // Extract identifiers — try common parcel ID field names\n const idFields = ['UPN', 'upn', 'id', 'parcelid', 'parcel_id', 'PARCELID', 'PARCEL_ID', 'ID'];\n const getLabel = (props) => {\n for (const field of idFields) {\n if (props[field] !== undefined && props[field] !== null && String(props[field]).trim()) {\n return { field, value: String(props[field]) };\n }\n }\n return { field: 'id', value: 'Unknown' };\n };\n\n const labelA = getLabel(propsA);\n const labelB = getLabel(propsB);\n\n const html = `\n
    \n 🔗 Merged Parcel — Choose Identifier\n \n
    \n
    \n

    \n Select which parcel's attributes the merged polygon should keep:\n

    \n \n \n
    \n \n \n
    \n
    \n `;\n\n this.mergePopupElement.innerHTML = html;\n this.mergePopup.setPosition(coordinate);\n\n // Close / Cancel — keep parcel A properties (the default from clone)\n const close = () => {\n this.mergePopup.setPosition(undefined);\n };\n this.mergePopupElement.querySelector('.merge-popup-close').addEventListener('click', close);\n this.mergePopupElement.querySelector('.merge-popup-cancel').addEventListener('click', close);\n\n // Confirm — apply chosen parcel's properties\n this.mergePopupElement.querySelector('.merge-popup-confirm').addEventListener('click', () => {\n const choice = this.mergePopupElement.querySelector('input[name=\"merge-choice\"]:checked').value;\n const chosenProps = choice === 'A' ? propsA : propsB;\n\n // Copy all properties (except geometry) onto the merged feature\n const skipKeys = ['geometry'];\n for (const [key, value] of Object.entries(chosenProps)) {\n if (skipKeys.includes(key)) continue;\n mergedFeature.set(key, value);\n }\n // Ensure _layerType is preserved\n mergedFeature.set('_layerType', 'parcel');\n\n // Notify parcel edit callbacks\n for (const cb of this._parcelEditCallbacks) {\n cb(mergedFeature, chosenProps);\n }\n\n close();\n });\n\n // Highlight radio labels on selection\n const labels = this.mergePopupElement.querySelectorAll('label');\n const radios = this.mergePopupElement.querySelectorAll('input[name=\"merge-choice\"]');\n const updateHighlight = () => {\n labels.forEach((lbl) => {\n const radio = lbl.querySelector('input');\n lbl.style.borderColor = radio.checked ? (radio.value === 'A' ? '#0ea5e9' : '#f59e0b') : 'var(--border, #1e1a4b1f)';\n });\n };\n radios.forEach((r) => r.addEventListener('change', updateHighlight));\n updateHighlight();\n }\n\n // ============================================================================\n // Divide Polygon Popup (number input)\n // ============================================================================\n\n /**\n * Create the divide polygon popup overlay.\n * Shown after the user selects a polygon with the Divide tool, so they\n * can enter the number of equal pieces.\n */\n createDividePopup() {\n this.dividePopupElement = document.createElement('div');\n this.dividePopupElement.className = 'map-divide-popup';\n this.dividePopupElement.style.cssText = `\n position: absolute;\n background: var(--card, #fff);\n color: var(--card-foreground, #1e1a4b);\n border-radius: 10px;\n box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n font-family: var(--font-body, 'Exo', sans-serif);\n font-size: 13px;\n min-width: 260px;\n max-width: 320px;\n z-index: 1002;\n border: 2px solid #8b5cf6;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n `;\n\n this.dividePopup = new Overlay({\n element: this.dividePopupElement,\n positioning: 'bottom-center',\n offset: [0, -10],\n stopEvent: true,\n autoPan: true,\n autoPanAnimation: { duration: 250 },\n });\n\n this.map.addOverlay(this.dividePopup);\n }\n\n /**\n * Show the divide popup so the user can enter the number of divisions.\n *\n * @param {Feature} feature The selected polygon feature\n * @param {VectorSource} source The source containing the feature\n * @param {Array} coordinate Map coordinate [x, y] for popup placement\n */\n showDividePopup(feature, source, coordinate) {\n const html = `\n
    \n Divide Polygon\n \n
    \n
    \n

    \n Enter the number of equal pieces:\n

    \n \n
    \n \n \n
    \n
    \n `;\n\n this.dividePopupElement.innerHTML = html;\n this.dividePopup.setPosition(coordinate);\n\n const input = this.dividePopupElement.querySelector('.divide-input');\n input.focus();\n input.select();\n\n // Close / Cancel\n const cancel = () => {\n this.hideDividePopup();\n this._polygonDivideInteraction.cancelDivide();\n };\n this.dividePopupElement.querySelector('.divide-popup-close').addEventListener('click', cancel);\n this.dividePopupElement.querySelector('.divide-popup-cancel').addEventListener('click', cancel);\n\n // Confirm\n this.dividePopupElement.querySelector('.divide-popup-confirm').addEventListener('click', () => {\n const n = parseInt(input.value, 10);\n if (!n || n < 2) {\n input.style.borderColor = '#ef4444';\n return;\n }\n this.hideDividePopup();\n this._polygonDivideInteraction.performDivide(n);\n });\n\n // Allow Enter key to confirm\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n this.dividePopupElement.querySelector('.divide-popup-confirm').click();\n }\n });\n }\n\n /**\n * Hide the divide popup.\n */\n hideDividePopup() {\n this.dividePopup.setPosition(undefined);\n }\n\n // ============================================================================\n // Drawn Polygon Attribute Popup\n // ============================================================================\n\n /**\n * Create the drawn polygon attribute popup overlay.\n * Shown after the area measurement polygon is completed so the user can\n * attach parcel-like attributes to the drawn polygon.\n */\n createDrawnPolygonPopup() {\n this.drawnPolygonElement = document.createElement('div');\n this.drawnPolygonElement.className = 'map-drawn-polygon-popup';\n this.drawnPolygonElement.style.cssText = `\n position: absolute;\n background: var(--card, #fff);\n border-radius: var(--radius-xl, 0.75rem);\n box-shadow: 0 4px 20px rgba(0,0,0,0.2);\n font-family: var(--font-body, 'Exo', sans-serif);\n font-size: 13px;\n min-width: 280px;\n max-width: 360px;\n max-height: 420px;\n z-index: 1002;\n border: 2px solid var(--success, #006b3f);\n overflow: hidden;\n display: flex;\n flex-direction: column;\n `;\n\n this.drawnPolygonPopup = new Overlay({\n element: this.drawnPolygonElement,\n positioning: 'bottom-center',\n offset: [0, -10],\n stopEvent: true,\n autoPan: true,\n autoPanAnimation: { duration: 250 },\n });\n\n this.map.addOverlay(this.drawnPolygonPopup);\n this._drawnPolygonCallbacks = [];\n this._drawnPolygonFeature = null;\n }\n\n /**\n * Get attribute keys from existing parcel features on the map.\n * Scans the overlay group for the first feature with _layerType='parcel'\n * and returns its property key names (excluding internal keys).\n *\n * @returns {string[]} Array of attribute key names\n */\n getParcelAttributeKeys() {\n const skipKeys = ['geometry', '_layerType'];\n const keys = [];\n\n const scanGroup = (group) => {\n if (keys.length > 0) return;\n group.getLayers().forEach((layer) => {\n if (keys.length > 0) return;\n if (layer instanceof LayerGroup) {\n scanGroup(layer);\n } else if (layer instanceof VectorLayer) {\n const source = layer.getSource();\n if (!source) return;\n for (const f of source.getFeatures()) {\n if (f.get('_layerType') !== 'parcel') continue;\n const props = f.getProperties();\n for (const key of Object.keys(props)) {\n if (!skipKeys.includes(key)) keys.push(key);\n }\n return; // one parcel is enough for the schema\n }\n }\n });\n };\n\n scanGroup(this.overlayGroup);\n return keys;\n }\n\n /**\n * Show the drawn polygon attribute popup.\n * Discovers attribute keys from existing parcel features and creates\n * a blank form with those fields.\n *\n * @param {Feature} feature - The drawn polygon feature\n * @param {Array} coordinate - Map coordinate [x, y] for popup placement\n */\n showDrawnPolygonPopup(feature, coordinate) {\n this._drawnPolygonFeature = feature;\n\n // Discover attribute keys from existing parcels\n const attributeKeys = this.getParcelAttributeKeys();\n\n if (attributeKeys.length === 0) {\n console.warn('[MapView] No parcel attributes found — cannot build form');\n return;\n }\n\n // Build form fields (all blank)\n let fieldsHtml = '';\n for (const key of attributeKeys) {\n const escapedKey = this.escapeHtml(key);\n fieldsHtml += `\n
    \n \n \n
    \n `;\n }\n\n // Area display\n const geom = feature.getGeometry();\n const areaSqm = getArea(geom, { projection: 'EPSG:3857' });\n const areaFormatted = formatArea(areaSqm);\n\n const html = `\n
    \n 📐 Polygon Attributes\n \n
    \n
    \n Area: ${areaFormatted}\n
    \n
    \n ${fieldsHtml}\n
    \n \n \n
    \n
    \n `;\n\n this.drawnPolygonElement.innerHTML = html;\n this.drawnPolygonPopup.setPosition(coordinate);\n\n // Close / Cancel handlers\n this.drawnPolygonElement.querySelector('.drawn-polygon-close').addEventListener('click', () => {\n this.hideDrawnPolygonPopup();\n });\n this.drawnPolygonElement.querySelector('.drawn-polygon-cancel').addEventListener('click', () => {\n this.hideDrawnPolygonPopup();\n });\n\n // Form submit handler\n const form = this.drawnPolygonElement.querySelector('.drawn-polygon-form');\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n\n const formData = new FormData(form);\n const props = {};\n for (const [key, value] of formData.entries()) {\n props[key] = value;\n }\n\n // Set properties on the feature\n for (const [key, value] of Object.entries(props)) {\n this._drawnPolygonFeature.set(key, value);\n }\n\n // Tag as parcel so it integrates with existing parcel tools\n this._drawnPolygonFeature.set('_layerType', 'parcel');\n\n // Notify listeners\n for (const cb of this._drawnPolygonCallbacks) {\n cb(this._drawnPolygonFeature, props);\n }\n\n this.hideDrawnPolygonPopup();\n });\n }\n\n /**\n * Hide the drawn polygon attribute popup.\n */\n hideDrawnPolygonPopup() {\n this.drawnPolygonPopup.setPosition(undefined);\n this._drawnPolygonFeature = null;\n }\n\n /**\n * Register a callback for when drawn polygon attributes are saved.\n * Callback receives (feature, properties).\n *\n * @param {Function} callback\n */\n onDrawnPolygonSave(callback) {\n this._drawnPolygonCallbacks.push(callback);\n }\n\n /**\n * Register a double-click callback.\n * Callback receives (lon, lat, feature, event).\n * Feature is the first feature found at the click pixel across all overlay layers,\n * or null if no feature was hit.\n * When a feature is hit, the default double-click-zoom is suppressed.\n */\n onDblClick(callback) {\n this.dblClickCallbacks.push(callback);\n\n // Set up the listener once\n if (this.dblClickCallbacks.length === 1) {\n this.map.on('dblclick', (evt) => {\n const [lon, lat] = toLonLat(evt.coordinate);\n\n // Find any feature at the clicked pixel (overlay layers, not just markers)\n let clickedFeature = null;\n this.map.forEachFeatureAtPixel(evt.pixel, (feature) => {\n clickedFeature = feature;\n return true; // stop at first hit\n });\n\n // If a feature was hit, prevent the default double-click zoom\n if (clickedFeature) {\n evt.preventDefault();\n evt.stopPropagation();\n }\n\n // Call all registered callbacks\n for (const cb of this.dblClickCallbacks) {\n cb(lon, lat, clickedFeature, evt);\n }\n\n // Return false to suppress DoubleClickZoom interaction when on a feature\n if (clickedFeature) return false;\n });\n }\n\n return () => {\n const idx = this.dblClickCallbacks.indexOf(callback);\n if (idx > -1) this.dblClickCallbacks.splice(idx, 1);\n };\n }\n\n /**\n * Escape HTML to prevent XSS\n */\n escapeHtml(text) {\n if (!text) return '';\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n /**\n * Create the Add Location popup form overlay\n */\n createAddLocationPopup() {\n // Create popup container element\n this.addLocationPopupElement = document.createElement('div');\n this.addLocationPopupElement.className = 'map-add-location-popup';\n this.addLocationPopupElement.innerHTML = `\n
    \n ➕ Add Location\n \n
    \n
    \n
    \n \n \n
    \n
    \n \n \n
    \n
    \n \n \n
    \n
    \n 📍 \n
    \n \n
    \n `;\n\n // Create the overlay\n this.addLocationPopup = new Overlay({\n element: this.addLocationPopupElement,\n positioning: 'bottom-center',\n offset: [0, -10],\n stopEvent: true, // Prevent click from propagating\n autoPan: true,\n autoPanAnimation: {\n duration: 250,\n },\n });\n\n this.map.addOverlay(this.addLocationPopup);\n\n // Store clicked coordinates\n this.addLocationCoords = null;\n\n // Set up close button handler\n const closeBtn = this.addLocationPopupElement.querySelector('.add-location-popup-close');\n closeBtn.addEventListener('click', () => {\n this.hideAddLocationPopup();\n });\n\n // Store form submit callbacks\n this.addLocationCallbacks = [];\n }\n\n /**\n * Show the Add Location popup at the specified coordinate\n */\n showAddLocationPopup(coordinate) {\n const [lon, lat] = toLonLat(coordinate);\n this.addLocationCoords = { lon, lat };\n\n // Update coordinates display\n const coordsEl = this.addLocationPopupElement.querySelector('#map-location-coords');\n coordsEl.textContent = `${lon.toFixed(6)}, ${lat.toFixed(6)}`;\n\n // Reset form\n const form = this.addLocationPopupElement.querySelector('#map-add-location-form');\n form.reset();\n\n // Position and show popup\n this.addLocationPopup.setPosition(coordinate);\n }\n\n /**\n * Hide the Add Location popup\n */\n hideAddLocationPopup() {\n this.addLocationPopup.setPosition(undefined);\n this.addLocationCoords = null;\n }\n\n /**\n * Register a callback for when a location is submitted via the map popup\n * Callback receives: { name, category, description, lon, lat }\n */\n onAddLocation(callback) {\n this.addLocationCallbacks.push(callback);\n\n // Set up form submit handler (only once)\n if (this.addLocationCallbacks.length === 1) {\n const form = this.addLocationPopupElement.querySelector('#map-add-location-form');\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n\n if (!this.addLocationCoords) return;\n\n const formData = new FormData(form);\n const data = {\n name: formData.get('name'),\n category: formData.get('category'),\n description: formData.get('description'),\n lon: this.addLocationCoords.lon,\n lat: this.addLocationCoords.lat,\n };\n\n // Call all registered callbacks\n this.addLocationCallbacks.forEach(cb => cb(data));\n\n // Hide popup after submission\n this.hideAddLocationPopup();\n });\n }\n }\n\n /**\n * Create base layers group for LayerSwitcher\n */\n createBaseLayers(defaultBasemap) {\n\n\n const topoLayer = new TileLayer({\n title: 'Topographic',\n type: 'base',\n zIndex: -100,\n visible: defaultBasemap === 'topo',\n source: new XYZ({\n url: 'https://{a-c}.tile.opentopomap.org/{z}/{x}/{y}.png',\n attributions: 'Map data: © OpenTopoMap',\n maxZoom: 17,\n crossOrigin: 'anonymous',\n }),\n });\n topoLayer.set('basemapKey', 'topo');\n\n const cartoLightLayer = new TileLayer({\n title: 'Carto Light',\n type: 'base',\n zIndex: -100,\n visible: defaultBasemap === 'carto-light',\n source: new XYZ({\n url: 'https://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',\n attributions: '© CARTO',\n maxZoom: 19,\n crossOrigin: 'anonymous',\n }),\n });\n cartoLightLayer.set('basemapKey', 'carto-light');\n\n const cartoDarkLayer = new TileLayer({\n title: 'Carto Dark',\n type: 'base',\n zIndex: -100,\n visible: defaultBasemap === 'carto-dark',\n source: new XYZ({\n url: 'https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png',\n attributions: '© CARTO',\n maxZoom: 19,\n crossOrigin: 'anonymous',\n }),\n });\n cartoDarkLayer.set('basemapKey', 'carto-dark');\n\n const osmCycleLayer = new TileLayer({\n title: 'OSM Cycle map',\n type: 'base',\n zIndex: -100,\n visible: false, //defaultBasemap === 'osm',\n source: new OSM({\n \t\t\t\t\t\"url\" : \"https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=ae1339c46dd3446b9c491e7336d38760\"\n\t\t\t\t\t\t}),\n });\n\n osmCycleLayer.set('basemapKey', 'cycle');\n\n const satelliteLayer = new TileLayer({\n title: 'Satellite',\n type: 'base',\n zIndex: -100,\n visible: defaultBasemap === 'satellite',\n source: new XYZ({\n url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',\n attributions: 'Tiles © Esri',\n maxZoom: 19,\n crossOrigin: 'anonymous',\n }),\n });\n satelliteLayer.set('basemapKey', 'satellite');\n const googleLayer = new TileLayer({\n title: 'Google Sat',\n type: 'base',\n zIndex: -100,\n visible: defaultBasemap === 'googlesat',\n source: new XYZ({\n// url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',\n url: 'http://mt0.google.com/vt/lyrs=y&hl=en&x={x}&y={y}&z={z}&s=Ga',\n attributions: 'Tiles © Google',\n maxZoom: 19,\n crossOrigin: 'anonymous',\n }),\n });\n googleLayer.set('basemapKey', 'googlesat');\n\n const osmLayer = new TileLayer({\n title: 'OpenStreetMap',\n type: 'base',\n zIndex: -100,\n visible: defaultBasemap === 'osm',\n source: new OSM(),\n });\n osmLayer.set('basemapKey', 'osm');\n\n // Remember the base-map layers so setBaseMap() can toggle visibility later\n this._baseMapLayers = [\n cartoLightLayer, cartoDarkLayer, osmCycleLayer,\n satelliteLayer, googleLayer, osmLayer, topoLayer,\n ];\n\n\n // Return LayerGroup. Hidden from the main LayerSwitcher — base maps are\n // managed by the dedicated base-map picker (see _createBaseMapPicker)\n // accessed via the layers-stack icon above the My Location button.\n const baseGroup = new LayerGroup({\n title: 'Base Maps',\n layers: [\n cartoLightLayer,\n cartoDarkLayer,\n satelliteLayer,\n osmCycleLayer,\n googleLayer,\n osmLayer,\n topoLayer,\n ],\n });\n baseGroup.set('displayInLayerSwitcher', false);\n return baseGroup;\n }\n\n /**\n * Switch the active base map by key.\n * Sets exactly one base layer visible; hides all others.\n *\n * @param {string} key Basemap key: 'none' | 'topo' | 'osm' | 'satellite' | 'googlesat' | 'carto-light' | 'carto-dark' | 'cycle'\n * @returns {boolean} true if the key matched a known base layer (or 'none')\n */\n setBaseMap(key) {\n if (!this._baseMapLayers) return false;\n // 'none' switches the base map off entirely — hide every base layer so the\n // map renders on a blank background (useful over imagery overlays / when a\n // full-coverage overlay should stand alone).\n if (key === 'none') {\n for (const layer of this._baseMapLayers) layer.setVisible(false);\n console.log('[MapView] Base map switched off (none)');\n this.map.dispatchEvent({ type: 'basemapchange', key: 'none' });\n return true;\n }\n let matched = false;\n for (const layer of this._baseMapLayers) {\n const on = layer.get('basemapKey') === key;\n layer.setVisible(on);\n if (on) matched = true;\n }\n if (matched) {\n console.log('[MapView] Base map switched to:', key);\n // Notify external UIs (Settings dropdown, base-map picker, …) so they\n // can keep their visible state in sync.\n this.map.dispatchEvent({ type: 'basemapchange', key });\n }\n return matched;\n }\n\n /**\n * Build the floating \"Base Map\" picker — a small icon button stacked\n * directly above the My Location control, plus a slide-out card with\n * thumbnail chips for every selectable base map.\n *\n * Hidden in tandem with the main LayerSwitcher: clicking outside the\n * picker (or making a selection) closes it.\n *\n * Two-way sync with the existing Settings dropdown is via the\n * `basemapchange` event fired from setBaseMap().\n */\n _createBaseMapPicker() {\n // Configuration — must match the basemapKey set in createBaseLayers.\n // The colour gradients hint at each base map's character so the chip is\n // recognisable without rendering an actual tile preview.\n const OPTIONS = [\n { key: 'topo', label: 'Topographic', grad: 'linear-gradient(135deg,#e8d5b7,#a67c52)' },\n { key: 'osm', label: 'OpenStreetMap',grad: 'linear-gradient(135deg,#d4e6f1,#85c1e9)' },\n { key: 'satellite', label: 'Satellite', grad: 'linear-gradient(135deg,#1b4332,#40916c)' },\n { key: 'googlesat', label: 'Google Sat', grad: 'linear-gradient(135deg,#2a5d3d,#4a8c5a)' },\n { key: 'carto-light', label: 'Carto Light', grad: 'linear-gradient(135deg,#f5f5f5,#d4d4d4)' },\n { key: 'carto-dark', label: 'Carto Dark', grad: 'linear-gradient(135deg,#1a1a2e,#0f3460)' },\n // \"None\" turns the base map off — checkerboard hints at a blank/transparent background.\n { key: 'none', label: 'None', grad: 'repeating-conic-gradient(#e5e7eb 0 25%, #fff 0 50%) 50% / 12px 12px' },\n ];\n\n const target = this.map.getTargetElement();\n if (!target) return;\n\n // ---------- Toggle button ----------\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'ls-basemap-toggle';\n btn.title = 'Switch base map';\n btn.setAttribute('aria-label', 'Switch base map');\n btn.innerHTML =\n '' +\n '' +\n '' +\n '' +\n '';\n target.appendChild(btn);\n\n // ---------- Picker panel ----------\n const panel = document.createElement('div');\n panel.className = 'ls-basemap-panel';\n panel.innerHTML =\n '
    Base Map
    ' +\n '
    ' +\n OPTIONS.map((opt) => `\n \n `).join('') +\n '
    ';\n target.appendChild(panel);\n\n this._basemapPanel = panel;\n this._basemapToggle = btn;\n\n /** Mark the radio matching the currently-visible base layer. */\n const syncSelection = (key) => {\n const k = key || this._baseMapLayers?.find((l) => l.getVisible())?.get('basemapKey');\n panel.querySelectorAll('input[name=\"lupmis-basemap\"]').forEach((r) => {\n r.checked = (r.value === k);\n });\n };\n syncSelection();\n\n // ---------- Events ----------\n\n // Toggle button → open / close the panel\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n const open = !panel.classList.contains('open');\n panel.classList.toggle('open', open);\n btn.classList.toggle('active', open);\n if (open) syncSelection();\n });\n\n // Click outside → close\n document.addEventListener('click', (e) => {\n if (!panel.classList.contains('open')) return;\n if (panel.contains(e.target) || btn.contains(e.target)) return;\n panel.classList.remove('open');\n btn.classList.remove('active');\n });\n\n // Selection → apply, persist, close\n panel.addEventListener('change', (e) => {\n const radio = e.target.closest('input[type=radio][name=\"lupmis-basemap\"]');\n if (!radio) return;\n const key = radio.value;\n this.setBaseMap(key);\n try { localStorage.setItem('default-basemap', key); } catch {}\n panel.classList.remove('open');\n btn.classList.remove('active');\n });\n\n // Keep the radio state synced when other UIs (Settings dropdown) change it\n this.map.on('basemapchange', (evt) => syncSelection(evt.key));\n }\n\n // ============================================================================\n // GPS: current-position + trail rendering, and the expandable Location control\n //\n // NOTE: MapView deliberately knows nothing about the GeoTracker engine,\n // SQLocal, or sync. It only (a) renders what it's told and (b) emits UI\n // intents via callbacks. main.js wires those intents to the GeoTracker so the\n // map stays reusable/decoupled.\n // ============================================================================\n\n /** Create the vector layers used to draw the live position and the trail. */\n _initGpsRendering() {\n this._gpsPositionSource = new VectorSource();\n this._gpsTrailSource = new VectorSource();\n this._gpsTrailCoords = []; // [ [x,y], ... ] in map projection\n\n // Trail line (drawn under the position marker)\n this._gpsTrailLayer = new VectorLayer({\n source: this._gpsTrailSource,\n zIndex: 940,\n style: new Style({\n stroke: new Stroke({ color: '#ff6d00', width: 4, lineCap: 'round', lineJoin: 'round' }),\n }),\n properties: { title: 'GPS Trail', displayInLayerSwitcher: false },\n });\n\n // Current position: accuracy halo + solid dot\n this._gpsPositionLayer = new VectorLayer({\n source: this._gpsPositionSource,\n zIndex: 950,\n style: (feature) => {\n if (feature.get('_kind') === 'accuracy') {\n return new Style({\n fill: new Fill({ color: 'rgba(0,94,184,0.12)' }),\n stroke: new Stroke({ color: 'rgba(0,94,184,0.35)', width: 1 }),\n });\n }\n return new Style({\n image: new Circle({\n radius: 7,\n fill: new Fill({ color: '#005eb8' }),\n stroke: new Stroke({ color: '#ffffff', width: 2.5 }),\n }),\n });\n },\n properties: { title: 'GPS Position', displayInLayerSwitcher: false },\n });\n\n this.map.addLayer(this._gpsTrailLayer);\n this.map.addLayer(this._gpsPositionLayer);\n\n this._gpsCallbacks = { locate: [], record: [] };\n this._gpsRecording = false;\n }\n\n /** Register a callback fired when the user taps \"Locate Me\". */\n onLocateMe(cb) { this._gpsCallbacks.locate.push(cb); }\n /** Register a callback fired when the user toggles trail recording. Receives the desired state (true=start). */\n onToggleRecording(cb) { this._gpsCallbacks.record.push(cb); }\n\n /**\n * Draw / move the current-position marker and accuracy halo.\n * @param {number} lon\n * @param {number} lat\n * @param {number|null} [accuracy] horizontal accuracy in metres\n */\n showCurrentPosition(lon, lat, accuracy = null) {\n if (lon == null || lat == null) return;\n const center = fromLonLat([lon, lat]);\n this._gpsPositionSource.clear();\n\n if (accuracy && accuracy > 0) {\n // Approximate the accuracy circle in projected units. Good enough for a\n // visual halo at typical zoom levels.\n const resAtLat = accuracy / Math.cos((lat * Math.PI) / 180);\n const halo = new Feature({ geometry: new PolygonGeom([this._circleRing(center, resAtLat)]) });\n halo.set('_kind', 'accuracy');\n this._gpsPositionSource.addFeature(halo);\n }\n const dot = new Feature({ geometry: new Point(center) });\n dot.set('_kind', 'dot');\n this._gpsPositionSource.addFeature(dot);\n }\n\n /** @private build a ring of coordinates approximating a circle (metres → projected). */\n _circleRing(center, radiusMeters, segments = 48) {\n // Convert metres to projected units (Web Mercator) roughly via the\n // resolution at the centre latitude.\n const ring = [];\n const metersPerUnit = 1; // EPSG:3857 units are metres (approx near equator/locally)\n const r = radiusMeters / metersPerUnit;\n for (let i = 0; i <= segments; i++) {\n const a = (i / segments) * 2 * Math.PI;\n ring.push([center[0] + r * Math.cos(a), center[1] + r * Math.sin(a)]);\n }\n return ring;\n }\n\n /** Smoothly center the view on a coordinate. */\n centerOn(lon, lat, zoom = 16) {\n const view = this.map.getView();\n view.animate({ center: fromLonLat([lon, lat]), zoom, duration: 500 });\n }\n\n /** Reset the trail line (call when a new recording starts). */\n startTrailRender() {\n this._gpsTrailCoords = [];\n this._gpsTrailSource.clear();\n }\n\n /** Append a coordinate to the growing trail line. */\n appendTrailPoint(lon, lat) {\n if (lon == null || lat == null) return;\n this._gpsTrailCoords.push(fromLonLat([lon, lat]));\n this._gpsTrailSource.clear();\n if (this._gpsTrailCoords.length >= 2) {\n this._gpsTrailSource.addFeature(new Feature({ geometry: new LineString(this._gpsTrailCoords) }));\n }\n }\n\n /** Remove the rendered trail (does not affect stored data). */\n clearTrailRender() {\n this._gpsTrailCoords = [];\n this._gpsTrailSource.clear();\n }\n\n /** Reflect recording state on the control button. */\n setRecordingState(active) {\n this._gpsRecording = !!active;\n if (this._recordBtn) {\n this._recordBtn.classList.toggle('recording', this._gpsRecording);\n this._recordBtn.title = this._gpsRecording ? 'Stop trail recording' : 'Record GPS trail';\n this._recordBtn.innerHTML = this._gpsRecording\n ? ''\n : '';\n }\n if (this._locateToggle) this._locateToggle.classList.toggle('recording', this._gpsRecording);\n }\n\n /**\n * Build the expandable \"My Location\" control: a main button that reveals two\n * sub-buttons (Locate Me, Record Trail). Anchored at the same spot the old\n * ol-ext GeolocationButton occupied (bottom-right), so the base-map picker\n * still lines up above it.\n */\n _createLocationControl() {\n const target = this.map.getTargetElement();\n if (!target) return;\n\n // Main toggle\n const toggle = document.createElement('button');\n toggle.type = 'button';\n toggle.className = 'ls-locate-toggle';\n toggle.title = 'My Location';\n toggle.setAttribute('aria-label', 'My Location');\n toggle.innerHTML = '';\n target.appendChild(toggle);\n\n // Sub-button cluster (hidden until the main button is tapped)\n const actions = document.createElement('div');\n actions.className = 'ls-locate-actions';\n actions.innerHTML =\n '' +\n '';\n target.appendChild(actions);\n\n this._locateToggle = toggle;\n this._locateActions = actions;\n this._locateMeBtn = actions.querySelector('.ls-locate-me');\n this._recordBtn = actions.querySelector('.ls-locate-record');\n\n const close = () => { actions.classList.remove('open'); toggle.classList.remove('active'); };\n const open = () => { actions.classList.add('open'); toggle.classList.add('active'); };\n\n toggle.addEventListener('click', (e) => {\n e.stopPropagation();\n actions.classList.contains('open') ? close() : open();\n });\n\n // Tap outside closes the cluster (but never while recording, so the stop\n // button stays reachable).\n document.addEventListener('click', (e) => {\n if (!actions.classList.contains('open')) return;\n if (actions.contains(e.target) || toggle.contains(e.target)) return;\n if (this._gpsRecording) return;\n close();\n });\n\n this._locateMeBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n for (const cb of this._gpsCallbacks.locate) { try { cb(); } catch (err) { console.error(err); } }\n if (!this._gpsRecording) close();\n });\n\n this._recordBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const next = !this._gpsRecording;\n for (const cb of this._gpsCallbacks.record) { try { cb(next); } catch (err) { console.error(err); } }\n });\n }\n\n /**\n * Get style for a feature (handles selection state)\n */\n getFeatureStyle(feature) {\n const category = feature.get('category') || 'default';\n const emoji = this.getEmoji(category);\n\n if (feature === this.selectedFeature) {\n // Return selected style with the correct emoji and highlight\n return [\n // Background highlight circle\n new Style({\n image: new Circle({\n radius: 22,\n fill: new Fill({ color: 'rgba(220, 38, 38, 0.25)' }),\n stroke: new Stroke({ color: '#dc2626', width: 3 }),\n }),\n }),\n // Emoji on top, larger\n new Style({\n text: new Text({\n text: emoji,\n font: '40px sans-serif',\n textBaseline: 'bottom',\n textAlign: 'center',\n offsetY: -5,\n }),\n }),\n ];\n }\n\n // Check for custom style\n const customStyle = feature.get('style');\n if (customStyle) {\n return customStyle;\n }\n\n // Return category-based emoji style\n if (this.categoryStyles[category]) {\n return this.categoryStyles[category];\n }\n\n return this.defaultStyle;\n }\n\n /**\n * Set category-based styles with emojis\n * @param {Object} styles - Map of category to config { emoji, label, fontSize }\n */\n setCategoryStyles(styles) {\n for (const [category, config] of Object.entries(styles)) {\n // Update category mapping if provided\n if (config.emoji) {\n if (!this.categoryEmojis[category]) {\n this.categoryEmojis[category] = { emoji: config.emoji, label: config.label || category };\n } else {\n this.categoryEmojis[category].emoji = config.emoji;\n if (config.label) {\n this.categoryEmojis[category].label = config.label;\n }\n }\n }\n\n // Create/update style\n const emoji = this.getEmoji(category);\n const fontSize = config.fontSize || 28;\n\n this.categoryStyles[category] = this.createEmojiStyle(emoji, fontSize);\n }\n\n // Refresh markers\n this.markerSource.changed();\n }\n\n /**\n * Add a single marker\n */\n addMarker(lon, lat, properties = {}) {\n console.log('[MapView] Adding marker at', lon, lat, 'with properties:', properties);\n\n const feature = new Feature({\n geometry: new Point(fromLonLat([lon, lat])),\n ...properties,\n });\n\n // Store original coordinates for easy access\n feature.set('lon', lon);\n feature.set('lat', lat);\n\n this.markerSource.addFeature(feature);\n console.log('[MapView] Marker added, total features:', this.markerSource.getFeatures().length);\n return feature;\n }\n\n /**\n * Add multiple markers from an array of location objects\n */\n addMarkers(locations) {\n console.log('[MapView] Adding', locations.length, 'markers');\n\n const features = locations.map((loc) => {\n const feature = new Feature({\n geometry: new Point(fromLonLat([loc.longitude, loc.latitude])),\n id: loc.id,\n name: loc.name,\n description: loc.description,\n category: loc.category,\n lon: loc.longitude,\n lat: loc.latitude,\n });\n return feature;\n });\n\n this.markerSource.addFeatures(features);\n console.log('[MapView] Markers added, total features:', this.markerSource.getFeatures().length);\n return features;\n }\n\n /**\n * Clear all markers\n */\n clearMarkers() {\n this.markerSource.clear();\n this.selectedFeature = null;\n }\n\n /**\n * Remove a specific marker by feature or ID\n */\n removeMarker(featureOrId) {\n if (typeof featureOrId === 'object') {\n this.markerSource.removeFeature(featureOrId);\n } else {\n const feature = this.markerSource.getFeatures().find(\n f => f.get('id') === featureOrId\n );\n if (feature) {\n this.markerSource.removeFeature(feature);\n }\n }\n }\n\n /**\n * Get all markers\n */\n getMarkers() {\n return this.markerSource.getFeatures();\n }\n\n /**\n * Find marker by ID\n */\n findMarker(id) {\n return this.markerSource.getFeatures().find(f => f.get('id') === id);\n }\n\n /**\n * Select a marker (highlights it)\n */\n selectMarker(featureOrId) {\n if (typeof featureOrId === 'object') {\n this.selectedFeature = featureOrId;\n } else {\n this.selectedFeature = this.findMarker(featureOrId);\n }\n this.markerSource.changed();\n return this.selectedFeature;\n }\n\n /**\n * Clear selection\n */\n clearSelection() {\n this.selectedFeature = null;\n this.markerSource.changed();\n }\n\n /**\n * Zoom to a specific location\n */\n zoomTo(lon, lat, zoom = 15) {\n this.map.getView().animate({\n center: fromLonLat([lon, lat]),\n zoom: zoom,\n duration: 500,\n });\n }\n\n /**\n * Fit view to show all markers\n */\n fitToMarkers(padding = 50) {\n const extent = this.markerSource.getExtent();\n if (extent && extent[0] !== Infinity) {\n this.map.getView().fit(extent, {\n padding: [padding, padding, padding, padding],\n duration: 500,\n maxZoom: 16,\n });\n }\n }\n\n /**\n * Get current map center in lon/lat\n */\n getCenter() {\n const center = this.map.getView().getCenter();\n return toLonLat(center);\n }\n\n /**\n * Get current zoom level\n */\n getZoom() {\n return this.map.getView().getZoom();\n }\n\n /**\n * Set map center\n */\n setCenter(lon, lat) {\n this.map.getView().setCenter(fromLonLat([lon, lat]));\n }\n\n /**\n * Set zoom level\n */\n setZoom(zoom) {\n this.map.getView().setZoom(zoom);\n }\n\n /**\n * Register click callback\n * Callback receives (lon, lat, feature, event)\n *\n * Single-click is delayed by 300 ms so that a double-click can cancel it.\n * If the click lands on an overlay feature (e.g. district boundary) the\n * single-click is suppressed entirely — only double-click will fire.\n */\n onClick(callback) {\n this.clickCallbacks.push(callback);\n\n // Set up click handler if this is the first callback\n if (this.clickCallbacks.length === 1) {\n this._clickTimer = null;\n\n // Double-click cancels any pending single-click\n this.map.on('dblclick', () => {\n if (this._clickTimer) {\n clearTimeout(this._clickTimer);\n this._clickTimer = null;\n }\n });\n\n this.map.on('click', (evt) => {\n // Cancel any previous pending click\n if (this._clickTimer) {\n clearTimeout(this._clickTimer);\n this._clickTimer = null;\n }\n\n // When NOT in edit / draw mode, immediately clear any feature\n // the Select interaction may have grabbed on this click so the\n // user never sees a selection flash.\n if (!this._editBarActive && this._selectInteraction) {\n this._selectInteraction.getFeatures().clear();\n }\n\n // Check what features sit under the click pixel\n let hasOverlayFeature = false;\n let hasParcelFeature = false;\n let markerFeature = null;\n this.map.forEachFeatureAtPixel(evt.pixel, (feature) => {\n if (feature.get('_layerType') === 'parcel') {\n hasParcelFeature = true;\n }\n if (feature.get('name')) {\n markerFeature = feature;\n }\n hasOverlayFeature = true;\n });\n\n // If an overlay feature was hit, suppress single-click\n // UNLESS it's a parcel or a location marker\n if (hasOverlayFeature && !hasParcelFeature && !markerFeature) {\n return;\n }\n\n // Delay the single-click to allow double-click to cancel it\n const [lon, lat] = toLonLat(evt.coordinate);\n this._clickTimer = setTimeout(() => {\n this._clickTimer = null;\n\n // Find location marker at pixel\n let clickedFeature = null;\n this.map.forEachFeatureAtPixel(evt.pixel, (feature) => {\n if (feature.get('name')) {\n clickedFeature = feature;\n return true;\n }\n });\n\n for (const cb of this.clickCallbacks) {\n cb(lon, lat, clickedFeature, evt);\n }\n }, 300);\n });\n }\n\n // Return unsubscribe function\n return () => {\n const index = this.clickCallbacks.indexOf(callback);\n if (index > -1) {\n this.clickCallbacks.splice(index, 1);\n }\n };\n }\n\n /**\n * Register pointer move callback (for hover effects)\n */\n onPointerMove(callback) {\n this.map.on('pointermove', (evt) => {\n if (evt.dragging) return;\n\n const [lon, lat] = toLonLat(evt.coordinate);\n\n // Only find location markers (features with 'name' property)\n let hoveredFeature = null;\n this.map.forEachFeatureAtPixel(evt.pixel, (feature) => {\n if (feature.get('name')) {\n hoveredFeature = feature;\n return true;\n }\n });\n\n // Change cursor\n this.map.getTargetElement().style.cursor = hoveredFeature ? 'pointer' : '';\n\n callback(lon, lat, hoveredFeature, evt);\n });\n }\n\n /**\n * Enable cursor change on marker hover\n * Note: This is now handled automatically by the popup system\n */\n enableHoverCursor() {\n // Cursor changes are now handled by setupHoverPopup()\n // This method is kept for backwards compatibility\n }\n\n /**\n * Add a GeoJSON layer (visible in LayerSwitcher).\n * By default the layer is added to the root overlay group.\n * Pass a targetGroup (LayerGroup) to nest it inside a specific group.\n *\n * @param {Object} geojson - GeoJSON FeatureCollection or Feature\n * @param {string} title - Layer title for the LayerSwitcher\n * @param {Object} [styleOptions] - Optional style configuration\n * @param {string} [styleOptions.strokeColor='#3b82f6'] - Stroke color\n * @param {number} [styleOptions.strokeWidth=2] - Stroke width\n * @param {string} [styleOptions.fillColor='rgba(59,130,246,0.1)'] - Fill color\n * @param {LayerGroup} [targetGroup] - Optional group to add the layer to\n * @returns {VectorLayer} The created layer\n */\n addGeoJSONLayer(geojson, title, styleOptions = {}, targetGroup = null) {\n const {\n strokeColor = '#3b82f6',\n strokeWidth = 2,\n fillColor = 'rgba(59,130,246,0.1)',\n // Optional line \"casing\": a thicker darker stroke drawn UNDERNEATH the\n // main stroke. Used for road-like layers to make light-colored lines\n // visible on any base map. Set lineCasingColor to enable; the casing\n // width defaults to strokeWidth + 2.\n lineCasingColor = null,\n lineCasingWidth = null,\n pointRadius = 5,\n pointFillColor = null, // defaults to strokeColor\n pointStrokeColor = '#ffffff',\n pointStrokeWidth = 1.5,\n } = styleOptions;\n\n const source = new VectorSource({\n features: new GeoJSON().readFeatures(geojson, {\n featureProjection: 'EPSG:3857',\n }),\n });\n\n // Build per-geometry styles. OpenLayers picks `image` for Point /\n // MultiPoint, `stroke`+`fill` for Polygon / MultiPolygon, and `stroke`\n // alone for LineString / MultiLineString. Putting all three on a single\n // Style is enough — but a Style with only stroke+fill leaves Points\n // invisible, which is what was happening on shapefile import.\n const fillStyle = new Fill({ color: fillColor });\n const pointStyle = new Circle({\n radius: pointRadius,\n fill: new Fill({ color: pointFillColor || strokeColor }),\n stroke: new Stroke({ color: pointStrokeColor, width: pointStrokeWidth }),\n });\n\n // If a line casing is requested, return an array of two Styles per\n // feature: the casing renders first (underneath), then the inner stroke.\n // For polygons the casing also outlines them; for points the casing has\n // no effect (Point geometries only render `image`).\n let layerStyle;\n if (lineCasingColor) {\n const casingW = lineCasingWidth != null ? lineCasingWidth : strokeWidth + 2;\n layerStyle = [\n new Style({\n stroke: new Stroke({ color: lineCasingColor, width: casingW }),\n }),\n new Style({\n stroke: new Stroke({ color: strokeColor, width: strokeWidth }),\n fill: fillStyle,\n image: pointStyle,\n }),\n ];\n } else {\n layerStyle = new Style({\n stroke: new Stroke({ color: strokeColor, width: strokeWidth }),\n fill: fillStyle,\n image: pointStyle,\n });\n }\n\n const layer = new VectorLayer({\n title: title,\n source: source,\n style: layerStyle,\n });\n layer.set('typeTag', styleOptions.typeTag || 'VEC');\n\n // Derive a friendly \"Vector / Polygon\" / \"Vector / Line\" / \"Vector / Point\"\n // subtitle from the first feature's geometry type, unless the caller\n // already supplied one in styleOptions.\n //\n // Layers created EMPTY (parcels, OSM_roads, …, populated later from the\n // API) leave the subtitle absent until the first feature arrives —\n // see the `addfeature` listener below.\n const describeFromGeom = (geomType) => {\n if (!geomType) return null;\n if (geomType.includes('Polygon')) return 'Vector / Polygon';\n if (geomType.includes('LineString')) return 'Vector / Line';\n if (geomType.includes('Point')) return 'Vector / Point';\n return 'Vector';\n };\n\n if (styleOptions.typeDescription) {\n layer.set('typeDescription', styleOptions.typeDescription);\n } else {\n const feats = source.getFeatures();\n const initial = describeFromGeom(feats[0]?.getGeometry?.()?.getType?.());\n if (initial) {\n layer.set('typeDescription', initial);\n } else {\n // Source is empty — wait for the first feature and set then.\n const once = (ev) => {\n const desc = describeFromGeom(ev.feature.getGeometry?.()?.getType?.());\n if (desc) layer.set('typeDescription', desc);\n source.un('addfeature', once);\n };\n source.on('addfeature', once);\n }\n }\n\n const group = targetGroup || this.overlayGroup;\n group.getLayers().push(layer);\n\n console.log('[MapView] GeoJSON layer added:', title, '→', source.getFeatures().length, 'features',\n targetGroup ? `(in group \"${targetGroup.get('title')}\")` : '');\n return layer;\n }\n\n /**\n * Add a LayerGroup to the overlay group.\n * Used to create layer categories from the remote catalogue;\n * individual vector layers will be added into these groups later.\n *\n * @param {number|string} id - Unique layer group id (from the API)\n * @param {string} title - Group title for the LayerSwitcher\n * @param {string} [description=''] - Group description (stored as property)\n * @returns {LayerGroup} The created (empty) layer group\n */\n addLayerGroup(id, title, description = '') {\n const group = new LayerGroup({\n title: title.trim(),\n });\n\n // Store metadata for later use\n group.set('layerId', id);\n group.set('description', description);\n\n this.overlayGroup.getLayers().push(group);\n\n console.log('[MapView] Layer group added:', title.trim(), '(id:', id + ')');\n return group;\n }\n\n /**\n * Add a WMS layer to a layer group.\n *\n * @param {string} groupTitle Title of the target LayerGroup (e.g. 'Biophysical Environment')\n * @param {string} title Display title for the layer\n * @param {string} url WMS server URL\n * @param {string} layers WMS LAYERS parameter\n * @param {Object} [options] Extra options\n * @param {string} [options.serverType='geoserver'] Server type hint ('geoserver'|'mapserver'|'qgis'|null)\n * @param {string} [options.style] WMS STYLES parameter (e.g. 'colours' for DEAfrica DEM)\n * @param {boolean} [options.visible=true] Initial visibility\n * @param {string} [options.attributions] Attribution HTML\n * @param {number} [options.opacity=1] Layer opacity (0–1). Use ~0.5 for background-style layers.\n * @param {number} [options.zIndex] Render z-index. Use negative values (e.g. -10) to force the\n * layer behind all default-z-index layers regardless of group order.\n * @param {string} [options.legendUrl] URL of a legend image to display while the layer is visible.\n * @param {boolean} [options.onlineOnly=false] If true, show a toast when the user toggles the layer on\n * while offline, explaining that the layer requires connectivity.\n * @returns {TileLayer|null} The created layer, or null if group not found\n */\n addWMSLayer(groupTitle, title, url, layers, options = {}) {\n const group = this.getLayerGroupByTitle(groupTitle);\n if (!group) {\n console.warn(`[MapView] Layer group \"${groupTitle}\" not found — cannot add WMS layer \"${title}\"`);\n return null;\n }\n\n const params = { LAYERS: layers, TILED: true, WIDTH: 256, HEIGHT: 256 };\n if (options.style !== undefined) params.STYLES = options.style;\n\n const wmsSource = new TileWMS({\n url,\n params,\n serverType: options.serverType !== undefined ? options.serverType : 'geoserver',\n crossOrigin: 'anonymous',\n hidpi: false,\n attributions: options.attributions,\n });\n\n const wmsLayer = new TileLayer({\n title,\n visible: options.visible !== undefined ? options.visible : true,\n source: wmsSource,\n opacity: options.opacity !== undefined ? options.opacity : 1,\n zIndex: options.zIndex,\n });\n wmsLayer.set('typeTag', 'WMS');\n wmsLayer.set('typeDescription', 'WMS / Raster');\n\n // Show toast on tile load errors (e.g. server rejects request)\n wmsSource.on('tileloaderror', () => {\n showToast(`WMS layer \"${title}\" — tile load error. Check the URL and layer name.`, 'warning', 5000);\n });\n\n group.getLayers().push(wmsLayer);\n\n // Register legend AFTER push so that a failure here doesn't block the LayerSwitcher\n if (options.legendUrl) {\n try {\n this._registerLegend(wmsLayer, title, options.legendUrl);\n } catch (err) {\n console.warn(`[MapView] Could not register legend for \"${title}\":`, err);\n }\n }\n\n // Online-only warning: when the user toggles the layer on while offline,\n // surface a toast explaining why nothing will render.\n if (options.onlineOnly) {\n this._attachOnlineOnlyHandler(wmsLayer, title);\n }\n\n console.log(`[MapView] WMS layer added: \"${title}\" → group \"${groupTitle}\"`);\n return wmsLayer;\n }\n\n /**\n * Add an XYZ tile layer to a layer group.\n *\n * @param {string} groupTitle Title of the target LayerGroup\n * @param {string} title Display title for the layer\n * @param {string} url XYZ tile URL template (with {z}/{x}/{y} placeholders)\n * @param {Object} [options] Extra options\n * @param {boolean} [options.visible=true] Initial visibility\n * @param {string} [options.attributions] Attribution HTML\n * @param {number} [options.maxZoom=19] Maximum zoom level\n * @param {number} [options.opacity=1] Layer opacity (0–1). Use ~0.5 for background-style layers.\n * @param {number} [options.zIndex] Render z-index. Use negative values to force behind other layers.\n * @param {string} [options.legendUrl] URL of a legend image to display while the layer is visible.\n * @param {boolean} [options.onlineOnly=false] If true, show a toast when the user toggles the layer on\n * while offline, explaining that the layer requires connectivity.\n * @returns {TileLayer|null} The created layer, or null if group not found\n */\n addXYZLayer(groupTitle, title, url, options = {}) {\n const group = this.getLayerGroupByTitle(groupTitle);\n if (!group) {\n console.warn(`[MapView] Layer group \"${groupTitle}\" not found — cannot add XYZ layer \"${title}\"`);\n return null;\n }\n\n const xyzSource = new XYZ({\n url,\n crossOrigin: 'anonymous',\n maxZoom: options.maxZoom !== undefined ? options.maxZoom : 19,\n attributions: options.attributions,\n });\n\n const xyzLayer = new TileLayer({\n title,\n visible: options.visible !== undefined ? options.visible : true,\n source: xyzSource,\n opacity: options.opacity !== undefined ? options.opacity : 1,\n zIndex: options.zIndex,\n });\n xyzLayer.set('typeTag', 'XYZ');\n xyzLayer.set('typeDescription', 'XYZ / Tile');\n\n // Show toast on tile load errors\n xyzSource.on('tileloaderror', () => {\n showToast(`XYZ layer \"${title}\" — tile load error. Check the URL.`, 'warning', 5000);\n });\n\n group.getLayers().push(xyzLayer);\n\n // Register legend AFTER push so that a failure here doesn't block the LayerSwitcher\n if (options.legendUrl) {\n try {\n this._registerLegend(xyzLayer, title, options.legendUrl);\n } catch (err) {\n console.warn(`[MapView] Could not register legend for \"${title}\":`, err);\n }\n }\n\n // Online-only warning: when the user toggles the layer on while offline,\n // surface a toast explaining why nothing will render.\n if (options.onlineOnly) {\n this._attachOnlineOnlyHandler(xyzLayer, title);\n }\n\n console.log(`[MapView] XYZ layer added: \"${title}\" → group \"${groupTitle}\"`);\n return xyzLayer;\n }\n\n // ============================================================================\n // Add External Layer Dialog\n // ============================================================================\n\n /**\n * Create the add-layer dialog overlay (hidden by default).\n * Appended to the map target element so it stays within the map viewport.\n */\n _createAddLayerDialog() {\n this._addLayerDialog = document.createElement('div');\n this._addLayerDialog.className = 'map-add-layer-dialog';\n this._addLayerDialog.style.cssText = `\n display:none;position:absolute;top:0;left:0;right:0;bottom:0;\n z-index:1100;background:rgba(0,0,0,0.4);\n align-items:center;justify-content:center;\n `;\n\n const card = document.createElement('div');\n card.style.cssText = `\n background:var(--card, #fff);color:var(--card-foreground, #1e1a4b);\n border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,0.35);\n font-family:var(--font-body, 'Exo', sans-serif);font-size:13px;\n width:340px;max-width:90vw;border:2px solid #10b981;overflow:hidden;\n `;\n\n card.innerHTML = `\n
    \n Add External Layer\n \n
    \n
    \n
    \n \n
    \n \n \n \n
    \n
    \n
    \n \n \n
    \n
    \n \n \n
    \n WMS LAYERS parameter (e.g. workspace:layer)\n
    \n
    \n
    \n \n \n
    \n
    \n \n \n
    \n
    \n `;\n\n this._addLayerDialog.appendChild(card);\n this.map.getTargetElement().appendChild(this._addLayerDialog);\n\n // Type radio change — toggle layer name row visibility\n const nameRow = card.querySelector('.add-layer-name-row');\n const nameHint = card.querySelector('.add-layer-name-hint');\n const urlInput = card.querySelector('.add-layer-url');\n card.querySelectorAll('input[name=\"add-layer-type\"]').forEach((radio) => {\n radio.addEventListener('change', () => {\n const type = radio.value;\n if (type === 'xyz') {\n nameRow.style.display = 'none';\n urlInput.placeholder = 'https://example.com/tiles/{z}/{x}/{y}.png';\n } else {\n nameRow.style.display = '';\n urlInput.placeholder = type === 'wms'\n ? 'https://example.com/wms'\n : 'https://example.com/wfs';\n nameHint.textContent = type === 'wms'\n ? 'WMS LAYERS parameter (e.g. workspace:layer)'\n : 'WFS typename (e.g. workspace:layer)';\n }\n });\n });\n\n // Close / Cancel\n const close = () => this._hideAddLayerDialog();\n card.querySelector('.add-layer-close').addEventListener('click', close);\n card.querySelector('.add-layer-cancel').addEventListener('click', close);\n this._addLayerDialog.addEventListener('click', (e) => {\n if (e.target === this._addLayerDialog) close();\n });\n\n // Confirm\n card.querySelector('.add-layer-confirm').addEventListener('click', () => {\n const type = card.querySelector('input[name=\"add-layer-type\"]:checked').value;\n const url = card.querySelector('.add-layer-url').value.trim();\n const layerName = card.querySelector('.add-layer-name').value.trim();\n const title = card.querySelector('.add-layer-title').value.trim();\n\n if (!url) {\n card.querySelector('.add-layer-url').style.borderColor = '#ef4444';\n return;\n }\n if ((type === 'wms' || type === 'wfs') && !layerName) {\n card.querySelector('.add-layer-name').style.borderColor = '#ef4444';\n return;\n }\n if (!title) {\n card.querySelector('.add-layer-title').style.borderColor = '#ef4444';\n return;\n }\n\n this._addExternalLayer(type, url, layerName, title);\n this._hideAddLayerDialog();\n });\n\n // Enter key to confirm\n card.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n card.querySelector('.add-layer-confirm').click();\n }\n if (e.key === 'Escape') {\n e.preventDefault();\n close();\n }\n });\n }\n\n /**\n * Show the add-layer dialog.\n */\n showAddLayerDialog() {\n const dlg = this._addLayerDialog;\n // Reset form\n dlg.querySelector('.add-layer-url').value = '';\n dlg.querySelector('.add-layer-name').value = '';\n dlg.querySelector('.add-layer-title').value = '';\n dlg.querySelectorAll('input[name=\"add-layer-type\"]')[0].checked = true;\n dlg.querySelector('.add-layer-name-row').style.display = '';\n dlg.querySelector('.add-layer-url').placeholder = 'https://example.com/wms';\n dlg.querySelector('.add-layer-name-hint').textContent = 'WMS LAYERS parameter (e.g. workspace:layer)';\n\n // Reset border colours\n dlg.querySelectorAll('input[type=\"text\"]').forEach((inp) => {\n inp.style.borderColor = 'var(--border, #1e1a4b1f)';\n });\n\n dlg.style.display = 'flex';\n dlg.querySelector('.add-layer-url').focus();\n }\n\n /**\n * Hide the add-layer dialog.\n */\n _hideAddLayerDialog() {\n this._addLayerDialog.style.display = 'none';\n }\n\n /**\n * Add an external layer to the \"External Source\" group.\n *\n * @param {string} type 'wms' | 'wfs' | 'xyz'\n * @param {string} url Server URL\n * @param {string} layerName WMS LAYERS / WFS typename (ignored for XYZ)\n * @param {string} title Display title in layer switcher\n */\n _addExternalLayer(type, url, layerName, title) {\n const group = this._externalSourceGroup;\n if (!group) {\n showToast('Layer group \"External Source\" not found.', 'error', 4000);\n return;\n }\n\n let layer;\n\n switch (type) {\n case 'wms': {\n const wmsSrc = new TileWMS({\n url,\n params: { LAYERS: layerName, TILED: true, WIDTH: 256, HEIGHT: 256 },\n serverType: 'geoserver',\n crossOrigin: 'anonymous',\n hidpi: false,\n });\n layer = new TileLayer({\n title,\n visible: true,\n source: wmsSrc,\n });\n wmsSrc.on('tileloaderror', () => {\n showToast(`WMS \"${title}\" — tile load error. Check URL and layer name.`, 'warning', 5000);\n });\n break;\n }\n\n case 'wfs': {\n const wfsUrl = `${url}${url.includes('?') ? '&' : '?'}` +\n `service=WFS&version=1.1.0&request=GetFeature` +\n `&typename=${encodeURIComponent(layerName)}` +\n `&outputFormat=application/json&srsname=EPSG:3857`;\n\n const wfsSource = new VectorSource({\n url: wfsUrl,\n format: new GeoJSON(),\n });\n wfsSource.on('featuresloaderror', () => {\n showToast(`WFS \"${title}\" — load error. Check URL and layer name.`, 'warning', 5000);\n });\n\n layer = new VectorLayer({\n title,\n visible: true,\n source: wfsSource,\n style: new Style({\n stroke: new Stroke({ color: '#e11d48', width: 2 }),\n fill: new Fill({ color: 'rgba(225,29,72,0.15)' }),\n }),\n });\n break;\n }\n\n case 'xyz':\n layer = new TileLayer({\n title,\n visible: true,\n source: new XYZ({\n url,\n crossOrigin: 'anonymous',\n }),\n });\n layer.getSource().on('tileloaderror', () => {\n showToast(`XYZ \"${title}\" — tile load error. Check the URL template.`, 'warning', 5000);\n });\n break;\n\n default:\n showToast(`Unknown layer type: ${type}`, 'error', 4000);\n return;\n }\n\n // Tag for the LayerSwitcher chip\n layer.set('typeTag', type.toUpperCase()); // 'WMS' | 'WFS' | 'XYZ'\n layer.set('typeDescription', {\n wms: 'WMS / Raster',\n wfs: 'WFS / Vector',\n xyz: 'XYZ / Tile',\n }[type] || type.toUpperCase());\n\n // User-added external layers ARE removable — they're not part of the\n // app's built-in data model.\n layer.set('removable', true);\n\n group.getLayers().push(layer);\n showToast(`Layer \"${title}\" added to External Source.`, 'success', 3000);\n console.log(`[MapView] External ${type.toUpperCase()} layer added: \"${title}\"`);\n }\n\n // ============================================================================\n // LayerSwitcher decoration (Option A — visual refresh)\n // ============================================================================\n\n /**\n * Decorate a layer's
  • after ol-ext renders it:\n * • inject a type-tag chip next to the layer label\n * • inject the green \"+\" button on the External Source group header\n *\n * Idempotent — safe to call repeatedly; each injected element checks\n * whether it already exists in the row.\n */\n _decorateLayerListItem(layer, li) {\n // 1. Type-tag chip (e.g. WMS / XYZ / VEC) next to the layer name.\n // Inserted INSIDE the label's so it doesn't collide with the\n // label's left padding (where ol-ext draws the checkbox via ::before).\n const tag = layer.get('typeTag'); // 'WMS' | 'WFS' | 'XYZ' | 'VEC' | 'GEO' | 'BASE'\n if (tag) {\n const labelSpan = li.querySelector(':scope > .li-content > label > span');\n if (labelSpan && !labelSpan.querySelector(':scope > .ls-type-tag')) {\n const chip = document.createElement('span');\n chip.className = `ls-type-tag ls-type-tag-${String(tag).toLowerCase()}`;\n chip.textContent = String(tag);\n chip.title = `${tag} layer`;\n labelSpan.appendChild(chip);\n }\n }\n\n // 2. Replace ol-ext's bar-drawn +/- chevron with the GeoView chevron SVG.\n // The SVG uses stroke=\"currentColor\", so CSS `color` (set in\n // layerswitcher.css) tints it. Rotation handles the open/closed state.\n const btnBar = li.querySelector(':scope > .ol-layerswitcher-buttons');\n if (btnBar) {\n const chevronEl = btnBar.querySelector(':scope > .expend-layers, :scope > .collapse-layers');\n if (chevronEl && !chevronEl.querySelector(':scope > svg.ls-chevron-svg')) {\n chevronEl.innerHTML =\n '' +\n '' +\n '';\n }\n }\n\n // 3. Layer-type subtitle (\"Vector / Polygon\", \"WMS / Raster\", …) below\n // the layer name. Only rendered when layer.get('typeDescription') is\n // set. For layers that start empty and gain features later (the API\n // loaders), we listen for `change:typeDescription` and update or\n // insert the subtitle then.\n const content = li.querySelector(':scope > .li-content');\n const ensureSubtitle = () => {\n if (!content) return;\n const text = layer.get('typeDescription');\n let sub = content.querySelector(':scope > .ls-layer-subtitle');\n if (!text) {\n if (sub) sub.remove();\n return;\n }\n if (!sub) {\n sub = document.createElement('div');\n sub.className = 'ls-layer-subtitle';\n const label = content.querySelector(':scope > label');\n if (label && label.nextSibling) {\n content.insertBefore(sub, label.nextSibling);\n } else {\n content.appendChild(sub);\n }\n }\n sub.textContent = text;\n };\n ensureSubtitle();\n if (!layer._lsSubtitleHooked) {\n layer._lsSubtitleHooked = true;\n layer.on('change:typeDescription', () => {\n // The
  • may not exist any more (panel re-rendered between events)\n // — guard with a fresh lookup via the LayerSwitcher next time it draws.\n // For now we just call ensureSubtitle bound to the original li.\n ensureSubtitle();\n });\n }\n\n // 4. Per-layer Remove button — only for layers explicitly marked\n // `removable: true` (external sources, imported files, …). Built-in\n // layers (Parcels, OSM_roads, district boundary, …) are NOT removable\n // so the user can't accidentally delete them.\n if (layer.get('removable') === true && btnBar && !btnBar.querySelector(':scope > .ls-remove-btn')) {\n const removeBtn = document.createElement('button');\n removeBtn.type = 'button';\n removeBtn.className = 'ls-remove-btn';\n removeBtn.title = 'Remove this layer';\n removeBtn.setAttribute('aria-label', 'Remove layer');\n removeBtn.innerHTML =\n '' +\n '' +\n '';\n removeBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n this._removeLayer(layer);\n });\n btnBar.appendChild(removeBtn);\n }\n\n // 5. \"+\" button on the External Source group\n const groupTitle = (layer.get('title') || '').toLowerCase();\n if (groupTitle.includes('external')) {\n this._externalSourceGroup = layer;\n // btnBar already resolved above (same .ol-layerswitcher-buttons element)\n if (btnBar && !btnBar.querySelector('.ol-add-layer')) {\n const addBtn = document.createElement('span');\n addBtn.className = 'ol-add-layer';\n addBtn.title = 'Add external layer';\n addBtn.textContent = '+';\n addBtn.style.cssText = `\n display:inline-flex !important;align-items:center;justify-content:center;\n width:22px !important;height:22px !important;border-radius:50%;\n background:#41b6a6 !important;color:#fff !important;\n font-size:15px !important;font-weight:700;\n cursor:pointer;line-height:1 !important;\n margin:0 4px 0 0;vertical-align:middle;\n transition:background 0.2s;box-sizing:border-box;border:none;\n `;\n addBtn.addEventListener('mouseenter', () => { addBtn.style.background = '#329686'; });\n addBtn.addEventListener('mouseleave', () => { addBtn.style.background = '#41b6a6'; });\n addBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n this.showAddLayerDialog();\n });\n btnBar.prepend(addBtn);\n }\n }\n }\n\n /**\n * Remove a layer from its parent group, after confirmation. Only called\n * from the per-layer × button injected by `_decorateLayerListItem` — and\n * that button is only injected for layers marked `removable: true`, so\n * built-in layers (Parcels, OSM_roads, …) can never reach this path.\n *\n * @param {Layer} layer\n */\n _removeLayer(layer) {\n const title = layer.get('title') || 'this layer';\n if (!confirm(`Remove \"${title}\" from the map?\\n\\nThis only affects the current session — built-in layers cannot be removed.`)) {\n return;\n }\n\n // Find the parent group that owns this layer and call .remove() on its\n // collection. Walk recursively from the overlay group.\n const visit = (group) => {\n const layers = group.getLayers();\n if (layers.getArray().includes(layer)) {\n layers.remove(layer);\n return true;\n }\n let removed = false;\n layers.forEach((child) => {\n if (!removed && child.getLayers) {\n removed = visit(child);\n }\n });\n return removed;\n };\n\n const ok = visit(this.overlayGroup);\n if (ok) {\n console.log(`[MapView] Removed layer \"${title}\"`);\n showToast(`Removed \"${title}\" from the map.`, 'info', 3000);\n } else {\n console.warn(`[MapView] Could not find layer \"${title}\" in any group`);\n }\n }\n\n /**\n * Inject (or refresh) the panel chrome — an \"active count\" badge at the top\n * and a footer row with \"Reset overlays\" button at the bottom.\n *\n * The chrome lives in `.panel-container` (the wrapping
    ), not inside\n * the `
      ` — that way the badge and footer are siblings of\n * the layer list rather than malformed children of a `
        `.\n *\n * Called from drawlist via queueMicrotask, so it runs once per redraw cycle\n * regardless of how many layers are in the panel.\n */\n _refreshLayerSwitcherChrome(layerSwitcher) {\n const panelContainer = layerSwitcher.element?.querySelector('.panel-container');\n const ul = layerSwitcher.element?.querySelector('ul.panel');\n if (!panelContainer || !ul) return;\n\n // --- Active-count badge (top of panel-container, before the
          ) ---\n let badge = panelContainer.querySelector(':scope > .ls-active-badge');\n if (!badge) {\n badge = document.createElement('div');\n badge.className = 'ls-active-badge';\n badge.innerHTML = `\n Layers\n 0 active\n `;\n panelContainer.insertBefore(badge, ul);\n }\n\n // --- Footer row (bottom of panel-container, after the
            ) ---\n let footer = panelContainer.querySelector(':scope > .ls-footer-row');\n if (!footer) {\n footer = document.createElement('div');\n footer.className = 'ls-footer-row';\n footer.innerHTML = `\n — layers total\n \n `;\n panelContainer.appendChild(footer);\n\n footer.querySelector('.ls-footer-btn').addEventListener('click', (e) => {\n e.stopPropagation();\n this._resetAllOverlays();\n });\n }\n\n // --- Update counters ---\n const counts = this._countLayers();\n badge.querySelector('.ls-active-badge-count').textContent =\n `${counts.activeOverlays} active`;\n footer.querySelector('.ls-footer-note').textContent =\n `${counts.totalOverlays} overlay${counts.totalOverlays === 1 ? '' : 's'}`;\n }\n\n /**\n * Walk the overlay group recursively, returning the number of *leaf* layers\n * (i.e. not groups) and how many of them are currently visible.\n * Excludes base maps and internal layers (Markers, Drawings, vertex overlay).\n */\n _countLayers() {\n let totalOverlays = 0;\n let activeOverlays = 0;\n\n const HIDDEN_INTERNAL = new Set(['__vertex_highlight__']);\n\n const visit = (group) => {\n group.getLayers().forEach((layer) => {\n if (layer.get('displayInLayerSwitcher') === false) return;\n if (HIDDEN_INTERNAL.has(layer.get('title'))) return;\n\n if (layer.getLayers) {\n visit(layer);\n } else {\n totalOverlays++;\n if (layer.getVisible()) activeOverlays++;\n }\n });\n };\n if (this.overlayGroup) visit(this.overlayGroup);\n return { totalOverlays, activeOverlays };\n }\n\n /**\n * Hide every overlay layer (base maps stay on). Wired to the footer\n * \"Reset overlays\" button.\n */\n _resetAllOverlays() {\n const HIDDEN_INTERNAL = new Set(['__vertex_highlight__']);\n const visit = (group) => {\n group.getLayers().forEach((layer) => {\n if (layer.get('displayInLayerSwitcher') === false) return;\n if (HIDDEN_INTERNAL.has(layer.get('title'))) return;\n\n if (layer.getLayers) {\n visit(layer);\n } else {\n layer.setVisible(false);\n }\n });\n };\n if (this.overlayGroup) visit(this.overlayGroup);\n console.log('[MapView] Reset overlays — all hidden');\n }\n\n /**\n * Hook `change:visible` on every overlay leaf so the active-count badge\n * stays in sync even when ol-ext doesn't re-fire drawlist (e.g. ticking\n * a checkbox just toggles visibility without rebuilding the list).\n * Also re-hooks when a new layer is added.\n */\n _wireLayerSwitcherVisibilityHooks(layerSwitcher) {\n const refresh = () => this._refreshLayerSwitcherChrome(layerSwitcher);\n\n const hookLayer = (layer) => {\n if (layer._lsVisHooked) return;\n layer._lsVisHooked = true;\n layer.on('change:visible', refresh);\n };\n\n const visit = (group) => {\n group.getLayers().forEach((layer) => {\n if (layer.getLayers) {\n visit(layer);\n // Listen for layers added later to this group\n if (!group._lsAddHooked) {\n group._lsAddHooked = true;\n group.getLayers().on('add', (ev) => {\n const added = ev.element;\n if (added.getLayers) visit(added); else hookLayer(added);\n refresh();\n });\n }\n } else {\n hookLayer(layer);\n }\n });\n };\n\n if (this.overlayGroup) visit(this.overlayGroup);\n }\n\n // ============================================================================\n // Online-Only Layer Helper\n // ============================================================================\n\n /**\n * Attach a `change:visible` listener that shows an info toast when the user\n * toggles a layer ON while the device is offline. Used for layers that fetch\n * tiles or features from a remote service and therefore have no useful\n * cached state.\n *\n * The check uses navigator.onLine, which is the same signal as the rest of\n * the app's online detection.\n *\n * @param {Layer} layer\n * @param {string} title Display title used in the toast message\n */\n _attachOnlineOnlyHandler(layer, title) {\n layer.set('onlineOnly', true);\n layer.on('change:visible', () => {\n if (layer.getVisible() && !navigator.onLine) {\n showToast(\n `\"${title}\" requires an internet connection. Connect to view this layer.`,\n 'info',\n 5000,\n );\n }\n });\n }\n\n // ============================================================================\n // Legend Panel — shows legend images for visible layers that have one\n // ============================================================================\n\n /**\n * Create the legend panel, positioned bottom-right inside the map target.\n * Hidden when no visible layers have a registered legend.\n */\n _createLegendPanel() {\n this._legendPanel = document.createElement('div');\n this._legendPanel.className = 'map-legend-panel';\n this._legendPanel.style.cssText = `\n position:absolute;right:10px;bottom:40px;z-index:900;\n display:none;flex-direction:column;gap:6px;\n background:var(--card, #fff);color:var(--card-foreground, #1e1a4b);\n border:1px solid var(--border, #1e1a4b1f);border-radius:8px;\n box-shadow:0 4px 12px rgba(0,0,0,0.15);\n font-family:var(--font-body, 'Exo', sans-serif);font-size:11px;\n max-width:220px;max-height:60%;overflow-y:auto;\n padding:8px 10px;\n `;\n this.map.getTargetElement().appendChild(this._legendPanel);\n\n // Map of layer → { wrapper, title, imgUrl }\n this._legendEntries = new Map();\n }\n\n /**\n * Register a layer's legend image and wire up visibility tracking.\n * Called from addWMSLayer / addXYZLayer when a legendUrl is supplied.\n *\n * @param {Layer} layer The OpenLayers layer\n * @param {string} title Display title for the legend header\n * @param {string} legendUrl URL of the legend image\n */\n _registerLegend(layer, title, legendUrl) {\n if (!this._legendPanel) return;\n\n // Build the legend entry — a div with header + image\n const wrapper = document.createElement('div');\n wrapper.className = 'map-legend-entry';\n wrapper.style.cssText = 'border-bottom:1px solid var(--border, #1e1a4b1f);padding-bottom:6px;';\n wrapper.innerHTML = `\n
            \n ${this._escapeHtml(title)}\n
            \n \"${this._escapeHtml(title)}\n `;\n\n this._legendEntries.set(layer, wrapper);\n\n // Listen for visibility changes. Wrap in try/catch so a DOM error here\n // cannot break the LayerSwitcher's click handler (which fires change:visible\n // synchronously and relies on a subsequent setTimeout to update the checkbox).\n const update = () => {\n try { this._updateLegendPanel(); }\n catch (err) { console.warn('[MapView] legend panel update failed:', err); }\n };\n layer.on('change:visible', update);\n\n // Trigger initial state\n update();\n }\n\n /**\n * Refresh the legend panel contents: include entries for each visible\n * registered layer, and show/hide the panel based on whether any are visible.\n */\n _updateLegendPanel() {\n if (!this._legendPanel) return;\n\n // Rebuild children from scratch in a stable order (Map iteration order = insertion order)\n const children = [];\n for (const [layer, wrapper] of this._legendEntries) {\n if (layer.getVisible()) children.push(wrapper);\n }\n\n // Remove trailing bottom-border on the last entry for a clean look\n this._legendEntries.forEach((w) => {\n w.style.borderBottom = '1px solid var(--border, #1e1a4b1f)';\n w.style.paddingBottom = '6px';\n });\n if (children.length > 0) {\n children[children.length - 1].style.borderBottom = 'none';\n children[children.length - 1].style.paddingBottom = '0';\n }\n\n // Swap the DOM children\n this._legendPanel.replaceChildren(...children);\n this._legendPanel.style.display = children.length > 0 ? 'flex' : 'none';\n }\n\n /**\n * Escape HTML special characters for safe text insertion.\n */\n _escapeHtml(str) {\n return String(str)\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n }\n\n /**\n * Find a LayerGroup inside the overlay group by its layerId.\n *\n * @param {number|string} id - The layerId to search for\n * @returns {LayerGroup|null} The matching group, or null\n */\n getLayerGroup(id) {\n let found = null;\n this.overlayGroup.getLayers().forEach((layer) => {\n if (layer.get('layerId') === id) {\n found = layer;\n }\n });\n return found;\n }\n\n /**\n * Find a LayerGroup inside the overlay group by its title.\n *\n * @param {string} title - The group title to search for\n * @returns {LayerGroup|null} The matching group, or null\n */\n getLayerGroupByTitle(title) {\n let found = null;\n this.overlayGroup.getLayers().forEach((layer) => {\n if (layer.get('title') === title) {\n found = layer;\n }\n });\n return found;\n }\n\n /**\n * Get the overlay LayerGroup for advanced usage\n */\n getOverlayGroup() {\n return this.overlayGroup;\n }\n\n /**\n * Get the OpenLayers map instance for advanced usage\n */\n getMap() {\n return this.map;\n }\n\n // ============================================================================\n // Extent Helpers (used by offline-tile downloader)\n // ============================================================================\n\n /**\n * Get the current map view's visible extent in EPSG:3857 (Web Mercator).\n * @returns {Array} [minX, minY, maxX, maxY]\n */\n getCurrentViewExtent() {\n const view = this.map.getView();\n const size = this.map.getSize();\n if (!size) return null;\n return view.calculateExtent(size);\n }\n\n /**\n * Get the bounding extent of the District Boundary layer (if present).\n * Searches the overlay group for a vector layer titled \"District Boundary\"\n * and returns the extent of its source.\n *\n * @returns {{ extent: Array, title: string } | null}\n */\n getDistrictBoundaryExtent() {\n let found = null;\n const visit = (group) => {\n group.getLayers().forEach((layer) => {\n if (layer.getLayers) {\n visit(layer); // sub-group\n } else if (layer.get('title') === 'District Boundary') {\n const src = layer.getSource && layer.getSource();\n if (src && typeof src.getExtent === 'function') {\n const ex = src.getExtent();\n if (ex && Number.isFinite(ex[0])) {\n found = { extent: ex, title: layer.get('title') };\n }\n }\n }\n });\n };\n visit(this.overlayGroup);\n return found;\n }\n\n /**\n * Get the marker source for advanced usage\n */\n getMarkerSource() {\n return this.markerSource;\n }\n\n /**\n * Get the markers layer for advanced usage\n */\n getMarkersLayer() {\n return this.markersLayer;\n }\n\n /**\n * Update map size (call after container resize)\n */\n updateSize() {\n this.map.updateSize();\n }\n\n /**\n * Register a callback for when a search result is selected\n * Callback receives: { coordinate, lonLat: [lon, lat], name, searchResult }\n * Navigation to the location happens automatically\n */\n onSearchSelect(callback) {\n this.searchSelectCallbacks.push(callback);\n }\n\n /**\n * Navigate/fly to a specific location\n * @param {number} lon - Longitude\n * @param {number} lat - Latitude\n * @param {number} zoom - Zoom level (default: 14)\n * @param {number} duration - Animation duration in ms (default: 500)\n */\n navigateTo(lon, lat, zoom = 14, duration = 500) {\n const coordinate = fromLonLat([lon, lat]);\n this.map.getView().animate({\n center: coordinate,\n zoom: zoom,\n duration: duration,\n });\n }\n}\n\n// Export OpenLayers utilities for convenience\nexport { fromLonLat, toLonLat };\n\nexport default MapView;\n","/**\n * MapTools - Drawing and Measurement Tools\n * \n * Provides:\n * - Circle measurement tool with radius/area tooltip\n * - Control bar with drawing tools\n * - Line and polygon measurement\n * \n * Refactored from olmapstuffgis.js for LUPMIS PWA\n */\n\nimport { Draw } from 'ol/interaction';\nimport { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style';\nimport { Vector as VectorLayer } from 'ol/layer';\nimport { Vector as VectorSource } from 'ol/source';\nimport Overlay from 'ol/Overlay';\nimport { LineString, Circle, Polygon } from 'ol/geom';\nimport { getLength, getArea } from 'ol/sphere';\nimport Feature from 'ol/Feature';\nimport { unByKey } from 'ol/Observable';\nimport { formatLength, formatArea, formatCircleExtent } from '../units.js';\n\n// ol-ext imports\nimport EditBar from 'ol-ext/control/EditBar';\nimport Toggle from 'ol-ext/control/Toggle';\nimport Button from 'ol-ext/control/Button';\n\nexport class MapTools {\n constructor(map, options = {}) {\n this.map = map;\n this.options = options;\n \n // Create measurement layer\n this.measureSource = new VectorSource();\n this.measureLayer = new VectorLayer({\n source: this.measureSource,\n style: this.getMeasureStyle(),\n title: 'Measurements',\n zIndex: 100,\n });\n\n // Create drawing layer (utility layer for temporary draw interactions;\n // hidden from LayerSwitcher since the EditBar has its own \"Drawings\" group)\n this.drawSource = new VectorSource();\n this.drawLayer = new VectorLayer({\n source: this.drawSource,\n style: this.getDrawStyle(),\n title: 'Draw sketches',\n displayInLayerSwitcher: false,\n zIndex: 99,\n });\n\n // Insert both layers just before the last layer (Overlays group)\n // so the LayerSwitcher order becomes: Overlays > Measurements > Markers > Base Maps\n const layers = this.map.getLayers();\n const overlayIdx = layers.getLength() - 1; // Overlays is the last layer\n layers.insertAt(overlayIdx, this.drawLayer);\n layers.insertAt(overlayIdx, this.measureLayer);\n \n // Active interaction\n this.activeInteraction = null;\n this.measureTooltip = null;\n this.measureTooltipElement = null;\n \n // Callbacks\n this.onMeasureCompleteCallbacks = [];\n this.onDrawCompleteCallbacks = [];\n }\n \n /**\n * Get style for measurement features\n */\n getMeasureStyle() {\n return new Style({\n fill: new Fill({\n color: 'rgba(255, 233, 106, 0.2)'\n }),\n stroke: new Stroke({\n color: '#8B008B',\n lineDash: [10, 10],\n width: 2\n }),\n image: new CircleStyle({\n radius: 5,\n stroke: new Stroke({\n color: '#8B008B'\n }),\n fill: new Fill({\n color: 'rgba(255, 233, 106, 0.5)'\n })\n })\n });\n }\n \n /**\n * Get style for drawing features\n */\n getDrawStyle() {\n return new Style({\n fill: new Fill({\n color: 'rgba(255, 233, 106, 0.3)'\n }),\n stroke: new Stroke({\n color: '#8B008B',\n width: 2\n }),\n image: new CircleStyle({\n radius: 6,\n stroke: new Stroke({\n color: '#8B008B',\n width: 2\n }),\n fill: new Fill({\n color: '#FFE96A'\n })\n })\n });\n }\n \n /**\n * Create measurement tooltip overlay\n */\n createMeasureTooltip() {\n if (this.measureTooltipElement) {\n this.measureTooltipElement.parentNode.removeChild(this.measureTooltipElement);\n }\n this.measureTooltipElement = document.createElement('div');\n this.measureTooltipElement.className = 'measure-tooltip';\n this.measureTooltip = new Overlay({\n element: this.measureTooltipElement,\n offset: [15, 0],\n positioning: 'center-left',\n stopEvent: false,\n });\n this.map.addOverlay(this.measureTooltip);\n }\n \n /**\n * Remove any active interaction\n */\n deactivate() {\n if (this.activeInteraction) {\n this.map.removeInteraction(this.activeInteraction);\n this.activeInteraction = null;\n }\n if (this.measureTooltip) {\n this.map.removeOverlay(this.measureTooltip);\n this.measureTooltip = null;\n }\n if (this.measureTooltipElement && this.measureTooltipElement.parentNode) {\n this.measureTooltipElement.parentNode.removeChild(this.measureTooltipElement);\n this.measureTooltipElement = null;\n }\n }\n \n /**\n * Start circle measurement tool\n * Draws a circle and shows radius + area in tooltip\n */\n startCircleMeasure() {\n this.deactivate();\n this.createMeasureTooltip();\n \n const drawCircle = new Draw({\n source: this.measureSource,\n type: 'Circle',\n style: new Style({\n fill: new Fill({\n color: 'rgba(255, 233, 106, 0.2)'\n }),\n stroke: new Stroke({\n color: 'rgba(139, 0, 139, 0.7)',\n lineDash: [10, 10],\n width: 2\n }),\n image: new CircleStyle({\n radius: 5,\n stroke: new Stroke({\n color: 'rgba(139, 0, 139, 0.7)'\n }),\n fill: new Fill({\n color: 'rgba(255, 233, 106, 0.5)'\n })\n })\n })\n });\n \n this.activeInteraction = drawCircle;\n this.map.addInteraction(drawCircle);\n \n let listener;\n \n drawCircle.on('drawstart', (evt) => {\n const sketch = evt.feature;\n \n listener = sketch.getGeometry().on('change', (e) => {\n const geom = e.target;\n \n if (geom instanceof Circle) {\n const radius = geom.getRadius();\n const area = formatCircleExtent(radius);\n const radiusFormatted = formatLength(radius);\n \n const output = `${radiusFormatted}
            ${area}`;\n \n this.measureTooltipElement.innerHTML = output;\n this.measureTooltip.setPosition(geom.getLastCoordinate());\n }\n });\n });\n \n drawCircle.on('drawend', (evt) => {\n const feature = evt.feature;\n const geom = feature.getGeometry();\n const center = geom.getCenter();\n const radius = geom.getRadius();\n\n // Tag the circle feature so the dblclick handler can identify it\n feature.set('_layerType', 'measure_circle');\n feature.set('_radius', radius);\n feature.set('_center', center);\n\n // Create radius line for visualization\n const radiusLine = new Feature({\n geometry: new LineString([\n center,\n [center[0] + radius, center[1]]\n ])\n });\n radiusLine.set('_layerType', 'measure_circle_radius');\n this.measureSource.addFeature(radiusLine);\n \n // Make tooltip static\n this.measureTooltipElement.className = 'measure-tooltip measure-tooltip-static';\n this.measureTooltip.setOffset([0, -7]);\n \n // Create new tooltip for next measurement\n this.measureTooltipElement = null;\n this.createMeasureTooltip();\n \n unByKey(listener);\n \n // Trigger callbacks\n const result = {\n type: 'circle',\n center: center,\n radius: radius,\n area: Math.PI * radius * radius,\n feature: feature,\n };\n this.onMeasureCompleteCallbacks.forEach(cb => cb(result));\n });\n \n return drawCircle;\n }\n \n /**\n * Start line measurement tool\n */\n startLineMeasure() {\n this.deactivate();\n this.createMeasureTooltip();\n \n const drawLine = new Draw({\n source: this.measureSource,\n type: 'LineString',\n style: this.getMeasureStyle(),\n });\n \n this.activeInteraction = drawLine;\n this.map.addInteraction(drawLine);\n \n let listener;\n \n drawLine.on('drawstart', (evt) => {\n const sketch = evt.feature;\n \n listener = sketch.getGeometry().on('change', (e) => {\n const geom = e.target;\n const length = getLength(geom);\n const output = formatLength(length);\n \n this.measureTooltipElement.innerHTML = output;\n this.measureTooltip.setPosition(geom.getLastCoordinate());\n });\n });\n \n drawLine.on('drawend', (evt) => {\n const feature = evt.feature;\n const geom = feature.getGeometry();\n const length = getLength(geom);\n \n this.measureTooltipElement.className = 'measure-tooltip measure-tooltip-static';\n this.measureTooltipElement = null;\n this.createMeasureTooltip();\n \n unByKey(listener);\n \n const result = {\n type: 'line',\n length: length,\n feature: feature,\n };\n this.onMeasureCompleteCallbacks.forEach(cb => cb(result));\n });\n \n return drawLine;\n }\n \n /**\n * Start polygon/area measurement tool\n */\n startAreaMeasure() {\n this.deactivate();\n this.createMeasureTooltip();\n \n const drawPolygon = new Draw({\n source: this.measureSource,\n type: 'Polygon',\n style: this.getMeasureStyle(),\n });\n \n this.activeInteraction = drawPolygon;\n this.map.addInteraction(drawPolygon);\n \n let listener;\n \n drawPolygon.on('drawstart', (evt) => {\n const sketch = evt.feature;\n \n listener = sketch.getGeometry().on('change', (e) => {\n const geom = e.target;\n const area = getArea(geom);\n const output = formatArea(area);\n \n this.measureTooltipElement.innerHTML = output;\n this.measureTooltip.setPosition(geom.getInteriorPoint().getCoordinates());\n });\n });\n \n drawPolygon.on('drawend', (evt) => {\n const feature = evt.feature;\n const geom = feature.getGeometry();\n const area = getArea(geom);\n\n // Tag so the double-click handler can identify it\n feature.set('_layerType', 'measure_area');\n feature.set('_area', area);\n\n this.measureTooltipElement.className = 'measure-tooltip measure-tooltip-static';\n this.measureTooltipElement = null;\n this.createMeasureTooltip();\n\n unByKey(listener);\n\n const result = {\n type: 'polygon',\n area: area,\n feature: feature,\n coordinate: geom.getInteriorPoint().getCoordinates(),\n };\n this.onMeasureCompleteCallbacks.forEach(cb => cb(result));\n });\n \n return drawPolygon;\n }\n \n /**\n * Start point drawing tool\n */\n startDrawPoint() {\n this.deactivate();\n \n const drawPoint = new Draw({\n source: this.drawSource,\n type: 'Point',\n style: this.getDrawStyle(),\n });\n \n this.activeInteraction = drawPoint;\n this.map.addInteraction(drawPoint);\n \n drawPoint.on('drawend', (evt) => {\n const result = {\n type: 'point',\n feature: evt.feature,\n };\n this.onDrawCompleteCallbacks.forEach(cb => cb(result));\n });\n \n return drawPoint;\n }\n \n /**\n * Start line drawing tool\n */\n startDrawLine() {\n this.deactivate();\n \n const drawLine = new Draw({\n source: this.drawSource,\n type: 'LineString',\n style: this.getDrawStyle(),\n });\n \n this.activeInteraction = drawLine;\n this.map.addInteraction(drawLine);\n \n drawLine.on('drawend', (evt) => {\n const result = {\n type: 'line',\n feature: evt.feature,\n };\n this.onDrawCompleteCallbacks.forEach(cb => cb(result));\n });\n \n return drawLine;\n }\n \n /**\n * Start polygon drawing tool\n */\n startDrawPolygon() {\n this.deactivate();\n \n const drawPolygon = new Draw({\n source: this.drawSource,\n type: 'Polygon',\n style: this.getDrawStyle(),\n });\n \n this.activeInteraction = drawPolygon;\n this.map.addInteraction(drawPolygon);\n \n drawPolygon.on('drawend', (evt) => {\n const result = {\n type: 'polygon',\n feature: evt.feature,\n };\n this.onDrawCompleteCallbacks.forEach(cb => cb(result));\n });\n \n return drawPolygon;\n }\n \n /**\n * Clear all measurements\n */\n clearMeasurements() {\n this.measureSource.clear();\n // Remove static tooltips\n const tooltips = document.querySelectorAll('.measure-tooltip-static');\n tooltips.forEach(el => el.parentNode.removeChild(el));\n }\n \n /**\n * Clear all drawings\n */\n clearDrawings() {\n this.drawSource.clear();\n }\n \n /**\n * Clear all (measurements + drawings)\n */\n clearAll() {\n this.clearMeasurements();\n this.clearDrawings();\n }\n \n /**\n * Register callback for measurement completion\n */\n onMeasureComplete(callback) {\n this.onMeasureCompleteCallbacks.push(callback);\n }\n \n /**\n * Register callback for drawing completion\n */\n onDrawComplete(callback) {\n this.onDrawCompleteCallbacks.push(callback);\n }\n \n /**\n * Create a control bar with measurement and drawing tools\n * Returns the ol-ext Bar control\n */\n createControlBar(options = {}) {\n const position = options.position || 'top-left';\n \n // Main control bar\n const mainBar = new EditBar({\n group: true,\n className: 'map-tools-bar',\n });\n \n // Measurement toggle group\n const measureBar = new EditBar({\n toggleOne: true,\n group: true,\n });\n \n // Circle measure button\n const circleBtn = new Toggle({\n html: '',\n title: 'Measure Circle (radius & area)',\n className: 'measure-circle-btn',\n onToggle: (active) => {\n if (active) {\n this.startCircleMeasure();\n } else {\n this.deactivate();\n }\n }\n });\n measureBar.addControl(circleBtn);\n \n // Line measure button\n const lineBtn = new Toggle({\n html: '📏',\n title: 'Measure Distance',\n className: 'measure-line-btn',\n onToggle: (active) => {\n if (active) {\n this.startLineMeasure();\n } else {\n this.deactivate();\n }\n }\n });\n measureBar.addControl(lineBtn);\n \n // Area measure button\n const areaBtn = new Toggle({\n html: '',\n title: 'Measure Area',\n className: 'measure-area-btn',\n onToggle: (active) => {\n if (active) {\n this.startAreaMeasure();\n } else {\n this.deactivate();\n }\n }\n });\n measureBar.addControl(areaBtn);\n \n // Clear measurements button\n const clearBtn = new Button({\n html: '🗑️',\n title: 'Clear Measurements',\n className: 'clear-measure-btn',\n handleClick: () => {\n this.clearMeasurements();\n // Deactivate any active toggle\n circleBtn.setActive(false);\n lineBtn.setActive(false);\n areaBtn.setActive(false);\n }\n });\n measureBar.addControl(clearBtn);\n \n mainBar.addControl(measureBar);\n \n return mainBar;\n }\n \n /**\n * Get the measure layer\n */\n getMeasureLayer() {\n return this.measureLayer;\n }\n \n /**\n * Get the draw layer\n */\n getDrawLayer() {\n return this.drawLayer;\n }\n \n /**\n * Get the measure source\n */\n getMeasureSource() {\n return this.measureSource;\n }\n \n /**\n * Get the draw source\n */\n getDrawSource() {\n return this.drawSource;\n }\n \n /**\n * Check if any tool is currently active\n */\n isActive() {\n return this.activeInteraction !== null;\n }\n}\n\nexport default MapTools;\n","/**\n * PWA Module\n * \n * Handles Progressive Web App functionality:\n * - Service Worker registration\n * - Install prompt handling\n * - Offline detection\n * - Update notifications\n * \n * Note: The Service Worker (sw.js) handles caching.\n * The SharedWorker (shared-db-worker.js) handles database.\n * They are separate workers with different purposes.\n */\n\n// ============================================================================\n// Service Worker Registration\n// ============================================================================\n\nlet swRegistration = null;\n\nexport async function registerServiceWorker() {\n if (!('serviceWorker' in navigator)) {\n console.warn('[PWA] Service Workers not supported');\n return null;\n }\n \n try {\n swRegistration = await navigator.serviceWorker.register('/sw.js', {\n scope: '/'\n });\n \n console.log('[PWA] Service Worker registered:', swRegistration.scope);\n \n // Handle updates\n swRegistration.addEventListener('updatefound', () => {\n const newWorker = swRegistration.installing;\n \n newWorker.addEventListener('statechange', () => {\n if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {\n // New version available\n console.log('[PWA] New version available');\n showUpdateNotification();\n }\n });\n });\n \n return swRegistration;\n \n } catch (error) {\n console.error('[PWA] Service Worker registration failed:', error);\n return null;\n }\n}\n\n// ============================================================================\n// Install Prompt\n// ============================================================================\n\nlet deferredPrompt = null;\nlet installButton = null;\n\n/**\n * Initialize install prompt handling\n * @param {string|HTMLElement} buttonSelector - Button element or selector\n */\nexport function initInstallPrompt(buttonSelector = '#install-btn') {\n installButton = typeof buttonSelector === 'string' \n ? document.querySelector(buttonSelector)\n : buttonSelector;\n \n if (!installButton) {\n console.warn('[PWA] Install button not found:', buttonSelector);\n return;\n }\n \n // Initially hide the button\n installButton.style.display = 'none';\n \n // Listen for the beforeinstallprompt event\n window.addEventListener('beforeinstallprompt', (e) => {\n e.preventDefault();\n deferredPrompt = e;\n \n // Show the install button\n installButton.style.display = 'block';\n console.log('[PWA] Install prompt ready');\n });\n \n // Handle install button click\n installButton.addEventListener('click', async () => {\n if (!deferredPrompt) {\n // Show manual instructions for Safari\n showManualInstallInstructions();\n return;\n }\n \n deferredPrompt.prompt();\n const { outcome } = await deferredPrompt.userChoice;\n \n console.log('[PWA] Install prompt outcome:', outcome);\n \n deferredPrompt = null;\n installButton.style.display = 'none';\n });\n \n // Hide button if app is already installed\n window.addEventListener('appinstalled', () => {\n console.log('[PWA] App installed');\n deferredPrompt = null;\n installButton.style.display = 'none';\n });\n \n // Check if running as installed PWA\n if (window.matchMedia('(display-mode: standalone)').matches) {\n installButton.style.display = 'none';\n }\n}\n\nfunction showManualInstallInstructions() {\n const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);\n const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);\n \n let message = 'To install this app:\\n\\n';\n \n if (isIOS) {\n message += '1. Tap the Share button (square with arrow)\\n';\n message += '2. Scroll down and tap \"Add to Home Screen\"';\n } else if (isSafari) {\n message += '1. Click File menu\\n';\n message += '2. Click \"Add to Dock\"';\n } else {\n message += '1. Click the menu button (three dots)\\n';\n message += '2. Click \"Install\" or \"Add to Home Screen\"';\n }\n \n alert(message);\n}\n\n// ============================================================================\n// Offline Detection\n// ============================================================================\n\nlet offlineIndicator = null;\nconst offlineListeners = new Set();\n\n/**\n * Initialize offline detection\n * @param {string|HTMLElement} indicatorSelector - Element to show when offline\n */\nexport function initOfflineDetection(indicatorSelector = '#offline-indicator') {\n offlineIndicator = typeof indicatorSelector === 'string'\n ? document.querySelector(indicatorSelector)\n : indicatorSelector;\n \n // Set initial state\n updateOfflineUI(!navigator.onLine);\n \n // Listen for online/offline events\n window.addEventListener('online', () => {\n console.log('[PWA] Back online');\n updateOfflineUI(false);\n notifyOfflineListeners(false);\n });\n \n window.addEventListener('offline', () => {\n console.log('[PWA] Gone offline');\n updateOfflineUI(true);\n notifyOfflineListeners(true);\n });\n}\n\nfunction updateOfflineUI(isOffline) {\n if (offlineIndicator) {\n offlineIndicator.style.display = isOffline ? 'block' : 'none';\n }\n \n // Also toggle a class on body for CSS styling\n document.body.classList.toggle('is-offline', isOffline);\n}\n\n/**\n * Subscribe to offline state changes\n * @param {Function} listener - Callback(isOffline: boolean)\n * @returns {Function} Unsubscribe function\n */\nexport function onOfflineChange(listener) {\n offlineListeners.add(listener);\n // Immediately call with current state\n listener(!navigator.onLine);\n return () => offlineListeners.delete(listener);\n}\n\nfunction notifyOfflineListeners(isOffline) {\n for (const listener of offlineListeners) {\n try {\n listener(isOffline);\n } catch (e) {\n console.error('[PWA] Offline listener error:', e);\n }\n }\n}\n\n/**\n * Check if currently online\n */\nexport function isOnline() {\n return navigator.onLine;\n}\n\n// ============================================================================\n// Update Handling\n// ============================================================================\n\nlet updateCallback = null;\n\n/**\n * Set callback for when updates are available\n * @param {Function} callback - Called when new version is ready\n */\nexport function onUpdateAvailable(callback) {\n updateCallback = callback;\n}\n\nfunction showUpdateNotification() {\n if (updateCallback) {\n updateCallback();\n return;\n }\n \n // Default behavior\n if (confirm('A new version is available. Reload now?')) {\n applyUpdate();\n }\n}\n\n/**\n * Apply pending update (reload with new version)\n */\nexport function applyUpdate() {\n if (swRegistration?.waiting) {\n swRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });\n }\n window.location.reload();\n}\n\n// ============================================================================\n// Communication with Service Worker\n// ============================================================================\n\n/**\n * Send a message to the service worker\n * @param {Object} message - Message to send\n */\nexport function postToServiceWorker(message) {\n navigator.serviceWorker.controller?.postMessage(message);\n}\n\n/**\n * Request the service worker to cache specific modules\n * @param {string[]} moduleNames - Array of module names to cache\n */\nexport function cacheModules(moduleNames) {\n postToServiceWorker({\n type: 'CACHE_MODULES',\n payload: { modules: moduleNames }\n });\n}\n\n/**\n * Request the service worker to clear user-specific caches\n * (Call this on logout)\n */\nexport function clearUserCaches() {\n postToServiceWorker({\n type: 'CLEAR_USER_CACHE'\n });\n}\n\n/**\n * Get the Service Worker we can postMessage to. Resolves with:\n * • `navigator.serviceWorker.controller` if it's already in control of the\n * page (fastest path), or\n * • `registration.active` once `navigator.serviceWorker.ready` resolves\n * (covers the first-load case before the SW has claimed the page).\n *\n * Rejects after `timeoutMs` if no SW becomes available — which would only\n * happen in a private/incognito context, an unsupported browser, or when\n * registration genuinely failed.\n *\n * @param {{ timeoutMs?: number }} [opts]\n * @returns {Promise}\n */\nexport async function getActiveServiceWorker({ timeoutMs = 10000 } = {}) {\n if (!('serviceWorker' in navigator)) {\n throw new Error('Service Workers not supported in this browser');\n }\n\n // Fastest path — page is already SW-controlled\n if (navigator.serviceWorker.controller) {\n return navigator.serviceWorker.controller;\n }\n\n // Otherwise wait for the registration to become ready (active SW exists\n // for this scope, even if it hasn't claimed THIS page yet)\n const ready = navigator.serviceWorker.ready;\n const timeout = new Promise((_, reject) =>\n setTimeout(() => reject(new Error('Service-worker readiness timeout')), timeoutMs)\n );\n\n const registration = await Promise.race([ready, timeout]);\n\n // The controller may have appeared while we were waiting; otherwise use\n // the registration's active worker (we can still postMessage to it — caches\n // are shared across the origin)\n const sw = navigator.serviceWorker.controller || registration.active;\n if (!sw) {\n throw new Error('No active service worker available');\n }\n return sw;\n}\n\n/**\n * Subscribe to controller-change events. The callback fires whenever a new\n * Service Worker takes control of the page (e.g. after an SW update or first\n * activation on initial load). Useful for re-querying SW-backed state once\n * the SW has actually taken over.\n *\n * @param {() => void} callback\n * @returns {() => void} unsubscribe function\n */\nexport function onServiceWorkerControllerChange(callback) {\n if (!('serviceWorker' in navigator)) return () => {};\n const handler = () => {\n try { callback(); } catch (e) { console.error('[PWA] controllerchange handler error:', e); }\n };\n navigator.serviceWorker.addEventListener('controllerchange', handler);\n return () => navigator.serviceWorker.removeEventListener('controllerchange', handler);\n}\n\n/**\n * Send a message to the service worker and wait for a single reply with the\n * given response type. Waits for the SW to become available if it isn't yet.\n * Resolves with the reply payload, or rejects after a timeout.\n *\n * @template T\n * @param {string} requestType - Message type to send (e.g. 'GET_TILE_STATS')\n * @param {string} responseType - Message type expected back (e.g. 'TILE_STATS')\n * @param {Object} [extra={}] - Extra fields merged into the outgoing message\n * @param {number} [timeoutMs=5000] Reply timeout (after the SW is available)\n * @param {number} [readyTimeoutMs=10000] Timeout for the SW to be available\n * @returns {Promise}\n */\nasync function requestFromServiceWorker(requestType, responseType, extra = {}, timeoutMs = 5000, readyTimeoutMs = 10000) {\n const sw = await getActiveServiceWorker({ timeoutMs: readyTimeoutMs });\n\n return new Promise((resolve, reject) => {\n const channel = new MessageChannel();\n const timer = setTimeout(() => {\n channel.port1.close();\n reject(new Error(`Service-worker reply \"${responseType}\" timed out`));\n }, timeoutMs);\n\n channel.port1.onmessage = (event) => {\n if (event.data?.type === responseType) {\n clearTimeout(timer);\n channel.port1.close();\n const { type, ...rest } = event.data;\n resolve(rest);\n }\n };\n\n sw.postMessage({ type: requestType, ...extra }, [channel.port2]);\n });\n}\n\n/**\n * Get statistics about tiles cached locally on this device, broken down by\n * provider. Waits up to `readyTimeoutMs` for the service worker to become\n * available. Returns null only if the SW genuinely cannot be reached\n * (private mode, registration failure, or timeout).\n *\n * @returns {Promise<{\n * totals: { count: number, estBytes: number },\n * byProvider: Array<{ key: string, label: string, count: number, limit: number, estBytes: number }>\n * } | null>}\n */\nexport async function getTileCacheStats() {\n try {\n const reply = await requestFromServiceWorker('GET_TILE_STATS', 'TILE_STATS');\n return reply.stats;\n } catch (err) {\n console.warn('[PWA] getTileCacheStats failed:', err);\n return null;\n }\n}\n\n/**\n * Delete every cached tile from this device. Doesn't touch the app shell,\n * modules, or API caches — only the per-provider tile buckets.\n * Waits for the SW to be available before sending the request.\n *\n * @returns {Promise} true if the request was acknowledged\n */\nexport async function clearTileCaches() {\n try {\n await requestFromServiceWorker('CLEAR_TILE_CACHES', 'TILE_CACHES_CLEARED');\n return true;\n } catch (err) {\n console.warn('[PWA] clearTileCaches failed:', err);\n return false;\n }\n}\n\n/**\n * Delete cached tiles for a single provider. `cacheName` must match one of\n * the per-provider caches reported by `getTileCacheStats()` (e.g.\n * `tiles-osm-v4`, `tiles-topo-v4`). Unknown names are rejected by the SW.\n *\n * @param {string} cacheName\n * @returns {Promise} true if the cache was actually deleted\n */\nexport async function clearTileCacheForProvider(cacheName) {\n if (!cacheName) return false;\n try {\n const reply = await requestFromServiceWorker(\n 'CLEAR_TILE_CACHE', 'TILE_CACHE_CLEARED',\n { cacheName },\n );\n return !!reply.deleted;\n } catch (err) {\n console.warn(`[PWA] clearTileCacheForProvider(${cacheName}) failed:`, err);\n return false;\n }\n}\n\n/**\n * Get total disk used by this origin (Cache API + IndexedDB + OPFS).\n * Returns null if the Storage API is not available.\n *\n * @returns {Promise<{ usage: number, quota: number } | null>}\n */\nexport async function getStorageEstimate() {\n if (!navigator.storage?.estimate) return null;\n try {\n const { usage, quota } = await navigator.storage.estimate();\n return { usage: usage || 0, quota: quota || 0 };\n } catch (err) {\n console.warn('[PWA] getStorageEstimate failed:', err);\n return null;\n }\n}\n\n// ============================================================================\n// Auto-initialization\n// ============================================================================\n\n/**\n * Initialize all PWA features\n * @param {Object} options\n */\nexport async function initPWA(options = {}) {\n const {\n installButton = '#install-btn',\n offlineIndicator = '#offline-indicator',\n autoRegisterSW = true\n } = options;\n \n if (autoRegisterSW) {\n await registerServiceWorker();\n }\n \n initInstallPrompt(installButton);\n initOfflineDetection(offlineIndicator);\n \n console.log('[PWA] Initialized');\n}\n\n// Export for direct use\nexport default {\n registerServiceWorker,\n initInstallPrompt,\n initOfflineDetection,\n initPWA,\n isOnline,\n onOfflineChange,\n onUpdateAvailable,\n applyUpdate,\n postToServiceWorker,\n cacheModules,\n clearUserCaches,\n getTileCacheStats,\n clearTileCaches,\n clearTileCacheForProvider,\n getStorageEstimate,\n getActiveServiceWorker,\n onServiceWorkerControllerChange,\n};\n","/**\n * Offline Tile Downloader\n *\n * Pre-fetches map tiles for a given extent and zoom range so they are stored\n * in the Service Worker's per-host tile cache for offline use.\n *\n * The downloader simply issues `fetch()` calls; the existing SW intercepts\n * them and routes to the right cache bucket. No direct Cache API access is\n * needed here — the SW is the single source of truth for storage.\n *\n * Throttling defaults are conservative to respect tile-server usage policies:\n * • 2 concurrent requests\n * • 50 ms inter-batch delay\n * • Standard browser User-Agent / Referer headers\n *\n * Usage:\n * const downloader = new OfflineTileDownloader({\n * baseMap: 'topo',\n * extent3857: [minX, minY, maxX, maxY], // EPSG:3857\n * minZoom: 10,\n * maxZoom: 15,\n * onProgress: (s) => console.log(s),\n * });\n * await downloader.start();\n * downloader.cancel(); // any time\n */\n\n// ============================================================================\n// Base-map URL templates\n// ============================================================================\n\n/**\n * Tile URL templates for base maps that may be downloaded for offline use.\n *\n * The SW recognises these hosts in `getTileCacheName()` and routes them to\n * the matching `tiles-*-vN` cache. If you add a new entry here, also add\n * the host to the SW's classifier or the tiles will not be cached.\n */\nexport const BASEMAP_TEMPLATES = {\n topo: {\n url: 'https://a.tile.opentopomap.org/{z}/{x}/{y}.png',\n label: 'Topographic',\n maxZoom: 17,\n cacheKey: 'tiles-topo',\n },\n osm: {\n url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png',\n label: 'OpenStreetMap',\n maxZoom: 19,\n cacheKey: 'tiles-osm',\n },\n};\n\n// Approximate bytes per raster tile — used for storage estimates.\nexport const AVG_TILE_BYTES = 30 * 1024;\n\n// ============================================================================\n// Tile coordinate math (Web Mercator XYZ scheme)\n// ============================================================================\n\nconst ORIGIN_SHIFT = 2 * Math.PI * 6378137 / 2; // 20037508.342789244\n\n/** Convert Web Mercator metres → (lon, lat) in degrees. */\nfunction metersToLonLat(x, y) {\n const lon = (x / ORIGIN_SHIFT) * 180;\n let lat = (y / ORIGIN_SHIFT) * 180;\n lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180)) - Math.PI / 2);\n return [lon, lat];\n}\n\n/** Tile (x, y) in XYZ scheme for a given lon/lat at zoom z. */\nfunction lonLatToTile(lon, lat, z) {\n const n = Math.pow(2, z);\n const x = Math.floor((lon + 180) / 360 * n);\n const latRad = lat * Math.PI / 180;\n const y = Math.floor(\n (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n\n );\n return { x, y };\n}\n\n/** Tile range covering an EPSG:3857 extent at a given zoom level. */\nexport function tileRangeForExtent(extent3857, z) {\n const [minX, minY, maxX, maxY] = extent3857;\n const [minLon, minLat] = metersToLonLat(minX, minY);\n const [maxLon, maxLat] = metersToLonLat(maxX, maxY);\n\n const tl = lonLatToTile(minLon, maxLat, z); // top-left in XYZ (NW)\n const br = lonLatToTile(maxLon, minLat, z); // bottom-right (SE)\n\n const n = Math.pow(2, z);\n const minTileX = Math.max(0, Math.min(tl.x, br.x));\n const maxTileX = Math.min(n - 1, Math.max(tl.x, br.x));\n const minTileY = Math.max(0, Math.min(tl.y, br.y));\n const maxTileY = Math.min(n - 1, Math.max(tl.y, br.y));\n\n return {\n z,\n minX: minTileX, maxX: maxTileX,\n minY: minTileY, maxY: maxTileY,\n count: (maxTileX - minTileX + 1) * (maxTileY - minTileY + 1),\n };\n}\n\n/** Total tile count for an extent across a zoom range (inclusive). */\nexport function countTiles(extent3857, minZ, maxZ) {\n let total = 0;\n for (let z = minZ; z <= maxZ; z++) {\n total += tileRangeForExtent(extent3857, z).count;\n }\n return total;\n}\n\n/**\n * Enumerate every tile in an extent across a zoom range.\n * Returns an array of { z, x, y } objects. For very large ranges this can be\n * large — the caller is expected to validate the count first.\n */\nexport function enumerateTiles(extent3857, minZ, maxZ) {\n const out = [];\n for (let z = minZ; z <= maxZ; z++) {\n const r = tileRangeForExtent(extent3857, z);\n for (let x = r.minX; x <= r.maxX; x++) {\n for (let y = r.minY; y <= r.maxY; y++) {\n out.push({ z, x, y });\n }\n }\n }\n return out;\n}\n\n/**\n * Format a tile URL for a given coordinate using a {z}/{x}/{y} template.\n */\nexport function formatTileUrl(template, { z, x, y }) {\n return template\n .replace('{z}', z)\n .replace('{x}', x)\n .replace('{y}', y);\n}\n\n// ============================================================================\n// OfflineTileDownloader\n// ============================================================================\n\n/**\n * Concurrent, throttled tile downloader. Issues `fetch()` per tile; the\n * service worker handles caching transparently.\n *\n * Events via `onProgress` callback:\n * { phase: 'running' | 'done' | 'cancelled' | 'error',\n * done, total, ok, failed, cached,\n * elapsedMs, etaMs }\n */\nexport class OfflineTileDownloader {\n constructor({\n baseMap, // 'topo' | 'osm'\n extent3857, // [minX, minY, maxX, maxY]\n minZoom,\n maxZoom,\n concurrency = 2, // OSM ToS-friendly default\n interBatchDelayMs = 50,\n onProgress = () => {},\n }) {\n const tpl = BASEMAP_TEMPLATES[baseMap];\n if (!tpl) throw new Error(`Unknown base map: ${baseMap}`);\n if (maxZoom > tpl.maxZoom) {\n console.warn(`[OfflineTiles] ${baseMap}: maxZoom ${maxZoom} > supported ${tpl.maxZoom}; clamping`);\n maxZoom = tpl.maxZoom;\n }\n\n this.baseMap = baseMap;\n this.template = tpl.url;\n this.extent = extent3857;\n this.minZoom = minZoom;\n this.maxZoom = maxZoom;\n this.concurrency = Math.max(1, Math.min(concurrency, 6));\n this.interBatchDelayMs = interBatchDelayMs;\n this.onProgress = onProgress;\n\n this._abortCtrl = null;\n this._cancelled = false;\n }\n\n /**\n * Begin downloading. Returns a Promise that resolves with the final stats\n * when complete, or when cancelled.\n */\n async start() {\n if (this._abortCtrl) throw new Error('Downloader already started');\n this._abortCtrl = new AbortController();\n this._cancelled = false;\n\n const tiles = enumerateTiles(this.extent, this.minZoom, this.maxZoom);\n const total = tiles.length;\n const startedAt = Date.now();\n\n let done = 0, ok = 0, failed = 0, cached = 0;\n\n const emit = (phase) => {\n const elapsedMs = Date.now() - startedAt;\n const etaMs = done > 0 ? Math.round((elapsedMs / done) * (total - done)) : null;\n this.onProgress({ phase, done, total, ok, failed, cached, elapsedMs, etaMs });\n };\n\n emit('running');\n\n // Process in chunks of `concurrency`\n for (let i = 0; i < tiles.length; i += this.concurrency) {\n if (this._cancelled) break;\n\n const batch = tiles.slice(i, i + this.concurrency);\n await Promise.all(batch.map(async (t) => {\n if (this._cancelled) return;\n const url = formatTileUrl(this.template, t);\n\n try {\n const res = await fetch(url, {\n signal: this._abortCtrl.signal,\n // Hint the SW that this is a passive prefetch\n cache: 'default',\n });\n\n if (res.ok) {\n ok++;\n // Detect \"served from SW cache\" via headers — not reliable across\n // implementations, so we just count all 200s as ok. Reading the body\n // (or cancelling it) lets the browser GC the response promptly.\n if (res.body) res.body.cancel().catch(() => {});\n } else if (res.status === 408) {\n // Our SW returns 408 when offline AND nothing cached. Treat as failed.\n failed++;\n } else {\n failed++;\n }\n } catch (err) {\n if (err.name === 'AbortError') {\n // Cancellation — don't count\n } else {\n failed++;\n }\n }\n done++;\n }));\n\n emit('running');\n\n if (this.interBatchDelayMs > 0 && i + this.concurrency < tiles.length) {\n await new Promise((r) => setTimeout(r, this.interBatchDelayMs));\n }\n }\n\n emit(this._cancelled ? 'cancelled' : 'done');\n\n return {\n phase: this._cancelled ? 'cancelled' : 'done',\n done, total, ok, failed, cached,\n elapsedMs: Date.now() - startedAt,\n };\n }\n\n /**\n * Cancel an in-flight download. Resolves on the next batch boundary.\n */\n cancel() {\n this._cancelled = true;\n if (this._abortCtrl) this._abortCtrl.abort();\n }\n}\n\n// ============================================================================\n// Predefined extents\n// ============================================================================\n\n/**\n * Whole-of-Ghana bounding box in EPSG:3857.\n * Approximate: -3.3°W → 1.2°E, 4.5°N → 11.2°N.\n */\nexport const GHANA_EXTENT_3857 = (() => {\n const lonLatToMeters = (lon, lat) => {\n const x = lon * ORIGIN_SHIFT / 180;\n const y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);\n return [x, y * ORIGIN_SHIFT / 180];\n };\n const sw = lonLatToMeters(-3.3, 4.5);\n const ne = lonLatToMeters(1.2, 11.2);\n return [sw[0], sw[1], ne[0], ne[1]];\n})();\n\n// Useful for size estimates\nexport function estimatedSizeBytes(tileCount) {\n return tileCount * AVG_TILE_BYTES;\n}\n","/**\n * Remote Database Module\n *\n * Handles all API communication with the PostgreSQL backend server.\n * Provides GET and POST methods for fetching and pushing data.\n *\n * Usage:\n * import { remoteGet, remotePost, getDistrictBoundary } from './remotedb.js';\n *\n * const boundary = await getDistrictBoundary();\n */\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\nconst API_BASE = 'https://api.lupmis4luspa.org/api/spatial_planning';\n\n/**\n * Per-request credentials sent with every API call.\n *\n * `district_id` is resolved dynamically — when the PWA is loaded via the PHP\n * entry point (public/index.php), the SSO session is injected into the page\n * as `window.LUPMIS_SESSION` and we read the authenticated user's district\n * from there. In local development (Vite serves index.html directly without\n * PHP), the global is undefined and we fall back to the hard-coded test\n * district below.\n *\n * `api_token` is currently a single global app token — not per-user.\n */\nconst FALLBACK_DISTRICT_ID = '1';\nconst API_TOKEN = '1c46538c712e9b5b';\n\n/**\n * Returns the authenticated user's district_id, or the dev fallback.\n * The getter runs on each spread of API_CREDENTIALS, so changing the\n * session at runtime (rare but possible) takes effect immediately.\n */\nfunction resolveDistrictId() {\n try {\n const id = (typeof window !== 'undefined') && window.LUPMIS_SESSION?.district_id;\n if (id !== null && id !== undefined && String(id).length > 0) {\n return String(id);\n }\n } catch { /* no-op */ }\n return FALLBACK_DISTRICT_ID;\n}\n\nconst API_CREDENTIALS = {\n get district_id() { return resolveDistrictId(); },\n api_token: API_TOKEN,\n};\n\n/**\n * Get the full session payload (or null if not authenticated).\n * Exposed for UI code that wants to display the user's name, email, etc.\n *\n * Dev-mode helper: setting `localStorage['dev-session']` to a JSON object\n * (e.g. via `lupmisDevSession({...})` in the console) overrides the real\n * session — useful when running against `localhost:5173` to test the\n * authenticated UI without standing up a PHP server.\n */\nexport function getSession() {\n // 1. Real session injected by index.php (production)\n if (typeof window !== 'undefined' && window.LUPMIS_SESSION && window.LUPMIS_SESSION.user_id) {\n return window.LUPMIS_SESSION;\n }\n // 2. Dev-mode override (developer's own localStorage tweak)\n try {\n const raw = localStorage.getItem('dev-session');\n if (raw) {\n const parsed = JSON.parse(raw);\n if (parsed && parsed.user_id) return parsed;\n }\n } catch { /* ignore */ }\n return null;\n}\n\n// Console helper — set a fake session for dev work. Reload to apply.\n// lupmisDevSession({ user_id: 42, district_id: '1', full_name: 'Test User', ... })\n// Clear it via lupmisDevSession(null) or localStorage.removeItem('dev-session').\nif (typeof window !== 'undefined') {\n window.lupmisDevSession = (payload) => {\n if (payload == null) {\n localStorage.removeItem('dev-session');\n console.log('[Dev] Session override cleared. Reload to apply.');\n } else {\n localStorage.setItem('dev-session', JSON.stringify(payload));\n console.log('[Dev] Session override saved. Reload to apply:', payload);\n }\n };\n}\n\n// ============================================================================\n// Server Reachability\n// ============================================================================\n\n/** Default timeout for API requests (ms) */\nconst REQUEST_TIMEOUT = 30_000;\n\n/** Timeout for the fast reachability probe (ms) */\nconst PING_TIMEOUT = 5_000;\n\n/** Cached result of the last reachability check */\nlet _serverReachable = null;\n\n/**\n * Quick probe to determine if the API server is responding.\n * Sends a small POST to a lightweight endpoint with a short timeout.\n * The result is cached so subsequent calls within the same page load\n * return immediately.\n *\n * @param {boolean} [force=false] - Re-check even if a cached result exists\n * @returns {Promise} true if the server responded in time\n */\nexport async function checkServerReachable(force = false) {\n if (_serverReachable !== null && !force) return _serverReachable;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), PING_TIMEOUT);\n\n try {\n const response = await fetch(`${API_BASE}/get_layers.php`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },\n body: JSON.stringify(API_CREDENTIALS),\n signal: controller.signal,\n });\n _serverReachable = response.ok;\n } catch {\n _serverReachable = false;\n } finally {\n clearTimeout(timer);\n }\n\n console.log('[RemoteDB] Server reachable:', _serverReachable);\n return _serverReachable;\n}\n\n/**\n * Returns the cached server-reachability flag (synchronous).\n * Returns null if checkServerReachable() has not been called yet.\n * @returns {boolean|null}\n */\nexport function isServerReachable() {\n return _serverReachable;\n}\n\n// ============================================================================\n// Core Request Helpers\n// ============================================================================\n\n/**\n * Create an AbortController that auto-aborts after `ms` milliseconds.\n * If the caller already supplied a signal in `options`, it is combined\n * so that either the caller's abort or the timeout will cancel the request.\n */\nfunction withTimeout(options, ms = REQUEST_TIMEOUT) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n\n // If the caller provided their own signal, chain it\n if (options.signal) {\n options.signal.addEventListener('abort', () => controller.abort());\n }\n\n return {\n signal: controller.signal,\n clear: () => clearTimeout(timer),\n };\n}\n\n/**\n * Perform a GET request to the remote API.\n * Credentials are sent as URL query parameters.\n * Automatically times out after REQUEST_TIMEOUT ms.\n *\n * @param {string} endpoint - API endpoint filename (e.g. 'get_district_boundary.php')\n * @param {Object} [params={}] - Additional query parameters\n * @param {Object} [options={}] - Extra fetch options\n * @returns {Promise} Parsed JSON response\n */\nexport async function remoteGet(endpoint, params = {}, options = {}) {\n const url = new URL(`${API_BASE}/${endpoint}`);\n\n // Attach credentials and any extra params as query string\n const allParams = { ...API_CREDENTIALS, ...params };\n for (const [key, value] of Object.entries(allParams)) {\n url.searchParams.set(key, value);\n }\n\n console.log('[RemoteDB] GET', url.toString());\n\n const timeout = withTimeout(options);\n try {\n const response = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n 'Accept': 'application/json'\n },\n ...options,\n signal: timeout.signal,\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n console.log('[RemoteDB] GET response:', endpoint, '→', typeof data === 'object' ? `${Array.isArray(data) ? data.length + ' items' : 'object'}` : data);\n return data;\n\n } catch (error) {\n if (error.name === 'AbortError') {\n console.error('[RemoteDB] GET timed out:', endpoint);\n throw new Error(`Request timed out: ${endpoint}`);\n }\n console.error('[RemoteDB] GET failed:', endpoint, error);\n throw error;\n } finally {\n timeout.clear();\n }\n}\n\n/**\n * Perform a POST request to the remote API.\n * Credentials are included in the JSON body.\n * Automatically times out after REQUEST_TIMEOUT ms.\n *\n * @param {string} endpoint - API endpoint filename (e.g. 'some_endpoint.php')\n * @param {Object} [body={}] - Request payload (credentials are merged in)\n * @param {Object} [options={}] - Extra fetch options\n * @returns {Promise} Parsed JSON response\n */\nexport async function remotePost(endpoint, body = {}, options = {}) {\n const url = `${API_BASE}/${endpoint}`;\n\n const payload = { ...API_CREDENTIALS, ...body };\n\n console.log('[RemoteDB] POST', url);\n\n const timeout = withTimeout(options);\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json'\n },\n body: JSON.stringify(payload),\n ...options,\n signal: timeout.signal,\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n console.log('[RemoteDB] POST response:', endpoint, '→', typeof data === 'object' ? `${Array.isArray(data) ? data.length + ' items' : 'object'}` : data);\n return data;\n\n } catch (error) {\n if (error.name === 'AbortError') {\n console.error('[RemoteDB] POST timed out:', endpoint);\n throw new Error(`Request timed out: ${endpoint}`);\n }\n console.error('[RemoteDB] POST failed:', endpoint, error);\n throw error;\n } finally {\n timeout.clear();\n }\n}\n\n// ============================================================================\n// Spatial Planning Endpoints\n// ============================================================================\n\n/**\n * Fetch district boundary geometry from the server.\n *\n * @returns {Promise} District boundary GeoJSON or API response\n */\nexport async function getDistrictBoundary() {\n return remotePost('get_district_boundary.php');\n}\n\n/**\n * Fetch the list of available map layer categories from the server.\n *\n * Response format:\n * { success: true, data: [{ id, name, description, createdt, editdt }, ...] }\n *\n * @returns {Promise} Layer categories list\n */\nexport async function getLayers() {\n return remotePost('get_layers.php');\n}\n\n/**\n * Fetch all collector zones for the current district.\n *\n * Expected response:\n * { success: true, data: [{ id, zone_name, boundary: \"MULTIPOLYGON(...)\", ... }, ...] }\n *\n * @returns {Promise} Collector zones list\n */\nexport async function getCollectorZones() {\n return remotePost('get_all_collector_zone_per_district.php');\n}\n\n/**\n * Fetch all parcels for the current district.\n *\n * Expected response:\n * { success: true, data: [{ id, ..., polygon: \"POLYGON(...)\" | \"MULTIPOLYGON(...)\", ... }, ...] }\n *\n * @returns {Promise} Parcels list\n */\nexport async function getDistrictParcels() {\n return remotePost('get_parcels_per_district.php');\n}\n\n/**\n * Fetch all building footprints for the current district.\n *\n * Expected response:\n * { success: true, data: [{ id, ..., polygon: \"POLYGON(...)\" | \"MULTIPOLYGON(...)\", ... }, ...] }\n *\n * @returns {Promise} Building footprints list\n */\nexport async function getBuildingFootprints() {\n return remotePost('get_all_footprint_per_district.php');\n}\n\n/**\n * Fetch the Contours hillshade elevation layer from the server.\n *\n * Source: table `be_contour_hillside` in the local PostgreSQL `public` schema\n * (imported from OpenTopography's viz.hh_hillshade).\n *\n * The current district_id is passed automatically via API_CREDENTIALS.\n *\n * Expected response:\n * { success: true, data: [{ id, elevation, geom: \"LINESTRING(...)\" | \"MULTILINESTRING(...)\" | \"POLYGON(...)\", ... }, ...] }\n *\n * @returns {Promise} Contours hillshade list\n */\nexport async function getContoursHillshade() {\n return remotePost('get_contours_hillshade.php');\n}\n\n/**\n * Fetch the OSM roads layer from the server.\n *\n * Source: table `pi_osm_roads` in the local PostgreSQL `public` schema\n * (imported from OpenStreetMap road network for the district).\n *\n * Expected response:\n * { success: true, data: [{ id, ..., geom: \"LINESTRING(...)\" | \"MULTILINESTRING(...)\", ... }, ...] }\n *\n * @returns {Promise} OSM roads list\n */\nexport async function getOSMRoads() {\n return remotePost('get_osm_roads.php');\n}\n\n/**\n * Push a recorded GPS trail (with all its points) to the server.\n *\n * Implements the GeoTracker \"sync adapter\" contract: store-and-forward — the\n * whole trail is uploaded once recording stops (and retried when back online).\n * `district_id` and `api_token` are attached automatically by remotePost().\n *\n * ── SERVER SIDE (NOT YET CREATED) ───────────────────────────────────────\n * Proposed endpoint: `save_gps_trail.php`\n * Proposed PostgreSQL/PostGIS tables (SRID 4326):\n *\n * CREATE TABLE be_gps_trail (\n * id SERIAL PRIMARY KEY,\n * client_uuid TEXT UNIQUE, -- de-dupe re-syncs\n * district_id INTEGER,\n * name TEXT,\n * started_at TIMESTAMPTZ,\n * ended_at TIMESTAMPTZ,\n * point_count INTEGER,\n * distance_m DOUBLE PRECISION,\n * track GEOMETRY(LineStringZ, 4326), -- optional aggregate line\n * createdt TIMESTAMPTZ DEFAULT now()\n * );\n * CREATE TABLE be_gps_trail_point (\n * id SERIAL PRIMARY KEY,\n * trail_id INTEGER REFERENCES be_gps_trail(id) ON DELETE CASCADE,\n * seq INTEGER,\n * geom GEOMETRY(PointZ, 4326),\n * accuracy DOUBLE PRECISION,\n * altitude DOUBLE PRECISION,\n * heading DOUBLE PRECISION,\n * speed DOUBLE PRECISION,\n * satellites INTEGER, -- nullable (web has no sat count)\n * recorded_at TIMESTAMPTZ\n * );\n *\n * Request body (JSON, plus injected credentials):\n * { client_uuid, name, started_at, ended_at, point_count, distance_m,\n * points: [ { seq, longitude, latitude, altitude, accuracy,\n * altitude_accuracy, heading, speed, satellites, recorded_at } ] }\n *\n * Expected response: { success: true, id: }\n * Should be idempotent on client_uuid (INSERT ... ON CONFLICT DO UPDATE).\n * ─────────────────────────────────────────────────────────────────────────\n *\n * @param {Object} trail local trail row (client_uuid, name, started_at, …)\n * @param {Array} points local point rows (seq, longitude, latitude, …)\n * @returns {Promise<{ remoteId: (string|number|null) }>}\n */\nexport async function pushGpsTrail(trail, points) {\n const payload = {\n client_uuid: trail.client_uuid,\n name: trail.name ?? null,\n started_at: trail.started_at,\n ended_at: trail.ended_at,\n point_count: trail.point_count ?? points.length,\n distance_m: trail.distance_m ?? 0,\n points: (points || []).map((p) => ({\n seq: p.seq,\n longitude: p.longitude,\n latitude: p.latitude,\n altitude: p.altitude ?? null,\n accuracy: p.accuracy ?? null,\n altitude_accuracy: p.altitude_accuracy ?? null,\n heading: p.heading ?? null,\n speed: p.speed ?? null,\n satellites: p.satellites ?? null,\n recorded_at: p.recorded_at,\n })),\n };\n const res = await remotePost('save_gps_trail.php', payload);\n return { remoteId: res?.id ?? res?.remote_id ?? null };\n}\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport default {\n getSession,\n checkServerReachable,\n isServerReachable,\n remoteGet,\n remotePost,\n getDistrictBoundary,\n getLayers,\n getDistrictParcels,\n getCollectorZones,\n getBuildingFootprints,\n getContoursHillshade,\n getOSMRoads,\n pushGpsTrail,\n};\n","/**\n * geo-utils.js — pure, dependency-free geospatial helpers for the GeoTracker\n * module. No browser APIs, no framework imports — safe to reuse anywhere\n * (including Node, web workers, or other projects).\n *\n * @module geotracker/geo-utils\n */\n\nconst EARTH_RADIUS_M = 6371008.8; // mean Earth radius (metres)\nconst DEG2RAD = Math.PI / 180;\n\n/**\n * Great-circle distance between two lon/lat points using the haversine\n * formula.\n *\n * @param {number} lon1\n * @param {number} lat1\n * @param {number} lon2\n * @param {number} lat2\n * @returns {number} distance in metres\n */\nexport function haversineMeters(lon1, lat1, lon2, lat2) {\n const dLat = (lat2 - lat1) * DEG2RAD;\n const dLon = (lon2 - lon1) * DEG2RAD;\n const a =\n Math.sin(dLat / 2) ** 2 +\n Math.cos(lat1 * DEG2RAD) * Math.cos(lat2 * DEG2RAD) * Math.sin(dLon / 2) ** 2;\n return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(a)));\n}\n\n/**\n * Total length of a polyline expressed as an array of points.\n * @param {Array<{lon:number, lat:number}>} points\n * @returns {number} metres\n */\nexport function pathLengthMeters(points) {\n let total = 0;\n for (let i = 1; i < points.length; i++) {\n total += haversineMeters(points[i - 1].lon, points[i - 1].lat, points[i].lon, points[i].lat);\n }\n return total;\n}\n\n/**\n * Format a latitude or longitude for compact display.\n * @param {number} value\n * @param {number} [decimals=5] ~1.1 m precision at 5 dp\n * @returns {string}\n */\nexport function formatCoord(value, decimals = 5) {\n if (value == null || Number.isNaN(value)) return '—';\n return value.toFixed(decimals);\n}\n\n/**\n * Format a distance in metres into a friendly string (m below 1 km, km above).\n * @param {number} meters\n * @returns {string}\n */\nexport function formatDistance(meters) {\n if (meters == null || Number.isNaN(meters)) return '—';\n if (meters < 1000) return `${Math.round(meters)} m`;\n return `${(meters / 1000).toFixed(2)} km`;\n}\n\n/**\n * Format a horizontal accuracy (metres) into a friendly ± string.\n * @param {number|null} meters\n * @returns {string}\n */\nexport function formatAccuracy(meters) {\n if (meters == null || Number.isNaN(meters)) return '—';\n return `±${Math.round(meters)} m`;\n}\n\n/**\n * Classify a horizontal accuracy into a qualitative fix-quality bucket. Useful\n * for colour-coding the UI without needing satellite count.\n * @param {number|null} meters\n * @returns {'good'|'fair'|'poor'|'none'}\n */\nexport function accuracyQuality(meters) {\n if (meters == null || Number.isNaN(meters)) return 'none';\n if (meters <= 10) return 'good';\n if (meters <= 30) return 'fair';\n return 'poor';\n}\n","/**\n * GeoTracker.js — a framework-agnostic GPS live-position + trail-recording\n * engine. It has **no** dependency on OpenLayers, Bootstrap, SQLocal, or any\n * LUPMIS code, so it can be dropped into any web project. Persistence and\n * server sync are provided by the host through small adapter objects.\n *\n * ─────────────────────────────────────────────────────────────────────────\n * STORAGE ADAPTER (required for recording) — all methods may be async:\n * createTrail(meta) -> trailId // meta: {uuid,name,startedAt,...}\n * addPoint(trailId, point) -> void // point: normalized fix (see below)\n * finishTrail(trailId, summary)-> void // summary: {endedAt,pointCount,distanceM}\n * getUnsyncedTrails() -> Array // trails with synced=0 and completed\n * getTrailPoints(trailId) -> Array\n * markTrailSynced(trailId, remoteId) -> void\n *\n * SYNC ADAPTER (optional) — store-and-forward:\n * pushTrail(trail, points) -> { remoteId } | throws\n * isOnline?() -> boolean // optional connectivity probe\n *\n * NORMALIZED FIX shape emitted on 'position' and stored via addPoint:\n * { lon, lat, accuracy, altitude, altitudeAccuracy, heading, speed,\n * satellites:null, timestamp }\n * (satellites is always null on the web Geolocation API — kept for parity\n * with native builds that can populate it.)\n * ─────────────────────────────────────────────────────────────────────────\n *\n * @module geotracker/GeoTracker\n */\n\nimport { haversineMeters } from './geo-utils.js';\n\n/** @typedef {'idle'|'watching'|'recording'} GeoTrackerState */\n\nconst DEFAULTS = {\n /** Minimum metres between two recorded trail points. */\n minDistanceM: 5,\n /** Ignore fixes arriving faster than this (throttle, ms). */\n minIntervalMs: 1000,\n /** Record a point at least this often even when stationary (heartbeat, ms). */\n heartbeatMs: 20000,\n /** Drop fixes worse than this horizontal accuracy (metres). 0 = accept all. */\n maxAccuracyM: 50,\n /** navigator.geolocation options. */\n enableHighAccuracy: true,\n timeoutMs: 15000,\n maximumAgeMs: 0,\n};\n\nexport class GeoTracker {\n /**\n * @param {object} [options]\n * @param {object} [options.storage] storage adapter (see module docs)\n * @param {object} [options.sync] sync adapter (see module docs)\n * @param {Geolocation} [options.geolocation] inject navigator.geolocation (for tests)\n * @param {number} [options.minDistanceM]\n * @param {number} [options.minIntervalMs]\n * @param {number} [options.heartbeatMs]\n * @param {number} [options.maxAccuracyM]\n * @param {boolean} [options.enableHighAccuracy]\n */\n constructor(options = {}) {\n this.opts = { ...DEFAULTS, ...options };\n this.storage = options.storage || null;\n this.sync = options.sync || null;\n this._geo = options.geolocation ||\n (typeof navigator !== 'undefined' ? navigator.geolocation : null);\n\n /** @type {GeoTrackerState} */\n this._state = 'idle';\n this._watchId = null;\n this._live = false; // live readout requested\n this._recording = false; // recording in progress\n\n this._activeTrailId = null;\n this._activeTrailUuid = null;\n this._lastRecorded = null; // last point actually written {lon,lat,timestamp}\n this._lastRecordedAt = 0;\n this._distanceM = 0;\n this._pointCount = 0;\n this._lastFix = null; // most recent normalized fix (any quality)\n\n /** @type {Record>} */\n this._listeners = Object.create(null);\n }\n\n // ── Events ────────────────────────────────────────────────────────────\n\n /**\n * Subscribe to an event. Returns an unsubscribe function.\n * Events: 'position' | 'point' | 'statechange' | 'trailstart' |\n * 'trailstop' | 'error' | 'syncstatus'\n * @param {string} event\n * @param {Function} cb\n * @returns {() => void}\n */\n on(event, cb) {\n (this._listeners[event] || (this._listeners[event] = new Set())).add(cb);\n return () => this._listeners[event]?.delete(cb);\n }\n\n _emit(event, payload) {\n const set = this._listeners[event];\n if (!set) return;\n for (const cb of set) {\n try { cb(payload); } catch (err) { console.error(`[GeoTracker] listener for \"${event}\" threw`, err); }\n }\n }\n\n // ── Public state ──────────────────────────────────────────────────────\n\n /** @returns {GeoTrackerState} */\n get state() { return this._state; }\n get isRecording() { return this._recording; }\n get lastFix() { return this._lastFix; }\n get isSupported() { return !!this._geo; }\n\n _setState(s) {\n if (this._state === s) return;\n this._state = s;\n this._emit('statechange', s);\n }\n\n // ── Live readout (watch without recording) ──────────────────────────────\n\n /**\n * Begin a position watch purely for the live readout (no trail is recorded).\n * Safe to call repeatedly.\n */\n startLive() {\n if (!this._geo) { this._emit('error', new Error('Geolocation not supported')); return; }\n this._live = true;\n this._ensureWatch();\n }\n\n /** Stop the live readout. Has no effect while a recording is in progress. */\n stopLive() {\n this._live = false;\n if (!this._recording) this._teardownWatch();\n }\n\n /**\n * One-shot position request (e.g. for a \"Locate me\" button). Resolves with a\n * normalized fix. Does not start/stop the watch.\n * @returns {Promise}\n */\n getCurrentPosition() {\n return new Promise((resolve, reject) => {\n if (!this._geo) { reject(new Error('Geolocation not supported')); return; }\n this._geo.getCurrentPosition(\n (pos) => {\n const fix = GeoTracker.normalize(pos);\n this._lastFix = fix;\n this._emit('position', fix);\n resolve(fix);\n },\n (err) => { this._emit('error', err); reject(err); },\n {\n enableHighAccuracy: this.opts.enableHighAccuracy,\n timeout: this.opts.timeoutMs,\n maximumAge: this.opts.maximumAgeMs,\n }\n );\n });\n }\n\n // ── Recording ───────────────────────────────────────────────────────────\n\n /**\n * Start recording a new trail. Creates the trail in storage, then records\n * filtered points as the device moves.\n * @param {object} [meta] e.g. { name, districtId }\n * @returns {Promise<{trailId:*, uuid:string}>}\n */\n async startRecording(meta = {}) {\n if (!this._geo) throw new Error('Geolocation not supported');\n if (!this.storage) throw new Error('GeoTracker: no storage adapter configured');\n if (this._recording) return { trailId: this._activeTrailId, uuid: this._activeTrailUuid };\n\n const uuid = GeoTracker.uuid();\n const startedAt = new Date().toISOString();\n const trailMeta = { uuid, name: meta.name || null, startedAt, ...meta };\n const trailId = await this.storage.createTrail(trailMeta);\n\n this._activeTrailId = trailId;\n this._activeTrailUuid = uuid;\n this._lastRecorded = null;\n this._lastRecordedAt = 0;\n this._distanceM = 0;\n this._pointCount = 0;\n this._recording = true;\n\n this._ensureWatch();\n this._setState('recording');\n this._emit('trailstart', { trailId, uuid, startedAt });\n return { trailId, uuid };\n }\n\n /**\n * Stop the active recording, finalise the trail summary, and (if a sync\n * adapter is present) attempt to push it immediately.\n * @returns {Promise<{trailId:*, pointCount:number, distanceM:number, synced:boolean}>}\n */\n async stopRecording() {\n if (!this._recording) return null;\n const trailId = this._activeTrailId;\n const endedAt = new Date().toISOString();\n const summary = { endedAt, pointCount: this._pointCount, distanceM: this._distanceM };\n\n this._recording = false;\n if (!this._live) this._teardownWatch();\n this._setState(this._live ? 'watching' : 'idle');\n\n try {\n await this.storage.finishTrail(trailId, summary);\n } catch (err) {\n this._emit('error', err);\n }\n this._emit('trailstop', { trailId, ...summary });\n\n let synced = false;\n if (this.sync) {\n try { synced = await this._syncTrail(trailId); }\n catch (err) { this._emit('error', err); }\n }\n\n this._activeTrailId = null;\n this._activeTrailUuid = null;\n return { trailId, pointCount: summary.pointCount, distanceM: summary.distanceM, synced };\n }\n\n // ── Sync (store-and-forward) ────────────────────────────────────────────\n\n /**\n * Push all completed-but-unsynced trails to the server via the sync adapter.\n * Call on app start and whenever connectivity returns.\n * @returns {Promise<{pushed:number, failed:number}>}\n */\n async syncPending() {\n if (!this.sync || !this.storage) return { pushed: 0, failed: 0 };\n if (this.sync.isOnline && !this.sync.isOnline()) return { pushed: 0, failed: 0 };\n\n let pushed = 0, failed = 0;\n const trails = await this.storage.getUnsyncedTrails();\n for (const trail of trails) {\n try {\n const ok = await this._syncTrail(trail.id ?? trail.trailId, trail);\n ok ? pushed++ : failed++;\n } catch (err) {\n failed++;\n this._emit('error', err);\n }\n }\n this._emit('syncstatus', { pushed, failed });\n return { pushed, failed };\n }\n\n /** @private push a single trail by id. */\n async _syncTrail(trailId, trailRow) {\n const points = await this.storage.getTrailPoints(trailId);\n const trail = trailRow || { id: trailId };\n const result = await this.sync.pushTrail(trail, points);\n const remoteId = result && (result.remoteId ?? result.id ?? null);\n await this.storage.markTrailSynced(trailId, remoteId);\n return true;\n }\n\n // ── Internal watch handling ──────────────────────────────────────────────\n\n /** @private start the geolocation watch if not already running. */\n _ensureWatch() {\n if (this._watchId != null || !this._geo) {\n if (this._state === 'idle' && this._live) this._setState('watching');\n return;\n }\n this._watchId = this._geo.watchPosition(\n (pos) => this._onFix(pos),\n (err) => this._emit('error', err),\n {\n enableHighAccuracy: this.opts.enableHighAccuracy,\n timeout: this.opts.timeoutMs,\n maximumAge: this.opts.maximumAgeMs,\n }\n );\n if (!this._recording) this._setState('watching');\n }\n\n /** @private stop the geolocation watch. */\n _teardownWatch() {\n if (this._watchId != null && this._geo) {\n this._geo.clearWatch(this._watchId);\n }\n this._watchId = null;\n }\n\n /** @private handle a raw Geolocation fix. */\n async _onFix(pos) {\n const fix = GeoTracker.normalize(pos);\n this._lastFix = fix;\n this._emit('position', fix); // always emit for the live readout\n\n if (!this._recording) return;\n\n const { minIntervalMs, minDistanceM, heartbeatMs, maxAccuracyM } = this.opts;\n const now = fix.timestamp;\n\n // Throttle very frequent fixes.\n if (this._lastRecordedAt && (now - this._lastRecordedAt) < minIntervalMs) return;\n // Drop low-quality fixes (unless this is the very first point).\n if (maxAccuracyM > 0 && fix.accuracy != null && fix.accuracy > maxAccuracyM && this._lastRecorded) return;\n\n let keep = false;\n let stepM = 0;\n if (!this._lastRecorded) {\n keep = true; // always record the first point\n } else {\n stepM = haversineMeters(this._lastRecorded.lon, this._lastRecorded.lat, fix.lon, fix.lat);\n const elapsed = now - this._lastRecordedAt;\n if (stepM >= minDistanceM || elapsed >= heartbeatMs) keep = true;\n }\n if (!keep) return;\n\n if (this._lastRecorded) this._distanceM += stepM;\n this._pointCount += 1;\n this._lastRecorded = { lon: fix.lon, lat: fix.lat, timestamp: now };\n this._lastRecordedAt = now;\n\n try {\n await this.storage.addPoint(this._activeTrailId, { ...fix, seq: this._pointCount });\n this._emit('point', {\n trailId: this._activeTrailId,\n seq: this._pointCount,\n point: fix,\n distanceM: this._distanceM,\n pointCount: this._pointCount,\n });\n } catch (err) {\n this._emit('error', err);\n }\n }\n\n // ── Static helpers ────────────────────────────────────────────────────────\n\n /** Normalize a browser GeolocationPosition into the module's fix shape. */\n static normalize(pos) {\n const c = pos.coords || {};\n const num = (v) => (v != null && !Number.isNaN(v) ? v : null);\n return {\n lon: c.longitude,\n lat: c.latitude,\n accuracy: num(c.accuracy),\n altitude: num(c.altitude),\n altitudeAccuracy: num(c.altitudeAccuracy),\n heading: num(c.heading),\n speed: num(c.speed),\n satellites: null, // not exposed by the web Geolocation API\n timestamp: pos.timestamp || Date.now(),\n };\n }\n\n /** RFC4122-ish UUID, using crypto when available. */\n static uuid() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (ch) => {\n const r = (Math.random() * 16) | 0;\n const v = ch === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n }\n}\n\nexport default GeoTracker;\n","/**\n * geotracker-lupmis.js — LUPMIS2 integration layer for the reusable GeoTracker.\n *\n * This is the ONLY place that couples the generic src/geotracker/ engine to\n * LUPMIS specifics (SQLocal storage, the PHP sync endpoint, district id,\n * online checks). To reuse GeoTracker in another app, copy src/geotracker/ and\n * write a file like this one with that app's storage + sync adapters.\n *\n * @module geotracker-lupmis\n */\n\nimport { GeoTracker } from './geotracker/GeoTracker.js';\nimport {\n createGpsTrail,\n addGpsTrailPoint,\n finishGpsTrail,\n getUnsyncedGpsTrails,\n getGpsTrailPoints,\n markGpsTrailSynced,\n} from './database.js';\nimport { pushGpsTrail } from './remotedb.js';\nimport { isOnline } from './pwa.js';\nimport { getSession } from './remotedb.js';\n\n/**\n * Storage adapter — maps the GeoTracker contract onto the SQLocal helpers in\n * database.js. The district id is stamped onto each trail at creation time.\n */\nconst sqlocalStorage = {\n async createTrail(meta) {\n const districtId = meta.districtId\n ?? getSession()?.district_id\n ?? null;\n return createGpsTrail({ ...meta, districtId: districtId != null ? String(districtId) : null });\n },\n addPoint: (trailId, point) => addGpsTrailPoint(trailId, point),\n finishTrail: (trailId, summary) => finishGpsTrail(trailId, summary),\n getUnsyncedTrails: () => getUnsyncedGpsTrails(),\n getTrailPoints: (trailId) => getGpsTrailPoints(trailId),\n markTrailSynced: (trailId, remote) => markGpsTrailSynced(trailId, remote),\n};\n\n/**\n * Sync adapter — store-and-forward upload via the PHP endpoint. `isOnline()`\n * lets the tracker skip pushes while offline (it retries later).\n */\nconst remoteSync = {\n pushTrail: (trail, points) => pushGpsTrail(trail, points),\n isOnline: () => isOnline(),\n};\n\n/**\n * The configured, app-wide tracker instance. Tunables chosen for field\n * walking/driving: a point every ~5 m, throttled to ≥1 s, with a 20 s\n * heartbeat so stationary pauses still leave a breadcrumb, dropping fixes\n * worse than 50 m accuracy.\n */\nexport const geoTracker = new GeoTracker({\n storage: sqlocalStorage,\n sync: remoteSync,\n minDistanceM: 5,\n minIntervalMs: 1000,\n heartbeatMs: 20000,\n maxAccuracyM: 50,\n enableHighAccuracy: true,\n});\n\nexport default geoTracker;\n","/**\n * Main Application Entry Point\n *\n * Demonstrates integration of:\n * - Bootstrap 5.3 for UI components\n * - SQLocal (SQLite in browser via OPFS)\n * - BroadcastChannel for cross-tab sync\n * - OpenLayers map with ol-ext LayerSwitcher\n * - PWA features (Service Worker, install prompt, offline detection)\n */\n\n// Bootstrap CSS and JS\nimport 'bootstrap/dist/css/bootstrap.min.css';\nimport 'bootstrap-icons/font/bootstrap-icons.css';\nimport { Modal, Offcanvas } from 'bootstrap';\n\n// Database module (uses SQLocal directly, BroadcastChannel for tab sync)\nimport {\n sql,\n dbReady,\n initSchema,\n addLocation,\n getLocations,\n getLocationCount,\n getDatabaseStatus,\n downloadDatabase,\n onDatabaseChange,\n exportToGeoJSON,\n saveRemoteData,\n getRemoteData,\n saveCollectorZones,\n getLocalCollectorZones,\n saveParcels,\n getLocalParcels,\n updateParcel,\n insertNewParcel,\n saveBuildingFootprints,\n getLocalBuildingFootprints,\n saveOSMRoads,\n getLocalOSMRoads,\n isCachedLayerTable,\n clearTable,\n clearAllCachedLayers,\n getTableStats,\n getTableContent\n} from './src/database.js';\n\n// Map component with OpenLayers and ol-ext LayerSwitcher\nimport { MapView } from './src/components/MapView.js';\n\n// OpenLayers GeoJSON format (for updating layer sources directly)\nimport GeoJSON from 'ol/format/GeoJSON';\n\n// OpenLayers WKT format (for writing drawn polygon geometries to database)\nimport WKT from 'ol/format/WKT';\n\n// OpenLayers KML format (for KML file import)\nimport KML from 'ol/format/KML';\n\n// Shapefile parser (reads .zip containing .shp/.dbf/.shx/.prj)\n// Lazy-loaded — only fetched the first time the user imports a shapefile.\nlet _shpModule = null;\nasync function getShp() {\n if (!_shpModule) {\n const mod = await import('shpjs');\n _shpModule = mod.default || mod;\n }\n return _shpModule;\n}\n\n// Map measurement and drawing tools\nimport { MapTools } from './src/components/MapTools.js';\n\n// PWA module (registers Service Worker, handles install/offline)\nimport { initPWA, isOnline, onOfflineChange, getTileCacheStats, clearTileCaches, clearTileCacheForProvider, getStorageEstimate, onServiceWorkerControllerChange } from './src/pwa.js';\nimport {\n BASEMAP_TEMPLATES,\n GHANA_EXTENT_3857,\n countTiles,\n estimatedSizeBytes,\n OfflineTileDownloader,\n} from './src/offlineTiles.js';\n\n// Remote database API (PostgreSQL backend)\nimport { checkServerReachable, isServerReachable, getDistrictBoundary, getLayers, getCollectorZones, getDistrictParcels, getBuildingFootprints, getContoursHillshade, getOSMRoads, getSession } from './src/remotedb.js';\n\n// GPS live-position + trail recording (reusable engine + LUPMIS wiring)\nimport { geoTracker } from './src/geotracker-lupmis.js';\nimport { formatCoord, formatAccuracy, formatDistance, accuracyQuality } from './src/geotracker/geo-utils.js';\n\n// Map instance (global for access across functions)\nlet mapView = null;\nlet mapTools = null;\n\n// Current interaction mode: 'addLocation' | 'measureCircle' | 'measureLine' | 'measureArea'\nlet currentMode = 'addLocation';\n\n// ============================================================================\n// Application Initialization\n// ============================================================================\n\nasync function initApp() {\n console.log('[App] Initializing...');\n\n // 1. Initialize PWA features (Service Worker, install prompt, offline detection)\n await initPWA({\n installButton: '#install-btn',\n offlineIndicator: '#offline-indicator',\n autoRegisterSW: true\n });\n\n // 2. Initialize the map\n // Restore the user's preferred default base map from localStorage\n const savedBasemap = localStorage.getItem('default-basemap') || 'topo';\n\n mapView = new MapView('map', {\n center: [-1.5, 7.5], // Ghana\n zoom: 7,\n basemap: savedBasemap,\n });\n\n // Initialize map measurement tools\n mapTools = new MapTools(mapView.getMap());\n\n // Wire up GPS live-position + trail recording\n initGpsTracking();\n\n // Handle measurement results\n mapTools.onMeasureComplete((result) => {\n console.log('[MapTools] Measurement complete:', result);\n\n // Only show the Polygon Attributes popup for polygons drawn with the\n // Draw tool — NOT for area measurements (which have _layerType = 'measure_area').\n if (result.type === 'polygon' && result.coordinate) {\n const lt = result.feature?.get('_layerType');\n if (lt !== 'measure_area') {\n mapView?.showDrawnPolygonPopup(result.feature, result.coordinate);\n }\n }\n });\n\n // Category emojis are set up in MapView:\n // 'water': '💧', 'school': '🏫', 'health': '🏥',\n // 'market': '🏪', 'default': '📍', 'other': '📌'\n\n // Set up map click handler immediately after map creation\n mapView.onClick((lon, lat, feature, evt) => {\n console.log('[MapClick] Clicked at:', lon.toFixed(4), lat.toFixed(4));\n console.log('[MapClick] currentMode =', currentMode);\n\n // In draw or measurement modes, clicks drive the tool — don't\n // open popups or select features.\n if (currentMode === 'draw' || currentMode.startsWith('measure')) {\n return;\n }\n\n // Check if a parcel feature was clicked\n let parcelFeature = null;\n mapView.getMap().forEachFeatureAtPixel(evt.pixel, (f) => {\n if (f.get('_layerType') === 'parcel') {\n parcelFeature = f;\n return true; // stop at first parcel hit\n }\n });\n\n // Parcel click: open Edit Attributes form in ANY non-draw mode.\n // The feature is NOT selected — only the popup is shown.\n if (parcelFeature) {\n console.log('[MapClick] Clicked on parcel → Edit Attributes');\n mapView.showParcelEditPopup(parcelFeature, evt.coordinate);\n return;\n }\n\n // Non-parcel clicks (markers, empty space) only in addLocation mode\n if (currentMode !== 'addLocation') {\n return;\n }\n\n if (feature) {\n // Clicked on existing marker - select it and show details\n console.log('[MapClick] Clicked on marker:', feature.getId());\n mapView.selectMarker(feature);\n showLocationDetails(feature);\n } else {\n // Clicked on empty space - show add location popup at click position\n console.log('[MapClick] Empty space → Add Location popup');\n mapView.clearSelection();\n mapView.showAddLocationPopup(evt.coordinate);\n }\n });\n\n // Set up double-click handler for overlay feature info\n // Uses '_layerType' property to distinguish zone features from other layers\n mapView.onDblClick((lon, lat, feature, evt) => {\n if (!feature) return;\n\n const layerType = feature.get('_layerType');\n console.log('[App] Double-click on feature, _layerType:', layerType || 'none');\n\n if (layerType === 'measure_circle') {\n // Circle measurement: show intersection analysis with other layers\n mapView.showCircleIntersectionPopup(feature, evt.coordinate);\n } else if (layerType === 'measure_circle_radius') {\n // Clicked on the radius line — ignore\n return;\n } else if (layerType === 'measure_area') {\n // Area measurement polygon: show intersection analysis\n mapView.showAreaIntersectionPopup(feature, evt.coordinate);\n } else if (layerType === 'collector_zone') {\n mapView.showInfoPopup(feature, evt.coordinate, {\n title: 'Zone Info',\n color: '#7c3aed',\n });\n } else if (layerType === 'parcel') {\n mapView.showInfoPopup(feature, evt.coordinate, {\n title: 'Parcel Info',\n color: '#0ea5e9',\n });\n } else {\n mapView.showInfoPopup(feature, evt.coordinate, {\n title: 'Feature Info',\n color: '#e11d48',\n });\n }\n });\n\n // Set up handler for the map add location popup form\n mapView.onAddLocation(async (data) => {\n console.log('[App] Add location from map popup:', data);\n try {\n const result = await addLocation(data.name, data.lon, data.lat, {\n description: data.description || null,\n category: data.category || 'default'\n });\n console.log('[App] Location added:', data.name, 'id:', result.id);\n\n await loadLocations();\n\n // Zoom to the new location on the map\n mapView?.zoomTo(data.lon, data.lat, 14);\n\n // Select the new marker\n if (result.id) {\n mapView?.selectMarker(result.id);\n }\n\n showSuccess('Location added successfully');\n\n } catch (error) {\n console.error('[App] Failed to add location:', error);\n showError('Failed to add location: ' + error.message);\n }\n });\n\n // Set up parcel edit save handler\n mapView.onParcelEdit(async (feature, updatedProps) => {\n const parcelId = updatedProps.id || updatedProps.parcelid || updatedProps.parcel_id;\n console.log('[App] Parcel edit saved:', parcelId, updatedProps);\n\n if (!parcelId) {\n console.warn('[App] No parcel ID found in updated properties — skipping local save');\n return;\n }\n\n try {\n await updateParcel(parcelId, updatedProps);\n showSuccess('Parcel updated locally');\n } catch (error) {\n console.error('[App] Failed to save parcel update:', error);\n showError('Failed to save parcel: ' + error.message);\n }\n });\n\n // Set up drawn polygon attribute save handler\n const wktFormat = new WKT();\n mapView.onDrawnPolygonSave(async (feature, props) => {\n console.log('[App] Drawn polygon attributes saved:', props);\n\n try {\n // Convert the OL geometry (EPSG:3857) to WKT in EPSG:4326 for storage\n const wktString = wktFormat.writeGeometry(feature.getGeometry(), {\n dataProjection: 'EPSG:4326',\n featureProjection: 'EPSG:3857',\n });\n\n const result = await insertNewParcel(wktString, props);\n console.log('[App] New parcel inserted with id:', result.id);\n showSuccess('New parcel saved (pending verification)');\n } catch (error) {\n console.error('[App] Failed to save new parcel:', error);\n showError('Failed to save parcel: ' + error.message);\n }\n });\n\n // 3. Initialize database\n try {\n console.log('[App] Initializing database...');\n\n // Initialize schema (creates tables if they don't exist)\n // This also resolves dbReady when complete\n await initSchema();\n\n // Now dbReady should be resolved\n console.log('[App] Database ready');\n\n // Show database status\n const status = await getDatabaseStatus();\n console.log('[App] Database status:', status);\n\n // Quick server reachability check (5 s timeout) — if the API server\n // is down, all load functions will skip remote fetches and fall back\n // to local cached data immediately, keeping the app responsive.\n if (isOnline()) {\n const reachable = await checkServerReachable();\n if (!reachable) {\n console.warn('[App] API server unreachable — using local data only');\n showWarning('Server not responding — loading cached data.');\n }\n }\n\n // Load remote overlays (needs remote_data table from initSchema)\n // loadLayers must complete first so the layer groups exist\n // before loadDistrictBoundary adds into the Administration group.\n await loadLayers();\n\n // Initialise EditBar with its own \"Drawings\" layer group\n mapView?.initEditBar();\n\n loadDistrictBoundary();\n loadCollectorZones();\n loadParcels();\n loadBuildingFootprints();\n loadContoursHillshade();\n loadOSMRoads();\n loadExternalWMSLayers();\n\n } catch (error) {\n console.error('[App] Database initialization failed:', error);\n showError('Failed to initialize database. Please refresh the page.');\n return;\n }\n\n // 4. Initialize UI\n initUI();\n\n // 5. Load initial data and display on map\n await loadLocations();\n\n // 6. Listen for database changes (local + other tabs)\n onDatabaseChange((change) => {\n console.log('[App] Database change:', change);\n if (change.table === 'locations' && !change.local) {\n // Reload locations when another tab makes changes\n loadLocations();\n }\n if (change.table === 'parcels') {\n // Refresh the Local Data stats panel if it is visible\n const statsContainer = document.getElementById('local-data-stats');\n if (statsContainer && !statsContainer.classList.contains('d-none')) {\n refreshLocalDataStats();\n }\n }\n });\n\n // 7. Set up offline handling\n onOfflineChange((offline) => {\n if (offline) {\n console.log('[App] Working offline - data will sync when back online');\n } else {\n console.log('[App] Back online - syncing data...');\n syncData();\n }\n });\n\n // 8. Fieldwork mode (high-contrast + large touch targets)\n initFieldworkMode();\n\n // 9. Measurement system toggle (metric / imperial)\n initMeasurementSystem();\n\n // 10. Dark mode\n initDarkMode();\n\n // 11. Default base map selector\n initDefaultBasemap();\n\n // 12. Offline tile-cache stats card\n initOfflineTileCache();\n\n // 13. Offline-download dialog\n initOfflineDownloadDialog();\n\n // 14. Account card (signed-in user + sign-out)\n initAccountCard();\n\n console.log('[App] Initialized successfully');\n}\n\n// ============================================================================\n// UI Initialization\n// ============================================================================\n\nfunction initUI() {\n console.log('[initUI] Starting UI initialization...');\n\n // Message log (persistent stack in right panel)\n initMessageLog();\n\n // Export button\n const exportBtn = document.getElementById('export-btn');\n if (exportBtn) {\n exportBtn.addEventListener('click', handleExport);\n }\n\n // Local Data button — shows tables and record counts\n const localDataBtn = document.getElementById('local-data-btn');\n if (localDataBtn) {\n localDataBtn.addEventListener('click', () => refreshLocalDataStats());\n }\n\n // File import buttons (Shapefile, GeoJSON, KML)\n const importShpBtn = document.getElementById('import-shp-btn');\n const shpFileInput = document.getElementById('shp-file-input');\n if (importShpBtn && shpFileInput) {\n importShpBtn.addEventListener('click', () => shpFileInput.click());\n shpFileInput.addEventListener('change', handleShapefileImport);\n }\n\n const importGeoJSONBtn = document.getElementById('import-geojson-btn');\n const geojsonFileInput = document.getElementById('geojson-file-input');\n if (importGeoJSONBtn && geojsonFileInput) {\n importGeoJSONBtn.addEventListener('click', () => geojsonFileInput.click());\n geojsonFileInput.addEventListener('change', handleGeoJSONImport);\n }\n\n const importKMLBtn = document.getElementById('import-kml-btn');\n const kmlFileInput = document.getElementById('kml-file-input');\n if (importKMLBtn && kmlFileInput) {\n importKMLBtn.addEventListener('click', () => kmlFileInput.click());\n kmlFileInput.addEventListener('change', handleKMLImport);\n }\n\n // Drag-and-drop file import on the map\n initMapDropZone();\n\n // GeoJSON Export button\n const exportGeoJSONBtn = document.getElementById('exportGeoJSON-btn');\n if (exportGeoJSONBtn) {\n exportGeoJSONBtn.addEventListener('click', handleExportGeoJSON);\n }\n\n // Status button\n const statusBtn = document.getElementById('status-btn');\n if (statusBtn) {\n statusBtn.addEventListener('click', handleShowStatus);\n }\n\n // Fit to markers button\n const fitBtn = document.getElementById('fit-btn');\n if (fitBtn) {\n fitBtn.addEventListener('click', () => mapView?.fitToMarkers());\n }\n\n // ============================================\n // Mode Selector & Measurement Tools (Bottom Dock)\n // ============================================\n\n const addLocationBtn = document.getElementById('dock-btn-add-location');\n const measureCircleBtn = document.getElementById('dock-btn-measure-circle');\n const measureLineBtn = document.getElementById('dock-btn-measure-line');\n const measureAreaBtn = document.getElementById('dock-btn-measure-area');\n const drawBtn = document.getElementById('dock-btn-draw');\n const clearBtn = document.getElementById('dock-btn-clear');\n\n // Debug: Check if buttons are found\n console.log('[initUI] Buttons found:', {\n addLocation: !!addLocationBtn,\n measureCircle: !!measureCircleBtn,\n measureLine: !!measureLineBtn,\n measureArea: !!measureAreaBtn,\n draw: !!drawBtn,\n clear: !!clearBtn\n });\n\n // All mode buttons (mutually exclusive)\n const modeButtons = [addLocationBtn, measureCircleBtn, measureLineBtn, measureAreaBtn, drawBtn];\n\n // Helper to set active mode and update button states\n // Note: This updates the module-level currentMode variable\n const setMode = (mode, activeBtn) => {\n console.log('[setMode] Changing mode from', currentMode, 'to', mode);\n currentMode = mode;\n console.log('[setMode] currentMode is now:', currentMode);\n\n // Update button active states\n modeButtons.forEach(btn => {\n if (btn) btn.classList.toggle('active', btn === activeBtn);\n });\n\n // Deactivate any measurement tool when switching modes\n mapTools?.deactivate();\n\n // Leave edit mode when switching away from draw\n if (mode !== 'draw') {\n mapView?.setEditMode(false);\n }\n\n // Hide add location popup when leaving addLocation mode\n if (mode !== 'addLocation') {\n mapView?.hideAddLocationPopup();\n }\n\n // Activate the appropriate tool for the new mode\n switch (mode) {\n case 'measureCircle':\n mapTools?.startCircleMeasure();\n break;\n case 'measureLine':\n mapTools?.startLineMeasure();\n break;\n case 'measureArea':\n mapTools?.startAreaMeasure();\n break;\n case 'draw':\n mapView?.setEditMode(true);\n break;\n // addLocation mode doesn't need tool activation\n }\n };\n\n // Add Location mode button\n if (addLocationBtn) {\n addLocationBtn.addEventListener('click', () => {\n console.log('[Button] Add Location clicked');\n setMode('addLocation', addLocationBtn);\n });\n }\n\n // Circle measurement button\n if (measureCircleBtn) {\n measureCircleBtn.addEventListener('click', () => {\n console.log('[Button] Circle clicked, currentMode is:', currentMode);\n if (currentMode === 'measureCircle') {\n // Toggle off - return to addLocation mode\n setMode('addLocation', addLocationBtn);\n } else {\n setMode('measureCircle', measureCircleBtn);\n}\n });\n }\n\n // Line measurement button\n if (measureLineBtn) {\n measureLineBtn.addEventListener('click', () => {\n console.log('[Button] Line clicked, currentMode is:', currentMode);\n if (currentMode === 'measureLine') {\n setMode('addLocation', addLocationBtn);\n } else {\n setMode('measureLine', measureLineBtn);\n }\n });\n }\n\n // Area measurement button\n if (measureAreaBtn) {\n measureAreaBtn.addEventListener('click', () => {\n console.log('[Button] Area clicked, currentMode is:', currentMode);\n if (currentMode === 'measureArea') {\n setMode('addLocation', addLocationBtn);\n } else {\n setMode('measureArea', measureAreaBtn);\n }\n });\n }\n\n // Draw / Edit button\n if (drawBtn) {\n drawBtn.addEventListener('click', () => {\n console.log('[Button] Draw clicked, currentMode is:', currentMode);\n if (currentMode === 'draw') {\n setMode('addLocation', addLocationBtn);\n } else {\n setMode('draw', drawBtn);\n }\n });\n }\n\n // Clear button - clears measurements but stays in current mode\n if (clearBtn) {\n clearBtn.addEventListener('click', () => {\n mapTools?.clearMeasurements();\n // If in a measurement mode, restart the tool\n if (currentMode.startsWith('measure')) {\n mapTools?.deactivate();\n switch (currentMode) {\n case 'measureCircle':\n mapTools?.startCircleMeasure();\n break;\n case 'measureLine':\n mapTools?.startLineMeasure();\n break;\n case 'measureArea':\n mapTools?.startAreaMeasure();\n break;\n }\n }\n });\n }\n}\n\n// ============================================================================\n// Location Handlers\n// ============================================================================\n\nasync function handleAddLocation(event) {\n event.preventDefault();\n\n const form = event.target;\n const formData = new FormData(form);\n\n const name = formData.get('name');\n const longitude = parseFloat(formData.get('longitude'));\n const latitude = parseFloat(formData.get('latitude'));\n const description = formData.get('description') || null;\n const category = formData.get('category') || 'default';\n\n if (!name || isNaN(longitude) || isNaN(latitude)) {\n showError('Please fill in all required fields');\n return;\n }\n\n try {\n const result = await addLocation(name, longitude, latitude, { description, category });\n console.log('[App] Location added:', name, 'id:', result.id);\n\n form.reset();\n await loadLocations();\n\n // Zoom to the new location on the map\n mapView?.zoomTo(longitude, latitude, 14);\n\n // Select the new marker\n if (result.id) {\n mapView?.selectMarker(result.id);\n }\n\n showSuccess('Location added successfully');\n\n } catch (error) {\n console.error('[App] Failed to add location:', error);\n showError('Failed to add location: ' + error.message);\n }\n}\n\nasync function loadLocations() {\n try {\n console.log('[App] Loading locations...');\n const locations = await getLocations();\n console.log('[App] Locations loaded:', locations);\n\n // Update the list\n renderLocations(locations);\n\n // Update the map markers\n if (mapView) {\n mapView.clearMarkers();\n if (locations.length > 0) {\n mapView.addMarkers(locations);\n console.log('[App] Added', locations.length, 'markers to map');\n }\n }\n\n // Update count display\n const countEl = document.getElementById('location-count');\n if (countEl) {\n countEl.textContent = locations.length;\n }\n\n } catch (error) {\n console.error('[App] Failed to load locations:', error);\n }\n}\n\n/**\n * Show details for a selected location\n */\nfunction showLocationDetails(feature) {\n const name = feature.get('name');\n const description = feature.get('description');\n const category = feature.get('category');\n const lon = feature.get('lon') || feature.get('longitude');\n const lat = feature.get('lat') || feature.get('latitude');\n\n // You could show a popup or info panel here\n // For now, just log to console\n console.log('[App] Selected location:', { name, description, category, lon, lat });\n\n // Optionally zoom to the location\n // mapView.zoomTo(lon, lat, 14);\n}\n\nfunction renderLocations(locations) {\n const container = document.getElementById('locations-list');\n if (!container) return;\n\n // Also update mobile count\n const mobileCount = document.getElementById('location-count-mobile');\n if (mobileCount) {\n mobileCount.textContent = locations.length;\n }\n\n if (locations.length === 0) {\n container.innerHTML = `\n
            \n

            No locations yet.

            \n Click the map or fill the form above!\n
            \n `;\n return;\n }\n\n // Category emoji mapping\n const categoryEmojis = {\n 'water': '💧',\n 'school': '🏫',\n 'health': '🏥',\n 'market': '🏪',\n 'default': '📍',\n 'other': '📌'\n };\n\n container.innerHTML = locations.map(loc => {\n const emoji = categoryEmojis[loc.category] || '📍';\n return `\n \n
            \n
            \n
            ${emoji} ${escapeHtml(loc.name)}
            \n ${loc.latitude.toFixed(5)}, ${loc.longitude.toFixed(5)}\n
            \n ${loc.category}\n
            \n ${loc.description ? `${escapeHtml(loc.description)}` : ''}\n
            \n `;\n }).join('');\n\n // Add click handlers to zoom to location\n container.querySelectorAll('.location-item').forEach(item => {\n item.addEventListener('click', (e) => {\n e.preventDefault();\n const lon = parseFloat(item.dataset.lon);\n const lat = parseFloat(item.dataset.lat);\n const id = parseInt(item.dataset.id);\n\n // Zoom to location on map\n mapView?.zoomTo(lon, lat, 14);\n\n // Select the marker\n mapView?.selectMarker(id);\n });\n });\n}\n\n// ============================================================================\n// Local Data Stats\n// ============================================================================\n\n/**\n * Refresh the Local Data stats panel in the left offcanvas.\n * If the panel is already visible it updates in-place; otherwise it opens it.\n */\nasync function refreshLocalDataStats() {\n const statsContainer = document.getElementById('local-data-stats');\n const tbody = document.getElementById('local-data-tbody');\n const clearAllBtn = document.getElementById('clear-all-cached-btn');\n if (!statsContainer || !tbody) return;\n\n try {\n const stats = await getTableStats();\n\n tbody.innerHTML = stats.map((t) => {\n const isCached = isCachedLayerTable(t.name);\n const clearBtn = isCached\n ? ``\n : '';\n return `\n \n \n ${escapeHtml(t.name)}\n \n ${t.count}\n ${clearBtn}\n \n `;\n }).join('');\n statsContainer.classList.remove('d-none');\n\n // Table-name link → open content modal\n tbody.querySelectorAll('.table-name-link').forEach((link) => {\n link.addEventListener('click', (e) => {\n e.preventDefault();\n showTableContent(link.dataset.table);\n });\n });\n\n // Per-row clear → confirm, clear that table, refresh stats\n tbody.querySelectorAll('.table-clear-btn').forEach((btn) => {\n btn.addEventListener('click', async (e) => {\n e.preventDefault();\n const tableName = btn.dataset.table;\n if (!confirm(`Clear local cache for \"${tableName}\"?\\n\\nThe data will be re-downloaded from the server on the next app start.`)) return;\n try {\n const removed = await clearTable(tableName);\n showSuccess(`Cleared ${removed} row${removed === 1 ? '' : 's'} from \"${tableName}\". It will re-download on next start.`);\n await refreshLocalDataStats();\n } catch (err) {\n console.error('[App] Per-table clear failed:', err);\n showError(`Could not clear \"${tableName}\": ${err.message}`);\n }\n });\n });\n } catch (error) {\n console.error('[App] Failed to load table stats:', error);\n tbody.innerHTML = `Failed to load`;\n statsContainer.classList.remove('d-none');\n }\n\n // Bulk-clear button — wire up once\n if (clearAllBtn && !clearAllBtn._wired) {\n clearAllBtn._wired = true;\n clearAllBtn.addEventListener('click', handleClearAllCachedLayers);\n }\n}\n\n/**\n * Clear every cached layer table and offer to reload the app so the layers\n * re-download immediately. If the user dismisses the reload prompt, the\n * fresh fetch will happen on the next manual app start.\n */\nasync function handleClearAllCachedLayers() {\n if (!confirm(\n 'Delete all cached map layers from this device?\\n\\n' +\n 'The next time the app starts (or after a reload), every layer will be ' +\n 're-downloaded from the server. Your locally drawn data is not affected.'\n )) return;\n\n try {\n const results = await clearAllCachedLayers();\n const total = results.reduce((s, r) => s + r.count, 0);\n showSuccess(`Cleared ${total} row${total === 1 ? '' : 's'} across ${results.length} table${results.length === 1 ? '' : 's'}.`);\n await refreshLocalDataStats();\n\n if (confirm('Reload the app now to re-download the layers fresh from the server?')) {\n window.location.reload();\n }\n } catch (err) {\n console.error('[App] Clear-all failed:', err);\n showError('Failed to clear cached layers: ' + err.message);\n }\n}\n\n// ============================================================================\n// Table Content Viewer\n// ============================================================================\n\n/**\n * Load and display all rows of a table in a modal.\n * @param {string} tableName - The table to show\n */\nasync function showTableContent(tableName) {\n const modalTitle = document.getElementById('tableContentModalLabel');\n const modalBody = document.getElementById('table-content-body');\n const modalInfo = document.getElementById('table-content-info');\n\n // Set title and show spinner\n modalTitle.textContent = `Table: ${tableName}`;\n modalBody.innerHTML = `\n
            \n
            \n Loading...\n
            \n
            \n `;\n modalInfo.textContent = '';\n\n // Open the modal\n const modal = new Modal(document.getElementById('tableContentModal'));\n modal.show();\n\n try {\n const { columns, rows } = await getTableContent(tableName);\n\n if (rows.length === 0) {\n modalBody.innerHTML = `
            Table is empty
            `;\n modalInfo.textContent = '0 rows';\n return;\n }\n\n // Build a responsive table\n const headerCells = columns.map(c => `${escapeHtml(c)}`).join('');\n const bodyRows = rows.map(row => {\n const cells = columns.map(c => {\n let val = row[c];\n if (val === null || val === undefined) return 'NULL';\n val = String(val);\n // Truncate long values for display\n const display = val.length > 120 ? val.substring(0, 120) + '...' : val;\n return `${escapeHtml(display)}`;\n }).join('');\n return `${cells}`;\n }).join('');\n\n modalBody.innerHTML = `\n
            \n \n \n ${headerCells}\n \n ${bodyRows}\n
            \n
            \n `;\n\n modalInfo.textContent = `${rows.length}${rows.length >= 200 ? '+' : ''} row(s), ${columns.length} column(s)`;\n\n } catch (error) {\n console.error('[App] Failed to load table content:', error);\n modalBody.innerHTML = `
            Failed to load: ${escapeHtml(error.message)}
            `;\n }\n}\n\n// ============================================================================\n// Export Handler\n// ============================================================================\n\nasync function handleExport() {\n try {\n await downloadDatabase('lupmis-backup.sqlite3');\n showSuccess('Database exported successfully');\n } catch (error) {\n console.error('[App] Export failed:', error);\n showError('Export failed: ' + error.message);\n }\n}\n\n// Export as GeoJSON file\nasync function handleExportGeoJSON() {\n try {\n\t const geojson = await exportToGeoJSON();\n\n\t // Download as file\n\t const blob = new Blob([JSON.stringify(geojson, null, 2)], { type: 'application/json' });\n\t const url = URL.createObjectURL(blob);\n\t const a = document.createElement('a');\n\t a.href = url;\n\t a.download = 'locations.geojson';\n\t a.click();\n\t URL.revokeObjectURL(url);\n\n\t\tshowSuccess(`Exported ${geojson.features.length} location(s)`);\n\t}catch (error) {\n console.error('[App] GeoJSON Export failed:', error);\n showError('GeoJSON Export failed: ' + error.message);\n }\n}\n\n// ============================================================================\n// Status Handler\n// ============================================================================\n\nasync function handleShowStatus() {\n try {\n const status = await getDatabaseStatus();\n\n // Update modal content\n const statusContent = document.getElementById('status-content');\n if (statusContent) {\n statusContent.innerHTML = `\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
            Ready:${status.ready ? 'Yes' : 'No'}
            Online:${isOnline() ? 'Yes' : 'Offline'}
            Database:${status.databasePath || 'N/A'}
            Tables:${status.tables.map(t => `${t}`).join('')}
            Locations:${status.locationCount}
            \n `;\n }\n\n // Show the modal using Bootstrap\n const statusModal = new Modal(document.getElementById('statusModal'));\n statusModal.show();\n\n } catch (error) {\n console.error('[App] Failed to get status:', error);\n showError('Failed to get status');\n }\n}\n\n// ============================================================================\n// Remote Data Loading\n// ============================================================================\n\n/**\n * Parse a coordinate ring string into an array of [lon, lat] pairs.\n * @param {string} ringStr - e.g. \"lon lat,lon lat,...\"\n * @returns {Array} Array of [lon, lat]\n */\nfunction parseCoordRing(ringStr) {\n return ringStr.replace(/^\\(+/, '').replace(/\\)+$/, '')\n .split(',')\n .map(pair => {\n const [lon, lat] = pair.trim().split(/\\s+/).map(Number);\n return [lon, lat];\n });\n}\n\n/**\n * Parse a WKT POLYGON string into GeoJSON geometry.\n * Handles: POLYGON((lon lat,...),(hole,...))\n *\n * @param {string} wkt - WKT POLYGON string\n * @returns {Object} GeoJSON geometry { type: 'Polygon', coordinates: [...] }\n */\nfunction parseWKTPolygon(wkt) {\n const inner = wkt.trim()\n .replace(/^POLYGON\\s*\\(\\s*/i, '')\n .replace(/\\s*\\)$/, '');\n\n const ringStrings = inner.split('),(');\n const rings = ringStrings.map(parseCoordRing);\n\n return { type: 'Polygon', coordinates: rings };\n}\n\n/**\n * Parse a WKT MULTIPOLYGON string into GeoJSON geometry.\n * Handles: MULTIPOLYGON(((lon lat,...),(hole),...),((polygon2)))\n *\n * @param {string} wkt - WKT MULTIPOLYGON string\n * @returns {Object} GeoJSON geometry { type: 'MultiPolygon', coordinates: [...] }\n */\nfunction parseWKTMultiPolygon(wkt) {\n const inner = wkt.trim()\n .replace(/^MULTIPOLYGON\\s*\\(\\s*/i, '')\n .replace(/\\s*\\)$/, '');\n\n const polygonStrings = inner.split(')),((');\n\n const polygons = polygonStrings.map(polyStr => {\n const cleaned = polyStr.replace(/^\\(+/, '').replace(/\\)+$/, '');\n const ringStrings = cleaned.split('),(');\n return ringStrings.map(parseCoordRing);\n });\n\n return { type: 'MultiPolygon', coordinates: polygons };\n}\n\n/**\n * Parse any supported WKT geometry string (POLYGON or MULTIPOLYGON).\n * @param {string} wkt - WKT geometry string\n * @returns {Object|null} GeoJSON geometry or null if unsupported\n */\nfunction parseWKT(wkt) {\n if (!wkt) return null;\n const trimmed = wkt.trim().toUpperCase();\n if (trimmed.startsWith('MULTIPOLYGON')) return parseWKTMultiPolygon(wkt);\n if (trimmed.startsWith('POLYGON')) return parseWKTPolygon(wkt);\n console.warn('[App] Unsupported WKT type:', trimmed.substring(0, 30));\n return null;\n}\n\n/**\n * Convert the API response to a GeoJSON FeatureCollection.\n * The API returns: { success, data: { boundary: \"MULTIPOLYGON(...)\", districtid, district_name } }\n *\n * @param {Object} apiResponse - Raw API response\n * @returns {Object|null} GeoJSON FeatureCollection or null\n */\nfunction apiResponseToGeoJSON(apiResponse) {\n if (!apiResponse?.success || !apiResponse?.data?.boundary) {\n console.warn('[App] API response missing success or boundary data');\n return null;\n }\n\n const { boundary, districtid, district_name } = apiResponse.data;\n const geometry = parseWKT(boundary);\n\n return {\n type: 'FeatureCollection',\n features: [{\n type: 'Feature',\n properties: {\n districtid: districtid,\n district_name: district_name\n },\n geometry: geometry\n }]\n };\n}\n\n/**\n * Convert the collector zones API response to a GeoJSON FeatureCollection.\n * The API returns: { success, data: [{ id, zone_name, boundary, ... }, ...] }\n * Each zone feature gets a '_layerType' = 'collector_zone' property for identification.\n *\n * @param {Array} zones - Array of zone objects from the API\n * @returns {Object|null} GeoJSON FeatureCollection or null\n */\nfunction zonesToGeoJSON(zones) {\n if (!Array.isArray(zones) || zones.length === 0) return null;\n\n const features = [];\n for (const zone of zones) {\n // API returns WKT in 'polygon' field (not 'boundary')\n const wkt = zone.polygon || zone.boundary;\n const geometry = parseWKT(wkt);\n if (!geometry) continue;\n\n // Collect all properties except the raw WKT geometry\n const properties = { _layerType: 'collector_zone' };\n for (const [key, value] of Object.entries(zone)) {\n if (key === 'polygon' || key === 'boundary') continue;\n properties[key] = value;\n }\n\n features.push({ type: 'Feature', properties, geometry });\n }\n\n if (features.length === 0) return null;\n return { type: 'FeatureCollection', features };\n}\n\n/**\n * Load district boundary with local-first strategy:\n * 1. Always read from local SQLite cache (GeoJSON) first — instant, works offline\n * 2. If online, fetch from API, convert WKT → GeoJSON, cache and display\n */\nasync function loadDistrictBoundary() {\n const CACHE_KEY = 'district_boundary';\n const ADMIN_GROUP_ID = 1; // Administration layer group\n const boundaryStyle = {\n strokeColor: '#e11d48',\n strokeWidth: 2.5,\n fillColor: 'rgba(225,29,72,0.08)',\n typeDescription: 'Vector / Polygon',\n };\n\n // Target group: Administration (id 1), fall back to root overlay group\n const adminGroup = mapView?.getLayerGroup(ADMIN_GROUP_ID) || null;\n\n /**\n * Remove existing District Boundary layer from a group's layers.\n */\n function removeBoundaryLayer(group) {\n if (!group) return;\n const layers = group.getLayers();\n const toRemove = [];\n layers.forEach((layer) => {\n if (layer.get('title') === 'District Boundary') {\n toRemove.push(layer);\n }\n });\n toRemove.forEach((layer) => layers.remove(layer));\n }\n\n /**\n * Zoom the map to fit the boundary layer's extent.\n */\n function zoomToBoundary(layer) {\n if (!layer || !mapView) return;\n const extent = layer.getSource().getExtent();\n if (extent && extent[0] !== Infinity) {\n mapView.getMap().getView().fit(extent, {\n padding: [40, 40, 40, 40],\n duration: 600,\n });\n }\n }\n\n try {\n // Step 1: Load from local cache (already stored as GeoJSON)\n const cached = await getRemoteData(CACHE_KEY);\n if (cached) {\n console.log('[App] District boundary loaded from local cache');\n const layer = mapView?.addGeoJSONLayer(cached, 'District Boundary', boundaryStyle, adminGroup);\n zoomToBoundary(layer);\n }\n\n // Step 2: If online and server reachable, fetch fresh data from the API\n if (isOnline() && isServerReachable()) {\n console.log('[App] Fetching district boundary from API...');\n const apiResponse = await getDistrictBoundary();\n\n // Convert WKT response to GeoJSON\n const geojson = apiResponseToGeoJSON(apiResponse);\n if (!geojson) {\n console.warn('[App] Could not convert API response to GeoJSON');\n return;\n }\n\n console.log('[App] District boundary:', geojson.features[0]?.properties?.district_name,\n '→', geojson.features[0]?.geometry?.coordinates?.length, 'polygon(s)');\n\n // Save converted GeoJSON to local cache for offline use\n await saveRemoteData(CACHE_KEY, geojson);\n\n // Replace old cached layer if present\n if (cached) {\n removeBoundaryLayer(adminGroup || mapView?.getOverlayGroup());\n }\n\n const layer = mapView?.addGeoJSONLayer(geojson, 'District Boundary', boundaryStyle, adminGroup);\n zoomToBoundary(layer);\n console.log('[App] District boundary loaded from API');\n\n } else if (!cached) {\n console.log('[App] District boundary not available — offline and no local cache');\n }\n\n } catch (error) {\n console.error('[App] Failed to load district boundary:', error);\n }\n}\n\n/**\n * Load collector zones with local-first strategy:\n * 1. Read from local collector_zones table → convert to GeoJSON → display\n * 2. If online, fetch from API → save to local table → convert → display\n *\n * The \"Zones\" layer is added to the Administration LayerGroup (id 1),\n * initially not visible. It becomes visible when toggled in the LayerSwitcher.\n */\nasync function loadCollectorZones() {\n const ADMIN_GROUP_ID = 1;\n const zoneStyle = {\n strokeColor: '#7c3aed',\n strokeWidth: 1.5,\n fillColor: 'rgba(124,58,237,0.12)',\n typeDescription: 'Vector / Polygon',\n };\n\n const adminGroup = mapView?.getLayerGroup(ADMIN_GROUP_ID) || null;\n console.log('[App] loadCollectorZones — adminGroup:', adminGroup ? adminGroup.get('title') : 'null');\n\n // Create the Zones layer immediately (empty) so it always appears\n // in the LayerSwitcher. Features will be added once data is available.\n const emptyGeoJSON = { type: 'FeatureCollection', features: [] };\n const zonesLayer = mapView?.addGeoJSONLayer(emptyGeoJSON, 'Zones', zoneStyle, adminGroup);\n if (!zonesLayer) {\n console.warn('[App] Could not create Zones layer');\n return;\n }\n zonesLayer.setVisible(false);\n\n // Warn when the user enables the layer but it has no data\n zonesLayer.on('change:visible', () => {\n if (zonesLayer.getVisible() && zonesLayer.getSource().getFeatures().length === 0) {\n showError('No collector zones available locally. Connect to the internet to download zone data.');\n }\n });\n\n /**\n * Replace the layer's source features with features parsed from GeoJSON.\n */\n function setZoneFeatures(geojson) {\n const newFeatures = new GeoJSON().readFeatures(geojson, {\n featureProjection: 'EPSG:3857',\n });\n zonesLayer.getSource().clear();\n zonesLayer.getSource().addFeatures(newFeatures);\n }\n\n try {\n // Step 1: Load from local table\n const cached = await getLocalCollectorZones();\n if (cached) {\n const geojson = zonesToGeoJSON(cached);\n if (geojson) {\n console.log('[App] Collector zones loaded from local cache:', geojson.features.length, 'zones');\n setZoneFeatures(geojson);\n }\n }\n\n // Step 2: If online and server reachable, fetch fresh data from the API\n if (isOnline() && isServerReachable()) {\n console.log('[App] Fetching collector zones from API...');\n const apiResponse = await getCollectorZones();\n\n if (!apiResponse?.success || !Array.isArray(apiResponse?.data)) {\n console.warn('[App] getCollectorZones API response invalid:', apiResponse);\n return;\n }\n\n const zones = apiResponse.data;\n console.log('[App] Collector zones from API:', zones.length, 'entries');\n\n // Save to local table\n await saveCollectorZones(zones);\n\n // Convert to GeoJSON and update the existing layer\n const geojson = zonesToGeoJSON(zones);\n if (!geojson) {\n console.warn('[App] Could not convert zones to GeoJSON');\n return;\n }\n\n setZoneFeatures(geojson);\n console.log('[App] Collector zones updated from API:', geojson.features.length, 'zones');\n\n } else if (!cached) {\n console.log('[App] Collector zones not available — offline and no local cache');\n }\n\n } catch (error) {\n console.error('[App] Failed to load collector zones:', error);\n }\n}\n\n/**\n * Convert parcels data to a GeoJSON FeatureCollection.\n * Each parcel feature gets a '_layerType' = 'parcel' property for identification.\n *\n * @param {Array} parcels - Array of parcel objects from the API\n * @returns {Object|null} GeoJSON FeatureCollection or null\n */\nfunction parcelsToGeoJSON(parcels) {\n if (!Array.isArray(parcels) || parcels.length === 0) return null;\n\n // Deduplicate by id — the API may return the same parcel more than once\n const seen = new Set();\n const features = [];\n for (const parcel of parcels) {\n const id = parcel.id || parcel.parcelid || parcel.parcel_id;\n if (id != null) {\n if (seen.has(id)) continue;\n seen.add(id);\n }\n\n // Prefer the GeoJSON geometry (sp_boundary) if available; fall back to WKT\n let geometry = null;\n if (parcel.sp_boundary && parcel.sp_boundary.type && parcel.sp_boundary.coordinates) {\n geometry = { type: parcel.sp_boundary.type, coordinates: parcel.sp_boundary.coordinates };\n } else {\n const wkt = parcel.boundary || parcel.polygon || parcel.geom || parcel.wkt;\n geometry = parseWKT(wkt);\n }\n if (!geometry) continue;\n\n // Collect all properties except bulky geometry fields\n const skipKeys = new Set(['polygon', 'boundary', 'geom', 'wkt', 'textboundary', 'sp_boundary']);\n const properties = { _layerType: 'parcel' };\n for (const [key, value] of Object.entries(parcel)) {\n if (skipKeys.has(key)) continue;\n properties[key] = value;\n }\n\n features.push({ type: 'Feature', properties, geometry });\n }\n\n if (features.length === 0) return null;\n return { type: 'FeatureCollection', features };\n}\n\n/**\n * Load parcels with local-first strategy:\n * 1. Read from local parcels table → convert to GeoJSON → display\n * 2. If online, fetch from API → save to local table → convert → display\n *\n * The \"Parcels\" layer is added to the \"Land Use and Land Tenure\" LayerGroup (id 4),\n * initially not visible. It becomes visible when toggled in the LayerSwitcher.\n */\nasync function loadParcels() {\n const LAND_USE_GROUP_ID = 4;\n const parcelStyle = {\n strokeColor: '#0ea5e9',\n strokeWidth: 1.5,\n fillColor: 'rgba(14,165,233,0.12)',\n typeDescription: 'Vector / Polygon',\n };\n\n const landUseGroup = mapView?.getLayerGroup(LAND_USE_GROUP_ID) || null;\n console.log('[App] loadParcels — landUseGroup:', landUseGroup ? landUseGroup.get('title') : 'null');\n\n // Create the Parcels layer immediately (empty) so it always appears\n // in the LayerSwitcher. Features will be added once data is available.\n const emptyGeoJSON = { type: 'FeatureCollection', features: [] };\n const parcelsLayer = mapView?.addGeoJSONLayer(emptyGeoJSON, 'Parcels', parcelStyle, landUseGroup);\n if (!parcelsLayer) {\n console.warn('[App] Could not create Parcels layer');\n return;\n }\n parcelsLayer.setVisible(false);\n\n // Warn when the user enables the layer but it has no data\n parcelsLayer.on('change:visible', () => {\n if (parcelsLayer.getVisible() && parcelsLayer.getSource().getFeatures().length === 0) {\n showError('No parcels available locally. Connect to the internet to download parcel data.');\n }\n });\n\n /**\n * Replace the layer's source features with features parsed from GeoJSON.\n */\n function setParcelFeatures(geojson) {\n const newFeatures = new GeoJSON().readFeatures(geojson, {\n featureProjection: 'EPSG:3857',\n });\n parcelsLayer.getSource().clear();\n parcelsLayer.getSource().addFeatures(newFeatures);\n }\n\n try {\n // Step 1: Load from local table\n const cached = await getLocalParcels();\n if (cached) {\n const geojson = parcelsToGeoJSON(cached);\n if (geojson) {\n console.log('[App] Parcels loaded from local cache:', geojson.features.length, 'parcels');\n setParcelFeatures(geojson);\n }\n }\n\n // Step 2: If online and server reachable, fetch fresh data from the API\n if (isOnline() && isServerReachable()) {\n console.log('[App] Fetching parcels from API...');\n const apiResponse = await getDistrictParcels();\n\n if (!apiResponse?.success || !Array.isArray(apiResponse?.data)) {\n console.warn('[App] getDistrictParcels API response invalid:', apiResponse);\n return;\n }\n\n const parcels = apiResponse.data;\n console.log('[App] Parcels from API:', parcels.length, 'entries');\n\n // Log first parcel's keys for debugging field names\n if (parcels.length > 0) {\n console.log('[App] First parcel keys:', Object.keys(parcels[0]));\n }\n\n // Save to local table\n await saveParcels(parcels);\n\n // Convert to GeoJSON and update the existing layer\n const geojson = parcelsToGeoJSON(parcels);\n if (!geojson) {\n console.warn('[App] Could not convert parcels to GeoJSON');\n return;\n }\n\n setParcelFeatures(geojson);\n console.log('[App] Parcels updated from API:', geojson.features.length, 'parcels');\n\n } else if (!cached) {\n console.log('[App] Parcels not available — offline and no local cache');\n }\n\n } catch (error) {\n console.error('[App] Failed to load parcels:', error);\n }\n}\n\n/**\n * Convert an array of building footprint objects to a GeoJSON FeatureCollection.\n * Each footprint's WKT geometry field is parsed; all other fields become properties.\n *\n * @param {Array} footprints - Array of footprint objects\n * @returns {Object|null} GeoJSON FeatureCollection, or null if no valid features\n */\nfunction footprintsToGeoJSON(footprints) {\n if (!Array.isArray(footprints) || footprints.length === 0) return null;\n\n const geomKeys = ['polygon', 'boundary', 'geom', 'wkt', 'footprint'];\n\n const features = [];\n for (const fp of footprints) {\n const raw = fp.polygon || fp.boundary || fp.geom || fp.wkt || fp.footprint;\n // Geometry may be WKT string or GeoJSON object\n let geometry;\n if (typeof raw === 'object' && raw !== null && raw.type) {\n // Already a GeoJSON geometry object\n geometry = raw;\n } else {\n geometry = parseWKT(raw);\n }\n if (!geometry) continue;\n\n const properties = { _layerType: 'building_footprint' };\n for (const [key, value] of Object.entries(fp)) {\n if (geomKeys.includes(key)) continue;\n // Skip nested objects that aren't useful as flat properties\n if (typeof value === 'object' && value !== null) continue;\n properties[key] = value;\n }\n\n features.push({ type: 'Feature', properties, geometry });\n }\n\n if (features.length === 0) return null;\n return { type: 'FeatureCollection', features };\n}\n\n/**\n * Load building footprints with local-first strategy:\n * 1. Read from local building_footprints table → convert to GeoJSON → display\n * 2. If online, fetch from API → save to local table → convert → display\n *\n * The \"Building footprints\" layer is added to the \"Physical Infrastructures\" LayerGroup (id 5),\n * initially not visible. It becomes visible when toggled in the LayerSwitcher.\n */\nasync function loadBuildingFootprints() {\n const PHYS_INFRA_GROUP_ID = 5;\n const footprintStyle = {\n strokeColor: '#8b6f47',\n strokeWidth: 1,\n fillColor: 'rgba(139,111,71,0.18)',\n typeDescription: 'Vector / Polygon',\n };\n\n const physInfraGroup = mapView?.getLayerGroup(PHYS_INFRA_GROUP_ID) || null;\n console.log('[App] loadBuildingFootprints — physInfraGroup:', physInfraGroup ? physInfraGroup.get('title') : 'null');\n\n // Create the layer immediately (empty) so it always appears in the LayerSwitcher.\n const emptyGeoJSON = { type: 'FeatureCollection', features: [] };\n const footprintsLayer = mapView?.addGeoJSONLayer(emptyGeoJSON, 'Building footprints', footprintStyle, physInfraGroup);\n if (!footprintsLayer) {\n console.warn('[App] Could not create Building footprints layer');\n return;\n }\n footprintsLayer.setVisible(false);\n\n // Warn when the user enables the layer but it has no data\n footprintsLayer.on('change:visible', () => {\n if (footprintsLayer.getVisible() && footprintsLayer.getSource().getFeatures().length === 0) {\n showError('No building footprints available locally. Connect to the internet to download footprint data.');\n }\n });\n\n /**\n * Replace the layer's source features with features parsed from GeoJSON.\n */\n function setFootprintFeatures(geojson) {\n const newFeatures = new GeoJSON().readFeatures(geojson, {\n featureProjection: 'EPSG:3857',\n });\n footprintsLayer.getSource().clear();\n footprintsLayer.getSource().addFeatures(newFeatures);\n }\n\n try {\n // Step 1: Load from local table\n const cached = await getLocalBuildingFootprints();\n if (cached) {\n const geojson = footprintsToGeoJSON(cached);\n if (geojson) {\n console.log('[App] Building footprints loaded from local cache:', geojson.features.length, 'footprints');\n setFootprintFeatures(geojson);\n }\n }\n\n // Step 2: If online and server reachable, fetch fresh data from the API\n if (isOnline() && isServerReachable()) {\n console.log('[App] Fetching building footprints from API...');\n const apiResponse = await getBuildingFootprints();\n\n if (!apiResponse?.success || !Array.isArray(apiResponse?.data)) {\n console.warn('[App] getBuildingFootprints API response invalid:', apiResponse);\n return;\n }\n\n const footprints = apiResponse.data;\n console.log('[App] Building footprints from API:', footprints.length, 'entries');\n\n // Log first footprint's keys for debugging field names\n if (footprints.length > 0) {\n console.log('[App] First footprint keys:', Object.keys(footprints[0]));\n }\n\n // Save to local table\n await saveBuildingFootprints(footprints);\n\n // Convert to GeoJSON and update the existing layer\n const geojson = footprintsToGeoJSON(footprints);\n if (!geojson) {\n console.warn('[App] Could not convert building footprints to GeoJSON');\n return;\n }\n\n setFootprintFeatures(geojson);\n console.log('[App] Building footprints updated from API:', geojson.features.length, 'footprints');\n\n } else if (!cached) {\n console.log('[App] Building footprints not available — offline and no local cache');\n }\n\n } catch (error) {\n console.error('[App] Failed to load building footprints:', error);\n }\n}\n\n/**\n * Convert an array of DB rows (each with a WKT geom field) to GeoJSON.\n * Uses OpenLayers' WKT parser so LINESTRING, MULTILINESTRING, POLYGON, etc.\n * are all supported out of the box.\n *\n * @param {Array} rows — API rows, each having a WKT-valued geom/geometry/wkt field\n * @param {string} layerType — value to store in each feature's _layerType property\n * @returns {Object|null} GeoJSON FeatureCollection, or null if no valid rows\n */\nfunction wktRowsToGeoJSON(rows, layerType) {\n if (!Array.isArray(rows) || rows.length === 0) return null;\n\n const wktFormat = new WKT();\n const geojsonFormat = new GeoJSON();\n // Field-name fallbacks — different endpoints alias the geometry column\n // differently (e.g. get_osm_roads uses `road`, get_contours_hillshade uses\n // `geom`). The first non-null match wins.\n const geomKeys = ['geom', 'geometry', 'wkt', 'polygon', 'boundary', 'road', 'line'];\n\n const features = [];\n for (const row of rows) {\n const raw = row.geom || row.geometry || row.wkt || row.polygon || row.boundary || row.road || row.line;\n if (!raw) continue;\n\n let olGeom;\n try {\n if (typeof raw === 'object' && raw !== null && raw.type) {\n // Already a GeoJSON geometry — just pass through\n features.push({\n type: 'Feature',\n properties: flattenProps(row, geomKeys, layerType),\n geometry: raw,\n });\n continue;\n }\n olGeom = wktFormat.readGeometry(raw);\n } catch (err) {\n console.warn(`[App] Could not parse WKT for ${layerType}:`, err, raw?.toString().slice(0, 60));\n continue;\n }\n\n const geometry = JSON.parse(geojsonFormat.writeGeometry(olGeom));\n features.push({\n type: 'Feature',\n properties: flattenProps(row, geomKeys, layerType),\n geometry,\n });\n }\n\n if (features.length === 0) return null;\n return { type: 'FeatureCollection', features };\n}\n\n/**\n * Flatten a DB row into properties, skipping geometry fields and nested objects.\n */\nfunction flattenProps(row, skipKeys, layerType) {\n const props = { _layerType: layerType };\n for (const [key, value] of Object.entries(row)) {\n if (skipKeys.includes(key)) continue;\n if (typeof value === 'object' && value !== null) continue;\n props[key] = value;\n }\n return props;\n}\n\n/**\n * Load the \"Contours hillshade\" layer — elevation contours derived from\n * OpenTopography, stored in PostgreSQL as `contours_hillshade`.\n *\n * Added to the \"Biophysical Environment\" LayerGroup, initially not visible.\n * No local caching (the server is the source of truth).\n */\nasync function loadContoursHillshade() {\n const contoursStyle = {\n strokeColor: '#78716c', // warm grey — traditional contour colour\n strokeWidth: 0.8,\n typeDescription: 'Vector / Line',\n fillColor: 'rgba(0,0,0,0)',\n };\n\n const biophysGroup = mapView?.getLayerGroupByTitle('Biophysical Environment');\n console.log('[App] loadContoursHillshade — group:', biophysGroup ? biophysGroup.get('title') : 'null');\n\n // Create empty layer first so it always appears in the LayerSwitcher\n const emptyGeoJSON = { type: 'FeatureCollection', features: [] };\n const contoursLayer = mapView?.addGeoJSONLayer(emptyGeoJSON, 'Contours hillshade', contoursStyle, biophysGroup);\n if (!contoursLayer) {\n console.warn('[App] Could not create Contours hillshade layer');\n return;\n }\n contoursLayer.setVisible(false);\n\n // Warn when the user enables the layer but it has no data\n contoursLayer.on('change:visible', () => {\n if (contoursLayer.getVisible() && contoursLayer.getSource().getFeatures().length === 0) {\n showError('No Contours hillshade data available. Connect to the internet to download it.');\n }\n });\n\n // Fetch from API (only when online and server reachable — no local cache)\n if (!isOnline() || !isServerReachable()) {\n console.log('[App] Contours hillshade not available — offline or server unreachable');\n return;\n }\n\n try {\n console.log('[App] Fetching contours_hillshade from API...');\n const apiResponse = await getContoursHillshade();\n\n if (!apiResponse?.success || !Array.isArray(apiResponse?.data)) {\n console.warn('[App] getContoursHillshade API response invalid:', apiResponse);\n return;\n }\n\n const rows = apiResponse.data;\n console.log('[App] Contours hillshade from API:', rows.length, 'rows');\n if (rows.length > 0) {\n console.log('[App] First row keys:', Object.keys(rows[0]));\n }\n\n const geojson = wktRowsToGeoJSON(rows, 'contours_hillshade');\n if (!geojson) {\n console.warn('[App] Could not convert contours to GeoJSON');\n return;\n }\n\n const features = new GeoJSON().readFeatures(geojson, { featureProjection: 'EPSG:3857' });\n contoursLayer.getSource().clear();\n contoursLayer.getSource().addFeatures(features);\n console.log('[App] Contours hillshade loaded:', features.length, 'features');\n\n } catch (error) {\n console.error('[App] Failed to load contours_hillshade:', error);\n }\n}\n\n/**\n * Load the \"OSM_roads\" layer — OpenStreetMap road network for the district.\n *\n * Added to the \"Physical Infrastructures\" LayerGroup (id 5), initially not\n * visible — becomes visible when the user toggles it in the LayerSwitcher.\n *\n * Local-first caching:\n * 1. Read from the local `osm_roads` table → render immediately if available\n * 2. If online, fetch from the API → overwrite the local table → re-render\n */\nasync function loadOSMRoads() {\n const PHYS_INFRA_GROUP_ID = 5;\n // Cartographic road casing: a black outer stroke makes the light-coloured\n // inner stroke (the \"road body\") readable on every base map.\n const roadsStyle = {\n strokeColor: '#F0F1F0', // inner — road body\n strokeWidth: 1.5,\n lineCasingColor: '#000000', // outer — black casing\n lineCasingWidth: 3.5,\n fillColor: 'rgba(0,0,0,0)',\n typeDescription: 'Vector / Line',\n };\n\n const physInfraGroup = mapView?.getLayerGroup(PHYS_INFRA_GROUP_ID) || null;\n console.log('[App] loadOSMRoads — group:', physInfraGroup ? physInfraGroup.get('title') : 'null');\n\n // Create the layer immediately (empty) so it appears in the LayerSwitcher\n // even when offline.\n const emptyGeoJSON = { type: 'FeatureCollection', features: [] };\n const roadsLayer = mapView?.addGeoJSONLayer(emptyGeoJSON, 'OSM_roads', roadsStyle, physInfraGroup);\n if (!roadsLayer) {\n console.warn('[App] Could not create OSM_roads layer');\n return;\n }\n roadsLayer.setVisible(false);\n\n // Warn only when the layer is enabled AND truly empty AND no source is reachable\n roadsLayer.on('change:visible', () => {\n if (roadsLayer.getVisible() && roadsLayer.getSource().getFeatures().length === 0) {\n showError('No OSM roads available locally. Connect to the internet to download them.');\n }\n });\n\n /** Replace the layer's features with those parsed from the API/cache rows. */\n function setRoadFeatures(rows) {\n const geojson = wktRowsToGeoJSON(rows, 'osm_road');\n if (!geojson) {\n console.warn('[App] Could not convert OSM roads to GeoJSON');\n return 0;\n }\n const features = new GeoJSON().readFeatures(geojson, { featureProjection: 'EPSG:3857' });\n roadsLayer.getSource().clear();\n roadsLayer.getSource().addFeatures(features);\n return features.length;\n }\n\n try {\n // Step 1 — local cache (works offline, instant)\n const cached = await getLocalOSMRoads();\n if (cached) {\n const n = setRoadFeatures(cached);\n console.log('[App] OSM_roads loaded from local cache:', n, 'features');\n }\n\n // Step 2 — fetch fresh from API when online\n if (isOnline() && isServerReachable()) {\n console.log('[App] Fetching OSM_roads from API...');\n const apiResponse = await getOSMRoads();\n\n if (!apiResponse?.success || !Array.isArray(apiResponse?.data)) {\n console.warn('[App] getOSMRoads API response invalid:', apiResponse);\n return;\n }\n\n const rows = apiResponse.data;\n console.log('[App] OSM_roads from API:', rows.length, 'rows');\n if (rows.length > 0) {\n console.log('[App] First row keys:', Object.keys(rows[0]));\n }\n\n // Persist to local table so it's available next time offline\n await saveOSMRoads(rows);\n\n const n = setRoadFeatures(rows);\n console.log('[App] OSM_roads updated from API:', n, 'features');\n\n } else if (!cached) {\n console.log('[App] OSM_roads not available — offline and no local cache');\n }\n\n } catch (error) {\n console.error('[App] Failed to load OSM_roads:', error);\n }\n}\n\n/**\n * Add external WMS/XYZ layers to the map.\n * Called after loadLayers() so the target layer groups already exist.\n */\nfunction loadExternalWMSLayers() {\n // DEAfrica Coastlines v0.4.0 — annual shorelines & rates of change\n // Source: Digital Earth Africa GeoServer\n // Latest available version as of 2026: v0.4.0\n mapView?.addWMSLayer(\n 'Biophysical Environment',\n 'DEAfrica Coastlines v0.4',\n 'https://geoserver.digitalearth.africa/geoserver/wms',\n 'coastlines:DEAfrica_Coastlines',\n { serverType: 'geoserver', visible: false, onlineOnly: true }\n );\n\n // Note: OpenTopoMap is available as the \"Topographic\" base map —\n // no separate overlay in \"Biophysical Environment\" needed.\n\n // Digital Earth Africa — SRTM-derived Slope (30m)\n // Shows terrain steepness as a background overlay — hills and valleys stand\n // out naturally, reading like a traditional shaded-relief topographic map.\n // Service: datacube-ows (not GeoServer).\n // Layer 'srtm_deriv' styles: 'style_slope', 'style_mrvbf' (valley bottoms),\n // 'style_mrrtf' (ridge tops).\n mapView?.addWMSLayer(\n 'Biophysical Environment',\n 'DEAfrica Slope (SRTM 30m)',\n 'https://ows.digitalearth.africa/wms',\n 'srtm_deriv',\n {\n serverType: null,\n style: 'style_slope',\n visible: false,\n opacity: 0.5,\n zIndex: -50,\n onlineOnly: true,\n attributions:\n '© Digital Earth Africa — ' +\n 'SRTM-derived Slope',\n legendUrl: 'https://ows.digitalearth.africa/legend/srtm_deriv/style_slope/legend.png',\n }\n );\n}\n\n/**\n * Load layer categories from the API and create empty VectorLayers on the map.\n * Uses local-first caching — reads from SQLite first, then refreshes from API when online.\n *\n * API response: { success: true, data: [{ id, name, description, createdt, editdt }, ...] }\n */\nasync function loadLayers() {\n const CACHE_KEY = 'layer_categories';\n\n /**\n * Create layer groups on the map from the layer category list.\n * @param {Array} layers - Array of { id, name, description, ... }\n */\n function createLayerGroupsOnMap(layers) {\n // Sort by id descending so that id 1 is pushed last → ends up\n // at the top of the layer stack and at the top of the LayerSwitcher.\n const sorted = [...layers].sort((a, b) => b.id - a.id);\n for (const layer of sorted) {\n mapView?.addLayerGroup(layer.id, layer.name, layer.description || '');\n }\n console.log('[App] Created', layers.length, 'layer groups on map');\n }\n\n try {\n // Step 1: Load from local cache\n const cached = await getRemoteData(CACHE_KEY);\n if (cached) {\n console.log('[App] Layer categories loaded from local cache:', cached.length, 'entries');\n createLayerGroupsOnMap(cached);\n }\n\n // Step 2: If online and server reachable, fetch fresh data from the API\n if (isOnline() && isServerReachable()) {\n console.log('[App] Fetching layer categories from API...');\n const apiResponse = await getLayers();\n\n if (!apiResponse?.success || !Array.isArray(apiResponse?.data)) {\n console.warn('[App] getLayers API response invalid:', apiResponse);\n return;\n }\n\n const layers = apiResponse.data;\n console.log('[App] Layer categories from API:', layers.length, 'entries');\n\n // Save to local cache\n await saveRemoteData(CACHE_KEY, layers);\n\n // Replace layers on map if we already created from cache\n if (cached) {\n // Remove previously created empty layers (keep District Boundary)\n const overlayLayers = mapView?.getOverlayGroup()?.getLayers();\n if (overlayLayers) {\n const toRemove = [];\n overlayLayers.forEach((layer) => {\n if (layer.get('layerId') !== undefined) {\n toRemove.push(layer);\n }\n });\n toRemove.forEach((layer) => overlayLayers.remove(layer));\n }\n }\n\n createLayerGroupsOnMap(layers);\n console.log('[App] Layer categories refreshed from API');\n\n } else if (!cached) {\n console.log('[App] Layer categories not available — offline and no local cache');\n }\n\n } catch (error) {\n console.error('[App] Failed to load layer categories:', error);\n }\n}\n\n// ============================================================================\n// Sync (placeholder - implement based on your backend)\n// ============================================================================\n\nasync function syncData() {\n if (!isOnline()) {\n console.log('[App] Cannot sync - offline');\n return;\n }\n\n // TODO: Implement sync with your backend\n // Example:\n // const unsynced = await getUnsyncedLocations();\n // for (const location of unsynced) {\n // await fetch('/api/locations', {\n // method: 'POST',\n // body: JSON.stringify(location)\n // });\n // await markLocationsSynced([location.id]);\n // }\n\n console.log('[App] Sync placeholder - implement based on your backend');\n}\n\n// ============================================================================\n// File Import (Shapefile, GeoJSON, KML)\n// ============================================================================\n\n/** All layers added by file imports — shared across formats. */\nconst importedFileLayers = [];\n\n/** Default style for imported layers. */\nconst IMPORT_STYLE = {\n strokeColor: '#e11d48',\n strokeWidth: 2,\n fillColor: 'rgba(225,29,72,0.12)',\n};\n\n/**\n * Show an error message inside the left panel's file-import alert area.\n */\nfunction showFileImportError(message) {\n logMessage('error', message);\n const el = document.getElementById('file-import-alert');\n if (el) {\n el.querySelector('.message-text').textContent = message;\n el.classList.remove('d-none');\n setTimeout(() => el.classList.add('d-none'), 8000);\n }\n}\n\n/**\n * Add a GeoJSON FeatureCollection (or array of them) to the map, zoom to\n * the data, and refresh the imported-layers info card.\n *\n * @param {Object|Object[]} geojsonInput - Single FeatureCollection or array\n * @param {string} fallbackName - Layer name when the FC has no fileName\n * @param {string} tag - Log prefix, e.g. 'ShpImport'\n */\nfunction addImportedGeoJSON(geojsonInput, fallbackName, tag) {\n const collections = Array.isArray(geojsonInput) ? geojsonInput : [geojsonInput];\n\n let totalFeatures = 0;\n for (const fc of collections) {\n if (!fc || fc.type !== 'FeatureCollection' || !fc.features?.length) continue;\n\n const layerName = fc.fileName\n ? fc.fileName.replace(/\\.[^/.]+$/, '')\n : fallbackName;\n\n const layer = mapView?.addGeoJSONLayer(fc, layerName, IMPORT_STYLE);\n if (layer) {\n // Imported file layers are not part of the built-in data model;\n // the user can remove them via the LayerSwitcher × button.\n layer.set('removable', true);\n layer.set('typeTag', 'GEO');\n importedFileLayers.push(layer);\n totalFeatures += fc.features.length;\n }\n }\n\n if (totalFeatures === 0) {\n showFileImportError('No features found in the file.');\n return;\n }\n\n console.log(`[${tag}] Added ${totalFeatures} feature(s) from ${collections.length} layer(s)`);\n\n // Zoom to the last imported layer\n const lastLayer = importedFileLayers[importedFileLayers.length - 1];\n if (lastLayer) {\n const extent = lastLayer.getSource().getExtent();\n mapView?.getMap().getView().fit(extent, { padding: [50, 50, 50, 50], maxZoom: 18 });\n }\n\n refreshImportedLayersCard();\n}\n\n/**\n * Rebuild the imported-layers info card in the left panel.\n */\nfunction refreshImportedLayersCard() {\n const infoEl = document.getElementById('imported-layers-info');\n if (!infoEl) return;\n\n if (importedFileLayers.length === 0) {\n infoEl.innerHTML = '';\n infoEl.classList.add('d-none');\n return;\n }\n\n infoEl.innerHTML = `\n
            \n
            \n
            Imported Layers
            \n \n
            \n
              \n
              `;\n\n const listEl = infoEl.querySelector('#imported-layers-list');\n importedFileLayers.forEach((l, idx) => {\n const li = document.createElement('li');\n li.className = 'list-group-item d-flex justify-content-between align-items-center py-2';\n li.innerHTML = `${escapeHtml(l.get('title'))}\n \n ${l.getSource().getFeatures().length}\n \n `;\n listEl.appendChild(li);\n });\n infoEl.classList.remove('d-none');\n\n // Per-layer remove buttons\n infoEl.querySelectorAll('[data-remove-idx]').forEach(btn => {\n btn.addEventListener('click', () => {\n removeImportedLayer(Number(btn.dataset.removeIdx));\n });\n });\n\n // Remove-all button\n infoEl.querySelector('#remove-imported-layers')?.addEventListener('click', () => {\n removeImportedLayers();\n });\n}\n\n/**\n * Remove a single imported layer by its index in importedFileLayers.\n */\nfunction removeImportedLayer(idx) {\n if (idx < 0 || idx >= importedFileLayers.length) return;\n const layer = importedFileLayers[idx];\n const overlayGroup = mapView?.getOverlayGroup();\n if (overlayGroup) {\n overlayGroup.getLayers().remove(layer);\n }\n importedFileLayers.splice(idx, 1);\n refreshImportedLayersCard();\n console.log('[FileImport] Removed layer:', layer.get('title'));\n}\n\n/**\n * Remove all imported layers from the map and clear the info card.\n */\nfunction removeImportedLayers() {\n const overlayGroup = mapView?.getOverlayGroup();\n if (overlayGroup) {\n for (const layer of importedFileLayers) {\n overlayGroup.getLayers().remove(layer);\n }\n }\n importedFileLayers.length = 0;\n refreshImportedLayersCard();\n console.log('[FileImport] All imported layers removed');\n}\n\n// ---------------------------------------------------------------------------\n// Shapefile (.shp / .zip)\n// ---------------------------------------------------------------------------\n\n/**\n * Build a lookup of selected files keyed by lowercase extension.\n */\nfunction indexFilesByExtension(files) {\n const map = {};\n for (const f of files) {\n const ext = f.name.split('.').pop().toLowerCase();\n map[ext] = f;\n }\n return map;\n}\n\nasync function handleShapefileImport(evt) {\n const files = evt.target.files;\n if (!files || files.length === 0) return;\n\n const MAX_FILE_SIZE = 200 * 1024 * 1024;\n const totalSize = Array.from(files).reduce((s, f) => s + f.size, 0);\n if (totalSize > MAX_FILE_SIZE) {\n const sizeMB = (totalSize / (1024 * 1024)).toFixed(0);\n showFileImportError(\n `Files too large (${sizeMB} MB total). Maximum supported size is 200 MB.`\n );\n evt.target.value = '';\n return;\n }\n\n try {\n let geojson;\n let displayName;\n const byExt = indexFilesByExtension(files);\n\n if (byExt.zip) {\n const file = byExt.zip;\n displayName = file.name.replace(/\\.zip$/i, '');\n console.log('[ShpImport] Parsing zip', file.name, '(' + (file.size / 1024).toFixed(1) + ' KB)');\n const shp = await getShp();\n geojson = await shp(await file.arrayBuffer());\n\n } else if (byExt.shp) {\n displayName = byExt.shp.name.replace(/\\.shp$/i, '');\n\n const required = ['dbf', 'shx', 'prj'];\n const missing = required.filter(ext => !byExt[ext]);\n if (missing.length > 0) {\n showFileImportError('Missing required file(s): ' + missing.map(e => '.' + e).join(', ')\n + '. Please select .shp, .dbf, .shx and .prj together.');\n evt.target.value = '';\n return;\n }\n\n const shpObj = {};\n shpObj.shp = await byExt.shp.arrayBuffer();\n shpObj.dbf = await byExt.dbf.arrayBuffer();\n shpObj.prj = await new Response(byExt.prj).text();\n if (byExt.cpg) shpObj.cpg = await new Response(byExt.cpg).text();\n\n console.log('[ShpImport] Parsing loose files:',\n Object.keys(byExt).map(e => '.' + e).join(', '),\n '(' + (byExt.shp.size / 1024).toFixed(1) + ' KB .shp)');\n\n const shp = await getShp();\n geojson = await shp(shpObj);\n\n } else {\n showFileImportError('Please select a .zip or at least a .shp file.');\n evt.target.value = '';\n return;\n }\n\n addImportedGeoJSON(geojson, displayName, 'ShpImport');\n } catch (error) {\n console.error('[ShpImport] Failed:', error);\n showFileImportError('Failed to parse shapefile: ' + error.message);\n }\n\n evt.target.value = '';\n}\n\n// ---------------------------------------------------------------------------\n// GeoJSON (.geojson / .json)\n// ---------------------------------------------------------------------------\n\nasync function handleGeoJSONImport(evt) {\n const file = evt.target.files?.[0];\n if (!file) return;\n\n // Guard: reject files larger than 200 MB — JSON.parse cannot reliably\n // handle them in a single pass and the browser will freeze or crash.\n const MAX_FILE_SIZE = 200 * 1024 * 1024; // 200 MB\n if (file.size > MAX_FILE_SIZE) {\n const sizeMB = (file.size / (1024 * 1024)).toFixed(0);\n showFileImportError(\n `File too large (${sizeMB} MB). Maximum supported size is 200 MB. `\n + 'Consider splitting the file into smaller tiles with ogr2ogr or QGIS.'\n );\n evt.target.value = '';\n return;\n }\n\n try {\n const text = await file.text();\n console.log('[GeoJSONImport] Parsing', file.name, '(' + (file.size / 1024).toFixed(1) + ' KB)');\n\n const parsed = JSON.parse(text);\n\n // Normalise to a FeatureCollection\n let fc;\n if (parsed.type === 'FeatureCollection') {\n fc = parsed;\n } else if (parsed.type === 'Feature') {\n fc = { type: 'FeatureCollection', features: [parsed] };\n } else if (parsed.type && parsed.coordinates) {\n // Bare geometry object\n fc = { type: 'FeatureCollection', features: [{ type: 'Feature', geometry: parsed, properties: {} }] };\n } else {\n showFileImportError('The file does not contain valid GeoJSON.');\n evt.target.value = '';\n return;\n }\n\n const displayName = file.name.replace(/\\.(geo)?json$/i, '');\n addImportedGeoJSON(fc, displayName, 'GeoJSONImport');\n } catch (error) {\n console.error('[GeoJSONImport] Failed:', error);\n const sizeMB = (file.size / (1024 * 1024)).toFixed(1);\n showFileImportError(\n `Failed to import \"${file.name}\" (${sizeMB} MB): ${error.message}`\n );\n }\n\n evt.target.value = '';\n}\n\n// ---------------------------------------------------------------------------\n// KML (.kml)\n// ---------------------------------------------------------------------------\n\nasync function handleKMLImport(evt) {\n const file = evt.target.files?.[0];\n if (!file) return;\n\n const MAX_FILE_SIZE = 200 * 1024 * 1024;\n if (file.size > MAX_FILE_SIZE) {\n const sizeMB = (file.size / (1024 * 1024)).toFixed(0);\n showFileImportError(\n `File too large (${sizeMB} MB). Maximum supported size is 200 MB.`\n );\n evt.target.value = '';\n return;\n }\n\n try {\n const text = await file.text();\n console.log('[KMLImport] Parsing', file.name, '(' + (file.size / 1024).toFixed(1) + ' KB)');\n\n const kmlFormat = new KML({ extractStyles: false });\n const features = kmlFormat.readFeatures(text, {\n featureProjection: 'EPSG:3857',\n });\n\n if (!features || features.length === 0) {\n showFileImportError('No features found in the KML file.');\n evt.target.value = '';\n return;\n }\n\n // Convert OL features back to GeoJSON so we can use the shared pipeline\n const geojsonFormat = new GeoJSON();\n const fc = JSON.parse(geojsonFormat.writeFeatures(features, {\n featureProjection: 'EPSG:3857',\n dataProjection: 'EPSG:4326',\n }));\n\n const displayName = file.name.replace(/\\.kml$/i, '');\n addImportedGeoJSON(fc, displayName, 'KMLImport');\n } catch (error) {\n console.error('[KMLImport] Failed:', error);\n const sizeMB = (file.size / (1024 * 1024)).toFixed(1);\n showFileImportError(\n `Failed to import \"${file.name}\" (${sizeMB} MB): ${error.message}`\n );\n }\n\n evt.target.value = '';\n}\n\n// ---------------------------------------------------------------------------\n// Drag-and-drop on the map\n// ---------------------------------------------------------------------------\n\n/**\n * Set up the map container as a drop zone for .shp/.zip, .geojson/.json, .kml\n * files. Dragging files over the map shows a visual overlay; dropping them\n * routes to the correct import handler.\n */\nfunction initMapDropZone() {\n const container = document.querySelector('.map-container');\n if (!container) return;\n\n let dragCounter = 0; // track nested enter/leave events\n\n container.addEventListener('dragenter', (e) => {\n e.preventDefault();\n dragCounter++;\n container.classList.add('drag-over');\n });\n\n container.addEventListener('dragover', (e) => {\n e.preventDefault(); // required to allow drop\n });\n\n container.addEventListener('dragleave', (e) => {\n e.preventDefault();\n dragCounter--;\n if (dragCounter <= 0) {\n dragCounter = 0;\n container.classList.remove('drag-over');\n }\n });\n\n container.addEventListener('drop', (e) => {\n e.preventDefault();\n dragCounter = 0;\n container.classList.remove('drag-over');\n\n const files = e.dataTransfer?.files;\n if (!files || files.length === 0) return;\n\n // Build extension lookup to decide which handler to use\n const byExt = indexFilesByExtension(files);\n const exts = Object.keys(byExt);\n\n if (byExt.zip || byExt.shp) {\n // Shapefile import (zip or loose .shp + companions)\n const fakeEvt = { target: { files, value: '' } };\n Object.defineProperty(fakeEvt.target, 'value', { writable: true });\n handleShapefileImport(fakeEvt);\n } else if (byExt.geojson || byExt.json) {\n const file = byExt.geojson || byExt.json;\n const fakeEvt = { target: { files: [file], value: '' } };\n Object.defineProperty(fakeEvt.target, 'value', { writable: true });\n handleGeoJSONImport(fakeEvt);\n } else if (byExt.kml) {\n const fakeEvt = { target: { files: [byExt.kml], value: '' } };\n Object.defineProperty(fakeEvt.target, 'value', { writable: true });\n handleKMLImport(fakeEvt);\n } else {\n showFileImportError(\n 'Unsupported file type(s): ' + exts.map(e => '.' + e).join(', ')\n + '. Drop .zip, .shp, .geojson, .json, or .kml files.'\n );\n }\n });\n\n console.log('[FileImport] Map drop zone initialised');\n}\n\n// ============================================================================\n// Utilities\n// ============================================================================\n\nfunction escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\n// ============================================================================\n// Message Log — persistent stack in the right panel\n// ============================================================================\n\nconst MESSAGE_LOG_MAX = 50;\n\nconst MSG_CONFIG = {\n error: { icon: 'bi-x-circle-fill', color: 'var(--destructive, #dc3545)' },\n warning: { icon: 'bi-exclamation-triangle-fill', color: 'var(--warning, #ffc107)' },\n success: { icon: 'bi-check-circle-fill', color: 'var(--success, #198754)' },\n info: { icon: 'bi-info-circle-fill', color: 'var(--primary, #0d6efd)' },\n};\n\n/**\n * Append a message to the persistent log in the right panel.\n * Also logs to the browser console.\n *\n * @param {'error'|'warning'|'success'|'info'} type\n * @param {string} text\n */\nfunction logMessage(type, text) {\n const cfg = MSG_CONFIG[type] || MSG_CONFIG.info;\n\n // Console mirror\n const consoleFn = type === 'error' ? console.error\n : type === 'warning' ? console.warn\n : console.log;\n consoleFn('[App]', text);\n\n const log = document.getElementById('message-log');\n if (!log) return;\n\n // Remove the \"No messages yet\" placeholder if present\n const placeholder = log.querySelector('.text-muted');\n if (placeholder) placeholder.remove();\n\n // Build the entry\n const entry = document.createElement('div');\n entry.className = 'list-group-item message-log-entry py-2 px-3';\n const now = new Date();\n const time = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n entry.innerHTML =\n `
              ` +\n `` +\n `
              ${escapeHtml(text)}
              ` +\n `${time}` +\n `
              `;\n\n // Prepend (newest first)\n log.prepend(entry);\n\n // Cap the list\n while (log.children.length > MESSAGE_LOG_MAX) {\n log.lastElementChild.remove();\n }\n}\n\n/** Wire up the \"clear\" button */\nfunction initMessageLog() {\n const btn = document.getElementById('clear-message-log');\n if (btn) {\n btn.addEventListener('click', () => {\n const log = document.getElementById('message-log');\n if (log) {\n log.innerHTML = '
              No messages yet.
              ';\n }\n });\n }\n}\n\n// ============================================================================\n// GPS live-position + trail recording\n//\n// Wiring only — all GPS logic lives in the reusable src/geotracker/ engine and\n// the LUPMIS adapter in src/geotracker-lupmis.js. Here we connect the engine's\n// events to the navbar readout and the map's render/control hooks.\n// ============================================================================\n\nfunction initGpsTracking() {\n const readout = document.getElementById('gps-readout');\n const coordsEl = document.getElementById('gps-coords');\n const accEl = document.getElementById('gps-accuracy');\n const satsEl = document.getElementById('gps-sats');\n\n if (!geoTracker.isSupported) {\n if (coordsEl) coordsEl.textContent = 'No GPS';\n return;\n }\n\n // Live navbar readout — fires for every fix (one-shot Locate or watch).\n geoTracker.on('position', (fix) => {\n if (coordsEl) coordsEl.textContent = `${formatCoord(fix.lat)}, ${formatCoord(fix.lon)}`;\n if (accEl) accEl.textContent = formatAccuracy(fix.accuracy);\n if (satsEl) satsEl.textContent = `${fix.satellites != null ? fix.satellites : '—'} sat`;\n if (readout) {\n readout.classList.add('active');\n readout.classList.remove('quality-good', 'quality-fair', 'quality-poor');\n readout.classList.add('quality-' + accuracyQuality(fix.accuracy));\n }\n mapView?.showCurrentPosition(fix.lon, fix.lat, fix.accuracy);\n });\n\n // Each recorded waypoint extends the on-map trail line.\n geoTracker.on('point', (evt) => {\n mapView?.appendTrailPoint(evt.point.lon, evt.point.lat);\n });\n\n geoTracker.on('error', (err) => {\n console.warn('[GPS]', err?.message || err);\n if (err && err.code === 1) { // PERMISSION_DENIED\n showError('Location permission denied. Enable location access to use GPS.');\n }\n });\n\n // \"Locate me\" → one-shot position + recenter.\n mapView.onLocateMe(async () => {\n try {\n const fix = await geoTracker.getCurrentPosition();\n mapView.centerOn(fix.lon, fix.lat, 16);\n } catch (err) {\n showError('Could not get your location: ' + (err?.message || err));\n }\n });\n\n // \"Record trail\" → start/stop. Recording persists locally and syncs on stop.\n mapView.onToggleRecording(async (start) => {\n if (start) {\n try {\n await dbReady;\n mapView.startTrailRender();\n mapView.setRecordingState(true);\n readout?.classList.add('recording');\n await geoTracker.startRecording({ name: `Trail ${new Date().toLocaleString()}` });\n showSuccess('GPS trail recording started');\n } catch (err) {\n mapView.setRecordingState(false);\n readout?.classList.remove('recording');\n showError('Could not start recording: ' + (err?.message || err));\n }\n } else {\n try {\n const res = await geoTracker.stopRecording();\n mapView.setRecordingState(false);\n readout?.classList.remove('recording');\n if (res) {\n const msg = `Trail saved: ${res.pointCount} points, ${formatDistance(res.distanceM)}` +\n (res.synced ? ' — synced' : ' — will sync when online');\n showSuccess(msg);\n }\n } catch (err) {\n showError('Error stopping recording: ' + (err?.message || err));\n }\n }\n });\n\n // Retry uploading trails recorded while offline — on load and when back online.\n const trySync = async () => {\n if (!isOnline()) return;\n try {\n await dbReady;\n const r = await geoTracker.syncPending();\n if (r.pushed) console.log(`[GPS] Synced ${r.pushed} pending trail(s)`);\n } catch (e) {\n console.warn('[GPS] pending-sync error', e);\n }\n };\n trySync();\n onOfflineChange((offline) => { if (!offline) trySync(); });\n}\n\n// ============================================================================\n// Toast-style alerts (auto-dismiss) + persistent log\n// ============================================================================\n\nfunction showError(message) {\n logMessage('error', message);\n const el = document.getElementById('error-message');\n if (el) {\n el.querySelector('.message-text').textContent = message;\n el.classList.remove('d-none');\n setTimeout(() => el.classList.add('d-none'), 5000);\n }\n}\n\nfunction showSuccess(message) {\n logMessage('success', message);\n const el = document.getElementById('success-message');\n if (el) {\n el.querySelector('.message-text').textContent = message;\n el.classList.remove('d-none');\n setTimeout(() => el.classList.add('d-none'), 3000);\n }\n}\n\nfunction showWarning(message) {\n logMessage('warning', message);\n const el = document.getElementById('warning-message');\n if (el) {\n el.querySelector('.message-text').textContent = message;\n el.classList.remove('d-none');\n setTimeout(() => el.classList.add('d-none'), 5000);\n }\n}\n\n// ============================================================================\n// Fieldwork Mode\n// ============================================================================\n\nfunction initFieldworkMode() {\n const toggle = document.getElementById('fieldwork-mode-toggle');\n if (!toggle) return;\n\n // Restore saved preference\n const saved = localStorage.getItem('fieldwork-mode');\n if (saved === 'true') {\n document.documentElement.classList.add('fieldwork-mode');\n toggle.checked = true;\n }\n\n toggle.addEventListener('change', () => {\n document.documentElement.classList.toggle('fieldwork-mode', toggle.checked);\n localStorage.setItem('fieldwork-mode', toggle.checked);\n console.log('[Settings] Fieldwork mode', toggle.checked ? 'ON' : 'OFF');\n });\n}\n\n// ============================================================================\n// Dark Mode\n// ============================================================================\n\nfunction initDarkMode() {\n const toggle = document.getElementById('dark-mode-toggle');\n if (!toggle) return;\n\n function applyDark(on) {\n document.documentElement.classList.toggle('dark-mode', on);\n // Bootstrap 5.3 built-in dark mode support\n document.documentElement.setAttribute('data-bs-theme', on ? 'dark' : 'light');\n }\n\n // Restore saved preference\n const saved = localStorage.getItem('dark-mode');\n if (saved === 'true') {\n toggle.checked = true;\n applyDark(true);\n }\n\n toggle.addEventListener('change', () => {\n applyDark(toggle.checked);\n localStorage.setItem('dark-mode', toggle.checked);\n console.log('[Settings] Dark mode', toggle.checked ? 'ON' : 'OFF');\n });\n}\n\n// ============================================================================\n// Measurement System\n// ============================================================================\n\nfunction initMeasurementSystem() {\n const toggle = document.getElementById('measurement-system-toggle');\n const label = document.getElementById('measurement-system-label');\n if (!toggle) return;\n\n function updateLabel() {\n if (label) label.textContent = toggle.checked ? 'Imperial' : 'Metric';\n }\n\n // Restore saved preference\n const saved = localStorage.getItem('measurement-system');\n if (saved === 'imperial') {\n toggle.checked = true;\n }\n updateLabel();\n\n // Apply saved setting to the scale bar on load\n mapView?.setScaleBarUnits(saved || 'metric');\n\n toggle.addEventListener('change', () => {\n const system = toggle.checked ? 'imperial' : 'metric';\n localStorage.setItem('measurement-system', system);\n updateLabel();\n mapView?.setScaleBarUnits(system);\n console.log('[Settings] Measurement system:', system);\n });\n}\n\n/**\n * Default base map selector — persisted in localStorage.\n * Keys must match those handled by MapView.setBaseMap().\n */\nfunction initDefaultBasemap() {\n const select = document.getElementById('default-basemap-select');\n if (!select) return;\n\n // Restore saved preference (default: topo)\n const saved = localStorage.getItem('default-basemap') || 'topo';\n select.value = saved;\n\n select.addEventListener('change', () => {\n const key = select.value;\n localStorage.setItem('default-basemap', key);\n mapView?.setBaseMap(key);\n console.log('[Settings] Default base map:', key);\n });\n\n // Keep the dropdown in sync when the user switches via the floating\n // base-map picker (or any other UI) — MapView fires `basemapchange`\n // from setBaseMap().\n mapView?.getMap()?.on('basemapchange', (evt) => {\n if (evt?.key && select.value !== evt.key) {\n select.value = evt.key;\n try { localStorage.setItem('default-basemap', evt.key); } catch {}\n }\n });\n}\n\n/**\n * Offline Map Tiles card — shows per-provider cache stats and offers a\n * \"Clear cached tiles\" button. Stats refresh whenever the Settings panel\n * is opened so the numbers are always current.\n */\nfunction initOfflineTileCache() {\n const statsEl = document.getElementById('tile-cache-stats');\n const clearBtn = document.getElementById('clear-tiles-btn');\n const offcanvas = document.getElementById('offcanvasBottom');\n if (!statsEl || !clearBtn || !offcanvas) return;\n\n /** Format a byte count into a human-friendly string. */\n function fmtBytes(bytes) {\n if (!bytes) return '0 KB';\n if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(0) + ' KB';\n if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';\n return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';\n }\n\n // Track in-flight refresh so rapid calls don't overlap and to allow a\n // controllerchange handler to know when a refresh is already underway.\n let refreshInFlight = null;\n\n /** Render the stats panel. */\n async function refresh() {\n if (refreshInFlight) return refreshInFlight;\n\n // If the SW hasn't taken control yet, give the user a friendly hint\n // instead of immediately failing. The wait inside getTileCacheStats()\n // will resolve once the SW becomes available, at which point this\n // refresh completes normally — no reload needed.\n const swActive = !!navigator.serviceWorker?.controller;\n statsEl.innerHTML = swActive\n ? '
              Loading…
              '\n : '
              Initialising service worker…
              ';\n\n refreshInFlight = (async () => {\n try {\n const stats = await getTileCacheStats();\n\n if (!stats) {\n statsEl.innerHTML = `\n
              \n Tile cache stats unavailable. Try reloading the page if this persists.\n
              `;\n return;\n }\n\n const total = stats.totals;\n const rows = stats.byProvider\n .filter((p) => p.count > 0)\n .map((p) => `\n \n ${escapeHtml(p.label)}\n ${p.count.toLocaleString()} / ${p.limit.toLocaleString()}\n ${fmtBytes(p.estBytes)}\n \n \n \n `).join('');\n\n let storageNote = '';\n const est = await getStorageEstimate();\n if (est && est.quota > 0) {\n const pct = ((est.usage / est.quota) * 100).toFixed(1);\n storageNote = `\n
              \n Total app storage: ${fmtBytes(est.usage)} of ${fmtBytes(est.quota)} available (${pct}%)\n
              `;\n }\n\n if (total.count === 0) {\n statsEl.innerHTML = `\n
              \n No tiles cached yet. Pan and zoom the map to start caching tiles automatically.\n
              ${storageNote}`;\n clearBtn.disabled = true;\n return;\n }\n\n statsEl.innerHTML = `\n
              \n ${total.count.toLocaleString()} tiles cached, ~${fmtBytes(total.estBytes)} on this device\n
              \n \n \n \n \n \n \n \n ${rows}\n
              Base mapCached / limitApprox. size
              ${storageNote}`;\n clearBtn.disabled = false;\n\n // Per-provider Clear — confirm, clear that bucket only, refresh\n statsEl.querySelectorAll('.provider-clear-btn').forEach((btn) => {\n btn.addEventListener('click', async (e) => {\n e.preventDefault();\n const cacheName = btn.dataset.cache;\n const label = btn.dataset.label || cacheName;\n if (!confirm(`Clear cached \"${label}\" tiles?\\n\\nOther providers are not affected. The tiles will re-download as you browse online.`)) {\n return;\n }\n btn.disabled = true;\n const ok = await clearTileCacheForProvider(cacheName);\n if (ok) {\n console.log(`[Settings] Cleared tile cache for ${label}`);\n } else {\n console.warn(`[Settings] Could not clear tile cache for ${label}`);\n }\n await refresh();\n });\n });\n } finally {\n refreshInFlight = null;\n }\n })();\n\n return refreshInFlight;\n }\n\n // Clear button — confirm, then clear, then refresh\n clearBtn.addEventListener('click', async () => {\n if (!confirm('Clear all cached map tiles from this device? You will need to be online to view them again.')) {\n return;\n }\n clearBtn.disabled = true;\n const ok = await clearTileCaches();\n if (ok) {\n console.log('[Settings] Tile caches cleared');\n } else {\n console.warn('[Settings] Tile-cache clear failed');\n }\n await refresh();\n });\n\n // Refresh stats whenever the Settings offcanvas opens\n offcanvas.addEventListener('show.bs.offcanvas', refresh);\n\n // Auto-refresh when a (new) service worker takes control of the page —\n // makes the panel populate as soon as the SW is available, even if the\n // user is staring at it during initial install or during an SW update.\n onServiceWorkerControllerChange(() => {\n console.log('[Settings] SW controller changed → refreshing tile-cache stats');\n refresh();\n });\n\n // Also do an initial render so the card isn't empty if Settings is open\n // immediately on load.\n refresh();\n}\n\n/**\n * Offline-download dialog (Phase 2). Allows users to pre-fetch tiles for a\n * chosen extent and zoom range so they can use the map without connectivity.\n */\nfunction initOfflineDownloadDialog() {\n const triggerBtn = document.getElementById('download-tiles-btn');\n const modalEl = document.getElementById('offline-download-modal');\n if (!triggerBtn || !modalEl) return;\n\n const modal = Modal.getOrCreateInstance(modalEl);\n\n // ----- Element refs -----\n const formView = document.getElementById('offline-download-form-view');\n const progressView = document.getElementById('offline-download-progress-view');\n const doneView = document.getElementById('offline-download-done-view');\n const cancelBtn = document.getElementById('offline-download-cancel-btn');\n const startBtn = document.getElementById('offline-download-start-btn');\n const closeDoneBtn = document.getElementById('offline-download-close-done-btn');\n const headerCloseBtn = document.getElementById('offline-download-close-btn');\n\n const basemapSelect = document.getElementById('offline-basemap-select');\n const minZoomInput = document.getElementById('offline-min-zoom');\n const maxZoomInput = document.getElementById('offline-max-zoom');\n const ackCheck = document.getElementById('offline-ack-check');\n const estimateEl = document.getElementById('offline-estimate-detail');\n const estimateBox = document.getElementById('offline-estimate');\n\n const areaViewRadio = document.getElementById('offline-area-view');\n const areaDistrictRadio = document.getElementById('offline-area-district');\n const areaGhanaRadio = document.getElementById('offline-area-ghana');\n const areaViewInfo = document.getElementById('offline-area-view-info');\n const areaDistrictInfo = document.getElementById('offline-area-district-info');\n\n const progressBar = document.getElementById('offline-progress-bar');\n const progressPercent = document.getElementById('offline-progress-percent');\n const progressCounts = document.getElementById('offline-progress-counts');\n const progressOk = document.getElementById('offline-progress-ok');\n const progressFailed = document.getElementById('offline-progress-failed');\n const progressEta = document.getElementById('offline-progress-eta');\n\n const doneTitle = document.getElementById('offline-done-title');\n const doneDetail = document.getElementById('offline-done-detail');\n\n // ----- State -----\n let currentDownloader = null;\n\n /** Format byte count for display. */\n function fmtBytes(b) {\n if (!b) return '0 KB';\n if (b < 1024 * 1024) return (b / 1024).toFixed(0) + ' KB';\n if (b < 1024 * 1024 * 1024) return (b / (1024 * 1024)).toFixed(1) + ' MB';\n return (b / (1024 * 1024 * 1024)).toFixed(2) + ' GB';\n }\n\n /** Format ms → human-readable duration. */\n function fmtDuration(ms) {\n if (!ms || ms < 1000) return '< 1 s';\n const s = Math.round(ms / 1000);\n if (s < 60) return s + ' s';\n const m = Math.floor(s / 60);\n const r = s % 60;\n if (m < 60) return `${m} min ${r} s`;\n const h = Math.floor(m / 60);\n return `${h} h ${m % 60} min`;\n }\n\n /** Get the chosen extent based on the radio selection. Returns null if invalid. */\n function getSelectedExtent() {\n if (areaViewRadio.checked) {\n return mapView?.getCurrentViewExtent() || null;\n }\n if (areaDistrictRadio.checked) {\n return mapView?.getDistrictBoundaryExtent()?.extent || null;\n }\n if (areaGhanaRadio.checked) {\n return GHANA_EXTENT_3857;\n }\n return null;\n }\n\n /** Recalculate and update the live estimate display. */\n function updateEstimate() {\n const baseMap = basemapSelect.value;\n const minZ = parseInt(minZoomInput.value, 10);\n const maxZ = parseInt(maxZoomInput.value, 10);\n\n if (Number.isNaN(minZ) || Number.isNaN(maxZ) || minZ > maxZ) {\n estimateEl.textContent = 'Invalid zoom range';\n estimateBox.classList.replace('alert-info', 'alert-warning');\n startBtn.disabled = true;\n return;\n }\n\n const extent = getSelectedExtent();\n if (!extent) {\n estimateEl.textContent = 'Selected area is not available.';\n estimateBox.classList.replace('alert-info', 'alert-warning');\n startBtn.disabled = true;\n return;\n }\n\n const tplMaxZoom = BASEMAP_TEMPLATES[baseMap]?.maxZoom ?? 19;\n const effMaxZ = Math.min(maxZ, tplMaxZoom);\n const count = countTiles(extent, minZ, effMaxZ);\n const bytes = estimatedSizeBytes(count);\n\n let warningHTML = '';\n if (effMaxZ < maxZ) {\n warningHTML = `
              Zoom ${maxZ} is above this provider's max (${tplMaxZoom}); will clamp to ${tplMaxZoom}.`;\n }\n if (count > 8000) {\n warningHTML += `
              More than 8 000 tiles — exceeds the per-provider cache limit. Earlier tiles will be evicted as new ones arrive.`;\n }\n\n estimateEl.innerHTML =\n `${count.toLocaleString()} tiles · ` +\n `~${fmtBytes(bytes)}` +\n warningHTML;\n\n estimateBox.classList.toggle('alert-warning', !!warningHTML);\n estimateBox.classList.toggle('alert-info', !warningHTML);\n\n startBtn.disabled = !ackCheck.checked || count === 0;\n }\n\n /** Update the area-radio info labels (tile count + size estimate). */\n function updateAreaInfos() {\n const view = mapView?.getCurrentViewExtent();\n if (view) {\n areaViewInfo.textContent = ' · ready';\n } else {\n areaViewInfo.textContent = '';\n }\n\n const dist = mapView?.getDistrictBoundaryExtent();\n if (dist) {\n areaDistrictInfo.textContent = '';\n areaDistrictRadio.disabled = false;\n } else {\n areaDistrictInfo.textContent = ' (not loaded — connect online to fetch)';\n areaDistrictRadio.disabled = true;\n if (areaDistrictRadio.checked) areaViewRadio.checked = true;\n }\n }\n\n /** Reset the modal to its initial form state. */\n function resetModal() {\n formView.classList.remove('d-none');\n progressView.classList.add('d-none');\n doneView.classList.add('d-none');\n\n startBtn.classList.remove('d-none');\n cancelBtn.classList.remove('d-none');\n cancelBtn.textContent = 'Cancel';\n closeDoneBtn.classList.add('d-none');\n headerCloseBtn.disabled = false;\n\n ackCheck.checked = false;\n startBtn.disabled = true;\n\n currentDownloader = null;\n }\n\n // ----- Event wiring -----\n\n triggerBtn.addEventListener('click', () => {\n resetModal();\n updateAreaInfos();\n updateEstimate();\n modal.show();\n });\n\n // Recalculate estimate on any input change\n basemapSelect.addEventListener('change', updateEstimate);\n minZoomInput.addEventListener('input', updateEstimate);\n maxZoomInput.addEventListener('input', updateEstimate);\n areaViewRadio.addEventListener('change', updateEstimate);\n areaDistrictRadio.addEventListener('change', updateEstimate);\n areaGhanaRadio.addEventListener('change', updateEstimate);\n ackCheck.addEventListener('change', updateEstimate);\n\n // Start the download\n startBtn.addEventListener('click', async () => {\n const baseMap = basemapSelect.value;\n const minZ = parseInt(minZoomInput.value, 10);\n const maxZ = parseInt(maxZoomInput.value, 10);\n const extent = getSelectedExtent();\n if (!extent) return;\n\n // Switch UI to progress view\n formView.classList.add('d-none');\n progressView.classList.remove('d-none');\n startBtn.classList.add('d-none');\n cancelBtn.textContent = 'Cancel download';\n headerCloseBtn.disabled = true;\n\n progressBar.style.width = '0%';\n progressBar.setAttribute('aria-valuenow', '0');\n progressPercent.textContent = '0%';\n progressCounts.textContent = '0 of 0 tiles';\n progressOk.textContent = '0';\n progressFailed.textContent = '0';\n progressEta.textContent = '—';\n\n currentDownloader = new OfflineTileDownloader({\n baseMap,\n extent3857: extent,\n minZoom: minZ,\n maxZoom: maxZ,\n onProgress: (s) => {\n if (s.total > 0) {\n const pct = Math.min(100, Math.round((s.done / s.total) * 100));\n progressBar.style.width = pct + '%';\n progressBar.setAttribute('aria-valuenow', String(pct));\n progressPercent.textContent = pct + '%';\n progressCounts.textContent = `${s.done.toLocaleString()} of ${s.total.toLocaleString()} tiles`;\n }\n progressOk.textContent = s.ok.toLocaleString();\n progressFailed.textContent = s.failed.toLocaleString();\n progressEta.textContent = s.etaMs != null ? fmtDuration(s.etaMs) : '—';\n },\n });\n\n let result;\n try {\n result = await currentDownloader.start();\n } catch (err) {\n console.error('[OfflineDownload] failed:', err);\n result = { phase: 'error', done: 0, total: 0, ok: 0, failed: 0 };\n }\n\n // Switch UI to done view\n progressView.classList.add('d-none');\n doneView.classList.remove('d-none');\n cancelBtn.classList.add('d-none');\n closeDoneBtn.classList.remove('d-none');\n headerCloseBtn.disabled = false;\n\n if (result.phase === 'cancelled') {\n doneTitle.textContent = 'Download cancelled';\n doneDetail.innerHTML = `Stopped after ${result.done.toLocaleString()} of ${result.total.toLocaleString()} tiles.
              ` +\n `${result.ok.toLocaleString()} fetched · ${result.failed.toLocaleString()} failed.`;\n } else if (result.phase === 'error') {\n doneTitle.textContent = 'Download failed';\n doneDetail.textContent = 'See console for details.';\n } else {\n doneTitle.textContent = 'Download complete';\n doneDetail.innerHTML = `${result.ok.toLocaleString()} tiles cached` +\n (result.failed > 0 ? `, ${result.failed.toLocaleString()} failed` : '') +\n `.
              Took ${fmtDuration(result.elapsedMs)}.`;\n }\n });\n\n // Cancel button — either close modal (form view) or cancel download (progress view)\n cancelBtn.addEventListener('click', () => {\n if (currentDownloader) {\n currentDownloader.cancel();\n }\n });\n\n // When modal is fully hidden, reset for next time\n modalEl.addEventListener('hidden.bs.modal', () => {\n if (currentDownloader) currentDownloader.cancel();\n resetModal();\n });\n}\n\n/**\n * Account card — displays the signed-in user from window.LUPMIS_SESSION\n * (injected by public/index.php) and wires the \"Sign out\" button.\n *\n * In local dev (no PHP), window.LUPMIS_SESSION is absent / empty and the\n * card shows \"Guest (no session)\" without a Sign-out button.\n */\n/**\n * Account UI — populates the right-side Menu offcanvas (id=\"menuOffcanvas\")\n * with the signed-in user's details, and wires the Sign-out button.\n * The Menu is opened from the navbar Menu button (id=\"menu-btn\").\n *\n * Three states:\n * • authenticated — show name, email, district info, and \"Sign out\"\n * • unauthenticated (PHP ran, no SSO cookie) — show \"Sign in\" link\n * • no-session (window.LUPMIS_SESSION undefined → dev mode) — show\n * a warning note that the page wasn't served via index.php\n */\nfunction initAccountCard() {\n const session = getSession();\n const menuBtn = document.getElementById('menu-btn');\n const avatarEl = document.getElementById('menu-user-avatar');\n const nameEl = document.getElementById('menu-user-name');\n const emailEl = document.getElementById('menu-user-email');\n const detailEl = document.getElementById('menu-user-detail');\n const signoutBtn = document.getElementById('menu-signout-btn');\n const signinLink = document.getElementById('menu-signin-link');\n const noSessNote = document.getElementById('menu-no-session-note');\n\n if (!menuBtn || !avatarEl || !nameEl || !emailEl || !detailEl || !signoutBtn) {\n console.warn('[AccountMenu] One or more elements missing — shell may be stale. Hard-refresh.');\n return;\n }\n\n const isAuthenticated = !!session && !!session.user_id;\n\n if (isAuthenticated) {\n // ---------- Authenticated state ----------\n const displayName = [session.title, session.full_name].filter(Boolean).join(' ').trim()\n || session.username || 'Authenticated user';\n const initial = (session.full_name || session.username || '?').trim().charAt(0).toUpperCase();\n avatarEl.textContent = initial;\n avatarEl.style.background = 'var(--brand-navy, #1e1a4b)';\n nameEl.textContent = displayName;\n emailEl.textContent = session.email || '';\n\n const bits = [];\n if (session.district_id != null) bits.push(`District ${escapeHtml(String(session.district_id))}`);\n if (session.region_id != null) bits.push(`Region ${escapeHtml(String(session.region_id))}`);\n if (session.ua_position) bits.push(escapeHtml(session.ua_position));\n detailEl.innerHTML = bits.join(' · ') || 'No district info';\n\n signoutBtn.classList.remove('d-none');\n signoutBtn.addEventListener('click', () => handleSignOut(session), { once: false });\n signinLink?.classList.add('d-none');\n noSessNote?.classList.add('d-none');\n menuBtn.removeAttribute('data-state');\n menuBtn.setAttribute('title', `Menu — ${displayName}`);\n } else if (typeof window.LUPMIS_SESSION === 'undefined') {\n // ---------- Dev mode (no PHP processing) ----------\n avatarEl.innerHTML = '';\n avatarEl.style.background = 'var(--brand-orange-warm, #ff9e1b)';\n nameEl.textContent = 'No session injected';\n emailEl.textContent = '';\n detailEl.textContent = '';\n\n signoutBtn.classList.add('d-none');\n signinLink?.classList.add('d-none');\n noSessNote?.classList.remove('d-none');\n menuBtn.dataset.state = 'no-session';\n menuBtn.setAttribute('title', 'Menu (no session — dev mode)');\n } else {\n // ---------- PHP ran but the user has no valid SSO session ----------\n avatarEl.innerHTML = '';\n avatarEl.style.background = 'var(--brand-gray-medium, #7a7a7a)';\n nameEl.textContent = 'Not signed in';\n emailEl.textContent = '';\n detailEl.textContent = '';\n\n signoutBtn.classList.add('d-none');\n signinLink?.classList.remove('d-none');\n noSessNote?.classList.add('d-none');\n menuBtn.dataset.state = 'unauthenticated';\n menuBtn.setAttribute('title', 'Menu (not signed in)');\n }\n}\n\n// Legacy chip+popover removed — replaced by the navbar Menu button +\n// right-side menuOffcanvas. See initAccountCard above.\n\n/**\n * Sign-out flow:\n * 1. Confirm with the user.\n * 2. Best-effort fire-and-forget call to the SSO logout endpoint so the\n * server-side token is invalidated (no-cors mode tolerates CORS issues).\n * 3. Expire the local sso_auth_token cookie on the parent domain so the\n * browser stops sending it.\n * 4. Redirect to the SSO login page — leaves the user on familiar ground\n * (and on next visit, index.php sees no session and serves a fresh\n * page with no LUPMIS_SESSION).\n */\nasync function handleSignOut(session) {\n if (!confirm(`Return to Landing Page, ${session?.full_name || session?.username || 'user'}?`)) {\n return;\n }\n\n // 1. Best-effort: invalidate the SSO token server-side\n const cookieToken = document.cookie\n .split(';')\n .map((c) => c.trim())\n .find((c) => c.startsWith('sso_auth_token='))\n ?.split('=')[1];\n if (cookieToken) {\n try {\n // no-cors swallows CORS errors; we don't read the response\n await fetch('https://lupmis4luspa.org/sso/logout?token=' + encodeURIComponent(cookieToken), {\n method: 'GET',\n mode: 'no-cors',\n credentials: 'include',\n cache: 'no-store',\n });\n } catch (err) {\n console.warn('[Signout] Best-effort SSO logout call failed:', err);\n }\n }\n\n // 2. Clear the cookie on the shared parent domain\n // Set with both leading-dot and no-dot variants; browsers vary on which sticks.\n const past = 'Thu, 01 Jan 1970 00:00:00 GMT';\n document.cookie = `sso_auth_token=; expires=${past}; path=/; domain=.lupmis4luspa.org`;\n document.cookie = `sso_auth_token=; expires=${past}; path=/; domain=lupmis4luspa.org`;\n document.cookie = `sso_auth_token=; expires=${past}; path=/`;\n\n // 3. Redirect to the central LUSPA login\n window.location.href = 'https://lupmis4luspa.org/';\n}\n\n// ============================================================================\n// Start Application\n// ============================================================================\n\n// Wait for DOM to be ready\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', initApp);\n} else {\n initApp();\n}\n"],"file":"assets/index-DJ2WL3EC.js"} \ No newline at end of file diff --git a/dist/assets/index-YjHYbDyk.js b/dist/assets/index-YjHYbDyk.js new file mode 100644 index 0000000..1216ea0 --- /dev/null +++ b/dist/assets/index-YjHYbDyk.js @@ -0,0 +1,803 @@ +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 ft,h as M,F as I,j as k,k as ae,m as Bt,b as R,V as A,L as ie,D as Ee,P as Ke,Q as rt,n as se,U as ve,M as Kt,W as cn,X,Y as dn,S as un,G as pn,Z as fn,o as Xe,O as ge,$ as Ue,a0 as st,a1 as xe,A as hn,T as ee,a2 as _e,a3 as Vt,a4 as ce,a5 as Xt,e as gn,u as _t,s as mn,a6 as Lo,a7 as yn}from"./openlayers-CvK8xBSr.js";import{M as Gt}from"./bootstrap-D1-uvFxm.js";import{o as bn,a as wn,b as vn,c as _n,d as Ct,e as Yt,f as qe,g as En,h as me,i as xn,j as Sn}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 a of o)if(a.type==="childList")for(const s of a.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&n(s)}).observe(document,{childList:!0,subtree:!0});function t(o){const a={};return o.integrity&&(a.integrity=o.integrity),o.referrerPolicy&&(a.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?a.credentials="include":o.crossOrigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function n(o){if(o.ep)return;o.ep=!0;const a=t(o);fetch(o.href,a)}})();const Zt="function",Ae="64e10b34-2bf7-4616-9668-f99de5aa046e",Ln="get",Tn="has",kn="set",{isArray:tt}=Array;let{SharedArrayBuffer:at,window:Pn}=globalThis,{notify:To,wait:ko,waitAsync:it}=Atomics,Po=null;it||(it=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 at(4)}catch{at=ArrayBuffer;const e=new WeakMap;if(Pn){const t=new Map,{prototype:{postMessage:n}}=Worker,o=a=>{const s=a.data?.[Ae];if(!tt(s)){a.stopImmediatePropagation();const{id:i,sb:l}=s;t.get(i)(l)}};Po=function(a,...s){const i=a?.[Ae];if(tt(i)){const[l,c]=i;e.set(c,l),this.addEventListener("message",o)}return n.call(this,a,...s)},it=a=>({value:new Promise(s=>{t.set(e.get(a),s)}).then(s=>{t.delete(e.get(a)),e.delete(a);for(let i=0;i({[Ae]:{id:n,sb:o}});To=n=>{postMessage(t(e.get(n),n))},addEventListener("message",n=>{const o=n.data?.[Ae];if(tt(o)){const[a,s]=o;e.set(s,a)}})}}/*! (c) Andrea Giammarchi - ISC */const{Int32Array:At,Map:Jt,Uint16Array:Dt}=globalThis,{BYTES_PER_ELEMENT:Qt}=At,{BYTES_PER_ELEMENT:Mn}=Dt,In=(r,e,t)=>{for(;ko(r,0,0,e)==="timed-out";)t()},Ft=new WeakSet,Et=new WeakMap,Cn={value:{then:r=>r()}};let An=0;const qt=(r,{parse:e=JSON.parse,stringify:t=JSON.stringify,transform:n,interrupt:o}=JSON)=>{if(!Et.has(r)){const a=Po||r.postMessage,s=(p,...h)=>a.call(r,{[Ae]:h},{transfer:p}),i=typeof o===Zt?o:o?.handler,l=o?.delay||42,c=new TextDecoder("utf-16"),d=(p,h)=>p?it(h,0):(i?In(h,l,i):ko(h,0),Cn);let u=!1;Et.set(r,new Proxy(new Jt,{[Tn]:(p,h)=>typeof h=="string"&&!h.startsWith("_"),[Ln]:(p,h)=>h==="then"?null:((...f)=>{const b=An++;let g=new At(new at(Qt*2)),m=[];Ft.has(f.at(-1)||m)&&Ft.delete(m=f.pop()),s(m,b,g,h,n?f.map(n):f);const y=r!==globalThis;let w=0;return u&&y&&(w=setTimeout(console.warn,1e3,`💀🔒 - Possible deadlock if proxy.${h}(...args) is awaited`)),d(y,g).value.then(()=>{clearTimeout(w);const S=g[1];if(!S)return;const x=Mn*S;return g=new At(new at(x+x%Qt)),s([],b,g),d(y,g).value.then(()=>e(c.decode(new Dt(g.buffer).slice(0,S))))})}),[kn](p,h,f){const b=typeof f;if(b!==Zt)throw new Error(`Unable to assign ${h} as ${b}`);if(!p.size){const g=new Jt;r.addEventListener("message",async m=>{const y=m.data?.[Ae];if(tt(y)){m.stopImmediatePropagation();const[w,S,...x]=y;let v;if(x.length){const[L,P]=x;if(p.has(L)){u=!0;try{const T=await p.get(L)(...P);if(T!==void 0){const z=t(n?n(T):T);g.set(w,z),S[1]=z.length}}catch(T){v=T}finally{u=!1}}else v=new Error(`Unsupported action: ${L}`);S[0]=1}else{const L=g.get(w);g.delete(w);for(let P=new Dt(S.buffer),T=0;T(Ft.add(r),r);function eo(){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 Mo(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 a=[];let s=!1;for(;!s;){const d=await o.read();d.value&&a.push(d.value),s=d.done}const i=a.reduce((d,u)=>d+u.length,0),l=new Uint8Array(i);let c=0;return a.forEach(d=>{l.set(d,c),c+=d.length}),l.buffer}}else return t}class lt{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 ft(async()=>{const{default:a}=await import("./index-DTMgZTfd.js");return{default:a}},[]);this.sqlite3InitModule=o}this.sqlite3||(this.sqlite3=await this.sqlite3InitModule()),this.db&&await this.destroy(),this.db=new this.sqlite3.oo1.DB(t,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 a of e){let s=o.get(a.sql);if(!s){const c=n.prepare(a.sql);o.set(a.sql,c),s=c}a.params?.length&&s.bind(a.params);let i=[],l=[];for(;s.step();)i=s.getColumnNames([]),l.push(s.get([]));t.push({columns:i,rows:l}),s.reset()}}finally{o.forEach(a=>{a.finalize()})}}),t}async isDatabasePersisted(){return!1}async getDatabaseSizeBytes(){const t=(await this.exec({sql:`SELECT page_count * page_size AS size + FROM pragma_page_count(), pragma_page_size()`,method:"get"}))?.rows?.[0];if(typeof t!="number")throw new Error("Failed to query database size");return t}async createFunction(e){if(!this.db)throw new Error("Driver not initialized");switch(e.type){case"callback":case"scalar":this.db.createFunction({name:e.name,xFunc:(t,...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 Mo(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,a,s)=>{this.writeCallbacks.forEach(i=>{i({table:a,rowid:s,operation:e[n]})})},0)}closeDb(){this.db&&(this.db.close(),this.db=void 0)}}function Dn(r,e,t){let n,o,a,s,i,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,a=u?Math.max(Number(t.maxWait)||0,e):0,p="trailing"in t?!!t.trailing:p);function h(v){const L=n,P=o;return n=o=void 0,c=v,s=r.apply(P,L),s}function f(v){return c=v,i=setTimeout(m,e),d?h(v):s}function b(v){const L=v-(l??0),P=v-c,T=e-L;return u?Math.min(T,a-P):T}function g(v){const L=v-(l??0),P=v-c;return l===void 0||L>=e||L<0||u&&P>=a}function m(){const v=Date.now();if(g(v))return y(v);i=setTimeout(m,b(v))}function y(v){return i=void 0,p&&n?h(v):(n=o=void 0,s)}function w(){i!==void 0&&clearTimeout(i),c=0,n=l=o=i=void 0}function S(){return i===void 0?s:y(Date.now())}function x(){const v=Date.now(),L=g(v);if(n=arguments,o=this,l=v,L){if(i===void 0)return f(l);if(u)return i=setTimeout(m,e),h(l)}return i===void 0&&(i=setTimeout(m,e)),s}return x.cancel=w,x.flush=S,x}function ot(){return crypto.randomUUID()}function Io(r,e){switch(r){case"session":case":sessionStorage:":let t=sessionStorage._sqlocal_session_key;return t||(t=ot(),sessionStorage._sqlocal_session_key=t),`session:${t}`;case"local":case":localStorage:":return"local";case":memory:":return`memory:${e}`;default:return`path:${r}`}}class Ye{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:eo()}),Object.defineProperty(this,"transactionMutex",{enumerable:!0,configurable:!0,writable:!0,value:eo()}),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 lt,await this.driver.init(this.config)}const a=Io(this.config.databasePath,this.config.clientKey);this.reinitChannel=new BroadcastChannel(`_sqlocal_reinit_(${a})`),this.reinitChannel.onmessage=s=>{const i=s.data;if(this.config.clientKey!==i.clientKey)switch(i.type){case"reinit":this.init(i.reason);break;case"close":this.driver.destroy();break}},this.config.reactive&&(this.effectsChannel=new BroadcastChannel(`_sqlocal_effects_(${a})`),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(a){this.emitMessage({type:"error",error:a,queryKey:null}),await this.destroy()}finally{await this.initMutex.unlock()}}}}),Object.defineProperty(this,"postMessage",{enumerable:!0,configurable:!0,writable:!0,value:async(o,a)=>{const 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,a=[])=>{this.onmessage&&this.onmessage(o,a)}}),Object.defineProperty(this,"emitEffects",{enumerable:!0,configurable:!0,writable:!0,value:()=>{!this.effectsChannel||this.dirtyTables.size===0||(this.effectsChannel.postMessage({type:"effects",tables:[...this.dirtyTables]}),this.dirtyTables.clear())}}),Object.defineProperty(this,"emitEffectsDebounced",{enumerable:!0,configurable:!0,writable:!0,value:Dn(()=>this.emitEffects(),32,{maxWait:180})}),Object.defineProperty(this,"editConfig",{enumerable:!0,configurable:!0,writable:!0,value:o=>{this.config=o.config,this.init("initial")}}),Object.defineProperty(this,"exec",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{try{const a={type:"data",queryKey:o.queryKey,data:[]};switch(o.type){case"query":const s=this.transactionKey!==null&&this.transactionKey===o.transactionKey;try{s||await this.transactionMutex.lock();const i=await this.driver.exec(o);a.data.push(i)}finally{s||await this.transactionMutex.unlock()}break;case"batch":try{await this.transactionMutex.lock();const i=await this.driver.execBatch(o.statements);a.data.push(...i)}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 i=o.action==="commit"?"COMMIT":"ROLLBACK";await this.driver.exec({sql:i}),this.transactionKey=null,await this.transactionMutex.unlock()}break}this.emitMessage(a)}catch(a){this.emitMessage({type:"error",error:a,queryKey:o.queryKey})}}}),Object.defineProperty(this,"execInitStatements",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{if(this.config.onInitStatements)for(let o of this.config.onInitStatements)await this.driver.exec(o)}}),Object.defineProperty(this,"getDatabaseInfo",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{try{this.emitMessage({type:"info",queryKey:o.queryKey,info:{databasePath:this.config.databasePath,storageType:this.driver.storageType,databaseSizeBytes:await this.driver.getDatabaseSizeBytes(),persisted:await this.driver.isDatabasePersisted()}})}catch(a){this.emitMessage({type:"error",queryKey:o.queryKey,error:a})}}}),Object.defineProperty(this,"createUserFunction",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{functionName:a,functionType:s,queryKey:i}=o;let l;if(this.userFunctions.has(a)){this.emitMessage({type:"error",error:new Error(`A user-defined function with the name "${a}" has already been created for this SQLocal instance.`),queryKey:i});return}switch(s){case"callback":l={type:s,name:a,func:(...c)=>{this.emitMessage({type:"callback",name:a,args:c})}};break;case"scalar":l={type:s,name:a,func:this.proxy[`_sqlocal_func_${a}`]};break;case"aggregate":l={type:s,name:a,func:{step:this.proxy[`_sqlocal_func_${a}_step`],final:this.proxy[`_sqlocal_func_${a}_final`]}};break}try{await this.initUserFunction(l),this.emitMessage({type:"success",queryKey:i})}catch(c){this.emitMessage({type:"error",error:c,queryKey:i})}}}),Object.defineProperty(this,"initUserFunction",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{await this.driver.createFunction(o),this.userFunctions.set(o.name,o)}}),Object.defineProperty(this,"importDb",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{queryKey:a,database:s}=o;let i=!1;try{await this.driver.import(s),this.driver.storageType==="memory"&&await this.execInitStatements()}catch(l){this.emitMessage({type:"error",error:l,queryKey:a}),i=!0}finally{this.driver.storageType!=="memory"&&await this.init("overwrite")}i||this.emitMessage({type:"success",queryKey:a})}}),Object.defineProperty(this,"exportDb",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{queryKey:a}=o;try{const{name:s,data:i}=await this.driver.export();this.emitMessage({type:"buffer",queryKey:a,bufferName:s,buffer:i},[i])}catch(s){this.emitMessage({type:"error",error:s,queryKey:a})}}}),Object.defineProperty(this,"deleteDb",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{queryKey:a}=o;let s=!1;try{await this.driver.clear()}catch(i){this.emitMessage({type:"error",error:i,queryKey:a}),s=!0}finally{await this.init("delete")}s||this.emitMessage({type:"success",queryKey:a})}}),Object.defineProperty(this,"destroy",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{await this.driver.exec({sql:"PRAGMA optimize"}),await this.driver.destroy(),this.effectsChannel&&(this.emitEffectsDebounced.flush(),this.effectsChannel.close(),this.effectsChannel=void 0),this.reinitChannel&&(this.reinitChannel.close(),this.reinitChannel=void 0),o&&this.emitMessage({type:"success",queryKey:o.queryKey})}});const n=typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope?qt(globalThis):globalThis;this.proxy=n,this.driver=e}}function ct(r,...e){return{sql:r.join("?"),params:e}}function Fn(r){return!r.some(e=>!Array.isArray(e))}function xt(r,e){let t;return Fn(r)?t=r:t=[r],t.map(n=>{const o={};return e.forEach((a,s)=>{o[a]=n[s]}),o})}function On(r){return typeof r=="object"&&r!==null&&"getSQL"in r&&typeof r.getSQL=="function"}function Rn(r){return typeof r=="object"&&r!==null&&"sql"in r&&typeof r.sql=="string"&&"params"in r}function to(r){if(typeof r=="function"&&(r=r(ct)),On(r))try{if(!("toSQL"in r&&typeof r.toSQL=="function"))throw 1;const n=r.toSQL();if(!Rn(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 oo(r,e){let t;return typeof r=="string"?t={sql:r,params:e}:t=ct(r,...e),t}async function Ze(r,e,t,n){return!e&&"locks"in navigator?navigator.locks.request(`_sqlocal_mutation_(${t.databasePath})`,{mode:r},n):n()}class no extends lt{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 ft(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 lt;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 Co,Ao;class $n{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[h,f]=u.get(d.queryKey);d.type==="error"?f(d.error):h(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=>Ze("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=ot();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 h=await this.createQuery({type:"query",transactionKey:p,sql:c,params:d,method:u}),f={rows:[],columns:[]};return h.type==="data"&&(f.rows=h.data[0]?.rows??[],f.columns=h.data[0]?.columns??[]),f}}),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,h)=>{u[h]=p}),u}}),Object.defineProperty(this,"sql",{enumerable:!0,configurable:!0,writable:!0,value:async(c,...d)=>{const u=oo(c,d),{rows:p,columns:h}=await this.exec(u.sql,u.params,"all");return xt(p,h)}}),Object.defineProperty(this,"batch",{enumerable:!0,configurable:!0,writable:!0,value:async c=>{const d=c(ct);return(await this.execBatch(d)).map(({rows:p,columns:h})=>xt(p,h))}}),Object.defineProperty(this,"beginTransaction",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{const c=ot();await this.createQuery({type:"transaction",transactionKey:c,action:"begin"});const d=async f=>{const b=to(f);if(b.exec)return this.transactionQueryKeyQueue.push(c),b.exec();const{rows:g,columns:m}=await this.exec(b.sql,b.params,"all",c);return xt(g,m)};return{query:d,sql:async(f,...b)=>{const g=oo(f,b);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=>Ze("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,h=0;const f=to(c),b=new Set,g=new Set,m=new Set,y=async()=>{try{const S=++h;if(b.size===0){const v=await this.sql("SELECT name, wr FROM tables_used(?) WHERE type = 'table'",f.sql),L=new Set,P=new Set;if(v.forEach(T=>{typeof T.name=="string"&&(T.wr?P.add(T.name):L.add(T.name))}),L.size===0)throw new Error("The passed SQL does not read any tables.");if(Array.from(P).some(T=>L.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.");L.forEach(T=>b.add(T))}const x=f.exec?await f.exec():await this.sql(f.sql,...f.params);S===h&&(d=x,u=!0,g.forEach(v=>v(d)))}catch(S){m.forEach(x=>{x(S instanceof Error?S:new Error(String(S)))})}},w=S=>{S.data.tables.some(x=>b.has(x))&&y()};return{get value(){return d},subscribe:(S,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=v=>{throw v}),g.add(S),m.add(x),p?u&&S(d):(this.effectsChannel.addEventListener("message",w),p=!0,y()),{unsubscribe:()=>{g.delete(S),m.delete(x),g.size===0&&(this.effectsChannel?.removeEventListener("message",w),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 Ze("exclusive",!1,this.config,async()=>{try{this.broadcast({type:"close",clientKey:this.clientKey});const u=await Mo(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 Ze("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,Co,{enumerable:!0,configurable:!0,writable:!0,value:()=>{this.destroy()}}),Object.defineProperty(this,Ao,{enumerable:!0,configurable:!0,writable:!0,value:async()=>{await this.destroy()}});const t=typeof e=="string"?{databasePath:e}:e,{onInit:n,onConnect:o,processor:a,...s}=t,{databasePath:i}=s;this.config=t,this.clientKey=ot();const l=Io(i,this.clientKey);if(this.reinitChannel=new BroadcastChannel(`_sqlocal_reinit_(${l})`),s.reactive&&(this.effectsChannel=new BroadcastChannel(`_sqlocal_effects_(${l})`)),typeof a<"u")this.processor=a;else if(i==="local"||i===":localStorage:"){const c=new no("local");this.processor=new Ye(c)}else if(i==="session"||i===":sessionStorage:"){const c=new no("session");this.processor=new Ye(c)}else if(typeof globalThis.Worker<"u"&&i!==":memory:")this.processor=new Worker(new URL("/assets/worker-CuIBOSaM.js",import.meta.url),{type:"module"});else{const c=new lt;this.processor=new Ye(c)}this.processor instanceof Ye?(this.processor.onmessage=c=>this.processMessageEvent(c),this.proxy=globalThis):(this.processor.addEventListener("message",this.processMessageEvent),this.proxy=qt(this.processor)),this.processor.postMessage({type:"config",config:{...s,clientKey:this.clientKey,onInitStatements:n?.(ct)??[]}})}}Co=Symbol.dispose,Ao=Symbol.asyncDispose;const zt="lupmis2.db",Nn="lupmis-db-sync",Do=new $n(zt),{sql:E}=Do;console.log("[Database] SQLocal instance created for:",zt);const Fo=new BroadcastChannel(Nn);let Oo=!1,Ro,$o;const ro=new Promise((r,e)=>{Ro=r,$o=e}),dt=new Set;function Bn(r){return dt.add(r),()=>dt.delete(r)}Fo.onmessage=r=>{const{type:e,payload:t}=r.data;if(e==="DB_CHANGE")for(const n of dt)try{n(t)}catch(o){console.error("[Database] Change listener error:",o)}};function Te(r,e,t=null){Fo.postMessage({type:"DB_CHANGE",payload:{table:r,action:e,id:t,timestamp:Date.now()}});for(const n of dt)try{n({table:r,action:e,id:t,timestamp:Date.now(),local:!0})}catch(o){console.error("[Database] Change listener error:",o)}}async function Gn(){try{console.log("[Database] Initializing schema...");const r=await E`SELECT sqlite_version() as version`;console.log("[Database] SQLite version:",r[0]?.version),console.log("[Database] Creating locations table..."),await E` + 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 E`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 E` + 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 E` + 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 E` + CREATE TABLE IF NOT EXISTS collector_zones ( + id INTEGER PRIMARY KEY, + zone_name TEXT, + geometry_wkt TEXT, + properties TEXT, + fetched_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + `,console.log("[Database] Creating parcels table...");try{const n=await E`PRAGMA table_info(parcels)`;n.length>0&&!n.some(o=>o.name==="upn")&&(console.log("[Database] Migrating parcels table to lu_parcels structure (dropping old cache)..."),await E`DROP TABLE parcels`)}catch(n){console.warn("[Database] parcels migration check failed:",n)}await E` + CREATE TABLE IF NOT EXISTS parcels ( + id INTEGER PRIMARY KEY, + upn TEXT, + style INTEGER, + landuse TEXT, + zone_code TEXT, + zone_name TEXT, + sector TEXT, + block TEXT, + parcel_no TEXT, + prop_no TEXT, + st_name TEXT, + prop_add TEXT, + fac_name TEXT, + min_height INTEGER, + max_height INTEGER, + eff_date TEXT, + lp_name TEXT, + locality TEXT, + mmda TEXT, + last_update TEXT, + remarks TEXT, + geometry_wkt TEXT, + created_at TEXT, + updated_at TEXT, + districtid INTEGER, + status TEXT DEFAULT 'verified', + fetched_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + `,console.log("[Database] Creating building_footprints table..."),await E` + 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 E` + 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 E` + 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 E` + 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 E`CREATE INDEX IF NOT EXISTS idx_locations_category ON locations(category)`,await E`CREATE INDEX IF NOT EXISTS idx_locations_synced ON locations(synced)`,await E`CREATE INDEX IF NOT EXISTS idx_gps_trails_synced ON gps_trails(synced, status)`,await E`CREATE INDEX IF NOT EXISTS idx_gps_trail_points_trail ON gps_trail_points(trail_id, seq)`;const t=await E`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;console.log("[Database] All tables:",t.map(n=>n.name)),Oo=!0,Ro(!0),console.log("[Database] ✓ Schema initialized")}catch(r){throw console.error("[Database] ✗ Schema init failed:",r),$o(r),r}}async function qn(r,e,t,n={}){const{description:o=null,category:a="default"}=n;console.log("[Database] Adding location:",r,e,t,a);try{const s=await E`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 E` + INSERT INTO locations (name, longitude, latitude, description, category) + VALUES (${r}, ${e}, ${t}, ${o}, ${a}) + `,console.log("[Database] INSERT completed");const l=(await E`SELECT last_insert_rowid() as id`)[0]?.id;console.log("[Database] New ID:",l);const c=await E`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 E` + INSERT INTO sync_log (table_name, record_id, action) + VALUES ('locations', ${l}, 'INSERT') + `,Te("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 No(r={}){const{category:e=null,limit:t=1e3}=r;try{const n=await E`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 E` + SELECT * FROM locations + WHERE category = ${e} + ORDER BY created_at DESC + LIMIT ${t} + `:o=await E` + 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 zn(){try{return(await E`SELECT COUNT(*) as count FROM locations`)[0]?.count??0}catch(r){return console.error("[Database] getLocationCount error:",r),0}}async function Bo(r,e){try{const t=JSON.stringify(e);await E` + 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 Go(r){try{const e=await E`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 jn(r){try{await E`DELETE FROM collector_zones`;for(const e of r){const t=JSON.stringify(e);await E` + 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 Un(){try{const r=await E`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}}function Y(r){if(r===""||r===null||r===void 0)return null;const e=Number(r);return Number.isNaN(e)?null:e}async function Hn(r){try{await E`BEGIN`,await E`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=t.boundary||t.geometry_wkt||t.polygon||t.wkt||(typeof t.geom=="string"?t.geom:"");await E` + INSERT OR REPLACE INTO parcels ( + id, upn, style, landuse, zone_code, zone_name, sector, block, parcel_no, + prop_no, st_name, prop_add, fac_name, min_height, max_height, eff_date, + lp_name, locality, mmda, last_update, remarks, geometry_wkt, + created_at, updated_at, districtid, status, fetched_at + ) VALUES ( + ${n}, ${t.upn??null}, ${Y(t.style)}, ${t.landuse??null}, + ${t.zone_code??null}, ${t.zone_name??null}, ${t.sector??null}, + ${t.block??null}, ${t.parcel_no??null}, ${t.prop_no??null}, + ${t.st_name??null}, ${t.prop_add??null}, ${t.fac_name??null}, + ${Y(t.min_height)}, ${Y(t.max_height)}, ${t.eff_date??null}, + ${t.lp_name??null}, ${t.locality??null}, ${t.mmda??null}, + ${t.last_update??null}, ${t.remarks??null}, ${o}, + ${t.created_at??null}, ${t.updated_at??null}, ${Y(t.districtid)}, + 'verified', CURRENT_TIMESTAMP + ) + `,e++}await E`COMMIT`,console.log("[Database] ✓ Saved",e,"parcels (from",r.length,"rows,",r.length-e,"skipped/replaced)")}catch(e){try{await E`ROLLBACK`}catch{}throw console.error("[Database] ✗ Failed to save parcels:",e),e}}async function Wn(){try{const r=await E`SELECT * FROM parcels ORDER BY id`;return r.length===0?null:r}catch(r){return console.error("[Database] ✗ Failed to read local parcels:",r),null}}async function Kn(r,e){try{await E` + UPDATE parcels SET + upn = ${e.upn??null}, + style = ${Y(e.style)}, + landuse = ${e.landuse??null}, + zone_code = ${e.zone_code??null}, + zone_name = ${e.zone_name??null}, + sector = ${e.sector??null}, + block = ${e.block??null}, + parcel_no = ${e.parcel_no??null}, + prop_no = ${e.prop_no??null}, + st_name = ${e.st_name??null}, + prop_add = ${e.prop_add??null}, + fac_name = ${e.fac_name??null}, + min_height = ${Y(e.min_height)}, + max_height = ${Y(e.max_height)}, + eff_date = ${e.eff_date??null}, + lp_name = ${e.lp_name??null}, + locality = ${e.locality??null}, + mmda = ${e.mmda??null}, + last_update = ${e.last_update??null}, + remarks = ${e.remarks??null}, + districtid = ${Y(e.districtid)}, + updated_at = CURRENT_TIMESTAMP + WHERE id = ${r} + `,console.log("[Database] ✓ Parcel updated:",r),Te("parcels","UPDATE",r)}catch(t){throw console.error("[Database] ✗ Failed to update parcel:",r,t),t}}async function Vn(r,e={}){try{await E` + INSERT INTO parcels ( + id, upn, style, landuse, zone_code, zone_name, sector, block, parcel_no, + prop_no, st_name, prop_add, fac_name, min_height, max_height, eff_date, + lp_name, locality, mmda, last_update, remarks, geometry_wkt, + created_at, updated_at, districtid, status, fetched_at + ) VALUES ( + NULL, ${e.upn??null}, ${Y(e.style)}, ${e.landuse??null}, + ${e.zone_code??null}, ${e.zone_name??null}, ${e.sector??null}, + ${e.block??null}, ${e.parcel_no??null}, ${e.prop_no??null}, + ${e.st_name??null}, ${e.prop_add??null}, ${e.fac_name??null}, + ${Y(e.min_height)}, ${Y(e.max_height)}, ${e.eff_date??null}, + ${e.lp_name??null}, ${e.locality??null}, ${e.mmda??null}, + ${e.last_update??null}, ${e.remarks??null}, ${r}, + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ${Y(e.districtid)}, + 'new', CURRENT_TIMESTAMP + ) + `;const n=(await E`SELECT last_insert_rowid() as id`)[0]?.id;return console.log("[Database] ✓ New parcel inserted:",n,"(status: new)"),Te("parcels","INSERT",n),{id:n}}catch(t){throw console.error("[Database] ✗ Failed to insert new parcel:",t),t}}async function Xn(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 E`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 a=e.id||e.footprint_id||e.building_id||null;await E` + INSERT INTO building_footprints (id, geometry_wkt, properties, fetched_at) + VALUES (${a!==null&&typeof a=="object"?null:a}, ${o}, ${t}, CURRENT_TIMESTAMP) + `}console.log("[Database] ✓ Saved",r.length,"building footprints")}catch(e){throw console.error("[Database] ✗ Failed to save building footprints:",e),e}}async function Yn(){try{const r=await E`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 Zn(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 E`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 a=e.osm_id??e.osmid??e.id??null;await E` + INSERT OR REPLACE INTO osm_roads (osm_id, geometry_wkt, properties, fetched_at) + VALUES (${a!==null&&typeof a=="object"?null:a}, ${o}, ${t}, CURRENT_TIMESTAMP) + `}console.log("[Database] ✓ Saved",r.length,"OSM roads")}catch(e){throw console.error("[Database] ✗ Failed to save OSM roads:",e),e}}async function Jn(){try{const r=await E`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 Qn(){return Do.getDatabaseFile()}async function er(r="lupmis-backup.sqlite3"){const e=await Qn(),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 tr(){return{type:"FeatureCollection",features:(await No()).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 jt(){try{const r=await E` + SELECT name FROM sqlite_master + WHERE type='table' AND name NOT LIKE 'sqlite_%' + ORDER BY name + `,e=await zn();return{ready:Oo,databasePath:zt,tables:r.map(t=>t.name),locationCount:e}}catch(r){return{ready:!1,error:r.message}}}const qo=Object.freeze(["parcels","building_footprints","osm_roads","collector_zones","remote_data"]);function zo(r){return qo.includes(r)}async function jo(r){if(!zo(r))throw new Error(`Refusing to clear "${r}" — not a known cached-layer table`);const t=(await E(`SELECT COUNT(*) AS n FROM "${r}"`))[0]?.n??0;return await E(`DELETE FROM "${r}"`),console.log(`[Database] ✓ Cleared "${r}" (${t} rows)`),Te(r,"CLEAR",null),t}async function or(){const r=await E` + 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 qo)if(e.has(o))try{const a=await jo(o);t.push({table:o,count:a})}catch(a){console.error(`[Database] Failed to clear ${o}:`,a),t.push({table:o,count:0,error:a.message})}const n=t.reduce((o,a)=>o+a.count,0);return console.log(`[Database] ✓ Cleared all cached layers: ${n} rows across ${t.length} tables`),t}async function nr(){const r=await E` + 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(e)}async function rr(r,e=200){if((await E` + SELECT name FROM sqlite_master + WHERE type='table' AND name = ${r} + `).length===0)throw new Error(`Table "${r}" does not exist`);const n=await E(`SELECT * FROM "${r}" LIMIT ${e}`);return{columns:n.length>0?Object.keys(n[0]):[],rows:n}}async function sr(){console.log("=== DATABASE TEST ===");try{const r=await E`SELECT sqlite_version() as v`;console.log("1. SQLite version:",r[0].v);const e=await E`SELECT name FROM sqlite_master WHERE type='table'`;console.log("2. Tables:",e.map(o=>o.name)),console.log("3. Inserting test row..."),await E`INSERT INTO locations (name, longitude, latitude, category) VALUES ('TEST', -1.0, 7.0, 'test')`;const t=await E`SELECT * FROM locations WHERE name = 'TEST'`;console.log("4. Test row:",t);const n=await E`SELECT COUNT(*) as c FROM locations`;return console.log("5. Total rows:",n[0].c),await E`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=sr,window.dbStatus=jt);async function ar(r){const{uuid:e,name:t=null,startedAt:n,districtId:o=null}=r;await E` + INSERT INTO gps_trails (client_uuid, name, district_id, started_at, status) + VALUES (${e}, ${t}, ${o}, ${n}, 'recording') + `;const s=(await E`SELECT last_insert_rowid() as id`)[0]?.id;return Te("gps_trails","insert",s),s}async function ir(r,e){const{seq:t,lon:n,lat:o,altitude:a=null,accuracy:s=null,altitudeAccuracy:i=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 E` + INSERT INTO gps_trail_points + (trail_id, seq, longitude, latitude, altitude, accuracy, altitude_accuracy, heading, speed, satellites, recorded_at) + VALUES + (${r}, ${t}, ${n}, ${o}, ${a}, ${s}, ${i}, ${l}, ${c}, ${d}, ${p}) + `}async function lr(r,e){const{endedAt:t,pointCount:n=0,distanceM:o=0}=e;await E` + UPDATE gps_trails + SET ended_at = ${t}, point_count = ${n}, distance_m = ${o}, status = 'completed' + WHERE id = ${r} + `,Te("gps_trails","update",r)}async function cr(){return E`SELECT * FROM gps_trails WHERE synced = 0 AND status = 'completed' ORDER BY started_at ASC`}async function dr(r){return E`SELECT * FROM gps_trail_points WHERE trail_id = ${r} ORDER BY seq ASC`}async function ur(r,e=null){await E`UPDATE gps_trails SET synced = 1, remote_id = ${e} WHERE id = ${r}`,Te("gps_trails","update",r)}const Uo=3.28084,Ho=621371e-9,Wo=10.7639,Ko=247105e-9,Vo=3861e-10;function ht(){return localStorage.getItem("measurement-system")||"metric"}function ut(r){if(ht()==="imperial"){const e=r*Uo;return e>=5280?Math.round(r*Ho*100)/100+" mi":Math.round(e)+" ft"}return r>1e3?Math.round(r/1e3*100)/100+" km":Math.round(r*100)/100+" m"}function pr(r){if(ht()==="imperial"){const e=r*Uo,t=r*Ho;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 He(r){if(ht()==="imperial"){const e=r*Ko;return e>=640?Math.round(r*Vo*100)/100+" mi²":e>=1?Math.round(e*100)/100+" acres":Math.round(r*Wo).toLocaleString("en")+" ft²"}return r>1e6?Math.round(r/1e6*100)/100+" km²":Math.round(r*100)/100+" m²"}function fr(r){if(ht()==="imperial"){const e=r*Wo,t=r*Ko,n=r*Vo;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 hr(r){return He(Math.PI*r*r)}function gr(r,e,t,n,o=1e-10){const a=e[0]-r[0],s=e[1]-r[1],i=n[0]-t[0],l=n[1]-t[1],c=a*l-s*i;if(Math.abs(c)1+o||h<-o||h>1+o?null:{point:[r[0]+p*a,r[1]+p*s],t:Math.max(0,Math.min(1,p)),u:Math.max(0,Math.min(1,h))}}function Xo(r){let e=0;for(let t=0,n=r.length;tr[1]!=l>r[1]&&r[0]<(i-a)*(r[1]-s)/(l-s)+a&&(t=!t)}return t}function Ve(r,e){return(r[0]-e[0])**2+(r[1]-e[1])**2}function mr(r,e){const t=[];for(let o=0;oo.lineSegIdx!==a.lineSegIdx?o.lineSegIdx-a.lineSegIdx:o.lineT-a.lineT),t}function yr(r,e){const t=e.map((a,s)=>({...a,origOrder:s}));t.sort((a,s)=>a.ringSegIdx!==s.ringSegIdx?a.ringSegIdx-s.ringSegIdx:a.ringT-s.ringT);const n=r.slice(),o=new Array(t.length);for(let a=t.length-1;a>=0;a--){const s=t[a],i=s.ringSegIdx+1,l=1e-6;if(Ve(s.point,n[s.ringSegIdx])=i&&o[t[c].origOrder]++}return{ring:n,indices:o}}function so(r,e,t){const n=r.length-1,o=(e%n+n)%n,a=(t%n+n)%n,s=[];let i=o;for(;s.push(r[i]),i!==a;)i=(i+1)%n;return s}function ao(r,e,t){const n=[e.point],o=e.lineSegIdx,a=t.lineSegIdx;for(let s=o+1;s<=a;s++)n.push(r[s]);return Ve(n[n.length-1],t.point)>1e-10&&n.push(t.point),n}function io(r,e){const t=Xo(r);return e&&t<0||!e&&t>0?r.slice().reverse():r}function lo(r){if(r.length<2)return r;const e=r[0],t=r[r.length-1];return Ve(e,t)>1e-10?[...r,e.slice()]:r}function br(r,e){let t=1/0,n=1/0,o=-1/0,a=-1/0;for(const c of e)c[0]o&&(o=c[0]),c[1]>a&&(a=c[1]);const s=Math.sqrt((o-t)**2+(a-n)**2)||1,i=r.slice();if(Ot(i[0],e)){const c=i[0],d=i[1],u=c[0]-d[0],p=c[1]-d[1],h=Math.sqrt(u*u+p*p)||1,f=s*2/h;i[0]=[c[0]+u*f,c[1]+p*f]}const l=i.length-1;if(Ot(i[l],e)){const c=i[l],d=i[l-1],u=c[0]-d[0],p=c[1]-d[1],h=Math.sqrt(u*u+p*p)||1,f=s*2/h;i[l]=[c[0]+u*f,c[1]+p*f]}return i}function nt(r,e){const t=r[0],n=r.slice(1),o=br(e,t),a=mr(t,o);if(a.length!==2)return console.warn(`[polygonSplit] Expected 2 intersections, got ${a.length}`),null;const[s,i]=a,{ring:l,indices:c}=yr(t,a),d=c[0],u=c[1],[p,h]=d0,x=io(m,S),v=io(w,S),L=[x],P=[v];for(const T of n){const z=wr(T);Ot(z,x)?L.push(T):P.push(T)}return[L,P]}function wr(r){let e=0,t=0;const n=r.length-1;for(let o=0;o{a.style.opacity="1",a.style.transform="translateY(0)"});const s=()=>{a.style.opacity="0",a.style.transform="translateY(-8px)",setTimeout(()=>a.remove(),300)};a.addEventListener("click",s),setTimeout(s,t)}const Je=[{stroke:"#ef4444",fill:"rgba(239,68,68,0.25)"},{stroke:"#3b82f6",fill:"rgba(59,130,246,0.25)"}],_r=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:"#f43f5e",width:2,lineDash:[8,6]}),image:new ae({radius:5,fill:new I({color:"#f43f5e"}),stroke:new k({color:"#fff",width:1.5})})});class xr extends Bt{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 R({useSpatialIndex:!1}),this._overlayLayer=new A({source:this._overlaySource,displayInLayerSwitcher:!1,style:_r})}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 R?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 a=o.getClosestFeatureToCoordinate(e.coordinate);if(!a)continue;const s=a.getGeometry();if(!s)continue;const i=s.getType();if(i!=="Polygon"&&i!=="MultiPolygon")continue;const l=s.getClosestPoint(e.coordinate),d=new ie([e.coordinate,l]).getLength()/e.frameState.viewState.resolution;d{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 a;o.getType()==="Polygon"?a=o.getCoordinates():o.getType()==="MultiPolygon"&&(a=o.getCoordinates()[0]);const s=nt(a,e);if(!s){console.warn("[PolygonSplit] Split failed — line must cross the polygon boundary at exactly 2 points."),this._removeDrawInteraction(),this._startDrawPhase();return}const[i,l]=s,c=t.clone();c.setGeometry(new Ke(i)),c.setStyle(new M({stroke:new k({color:Je[0].stroke,width:2.5}),fill:new I({color:Je[0].fill})}));const d=t.clone();d.setGeometry(new Ke(l)),d.setStyle(new M({stroke:new k({color:Je[1].stroke,width:2.5}),fill:new I({color:Je[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 h=this.getMap();h&&(h.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 a=o.getGeometry();if(!a)continue;const s=a.getClosestPoint(e.coordinate),l=new ie([e.coordinate,s]).getLength()/e.frameState.viewState.resolution;lr[1]!=l>r[1]&&r[0]<(i-a)*(r[1]-s)/(l-s)+a&&(t=!t)}return t}function Lr(r,e){const t=ze(r);return e&&t<0||!e&&t>0?r.slice().reverse():r}function Tr(r){return r.length<2?r:Fe(r[0],r[r.length-1])>1e-10?[...r,r[0].slice()]:r}function De(r,e,t){const n=t[0]-e[0],o=t[1]-e[1],a=n*n+o*o;if(a<1e-20)return Fe(r,e);let s=((r[0]-e[0])*n+(r[1]-e[1])*o)/a;s=Math.max(0,Math.min(1,s));const i=e[0]+s*n,l=e[1]+s*o;return(r[0]-i)**2+(r[1]-l)**2}function uo(r,e){let t=0,n=1/0;const o=r.length-1;for(let a=0;a0;){const v=(y+1)%a,L=g?(S-1+s)%s:(S+1)%s;if(v===m||L===w)break;if(Me(r[v],e[L],i)){y=v,S=L;continue}if(De(r[v],e[S],e[L])0;){const v=(m-1+a)%a,L=g?(w+1)%s:(w-1+s)%s;if(v===y||L===S)break;if(Me(r[v],e[L],i)){m=v,w=L;continue}if(De(r[v],e[w],e[L])n+1)););return o}function Pr(r,e,t,n,o=5){const a=r[0],s=e[0],i=r.slice(1),l=e.slice(1),c=uo(a,t),d=uo(s,n),u=kr(a,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:h,startB:f,endB:b,reversed:g}=u;a.length-1,s.length-1;const m=St(a,h,p);let y;g?y=St(s,f,b):y=St(s,b,f);const w=[...m,...y.slice(1)],S=o*o;w.length>2&&Fe(w[w.length-1],w[0])T*1.5)return console.warn(`[polygonMerge] Area mismatch: A=${v.toFixed(1)}, B=${L.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 z=ze(a)>0,U=Lr(x,z),D=[...i,...l].filter(ne=>{const V=ne.reduce((G,ue)=>G+ue[0],0)/(ne.length-1),de=ne.reduce((G,ue)=>G+ue[1],0)/(ne.length-1);return Sr([V,de],U)});return{coords:[U,...D]}}const po=new M({stroke:new k({color:"#0ea5e9",width:3}),fill:new I({color:"rgba(14,165,233,0.15)"})}),Mr=new M({stroke:new k({color:"#f59e0b",width:3}),fill:new I({color:"rgba(245,158,11,0.15)"})}),Ir=new M({stroke:new k({color:"#0ea5e9",width:3}),fill:new I({color:"rgba(14,165,233,0.15)"}),text:new rt({text:"A",font:"bold 22px Exo, sans-serif",fill:new I({color:"#0ea5e9"}),stroke:new k({color:"#fff",width:4}),overflow:!0})}),Cr=new M({stroke:new k({color:"#f59e0b",width:3}),fill:new I({color:"rgba(245,158,11,0.15)"}),text:new rt({text:"B",font:"bold 22px Exo, sans-serif",fill:new I({color:"#f59e0b"}),stroke:new k({color:"#fff",width:4}),overflow:!0})}),Ar=new M({stroke:new k({color:"#ec4899",width:4,lineDash:[10,6]})}),Dr=new M({stroke:new k({color:"#10b981",width:2.5}),fill:new I({color:"rgba(16,185,129,0.3)"})});class Fr extends Bt{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 R({useSpatialIndex:!1}),this._highlightLayer=new A({source:this._highlightSource,displayInLayerSwitcher:!1,style:t=>t.get("_highlightStyle")||po}),this._edgeSource=new R({useSpatialIndex:!1}),this._edgeLayer=new A({source:this._edgeSource,displayInLayerSwitcher:!1,style:Ar})}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 R?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 a=this._phase==="select_a"?po:Mr,s=o.feature.clone();s.set("_highlightStyle",a),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 a of this._getSources()){const s=a.getClosestFeatureToCoordinate(e.coordinate);if(!s||t&&s===t)continue;const i=s.getGeometry();if(!i)continue;const l=i.getType();if(l!=="Polygon"&&l!=="MultiPolygon")continue;const c=i.getClosestPoint(e.coordinate),u=new ie([e.coordinate,c]).getLength()/e.frameState.viewState.resolution;u{t.get("_permanent")&&e.push(t)}),e.forEach(t=>this._highlightSource.removeFeature(t)),this._featureA){const t=this._featureA.clone();t.set("_highlightStyle",Ir),t.set("_permanent",!0),this._highlightSource.addFeature(t)}if(this._featureB){const t=this._featureB.clone();t.set("_highlightStyle",Cr),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 Or(r,e){return(r[0]-e[0])**2+(r[1]-e[1])**2}function fo(r){let e=0;for(let t=0,n=r.length;tt&&(t=d,n=c)}const o=r[n],a=r[n+1],s=Math.sqrt(t),i=[(a[0]-o[0])/s,(a[1]-o[1])/s],l=[-i[1],i[0]];return{p0:o,p1:a,along:i,perp:l}}function Lt(r,e,t,n,o){const a=r[0]+n*e[0],s=r[1]+n*e[1];return[[a-o*t[0],s-o*t[1]],[a+o*t[0],s+o*t[1]]]}function Ie(r,e,t){const n=r[0],o=n.length-1;let a=0,s=0;for(let c=0;cu&&(u=w)}const p=(u-d)*1.5,h=[];let f=r,b=e;for(let g=0;gv&&(v=V)}let L=x,P=v,T=null,z=null,U=1/0;for(let K=0;K<40;K++){const D=(L+P)/2,ne=Lt(l,s,i,D,p),V=nt(f,ne);if(!V){const C=(P-L)*.01,q=Lt(l,s,i,D+C,p),Q=nt(f,q);if(Q){const[be,we]=Q,Re=Ie(be,l,s),$e=Ie(we,l,s),Ne=Re<$e?be:we,wt=Re<$e?we:be,vt=Ge(Ne),Be=Math.abs(vt-y);Bethis._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 R({useSpatialIndex:!1}),this._overlayLayer=new A({source:this._overlaySource,displayInLayerSwitcher:!1,style:Nr}),this._edgeSource=new R({useSpatialIndex:!1}),this._edgeLayer=new A({source:this._edgeSource,displayInLayerSwitcher:!1,style:Br})}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 R?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 a=o.getClosestFeatureToCoordinate(e.coordinate);if(!a)continue;const s=a.getGeometry();if(!s)continue;const i=s.getType();if(i!=="Polygon"&&i!=="MultiPolygon")continue;const l=s.getClosestPoint(e.coordinate),d=new ie([e.coordinate,l]).getLength()/e.frameState.viewState.resolution;d{const f=t.clone();return f.setGeometry(new Ke(p)),f.setStyle(new M({stroke:new k({color:i[h].stroke,width:2.5}),fill:new I({color:i[h].fill})})),f}),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 a=o.getGeometry();if(!a)continue;const s=a.getClosestPoint(e.coordinate),l=new ie([e.coordinate,s]).getLength()/e.frameState.viewState.resolution;l{const l=this.categoryEmojis[i];return l?l.emoji:"📍"},this.getCategoryOptionsHtml=()=>Object.entries(this.categoryEmojis).map(([i,{emoji:l,label:c}])=>``).join(` + `),this.createEmojiStyle=(i,l=24)=>new M({text:new rt({text:i,font:`${l}px sans-serif`,textBaseline:"bottom",textAlign:"center",offsetY:-5})}),this.defaultStyle=this.createEmojiStyle("📍",32),this.selectedStyle=this.createEmojiStyle("📍",42),this.categoryStyles={};for(const[i,{emoji:l}]of Object.entries(this.categoryEmojis))this.categoryStyles[i]=this.createEmojiStyle(l,32);const n=this.createBaseLayers(t.basemap||"topo");this.markersLayer=new A({title:"Markers",source:this.markerSource,style:i=>this.getFeatureStyle(i),visible:!1}),this.overlayGroup=new ve({title:"Overlays"}),this.map=new Kt({target:e,layers:[n,this.markersLayer,this.overlayGroup],view:new cn({center:X(t.center||[0,0]),zoom:t.zoom||2,minZoom:t.minZoom||2,maxZoom:t.maxZoom||19})});const o=new bn({collapsed:!0,mouseover:!0,extent:!0,trash:!1,oninfo:null});this.map.addControl(o),queueMicrotask(()=>{const i=o.element?.querySelector(":scope > button");if(i){const l="/".replace(/\/?$/,"/");i.style.backgroundImage=`url('${l}app-icons/luspa-72x72.png')`}});let a=!1;o.on("drawlist",i=>{this._decorateLayerListItem(i.layer,i.li),a||(a=!0,queueMicrotask(()=>{a=!1,this._refreshLayerSwitcherChrome(o)}))}),this.map.getLayers().on("change",()=>{this._refreshLayerSwitcherChrome(o)}),this._wireLayerSwitcherVisibilityHooks(o),this._createAddLayerDialog(),this._createLegendPanel(),this.scaleBar=new dn({bar:!0,steps:4,text:!0,minWidth:140}),this.map.addControl(this.scaleBar),this._initGpsRendering(),this._createLocationControl(),this._createBaseMapPicker();const s=new wn({placeholder:"Search location...",typing:300,minLength:3,maxItems:10,collapsed:!0});this.map.addControl(s),s.on("select",i=>{const l=i.search;if(l){const c=parseFloat(l.lon),d=parseFloat(l.lat),u=[c,d],p=X(u);this.navigateTo(c,d,14);const h={coordinate:p,lonLat:u,name:l.display_name||l.name||"Unknown",searchResult:l};this.searchSelectCallbacks.forEach(f=>f(h))}}),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 R,this.drawingsLayer=new A({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 ae({radius:6,fill:new I({color:"#f59e0b"}),stroke:new k({color:"#fff",width:1.5})})})}),this._drawingsGroup=new ve({title:"Drawings",layers:[this.drawingsLayer]});const e=this.map.getLayers(),t=e.getArray().indexOf(this.overlayGroup);e.insertAt(t>=0?t:e.getLength(),this._drawingsGroup),this._selectInteraction=new un({condition:pn,filter:(f,b)=>!!b,layers:f=>f instanceof A}),this._selectInteraction.setActive(!1),this.map.addInteraction(this._selectInteraction),this._modifyInteraction=new vn({features:this._selectInteraction.getFeatures()}),this._modifyInteraction.setActive(!1),this._undoRedo=new _n,this.map.addInteraction(this._undoRedo),this.editBar=new Ct({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 Yt({group:!0,className:"ol-editbar-actions",controls:[new qe({html:'',className:"ol-undo",title:"Undo",handleClick:()=>{this._undoRedo.hasUndo()&&this._undoRedo.undo()}}),new qe({html:'',className:"ol-redo",title:"Redo",handleClick:()=>{this._undoRedo.hasRedo()&&this._undoRedo.redo()}}),new qe({html:'',className:"ol-save",title:"Save drawings",handleClick:()=>{this.dispatchEditEvent("save")}})]});this.editBar.addControl(n),this._lineSplitInteraction=new En,this._polygonSplitInteraction=new xr,this.map.addInteraction(this._lineSplitInteraction),this.map.addInteraction(this._polygonSplitInteraction),this._lineSplitInteraction.setActive(!1),this._polygonSplitInteraction.setActive(!1),this._polygonSplitInteraction.on("splitpick",f=>{const b=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"];for(const g of f.features)if(g!==f.picked)for(const m of b)g.get(m)!==void 0&&g.set(m,"")}),this._polygonDivideInteraction=new qr,this.map.addInteraction(this._polygonDivideInteraction),this._polygonDivideInteraction.setActive(!1);const o=new me({html:'',className:"ol-split-line",title:"Split Lines",name:"SplitLine",interaction:this._lineSplitInteraction,autoActivate:!0}),a=new me({html:'',className:"ol-split-polygon",title:"Split Polygons",name:"SplitPolygon",interaction:this._polygonSplitInteraction}),s=new me({html:'',className:"ol-split-divide",title:"Divide Polygon",name:"DividePolygon",interaction:this._polygonDivideInteraction}),i=new Yt({toggleOne:!0,autoDeactivate:!0,controls:[o,a,s]}),l=new me({className:"ol-split",title:"Split",name:"Split",bar:i,onToggle:f=>{f||(this._lineSplitInteraction.setActive(!1),this._polygonSplitInteraction.setActive(!1),this._polygonDivideInteraction.setActive(!1))}});this.editBar.addControl(l),this._polygonDivideInteraction.on("divideform",f=>{this.showDividePopup(f.feature,f.source,f.coordinate)}),this._polygonDivideInteraction.on("dividecancel",()=>{this.hideDividePopup()}),this._polygonDivideInteraction.on("dividepick",f=>{const b=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"];for(const g of f.features)if(g!==f.picked)for(const m of b)g.get(m)!==void 0&&g.set(m,"")}),this._polygonMergeInteraction=new Fr,this.map.addInteraction(this._polygonMergeInteraction),this._polygonMergeInteraction.setActive(!1);const c=new me({html:'',className:"ol-merge",title:"Merge Polygons",name:"Merge",interaction:this._polygonMergeInteraction});this.editBar.addControl(c),this._polygonMergeInteraction.on("mergedparcel",f=>{this.showMergeIdentifierPopup(f.merged,f.propsA,f.propsB,f.coordinate)});const d=this.editBar.element;if(d&&n.element&&n.element.parentNode===d){const f=document.createElement("div");f.className="ol-editbar-break",d.insertBefore(f,n.element)}this._snapGuidesEnabled=localStorage.getItem("snap-guides-enabled")==="1",this._snapGuides=new xn({pixelTolerance:10,vectorClass:fn}),this.map.addInteraction(this._snapGuides);const u=["DrawPoint","DrawLine","DrawPolygon","DrawHole","DrawRegular"];for(const f of u){const b=this.editBar.getInteraction(f);b&&b.on("change:active",()=>{b.getActive()&&this._snapGuides.setDrawInteraction(b)})}this._modifyInteraction&&this._snapGuides.setModifyInteraction(this._modifyInteraction);const p=new qe({html:'',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 f=this._drawingsGroup.getVisible();this.setEditMode(f)}),("ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0)&&(this.touchCursor=new Sn({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",f=>{const b=f.feature,g=b.getGeometry();if(!g||g.getType()!=="Polygon")return;const m=g.getInteriorPoint().getCoordinates();this.showDrawnPolygonPopup(b,m)}),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 R,this._vertexOverlayLayer=new A({title:"__vertex_highlight__",source:this._vertexOverlaySource,zIndex:990,style:new M({image:new ae({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 a=this._collectAllVertices(n);for(const s of a)this._vertexOverlaySource.addFeature(new se(new Xe(s)));t.on("change",this._onSelectedFeatureGeomChange),this._vertexTrackedFeatures.add(t)}}_collectAllVertices(e){const t=[],n=i=>Array.isArray(i)&&typeof i[0]=="number",o=(i,l)=>{const c=l&&i.length>1?i.length-1:i.length;for(let d=0;d{if(n(l))t.push(l);else if(Array.isArray(l))for(const c of l)i(c)};i(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 ge({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",a=e.get("description"),s=e.get("lon"),i=e.get("lat");let c=` +
              + ${this.getEmoji(o)} ${this.escapeHtml(n)} +
              + `;const u={water:"#3b82f6",school:"#f59e0b",health:"#ef4444",market:"#8b5cf6",default:"#2d5016",other:"#6b7280"}[o]||"#6b7280";c+=` +
              + ${o} +
              + `,a&&(c+=` +
              + ${this.escapeHtml(a)} +
              + `),s!==void 0&&i!==void 0&&(c+=` +
              + ${Number(s).toFixed(5)}, ${Number(i).toFixed(5)} +
              + `),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 ge({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:a="#e11d48"}=n,s=e.getProperties(),i=e.getGeometry(),l=i.getType(),c=["geometry","_layerType"];let d="";for(const[p,h]of Object.entries(s))c.includes(p)||h===void 0||h===null||(d+=` + + ${this.escapeHtml(p)} + ${this.escapeHtml(String(h))} + + `);if(l==="Polygon"||l==="MultiPolygon"){const p=Ue(i,{projection:"EPSG:3857"}),h=fr(p);d+=` + + area + ${h} + + `}else if(l==="LineString"||l==="MultiLineString"){const p=st(i,{projection:"EPSG:3857"}),h=pr(p);d+=` + + length + ${h} + + `}else if(l==="Point"){const p=xe(i.getCoordinates()),h=p[0].toFixed(6),f=p[1].toFixed(6);d+=` + + longitude + ${h} + + + latitude + ${f} + + `}const u=` +
              + ${this.escapeHtml(o)} + +
              +
              + + ${d} +
              +
              + `;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 a=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:a.map(s=>this.escapeHtml(s)).join(", "),color:"#7c3aed"})}for(const[a,s]of Object.entries(n))o.push({label:this.escapeHtml(a),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 a of n){if(a.empty){o+=` + + ${a.value} + `;continue}const s=a.color||"var(--muted-foreground, #7a7a7a)",i=a._first?"":"border-top:1px solid var(--border, #1e1a4b1f);";o+=` + + ${a.label} + ${a.value} + `}return` +
              + ${e} ${t} + +
              +
              + + ${o} +
              +
              +
              + +
              `}_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 a=n.filter(s=>!s.empty).map(s=>({label:s.label,value:s.value.replace(/<[^>]*>/g,"")}));ft(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:a})}).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=hn(n,64),a=o.getExtent(),s=e.get("_radius")||n.getRadius(),i=[],l=[],c={},d=g=>{const m=g.getGeometry();if(!m)return!1;const y=m.getExtent();return y[2]a[2]||y[3]a[3]?!1:o.intersectsExtent(y)&&this._geometriesIntersect(o,m)},u=(g,m)=>{g.getLayers().forEach(y=>{if(y instanceof ve)u(y,y.get("title")||m);else if(y instanceof A&&y.getVisible()){const w=y.get("title")||m||"Unknown",S=y.getSource();if(!S)return;const x=S.getFeaturesInExtent(a);for(const v of x){const L=v.get("_layerType");L==="measure_circle"||L==="measure_circle_radius"||d(v)&&(L==="parcel"?i.push(v):L==="collector_zone"?l.push(v):(c[w]||(c[w]=[]),c[w].push(v)))}}})};u(this.overlayGroup,"Overlays");const p=ut(s),h=Math.PI*s*s,f=He(h),b=[{label:"Radius",value:p,_first:!0},{label:"Area",value:f},...this._collectIntersectionRows(i,l,c)];this._showAnalysisPopup("⭕","Circle Analysis",b,t)}showAreaIntersectionPopup(e,t){const n=e.getGeometry();if(!n)return;const o=n.getExtent(),a=Ue(n,{projection:"EPSG:3857"}),s=He(a),i=st(n,{projection:"EPSG:3857"}),l=ut(i),c=[],d=[],u={},p=b=>{const g=b.getGeometry();if(!g)return!1;const m=g.getExtent();return m[2]o[2]||m[3]o[3]?!1:n.intersectsExtent(m)&&this._geometriesIntersect(n,g)},h=(b,g)=>{b.getLayers().forEach(m=>{if(m instanceof ve)h(m,m.get("title")||g);else if(m instanceof A&&m.getVisible()){const y=m.get("title")||g||"Unknown",w=m.getSource();if(!w)return;const S=w.getFeaturesInExtent(o);for(const x of S){const v=x.get("_layerType");v==="measure_area"||v==="measure_circle"||v==="measure_circle_radius"||p(x)&&(v==="parcel"?c.push(x):v==="collector_zone"?d.push(x):(u[y]||(u[y]=[]),u[y].push(x)))}}})};h(this.overlayGroup,"Overlays");const f=[{label:"Area",value:s,_first:!0},{label:"Perimeter",value:l},...this._collectIntersectionRows(c,d,u)];this._showAnalysisPopup("📐","Area Analysis",f,t)}_geometriesIntersect(e,t){const n=t.getType();if(n==="Polygon"||n==="MultiPolygon"){const o=t.getFlatCoordinates(),a=t.getStride();for(let l=0;l + + + + `}const s=` +
              + ✏️ Edit Parcel + +
              +
              + ${a} +
              + + +
              +
              + `;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 i=this.parcelEditElement.querySelector(".parcel-edit-form");i.addEventListener("submit",l=>{l.preventDefault();const c=new FormData(i),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 ge({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 a=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"],s=f=>{for(const b of a)if(f[b]!==void 0&&f[b]!==null&&String(f[b]).trim())return{field:b,value:String(f[b])};return{field:"id",value:"Unknown"}},i=s(t),l=s(n),c=` +
              + 🔗 Merged Parcel — Choose Identifier + +
              +
              +

              + Select which parcel's attributes the merged polygon should keep: +

              + + +
              + + +
              +
              + `;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 b=this.mergePopupElement.querySelector('input[name="merge-choice"]:checked').value==="A"?t:n,g=["geometry"];for(const[m,y]of Object.entries(b))g.includes(m)||e.set(m,y);e.set("_layerType","parcel");for(const m of this._parcelEditCallbacks)m(e,b);d()});const u=this.mergePopupElement.querySelectorAll("label"),p=this.mergePopupElement.querySelectorAll('input[name="merge-choice"]'),h=()=>{u.forEach(f=>{const b=f.querySelector("input");f.style.borderColor=b.checked?b.value==="A"?"#0ea5e9":"#f59e0b":"var(--border, #1e1a4b1f)"})};p.forEach(f=>f.addEventListener("change",h)),h()}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 ge({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=` +
              + Divide Polygon + +
              +
              +

              + Enter the number of equal pieces: +

              + +
              + + +
              +
              + `;this.dividePopupElement.innerHTML=o,this.dividePopup.setPosition(n);const a=this.dividePopupElement.querySelector(".divide-input");a.focus(),a.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 i=parseInt(a.value,10);if(!i||i<2){a.style.borderColor="#ef4444";return}this.hideDividePopup(),this._polygonDivideInteraction.performDivide(i)}),a.addEventListener("keydown",i=>{i.key==="Enter"&&(i.preventDefault(),this.dividePopupElement.querySelector(".divide-popup-confirm").click())})}hideDividePopup(){this.dividePopup.setPosition(void 0)}createDrawnPolygonPopup(){this.drawnPolygonElement=document.createElement("div"),this.drawnPolygonElement.className="map-drawn-polygon-popup",this.drawnPolygonElement.style.cssText=` + position: absolute; + background: var(--card, #fff); + border-radius: var(--radius-xl, 0.75rem); + box-shadow: 0 4px 20px rgba(0,0,0,0.2); + font-family: var(--font-body, 'Exo', sans-serif); + font-size: 13px; + min-width: 280px; + max-width: 360px; + max-height: 420px; + z-index: 1002; + border: 2px solid var(--success, #006b3f); + overflow: hidden; + display: flex; + flex-direction: column; + `,this.drawnPolygonPopup=new ge({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(a=>{if(!(t.length>0)){if(a instanceof ve)n(a);else if(a instanceof A){const s=a.getSource();if(!s)return;for(const i of s.getFeatures()){if(i.get("_layerType")!=="parcel")continue;const l=i.getProperties();for(const c of Object.keys(l))e.includes(c)||t.push(c);return}}}})};return 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+=` +
              + + +
              + `}const a=e.getGeometry(),s=Ue(a,{projection:"EPSG:3857"}),l=` +
              + 📐 Polygon Attributes + +
              +
              + Area: ${He(s)} +
              +
              + ${o} +
              + + +
              +
              + `;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[h,f]of u.entries())p[h]=f;for(const[h,f]of Object.entries(p))this._drawnPolygonFeature.set(h,f);this._drawnPolygonFeature.set("_layerType","parcel");for(const h of this._drawnPolygonCallbacks)h(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]=xe(t.coordinate);let a=null;this.map.forEachFeatureAtPixel(t.pixel,s=>(a=s,!0)),a&&(t.preventDefault(),t.stopPropagation());for(const s of this.dblClickCallbacks)s(n,o,a,t);if(a)return!1}),()=>{const t=this.dblClickCallbacks.indexOf(e);t>-1&&this.dblClickCallbacks.splice(t,1)}}escapeHtml(e){if(!e)return"";const t=document.createElement("div");return t.textContent=e,t.innerHTML}createAddLocationPopup(){this.addLocationPopupElement=document.createElement("div"),this.addLocationPopupElement.className="map-add-location-popup",this.addLocationPopupElement.innerHTML=` +
              + ➕ Add Location + +
              +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              + 📍 +
              + +
              + `,this.addLocationPopup=new ge({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]=xe(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),a={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(a)),this.hideAddLocationPopup()})}}createBaseLayers(e){const t=new ee({title:"Topographic",type:"base",zIndex:-100,visible:e==="topo",source:new _e({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 ee({title:"Carto Light",type:"base",zIndex:-100,visible:e==="carto-light",source:new _e({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 ee({title:"Carto Dark",type:"base",zIndex:-100,visible:e==="carto-dark",source:new _e({url:"https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});o.set("basemapKey","carto-dark");const a=new ee({title:"OSM Cycle map",type:"base",zIndex:-100,visible:!1,source:new Vt({url:"https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=ae1339c46dd3446b9c491e7336d38760"})});a.set("basemapKey","cycle");const s=new ee({title:"Satellite",type:"base",zIndex:-100,visible:e==="satellite",source:new _e({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 i=new ee({title:"Google Sat",type:"base",zIndex:-100,visible:e==="googlesat",source:new _e({url:"http://mt0.google.com/vt/lyrs=y&hl=en&x={x}&y={y}&z={z}&s=Ga",attributions:"Tiles © Google",maxZoom:19,crossOrigin:"anonymous"})});i.set("basemapKey","googlesat");const l=new ee({title:"OpenStreetMap",type:"base",zIndex:-100,visible:e==="osm",source:new Vt});l.set("basemapKey","osm"),this._baseMapLayers=[n,o,a,s,i,l,t];const c=new ve({title:"Base Maps",layers:[n,o,s,a,i,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='',t.appendChild(n);const o=document.createElement("div");o.className="ls-basemap-panel",o.innerHTML='
              Base Map
              '+e.map(s=>` + + `).join("")+"
              ",t.appendChild(o),this._basemapPanel=o,this._basemapToggle=n;const a=s=>{const i=s||this._baseMapLayers?.find(l=>l.getVisible())?.get("basemapKey");o.querySelectorAll('input[name="lupmis-basemap"]').forEach(l=>{l.checked=l.value===i})};a(),n.addEventListener("click",s=>{s.stopPropagation();const i=!o.classList.contains("open");o.classList.toggle("open",i),n.classList.toggle("active",i),i&&a()}),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 i=s.target.closest('input[type=radio][name="lupmis-basemap"]');if(!i)return;const l=i.value;this.setBaseMap(l);try{localStorage.setItem("default-basemap",l)}catch{}o.classList.remove("open"),n.classList.remove("active")}),this.map.on("basemapchange",s=>a(s.key))}_initGpsRendering(){this._gpsPositionSource=new R,this._gpsTrailSource=new R,this._gpsTrailCoords=[],this._gpsTrailLayer=new A({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 A({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 ae({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=X([e,t]);if(this._gpsPositionSource.clear(),n&&n>0){const s=n/Math.cos(t*Math.PI/180),i=new se({geometry:new Ke([this._circleRing(o,s)])});i.set("_kind","accuracy"),this._gpsPositionSource.addFeature(i)}const a=new se({geometry:new Xe(o)});a.set("_kind","dot"),this._gpsPositionSource.addFeature(a)}_circleRing(e,t,n=48){const o=[],s=t/1;for(let i=0;i<=n;i++){const l=i/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:X([e,t]),zoom:n,duration:500})}startTrailRender(){this._gpsTrailCoords=[],this._gpsTrailSource.clear()}appendTrailPoint(e,t){e==null||t==null||(this._gpsTrailCoords.push(X([e,t])),this._gpsTrailSource.clear(),this._gpsTrailCoords.length>=2&&this._gpsTrailSource.addFeature(new se({geometry:new ie(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?'':''),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='',e.appendChild(t);const n=document.createElement("div");n.className="ls-locate-actions",n.innerHTML='',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")},a=()=>{n.classList.add("open"),t.classList.add("active")};t.addEventListener("click",s=>{s.stopPropagation(),n.classList.contains("open")?o():a()}),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 i of this._gpsCallbacks.locate)try{i()}catch(l){console.error(l)}this._gpsRecording||o()}),this._recordBtn.addEventListener("click",s=>{s.stopPropagation();const i=!this._gpsRecording;for(const l of this._gpsCallbacks.record)try{l(i)}catch(c){console.error(c)}})}getFeatureStyle(e){const t=e.get("category")||"default",n=this.getEmoji(t);if(e===this.selectedFeature)return[new M({image:new ae({radius:22,fill:new I({color:"rgba(220, 38, 38, 0.25)"}),stroke:new k({color:"#dc2626",width:3})})}),new M({text:new rt({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),a=n.fontSize||28;this.categoryStyles[t]=this.createEmojiStyle(o,a)}this.markerSource.changed()}addMarker(e,t,n={}){console.log("[MapView] Adding marker at",e,t,"with properties:",n);const o=new se({geometry:new Xe(X([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 se({geometry:new Xe(X([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:X([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 xe(e)}getZoom(){return this.map.getView().getZoom()}setCenter(e,t){this.map.getView().setCenter(X([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,a=null;if(this.map.forEachFeatureAtPixel(t.pixel,l=>{l.get("_layerType")==="parcel"&&(o=!0),l.get("name")&&(a=l),n=!0}),n&&!o&&!a)return;const[s,i]=xe(t.coordinate);this._clickTimer=setTimeout(()=>{this._clickTimer=null;let l=null;this.map.forEachFeatureAtPixel(t.pixel,c=>{if(c.get("name"))return l=c,!0});for(const c of this.clickCallbacks)c(s,i,l,t)},300)})),()=>{const t=this.clickCallbacks.indexOf(e);t>-1&&this.clickCallbacks.splice(t,1)}}onPointerMove(e){this.map.on("pointermove",t=>{if(t.dragging)return;const[n,o]=xe(t.coordinate);let a=null;this.map.forEachFeatureAtPixel(t.pixel,s=>{if(s.get("name"))return a=s,!0}),this.map.getTargetElement().style.cursor=a?"pointer":"",e(n,o,a,t)})}enableHoverCursor(){}addGeoJSONLayer(e,t,n={},o=null){const{strokeColor:a="#3b82f6",strokeWidth:s=2,fillColor:i="rgba(59,130,246,0.1)",lineCasingColor:l=null,lineCasingWidth:c=null,pointRadius:d=5,pointFillColor:u=null,pointStrokeColor:p="#ffffff",pointStrokeWidth:h=1.5}=n,f=new R({features:new ce().readFeatures(e,{featureProjection:"EPSG:3857"})}),b=new I({color:i}),g=new ae({radius:d,fill:new I({color:u||a}),stroke:new k({color:p,width:h})});let m;if(l){const x=c??s+2;m=[new M({stroke:new k({color:l,width:x})}),new M({stroke:new k({color:a,width:s}),fill:b,image:g})]}else m=new M({stroke:new k({color:a,width:s}),fill:b,image:g});const y=new A({title:t,source:f,style:m});y.set("typeTag",n.typeTag||"VEC");const w=x=>x?x.includes("Polygon")?"Vector / Polygon":x.includes("LineString")?"Vector / Line":x.includes("Point")?"Vector / Point":"Vector":null;if(n.typeDescription)y.set("typeDescription",n.typeDescription);else{const x=f.getFeatures(),v=w(x[0]?.getGeometry?.()?.getType?.());if(v)y.set("typeDescription",v);else{const L=P=>{const T=w(P.feature.getGeometry?.()?.getType?.());T&&y.set("typeDescription",T),f.un("addfeature",L)};f.on("addfeature",L)}}return(o||this.overlayGroup).getLayers().push(y),console.log("[MapView] GeoJSON layer added:",t,"→",f.getFeatures().length,"features",o?`(in group "${o.get("title")}")`:""),y}addLayerGroup(e,t,n=""){const o=new ve({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,a={}){const s=this.getLayerGroupByTitle(e);if(!s)return console.warn(`[MapView] Layer group "${e}" not found — cannot add WMS layer "${t}"`),null;const i={LAYERS:o,TILED:!0,WIDTH:256,HEIGHT:256};a.style!==void 0&&(i.STYLES=a.style);const l=new Xt({url:n,params:i,serverType:a.serverType!==void 0?a.serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1,attributions:a.attributions}),c=new ee({title:t,visible:a.visible!==void 0?a.visible:!0,source:l,opacity:a.opacity!==void 0?a.opacity:1,zIndex:a.zIndex});if(c.set("typeTag","WMS"),c.set("typeDescription","WMS / Raster"),l.on("tileloaderror",()=>{O(`WMS layer "${t}" — tile load error. Check the URL and layer name.`,"warning",5e3)}),s.getLayers().push(c),a.legendUrl)try{this._registerLegend(c,t,a.legendUrl)}catch(d){console.warn(`[MapView] Could not register legend for "${t}":`,d)}return a.onlineOnly&&this._attachOnlineOnlyHandler(c,t),console.log(`[MapView] WMS layer added: "${t}" → group "${e}"`),c}addXYZLayer(e,t,n,o={}){const a=this.getLayerGroupByTitle(e);if(!a)return console.warn(`[MapView] Layer group "${e}" not found — cannot add XYZ layer "${t}"`),null;const s=new _e({url:n,crossOrigin:"anonymous",maxZoom:o.maxZoom!==void 0?o.maxZoom:19,attributions:o.attributions}),i=new ee({title:t,visible:o.visible!==void 0?o.visible:!0,source:s,opacity:o.opacity!==void 0?o.opacity:1,zIndex:o.zIndex});if(i.set("typeTag","XYZ"),i.set("typeDescription","XYZ / Tile"),s.on("tileloaderror",()=>{O(`XYZ layer "${t}" — tile load error. Check the URL.`,"warning",5e3)}),a.getLayers().push(i),o.legendUrl)try{this._registerLegend(i,t,o.legendUrl)}catch(l){console.warn(`[MapView] Could not register legend for "${t}":`,l)}return o.onlineOnly&&this._attachOnlineOnlyHandler(i,t),console.log(`[MapView] XYZ layer added: "${t}" → group "${e}"`),i}_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=` +
              + Add External Layer + +
              +
              +
              + +
              + + + +
              +
              +
              + + +
              +
              + + +
              + WMS LAYERS parameter (e.g. workspace:layer) +
              +
              +
              + + +
              +
              + + +
              +
              + `,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 i=s.value;i==="xyz"?(t.style.display="none",o.placeholder="https://example.com/tiles/{z}/{x}/{y}.png"):(t.style.display="",o.placeholder=i==="wms"?"https://example.com/wms":"https://example.com/wfs",n.textContent=i==="wms"?"WMS LAYERS parameter (e.g. workspace:layer)":"WFS typename (e.g. workspace:layer)")})});const a=()=>this._hideAddLayerDialog();e.querySelector(".add-layer-close").addEventListener("click",a),e.querySelector(".add-layer-cancel").addEventListener("click",a),this._addLayerDialog.addEventListener("click",s=>{s.target===this._addLayerDialog&&a()}),e.querySelector(".add-layer-confirm").addEventListener("click",()=>{const s=e.querySelector('input[name="add-layer-type"]:checked').value,i=e.querySelector(".add-layer-url").value.trim(),l=e.querySelector(".add-layer-name").value.trim(),c=e.querySelector(".add-layer-title").value.trim();if(!i){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,i,l,c),this._hideAddLayerDialog()}),e.addEventListener("keydown",s=>{s.key==="Enter"&&(s.preventDefault(),e.querySelector(".add-layer-confirm").click()),s.key==="Escape"&&(s.preventDefault(),a())})}showAddLayerDialog(){const e=this._addLayerDialog;e.querySelector(".add-layer-url").value="",e.querySelector(".add-layer-name").value="",e.querySelector(".add-layer-title").value="",e.querySelectorAll('input[name="add-layer-type"]')[0].checked=!0,e.querySelector(".add-layer-name-row").style.display="",e.querySelector(".add-layer-url").placeholder="https://example.com/wms",e.querySelector(".add-layer-name-hint").textContent="WMS LAYERS parameter (e.g. workspace:layer)",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 a=this._externalSourceGroup;if(!a){O('Layer group "External Source" not found.',"error",4e3);return}let s;switch(e){case"wms":{const i=new Xt({url:t,params:{LAYERS:n,TILED:!0,WIDTH:256,HEIGHT:256},serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1});s=new ee({title:o,visible:!0,source:i}),i.on("tileloaderror",()=>{O(`WMS "${o}" — tile load error. Check URL and layer name.`,"warning",5e3)});break}case"wfs":{const i=`${t}${t.includes("?")?"&":"?"}service=WFS&version=1.1.0&request=GetFeature&typename=${encodeURIComponent(n)}&outputFormat=application/json&srsname=EPSG:3857`,l=new R({url:i,format:new ce});l.on("featuresloaderror",()=>{O(`WFS "${o}" — load error. Check URL and layer name.`,"warning",5e3)}),s=new A({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 ee({title:o,visible:!0,source:new _e({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),a.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='')}const a=t.querySelector(":scope > .li-content"),s=()=>{if(!a)return;const l=e.get("typeDescription");let c=a.querySelector(":scope > .ls-layer-subtitle");if(!l){c&&c.remove();return}if(!c){c=document.createElement("div"),c.className="ls-layer-subtitle";const d=a.querySelector(":scope > label");d&&d.nextSibling?a.insertBefore(c,d.nextSibling):a.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='',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=a=>{const s=a.getLayers();if(s.getArray().includes(e))return s.remove(e),!0;let i=!1;return s.forEach(l=>{!i&&l.getLayers&&(i=n(l))}),i};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=` + Layers + 0 active + `,t.insertBefore(o,n));let a=t.querySelector(":scope > .ls-footer-row");a||(a=document.createElement("div"),a.className="ls-footer-row",a.innerHTML=` + — layers total + + `,t.appendChild(a),a.querySelector(".ls-footer-btn").addEventListener("click",i=>{i.stopPropagation(),this._resetAllOverlays()}));const s=this._countLayers();o.querySelector(".ls-active-badge-count").textContent=`${s.activeOverlays} active`,a.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=a=>{a.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=a=>{a._lsVisHooked||(a._lsVisHooked=!0,a.on("change:visible",t))},o=a=>{a.getLayers().forEach(s=>{s.getLayers?(o(s),a._lsAddHooked||(a._lsAddHooked=!0,a.getLayers().on("add",i=>{const l=i.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 Kt}_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=` +
              + ${this._escapeHtml(t)} +
              + ${this._escapeHtml(t)} legend + `,this._legendEntries.set(e,o);const a=()=>{try{this._updateLegendPanel()}catch(s){console.warn("[MapView] legend panel update failed:",s)}};e.on("change:visible",a),a()}_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,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}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 a=o.getSource&&o.getSource();if(a&&typeof a.getExtent=="function"){const s=a.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 a=X([e,t]);this.map.getView().animate({center:a,zoom:n,duration:o})}}class jr{constructor(e,t={}){this.map=e,this.options=t,this.measureSource=new R,this.measureLayer=new A({source:this.measureSource,style:this.getMeasureStyle(),title:"Measurements",zIndex:100}),this.drawSource=new R,this.drawLayer=new A({source:this.drawSource,style:this.getDrawStyle(),title:"Draw sketches",displayInLayerSwitcher:!1,zIndex:99});const n=this.map.getLayers();let o=n.getArray().findIndex(a=>a.get("title")==="Overlays");o<0&&(o=n.getLength()),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 ae({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 ae({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 ge({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 Ee({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 ae({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",a=>{const s=a.target;if(s instanceof gn){const i=s.getRadius(),l=hr(i),d=`${ut(i)}
              ${l}`;this.measureTooltipElement.innerHTML=d,this.measureTooltip.setPosition(s.getLastCoordinate())}})}),e.on("drawend",n=>{const o=n.feature,a=o.getGeometry(),s=a.getCenter(),i=a.getRadius();o.set("_layerType","measure_circle"),o.set("_radius",i),o.set("_center",s);const l=new se({geometry:new ie([s,[s[0]+i,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(),_t(t);const c={type:"circle",center:s,radius:i,area:Math.PI*i*i,feature:o};this.onMeasureCompleteCallbacks.forEach(d=>d(c))}),e}startLineMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new Ee({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",a=>{const s=a.target,i=st(s),l=ut(i);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(s.getLastCoordinate())})}),e.on("drawend",n=>{const o=n.feature,a=o.getGeometry(),s=st(a);this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),_t(t);const i={type:"line",length:s,feature:o};this.onMeasureCompleteCallbacks.forEach(l=>l(i))}),e}startAreaMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new Ee({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",a=>{const s=a.target,i=Ue(s),l=He(i);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(s.getInteriorPoint().getCoordinates())})}),e.on("drawend",n=>{const o=n.feature,a=o.getGeometry(),s=Ue(a);o.set("_layerType","measure_area"),o.set("_area",s),this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),_t(t);const i={type:"polygon",area:s,feature:o,coordinate:a.getInteriorPoint().getCoordinates()};this.onMeasureCompleteCallbacks.forEach(l=>l(i))}),e}startDrawPoint(){this.deactivate();const e=new Ee({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 Ee({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 Ee({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 Ct({group:!0,className:"map-tools-bar"}),n=new Ct({toggleOne:!0,group:!0}),o=new me({html:'',title:"Measure Circle (radius & area)",className:"measure-circle-btn",onToggle:l=>{l?this.startCircleMeasure():this.deactivate()}});n.addControl(o);const a=new me({html:'📏',title:"Measure Distance",className:"measure-line-btn",onToggle:l=>{l?this.startLineMeasure():this.deactivate()}});n.addControl(a);const s=new me({html:'',title:"Measure Area",className:"measure-area-btn",onToggle:l=>{l?this.startAreaMeasure():this.deactivate()}});n.addControl(s);const i=new qe({html:'🗑️',title:"Clear Measurements",className:"clear-measure-btn",handleClick:()=>{this.clearMeasurements(),o.setActive(!1),a.setActive(!1),s.setActive(!1)}});return n.addControl(i),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 Le=null;async function Ur(){if(!("serviceWorker"in navigator))return console.warn("[PWA] Service Workers not supported"),null;try{return Le=await navigator.serviceWorker.register("/sw.js",{scope:"/"}),console.log("[PWA] Service Worker registered:",Le.scope),Le.addEventListener("updatefound",()=>{const r=Le.installing;r.addEventListener("statechange",()=>{r.state==="installed"&&navigator.serviceWorker.controller&&(console.log("[PWA] New version available"),Vr())})}),Le}catch(r){return console.error("[PWA] Service Worker registration failed:",r),null}}let Ce=null,fe=null;function Hr(r="#install-btn"){if(fe=typeof r=="string"?document.querySelector(r):r,!fe){console.warn("[PWA] Install button not found:",r);return}fe.style.display="none",window.addEventListener("beforeinstallprompt",e=>{e.preventDefault(),Ce=e,fe.style.display="block",console.log("[PWA] Install prompt ready")}),fe.addEventListener("click",async()=>{if(!Ce){Wr();return}Ce.prompt();const{outcome:e}=await Ce.userChoice;console.log("[PWA] Install prompt outcome:",e),Ce=null,fe.style.display="none"}),window.addEventListener("appinstalled",()=>{console.log("[PWA] App installed"),Ce=null,fe.style.display="none"}),window.matchMedia("(display-mode: standalone)").matches&&(fe.style.display="none")}function Wr(){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 Rt=null;const $t=new Set;function Kr(r="#offline-indicator"){Rt=typeof r=="string"?document.querySelector(r):r,Tt(!navigator.onLine),window.addEventListener("online",()=>{console.log("[PWA] Back online"),Tt(!1),ho(!1)}),window.addEventListener("offline",()=>{console.log("[PWA] Gone offline"),Tt(!0),ho(!0)})}function Tt(r){Rt&&(Rt.style.display=r?"block":"none"),document.body.classList.toggle("is-offline",r)}function Yo(r){return $t.add(r),r(!navigator.onLine),()=>$t.delete(r)}function ho(r){for(const e of $t)try{e(r)}catch(t){console.error("[PWA] Offline listener error:",t)}}function W(){return navigator.onLine}function Vr(){confirm("A new version is available. Reload now?")&&Xr()}function Xr(){Le?.waiting&&Le.waiting.postMessage({type:"SKIP_WAITING"}),window.location.reload()}async function Yr({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((a,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 Zr(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 Ut(r,e,t={},n=5e3,o=1e4){const a=await Yr({timeoutMs:o});return new Promise((s,i)=>{const l=new MessageChannel,c=setTimeout(()=>{l.port1.close(),i(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)}},a.postMessage({type:r,...t},[l.port2])})}async function Jr(){try{return(await Ut("GET_TILE_STATS","TILE_STATS")).stats}catch(r){return console.warn("[PWA] getTileCacheStats failed:",r),null}}async function Qr(){try{return await Ut("CLEAR_TILE_CACHES","TILE_CACHES_CLEARED"),!0}catch(r){return console.warn("[PWA] clearTileCaches failed:",r),!1}}async function es(r){if(!r)return!1;try{return!!(await Ut("CLEAR_TILE_CACHE","TILE_CACHE_CLEARED",{cacheName:r})).deleted}catch(e){return console.warn(`[PWA] clearTileCacheForProvider(${r}) failed:`,e),!1}}async function ts(){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 os(r={}){const{installButton:e="#install-btn",offlineIndicator:t="#offline-indicator",autoRegisterSW:n=!0}=r;n&&await Ur(),Hr(e),Kr(t),console.log("[PWA] Initialized")}const Zo={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"}},ns=30*1024,pt=2*Math.PI*6378137/2;function go(r,e){const t=r/pt*180;let n=e/pt*180;return n=180/Math.PI*(2*Math.atan(Math.exp(n*Math.PI/180))-Math.PI/2),[t,n]}function mo(r,e,t){const n=Math.pow(2,t),o=Math.floor((r+180)/360*n),a=e*Math.PI/180,s=Math.floor((1-Math.log(Math.tan(a)+1/Math.cos(a))/Math.PI)/2*n);return{x:o,y:s}}function Jo(r,e){const[t,n,o,a]=r,[s,i]=go(t,n),[l,c]=go(o,a),d=mo(s,c,e),u=mo(l,i,e),p=Math.pow(2,e),h=Math.max(0,Math.min(d.x,u.x)),f=Math.min(p-1,Math.max(d.x,u.x)),b=Math.max(0,Math.min(d.y,u.y)),g=Math.min(p-1,Math.max(d.y,u.y));return{z:e,minX:h,maxX:f,minY:b,maxY:g,count:(f-h+1)*(g-b+1)}}function rs(r,e,t){let n=0;for(let o=e;o<=t;o++)n+=Jo(r,o).count;return n}function ss(r,e,t){const n=[];for(let o=e;o<=t;o++){const a=Jo(r,o);for(let s=a.minX;s<=a.maxX;s++)for(let i=a.minY;i<=a.maxY;i++)n.push({z:o,x:s,y:i})}return n}function as(r,{z:e,x:t,y:n}){return r.replace("{z}",e).replace("{x}",t).replace("{y}",n)}class is{constructor({baseMap:e,extent3857:t,minZoom:n,maxZoom:o,concurrency:a=2,interBatchDelayMs:s=50,onProgress:i=()=>{}}){const l=Zo[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(a,6)),this.interBatchDelayMs=s,this.onProgress=i,this._abortCtrl=null,this._cancelled=!1}async start(){if(this._abortCtrl)throw new Error("Downloader already started");this._abortCtrl=new AbortController,this._cancelled=!1;const e=ss(this.extent,this.minZoom,this.maxZoom),t=e.length,n=Date.now();let o=0,a=0,s=0,i=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:a,failed:s,cached:i,elapsedMs:d,etaMs:u})};l("running");for(let c=0;c{if(this._cancelled)return;const p=as(this.template,u);try{const h=await fetch(p,{signal:this._abortCtrl.signal,cache:"default"});h.ok?(a++,h.body&&h.body.cancel().catch(()=>{})):(h.status,s++)}catch(h){h.name==="AbortError"||s++}o++})),l("running"),this.interBatchDelayMs>0&&c+this.concurrencysetTimeout(u,this.interBatchDelayMs))}return l(this._cancelled?"cancelled":"done"),{phase:this._cancelled?"cancelled":"done",done:o,total:t,ok:a,failed:s,cached:i,elapsedMs:Date.now()-n}}cancel(){this._cancelled=!0,this._abortCtrl&&this._abortCtrl.abort()}}const ls=(()=>{const r=(n,o)=>{const a=n*pt/180,s=Math.log(Math.tan((90+o)*Math.PI/360))/(Math.PI/180);return[a,s*pt/180]},e=r(-3.3,4.5),t=r(1.2,11.2);return[e[0],e[1],t[0],t[1]]})();function cs(r){return r*ns}const Qo="https://api.lupmis4luspa.org/api/spatial_planning",kt="1",ds="1c46538c712e9b5b";function us(){try{if(typeof window>"u")return kt;const r=window.LUPMIS_SESSION;if(!r||typeof r!="object")return kt;const e=r.district_id;return e==null||String(e).length===0?null:String(e)}catch{}return kt}const en={get district_id(){return us()},api_token:ds};function tn(){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 ps=3e4,fs=5e3;let Se=null;async function hs(r=!1){if(Se!==null&&!r)return Se;const e=new AbortController,t=setTimeout(()=>e.abort(),fs);try{Se=(await fetch(`${Qo}/get_layers.php`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(en),signal:e.signal})).ok}catch{Se=!1}finally{clearTimeout(t)}return console.log("[RemoteDB] Server reachable:",Se),Se}function ke(){return Se}function gs(r,e=ps){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 ye(r,e={},t={}){const n=`${Qo}/${r}`,o={...en,...e};console.log("[RemoteDB] POST",n);const a=gs(t);try{const s=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(o),...t,signal:a.signal});if(!s.ok)throw new Error(`HTTP ${s.status}: ${s.statusText}`);const i=await s.json();return console.log("[RemoteDB] POST response:",r,"→",typeof i=="object"?`${Array.isArray(i)?i.length+" items":"object"}`:i),i}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{a.clear()}}async function ms(){return ye("get_district_boundary.php")}async function ys(){return ye("get_layers.php")}async function bs(){return ye("get_all_collector_zone_per_district.php")}async function ws(){return ye("get_parcels_per_district.php")}async function vs(){return ye("get_all_footprint_per_district.php")}async function _s(){return ye("get_contours_hillshade.php")}async function Es(){return ye("get_osm_roads.php")}async function xs(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 ye("save_gps_trail.php",t);return{remoteId:n?.id??n?.remote_id??null}}const Ss=63710088e-1,et=Math.PI/180;function Ls(r,e,t,n){const o=(n-e)*et,a=(t-r)*et,s=Math.sin(o/2)**2+Math.cos(e*et)*Math.cos(n*et)*Math.sin(a/2)**2;return 2*Ss*Math.asin(Math.min(1,Math.sqrt(s)))}function yo(r,e=5){return r==null||Number.isNaN(r)?"—":r.toFixed(e)}function Ts(r){return r==null||Number.isNaN(r)?"—":r<1e3?`${Math.round(r)} m`:`${(r/1e3).toFixed(2)} km`}function ks(r){return r==null||Number.isNaN(r)?"—":`±${Math.round(r)} m`}function Ps(r){return r==null||Number.isNaN(r)?"none":r<=10?"good":r<=30?"fair":"poor"}const Ms={minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0,timeoutMs:15e3,maximumAgeMs:0};class We{constructor(e={}){this.opts={...Ms,...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(a){console.error(`[GeoTracker] listener for "${e}" threw`,a)}}get state(){return this._state}get isRecording(){return this._recording}get lastFix(){return this._lastFix}get isSupported(){return!!this._geo}_setState(e){this._state!==e&&(this._state=e,this._emit("statechange",e))}startLive(){if(!this._geo){this._emit("error",new Error("Geolocation not supported"));return}this._live=!0,this._ensureWatch()}stopLive(){this._live=!1,this._recording||this._teardownWatch()}getCurrentPosition(){return new Promise((e,t)=>{if(!this._geo){t(new Error("Geolocation not supported"));return}this._geo.getCurrentPosition(n=>{const o=We.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=We.uuid(),n=new Date().toISOString(),o={uuid:t,name:e.name||null,startedAt:n,...e},a=await this.storage.createTrail(o);return this._activeTrailId=a,this._activeTrailUuid=t,this._lastRecorded=null,this._lastRecordedAt=0,this._distanceM=0,this._pointCount=0,this._recording=!0,this._ensureWatch(),this._setState("recording"),this._emit("trailstart",{trailId:a,uuid:t,startedAt:n}),{trailId:a,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(a){this._emit("error",a)}this._emit("trailstop",{trailId:e,...n});let o=!1;if(this.sync)try{o=await this._syncTrail(e)}catch(a){this._emit("error",a)}return this._activeTrailId=null,this._activeTrailUuid=null,{trailId:e,pointCount: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(a){t++,this._emit("error",a)}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},a=await this.sync.pushTrail(o,n),s=a&&(a.remoteId??a.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=We.normalize(e);if(this._lastFix=t,this._emit("position",t),!this._recording)return;const{minIntervalMs:n,minDistanceM:o,heartbeatMs:a,maxAccuracyM:s}=this.opts,i=t.timestamp;if(this._lastRecordedAt&&i-this._lastRecordedAt0&&t.accuracy!=null&&t.accuracy>s&&this._lastRecorded)return;let l=!1,c=0;if(!this._lastRecorded)l=!0;else{c=Ls(this._lastRecorded.lon,this._lastRecorded.lat,t.lon,t.lat);const d=i-this._lastRecordedAt;(c>=o||d>=a)&&(l=!0)}if(l){this._lastRecorded&&(this._distanceM+=c),this._pointCount+=1,this._lastRecorded={lon:t.lon,lat:t.lat,timestamp:i},this._lastRecordedAt=i;try{await this.storage.addPoint(this._activeTrailId,{...t,seq:this._pointCount}),this._emit("point",{trailId:this._activeTrailId,seq:this._pointCount,point:t,distanceM:this._distanceM,pointCount:this._pointCount})}catch(d){this._emit("error",d)}}}static normalize(e){const t=e.coords||{},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 Is={async createTrail(r){const e=r.districtId??tn()?.district_id??null;return ar({...r,districtId:e!=null?String(e):null})},addPoint:(r,e)=>ir(r,e),finishTrail:(r,e)=>lr(r,e),getUnsyncedTrails:()=>cr(),getTrailPoints:r=>dr(r),markTrailSynced:(r,e)=>ur(r,e)},Cs={pushTrail:(r,e)=>xs(r,e),isOnline:()=>W()},he=new We({storage:Is,sync:Cs,minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0}),As=new Set(["set:view","set:selected","clear:selected","set:basemap"]);function Ds({mapView:r,embedConfig:e}){const t=r.getMap(),n=window.parent&&window.parent!==window?window.parent:null,o=new R,a=new A({source:o,zIndex:9999,style:new M({stroke:new k({color:"#f97316",width:3}),fill:new I({color:"rgba(249,115,22,0.18)"})}),properties:{title:"Permit selection",displayInLayerSwitcher:!1}});t.addLayer(a);let s=null,i=e?.upn?String(e.upn):null,l=!1;function c(g){if(!n){console.warn("[embed-bridge] No parent window — would have sent:",g);return}try{n.postMessage(g,"*")}catch(m){console.warn("[embed-bridge] postMessage failed:",m)}}function d(g,m){c({type:"error",code:g,message:m})}function u(){l||(l=!0,c({type:"ready"}))}function p(g,m,y){const w=g.getProperties();let S=m,x=y;if(S==null||x==null){const v=g.getGeometry()?.getExtent();if(v){const[L,P]=xe(mn(v));S=L,x=P}}return{type:"parcel:select",upn:w.upn??null,parcel_id:w.id??null,lon:S??null,lat:x??null,zone_code:w.zone_code??null,zone_name:w.zone_name??null,landuse:w.landuse??null,min_height:w.min_height??null,max_height:w.max_height??null}}function h(g){if(o.clear(),g){const m=g.clone();o.addFeature(m)}}r.onClick((g,m,y,w)=>{let S=null;t.forEachFeatureAtPixel(w.pixel,x=>{if(x.get("_layerType")==="parcel")return S=x,!0}),S?(h(S),c(p(S,g,m))):(h(null),c({type:"parcel:cleared"}))}),window.addEventListener("message",g=>{const m=g.data;if(!(!m||typeof m!="object"||!As.has(m.type)))try{switch(m.type){case"set:view":{if(typeof m.lon=="number"&&typeof m.lat=="number"){const y=t.getView();y.setCenter(X([m.lon,m.lat])),typeof m.zoom=="number"&&y.setZoom(m.zoom)}break}case"set:selected":m.upn&&f(String(m.upn));break;case"clear:selected":h(null),i=null;break;case"set:basemap":m.key&&typeof r.setBaseMap=="function"&&r.setBaseMap(m.key);break}}catch(y){d("COMMAND_FAILED",`Failed to handle ${m.type}: ${y.message}`)}});function f(g){if(!s){i=g;return}const y=s.getSource().getFeatures().find(S=>String(S.get("upn")??"")===g);if(!y){i=g;return}i=null,h(y);const w=y.getGeometry()?.getExtent();w&&t.getView().fit(w,{padding:[50,50,50,50],duration:400,maxZoom:17}),c(p(y,null,null))}function b(g){s=g;const m=g.getSource(),y=()=>{queueMicrotask(()=>{i&&f(i),u()})};if(m.getFeatures().length>0)y();else{let w=!1;m.on("addfeature",()=>{w||(w=!0,queueMicrotask(()=>{w=!1,i&&f(i),u()}))})}}if(e?.basemap&&typeof r.setBaseMap=="function"&&r.setBaseMap(e.basemap),typeof e?.lon=="number"&&typeof e?.lat=="number"){const g=t.getView();g.setCenter(X([e.lon,e.lat])),g.setZoom(typeof e?.zoom=="number"?e.zoom:15)}return{attachParcelsLayer:b,emitError:d}}let Pt=null;async function bo(){if(!Pt){const r=await ft(()=>import("./shpjs-CNrRgkgn.js"),[]);Pt=r.default||r}return Pt}let _=null,te=null,oe=null,Mt=null;const Nt=typeof window<"u"&&window.LUPMIS_EMBED||null,je=!!(Nt&&Nt.mode==="permit");let B=je?"embed-permit":"addLocation";function Fs(){const r=typeof window<"u"?window.LUPMIS_SESSION:null;if(!r||typeof r!="object")return!1;const e=r.district_id;if(e!=null&&String(e).length>0)return!1;console.warn("[App] Authenticated user has no district assigned; halting init.");const t=document.createElement("div");t.id="no-district-overlay",t.setAttribute("role","alertdialog"),t.setAttribute("aria-modal","true"),t.style.cssText="position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.98);padding:24px;";const n=r.full_name||r.username||"You";return t.innerHTML=` +
              +
              🛑
              +

              + No district assigned +

              +

              + ${$(n)}, your user profile is not associated with any + district. LUPMIS2 cannot load the relevant map data without one. +

              +

              + Please contact the system administrator to have a district assigned + to your account. +

              + +
              `,document.body.appendChild(t),t.querySelector("#no-district-portal-btn")?.addEventListener("click",()=>{window.location.href="https://lupmis4luspa.org/"}),!0}async function wo(){if(console.log("[App] Initializing..."),Fs())return;await os({installButton:"#install-btn",offlineIndicator:"#offline-indicator",autoRegisterSW:!0});const r=localStorage.getItem("default-basemap")||"topo";_=new zr("map",{center:[-1.5,7.5],zoom:7,basemap:r}),te=new jr(_.getMap()),ia(),te.onMeasureComplete(t=>{console.log("[MapTools] Measurement complete:",t),t.type==="polygon"&&t.coordinate&&t.feature?.get("_layerType")!=="measure_area"&&_?.showDrawnPolygonPopup(t.feature,t.coordinate)}),je&&(Mt=Ds({mapView:_,embedConfig:Nt})),_.onClick((t,n,o,a)=>{if(je||(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(_.getMap().forEachFeatureAtPixel(a.pixel,i=>{if(i.get("_layerType")==="parcel")return s=i,!0}),s){console.log("[MapClick] Clicked on parcel → Edit Attributes"),_.showParcelEditPopup(s,a.coordinate);return}B==="addLocation"&&(o?(console.log("[MapClick] Clicked on marker:",o.getId()),_.selectMarker(o),Rs(o)):(console.log("[MapClick] Empty space → Add Location popup"),_.clearSelection(),_.showAddLocationPopup(a.coordinate)))}),_.onDblClick((t,n,o,a)=>{if(je||!o)return;const s=o.get("_layerType");if(console.log("[App] Double-click on feature, _layerType:",s||"none"),s==="measure_circle")_.showCircleIntersectionPopup(o,a.coordinate);else{if(s==="measure_circle_radius")return;s==="measure_area"?_.showAreaIntersectionPopup(o,a.coordinate):s==="collector_zone"?_.showInfoPopup(o,a.coordinate,{title:"Zone Info",color:"#7c3aed"}):s==="parcel"?_.showInfoPopup(o,a.coordinate,{title:"Parcel Info",color:"#0ea5e9"}):_.showInfoPopup(o,a.coordinate,{title:"Feature Info",color:"#e11d48"})}}),_.onAddLocation(async t=>{console.log("[App] Add location from map popup:",t);try{const n=await qn(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 It(),_?.zoomTo(t.lon,t.lat,14),n.id&&_?.selectMarker(n.id),le("Location added successfully")}catch(n){console.error("[App] Failed to add location:",n),N("Failed to add location: "+n.message)}}),_.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 Kn(o,n),le("Parcel updated locally")}catch(a){console.error("[App] Failed to save parcel update:",a),N("Failed to save parcel: "+a.message)}});const e=new Lo;_.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"}),a=await Vn(o,n);console.log("[App] New parcel inserted with id:",a.id),le("New parcel saved (pending verification)")}catch(o){console.error("[App] Failed to save new parcel:",o),N("Failed to save parcel: "+o.message)}});try{console.log("[App] Initializing database..."),await Gn(),console.log("[App] Database ready");const t=await jt();console.log("[App] Database status:",t),W()&&(await hs()||(console.warn("[App] API server unreachable — using local data only"),la("Server not responding — loading cached data."))),await Qs(),_?.initEditBar(),Ws(),Ks(),Vs(),je&&Mt&&oe&&(oe.setVisible(!0),Mt.attachParcelsLayer(oe)),Xs(),Ys(),Zs(),Js()}catch(t){console.error("[App] Database initialization failed:",t),N("Failed to initialize database. Please refresh the page.");return}Os(),await It(),Bn(t=>{if(console.log("[App] Database change:",t),t.table==="locations"&&!t.local&&It(),t.table==="parcels"){const n=document.getElementById("local-data-stats");n&&!n.classList.contains("d-none")&>()}}),Yo(t=>{t?console.log("[App] Working offline - data will sync when back online"):(console.log("[App] Back online - syncing data..."),ea())}),ca(),ua(),da(),pa(),fa(),ha(),ga(),console.log("[App] Initialized successfully")}function Os(){console.log("[initUI] Starting UI initialization..."),aa();const r=document.getElementById("export-btn");r&&r.addEventListener("click",Gs);const e=document.getElementById("local-data-btn");e&&e.addEventListener("click",()=>gt());const t=document.getElementById("import-shp-btn"),n=document.getElementById("shp-file-input");t&&n&&(t.addEventListener("click",()=>n.click()),n.addEventListener("change",sn));const o=document.getElementById("import-geojson-btn"),a=document.getElementById("geojson-file-input");o&&a&&(o.addEventListener("click",()=>a.click()),a.addEventListener("change",an));const s=document.getElementById("import-kml-btn"),i=document.getElementById("kml-file-input");s&&i&&(s.addEventListener("click",()=>i.click()),i.addEventListener("change",ln)),ra();const l=document.getElementById("exportGeoJSON-btn");l&&l.addEventListener("click",qs);const c=document.getElementById("status-btn");c&&c.addEventListener("click",zs);const d=document.getElementById("fit-btn");d&&d.addEventListener("click",()=>_?.fitToMarkers());const u=document.getElementById("dock-btn-add-location"),p=document.getElementById("dock-btn-measure-circle"),h=document.getElementById("dock-btn-measure-line"),f=document.getElementById("dock-btn-measure-area"),b=document.getElementById("dock-btn-draw"),g=document.getElementById("dock-btn-clear");console.log("[initUI] Buttons found:",{addLocation:!!u,measureCircle:!!p,measureLine:!!h,measureArea:!!f,draw:!!b,clear:!!g});const m=[u,p,h,f,b],y=(w,S)=>{switch(console.log("[setMode] Changing mode from",B,"to",w),B=w,console.log("[setMode] currentMode is now:",B),m.forEach(x=>{x&&x.classList.toggle("active",x===S)}),te?.deactivate(),w!=="draw"&&_?.setEditMode(!1),w!=="addLocation"&&_?.hideAddLocationPopup(),w){case"measureCircle":te?.startCircleMeasure();break;case"measureLine":te?.startLineMeasure();break;case"measureArea":te?.startAreaMeasure();break;case"draw":_?.setEditMode(!0);break}};u&&u.addEventListener("click",()=>{console.log("[Button] Add Location clicked"),y("addLocation",u)}),p&&p.addEventListener("click",()=>{console.log("[Button] Circle clicked, currentMode is:",B),B==="measureCircle"?y("addLocation",u):y("measureCircle",p)}),h&&h.addEventListener("click",()=>{console.log("[Button] Line clicked, currentMode is:",B),B==="measureLine"?y("addLocation",u):y("measureLine",h)}),f&&f.addEventListener("click",()=>{console.log("[Button] Area clicked, currentMode is:",B),B==="measureArea"?y("addLocation",u):y("measureArea",f)}),b&&b.addEventListener("click",()=>{console.log("[Button] Draw clicked, currentMode is:",B),B==="draw"?y("addLocation",u):y("draw",b)}),g&&g.addEventListener("click",()=>{if(te?.clearMeasurements(),B.startsWith("measure"))switch(te?.deactivate(),B){case"measureCircle":te?.startCircleMeasure();break;case"measureLine":te?.startLineMeasure();break;case"measureArea":te?.startAreaMeasure();break}})}async function It(){try{console.log("[App] Loading locations...");const r=await No();console.log("[App] Locations loaded:",r),$s(r),_&&(_.clearMarkers(),r.length>0&&(_.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 Rs(r){const e=r.get("name"),t=r.get("description"),n=r.get("category"),o=r.get("lon")||r.get("longitude"),a=r.get("lat")||r.get("latitude");console.log("[App] Selected location:",{name:e,description:t,category:n,lon:o,lat:a})}function $s(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=` +
              +

              No locations yet.

              + Click the map or fill the form above! +
              + `;return}const n={water:"💧",school:"🏫",health:"🏥",market:"🏪",default:"📍",other:"📌"};e.innerHTML=r.map(o=>{const a=n[o.category]||"📍";return` + +
              +
              +
              ${a} ${$(o.name)}
              + ${o.latitude.toFixed(5)}, ${o.longitude.toFixed(5)} +
              + ${o.category} +
              + ${o.description?`${$(o.description)}`:""} +
              + `}).join(""),e.querySelectorAll(".location-item").forEach(o=>{o.addEventListener("click",a=>{a.preventDefault();const s=parseFloat(o.dataset.lon),i=parseFloat(o.dataset.lat),l=parseInt(o.dataset.id);_?.zoomTo(s,i,14),_?.selectMarker(l)})})}async function gt(){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 nr();e.innerHTML=n.map(o=>{const s=zo(o.name)?``:"";return` + + + ${$(o.name)} + + ${o.count} + ${s} + + `}).join(""),r.classList.remove("d-none"),e.querySelectorAll(".table-name-link").forEach(o=>{o.addEventListener("click",a=>{a.preventDefault(),Bs(o.dataset.table)})}),e.querySelectorAll(".table-clear-btn").forEach(o=>{o.addEventListener("click",async a=>{a.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 i=await jo(s);le(`Cleared ${i} row${i===1?"":"s"} from "${s}". It will re-download on next start.`),await gt()}catch(i){console.error("[App] Per-table clear failed:",i),N(`Could not clear "${s}": ${i.message}`)}})})}catch(n){console.error("[App] Failed to load table stats:",n),e.innerHTML='Failed to load',r.classList.remove("d-none")}t&&!t._wired&&(t._wired=!0,t.addEventListener("click",Ns))}}async function Ns(){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 or(),e=r.reduce((t,n)=>t+n.count,0);le(`Cleared ${e} row${e===1?"":"s"} across ${r.length} table${r.length===1?"":"s"}.`),await gt(),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),N("Failed to clear cached layers: "+r.message)}}async function Bs(r){const e=document.getElementById("tableContentModalLabel"),t=document.getElementById("table-content-body"),n=document.getElementById("table-content-info");e.textContent=`Table: ${r}`,t.innerHTML=` +
              +
              + Loading... +
              +
              + `,n.textContent="",new Gt(document.getElementById("tableContentModal")).show();try{const{columns:a,rows:s}=await rr(r);if(s.length===0){t.innerHTML='
              Table is empty
              ',n.textContent="0 rows";return}const i=a.map(c=>`${$(c)}`).join(""),l=s.map(c=>`${a.map(u=>{let p=c[u];if(p==null)return'NULL';p=String(p);const h=p.length>120?p.substring(0,120)+"...":p;return`${$(h)}`}).join("")}`).join("");t.innerHTML=` +
              + + + ${i} + + ${l} +
              +
              + `,n.textContent=`${s.length}${s.length>=200?"+":""} row(s), ${a.length} column(s)`}catch(a){console.error("[App] Failed to load table content:",a),t.innerHTML=`
              Failed to load: ${$(a.message)}
              `}}async function Gs(){try{await er("lupmis-backup.sqlite3"),le("Database exported successfully")}catch(r){console.error("[App] Export failed:",r),N("Export failed: "+r.message)}}async function qs(){try{const r=await tr(),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),le(`Exported ${r.features.length} location(s)`)}catch(r){console.error("[App] GeoJSON Export failed:",r),N("GeoJSON Export failed: "+r.message)}}async function zs(){try{const r=await jt(),e=document.getElementById("status-content");e&&(e.innerHTML=` + + + + + + + + + + + + + + + + + + + + + + + +
              Ready:${r.ready?"Yes":"No"}
              Online:${W()?"Yes":"Offline"}
              Database:${r.databasePath||"N/A"}
              Tables:${r.tables.map(n=>`${n}`).join("")}
              Locations:${r.locationCount}
              + `),new Gt(document.getElementById("statusModal")).show()}catch(r){console.error("[App] Failed to get status:",r),N("Failed to get status")}}function on(r){return r.replace(/^\(+/,"").replace(/\)+$/,"").split(",").map(e=>{const[t,n]=e.trim().split(/\s+/).map(Number);return[t,n]})}function js(r){return{type:"Polygon",coordinates:r.trim().replace(/^POLYGON\s*\(\s*/i,"").replace(/\s*\)$/,"").split("),(").map(on)}}function Us(r){return{type:"MultiPolygon",coordinates:r.trim().replace(/^MULTIPOLYGON\s*\(\s*/i,"").replace(/\s*\)$/,"").split(")),((").map(o=>o.replace(/^\(+/,"").replace(/\)+$/,"").split("),(").map(on))}}function mt(r){if(!r)return null;const e=r.trim().toUpperCase();return e.startsWith("MULTIPOLYGON")?Us(r):e.startsWith("POLYGON")?js(r):(console.warn("[App] Unsupported WKT type:",e.substring(0,30)),null)}function Hs(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=mt(e);return{type:"FeatureCollection",features:[{type:"Feature",properties:{districtid:t,district_name:n},geometry:o}]}}function vo(r){if(!Array.isArray(r)||r.length===0)return null;const e=[];for(const t of r){const n=t.polygon||t.boundary,o=mt(n);if(!o)continue;const a={_layerType:"collector_zone"};for(const[s,i]of Object.entries(t))s==="polygon"||s==="boundary"||(a[s]=i);e.push({type:"Feature",properties:a,geometry:o})}return e.length===0?null:{type:"FeatureCollection",features:e}}async function Ws(){const r="district_boundary",t={strokeColor:"#e11d48",strokeWidth:2.5,fillColor:"rgba(225,29,72,0.08)",typeDescription:"Vector / Polygon"},n=_?.getLayerGroup(1)||null;function o(s){if(!s)return;const i=s.getLayers(),l=[];i.forEach(c=>{c.get("title")==="District Boundary"&&l.push(c)}),l.forEach(c=>i.remove(c))}function a(s){if(!s||!_)return;const i=s.getSource().getExtent();i&&i[0]!==1/0&&_.getMap().getView().fit(i,{padding:[40,40,40,40],duration:600})}try{const s=await Go(r);if(s){console.log("[App] District boundary loaded from local cache");const i=_?.addGeoJSONLayer(s,"District Boundary",t,n);a(i)}if(W()&&ke()){console.log("[App] Fetching district boundary from API...");const i=await ms(),l=Hs(i);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 Bo(r,l),s&&o(n||_?.getOverlayGroup());const c=_?.addGeoJSONLayer(l,"District Boundary",t,n);a(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 Ks(){const e={strokeColor:"#7c3aed",strokeWidth:1.5,fillColor:"rgba(124,58,237,0.12)",typeDescription:"Vector / Polygon"},t=_?.getLayerGroup(1)||null;console.log("[App] loadCollectorZones — adminGroup:",t?t.get("title"):"null");const n={type:"FeatureCollection",features:[]},o=_?.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&&N("No collector zones available locally. Connect to the internet to download zone data.")});function a(s){const i=new ce().readFeatures(s,{featureProjection:"EPSG:3857"});o.getSource().clear(),o.getSource().addFeatures(i)}try{const s=await Un();if(s){const i=vo(s);i&&(console.log("[App] Collector zones loaded from local cache:",i.features.length,"zones"),a(i))}if(W()&&ke()){console.log("[App] Fetching collector zones from API...");const i=await bs();if(!i?.success||!Array.isArray(i?.data)){console.warn("[App] getCollectorZones API response invalid:",i);return}const l=i.data;console.log("[App] Collector zones from API:",l.length,"entries"),await jn(l);const c=vo(l);if(!c){console.warn("[App] Could not convert zones to GeoJSON");return}a(c),console.log("[App] Collector zones updated from API:",c.features.length,"zones")}else 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 _o(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 a=null;if(n.geom&&n.geom.type&&n.geom.coordinates)a={type:n.geom.type,coordinates:n.geom.coordinates};else if(n.sp_boundary&&n.sp_boundary.type&&n.sp_boundary.coordinates)a={type:n.sp_boundary.type,coordinates:n.sp_boundary.coordinates};else{const l=n.boundary||n.geometry_wkt||n.polygon||n.wkt;a=mt(l)}if(!a)continue;const s=new Set(["polygon","boundary","geom","geometry_wkt","wkt","textboundary","sp_boundary","fetched_at"]),i={_layerType:"parcel"};for(const[l,c]of Object.entries(n))s.has(l)||(i[l]=c);t.push({type:"Feature",properties:i,geometry:a})}return t.length===0?null:{type:"FeatureCollection",features:t}}async function Vs(){const e={strokeColor:"#0ea5e9",strokeWidth:1.5,fillColor:"rgba(14,165,233,0.12)",typeDescription:"Vector / Polygon"},t=_?.getLayerGroup(4)||null;console.log("[App] loadParcels — landUseGroup:",t?t.get("title"):"null");const n={type:"FeatureCollection",features:[]};if(oe=_?.addGeoJSONLayer(n,"Parcels",e,t),!oe){console.warn("[App] Could not create Parcels layer");return}oe.setVisible(!1),oe.on("change:visible",()=>{oe.getVisible()&&oe.getSource().getFeatures().length===0&&N("No parcels available locally. Connect to the internet to download parcel data.")});function o(a){const s=new ce().readFeatures(a,{featureProjection:"EPSG:3857"});oe.getSource().clear(),oe.getSource().addFeatures(s)}try{const a=await Wn();if(a){const s=_o(a);s&&(console.log("[App] Parcels loaded from local cache:",s.features.length,"parcels"),o(s))}if(W()&&ke()){console.log("[App] Fetching parcels from API...");const s=await ws();if(!s?.success||!Array.isArray(s?.data)){console.warn("[App] getDistrictParcels API response invalid:",s);return}const i=s.data;console.log("[App] Parcels from API:",i.length,"entries"),i.length>0&&console.log("[App] First parcel keys:",Object.keys(i[0])),await Hn(i);const l=_o(i);if(!l){console.warn("[App] Could not convert parcels to GeoJSON");return}o(l),console.log("[App] Parcels updated from API:",l.features.length,"parcels")}else a||console.log("[App] Parcels not available — offline and no local cache")}catch(a){console.error("[App] Failed to load parcels:",a)}}function Eo(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 a;if(typeof o=="object"&&o!==null&&o.type?a=o:a=mt(o),!a)continue;const s={_layerType:"building_footprint"};for(const[i,l]of Object.entries(n))e.includes(i)||typeof l=="object"&&l!==null||(s[i]=l);t.push({type:"Feature",properties:s,geometry:a})}return t.length===0?null:{type:"FeatureCollection",features:t}}async function Xs(){const e={strokeColor:"#8b6f47",strokeWidth:1,fillColor:"rgba(139,111,71,0.18)",typeDescription:"Vector / Polygon"},t=_?.getLayerGroup(5)||null;console.log("[App] loadBuildingFootprints — physInfraGroup:",t?t.get("title"):"null");const n={type:"FeatureCollection",features:[]},o=_?.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&&N("No building footprints available locally. Connect to the internet to download footprint data.")});function a(s){const i=new ce().readFeatures(s,{featureProjection:"EPSG:3857"});o.getSource().clear(),o.getSource().addFeatures(i)}try{const s=await Yn();if(s){const i=Eo(s);i&&(console.log("[App] Building footprints loaded from local cache:",i.features.length,"footprints"),a(i))}if(W()&&ke()){console.log("[App] Fetching building footprints from API...");const i=await vs();if(!i?.success||!Array.isArray(i?.data)){console.warn("[App] getBuildingFootprints API response invalid:",i);return}const l=i.data;console.log("[App] Building footprints from API:",l.length,"entries"),l.length>0&&console.log("[App] First footprint keys:",Object.keys(l[0])),await Xn(l);const c=Eo(l);if(!c){console.warn("[App] Could not convert building footprints to GeoJSON");return}a(c),console.log("[App] Building footprints updated from API:",c.features.length,"footprints")}else 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 nn(r,e){if(!Array.isArray(r)||r.length===0)return null;const t=new Lo,n=new ce,o=["geom","geometry","wkt","polygon","boundary","road","line"],a=[];for(const s of r){const i=s.geom||s.geometry||s.wkt||s.polygon||s.boundary||s.road||s.line;if(!i)continue;let l;try{if(typeof i=="object"&&i!==null&&i.type){a.push({type:"Feature",properties:xo(s,o,e),geometry:i});continue}l=t.readGeometry(i)}catch(d){console.warn(`[App] Could not parse WKT for ${e}:`,d,i?.toString().slice(0,60));continue}const c=JSON.parse(n.writeGeometry(l));a.push({type:"Feature",properties:xo(s,o,e),geometry:c})}return a.length===0?null:{type:"FeatureCollection",features:a}}function xo(r,e,t){const n={_layerType:t};for(const[o,a]of Object.entries(r))e.includes(o)||typeof a=="object"&&a!==null||(n[o]=a);return n}async function Ys(){const r={strokeColor:"#78716c",strokeWidth:.8,typeDescription:"Vector / Line",fillColor:"rgba(0,0,0,0)"},e=_?.getLayerGroupByTitle("Biophysical Environment");console.log("[App] loadContoursHillshade — group:",e?e.get("title"):"null");const t={type:"FeatureCollection",features:[]},n=_?.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&&N("No Contours hillshade data available. Connect to the internet to download it.")}),!W()||!ke()){console.log("[App] Contours hillshade not available — offline or server unreachable");return}try{console.log("[App] Fetching contours_hillshade from API...");const o=await _s();if(!o?.success||!Array.isArray(o?.data)){console.warn("[App] getContoursHillshade API response invalid:",o);return}const a=o.data;console.log("[App] Contours hillshade from API:",a.length,"rows"),a.length>0&&console.log("[App] First row keys:",Object.keys(a[0]));const s=nn(a,"contours_hillshade");if(!s){console.warn("[App] Could not convert contours to GeoJSON");return}const i=new ce().readFeatures(s,{featureProjection:"EPSG:3857"});n.getSource().clear(),n.getSource().addFeatures(i),console.log("[App] Contours hillshade loaded:",i.length,"features")}catch(o){console.error("[App] Failed to load contours_hillshade:",o)}}async function Zs(){const e={strokeColor:"#F0F1F0",strokeWidth:1.5,lineCasingColor:"#000000",lineCasingWidth:3.5,fillColor:"rgba(0,0,0,0)",typeDescription:"Vector / Line"},t=_?.getLayerGroup(5)||null;console.log("[App] loadOSMRoads — group:",t?t.get("title"):"null");const n={type:"FeatureCollection",features:[]},o=_?.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&&N("No OSM roads available locally. Connect to the internet to download them.")});function a(s){const i=nn(s,"osm_road");if(!i)return console.warn("[App] Could not convert OSM roads to GeoJSON"),0;const l=new ce().readFeatures(i,{featureProjection:"EPSG:3857"});return o.getSource().clear(),o.getSource().addFeatures(l),l.length}try{const s=await Jn();if(s){const i=a(s);console.log("[App] OSM_roads loaded from local cache:",i,"features")}if(W()&&ke()){console.log("[App] Fetching OSM_roads from API...");const i=await Es();if(!i?.success||!Array.isArray(i?.data)){console.warn("[App] getOSMRoads API response invalid:",i);return}const l=i.data;console.log("[App] OSM_roads from API:",l.length,"rows"),l.length>0&&console.log("[App] First row keys:",Object.keys(l[0])),await Zn(l);const c=a(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 Js(){_?.addWMSLayer("Biophysical Environment","DEAfrica Coastlines v0.4","https://geoserver.digitalearth.africa/geoserver/wms","coastlines:DEAfrica_Coastlines",{serverType:"geoserver",visible:!1,onlineOnly:!0}),_?.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:'© Digital Earth Africa — SRTM-derived Slope',legendUrl:"https://ows.digitalearth.africa/legend/srtm_deriv/style_slope/legend.png"})}async function Qs(){const r="layer_categories";function e(t){const n=[...t].sort((o,a)=>a.id-o.id);for(const o of n)_?.addLayerGroup(o.id,o.name,o.description||"");console.log("[App] Created",t.length,"layer groups on map")}try{const t=await Go(r);if(t&&(console.log("[App] Layer categories loaded from local cache:",t.length,"entries"),e(t)),W()&&ke()){console.log("[App] Fetching layer categories from API...");const n=await ys();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 Bo(r,o),t){const a=_?.getOverlayGroup()?.getLayers();if(a){const s=[];a.forEach(i=>{i.get("layerId")!==void 0&&s.push(i)}),s.forEach(i=>a.remove(i))}}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 ea(){if(!W()){console.log("[App] Cannot sync - offline");return}console.log("[App] Sync placeholder - implement based on your backend")}const re=[],ta={strokeColor:"#e11d48",strokeWidth:2,fillColor:"rgba(225,29,72,0.12)"};function Z(r){yt("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 Ht(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 i=s.fileName?s.fileName.replace(/\.[^/.]+$/,""):e,l=_?.addGeoJSONLayer(s,i,ta);l&&(l.set("removable",!0),l.set("typeTag","GEO"),re.push(l),o+=s.features.length)}if(o===0){Z("No features found in the file.");return}console.log(`[${t}] Added ${o} feature(s) from ${n.length} layer(s)`);const a=re[re.length-1];if(a){const s=a.getSource().getExtent();_?.getMap().getView().fit(s,{padding:[50,50,50,50],maxZoom:18})}Wt()}function Wt(){const r=document.getElementById("imported-layers-info");if(!r)return;if(re.length===0){r.innerHTML="",r.classList.add("d-none");return}r.innerHTML=` +
              +
              +
              Imported Layers
              + +
              +
                +
                `;const e=r.querySelector("#imported-layers-list");re.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=`${$(t.get("title"))} + + ${t.getSource().getFeatures().length} + + `,e.appendChild(o)}),r.classList.remove("d-none"),r.querySelectorAll("[data-remove-idx]").forEach(t=>{t.addEventListener("click",()=>{oa(Number(t.dataset.removeIdx))})}),r.querySelector("#remove-imported-layers")?.addEventListener("click",()=>{na()})}function oa(r){if(r<0||r>=re.length)return;const e=re[r],t=_?.getOverlayGroup();t&&t.getLayers().remove(e),re.splice(r,1),Wt(),console.log("[FileImport] Removed layer:",e.get("title"))}function na(){const r=_?.getOverlayGroup();if(r)for(const e of re)r.getLayers().remove(e);re.length=0,Wt(),console.log("[FileImport] All imported layers removed")}function rn(r){const e={};for(const t of r){const n=t.name.split(".").pop().toLowerCase();e[n]=t}return e}async function sn(r){const e=r.target.files;if(!e||e.length===0)return;const t=200*1024*1024,n=Array.from(e).reduce((o,a)=>o+a.size,0);if(n>t){const o=(n/1048576).toFixed(0);Z(`Files too large (${o} MB total). Maximum supported size is 200 MB.`),r.target.value="";return}try{let o,a;const s=rn(e);if(s.zip){const i=s.zip;a=i.name.replace(/\.zip$/i,""),console.log("[ShpImport] Parsing zip",i.name,"("+(i.size/1024).toFixed(1)+" KB)"),o=await(await bo())(await i.arrayBuffer())}else if(s.shp){a=s.shp.name.replace(/\.shp$/i,"");const l=["dbf","shx","prj"].filter(u=>!s[u]);if(l.length>0){Z("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 bo())(c)}else{Z("Please select a .zip or at least a .shp file."),r.target.value="";return}Ht(o,a,"ShpImport")}catch(o){console.error("[ShpImport] Failed:",o),Z("Failed to parse shapefile: "+o.message)}r.target.value=""}async function an(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);Z(`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 a;if(o.type==="FeatureCollection")a=o;else if(o.type==="Feature")a={type:"FeatureCollection",features:[o]};else if(o.type&&o.coordinates)a={type:"FeatureCollection",features:[{type:"Feature",geometry:o,properties:{}}]};else{Z("The file does not contain valid GeoJSON."),r.target.value="";return}const s=e.name.replace(/\.(geo)?json$/i,"");Ht(a,s,"GeoJSONImport")}catch(n){console.error("[GeoJSONImport] Failed:",n);const o=(e.size/(1024*1024)).toFixed(1);Z(`Failed to import "${e.name}" (${o} MB): ${n.message}`)}r.target.value=""}async function ln(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);Z(`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 a=new yn({extractStyles:!1}).readFeatures(n,{featureProjection:"EPSG:3857"});if(!a||a.length===0){Z("No features found in the KML file."),r.target.value="";return}const s=new ce,i=JSON.parse(s.writeFeatures(a,{featureProjection:"EPSG:3857",dataProjection:"EPSG:4326"})),l=e.name.replace(/\.kml$/i,"");Ht(i,l,"KMLImport")}catch(n){console.error("[KMLImport] Failed:",n);const o=(e.size/(1024*1024)).toFixed(1);Z(`Failed to import "${e.name}" (${o} MB): ${n.message}`)}r.target.value=""}function ra(){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=rn(n),a=Object.keys(o);if(o.zip||o.shp){const s={target:{files:n,value:""}};Object.defineProperty(s.target,"value",{writable:!0}),sn(s)}else if(o.geojson||o.json){const i={target:{files:[o.geojson||o.json],value:""}};Object.defineProperty(i.target,"value",{writable:!0}),an(i)}else if(o.kml){const s={target:{files:[o.kml],value:""}};Object.defineProperty(s.target,"value",{writable:!0}),ln(s)}else Z("Unsupported file type(s): "+a.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 sa=50,So={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 yt(r,e){const t=So[r]||So.info;(r==="error"?console.error:r==="warning"?console.warn:console.log)("[App]",e);const o=document.getElementById("message-log");if(!o)return;const a=o.querySelector(".text-muted");a&&a.remove();const 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=`
                ${$(e)}
                ${l}
                `,o.prepend(s);o.children.length>sa;)o.lastElementChild.remove()}function aa(){const r=document.getElementById("clear-message-log");r&&r.addEventListener("click",()=>{const e=document.getElementById("message-log");e&&(e.innerHTML='
                No messages yet.
                ')})}function ia(){const r=document.getElementById("gps-readout"),e=document.getElementById("gps-coords"),t=document.getElementById("gps-accuracy"),n=document.getElementById("gps-sats");if(!he.isSupported){e&&(e.textContent="No GPS");return}he.on("position",a=>{e&&(e.textContent=`${yo(a.lat)}, ${yo(a.lon)}`),t&&(t.textContent=ks(a.accuracy)),n&&(n.textContent=`${a.satellites!=null?a.satellites:"—"} sat`),r&&(r.classList.add("active"),r.classList.remove("quality-good","quality-fair","quality-poor"),r.classList.add("quality-"+Ps(a.accuracy))),_?.showCurrentPosition(a.lon,a.lat,a.accuracy)}),he.on("point",a=>{_?.appendTrailPoint(a.point.lon,a.point.lat)}),he.on("error",a=>{console.warn("[GPS]",a?.message||a),a&&a.code===1&&N("Location permission denied. Enable location access to use GPS.")}),_.onLocateMe(async()=>{try{const a=await he.getCurrentPosition();_.centerOn(a.lon,a.lat,16)}catch(a){N("Could not get your location: "+(a?.message||a))}}),_.onToggleRecording(async a=>{if(a)try{await ro,_.startTrailRender(),_.setRecordingState(!0),r?.classList.add("recording"),await he.startRecording({name:`Trail ${new Date().toLocaleString()}`}),le("GPS trail recording started")}catch(s){_.setRecordingState(!1),r?.classList.remove("recording"),N("Could not start recording: "+(s?.message||s))}else try{const s=await he.stopRecording();if(_.setRecordingState(!1),r?.classList.remove("recording"),s){const i=`Trail saved: ${s.pointCount} points, ${Ts(s.distanceM)}`+(s.synced?" — synced":" — will sync when online");le(i)}}catch(s){N("Error stopping recording: "+(s?.message||s))}});const o=async()=>{if(W())try{await ro;const a=await he.syncPending();a.pushed&&console.log(`[GPS] Synced ${a.pushed} pending trail(s)`)}catch(a){console.warn("[GPS] pending-sync error",a)}};o(),Yo(a=>{a||o()})}function N(r){yt("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 le(r){yt("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 la(r){yt("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 ca(){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 da(){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 ua(){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(),_?.setScaleBarUnits(n||"metric"),r.addEventListener("change",()=>{const o=r.checked?"imperial":"metric";localStorage.setItem("measurement-system",o),t(),_?.setScaleBarUnits(o),console.log("[Settings] Measurement system:",o)})}function pa(){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),_?.setBaseMap(t),console.log("[Settings] Default base map:",t)}),_?.getMap()?.on("basemapchange",t=>{if(t?.key&&r.value!==t.key){r.value=t.key;try{localStorage.setItem("default-basemap",t.key)}catch{}}})}function fa(){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 a(){if(o)return o;const s=!!navigator.serviceWorker?.controller;return r.innerHTML=s?'
                Loading…
                ':'
                Initialising service worker…
                ',o=(async()=>{try{const i=await Jr();if(!i){r.innerHTML=` +
                + Tile cache stats unavailable. Try reloading the page if this persists. +
                `;return}const l=i.totals,c=i.byProvider.filter(p=>p.count>0).map(p=>` + + ${$(p.label)} + ${p.count.toLocaleString()} / ${p.limit.toLocaleString()} + ${n(p.estBytes)} + + + + `).join("");let d="";const u=await ts();if(u&&u.quota>0){const p=(u.usage/u.quota*100).toFixed(1);d=` +
                + Total app storage: ${n(u.usage)} of ${n(u.quota)} available (${p}%) +
                `}if(l.count===0){r.innerHTML=` +
                + No tiles cached yet. Pan and zoom the map to start caching tiles automatically. +
                ${d}`,e.disabled=!0;return}r.innerHTML=` +
                + ${l.count.toLocaleString()} tiles cached, ~${n(l.estBytes)} on this device +
                + + + + + + + + ${c} +
                Base mapCached / limitApprox. size
                ${d}`,e.disabled=!1,r.querySelectorAll(".provider-clear-btn").forEach(p=>{p.addEventListener("click",async h=>{h.preventDefault();const f=p.dataset.cache,b=p.dataset.label||f;if(!confirm(`Clear cached "${b}" tiles? + +Other providers are not affected. The tiles will re-download as you browse online.`))return;p.disabled=!0,await es(f)?console.log(`[Settings] Cleared tile cache for ${b}`):console.warn(`[Settings] Could not clear tile cache for ${b}`),await a()})})}finally{o=null}})(),o}e.addEventListener("click",async()=>{if(!confirm("Clear all cached map tiles from this device? You will need to be online to view them again."))return;e.disabled=!0,await Qr()?console.log("[Settings] Tile caches cleared"):console.warn("[Settings] Tile-cache clear failed"),await a()}),t.addEventListener("show.bs.offcanvas",a),Zr(()=>{console.log("[Settings] SW controller changed → refreshing tile-cache stats"),a()}),a()}function ha(){const r=document.getElementById("download-tiles-btn"),e=document.getElementById("offline-download-modal");if(!r||!e)return;const t=Gt.getOrCreateInstance(e),n=document.getElementById("offline-download-form-view"),o=document.getElementById("offline-download-progress-view"),a=document.getElementById("offline-download-done-view"),s=document.getElementById("offline-download-cancel-btn"),i=document.getElementById("offline-download-start-btn"),l=document.getElementById("offline-download-close-done-btn"),c=document.getElementById("offline-download-close-btn"),d=document.getElementById("offline-basemap-select"),u=document.getElementById("offline-min-zoom"),p=document.getElementById("offline-max-zoom"),h=document.getElementById("offline-ack-check"),f=document.getElementById("offline-estimate-detail"),b=document.getElementById("offline-estimate"),g=document.getElementById("offline-area-view"),m=document.getElementById("offline-area-district"),y=document.getElementById("offline-area-ghana"),w=document.getElementById("offline-area-view-info"),S=document.getElementById("offline-area-district-info"),x=document.getElementById("offline-progress-bar"),v=document.getElementById("offline-progress-percent"),L=document.getElementById("offline-progress-counts"),P=document.getElementById("offline-progress-ok"),T=document.getElementById("offline-progress-failed"),z=document.getElementById("offline-progress-eta"),U=document.getElementById("offline-done-title"),K=document.getElementById("offline-done-detail");let D=null;function ne(F){return F?F<1024*1024?(F/1024).toFixed(0)+" KB":F<1024*1024*1024?(F/(1024*1024)).toFixed(1)+" MB":(F/(1024*1024*1024)).toFixed(2)+" GB":"0 KB"}function V(F){if(!F||F<1e3)return"< 1 s";const H=Math.round(F/1e3);if(H<60)return H+" s";const j=Math.floor(H/60),J=H%60;return j<60?`${j} min ${J} s`:`${Math.floor(j/60)} h ${j%60} min`}function de(){return g.checked?_?.getCurrentViewExtent()||null:m.checked?_?.getDistrictBoundaryExtent()?.extent||null:y.checked?ls:null}function G(){const F=d.value,H=parseInt(u.value,10),j=parseInt(p.value,10);if(Number.isNaN(H)||Number.isNaN(j)||H>j){f.textContent="Invalid zoom range",b.classList.replace("alert-info","alert-warning"),i.disabled=!0;return}const J=de();if(!J){f.textContent="Selected area is not available.",b.classList.replace("alert-info","alert-warning"),i.disabled=!0;return}const C=Zo[F]?.maxZoom??19,q=Math.min(j,C),Q=rs(J,H,q),bt=cs(Q);let pe="";qZoom ${j} is above this provider's max (${C}); will clamp to ${C}.`),Q>8e3&&(pe+='
                More than 8 000 tiles — exceeds the per-provider cache limit. Earlier tiles will be evicted as new ones arrive.'),f.innerHTML=`${Q.toLocaleString()} tiles · ~${ne(bt)}`+pe,b.classList.toggle("alert-warning",!!pe),b.classList.toggle("alert-info",!pe),i.disabled=!h.checked||Q===0}function ue(){_?.getCurrentViewExtent()?w.textContent=" · ready":w.textContent="",_?.getDistrictBoundaryExtent()?(S.textContent="",m.disabled=!1):(S.textContent=" (not loaded — connect online to fetch)",m.disabled=!0,m.checked&&(g.checked=!0))}function Oe(){n.classList.remove("d-none"),o.classList.add("d-none"),a.classList.add("d-none"),i.classList.remove("d-none"),s.classList.remove("d-none"),s.textContent="Cancel",l.classList.add("d-none"),c.disabled=!1,h.checked=!1,i.disabled=!0,D=null}r.addEventListener("click",()=>{Oe(),ue(),G(),t.show()}),d.addEventListener("change",G),u.addEventListener("input",G),p.addEventListener("input",G),g.addEventListener("change",G),m.addEventListener("change",G),y.addEventListener("change",G),h.addEventListener("change",G),i.addEventListener("click",async()=>{const F=d.value,H=parseInt(u.value,10),j=parseInt(p.value,10),J=de();if(!J)return;n.classList.add("d-none"),o.classList.remove("d-none"),i.classList.add("d-none"),s.textContent="Cancel download",c.disabled=!0,x.style.width="0%",x.setAttribute("aria-valuenow","0"),v.textContent="0%",L.textContent="0 of 0 tiles",P.textContent="0",T.textContent="0",z.textContent="—",D=new is({baseMap:F,extent3857:J,minZoom:H,maxZoom:j,onProgress:q=>{if(q.total>0){const Q=Math.min(100,Math.round(q.done/q.total*100));x.style.width=Q+"%",x.setAttribute("aria-valuenow",String(Q)),v.textContent=Q+"%",L.textContent=`${q.done.toLocaleString()} of ${q.total.toLocaleString()} tiles`}P.textContent=q.ok.toLocaleString(),T.textContent=q.failed.toLocaleString(),z.textContent=q.etaMs!=null?V(q.etaMs):"—"}});let C;try{C=await D.start()}catch(q){console.error("[OfflineDownload] failed:",q),C={phase:"error",done:0,total:0,ok:0,failed:0}}o.classList.add("d-none"),a.classList.remove("d-none"),s.classList.add("d-none"),l.classList.remove("d-none"),c.disabled=!1,C.phase==="cancelled"?(U.textContent="Download cancelled",K.innerHTML=`Stopped after ${C.done.toLocaleString()} of ${C.total.toLocaleString()} tiles.
                ${C.ok.toLocaleString()} fetched · ${C.failed.toLocaleString()} failed.`):C.phase==="error"?(U.textContent="Download failed",K.textContent="See console for details."):(U.textContent="Download complete",K.innerHTML=`${C.ok.toLocaleString()} tiles cached`+(C.failed>0?`, ${C.failed.toLocaleString()} failed`:"")+`.
                Took ${V(C.elapsedMs)}.`)}),s.addEventListener("click",()=>{D&&D.cancel()}),e.addEventListener("hidden.bs.modal",()=>{D&&D.cancel(),Oe()})}function ga(){const r=tn(),e=document.getElementById("menu-btn"),t=document.getElementById("menu-user-avatar"),n=document.getElementById("menu-user-name"),o=document.getElementById("menu-user-email"),a=document.getElementById("menu-user-detail"),s=document.getElementById("menu-signout-btn"),i=document.getElementById("menu-signin-link"),l=document.getElementById("menu-no-session-note");if(!e||!t||!n||!o||!a||!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)),a.innerHTML=p.join(" · ")||"No district info",s.classList.remove("d-none"),s.addEventListener("click",()=>ma(r),{once:!1}),i?.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='',t.style.background="var(--brand-orange-warm, #ff9e1b)",n.textContent="No session injected",o.textContent="",a.textContent="",s.classList.add("d-none"),i?.classList.add("d-none"),l?.classList.remove("d-none"),e.dataset.state="no-session",e.setAttribute("title","Menu (no session — dev mode)")):(t.innerHTML='',t.style.background="var(--brand-gray-medium, #7a7a7a)",n.textContent="Not signed in",o.textContent="",a.textContent="",s.classList.add("d-none"),i?.classList.remove("d-none"),l?.classList.add("d-none"),e.dataset.state="unauthenticated",e.setAttribute("title","Menu (not signed in)"))}async function ma(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",wo):wo(); +//# sourceMappingURL=index-YjHYbDyk.js.map diff --git a/dist/assets/index-YjHYbDyk.js.map b/dist/assets/index-YjHYbDyk.js.map new file mode 100644 index 0000000..df8f333 --- /dev/null +++ b/dist/assets/index-YjHYbDyk.js.map @@ -0,0 +1 @@ +{"version":3,"mappings":"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","names":["FUNCTION","CHANNEL","GET","HAS","SET","isArray","SharedArrayBuffer","window","notify","wait","waitAsync","postPatched","buffer","onmessage","w","ids","resolvers","postMessage","listener","event","details","id","sb","data","rest","resolve","buff","as","Int32Array","Map","Uint16Array","I32_BYTES","UI16_BYTES","waitInterrupt","delay","handler","buffers","context","syncResult","fn","uid","coincident","self","parse","stringify","transform","interrupt","sendMessage","post","transfer","args","decoder","waitFor","isAsync","seppuku","_","action","deadlock","length","bytes","actions","callback","type","results","error","result","serialized","ui16a","i","createMutex","promise","res","normalizeDatabaseFile","dbFile","convertStreamTo","bufferOrStream","reader","chunks","streamDone","chunk","arrayLength","offset","SQLiteMemoryDriver","sqlite3InitModule","config","databasePath","flags","__vitePreload","statement","statements","tx","prepared","stmt","newStmt","columns","rows","size","database","dataPointer","resultCode","pointer","readOnly","verbose","db","statementData","opMap","_ctx","opId","_db","table","rowid","cb","debounce","func","options","lastArgs","lastThis","maxWait","timerId","lastCallTime","lastInvokeTime","leading","maxing","trailing","invokeFunc","time","thisArg","leadingEdge","timerExpired","remainingWait","timeSinceLastCall","timeSinceLastInvoke","timeWaiting","shouldInvoke","trailingEdge","cancel","flush","debounced","isInvoking","getQueryKey","getDatabaseKey","clientKey","sessionKey","SQLocalProcessor","driver","reason","dbKey","message","change","_transfer","response","partOfTransaction","sql","name","queryKey","errored","proxy","sqlTag","queryTemplate","params","isArrayOfArrays","row","convertRowsToObjects","checkedRows","rowObj","column","columnIndex","isDrizzleStatement","isStatement","normalizeStatement","drizzleStatement","exec","normalizeSql","maybeQueryTemplate","mutationLock","mode","bypass","mutation","SQLiteKvvfsDriver","storageType","memdb","_a","_b","SQLocal","queries","reject","userCallback","method","transactionKey","resultIndex","passStatements","query","passStatement","transaction","err","value","gotFirstValue","isListening","updateCount","watchedTables","subObservers","errObservers","runStatement","updateOrder","usedTables","readTables","writtenTables","observer","onEffect","onData","onError","funcName","key","attachFunction","databaseFile","beforeUnlock","clientConfig","onInit","onConnect","processor","commonConfig","DATABASE_PATH","BROADCAST_CHANNEL","channel","isReady","readyResolve","readyReject","dbReady","changeListeners","onDatabaseChange","payload","e","broadcastChange","initSchema","testResult","tablesAfterLocations","cols","c","allTables","t","addLocation","longitude","latitude","description","category","tableCheck","newId","verifyResult","getLocations","limit","getLocationCount","saveRemoteData","json","getRemoteData","parsed","saveCollectorZones","zones","z","props","getLocalCollectorZones","r","numOrNull","v","n","saveParcels","parcels","saved","p","wkt","getLocalParcels","updateParcel","parcelId","insertNewParcel","geometryWkt","saveBuildingFootprints","footprints","first","types","k","f","rawWkt","rawId","getLocalBuildingFootprints","saveOSMRoads","roads","getLocalOSMRoads","exportDatabase","downloadDatabase","filename","blob","url","a","exportToGeoJSON","loc","getDatabaseStatus","tables","locationCount","CACHED_LAYER_TABLES","isCachedLayerTable","tableName","clearTable","count","clearAllCachedLayers","existing","existingNames","total","s","getTableStats","getTableContent","testDatabase","version","createGpsTrail","meta","uuid","startedAt","districtId","addGpsTrailPoint","trailId","point","seq","lon","lat","altitude","accuracy","altitudeAccuracy","heading","speed","satellites","timestamp","recordedAt","finishGpsTrail","summary","endedAt","pointCount","distanceM","getUnsyncedGpsTrails","getGpsTrailPoints","markGpsTrailSynced","remoteId","M_TO_FT","M_TO_MI","SQM_TO_SQFT","SQM_TO_ACRE","SQM_TO_SQMI","getSystem","formatLength","metres","ft","formatLengthFull","mi","formatArea","sqMetres","acres","formatAreaFull","sqft","sqmi","formatCircleExtent","radiusMetres","segmentIntersection","p1","p2","p3","p4","eps","dx1","dy1","dx2","dy2","denom","dx3","dy3","u","signedArea","ring","area","pointInRing","pt","inside","j","xi","yi","xj","yj","dist2","b","findIntersections","line","hits","li","ri","ix","isDup","h","insertPointsIntoRing","sorted","expanded","indices","insertIdx","snapDist","ringSlice","i0","i1","start","end","idx","cuttingLineSlice","hit0","hit1","startSeg","endSeg","ensureWinding","ccw","closeRing","coords","last","extendLineOutsideRing","minX","minY","maxX","maxY","diag","p0","dx","dy","len","scale","pN","pN1","splitPolygonByLine","polygonCoords","lineCoords","exteriorRing","holes","extendedLine","expandedRing","idx0","idx1","iA","iB","cutForward","cutReverse","sliceAB","ringA","sliceBA","ringB","originalCCW","finalA","finalB","polyA","polyB","hole","centroid","holeCentroid","cx","cy","THEMES","container","ensureContainer","showToast","duration","parent","theme","el","dismiss","SPLIT_COLORS","HIGHLIGHT_STYLE","Style","Stroke","Fill","SKETCH_STYLE","CircleStyle","PolygonSplitInteraction","ol_interaction_Interaction","VectorSource","VectorLayer","map","active","sources","collect","layers","layer","hit","clone","best","bestDist","source","feat","geom","closest","distPx","LineString","ol_interaction_Draw","evt","cuttingLine","cuttingLineCoords","feature","coordsA","coordsB","featureA","PolygonGeom","featureB","splitFeatures","distToSegmentSq","segA","segB","lenSq","projX","projY","findClosestEdge","clickCoord","bestIdx","d","coordsEqual","tolSq","isVertexNearRing","findSharedBoundary","seedIdxA","seedIdxB","tolerance","nA","nB","a0","a1","b0","b1","a0NearB","a1NearB","b0NearA","b1NearA","reversed","startA","endA","startB","endB","safety","nextA","nextB","prevA","prevB","walkRing","fromIdx","toIdx","mergePolygons","polygonCoordsA","polygonCoordsB","clickCoordA","clickCoordB","holesA","holesB","seedA","seedB","shared","partA","partB","merged","mergedRing","areaA","areaB","areaMerged","expectedArea","finalRing","validHoles","HIGHLIGHT_A","HIGHLIGHT_B","LABEL_A","Text","LABEL_B","EDGE_STYLE","MERGE_STYLE","PolygonMergeInteraction","skipFeature","style","edge","edgeFeat","Feature","resolution","bestSeg","sourceA","sourceB","geomA","geomB","mergedFeature","evtData","afterEvt","isParcelA","isParcelB","toRemove","cloneA","cloneB","polygonArea","longestEdge","bestLen","bestI","along","perp","makeCuttingLine","origin","extent","centroidT","sx","sy","dividePolygon","edgeCoords","nVerts","perpMin","perpMax","pieces","remaining","remainingCount","remainingArea","targetArea","remRing","remN","rMin","rMax","lo","hi","bestPiece","bestRemaining","bestError","iter","mid","nudge","lineA","resultA","halfA","halfB","tA","tB","nearPiece","farPiece","nearArea","lineB","resultB","pieceColors","colors","hue","PolygonDivideInteraction","ext","center","newFeatures","MapView","targetId","cat","emoji","label","fontSize","baseLayers","LayerGroup","View","fromLonLat","layerSwitcher","LayerSwitcher","btn","baseUrl","_lsChromeScheduled","ScaleLine","searchNominatim","SearchNominatim","searchResult","lonLat","coordinate","Circle","mapLayers","overlayIdx","Select","clickCondition","ModifyFeature","UndoRedo","EditBar","extraBar","Bar","Button","Split","idFields","field","splitLineToggle","Toggle","splitPolyToggle","splitDivideToggle","splitSubBar","splitParentToggle","mergeToggle","editbarEl","breakEl","SnapGuides","VectorImageLayer","drawToolNames","interaction","snapToggleBtn","visible","TouchCursor","listeners","selected","Point","out","isCoord","visitRing","isPolygonRing","poly","walk","sub","system","Overlay","currentFeature","html","catColor","title","color","properties","geometry","geomType","skipKeys","areaSqm","getArea","areaFormatted","lengthM","getLength","lengthFormatted","toLonLat","parcelFeatures","zoneFeatures","otherByLayer","dataRows","names","features","tableRows","labelColor","border","pdfRows","exportAnalysisPDF","circleFeature","circleGeom","circlePoly","fromCircle","circleExtent","radius","intersectsCircle","fExtent","scanGroup","group","groupTitle","layerTitle","candidates","fType","radiusFormatted","polygonFeature","polyGeom","polyExtent","perimeterM","perimeterFormatted","intersectsPoly","typeB","flatB","stride","flatA","strideA","fieldsHtml","displayVal","escapedKey","escapedVal","form","formData","updatedProps","propsA","propsB","getLabel","labelA","labelB","close","chosenProps","labels","radios","updateHighlight","lbl","radio","input","keys","attributeKeys","clickedFeature","text","div","coordsEl","defaultBasemap","topoLayer","TileLayer","XYZ","cartoLightLayer","cartoDarkLayer","osmCycleLayer","OSM","satelliteLayer","googleLayer","osmLayer","baseGroup","matched","on","OPTIONS","target","panel","opt","syncSelection","open","resAtLat","halo","dot","radiusMeters","segments","zoom","toggle","next","customStyle","styles","locations","featureOrId","padding","hasOverlayFeature","hasParcelFeature","markerFeature","index","hoveredFeature","geojson","styleOptions","targetGroup","strokeColor","strokeWidth","fillColor","lineCasingColor","lineCasingWidth","pointRadius","pointFillColor","pointStrokeColor","pointStrokeWidth","GeoJSON","fillStyle","pointStyle","layerStyle","casingW","describeFromGeom","feats","initial","once","ev","desc","wmsSource","TileWMS","wmsLayer","xyzSource","xyzLayer","card","nameRow","nameHint","urlInput","layerName","dlg","inp","wmsSrc","wfsUrl","wfsSource","tag","labelSpan","chip","btnBar","chevronEl","content","ensureSubtitle","removeBtn","addBtn","visit","removed","child","panelContainer","ul","badge","footer","counts","totalOverlays","activeOverlays","HIDDEN_INTERNAL","refresh","hookLayer","added","legendUrl","wrapper","update","children","str","found","view","src","ex","MapTools","l","drawCircle","Draw","output","radiusLine","unByKey","drawLine","drawPolygon","drawPoint","mainBar","measureBar","circleBtn","lineBtn","areaBtn","clearBtn","swRegistration","registerServiceWorker","newWorker","showUpdateNotification","deferredPrompt","installButton","initInstallPrompt","buttonSelector","showManualInstallInstructions","outcome","isIOS","isSafari","offlineIndicator","offlineListeners","initOfflineDetection","indicatorSelector","updateOfflineUI","notifyOfflineListeners","isOffline","onOfflineChange","isOnline","applyUpdate","getActiveServiceWorker","timeoutMs","ready","timeout","registration","sw","onServiceWorkerControllerChange","requestFromServiceWorker","requestType","responseType","extra","readyTimeoutMs","timer","getTileCacheStats","clearTileCaches","clearTileCacheForProvider","cacheName","getStorageEstimate","usage","quota","initPWA","autoRegisterSW","BASEMAP_TEMPLATES","AVG_TILE_BYTES","ORIGIN_SHIFT","metersToLonLat","x","y","lonLatToTile","latRad","tileRangeForExtent","extent3857","minLon","minLat","maxLon","maxLat","tl","br","minTileX","maxTileX","minTileY","maxTileY","countTiles","minZ","maxZ","enumerateTiles","formatTileUrl","template","OfflineTileDownloader","baseMap","minZoom","maxZoom","concurrency","interBatchDelayMs","onProgress","tpl","tiles","done","ok","failed","cached","emit","phase","elapsedMs","etaMs","batch","GHANA_EXTENT_3857","lonLatToMeters","ne","estimatedSizeBytes","tileCount","API_BASE","FALLBACK_DISTRICT_ID","API_TOKEN","resolveDistrictId","session","API_CREDENTIALS","getSession","raw","REQUEST_TIMEOUT","PING_TIMEOUT","_serverReachable","checkServerReachable","force","controller","isServerReachable","withTimeout","ms","remotePost","endpoint","body","getDistrictBoundary","getLayers","getCollectorZones","getDistrictParcels","getBuildingFootprints","getContoursHillshade","getOSMRoads","pushGpsTrail","trail","points","EARTH_RADIUS_M","DEG2RAD","haversineMeters","lon1","lat1","lon2","lat2","dLat","dLon","formatCoord","decimals","formatDistance","meters","formatAccuracy","accuracyQuality","DEFAULTS","GeoTracker","set","pos","fix","trailMeta","synced","pushed","trails","trailRow","minIntervalMs","minDistanceM","heartbeatMs","maxAccuracyM","now","keep","stepM","elapsed","num","ch","sqlocalStorage","remote","remoteSync","geoTracker","KNOWN_COMMANDS","createEmbedBridge","mapView","embedConfig","highlightSource","highlightLayer","parcelsLayer","pendingSelectUpn","readyEmitted","send","emitError","code","emitReady","parcelPayload","outLon","outLat","getCenter","highlightFeature","_markerFeature","parcelFeature","msg","selectByUpn","upn","attachParcelsLayer","drain","scheduled","_shpModule","getShp","mod","mapTools","embedBridge","EMBED_CONFIG","IS_EMBED_PERMIT","currentMode","showNoDistrictBlockerIfNeeded","overlay","escapeHtml","initApp","savedBasemap","initGpsTracking","showLocationDetails","layerType","loadLocations","showSuccess","showError","wktFormat","WKT","wktString","status","showWarning","loadLayers","loadDistrictBoundary","loadCollectorZones","loadParcels","loadBuildingFootprints","loadContoursHillshade","loadOSMRoads","loadExternalWMSLayers","initUI","statsContainer","refreshLocalDataStats","offline","syncData","initFieldworkMode","initMeasurementSystem","initDarkMode","initDefaultBasemap","initOfflineTileCache","initOfflineDownloadDialog","initAccountCard","initMessageLog","exportBtn","handleExport","localDataBtn","importShpBtn","shpFileInput","handleShapefileImport","importGeoJSONBtn","geojsonFileInput","handleGeoJSONImport","importKMLBtn","kmlFileInput","handleKMLImport","initMapDropZone","exportGeoJSONBtn","handleExportGeoJSON","statusBtn","handleShowStatus","fitBtn","addLocationBtn","measureCircleBtn","measureLineBtn","measureAreaBtn","drawBtn","modeButtons","setMode","activeBtn","renderLocations","countEl","mobileCount","categoryEmojis","item","tbody","clearAllBtn","stats","link","showTableContent","handleClearAllCachedLayers","modalTitle","modalBody","modalInfo","Modal","headerCells","bodyRows","val","display","statusContent","parseCoordRing","ringStr","pair","parseWKTPolygon","parseWKTMultiPolygon","polyStr","parseWKT","trimmed","apiResponseToGeoJSON","apiResponse","boundary","districtid","district_name","zonesToGeoJSON","zone","CACHE_KEY","boundaryStyle","adminGroup","removeBoundaryLayer","zoomToBoundary","zoneStyle","emptyGeoJSON","zonesLayer","setZoneFeatures","parcelsToGeoJSON","seen","parcel","parcelStyle","landUseGroup","setParcelFeatures","footprintsToGeoJSON","geomKeys","fp","footprintStyle","physInfraGroup","footprintsLayer","setFootprintFeatures","wktRowsToGeoJSON","geojsonFormat","olGeom","flattenProps","contoursStyle","biophysGroup","contoursLayer","roadsStyle","roadsLayer","setRoadFeatures","createLayerGroupsOnMap","overlayLayers","importedFileLayers","IMPORT_STYLE","showFileImportError","logMessage","addImportedGeoJSON","geojsonInput","fallbackName","collections","totalFeatures","fc","lastLayer","refreshImportedLayersCard","infoEl","listEl","removeImportedLayer","removeImportedLayers","overlayGroup","indexFilesByExtension","files","MAX_FILE_SIZE","totalSize","sizeMB","displayName","byExt","file","missing","shpObj","KML","dragCounter","exts","fakeEvt","MESSAGE_LOG_MAX","MSG_CONFIG","cfg","log","placeholder","entry","readout","accEl","satsEl","trySync","applyDark","updateLabel","select","statsEl","offcanvas","fmtBytes","refreshInFlight","swActive","storageNote","est","pct","triggerBtn","modalEl","modal","formView","progressView","doneView","cancelBtn","startBtn","closeDoneBtn","headerCloseBtn","basemapSelect","minZoomInput","maxZoomInput","ackCheck","estimateEl","estimateBox","areaViewRadio","areaDistrictRadio","areaGhanaRadio","areaViewInfo","areaDistrictInfo","progressBar","progressPercent","progressCounts","progressOk","progressFailed","progressEta","doneTitle","doneDetail","currentDownloader","fmtDuration","m","getSelectedExtent","updateEstimate","tplMaxZoom","effMaxZ","warningHTML","updateAreaInfos","resetModal","menuBtn","avatarEl","nameEl","emailEl","detailEl","signoutBtn","signinLink","noSessNote","bits","handleSignOut","cookieToken","past"],"ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18],"sources":["../../node_modules/proxy-target/esm/types.js","../../node_modules/coincident/esm/channel.js","../../node_modules/proxy-target/esm/traps.js","../../node_modules/coincident/esm/bridge.js","../../node_modules/coincident/esm/index.js","../../node_modules/sqlocal/dist/lib/create-mutex.js","../../node_modules/sqlocal/dist/lib/normalize-database-file.js","../../node_modules/sqlocal/dist/drivers/sqlite-memory-driver.js","../../node_modules/sqlocal/dist/lib/debounce.js","../../node_modules/sqlocal/dist/lib/get-query-key.js","../../node_modules/sqlocal/dist/lib/get-database-key.js","../../node_modules/sqlocal/dist/processor.js","../../node_modules/sqlocal/dist/lib/sql-tag.js","../../node_modules/sqlocal/dist/lib/convert-rows-to-objects.js","../../node_modules/sqlocal/dist/lib/normalize-statement.js","../../node_modules/sqlocal/dist/lib/normalize-sql.js","../../node_modules/sqlocal/dist/lib/mutation-lock.js","../../node_modules/sqlocal/dist/drivers/sqlite-kvvfs-driver.js","../../node_modules/sqlocal/dist/client.js","../../src/database.js","../../src/units.js","../../src/geom/polygonSplit.js","../../src/toast.js","../../src/interactions/PolygonSplitInteraction.js","../../src/geom/polygonMerge.js","../../src/interactions/PolygonMergeInteraction.js","../../src/geom/polygonDivide.js","../../src/interactions/PolygonDivideInteraction.js","../../src/components/MapView.js","../../src/components/MapTools.js","../../src/pwa.js","../../src/offlineTiles.js","../../src/remotedb.js","../../src/geotracker/geo-utils.js","../../src/geotracker/GeoTracker.js","../../src/geotracker-lupmis.js","../../src/embed-bridge.js","../../main.js"],"sourcesContent":["export const ARRAY = 'array';\nexport const BIGINT = 'bigint';\nexport const BOOLEAN = 'boolean';\nexport const FUNCTION = 'function';\nexport const NULL = 'null';\nexport const NUMBER = 'number';\nexport const OBJECT = 'object';\nexport const STRING = 'string';\nexport const SYMBOL = 'symbol';\nexport const UNDEFINED = 'undefined';\n","// ⚠️ AUTOMATICALLY GENERATED - DO NOT CHANGE\nexport const CHANNEL = '64e10b34-2bf7-4616-9668-f99de5aa046e';\n\nexport const MAIN = 'M' + CHANNEL;\nexport const THREAD = 'T' + CHANNEL;\n","export const APPLY = 'apply';\nexport const CONSTRUCT = 'construct';\nexport const DEFINE_PROPERTY = 'defineProperty';\nexport const DELETE_PROPERTY = 'deleteProperty';\nexport const GET = 'get';\nexport const GET_OWN_PROPERTY_DESCRIPTOR = 'getOwnPropertyDescriptor';\nexport const GET_PROTOTYPE_OF = 'getPrototypeOf';\nexport const HAS = 'has';\nexport const IS_EXTENSIBLE = 'isExtensible';\nexport const OWN_KEYS = 'ownKeys';\nexport const PREVENT_EXTENSION = 'preventExtensions';\nexport const SET = 'set';\nexport const SET_PROTOTYPE_OF = 'setPrototypeOf';\n","// The goal of this file is to normalize SAB\n// at least in main -> worker() use cases.\n// This still cannot possibly solve the sync\n// worker -> main() use case if SharedArrayBuffer\n// is not available or usable.\n\nimport {CHANNEL} from './channel.js';\n\nconst {isArray} = Array;\n\nlet {SharedArrayBuffer, window} = globalThis;\nlet {notify, wait, waitAsync} = Atomics;\nlet postPatched = null;\n\n// This is needed for some version of Firefox\nif (!waitAsync) {\n waitAsync = buffer => ({\n value: new Promise(onmessage => {\n // encodeURIComponent('onmessage=({data:b})=>(Atomics.wait(b,0),postMessage(0))')\n let w = new Worker('data:application/javascript,onmessage%3D(%7Bdata%3Ab%7D)%3D%3E(Atomics.wait(b%2C0)%2CpostMessage(0))');\n w.onmessage = onmessage;\n w.postMessage(buffer);\n })\n });\n}\n\n// Monkey-patch SharedArrayBuffer if needed\ntry {\n new SharedArrayBuffer(4);\n}\ncatch (_) {\n SharedArrayBuffer = ArrayBuffer;\n\n const ids = new WeakMap;\n // patch only main -> worker():async use case\n if (window) {\n const resolvers = new Map;\n const {prototype: {postMessage}} = Worker;\n\n const listener = event => {\n const details = event.data?.[CHANNEL];\n if (!isArray(details)) {\n event.stopImmediatePropagation();\n const { id, sb } = details;\n resolvers.get(id)(sb);\n }\n };\n\n postPatched = function (data, ...rest) {\n const details = data?.[CHANNEL];\n if (isArray(details)) {\n const [id, sb] = details;\n ids.set(sb, id);\n this.addEventListener('message', listener);\n }\n return postMessage.call(this, data, ...rest);\n };\n\n waitAsync = sb => ({\n value: new Promise(resolve => {\n resolvers.set(ids.get(sb), resolve);\n }).then(buff => {\n resolvers.delete(ids.get(sb));\n ids.delete(sb);\n for (let i = 0; i < buff.length; i++) sb[i] = buff[i];\n return 'ok';\n })\n });\n }\n else {\n const as = (id, sb) => ({[CHANNEL]: { id, sb }});\n\n notify = sb => {\n postMessage(as(ids.get(sb), sb));\n };\n\n addEventListener('message', event => {\n const details = event.data?.[CHANNEL];\n if (isArray(details)) {\n const [id, sb] = details;\n ids.set(sb, id);\n }\n });\n }\n}\n\nexport {SharedArrayBuffer, isArray, notify, postPatched, wait, waitAsync};\n","/*! (c) Andrea Giammarchi - ISC */\n\nimport {FUNCTION} from 'proxy-target/types';\n\nimport {CHANNEL} from './channel.js';\nimport {GET, HAS, SET} from './shared/traps.js';\n\nimport {SharedArrayBuffer, isArray, notify, postPatched, wait, waitAsync} from './bridge.js';\n\n// just minifier friendly for Blob Workers' cases\nconst {Int32Array, Map, Uint16Array} = globalThis;\n\n// common constants / utilities for repeated operations\nconst {BYTES_PER_ELEMENT: I32_BYTES} = Int32Array;\nconst {BYTES_PER_ELEMENT: UI16_BYTES} = Uint16Array;\n\nconst waitInterrupt = (sb, delay, handler) => {\n while (wait(sb, 0, 0, delay) === 'timed-out')\n handler();\n};\n\n// retain buffers to transfer\nconst buffers = new WeakSet;\n\n// retain either main threads or workers global context\nconst context = new WeakMap;\n\nconst syncResult = {value: {then: fn => fn()}};\n\n// used to generate a unique `id` per each worker `postMessage` \"transaction\"\nlet uid = 0;\n\n/**\n * @typedef {Object} Interrupt used to sanity-check interrupts while waiting synchronously.\n * @prop {function} [handler] a callback invoked every `delay` milliseconds.\n * @prop {number} [delay=42] define `handler` invokes in terms of milliseconds.\n */\n\n/**\n * Create once a `Proxy` able to orchestrate synchronous `postMessage` out of the box.\n * @param {globalThis | Worker} self the context in which code should run\n * @param {{parse: (serialized: string) => any, stringify: (serializable: any) => string, transform?: (value:any) => any, interrupt?: () => void | Interrupt}} [JSON] an optional `JSON` like interface to `parse` or `stringify` content with extra `transform` ability.\n * @returns {ProxyHandler | ProxyHandler}\n */\nconst coincident = (self, {parse = JSON.parse, stringify = JSON.stringify, transform, interrupt} = JSON) => {\n // create a Proxy once for the given context (globalThis or Worker instance)\n if (!context.has(self)) {\n // ensure no SAB gets a chance to pass through this call\n const sendMessage = postPatched || self.postMessage;\n // ensure the CHANNEL and data are posted correctly\n const post = (transfer, ...args) => sendMessage.call(self, {[CHANNEL]: args}, {transfer});\n\n const handler = typeof interrupt === FUNCTION ? interrupt : interrupt?.handler;\n const delay = interrupt?.delay || 42;\n const decoder = new TextDecoder('utf-16');\n\n // automatically uses sync wait (worker -> main)\n // or fallback to async wait (main -> worker)\n const waitFor = (isAsync, sb) => isAsync ?\n waitAsync(sb, 0) :\n ((handler ? waitInterrupt(sb, delay, handler) : wait(sb, 0)), syncResult);\n\n // prevent Harakiri https://github.com/WebReflection/coincident/issues/18\n let seppuku = false;\n\n context.set(self, new Proxy(new Map, {\n // there is very little point in checking prop in proxy for this very specific case\n // and I don't want to orchestrate a whole roundtrip neither, as stuff would fail\n // regardless if from Worker we access non existent Main callback, and vice-versa.\n // This is here mostly to guarantee that if such check is performed, at least the\n // get trap goes through and then it's up to developers guarantee they are accessing\n // stuff that actually exists elsewhere.\n [HAS]: (_, action) => typeof action === 'string' && !action.startsWith('_'),\n\n // worker related: get any utility that should be available on the main thread\n [GET]: (_, action) => action === 'then' ? null : ((...args) => {\n // transaction id\n const id = uid++;\n\n // first contact: just ask for how big the buffer should be\n // the value would be stored at index [1] while [0] is just control\n let sb = new Int32Array(new SharedArrayBuffer(I32_BYTES * 2));\n\n // if a transfer list has been passed, drop it from args\n let transfer = [];\n if (buffers.has(args.at(-1) || transfer))\n buffers.delete(transfer = args.pop());\n\n // ask for invoke with arguments and wait for it\n post(transfer, id, sb, action, transform ? args.map(transform) : args);\n\n // helps deciding how to wait for results\n const isAsync = self !== globalThis;\n\n // warn users about possible deadlock still allowing them\n // to explicitly `proxy.invoke().then(...)` without blocking\n let deadlock = 0;\n if (seppuku && isAsync)\n deadlock = setTimeout(console.warn, 1000, `💀🔒 - Possible deadlock if proxy.${action}(...args) is awaited`);\n\n return waitFor(isAsync, sb).value.then(() => {\n clearTimeout(deadlock);\n\n // commit transaction using the returned / needed buffer length\n const length = sb[1];\n\n // filter undefined results\n if (!length) return;\n\n // calculate the needed ui16 bytes length to store the result string\n const bytes = UI16_BYTES * length;\n\n // round up to the next amount of bytes divided by 4 to allow i32 operations\n sb = new Int32Array(new SharedArrayBuffer(bytes + (bytes % I32_BYTES)));\n\n // ask for results and wait for it\n post([], id, sb);\n return waitFor(isAsync, sb).value.then(() => parse(\n decoder.decode(new Uint16Array(sb.buffer).slice(0, length)))\n );\n });\n }),\n\n // main thread related: react to any utility a worker is asking for\n [SET](actions, action, callback) {\n const type = typeof callback;\n if (type !== FUNCTION)\n throw new Error(`Unable to assign ${action} as ${type}`);\n // lazy event listener and logic handling, triggered once by setters actions\n if (!actions.size) {\n // maps results by `id` as they are asked for\n const results = new Map;\n // add the event listener once (first defined setter, all others work the same)\n self.addEventListener('message', async (event) => {\n // grub the very same library CHANNEL; ignore otherwise\n const details = event.data?.[CHANNEL];\n if (isArray(details)) {\n // if early enough, avoid leaking data to other listeners\n event.stopImmediatePropagation();\n const [id, sb, ...rest] = details;\n let error;\n // action available: it must be defined/known on the main thread\n if (rest.length) {\n const [action, args] = rest;\n if (actions.has(action)) {\n seppuku = true;\n try {\n // await for result either sync or async and serialize it\n const result = await actions.get(action)(...args);\n if (result !== void 0) {\n const serialized = stringify(transform ? transform(result) : result);\n // store the result for \"the very next\" event listener call\n results.set(id, serialized);\n // communicate the required SharedArrayBuffer length out of the\n // resulting serialized string\n sb[1] = serialized.length;\n }\n }\n catch (_) {\n error = _;\n }\n finally {\n seppuku = false;\n }\n }\n // unknown action should be notified as missing on the main thread\n else {\n error = new Error(`Unsupported action: ${action}`);\n }\n // unlock the wait lock later on\n sb[0] = 1;\n }\n // no action means: get results out of the well known `id`\n // wait lock automatically unlocked here as no `0` value would\n // possibly ever land at index `0`\n else {\n const result = results.get(id);\n results.delete(id);\n // populate the SharedArrayBuffer with utf-16 chars code\n for (let ui16a = new Uint16Array(sb.buffer), i = 0; i < result.length; i++)\n ui16a[i] = result.charCodeAt(i);\n }\n // release te worker waiting either the length or the result\n notify(sb, 0);\n if (error) throw error;\n }\n });\n }\n // store this action callback allowing the setter in the process\n return !!actions.set(action, callback);\n }\n }));\n }\n return context.get(self);\n};\n\ncoincident.transfer = (...args) => (buffers.add(args), args);\n\nexport default coincident;\n","export function createMutex() {\n let promise;\n let resolve;\n const lock = async () => {\n while (promise) {\n await promise;\n }\n promise = new Promise((res) => {\n resolve = res;\n });\n };\n const unlock = async () => {\n const res = resolve;\n promise = undefined;\n resolve = undefined;\n res?.();\n };\n return { lock, unlock };\n}\n//# sourceMappingURL=create-mutex.js.map","export async function normalizeDatabaseFile(dbFile, convertStreamTo) {\n let bufferOrStream;\n if (dbFile instanceof Blob) {\n bufferOrStream = dbFile.stream();\n }\n else {\n bufferOrStream = dbFile;\n }\n if (bufferOrStream instanceof ReadableStream && convertStreamTo) {\n const stream = bufferOrStream;\n const reader = stream.getReader();\n switch (convertStreamTo) {\n case 'callback':\n return async () => {\n const chunk = await reader.read();\n return chunk.value;\n };\n case 'buffer':\n const chunks = [];\n let streamDone = false;\n while (!streamDone) {\n const chunk = await reader.read();\n if (chunk.value)\n chunks.push(chunk.value);\n streamDone = chunk.done;\n }\n const arrayLength = chunks.reduce((length, chunk) => {\n return length + chunk.length;\n }, 0);\n const buffer = new Uint8Array(arrayLength);\n let offset = 0;\n chunks.forEach((chunk) => {\n buffer.set(chunk, offset);\n offset += chunk.length;\n });\n return buffer.buffer;\n }\n }\n else {\n return bufferOrStream;\n }\n}\n//# sourceMappingURL=normalize-database-file.js.map","import { normalizeDatabaseFile } from '../lib/normalize-database-file.js';\nexport class SQLiteMemoryDriver {\n constructor(sqlite3InitModule) {\n Object.defineProperty(this, \"sqlite3InitModule\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: sqlite3InitModule\n });\n Object.defineProperty(this, \"sqlite3\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"db\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"config\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"pointers\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: []\n });\n Object.defineProperty(this, \"writeCallbacks\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: new Set()\n });\n Object.defineProperty(this, \"storageType\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: 'memory'\n });\n }\n async init(config) {\n const { databasePath } = config;\n const flags = this.getFlags(config);\n if (!this.sqlite3InitModule) {\n const { default: sqlite3InitModule } = await import('@sqlite.org/sqlite-wasm');\n this.sqlite3InitModule = sqlite3InitModule;\n }\n if (!this.sqlite3) {\n this.sqlite3 = await this.sqlite3InitModule();\n }\n if (this.db) {\n await this.destroy();\n }\n this.db = new this.sqlite3.oo1.DB(databasePath, flags);\n this.config = config;\n this.initWriteHook();\n }\n onWrite(callback) {\n this.writeCallbacks.add(callback);\n return () => {\n this.writeCallbacks.delete(callback);\n };\n }\n async exec(statement) {\n if (!this.db)\n throw new Error('Driver not initialized');\n return this.execOnDb(this.db, statement);\n }\n async execBatch(statements) {\n if (!this.db)\n throw new Error('Driver not initialized');\n const results = [];\n this.db.transaction((tx) => {\n const prepared = new Map();\n try {\n for (let statement of statements) {\n let stmt = prepared.get(statement.sql);\n if (!stmt) {\n const newStmt = tx.prepare(statement.sql);\n prepared.set(statement.sql, newStmt);\n stmt = newStmt;\n }\n if (statement.params?.length) {\n stmt.bind(statement.params);\n }\n let columns = [];\n let rows = [];\n while (stmt.step()) {\n columns = stmt.getColumnNames([]);\n rows.push(stmt.get([]));\n }\n results.push({ columns, rows });\n stmt.reset();\n }\n }\n finally {\n prepared.forEach((stmt) => {\n stmt.finalize();\n });\n }\n });\n return results;\n }\n async isDatabasePersisted() {\n return false;\n }\n async getDatabaseSizeBytes() {\n const sizeResult = await this.exec({\n sql: `SELECT page_count * page_size AS size \n\t\t\t\tFROM pragma_page_count(), pragma_page_size()`,\n method: 'get',\n });\n const size = sizeResult?.rows?.[0];\n if (typeof size !== 'number') {\n throw new Error('Failed to query database size');\n }\n return size;\n }\n async createFunction(fn) {\n if (!this.db)\n throw new Error('Driver not initialized');\n switch (fn.type) {\n case 'callback':\n case 'scalar':\n this.db.createFunction({\n name: fn.name,\n xFunc: (_, ...args) => fn.func(...args),\n arity: -1,\n });\n break;\n case 'aggregate':\n this.db.createFunction({\n name: fn.name,\n xStep: (_, ...args) => fn.func.step(...args),\n xFinal: (_, ...args) => fn.func.final(...args),\n arity: -1,\n });\n break;\n }\n }\n async import(database) {\n if (!this.sqlite3 || !this.db || !this.config) {\n throw new Error('Driver not initialized');\n }\n const data = await normalizeDatabaseFile(database, 'buffer');\n const dataPointer = this.sqlite3.wasm.allocFromTypedArray(data);\n this.pointers.push(dataPointer);\n const resultCode = this.sqlite3.capi.sqlite3_deserialize(this.db, 'main', dataPointer, data.byteLength, data.byteLength, this.config.readOnly\n ? this.sqlite3.capi.SQLITE_DESERIALIZE_READONLY\n : this.sqlite3.capi.SQLITE_DESERIALIZE_RESIZEABLE);\n this.db.checkRc(resultCode);\n }\n async export() {\n if (!this.sqlite3 || !this.db) {\n throw new Error('Driver not initialized');\n }\n return {\n name: 'database.sqlite3',\n data: this.sqlite3.capi.sqlite3_js_db_export(this.db),\n };\n }\n async clear() { }\n async destroy() {\n this.closeDb();\n this.pointers.forEach((pointer) => this.sqlite3?.wasm.dealloc(pointer));\n this.pointers = [];\n this.writeCallbacks.clear();\n }\n getFlags(config) {\n const { readOnly, verbose } = config;\n const parts = [readOnly === true ? 'r' : 'cw', verbose === true ? 't' : ''];\n return parts.join('');\n }\n execOnDb(db, statement) {\n const statementData = {\n rows: [],\n columns: [],\n };\n const rows = db.exec({\n sql: statement.sql,\n bind: statement.params,\n returnValue: 'resultRows',\n rowMode: 'array',\n columnNames: statementData.columns,\n });\n switch (statement.method) {\n case 'run':\n break;\n case 'get':\n statementData.rows = rows[0] ?? [];\n break;\n case 'all':\n default:\n statementData.rows = rows;\n break;\n }\n return statementData;\n }\n initWriteHook() {\n if (!this.config?.reactive)\n return;\n if (!this.sqlite3 || !this.db) {\n throw new Error('Driver not initialized');\n }\n const opMap = {\n [this.sqlite3.capi.SQLITE_INSERT]: 'insert',\n [this.sqlite3.capi.SQLITE_UPDATE]: 'update',\n [this.sqlite3.capi.SQLITE_DELETE]: 'delete',\n };\n this.sqlite3.capi.sqlite3_update_hook(this.db, (_ctx, opId, _db, table, rowid) => {\n this.writeCallbacks.forEach((cb) => {\n cb({ table, rowid, operation: opMap[opId] });\n });\n }, 0);\n }\n closeDb() {\n if (this.db) {\n this.db.close();\n this.db = undefined;\n }\n }\n}\n//# sourceMappingURL=sqlite-memory-driver.js.map","/**\n * Lodash (Custom Build) \n * Build: `lodash modularize exports=\"es\" include=\"debounce\" -p -o ./`\n * Copyright JS Foundation and other contributors \n * Released under MIT license \n * Based on Underscore.js 1.8.3 \n * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n */\nexport function debounce(func, wait, options) {\n let lastArgs;\n let lastThis;\n let maxWait;\n let result;\n let timerId;\n let lastCallTime;\n let lastInvokeTime = 0;\n let leading = false;\n let maxing = false;\n let trailing = true;\n if (typeof func !== 'function') {\n throw new TypeError('Expected a function');\n }\n wait = Number(wait) || 0;\n if (typeof options === 'object' && options !== null) {\n leading = !!options.leading;\n maxing = 'maxWait' in options;\n maxWait = maxing ? Math.max(Number(options.maxWait) || 0, wait) : 0;\n trailing = 'trailing' in options ? !!options.trailing : trailing;\n }\n function invokeFunc(time) {\n const args = lastArgs;\n const thisArg = lastThis;\n lastArgs = lastThis = undefined;\n lastInvokeTime = time;\n result = func.apply(thisArg, args);\n return result;\n }\n function leadingEdge(time) {\n lastInvokeTime = time;\n timerId = setTimeout(timerExpired, wait);\n return leading ? invokeFunc(time) : result;\n }\n function remainingWait(time) {\n const timeSinceLastCall = time - (lastCallTime ?? 0);\n const timeSinceLastInvoke = time - lastInvokeTime;\n const timeWaiting = wait - timeSinceLastCall;\n return maxing\n ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)\n : timeWaiting;\n }\n function shouldInvoke(time) {\n const timeSinceLastCall = time - (lastCallTime ?? 0);\n const timeSinceLastInvoke = time - lastInvokeTime;\n return (lastCallTime === undefined ||\n timeSinceLastCall >= wait ||\n timeSinceLastCall < 0 ||\n (maxing && timeSinceLastInvoke >= maxWait));\n }\n function timerExpired() {\n const time = Date.now();\n if (shouldInvoke(time)) {\n return trailingEdge(time);\n }\n timerId = setTimeout(timerExpired, remainingWait(time));\n }\n function trailingEdge(time) {\n timerId = undefined;\n if (trailing && lastArgs) {\n return invokeFunc(time);\n }\n lastArgs = lastThis = undefined;\n return result;\n }\n function cancel() {\n if (timerId !== undefined) {\n clearTimeout(timerId);\n }\n lastInvokeTime = 0;\n lastArgs = lastCallTime = lastThis = timerId = undefined;\n }\n function flush() {\n return timerId === undefined ? result : trailingEdge(Date.now());\n }\n function debounced() {\n const time = Date.now();\n const isInvoking = shouldInvoke(time);\n // @ts-ignore\n lastArgs = arguments;\n // @ts-ignore\n lastThis = this;\n lastCallTime = time;\n if (isInvoking) {\n if (timerId === undefined) {\n return leadingEdge(lastCallTime);\n }\n if (maxing) {\n timerId = setTimeout(timerExpired, wait);\n return invokeFunc(lastCallTime);\n }\n }\n if (timerId === undefined) {\n timerId = setTimeout(timerExpired, wait);\n }\n return result;\n }\n debounced.cancel = cancel;\n debounced.flush = flush;\n return debounced;\n}\n//# sourceMappingURL=debounce.js.map","export function getQueryKey() {\n return crypto.randomUUID();\n}\n//# sourceMappingURL=get-query-key.js.map","import { getQueryKey } from './get-query-key.js';\nexport function getDatabaseKey(databasePath, clientKey) {\n switch (databasePath) {\n case 'session':\n case ':sessionStorage:':\n // The sessionStorage DB can be shared between clients in the same tab\n let sessionKey = sessionStorage._sqlocal_session_key;\n if (!sessionKey) {\n sessionKey = getQueryKey();\n sessionStorage._sqlocal_session_key = sessionKey;\n }\n return `session:${sessionKey}`;\n case 'local':\n case ':localStorage:':\n // There's only one localStorage DB per origin\n return 'local';\n case ':memory:':\n // Each memory DB is unique to a client\n return `memory:${clientKey}`;\n default:\n // OPFS DBs are shared by path across same-origin tabs\n return `path:${databasePath}`;\n }\n}\n//# sourceMappingURL=get-database-key.js.map","import coincident from 'coincident';\nimport { createMutex } from './lib/create-mutex.js';\nimport { SQLiteMemoryDriver } from './drivers/sqlite-memory-driver.js';\nimport { debounce } from './lib/debounce.js';\nimport { getDatabaseKey } from './lib/get-database-key.js';\nexport class SQLocalProcessor {\n constructor(driver) {\n Object.defineProperty(this, \"driver\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"config\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: {}\n });\n Object.defineProperty(this, \"userFunctions\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: new Map()\n });\n Object.defineProperty(this, \"initMutex\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: createMutex()\n });\n Object.defineProperty(this, \"transactionMutex\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: createMutex()\n });\n Object.defineProperty(this, \"transactionKey\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: null\n });\n Object.defineProperty(this, \"proxy\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"dirtyTables\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: new Set()\n });\n Object.defineProperty(this, \"effectsChannel\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"reinitChannel\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"onmessage\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"init\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (reason) => {\n if (!this.config.databasePath || !this.config.clientKey)\n return;\n await this.initMutex.lock();\n try {\n try {\n await this.driver.init(this.config);\n }\n catch {\n 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).`);\n this.config.databasePath = ':memory:';\n this.driver = new SQLiteMemoryDriver();\n await this.driver.init(this.config);\n }\n const dbKey = getDatabaseKey(this.config.databasePath, this.config.clientKey);\n this.reinitChannel = new BroadcastChannel(`_sqlocal_reinit_(${dbKey})`);\n this.reinitChannel.onmessage = (event) => {\n const message = event.data;\n if (this.config.clientKey === message.clientKey)\n return;\n switch (message.type) {\n case 'reinit':\n this.init(message.reason);\n break;\n case 'close':\n this.driver.destroy();\n break;\n }\n };\n if (this.config.reactive) {\n this.effectsChannel = new BroadcastChannel(`_sqlocal_effects_(${dbKey})`);\n this.driver.onWrite(async (change) => {\n this.dirtyTables.add(change.table);\n await this.transactionMutex.lock();\n this.emitEffectsDebounced();\n await this.transactionMutex.unlock();\n });\n }\n await Promise.all(Array.from(this.userFunctions.values()).map((fn) => {\n return this.initUserFunction(fn);\n }));\n await this.execInitStatements();\n this.emitMessage({ type: 'event', event: 'connect', reason });\n }\n catch (error) {\n this.emitMessage({\n type: 'error',\n error,\n queryKey: null,\n });\n await this.destroy();\n }\n finally {\n await this.initMutex.unlock();\n }\n }\n });\n Object.defineProperty(this, \"postMessage\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (event, _transfer) => {\n const message = event instanceof MessageEvent ? event.data : event;\n await this.initMutex.lock();\n switch (message.type) {\n case 'config':\n this.editConfig(message);\n break;\n case 'query':\n case 'batch':\n case 'transaction':\n this.exec(message);\n break;\n case 'function':\n this.createUserFunction(message);\n break;\n case 'getinfo':\n this.getDatabaseInfo(message);\n break;\n case 'import':\n this.importDb(message);\n break;\n case 'export':\n this.exportDb(message);\n break;\n case 'delete':\n this.deleteDb(message);\n break;\n case 'destroy':\n this.destroy(message);\n break;\n }\n await this.initMutex.unlock();\n }\n });\n Object.defineProperty(this, \"emitMessage\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: (message, transfer = []) => {\n if (this.onmessage) {\n this.onmessage(message, transfer);\n }\n }\n });\n Object.defineProperty(this, \"emitEffects\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: () => {\n if (!this.effectsChannel || this.dirtyTables.size === 0)\n return;\n this.effectsChannel.postMessage({\n type: 'effects',\n tables: [...this.dirtyTables],\n });\n this.dirtyTables.clear();\n }\n });\n Object.defineProperty(this, \"emitEffectsDebounced\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: debounce(() => this.emitEffects(), 32, {\n maxWait: 180,\n })\n });\n Object.defineProperty(this, \"editConfig\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: (message) => {\n this.config = message.config;\n this.init('initial');\n }\n });\n Object.defineProperty(this, \"exec\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n try {\n const response = {\n type: 'data',\n queryKey: message.queryKey,\n data: [],\n };\n switch (message.type) {\n case 'query':\n const partOfTransaction = this.transactionKey !== null &&\n this.transactionKey === message.transactionKey;\n try {\n if (!partOfTransaction) {\n await this.transactionMutex.lock();\n }\n const statementData = await this.driver.exec(message);\n response.data.push(statementData);\n }\n finally {\n if (!partOfTransaction) {\n await this.transactionMutex.unlock();\n }\n }\n break;\n case 'batch':\n try {\n await this.transactionMutex.lock();\n const results = await this.driver.execBatch(message.statements);\n response.data.push(...results);\n }\n finally {\n await this.transactionMutex.unlock();\n }\n break;\n case 'transaction':\n if (message.action === 'begin') {\n await this.transactionMutex.lock();\n this.transactionKey = message.transactionKey;\n await this.driver.exec({ sql: 'BEGIN' });\n }\n if ((message.action === 'commit' || message.action === 'rollback') &&\n this.transactionKey !== null &&\n this.transactionKey === message.transactionKey) {\n const sql = message.action === 'commit' ? 'COMMIT' : 'ROLLBACK';\n await this.driver.exec({ sql });\n this.transactionKey = null;\n await this.transactionMutex.unlock();\n }\n break;\n }\n this.emitMessage(response);\n }\n catch (error) {\n this.emitMessage({\n type: 'error',\n error,\n queryKey: message.queryKey,\n });\n }\n }\n });\n Object.defineProperty(this, \"execInitStatements\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async () => {\n if (this.config.onInitStatements) {\n for (let statement of this.config.onInitStatements) {\n await this.driver.exec(statement);\n }\n }\n }\n });\n Object.defineProperty(this, \"getDatabaseInfo\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n try {\n this.emitMessage({\n type: 'info',\n queryKey: message.queryKey,\n info: {\n databasePath: this.config.databasePath,\n storageType: this.driver.storageType,\n databaseSizeBytes: await this.driver.getDatabaseSizeBytes(),\n persisted: await this.driver.isDatabasePersisted(),\n },\n });\n }\n catch (error) {\n this.emitMessage({\n type: 'error',\n queryKey: message.queryKey,\n error,\n });\n }\n }\n });\n Object.defineProperty(this, \"createUserFunction\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n const { functionName: name, functionType: type, queryKey } = message;\n let fn;\n if (this.userFunctions.has(name)) {\n this.emitMessage({\n type: 'error',\n error: new Error(`A user-defined function with the name \"${name}\" has already been created for this SQLocal instance.`),\n queryKey,\n });\n return;\n }\n switch (type) {\n case 'callback':\n fn = {\n type,\n name,\n func: (...args) => {\n this.emitMessage({ type: 'callback', name, args });\n },\n };\n break;\n case 'scalar':\n fn = {\n type,\n name,\n func: this.proxy[`_sqlocal_func_${name}`],\n };\n break;\n case 'aggregate':\n fn = {\n type,\n name,\n func: {\n step: this.proxy[`_sqlocal_func_${name}_step`],\n final: this.proxy[`_sqlocal_func_${name}_final`],\n },\n };\n break;\n }\n try {\n await this.initUserFunction(fn);\n this.emitMessage({\n type: 'success',\n queryKey,\n });\n }\n catch (error) {\n this.emitMessage({\n type: 'error',\n error,\n queryKey,\n });\n }\n }\n });\n Object.defineProperty(this, \"initUserFunction\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (fn) => {\n await this.driver.createFunction(fn);\n this.userFunctions.set(fn.name, fn);\n }\n });\n Object.defineProperty(this, \"importDb\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n const { queryKey, database } = message;\n let errored = false;\n try {\n await this.driver.import(database);\n if (this.driver.storageType === 'memory') {\n await this.execInitStatements();\n }\n }\n catch (error) {\n this.emitMessage({\n type: 'error',\n error,\n queryKey,\n });\n errored = true;\n }\n finally {\n if (this.driver.storageType !== 'memory') {\n await this.init('overwrite');\n }\n }\n if (!errored) {\n this.emitMessage({\n type: 'success',\n queryKey,\n });\n }\n }\n });\n Object.defineProperty(this, \"exportDb\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n const { queryKey } = message;\n try {\n const { name, data } = await this.driver.export();\n this.emitMessage({\n type: 'buffer',\n queryKey,\n bufferName: name,\n buffer: data,\n }, [data]);\n }\n catch (error) {\n this.emitMessage({\n type: 'error',\n error,\n queryKey,\n });\n }\n }\n });\n Object.defineProperty(this, \"deleteDb\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n const { queryKey } = message;\n let errored = false;\n try {\n await this.driver.clear();\n }\n catch (error) {\n this.emitMessage({\n type: 'error',\n error,\n queryKey,\n });\n errored = true;\n }\n finally {\n await this.init('delete');\n }\n if (!errored) {\n this.emitMessage({\n type: 'success',\n queryKey,\n });\n }\n }\n });\n Object.defineProperty(this, \"destroy\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n await this.driver.exec({ sql: 'PRAGMA optimize' });\n await this.driver.destroy();\n if (this.effectsChannel) {\n this.emitEffectsDebounced.flush();\n this.effectsChannel.close();\n this.effectsChannel = undefined;\n }\n if (this.reinitChannel) {\n this.reinitChannel.close();\n this.reinitChannel = undefined;\n }\n if (message) {\n this.emitMessage({\n type: 'success',\n queryKey: message.queryKey,\n });\n }\n }\n });\n const isInWorker = typeof WorkerGlobalScope !== 'undefined' &&\n globalThis instanceof WorkerGlobalScope;\n const proxy = isInWorker ? coincident(globalThis) : globalThis;\n this.proxy = proxy;\n this.driver = driver;\n }\n}\n//# sourceMappingURL=processor.js.map","export function sqlTag(queryTemplate, ...params) {\n return {\n sql: queryTemplate.join('?'),\n params,\n };\n}\n//# sourceMappingURL=sql-tag.js.map","function isArrayOfArrays(rows) {\n return !rows.some((row) => !Array.isArray(row));\n}\nexport function convertRowsToObjects(rows, columns) {\n let checkedRows;\n if (isArrayOfArrays(rows)) {\n checkedRows = rows;\n }\n else {\n checkedRows = [rows];\n }\n return checkedRows.map((row) => {\n const rowObj = {};\n columns.forEach((column, columnIndex) => {\n rowObj[column] = row[columnIndex];\n });\n return rowObj;\n });\n}\n//# sourceMappingURL=convert-rows-to-objects.js.map","import { sqlTag } from './sql-tag.js';\nfunction isDrizzleStatement(statement) {\n return (typeof statement === 'object' &&\n statement !== null &&\n 'getSQL' in statement &&\n typeof statement.getSQL === 'function');\n}\nfunction isStatement(statement) {\n return (typeof statement === 'object' &&\n statement !== null &&\n 'sql' in statement === true &&\n typeof statement.sql === 'string' &&\n 'params' in statement === true);\n}\nexport function normalizeStatement(statement) {\n if (typeof statement === 'function') {\n statement = statement(sqlTag);\n }\n if (isDrizzleStatement(statement)) {\n try {\n if (!('toSQL' in statement && typeof statement.toSQL === 'function')) {\n throw 1;\n }\n const drizzleStatement = statement.toSQL();\n if (!isStatement(drizzleStatement)) {\n throw 2;\n }\n const exec = 'all' in statement && typeof statement.all === 'function'\n ? statement.all\n : undefined;\n return {\n ...drizzleStatement,\n exec: exec ? () => exec() : undefined,\n };\n }\n catch {\n throw new Error('The passed statement could not be parsed.');\n }\n }\n const sql = statement.sql;\n let params = [];\n if ('params' in statement) {\n params = statement.params;\n }\n else if ('parameters' in statement) {\n params = statement.parameters;\n }\n return { sql, params };\n}\n//# sourceMappingURL=normalize-statement.js.map","import { sqlTag } from './sql-tag.js';\nexport function normalizeSql(maybeQueryTemplate, params) {\n let statement;\n if (typeof maybeQueryTemplate === 'string') {\n statement = { sql: maybeQueryTemplate, params };\n }\n else {\n statement = sqlTag(maybeQueryTemplate, ...params);\n }\n return statement;\n}\n//# sourceMappingURL=normalize-sql.js.map","export async function mutationLock(mode, bypass, config, mutation) {\n if (!bypass && 'locks' in navigator) {\n return navigator.locks.request(`_sqlocal_mutation_(${config.databasePath})`, { mode }, mutation);\n }\n else {\n return mutation();\n }\n}\n//# sourceMappingURL=mutation-lock.js.map","import { SQLiteMemoryDriver } from './sqlite-memory-driver.js';\nexport class SQLiteKvvfsDriver extends SQLiteMemoryDriver {\n constructor(storageType, sqlite3InitModule) {\n super(sqlite3InitModule);\n Object.defineProperty(this, \"storageType\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: storageType\n });\n }\n async init(config) {\n const flags = this.getFlags(config);\n if (config.readOnly) {\n throw new Error(`SQLite storage type \"${this.storageType}\" does not support read-only mode.`);\n }\n if (!this.sqlite3InitModule) {\n const { default: sqlite3InitModule } = await import('@sqlite.org/sqlite-wasm');\n this.sqlite3InitModule = sqlite3InitModule;\n }\n if (!this.sqlite3) {\n this.sqlite3 = await this.sqlite3InitModule();\n }\n if (this.db) {\n await this.destroy();\n }\n this.db = new this.sqlite3.oo1.JsStorageDb({\n filename: this.storageType,\n flags,\n });\n this.config = config;\n this.initWriteHook();\n }\n async isDatabasePersisted() {\n return navigator.storage?.persisted();\n }\n async getDatabaseSizeBytes() {\n if (!this.db)\n throw new Error('Driver not initialized');\n return this.db.storageSize();\n }\n async import(database) {\n const memdb = new SQLiteMemoryDriver();\n await memdb.init({});\n await memdb.import(database);\n await this.clear();\n await memdb.exec({\n sql: `VACUUM INTO 'file:${this.storageType}?vfs=kvvfs'`,\n });\n await memdb.destroy();\n }\n async clear() {\n if (!this.db)\n throw new Error('Driver not initialized');\n this.db.clearStorage();\n }\n async destroy() {\n this.closeDb();\n this.writeCallbacks.clear();\n }\n}\n//# sourceMappingURL=sqlite-kvvfs-driver.js.map","var _a, _b;\nimport coincident from 'coincident';\nimport { SQLocalProcessor } from './processor.js';\nimport { sqlTag } from './lib/sql-tag.js';\nimport { convertRowsToObjects } from './lib/convert-rows-to-objects.js';\nimport { normalizeStatement } from './lib/normalize-statement.js';\nimport { getQueryKey } from './lib/get-query-key.js';\nimport { normalizeSql } from './lib/normalize-sql.js';\nimport { mutationLock } from './lib/mutation-lock.js';\nimport { normalizeDatabaseFile } from './lib/normalize-database-file.js';\nimport { SQLiteMemoryDriver } from './drivers/sqlite-memory-driver.js';\nimport { SQLiteKvvfsDriver } from './drivers/sqlite-kvvfs-driver.js';\nimport { getDatabaseKey } from './lib/get-database-key.js';\nexport class SQLocal {\n constructor(config) {\n Object.defineProperty(this, \"config\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"clientKey\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"processor\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"isDestroyed\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: false\n });\n Object.defineProperty(this, \"bypassMutationLock\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: false\n });\n Object.defineProperty(this, \"transactionQueryKeyQueue\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: []\n });\n Object.defineProperty(this, \"userCallbacks\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: new Map()\n });\n Object.defineProperty(this, \"queriesInProgress\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: new Map()\n });\n Object.defineProperty(this, \"proxy\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"reinitChannel\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"effectsChannel\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: void 0\n });\n Object.defineProperty(this, \"processMessageEvent\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: (event) => {\n const message = event instanceof MessageEvent ? event.data : event;\n const queries = this.queriesInProgress;\n switch (message.type) {\n case 'success':\n case 'data':\n case 'buffer':\n case 'info':\n case 'error':\n if (message.queryKey && queries.has(message.queryKey)) {\n const [resolve, reject] = queries.get(message.queryKey);\n if (message.type === 'error') {\n reject(message.error);\n }\n else {\n resolve(message);\n }\n queries.delete(message.queryKey);\n }\n else if (message.type === 'error') {\n throw message.error;\n }\n break;\n case 'callback':\n const userCallback = this.userCallbacks.get(message.name);\n if (userCallback) {\n userCallback(...(message.args ?? []));\n }\n break;\n case 'event':\n this.config.onConnect?.(message.reason);\n break;\n }\n }\n });\n Object.defineProperty(this, \"createQuery\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (message) => {\n return mutationLock('shared', this.bypassMutationLock ||\n message.type === 'import' ||\n message.type === 'delete', this.config, async () => {\n if (this.isDestroyed === true) {\n throw new Error('This SQLocal client has been destroyed. You will need to initialize a new client in order to make further queries.');\n }\n const queryKey = getQueryKey();\n switch (message.type) {\n case 'import':\n this.processor.postMessage({\n ...message,\n queryKey,\n }, [message.database]);\n break;\n default:\n this.processor.postMessage({\n ...message,\n queryKey,\n });\n break;\n }\n return new Promise((resolve, reject) => {\n this.queriesInProgress.set(queryKey, [resolve, reject]);\n });\n });\n }\n });\n Object.defineProperty(this, \"broadcast\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: (message) => {\n this.reinitChannel.postMessage(message);\n }\n });\n Object.defineProperty(this, \"exec\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (sql, params, method = 'all', transactionKey) => {\n const message = await this.createQuery({\n type: 'query',\n transactionKey,\n sql,\n params,\n method,\n });\n const data = {\n rows: [],\n columns: [],\n };\n if (message.type === 'data') {\n data.rows = message.data[0]?.rows ?? [];\n data.columns = message.data[0]?.columns ?? [];\n }\n return data;\n }\n });\n Object.defineProperty(this, \"execBatch\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (statements) => {\n const message = await this.createQuery({\n type: 'batch',\n statements,\n });\n const data = new Array(statements.length).fill({\n rows: [],\n columns: [],\n });\n if (message.type === 'data') {\n message.data.forEach((result, resultIndex) => {\n data[resultIndex] = result;\n });\n }\n return data;\n }\n });\n Object.defineProperty(this, \"sql\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (queryTemplate, ...params) => {\n const statement = normalizeSql(queryTemplate, params);\n const { rows, columns } = await this.exec(statement.sql, statement.params, 'all');\n const resultRecords = convertRowsToObjects(rows, columns);\n return resultRecords;\n }\n });\n Object.defineProperty(this, \"batch\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (passStatements) => {\n const statements = passStatements(sqlTag);\n const data = await this.execBatch(statements);\n return data.map(({ rows, columns }) => {\n const resultRecords = convertRowsToObjects(rows, columns);\n return resultRecords;\n });\n }\n });\n Object.defineProperty(this, \"beginTransaction\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async () => {\n const transactionKey = getQueryKey();\n await this.createQuery({\n type: 'transaction',\n transactionKey,\n action: 'begin',\n });\n const query = async (passStatement) => {\n const statement = normalizeStatement(passStatement);\n if (statement.exec) {\n this.transactionQueryKeyQueue.push(transactionKey);\n return statement.exec();\n }\n const { rows, columns } = await this.exec(statement.sql, statement.params, 'all', transactionKey);\n const resultRecords = convertRowsToObjects(rows, columns);\n return resultRecords;\n };\n const sql = async (queryTemplate, ...params) => {\n const statement = normalizeSql(queryTemplate, params);\n const resultRecords = await query(statement);\n return resultRecords;\n };\n const commit = async () => {\n await this.createQuery({\n type: 'transaction',\n transactionKey,\n action: 'commit',\n });\n };\n const rollback = async () => {\n await this.createQuery({\n type: 'transaction',\n transactionKey,\n action: 'rollback',\n });\n };\n return {\n query,\n sql,\n commit,\n rollback,\n };\n }\n });\n Object.defineProperty(this, \"transaction\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (transaction) => {\n return mutationLock('exclusive', false, this.config, async () => {\n let tx;\n this.bypassMutationLock = true;\n try {\n tx = await this.beginTransaction();\n const result = await transaction({\n sql: tx.sql,\n query: tx.query,\n });\n await tx.commit();\n return result;\n }\n catch (err) {\n await tx?.rollback();\n throw err;\n }\n finally {\n this.bypassMutationLock = false;\n }\n });\n }\n });\n Object.defineProperty(this, \"reactiveQuery\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: (passStatement) => {\n let value = [];\n let gotFirstValue = false;\n let isListening = false;\n let updateCount = 0;\n const statement = normalizeStatement(passStatement);\n const watchedTables = new Set();\n const subObservers = new Set();\n const errObservers = new Set();\n const runStatement = async () => {\n try {\n const updateOrder = ++updateCount;\n if (watchedTables.size === 0) {\n const usedTables = await this.sql(\"SELECT name, wr FROM tables_used(?) WHERE type = 'table'\", statement.sql);\n const readTables = new Set();\n const writtenTables = new Set();\n usedTables.forEach((table) => {\n if (typeof table.name !== 'string')\n return;\n table.wr\n ? writtenTables.add(table.name)\n : readTables.add(table.name);\n });\n if (readTables.size === 0) {\n throw new Error('The passed SQL does not read any tables.');\n }\n if (Array.from(writtenTables).some((table) => readTables.has(table))) {\n 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.');\n }\n readTables.forEach((name) => watchedTables.add(name));\n }\n const results = statement.exec\n ? await statement.exec()\n : await this.sql(statement.sql, ...statement.params);\n if (updateOrder === updateCount) {\n value = results;\n gotFirstValue = true;\n subObservers.forEach((observer) => observer(value));\n }\n }\n catch (err) {\n errObservers.forEach((observer) => {\n observer(err instanceof Error ? err : new Error(String(err)));\n });\n }\n };\n const onEffect = (message) => {\n if (message.data.tables.some((table) => watchedTables.has(table))) {\n runStatement();\n }\n };\n return {\n get value() {\n return value;\n },\n subscribe: (onData, onError) => {\n if (!this.effectsChannel) {\n throw new Error('This SQLocal instance is not configured for reactive queries. Set the \"reactive\" option to enable them.');\n }\n if (!onError) {\n onError = (err) => {\n throw err;\n };\n }\n subObservers.add(onData);\n errObservers.add(onError);\n if (!isListening) {\n this.effectsChannel.addEventListener('message', onEffect);\n isListening = true;\n runStatement();\n }\n else if (gotFirstValue) {\n onData(value);\n }\n return {\n unsubscribe: () => {\n subObservers.delete(onData);\n errObservers.delete(onError);\n if (subObservers.size !== 0)\n return;\n this.effectsChannel?.removeEventListener('message', onEffect);\n isListening = false;\n },\n };\n },\n };\n }\n });\n Object.defineProperty(this, \"createCallbackFunction\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (funcName, func) => {\n await this.createQuery({\n type: 'function',\n functionName: funcName,\n functionType: 'callback',\n });\n this.userCallbacks.set(funcName, func);\n }\n });\n Object.defineProperty(this, \"createScalarFunction\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (funcName, func) => {\n const key = `_sqlocal_func_${funcName}`;\n const attachFunction = () => {\n this.proxy[key] = func;\n };\n if (this.proxy === globalThis) {\n attachFunction();\n }\n await this.createQuery({\n type: 'function',\n functionName: funcName,\n functionType: 'scalar',\n });\n if (this.proxy !== globalThis) {\n attachFunction();\n }\n }\n });\n Object.defineProperty(this, \"createAggregateFunction\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (funcName, func) => {\n const key = `_sqlocal_func_${funcName}`;\n const attachFunction = () => {\n this.proxy[`${key}_step`] = func.step;\n this.proxy[`${key}_final`] = func.final;\n };\n if (this.proxy === globalThis) {\n attachFunction();\n }\n await this.createQuery({\n type: 'function',\n functionName: funcName,\n functionType: 'aggregate',\n });\n if (this.proxy !== globalThis) {\n attachFunction();\n }\n }\n });\n Object.defineProperty(this, \"getDatabaseInfo\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async () => {\n const message = await this.createQuery({ type: 'getinfo' });\n if (message.type === 'info') {\n return message.info;\n }\n else {\n throw new Error('The database failed to return valid information.');\n }\n }\n });\n Object.defineProperty(this, \"getDatabaseFile\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async () => {\n const message = await this.createQuery({ type: 'export' });\n if (message.type === 'buffer') {\n return new File([message.buffer], message.bufferName, {\n type: 'application/x-sqlite3',\n });\n }\n else {\n throw new Error('The database failed to export.');\n }\n }\n });\n Object.defineProperty(this, \"overwriteDatabaseFile\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (databaseFile, beforeUnlock) => {\n await mutationLock('exclusive', false, this.config, async () => {\n try {\n this.broadcast({\n type: 'close',\n clientKey: this.clientKey,\n });\n const database = await normalizeDatabaseFile(databaseFile, 'buffer');\n await this.createQuery({\n type: 'import',\n database,\n });\n if (typeof beforeUnlock === 'function') {\n this.bypassMutationLock = true;\n await beforeUnlock();\n }\n this.broadcast({\n type: 'reinit',\n clientKey: this.clientKey,\n reason: 'overwrite',\n });\n }\n finally {\n this.bypassMutationLock = false;\n }\n });\n }\n });\n Object.defineProperty(this, \"deleteDatabaseFile\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async (beforeUnlock) => {\n await mutationLock('exclusive', false, this.config, async () => {\n try {\n this.broadcast({\n type: 'close',\n clientKey: this.clientKey,\n });\n await this.createQuery({\n type: 'delete',\n });\n if (typeof beforeUnlock === 'function') {\n this.bypassMutationLock = true;\n await beforeUnlock();\n }\n this.broadcast({\n type: 'reinit',\n clientKey: this.clientKey,\n reason: 'delete',\n });\n }\n finally {\n this.bypassMutationLock = false;\n }\n });\n }\n });\n Object.defineProperty(this, \"destroy\", {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async () => {\n await this.createQuery({ type: 'destroy' });\n if (typeof globalThis.Worker !== 'undefined' &&\n this.processor instanceof Worker) {\n this.processor.removeEventListener('message', this.processMessageEvent);\n this.processor.terminate();\n }\n this.queriesInProgress.clear();\n this.userCallbacks.clear();\n this.reinitChannel.close();\n this.effectsChannel?.close();\n this.isDestroyed = true;\n }\n });\n Object.defineProperty(this, _a, {\n enumerable: true,\n configurable: true,\n writable: true,\n value: () => {\n this.destroy();\n }\n });\n Object.defineProperty(this, _b, {\n enumerable: true,\n configurable: true,\n writable: true,\n value: async () => {\n await this.destroy();\n }\n });\n const clientConfig = typeof config === 'string' ? { databasePath: config } : config;\n const { onInit, onConnect, processor, ...commonConfig } = clientConfig;\n const { databasePath } = commonConfig;\n this.config = clientConfig;\n this.clientKey = getQueryKey();\n const dbKey = getDatabaseKey(databasePath, this.clientKey);\n this.reinitChannel = new BroadcastChannel(`_sqlocal_reinit_(${dbKey})`);\n if (commonConfig.reactive) {\n this.effectsChannel = new BroadcastChannel(`_sqlocal_effects_(${dbKey})`);\n }\n if (typeof processor !== 'undefined') {\n this.processor = processor;\n }\n else if (databasePath === 'local' || databasePath === ':localStorage:') {\n const driver = new SQLiteKvvfsDriver('local');\n this.processor = new SQLocalProcessor(driver);\n }\n else if (databasePath === 'session' ||\n databasePath === ':sessionStorage:') {\n const driver = new SQLiteKvvfsDriver('session');\n this.processor = new SQLocalProcessor(driver);\n }\n else if (typeof globalThis.Worker !== 'undefined' &&\n databasePath !== ':memory:') {\n this.processor = new Worker(new URL('./worker', import.meta.url), {\n type: 'module',\n });\n }\n else {\n const driver = new SQLiteMemoryDriver();\n this.processor = new SQLocalProcessor(driver);\n }\n if (this.processor instanceof SQLocalProcessor) {\n this.processor.onmessage = (message) => this.processMessageEvent(message);\n this.proxy = globalThis;\n }\n else {\n this.processor.addEventListener('message', this.processMessageEvent);\n this.proxy = coincident(this.processor);\n }\n this.processor.postMessage({\n type: 'config',\n config: {\n ...commonConfig,\n clientKey: this.clientKey,\n onInitStatements: onInit?.(sqlTag) ?? [],\n },\n });\n }\n}\n_a = Symbol.dispose, _b = Symbol.asyncDispose;\n//# sourceMappingURL=client.js.map","/**\n * Database Module\n *\n * Uses SQLocal directly with BroadcastChannel for cross-tab coordination.\n *\n * Why this approach instead of SharedWorker?\n * - SQLocal already uses its own internal worker for OPFS access\n * - Wrapping it in another SharedWorker adds complexity and causes issues\n * - BroadcastChannel provides simple cross-tab communication\n * - Each tab has its own SQLocal instance but they share the same OPFS database file\n *\n * Usage:\n * import { sql, dbReady, addLocation, getLocations } from './database.js';\n *\n * await dbReady;\n * await addLocation('Point A', -1.5, 7.5);\n * const locations = await getLocations();\n */\n\nimport { SQLocal } from 'sqlocal';\n\n// Database configuration\nconst DATABASE_PATH = 'lupmis2.db';\nconst BROADCAST_CHANNEL = 'lupmis-db-sync';\n\n// Create SQLocal instance\nconst db = new SQLocal(DATABASE_PATH);\n\n// Get the sql tagged template function\nconst { sql } = db;\n\nconsole.log('[Database] SQLocal instance created for:', DATABASE_PATH);\n\n// Export sql for direct queries\nexport { sql };\n\n// Create broadcast channel for cross-tab coordination\nconst channel = new BroadcastChannel(BROADCAST_CHANNEL);\n\n// Track if database is ready\nlet isReady = false;\nlet readyResolve;\nlet readyReject;\n\nexport const dbReady = new Promise((resolve, reject) => {\n readyResolve = resolve;\n readyReject = reject;\n});\n\n// Database change listeners\nconst changeListeners = new Set();\n\n/**\n * Subscribe to database changes (from any tab)\n * @param {Function} listener - Called with { table, action, id }\n * @returns {Function} Unsubscribe function\n */\nexport function onDatabaseChange(listener) {\n changeListeners.add(listener);\n return () => changeListeners.delete(listener);\n}\n\n// Handle messages from other tabs\nchannel.onmessage = (event) => {\n const { type, payload } = event.data;\n if (type === 'DB_CHANGE') {\n // Notify local listeners about changes from other tabs\n for (const listener of changeListeners) {\n try {\n listener(payload);\n } catch (e) {\n console.error('[Database] Change listener error:', e);\n }\n }\n }\n};\n\n/**\n * Broadcast a database change to other tabs\n */\nfunction broadcastChange(table, action, id = null) {\n channel.postMessage({\n type: 'DB_CHANGE',\n payload: { table, action, id, timestamp: Date.now() }\n });\n\n // Also notify local listeners\n for (const listener of changeListeners) {\n try {\n listener({ table, action, id, timestamp: Date.now(), local: true });\n } catch (e) {\n console.error('[Database] Change listener error:', e);\n }\n }\n}\n\n// ============================================================================\n// Database Initialization\n// ============================================================================\n\n/**\n * Initialize the database schema\n */\nexport async function initSchema() {\n try {\n console.log('[Database] Initializing schema...');\n\n // Test connection\n const testResult = await sql`SELECT sqlite_version() as version`;\n console.log('[Database] SQLite version:', testResult[0]?.version);\n\n // Create locations table\n console.log('[Database] Creating locations table...');\n await sql`\n CREATE TABLE IF NOT EXISTS locations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n longitude REAL NOT NULL,\n latitude REAL NOT NULL,\n description TEXT,\n category TEXT DEFAULT 'default',\n created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n updated_at TEXT DEFAULT CURRENT_TIMESTAMP,\n synced INTEGER DEFAULT 0\n )\n `;\n\n // Verify table exists\n const tablesAfterLocations = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;\n console.log('[Database] Locations table exists:', tablesAfterLocations.length > 0);\n\n // Create sync_log table\n console.log('[Database] Creating sync_log table...');\n await sql`\n CREATE TABLE IF NOT EXISTS sync_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n table_name TEXT NOT NULL,\n record_id INTEGER NOT NULL,\n action TEXT NOT NULL,\n timestamp TEXT DEFAULT CURRENT_TIMESTAMP,\n synced INTEGER DEFAULT 0\n )\n `;\n\n // Create remote_data cache table\n console.log('[Database] Creating remote_data table...');\n await sql`\n CREATE TABLE IF NOT EXISTS remote_data (\n key TEXT PRIMARY KEY,\n data TEXT NOT NULL,\n fetched_at TEXT DEFAULT CURRENT_TIMESTAMP\n )\n `;\n\n // Create collector_zones table for caching zone data\n console.log('[Database] Creating collector_zones table...');\n await sql`\n CREATE TABLE IF NOT EXISTS collector_zones (\n id INTEGER PRIMARY KEY,\n zone_name TEXT,\n geometry_wkt TEXT,\n properties TEXT,\n fetched_at TEXT DEFAULT CURRENT_TIMESTAMP\n )\n `;\n\n // Create parcels table — mirrors the server's spatial.lu_parcels structure\n // (land-use parcels). Attribute columns match the server 1:1 so the local\n // data viewer shows real fields rather than a JSON blob.\n // status — local-only flag: 'verified' = downloaded from the API,\n // 'new' = drawn locally and pending server verification.\n // fetched_at — local-only cache timestamp.\n // Migrate older databases that used the previous JSON-blob schema\n // (id, geometry_wkt, properties, status, fetched_at): if the new `upn`\n // column is missing, drop the cached table so it is recreated with the\n // lu_parcels columns. Cached server parcels re-download on next load.\n console.log('[Database] Creating parcels table...');\n try {\n const cols = await sql`PRAGMA table_info(parcels)`;\n if (cols.length > 0 && !cols.some((c) => c.name === 'upn')) {\n console.log('[Database] Migrating parcels table to lu_parcels structure (dropping old cache)...');\n await sql`DROP TABLE parcels`;\n }\n } catch (e) {\n console.warn('[Database] parcels migration check failed:', e);\n }\n await sql`\n CREATE TABLE IF NOT EXISTS parcels (\n id INTEGER PRIMARY KEY,\n upn TEXT,\n style INTEGER,\n landuse TEXT,\n zone_code TEXT,\n zone_name TEXT,\n sector TEXT,\n block TEXT,\n parcel_no TEXT,\n prop_no TEXT,\n st_name TEXT,\n prop_add TEXT,\n fac_name TEXT,\n min_height INTEGER,\n max_height INTEGER,\n eff_date TEXT,\n lp_name TEXT,\n locality TEXT,\n mmda TEXT,\n last_update TEXT,\n remarks TEXT,\n geometry_wkt TEXT,\n created_at TEXT,\n updated_at TEXT,\n districtid INTEGER,\n status TEXT DEFAULT 'verified',\n fetched_at TEXT DEFAULT CURRENT_TIMESTAMP\n )\n `;\n\n // Create building_footprints table for caching footprint data\n console.log('[Database] Creating building_footprints table...');\n await sql`\n CREATE TABLE IF NOT EXISTS building_footprints (\n id INTEGER PRIMARY KEY,\n geometry_wkt TEXT,\n properties TEXT,\n fetched_at TEXT DEFAULT CURRENT_TIMESTAMP\n )\n `;\n\n // Create osm_roads table for caching the OSM road network\n console.log('[Database] Creating osm_roads table...');\n await sql`\n CREATE TABLE IF NOT EXISTS osm_roads (\n osm_id INTEGER PRIMARY KEY,\n geometry_wkt TEXT,\n properties TEXT,\n fetched_at TEXT DEFAULT CURRENT_TIMESTAMP\n )\n `;\n\n // ── GPS trails ──────────────────────────────────────────────────────\n // Recorded field-movement tracks. These are the local store for the\n // reusable GeoTracker module (src/geotracker/). `client_uuid` lets the\n // server de-duplicate re-synced trails; `satellites` is nullable because\n // the web Geolocation API does not expose it (only native builds do).\n console.log('[Database] Creating gps_trails table...');\n await sql`\n CREATE TABLE IF NOT EXISTS gps_trails (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n client_uuid TEXT UNIQUE,\n name TEXT,\n district_id TEXT,\n started_at TEXT NOT NULL,\n ended_at TEXT,\n status TEXT NOT NULL DEFAULT 'recording',\n point_count INTEGER NOT NULL DEFAULT 0,\n distance_m REAL NOT NULL DEFAULT 0,\n synced INTEGER NOT NULL DEFAULT 0,\n remote_id TEXT,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP\n )\n `;\n\n console.log('[Database] Creating gps_trail_points table...');\n await sql`\n CREATE TABLE IF NOT EXISTS gps_trail_points (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n trail_id INTEGER NOT NULL,\n seq INTEGER NOT NULL,\n longitude REAL NOT NULL,\n latitude REAL NOT NULL,\n altitude REAL,\n accuracy REAL,\n altitude_accuracy REAL,\n heading REAL,\n speed REAL,\n satellites INTEGER,\n recorded_at TEXT NOT NULL\n )\n `;\n\n // Create indexes\n await sql`CREATE INDEX IF NOT EXISTS idx_locations_category ON locations(category)`;\n await sql`CREATE INDEX IF NOT EXISTS idx_locations_synced ON locations(synced)`;\n await sql`CREATE INDEX IF NOT EXISTS idx_gps_trails_synced ON gps_trails(synced, status)`;\n await sql`CREATE INDEX IF NOT EXISTS idx_gps_trail_points_trail ON gps_trail_points(trail_id, seq)`;\n\n // Final verification\n const allTables = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;\n console.log('[Database] All tables:', allTables.map(t => t.name));\n\n isReady = true;\n readyResolve(true);\n console.log('[Database] ✓ Schema initialized');\n\n } catch (error) {\n console.error('[Database] ✗ Schema init failed:', error);\n readyReject(error);\n throw error;\n }\n}\n\n// ============================================================================\n// Location Operations\n// ============================================================================\n\n/**\n * Add a new location\n */\nexport async function addLocation(name, longitude, latitude, options = {}) {\n const { description = null, category = 'default' } = options;\n\n console.log('[Database] Adding location:', name, longitude, latitude, category);\n\n try {\n // Check table exists first\n const tableCheck = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;\n console.log('[Database] Table check before insert:', tableCheck);\n\n if (tableCheck.length === 0) {\n console.error('[Database] ✗ locations table does not exist!');\n throw new Error('locations table does not exist');\n }\n\n // Insert - using explicit values\n console.log('[Database] Executing INSERT...');\n await sql`\n INSERT INTO locations (name, longitude, latitude, description, category)\n VALUES (${name}, ${longitude}, ${latitude}, ${description}, ${category})\n `;\n console.log('[Database] INSERT completed');\n\n // Get the ID\n const idResult = await sql`SELECT last_insert_rowid() as id`;\n const newId = idResult[0]?.id;\n console.log('[Database] New ID:', newId);\n\n // Verify it was actually inserted\n const verifyResult = await sql`SELECT * FROM locations WHERE id = ${newId}`;\n console.log('[Database] Verify insert:', verifyResult);\n\n if (verifyResult.length === 0) {\n console.error('[Database] ✗ Insert verification failed - row not found!');\n throw new Error('Insert verification failed');\n }\n\n // Log for sync\n await sql`\n INSERT INTO sync_log (table_name, record_id, action)\n VALUES ('locations', ${newId}, 'INSERT')\n `;\n\n // Broadcast to other tabs\n broadcastChange('locations', 'INSERT', newId);\n\n console.log('[Database] ✓ Location added:', newId);\n return { id: newId };\n\n } catch (error) {\n console.error('[Database] ✗ Failed to add location:', error);\n throw error;\n}\n}\n\n/**\n * Get all locations\n */\nexport async function getLocations(options = {}) {\n const { category = null, limit = 1000 } = options;\n\n try {\n // First check if table exists\n const tableCheck = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;\n console.log('[Database] getLocations - table exists:', tableCheck.length > 0);\n\n if (tableCheck.length === 0) {\n console.warn('[Database] locations table does not exist yet');\n return [];\n }\n\n let results;\n if (category) {\n results = await sql`\n SELECT * FROM locations\n WHERE category = ${category}\n ORDER BY created_at DESC\n LIMIT ${limit}\n `;\n } else {\n results = await sql`\n SELECT * FROM locations\n ORDER BY created_at DESC\n LIMIT ${limit}\n `;\n}\n\n console.log('[Database] getLocations returned', results.length, 'rows');\n return results;\n\n } catch (error) {\n console.error('[Database] getLocations error:', error);\n return [];\n }\n}\n\nexport async function getLocation(id) {\n try {\n const results = await sql`SELECT * FROM locations WHERE id = ${id}`;\n return results[0] || null;\n } catch (error) {\n console.error('[Database] getLocation error:', error);\n return null;\n}\n}\n\n/**\n * Update a location\n */\nexport async function updateLocation(id, updates) {\n const { name, longitude, latitude, description, category } = updates;\n\n try {\n const location = await getLocation(id);\n if (!location) {\n throw new Error(`Location ${id} not found`);\n }\n\n await sql`\n UPDATE locations\n SET\n name = ${name ?? location.name},\n longitude = ${longitude ?? location.longitude},\n latitude = ${latitude ?? location.latitude},\n description = ${description ?? location.description},\n category = ${category ?? location.category},\n updated_at = CURRENT_TIMESTAMP,\n synced = 0\n WHERE id = ${id}\n `;\n\n // Log for sync\n await sql`\n INSERT INTO sync_log (table_name, record_id, action)\n VALUES ('locations', ${id}, 'UPDATE')\n `;\n\n // Broadcast to other tabs\n broadcastChange('locations', 'UPDATE', id);\n console.log('[Database] ✓ Location updated:', id);\n\n } catch (error) {\n console.error('[Database] ✗ updateLocation error:', error);\n throw error;\n}\n}\n\n/**\n * Delete a location\n */\nexport async function deleteLocation(id) {\n try {\n await sql`\n INSERT INTO sync_log (table_name, record_id, action)\n VALUES ('locations', ${id}, 'DELETE')\n `;\n\n await sql`DELETE FROM locations WHERE id = ${id}`;\n\n // Broadcast to other tabs\n broadcastChange('locations', 'DELETE', id);\n console.log('[Database] ✓ Location deleted:', id);\n\n } catch (error) {\n console.error('[Database] ✗ deleteLocation error:', error);\n throw error;\n}\n}\n\n/**\n * Get location count\n */\nexport async function getLocationCount() {\n try {\n const result = await sql`SELECT COUNT(*) as count FROM locations`;\n return result[0]?.count ?? 0;\n } catch (error) {\n console.error('[Database] getLocationCount error:', error);\n return 0;\n}\n}\n\n// ============================================================================\n// Sync Operations\n// ============================================================================\n\n/**\n * Get unsynced changes\n */\nexport async function getUnsyncedChanges() {\n return sql`SELECT * FROM sync_log WHERE synced = 0 ORDER BY timestamp ASC`;\n}\n\n/**\n * Mark changes as synced\n */\nexport async function markSynced(syncLogIds) {\n if (!syncLogIds.length) return;\n for (const id of syncLogIds) {\n await sql`UPDATE sync_log SET synced = 1 WHERE id = ${id}`;\n}\n}\n\n/**\n * Get locations that need syncing\n */\nexport async function getUnsyncedLocations() {\n return sql`SELECT * FROM locations WHERE synced = 0`;\n}\n\n/**\n * Mark locations as synced\n */\nexport async function markLocationsSynced(ids) {\n if (!ids.length) return;\n for (const id of ids) {\n await sql`UPDATE locations SET synced = 1 WHERE id = ${id}`;\n}\n}\n\n// ============================================================================\n// Remote Data Cache\n// ============================================================================\n\n/**\n * Save remote API data to the local cache.\n * Uses INSERT OR REPLACE so the same key is always overwritten.\n *\n * @param {string} key - Unique identifier (e.g. 'district_boundary')\n * @param {Object|Array} data - JSON-serialisable data to cache\n */\nexport async function saveRemoteData(key, data) {\n try {\n const json = JSON.stringify(data);\n await sql`\n INSERT OR REPLACE INTO remote_data (key, data, fetched_at)\n VALUES (${key}, ${json}, CURRENT_TIMESTAMP)\n `;\n console.log('[Database] ✓ Remote data cached:', key);\n } catch (error) {\n console.error('[Database] ✗ Failed to cache remote data:', key, error);\n throw error;\n }\n}\n\n/**\n * Retrieve cached remote data by key.\n *\n * @param {string} key - Unique identifier (e.g. 'district_boundary')\n * @returns {Promise} Parsed data, or null if not cached\n */\nexport async function getRemoteData(key) {\n try {\n const rows = await sql`SELECT data, fetched_at FROM remote_data WHERE key = ${key}`;\n if (rows.length === 0) return null;\n const parsed = JSON.parse(rows[0].data);\n console.log('[Database] ✓ Remote data loaded from cache:', key, '(fetched', rows[0].fetched_at + ')');\n return parsed;\n } catch (error) {\n console.error('[Database] ✗ Failed to read cached remote data:', key, error);\n return null;\n }\n}\n\n// ============================================================================\n// Collector Zones\n// ============================================================================\n\n/**\n * Save collector zones to the local table.\n * Replaces all existing rows.\n *\n * @param {Array} zones - Array of zone objects from the API\n */\nexport async function saveCollectorZones(zones) {\n try {\n await sql`DELETE FROM collector_zones`;\n for (const z of zones) {\n const props = JSON.stringify(z);\n await sql`\n INSERT INTO collector_zones (id, zone_name, geometry_wkt, properties, fetched_at)\n VALUES (${z.colzonenr || z.id}, ${z.colzonename || z.zone_name || ''}, ${z.polygon || z.boundary || ''}, ${props}, CURRENT_TIMESTAMP)\n `;\n }\n console.log('[Database] ✓ Saved', zones.length, 'collector zones');\n } catch (error) {\n console.error('[Database] ✗ Failed to save collector zones:', error);\n throw error;\n }\n}\n\n/**\n * Load all cached collector zones from the local table.\n * @returns {Promise} Array of zone objects, or null if empty\n */\nexport async function getLocalCollectorZones() {\n try {\n const rows = await sql`SELECT properties FROM collector_zones ORDER BY id`;\n if (rows.length === 0) return null;\n return rows.map(r => JSON.parse(r.properties));\n } catch (error) {\n console.error('[Database] ✗ Failed to read local collector zones:', error);\n return null;\n }\n}\n\n// ============================================================================\n// Parcels\n// ============================================================================\n\n/** Coerce a value for an INTEGER column; '' / null / undefined / NaN → null. */\nfunction numOrNull(v) {\n if (v === '' || v === null || v === undefined) return null;\n const n = Number(v);\n return Number.isNaN(n) ? null : n;\n}\n\n/**\n * Save parcels to the local table (mirrors spatial.lu_parcels).\n * Replaces all server-cached rows. Each API field maps to its own column.\n * Geometry is taken from the server `geom` field (WKT, EPSG:4326).\n *\n * @param {Array} parcels - Array of parcel objects from the API\n */\nexport async function saveParcels(parcels) {\n try {\n // Wrap the bulk load in a single transaction — with ~25k parcels, per-row\n // auto-commits on OPFS would be prohibitively slow.\n await sql`BEGIN`;\n await sql`DELETE FROM parcels`;\n let saved = 0;\n for (const p of parcels) {\n const id = p.id ?? p.parcelid ?? p.parcel_id ?? null;\n if (id == null) continue; // skip rows without a usable ID\n // Geometry must be a WKT *string* for the geometry_wkt TEXT column.\n // The API sends WKT in `boundary` and a GeoJSON *object* in `geom`, so\n // prefer the string fields and only accept `geom` when it is a string.\n const wkt = p.boundary || p.geometry_wkt || p.polygon || p.wkt\n || (typeof p.geom === 'string' ? p.geom : '');\n await sql`\n INSERT OR REPLACE INTO parcels (\n id, upn, style, landuse, zone_code, zone_name, sector, block, parcel_no,\n prop_no, st_name, prop_add, fac_name, min_height, max_height, eff_date,\n lp_name, locality, mmda, last_update, remarks, geometry_wkt,\n created_at, updated_at, districtid, status, fetched_at\n ) VALUES (\n ${id}, ${p.upn ?? null}, ${numOrNull(p.style)}, ${p.landuse ?? null},\n ${p.zone_code ?? null}, ${p.zone_name ?? null}, ${p.sector ?? null},\n ${p.block ?? null}, ${p.parcel_no ?? null}, ${p.prop_no ?? null},\n ${p.st_name ?? null}, ${p.prop_add ?? null}, ${p.fac_name ?? null},\n ${numOrNull(p.min_height)}, ${numOrNull(p.max_height)}, ${p.eff_date ?? null},\n ${p.lp_name ?? null}, ${p.locality ?? null}, ${p.mmda ?? null},\n ${p.last_update ?? null}, ${p.remarks ?? null}, ${wkt},\n ${p.created_at ?? null}, ${p.updated_at ?? null}, ${numOrNull(p.districtid)},\n 'verified', CURRENT_TIMESTAMP\n )\n `;\n saved++;\n }\n await sql`COMMIT`;\n console.log('[Database] ✓ Saved', saved, 'parcels (from', parcels.length, 'rows,', parcels.length - saved, 'skipped/replaced)');\n } catch (error) {\n try { await sql`ROLLBACK`; } catch { /* no active txn */ }\n console.error('[Database] ✗ Failed to save parcels:', error);\n throw error;\n }\n}\n\n/**\n * Load all cached parcels from the local table.\n * Each row is a plain object keyed by column name (incl. geometry_wkt).\n * @returns {Promise} Array of parcel rows, or null if empty\n */\nexport async function getLocalParcels() {\n try {\n const rows = await sql`SELECT * FROM parcels ORDER BY id`;\n if (rows.length === 0) return null;\n return rows;\n } catch (error) {\n console.error('[Database] ✗ Failed to read local parcels:', error);\n return null;\n }\n}\n\n/**\n * Update a single parcel's attribute columns in the local table.\n * Geometry, id, created_at and status are left unchanged; updated_at is bumped.\n *\n * @param {number|string} parcelId - The parcel id\n * @param {Object} p - Updated attribute values (keys = lu_parcels columns)\n */\nexport async function updateParcel(parcelId, p) {\n try {\n await sql`\n UPDATE parcels SET\n upn = ${p.upn ?? null},\n style = ${numOrNull(p.style)},\n landuse = ${p.landuse ?? null},\n zone_code = ${p.zone_code ?? null},\n zone_name = ${p.zone_name ?? null},\n sector = ${p.sector ?? null},\n block = ${p.block ?? null},\n parcel_no = ${p.parcel_no ?? null},\n prop_no = ${p.prop_no ?? null},\n st_name = ${p.st_name ?? null},\n prop_add = ${p.prop_add ?? null},\n fac_name = ${p.fac_name ?? null},\n min_height = ${numOrNull(p.min_height)},\n max_height = ${numOrNull(p.max_height)},\n eff_date = ${p.eff_date ?? null},\n lp_name = ${p.lp_name ?? null},\n locality = ${p.locality ?? null},\n mmda = ${p.mmda ?? null},\n last_update = ${p.last_update ?? null},\n remarks = ${p.remarks ?? null},\n districtid = ${numOrNull(p.districtid)},\n updated_at = CURRENT_TIMESTAMP\n WHERE id = ${parcelId}\n `;\n console.log('[Database] ✓ Parcel updated:', parcelId);\n broadcastChange('parcels', 'UPDATE', parcelId);\n } catch (error) {\n console.error('[Database] ✗ Failed to update parcel:', parcelId, error);\n throw error;\n }\n}\n\n/**\n * Insert a newly drawn parcel into the local table.\n * The parcel is tagged with status='new' to indicate it needs verification.\n *\n * @param {string} geometryWkt - WKT geometry string (EPSG:4326)\n * @param {Object} p - Attribute values from the form (keys = lu_parcels columns)\n * @returns {Promise<{id: number}>} The new row id\n */\nexport async function insertNewParcel(geometryWkt, p = {}) {\n try {\n await sql`\n INSERT INTO parcels (\n id, upn, style, landuse, zone_code, zone_name, sector, block, parcel_no,\n prop_no, st_name, prop_add, fac_name, min_height, max_height, eff_date,\n lp_name, locality, mmda, last_update, remarks, geometry_wkt,\n created_at, updated_at, districtid, status, fetched_at\n ) VALUES (\n NULL, ${p.upn ?? null}, ${numOrNull(p.style)}, ${p.landuse ?? null},\n ${p.zone_code ?? null}, ${p.zone_name ?? null}, ${p.sector ?? null},\n ${p.block ?? null}, ${p.parcel_no ?? null}, ${p.prop_no ?? null},\n ${p.st_name ?? null}, ${p.prop_add ?? null}, ${p.fac_name ?? null},\n ${numOrNull(p.min_height)}, ${numOrNull(p.max_height)}, ${p.eff_date ?? null},\n ${p.lp_name ?? null}, ${p.locality ?? null}, ${p.mmda ?? null},\n ${p.last_update ?? null}, ${p.remarks ?? null}, ${geometryWkt},\n CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ${numOrNull(p.districtid)},\n 'new', CURRENT_TIMESTAMP\n )\n `;\n const idResult = await sql`SELECT last_insert_rowid() as id`;\n const newId = idResult[0]?.id;\n console.log('[Database] ✓ New parcel inserted:', newId, '(status: new)');\n broadcastChange('parcels', 'INSERT', newId);\n return { id: newId };\n } catch (error) {\n console.error('[Database] ✗ Failed to insert new parcel:', error);\n throw error;\n }\n}\n\n// ============================================================================\n// Building Footprints\n// ============================================================================\n\n/**\n * Save building footprints to the local table.\n * Replaces all existing rows.\n *\n * @param {Array} footprints - Array of footprint objects from the API\n */\nexport async function saveBuildingFootprints(footprints) {\n try {\n // Log first entry's keys and value types to help debug field names\n if (footprints.length > 0) {\n const first = footprints[0];\n const types = {};\n for (const [k, v] of Object.entries(first)) {\n types[k] = v === null ? 'null' : typeof v;\n }\n console.log('[Database] First footprint field types:', types);\n }\n\n await sql`DELETE FROM building_footprints`;\n for (const f of footprints) {\n const props = JSON.stringify(f);\n\n // Geometry may arrive as a string (WKT) or an object (GeoJSON) —\n // coerce to a string so SQLocal can bind it.\n let rawWkt = f.polygon || f.boundary || f.geom || f.wkt || f.footprint || '';\n const wkt = typeof rawWkt === 'object' ? JSON.stringify(rawWkt) : String(rawWkt);\n\n // ID must be a primitive (number or null)\n let rawId = f.id || f.footprint_id || f.building_id || null;\n const id = (rawId !== null && typeof rawId === 'object') ? null : rawId;\n\n await sql`\n INSERT INTO building_footprints (id, geometry_wkt, properties, fetched_at)\n VALUES (${id}, ${wkt}, ${props}, CURRENT_TIMESTAMP)\n `;\n }\n console.log('[Database] ✓ Saved', footprints.length, 'building footprints');\n } catch (error) {\n console.error('[Database] ✗ Failed to save building footprints:', error);\n throw error;\n }\n}\n\n/**\n * Load all cached building footprints from the local table.\n * @returns {Promise} Array of footprint objects, or null if empty\n */\nexport async function getLocalBuildingFootprints() {\n try {\n const rows = await sql`SELECT properties FROM building_footprints ORDER BY id`;\n if (rows.length === 0) return null;\n return rows.map(r => JSON.parse(r.properties));\n } catch (error) {\n console.error('[Database] ✗ Failed to read local building footprints:', error);\n return null;\n }\n}\n\n/**\n * Save OSM roads to the local SQLite table.\n * Replaces all existing rows.\n *\n * @param {Array} roads - Array of road objects from the API\n */\nexport async function saveOSMRoads(roads) {\n try {\n if (roads.length > 0) {\n const first = roads[0];\n const types = {};\n for (const [k, v] of Object.entries(first)) {\n types[k] = v === null ? 'null' : typeof v;\n }\n console.log('[Database] First road field types:', types);\n }\n\n await sql`DELETE FROM osm_roads`;\n for (const r of roads) {\n const props = JSON.stringify(r);\n\n // Geometry — may arrive as WKT string or GeoJSON object\n let rawWkt = r.geom || r.geometry || r.wkt || r.road || r.line || '';\n const wkt = typeof rawWkt === 'object' ? JSON.stringify(rawWkt) : String(rawWkt);\n\n // osm_id must be a primitive — fall back to null if missing or malformed\n let rawId = r.osm_id ?? r.osmid ?? r.id ?? null;\n const osmId = (rawId !== null && typeof rawId === 'object') ? null : rawId;\n\n await sql`\n INSERT OR REPLACE INTO osm_roads (osm_id, geometry_wkt, properties, fetched_at)\n VALUES (${osmId}, ${wkt}, ${props}, CURRENT_TIMESTAMP)\n `;\n }\n console.log('[Database] ✓ Saved', roads.length, 'OSM roads');\n } catch (error) {\n console.error('[Database] ✗ Failed to save OSM roads:', error);\n throw error;\n }\n}\n\n/**\n * Load all cached OSM roads from the local table.\n * @returns {Promise} Array of road objects, or null if empty\n */\nexport async function getLocalOSMRoads() {\n try {\n const rows = await sql`SELECT properties FROM osm_roads ORDER BY osm_id`;\n if (rows.length === 0) return null;\n return rows.map(r => JSON.parse(r.properties));\n } catch (error) {\n console.error('[Database] ✗ Failed to read local OSM roads:', error);\n return null;\n }\n}\n\n// ============================================================================\n// Export / Import\n// ============================================================================\n\n/**\n * Export database for backup\n */\nexport async function exportDatabase() {\n return db.getDatabaseFile();\n}\n\n/**\n * Import database from backup\n */\nexport async function importDatabase(data) {\n await db.overwriteDatabaseFile(data);\n broadcastChange('*', 'IMPORT', null);\n}\n\n/**\n * Download database as file\n */\nexport async function downloadDatabase(filename = 'lupmis-backup.sqlite3') {\n const data = await exportDatabase();\n const blob = new Blob([data], { type: 'application/x-sqlite3' });\n const url = URL.createObjectURL(blob);\n\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n a.click();\n\n URL.revokeObjectURL(url);\n}\n\n// Export to GeoJSON\nexport async function exportToGeoJSON() {\n const locations = await getLocations();\n\n return {\n type: 'FeatureCollection',\n features: locations.map((loc) => ({\n type: 'Feature',\n properties: {\n id: loc.id,\n name: loc.name,\n category: loc.category,\n notes: loc.notes,\n created_at: loc.created_at,\n },\n geometry: {\n type: 'Point',\n coordinates: [loc.lon, loc.lat],\n },\n })),\n };\n}\n\n// ============================================================================\n// Utility & Debug\n// ============================================================================\n\n/**\n * Get database status\n */\nexport async function getDatabaseStatus() {\n try {\n const tables = await sql`\n SELECT name FROM sqlite_master\n WHERE type='table' AND name NOT LIKE 'sqlite_%'\n ORDER BY name\n `;\n\n const locationCount = await getLocationCount();\n\n return {\n ready: isReady,\n databasePath: DATABASE_PATH,\n tables: tables.map(t => t.name),\n locationCount\n };\n } catch (error) {\n return {\n ready: false,\n error: error.message\n };\n}\n}\n\n// ============================================================================\n// Cached-Layer Management\n// ============================================================================\n\n/**\n * Tables that hold data fetched from the server.\n *\n * Clearing these is safe — the corresponding loaders (loadParcels,\n * loadBuildingFootprints, loadOSMRoads, loadCollectorZones, …) will re-fetch\n * the data from the API on the next app start.\n *\n * NOT included: user-created tables (`locations`, `pending_changes`) — those\n * hold local work that must not be auto-deleted.\n */\nexport const CACHED_LAYER_TABLES = Object.freeze([\n 'parcels',\n 'building_footprints',\n 'osm_roads',\n 'collector_zones',\n 'remote_data',\n]);\n\n/**\n * Check whether a table name is in the cleared-layer allow-list.\n * @param {string} tableName\n * @returns {boolean}\n */\nexport function isCachedLayerTable(tableName) {\n return CACHED_LAYER_TABLES.includes(tableName);\n}\n\n/**\n * Delete all rows from a single cached-layer table.\n * Rejects unknown table names so this can't be abused to drop user data.\n *\n * @param {string} tableName - One of CACHED_LAYER_TABLES\n * @returns {Promise} Number of rows that were in the table before deletion\n */\nexport async function clearTable(tableName) {\n if (!isCachedLayerTable(tableName)) {\n throw new Error(`Refusing to clear \"${tableName}\" — not a known cached-layer table`);\n }\n\n const before = await sql(`SELECT COUNT(*) AS n FROM \"${tableName}\"`);\n const count = before[0]?.n ?? 0;\n\n await sql(`DELETE FROM \"${tableName}\"`);\n console.log(`[Database] ✓ Cleared \"${tableName}\" (${count} rows)`);\n broadcastChange(tableName, 'CLEAR', null);\n return count;\n}\n\n/**\n * Clear every cached-layer table (whatever exists in this database).\n * Tables that don't exist yet are skipped silently.\n *\n * @returns {Promise<{ table: string, count: number }[]>} per-table report\n */\nexport async function clearAllCachedLayers() {\n const existing = await sql`\n SELECT name FROM sqlite_master\n WHERE type='table' AND name IN (\n 'parcels', 'building_footprints', 'osm_roads', 'collector_zones', 'remote_data'\n )\n `;\n const existingNames = new Set(existing.map((r) => r.name));\n\n const results = [];\n for (const tableName of CACHED_LAYER_TABLES) {\n if (!existingNames.has(tableName)) continue;\n try {\n const count = await clearTable(tableName);\n results.push({ table: tableName, count });\n } catch (err) {\n console.error(`[Database] Failed to clear ${tableName}:`, err);\n results.push({ table: tableName, count: 0, error: err.message });\n }\n }\n\n const total = results.reduce((s, r) => s + r.count, 0);\n console.log(`[Database] ✓ Cleared all cached layers: ${total} rows across ${results.length} tables`);\n return results;\n}\n\n/**\n * Get a list of all tables with their row counts.\n * @returns {Promise>}\n */\nexport async function getTableStats() {\n const tables = await sql`\n SELECT name FROM sqlite_master\n WHERE type='table' AND name NOT LIKE 'sqlite_%'\n ORDER BY name\n `;\n\n if (tables.length === 0) return [];\n\n // Build a UNION ALL query — table names come from sqlite_master so safe to embed.\n // sql() accepts a plain string as first argument (not only tagged template).\n const query = tables\n .map(t => `SELECT '${t.name}' AS name, COUNT(*) AS count FROM \"${t.name}\"`)\n .join(' UNION ALL ');\n\n return sql(query);\n}\n\n/**\n * Get all rows from a given table (max 200 rows for safety).\n * Table name is validated against sqlite_master before querying.\n * @param {string} tableName - Name of the table to read\n * @param {number} [limit=200] - Max rows to return\n * @returns {Promise<{columns: string[], rows: Object[]}>}\n */\nexport async function getTableContent(tableName, limit = 200) {\n // Validate that the table actually exists (prevent injection)\n const valid = await sql`\n SELECT name FROM sqlite_master\n WHERE type='table' AND name = ${tableName}\n `;\n if (valid.length === 0) {\n throw new Error(`Table \"${tableName}\" does not exist`);\n }\n\n const rows = await sql(`SELECT * FROM \"${tableName}\" LIMIT ${limit}`);\n\n // Extract column names from the first row (or return empty)\n const columns = rows.length > 0 ? Object.keys(rows[0]) : [];\n\n return { columns, rows };\n}\n\n// Debug function - call from console to test\nexport async function testDatabase() {\n console.log('=== DATABASE TEST ===');\n\n try {\n // 1. Check connection\n const version = await sql`SELECT sqlite_version() as v`;\n console.log('1. SQLite version:', version[0].v);\n\n // 2. Check tables\n const tables = await sql`SELECT name FROM sqlite_master WHERE type='table'`;\n console.log('2. Tables:', tables.map(t => t.name));\n\n // 3. Try to insert a test row\n console.log('3. Inserting test row...');\n await sql`INSERT INTO locations (name, longitude, latitude, category) VALUES ('TEST', -1.0, 7.0, 'test')`;\n\n // 4. Read it back\n const rows = await sql`SELECT * FROM locations WHERE name = 'TEST'`;\n console.log('4. Test row:', rows);\n\n // 5. Count all rows\n const count = await sql`SELECT COUNT(*) as c FROM locations`;\n console.log('5. Total rows:', count[0].c);\n\n // 6. Delete test row\n await sql`DELETE FROM locations WHERE name = 'TEST'`;\n console.log('6. Test row deleted');\n\n console.log('=== TEST PASSED ===');\n return true;\n } catch (error) {\n console.error('=== TEST FAILED ===', error);\n return false;\n}\n}\n\n// Expose to window for debugging\nif (typeof window !== 'undefined') {\n window.testDatabase = testDatabase;\n window.dbStatus = getDatabaseStatus;\n}\n\nexport async function closeDatabase() {\n channel.close();\n if (db.destroy) {\n await db.destroy();\n}\n}\n\n// ============================================================================\n// GPS Trails (storage adapter for the reusable GeoTracker module)\n// ============================================================================\n//\n// These functions implement the GeoTracker \"storage adapter\" contract using\n// SQLocal. They are intentionally generic (no map / UI coupling) so the same\n// schema + helpers can be lifted into another app alongside src/geotracker/.\n\n/**\n * Create a new trail row (status='recording') and return its local id.\n * @param {object} meta { uuid, name, startedAt, districtId }\n * @returns {Promise} local trail id\n */\nexport async function createGpsTrail(meta) {\n const { uuid, name = null, startedAt, districtId = null } = meta;\n await sql`\n INSERT INTO gps_trails (client_uuid, name, district_id, started_at, status)\n VALUES (${uuid}, ${name}, ${districtId}, ${startedAt}, 'recording')\n `;\n const idResult = await sql`SELECT last_insert_rowid() as id`;\n const id = idResult[0]?.id;\n broadcastChange('gps_trails', 'insert', id);\n return id;\n}\n\n/**\n * Append one recorded point to a trail.\n * @param {number} trailId\n * @param {object} point normalized fix + { seq }\n */\nexport async function addGpsTrailPoint(trailId, point) {\n const {\n seq, lon, lat,\n altitude = null, accuracy = null, altitudeAccuracy = null,\n heading = null, speed = null, satellites = null, timestamp,\n } = point;\n const recordedAt = typeof timestamp === 'number' ? new Date(timestamp).toISOString() : (timestamp || new Date().toISOString());\n await sql`\n INSERT INTO gps_trail_points\n (trail_id, seq, longitude, latitude, altitude, accuracy, altitude_accuracy, heading, speed, satellites, recorded_at)\n VALUES\n (${trailId}, ${seq}, ${lon}, ${lat}, ${altitude}, ${accuracy}, ${altitudeAccuracy}, ${heading}, ${speed}, ${satellites}, ${recordedAt})\n `;\n}\n\n/**\n * Finalise a trail: mark completed and store the summary.\n * @param {number} trailId\n * @param {object} summary { endedAt, pointCount, distanceM }\n */\nexport async function finishGpsTrail(trailId, summary) {\n const { endedAt, pointCount = 0, distanceM = 0 } = summary;\n await sql`\n UPDATE gps_trails\n SET ended_at = ${endedAt}, point_count = ${pointCount}, distance_m = ${distanceM}, status = 'completed'\n WHERE id = ${trailId}\n `;\n broadcastChange('gps_trails', 'update', trailId);\n}\n\n/**\n * Trails that are completed but not yet pushed to the server.\n * @returns {Promise}\n */\nexport async function getUnsyncedGpsTrails() {\n return sql`SELECT * FROM gps_trails WHERE synced = 0 AND status = 'completed' ORDER BY started_at ASC`;\n}\n\n/**\n * All points of a trail, in recorded order.\n * @param {number} trailId\n * @returns {Promise}\n */\nexport async function getGpsTrailPoints(trailId) {\n return sql`SELECT * FROM gps_trail_points WHERE trail_id = ${trailId} ORDER BY seq ASC`;\n}\n\n/**\n * Mark a trail as synced and record the server-assigned id.\n * @param {number} trailId\n * @param {string|number|null} remoteId\n */\nexport async function markGpsTrailSynced(trailId, remoteId = null) {\n await sql`UPDATE gps_trails SET synced = 1, remote_id = ${remoteId} WHERE id = ${trailId}`;\n broadcastChange('gps_trails', 'update', trailId);\n}\n\n/**\n * List trails (most recent first) for a history/list UI.\n * @returns {Promise}\n */\nexport async function getGpsTrails() {\n return sql`SELECT * FROM gps_trails ORDER BY started_at DESC`;\n}\n\n/**\n * Delete a trail and all its points.\n * @param {number} trailId\n */\nexport async function deleteGpsTrail(trailId) {\n await sql`DELETE FROM gps_trail_points WHERE trail_id = ${trailId}`;\n await sql`DELETE FROM gps_trails WHERE id = ${trailId}`;\n broadcastChange('gps_trails', 'delete', trailId);\n}\n\nexport default {\n sql,\n dbReady,\n initSchema,\n addLocation,\n getLocations,\n getLocation,\n updateLocation,\n deleteLocation,\n getLocationCount,\n getUnsyncedChanges,\n getUnsyncedLocations,\n markSynced,\n markLocationsSynced,\n saveRemoteData,\n getRemoteData,\n saveCollectorZones,\n getLocalCollectorZones,\n saveParcels,\n getLocalParcels,\n updateParcel,\n insertNewParcel,\n saveBuildingFootprints,\n getLocalBuildingFootprints,\n saveOSMRoads,\n getLocalOSMRoads,\n createGpsTrail,\n addGpsTrailPoint,\n finishGpsTrail,\n getUnsyncedGpsTrails,\n getGpsTrailPoints,\n markGpsTrailSynced,\n getGpsTrails,\n deleteGpsTrail,\n CACHED_LAYER_TABLES,\n isCachedLayerTable,\n clearTable,\n clearAllCachedLayers,\n exportDatabase,\n exportToGeoJSON,\n importDatabase,\n downloadDatabase,\n getDatabaseStatus,\n getTableStats,\n getTableContent,\n testDatabase,\n onDatabaseChange,\n closeDatabase\n};\n","/**\n * Measurement unit formatting — Metric / Imperial.\n *\n * The active system is persisted in localStorage('measurement-system').\n * Every formatter reads the current setting so the UI updates immediately\n * after the user flips the toggle.\n *\n * All input values are in metres (length) or square metres (area).\n */\n\n// ── Conversion constants ────────────────────────────────────────────────────\nconst M_TO_FT = 3.28084;\nconst M_TO_MI = 0.000621371;\nconst SQM_TO_SQFT = 10.7639;\nconst SQM_TO_ACRE = 0.000247105;\nconst SQM_TO_SQMI = 3.861e-7;\n\n// ── System accessor ─────────────────────────────────────────────────────────\n\n/** @returns {'metric'|'imperial'} */\nexport function getSystem() {\n return localStorage.getItem('measurement-system') || 'metric';\n}\n\n// ── Length / distance ───────────────────────────────────────────────────────\n\n/**\n * Format a length value (in metres) for display.\n * Metric: m / km\n * Imperial: ft / mi\n */\nexport function formatLength(metres) {\n if (getSystem() === 'imperial') {\n const ft = metres * M_TO_FT;\n if (ft >= 5280) {\n return (Math.round(metres * M_TO_MI * 100) / 100) + ' mi';\n }\n return Math.round(ft) + ' ft';\n }\n // metric\n if (metres > 1000) {\n return (Math.round(metres / 1000 * 100) / 100) + ' km';\n }\n return (Math.round(metres * 100) / 100) + ' m';\n}\n\n/**\n * Format a length with both large and small units (for info popups).\n * Metric: \"1.23 km (1,230 m)\" or \"456 m\"\n * Imperial: \"1.23 mi (6,494 ft)\" or \"456 ft\"\n */\nexport function formatLengthFull(metres) {\n if (getSystem() === 'imperial') {\n const ft = metres * M_TO_FT;\n const mi = metres * M_TO_MI;\n if (ft >= 5280) {\n return `${mi.toFixed(2)} mi (${ft.toLocaleString('en', { maximumFractionDigits: 0 })} ft)`;\n }\n return `${ft.toLocaleString('en', { maximumFractionDigits: 1 })} ft`;\n }\n if (metres >= 1000) {\n return `${(metres / 1000).toFixed(2)} km (${metres.toLocaleString('en', { maximumFractionDigits: 0 })} m)`;\n }\n return `${metres.toLocaleString('en', { maximumFractionDigits: 1 })} m`;\n}\n\n// ── Area ────────────────────────────────────────────────────────────────────\n\n/**\n * Format an area value (in square metres) for display.\n * Metric: m² / km²\n * Imperial: ft² / acres / mi²\n */\nexport function formatArea(sqMetres) {\n if (getSystem() === 'imperial') {\n const acres = sqMetres * SQM_TO_ACRE;\n if (acres >= 640) {\n return (Math.round(sqMetres * SQM_TO_SQMI * 100) / 100) + ' mi²';\n }\n if (acres >= 1) {\n return (Math.round(acres * 100) / 100) + ' acres';\n }\n return Math.round(sqMetres * SQM_TO_SQFT).toLocaleString('en') + ' ft²';\n }\n // metric\n if (sqMetres > 1000000) {\n return (Math.round(sqMetres / 1000000 * 100) / 100) + ' km²';\n }\n return (Math.round(sqMetres * 100) / 100) + ' m²';\n}\n\n/**\n * Format an area with both large and small units (for info popups).\n * Metric: \"1.23 km² (1,230,000 m²)\" or \"456 m²\"\n * Imperial: \"1.23 mi² (787 acres)\" or \"2.5 acres\" or \"456 ft²\"\n */\nexport function formatAreaFull(sqMetres) {\n if (getSystem() === 'imperial') {\n const sqft = sqMetres * SQM_TO_SQFT;\n const acres = sqMetres * SQM_TO_ACRE;\n const sqmi = sqMetres * SQM_TO_SQMI;\n if (acres >= 640) {\n return `${sqmi.toFixed(2)} mi² (${acres.toLocaleString('en', { maximumFractionDigits: 0 })} acres)`;\n }\n if (acres >= 1) {\n return `${acres.toLocaleString('en', { maximumFractionDigits: 1 })} acres (${sqft.toLocaleString('en', { maximumFractionDigits: 0 })} ft²)`;\n }\n return `${sqft.toLocaleString('en', { maximumFractionDigits: 0 })} ft²`;\n }\n if (sqMetres > 1000000) {\n return `${(sqMetres / 1000000).toFixed(2)} km² (${sqMetres.toLocaleString('en', { maximumFractionDigits: 0 })} m²)`;\n }\n return `${sqMetres.toLocaleString('en', { maximumFractionDigits: 0 })} m²`;\n}\n\n// ── Circle helper ───────────────────────────────────────────────────────────\n\n/**\n * Format the area of a circle given its radius (in metres).\n */\nexport function formatCircleExtent(radiusMetres) {\n return formatArea(Math.PI * radiusMetres * radiusMetres);\n}\n","/**\n * Pure geometry functions for splitting a polygon by a line.\n *\n * No OpenLayers dependency — operates on raw coordinate arrays.\n */\n\n/**\n * Compute the intersection point of two 2D line segments.\n * Segment A: p1→p2, Segment B: p3→p4.\n *\n * @param {number[]} p1\n * @param {number[]} p2\n * @param {number[]} p3\n * @param {number[]} p4\n * @param {number} [eps=1e-10] tolerance for parallel check\n * @returns {{ point: number[], t: number, u: number } | null}\n * t = parametric position on segment A (0–1),\n * u = parametric position on segment B (0–1)\n */\nfunction segmentIntersection(p1, p2, p3, p4, eps = 1e-10) {\n const dx1 = p2[0] - p1[0];\n const dy1 = p2[1] - p1[1];\n const dx2 = p4[0] - p3[0];\n const dy2 = p4[1] - p3[1];\n\n const denom = dx1 * dy2 - dy1 * dx2;\n if (Math.abs(denom) < eps) return null; // parallel / collinear\n\n const dx3 = p3[0] - p1[0];\n const dy3 = p3[1] - p1[1];\n\n const t = (dx3 * dy2 - dy3 * dx2) / denom;\n const u = (dx3 * dy1 - dy3 * dx1) / denom;\n\n if (t < -eps || t > 1 + eps || u < -eps || u > 1 + eps) return null;\n\n return {\n point: [p1[0] + t * dx1, p1[1] + t * dy1],\n t: Math.max(0, Math.min(1, t)),\n u: Math.max(0, Math.min(1, u)),\n };\n}\n\n/**\n * Signed area of a ring (shoelace formula).\n * Positive = counter-clockwise, negative = clockwise.\n */\nfunction signedArea(ring) {\n let area = 0;\n for (let i = 0, n = ring.length; i < n - 1; i++) {\n area += (ring[i][0] * ring[i + 1][1]) - (ring[i + 1][0] * ring[i][1]);\n }\n return area / 2;\n}\n\n/**\n * Test whether a point is inside a ring (ray-casting algorithm).\n */\nfunction pointInRing(pt, ring) {\n let inside = false;\n for (let i = 0, j = ring.length - 2; i < ring.length - 1; j = i++) {\n const xi = ring[i][0], yi = ring[i][1];\n const xj = ring[j][0], yj = ring[j][1];\n if (((yi > pt[1]) !== (yj > pt[1])) &&\n (pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi)) {\n inside = !inside;\n }\n }\n return inside;\n}\n\n/**\n * Squared distance between two points.\n */\nfunction dist2(a, b) {\n return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;\n}\n\n/**\n * Find all intersection points between a cutting line and a polygon ring.\n *\n * @param {number[][]} ring Closed ring coordinates (first === last)\n * @param {number[][]} line LineString coordinates (2+ points)\n * @returns {Array<{ point: number[], ringSegIdx: number, ringT: number, lineSegIdx: number, lineT: number }>}\n */\nfunction findIntersections(ring, line) {\n const hits = [];\n const eps = 1e-10;\n\n for (let li = 0; li < line.length - 1; li++) {\n for (let ri = 0; ri < ring.length - 1; ri++) {\n const ix = segmentIntersection(ring[ri], ring[ri + 1], line[li], line[li + 1], eps);\n if (!ix) continue;\n\n // Skip if intersection is at the very start of the ring segment\n // but was already caught as the end of the previous segment\n const pt = ix.point;\n\n // Avoid duplicate hits at shared vertices\n let isDup = false;\n for (const h of hits) {\n if (dist2(h.point, pt) < 1e-6) {\n isDup = true;\n break;\n }\n }\n if (isDup) continue;\n\n hits.push({\n point: pt,\n ringSegIdx: ri,\n ringT: ix.t,\n lineSegIdx: li,\n lineT: ix.u,\n });\n }\n }\n\n // Sort by position along the cutting line\n hits.sort((a, b) => {\n if (a.lineSegIdx !== b.lineSegIdx) return a.lineSegIdx - b.lineSegIdx;\n return a.lineT - b.lineT;\n });\n\n return hits;\n}\n\n/**\n * Insert intersection points into a ring, returning the expanded ring\n * and the new indices of the inserted points.\n *\n * @param {number[][]} ring Closed ring (first === last)\n * @param {Array<{ point: number[], ringSegIdx: number, ringT: number }>} hits\n * Sorted by ringSegIdx then ringT.\n * @returns {{ ring: number[][], indices: number[] }}\n */\nfunction insertPointsIntoRing(ring, hits) {\n // Sort hits by ring position (segment index, then parametric t) so\n // we can insert from back to front without shifting earlier indices.\n const sorted = hits.map((h, i) => ({ ...h, origOrder: i }));\n sorted.sort((a, b) => {\n if (a.ringSegIdx !== b.ringSegIdx) return a.ringSegIdx - b.ringSegIdx;\n return a.ringT - b.ringT;\n });\n\n const expanded = ring.slice(); // copy\n const indices = new Array(sorted.length);\n\n // Insert from the end so that earlier insertions don't shift later indices.\n for (let k = sorted.length - 1; k >= 0; k--) {\n const h = sorted[k];\n const insertIdx = h.ringSegIdx + 1;\n\n // Check if this point is essentially identical to an existing vertex\n const snapDist = 1e-6;\n if (dist2(h.point, expanded[h.ringSegIdx]) < snapDist) {\n indices[h.origOrder] = h.ringSegIdx;\n continue;\n }\n if (dist2(h.point, expanded[h.ringSegIdx + 1]) < snapDist) {\n indices[h.origOrder] = h.ringSegIdx + 1;\n continue;\n }\n\n // Insert the new point\n expanded.splice(insertIdx, 0, h.point);\n indices[h.origOrder] = insertIdx;\n\n // Adjust indices for all previously recorded insertions\n // that reference a position >= insertIdx\n for (let j = k + 1; j < sorted.length; j++) {\n if (indices[sorted[j].origOrder] >= insertIdx) {\n indices[sorted[j].origOrder]++;\n }\n }\n }\n\n return { ring: expanded, indices };\n}\n\n/**\n * Extract a slice of a ring from index i0 to i1 (going forward, wrapping).\n * Both endpoints are included.\n *\n * @param {number[][]} ring Closed ring (first === last); length includes closing vertex\n * @param {number} i0 Start index (inclusive)\n * @param {number} i1 End index (inclusive)\n * @returns {number[][]}\n */\nfunction ringSlice(ring, i0, i1) {\n const n = ring.length - 1; // number of unique vertices (ring is closed)\n // Normalise indices into the [0, n-1] range\n const start = ((i0 % n) + n) % n;\n const end = ((i1 % n) + n) % n;\n const result = [];\n let idx = start;\n while (true) {\n result.push(ring[idx]);\n if (idx === end) break;\n idx = (idx + 1) % n;\n }\n return result;\n}\n\n/**\n * Extract the cutting line segment between two intersection points.\n *\n * @param {number[][]} line Full cutting line coordinates\n * @param {{ point: number[], lineSegIdx: number, lineT: number }} hit0\n * @param {{ point: number[], lineSegIdx: number, lineT: number }} hit1\n * @returns {number[][]} Coordinates from hit0.point to hit1.point along the line\n */\nfunction cuttingLineSlice(line, hit0, hit1) {\n const result = [hit0.point];\n\n // Include all intermediate line vertices between the two hit segments\n const startSeg = hit0.lineSegIdx;\n const endSeg = hit1.lineSegIdx;\n\n for (let i = startSeg + 1; i <= endSeg; i++) {\n result.push(line[i]);\n }\n\n // Add the end intersection point if it's not the same as the last vertex\n if (dist2(result[result.length - 1], hit1.point) > 1e-10) {\n result.push(hit1.point);\n }\n\n return result;\n}\n\n/**\n * Ensure a ring has the desired winding order.\n * @param {number[][]} ring Closed ring\n * @param {boolean} ccw true for counter-clockwise\n * @returns {number[][]}\n */\nfunction ensureWinding(ring, ccw) {\n const area = signedArea(ring);\n if ((ccw && area < 0) || (!ccw && area > 0)) {\n return ring.slice().reverse();\n }\n return ring;\n}\n\n/**\n * Close a ring (ensure first === last).\n */\nfunction closeRing(coords) {\n if (coords.length < 2) return coords;\n const first = coords[0];\n const last = coords[coords.length - 1];\n if (dist2(first, last) > 1e-10) {\n return [...coords, first.slice()];\n }\n return coords;\n}\n\n/**\n * Extend a cutting line so that both endpoints lie outside the polygon ring.\n * If an endpoint is inside, we extend the first/last segment outward past the\n * bounding box diagonal so it definitely exits.\n *\n * @param {number[][]} line Cutting line coordinates\n * @param {number[][]} ring Closed polygon ring\n * @returns {number[][]} Extended line (may be the original if already outside)\n */\nfunction extendLineOutsideRing(line, ring) {\n // Compute bounding-box diagonal for a generous extension distance\n let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;\n for (const pt of ring) {\n if (pt[0] < minX) minX = pt[0];\n if (pt[1] < minY) minY = pt[1];\n if (pt[0] > maxX) maxX = pt[0];\n if (pt[1] > maxY) maxY = pt[1];\n }\n const diag = Math.sqrt((maxX - minX) ** 2 + (maxY - minY) ** 2) || 1;\n\n const result = line.slice();\n\n // Extend start if inside\n if (pointInRing(result[0], ring)) {\n const p0 = result[0];\n const p1 = result[1];\n const dx = p0[0] - p1[0];\n const dy = p0[1] - p1[1];\n const len = Math.sqrt(dx * dx + dy * dy) || 1;\n const scale = diag * 2 / len;\n result[0] = [p0[0] + dx * scale, p0[1] + dy * scale];\n }\n\n // Extend end if inside\n const last = result.length - 1;\n if (pointInRing(result[last], ring)) {\n const pN = result[last];\n const pN1 = result[last - 1];\n const dx = pN[0] - pN1[0];\n const dy = pN[1] - pN1[1];\n const len = Math.sqrt(dx * dx + dy * dy) || 1;\n const scale = diag * 2 / len;\n result[last] = [pN[0] + dx * scale, pN[1] + dy * scale];\n }\n\n return result;\n}\n\n/**\n * Split a polygon by a cutting line.\n *\n * The cutting line can start or end inside the polygon — the algorithm will\n * automatically extend it outward so it crosses the boundary at exactly 2\n * points. Multi-vertex cutting lines (with corners or approximated arcs)\n * are fully supported.\n *\n * @param {number[][][]} polygonCoords Polygon coordinates:\n * [exteriorRing, ...holeRings] where each ring is closed (first === last)\n * @param {number[][]} lineCoords Cutting line coordinates (2+ points)\n * @returns {number[][][][] | null} Two polygon coordinate arrays, or null if split failed\n */\nexport function splitPolygonByLine(polygonCoords, lineCoords) {\n const exteriorRing = polygonCoords[0];\n const holes = polygonCoords.slice(1);\n\n // Extend the cutting line if its endpoints are inside the polygon\n const extendedLine = extendLineOutsideRing(lineCoords, exteriorRing);\n\n // 1. Find intersections between cutting line and exterior ring\n const hits = findIntersections(exteriorRing, extendedLine);\n\n // We need exactly 2 intersection points for a simple split\n if (hits.length !== 2) {\n console.warn(`[polygonSplit] Expected 2 intersections, got ${hits.length}`);\n return null;\n }\n\n const [hit0, hit1] = hits;\n\n // 2. Insert intersection points into the ring\n const { ring: expandedRing, indices } = insertPointsIntoRing(exteriorRing, hits);\n const idx0 = indices[0];\n const idx1 = indices[1];\n\n // Ensure idx0 < idx1 for consistent traversal\n const [iA, iB] = idx0 < idx1 ? [idx0, idx1] : [idx1, idx0];\n const [hitA, hitB] = idx0 < idx1 ? [hit0, hit1] : [hit1, hit0];\n\n // 3. Get the cutting line segment between the two intersection points\n const cutForward = idx0 < idx1\n ? cuttingLineSlice(extendedLine, hit0, hit1)\n : cuttingLineSlice(extendedLine, hit1, hit0);\n const cutReverse = cutForward.slice().reverse();\n\n // 4. Build two polygon rings\n // Ring A: walk ring from iA to iB (forward), then cutting line reversed back to iA\n const sliceAB = ringSlice(expandedRing, iA, iB);\n const ringA = closeRing([...sliceAB, ...cutReverse.slice(1)]);\n\n // Ring B: walk ring from iB to iA (wrapping), then cutting line forward back to iB\n const sliceBA = ringSlice(expandedRing, iB, iA);\n const ringB = closeRing([...sliceBA, ...cutForward.slice(1)]);\n\n // 5. Match winding order to original\n const originalCCW = signedArea(exteriorRing) > 0;\n const finalA = ensureWinding(ringA, originalCCW);\n const finalB = ensureWinding(ringB, originalCCW);\n\n // 6. Build polygon coordinate arrays, assigning holes to the correct piece\n const polyA = [finalA];\n const polyB = [finalB];\n\n for (const hole of holes) {\n // Use the centroid of the hole to determine containment\n const centroid = holeCentroid(hole);\n if (pointInRing(centroid, finalA)) {\n polyA.push(hole);\n } else {\n polyB.push(hole);\n }\n }\n\n return [polyA, polyB];\n}\n\n/**\n * Compute the centroid of a closed ring.\n */\nfunction holeCentroid(ring) {\n let cx = 0, cy = 0;\n const n = ring.length - 1; // exclude closing vertex\n for (let i = 0; i < n; i++) {\n cx += ring[i][0];\n cy += ring[i][1];\n }\n return [cx / n, cy / n];\n}\n","/**\n * Lightweight toast notification system.\n *\n * Usage:\n * import { showToast } from '../toast.js';\n *\n * showToast('Something went wrong', 'error');\n * showToast('Merge successful!', 'success');\n * showToast('Select two adjacent polygons', 'info');\n */\n\n// ── Palette ──────────────────────────────────────────────────────────────────\n\nconst THEMES = {\n success: { bg: '#10b981', icon: '\\u2705' }, // green\n error: { bg: '#ef4444', icon: '\\u274c' }, // red\n warning: { bg: '#f59e0b', icon: '\\u26a0\\ufe0f' }, // amber\n info: { bg: '#0ea5e9', icon: '\\u2139\\ufe0f' }, // cyan\n};\n\n// ── Container (created once, appended to ) ────────────────────────────\n\nlet container = null;\n\nfunction ensureContainer() {\n if (container) return container;\n container = document.createElement('div');\n container.style.cssText = `\n position: fixed;\n top: 16px;\n left: 50%;\n transform: translateX(-50%);\n z-index: 10000;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n pointer-events: none;\n `;\n document.body.appendChild(container);\n return container;\n}\n\n// ── Public API ──────────────────────────────────────────────────────────────\n\n/**\n * Display a toast notification.\n *\n * @param {string} message Plain-text message to show.\n * @param {'success'|'error'|'warning'|'info'} [type='info']\n * @param {number} [duration=4000] Auto-dismiss time in ms.\n */\nexport function showToast(message, type = 'info', duration = 4000) {\n const parent = ensureContainer();\n const theme = THEMES[type] || THEMES.info;\n\n const el = document.createElement('div');\n el.style.cssText = `\n background: ${theme.bg};\n color: #fff;\n padding: 10px 18px;\n border-radius: 8px;\n font-family: var(--font-body, 'Exo', sans-serif);\n font-size: 13px;\n font-weight: 600;\n box-shadow: 0 4px 12px rgba(0,0,0,0.25);\n pointer-events: auto;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.25s ease, transform 0.25s ease;\n transform: translateY(-8px);\n max-width: 420px;\n text-align: center;\n line-height: 1.4;\n `;\n el.textContent = `${theme.icon} ${message}`;\n\n parent.appendChild(el);\n\n // Animate in\n requestAnimationFrame(() => {\n el.style.opacity = '1';\n el.style.transform = 'translateY(0)';\n });\n\n // Dismiss helper\n const dismiss = () => {\n el.style.opacity = '0';\n el.style.transform = 'translateY(-8px)';\n setTimeout(() => el.remove(), 300);\n };\n\n // Click to dismiss early\n el.addEventListener('click', dismiss);\n\n // Auto-dismiss\n setTimeout(dismiss, duration);\n}\n","/**\n * PolygonSplitInteraction\n *\n * A two-phase OpenLayers interaction for splitting polygons:\n * Phase 1 – SELECT: hover to highlight, click to select a polygon\n * Phase 2 – DRAW: draw a cutting line, double-click to finish\n *\n * After a successful split the original feature is removed and two new\n * coloured features are added. The interaction fires `beforesplit` and\n * `aftersplit` events compatible with ol-ext's UndoRedo.\n */\n\nimport ol_interaction_Interaction from 'ol/interaction/Interaction';\nimport ol_interaction_Draw from 'ol/interaction/Draw';\nimport VectorSource from 'ol/source/Vector';\nimport VectorLayer from 'ol/layer/Vector';\nimport Feature from 'ol/Feature';\nimport { Style, Stroke, Fill, Circle as CircleStyle } from 'ol/style';\nimport { LineString } from 'ol/geom';\nimport { Polygon as PolygonGeom } from 'ol/geom';\nimport { splitPolygonByLine } from '../geom/polygonSplit.js';\nimport { showToast } from '../toast.js';\n\n// Marker colours for the two split pieces\nconst SPLIT_COLORS = [\n { stroke: '#ef4444', fill: 'rgba(239,68,68,0.25)' }, // red\n { stroke: '#3b82f6', fill: 'rgba(59,130,246,0.25)' }, // blue\n];\n\n// Highlight style for the selected polygon (phase 1)\nconst HIGHLIGHT_STYLE = new Style({\n stroke: new Stroke({ color: '#0ea5e9', width: 3 }),\n fill: new Fill({ color: 'rgba(14,165,233,0.15)' }),\n});\n\n// Style for the cutting-line sketch (phase 2)\nconst SKETCH_STYLE = new Style({\n stroke: new Stroke({ color: '#f43f5e', width: 2, lineDash: [8, 6] }),\n image: new CircleStyle({\n radius: 5,\n fill: new Fill({ color: '#f43f5e' }),\n stroke: new Stroke({ color: '#fff', width: 1.5 }),\n }),\n});\n\nexport class PolygonSplitInteraction extends ol_interaction_Interaction {\n /**\n * @param {Object} options\n * @param {VectorSource|VectorSource[]} [options.sources] Sources containing\n * polygons to split. If omitted the interaction searches all visible\n * vector layers on the map.\n * @param {number} [options.snapDistance=25] Pixel distance for hover highlight.\n */\n constructor(options = {}) {\n super({\n handleEvent: (e) => this._handleEvent(e),\n });\n\n this.snapDistance_ = options.snapDistance || 25;\n this._sources = options.sources\n ? (Array.isArray(options.sources) ? options.sources : [options.sources])\n : null;\n\n // Phase: 'select' | 'draw' | 'pick'\n this._phase = 'select';\n this._selectedFeature = null;\n this._selectedSource = null;\n this._drawInteraction = null;\n this._splitFeatures = null; // the two pieces (for pick phase)\n\n // Overlay layer for highlighting the polygon under the cursor / selected\n this._overlaySource = new VectorSource({ useSpatialIndex: false });\n this._overlayLayer = new VectorLayer({\n source: this._overlaySource,\n displayInLayerSwitcher: false,\n style: HIGHLIGHT_STYLE,\n });\n }\n\n /* ------------------------------------------------------------------ */\n /* Map lifecycle */\n /* ------------------------------------------------------------------ */\n\n setMap(map) {\n if (this.getMap()) {\n this.getMap().removeLayer(this._overlayLayer);\n this._removeDrawInteraction();\n }\n super.setMap(map);\n if (map) {\n this._overlayLayer.setMap(map);\n }\n }\n\n setActive(active) {\n super.setActive(active);\n if (!active) {\n this._reset();\n }\n }\n\n /* ------------------------------------------------------------------ */\n /* Source helpers */\n /* ------------------------------------------------------------------ */\n\n _getSources() {\n if (this._sources) return this._sources;\n if (!this.getMap()) return [];\n const sources = [];\n const collect = (layers) => {\n layers.forEach((layer) => {\n if (layer.getVisible()) {\n if (layer.getSource && layer.getSource() instanceof VectorSource) {\n sources.push(layer.getSource());\n } else if (layer.getLayers) {\n collect(layer.getLayers());\n }\n }\n });\n };\n collect(this.getMap().getLayers());\n return sources;\n }\n\n /* ------------------------------------------------------------------ */\n /* Event router */\n /* ------------------------------------------------------------------ */\n\n _handleEvent(e) {\n if (!this.getActive()) return true;\n\n if (this._phase === 'select') {\n if (e.type === 'pointermove') return this._onSelectMove(e);\n if (e.type === 'singleclick') return this._onSelectClick(e);\n }\n // In 'draw' phase the Draw interaction handles events directly;\n // we only intercept Escape to cancel.\n if (this._phase === 'draw') {\n if (e.type === 'keydown' && e.originalEvent?.key === 'Escape') {\n this._cancelDraw();\n return false;\n }\n }\n\n // In 'pick' phase the user selects which split piece keeps the UPN\n if (this._phase === 'pick') {\n if (e.type === 'pointermove') return this._onPickMove(e);\n if (e.type === 'singleclick') return this._onPickClick(e);\n if (e.type === 'keydown' && e.originalEvent?.key === 'Escape') {\n this._reset();\n return false;\n }\n }\n\n return true;\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 1: SELECT */\n /* ------------------------------------------------------------------ */\n\n _onSelectMove(e) {\n const map = this.getMap();\n if (!map) return true;\n\n this._overlaySource.clear();\n\n const hit = this._closestPolygon(e);\n if (hit) {\n // Show highlight copy\n const clone = hit.feature.clone();\n this._overlaySource.addFeature(clone);\n map.getTargetElement().style.cursor = 'pointer';\n } else {\n map.getTargetElement().style.cursor = '';\n }\n return true;\n }\n\n _onSelectClick(e) {\n const hit = this._closestPolygon(e);\n if (!hit) return true;\n\n this._selectedFeature = hit.feature;\n this._selectedSource = hit.source;\n\n // Keep highlight visible during draw phase\n this._overlaySource.clear();\n const clone = hit.feature.clone();\n this._overlaySource.addFeature(clone);\n\n this._startDrawPhase();\n return false; // consume the click\n }\n\n /**\n * Find the closest polygon feature within snap distance.\n */\n _closestPolygon(e) {\n let best = null;\n let bestDist = this.snapDistance_ + 1;\n\n for (const source of this._getSources()) {\n const feat = source.getClosestFeatureToCoordinate(e.coordinate);\n if (!feat) continue;\n const geom = feat.getGeometry();\n if (!geom) continue;\n const type = geom.getType();\n if (type !== 'Polygon' && type !== 'MultiPolygon') continue;\n\n const closest = geom.getClosestPoint(e.coordinate);\n const line = new LineString([e.coordinate, closest]);\n const distPx = line.getLength() / e.frameState.viewState.resolution;\n\n if (distPx < bestDist) {\n bestDist = distPx;\n best = { feature: feat, source, coord: closest };\n }\n }\n return best;\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 2: DRAW cutting line */\n /* ------------------------------------------------------------------ */\n\n _startDrawPhase() {\n this._phase = 'draw';\n const map = this.getMap();\n if (!map) return;\n\n map.getTargetElement().style.cursor = 'crosshair';\n\n this._drawInteraction = new ol_interaction_Draw({\n type: 'LineString',\n style: SKETCH_STYLE,\n });\n\n this._drawInteraction.on('drawend', (evt) => {\n const cuttingLine = evt.feature.getGeometry().getCoordinates();\n this._performSplit(cuttingLine);\n });\n\n map.addInteraction(this._drawInteraction);\n }\n\n _removeDrawInteraction() {\n if (this._drawInteraction && this.getMap()) {\n this.getMap().removeInteraction(this._drawInteraction);\n }\n this._drawInteraction = null;\n }\n\n _cancelDraw() {\n this._removeDrawInteraction();\n this._reset();\n }\n\n /* ------------------------------------------------------------------ */\n /* Split logic */\n /* ------------------------------------------------------------------ */\n\n _performSplit(cuttingLineCoords) {\n const feature = this._selectedFeature;\n const source = this._selectedSource;\n const geom = feature.getGeometry();\n\n let polygonCoords;\n if (geom.getType() === 'Polygon') {\n polygonCoords = geom.getCoordinates();\n } else if (geom.getType() === 'MultiPolygon') {\n // For MultiPolygon, try to split each sub-polygon and use the\n // first one that produces a valid result.\n // For now, use the first polygon ring.\n polygonCoords = geom.getCoordinates()[0];\n }\n\n const result = splitPolygonByLine(polygonCoords, cuttingLineCoords);\n\n if (!result) {\n console.warn('[PolygonSplit] Split failed — line must cross the polygon boundary at exactly 2 points.');\n // Stay in draw phase so user can retry\n this._removeDrawInteraction();\n this._startDrawPhase();\n return;\n }\n\n const [coordsA, coordsB] = result;\n\n // Create two new features from the split result\n const featureA = feature.clone();\n featureA.setGeometry(new PolygonGeom(coordsA));\n featureA.setStyle(new Style({\n stroke: new Stroke({ color: SPLIT_COLORS[0].stroke, width: 2.5 }),\n fill: new Fill({ color: SPLIT_COLORS[0].fill }),\n }));\n\n const featureB = feature.clone();\n featureB.setGeometry(new PolygonGeom(coordsB));\n featureB.setStyle(new Style({\n stroke: new Stroke({ color: SPLIT_COLORS[1].stroke, width: 2.5 }),\n fill: new Fill({ color: SPLIT_COLORS[1].fill }),\n }));\n\n // Dispatch beforesplit (compatible with ol-ext UndoRedo)\n const splitFeatures = [featureA, featureB];\n this.dispatchEvent({\n type: 'beforesplit',\n original: feature,\n features: splitFeatures,\n });\n source.dispatchEvent({\n type: 'beforesplit',\n original: feature,\n features: splitFeatures,\n });\n\n // Replace the original feature\n source.removeFeature(feature);\n source.addFeature(featureA);\n source.addFeature(featureB);\n\n // Dispatch aftersplit\n this.dispatchEvent({\n type: 'aftersplit',\n original: feature,\n features: splitFeatures,\n });\n source.dispatchEvent({\n type: 'aftersplit',\n original: feature,\n features: splitFeatures,\n });\n\n // Clean up draw interaction\n this._removeDrawInteraction();\n\n // If the original was a parcel, enter pick phase for UPN assignment\n const isParcel = feature.get('_layerType') === 'parcel';\n if (isParcel) {\n this._splitFeatures = splitFeatures;\n this._phase = 'pick';\n this._overlaySource.clear();\n const map = this.getMap();\n if (map) map.getTargetElement().style.cursor = '';\n showToast('Click the polygon that should keep the original identifier.', 'info', 5000);\n\n this.dispatchEvent({\n type: 'splitparcel',\n features: splitFeatures,\n originalProps: feature.getProperties(),\n source,\n });\n } else {\n this._reset();\n }\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 3: PICK — select which split piece keeps the UPN */\n /* ------------------------------------------------------------------ */\n\n _onPickMove(e) {\n const map = this.getMap();\n if (!map) return true;\n\n this._overlaySource.clear();\n\n const hit = this._closestSplitPiece(e);\n if (hit) {\n const clone = hit.clone();\n this._overlaySource.addFeature(clone);\n map.getTargetElement().style.cursor = 'pointer';\n } else {\n map.getTargetElement().style.cursor = '';\n }\n return true;\n }\n\n _onPickClick(e) {\n const hit = this._closestSplitPiece(e);\n if (!hit) return true;\n\n this.dispatchEvent({\n type: 'splitpick',\n picked: hit,\n features: this._splitFeatures,\n });\n\n this._reset();\n return false;\n }\n\n /**\n * Find the closest split piece to the cursor.\n */\n _closestSplitPiece(e) {\n if (!this._splitFeatures) return null;\n let best = null;\n let bestDist = this.snapDistance_ + 1;\n\n for (const feat of this._splitFeatures) {\n const geom = feat.getGeometry();\n if (!geom) continue;\n const closest = geom.getClosestPoint(e.coordinate);\n const line = new LineString([e.coordinate, closest]);\n const distPx = line.getLength() / e.frameState.viewState.resolution;\n if (distPx < bestDist) {\n bestDist = distPx;\n best = feat;\n }\n }\n return best;\n }\n\n /* ------------------------------------------------------------------ */\n /* Reset */\n /* ------------------------------------------------------------------ */\n\n _reset() {\n this._phase = 'select';\n this._selectedFeature = null;\n this._selectedSource = null;\n this._splitFeatures = null;\n this._overlaySource.clear();\n this._removeDrawInteraction();\n\n const map = this.getMap();\n if (map) {\n map.getTargetElement().style.cursor = '';\n }\n }\n}\n","/**\n * Pure geometry functions for merging two adjacent polygons.\n *\n * No OpenLayers dependency — operates on raw coordinate arrays.\n */\n\n/**\n * Squared distance between two points.\n */\nfunction dist2(a, b) {\n return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;\n}\n\n/**\n * Signed area of a ring (shoelace formula).\n * Positive = counter-clockwise, negative = clockwise.\n */\nfunction signedArea(ring) {\n let area = 0;\n for (let i = 0, n = ring.length; i < n - 1; i++) {\n area += (ring[i][0] * ring[i + 1][1]) - (ring[i + 1][0] * ring[i][1]);\n }\n return area / 2;\n}\n\n/**\n * Test whether a point is inside a ring (ray-casting algorithm).\n */\nfunction pointInRing(pt, ring) {\n let inside = false;\n for (let i = 0, j = ring.length - 2; i < ring.length - 1; j = i++) {\n const xi = ring[i][0], yi = ring[i][1];\n const xj = ring[j][0], yj = ring[j][1];\n if (((yi > pt[1]) !== (yj > pt[1])) &&\n (pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi)) {\n inside = !inside;\n }\n }\n return inside;\n}\n\n/**\n * Ensure a ring has the desired winding order.\n */\nfunction ensureWinding(ring, ccw) {\n const area = signedArea(ring);\n if ((ccw && area < 0) || (!ccw && area > 0)) {\n return ring.slice().reverse();\n }\n return ring;\n}\n\n/**\n * Close a ring (ensure first === last).\n */\nfunction closeRing(coords) {\n if (coords.length < 2) return coords;\n if (dist2(coords[0], coords[coords.length - 1]) > 1e-10) {\n return [...coords, coords[0].slice()];\n }\n return coords;\n}\n\n/**\n * Perpendicular distance from a point to a segment.\n *\n * @param {number[]} pt\n * @param {number[]} segA Segment start\n * @param {number[]} segB Segment end\n * @returns {number} Squared distance\n */\nfunction distToSegmentSq(pt, segA, segB) {\n const dx = segB[0] - segA[0];\n const dy = segB[1] - segA[1];\n const lenSq = dx * dx + dy * dy;\n\n if (lenSq < 1e-20) return dist2(pt, segA); // degenerate segment\n\n // Parametric position of the projection\n let t = ((pt[0] - segA[0]) * dx + (pt[1] - segA[1]) * dy) / lenSq;\n t = Math.max(0, Math.min(1, t));\n\n const projX = segA[0] + t * dx;\n const projY = segA[1] + t * dy;\n return (pt[0] - projX) ** 2 + (pt[1] - projY) ** 2;\n}\n\n/**\n * Find the ring edge closest to a click coordinate.\n *\n * @param {number[][]} ring Closed ring\n * @param {number[]} clickCoord [x, y]\n * @returns {{ segIdx: number, distSq: number }}\n */\nfunction findClosestEdge(ring, clickCoord) {\n let bestIdx = 0;\n let bestDist = Infinity;\n const n = ring.length - 1; // unique vertices\n\n for (let i = 0; i < n; i++) {\n const d = distToSegmentSq(clickCoord, ring[i], ring[(i + 1) % n === 0 ? n : i + 1]);\n if (d < bestDist) {\n bestDist = d;\n bestIdx = i;\n }\n }\n return { segIdx: bestIdx, distSq: bestDist };\n}\n\n/**\n * Check if two coordinates are equal within tolerance.\n */\nfunction coordsEqual(a, b, tolSq) {\n return dist2(a, b) < tolSq;\n}\n\n/**\n * Test whether a point lies within tolerance of any edge of a ring.\n *\n * Unlike coordsEqual (vertex-to-vertex), this checks whether the point is\n * close to the ring's *boundary* — it projects onto segments, so it works\n * even when the two polygons have different vertex density along the shared\n * edge or when vertices are slightly offset from separate digitisation.\n *\n * @param {number[]} pt Point to test\n * @param {number[][]} ring Closed ring\n * @param {number} tolSq Squared distance tolerance\n * @returns {boolean}\n */\nfunction isVertexNearRing(pt, ring, tolSq) {\n const n = ring.length - 1;\n for (let i = 0; i < n; i++) {\n if (distToSegmentSq(pt, ring[i], ring[i + 1]) < tolSq) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Find the shared boundary between two polygon rings.\n *\n * Adjacent polygons share edges in reverse winding direction:\n * if A walks P→Q along the shared boundary, B walks Q→P.\n *\n * The algorithm has two stages:\n *\n * 1. **Seed validation** — uses `isVertexNearRing` (vertex-to-edge proximity)\n * to confirm the user-clicked edges actually lie on a common boundary.\n * This is the forgiving check that handles offset vertices from separate\n * digitisation.\n *\n * 2. **Lockstep extension** — walks both rings together (A forward, B in\n * the opposite direction) and extends the shared boundary one vertex at\n * a time. Three cases are tried at each step:\n * a) Both rings advance: vertex-to-vertex match (classic case).\n * b) Only A advances: A has an extra vertex that projects onto B's\n * frontier edge (different vertex density).\n * c) Only B advances: vice-versa.\n * Because extension is coupled to the *frontier edge* of the other ring\n * (not the entire ring), it cannot overshoot into non-shared territory,\n * even for small or closely-spaced polygons.\n *\n * @param {number[][]} ringA Closed ring\n * @param {number[][]} ringB Closed ring\n * @param {number} seedIdxA Seed edge index on ring A\n * @param {number} seedIdxB Seed edge index on ring B\n * @param {number} tolerance Distance tolerance (in map units)\n * @returns {{ startA: number, endA: number, startB: number, endB: number, reversed: boolean } | null}\n */\nfunction findSharedBoundary(ringA, ringB, seedIdxA, seedIdxB, tolerance) {\n const nA = ringA.length - 1; // unique vertices\n const nB = ringB.length - 1;\n const tolSq = tolerance * tolerance;\n\n // ── Validate seed edges ─────────────────────────────────────────────\n // Both vertices of the seed edge on A must be near ring B's boundary,\n // or both vertices of the seed edge on B must be near ring A's boundary.\n // This uses vertex-to-edge proximity so it handles offset digitisation.\n const a0 = ringA[seedIdxA];\n const a1 = ringA[(seedIdxA + 1) % nA];\n const b0 = ringB[seedIdxB];\n const b1 = ringB[(seedIdxB + 1) % nB];\n\n const a0NearB = isVertexNearRing(a0, ringB, tolSq);\n const a1NearB = isVertexNearRing(a1, ringB, tolSq);\n const b0NearA = isVertexNearRing(b0, ringA, tolSq);\n const b1NearA = isVertexNearRing(b1, ringA, tolSq);\n\n if (!(a0NearB && a1NearB) && !(b0NearA && b1NearA)) {\n console.warn('[polygonMerge] Seed edges are not on the shared boundary');\n return null;\n }\n\n // ── Determine winding direction ─────────────────────────────────────\n // Reversed (the normal case): A's a0 ≈ B's b1 and A's a1 ≈ B's b0.\n let reversed;\n if (coordsEqual(a0, b1, tolSq) && coordsEqual(a1, b0, tolSq)) {\n reversed = true;\n } else if (coordsEqual(a0, b0, tolSq) && coordsEqual(a1, b1, tolSq)) {\n reversed = false;\n } else {\n // Vertices don't match exactly — use proximity to decide direction\n reversed = dist2(a0, b1) < dist2(a0, b0);\n }\n\n // ── Initialise shared boundary ──────────────────────────────────────\n let startA = seedIdxA;\n let endA = (seedIdxA + 1) % nA;\n let startB, endB;\n\n if (reversed) {\n // A walks startA → endA, B walks startB ← endB (reversed ring order)\n startB = (seedIdxB + 1) % nB;\n endB = seedIdxB;\n } else {\n startB = seedIdxB;\n endB = (seedIdxB + 1) % nB;\n }\n\n // ── Extend forward (endA++, endB-- if reversed) ─────────────────────\n // Walk both rings in lockstep. At each step try three strategies:\n // 1. Both advance — vertices match (vertex-to-vertex).\n // 2. Only A advances — A's next vertex projects onto B's frontier edge.\n // 3. Only B advances — B's next vertex projects onto A's frontier edge.\n let safety = nA + nB;\n while (safety-- > 0) {\n const nextA = (endA + 1) % nA;\n const nextB = reversed ? (endB - 1 + nB) % nB : (endB + 1) % nB;\n if (nextA === startA || nextB === startB) break; // wrapped around\n\n // Case 1: vertex-to-vertex match\n if (coordsEqual(ringA[nextA], ringB[nextB], tolSq)) {\n endA = nextA;\n endB = nextB;\n continue;\n }\n\n // Case 2: A has extra vertex — project onto B's frontier edge\n if (distToSegmentSq(ringA[nextA], ringB[endB], ringB[nextB]) < tolSq) {\n endA = nextA;\n continue;\n }\n\n // Case 3: B has extra vertex — project onto A's frontier edge\n if (distToSegmentSq(ringB[nextB], ringA[endA], ringA[nextA]) < tolSq) {\n endB = nextB;\n continue;\n }\n\n break; // no match — end of shared boundary\n }\n\n // ── Extend backward (startA--, startB++ if reversed) ────────────────\n safety = nA + nB;\n while (safety-- > 0) {\n const prevA = (startA - 1 + nA) % nA;\n const prevB = reversed ? (startB + 1) % nB : (startB - 1 + nB) % nB;\n if (prevA === endA || prevB === endB) break;\n\n // Case 1: vertex-to-vertex match\n if (coordsEqual(ringA[prevA], ringB[prevB], tolSq)) {\n startA = prevA;\n startB = prevB;\n continue;\n }\n\n // Case 2: A has extra vertex — project onto B's frontier edge\n if (distToSegmentSq(ringA[prevA], ringB[startB], ringB[prevB]) < tolSq) {\n startA = prevA;\n continue;\n }\n\n // Case 3: B has extra vertex — project onto A's frontier edge\n if (distToSegmentSq(ringB[prevB], ringA[startA], ringA[prevA]) < tolSq) {\n startB = prevB;\n continue;\n }\n\n break;\n }\n\n return { startA, endA, startB, endB, reversed };\n}\n\n/**\n * Walk a ring from startIdx to endIdx (exclusive), going forward and wrapping.\n * Skips startIdx and stops before reaching endIdx.\n * Returns the vertices of the non-shared portion.\n *\n * @param {number[][]} ring Closed ring\n * @param {number} fromIdx Start walking from this index (inclusive)\n * @param {number} toIdx Stop at this index (inclusive)\n * @returns {number[][]}\n */\nfunction walkRing(ring, fromIdx, toIdx) {\n const n = ring.length - 1;\n const result = [];\n let idx = fromIdx;\n while (true) {\n result.push(ring[idx]);\n if (idx === toIdx) break;\n idx = (idx + 1) % n;\n // Safety: prevent infinite loops\n if (result.length > n + 1) break;\n }\n return result;\n}\n\n/**\n * Merge two adjacent polygons along their shared boundary.\n *\n * @param {number[][][]} polygonCoordsA Polygon A coordinates [exteriorRing, ...holes]\n * @param {number[][][]} polygonCoordsB Polygon B coordinates [exteriorRing, ...holes]\n * @param {number[]} clickCoordA Click coordinate on the shared edge of polygon A\n * @param {number[]} clickCoordB Click coordinate on the shared edge of polygon B\n * @param {number} [tolerance=5] Distance tolerance in map units (default 5 metres in EPSG:3857).\n * A larger tolerance handles polygons that were digitised separately and\n * whose shared vertices don't coincide exactly.\n * @returns {{ coords: number[][][], error?: undefined } | { coords: null, error: string }}\n * On success: `{ coords: [...] }`. On failure: `{ coords: null, error: 'reason' }`.\n */\nexport function mergePolygons(polygonCoordsA, polygonCoordsB, clickCoordA, clickCoordB, tolerance = 5) {\n const ringA = polygonCoordsA[0];\n const ringB = polygonCoordsB[0];\n const holesA = polygonCoordsA.slice(1);\n const holesB = polygonCoordsB.slice(1);\n\n // 1. Find seed edges (closest to user clicks)\n const seedA = findClosestEdge(ringA, clickCoordA);\n const seedB = findClosestEdge(ringB, clickCoordB);\n\n // 2. Find shared boundary\n const shared = findSharedBoundary(ringA, ringB, seedA.segIdx, seedB.segIdx, tolerance);\n if (!shared) {\n console.warn('[polygonMerge] Could not find shared boundary between polygons — seed edges are not near the other ring');\n return { coords: null, error: 'The selected edges are not on a shared boundary. Click edges that lie on the common border between the two polygons.' };\n }\n\n const { startA, endA, startB, endB, reversed } = shared;\n const nA = ringA.length - 1;\n const nB = ringB.length - 1;\n\n // 3. Stitch the non-shared portions together\n // A's shared goes from startA → endA. Non-shared: endA → startA (forward, wrapping).\n // B's shared (reversed case) goes from startB backward to endB.\n // Non-shared: startB → endB (forward).\n // B's shared (same-dir case) goes from startB → endB.\n // Non-shared: endB → startB (forward, wrapping).\n //\n // The last vertex of partA (startA) must coincide with the first vertex\n // of partB for a clean join.\n\n const partA = walkRing(ringA, endA, startA);\n\n let partB;\n if (reversed) {\n // B's non-shared goes from startB forward to endB.\n // startB vertex ≈ startA vertex (they meet at one end of the shared boundary).\n partB = walkRing(ringB, startB, endB);\n } else {\n // B's non-shared goes from endB forward to startB.\n partB = walkRing(ringB, endB, startB);\n }\n\n // partA ends at startA, partB starts at a vertex that should coincide.\n // Skip the first vertex of partB to avoid the duplicate junction point.\n const merged = [...partA, ...partB.slice(1)];\n\n // Snap the closing junction. With non-coincident vertices (separate\n // digitisation) the last vertex of partB may be a few metres from the\n // first vertex of partA (ringA[endA]). Replace it to avoid a tiny\n // sliver edge that closeRing would otherwise create.\n const tolSq = tolerance * tolerance;\n if (merged.length > 2 && dist2(merged[merged.length - 1], merged[0]) < tolSq) {\n merged[merged.length - 1] = merged[0].slice();\n }\n\n const mergedRing = closeRing(merged);\n\n // 4. Validate: the merged ring should have a reasonable area\n const areaA = Math.abs(signedArea(ringA));\n const areaB = Math.abs(signedArea(ringB));\n const areaMerged = Math.abs(signedArea(mergedRing));\n const expectedArea = areaA + areaB;\n\n // Allow 10% tolerance for area mismatch (shared edges can cause slight differences)\n if (areaMerged < expectedArea * 0.5 || areaMerged > expectedArea * 1.5) {\n console.warn(`[polygonMerge] Area mismatch: A=${areaA.toFixed(1)}, B=${areaB.toFixed(1)}, merged=${areaMerged.toFixed(1)}, expected≈${expectedArea.toFixed(1)}`);\n return { coords: null, error: 'Merge produced an invalid polygon (area mismatch). The polygons may not be truly adjacent — try clicking closer to the shared boundary.' };\n }\n\n // 5. Match winding order to original\n const originalCCW = signedArea(ringA) > 0;\n const finalRing = ensureWinding(mergedRing, originalCCW);\n\n // 6. Collect holes from both polygons\n const allHoles = [...holesA, ...holesB];\n // Filter: only include holes that actually fall inside the merged ring\n const validHoles = allHoles.filter(hole => {\n const cx = hole.reduce((s, p) => s + p[0], 0) / (hole.length - 1);\n const cy = hole.reduce((s, p) => s + p[1], 0) / (hole.length - 1);\n return pointInRing([cx, cy], finalRing);\n });\n\n return { coords: [finalRing, ...validHoles] };\n}\n","/**\n * PolygonMergeInteraction\n *\n * A four-phase OpenLayers interaction for merging two adjacent polygons:\n * Phase 1 – SELECT_A: hover to highlight, click to select polygon A\n * Phase 2 – SELECT_B: hover to highlight, click to select polygon B\n * Phase 3 – CLICK_EDGE_A: hover highlights edge, click to pick shared edge on A\n * Phase 4 – CLICK_EDGE_B: hover highlights edge, click to pick shared edge on B → merge\n *\n * After a successful merge the two original features are removed and one\n * merged feature (coloured green) is added. If both originals were parcels,\n * a `mergedparcel` event is fired so external code can present a UPN chooser.\n */\n\nimport ol_interaction_Interaction from 'ol/interaction/Interaction';\nimport VectorSource from 'ol/source/Vector';\nimport VectorLayer from 'ol/layer/Vector';\nimport Feature from 'ol/Feature';\nimport { Style, Stroke, Fill, Text } from 'ol/style';\nimport { LineString, Polygon as PolygonGeom } from 'ol/geom';\nimport { mergePolygons } from '../geom/polygonMerge.js';\nimport { showToast } from '../toast.js';\n\n// ── Styles ───────────────────────────────────────────────────────────────────\n\nconst HIGHLIGHT_A = new Style({\n stroke: new Stroke({ color: '#0ea5e9', width: 3 }),\n fill: new Fill({ color: 'rgba(14,165,233,0.15)' }),\n});\n\nconst HIGHLIGHT_B = new Style({\n stroke: new Stroke({ color: '#f59e0b', width: 3 }),\n fill: new Fill({ color: 'rgba(245,158,11,0.15)' }),\n});\n\n// Labelled versions for permanent highlights (shown after selection)\nconst LABEL_A = new Style({\n stroke: new Stroke({ color: '#0ea5e9', width: 3 }),\n fill: new Fill({ color: 'rgba(14,165,233,0.15)' }),\n text: new Text({\n text: 'A',\n font: 'bold 22px Exo, sans-serif',\n fill: new Fill({ color: '#0ea5e9' }),\n stroke: new Stroke({ color: '#fff', width: 4 }),\n overflow: true,\n }),\n});\n\nconst LABEL_B = new Style({\n stroke: new Stroke({ color: '#f59e0b', width: 3 }),\n fill: new Fill({ color: 'rgba(245,158,11,0.15)' }),\n text: new Text({\n text: 'B',\n font: 'bold 22px Exo, sans-serif',\n fill: new Fill({ color: '#f59e0b' }),\n stroke: new Stroke({ color: '#fff', width: 4 }),\n overflow: true,\n }),\n});\n\nconst EDGE_STYLE = new Style({\n stroke: new Stroke({ color: '#ec4899', width: 4, lineDash: [10, 6] }),\n});\n\nconst MERGE_STYLE = new Style({\n stroke: new Stroke({ color: '#10b981', width: 2.5 }),\n fill: new Fill({ color: 'rgba(16,185,129,0.3)' }),\n});\n\n// ── Interaction ──────────────────────────────────────────────────────────────\n\nexport class PolygonMergeInteraction extends ol_interaction_Interaction {\n /**\n * @param {Object} [options]\n * @param {number} [options.snapDistance=25] Pixel distance for hover detection.\n * @param {number} [options.tolerance=5] Map-unit tolerance for shared-edge matching.\n */\n constructor(options = {}) {\n super({\n handleEvent: (e) => this._handleEvent(e),\n });\n\n this.snapDistance_ = options.snapDistance || 25;\n this.tolerance_ = options.tolerance || 5;\n\n // Phase: 'select_a' | 'select_b' | 'click_edge_a' | 'click_edge_b'\n this._phase = 'select_a';\n\n // Selected features and their sources\n this._featureA = null;\n this._sourceA = null;\n this._featureB = null;\n this._sourceB = null;\n\n // Clicked edge coordinates (map coords)\n this._edgeClickA = null;\n this._edgeClickB = null;\n\n // Overlay for polygon highlights\n this._highlightSource = new VectorSource({ useSpatialIndex: false });\n this._highlightLayer = new VectorLayer({\n source: this._highlightSource,\n displayInLayerSwitcher: false,\n style: (f) => f.get('_highlightStyle') || HIGHLIGHT_A,\n });\n\n // Overlay for edge highlights\n this._edgeSource = new VectorSource({ useSpatialIndex: false });\n this._edgeLayer = new VectorLayer({\n source: this._edgeSource,\n displayInLayerSwitcher: false,\n style: EDGE_STYLE,\n });\n }\n\n /* ------------------------------------------------------------------ */\n /* Map lifecycle */\n /* ------------------------------------------------------------------ */\n\n setMap(map) {\n if (this.getMap()) {\n this.getMap().removeLayer(this._highlightLayer);\n this.getMap().removeLayer(this._edgeLayer);\n }\n super.setMap(map);\n if (map) {\n this._highlightLayer.setMap(map);\n this._edgeLayer.setMap(map);\n }\n }\n\n setActive(active) {\n super.setActive(active);\n if (!active) this._reset();\n }\n\n /* ------------------------------------------------------------------ */\n /* Source helpers */\n /* ------------------------------------------------------------------ */\n\n _getSources() {\n if (!this.getMap()) return [];\n const sources = [];\n const collect = (layers) => {\n layers.forEach((layer) => {\n if (layer.getVisible()) {\n if (layer.getSource && layer.getSource() instanceof VectorSource) {\n sources.push(layer.getSource());\n } else if (layer.getLayers) {\n collect(layer.getLayers());\n }\n }\n });\n };\n collect(this.getMap().getLayers());\n return sources;\n }\n\n /* ------------------------------------------------------------------ */\n /* Event router */\n /* ------------------------------------------------------------------ */\n\n _handleEvent(e) {\n if (!this.getActive()) return true;\n\n // Escape cancels at any phase\n if (e.type === 'keydown' && e.originalEvent?.key === 'Escape') {\n this._reset();\n return false;\n }\n\n switch (this._phase) {\n case 'select_a':\n if (e.type === 'pointermove') return this._onSelectMove(e, null);\n if (e.type === 'singleclick') return this._onSelectAClick(e);\n break;\n case 'select_b':\n if (e.type === 'pointermove') return this._onSelectMove(e, this._featureA);\n if (e.type === 'singleclick') return this._onSelectBClick(e);\n break;\n case 'click_edge_a':\n if (e.type === 'pointermove') return this._onEdgeMove(e, this._featureA);\n if (e.type === 'singleclick') return this._onEdgeAClick(e);\n break;\n case 'click_edge_b':\n if (e.type === 'pointermove') return this._onEdgeMove(e, this._featureB);\n if (e.type === 'singleclick') return this._onEdgeBClick(e);\n break;\n }\n return true;\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 1 & 2: SELECT polygons */\n /* ------------------------------------------------------------------ */\n\n _onSelectMove(e, skipFeature) {\n const map = this.getMap();\n if (!map) return true;\n\n // Keep existing highlights for already-selected polygons\n this._highlightSource.clear();\n this._edgeSource.clear();\n this._rebuildHighlights();\n\n const hit = this._closestPolygon(e, skipFeature);\n if (hit) {\n const style = this._phase === 'select_a' ? HIGHLIGHT_A : HIGHLIGHT_B;\n const clone = hit.feature.clone();\n clone.set('_highlightStyle', style);\n this._highlightSource.addFeature(clone);\n map.getTargetElement().style.cursor = 'pointer';\n } else {\n map.getTargetElement().style.cursor = '';\n }\n return true;\n }\n\n _onSelectAClick(e) {\n const hit = this._closestPolygon(e, null);\n if (!hit) return true;\n\n this._featureA = hit.feature;\n this._sourceA = hit.source;\n this._phase = 'select_b';\n\n this._rebuildHighlights();\n return false;\n }\n\n _onSelectBClick(e) {\n const hit = this._closestPolygon(e, this._featureA);\n if (!hit) return true;\n\n this._featureB = hit.feature;\n this._sourceB = hit.source;\n this._phase = 'click_edge_a';\n\n this._rebuildHighlights();\n this.getMap().getTargetElement().style.cursor = 'crosshair';\n return false;\n }\n\n /**\n * Find the closest polygon feature within snap distance.\n * Optionally skip a feature (used in phase 2 to avoid re-selecting A).\n */\n _closestPolygon(e, skipFeature) {\n let best = null;\n let bestDist = this.snapDistance_ + 1;\n\n for (const source of this._getSources()) {\n const feat = source.getClosestFeatureToCoordinate(e.coordinate);\n if (!feat) continue;\n if (skipFeature && feat === skipFeature) continue;\n const geom = feat.getGeometry();\n if (!geom) continue;\n const type = geom.getType();\n if (type !== 'Polygon' && type !== 'MultiPolygon') continue;\n\n const closest = geom.getClosestPoint(e.coordinate);\n const line = new LineString([e.coordinate, closest]);\n const distPx = line.getLength() / e.frameState.viewState.resolution;\n\n if (distPx < bestDist) {\n bestDist = distPx;\n best = { feature: feat, source, coord: closest };\n }\n }\n return best;\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 3 & 4: CLICK edges */\n /* ------------------------------------------------------------------ */\n\n _onEdgeMove(e, feature) {\n const map = this.getMap();\n if (!map) return true;\n\n this._edgeSource.clear();\n\n const edge = this._closestEdgeSegment(feature, e);\n if (edge) {\n const edgeFeat = new Feature(new LineString([edge.segStart, edge.segEnd]));\n this._edgeSource.addFeature(edgeFeat);\n map.getTargetElement().style.cursor = 'crosshair';\n }\n return true;\n }\n\n _onEdgeAClick(e) {\n this._edgeClickA = e.coordinate;\n this._phase = 'click_edge_b';\n this._edgeSource.clear();\n return false;\n }\n\n _onEdgeBClick(e) {\n this._edgeClickB = e.coordinate;\n this._performMerge();\n return false;\n }\n\n /**\n * Find the closest edge segment of a polygon feature to the cursor.\n */\n _closestEdgeSegment(feature, e) {\n const geom = feature.getGeometry();\n let ring;\n if (geom.getType() === 'Polygon') {\n ring = geom.getCoordinates()[0];\n } else if (geom.getType() === 'MultiPolygon') {\n ring = geom.getCoordinates()[0][0];\n } else {\n return null;\n }\n\n const resolution = e.frameState.viewState.resolution;\n let bestDist = Infinity;\n let bestSeg = null;\n const n = ring.length - 1;\n\n for (let i = 0; i < n; i++) {\n const a = ring[i];\n const b = ring[i + 1];\n const dx = b[0] - a[0], dy = b[1] - a[1];\n const lenSq = dx * dx + dy * dy;\n if (lenSq < 1e-20) continue;\n\n let t = ((e.coordinate[0] - a[0]) * dx + (e.coordinate[1] - a[1]) * dy) / lenSq;\n t = Math.max(0, Math.min(1, t));\n const projX = a[0] + t * dx, projY = a[1] + t * dy;\n const distPx = Math.sqrt((e.coordinate[0] - projX) ** 2 + (e.coordinate[1] - projY) ** 2) / resolution;\n\n if (distPx < bestDist) {\n bestDist = distPx;\n bestSeg = { segStart: a, segEnd: b };\n }\n }\n return bestDist <= this.snapDistance_ ? bestSeg : null;\n }\n\n /* ------------------------------------------------------------------ */\n /* Merge logic */\n /* ------------------------------------------------------------------ */\n\n _performMerge() {\n const featureA = this._featureA;\n const featureB = this._featureB;\n const sourceA = this._sourceA;\n const sourceB = this._sourceB;\n\n // Extract polygon coordinates\n const geomA = featureA.getGeometry();\n const geomB = featureB.getGeometry();\n const coordsA = geomA.getType() === 'Polygon' ? geomA.getCoordinates() : geomA.getCoordinates()[0];\n const coordsB = geomB.getType() === 'Polygon' ? geomB.getCoordinates() : geomB.getCoordinates()[0];\n\n const result = mergePolygons(coordsA, coordsB, this._edgeClickA, this._edgeClickB, this.tolerance_);\n\n if (!result.coords) {\n showToast(result.error || 'Merge failed — try clicking on the shared boundary.', 'error', 5000);\n // Return to edge click phase for retry\n this._edgeClickA = null;\n this._edgeClickB = null;\n this._phase = 'click_edge_a';\n this._edgeSource.clear();\n return;\n }\n\n // Create merged feature (clone A for default properties)\n const mergedFeature = featureA.clone();\n mergedFeature.setGeometry(new PolygonGeom(result.coords));\n mergedFeature.setStyle(MERGE_STYLE);\n\n // Dispatch beforemerge events\n const evtData = {\n type: 'beforemerge',\n original: [featureA, featureB],\n merged: mergedFeature,\n };\n this.dispatchEvent(evtData);\n sourceA.dispatchEvent({ ...evtData });\n if (sourceB !== sourceA) {\n sourceB.dispatchEvent({ ...evtData });\n }\n\n // Replace originals with merged\n sourceA.removeFeature(featureA);\n sourceB.removeFeature(featureB);\n sourceA.addFeature(mergedFeature);\n\n // Dispatch aftermerge events\n const afterEvt = {\n type: 'aftermerge',\n original: [featureA, featureB],\n merged: mergedFeature,\n };\n this.dispatchEvent(afterEvt);\n sourceA.dispatchEvent({ ...afterEvt });\n if (sourceB !== sourceA) {\n sourceB.dispatchEvent({ ...afterEvt });\n }\n\n // If both features were parcels, fire mergedparcel so MapView can show the UPN chooser\n const isParcelA = featureA.get('_layerType') === 'parcel';\n const isParcelB = featureB.get('_layerType') === 'parcel';\n if (isParcelA && isParcelB) {\n this.dispatchEvent({\n type: 'mergedparcel',\n merged: mergedFeature,\n propsA: featureA.getProperties(),\n propsB: featureB.getProperties(),\n coordinate: this._edgeClickA,\n });\n showToast('Polygons merged — choose which identifier to keep.', 'success');\n } else {\n showToast('Polygons merged successfully.', 'success');\n }\n\n // Clean up\n this._reset();\n }\n\n /* ------------------------------------------------------------------ */\n /* Highlight management */\n /* ------------------------------------------------------------------ */\n\n /**\n * Rebuild the permanent highlights for already-selected polygons.\n */\n _rebuildHighlights() {\n // Remove previous non-hover highlights\n const toRemove = [];\n this._highlightSource.getFeatures().forEach((f) => {\n if (f.get('_permanent')) toRemove.push(f);\n });\n toRemove.forEach((f) => this._highlightSource.removeFeature(f));\n\n if (this._featureA) {\n const cloneA = this._featureA.clone();\n cloneA.set('_highlightStyle', LABEL_A);\n cloneA.set('_permanent', true);\n this._highlightSource.addFeature(cloneA);\n }\n if (this._featureB) {\n const cloneB = this._featureB.clone();\n cloneB.set('_highlightStyle', LABEL_B);\n cloneB.set('_permanent', true);\n this._highlightSource.addFeature(cloneB);\n }\n }\n\n /* ------------------------------------------------------------------ */\n /* Reset */\n /* ------------------------------------------------------------------ */\n\n _reset() {\n this._phase = 'select_a';\n this._featureA = null;\n this._sourceA = null;\n this._featureB = null;\n this._sourceB = null;\n this._edgeClickA = null;\n this._edgeClickB = null;\n this._highlightSource.clear();\n this._edgeSource.clear();\n\n const map = this.getMap();\n if (map) {\n map.getTargetElement().style.cursor = '';\n }\n }\n}\n","/**\n * Pure geometry functions for dividing a polygon into N equal-area pieces.\n *\n * No OpenLayers dependency — operates on raw coordinate arrays.\n *\n * The algorithm finds the polygon's longest edge, then places N-1 cutting\n * lines perpendicular to that edge. Each cutting-line position is found\n * via binary search so that the piece it cuts off has exactly 1/N of the\n * remaining area. The actual cut is delegated to `splitPolygonByLine()`.\n */\n\nimport { splitPolygonByLine } from './polygonSplit.js';\n\n// ── Utility helpers (self-contained) ─────────────────────────────────────────\n\nfunction dist2(a, b) {\n return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;\n}\n\n/**\n * Signed area of a ring (shoelace formula).\n */\nfunction signedArea(ring) {\n let area = 0;\n for (let i = 0, n = ring.length; i < n - 1; i++) {\n area += ring[i][0] * ring[i + 1][1] - ring[i + 1][0] * ring[i][1];\n }\n return area / 2;\n}\n\n/**\n * Absolute polygon area, accounting for holes.\n */\nfunction polygonArea(coords) {\n let area = Math.abs(signedArea(coords[0]));\n for (let i = 1; i < coords.length; i++) {\n area -= Math.abs(signedArea(coords[i]));\n }\n return area;\n}\n\n/**\n * Find the longest edge of a ring and return direction vectors.\n *\n * @param {number[][]} ring Closed ring\n * @returns {{ p0: number[], p1: number[], along: number[], perp: number[] }}\n * `along` = unit vector along the longest edge,\n * `perp` = unit vector perpendicular to `along` (rotated 90° CCW)\n */\nfunction longestEdge(ring) {\n const n = ring.length - 1; // unique vertices\n let bestLen = -1;\n let bestI = 0;\n\n for (let i = 0; i < n; i++) {\n const d = dist2(ring[i], ring[i + 1]);\n if (d > bestLen) {\n bestLen = d;\n bestI = i;\n }\n }\n\n const p0 = ring[bestI];\n const p1 = ring[bestI + 1];\n const len = Math.sqrt(bestLen);\n const along = [(p1[0] - p0[0]) / len, (p1[1] - p0[1]) / len];\n // Perpendicular: rotate 90° CCW\n const perp = [-along[1], along[0]];\n\n return { p0, p1, along, perp };\n}\n\n/**\n * Build a cutting line perpendicular to `along` at parameter `t`.\n *\n * The line passes through `origin + t * along` and extends `extent` units\n * in both `perp` directions — long enough to fully cross the polygon.\n */\nfunction makeCuttingLine(origin, along, perp, t, extent) {\n const cx = origin[0] + t * along[0];\n const cy = origin[1] + t * along[1];\n return [\n [cx - extent * perp[0], cy - extent * perp[1]],\n [cx + extent * perp[0], cy + extent * perp[1]],\n ];\n}\n\n/**\n * Project the centroid of a polygon's exterior ring onto the `along` axis.\n * Returns the scalar parameter `t` relative to `origin`.\n */\nfunction centroidT(coords, origin, along) {\n const ring = coords[0];\n const n = ring.length - 1;\n let sx = 0, sy = 0;\n for (let i = 0; i < n; i++) {\n sx += ring[i][0];\n sy += ring[i][1];\n }\n const cx = sx / n - origin[0];\n const cy = sy / n - origin[1];\n return cx * along[0] + cy * along[1];\n}\n\n// ── Main export ──────────────────────────────────────────────────────────────\n\n/**\n * Divide a polygon into N equal-area pieces by parallel cuts perpendicular\n * to a user-selected edge.\n *\n * @param {number[][][]} polygonCoords Polygon coordinates [ring, ...holes]\n * @param {number} n Number of pieces (must be >= 1)\n * @param {number[][]} edgeCoords The selected edge `[p0, p1]` — cuts will be\n * perpendicular to this edge direction.\n * @returns {{ pieces: number[][][][], error?: undefined } | { pieces: null, error: string }}\n */\nexport function dividePolygon(polygonCoords, n, edgeCoords) {\n if (!Number.isInteger(n) || n < 1) {\n return { pieces: null, error: 'Number of divisions must be a positive integer.' };\n }\n if (n === 1) {\n return { pieces: [polygonCoords] };\n }\n\n const ring = polygonCoords[0];\n const totalArea = polygonArea(polygonCoords);\n\n if (totalArea < 1e-6) {\n return { pieces: null, error: 'Polygon has no measurable area.' };\n }\n\n // 1. Determine cutting direction from the selected edge\n let p0, along, perp;\n if (edgeCoords && edgeCoords.length === 2) {\n p0 = edgeCoords[0];\n const dx = edgeCoords[1][0] - edgeCoords[0][0];\n const dy = edgeCoords[1][1] - edgeCoords[0][1];\n const len = Math.sqrt(dx * dx + dy * dy);\n if (len < 1e-10) {\n return { pieces: null, error: 'Selected edge has zero length.' };\n }\n along = [dx / len, dy / len];\n perp = [-along[1], along[0]];\n } else {\n // Fallback: use longest edge\n const edge = longestEdge(ring);\n p0 = edge.p0;\n along = edge.along;\n perp = edge.perp;\n }\n const origin = p0;\n\n // 2. Project all vertices onto the `along` axis to find extent\n const nVerts = ring.length - 1;\n let tMin = Infinity, tMax = -Infinity;\n for (let i = 0; i < nVerts; i++) {\n const dx = ring[i][0] - origin[0];\n const dy = ring[i][1] - origin[1];\n const t = dx * along[0] + dy * along[1];\n if (t < tMin) tMin = t;\n if (t > tMax) tMax = t;\n }\n\n // Cutting line extent: enough to cross the polygon in the `perp` direction\n let perpMin = Infinity, perpMax = -Infinity;\n for (let i = 0; i < nVerts; i++) {\n const dx = ring[i][0] - origin[0];\n const dy = ring[i][1] - origin[1];\n const p = dx * perp[0] + dy * perp[1];\n if (p < perpMin) perpMin = p;\n if (p > perpMax) perpMax = p;\n }\n const extent = (perpMax - perpMin) * 1.5; // generous overshoot\n\n // 3. Iteratively cut pieces\n const pieces = [];\n let remaining = polygonCoords;\n let remainingCount = n;\n\n for (let i = 0; i < n - 1; i++) {\n const remainingArea = polygonArea(remaining);\n const targetArea = remainingArea / remainingCount;\n\n // Re-project the remaining polygon to get its current t-range\n const remRing = remaining[0];\n const remN = remRing.length - 1;\n let rMin = Infinity, rMax = -Infinity;\n for (let j = 0; j < remN; j++) {\n const dx = remRing[j][0] - origin[0];\n const dy = remRing[j][1] - origin[1];\n const t = dx * along[0] + dy * along[1];\n if (t < rMin) rMin = t;\n if (t > rMax) rMax = t;\n }\n\n // Binary search for the cutting position\n let lo = rMin;\n let hi = rMax;\n let bestT = (lo + hi) / 2;\n let bestPiece = null;\n let bestRemaining = null;\n let bestError = Infinity;\n\n for (let iter = 0; iter < 40; iter++) {\n const mid = (lo + hi) / 2;\n const line = makeCuttingLine(origin, along, perp, mid, extent);\n const result = splitPolygonByLine(remaining, line);\n\n if (!result) {\n // Cutting line didn't produce a valid split — nudge and retry\n // Try slightly shifted positions\n const nudge = (hi - lo) * 0.01;\n const lineA = makeCuttingLine(origin, along, perp, mid + nudge, extent);\n const resultA = splitPolygonByLine(remaining, lineA);\n if (resultA) {\n const [halfA, halfB] = resultA;\n const tA = centroidT(halfA, origin, along);\n const tB = centroidT(halfB, origin, along);\n const nearPiece = tA < tB ? halfA : halfB;\n const farPiece = tA < tB ? halfB : halfA;\n const nearArea = polygonArea(nearPiece);\n const err = Math.abs(nearArea - targetArea);\n if (err < bestError) {\n bestError = err;\n bestT = mid + nudge;\n bestPiece = nearPiece;\n bestRemaining = farPiece;\n }\n }\n // Try the other direction\n const lineB = makeCuttingLine(origin, along, perp, mid - nudge, extent);\n const resultB = splitPolygonByLine(remaining, lineB);\n if (resultB) {\n const [halfA, halfB] = resultB;\n const tA = centroidT(halfA, origin, along);\n const tB = centroidT(halfB, origin, along);\n const nearPiece = tA < tB ? halfA : halfB;\n const farPiece = tA < tB ? halfB : halfA;\n const nearArea = polygonArea(nearPiece);\n const err = Math.abs(nearArea - targetArea);\n if (err < bestError) {\n bestError = err;\n bestT = mid - nudge;\n bestPiece = nearPiece;\n bestRemaining = farPiece;\n }\n }\n // Bisect anyway to keep converging\n lo = mid;\n continue;\n }\n\n const [halfA, halfB] = result;\n const tA = centroidT(halfA, origin, along);\n const tB = centroidT(halfB, origin, along);\n const nearPiece = tA < tB ? halfA : halfB;\n const farPiece = tA < tB ? halfB : halfA;\n const nearArea = polygonArea(nearPiece);\n\n const err = Math.abs(nearArea - targetArea);\n if (err < bestError) {\n bestError = err;\n bestT = mid;\n bestPiece = nearPiece;\n bestRemaining = farPiece;\n }\n\n // Converged?\n if (err / remainingArea < 0.001) break;\n\n // Adjust search range\n if (nearArea < targetArea) {\n lo = mid; // need to cut farther out\n } else {\n hi = mid; // need to cut closer\n }\n }\n\n if (!bestPiece || !bestRemaining) {\n return {\n pieces: null,\n error: `Could not find a valid cut for piece ${i + 1} of ${n}. The polygon shape may be too irregular for equal division.`,\n };\n }\n\n pieces.push(bestPiece);\n remaining = bestRemaining;\n remainingCount--;\n }\n\n // The last remaining piece is the Nth piece\n pieces.push(remaining);\n\n return { pieces };\n}\n","/**\n * PolygonDivideInteraction\n *\n * A three-phase OpenLayers interaction for dividing a polygon into N\n * equal-area pieces:\n * Phase 1 – SELECT: hover to highlight, click to select a polygon\n * Phase 2 – EDGE: hover to highlight edges, click to pick the divide\n * direction (cuts will be perpendicular to this edge)\n * Phase 3 – FORM: wait for the popup form to call performDivide(n)\n *\n * After a successful divide the original feature is removed and N new\n * coloured features are added. The interaction fires `beforedivide` and\n * `afterdivide` events compatible with ol-ext's UndoRedo.\n */\n\nimport ol_interaction_Interaction from 'ol/interaction/Interaction';\nimport VectorSource from 'ol/source/Vector';\nimport VectorLayer from 'ol/layer/Vector';\nimport Feature from 'ol/Feature';\nimport { Style, Stroke, Fill } from 'ol/style';\nimport { LineString, Polygon as PolygonGeom } from 'ol/geom';\nimport { dividePolygon } from '../geom/polygonDivide.js';\nimport { showToast } from '../toast.js';\n\n// Highlight style for the selected polygon (phase 1)\nconst HIGHLIGHT_STYLE = new Style({\n stroke: new Stroke({ color: '#0ea5e9', width: 3 }),\n fill: new Fill({ color: 'rgba(14,165,233,0.15)' }),\n});\n\n// Style for the hovered edge (phase 2)\nconst EDGE_STYLE = new Style({\n stroke: new Stroke({ color: '#8b5cf6', width: 4, lineDash: [10, 6] }),\n});\n\n/**\n * Generate N visually distinct colours using evenly-spaced HSL hues.\n */\nfunction pieceColors(n) {\n const colors = [];\n for (let i = 0; i < n; i++) {\n const hue = Math.round((i * 360) / n);\n colors.push({\n stroke: `hsl(${hue}, 70%, 45%)`,\n fill: `hsla(${hue}, 70%, 55%, 0.25)`,\n });\n }\n return colors;\n}\n\nexport class PolygonDivideInteraction extends ol_interaction_Interaction {\n /**\n * @param {Object} options\n * @param {VectorSource|VectorSource[]} [options.sources] Specific sources\n * to search. If omitted the interaction searches all visible vector layers.\n * @param {number} [options.snapDistance=25] Pixel distance for hover.\n */\n constructor(options = {}) {\n super({\n handleEvent: (e) => this._handleEvent(e),\n });\n\n this.snapDistance_ = options.snapDistance || 25;\n this._sources = options.sources\n ? (Array.isArray(options.sources) ? options.sources : [options.sources])\n : null;\n\n // Phase: 'select' | 'edge' | 'form' | 'pick'\n this._phase = 'select';\n this._selectedFeature = null;\n this._selectedSource = null;\n this._selectedEdge = null; // [p0, p1] — the edge the user clicked\n this._dividedFeatures = null; // features created after divide (for pick phase)\n\n // Overlay layer for polygon highlight\n this._overlaySource = new VectorSource({ useSpatialIndex: false });\n this._overlayLayer = new VectorLayer({\n source: this._overlaySource,\n displayInLayerSwitcher: false,\n style: HIGHLIGHT_STYLE,\n });\n\n // Overlay layer for edge highlight\n this._edgeSource = new VectorSource({ useSpatialIndex: false });\n this._edgeLayer = new VectorLayer({\n source: this._edgeSource,\n displayInLayerSwitcher: false,\n style: EDGE_STYLE,\n });\n }\n\n /* ------------------------------------------------------------------ */\n /* Map lifecycle */\n /* ------------------------------------------------------------------ */\n\n setMap(map) {\n if (this.getMap()) {\n this.getMap().removeLayer(this._overlayLayer);\n this.getMap().removeLayer(this._edgeLayer);\n }\n super.setMap(map);\n if (map) {\n this._overlayLayer.setMap(map);\n this._edgeLayer.setMap(map);\n }\n }\n\n setActive(active) {\n super.setActive(active);\n if (!active) {\n this._reset();\n }\n }\n\n /* ------------------------------------------------------------------ */\n /* Source helpers */\n /* ------------------------------------------------------------------ */\n\n _getSources() {\n if (this._sources) return this._sources;\n if (!this.getMap()) return [];\n const sources = [];\n const collect = (layers) => {\n layers.forEach((layer) => {\n if (layer.getVisible()) {\n if (layer.getSource && layer.getSource() instanceof VectorSource) {\n sources.push(layer.getSource());\n } else if (layer.getLayers) {\n collect(layer.getLayers());\n }\n }\n });\n };\n collect(this.getMap().getLayers());\n return sources;\n }\n\n /* ------------------------------------------------------------------ */\n /* Event router */\n /* ------------------------------------------------------------------ */\n\n _handleEvent(e) {\n if (!this.getActive()) return true;\n\n // Escape cancels at any phase\n if (e.type === 'keydown' && e.originalEvent?.key === 'Escape') {\n if (this._phase === 'form') {\n this.cancelDivide();\n } else {\n this._reset();\n }\n return false;\n }\n\n if (this._phase === 'select') {\n if (e.type === 'pointermove') return this._onSelectMove(e);\n if (e.type === 'singleclick') return this._onSelectClick(e);\n }\n\n if (this._phase === 'edge') {\n if (e.type === 'pointermove') return this._onEdgeMove(e);\n if (e.type === 'singleclick') return this._onEdgeClick(e);\n }\n\n if (this._phase === 'pick') {\n if (e.type === 'pointermove') return this._onPickMove(e);\n if (e.type === 'singleclick') return this._onPickClick(e);\n }\n\n return true;\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 1: SELECT polygon */\n /* ------------------------------------------------------------------ */\n\n _onSelectMove(e) {\n const map = this.getMap();\n if (!map) return true;\n\n this._overlaySource.clear();\n\n const hit = this._closestPolygon(e);\n if (hit) {\n const clone = hit.feature.clone();\n this._overlaySource.addFeature(clone);\n map.getTargetElement().style.cursor = 'pointer';\n } else {\n map.getTargetElement().style.cursor = '';\n }\n return true;\n }\n\n _onSelectClick(e) {\n const hit = this._closestPolygon(e);\n if (!hit) return true;\n\n this._selectedFeature = hit.feature;\n this._selectedSource = hit.source;\n\n // Keep polygon highlight visible during edge phase\n this._overlaySource.clear();\n const clone = hit.feature.clone();\n clone.set('_permanent', true);\n this._overlaySource.addFeature(clone);\n\n this._phase = 'edge';\n showToast('Click the edge to divide along.', 'info', 3000);\n return false;\n }\n\n _closestPolygon(e) {\n let best = null;\n let bestDist = this.snapDistance_ + 1;\n\n for (const source of this._getSources()) {\n const feat = source.getClosestFeatureToCoordinate(e.coordinate);\n if (!feat) continue;\n const geom = feat.getGeometry();\n if (!geom) continue;\n const type = geom.getType();\n if (type !== 'Polygon' && type !== 'MultiPolygon') continue;\n\n const closest = geom.getClosestPoint(e.coordinate);\n const line = new LineString([e.coordinate, closest]);\n const distPx = line.getLength() / e.frameState.viewState.resolution;\n\n if (distPx < bestDist) {\n bestDist = distPx;\n best = { feature: feat, source };\n }\n }\n return best;\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 2: EDGE selection */\n /* ------------------------------------------------------------------ */\n\n _onEdgeMove(e) {\n const map = this.getMap();\n if (!map) return true;\n\n this._edgeSource.clear();\n\n const edge = this._closestEdgeSegment(this._selectedFeature, e);\n if (edge) {\n const edgeFeat = new Feature(new LineString([edge.segStart, edge.segEnd]));\n this._edgeSource.addFeature(edgeFeat);\n map.getTargetElement().style.cursor = 'crosshair';\n } else {\n map.getTargetElement().style.cursor = '';\n }\n return true;\n }\n\n _onEdgeClick(e) {\n const edge = this._closestEdgeSegment(this._selectedFeature, e);\n if (!edge) return true;\n\n this._selectedEdge = [edge.segStart, edge.segEnd];\n this._edgeSource.clear();\n\n this._phase = 'form';\n\n // Dispatch divideform so MapView can show the popup\n const geom = this._selectedFeature.getGeometry();\n const ext = geom.getExtent();\n const center = [(ext[0] + ext[2]) / 2, (ext[1] + ext[3]) / 2];\n\n this.dispatchEvent({\n type: 'divideform',\n feature: this._selectedFeature,\n source: this._selectedSource,\n coordinate: center,\n });\n\n return false;\n }\n\n /**\n * Find the closest edge segment of a polygon feature to the cursor.\n */\n _closestEdgeSegment(feature, e) {\n const geom = feature.getGeometry();\n let ring;\n if (geom.getType() === 'Polygon') {\n ring = geom.getCoordinates()[0];\n } else if (geom.getType() === 'MultiPolygon') {\n ring = geom.getCoordinates()[0][0];\n } else {\n return null;\n }\n\n const resolution = e.frameState.viewState.resolution;\n let bestDist = Infinity;\n let bestSeg = null;\n const n = ring.length - 1;\n\n for (let i = 0; i < n; i++) {\n const a = ring[i];\n const b = ring[i + 1];\n const dx = b[0] - a[0], dy = b[1] - a[1];\n const lenSq = dx * dx + dy * dy;\n if (lenSq < 1e-20) continue;\n\n let t = ((e.coordinate[0] - a[0]) * dx + (e.coordinate[1] - a[1]) * dy) / lenSq;\n t = Math.max(0, Math.min(1, t));\n const projX = a[0] + t * dx, projY = a[1] + t * dy;\n const distPx = Math.sqrt((e.coordinate[0] - projX) ** 2 + (e.coordinate[1] - projY) ** 2) / resolution;\n\n if (distPx < bestDist) {\n bestDist = distPx;\n bestSeg = { segStart: a, segEnd: b };\n }\n }\n return bestDist <= this.snapDistance_ ? bestSeg : null;\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 3: FORM — called externally by the popup */\n /* ------------------------------------------------------------------ */\n\n /**\n * Divide the selected polygon into `n` equal-area pieces.\n * Called by the MapView popup's Confirm handler.\n *\n * @param {number} n Number of pieces (>= 2)\n */\n performDivide(n) {\n if (this._phase !== 'form' || !this._selectedFeature) return;\n\n const feature = this._selectedFeature;\n const source = this._selectedSource;\n const geom = feature.getGeometry();\n\n let polygonCoords;\n if (geom.getType() === 'Polygon') {\n polygonCoords = geom.getCoordinates();\n } else if (geom.getType() === 'MultiPolygon') {\n polygonCoords = geom.getCoordinates()[0];\n }\n\n const result = dividePolygon(polygonCoords, n, this._selectedEdge);\n\n if (!result.pieces) {\n showToast(result.error || 'Division failed.', 'error', 5000);\n this._reset();\n return;\n }\n\n // Create N new coloured features\n const colors = pieceColors(n);\n const newFeatures = result.pieces.map((coords, i) => {\n const f = feature.clone();\n f.setGeometry(new PolygonGeom(coords));\n f.setStyle(new Style({\n stroke: new Stroke({ color: colors[i].stroke, width: 2.5 }),\n fill: new Fill({ color: colors[i].fill }),\n }));\n return f;\n });\n\n // Dispatch beforedivide (UndoRedo compatible)\n const evtData = {\n type: 'beforedivide',\n original: feature,\n features: newFeatures,\n };\n this.dispatchEvent(evtData);\n source.dispatchEvent({ ...evtData });\n\n // Replace original with pieces\n source.removeFeature(feature);\n for (const f of newFeatures) {\n source.addFeature(f);\n }\n\n // Dispatch afterdivide\n const afterEvt = {\n type: 'afterdivide',\n original: feature,\n features: newFeatures,\n };\n this.dispatchEvent(afterEvt);\n source.dispatchEvent({ ...afterEvt });\n\n // If original was a parcel, enter pick phase for UPN assignment\n const isParcel = feature.get('_layerType') === 'parcel';\n if (isParcel) {\n this._dividedFeatures = newFeatures;\n this._phase = 'pick';\n showToast('Click the polygon that should keep the original identifier.', 'info', 5000);\n\n this.dispatchEvent({\n type: 'dividedparcel',\n features: newFeatures,\n originalProps: feature.getProperties(),\n source,\n });\n } else {\n showToast(`Polygon divided into ${n} equal pieces.`, 'success');\n this._reset();\n }\n }\n\n /* ------------------------------------------------------------------ */\n /* Phase 4: PICK — select which piece keeps the UPN */\n /* ------------------------------------------------------------------ */\n\n _onPickMove(e) {\n const map = this.getMap();\n if (!map) return true;\n\n this._overlaySource.clear();\n\n // Highlight whichever divided piece is under the cursor\n const hit = this._closestDividedPiece(e);\n if (hit) {\n const clone = hit.clone();\n this._overlaySource.addFeature(clone);\n map.getTargetElement().style.cursor = 'pointer';\n } else {\n map.getTargetElement().style.cursor = '';\n }\n return true;\n }\n\n _onPickClick(e) {\n const hit = this._closestDividedPiece(e);\n if (!hit) return true;\n\n this.dispatchEvent({\n type: 'dividepick',\n picked: hit,\n features: this._dividedFeatures,\n });\n\n this._reset();\n return false;\n }\n\n /**\n * Find the closest divided piece to the cursor.\n */\n _closestDividedPiece(e) {\n if (!this._dividedFeatures) return null;\n let best = null;\n let bestDist = this.snapDistance_ + 1;\n\n for (const feat of this._dividedFeatures) {\n const geom = feat.getGeometry();\n if (!geom) continue;\n const closest = geom.getClosestPoint(e.coordinate);\n const line = new LineString([e.coordinate, closest]);\n const distPx = line.getLength() / e.frameState.viewState.resolution;\n if (distPx < bestDist) {\n bestDist = distPx;\n best = feat;\n }\n }\n return best;\n }\n\n /**\n * Cancel the divide operation and return to select phase.\n * Called by the MapView popup's Cancel handler.\n */\n cancelDivide() {\n this.dispatchEvent({ type: 'dividecancel' });\n this._reset();\n }\n\n /* ------------------------------------------------------------------ */\n /* Reset */\n /* ------------------------------------------------------------------ */\n\n _reset() {\n this._phase = 'select';\n this._selectedFeature = null;\n this._selectedSource = null;\n this._selectedEdge = null;\n this._dividedFeatures = null;\n this._overlaySource.clear();\n this._edgeSource.clear();\n\n const map = this.getMap();\n if (map) {\n map.getTargetElement().style.cursor = '';\n }\n }\n}\n","/**\n * MapView Component\n *\n * OpenLayers map with ol-ext LayerSwitcher for base map selection.\n *\n * Usage:\n * import { MapView } from './components/MapView.js';\n *\n * const map = new MapView('map', {\n * center: [-1.5, 7.5], // Ghana\n * zoom: 7,\n * basemap: 'osm'\n * });\n *\n * map.onClick((lon, lat) => console.log('Clicked:', lon, lat));\n * map.addMarker(lon, lat, { name: 'Point A' });\n */\n\nimport Map from 'ol/Map';\nimport View from 'ol/View';\nimport Overlay from 'ol/Overlay';\nimport TileLayer from 'ol/layer/Tile';\nimport ImageLayer from 'ol/layer/Image';\nimport LayerGroup from 'ol/layer/Group';\nimport VectorLayer from 'ol/layer/Vector';\nimport VectorImageLayer from 'ol/layer/VectorImage';\nimport VectorSource from 'ol/source/Vector';\nimport ImageWMS from 'ol/source/ImageWMS';\nimport TileWMS from 'ol/source/TileWMS';\nimport OSM from 'ol/source/OSM';\nimport XYZ from 'ol/source/XYZ';\nimport { fromLonLat, toLonLat } from 'ol/proj';\nimport { Point, LineString, Polygon as PolygonGeom } from 'ol/geom';\nimport Feature from 'ol/Feature';\nimport { Style, Circle, Fill, Stroke, Text } from 'ol/style';\nimport GeoJSON from 'ol/format/GeoJSON';\nimport { getArea, getLength } from 'ol/sphere';\nimport { fromCircle } from 'ol/geom/Polygon';\nimport ScaleLine from 'ol/control/ScaleLine';\nimport { formatLength, formatLengthFull, formatArea, formatAreaFull } from '../units.js';\n\n// ol-ext LayerSwitcher\nimport LayerSwitcher from 'ol-ext/control/LayerSwitcher';\n\n// ol-ext SearchNominatim\nimport SearchNominatim from 'ol-ext/control/SearchNominatim';\n\n// ol-ext EditBar for drawing/editing features\nimport EditBar from 'ol-ext/control/EditBar';\nimport Bar from 'ol-ext/control/Bar';\nimport Button from 'ol-ext/control/Button';\n\n// ol-ext TouchCursor for touch-enabled devices\nimport TouchCursor from 'ol-ext/interaction/TouchCursor';\n\n// ol-ext ModifyFeature for cross-layer modification\nimport ModifyFeature from 'ol-ext/interaction/ModifyFeature';\n\n// ol-ext UndoRedo interaction\nimport UndoRedo from 'ol-ext/interaction/UndoRedo';\n\n// ol-ext SnapGuides — snaps drawing vertices to alignment guides\nimport SnapGuides from 'ol-ext/interaction/SnapGuides';\n\n// ol Select interaction (for custom multi-layer Select)\nimport Select from 'ol/interaction/Select';\nimport { click as clickCondition } from 'ol/events/condition';\n\n// ol-ext Split interaction (for line splitting) and Toggle control\nimport Split from 'ol-ext/interaction/Split';\nimport Toggle from 'ol-ext/control/Toggle';\nimport TextButton from 'ol-ext/control/TextButton';\n\n// Custom polygon split interaction\nimport { PolygonSplitInteraction } from '../interactions/PolygonSplitInteraction.js';\n\n// Custom polygon merge interaction\nimport { PolygonMergeInteraction } from '../interactions/PolygonMergeInteraction.js';\n\n// Custom polygon divide interaction\nimport { PolygonDivideInteraction } from '../interactions/PolygonDivideInteraction.js';\n\n// Toast notifications\nimport { showToast } from '../toast.js';\n\n// CSS imports\nimport 'ol/ol.css';\nimport 'ol-ext/dist/ol-ext.css';\nimport '../styles/layerswitcher.css';\n\nexport class MapView {\n constructor(targetId, options = {}) {\n this.options = options;\n this.markerSource = new VectorSource();\n this.clickCallbacks = [];\n\n // Category emoji and label mapping\n // Add new categories here - they will automatically appear in the dropdown\n this.categoryEmojis = {\n 'default': { emoji: '📍', label: 'Default' },\n 'water': { emoji: '💧', label: 'Water Point' },\n 'school': { emoji: '🏫', label: 'School' },\n 'health': { emoji: '🏥', label: 'Health Facility' },\n 'market': { emoji: '🏪', label: 'Market' },\n 'other': { emoji: '📌', label: 'Other' }\n };\n\n // Helper to get emoji for a category\n this.getEmoji = (category) => {\n const cat = this.categoryEmojis[category];\n return cat ? cat.emoji : '📍';\n };\n\n // Helper to generate category options HTML for select dropdowns\n this.getCategoryOptionsHtml = () => {\n return Object.entries(this.categoryEmojis)\n .map(([key, { emoji, label }]) =>\n ``\n )\n .join('\\n ');\n };\n\n // Create emoji style helper\n this.createEmojiStyle = (emoji, fontSize = 24) => {\n return new Style({\n text: new Text({\n text: emoji,\n font: `${fontSize}px sans-serif`,\n textBaseline: 'bottom',\n textAlign: 'center',\n offsetY: -5,\n }),\n });\n };\n\n // Default marker style (pin emoji)\n this.defaultStyle = this.createEmojiStyle('📍', 32);\n\n // Selected marker style (larger)\n this.selectedStyle = this.createEmojiStyle('📍', 42);\n\n // Initialize category styles with emojis\n this.categoryStyles = {};\n for (const [category, { emoji }] of Object.entries(this.categoryEmojis)) {\n this.categoryStyles[category] = this.createEmojiStyle(emoji, 32);\n }\n\n // Create base layers group\n const baseLayers = this.createBaseLayers(options.basemap || 'topo');\n\n // Markers layer — hidden at startup; the user enables it from the\n // LayerSwitcher when they want to see location markers / category pins.\n this.markersLayer = new VectorLayer({\n title: 'Markers',\n source: this.markerSource,\n style: (feature) => this.getFeatureStyle(feature),\n visible: false,\n });\n\n // Overlay layers group (for remote data like boundaries)\n this.overlayGroup = new LayerGroup({\n title: 'Overlays',\n });\n\n // Create map\n // Layer order (bottom → top): Base Maps, Markers, Overlays\n // MapTools will insert Measurements and Drawings between Markers and Overlays.\n // initEditBar() will insert its Drawings group above those.\n // Final LayerSwitcher order (top → bottom):\n // Overlays, Drawings, Measurements, Markers, Base Maps\n this.map = new Map({\n target: targetId,\n layers: [\n baseLayers,\n this.markersLayer,\n this.overlayGroup,\n ],\n view: new View({\n center: fromLonLat(options.center || [0, 0]),\n zoom: options.zoom || 2,\n minZoom: options.minZoom || 2,\n maxZoom: options.maxZoom || 19,\n })\n });\n\n // Add LayerSwitcher control\n const layerSwitcher = new LayerSwitcher({\n collapsed: true,\n mouseover: true,\n extent: true,\n trash: false,\n oninfo: null,\n });\n this.map.addControl(layerSwitcher);\n\n // Apply the LUSPA branded icon to the LayerSwitcher's collapse button.\n // Done in JS so the URL respects Vite's BASE_URL — survives deployment\n // under any sub-path.\n // NOTE: folder name is `app-icons`, NOT `icons` — Apache aliases `/icons/`\n // by default to its own directory-listing thumbnails, which would\n // intercept this request server-side.\n queueMicrotask(() => {\n const btn = layerSwitcher.element?.querySelector(':scope > button');\n if (btn) {\n const baseUrl = (import.meta.env?.BASE_URL || '/').replace(/\\/?$/, '/');\n btn.style.backgroundImage = `url('${baseUrl}app-icons/luspa-72x72.png')`;\n }\n });\n\n // ------------------------------------------------------------------\n // Decorate each layer's
              • as it's rendered:\n // • inject a type-tag chip (WMS / XYZ / VEC / …) next to the label\n // • add a green \"+\" button to the \"External Source\" group header\n // After each draw cycle, refresh the panel chrome (active count badge\n // + footer reset button). Schedule once per cycle via a microtask.\n // ------------------------------------------------------------------\n let _lsChromeScheduled = false;\n layerSwitcher.on('drawlist', (evt) => {\n this._decorateLayerListItem(evt.layer, evt.li);\n\n if (!_lsChromeScheduled) {\n _lsChromeScheduled = true;\n queueMicrotask(() => {\n _lsChromeScheduled = false;\n this._refreshLayerSwitcherChrome(layerSwitcher);\n });\n }\n });\n\n // Re-render the chrome whenever any layer's visibility changes (so the\n // active-count badge updates even when the user toggles via the panel).\n this.map.getLayers().on('change', () => {\n this._refreshLayerSwitcherChrome(layerSwitcher);\n });\n // Hook visibility events on every layer (recursive into groups).\n this._wireLayerSwitcherVisibilityHooks(layerSwitcher);\n\n // Create the add-layer dialog (hidden by default)\n this._createAddLayerDialog();\n\n // Create the legend panel (shows legends for visible layers that have one)\n this._createLegendPanel();\n\n // Add ScaleBar control\n this.scaleBar = new ScaleLine({\n bar: true,\n steps: 4,\n text: true,\n minWidth: 140,\n });\n this.map.addControl(this.scaleBar);\n\n // GPS rendering layers (current position + recorded trail) and the\n // expandable \"My Location\" control (Locate Me + Record Trail sub-buttons).\n this._initGpsRendering();\n this._createLocationControl();\n\n // Dedicated base-map picker — sits above the My Location button\n this._createBaseMapPicker();\n\n // Add SearchNominatim control\n const searchNominatim = new SearchNominatim({\n placeholder: 'Search location...',\n typing: 300, // Delay before search (ms)\n minLength: 3, // Minimum characters to start search\n maxItems: 10, // Maximum results to show\n collapsed: true, // Start collapsed\n // Limit search to improve relevance (can be adjusted)\n // countrycodes: 'gh', // Uncomment to limit to Ghana\n });\n this.map.addControl(searchNominatim);\n\n // Handle search result selection\n searchNominatim.on('select', (event) => {\n const searchResult = event.search;\n if (searchResult) {\n // SearchNominatim returns a plain object with lon/lat properties (as strings)\n const lon = parseFloat(searchResult.lon);\n const lat = parseFloat(searchResult.lat);\n const lonLat = [lon, lat];\n const coordinate = fromLonLat(lonLat);\n\n // Navigate to the selected location\n this.navigateTo(lon, lat, 14);\n\n // Trigger search select callbacks\n const result = {\n coordinate: coordinate,\n lonLat: lonLat,\n name: searchResult.display_name || searchResult.name || 'Unknown',\n searchResult: searchResult,\n };\n this.searchSelectCallbacks.forEach(cb => cb(result));\n }\n });\n\n // Store reference for external access\n this.searchNominatim = searchNominatim;\n this.searchSelectCallbacks = [];\n\n // Track selected feature\n this.selectedFeature = null;\n\n // Create popup overlay for hover\n this.createPopup();\n\n // Create info popup for double-click feature details\n this.createInfoPopup();\n\n // Create Add Location popup form\n this.createAddLocationPopup();\n\n // Create editable parcel form popup\n this.createParcelEditPopup();\n\n // Create drawn polygon attribute popup\n this.createDrawnPolygonPopup();\n\n // Create merge identifier (UPN) chooser popup\n this.createMergePopup();\n\n // Create divide polygon popup (number input)\n this.createDividePopup();\n\n // Double-click callbacks\n this.dblClickCallbacks = [];\n\n // EditBar is set up lazily via initEditBar() once the Drawings\n // layer/group is available (called from main.js after loadLayers).\n this.editBar = null;\n this.drawingsSource = null;\n this.drawingsLayer = null;\n this.touchCursor = null;\n this._editBarActive = false;\n }\n\n // ============================================================================\n // EditBar + Drawings Layer + TouchCursor\n // ============================================================================\n\n /**\n * Initialise the EditBar with a dedicated \"Drawings\" LayerGroup.\n *\n * A \"Drawings\" LayerGroup is created at the top of the overlay stack\n * containing a \"sketches\" VectorLayer for storing drawn features.\n * The EditBar, Select and Modify interactions are only active while\n * edit mode is on; in all other cases normal click / double-click\n * behaviour is preserved.\n *\n * Call this once from main.js after the layer groups have been created.\n */\n initEditBar() {\n // 1. Create a \"Drawings\" LayerGroup with a \"sketches\" VectorLayer inside\n this.drawingsSource = new VectorSource();\n this.drawingsLayer = new VectorLayer({\n title: 'sketches',\n source: this.drawingsSource,\n style: new Style({\n stroke: new Stroke({ color: '#f59e0b', width: 2.5 }),\n fill: new Fill({ color: 'rgba(245,158,11,0.15)' }),\n image: new Circle({\n radius: 6,\n fill: new Fill({ color: '#f59e0b' }),\n stroke: new Stroke({ color: '#fff', width: 1.5 }),\n }),\n }),\n });\n\n this._drawingsGroup = new LayerGroup({\n title: 'Drawings',\n layers: [this.drawingsLayer],\n });\n // Insert as a top-level map layer just before the Overlays group so the\n // LayerSwitcher order is: Overlays > Drawings > Measurements > Markers > Base Maps.\n // Find Overlays by reference rather than assuming it is the last layer —\n // other layers (e.g. the GPS trail/position layers added by\n // _initGpsRendering) may sit on top of it.\n const mapLayers = this.map.getLayers();\n const overlayIdx = mapLayers.getArray().indexOf(this.overlayGroup);\n mapLayers.insertAt(overlayIdx >= 0 ? overlayIdx : mapLayers.getLength(), this._drawingsGroup);\n\n // 2. Create a Select interaction that works on ALL vector layers.\n // It starts INACTIVE so it doesn't steal clicks from normal handlers.\n this._selectInteraction = new Select({\n condition: clickCondition,\n filter: (feature, layer) => !!layer,\n layers: (layer) => layer instanceof VectorLayer,\n });\n this._selectInteraction.setActive(false);\n this.map.addInteraction(this._selectInteraction);\n\n // 3. Create a ModifyFeature interaction bound to the selection.\n // Also starts inactive.\n this._modifyInteraction = new ModifyFeature({\n features: this._selectInteraction.getFeatures(),\n });\n this._modifyInteraction.setActive(false);\n\n // 4. UndoRedo interaction — watches the drawings source\n this._undoRedo = new UndoRedo();\n this.map.addInteraction(this._undoRedo);\n\n // 5. Build the EditBar — all interactions enabled.\n this.editBar = new EditBar({\n source: this.drawingsSource,\n interactions: {\n Select: this._selectInteraction,\n ModifySelect: this._modifyInteraction,\n DrawPoint: true,\n DrawLine: true,\n DrawPolygon: true,\n DrawRegular: true,\n DrawHole: true,\n Delete: true,\n Info: true,\n Transform: true,\n Split: false,\n },\n });\n this.map.addControl(this.editBar);\n\n // 5b. Persistent vertex overlay — when edit mode is active and the user\n // selects a polygon (or line) for modification, render a small dot\n // at every vertex so the user can see all editable nodes at a glance.\n // ol-ext's ModifyFeature only renders the closest vertex on hover; this\n // overlay complements that without subclassing the interaction.\n this._setupVertexOverlay();\n\n // 6. Add extra buttons (Undo, Redo, Save) as a sub-bar\n // inside the EditBar so they appear inline.\n const extraBar = new Bar({\n group: true,\n // Stable class so CSS can move this group (undo/redo/save/snap) to a\n // second row on small screens — see `.ol-editbar-actions` media query.\n className: 'ol-editbar-actions',\n controls: [\n new Button({\n html: '',\n className: 'ol-undo',\n title: 'Undo',\n handleClick: () => {\n if (this._undoRedo.hasUndo()) this._undoRedo.undo();\n },\n }),\n new Button({\n html: '',\n className: 'ol-redo',\n title: 'Redo',\n handleClick: () => {\n if (this._undoRedo.hasRedo()) this._undoRedo.redo();\n },\n }),\n new Button({\n html: '',\n className: 'ol-save',\n title: 'Save drawings',\n handleClick: () => {\n this.dispatchEditEvent('save');\n },\n }),\n ],\n });\n this.editBar.addControl(extraBar);\n\n // 6a-split. Custom Split tool with Lines / Polygons sub-categories.\n // The default ol-ext Split only handles LineString. We add a parent\n // Toggle with a sub-bar containing two sub-toggles: \"Lines\" (ol-ext\n // Split) and \"Polygons\" (our PolygonSplitInteraction).\n // No explicit sources → both interactions search ALL visible vector layers,\n // so they work on drawn features, parcels, zones, and any other polygon layer.\n this._lineSplitInteraction = new Split();\n this._polygonSplitInteraction = new PolygonSplitInteraction();\n this.map.addInteraction(this._lineSplitInteraction);\n this.map.addInteraction(this._polygonSplitInteraction);\n this._lineSplitInteraction.setActive(false);\n this._polygonSplitInteraction.setActive(false);\n\n // When a parcel is split, the user picks which piece keeps the UPN.\n this._polygonSplitInteraction.on('splitpick', (evt) => {\n const idFields = ['UPN', 'upn', 'id', 'parcelid', 'parcel_id', 'PARCELID', 'PARCEL_ID', 'ID'];\n for (const feat of evt.features) {\n if (feat === evt.picked) continue;\n for (const field of idFields) {\n if (feat.get(field) !== undefined) {\n feat.set(field, '');\n }\n }\n }\n });\n\n // Polygon Divide interaction (parameter-driven equal-area division)\n this._polygonDivideInteraction = new PolygonDivideInteraction();\n this.map.addInteraction(this._polygonDivideInteraction);\n this._polygonDivideInteraction.setActive(false);\n\n const splitLineToggle = new Toggle({\n html: '',\n className: 'ol-split-line',\n title: 'Split Lines',\n name: 'SplitLine',\n interaction: this._lineSplitInteraction,\n autoActivate: true,\n });\n const splitPolyToggle = new Toggle({\n html: '',\n className: 'ol-split-polygon',\n title: 'Split Polygons',\n name: 'SplitPolygon',\n interaction: this._polygonSplitInteraction,\n });\n const splitDivideToggle = new Toggle({\n html: '',\n className: 'ol-split-divide',\n title: 'Divide Polygon',\n name: 'DividePolygon',\n interaction: this._polygonDivideInteraction,\n });\n\n const splitSubBar = new Bar({\n toggleOne: true,\n autoDeactivate: true,\n controls: [splitLineToggle, splitPolyToggle, splitDivideToggle],\n });\n\n const splitParentToggle = new Toggle({\n className: 'ol-split',\n title: 'Split',\n name: 'Split',\n bar: splitSubBar,\n onToggle: (active) => {\n if (!active) {\n this._lineSplitInteraction.setActive(false);\n this._polygonSplitInteraction.setActive(false);\n this._polygonDivideInteraction.setActive(false);\n }\n },\n });\n this.editBar.addControl(splitParentToggle);\n\n // Listen for divide form request → show divide popup\n this._polygonDivideInteraction.on('divideform', (evt) => {\n this.showDividePopup(evt.feature, evt.source, evt.coordinate);\n });\n this._polygonDivideInteraction.on('dividecancel', () => {\n this.hideDividePopup();\n });\n\n // When a parcel is divided, the user picks which piece keeps the UPN.\n // The picked piece gets the original properties; all others get UPN cleared.\n this._polygonDivideInteraction.on('dividepick', (evt) => {\n const idFields = ['UPN', 'upn', 'id', 'parcelid', 'parcel_id', 'PARCELID', 'PARCEL_ID', 'ID'];\n for (const feat of evt.features) {\n if (feat === evt.picked) continue;\n // Clear identifier fields on the non-picked pieces\n for (const field of idFields) {\n if (feat.get(field) !== undefined) {\n feat.set(field, '');\n }\n }\n }\n });\n\n // 6a-merge. Polygon Merge tool — select two adjacent polygons, click shared\n // edges, and merge them into one. For parcels, a UPN chooser popup appears.\n this._polygonMergeInteraction = new PolygonMergeInteraction();\n this.map.addInteraction(this._polygonMergeInteraction);\n this._polygonMergeInteraction.setActive(false);\n\n const mergeToggle = new Toggle({\n html: '',\n className: 'ol-merge',\n title: 'Merge Polygons',\n name: 'Merge',\n interaction: this._polygonMergeInteraction,\n });\n this.editBar.addControl(mergeToggle);\n\n // Listen for merged-parcel event → show UPN chooser\n this._polygonMergeInteraction.on('mergedparcel', (evt) => {\n this.showMergeIdentifierPopup(evt.merged, evt.propsA, evt.propsB, evt.coordinate);\n });\n\n // Small-screen layout: insert a zero-height, full-width flex line-break\n // immediately BEFORE the action group. On phones (see the .ol-editbar\n // media query) this forces the wrap to happen here, so the action group\n // (undo/redo/save/snap) together with the Split and Merge toggles all land\n // on a single second row instead of Split/Merge spilling onto a third row.\n // The break is display:none on wider screens, so desktop layout is unchanged.\n const editbarEl = this.editBar.element;\n if (editbarEl && extraBar.element && extraBar.element.parentNode === editbarEl) {\n const breakEl = document.createElement('div');\n breakEl.className = 'ol-editbar-break';\n editbarEl.insertBefore(breakEl, extraBar.element);\n }\n\n // 6b. SnapGuides — shows alignment guides while drawing.\n // Uses VectorImageLayer for GPU-friendly canvas rendering instead of\n // re-creating individual SVG elements on every guide update.\n this._snapGuidesEnabled = localStorage.getItem('snap-guides-enabled') === '1';\n this._snapGuides = new SnapGuides({\n pixelTolerance: 10,\n vectorClass: VectorImageLayer,\n });\n this.map.addInteraction(this._snapGuides);\n\n // Connect SnapGuides to whichever draw interaction becomes active.\n // setDrawInteraction() only tracks one at a time, so we re-bind\n // whenever a draw tool is activated.\n const drawToolNames = ['DrawPoint', 'DrawLine', 'DrawPolygon', 'DrawHole', 'DrawRegular'];\n for (const name of drawToolNames) {\n const interaction = this.editBar.getInteraction(name);\n if (interaction) {\n interaction.on('change:active', () => {\n if (interaction.getActive()) {\n this._snapGuides.setDrawInteraction(interaction);\n }\n });\n }\n }\n\n // Also connect SnapGuides to the Modify interaction for vertex editing\n if (this._modifyInteraction) {\n this._snapGuides.setModifyInteraction(this._modifyInteraction);\n }\n\n // 6c. Snap-guides toggle button (magnet icon) — persisted in localStorage\n const snapToggleBtn = new Button({\n html: '',\n className: 'ol-snap-toggle' + (this._snapGuidesEnabled ? ' ol-active' : ''),\n title: 'Toggle Snap Guides',\n handleClick: () => {\n this._snapGuidesEnabled = !this._snapGuidesEnabled;\n localStorage.setItem('snap-guides-enabled', this._snapGuidesEnabled ? '1' : '0');\n // Update visual state\n snapToggleBtn.element.classList.toggle('ol-active', this._snapGuidesEnabled);\n // Activate or deactivate the interaction\n if (this._snapGuides) {\n this._snapGuides.setActive(this._snapGuidesEnabled && this._editBarActive);\n }\n console.log('[MapView] Snap guides:', this._snapGuidesEnabled ? 'ON' : 'OFF');\n },\n });\n this._snapToggleBtn = snapToggleBtn;\n extraBar.addControl(snapToggleBtn);\n\n // Start hidden — use the full setEditMode(false) so the Select +\n // Modify interactions are deactivated (the EditBar constructor may\n // have re-activated them).\n this.setEditMode(false);\n\n // 7. Link EditBar visibility to the Drawings group's visibility.\n this._drawingsGroup.on('change:visible', () => {\n const visible = this._drawingsGroup.getVisible();\n this.setEditMode(visible);\n });\n\n // 8. Touch-device detection & TouchCursor setup\n const isTouchDevice = ('ontouchstart' in window) ||\n (navigator.maxTouchPoints > 0) ||\n (navigator.msMaxTouchPoints > 0);\n\n if (isTouchDevice) {\n this.touchCursor = new TouchCursor({\n className: 'ol-editbar-cursor',\n });\n this.map.addInteraction(this.touchCursor);\n this.touchCursor.setActive(false);\n console.log('[MapView] Touch device detected — TouchCursor added');\n }\n\n // 9. Listen for polygon features drawn via EditBar's DrawPolygon tool.\n // When a Polygon is added to the drawings source, show the attribute popup.\n this.drawingsSource.on('addfeature', (evt) => {\n const feature = evt.feature;\n const geom = feature.getGeometry();\n if (!geom || geom.getType() !== 'Polygon') return;\n\n const coordinate = geom.getInteriorPoint().getCoordinates();\n this.showDrawnPolygonPopup(feature, coordinate);\n });\n\n console.log('[MapView] EditBar initialised with Drawings group, UndoRedo and SnapGuides (default:', this._snapGuidesEnabled ? 'ON' : 'OFF', ')');\n }\n\n /**\n * Dispatch a custom edit event (e.g. 'save').\n * External code can listen via mapView.onEditEvent('save', callback).\n * @param {string} type\n */\n dispatchEditEvent(type) {\n if (!this._editEventListeners) return;\n const listeners = this._editEventListeners[type];\n if (listeners) {\n listeners.forEach((fn) => fn());\n }\n }\n\n /**\n * Listen for custom edit events (e.g. 'save').\n * @param {string} type - Event name\n * @param {Function} callback\n */\n onEditEvent(type, callback) {\n if (!this._editEventListeners) this._editEventListeners = {};\n if (!this._editEventListeners[type]) this._editEventListeners[type] = [];\n this._editEventListeners[type].push(callback);\n }\n\n /**\n * Toggle edit mode on or off.\n *\n * When ON: EditBar is visible, Select + Modify interactions are active.\n * When OFF: EditBar is hidden, Select + Modify are deactivated, any\n * current selection is cleared so normal click / double-click\n * events work without interference.\n *\n * @param {boolean} active\n */\n setEditMode(active) {\n this._editBarActive = !!active;\n\n if (this.editBar) {\n this.editBar.setVisible(this._editBarActive);\n\n if (!this._editBarActive) {\n // Deactivate all EditBar controls (DrawPoint, DrawLine, etc.)\n // so no draw interaction stays active in the background.\n this.editBar.deactivateControls();\n }\n }\n\n // Activate / deactivate Select + Modify\n if (this._selectInteraction) {\n if (!this._editBarActive) {\n // Clear any current selection first\n this._selectInteraction.getFeatures().clear();\n }\n this._selectInteraction.setActive(this._editBarActive);\n }\n if (this._modifyInteraction) {\n this._modifyInteraction.setActive(this._editBarActive);\n }\n\n // Toggle SnapGuides — only active when both edit mode AND the user toggle are on\n if (this._snapGuides) {\n this._snapGuides.setActive(this._snapGuidesEnabled && this._editBarActive);\n }\n\n // Toggle TouchCursor\n if (this.touchCursor) {\n this.touchCursor.setActive(this._editBarActive);\n }\n\n // Clear persistent vertex highlights when leaving edit mode\n if (!this._editBarActive && this._vertexOverlaySource) {\n this._vertexOverlaySource.clear();\n }\n\n console.log('[MapView] Edit mode:', this._editBarActive ? 'ON' : 'OFF');\n }\n\n /**\n * Check whether edit mode (select / modify) is currently active.\n * @returns {boolean}\n */\n isEditMode() {\n return this._editBarActive;\n }\n\n // ============================================================================\n // Persistent Vertex Highlight Overlay\n // ============================================================================\n\n /**\n * Create a vector layer that renders a small dot at every vertex of any\n * currently-selected feature (polygon, multipolygon, line, multiline).\n * Only active while edit mode is on.\n *\n * Hooks:\n * - `select` event from the Select interaction → rebuild dots for the new selection\n * - `change` event on the selected feature → reposition dots when a vertex is dragged\n */\n _setupVertexOverlay() {\n this._vertexOverlaySource = new VectorSource();\n this._vertexOverlayLayer = new VectorLayer({\n title: '__vertex_highlight__',\n source: this._vertexOverlaySource,\n // Render above all other overlays but below ModifyFeature's hover indicator\n zIndex: 990,\n style: new Style({\n image: new Circle({\n radius: 4,\n fill: new Fill({ color: 'rgba(14,165,233,0.85)' }), // brand blue\n stroke: new Stroke({ color: '#fff', width: 1.2 }),\n }),\n }),\n });\n // Hide from LayerSwitcher — purely visual, not user-toggleable\n this._vertexOverlayLayer.set('displayInLayerSwitcher', false);\n this.map.addLayer(this._vertexOverlayLayer);\n\n // Bound handler so we can attach/detach by reference\n this._onSelectedFeatureGeomChange = () => this._refreshVertexOverlay();\n\n // Track which feature(s) we're listening on, so we can unhook cleanly\n this._vertexTrackedFeatures = new Set();\n\n // When the selection changes, swap which features we listen to and rebuild dots\n this._selectInteraction.on('select', () => this._refreshVertexOverlay());\n }\n\n /**\n * Rebuild the vertex overlay from the current Select interaction's features.\n * No-ops when not in edit mode.\n */\n _refreshVertexOverlay() {\n if (!this._vertexOverlaySource) return;\n this._vertexOverlaySource.clear();\n\n // Detach change listeners from previously-tracked features\n if (this._vertexTrackedFeatures) {\n for (const f of this._vertexTrackedFeatures) {\n f.un('change', this._onSelectedFeatureGeomChange);\n }\n this._vertexTrackedFeatures.clear();\n }\n\n if (!this._editBarActive || !this._selectInteraction) return;\n\n const selected = this._selectInteraction.getFeatures().getArray();\n for (const feat of selected) {\n const geom = feat.getGeometry();\n if (!geom) continue;\n const type = geom.getType();\n if (!['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'].includes(type)) {\n continue;\n }\n const coords = this._collectAllVertices(geom);\n for (const c of coords) {\n this._vertexOverlaySource.addFeature(new Feature(new Point(c)));\n }\n // Listen for vertex moves on this feature\n feat.on('change', this._onSelectedFeatureGeomChange);\n this._vertexTrackedFeatures.add(feat);\n }\n }\n\n /**\n * Walk a (Multi)Polygon or (Multi)LineString geometry and return the flat\n * list of vertex coordinates. Polygon rings have a duplicate closing vertex\n * (last == first) which is dropped here so we don't render two dots on top\n * of each other.\n *\n * @param {Geometry} geom\n * @returns {Array>}\n */\n _collectAllVertices(geom) {\n const out = [];\n const isCoord = (v) => Array.isArray(v) && typeof v[0] === 'number';\n\n const visitRing = (ring, isPolygonRing) => {\n const len = isPolygonRing && ring.length > 1 ? ring.length - 1 : ring.length;\n for (let i = 0; i < len; i++) out.push(ring[i]);\n };\n\n const type = geom.getType();\n const coords = geom.getCoordinates();\n\n switch (type) {\n case 'Polygon':\n // coords = [outerRing, hole1, hole2, …]\n for (const ring of coords) visitRing(ring, true);\n break;\n case 'MultiPolygon':\n // coords = [poly1, poly2, …]; each poly = [outerRing, hole1, …]\n for (const poly of coords) for (const ring of poly) visitRing(ring, true);\n break;\n case 'LineString':\n visitRing(coords, false);\n break;\n case 'MultiLineString':\n for (const line of coords) visitRing(line, false);\n break;\n default:\n // Fallback: deep walk to find arrays of [x, y]\n const walk = (v) => {\n if (isCoord(v)) out.push(v);\n else if (Array.isArray(v)) for (const sub of v) walk(sub);\n };\n walk(coords);\n }\n return out;\n }\n\n /**\n * Get the Drawings layer for external access.\n * @returns {VectorLayer}\n */\n getDrawingsLayer() {\n return this.drawingsLayer;\n }\n\n /**\n * Get the Drawings source for external access.\n * @returns {VectorSource}\n */\n getDrawingsSource() {\n return this.drawingsSource;\n }\n\n /**\n * Get the EditBar control for external access.\n * @returns {EditBar}\n */\n getEditBar() {\n return this.editBar;\n }\n\n /**\n * Update the ScaleBar units ('metric' or 'imperial').\n * @param {'metric'|'imperial'} system\n */\n setScaleBarUnits(system) {\n if (this.scaleBar) {\n this.scaleBar.setUnits(system === 'imperial' ? 'imperial' : 'metric');\n }\n }\n\n /**\n * Create the popup overlay element and add to map\n */\n createPopup() {\n // Create popup container element\n this.popupElement = document.createElement('div');\n this.popupElement.className = 'map-popup';\n this.popupElement.style.cssText = `\n position: absolute;\n background: var(--card, #fff);\n color: var(--card-foreground, #1e1a4b);\n border-radius: 8px;\n padding: 10px 14px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.25);\n font-family: var(--font-body, 'Exo', sans-serif);\n font-size: 13px;\n min-width: 150px;\n max-width: 280px;\n pointer-events: none;\n z-index: 1000;\n border: 1px solid var(--border, #1e1a4b1f);\n `;\n\n // Create the overlay\n this.popup = new Overlay({\n element: this.popupElement,\n positioning: 'bottom-center',\n offset: [0, -15],\n stopEvent: false,\n });\n\n this.map.addOverlay(this.popup);\n\n // Set up hover handler\n this.setupHoverPopup();\n }\n\n /**\n * Set up the hover popup behavior\n */\n setupHoverPopup() {\n let currentFeature = null;\n\n this.map.on('pointermove', (evt) => {\n if (evt.dragging) {\n this.hidePopup();\n return;\n }\n\n // Only find features that are location markers (have 'name' property)\n const feature = this.map.forEachFeatureAtPixel(evt.pixel, (f) => {\n // Only return features that have a 'name' property (location markers)\n if (f.get('name')) {\n return f;\n }\n return null;\n });\n\n if (feature && feature !== currentFeature) {\n currentFeature = feature;\n this.showPopup(feature, evt.coordinate);\n } else if (!feature && currentFeature) {\n currentFeature = null;\n this.hidePopup();\n }\n\n // Update cursor - only show pointer for location markers\n this.map.getTargetElement().style.cursor = feature ? 'pointer' : '';\n });\n\n // Hide popup when mouse leaves the map\n this.map.getTargetElement().addEventListener('mouseleave', () => {\n this.hidePopup();\n currentFeature = null;\n });\n }\n\n /**\n * Show popup with feature attributes\n */\n showPopup(feature, coordinate) {\n const name = feature.get('name') || 'Unnamed';\n const category = feature.get('category') || 'default';\n const description = feature.get('description');\n const lon = feature.get('lon');\n const lat = feature.get('lat');\n const emoji = this.getEmoji(category);\n\n // Build popup content\n let html = `\n
                \n ${emoji} ${this.escapeHtml(name)}\n
                \n `;\n\n // Category badge\n const categoryColors = {\n 'water': '#3b82f6',\n 'school': '#f59e0b',\n 'health': '#ef4444',\n 'market': '#8b5cf6',\n 'default': '#2d5016',\n 'other': '#6b7280'\n };\n const catColor = categoryColors[category] || '#6b7280';\n html += `\n
                \n ${category}\n
                \n `;\n\n // Description if available\n if (description) {\n html += `\n
                \n ${this.escapeHtml(description)}\n
                \n `;\n }\n\n // Coordinates\n if (lon !== undefined && lat !== undefined) {\n html += `\n
                \n ${Number(lon).toFixed(5)}, ${Number(lat).toFixed(5)}\n
                \n `;\n }\n\n this.popupElement.innerHTML = html;\n this.popup.setPosition(coordinate);\n }\n\n /**\n * Hide the popup\n */\n hidePopup() {\n this.popup.setPosition(undefined);\n }\n\n /**\n * Create the info popup overlay for double-click feature details\n */\n createInfoPopup() {\n this.infoPopupElement = document.createElement('div');\n this.infoPopupElement.className = 'map-info-popup';\n this.infoPopupElement.style.cssText = `\n position: absolute;\n background: var(--card, #fff);\n color: var(--card-foreground, #1e1a4b);\n border-radius: 10px;\n padding: 0;\n box-shadow: 0 4px 16px rgba(0,0,0,0.3);\n font-family: var(--font-body, 'Exo', sans-serif);\n font-size: 13px;\n min-width: 220px;\n max-width: 320px;\n max-height: 70vh;\n display: flex;\n flex-direction: column;\n z-index: 1001;\n border: 1px solid var(--border, #1e1a4b1f);\n overflow: hidden;\n `;\n\n this.infoPopup = new Overlay({\n element: this.infoPopupElement,\n positioning: 'bottom-center',\n offset: [0, -10],\n stopEvent: true,\n autoPan: true,\n autoPanAnimation: { duration: 250 },\n });\n\n this.map.addOverlay(this.infoPopup);\n }\n\n /**\n * Show the info popup with feature attributes and area\n * @param {Feature} feature - OpenLayers feature\n * @param {Array} coordinate - Map coordinate [x, y]\n * @param {Object} [options] - Display options\n * @param {string} [options.title='Feature Info'] - Popup header title\n * @param {string} [options.color='#e11d48'] - Header background colour\n */\n showInfoPopup(feature, coordinate, options = {}) {\n const { title = 'Feature Info', color = '#e11d48' } = options;\n const properties = feature.getProperties();\n const geometry = feature.getGeometry();\n const geomType = geometry.getType();\n\n // Build attributes table rows (skip geometry and internal keys)\n const skipKeys = ['geometry', '_layerType'];\n let rows = '';\n for (const [key, value] of Object.entries(properties)) {\n if (skipKeys.includes(key) || value === undefined || value === null) continue;\n rows += `\n \n ${this.escapeHtml(key)}\n ${this.escapeHtml(String(value))}\n \n `;\n }\n\n // Add measurement row based on geometry type\n if (geomType === 'Polygon' || geomType === 'MultiPolygon') {\n // Area for polygons\n const areaSqm = getArea(geometry, { projection: 'EPSG:3857' });\n const areaFormatted = formatAreaFull(areaSqm);\n rows += `\n \n area\n ${areaFormatted}\n \n `;\n } else if (geomType === 'LineString' || geomType === 'MultiLineString') {\n // Length for lines\n const lengthM = getLength(geometry, { projection: 'EPSG:3857' });\n const lengthFormatted = formatLengthFull(lengthM);\n rows += `\n \n length\n ${lengthFormatted}\n \n `;\n } else if (geomType === 'Point') {\n // Coordinates for points\n const coords = toLonLat(geometry.getCoordinates());\n const lon = coords[0].toFixed(6);\n const lat = coords[1].toFixed(6);\n rows += `\n \n longitude\n ${lon}\n \n \n latitude\n ${lat}\n \n `;\n }\n\n const html = `\n
                \n ${this.escapeHtml(title)}\n \n
                \n
                \n \n ${rows}\n
                \n
                \n `;\n\n this.infoPopupElement.innerHTML = html;\n this.infoPopup.setPosition(coordinate);\n\n // Close button handler\n this.infoPopupElement.querySelector('#info-popup-close').addEventListener('click', () => {\n this.hideInfoPopup();\n });\n }\n\n /**\n * Hide the info popup\n */\n hideInfoPopup() {\n this.infoPopup.setPosition(undefined);\n }\n\n // ============================================================================\n // Circle Intersection Analysis\n // ============================================================================\n\n /**\n * Analyse which features from overlay layers intersect a measurement circle\n * and show the results in the info popup.\n *\n * @param {Feature} circleFeature - The measurement circle feature (Circle geometry)\n * @param {Array} coordinate - Map coordinate for popup placement [x, y]\n */\n /**\n * Collect intersection results (parcels, zones, other) into a\n * structured { label, value } array for both HTML and PDF rendering.\n */\n _collectIntersectionRows(parcelFeatures, zoneFeatures, otherByLayer) {\n const dataRows = [];\n\n if (parcelFeatures.length > 0) {\n dataRows.push({ label: 'Parcels', value: String(parcelFeatures.length), color: '#0ea5e9' });\n }\n\n if (zoneFeatures.length > 0) {\n const names = zoneFeatures.map(f =>\n f.get('colzonename') || f.get('zone_name') || f.get('name') || 'unnamed'\n );\n dataRows.push({ label: 'Zones', value: String(zoneFeatures.length), color: '#7c3aed' });\n dataRows.push({ label: 'Zone Names', value: names.map(n => this.escapeHtml(n)).join(', '), color: '#7c3aed' });\n }\n\n for (const [title, features] of Object.entries(otherByLayer)) {\n dataRows.push({ label: this.escapeHtml(title), value: `${features.length} feature(s)` });\n }\n\n if (dataRows.length === 0) {\n dataRows.push({ label: '', value: 'No intersecting features found', empty: true });\n }\n\n return dataRows;\n }\n\n /**\n * Build the full popup HTML for an analysis popup (circle or area).\n *\n * @param {string} emoji - Header emoji\n * @param {string} title - e.g. \"Circle Analysis\"\n * @param {Array<{label:string, value:string, color?:string, empty?:boolean}>} dataRows\n * @returns {string} HTML\n */\n _buildAnalysisPopupHtml(emoji, title, dataRows) {\n let tableRows = '';\n for (const row of dataRows) {\n if (row.empty) {\n tableRows += `\n \n ${row.value}\n `;\n continue;\n }\n const labelColor = row.color || 'var(--muted-foreground, #7a7a7a)';\n const border = row._first ? '' : 'border-top:1px solid var(--border, #1e1a4b1f);';\n tableRows += `\n \n ${row.label}\n ${row.value}\n `;\n }\n\n return `\n
                \n ${emoji} ${title}\n \n
                \n
                \n \n ${tableRows}\n
                \n
                \n
                \n \n
                `;\n }\n\n /**\n * Show the analysis popup, attach close + PDF export handlers.\n */\n _showAnalysisPopup(emoji, title, dataRows, coordinate) {\n this.infoPopupElement.innerHTML = this._buildAnalysisPopupHtml(emoji, title, dataRows);\n this.infoPopup.setPosition(coordinate);\n\n this.infoPopupElement.querySelector('#info-popup-close').addEventListener('click', () => {\n this.hideInfoPopup();\n });\n\n // PDF export — dynamic import so jspdf is only loaded on demand\n this.infoPopupElement.querySelector('#info-popup-export-pdf')?.addEventListener('click', () => {\n // Strip HTML from values and remove the color/empty keys for the PDF\n const pdfRows = dataRows\n .filter(r => !r.empty)\n .map(r => ({ label: r.label, value: r.value.replace(/<[^>]*>/g, '') }));\n\n import('../pdf-export.js').then(({ exportAnalysisPDF }) => {\n exportAnalysisPDF({ title, rows: pdfRows });\n }).catch(err => {\n console.error('[MapView] PDF export failed:', err);\n });\n });\n }\n\n showCircleIntersectionPopup(circleFeature, coordinate) {\n const circleGeom = circleFeature.getGeometry();\n if (!circleGeom || typeof circleGeom.getCenter !== 'function') return;\n\n // Convert the OL Circle to a polygon (64 sides) for intersection testing\n const circlePoly = fromCircle(circleGeom, 64);\n const circleExtent = circlePoly.getExtent();\n\n const radius = circleFeature.get('_radius') || circleGeom.getRadius();\n\n // Collect intersecting features grouped by layer type\n const parcelFeatures = [];\n const zoneFeatures = [];\n const otherByLayer = {};\n\n const intersectsCircle = (feature) => {\n const geom = feature.getGeometry();\n if (!geom) return false;\n const fExtent = geom.getExtent();\n if (\n fExtent[2] < circleExtent[0] ||\n fExtent[0] > circleExtent[2] ||\n fExtent[3] < circleExtent[1] ||\n fExtent[1] > circleExtent[3]\n ) {\n return false;\n }\n return circlePoly.intersectsExtent(fExtent) && this._geometriesIntersect(circlePoly, geom);\n };\n\n const scanGroup = (group, groupTitle) => {\n group.getLayers().forEach((layer) => {\n if (layer instanceof LayerGroup) {\n scanGroup(layer, layer.get('title') || groupTitle);\n } else if (layer instanceof VectorLayer && layer.getVisible()) {\n const layerTitle = layer.get('title') || groupTitle || 'Unknown';\n const source = layer.getSource();\n if (!source) return;\n\n const candidates = source.getFeaturesInExtent(circleExtent);\n for (const f of candidates) {\n const fType = f.get('_layerType');\n if (fType === 'measure_circle' || fType === 'measure_circle_radius') continue;\n\n if (!intersectsCircle(f)) continue;\n\n if (fType === 'parcel') {\n parcelFeatures.push(f);\n } else if (fType === 'collector_zone') {\n zoneFeatures.push(f);\n } else {\n if (!otherByLayer[layerTitle]) otherByLayer[layerTitle] = [];\n otherByLayer[layerTitle].push(f);\n }\n }\n }\n });\n };\n\n scanGroup(this.overlayGroup, 'Overlays');\n\n // Build structured data rows\n const radiusFormatted = formatLength(radius);\n const areaSqm = Math.PI * radius * radius;\n const areaFormatted = formatArea(areaSqm);\n\n const dataRows = [\n { label: 'Radius', value: radiusFormatted, _first: true },\n { label: 'Area', value: areaFormatted },\n ...this._collectIntersectionRows(parcelFeatures, zoneFeatures, otherByLayer),\n ];\n\n this._showAnalysisPopup('⭕', 'Circle Analysis', dataRows, coordinate);\n }\n\n /**\n * Show an intersection-analysis popup for a measured area polygon.\n * Same logic as showCircleIntersectionPopup but works with an\n * arbitrary Polygon geometry instead of a circle.\n *\n * @param {Feature} polygonFeature - The measure_area feature\n * @param {number[]} coordinate - Map coordinate for the popup anchor\n */\n showAreaIntersectionPopup(polygonFeature, coordinate) {\n const polyGeom = polygonFeature.getGeometry();\n if (!polyGeom) return;\n\n const polyExtent = polyGeom.getExtent();\n\n // Compute area via ol/sphere for geodesic accuracy\n const areaSqm = getArea(polyGeom, { projection: 'EPSG:3857' });\n const areaFormatted = formatArea(areaSqm);\n\n // Compute perimeter\n const perimeterM = getLength(polyGeom, { projection: 'EPSG:3857' });\n const perimeterFormatted = formatLength(perimeterM);\n\n // Collect intersecting features grouped by layer type\n const parcelFeatures = [];\n const zoneFeatures = [];\n const otherByLayer = {};\n\n const intersectsPoly = (feature) => {\n const geom = feature.getGeometry();\n if (!geom) return false;\n const fExtent = geom.getExtent();\n if (\n fExtent[2] < polyExtent[0] ||\n fExtent[0] > polyExtent[2] ||\n fExtent[3] < polyExtent[1] ||\n fExtent[1] > polyExtent[3]\n ) {\n return false;\n }\n return polyGeom.intersectsExtent(fExtent) && this._geometriesIntersect(polyGeom, geom);\n };\n\n const scanGroup = (group, groupTitle) => {\n group.getLayers().forEach((layer) => {\n if (layer instanceof LayerGroup) {\n scanGroup(layer, layer.get('title') || groupTitle);\n } else if (layer instanceof VectorLayer && layer.getVisible()) {\n const layerTitle = layer.get('title') || groupTitle || 'Unknown';\n const source = layer.getSource();\n if (!source) return;\n\n const candidates = source.getFeaturesInExtent(polyExtent);\n for (const f of candidates) {\n const fType = f.get('_layerType');\n if (fType === 'measure_area' || fType === 'measure_circle' || fType === 'measure_circle_radius') continue;\n\n if (!intersectsPoly(f)) continue;\n\n if (fType === 'parcel') {\n parcelFeatures.push(f);\n } else if (fType === 'collector_zone') {\n zoneFeatures.push(f);\n } else {\n if (!otherByLayer[layerTitle]) otherByLayer[layerTitle] = [];\n otherByLayer[layerTitle].push(f);\n }\n }\n }\n });\n };\n\n scanGroup(this.overlayGroup, 'Overlays');\n\n // Build structured data rows\n const dataRows = [\n { label: 'Area', value: areaFormatted, _first: true },\n { label: 'Perimeter', value: perimeterFormatted },\n ...this._collectIntersectionRows(parcelFeatures, zoneFeatures, otherByLayer),\n ];\n\n this._showAnalysisPopup('📐', 'Area Analysis', dataRows, coordinate);\n }\n\n /**\n * Test whether two geometries truly intersect (beyond just extent overlap).\n * Works for Polygon/MultiPolygon against any geometry type.\n *\n * @param {Geometry} geomA - First geometry (usually the circle polygon)\n * @param {Geometry} geomB - Second geometry\n * @returns {boolean}\n * @private\n */\n _geometriesIntersect(geomA, geomB) {\n const typeB = geomB.getType();\n\n // For polygons / multi-polygons: check if any coordinate of B is inside A,\n // or if any coordinate of A is inside B (covers overlap & containment).\n if (typeB === 'Polygon' || typeB === 'MultiPolygon') {\n // Check if any vertex of B lies inside A (use flatCoordinates for efficiency)\n const flatB = geomB.getFlatCoordinates();\n const stride = geomB.getStride();\n for (let i = 0; i < flatB.length; i += stride) {\n if (geomA.intersectsCoordinate([flatB[i], flatB[i + 1]])) return true;\n }\n // Check if any vertex of A lies inside B\n const flatA = geomA.getFlatCoordinates();\n const strideA = geomA.getStride();\n for (let i = 0; i < flatA.length; i += strideA) {\n if (geomB.intersectsCoordinate([flatA[i], flatA[i + 1]])) return true;\n }\n return false;\n }\n\n if (typeB === 'Point') {\n return geomA.intersectsCoordinate(geomB.getCoordinates());\n }\n\n if (typeB === 'LineString' || typeB === 'MultiLineString') {\n const flatB = geomB.getFlatCoordinates();\n const stride = geomB.getStride();\n for (let i = 0; i < flatB.length; i += stride) {\n if (geomA.intersectsCoordinate([flatB[i], flatB[i + 1]])) return true;\n }\n return false;\n }\n\n // Fallback: extent overlap is good enough\n return true;\n }\n\n // ============================================================================\n // Parcel Edit Popup (single-click editable form)\n // ============================================================================\n\n /**\n * Create the parcel edit popup overlay with a dynamic form.\n */\n createParcelEditPopup() {\n this.parcelEditElement = document.createElement('div');\n this.parcelEditElement.className = 'map-parcel-edit-popup';\n this.parcelEditElement.style.cssText = `\n position: absolute;\n background: var(--card, #fff);\n color: var(--card-foreground, #1e1a4b);\n border-radius: 10px;\n box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n font-family: var(--font-body, 'Exo', sans-serif);\n font-size: 13px;\n min-width: 280px;\n max-width: 360px;\n max-height: 420px;\n z-index: 1002;\n border: 2px solid var(--primary, #005eb8);\n overflow: hidden;\n display: flex;\n flex-direction: column;\n `;\n\n this.parcelEditPopup = new Overlay({\n element: this.parcelEditElement,\n positioning: 'bottom-center',\n offset: [0, -10],\n stopEvent: true,\n autoPan: true,\n autoPanAnimation: { duration: 250 },\n });\n\n this.map.addOverlay(this.parcelEditPopup);\n\n // Callbacks for save events\n this._parcelEditCallbacks = [];\n // Track the current feature being edited\n this._parcelEditFeature = null;\n }\n\n /**\n * Show the parcel edit popup with an editable form for all feature attributes.\n * Internal keys (_layerType, geometry) are excluded from the form.\n *\n * @param {Feature} feature - The OL feature to edit\n * @param {Array} coordinate - Map coordinate [x, y]\n */\n showParcelEditPopup(feature, coordinate) {\n this._parcelEditFeature = feature;\n const properties = feature.getProperties();\n\n // Keys to skip in the form\n const skipKeys = ['geometry', '_layerType'];\n\n // Build form fields from feature properties\n let fieldsHtml = '';\n for (const [key, value] of Object.entries(properties)) {\n if (skipKeys.includes(key)) continue;\n const displayVal = (value === null || value === undefined) ? '' : String(value);\n const escapedKey = this.escapeHtml(key);\n const escapedVal = this.escapeHtml(displayVal);\n fieldsHtml += `\n
                \n \n \n
                \n `;\n }\n\n const html = `\n
                \n ✏️ Edit Parcel\n \n
                \n
                \n ${fieldsHtml}\n
                \n \n \n
                \n
                \n `;\n\n this.parcelEditElement.innerHTML = html;\n this.parcelEditPopup.setPosition(coordinate);\n\n // Close / Cancel handlers\n this.parcelEditElement.querySelector('.parcel-edit-close').addEventListener('click', () => {\n this.hideParcelEditPopup();\n });\n this.parcelEditElement.querySelector('.parcel-edit-cancel').addEventListener('click', () => {\n this.hideParcelEditPopup();\n });\n\n // Form submit handler\n const form = this.parcelEditElement.querySelector('.parcel-edit-form');\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n\n // Collect all edited values\n const formData = new FormData(form);\n const updatedProps = {};\n for (const [key, value] of formData.entries()) {\n updatedProps[key] = value;\n }\n\n // Restore internal properties that were excluded from the form\n updatedProps._layerType = 'parcel';\n\n // Update the feature's properties in-place\n for (const [key, value] of Object.entries(updatedProps)) {\n this._parcelEditFeature.set(key, value);\n }\n\n // Notify external listeners\n for (const cb of this._parcelEditCallbacks) {\n cb(this._parcelEditFeature, updatedProps);\n }\n\n this.hideParcelEditPopup();\n });\n }\n\n /**\n * Hide the parcel edit popup.\n */\n hideParcelEditPopup() {\n this.parcelEditPopup.setPosition(undefined);\n this._parcelEditFeature = null;\n }\n\n /**\n * Register a callback for when a parcel edit is saved.\n * Callback receives (feature, updatedProperties).\n *\n * @param {Function} callback\n */\n onParcelEdit(callback) {\n this._parcelEditCallbacks.push(callback);\n }\n\n // ============================================================================\n // Merge Identifier (UPN) Chooser Popup\n // ============================================================================\n\n /**\n * Create the merge identifier popup overlay.\n * Shown after two parcels are merged so the user can choose which UPN to keep.\n */\n createMergePopup() {\n this.mergePopupElement = document.createElement('div');\n this.mergePopupElement.className = 'map-merge-popup';\n this.mergePopupElement.style.cssText = `\n position: absolute;\n background: var(--card, #fff);\n color: var(--card-foreground, #1e1a4b);\n border-radius: 10px;\n box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n font-family: var(--font-body, 'Exo', sans-serif);\n font-size: 13px;\n min-width: 280px;\n max-width: 360px;\n z-index: 1002;\n border: 2px solid #10b981;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n `;\n\n this.mergePopup = new Overlay({\n element: this.mergePopupElement,\n positioning: 'bottom-center',\n offset: [0, -10],\n stopEvent: true,\n autoPan: true,\n autoPanAnimation: { duration: 250 },\n });\n\n this.map.addOverlay(this.mergePopup);\n }\n\n /**\n * Show the merge identifier popup so the user can pick which parcel's\n * attributes (including UPN) the merged polygon should inherit.\n *\n * @param {Feature} mergedFeature The newly created merged feature\n * @param {Object} propsA Properties from original parcel A\n * @param {Object} propsB Properties from original parcel B\n * @param {Array} coordinate Map coordinate [x, y] for popup placement\n */\n showMergeIdentifierPopup(mergedFeature, propsA, propsB, coordinate) {\n // Extract identifiers — try common parcel ID field names\n const idFields = ['UPN', 'upn', 'id', 'parcelid', 'parcel_id', 'PARCELID', 'PARCEL_ID', 'ID'];\n const getLabel = (props) => {\n for (const field of idFields) {\n if (props[field] !== undefined && props[field] !== null && String(props[field]).trim()) {\n return { field, value: String(props[field]) };\n }\n }\n return { field: 'id', value: 'Unknown' };\n };\n\n const labelA = getLabel(propsA);\n const labelB = getLabel(propsB);\n\n const html = `\n
                \n 🔗 Merged Parcel — Choose Identifier\n \n
                \n
                \n

                \n Select which parcel's attributes the merged polygon should keep:\n

                \n \n \n
                \n \n \n
                \n
                \n `;\n\n this.mergePopupElement.innerHTML = html;\n this.mergePopup.setPosition(coordinate);\n\n // Close / Cancel — keep parcel A properties (the default from clone)\n const close = () => {\n this.mergePopup.setPosition(undefined);\n };\n this.mergePopupElement.querySelector('.merge-popup-close').addEventListener('click', close);\n this.mergePopupElement.querySelector('.merge-popup-cancel').addEventListener('click', close);\n\n // Confirm — apply chosen parcel's properties\n this.mergePopupElement.querySelector('.merge-popup-confirm').addEventListener('click', () => {\n const choice = this.mergePopupElement.querySelector('input[name=\"merge-choice\"]:checked').value;\n const chosenProps = choice === 'A' ? propsA : propsB;\n\n // Copy all properties (except geometry) onto the merged feature\n const skipKeys = ['geometry'];\n for (const [key, value] of Object.entries(chosenProps)) {\n if (skipKeys.includes(key)) continue;\n mergedFeature.set(key, value);\n }\n // Ensure _layerType is preserved\n mergedFeature.set('_layerType', 'parcel');\n\n // Notify parcel edit callbacks\n for (const cb of this._parcelEditCallbacks) {\n cb(mergedFeature, chosenProps);\n }\n\n close();\n });\n\n // Highlight radio labels on selection\n const labels = this.mergePopupElement.querySelectorAll('label');\n const radios = this.mergePopupElement.querySelectorAll('input[name=\"merge-choice\"]');\n const updateHighlight = () => {\n labels.forEach((lbl) => {\n const radio = lbl.querySelector('input');\n lbl.style.borderColor = radio.checked ? (radio.value === 'A' ? '#0ea5e9' : '#f59e0b') : 'var(--border, #1e1a4b1f)';\n });\n };\n radios.forEach((r) => r.addEventListener('change', updateHighlight));\n updateHighlight();\n }\n\n // ============================================================================\n // Divide Polygon Popup (number input)\n // ============================================================================\n\n /**\n * Create the divide polygon popup overlay.\n * Shown after the user selects a polygon with the Divide tool, so they\n * can enter the number of equal pieces.\n */\n createDividePopup() {\n this.dividePopupElement = document.createElement('div');\n this.dividePopupElement.className = 'map-divide-popup';\n this.dividePopupElement.style.cssText = `\n position: absolute;\n background: var(--card, #fff);\n color: var(--card-foreground, #1e1a4b);\n border-radius: 10px;\n box-shadow: 0 4px 20px rgba(0,0,0,0.3);\n font-family: var(--font-body, 'Exo', sans-serif);\n font-size: 13px;\n min-width: 260px;\n max-width: 320px;\n z-index: 1002;\n border: 2px solid #8b5cf6;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n `;\n\n this.dividePopup = new Overlay({\n element: this.dividePopupElement,\n positioning: 'bottom-center',\n offset: [0, -10],\n stopEvent: true,\n autoPan: true,\n autoPanAnimation: { duration: 250 },\n });\n\n this.map.addOverlay(this.dividePopup);\n }\n\n /**\n * Show the divide popup so the user can enter the number of divisions.\n *\n * @param {Feature} feature The selected polygon feature\n * @param {VectorSource} source The source containing the feature\n * @param {Array} coordinate Map coordinate [x, y] for popup placement\n */\n showDividePopup(feature, source, coordinate) {\n const html = `\n
                \n Divide Polygon\n \n
                \n
                \n

                \n Enter the number of equal pieces:\n

                \n \n
                \n \n \n
                \n
                \n `;\n\n this.dividePopupElement.innerHTML = html;\n this.dividePopup.setPosition(coordinate);\n\n const input = this.dividePopupElement.querySelector('.divide-input');\n input.focus();\n input.select();\n\n // Close / Cancel\n const cancel = () => {\n this.hideDividePopup();\n this._polygonDivideInteraction.cancelDivide();\n };\n this.dividePopupElement.querySelector('.divide-popup-close').addEventListener('click', cancel);\n this.dividePopupElement.querySelector('.divide-popup-cancel').addEventListener('click', cancel);\n\n // Confirm\n this.dividePopupElement.querySelector('.divide-popup-confirm').addEventListener('click', () => {\n const n = parseInt(input.value, 10);\n if (!n || n < 2) {\n input.style.borderColor = '#ef4444';\n return;\n }\n this.hideDividePopup();\n this._polygonDivideInteraction.performDivide(n);\n });\n\n // Allow Enter key to confirm\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n this.dividePopupElement.querySelector('.divide-popup-confirm').click();\n }\n });\n }\n\n /**\n * Hide the divide popup.\n */\n hideDividePopup() {\n this.dividePopup.setPosition(undefined);\n }\n\n // ============================================================================\n // Drawn Polygon Attribute Popup\n // ============================================================================\n\n /**\n * Create the drawn polygon attribute popup overlay.\n * Shown after the area measurement polygon is completed so the user can\n * attach parcel-like attributes to the drawn polygon.\n */\n createDrawnPolygonPopup() {\n this.drawnPolygonElement = document.createElement('div');\n this.drawnPolygonElement.className = 'map-drawn-polygon-popup';\n this.drawnPolygonElement.style.cssText = `\n position: absolute;\n background: var(--card, #fff);\n border-radius: var(--radius-xl, 0.75rem);\n box-shadow: 0 4px 20px rgba(0,0,0,0.2);\n font-family: var(--font-body, 'Exo', sans-serif);\n font-size: 13px;\n min-width: 280px;\n max-width: 360px;\n max-height: 420px;\n z-index: 1002;\n border: 2px solid var(--success, #006b3f);\n overflow: hidden;\n display: flex;\n flex-direction: column;\n `;\n\n this.drawnPolygonPopup = new Overlay({\n element: this.drawnPolygonElement,\n positioning: 'bottom-center',\n offset: [0, -10],\n stopEvent: true,\n autoPan: true,\n autoPanAnimation: { duration: 250 },\n });\n\n this.map.addOverlay(this.drawnPolygonPopup);\n this._drawnPolygonCallbacks = [];\n this._drawnPolygonFeature = null;\n }\n\n /**\n * Get attribute keys from existing parcel features on the map.\n * Scans the overlay group for the first feature with _layerType='parcel'\n * and returns its property key names (excluding internal keys).\n *\n * @returns {string[]} Array of attribute key names\n */\n getParcelAttributeKeys() {\n const skipKeys = ['geometry', '_layerType'];\n const keys = [];\n\n const scanGroup = (group) => {\n if (keys.length > 0) return;\n group.getLayers().forEach((layer) => {\n if (keys.length > 0) return;\n if (layer instanceof LayerGroup) {\n scanGroup(layer);\n } else if (layer instanceof VectorLayer) {\n const source = layer.getSource();\n if (!source) return;\n for (const f of source.getFeatures()) {\n if (f.get('_layerType') !== 'parcel') continue;\n const props = f.getProperties();\n for (const key of Object.keys(props)) {\n if (!skipKeys.includes(key)) keys.push(key);\n }\n return; // one parcel is enough for the schema\n }\n }\n });\n };\n\n scanGroup(this.overlayGroup);\n return keys;\n }\n\n /**\n * Show the drawn polygon attribute popup.\n * Discovers attribute keys from existing parcel features and creates\n * a blank form with those fields.\n *\n * @param {Feature} feature - The drawn polygon feature\n * @param {Array} coordinate - Map coordinate [x, y] for popup placement\n */\n showDrawnPolygonPopup(feature, coordinate) {\n this._drawnPolygonFeature = feature;\n\n // Discover attribute keys from existing parcels\n const attributeKeys = this.getParcelAttributeKeys();\n\n if (attributeKeys.length === 0) {\n console.warn('[MapView] No parcel attributes found — cannot build form');\n return;\n }\n\n // Build form fields (all blank)\n let fieldsHtml = '';\n for (const key of attributeKeys) {\n const escapedKey = this.escapeHtml(key);\n fieldsHtml += `\n
                \n \n \n
                \n `;\n }\n\n // Area display\n const geom = feature.getGeometry();\n const areaSqm = getArea(geom, { projection: 'EPSG:3857' });\n const areaFormatted = formatArea(areaSqm);\n\n const html = `\n
                \n 📐 Polygon Attributes\n \n
                \n
                \n Area: ${areaFormatted}\n
                \n
                \n ${fieldsHtml}\n
                \n \n \n
                \n
                \n `;\n\n this.drawnPolygonElement.innerHTML = html;\n this.drawnPolygonPopup.setPosition(coordinate);\n\n // Close / Cancel handlers\n this.drawnPolygonElement.querySelector('.drawn-polygon-close').addEventListener('click', () => {\n this.hideDrawnPolygonPopup();\n });\n this.drawnPolygonElement.querySelector('.drawn-polygon-cancel').addEventListener('click', () => {\n this.hideDrawnPolygonPopup();\n });\n\n // Form submit handler\n const form = this.drawnPolygonElement.querySelector('.drawn-polygon-form');\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n\n const formData = new FormData(form);\n const props = {};\n for (const [key, value] of formData.entries()) {\n props[key] = value;\n }\n\n // Set properties on the feature\n for (const [key, value] of Object.entries(props)) {\n this._drawnPolygonFeature.set(key, value);\n }\n\n // Tag as parcel so it integrates with existing parcel tools\n this._drawnPolygonFeature.set('_layerType', 'parcel');\n\n // Notify listeners\n for (const cb of this._drawnPolygonCallbacks) {\n cb(this._drawnPolygonFeature, props);\n }\n\n this.hideDrawnPolygonPopup();\n });\n }\n\n /**\n * Hide the drawn polygon attribute popup.\n */\n hideDrawnPolygonPopup() {\n this.drawnPolygonPopup.setPosition(undefined);\n this._drawnPolygonFeature = null;\n }\n\n /**\n * Register a callback for when drawn polygon attributes are saved.\n * Callback receives (feature, properties).\n *\n * @param {Function} callback\n */\n onDrawnPolygonSave(callback) {\n this._drawnPolygonCallbacks.push(callback);\n }\n\n /**\n * Register a double-click callback.\n * Callback receives (lon, lat, feature, event).\n * Feature is the first feature found at the click pixel across all overlay layers,\n * or null if no feature was hit.\n * When a feature is hit, the default double-click-zoom is suppressed.\n */\n onDblClick(callback) {\n this.dblClickCallbacks.push(callback);\n\n // Set up the listener once\n if (this.dblClickCallbacks.length === 1) {\n this.map.on('dblclick', (evt) => {\n const [lon, lat] = toLonLat(evt.coordinate);\n\n // Find any feature at the clicked pixel (overlay layers, not just markers)\n let clickedFeature = null;\n this.map.forEachFeatureAtPixel(evt.pixel, (feature) => {\n clickedFeature = feature;\n return true; // stop at first hit\n });\n\n // If a feature was hit, prevent the default double-click zoom\n if (clickedFeature) {\n evt.preventDefault();\n evt.stopPropagation();\n }\n\n // Call all registered callbacks\n for (const cb of this.dblClickCallbacks) {\n cb(lon, lat, clickedFeature, evt);\n }\n\n // Return false to suppress DoubleClickZoom interaction when on a feature\n if (clickedFeature) return false;\n });\n }\n\n return () => {\n const idx = this.dblClickCallbacks.indexOf(callback);\n if (idx > -1) this.dblClickCallbacks.splice(idx, 1);\n };\n }\n\n /**\n * Escape HTML to prevent XSS\n */\n escapeHtml(text) {\n if (!text) return '';\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n /**\n * Create the Add Location popup form overlay\n */\n createAddLocationPopup() {\n // Create popup container element\n this.addLocationPopupElement = document.createElement('div');\n this.addLocationPopupElement.className = 'map-add-location-popup';\n this.addLocationPopupElement.innerHTML = `\n
                \n ➕ Add Location\n \n
                \n
                \n
                \n \n \n
                \n
                \n \n \n
                \n
                \n \n \n
                \n
                \n 📍 \n
                \n \n
                \n `;\n\n // Create the overlay\n this.addLocationPopup = new Overlay({\n element: this.addLocationPopupElement,\n positioning: 'bottom-center',\n offset: [0, -10],\n stopEvent: true, // Prevent click from propagating\n autoPan: true,\n autoPanAnimation: {\n duration: 250,\n },\n });\n\n this.map.addOverlay(this.addLocationPopup);\n\n // Store clicked coordinates\n this.addLocationCoords = null;\n\n // Set up close button handler\n const closeBtn = this.addLocationPopupElement.querySelector('.add-location-popup-close');\n closeBtn.addEventListener('click', () => {\n this.hideAddLocationPopup();\n });\n\n // Store form submit callbacks\n this.addLocationCallbacks = [];\n }\n\n /**\n * Show the Add Location popup at the specified coordinate\n */\n showAddLocationPopup(coordinate) {\n const [lon, lat] = toLonLat(coordinate);\n this.addLocationCoords = { lon, lat };\n\n // Update coordinates display\n const coordsEl = this.addLocationPopupElement.querySelector('#map-location-coords');\n coordsEl.textContent = `${lon.toFixed(6)}, ${lat.toFixed(6)}`;\n\n // Reset form\n const form = this.addLocationPopupElement.querySelector('#map-add-location-form');\n form.reset();\n\n // Position and show popup\n this.addLocationPopup.setPosition(coordinate);\n }\n\n /**\n * Hide the Add Location popup\n */\n hideAddLocationPopup() {\n this.addLocationPopup.setPosition(undefined);\n this.addLocationCoords = null;\n }\n\n /**\n * Register a callback for when a location is submitted via the map popup\n * Callback receives: { name, category, description, lon, lat }\n */\n onAddLocation(callback) {\n this.addLocationCallbacks.push(callback);\n\n // Set up form submit handler (only once)\n if (this.addLocationCallbacks.length === 1) {\n const form = this.addLocationPopupElement.querySelector('#map-add-location-form');\n form.addEventListener('submit', (e) => {\n e.preventDefault();\n\n if (!this.addLocationCoords) return;\n\n const formData = new FormData(form);\n const data = {\n name: formData.get('name'),\n category: formData.get('category'),\n description: formData.get('description'),\n lon: this.addLocationCoords.lon,\n lat: this.addLocationCoords.lat,\n };\n\n // Call all registered callbacks\n this.addLocationCallbacks.forEach(cb => cb(data));\n\n // Hide popup after submission\n this.hideAddLocationPopup();\n });\n }\n }\n\n /**\n * Create base layers group for LayerSwitcher\n */\n createBaseLayers(defaultBasemap) {\n\n\n const topoLayer = new TileLayer({\n title: 'Topographic',\n type: 'base',\n zIndex: -100,\n visible: defaultBasemap === 'topo',\n source: new XYZ({\n url: 'https://{a-c}.tile.opentopomap.org/{z}/{x}/{y}.png',\n attributions: 'Map data: © OpenTopoMap',\n maxZoom: 17,\n crossOrigin: 'anonymous',\n }),\n });\n topoLayer.set('basemapKey', 'topo');\n\n const cartoLightLayer = new TileLayer({\n title: 'Carto Light',\n type: 'base',\n zIndex: -100,\n visible: defaultBasemap === 'carto-light',\n source: new XYZ({\n url: 'https://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',\n attributions: '© CARTO',\n maxZoom: 19,\n crossOrigin: 'anonymous',\n }),\n });\n cartoLightLayer.set('basemapKey', 'carto-light');\n\n const cartoDarkLayer = new TileLayer({\n title: 'Carto Dark',\n type: 'base',\n zIndex: -100,\n visible: defaultBasemap === 'carto-dark',\n source: new XYZ({\n url: 'https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png',\n attributions: '© CARTO',\n maxZoom: 19,\n crossOrigin: 'anonymous',\n }),\n });\n cartoDarkLayer.set('basemapKey', 'carto-dark');\n\n const osmCycleLayer = new TileLayer({\n title: 'OSM Cycle map',\n type: 'base',\n zIndex: -100,\n visible: false, //defaultBasemap === 'osm',\n source: new OSM({\n \t\t\t\t\t\"url\" : \"https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=ae1339c46dd3446b9c491e7336d38760\"\n\t\t\t\t\t\t}),\n });\n\n osmCycleLayer.set('basemapKey', 'cycle');\n\n const satelliteLayer = new TileLayer({\n title: 'Satellite',\n type: 'base',\n zIndex: -100,\n visible: defaultBasemap === 'satellite',\n source: new XYZ({\n url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',\n attributions: 'Tiles © Esri',\n maxZoom: 19,\n crossOrigin: 'anonymous',\n }),\n });\n satelliteLayer.set('basemapKey', 'satellite');\n const googleLayer = new TileLayer({\n title: 'Google Sat',\n type: 'base',\n zIndex: -100,\n visible: defaultBasemap === 'googlesat',\n source: new XYZ({\n// url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',\n url: 'http://mt0.google.com/vt/lyrs=y&hl=en&x={x}&y={y}&z={z}&s=Ga',\n attributions: 'Tiles © Google',\n maxZoom: 19,\n crossOrigin: 'anonymous',\n }),\n });\n googleLayer.set('basemapKey', 'googlesat');\n\n const osmLayer = new TileLayer({\n title: 'OpenStreetMap',\n type: 'base',\n zIndex: -100,\n visible: defaultBasemap === 'osm',\n source: new OSM(),\n });\n osmLayer.set('basemapKey', 'osm');\n\n // Remember the base-map layers so setBaseMap() can toggle visibility later\n this._baseMapLayers = [\n cartoLightLayer, cartoDarkLayer, osmCycleLayer,\n satelliteLayer, googleLayer, osmLayer, topoLayer,\n ];\n\n\n // Return LayerGroup. Hidden from the main LayerSwitcher — base maps are\n // managed by the dedicated base-map picker (see _createBaseMapPicker)\n // accessed via the layers-stack icon above the My Location button.\n const baseGroup = new LayerGroup({\n title: 'Base Maps',\n layers: [\n cartoLightLayer,\n cartoDarkLayer,\n satelliteLayer,\n osmCycleLayer,\n googleLayer,\n osmLayer,\n topoLayer,\n ],\n });\n baseGroup.set('displayInLayerSwitcher', false);\n return baseGroup;\n }\n\n /**\n * Switch the active base map by key.\n * Sets exactly one base layer visible; hides all others.\n *\n * @param {string} key Basemap key: 'none' | 'topo' | 'osm' | 'satellite' | 'googlesat' | 'carto-light' | 'carto-dark' | 'cycle'\n * @returns {boolean} true if the key matched a known base layer (or 'none')\n */\n setBaseMap(key) {\n if (!this._baseMapLayers) return false;\n // 'none' switches the base map off entirely — hide every base layer so the\n // map renders on a blank background (useful over imagery overlays / when a\n // full-coverage overlay should stand alone).\n if (key === 'none') {\n for (const layer of this._baseMapLayers) layer.setVisible(false);\n console.log('[MapView] Base map switched off (none)');\n this.map.dispatchEvent({ type: 'basemapchange', key: 'none' });\n return true;\n }\n let matched = false;\n for (const layer of this._baseMapLayers) {\n const on = layer.get('basemapKey') === key;\n layer.setVisible(on);\n if (on) matched = true;\n }\n if (matched) {\n console.log('[MapView] Base map switched to:', key);\n // Notify external UIs (Settings dropdown, base-map picker, …) so they\n // can keep their visible state in sync.\n this.map.dispatchEvent({ type: 'basemapchange', key });\n }\n return matched;\n }\n\n /**\n * Build the floating \"Base Map\" picker — a small icon button stacked\n * directly above the My Location control, plus a slide-out card with\n * thumbnail chips for every selectable base map.\n *\n * Hidden in tandem with the main LayerSwitcher: clicking outside the\n * picker (or making a selection) closes it.\n *\n * Two-way sync with the existing Settings dropdown is via the\n * `basemapchange` event fired from setBaseMap().\n */\n _createBaseMapPicker() {\n // Configuration — must match the basemapKey set in createBaseLayers.\n // The colour gradients hint at each base map's character so the chip is\n // recognisable without rendering an actual tile preview.\n const OPTIONS = [\n { key: 'topo', label: 'Topographic', grad: 'linear-gradient(135deg,#e8d5b7,#a67c52)' },\n { key: 'osm', label: 'OpenStreetMap',grad: 'linear-gradient(135deg,#d4e6f1,#85c1e9)' },\n { key: 'satellite', label: 'Satellite', grad: 'linear-gradient(135deg,#1b4332,#40916c)' },\n { key: 'googlesat', label: 'Google Sat', grad: 'linear-gradient(135deg,#2a5d3d,#4a8c5a)' },\n { key: 'carto-light', label: 'Carto Light', grad: 'linear-gradient(135deg,#f5f5f5,#d4d4d4)' },\n { key: 'carto-dark', label: 'Carto Dark', grad: 'linear-gradient(135deg,#1a1a2e,#0f3460)' },\n // \"None\" turns the base map off — checkerboard hints at a blank/transparent background.\n { key: 'none', label: 'None', grad: 'repeating-conic-gradient(#e5e7eb 0 25%, #fff 0 50%) 50% / 12px 12px' },\n ];\n\n const target = this.map.getTargetElement();\n if (!target) return;\n\n // ---------- Toggle button ----------\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'ls-basemap-toggle';\n btn.title = 'Switch base map';\n btn.setAttribute('aria-label', 'Switch base map');\n btn.innerHTML =\n '' +\n '' +\n '' +\n '' +\n '';\n target.appendChild(btn);\n\n // ---------- Picker panel ----------\n const panel = document.createElement('div');\n panel.className = 'ls-basemap-panel';\n panel.innerHTML =\n '
                Base Map
                ' +\n '
                ' +\n OPTIONS.map((opt) => `\n \n `).join('') +\n '
                ';\n target.appendChild(panel);\n\n this._basemapPanel = panel;\n this._basemapToggle = btn;\n\n /** Mark the radio matching the currently-visible base layer. */\n const syncSelection = (key) => {\n const k = key || this._baseMapLayers?.find((l) => l.getVisible())?.get('basemapKey');\n panel.querySelectorAll('input[name=\"lupmis-basemap\"]').forEach((r) => {\n r.checked = (r.value === k);\n });\n };\n syncSelection();\n\n // ---------- Events ----------\n\n // Toggle button → open / close the panel\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n const open = !panel.classList.contains('open');\n panel.classList.toggle('open', open);\n btn.classList.toggle('active', open);\n if (open) syncSelection();\n });\n\n // Click outside → close\n document.addEventListener('click', (e) => {\n if (!panel.classList.contains('open')) return;\n if (panel.contains(e.target) || btn.contains(e.target)) return;\n panel.classList.remove('open');\n btn.classList.remove('active');\n });\n\n // Selection → apply, persist, close\n panel.addEventListener('change', (e) => {\n const radio = e.target.closest('input[type=radio][name=\"lupmis-basemap\"]');\n if (!radio) return;\n const key = radio.value;\n this.setBaseMap(key);\n try { localStorage.setItem('default-basemap', key); } catch {}\n panel.classList.remove('open');\n btn.classList.remove('active');\n });\n\n // Keep the radio state synced when other UIs (Settings dropdown) change it\n this.map.on('basemapchange', (evt) => syncSelection(evt.key));\n }\n\n // ============================================================================\n // GPS: current-position + trail rendering, and the expandable Location control\n //\n // NOTE: MapView deliberately knows nothing about the GeoTracker engine,\n // SQLocal, or sync. It only (a) renders what it's told and (b) emits UI\n // intents via callbacks. main.js wires those intents to the GeoTracker so the\n // map stays reusable/decoupled.\n // ============================================================================\n\n /** Create the vector layers used to draw the live position and the trail. */\n _initGpsRendering() {\n this._gpsPositionSource = new VectorSource();\n this._gpsTrailSource = new VectorSource();\n this._gpsTrailCoords = []; // [ [x,y], ... ] in map projection\n\n // Trail line (drawn under the position marker)\n this._gpsTrailLayer = new VectorLayer({\n source: this._gpsTrailSource,\n zIndex: 940,\n style: new Style({\n stroke: new Stroke({ color: '#ff6d00', width: 4, lineCap: 'round', lineJoin: 'round' }),\n }),\n properties: { title: 'GPS Trail', displayInLayerSwitcher: false },\n });\n\n // Current position: accuracy halo + solid dot\n this._gpsPositionLayer = new VectorLayer({\n source: this._gpsPositionSource,\n zIndex: 950,\n style: (feature) => {\n if (feature.get('_kind') === 'accuracy') {\n return new Style({\n fill: new Fill({ color: 'rgba(0,94,184,0.12)' }),\n stroke: new Stroke({ color: 'rgba(0,94,184,0.35)', width: 1 }),\n });\n }\n return new Style({\n image: new Circle({\n radius: 7,\n fill: new Fill({ color: '#005eb8' }),\n stroke: new Stroke({ color: '#ffffff', width: 2.5 }),\n }),\n });\n },\n properties: { title: 'GPS Position', displayInLayerSwitcher: false },\n });\n\n this.map.addLayer(this._gpsTrailLayer);\n this.map.addLayer(this._gpsPositionLayer);\n\n this._gpsCallbacks = { locate: [], record: [] };\n this._gpsRecording = false;\n }\n\n /** Register a callback fired when the user taps \"Locate Me\". */\n onLocateMe(cb) { this._gpsCallbacks.locate.push(cb); }\n /** Register a callback fired when the user toggles trail recording. Receives the desired state (true=start). */\n onToggleRecording(cb) { this._gpsCallbacks.record.push(cb); }\n\n /**\n * Draw / move the current-position marker and accuracy halo.\n * @param {number} lon\n * @param {number} lat\n * @param {number|null} [accuracy] horizontal accuracy in metres\n */\n showCurrentPosition(lon, lat, accuracy = null) {\n if (lon == null || lat == null) return;\n const center = fromLonLat([lon, lat]);\n this._gpsPositionSource.clear();\n\n if (accuracy && accuracy > 0) {\n // Approximate the accuracy circle in projected units. Good enough for a\n // visual halo at typical zoom levels.\n const resAtLat = accuracy / Math.cos((lat * Math.PI) / 180);\n const halo = new Feature({ geometry: new PolygonGeom([this._circleRing(center, resAtLat)]) });\n halo.set('_kind', 'accuracy');\n this._gpsPositionSource.addFeature(halo);\n }\n const dot = new Feature({ geometry: new Point(center) });\n dot.set('_kind', 'dot');\n this._gpsPositionSource.addFeature(dot);\n }\n\n /** @private build a ring of coordinates approximating a circle (metres → projected). */\n _circleRing(center, radiusMeters, segments = 48) {\n // Convert metres to projected units (Web Mercator) roughly via the\n // resolution at the centre latitude.\n const ring = [];\n const metersPerUnit = 1; // EPSG:3857 units are metres (approx near equator/locally)\n const r = radiusMeters / metersPerUnit;\n for (let i = 0; i <= segments; i++) {\n const a = (i / segments) * 2 * Math.PI;\n ring.push([center[0] + r * Math.cos(a), center[1] + r * Math.sin(a)]);\n }\n return ring;\n }\n\n /** Smoothly center the view on a coordinate. */\n centerOn(lon, lat, zoom = 16) {\n const view = this.map.getView();\n view.animate({ center: fromLonLat([lon, lat]), zoom, duration: 500 });\n }\n\n /** Reset the trail line (call when a new recording starts). */\n startTrailRender() {\n this._gpsTrailCoords = [];\n this._gpsTrailSource.clear();\n }\n\n /** Append a coordinate to the growing trail line. */\n appendTrailPoint(lon, lat) {\n if (lon == null || lat == null) return;\n this._gpsTrailCoords.push(fromLonLat([lon, lat]));\n this._gpsTrailSource.clear();\n if (this._gpsTrailCoords.length >= 2) {\n this._gpsTrailSource.addFeature(new Feature({ geometry: new LineString(this._gpsTrailCoords) }));\n }\n }\n\n /** Remove the rendered trail (does not affect stored data). */\n clearTrailRender() {\n this._gpsTrailCoords = [];\n this._gpsTrailSource.clear();\n }\n\n /** Reflect recording state on the control button. */\n setRecordingState(active) {\n this._gpsRecording = !!active;\n if (this._recordBtn) {\n this._recordBtn.classList.toggle('recording', this._gpsRecording);\n this._recordBtn.title = this._gpsRecording ? 'Stop trail recording' : 'Record GPS trail';\n this._recordBtn.innerHTML = this._gpsRecording\n ? ''\n : '';\n }\n if (this._locateToggle) this._locateToggle.classList.toggle('recording', this._gpsRecording);\n }\n\n /**\n * Build the expandable \"My Location\" control: a main button that reveals two\n * sub-buttons (Locate Me, Record Trail). Anchored at the same spot the old\n * ol-ext GeolocationButton occupied (bottom-right), so the base-map picker\n * still lines up above it.\n */\n _createLocationControl() {\n const target = this.map.getTargetElement();\n if (!target) return;\n\n // Main toggle\n const toggle = document.createElement('button');\n toggle.type = 'button';\n toggle.className = 'ls-locate-toggle';\n toggle.title = 'My Location';\n toggle.setAttribute('aria-label', 'My Location');\n toggle.innerHTML = '';\n target.appendChild(toggle);\n\n // Sub-button cluster (hidden until the main button is tapped)\n const actions = document.createElement('div');\n actions.className = 'ls-locate-actions';\n actions.innerHTML =\n '' +\n '';\n target.appendChild(actions);\n\n this._locateToggle = toggle;\n this._locateActions = actions;\n this._locateMeBtn = actions.querySelector('.ls-locate-me');\n this._recordBtn = actions.querySelector('.ls-locate-record');\n\n const close = () => { actions.classList.remove('open'); toggle.classList.remove('active'); };\n const open = () => { actions.classList.add('open'); toggle.classList.add('active'); };\n\n toggle.addEventListener('click', (e) => {\n e.stopPropagation();\n actions.classList.contains('open') ? close() : open();\n });\n\n // Tap outside closes the cluster (but never while recording, so the stop\n // button stays reachable).\n document.addEventListener('click', (e) => {\n if (!actions.classList.contains('open')) return;\n if (actions.contains(e.target) || toggle.contains(e.target)) return;\n if (this._gpsRecording) return;\n close();\n });\n\n this._locateMeBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n for (const cb of this._gpsCallbacks.locate) { try { cb(); } catch (err) { console.error(err); } }\n if (!this._gpsRecording) close();\n });\n\n this._recordBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n const next = !this._gpsRecording;\n for (const cb of this._gpsCallbacks.record) { try { cb(next); } catch (err) { console.error(err); } }\n });\n }\n\n /**\n * Get style for a feature (handles selection state)\n */\n getFeatureStyle(feature) {\n const category = feature.get('category') || 'default';\n const emoji = this.getEmoji(category);\n\n if (feature === this.selectedFeature) {\n // Return selected style with the correct emoji and highlight\n return [\n // Background highlight circle\n new Style({\n image: new Circle({\n radius: 22,\n fill: new Fill({ color: 'rgba(220, 38, 38, 0.25)' }),\n stroke: new Stroke({ color: '#dc2626', width: 3 }),\n }),\n }),\n // Emoji on top, larger\n new Style({\n text: new Text({\n text: emoji,\n font: '40px sans-serif',\n textBaseline: 'bottom',\n textAlign: 'center',\n offsetY: -5,\n }),\n }),\n ];\n }\n\n // Check for custom style\n const customStyle = feature.get('style');\n if (customStyle) {\n return customStyle;\n }\n\n // Return category-based emoji style\n if (this.categoryStyles[category]) {\n return this.categoryStyles[category];\n }\n\n return this.defaultStyle;\n }\n\n /**\n * Set category-based styles with emojis\n * @param {Object} styles - Map of category to config { emoji, label, fontSize }\n */\n setCategoryStyles(styles) {\n for (const [category, config] of Object.entries(styles)) {\n // Update category mapping if provided\n if (config.emoji) {\n if (!this.categoryEmojis[category]) {\n this.categoryEmojis[category] = { emoji: config.emoji, label: config.label || category };\n } else {\n this.categoryEmojis[category].emoji = config.emoji;\n if (config.label) {\n this.categoryEmojis[category].label = config.label;\n }\n }\n }\n\n // Create/update style\n const emoji = this.getEmoji(category);\n const fontSize = config.fontSize || 28;\n\n this.categoryStyles[category] = this.createEmojiStyle(emoji, fontSize);\n }\n\n // Refresh markers\n this.markerSource.changed();\n }\n\n /**\n * Add a single marker\n */\n addMarker(lon, lat, properties = {}) {\n console.log('[MapView] Adding marker at', lon, lat, 'with properties:', properties);\n\n const feature = new Feature({\n geometry: new Point(fromLonLat([lon, lat])),\n ...properties,\n });\n\n // Store original coordinates for easy access\n feature.set('lon', lon);\n feature.set('lat', lat);\n\n this.markerSource.addFeature(feature);\n console.log('[MapView] Marker added, total features:', this.markerSource.getFeatures().length);\n return feature;\n }\n\n /**\n * Add multiple markers from an array of location objects\n */\n addMarkers(locations) {\n console.log('[MapView] Adding', locations.length, 'markers');\n\n const features = locations.map((loc) => {\n const feature = new Feature({\n geometry: new Point(fromLonLat([loc.longitude, loc.latitude])),\n id: loc.id,\n name: loc.name,\n description: loc.description,\n category: loc.category,\n lon: loc.longitude,\n lat: loc.latitude,\n });\n return feature;\n });\n\n this.markerSource.addFeatures(features);\n console.log('[MapView] Markers added, total features:', this.markerSource.getFeatures().length);\n return features;\n }\n\n /**\n * Clear all markers\n */\n clearMarkers() {\n this.markerSource.clear();\n this.selectedFeature = null;\n }\n\n /**\n * Remove a specific marker by feature or ID\n */\n removeMarker(featureOrId) {\n if (typeof featureOrId === 'object') {\n this.markerSource.removeFeature(featureOrId);\n } else {\n const feature = this.markerSource.getFeatures().find(\n f => f.get('id') === featureOrId\n );\n if (feature) {\n this.markerSource.removeFeature(feature);\n }\n }\n }\n\n /**\n * Get all markers\n */\n getMarkers() {\n return this.markerSource.getFeatures();\n }\n\n /**\n * Find marker by ID\n */\n findMarker(id) {\n return this.markerSource.getFeatures().find(f => f.get('id') === id);\n }\n\n /**\n * Select a marker (highlights it)\n */\n selectMarker(featureOrId) {\n if (typeof featureOrId === 'object') {\n this.selectedFeature = featureOrId;\n } else {\n this.selectedFeature = this.findMarker(featureOrId);\n }\n this.markerSource.changed();\n return this.selectedFeature;\n }\n\n /**\n * Clear selection\n */\n clearSelection() {\n this.selectedFeature = null;\n this.markerSource.changed();\n }\n\n /**\n * Zoom to a specific location\n */\n zoomTo(lon, lat, zoom = 15) {\n this.map.getView().animate({\n center: fromLonLat([lon, lat]),\n zoom: zoom,\n duration: 500,\n });\n }\n\n /**\n * Fit view to show all markers\n */\n fitToMarkers(padding = 50) {\n const extent = this.markerSource.getExtent();\n if (extent && extent[0] !== Infinity) {\n this.map.getView().fit(extent, {\n padding: [padding, padding, padding, padding],\n duration: 500,\n maxZoom: 16,\n });\n }\n }\n\n /**\n * Get current map center in lon/lat\n */\n getCenter() {\n const center = this.map.getView().getCenter();\n return toLonLat(center);\n }\n\n /**\n * Get current zoom level\n */\n getZoom() {\n return this.map.getView().getZoom();\n }\n\n /**\n * Set map center\n */\n setCenter(lon, lat) {\n this.map.getView().setCenter(fromLonLat([lon, lat]));\n }\n\n /**\n * Set zoom level\n */\n setZoom(zoom) {\n this.map.getView().setZoom(zoom);\n }\n\n /**\n * Register click callback\n * Callback receives (lon, lat, feature, event)\n *\n * Single-click is delayed by 300 ms so that a double-click can cancel it.\n * If the click lands on an overlay feature (e.g. district boundary) the\n * single-click is suppressed entirely — only double-click will fire.\n */\n onClick(callback) {\n this.clickCallbacks.push(callback);\n\n // Set up click handler if this is the first callback\n if (this.clickCallbacks.length === 1) {\n this._clickTimer = null;\n\n // Double-click cancels any pending single-click\n this.map.on('dblclick', () => {\n if (this._clickTimer) {\n clearTimeout(this._clickTimer);\n this._clickTimer = null;\n }\n });\n\n this.map.on('click', (evt) => {\n // Cancel any previous pending click\n if (this._clickTimer) {\n clearTimeout(this._clickTimer);\n this._clickTimer = null;\n }\n\n // When NOT in edit / draw mode, immediately clear any feature\n // the Select interaction may have grabbed on this click so the\n // user never sees a selection flash.\n if (!this._editBarActive && this._selectInteraction) {\n this._selectInteraction.getFeatures().clear();\n }\n\n // Check what features sit under the click pixel\n let hasOverlayFeature = false;\n let hasParcelFeature = false;\n let markerFeature = null;\n this.map.forEachFeatureAtPixel(evt.pixel, (feature) => {\n if (feature.get('_layerType') === 'parcel') {\n hasParcelFeature = true;\n }\n if (feature.get('name')) {\n markerFeature = feature;\n }\n hasOverlayFeature = true;\n });\n\n // If an overlay feature was hit, suppress single-click\n // UNLESS it's a parcel or a location marker\n if (hasOverlayFeature && !hasParcelFeature && !markerFeature) {\n return;\n }\n\n // Delay the single-click to allow double-click to cancel it\n const [lon, lat] = toLonLat(evt.coordinate);\n this._clickTimer = setTimeout(() => {\n this._clickTimer = null;\n\n // Find location marker at pixel\n let clickedFeature = null;\n this.map.forEachFeatureAtPixel(evt.pixel, (feature) => {\n if (feature.get('name')) {\n clickedFeature = feature;\n return true;\n }\n });\n\n for (const cb of this.clickCallbacks) {\n cb(lon, lat, clickedFeature, evt);\n }\n }, 300);\n });\n }\n\n // Return unsubscribe function\n return () => {\n const index = this.clickCallbacks.indexOf(callback);\n if (index > -1) {\n this.clickCallbacks.splice(index, 1);\n }\n };\n }\n\n /**\n * Register pointer move callback (for hover effects)\n */\n onPointerMove(callback) {\n this.map.on('pointermove', (evt) => {\n if (evt.dragging) return;\n\n const [lon, lat] = toLonLat(evt.coordinate);\n\n // Only find location markers (features with 'name' property)\n let hoveredFeature = null;\n this.map.forEachFeatureAtPixel(evt.pixel, (feature) => {\n if (feature.get('name')) {\n hoveredFeature = feature;\n return true;\n }\n });\n\n // Change cursor\n this.map.getTargetElement().style.cursor = hoveredFeature ? 'pointer' : '';\n\n callback(lon, lat, hoveredFeature, evt);\n });\n }\n\n /**\n * Enable cursor change on marker hover\n * Note: This is now handled automatically by the popup system\n */\n enableHoverCursor() {\n // Cursor changes are now handled by setupHoverPopup()\n // This method is kept for backwards compatibility\n }\n\n /**\n * Add a GeoJSON layer (visible in LayerSwitcher).\n * By default the layer is added to the root overlay group.\n * Pass a targetGroup (LayerGroup) to nest it inside a specific group.\n *\n * @param {Object} geojson - GeoJSON FeatureCollection or Feature\n * @param {string} title - Layer title for the LayerSwitcher\n * @param {Object} [styleOptions] - Optional style configuration\n * @param {string} [styleOptions.strokeColor='#3b82f6'] - Stroke color\n * @param {number} [styleOptions.strokeWidth=2] - Stroke width\n * @param {string} [styleOptions.fillColor='rgba(59,130,246,0.1)'] - Fill color\n * @param {LayerGroup} [targetGroup] - Optional group to add the layer to\n * @returns {VectorLayer} The created layer\n */\n addGeoJSONLayer(geojson, title, styleOptions = {}, targetGroup = null) {\n const {\n strokeColor = '#3b82f6',\n strokeWidth = 2,\n fillColor = 'rgba(59,130,246,0.1)',\n // Optional line \"casing\": a thicker darker stroke drawn UNDERNEATH the\n // main stroke. Used for road-like layers to make light-colored lines\n // visible on any base map. Set lineCasingColor to enable; the casing\n // width defaults to strokeWidth + 2.\n lineCasingColor = null,\n lineCasingWidth = null,\n pointRadius = 5,\n pointFillColor = null, // defaults to strokeColor\n pointStrokeColor = '#ffffff',\n pointStrokeWidth = 1.5,\n } = styleOptions;\n\n const source = new VectorSource({\n features: new GeoJSON().readFeatures(geojson, {\n featureProjection: 'EPSG:3857',\n }),\n });\n\n // Build per-geometry styles. OpenLayers picks `image` for Point /\n // MultiPoint, `stroke`+`fill` for Polygon / MultiPolygon, and `stroke`\n // alone for LineString / MultiLineString. Putting all three on a single\n // Style is enough — but a Style with only stroke+fill leaves Points\n // invisible, which is what was happening on shapefile import.\n const fillStyle = new Fill({ color: fillColor });\n const pointStyle = new Circle({\n radius: pointRadius,\n fill: new Fill({ color: pointFillColor || strokeColor }),\n stroke: new Stroke({ color: pointStrokeColor, width: pointStrokeWidth }),\n });\n\n // If a line casing is requested, return an array of two Styles per\n // feature: the casing renders first (underneath), then the inner stroke.\n // For polygons the casing also outlines them; for points the casing has\n // no effect (Point geometries only render `image`).\n let layerStyle;\n if (lineCasingColor) {\n const casingW = lineCasingWidth != null ? lineCasingWidth : strokeWidth + 2;\n layerStyle = [\n new Style({\n stroke: new Stroke({ color: lineCasingColor, width: casingW }),\n }),\n new Style({\n stroke: new Stroke({ color: strokeColor, width: strokeWidth }),\n fill: fillStyle,\n image: pointStyle,\n }),\n ];\n } else {\n layerStyle = new Style({\n stroke: new Stroke({ color: strokeColor, width: strokeWidth }),\n fill: fillStyle,\n image: pointStyle,\n });\n }\n\n const layer = new VectorLayer({\n title: title,\n source: source,\n style: layerStyle,\n });\n layer.set('typeTag', styleOptions.typeTag || 'VEC');\n\n // Derive a friendly \"Vector / Polygon\" / \"Vector / Line\" / \"Vector / Point\"\n // subtitle from the first feature's geometry type, unless the caller\n // already supplied one in styleOptions.\n //\n // Layers created EMPTY (parcels, OSM_roads, …, populated later from the\n // API) leave the subtitle absent until the first feature arrives —\n // see the `addfeature` listener below.\n const describeFromGeom = (geomType) => {\n if (!geomType) return null;\n if (geomType.includes('Polygon')) return 'Vector / Polygon';\n if (geomType.includes('LineString')) return 'Vector / Line';\n if (geomType.includes('Point')) return 'Vector / Point';\n return 'Vector';\n };\n\n if (styleOptions.typeDescription) {\n layer.set('typeDescription', styleOptions.typeDescription);\n } else {\n const feats = source.getFeatures();\n const initial = describeFromGeom(feats[0]?.getGeometry?.()?.getType?.());\n if (initial) {\n layer.set('typeDescription', initial);\n } else {\n // Source is empty — wait for the first feature and set then.\n const once = (ev) => {\n const desc = describeFromGeom(ev.feature.getGeometry?.()?.getType?.());\n if (desc) layer.set('typeDescription', desc);\n source.un('addfeature', once);\n };\n source.on('addfeature', once);\n }\n }\n\n const group = targetGroup || this.overlayGroup;\n group.getLayers().push(layer);\n\n console.log('[MapView] GeoJSON layer added:', title, '→', source.getFeatures().length, 'features',\n targetGroup ? `(in group \"${targetGroup.get('title')}\")` : '');\n return layer;\n }\n\n /**\n * Add a LayerGroup to the overlay group.\n * Used to create layer categories from the remote catalogue;\n * individual vector layers will be added into these groups later.\n *\n * @param {number|string} id - Unique layer group id (from the API)\n * @param {string} title - Group title for the LayerSwitcher\n * @param {string} [description=''] - Group description (stored as property)\n * @returns {LayerGroup} The created (empty) layer group\n */\n addLayerGroup(id, title, description = '') {\n const group = new LayerGroup({\n title: title.trim(),\n });\n\n // Store metadata for later use\n group.set('layerId', id);\n group.set('description', description);\n\n this.overlayGroup.getLayers().push(group);\n\n console.log('[MapView] Layer group added:', title.trim(), '(id:', id + ')');\n return group;\n }\n\n /**\n * Add a WMS layer to a layer group.\n *\n * @param {string} groupTitle Title of the target LayerGroup (e.g. 'Biophysical Environment')\n * @param {string} title Display title for the layer\n * @param {string} url WMS server URL\n * @param {string} layers WMS LAYERS parameter\n * @param {Object} [options] Extra options\n * @param {string} [options.serverType='geoserver'] Server type hint ('geoserver'|'mapserver'|'qgis'|null)\n * @param {string} [options.style] WMS STYLES parameter (e.g. 'colours' for DEAfrica DEM)\n * @param {boolean} [options.visible=true] Initial visibility\n * @param {string} [options.attributions] Attribution HTML\n * @param {number} [options.opacity=1] Layer opacity (0–1). Use ~0.5 for background-style layers.\n * @param {number} [options.zIndex] Render z-index. Use negative values (e.g. -10) to force the\n * layer behind all default-z-index layers regardless of group order.\n * @param {string} [options.legendUrl] URL of a legend image to display while the layer is visible.\n * @param {boolean} [options.onlineOnly=false] If true, show a toast when the user toggles the layer on\n * while offline, explaining that the layer requires connectivity.\n * @returns {TileLayer|null} The created layer, or null if group not found\n */\n addWMSLayer(groupTitle, title, url, layers, options = {}) {\n const group = this.getLayerGroupByTitle(groupTitle);\n if (!group) {\n console.warn(`[MapView] Layer group \"${groupTitle}\" not found — cannot add WMS layer \"${title}\"`);\n return null;\n }\n\n const params = { LAYERS: layers, TILED: true, WIDTH: 256, HEIGHT: 256 };\n if (options.style !== undefined) params.STYLES = options.style;\n\n const wmsSource = new TileWMS({\n url,\n params,\n serverType: options.serverType !== undefined ? options.serverType : 'geoserver',\n crossOrigin: 'anonymous',\n hidpi: false,\n attributions: options.attributions,\n });\n\n const wmsLayer = new TileLayer({\n title,\n visible: options.visible !== undefined ? options.visible : true,\n source: wmsSource,\n opacity: options.opacity !== undefined ? options.opacity : 1,\n zIndex: options.zIndex,\n });\n wmsLayer.set('typeTag', 'WMS');\n wmsLayer.set('typeDescription', 'WMS / Raster');\n\n // Show toast on tile load errors (e.g. server rejects request)\n wmsSource.on('tileloaderror', () => {\n showToast(`WMS layer \"${title}\" — tile load error. Check the URL and layer name.`, 'warning', 5000);\n });\n\n group.getLayers().push(wmsLayer);\n\n // Register legend AFTER push so that a failure here doesn't block the LayerSwitcher\n if (options.legendUrl) {\n try {\n this._registerLegend(wmsLayer, title, options.legendUrl);\n } catch (err) {\n console.warn(`[MapView] Could not register legend for \"${title}\":`, err);\n }\n }\n\n // Online-only warning: when the user toggles the layer on while offline,\n // surface a toast explaining why nothing will render.\n if (options.onlineOnly) {\n this._attachOnlineOnlyHandler(wmsLayer, title);\n }\n\n console.log(`[MapView] WMS layer added: \"${title}\" → group \"${groupTitle}\"`);\n return wmsLayer;\n }\n\n /**\n * Add an XYZ tile layer to a layer group.\n *\n * @param {string} groupTitle Title of the target LayerGroup\n * @param {string} title Display title for the layer\n * @param {string} url XYZ tile URL template (with {z}/{x}/{y} placeholders)\n * @param {Object} [options] Extra options\n * @param {boolean} [options.visible=true] Initial visibility\n * @param {string} [options.attributions] Attribution HTML\n * @param {number} [options.maxZoom=19] Maximum zoom level\n * @param {number} [options.opacity=1] Layer opacity (0–1). Use ~0.5 for background-style layers.\n * @param {number} [options.zIndex] Render z-index. Use negative values to force behind other layers.\n * @param {string} [options.legendUrl] URL of a legend image to display while the layer is visible.\n * @param {boolean} [options.onlineOnly=false] If true, show a toast when the user toggles the layer on\n * while offline, explaining that the layer requires connectivity.\n * @returns {TileLayer|null} The created layer, or null if group not found\n */\n addXYZLayer(groupTitle, title, url, options = {}) {\n const group = this.getLayerGroupByTitle(groupTitle);\n if (!group) {\n console.warn(`[MapView] Layer group \"${groupTitle}\" not found — cannot add XYZ layer \"${title}\"`);\n return null;\n }\n\n const xyzSource = new XYZ({\n url,\n crossOrigin: 'anonymous',\n maxZoom: options.maxZoom !== undefined ? options.maxZoom : 19,\n attributions: options.attributions,\n });\n\n const xyzLayer = new TileLayer({\n title,\n visible: options.visible !== undefined ? options.visible : true,\n source: xyzSource,\n opacity: options.opacity !== undefined ? options.opacity : 1,\n zIndex: options.zIndex,\n });\n xyzLayer.set('typeTag', 'XYZ');\n xyzLayer.set('typeDescription', 'XYZ / Tile');\n\n // Show toast on tile load errors\n xyzSource.on('tileloaderror', () => {\n showToast(`XYZ layer \"${title}\" — tile load error. Check the URL.`, 'warning', 5000);\n });\n\n group.getLayers().push(xyzLayer);\n\n // Register legend AFTER push so that a failure here doesn't block the LayerSwitcher\n if (options.legendUrl) {\n try {\n this._registerLegend(xyzLayer, title, options.legendUrl);\n } catch (err) {\n console.warn(`[MapView] Could not register legend for \"${title}\":`, err);\n }\n }\n\n // Online-only warning: when the user toggles the layer on while offline,\n // surface a toast explaining why nothing will render.\n if (options.onlineOnly) {\n this._attachOnlineOnlyHandler(xyzLayer, title);\n }\n\n console.log(`[MapView] XYZ layer added: \"${title}\" → group \"${groupTitle}\"`);\n return xyzLayer;\n }\n\n // ============================================================================\n // Add External Layer Dialog\n // ============================================================================\n\n /**\n * Create the add-layer dialog overlay (hidden by default).\n * Appended to the map target element so it stays within the map viewport.\n */\n _createAddLayerDialog() {\n this._addLayerDialog = document.createElement('div');\n this._addLayerDialog.className = 'map-add-layer-dialog';\n this._addLayerDialog.style.cssText = `\n display:none;position:absolute;top:0;left:0;right:0;bottom:0;\n z-index:1100;background:rgba(0,0,0,0.4);\n align-items:center;justify-content:center;\n `;\n\n const card = document.createElement('div');\n card.style.cssText = `\n background:var(--card, #fff);color:var(--card-foreground, #1e1a4b);\n border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,0.35);\n font-family:var(--font-body, 'Exo', sans-serif);font-size:13px;\n width:340px;max-width:90vw;border:2px solid #10b981;overflow:hidden;\n `;\n\n card.innerHTML = `\n
                \n Add External Layer\n \n
                \n
                \n
                \n \n
                \n \n \n \n
                \n
                \n
                \n \n \n
                \n
                \n \n \n
                \n WMS LAYERS parameter (e.g. workspace:layer)\n
                \n
                \n
                \n \n \n
                \n
                \n \n \n
                \n
                \n `;\n\n this._addLayerDialog.appendChild(card);\n this.map.getTargetElement().appendChild(this._addLayerDialog);\n\n // Type radio change — toggle layer name row visibility\n const nameRow = card.querySelector('.add-layer-name-row');\n const nameHint = card.querySelector('.add-layer-name-hint');\n const urlInput = card.querySelector('.add-layer-url');\n card.querySelectorAll('input[name=\"add-layer-type\"]').forEach((radio) => {\n radio.addEventListener('change', () => {\n const type = radio.value;\n if (type === 'xyz') {\n nameRow.style.display = 'none';\n urlInput.placeholder = 'https://example.com/tiles/{z}/{x}/{y}.png';\n } else {\n nameRow.style.display = '';\n urlInput.placeholder = type === 'wms'\n ? 'https://example.com/wms'\n : 'https://example.com/wfs';\n nameHint.textContent = type === 'wms'\n ? 'WMS LAYERS parameter (e.g. workspace:layer)'\n : 'WFS typename (e.g. workspace:layer)';\n }\n });\n });\n\n // Close / Cancel\n const close = () => this._hideAddLayerDialog();\n card.querySelector('.add-layer-close').addEventListener('click', close);\n card.querySelector('.add-layer-cancel').addEventListener('click', close);\n this._addLayerDialog.addEventListener('click', (e) => {\n if (e.target === this._addLayerDialog) close();\n });\n\n // Confirm\n card.querySelector('.add-layer-confirm').addEventListener('click', () => {\n const type = card.querySelector('input[name=\"add-layer-type\"]:checked').value;\n const url = card.querySelector('.add-layer-url').value.trim();\n const layerName = card.querySelector('.add-layer-name').value.trim();\n const title = card.querySelector('.add-layer-title').value.trim();\n\n if (!url) {\n card.querySelector('.add-layer-url').style.borderColor = '#ef4444';\n return;\n }\n if ((type === 'wms' || type === 'wfs') && !layerName) {\n card.querySelector('.add-layer-name').style.borderColor = '#ef4444';\n return;\n }\n if (!title) {\n card.querySelector('.add-layer-title').style.borderColor = '#ef4444';\n return;\n }\n\n this._addExternalLayer(type, url, layerName, title);\n this._hideAddLayerDialog();\n });\n\n // Enter key to confirm\n card.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n card.querySelector('.add-layer-confirm').click();\n }\n if (e.key === 'Escape') {\n e.preventDefault();\n close();\n }\n });\n }\n\n /**\n * Show the add-layer dialog.\n */\n showAddLayerDialog() {\n const dlg = this._addLayerDialog;\n // Reset form\n dlg.querySelector('.add-layer-url').value = '';\n dlg.querySelector('.add-layer-name').value = '';\n dlg.querySelector('.add-layer-title').value = '';\n dlg.querySelectorAll('input[name=\"add-layer-type\"]')[0].checked = true;\n dlg.querySelector('.add-layer-name-row').style.display = '';\n dlg.querySelector('.add-layer-url').placeholder = 'https://example.com/wms';\n dlg.querySelector('.add-layer-name-hint').textContent = 'WMS LAYERS parameter (e.g. workspace:layer)';\n\n // Reset border colours\n dlg.querySelectorAll('input[type=\"text\"]').forEach((inp) => {\n inp.style.borderColor = 'var(--border, #1e1a4b1f)';\n });\n\n dlg.style.display = 'flex';\n dlg.querySelector('.add-layer-url').focus();\n }\n\n /**\n * Hide the add-layer dialog.\n */\n _hideAddLayerDialog() {\n this._addLayerDialog.style.display = 'none';\n }\n\n /**\n * Add an external layer to the \"External Source\" group.\n *\n * @param {string} type 'wms' | 'wfs' | 'xyz'\n * @param {string} url Server URL\n * @param {string} layerName WMS LAYERS / WFS typename (ignored for XYZ)\n * @param {string} title Display title in layer switcher\n */\n _addExternalLayer(type, url, layerName, title) {\n const group = this._externalSourceGroup;\n if (!group) {\n showToast('Layer group \"External Source\" not found.', 'error', 4000);\n return;\n }\n\n let layer;\n\n switch (type) {\n case 'wms': {\n const wmsSrc = new TileWMS({\n url,\n params: { LAYERS: layerName, TILED: true, WIDTH: 256, HEIGHT: 256 },\n serverType: 'geoserver',\n crossOrigin: 'anonymous',\n hidpi: false,\n });\n layer = new TileLayer({\n title,\n visible: true,\n source: wmsSrc,\n });\n wmsSrc.on('tileloaderror', () => {\n showToast(`WMS \"${title}\" — tile load error. Check URL and layer name.`, 'warning', 5000);\n });\n break;\n }\n\n case 'wfs': {\n const wfsUrl = `${url}${url.includes('?') ? '&' : '?'}` +\n `service=WFS&version=1.1.0&request=GetFeature` +\n `&typename=${encodeURIComponent(layerName)}` +\n `&outputFormat=application/json&srsname=EPSG:3857`;\n\n const wfsSource = new VectorSource({\n url: wfsUrl,\n format: new GeoJSON(),\n });\n wfsSource.on('featuresloaderror', () => {\n showToast(`WFS \"${title}\" — load error. Check URL and layer name.`, 'warning', 5000);\n });\n\n layer = new VectorLayer({\n title,\n visible: true,\n source: wfsSource,\n style: new Style({\n stroke: new Stroke({ color: '#e11d48', width: 2 }),\n fill: new Fill({ color: 'rgba(225,29,72,0.15)' }),\n }),\n });\n break;\n }\n\n case 'xyz':\n layer = new TileLayer({\n title,\n visible: true,\n source: new XYZ({\n url,\n crossOrigin: 'anonymous',\n }),\n });\n layer.getSource().on('tileloaderror', () => {\n showToast(`XYZ \"${title}\" — tile load error. Check the URL template.`, 'warning', 5000);\n });\n break;\n\n default:\n showToast(`Unknown layer type: ${type}`, 'error', 4000);\n return;\n }\n\n // Tag for the LayerSwitcher chip\n layer.set('typeTag', type.toUpperCase()); // 'WMS' | 'WFS' | 'XYZ'\n layer.set('typeDescription', {\n wms: 'WMS / Raster',\n wfs: 'WFS / Vector',\n xyz: 'XYZ / Tile',\n }[type] || type.toUpperCase());\n\n // User-added external layers ARE removable — they're not part of the\n // app's built-in data model.\n layer.set('removable', true);\n\n group.getLayers().push(layer);\n showToast(`Layer \"${title}\" added to External Source.`, 'success', 3000);\n console.log(`[MapView] External ${type.toUpperCase()} layer added: \"${title}\"`);\n }\n\n // ============================================================================\n // LayerSwitcher decoration (Option A — visual refresh)\n // ============================================================================\n\n /**\n * Decorate a layer's
              • after ol-ext renders it:\n * • inject a type-tag chip next to the layer label\n * • inject the green \"+\" button on the External Source group header\n *\n * Idempotent — safe to call repeatedly; each injected element checks\n * whether it already exists in the row.\n */\n _decorateLayerListItem(layer, li) {\n // 1. Type-tag chip (e.g. WMS / XYZ / VEC) next to the layer name.\n // Inserted INSIDE the label's so it doesn't collide with the\n // label's left padding (where ol-ext draws the checkbox via ::before).\n const tag = layer.get('typeTag'); // 'WMS' | 'WFS' | 'XYZ' | 'VEC' | 'GEO' | 'BASE'\n if (tag) {\n const labelSpan = li.querySelector(':scope > .li-content > label > span');\n if (labelSpan && !labelSpan.querySelector(':scope > .ls-type-tag')) {\n const chip = document.createElement('span');\n chip.className = `ls-type-tag ls-type-tag-${String(tag).toLowerCase()}`;\n chip.textContent = String(tag);\n chip.title = `${tag} layer`;\n labelSpan.appendChild(chip);\n }\n }\n\n // 2. Replace ol-ext's bar-drawn +/- chevron with the GeoView chevron SVG.\n // The SVG uses stroke=\"currentColor\", so CSS `color` (set in\n // layerswitcher.css) tints it. Rotation handles the open/closed state.\n const btnBar = li.querySelector(':scope > .ol-layerswitcher-buttons');\n if (btnBar) {\n const chevronEl = btnBar.querySelector(':scope > .expend-layers, :scope > .collapse-layers');\n if (chevronEl && !chevronEl.querySelector(':scope > svg.ls-chevron-svg')) {\n chevronEl.innerHTML =\n '' +\n '' +\n '';\n }\n }\n\n // 3. Layer-type subtitle (\"Vector / Polygon\", \"WMS / Raster\", …) below\n // the layer name. Only rendered when layer.get('typeDescription') is\n // set. For layers that start empty and gain features later (the API\n // loaders), we listen for `change:typeDescription` and update or\n // insert the subtitle then.\n const content = li.querySelector(':scope > .li-content');\n const ensureSubtitle = () => {\n if (!content) return;\n const text = layer.get('typeDescription');\n let sub = content.querySelector(':scope > .ls-layer-subtitle');\n if (!text) {\n if (sub) sub.remove();\n return;\n }\n if (!sub) {\n sub = document.createElement('div');\n sub.className = 'ls-layer-subtitle';\n const label = content.querySelector(':scope > label');\n if (label && label.nextSibling) {\n content.insertBefore(sub, label.nextSibling);\n } else {\n content.appendChild(sub);\n }\n }\n sub.textContent = text;\n };\n ensureSubtitle();\n if (!layer._lsSubtitleHooked) {\n layer._lsSubtitleHooked = true;\n layer.on('change:typeDescription', () => {\n // The
              • may not exist any more (panel re-rendered between events)\n // — guard with a fresh lookup via the LayerSwitcher next time it draws.\n // For now we just call ensureSubtitle bound to the original li.\n ensureSubtitle();\n });\n }\n\n // 4. Per-layer Remove button — only for layers explicitly marked\n // `removable: true` (external sources, imported files, …). Built-in\n // layers (Parcels, OSM_roads, district boundary, …) are NOT removable\n // so the user can't accidentally delete them.\n if (layer.get('removable') === true && btnBar && !btnBar.querySelector(':scope > .ls-remove-btn')) {\n const removeBtn = document.createElement('button');\n removeBtn.type = 'button';\n removeBtn.className = 'ls-remove-btn';\n removeBtn.title = 'Remove this layer';\n removeBtn.setAttribute('aria-label', 'Remove layer');\n removeBtn.innerHTML =\n '' +\n '' +\n '';\n removeBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n this._removeLayer(layer);\n });\n btnBar.appendChild(removeBtn);\n }\n\n // 5. \"+\" button on the External Source group\n const groupTitle = (layer.get('title') || '').toLowerCase();\n if (groupTitle.includes('external')) {\n this._externalSourceGroup = layer;\n // btnBar already resolved above (same .ol-layerswitcher-buttons element)\n if (btnBar && !btnBar.querySelector('.ol-add-layer')) {\n const addBtn = document.createElement('span');\n addBtn.className = 'ol-add-layer';\n addBtn.title = 'Add external layer';\n addBtn.textContent = '+';\n addBtn.style.cssText = `\n display:inline-flex !important;align-items:center;justify-content:center;\n width:22px !important;height:22px !important;border-radius:50%;\n background:#41b6a6 !important;color:#fff !important;\n font-size:15px !important;font-weight:700;\n cursor:pointer;line-height:1 !important;\n margin:0 4px 0 0;vertical-align:middle;\n transition:background 0.2s;box-sizing:border-box;border:none;\n `;\n addBtn.addEventListener('mouseenter', () => { addBtn.style.background = '#329686'; });\n addBtn.addEventListener('mouseleave', () => { addBtn.style.background = '#41b6a6'; });\n addBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n this.showAddLayerDialog();\n });\n btnBar.prepend(addBtn);\n }\n }\n }\n\n /**\n * Remove a layer from its parent group, after confirmation. Only called\n * from the per-layer × button injected by `_decorateLayerListItem` — and\n * that button is only injected for layers marked `removable: true`, so\n * built-in layers (Parcels, OSM_roads, …) can never reach this path.\n *\n * @param {Layer} layer\n */\n _removeLayer(layer) {\n const title = layer.get('title') || 'this layer';\n if (!confirm(`Remove \"${title}\" from the map?\\n\\nThis only affects the current session — built-in layers cannot be removed.`)) {\n return;\n }\n\n // Find the parent group that owns this layer and call .remove() on its\n // collection. Walk recursively from the overlay group.\n const visit = (group) => {\n const layers = group.getLayers();\n if (layers.getArray().includes(layer)) {\n layers.remove(layer);\n return true;\n }\n let removed = false;\n layers.forEach((child) => {\n if (!removed && child.getLayers) {\n removed = visit(child);\n }\n });\n return removed;\n };\n\n const ok = visit(this.overlayGroup);\n if (ok) {\n console.log(`[MapView] Removed layer \"${title}\"`);\n showToast(`Removed \"${title}\" from the map.`, 'info', 3000);\n } else {\n console.warn(`[MapView] Could not find layer \"${title}\" in any group`);\n }\n }\n\n /**\n * Inject (or refresh) the panel chrome — an \"active count\" badge at the top\n * and a footer row with \"Reset overlays\" button at the bottom.\n *\n * The chrome lives in `.panel-container` (the wrapping
                ), not inside\n * the `
                  ` — that way the badge and footer are siblings of\n * the layer list rather than malformed children of a `
                    `.\n *\n * Called from drawlist via queueMicrotask, so it runs once per redraw cycle\n * regardless of how many layers are in the panel.\n */\n _refreshLayerSwitcherChrome(layerSwitcher) {\n const panelContainer = layerSwitcher.element?.querySelector('.panel-container');\n const ul = layerSwitcher.element?.querySelector('ul.panel');\n if (!panelContainer || !ul) return;\n\n // --- Active-count badge (top of panel-container, before the
                      ) ---\n let badge = panelContainer.querySelector(':scope > .ls-active-badge');\n if (!badge) {\n badge = document.createElement('div');\n badge.className = 'ls-active-badge';\n badge.innerHTML = `\n Layers\n 0 active\n `;\n panelContainer.insertBefore(badge, ul);\n }\n\n // --- Footer row (bottom of panel-container, after the
                        ) ---\n let footer = panelContainer.querySelector(':scope > .ls-footer-row');\n if (!footer) {\n footer = document.createElement('div');\n footer.className = 'ls-footer-row';\n footer.innerHTML = `\n — layers total\n \n `;\n panelContainer.appendChild(footer);\n\n footer.querySelector('.ls-footer-btn').addEventListener('click', (e) => {\n e.stopPropagation();\n this._resetAllOverlays();\n });\n }\n\n // --- Update counters ---\n const counts = this._countLayers();\n badge.querySelector('.ls-active-badge-count').textContent =\n `${counts.activeOverlays} active`;\n footer.querySelector('.ls-footer-note').textContent =\n `${counts.totalOverlays} overlay${counts.totalOverlays === 1 ? '' : 's'}`;\n }\n\n /**\n * Walk the overlay group recursively, returning the number of *leaf* layers\n * (i.e. not groups) and how many of them are currently visible.\n * Excludes base maps and internal layers (Markers, Drawings, vertex overlay).\n */\n _countLayers() {\n let totalOverlays = 0;\n let activeOverlays = 0;\n\n const HIDDEN_INTERNAL = new Set(['__vertex_highlight__']);\n\n const visit = (group) => {\n group.getLayers().forEach((layer) => {\n if (layer.get('displayInLayerSwitcher') === false) return;\n if (HIDDEN_INTERNAL.has(layer.get('title'))) return;\n\n if (layer.getLayers) {\n visit(layer);\n } else {\n totalOverlays++;\n if (layer.getVisible()) activeOverlays++;\n }\n });\n };\n if (this.overlayGroup) visit(this.overlayGroup);\n return { totalOverlays, activeOverlays };\n }\n\n /**\n * Hide every overlay layer (base maps stay on). Wired to the footer\n * \"Reset overlays\" button.\n */\n _resetAllOverlays() {\n const HIDDEN_INTERNAL = new Set(['__vertex_highlight__']);\n const visit = (group) => {\n group.getLayers().forEach((layer) => {\n if (layer.get('displayInLayerSwitcher') === false) return;\n if (HIDDEN_INTERNAL.has(layer.get('title'))) return;\n\n if (layer.getLayers) {\n visit(layer);\n } else {\n layer.setVisible(false);\n }\n });\n };\n if (this.overlayGroup) visit(this.overlayGroup);\n console.log('[MapView] Reset overlays — all hidden');\n }\n\n /**\n * Hook `change:visible` on every overlay leaf so the active-count badge\n * stays in sync even when ol-ext doesn't re-fire drawlist (e.g. ticking\n * a checkbox just toggles visibility without rebuilding the list).\n * Also re-hooks when a new layer is added.\n */\n _wireLayerSwitcherVisibilityHooks(layerSwitcher) {\n const refresh = () => this._refreshLayerSwitcherChrome(layerSwitcher);\n\n const hookLayer = (layer) => {\n if (layer._lsVisHooked) return;\n layer._lsVisHooked = true;\n layer.on('change:visible', refresh);\n };\n\n const visit = (group) => {\n group.getLayers().forEach((layer) => {\n if (layer.getLayers) {\n visit(layer);\n // Listen for layers added later to this group\n if (!group._lsAddHooked) {\n group._lsAddHooked = true;\n group.getLayers().on('add', (ev) => {\n const added = ev.element;\n if (added.getLayers) visit(added); else hookLayer(added);\n refresh();\n });\n }\n } else {\n hookLayer(layer);\n }\n });\n };\n\n if (this.overlayGroup) visit(this.overlayGroup);\n }\n\n // ============================================================================\n // Online-Only Layer Helper\n // ============================================================================\n\n /**\n * Attach a `change:visible` listener that shows an info toast when the user\n * toggles a layer ON while the device is offline. Used for layers that fetch\n * tiles or features from a remote service and therefore have no useful\n * cached state.\n *\n * The check uses navigator.onLine, which is the same signal as the rest of\n * the app's online detection.\n *\n * @param {Layer} layer\n * @param {string} title Display title used in the toast message\n */\n _attachOnlineOnlyHandler(layer, title) {\n layer.set('onlineOnly', true);\n layer.on('change:visible', () => {\n if (layer.getVisible() && !navigator.onLine) {\n showToast(\n `\"${title}\" requires an internet connection. Connect to view this layer.`,\n 'info',\n 5000,\n );\n }\n });\n }\n\n // ============================================================================\n // Legend Panel — shows legend images for visible layers that have one\n // ============================================================================\n\n /**\n * Create the legend panel, positioned bottom-right inside the map target.\n * Hidden when no visible layers have a registered legend.\n */\n _createLegendPanel() {\n this._legendPanel = document.createElement('div');\n this._legendPanel.className = 'map-legend-panel';\n this._legendPanel.style.cssText = `\n position:absolute;right:10px;bottom:40px;z-index:900;\n display:none;flex-direction:column;gap:6px;\n background:var(--card, #fff);color:var(--card-foreground, #1e1a4b);\n border:1px solid var(--border, #1e1a4b1f);border-radius:8px;\n box-shadow:0 4px 12px rgba(0,0,0,0.15);\n font-family:var(--font-body, 'Exo', sans-serif);font-size:11px;\n max-width:220px;max-height:60%;overflow-y:auto;\n padding:8px 10px;\n `;\n this.map.getTargetElement().appendChild(this._legendPanel);\n\n // Map of layer → { wrapper, title, imgUrl }\n this._legendEntries = new Map();\n }\n\n /**\n * Register a layer's legend image and wire up visibility tracking.\n * Called from addWMSLayer / addXYZLayer when a legendUrl is supplied.\n *\n * @param {Layer} layer The OpenLayers layer\n * @param {string} title Display title for the legend header\n * @param {string} legendUrl URL of the legend image\n */\n _registerLegend(layer, title, legendUrl) {\n if (!this._legendPanel) return;\n\n // Build the legend entry — a div with header + image\n const wrapper = document.createElement('div');\n wrapper.className = 'map-legend-entry';\n wrapper.style.cssText = 'border-bottom:1px solid var(--border, #1e1a4b1f);padding-bottom:6px;';\n wrapper.innerHTML = `\n
                        \n ${this._escapeHtml(title)}\n
                        \n \"${this._escapeHtml(title)}\n `;\n\n this._legendEntries.set(layer, wrapper);\n\n // Listen for visibility changes. Wrap in try/catch so a DOM error here\n // cannot break the LayerSwitcher's click handler (which fires change:visible\n // synchronously and relies on a subsequent setTimeout to update the checkbox).\n const update = () => {\n try { this._updateLegendPanel(); }\n catch (err) { console.warn('[MapView] legend panel update failed:', err); }\n };\n layer.on('change:visible', update);\n\n // Trigger initial state\n update();\n }\n\n /**\n * Refresh the legend panel contents: include entries for each visible\n * registered layer, and show/hide the panel based on whether any are visible.\n */\n _updateLegendPanel() {\n if (!this._legendPanel) return;\n\n // Rebuild children from scratch in a stable order (Map iteration order = insertion order)\n const children = [];\n for (const [layer, wrapper] of this._legendEntries) {\n if (layer.getVisible()) children.push(wrapper);\n }\n\n // Remove trailing bottom-border on the last entry for a clean look\n this._legendEntries.forEach((w) => {\n w.style.borderBottom = '1px solid var(--border, #1e1a4b1f)';\n w.style.paddingBottom = '6px';\n });\n if (children.length > 0) {\n children[children.length - 1].style.borderBottom = 'none';\n children[children.length - 1].style.paddingBottom = '0';\n }\n\n // Swap the DOM children\n this._legendPanel.replaceChildren(...children);\n this._legendPanel.style.display = children.length > 0 ? 'flex' : 'none';\n }\n\n /**\n * Escape HTML special characters for safe text insertion.\n */\n _escapeHtml(str) {\n return String(str)\n .replace(/&/g, '&')\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n }\n\n /**\n * Find a LayerGroup inside the overlay group by its layerId.\n *\n * @param {number|string} id - The layerId to search for\n * @returns {LayerGroup|null} The matching group, or null\n */\n getLayerGroup(id) {\n let found = null;\n this.overlayGroup.getLayers().forEach((layer) => {\n if (layer.get('layerId') === id) {\n found = layer;\n }\n });\n return found;\n }\n\n /**\n * Find a LayerGroup inside the overlay group by its title.\n *\n * @param {string} title - The group title to search for\n * @returns {LayerGroup|null} The matching group, or null\n */\n getLayerGroupByTitle(title) {\n let found = null;\n this.overlayGroup.getLayers().forEach((layer) => {\n if (layer.get('title') === title) {\n found = layer;\n }\n });\n return found;\n }\n\n /**\n * Get the overlay LayerGroup for advanced usage\n */\n getOverlayGroup() {\n return this.overlayGroup;\n }\n\n /**\n * Get the OpenLayers map instance for advanced usage\n */\n getMap() {\n return this.map;\n }\n\n // ============================================================================\n // Extent Helpers (used by offline-tile downloader)\n // ============================================================================\n\n /**\n * Get the current map view's visible extent in EPSG:3857 (Web Mercator).\n * @returns {Array} [minX, minY, maxX, maxY]\n */\n getCurrentViewExtent() {\n const view = this.map.getView();\n const size = this.map.getSize();\n if (!size) return null;\n return view.calculateExtent(size);\n }\n\n /**\n * Get the bounding extent of the District Boundary layer (if present).\n * Searches the overlay group for a vector layer titled \"District Boundary\"\n * and returns the extent of its source.\n *\n * @returns {{ extent: Array, title: string } | null}\n */\n getDistrictBoundaryExtent() {\n let found = null;\n const visit = (group) => {\n group.getLayers().forEach((layer) => {\n if (layer.getLayers) {\n visit(layer); // sub-group\n } else if (layer.get('title') === 'District Boundary') {\n const src = layer.getSource && layer.getSource();\n if (src && typeof src.getExtent === 'function') {\n const ex = src.getExtent();\n if (ex && Number.isFinite(ex[0])) {\n found = { extent: ex, title: layer.get('title') };\n }\n }\n }\n });\n };\n visit(this.overlayGroup);\n return found;\n }\n\n /**\n * Get the marker source for advanced usage\n */\n getMarkerSource() {\n return this.markerSource;\n }\n\n /**\n * Get the markers layer for advanced usage\n */\n getMarkersLayer() {\n return this.markersLayer;\n }\n\n /**\n * Update map size (call after container resize)\n */\n updateSize() {\n this.map.updateSize();\n }\n\n /**\n * Register a callback for when a search result is selected\n * Callback receives: { coordinate, lonLat: [lon, lat], name, searchResult }\n * Navigation to the location happens automatically\n */\n onSearchSelect(callback) {\n this.searchSelectCallbacks.push(callback);\n }\n\n /**\n * Navigate/fly to a specific location\n * @param {number} lon - Longitude\n * @param {number} lat - Latitude\n * @param {number} zoom - Zoom level (default: 14)\n * @param {number} duration - Animation duration in ms (default: 500)\n */\n navigateTo(lon, lat, zoom = 14, duration = 500) {\n const coordinate = fromLonLat([lon, lat]);\n this.map.getView().animate({\n center: coordinate,\n zoom: zoom,\n duration: duration,\n });\n }\n}\n\n// Export OpenLayers utilities for convenience\nexport { fromLonLat, toLonLat };\n\nexport default MapView;\n","/**\n * MapTools - Drawing and Measurement Tools\n * \n * Provides:\n * - Circle measurement tool with radius/area tooltip\n * - Control bar with drawing tools\n * - Line and polygon measurement\n * \n * Refactored from olmapstuffgis.js for LUPMIS PWA\n */\n\nimport { Draw } from 'ol/interaction';\nimport { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style';\nimport { Vector as VectorLayer } from 'ol/layer';\nimport { Vector as VectorSource } from 'ol/source';\nimport Overlay from 'ol/Overlay';\nimport { LineString, Circle, Polygon } from 'ol/geom';\nimport { getLength, getArea } from 'ol/sphere';\nimport Feature from 'ol/Feature';\nimport { unByKey } from 'ol/Observable';\nimport { formatLength, formatArea, formatCircleExtent } from '../units.js';\n\n// ol-ext imports\nimport EditBar from 'ol-ext/control/EditBar';\nimport Toggle from 'ol-ext/control/Toggle';\nimport Button from 'ol-ext/control/Button';\n\nexport class MapTools {\n constructor(map, options = {}) {\n this.map = map;\n this.options = options;\n \n // Create measurement layer\n this.measureSource = new VectorSource();\n this.measureLayer = new VectorLayer({\n source: this.measureSource,\n style: this.getMeasureStyle(),\n title: 'Measurements',\n zIndex: 100,\n });\n\n // Create drawing layer (utility layer for temporary draw interactions;\n // hidden from LayerSwitcher since the EditBar has its own \"Drawings\" group)\n this.drawSource = new VectorSource();\n this.drawLayer = new VectorLayer({\n source: this.drawSource,\n style: this.getDrawStyle(),\n title: 'Draw sketches',\n displayInLayerSwitcher: false,\n zIndex: 99,\n });\n\n // Insert both layers just before the Overlays group so the LayerSwitcher\n // order becomes: Overlays > Measurements > Markers > Base Maps. Locate the\n // Overlays group by title rather than assuming it is the last layer —\n // other layers (e.g. the GPS trail/position layers) may sit on top of it.\n const layers = this.map.getLayers();\n let overlayIdx = layers.getArray().findIndex((l) => l.get('title') === 'Overlays');\n if (overlayIdx < 0) overlayIdx = layers.getLength();\n layers.insertAt(overlayIdx, this.drawLayer);\n layers.insertAt(overlayIdx, this.measureLayer);\n \n // Active interaction\n this.activeInteraction = null;\n this.measureTooltip = null;\n this.measureTooltipElement = null;\n \n // Callbacks\n this.onMeasureCompleteCallbacks = [];\n this.onDrawCompleteCallbacks = [];\n }\n \n /**\n * Get style for measurement features\n */\n getMeasureStyle() {\n return new Style({\n fill: new Fill({\n color: 'rgba(255, 233, 106, 0.2)'\n }),\n stroke: new Stroke({\n color: '#8B008B',\n lineDash: [10, 10],\n width: 2\n }),\n image: new CircleStyle({\n radius: 5,\n stroke: new Stroke({\n color: '#8B008B'\n }),\n fill: new Fill({\n color: 'rgba(255, 233, 106, 0.5)'\n })\n })\n });\n }\n \n /**\n * Get style for drawing features\n */\n getDrawStyle() {\n return new Style({\n fill: new Fill({\n color: 'rgba(255, 233, 106, 0.3)'\n }),\n stroke: new Stroke({\n color: '#8B008B',\n width: 2\n }),\n image: new CircleStyle({\n radius: 6,\n stroke: new Stroke({\n color: '#8B008B',\n width: 2\n }),\n fill: new Fill({\n color: '#FFE96A'\n })\n })\n });\n }\n \n /**\n * Create measurement tooltip overlay\n */\n createMeasureTooltip() {\n if (this.measureTooltipElement) {\n this.measureTooltipElement.parentNode.removeChild(this.measureTooltipElement);\n }\n this.measureTooltipElement = document.createElement('div');\n this.measureTooltipElement.className = 'measure-tooltip';\n this.measureTooltip = new Overlay({\n element: this.measureTooltipElement,\n offset: [15, 0],\n positioning: 'center-left',\n stopEvent: false,\n });\n this.map.addOverlay(this.measureTooltip);\n }\n \n /**\n * Remove any active interaction\n */\n deactivate() {\n if (this.activeInteraction) {\n this.map.removeInteraction(this.activeInteraction);\n this.activeInteraction = null;\n }\n if (this.measureTooltip) {\n this.map.removeOverlay(this.measureTooltip);\n this.measureTooltip = null;\n }\n if (this.measureTooltipElement && this.measureTooltipElement.parentNode) {\n this.measureTooltipElement.parentNode.removeChild(this.measureTooltipElement);\n this.measureTooltipElement = null;\n }\n }\n \n /**\n * Start circle measurement tool\n * Draws a circle and shows radius + area in tooltip\n */\n startCircleMeasure() {\n this.deactivate();\n this.createMeasureTooltip();\n \n const drawCircle = new Draw({\n source: this.measureSource,\n type: 'Circle',\n style: new Style({\n fill: new Fill({\n color: 'rgba(255, 233, 106, 0.2)'\n }),\n stroke: new Stroke({\n color: 'rgba(139, 0, 139, 0.7)',\n lineDash: [10, 10],\n width: 2\n }),\n image: new CircleStyle({\n radius: 5,\n stroke: new Stroke({\n color: 'rgba(139, 0, 139, 0.7)'\n }),\n fill: new Fill({\n color: 'rgba(255, 233, 106, 0.5)'\n })\n })\n })\n });\n \n this.activeInteraction = drawCircle;\n this.map.addInteraction(drawCircle);\n \n let listener;\n \n drawCircle.on('drawstart', (evt) => {\n const sketch = evt.feature;\n \n listener = sketch.getGeometry().on('change', (e) => {\n const geom = e.target;\n \n if (geom instanceof Circle) {\n const radius = geom.getRadius();\n const area = formatCircleExtent(radius);\n const radiusFormatted = formatLength(radius);\n \n const output = `${radiusFormatted}
                        ${area}`;\n \n this.measureTooltipElement.innerHTML = output;\n this.measureTooltip.setPosition(geom.getLastCoordinate());\n }\n });\n });\n \n drawCircle.on('drawend', (evt) => {\n const feature = evt.feature;\n const geom = feature.getGeometry();\n const center = geom.getCenter();\n const radius = geom.getRadius();\n\n // Tag the circle feature so the dblclick handler can identify it\n feature.set('_layerType', 'measure_circle');\n feature.set('_radius', radius);\n feature.set('_center', center);\n\n // Create radius line for visualization\n const radiusLine = new Feature({\n geometry: new LineString([\n center,\n [center[0] + radius, center[1]]\n ])\n });\n radiusLine.set('_layerType', 'measure_circle_radius');\n this.measureSource.addFeature(radiusLine);\n \n // Make tooltip static\n this.measureTooltipElement.className = 'measure-tooltip measure-tooltip-static';\n this.measureTooltip.setOffset([0, -7]);\n \n // Create new tooltip for next measurement\n this.measureTooltipElement = null;\n this.createMeasureTooltip();\n \n unByKey(listener);\n \n // Trigger callbacks\n const result = {\n type: 'circle',\n center: center,\n radius: radius,\n area: Math.PI * radius * radius,\n feature: feature,\n };\n this.onMeasureCompleteCallbacks.forEach(cb => cb(result));\n });\n \n return drawCircle;\n }\n \n /**\n * Start line measurement tool\n */\n startLineMeasure() {\n this.deactivate();\n this.createMeasureTooltip();\n \n const drawLine = new Draw({\n source: this.measureSource,\n type: 'LineString',\n style: this.getMeasureStyle(),\n });\n \n this.activeInteraction = drawLine;\n this.map.addInteraction(drawLine);\n \n let listener;\n \n drawLine.on('drawstart', (evt) => {\n const sketch = evt.feature;\n \n listener = sketch.getGeometry().on('change', (e) => {\n const geom = e.target;\n const length = getLength(geom);\n const output = formatLength(length);\n \n this.measureTooltipElement.innerHTML = output;\n this.measureTooltip.setPosition(geom.getLastCoordinate());\n });\n });\n \n drawLine.on('drawend', (evt) => {\n const feature = evt.feature;\n const geom = feature.getGeometry();\n const length = getLength(geom);\n \n this.measureTooltipElement.className = 'measure-tooltip measure-tooltip-static';\n this.measureTooltipElement = null;\n this.createMeasureTooltip();\n \n unByKey(listener);\n \n const result = {\n type: 'line',\n length: length,\n feature: feature,\n };\n this.onMeasureCompleteCallbacks.forEach(cb => cb(result));\n });\n \n return drawLine;\n }\n \n /**\n * Start polygon/area measurement tool\n */\n startAreaMeasure() {\n this.deactivate();\n this.createMeasureTooltip();\n \n const drawPolygon = new Draw({\n source: this.measureSource,\n type: 'Polygon',\n style: this.getMeasureStyle(),\n });\n \n this.activeInteraction = drawPolygon;\n this.map.addInteraction(drawPolygon);\n \n let listener;\n \n drawPolygon.on('drawstart', (evt) => {\n const sketch = evt.feature;\n \n listener = sketch.getGeometry().on('change', (e) => {\n const geom = e.target;\n const area = getArea(geom);\n const output = formatArea(area);\n \n this.measureTooltipElement.innerHTML = output;\n this.measureTooltip.setPosition(geom.getInteriorPoint().getCoordinates());\n });\n });\n \n drawPolygon.on('drawend', (evt) => {\n const feature = evt.feature;\n const geom = feature.getGeometry();\n const area = getArea(geom);\n\n // Tag so the double-click handler can identify it\n feature.set('_layerType', 'measure_area');\n feature.set('_area', area);\n\n this.measureTooltipElement.className = 'measure-tooltip measure-tooltip-static';\n this.measureTooltipElement = null;\n this.createMeasureTooltip();\n\n unByKey(listener);\n\n const result = {\n type: 'polygon',\n area: area,\n feature: feature,\n coordinate: geom.getInteriorPoint().getCoordinates(),\n };\n this.onMeasureCompleteCallbacks.forEach(cb => cb(result));\n });\n \n return drawPolygon;\n }\n \n /**\n * Start point drawing tool\n */\n startDrawPoint() {\n this.deactivate();\n \n const drawPoint = new Draw({\n source: this.drawSource,\n type: 'Point',\n style: this.getDrawStyle(),\n });\n \n this.activeInteraction = drawPoint;\n this.map.addInteraction(drawPoint);\n \n drawPoint.on('drawend', (evt) => {\n const result = {\n type: 'point',\n feature: evt.feature,\n };\n this.onDrawCompleteCallbacks.forEach(cb => cb(result));\n });\n \n return drawPoint;\n }\n \n /**\n * Start line drawing tool\n */\n startDrawLine() {\n this.deactivate();\n \n const drawLine = new Draw({\n source: this.drawSource,\n type: 'LineString',\n style: this.getDrawStyle(),\n });\n \n this.activeInteraction = drawLine;\n this.map.addInteraction(drawLine);\n \n drawLine.on('drawend', (evt) => {\n const result = {\n type: 'line',\n feature: evt.feature,\n };\n this.onDrawCompleteCallbacks.forEach(cb => cb(result));\n });\n \n return drawLine;\n }\n \n /**\n * Start polygon drawing tool\n */\n startDrawPolygon() {\n this.deactivate();\n \n const drawPolygon = new Draw({\n source: this.drawSource,\n type: 'Polygon',\n style: this.getDrawStyle(),\n });\n \n this.activeInteraction = drawPolygon;\n this.map.addInteraction(drawPolygon);\n \n drawPolygon.on('drawend', (evt) => {\n const result = {\n type: 'polygon',\n feature: evt.feature,\n };\n this.onDrawCompleteCallbacks.forEach(cb => cb(result));\n });\n \n return drawPolygon;\n }\n \n /**\n * Clear all measurements\n */\n clearMeasurements() {\n this.measureSource.clear();\n // Remove static tooltips\n const tooltips = document.querySelectorAll('.measure-tooltip-static');\n tooltips.forEach(el => el.parentNode.removeChild(el));\n }\n \n /**\n * Clear all drawings\n */\n clearDrawings() {\n this.drawSource.clear();\n }\n \n /**\n * Clear all (measurements + drawings)\n */\n clearAll() {\n this.clearMeasurements();\n this.clearDrawings();\n }\n \n /**\n * Register callback for measurement completion\n */\n onMeasureComplete(callback) {\n this.onMeasureCompleteCallbacks.push(callback);\n }\n \n /**\n * Register callback for drawing completion\n */\n onDrawComplete(callback) {\n this.onDrawCompleteCallbacks.push(callback);\n }\n \n /**\n * Create a control bar with measurement and drawing tools\n * Returns the ol-ext Bar control\n */\n createControlBar(options = {}) {\n const position = options.position || 'top-left';\n \n // Main control bar\n const mainBar = new EditBar({\n group: true,\n className: 'map-tools-bar',\n });\n \n // Measurement toggle group\n const measureBar = new EditBar({\n toggleOne: true,\n group: true,\n });\n \n // Circle measure button\n const circleBtn = new Toggle({\n html: '',\n title: 'Measure Circle (radius & area)',\n className: 'measure-circle-btn',\n onToggle: (active) => {\n if (active) {\n this.startCircleMeasure();\n } else {\n this.deactivate();\n }\n }\n });\n measureBar.addControl(circleBtn);\n \n // Line measure button\n const lineBtn = new Toggle({\n html: '📏',\n title: 'Measure Distance',\n className: 'measure-line-btn',\n onToggle: (active) => {\n if (active) {\n this.startLineMeasure();\n } else {\n this.deactivate();\n }\n }\n });\n measureBar.addControl(lineBtn);\n \n // Area measure button\n const areaBtn = new Toggle({\n html: '',\n title: 'Measure Area',\n className: 'measure-area-btn',\n onToggle: (active) => {\n if (active) {\n this.startAreaMeasure();\n } else {\n this.deactivate();\n }\n }\n });\n measureBar.addControl(areaBtn);\n \n // Clear measurements button\n const clearBtn = new Button({\n html: '🗑️',\n title: 'Clear Measurements',\n className: 'clear-measure-btn',\n handleClick: () => {\n this.clearMeasurements();\n // Deactivate any active toggle\n circleBtn.setActive(false);\n lineBtn.setActive(false);\n areaBtn.setActive(false);\n }\n });\n measureBar.addControl(clearBtn);\n \n mainBar.addControl(measureBar);\n \n return mainBar;\n }\n \n /**\n * Get the measure layer\n */\n getMeasureLayer() {\n return this.measureLayer;\n }\n \n /**\n * Get the draw layer\n */\n getDrawLayer() {\n return this.drawLayer;\n }\n \n /**\n * Get the measure source\n */\n getMeasureSource() {\n return this.measureSource;\n }\n \n /**\n * Get the draw source\n */\n getDrawSource() {\n return this.drawSource;\n }\n \n /**\n * Check if any tool is currently active\n */\n isActive() {\n return this.activeInteraction !== null;\n }\n}\n\nexport default MapTools;\n","/**\n * PWA Module\n * \n * Handles Progressive Web App functionality:\n * - Service Worker registration\n * - Install prompt handling\n * - Offline detection\n * - Update notifications\n * \n * Note: The Service Worker (sw.js) handles caching.\n * The SharedWorker (shared-db-worker.js) handles database.\n * They are separate workers with different purposes.\n */\n\n// ============================================================================\n// Service Worker Registration\n// ============================================================================\n\nlet swRegistration = null;\n\nexport async function registerServiceWorker() {\n if (!('serviceWorker' in navigator)) {\n console.warn('[PWA] Service Workers not supported');\n return null;\n }\n \n try {\n swRegistration = await navigator.serviceWorker.register('/sw.js', {\n scope: '/'\n });\n \n console.log('[PWA] Service Worker registered:', swRegistration.scope);\n \n // Handle updates\n swRegistration.addEventListener('updatefound', () => {\n const newWorker = swRegistration.installing;\n \n newWorker.addEventListener('statechange', () => {\n if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {\n // New version available\n console.log('[PWA] New version available');\n showUpdateNotification();\n }\n });\n });\n \n return swRegistration;\n \n } catch (error) {\n console.error('[PWA] Service Worker registration failed:', error);\n return null;\n }\n}\n\n// ============================================================================\n// Install Prompt\n// ============================================================================\n\nlet deferredPrompt = null;\nlet installButton = null;\n\n/**\n * Initialize install prompt handling\n * @param {string|HTMLElement} buttonSelector - Button element or selector\n */\nexport function initInstallPrompt(buttonSelector = '#install-btn') {\n installButton = typeof buttonSelector === 'string' \n ? document.querySelector(buttonSelector)\n : buttonSelector;\n \n if (!installButton) {\n console.warn('[PWA] Install button not found:', buttonSelector);\n return;\n }\n \n // Initially hide the button\n installButton.style.display = 'none';\n \n // Listen for the beforeinstallprompt event\n window.addEventListener('beforeinstallprompt', (e) => {\n e.preventDefault();\n deferredPrompt = e;\n \n // Show the install button\n installButton.style.display = 'block';\n console.log('[PWA] Install prompt ready');\n });\n \n // Handle install button click\n installButton.addEventListener('click', async () => {\n if (!deferredPrompt) {\n // Show manual instructions for Safari\n showManualInstallInstructions();\n return;\n }\n \n deferredPrompt.prompt();\n const { outcome } = await deferredPrompt.userChoice;\n \n console.log('[PWA] Install prompt outcome:', outcome);\n \n deferredPrompt = null;\n installButton.style.display = 'none';\n });\n \n // Hide button if app is already installed\n window.addEventListener('appinstalled', () => {\n console.log('[PWA] App installed');\n deferredPrompt = null;\n installButton.style.display = 'none';\n });\n \n // Check if running as installed PWA\n if (window.matchMedia('(display-mode: standalone)').matches) {\n installButton.style.display = 'none';\n }\n}\n\nfunction showManualInstallInstructions() {\n const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);\n const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);\n \n let message = 'To install this app:\\n\\n';\n \n if (isIOS) {\n message += '1. Tap the Share button (square with arrow)\\n';\n message += '2. Scroll down and tap \"Add to Home Screen\"';\n } else if (isSafari) {\n message += '1. Click File menu\\n';\n message += '2. Click \"Add to Dock\"';\n } else {\n message += '1. Click the menu button (three dots)\\n';\n message += '2. Click \"Install\" or \"Add to Home Screen\"';\n }\n \n alert(message);\n}\n\n// ============================================================================\n// Offline Detection\n// ============================================================================\n\nlet offlineIndicator = null;\nconst offlineListeners = new Set();\n\n/**\n * Initialize offline detection\n * @param {string|HTMLElement} indicatorSelector - Element to show when offline\n */\nexport function initOfflineDetection(indicatorSelector = '#offline-indicator') {\n offlineIndicator = typeof indicatorSelector === 'string'\n ? document.querySelector(indicatorSelector)\n : indicatorSelector;\n \n // Set initial state\n updateOfflineUI(!navigator.onLine);\n \n // Listen for online/offline events\n window.addEventListener('online', () => {\n console.log('[PWA] Back online');\n updateOfflineUI(false);\n notifyOfflineListeners(false);\n });\n \n window.addEventListener('offline', () => {\n console.log('[PWA] Gone offline');\n updateOfflineUI(true);\n notifyOfflineListeners(true);\n });\n}\n\nfunction updateOfflineUI(isOffline) {\n if (offlineIndicator) {\n offlineIndicator.style.display = isOffline ? 'block' : 'none';\n }\n \n // Also toggle a class on body for CSS styling\n document.body.classList.toggle('is-offline', isOffline);\n}\n\n/**\n * Subscribe to offline state changes\n * @param {Function} listener - Callback(isOffline: boolean)\n * @returns {Function} Unsubscribe function\n */\nexport function onOfflineChange(listener) {\n offlineListeners.add(listener);\n // Immediately call with current state\n listener(!navigator.onLine);\n return () => offlineListeners.delete(listener);\n}\n\nfunction notifyOfflineListeners(isOffline) {\n for (const listener of offlineListeners) {\n try {\n listener(isOffline);\n } catch (e) {\n console.error('[PWA] Offline listener error:', e);\n }\n }\n}\n\n/**\n * Check if currently online\n */\nexport function isOnline() {\n return navigator.onLine;\n}\n\n// ============================================================================\n// Update Handling\n// ============================================================================\n\nlet updateCallback = null;\n\n/**\n * Set callback for when updates are available\n * @param {Function} callback - Called when new version is ready\n */\nexport function onUpdateAvailable(callback) {\n updateCallback = callback;\n}\n\nfunction showUpdateNotification() {\n if (updateCallback) {\n updateCallback();\n return;\n }\n \n // Default behavior\n if (confirm('A new version is available. Reload now?')) {\n applyUpdate();\n }\n}\n\n/**\n * Apply pending update (reload with new version)\n */\nexport function applyUpdate() {\n if (swRegistration?.waiting) {\n swRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });\n }\n window.location.reload();\n}\n\n// ============================================================================\n// Communication with Service Worker\n// ============================================================================\n\n/**\n * Send a message to the service worker\n * @param {Object} message - Message to send\n */\nexport function postToServiceWorker(message) {\n navigator.serviceWorker.controller?.postMessage(message);\n}\n\n/**\n * Request the service worker to cache specific modules\n * @param {string[]} moduleNames - Array of module names to cache\n */\nexport function cacheModules(moduleNames) {\n postToServiceWorker({\n type: 'CACHE_MODULES',\n payload: { modules: moduleNames }\n });\n}\n\n/**\n * Request the service worker to clear user-specific caches\n * (Call this on logout)\n */\nexport function clearUserCaches() {\n postToServiceWorker({\n type: 'CLEAR_USER_CACHE'\n });\n}\n\n/**\n * Get the Service Worker we can postMessage to. Resolves with:\n * • `navigator.serviceWorker.controller` if it's already in control of the\n * page (fastest path), or\n * • `registration.active` once `navigator.serviceWorker.ready` resolves\n * (covers the first-load case before the SW has claimed the page).\n *\n * Rejects after `timeoutMs` if no SW becomes available — which would only\n * happen in a private/incognito context, an unsupported browser, or when\n * registration genuinely failed.\n *\n * @param {{ timeoutMs?: number }} [opts]\n * @returns {Promise}\n */\nexport async function getActiveServiceWorker({ timeoutMs = 10000 } = {}) {\n if (!('serviceWorker' in navigator)) {\n throw new Error('Service Workers not supported in this browser');\n }\n\n // Fastest path — page is already SW-controlled\n if (navigator.serviceWorker.controller) {\n return navigator.serviceWorker.controller;\n }\n\n // Otherwise wait for the registration to become ready (active SW exists\n // for this scope, even if it hasn't claimed THIS page yet)\n const ready = navigator.serviceWorker.ready;\n const timeout = new Promise((_, reject) =>\n setTimeout(() => reject(new Error('Service-worker readiness timeout')), timeoutMs)\n );\n\n const registration = await Promise.race([ready, timeout]);\n\n // The controller may have appeared while we were waiting; otherwise use\n // the registration's active worker (we can still postMessage to it — caches\n // are shared across the origin)\n const sw = navigator.serviceWorker.controller || registration.active;\n if (!sw) {\n throw new Error('No active service worker available');\n }\n return sw;\n}\n\n/**\n * Subscribe to controller-change events. The callback fires whenever a new\n * Service Worker takes control of the page (e.g. after an SW update or first\n * activation on initial load). Useful for re-querying SW-backed state once\n * the SW has actually taken over.\n *\n * @param {() => void} callback\n * @returns {() => void} unsubscribe function\n */\nexport function onServiceWorkerControllerChange(callback) {\n if (!('serviceWorker' in navigator)) return () => {};\n const handler = () => {\n try { callback(); } catch (e) { console.error('[PWA] controllerchange handler error:', e); }\n };\n navigator.serviceWorker.addEventListener('controllerchange', handler);\n return () => navigator.serviceWorker.removeEventListener('controllerchange', handler);\n}\n\n/**\n * Send a message to the service worker and wait for a single reply with the\n * given response type. Waits for the SW to become available if it isn't yet.\n * Resolves with the reply payload, or rejects after a timeout.\n *\n * @template T\n * @param {string} requestType - Message type to send (e.g. 'GET_TILE_STATS')\n * @param {string} responseType - Message type expected back (e.g. 'TILE_STATS')\n * @param {Object} [extra={}] - Extra fields merged into the outgoing message\n * @param {number} [timeoutMs=5000] Reply timeout (after the SW is available)\n * @param {number} [readyTimeoutMs=10000] Timeout for the SW to be available\n * @returns {Promise}\n */\nasync function requestFromServiceWorker(requestType, responseType, extra = {}, timeoutMs = 5000, readyTimeoutMs = 10000) {\n const sw = await getActiveServiceWorker({ timeoutMs: readyTimeoutMs });\n\n return new Promise((resolve, reject) => {\n const channel = new MessageChannel();\n const timer = setTimeout(() => {\n channel.port1.close();\n reject(new Error(`Service-worker reply \"${responseType}\" timed out`));\n }, timeoutMs);\n\n channel.port1.onmessage = (event) => {\n if (event.data?.type === responseType) {\n clearTimeout(timer);\n channel.port1.close();\n const { type, ...rest } = event.data;\n resolve(rest);\n }\n };\n\n sw.postMessage({ type: requestType, ...extra }, [channel.port2]);\n });\n}\n\n/**\n * Get statistics about tiles cached locally on this device, broken down by\n * provider. Waits up to `readyTimeoutMs` for the service worker to become\n * available. Returns null only if the SW genuinely cannot be reached\n * (private mode, registration failure, or timeout).\n *\n * @returns {Promise<{\n * totals: { count: number, estBytes: number },\n * byProvider: Array<{ key: string, label: string, count: number, limit: number, estBytes: number }>\n * } | null>}\n */\nexport async function getTileCacheStats() {\n try {\n const reply = await requestFromServiceWorker('GET_TILE_STATS', 'TILE_STATS');\n return reply.stats;\n } catch (err) {\n console.warn('[PWA] getTileCacheStats failed:', err);\n return null;\n }\n}\n\n/**\n * Delete every cached tile from this device. Doesn't touch the app shell,\n * modules, or API caches — only the per-provider tile buckets.\n * Waits for the SW to be available before sending the request.\n *\n * @returns {Promise} true if the request was acknowledged\n */\nexport async function clearTileCaches() {\n try {\n await requestFromServiceWorker('CLEAR_TILE_CACHES', 'TILE_CACHES_CLEARED');\n return true;\n } catch (err) {\n console.warn('[PWA] clearTileCaches failed:', err);\n return false;\n }\n}\n\n/**\n * Delete cached tiles for a single provider. `cacheName` must match one of\n * the per-provider caches reported by `getTileCacheStats()` (e.g.\n * `tiles-osm-v4`, `tiles-topo-v4`). Unknown names are rejected by the SW.\n *\n * @param {string} cacheName\n * @returns {Promise} true if the cache was actually deleted\n */\nexport async function clearTileCacheForProvider(cacheName) {\n if (!cacheName) return false;\n try {\n const reply = await requestFromServiceWorker(\n 'CLEAR_TILE_CACHE', 'TILE_CACHE_CLEARED',\n { cacheName },\n );\n return !!reply.deleted;\n } catch (err) {\n console.warn(`[PWA] clearTileCacheForProvider(${cacheName}) failed:`, err);\n return false;\n }\n}\n\n/**\n * Get total disk used by this origin (Cache API + IndexedDB + OPFS).\n * Returns null if the Storage API is not available.\n *\n * @returns {Promise<{ usage: number, quota: number } | null>}\n */\nexport async function getStorageEstimate() {\n if (!navigator.storage?.estimate) return null;\n try {\n const { usage, quota } = await navigator.storage.estimate();\n return { usage: usage || 0, quota: quota || 0 };\n } catch (err) {\n console.warn('[PWA] getStorageEstimate failed:', err);\n return null;\n }\n}\n\n// ============================================================================\n// Auto-initialization\n// ============================================================================\n\n/**\n * Initialize all PWA features\n * @param {Object} options\n */\nexport async function initPWA(options = {}) {\n const {\n installButton = '#install-btn',\n offlineIndicator = '#offline-indicator',\n autoRegisterSW = true\n } = options;\n \n if (autoRegisterSW) {\n await registerServiceWorker();\n }\n \n initInstallPrompt(installButton);\n initOfflineDetection(offlineIndicator);\n \n console.log('[PWA] Initialized');\n}\n\n// Export for direct use\nexport default {\n registerServiceWorker,\n initInstallPrompt,\n initOfflineDetection,\n initPWA,\n isOnline,\n onOfflineChange,\n onUpdateAvailable,\n applyUpdate,\n postToServiceWorker,\n cacheModules,\n clearUserCaches,\n getTileCacheStats,\n clearTileCaches,\n clearTileCacheForProvider,\n getStorageEstimate,\n getActiveServiceWorker,\n onServiceWorkerControllerChange,\n};\n","/**\n * Offline Tile Downloader\n *\n * Pre-fetches map tiles for a given extent and zoom range so they are stored\n * in the Service Worker's per-host tile cache for offline use.\n *\n * The downloader simply issues `fetch()` calls; the existing SW intercepts\n * them and routes to the right cache bucket. No direct Cache API access is\n * needed here — the SW is the single source of truth for storage.\n *\n * Throttling defaults are conservative to respect tile-server usage policies:\n * • 2 concurrent requests\n * • 50 ms inter-batch delay\n * • Standard browser User-Agent / Referer headers\n *\n * Usage:\n * const downloader = new OfflineTileDownloader({\n * baseMap: 'topo',\n * extent3857: [minX, minY, maxX, maxY], // EPSG:3857\n * minZoom: 10,\n * maxZoom: 15,\n * onProgress: (s) => console.log(s),\n * });\n * await downloader.start();\n * downloader.cancel(); // any time\n */\n\n// ============================================================================\n// Base-map URL templates\n// ============================================================================\n\n/**\n * Tile URL templates for base maps that may be downloaded for offline use.\n *\n * The SW recognises these hosts in `getTileCacheName()` and routes them to\n * the matching `tiles-*-vN` cache. If you add a new entry here, also add\n * the host to the SW's classifier or the tiles will not be cached.\n */\nexport const BASEMAP_TEMPLATES = {\n topo: {\n url: 'https://a.tile.opentopomap.org/{z}/{x}/{y}.png',\n label: 'Topographic',\n maxZoom: 17,\n cacheKey: 'tiles-topo',\n },\n osm: {\n url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png',\n label: 'OpenStreetMap',\n maxZoom: 19,\n cacheKey: 'tiles-osm',\n },\n};\n\n// Approximate bytes per raster tile — used for storage estimates.\nexport const AVG_TILE_BYTES = 30 * 1024;\n\n// ============================================================================\n// Tile coordinate math (Web Mercator XYZ scheme)\n// ============================================================================\n\nconst ORIGIN_SHIFT = 2 * Math.PI * 6378137 / 2; // 20037508.342789244\n\n/** Convert Web Mercator metres → (lon, lat) in degrees. */\nfunction metersToLonLat(x, y) {\n const lon = (x / ORIGIN_SHIFT) * 180;\n let lat = (y / ORIGIN_SHIFT) * 180;\n lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180)) - Math.PI / 2);\n return [lon, lat];\n}\n\n/** Tile (x, y) in XYZ scheme for a given lon/lat at zoom z. */\nfunction lonLatToTile(lon, lat, z) {\n const n = Math.pow(2, z);\n const x = Math.floor((lon + 180) / 360 * n);\n const latRad = lat * Math.PI / 180;\n const y = Math.floor(\n (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n\n );\n return { x, y };\n}\n\n/** Tile range covering an EPSG:3857 extent at a given zoom level. */\nexport function tileRangeForExtent(extent3857, z) {\n const [minX, minY, maxX, maxY] = extent3857;\n const [minLon, minLat] = metersToLonLat(minX, minY);\n const [maxLon, maxLat] = metersToLonLat(maxX, maxY);\n\n const tl = lonLatToTile(minLon, maxLat, z); // top-left in XYZ (NW)\n const br = lonLatToTile(maxLon, minLat, z); // bottom-right (SE)\n\n const n = Math.pow(2, z);\n const minTileX = Math.max(0, Math.min(tl.x, br.x));\n const maxTileX = Math.min(n - 1, Math.max(tl.x, br.x));\n const minTileY = Math.max(0, Math.min(tl.y, br.y));\n const maxTileY = Math.min(n - 1, Math.max(tl.y, br.y));\n\n return {\n z,\n minX: minTileX, maxX: maxTileX,\n minY: minTileY, maxY: maxTileY,\n count: (maxTileX - minTileX + 1) * (maxTileY - minTileY + 1),\n };\n}\n\n/** Total tile count for an extent across a zoom range (inclusive). */\nexport function countTiles(extent3857, minZ, maxZ) {\n let total = 0;\n for (let z = minZ; z <= maxZ; z++) {\n total += tileRangeForExtent(extent3857, z).count;\n }\n return total;\n}\n\n/**\n * Enumerate every tile in an extent across a zoom range.\n * Returns an array of { z, x, y } objects. For very large ranges this can be\n * large — the caller is expected to validate the count first.\n */\nexport function enumerateTiles(extent3857, minZ, maxZ) {\n const out = [];\n for (let z = minZ; z <= maxZ; z++) {\n const r = tileRangeForExtent(extent3857, z);\n for (let x = r.minX; x <= r.maxX; x++) {\n for (let y = r.minY; y <= r.maxY; y++) {\n out.push({ z, x, y });\n }\n }\n }\n return out;\n}\n\n/**\n * Format a tile URL for a given coordinate using a {z}/{x}/{y} template.\n */\nexport function formatTileUrl(template, { z, x, y }) {\n return template\n .replace('{z}', z)\n .replace('{x}', x)\n .replace('{y}', y);\n}\n\n// ============================================================================\n// OfflineTileDownloader\n// ============================================================================\n\n/**\n * Concurrent, throttled tile downloader. Issues `fetch()` per tile; the\n * service worker handles caching transparently.\n *\n * Events via `onProgress` callback:\n * { phase: 'running' | 'done' | 'cancelled' | 'error',\n * done, total, ok, failed, cached,\n * elapsedMs, etaMs }\n */\nexport class OfflineTileDownloader {\n constructor({\n baseMap, // 'topo' | 'osm'\n extent3857, // [minX, minY, maxX, maxY]\n minZoom,\n maxZoom,\n concurrency = 2, // OSM ToS-friendly default\n interBatchDelayMs = 50,\n onProgress = () => {},\n }) {\n const tpl = BASEMAP_TEMPLATES[baseMap];\n if (!tpl) throw new Error(`Unknown base map: ${baseMap}`);\n if (maxZoom > tpl.maxZoom) {\n console.warn(`[OfflineTiles] ${baseMap}: maxZoom ${maxZoom} > supported ${tpl.maxZoom}; clamping`);\n maxZoom = tpl.maxZoom;\n }\n\n this.baseMap = baseMap;\n this.template = tpl.url;\n this.extent = extent3857;\n this.minZoom = minZoom;\n this.maxZoom = maxZoom;\n this.concurrency = Math.max(1, Math.min(concurrency, 6));\n this.interBatchDelayMs = interBatchDelayMs;\n this.onProgress = onProgress;\n\n this._abortCtrl = null;\n this._cancelled = false;\n }\n\n /**\n * Begin downloading. Returns a Promise that resolves with the final stats\n * when complete, or when cancelled.\n */\n async start() {\n if (this._abortCtrl) throw new Error('Downloader already started');\n this._abortCtrl = new AbortController();\n this._cancelled = false;\n\n const tiles = enumerateTiles(this.extent, this.minZoom, this.maxZoom);\n const total = tiles.length;\n const startedAt = Date.now();\n\n let done = 0, ok = 0, failed = 0, cached = 0;\n\n const emit = (phase) => {\n const elapsedMs = Date.now() - startedAt;\n const etaMs = done > 0 ? Math.round((elapsedMs / done) * (total - done)) : null;\n this.onProgress({ phase, done, total, ok, failed, cached, elapsedMs, etaMs });\n };\n\n emit('running');\n\n // Process in chunks of `concurrency`\n for (let i = 0; i < tiles.length; i += this.concurrency) {\n if (this._cancelled) break;\n\n const batch = tiles.slice(i, i + this.concurrency);\n await Promise.all(batch.map(async (t) => {\n if (this._cancelled) return;\n const url = formatTileUrl(this.template, t);\n\n try {\n const res = await fetch(url, {\n signal: this._abortCtrl.signal,\n // Hint the SW that this is a passive prefetch\n cache: 'default',\n });\n\n if (res.ok) {\n ok++;\n // Detect \"served from SW cache\" via headers — not reliable across\n // implementations, so we just count all 200s as ok. Reading the body\n // (or cancelling it) lets the browser GC the response promptly.\n if (res.body) res.body.cancel().catch(() => {});\n } else if (res.status === 408) {\n // Our SW returns 408 when offline AND nothing cached. Treat as failed.\n failed++;\n } else {\n failed++;\n }\n } catch (err) {\n if (err.name === 'AbortError') {\n // Cancellation — don't count\n } else {\n failed++;\n }\n }\n done++;\n }));\n\n emit('running');\n\n if (this.interBatchDelayMs > 0 && i + this.concurrency < tiles.length) {\n await new Promise((r) => setTimeout(r, this.interBatchDelayMs));\n }\n }\n\n emit(this._cancelled ? 'cancelled' : 'done');\n\n return {\n phase: this._cancelled ? 'cancelled' : 'done',\n done, total, ok, failed, cached,\n elapsedMs: Date.now() - startedAt,\n };\n }\n\n /**\n * Cancel an in-flight download. Resolves on the next batch boundary.\n */\n cancel() {\n this._cancelled = true;\n if (this._abortCtrl) this._abortCtrl.abort();\n }\n}\n\n// ============================================================================\n// Predefined extents\n// ============================================================================\n\n/**\n * Whole-of-Ghana bounding box in EPSG:3857.\n * Approximate: -3.3°W → 1.2°E, 4.5°N → 11.2°N.\n */\nexport const GHANA_EXTENT_3857 = (() => {\n const lonLatToMeters = (lon, lat) => {\n const x = lon * ORIGIN_SHIFT / 180;\n const y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);\n return [x, y * ORIGIN_SHIFT / 180];\n };\n const sw = lonLatToMeters(-3.3, 4.5);\n const ne = lonLatToMeters(1.2, 11.2);\n return [sw[0], sw[1], ne[0], ne[1]];\n})();\n\n// Useful for size estimates\nexport function estimatedSizeBytes(tileCount) {\n return tileCount * AVG_TILE_BYTES;\n}\n","/**\n * Remote Database Module\n *\n * Handles all API communication with the PostgreSQL backend server.\n * Provides GET and POST methods for fetching and pushing data.\n *\n * Usage:\n * import { remoteGet, remotePost, getDistrictBoundary } from './remotedb.js';\n *\n * const boundary = await getDistrictBoundary();\n */\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\nconst API_BASE = 'https://api.lupmis4luspa.org/api/spatial_planning';\n\n/**\n * Per-request credentials sent with every API call.\n *\n * `district_id` is resolved dynamically — when the PWA is loaded via the PHP\n * entry point (public/index.php), the SSO session is injected into the page\n * as `window.LUPMIS_SESSION` and we read the authenticated user's district\n * from there. In local development (Vite serves index.html directly without\n * PHP), the global is undefined and we fall back to the hard-coded test\n * district below.\n *\n * `api_token` is currently a single global app token — not per-user.\n */\nconst FALLBACK_DISTRICT_ID = '1';\nconst API_TOKEN = '1c46538c712e9b5b';\n\n/**\n * Returns the authenticated user's district_id.\n *\n * - No SSO session at all (window.LUPMIS_SESSION undefined): we're in local\n * development → fall back to the hard-coded test district.\n * - Session present but no district_id: the user is authenticated but not\n * assigned to any district → return null. The bootstrap in main.js detects\n * this case BEFORE any API call and shows a blocking message; this null\n * is defence-in-depth so we never silently send district_id=1 for an\n * authenticated user.\n *\n * The getter runs on each spread of API_CREDENTIALS, so a session change at\n * runtime takes effect immediately.\n */\nfunction resolveDistrictId() {\n try {\n if (typeof window === 'undefined') return FALLBACK_DISTRICT_ID;\n const session = window.LUPMIS_SESSION;\n if (!session || typeof session !== 'object') return FALLBACK_DISTRICT_ID;\n const id = session.district_id;\n if (id === null || id === undefined || String(id).length === 0) return null;\n return String(id);\n } catch { /* no-op */ }\n return FALLBACK_DISTRICT_ID;\n}\n\nconst API_CREDENTIALS = {\n get district_id() { return resolveDistrictId(); },\n api_token: API_TOKEN,\n};\n\n/**\n * Get the full session payload (or null if not authenticated).\n * Exposed for UI code that wants to display the user's name, email, etc.\n *\n * Dev-mode helper: setting `localStorage['dev-session']` to a JSON object\n * (e.g. via `lupmisDevSession({...})` in the console) overrides the real\n * session — useful when running against `localhost:5173` to test the\n * authenticated UI without standing up a PHP server.\n */\nexport function getSession() {\n // 1. Real session injected by index.php (production)\n if (typeof window !== 'undefined' && window.LUPMIS_SESSION && window.LUPMIS_SESSION.user_id) {\n return window.LUPMIS_SESSION;\n }\n // 2. Dev-mode override (developer's own localStorage tweak)\n try {\n const raw = localStorage.getItem('dev-session');\n if (raw) {\n const parsed = JSON.parse(raw);\n if (parsed && parsed.user_id) return parsed;\n }\n } catch { /* ignore */ }\n return null;\n}\n\n// Console helper — set a fake session for dev work. Reload to apply.\n// lupmisDevSession({ user_id: 42, district_id: '1', full_name: 'Test User', ... })\n// Clear it via lupmisDevSession(null) or localStorage.removeItem('dev-session').\nif (typeof window !== 'undefined') {\n window.lupmisDevSession = (payload) => {\n if (payload == null) {\n localStorage.removeItem('dev-session');\n console.log('[Dev] Session override cleared. Reload to apply.');\n } else {\n localStorage.setItem('dev-session', JSON.stringify(payload));\n console.log('[Dev] Session override saved. Reload to apply:', payload);\n }\n };\n}\n\n// ============================================================================\n// Server Reachability\n// ============================================================================\n\n/** Default timeout for API requests (ms) */\nconst REQUEST_TIMEOUT = 30_000;\n\n/** Timeout for the fast reachability probe (ms) */\nconst PING_TIMEOUT = 5_000;\n\n/** Cached result of the last reachability check */\nlet _serverReachable = null;\n\n/**\n * Quick probe to determine if the API server is responding.\n * Sends a small POST to a lightweight endpoint with a short timeout.\n * The result is cached so subsequent calls within the same page load\n * return immediately.\n *\n * @param {boolean} [force=false] - Re-check even if a cached result exists\n * @returns {Promise} true if the server responded in time\n */\nexport async function checkServerReachable(force = false) {\n if (_serverReachable !== null && !force) return _serverReachable;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), PING_TIMEOUT);\n\n try {\n const response = await fetch(`${API_BASE}/get_layers.php`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },\n body: JSON.stringify(API_CREDENTIALS),\n signal: controller.signal,\n });\n _serverReachable = response.ok;\n } catch {\n _serverReachable = false;\n } finally {\n clearTimeout(timer);\n }\n\n console.log('[RemoteDB] Server reachable:', _serverReachable);\n return _serverReachable;\n}\n\n/**\n * Returns the cached server-reachability flag (synchronous).\n * Returns null if checkServerReachable() has not been called yet.\n * @returns {boolean|null}\n */\nexport function isServerReachable() {\n return _serverReachable;\n}\n\n// ============================================================================\n// Core Request Helpers\n// ============================================================================\n\n/**\n * Create an AbortController that auto-aborts after `ms` milliseconds.\n * If the caller already supplied a signal in `options`, it is combined\n * so that either the caller's abort or the timeout will cancel the request.\n */\nfunction withTimeout(options, ms = REQUEST_TIMEOUT) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), ms);\n\n // If the caller provided their own signal, chain it\n if (options.signal) {\n options.signal.addEventListener('abort', () => controller.abort());\n }\n\n return {\n signal: controller.signal,\n clear: () => clearTimeout(timer),\n };\n}\n\n/**\n * Perform a GET request to the remote API.\n * Credentials are sent as URL query parameters.\n * Automatically times out after REQUEST_TIMEOUT ms.\n *\n * @param {string} endpoint - API endpoint filename (e.g. 'get_district_boundary.php')\n * @param {Object} [params={}] - Additional query parameters\n * @param {Object} [options={}] - Extra fetch options\n * @returns {Promise} Parsed JSON response\n */\nexport async function remoteGet(endpoint, params = {}, options = {}) {\n const url = new URL(`${API_BASE}/${endpoint}`);\n\n // Attach credentials and any extra params as query string\n const allParams = { ...API_CREDENTIALS, ...params };\n for (const [key, value] of Object.entries(allParams)) {\n url.searchParams.set(key, value);\n }\n\n console.log('[RemoteDB] GET', url.toString());\n\n const timeout = withTimeout(options);\n try {\n const response = await fetch(url.toString(), {\n method: 'GET',\n headers: {\n 'Accept': 'application/json'\n },\n ...options,\n signal: timeout.signal,\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n console.log('[RemoteDB] GET response:', endpoint, '→', typeof data === 'object' ? `${Array.isArray(data) ? data.length + ' items' : 'object'}` : data);\n return data;\n\n } catch (error) {\n if (error.name === 'AbortError') {\n console.error('[RemoteDB] GET timed out:', endpoint);\n throw new Error(`Request timed out: ${endpoint}`);\n }\n console.error('[RemoteDB] GET failed:', endpoint, error);\n throw error;\n } finally {\n timeout.clear();\n }\n}\n\n/**\n * Perform a POST request to the remote API.\n * Credentials are included in the JSON body.\n * Automatically times out after REQUEST_TIMEOUT ms.\n *\n * @param {string} endpoint - API endpoint filename (e.g. 'some_endpoint.php')\n * @param {Object} [body={}] - Request payload (credentials are merged in)\n * @param {Object} [options={}] - Extra fetch options\n * @returns {Promise} Parsed JSON response\n */\nexport async function remotePost(endpoint, body = {}, options = {}) {\n const url = `${API_BASE}/${endpoint}`;\n\n const payload = { ...API_CREDENTIALS, ...body };\n\n console.log('[RemoteDB] POST', url);\n\n const timeout = withTimeout(options);\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json'\n },\n body: JSON.stringify(payload),\n ...options,\n signal: timeout.signal,\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const data = await response.json();\n console.log('[RemoteDB] POST response:', endpoint, '→', typeof data === 'object' ? `${Array.isArray(data) ? data.length + ' items' : 'object'}` : data);\n return data;\n\n } catch (error) {\n if (error.name === 'AbortError') {\n console.error('[RemoteDB] POST timed out:', endpoint);\n throw new Error(`Request timed out: ${endpoint}`);\n }\n console.error('[RemoteDB] POST failed:', endpoint, error);\n throw error;\n } finally {\n timeout.clear();\n }\n}\n\n// ============================================================================\n// Spatial Planning Endpoints\n// ============================================================================\n\n/**\n * Fetch district boundary geometry from the server.\n *\n * @returns {Promise} District boundary GeoJSON or API response\n */\nexport async function getDistrictBoundary() {\n return remotePost('get_district_boundary.php');\n}\n\n/**\n * Fetch the list of available map layer categories from the server.\n *\n * Response format:\n * { success: true, data: [{ id, name, description, createdt, editdt }, ...] }\n *\n * @returns {Promise} Layer categories list\n */\nexport async function getLayers() {\n return remotePost('get_layers.php');\n}\n\n/**\n * Fetch all collector zones for the current district.\n *\n * Expected response:\n * { success: true, data: [{ id, zone_name, boundary: \"MULTIPOLYGON(...)\", ... }, ...] }\n *\n * @returns {Promise} Collector zones list\n */\nexport async function getCollectorZones() {\n return remotePost('get_all_collector_zone_per_district.php');\n}\n\n/**\n * Fetch all parcels for the current district.\n *\n * Expected response:\n * { success: true, data: [{ id, ..., polygon: \"POLYGON(...)\" | \"MULTIPOLYGON(...)\", ... }, ...] }\n *\n * @returns {Promise} Parcels list\n */\nexport async function getDistrictParcels() {\n return remotePost('get_parcels_per_district.php');\n}\n\n/**\n * Fetch all building footprints for the current district.\n *\n * Expected response:\n * { success: true, data: [{ id, ..., polygon: \"POLYGON(...)\" | \"MULTIPOLYGON(...)\", ... }, ...] }\n *\n * @returns {Promise} Building footprints list\n */\nexport async function getBuildingFootprints() {\n return remotePost('get_all_footprint_per_district.php');\n}\n\n/**\n * Fetch the Contours hillshade elevation layer from the server.\n *\n * Source: table `be_contour_hillside` in the local PostgreSQL `public` schema\n * (imported from OpenTopography's viz.hh_hillshade).\n *\n * The current district_id is passed automatically via API_CREDENTIALS.\n *\n * Expected response:\n * { success: true, data: [{ id, elevation, geom: \"LINESTRING(...)\" | \"MULTILINESTRING(...)\" | \"POLYGON(...)\", ... }, ...] }\n *\n * @returns {Promise} Contours hillshade list\n */\nexport async function getContoursHillshade() {\n return remotePost('get_contours_hillshade.php');\n}\n\n/**\n * Fetch the OSM roads layer from the server.\n *\n * Source: table `pi_osm_roads` in the local PostgreSQL `public` schema\n * (imported from OpenStreetMap road network for the district).\n *\n * Expected response:\n * { success: true, data: [{ id, ..., geom: \"LINESTRING(...)\" | \"MULTILINESTRING(...)\", ... }, ...] }\n *\n * @returns {Promise} OSM roads list\n */\nexport async function getOSMRoads() {\n return remotePost('get_osm_roads.php');\n}\n\n/**\n * Push a recorded GPS trail (with all its points) to the server.\n *\n * Implements the GeoTracker \"sync adapter\" contract: store-and-forward — the\n * whole trail is uploaded once recording stops (and retried when back online).\n * `district_id` and `api_token` are attached automatically by remotePost().\n *\n * ── SERVER SIDE (NOT YET CREATED) ───────────────────────────────────────\n * Proposed endpoint: `save_gps_trail.php`\n * Proposed PostgreSQL/PostGIS tables (SRID 4326):\n *\n * CREATE TABLE be_gps_trail (\n * id SERIAL PRIMARY KEY,\n * client_uuid TEXT UNIQUE, -- de-dupe re-syncs\n * district_id INTEGER,\n * name TEXT,\n * started_at TIMESTAMPTZ,\n * ended_at TIMESTAMPTZ,\n * point_count INTEGER,\n * distance_m DOUBLE PRECISION,\n * track GEOMETRY(LineStringZ, 4326), -- optional aggregate line\n * createdt TIMESTAMPTZ DEFAULT now()\n * );\n * CREATE TABLE be_gps_trail_point (\n * id SERIAL PRIMARY KEY,\n * trail_id INTEGER REFERENCES be_gps_trail(id) ON DELETE CASCADE,\n * seq INTEGER,\n * geom GEOMETRY(PointZ, 4326),\n * accuracy DOUBLE PRECISION,\n * altitude DOUBLE PRECISION,\n * heading DOUBLE PRECISION,\n * speed DOUBLE PRECISION,\n * satellites INTEGER, -- nullable (web has no sat count)\n * recorded_at TIMESTAMPTZ\n * );\n *\n * Request body (JSON, plus injected credentials):\n * { client_uuid, name, started_at, ended_at, point_count, distance_m,\n * points: [ { seq, longitude, latitude, altitude, accuracy,\n * altitude_accuracy, heading, speed, satellites, recorded_at } ] }\n *\n * Expected response: { success: true, id: }\n * Should be idempotent on client_uuid (INSERT ... ON CONFLICT DO UPDATE).\n * ─────────────────────────────────────────────────────────────────────────\n *\n * @param {Object} trail local trail row (client_uuid, name, started_at, …)\n * @param {Array} points local point rows (seq, longitude, latitude, …)\n * @returns {Promise<{ remoteId: (string|number|null) }>}\n */\nexport async function pushGpsTrail(trail, points) {\n const payload = {\n client_uuid: trail.client_uuid,\n name: trail.name ?? null,\n started_at: trail.started_at,\n ended_at: trail.ended_at,\n point_count: trail.point_count ?? points.length,\n distance_m: trail.distance_m ?? 0,\n points: (points || []).map((p) => ({\n seq: p.seq,\n longitude: p.longitude,\n latitude: p.latitude,\n altitude: p.altitude ?? null,\n accuracy: p.accuracy ?? null,\n altitude_accuracy: p.altitude_accuracy ?? null,\n heading: p.heading ?? null,\n speed: p.speed ?? null,\n satellites: p.satellites ?? null,\n recorded_at: p.recorded_at,\n })),\n };\n const res = await remotePost('save_gps_trail.php', payload);\n return { remoteId: res?.id ?? res?.remote_id ?? null };\n}\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport default {\n getSession,\n checkServerReachable,\n isServerReachable,\n remoteGet,\n remotePost,\n getDistrictBoundary,\n getLayers,\n getDistrictParcels,\n getCollectorZones,\n getBuildingFootprints,\n getContoursHillshade,\n getOSMRoads,\n pushGpsTrail,\n};\n","/**\n * geo-utils.js — pure, dependency-free geospatial helpers for the GeoTracker\n * module. No browser APIs, no framework imports — safe to reuse anywhere\n * (including Node, web workers, or other projects).\n *\n * @module geotracker/geo-utils\n */\n\nconst EARTH_RADIUS_M = 6371008.8; // mean Earth radius (metres)\nconst DEG2RAD = Math.PI / 180;\n\n/**\n * Great-circle distance between two lon/lat points using the haversine\n * formula.\n *\n * @param {number} lon1\n * @param {number} lat1\n * @param {number} lon2\n * @param {number} lat2\n * @returns {number} distance in metres\n */\nexport function haversineMeters(lon1, lat1, lon2, lat2) {\n const dLat = (lat2 - lat1) * DEG2RAD;\n const dLon = (lon2 - lon1) * DEG2RAD;\n const a =\n Math.sin(dLat / 2) ** 2 +\n Math.cos(lat1 * DEG2RAD) * Math.cos(lat2 * DEG2RAD) * Math.sin(dLon / 2) ** 2;\n return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(a)));\n}\n\n/**\n * Total length of a polyline expressed as an array of points.\n * @param {Array<{lon:number, lat:number}>} points\n * @returns {number} metres\n */\nexport function pathLengthMeters(points) {\n let total = 0;\n for (let i = 1; i < points.length; i++) {\n total += haversineMeters(points[i - 1].lon, points[i - 1].lat, points[i].lon, points[i].lat);\n }\n return total;\n}\n\n/**\n * Format a latitude or longitude for compact display.\n * @param {number} value\n * @param {number} [decimals=5] ~1.1 m precision at 5 dp\n * @returns {string}\n */\nexport function formatCoord(value, decimals = 5) {\n if (value == null || Number.isNaN(value)) return '—';\n return value.toFixed(decimals);\n}\n\n/**\n * Format a distance in metres into a friendly string (m below 1 km, km above).\n * @param {number} meters\n * @returns {string}\n */\nexport function formatDistance(meters) {\n if (meters == null || Number.isNaN(meters)) return '—';\n if (meters < 1000) return `${Math.round(meters)} m`;\n return `${(meters / 1000).toFixed(2)} km`;\n}\n\n/**\n * Format a horizontal accuracy (metres) into a friendly ± string.\n * @param {number|null} meters\n * @returns {string}\n */\nexport function formatAccuracy(meters) {\n if (meters == null || Number.isNaN(meters)) return '—';\n return `±${Math.round(meters)} m`;\n}\n\n/**\n * Classify a horizontal accuracy into a qualitative fix-quality bucket. Useful\n * for colour-coding the UI without needing satellite count.\n * @param {number|null} meters\n * @returns {'good'|'fair'|'poor'|'none'}\n */\nexport function accuracyQuality(meters) {\n if (meters == null || Number.isNaN(meters)) return 'none';\n if (meters <= 10) return 'good';\n if (meters <= 30) return 'fair';\n return 'poor';\n}\n","/**\n * GeoTracker.js — a framework-agnostic GPS live-position + trail-recording\n * engine. It has **no** dependency on OpenLayers, Bootstrap, SQLocal, or any\n * LUPMIS code, so it can be dropped into any web project. Persistence and\n * server sync are provided by the host through small adapter objects.\n *\n * ─────────────────────────────────────────────────────────────────────────\n * STORAGE ADAPTER (required for recording) — all methods may be async:\n * createTrail(meta) -> trailId // meta: {uuid,name,startedAt,...}\n * addPoint(trailId, point) -> void // point: normalized fix (see below)\n * finishTrail(trailId, summary)-> void // summary: {endedAt,pointCount,distanceM}\n * getUnsyncedTrails() -> Array // trails with synced=0 and completed\n * getTrailPoints(trailId) -> Array\n * markTrailSynced(trailId, remoteId) -> void\n *\n * SYNC ADAPTER (optional) — store-and-forward:\n * pushTrail(trail, points) -> { remoteId } | throws\n * isOnline?() -> boolean // optional connectivity probe\n *\n * NORMALIZED FIX shape emitted on 'position' and stored via addPoint:\n * { lon, lat, accuracy, altitude, altitudeAccuracy, heading, speed,\n * satellites:null, timestamp }\n * (satellites is always null on the web Geolocation API — kept for parity\n * with native builds that can populate it.)\n * ─────────────────────────────────────────────────────────────────────────\n *\n * @module geotracker/GeoTracker\n */\n\nimport { haversineMeters } from './geo-utils.js';\n\n/** @typedef {'idle'|'watching'|'recording'} GeoTrackerState */\n\nconst DEFAULTS = {\n /** Minimum metres between two recorded trail points. */\n minDistanceM: 5,\n /** Ignore fixes arriving faster than this (throttle, ms). */\n minIntervalMs: 1000,\n /** Record a point at least this often even when stationary (heartbeat, ms). */\n heartbeatMs: 20000,\n /** Drop fixes worse than this horizontal accuracy (metres). 0 = accept all. */\n maxAccuracyM: 50,\n /** navigator.geolocation options. */\n enableHighAccuracy: true,\n timeoutMs: 15000,\n maximumAgeMs: 0,\n};\n\nexport class GeoTracker {\n /**\n * @param {object} [options]\n * @param {object} [options.storage] storage adapter (see module docs)\n * @param {object} [options.sync] sync adapter (see module docs)\n * @param {Geolocation} [options.geolocation] inject navigator.geolocation (for tests)\n * @param {number} [options.minDistanceM]\n * @param {number} [options.minIntervalMs]\n * @param {number} [options.heartbeatMs]\n * @param {number} [options.maxAccuracyM]\n * @param {boolean} [options.enableHighAccuracy]\n */\n constructor(options = {}) {\n this.opts = { ...DEFAULTS, ...options };\n this.storage = options.storage || null;\n this.sync = options.sync || null;\n this._geo = options.geolocation ||\n (typeof navigator !== 'undefined' ? navigator.geolocation : null);\n\n /** @type {GeoTrackerState} */\n this._state = 'idle';\n this._watchId = null;\n this._live = false; // live readout requested\n this._recording = false; // recording in progress\n\n this._activeTrailId = null;\n this._activeTrailUuid = null;\n this._lastRecorded = null; // last point actually written {lon,lat,timestamp}\n this._lastRecordedAt = 0;\n this._distanceM = 0;\n this._pointCount = 0;\n this._lastFix = null; // most recent normalized fix (any quality)\n\n /** @type {Record>} */\n this._listeners = Object.create(null);\n }\n\n // ── Events ────────────────────────────────────────────────────────────\n\n /**\n * Subscribe to an event. Returns an unsubscribe function.\n * Events: 'position' | 'point' | 'statechange' | 'trailstart' |\n * 'trailstop' | 'error' | 'syncstatus'\n * @param {string} event\n * @param {Function} cb\n * @returns {() => void}\n */\n on(event, cb) {\n (this._listeners[event] || (this._listeners[event] = new Set())).add(cb);\n return () => this._listeners[event]?.delete(cb);\n }\n\n _emit(event, payload) {\n const set = this._listeners[event];\n if (!set) return;\n for (const cb of set) {\n try { cb(payload); } catch (err) { console.error(`[GeoTracker] listener for \"${event}\" threw`, err); }\n }\n }\n\n // ── Public state ──────────────────────────────────────────────────────\n\n /** @returns {GeoTrackerState} */\n get state() { return this._state; }\n get isRecording() { return this._recording; }\n get lastFix() { return this._lastFix; }\n get isSupported() { return !!this._geo; }\n\n _setState(s) {\n if (this._state === s) return;\n this._state = s;\n this._emit('statechange', s);\n }\n\n // ── Live readout (watch without recording) ──────────────────────────────\n\n /**\n * Begin a position watch purely for the live readout (no trail is recorded).\n * Safe to call repeatedly.\n */\n startLive() {\n if (!this._geo) { this._emit('error', new Error('Geolocation not supported')); return; }\n this._live = true;\n this._ensureWatch();\n }\n\n /** Stop the live readout. Has no effect while a recording is in progress. */\n stopLive() {\n this._live = false;\n if (!this._recording) this._teardownWatch();\n }\n\n /**\n * One-shot position request (e.g. for a \"Locate me\" button). Resolves with a\n * normalized fix. Does not start/stop the watch.\n * @returns {Promise}\n */\n getCurrentPosition() {\n return new Promise((resolve, reject) => {\n if (!this._geo) { reject(new Error('Geolocation not supported')); return; }\n this._geo.getCurrentPosition(\n (pos) => {\n const fix = GeoTracker.normalize(pos);\n this._lastFix = fix;\n this._emit('position', fix);\n resolve(fix);\n },\n (err) => { this._emit('error', err); reject(err); },\n {\n enableHighAccuracy: this.opts.enableHighAccuracy,\n timeout: this.opts.timeoutMs,\n maximumAge: this.opts.maximumAgeMs,\n }\n );\n });\n }\n\n // ── Recording ───────────────────────────────────────────────────────────\n\n /**\n * Start recording a new trail. Creates the trail in storage, then records\n * filtered points as the device moves.\n * @param {object} [meta] e.g. { name, districtId }\n * @returns {Promise<{trailId:*, uuid:string}>}\n */\n async startRecording(meta = {}) {\n if (!this._geo) throw new Error('Geolocation not supported');\n if (!this.storage) throw new Error('GeoTracker: no storage adapter configured');\n if (this._recording) return { trailId: this._activeTrailId, uuid: this._activeTrailUuid };\n\n const uuid = GeoTracker.uuid();\n const startedAt = new Date().toISOString();\n const trailMeta = { uuid, name: meta.name || null, startedAt, ...meta };\n const trailId = await this.storage.createTrail(trailMeta);\n\n this._activeTrailId = trailId;\n this._activeTrailUuid = uuid;\n this._lastRecorded = null;\n this._lastRecordedAt = 0;\n this._distanceM = 0;\n this._pointCount = 0;\n this._recording = true;\n\n this._ensureWatch();\n this._setState('recording');\n this._emit('trailstart', { trailId, uuid, startedAt });\n return { trailId, uuid };\n }\n\n /**\n * Stop the active recording, finalise the trail summary, and (if a sync\n * adapter is present) attempt to push it immediately.\n * @returns {Promise<{trailId:*, pointCount:number, distanceM:number, synced:boolean}>}\n */\n async stopRecording() {\n if (!this._recording) return null;\n const trailId = this._activeTrailId;\n const endedAt = new Date().toISOString();\n const summary = { endedAt, pointCount: this._pointCount, distanceM: this._distanceM };\n\n this._recording = false;\n if (!this._live) this._teardownWatch();\n this._setState(this._live ? 'watching' : 'idle');\n\n try {\n await this.storage.finishTrail(trailId, summary);\n } catch (err) {\n this._emit('error', err);\n }\n this._emit('trailstop', { trailId, ...summary });\n\n let synced = false;\n if (this.sync) {\n try { synced = await this._syncTrail(trailId); }\n catch (err) { this._emit('error', err); }\n }\n\n this._activeTrailId = null;\n this._activeTrailUuid = null;\n return { trailId, pointCount: summary.pointCount, distanceM: summary.distanceM, synced };\n }\n\n // ── Sync (store-and-forward) ────────────────────────────────────────────\n\n /**\n * Push all completed-but-unsynced trails to the server via the sync adapter.\n * Call on app start and whenever connectivity returns.\n * @returns {Promise<{pushed:number, failed:number}>}\n */\n async syncPending() {\n if (!this.sync || !this.storage) return { pushed: 0, failed: 0 };\n if (this.sync.isOnline && !this.sync.isOnline()) return { pushed: 0, failed: 0 };\n\n let pushed = 0, failed = 0;\n const trails = await this.storage.getUnsyncedTrails();\n for (const trail of trails) {\n try {\n const ok = await this._syncTrail(trail.id ?? trail.trailId, trail);\n ok ? pushed++ : failed++;\n } catch (err) {\n failed++;\n this._emit('error', err);\n }\n }\n this._emit('syncstatus', { pushed, failed });\n return { pushed, failed };\n }\n\n /** @private push a single trail by id. */\n async _syncTrail(trailId, trailRow) {\n const points = await this.storage.getTrailPoints(trailId);\n const trail = trailRow || { id: trailId };\n const result = await this.sync.pushTrail(trail, points);\n const remoteId = result && (result.remoteId ?? result.id ?? null);\n await this.storage.markTrailSynced(trailId, remoteId);\n return true;\n }\n\n // ── Internal watch handling ──────────────────────────────────────────────\n\n /** @private start the geolocation watch if not already running. */\n _ensureWatch() {\n if (this._watchId != null || !this._geo) {\n if (this._state === 'idle' && this._live) this._setState('watching');\n return;\n }\n this._watchId = this._geo.watchPosition(\n (pos) => this._onFix(pos),\n (err) => this._emit('error', err),\n {\n enableHighAccuracy: this.opts.enableHighAccuracy,\n timeout: this.opts.timeoutMs,\n maximumAge: this.opts.maximumAgeMs,\n }\n );\n if (!this._recording) this._setState('watching');\n }\n\n /** @private stop the geolocation watch. */\n _teardownWatch() {\n if (this._watchId != null && this._geo) {\n this._geo.clearWatch(this._watchId);\n }\n this._watchId = null;\n }\n\n /** @private handle a raw Geolocation fix. */\n async _onFix(pos) {\n const fix = GeoTracker.normalize(pos);\n this._lastFix = fix;\n this._emit('position', fix); // always emit for the live readout\n\n if (!this._recording) return;\n\n const { minIntervalMs, minDistanceM, heartbeatMs, maxAccuracyM } = this.opts;\n const now = fix.timestamp;\n\n // Throttle very frequent fixes.\n if (this._lastRecordedAt && (now - this._lastRecordedAt) < minIntervalMs) return;\n // Drop low-quality fixes (unless this is the very first point).\n if (maxAccuracyM > 0 && fix.accuracy != null && fix.accuracy > maxAccuracyM && this._lastRecorded) return;\n\n let keep = false;\n let stepM = 0;\n if (!this._lastRecorded) {\n keep = true; // always record the first point\n } else {\n stepM = haversineMeters(this._lastRecorded.lon, this._lastRecorded.lat, fix.lon, fix.lat);\n const elapsed = now - this._lastRecordedAt;\n if (stepM >= minDistanceM || elapsed >= heartbeatMs) keep = true;\n }\n if (!keep) return;\n\n if (this._lastRecorded) this._distanceM += stepM;\n this._pointCount += 1;\n this._lastRecorded = { lon: fix.lon, lat: fix.lat, timestamp: now };\n this._lastRecordedAt = now;\n\n try {\n await this.storage.addPoint(this._activeTrailId, { ...fix, seq: this._pointCount });\n this._emit('point', {\n trailId: this._activeTrailId,\n seq: this._pointCount,\n point: fix,\n distanceM: this._distanceM,\n pointCount: this._pointCount,\n });\n } catch (err) {\n this._emit('error', err);\n }\n }\n\n // ── Static helpers ────────────────────────────────────────────────────────\n\n /** Normalize a browser GeolocationPosition into the module's fix shape. */\n static normalize(pos) {\n const c = pos.coords || {};\n const num = (v) => (v != null && !Number.isNaN(v) ? v : null);\n return {\n lon: c.longitude,\n lat: c.latitude,\n accuracy: num(c.accuracy),\n altitude: num(c.altitude),\n altitudeAccuracy: num(c.altitudeAccuracy),\n heading: num(c.heading),\n speed: num(c.speed),\n satellites: null, // not exposed by the web Geolocation API\n timestamp: pos.timestamp || Date.now(),\n };\n }\n\n /** RFC4122-ish UUID, using crypto when available. */\n static uuid() {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (ch) => {\n const r = (Math.random() * 16) | 0;\n const v = ch === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n }\n}\n\nexport default GeoTracker;\n","/**\n * geotracker-lupmis.js — LUPMIS2 integration layer for the reusable GeoTracker.\n *\n * This is the ONLY place that couples the generic src/geotracker/ engine to\n * LUPMIS specifics (SQLocal storage, the PHP sync endpoint, district id,\n * online checks). To reuse GeoTracker in another app, copy src/geotracker/ and\n * write a file like this one with that app's storage + sync adapters.\n *\n * @module geotracker-lupmis\n */\n\nimport { GeoTracker } from './geotracker/GeoTracker.js';\nimport {\n createGpsTrail,\n addGpsTrailPoint,\n finishGpsTrail,\n getUnsyncedGpsTrails,\n getGpsTrailPoints,\n markGpsTrailSynced,\n} from './database.js';\nimport { pushGpsTrail } from './remotedb.js';\nimport { isOnline } from './pwa.js';\nimport { getSession } from './remotedb.js';\n\n/**\n * Storage adapter — maps the GeoTracker contract onto the SQLocal helpers in\n * database.js. The district id is stamped onto each trail at creation time.\n */\nconst sqlocalStorage = {\n async createTrail(meta) {\n const districtId = meta.districtId\n ?? getSession()?.district_id\n ?? null;\n return createGpsTrail({ ...meta, districtId: districtId != null ? String(districtId) : null });\n },\n addPoint: (trailId, point) => addGpsTrailPoint(trailId, point),\n finishTrail: (trailId, summary) => finishGpsTrail(trailId, summary),\n getUnsyncedTrails: () => getUnsyncedGpsTrails(),\n getTrailPoints: (trailId) => getGpsTrailPoints(trailId),\n markTrailSynced: (trailId, remote) => markGpsTrailSynced(trailId, remote),\n};\n\n/**\n * Sync adapter — store-and-forward upload via the PHP endpoint. `isOnline()`\n * lets the tracker skip pushes while offline (it retries later).\n */\nconst remoteSync = {\n pushTrail: (trail, points) => pushGpsTrail(trail, points),\n isOnline: () => isOnline(),\n};\n\n/**\n * The configured, app-wide tracker instance. Tunables chosen for field\n * walking/driving: a point every ~5 m, throttled to ≥1 s, with a 20 s\n * heartbeat so stationary pauses still leave a breadcrumb, dropping fixes\n * worse than 50 m accuracy.\n */\nexport const geoTracker = new GeoTracker({\n storage: sqlocalStorage,\n sync: remoteSync,\n minDistanceM: 5,\n minIntervalMs: 1000,\n heartbeatMs: 20000,\n maxAccuracyM: 50,\n enableHighAccuracy: true,\n});\n\nexport default geoTracker;\n","/**\n * LUPMIS2 — iframe embed bridge\n *\n * Implements the postMessage contract defined in\n * LUPMIS2_Permit_Map_Integration.docx §2\n * which itself follows the iframe channel from\n * LUPMIS2_Reusable_Mapping_Concept.docx §3.2 / §4.\n *\n * Outbound (embed → host):\n * { type: 'ready' }\n * { type: 'parcel:select', upn, parcel_id, lon, lat,\n * zone_code, zone_name, landuse,\n * min_height, max_height }\n * { type: 'parcel:cleared' }\n * { type: 'error', code, message }\n *\n * Inbound (host → embed):\n * { type: 'set:view', lon, lat, zoom }\n * { type: 'set:selected', upn }\n * { type: 'clear:selected' }\n * { type: 'set:basemap', key }\n *\n * The bridge is framework-agnostic apart from the OpenLayers imports — it\n * lives one level above the proposed `map-core` library so once that library\n * is extracted (concept §3.1) this file can be lifted into it unchanged.\n *\n * Note on security: outbound messages are sent with target origin `*` because\n * the embed cannot know its parent's origin in advance and the payload is\n * non-sensitive (selection metadata only). The HOST is expected to verify\n * `event.origin === ''` before trusting any message, as\n * documented in §2.2 of the integration doc. Inbound commands are\n * type-checked against a strict whitelist before being acted on.\n */\n\nimport { fromLonLat, toLonLat } from 'ol/proj.js';\nimport { getCenter } from 'ol/extent.js';\nimport VectorLayer from 'ol/layer/Vector.js';\nimport VectorSource from 'ol/source/Vector.js';\nimport { Style, Stroke, Fill } from 'ol/style.js';\n\nconst KNOWN_COMMANDS = new Set([\n 'set:view',\n 'set:selected',\n 'clear:selected',\n 'set:basemap',\n]);\n\n/**\n * Create and install the embed bridge.\n *\n * @param {Object} opts\n * @param {Object} opts.mapView — the MapView instance (uses onClick,\n * getMap, setBaseMap).\n * @param {Object} opts.embedConfig — window.LUPMIS_EMBED contents.\n * @returns {{ attachParcelsLayer: (layer) => void, emitError: (code, msg) => void }}\n */\nexport function createEmbedBridge({ mapView, embedConfig }) {\n const map = mapView.getMap();\n const parent = (window.parent && window.parent !== window) ? window.parent : null;\n\n // -------------------------------------------------------------------------\n // Highlight layer — visual cue for the currently-selected parcel.\n // -------------------------------------------------------------------------\n const highlightSource = new VectorSource();\n const highlightLayer = new VectorLayer({\n source: highlightSource,\n zIndex: 9999,\n style: new Style({\n stroke: new Stroke({ color: '#f97316', width: 3 }),\n fill: new Fill({ color: 'rgba(249,115,22,0.18)' }),\n }),\n properties: {\n title: 'Permit selection',\n displayInLayerSwitcher: false,\n },\n });\n map.addLayer(highlightLayer);\n\n let parcelsLayer = null;\n let pendingSelectUpn = embedConfig?.upn ? String(embedConfig.upn) : null;\n let readyEmitted = false;\n\n // -------------------------------------------------------------------------\n // Outbound\n // -------------------------------------------------------------------------\n function send(message) {\n if (!parent) {\n console.warn('[embed-bridge] No parent window — would have sent:', message);\n return;\n }\n try {\n parent.postMessage(message, '*');\n } catch (e) {\n console.warn('[embed-bridge] postMessage failed:', e);\n }\n }\n\n function emitError(code, message) {\n send({ type: 'error', code, message });\n }\n\n function emitReady() {\n if (readyEmitted) return;\n readyEmitted = true;\n send({ type: 'ready' });\n }\n\n /** Build a `parcel:select` payload from a feature and (optionally) the click point. */\n function parcelPayload(feature, lon, lat) {\n const p = feature.getProperties();\n let outLon = lon, outLat = lat;\n if (outLon == null || outLat == null) {\n const ext = feature.getGeometry()?.getExtent();\n if (ext) {\n const [cx, cy] = toLonLat(getCenter(ext));\n outLon = cx; outLat = cy;\n }\n }\n return {\n type: 'parcel:select',\n upn: p.upn ?? null,\n parcel_id: p.id ?? null,\n lon: outLon ?? null,\n lat: outLat ?? null,\n zone_code: p.zone_code ?? null,\n zone_name: p.zone_name ?? null,\n landuse: p.landuse ?? null,\n min_height: p.min_height ?? null,\n max_height: p.max_height ?? null,\n };\n }\n\n function highlightFeature(feature) {\n highlightSource.clear();\n if (feature) {\n const clone = feature.clone();\n highlightSource.addFeature(clone);\n }\n }\n\n // -------------------------------------------------------------------------\n // Click → parcel:select / parcel:cleared\n // -------------------------------------------------------------------------\n mapView.onClick((lon, lat, _markerFeature, evt) => {\n let parcelFeature = null;\n map.forEachFeatureAtPixel(evt.pixel, (f) => {\n if (f.get('_layerType') === 'parcel') {\n parcelFeature = f;\n return true;\n }\n });\n if (parcelFeature) {\n highlightFeature(parcelFeature);\n send(parcelPayload(parcelFeature, lon, lat));\n } else {\n highlightFeature(null);\n send({ type: 'parcel:cleared' });\n }\n });\n\n // -------------------------------------------------------------------------\n // Inbound commands\n // -------------------------------------------------------------------------\n window.addEventListener('message', (event) => {\n const msg = event.data;\n if (!msg || typeof msg !== 'object' || !KNOWN_COMMANDS.has(msg.type)) return;\n try {\n switch (msg.type) {\n case 'set:view': {\n if (typeof msg.lon === 'number' && typeof msg.lat === 'number') {\n const view = map.getView();\n view.setCenter(fromLonLat([msg.lon, msg.lat]));\n if (typeof msg.zoom === 'number') view.setZoom(msg.zoom);\n }\n break;\n }\n case 'set:selected':\n if (msg.upn) selectByUpn(String(msg.upn));\n break;\n case 'clear:selected':\n highlightFeature(null);\n pendingSelectUpn = null;\n break;\n case 'set:basemap':\n if (msg.key && typeof mapView.setBaseMap === 'function') {\n mapView.setBaseMap(msg.key);\n }\n break;\n }\n } catch (e) {\n emitError('COMMAND_FAILED', `Failed to handle ${msg.type}: ${e.message}`);\n }\n });\n\n /**\n * Find the parcel with the given UPN, highlight it, fit the view to it,\n * and emit a synthesized parcel:select so the host receives the metadata.\n * If the parcels haven't finished loading yet, the UPN is queued and the\n * lookup is retried as features stream in.\n */\n function selectByUpn(upn) {\n if (!parcelsLayer) { pendingSelectUpn = upn; return; }\n const features = parcelsLayer.getSource().getFeatures();\n const feature = features.find((f) => String(f.get('upn') ?? '') === upn);\n if (!feature) { pendingSelectUpn = upn; return; }\n\n pendingSelectUpn = null;\n highlightFeature(feature);\n const ext = feature.getGeometry()?.getExtent();\n if (ext) {\n map.getView().fit(ext, { padding: [50, 50, 50, 50], duration: 400, maxZoom: 17 });\n }\n send(parcelPayload(feature, null, null));\n }\n\n // -------------------------------------------------------------------------\n // Parcels attached → emit `ready`, drain any pending set:selected\n // -------------------------------------------------------------------------\n function attachParcelsLayer(layer) {\n parcelsLayer = layer;\n const source = layer.getSource();\n\n const drain = () => {\n // Microtask hop so a batch addFeatures() finishes before we react.\n queueMicrotask(() => {\n if (pendingSelectUpn) selectByUpn(pendingSelectUpn);\n emitReady();\n });\n };\n\n if (source.getFeatures().length > 0) {\n drain();\n } else {\n // Stay subscribed: parcels may arrive in waves (cache then API refresh),\n // and a pending UPN may only resolve after the second wave.\n let scheduled = false;\n source.on('addfeature', () => {\n if (scheduled) return;\n scheduled = true;\n queueMicrotask(() => {\n scheduled = false;\n if (pendingSelectUpn) selectByUpn(pendingSelectUpn);\n emitReady();\n });\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // Apply initial config: basemap + view (UPN is handled after parcels load)\n // -------------------------------------------------------------------------\n if (embedConfig?.basemap && typeof mapView.setBaseMap === 'function') {\n mapView.setBaseMap(embedConfig.basemap);\n }\n if (typeof embedConfig?.lon === 'number' && typeof embedConfig?.lat === 'number') {\n const view = map.getView();\n view.setCenter(fromLonLat([embedConfig.lon, embedConfig.lat]));\n view.setZoom(typeof embedConfig?.zoom === 'number' ? embedConfig.zoom : 15);\n }\n\n return { attachParcelsLayer, emitError };\n}\n","/**\n * Main Application Entry Point\n *\n * Demonstrates integration of:\n * - Bootstrap 5.3 for UI components\n * - SQLocal (SQLite in browser via OPFS)\n * - BroadcastChannel for cross-tab sync\n * - OpenLayers map with ol-ext LayerSwitcher\n * - PWA features (Service Worker, install prompt, offline detection)\n */\n\n// Bootstrap CSS and JS\nimport 'bootstrap/dist/css/bootstrap.min.css';\nimport 'bootstrap-icons/font/bootstrap-icons.css';\nimport { Modal, Offcanvas } from 'bootstrap';\n\n// Database module (uses SQLocal directly, BroadcastChannel for tab sync)\nimport {\n sql,\n dbReady,\n initSchema,\n addLocation,\n getLocations,\n getLocationCount,\n getDatabaseStatus,\n downloadDatabase,\n onDatabaseChange,\n exportToGeoJSON,\n saveRemoteData,\n getRemoteData,\n saveCollectorZones,\n getLocalCollectorZones,\n saveParcels,\n getLocalParcels,\n updateParcel,\n insertNewParcel,\n saveBuildingFootprints,\n getLocalBuildingFootprints,\n saveOSMRoads,\n getLocalOSMRoads,\n isCachedLayerTable,\n clearTable,\n clearAllCachedLayers,\n getTableStats,\n getTableContent\n} from './src/database.js';\n\n// Map component with OpenLayers and ol-ext LayerSwitcher\nimport { MapView } from './src/components/MapView.js';\n\n// OpenLayers GeoJSON format (for updating layer sources directly)\nimport GeoJSON from 'ol/format/GeoJSON';\n\n// OpenLayers WKT format (for writing drawn polygon geometries to database)\nimport WKT from 'ol/format/WKT';\n\n// OpenLayers KML format (for KML file import)\nimport KML from 'ol/format/KML';\n\n// Shapefile parser (reads .zip containing .shp/.dbf/.shx/.prj)\n// Lazy-loaded — only fetched the first time the user imports a shapefile.\nlet _shpModule = null;\nasync function getShp() {\n if (!_shpModule) {\n const mod = await import('shpjs');\n _shpModule = mod.default || mod;\n }\n return _shpModule;\n}\n\n// Map measurement and drawing tools\nimport { MapTools } from './src/components/MapTools.js';\n\n// PWA module (registers Service Worker, handles install/offline)\nimport { initPWA, isOnline, onOfflineChange, getTileCacheStats, clearTileCaches, clearTileCacheForProvider, getStorageEstimate, onServiceWorkerControllerChange } from './src/pwa.js';\nimport {\n BASEMAP_TEMPLATES,\n GHANA_EXTENT_3857,\n countTiles,\n estimatedSizeBytes,\n OfflineTileDownloader,\n} from './src/offlineTiles.js';\n\n// Remote database API (PostgreSQL backend)\nimport { checkServerReachable, isServerReachable, getDistrictBoundary, getLayers, getCollectorZones, getDistrictParcels, getBuildingFootprints, getContoursHillshade, getOSMRoads, getSession } from './src/remotedb.js';\n\n// GPS live-position + trail recording (reusable engine + LUPMIS wiring)\nimport { geoTracker } from './src/geotracker-lupmis.js';\nimport { formatCoord, formatAccuracy, formatDistance, accuracyQuality } from './src/geotracker/geo-utils.js';\n\n// Iframe embed bridge (see public/embed.php + LUPMIS2_Permit_Map_Integration.docx)\nimport { createEmbedBridge } from './src/embed-bridge.js';\n\n// Map instance (global for access across functions)\nlet mapView = null;\nlet mapTools = null;\n// Module-level reference so the embed bridge can access the parcels layer\n// once loadParcels() has created it.\nlet parcelsLayer = null;\nlet embedBridge = null;\n\n// Iframe embed mode. Set by public/embed.php when serving the /embed route;\n// undefined for the normal /index.php entry point.\nconst EMBED_CONFIG = (typeof window !== 'undefined' && window.LUPMIS_EMBED) || null;\nconst IS_EMBED_PERMIT = !!(EMBED_CONFIG && EMBED_CONFIG.mode === 'permit');\n\n// Current interaction mode: 'addLocation' | 'measureCircle' | 'measureLine' | 'measureArea'\n// In embed permit mode we don't want the default Add-Location click to fire,\n// so start the mode in a neutral state.\nlet currentMode = IS_EMBED_PERMIT ? 'embed-permit' : 'addLocation';\n\n// ============================================================================\n// Application Initialization\n// ============================================================================\n\n/**\n * Pre-flight: when an SSO session is present but the user has no district\n * assigned, the app cannot function (every API call is scoped to a district).\n * Show a blocking message and halt initialisation so we never silently fall\n * back to a default district.\n *\n * Local dev (no window.LUPMIS_SESSION at all) is *not* affected — that path\n * still uses the remotedb FALLBACK_DISTRICT_ID for testing.\n *\n * @returns {boolean} true if the user is blocked (init should abort)\n */\nfunction showNoDistrictBlockerIfNeeded() {\n const session = (typeof window !== 'undefined') ? window.LUPMIS_SESSION : null;\n if (!session || typeof session !== 'object') return false; // dev mode\n const id = session.district_id;\n if (id !== null && id !== undefined && String(id).length > 0) return false;\n\n // Authenticated but no district — render an overlay and abort init.\n console.warn('[App] Authenticated user has no district assigned; halting init.');\n const overlay = document.createElement('div');\n overlay.id = 'no-district-overlay';\n overlay.setAttribute('role', 'alertdialog');\n overlay.setAttribute('aria-modal', 'true');\n overlay.style.cssText =\n 'position:fixed;inset:0;z-index:99999;display:flex;align-items:center;' +\n 'justify-content:center;background:rgba(255,255,255,0.98);padding:24px;';\n const name = session.full_name || session.username || 'You';\n overlay.innerHTML = `\n
                        \n
                        🛑
                        \n

                        \n No district assigned\n

                        \n

                        \n ${escapeHtml(name)}, your user profile is not associated with any\n district. LUPMIS2 cannot load the relevant map data without one.\n

                        \n

                        \n Please contact the system administrator to have a district assigned\n to your account.\n

                        \n \n
                        `;\n document.body.appendChild(overlay);\n overlay.querySelector('#no-district-portal-btn')?.addEventListener('click', () => {\n window.location.href = 'https://lupmis4luspa.org/';\n });\n return true;\n}\n\nasync function initApp() {\n console.log('[App] Initializing...');\n\n // Pre-flight: authenticated user must have a district assigned.\n if (showNoDistrictBlockerIfNeeded()) return;\n\n // 1. Initialize PWA features (Service Worker, install prompt, offline detection)\n await initPWA({\n installButton: '#install-btn',\n offlineIndicator: '#offline-indicator',\n autoRegisterSW: true\n });\n\n // 2. Initialize the map\n // Restore the user's preferred default base map from localStorage\n const savedBasemap = localStorage.getItem('default-basemap') || 'topo';\n\n mapView = new MapView('map', {\n center: [-1.5, 7.5], // Ghana\n zoom: 7,\n basemap: savedBasemap,\n });\n\n // Initialize map measurement tools\n mapTools = new MapTools(mapView.getMap());\n\n // Wire up GPS live-position + trail recording\n initGpsTracking();\n\n // Handle measurement results\n mapTools.onMeasureComplete((result) => {\n console.log('[MapTools] Measurement complete:', result);\n\n // Only show the Polygon Attributes popup for polygons drawn with the\n // Draw tool — NOT for area measurements (which have _layerType = 'measure_area').\n if (result.type === 'polygon' && result.coordinate) {\n const lt = result.feature?.get('_layerType');\n if (lt !== 'measure_area') {\n mapView?.showDrawnPolygonPopup(result.feature, result.coordinate);\n }\n }\n });\n\n // Category emojis are set up in MapView:\n // 'water': '💧', 'school': '🏫', 'health': '🏥',\n // 'market': '🏪', 'default': '📍', 'other': '📌'\n\n // In iframe embed permit mode, install the postMessage bridge BEFORE the\n // regular handlers so its outbound parcel:select / parcel:cleared events\n // are wired up; the regular click/dblclick handlers below short-circuit in\n // that mode (the bridge owns map interaction in the embed).\n if (IS_EMBED_PERMIT) {\n embedBridge = createEmbedBridge({ mapView, embedConfig: EMBED_CONFIG });\n }\n\n // Set up map click handler immediately after map creation\n mapView.onClick((lon, lat, feature, evt) => {\n // Embed permit mode: the bridge handles parcel selection itself; the\n // normal popup/add-location behaviour does not apply.\n if (IS_EMBED_PERMIT) return;\n\n console.log('[MapClick] Clicked at:', lon.toFixed(4), lat.toFixed(4));\n console.log('[MapClick] currentMode =', currentMode);\n\n // In draw or measurement modes, clicks drive the tool — don't\n // open popups or select features.\n if (currentMode === 'draw' || currentMode.startsWith('measure')) {\n return;\n }\n\n // Check if a parcel feature was clicked\n let parcelFeature = null;\n mapView.getMap().forEachFeatureAtPixel(evt.pixel, (f) => {\n if (f.get('_layerType') === 'parcel') {\n parcelFeature = f;\n return true; // stop at first parcel hit\n }\n });\n\n // Parcel click: open Edit Attributes form in ANY non-draw mode.\n // The feature is NOT selected — only the popup is shown.\n if (parcelFeature) {\n console.log('[MapClick] Clicked on parcel → Edit Attributes');\n mapView.showParcelEditPopup(parcelFeature, evt.coordinate);\n return;\n }\n\n // Non-parcel clicks (markers, empty space) only in addLocation mode\n if (currentMode !== 'addLocation') {\n return;\n }\n\n if (feature) {\n // Clicked on existing marker - select it and show details\n console.log('[MapClick] Clicked on marker:', feature.getId());\n mapView.selectMarker(feature);\n showLocationDetails(feature);\n } else {\n // Clicked on empty space - show add location popup at click position\n console.log('[MapClick] Empty space → Add Location popup');\n mapView.clearSelection();\n mapView.showAddLocationPopup(evt.coordinate);\n }\n });\n\n // Set up double-click handler for overlay feature info\n // Uses '_layerType' property to distinguish zone features from other layers\n mapView.onDblClick((lon, lat, feature, evt) => {\n // Embed permit mode shows no info popups (the host owns the UI).\n if (IS_EMBED_PERMIT) return;\n if (!feature) return;\n\n const layerType = feature.get('_layerType');\n console.log('[App] Double-click on feature, _layerType:', layerType || 'none');\n\n if (layerType === 'measure_circle') {\n // Circle measurement: show intersection analysis with other layers\n mapView.showCircleIntersectionPopup(feature, evt.coordinate);\n } else if (layerType === 'measure_circle_radius') {\n // Clicked on the radius line — ignore\n return;\n } else if (layerType === 'measure_area') {\n // Area measurement polygon: show intersection analysis\n mapView.showAreaIntersectionPopup(feature, evt.coordinate);\n } else if (layerType === 'collector_zone') {\n mapView.showInfoPopup(feature, evt.coordinate, {\n title: 'Zone Info',\n color: '#7c3aed',\n });\n } else if (layerType === 'parcel') {\n mapView.showInfoPopup(feature, evt.coordinate, {\n title: 'Parcel Info',\n color: '#0ea5e9',\n });\n } else {\n mapView.showInfoPopup(feature, evt.coordinate, {\n title: 'Feature Info',\n color: '#e11d48',\n });\n }\n });\n\n // Set up handler for the map add location popup form\n mapView.onAddLocation(async (data) => {\n console.log('[App] Add location from map popup:', data);\n try {\n const result = await addLocation(data.name, data.lon, data.lat, {\n description: data.description || null,\n category: data.category || 'default'\n });\n console.log('[App] Location added:', data.name, 'id:', result.id);\n\n await loadLocations();\n\n // Zoom to the new location on the map\n mapView?.zoomTo(data.lon, data.lat, 14);\n\n // Select the new marker\n if (result.id) {\n mapView?.selectMarker(result.id);\n }\n\n showSuccess('Location added successfully');\n\n } catch (error) {\n console.error('[App] Failed to add location:', error);\n showError('Failed to add location: ' + error.message);\n }\n });\n\n // Set up parcel edit save handler\n mapView.onParcelEdit(async (feature, updatedProps) => {\n const parcelId = updatedProps.id || updatedProps.parcelid || updatedProps.parcel_id;\n console.log('[App] Parcel edit saved:', parcelId, updatedProps);\n\n if (!parcelId) {\n console.warn('[App] No parcel ID found in updated properties — skipping local save');\n return;\n }\n\n try {\n await updateParcel(parcelId, updatedProps);\n showSuccess('Parcel updated locally');\n } catch (error) {\n console.error('[App] Failed to save parcel update:', error);\n showError('Failed to save parcel: ' + error.message);\n }\n });\n\n // Set up drawn polygon attribute save handler\n const wktFormat = new WKT();\n mapView.onDrawnPolygonSave(async (feature, props) => {\n console.log('[App] Drawn polygon attributes saved:', props);\n\n try {\n // Convert the OL geometry (EPSG:3857) to WKT in EPSG:4326 for storage\n const wktString = wktFormat.writeGeometry(feature.getGeometry(), {\n dataProjection: 'EPSG:4326',\n featureProjection: 'EPSG:3857',\n });\n\n const result = await insertNewParcel(wktString, props);\n console.log('[App] New parcel inserted with id:', result.id);\n showSuccess('New parcel saved (pending verification)');\n } catch (error) {\n console.error('[App] Failed to save new parcel:', error);\n showError('Failed to save parcel: ' + error.message);\n }\n });\n\n // 3. Initialize database\n try {\n console.log('[App] Initializing database...');\n\n // Initialize schema (creates tables if they don't exist)\n // This also resolves dbReady when complete\n await initSchema();\n\n // Now dbReady should be resolved\n console.log('[App] Database ready');\n\n // Show database status\n const status = await getDatabaseStatus();\n console.log('[App] Database status:', status);\n\n // Quick server reachability check (5 s timeout) — if the API server\n // is down, all load functions will skip remote fetches and fall back\n // to local cached data immediately, keeping the app responsive.\n if (isOnline()) {\n const reachable = await checkServerReachable();\n if (!reachable) {\n console.warn('[App] API server unreachable — using local data only');\n showWarning('Server not responding — loading cached data.');\n }\n }\n\n // Load remote overlays (needs remote_data table from initSchema)\n // loadLayers must complete first so the layer groups exist\n // before loadDistrictBoundary adds into the Administration group.\n await loadLayers();\n\n // Initialise EditBar with its own \"Drawings\" layer group\n mapView?.initEditBar();\n\n loadDistrictBoundary();\n loadCollectorZones();\n loadParcels();\n // In embed permit mode the parcels layer is the user's working surface,\n // so make it visible immediately and hand the layer to the bridge so it\n // can emit `ready` (and resolve any pending `set:selected` UPN) once the\n // features arrive. loadParcels() runs its synchronous prologue (creating\n // the layer and assigning the module-level reference) before returning\n // its promise, so `parcelsLayer` is already set here.\n if (IS_EMBED_PERMIT && embedBridge && parcelsLayer) {\n parcelsLayer.setVisible(true);\n embedBridge.attachParcelsLayer(parcelsLayer);\n }\n loadBuildingFootprints();\n loadContoursHillshade();\n loadOSMRoads();\n loadExternalWMSLayers();\n\n } catch (error) {\n console.error('[App] Database initialization failed:', error);\n showError('Failed to initialize database. Please refresh the page.');\n return;\n }\n\n // 4. Initialize UI\n initUI();\n\n // 5. Load initial data and display on map\n await loadLocations();\n\n // 6. Listen for database changes (local + other tabs)\n onDatabaseChange((change) => {\n console.log('[App] Database change:', change);\n if (change.table === 'locations' && !change.local) {\n // Reload locations when another tab makes changes\n loadLocations();\n }\n if (change.table === 'parcels') {\n // Refresh the Local Data stats panel if it is visible\n const statsContainer = document.getElementById('local-data-stats');\n if (statsContainer && !statsContainer.classList.contains('d-none')) {\n refreshLocalDataStats();\n }\n }\n });\n\n // 7. Set up offline handling\n onOfflineChange((offline) => {\n if (offline) {\n console.log('[App] Working offline - data will sync when back online');\n } else {\n console.log('[App] Back online - syncing data...');\n syncData();\n }\n });\n\n // 8. Fieldwork mode (high-contrast + large touch targets)\n initFieldworkMode();\n\n // 9. Measurement system toggle (metric / imperial)\n initMeasurementSystem();\n\n // 10. Dark mode\n initDarkMode();\n\n // 11. Default base map selector\n initDefaultBasemap();\n\n // 12. Offline tile-cache stats card\n initOfflineTileCache();\n\n // 13. Offline-download dialog\n initOfflineDownloadDialog();\n\n // 14. Account card (signed-in user + sign-out)\n initAccountCard();\n\n console.log('[App] Initialized successfully');\n}\n\n// ============================================================================\n// UI Initialization\n// ============================================================================\n\nfunction initUI() {\n console.log('[initUI] Starting UI initialization...');\n\n // Message log (persistent stack in right panel)\n initMessageLog();\n\n // Export button\n const exportBtn = document.getElementById('export-btn');\n if (exportBtn) {\n exportBtn.addEventListener('click', handleExport);\n }\n\n // Local Data button — shows tables and record counts\n const localDataBtn = document.getElementById('local-data-btn');\n if (localDataBtn) {\n localDataBtn.addEventListener('click', () => refreshLocalDataStats());\n }\n\n // File import buttons (Shapefile, GeoJSON, KML)\n const importShpBtn = document.getElementById('import-shp-btn');\n const shpFileInput = document.getElementById('shp-file-input');\n if (importShpBtn && shpFileInput) {\n importShpBtn.addEventListener('click', () => shpFileInput.click());\n shpFileInput.addEventListener('change', handleShapefileImport);\n }\n\n const importGeoJSONBtn = document.getElementById('import-geojson-btn');\n const geojsonFileInput = document.getElementById('geojson-file-input');\n if (importGeoJSONBtn && geojsonFileInput) {\n importGeoJSONBtn.addEventListener('click', () => geojsonFileInput.click());\n geojsonFileInput.addEventListener('change', handleGeoJSONImport);\n }\n\n const importKMLBtn = document.getElementById('import-kml-btn');\n const kmlFileInput = document.getElementById('kml-file-input');\n if (importKMLBtn && kmlFileInput) {\n importKMLBtn.addEventListener('click', () => kmlFileInput.click());\n kmlFileInput.addEventListener('change', handleKMLImport);\n }\n\n // Drag-and-drop file import on the map\n initMapDropZone();\n\n // GeoJSON Export button\n const exportGeoJSONBtn = document.getElementById('exportGeoJSON-btn');\n if (exportGeoJSONBtn) {\n exportGeoJSONBtn.addEventListener('click', handleExportGeoJSON);\n }\n\n // Status button\n const statusBtn = document.getElementById('status-btn');\n if (statusBtn) {\n statusBtn.addEventListener('click', handleShowStatus);\n }\n\n // Fit to markers button\n const fitBtn = document.getElementById('fit-btn');\n if (fitBtn) {\n fitBtn.addEventListener('click', () => mapView?.fitToMarkers());\n }\n\n // ============================================\n // Mode Selector & Measurement Tools (Bottom Dock)\n // ============================================\n\n const addLocationBtn = document.getElementById('dock-btn-add-location');\n const measureCircleBtn = document.getElementById('dock-btn-measure-circle');\n const measureLineBtn = document.getElementById('dock-btn-measure-line');\n const measureAreaBtn = document.getElementById('dock-btn-measure-area');\n const drawBtn = document.getElementById('dock-btn-draw');\n const clearBtn = document.getElementById('dock-btn-clear');\n\n // Debug: Check if buttons are found\n console.log('[initUI] Buttons found:', {\n addLocation: !!addLocationBtn,\n measureCircle: !!measureCircleBtn,\n measureLine: !!measureLineBtn,\n measureArea: !!measureAreaBtn,\n draw: !!drawBtn,\n clear: !!clearBtn\n });\n\n // All mode buttons (mutually exclusive)\n const modeButtons = [addLocationBtn, measureCircleBtn, measureLineBtn, measureAreaBtn, drawBtn];\n\n // Helper to set active mode and update button states\n // Note: This updates the module-level currentMode variable\n const setMode = (mode, activeBtn) => {\n console.log('[setMode] Changing mode from', currentMode, 'to', mode);\n currentMode = mode;\n console.log('[setMode] currentMode is now:', currentMode);\n\n // Update button active states\n modeButtons.forEach(btn => {\n if (btn) btn.classList.toggle('active', btn === activeBtn);\n });\n\n // Deactivate any measurement tool when switching modes\n mapTools?.deactivate();\n\n // Leave edit mode when switching away from draw\n if (mode !== 'draw') {\n mapView?.setEditMode(false);\n }\n\n // Hide add location popup when leaving addLocation mode\n if (mode !== 'addLocation') {\n mapView?.hideAddLocationPopup();\n }\n\n // Activate the appropriate tool for the new mode\n switch (mode) {\n case 'measureCircle':\n mapTools?.startCircleMeasure();\n break;\n case 'measureLine':\n mapTools?.startLineMeasure();\n break;\n case 'measureArea':\n mapTools?.startAreaMeasure();\n break;\n case 'draw':\n mapView?.setEditMode(true);\n break;\n // addLocation mode doesn't need tool activation\n }\n };\n\n // Add Location mode button\n if (addLocationBtn) {\n addLocationBtn.addEventListener('click', () => {\n console.log('[Button] Add Location clicked');\n setMode('addLocation', addLocationBtn);\n });\n }\n\n // Circle measurement button\n if (measureCircleBtn) {\n measureCircleBtn.addEventListener('click', () => {\n console.log('[Button] Circle clicked, currentMode is:', currentMode);\n if (currentMode === 'measureCircle') {\n // Toggle off - return to addLocation mode\n setMode('addLocation', addLocationBtn);\n } else {\n setMode('measureCircle', measureCircleBtn);\n}\n });\n }\n\n // Line measurement button\n if (measureLineBtn) {\n measureLineBtn.addEventListener('click', () => {\n console.log('[Button] Line clicked, currentMode is:', currentMode);\n if (currentMode === 'measureLine') {\n setMode('addLocation', addLocationBtn);\n } else {\n setMode('measureLine', measureLineBtn);\n }\n });\n }\n\n // Area measurement button\n if (measureAreaBtn) {\n measureAreaBtn.addEventListener('click', () => {\n console.log('[Button] Area clicked, currentMode is:', currentMode);\n if (currentMode === 'measureArea') {\n setMode('addLocation', addLocationBtn);\n } else {\n setMode('measureArea', measureAreaBtn);\n }\n });\n }\n\n // Draw / Edit button\n if (drawBtn) {\n drawBtn.addEventListener('click', () => {\n console.log('[Button] Draw clicked, currentMode is:', currentMode);\n if (currentMode === 'draw') {\n setMode('addLocation', addLocationBtn);\n } else {\n setMode('draw', drawBtn);\n }\n });\n }\n\n // Clear button - clears measurements but stays in current mode\n if (clearBtn) {\n clearBtn.addEventListener('click', () => {\n mapTools?.clearMeasurements();\n // If in a measurement mode, restart the tool\n if (currentMode.startsWith('measure')) {\n mapTools?.deactivate();\n switch (currentMode) {\n case 'measureCircle':\n mapTools?.startCircleMeasure();\n break;\n case 'measureLine':\n mapTools?.startLineMeasure();\n break;\n case 'measureArea':\n mapTools?.startAreaMeasure();\n break;\n }\n }\n });\n }\n}\n\n// ============================================================================\n// Location Handlers\n// ============================================================================\n\nasync function handleAddLocation(event) {\n event.preventDefault();\n\n const form = event.target;\n const formData = new FormData(form);\n\n const name = formData.get('name');\n const longitude = parseFloat(formData.get('longitude'));\n const latitude = parseFloat(formData.get('latitude'));\n const description = formData.get('description') || null;\n const category = formData.get('category') || 'default';\n\n if (!name || isNaN(longitude) || isNaN(latitude)) {\n showError('Please fill in all required fields');\n return;\n }\n\n try {\n const result = await addLocation(name, longitude, latitude, { description, category });\n console.log('[App] Location added:', name, 'id:', result.id);\n\n form.reset();\n await loadLocations();\n\n // Zoom to the new location on the map\n mapView?.zoomTo(longitude, latitude, 14);\n\n // Select the new marker\n if (result.id) {\n mapView?.selectMarker(result.id);\n }\n\n showSuccess('Location added successfully');\n\n } catch (error) {\n console.error('[App] Failed to add location:', error);\n showError('Failed to add location: ' + error.message);\n }\n}\n\nasync function loadLocations() {\n try {\n console.log('[App] Loading locations...');\n const locations = await getLocations();\n console.log('[App] Locations loaded:', locations);\n\n // Update the list\n renderLocations(locations);\n\n // Update the map markers\n if (mapView) {\n mapView.clearMarkers();\n if (locations.length > 0) {\n mapView.addMarkers(locations);\n console.log('[App] Added', locations.length, 'markers to map');\n }\n }\n\n // Update count display\n const countEl = document.getElementById('location-count');\n if (countEl) {\n countEl.textContent = locations.length;\n }\n\n } catch (error) {\n console.error('[App] Failed to load locations:', error);\n }\n}\n\n/**\n * Show details for a selected location\n */\nfunction showLocationDetails(feature) {\n const name = feature.get('name');\n const description = feature.get('description');\n const category = feature.get('category');\n const lon = feature.get('lon') || feature.get('longitude');\n const lat = feature.get('lat') || feature.get('latitude');\n\n // You could show a popup or info panel here\n // For now, just log to console\n console.log('[App] Selected location:', { name, description, category, lon, lat });\n\n // Optionally zoom to the location\n // mapView.zoomTo(lon, lat, 14);\n}\n\nfunction renderLocations(locations) {\n const container = document.getElementById('locations-list');\n if (!container) return;\n\n // Also update mobile count\n const mobileCount = document.getElementById('location-count-mobile');\n if (mobileCount) {\n mobileCount.textContent = locations.length;\n }\n\n if (locations.length === 0) {\n container.innerHTML = `\n
                        \n

                        No locations yet.

                        \n Click the map or fill the form above!\n
                        \n `;\n return;\n }\n\n // Category emoji mapping\n const categoryEmojis = {\n 'water': '💧',\n 'school': '🏫',\n 'health': '🏥',\n 'market': '🏪',\n 'default': '📍',\n 'other': '📌'\n };\n\n container.innerHTML = locations.map(loc => {\n const emoji = categoryEmojis[loc.category] || '📍';\n return `\n \n
                        \n
                        \n
                        ${emoji} ${escapeHtml(loc.name)}
                        \n ${loc.latitude.toFixed(5)}, ${loc.longitude.toFixed(5)}\n
                        \n ${loc.category}\n
                        \n ${loc.description ? `${escapeHtml(loc.description)}` : ''}\n
                        \n `;\n }).join('');\n\n // Add click handlers to zoom to location\n container.querySelectorAll('.location-item').forEach(item => {\n item.addEventListener('click', (e) => {\n e.preventDefault();\n const lon = parseFloat(item.dataset.lon);\n const lat = parseFloat(item.dataset.lat);\n const id = parseInt(item.dataset.id);\n\n // Zoom to location on map\n mapView?.zoomTo(lon, lat, 14);\n\n // Select the marker\n mapView?.selectMarker(id);\n });\n });\n}\n\n// ============================================================================\n// Local Data Stats\n// ============================================================================\n\n/**\n * Refresh the Local Data stats panel in the left offcanvas.\n * If the panel is already visible it updates in-place; otherwise it opens it.\n */\nasync function refreshLocalDataStats() {\n const statsContainer = document.getElementById('local-data-stats');\n const tbody = document.getElementById('local-data-tbody');\n const clearAllBtn = document.getElementById('clear-all-cached-btn');\n if (!statsContainer || !tbody) return;\n\n try {\n const stats = await getTableStats();\n\n tbody.innerHTML = stats.map((t) => {\n const isCached = isCachedLayerTable(t.name);\n const clearBtn = isCached\n ? ``\n : '';\n return `\n \n \n ${escapeHtml(t.name)}\n \n ${t.count}\n ${clearBtn}\n \n `;\n }).join('');\n statsContainer.classList.remove('d-none');\n\n // Table-name link → open content modal\n tbody.querySelectorAll('.table-name-link').forEach((link) => {\n link.addEventListener('click', (e) => {\n e.preventDefault();\n showTableContent(link.dataset.table);\n });\n });\n\n // Per-row clear → confirm, clear that table, refresh stats\n tbody.querySelectorAll('.table-clear-btn').forEach((btn) => {\n btn.addEventListener('click', async (e) => {\n e.preventDefault();\n const tableName = btn.dataset.table;\n if (!confirm(`Clear local cache for \"${tableName}\"?\\n\\nThe data will be re-downloaded from the server on the next app start.`)) return;\n try {\n const removed = await clearTable(tableName);\n showSuccess(`Cleared ${removed} row${removed === 1 ? '' : 's'} from \"${tableName}\". It will re-download on next start.`);\n await refreshLocalDataStats();\n } catch (err) {\n console.error('[App] Per-table clear failed:', err);\n showError(`Could not clear \"${tableName}\": ${err.message}`);\n }\n });\n });\n } catch (error) {\n console.error('[App] Failed to load table stats:', error);\n tbody.innerHTML = `Failed to load`;\n statsContainer.classList.remove('d-none');\n }\n\n // Bulk-clear button — wire up once\n if (clearAllBtn && !clearAllBtn._wired) {\n clearAllBtn._wired = true;\n clearAllBtn.addEventListener('click', handleClearAllCachedLayers);\n }\n}\n\n/**\n * Clear every cached layer table and offer to reload the app so the layers\n * re-download immediately. If the user dismisses the reload prompt, the\n * fresh fetch will happen on the next manual app start.\n */\nasync function handleClearAllCachedLayers() {\n if (!confirm(\n 'Delete all cached map layers from this device?\\n\\n' +\n 'The next time the app starts (or after a reload), every layer will be ' +\n 're-downloaded from the server. Your locally drawn data is not affected.'\n )) return;\n\n try {\n const results = await clearAllCachedLayers();\n const total = results.reduce((s, r) => s + r.count, 0);\n showSuccess(`Cleared ${total} row${total === 1 ? '' : 's'} across ${results.length} table${results.length === 1 ? '' : 's'}.`);\n await refreshLocalDataStats();\n\n if (confirm('Reload the app now to re-download the layers fresh from the server?')) {\n window.location.reload();\n }\n } catch (err) {\n console.error('[App] Clear-all failed:', err);\n showError('Failed to clear cached layers: ' + err.message);\n }\n}\n\n// ============================================================================\n// Table Content Viewer\n// ============================================================================\n\n/**\n * Load and display all rows of a table in a modal.\n * @param {string} tableName - The table to show\n */\nasync function showTableContent(tableName) {\n const modalTitle = document.getElementById('tableContentModalLabel');\n const modalBody = document.getElementById('table-content-body');\n const modalInfo = document.getElementById('table-content-info');\n\n // Set title and show spinner\n modalTitle.textContent = `Table: ${tableName}`;\n modalBody.innerHTML = `\n
                        \n
                        \n Loading...\n
                        \n
                        \n `;\n modalInfo.textContent = '';\n\n // Open the modal\n const modal = new Modal(document.getElementById('tableContentModal'));\n modal.show();\n\n try {\n const { columns, rows } = await getTableContent(tableName);\n\n if (rows.length === 0) {\n modalBody.innerHTML = `
                        Table is empty
                        `;\n modalInfo.textContent = '0 rows';\n return;\n }\n\n // Build a responsive table\n const headerCells = columns.map(c => `${escapeHtml(c)}`).join('');\n const bodyRows = rows.map(row => {\n const cells = columns.map(c => {\n let val = row[c];\n if (val === null || val === undefined) return 'NULL';\n val = String(val);\n // Truncate long values for display\n const display = val.length > 120 ? val.substring(0, 120) + '...' : val;\n return `${escapeHtml(display)}`;\n }).join('');\n return `${cells}`;\n }).join('');\n\n modalBody.innerHTML = `\n
                        \n \n \n ${headerCells}\n \n ${bodyRows}\n
                        \n
                        \n `;\n\n modalInfo.textContent = `${rows.length}${rows.length >= 200 ? '+' : ''} row(s), ${columns.length} column(s)`;\n\n } catch (error) {\n console.error('[App] Failed to load table content:', error);\n modalBody.innerHTML = `
                        Failed to load: ${escapeHtml(error.message)}
                        `;\n }\n}\n\n// ============================================================================\n// Export Handler\n// ============================================================================\n\nasync function handleExport() {\n try {\n await downloadDatabase('lupmis-backup.sqlite3');\n showSuccess('Database exported successfully');\n } catch (error) {\n console.error('[App] Export failed:', error);\n showError('Export failed: ' + error.message);\n }\n}\n\n// Export as GeoJSON file\nasync function handleExportGeoJSON() {\n try {\n\t const geojson = await exportToGeoJSON();\n\n\t // Download as file\n\t const blob = new Blob([JSON.stringify(geojson, null, 2)], { type: 'application/json' });\n\t const url = URL.createObjectURL(blob);\n\t const a = document.createElement('a');\n\t a.href = url;\n\t a.download = 'locations.geojson';\n\t a.click();\n\t URL.revokeObjectURL(url);\n\n\t\tshowSuccess(`Exported ${geojson.features.length} location(s)`);\n\t}catch (error) {\n console.error('[App] GeoJSON Export failed:', error);\n showError('GeoJSON Export failed: ' + error.message);\n }\n}\n\n// ============================================================================\n// Status Handler\n// ============================================================================\n\nasync function handleShowStatus() {\n try {\n const status = await getDatabaseStatus();\n\n // Update modal content\n const statusContent = document.getElementById('status-content');\n if (statusContent) {\n statusContent.innerHTML = `\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
                        Ready:${status.ready ? 'Yes' : 'No'}
                        Online:${isOnline() ? 'Yes' : 'Offline'}
                        Database:${status.databasePath || 'N/A'}
                        Tables:${status.tables.map(t => `${t}`).join('')}
                        Locations:${status.locationCount}
                        \n `;\n }\n\n // Show the modal using Bootstrap\n const statusModal = new Modal(document.getElementById('statusModal'));\n statusModal.show();\n\n } catch (error) {\n console.error('[App] Failed to get status:', error);\n showError('Failed to get status');\n }\n}\n\n// ============================================================================\n// Remote Data Loading\n// ============================================================================\n\n/**\n * Parse a coordinate ring string into an array of [lon, lat] pairs.\n * @param {string} ringStr - e.g. \"lon lat,lon lat,...\"\n * @returns {Array} Array of [lon, lat]\n */\nfunction parseCoordRing(ringStr) {\n return ringStr.replace(/^\\(+/, '').replace(/\\)+$/, '')\n .split(',')\n .map(pair => {\n const [lon, lat] = pair.trim().split(/\\s+/).map(Number);\n return [lon, lat];\n });\n}\n\n/**\n * Parse a WKT POLYGON string into GeoJSON geometry.\n * Handles: POLYGON((lon lat,...),(hole,...))\n *\n * @param {string} wkt - WKT POLYGON string\n * @returns {Object} GeoJSON geometry { type: 'Polygon', coordinates: [...] }\n */\nfunction parseWKTPolygon(wkt) {\n const inner = wkt.trim()\n .replace(/^POLYGON\\s*\\(\\s*/i, '')\n .replace(/\\s*\\)$/, '');\n\n const ringStrings = inner.split('),(');\n const rings = ringStrings.map(parseCoordRing);\n\n return { type: 'Polygon', coordinates: rings };\n}\n\n/**\n * Parse a WKT MULTIPOLYGON string into GeoJSON geometry.\n * Handles: MULTIPOLYGON(((lon lat,...),(hole),...),((polygon2)))\n *\n * @param {string} wkt - WKT MULTIPOLYGON string\n * @returns {Object} GeoJSON geometry { type: 'MultiPolygon', coordinates: [...] }\n */\nfunction parseWKTMultiPolygon(wkt) {\n const inner = wkt.trim()\n .replace(/^MULTIPOLYGON\\s*\\(\\s*/i, '')\n .replace(/\\s*\\)$/, '');\n\n const polygonStrings = inner.split(')),((');\n\n const polygons = polygonStrings.map(polyStr => {\n const cleaned = polyStr.replace(/^\\(+/, '').replace(/\\)+$/, '');\n const ringStrings = cleaned.split('),(');\n return ringStrings.map(parseCoordRing);\n });\n\n return { type: 'MultiPolygon', coordinates: polygons };\n}\n\n/**\n * Parse any supported WKT geometry string (POLYGON or MULTIPOLYGON).\n * @param {string} wkt - WKT geometry string\n * @returns {Object|null} GeoJSON geometry or null if unsupported\n */\nfunction parseWKT(wkt) {\n if (!wkt) return null;\n const trimmed = wkt.trim().toUpperCase();\n if (trimmed.startsWith('MULTIPOLYGON')) return parseWKTMultiPolygon(wkt);\n if (trimmed.startsWith('POLYGON')) return parseWKTPolygon(wkt);\n console.warn('[App] Unsupported WKT type:', trimmed.substring(0, 30));\n return null;\n}\n\n/**\n * Convert the API response to a GeoJSON FeatureCollection.\n * The API returns: { success, data: { boundary: \"MULTIPOLYGON(...)\", districtid, district_name } }\n *\n * @param {Object} apiResponse - Raw API response\n * @returns {Object|null} GeoJSON FeatureCollection or null\n */\nfunction apiResponseToGeoJSON(apiResponse) {\n if (!apiResponse?.success || !apiResponse?.data?.boundary) {\n console.warn('[App] API response missing success or boundary data');\n return null;\n }\n\n const { boundary, districtid, district_name } = apiResponse.data;\n const geometry = parseWKT(boundary);\n\n return {\n type: 'FeatureCollection',\n features: [{\n type: 'Feature',\n properties: {\n districtid: districtid,\n district_name: district_name\n },\n geometry: geometry\n }]\n };\n}\n\n/**\n * Convert the collector zones API response to a GeoJSON FeatureCollection.\n * The API returns: { success, data: [{ id, zone_name, boundary, ... }, ...] }\n * Each zone feature gets a '_layerType' = 'collector_zone' property for identification.\n *\n * @param {Array} zones - Array of zone objects from the API\n * @returns {Object|null} GeoJSON FeatureCollection or null\n */\nfunction zonesToGeoJSON(zones) {\n if (!Array.isArray(zones) || zones.length === 0) return null;\n\n const features = [];\n for (const zone of zones) {\n // API returns WKT in 'polygon' field (not 'boundary')\n const wkt = zone.polygon || zone.boundary;\n const geometry = parseWKT(wkt);\n if (!geometry) continue;\n\n // Collect all properties except the raw WKT geometry\n const properties = { _layerType: 'collector_zone' };\n for (const [key, value] of Object.entries(zone)) {\n if (key === 'polygon' || key === 'boundary') continue;\n properties[key] = value;\n }\n\n features.push({ type: 'Feature', properties, geometry });\n }\n\n if (features.length === 0) return null;\n return { type: 'FeatureCollection', features };\n}\n\n/**\n * Load district boundary with local-first strategy:\n * 1. Always read from local SQLite cache (GeoJSON) first — instant, works offline\n * 2. If online, fetch from API, convert WKT → GeoJSON, cache and display\n */\nasync function loadDistrictBoundary() {\n const CACHE_KEY = 'district_boundary';\n const ADMIN_GROUP_ID = 1; // Administration layer group\n const boundaryStyle = {\n strokeColor: '#e11d48',\n strokeWidth: 2.5,\n fillColor: 'rgba(225,29,72,0.08)',\n typeDescription: 'Vector / Polygon',\n };\n\n // Target group: Administration (id 1), fall back to root overlay group\n const adminGroup = mapView?.getLayerGroup(ADMIN_GROUP_ID) || null;\n\n /**\n * Remove existing District Boundary layer from a group's layers.\n */\n function removeBoundaryLayer(group) {\n if (!group) return;\n const layers = group.getLayers();\n const toRemove = [];\n layers.forEach((layer) => {\n if (layer.get('title') === 'District Boundary') {\n toRemove.push(layer);\n }\n });\n toRemove.forEach((layer) => layers.remove(layer));\n }\n\n /**\n * Zoom the map to fit the boundary layer's extent.\n */\n function zoomToBoundary(layer) {\n if (!layer || !mapView) return;\n const extent = layer.getSource().getExtent();\n if (extent && extent[0] !== Infinity) {\n mapView.getMap().getView().fit(extent, {\n padding: [40, 40, 40, 40],\n duration: 600,\n });\n }\n }\n\n try {\n // Step 1: Load from local cache (already stored as GeoJSON)\n const cached = await getRemoteData(CACHE_KEY);\n if (cached) {\n console.log('[App] District boundary loaded from local cache');\n const layer = mapView?.addGeoJSONLayer(cached, 'District Boundary', boundaryStyle, adminGroup);\n zoomToBoundary(layer);\n }\n\n // Step 2: If online and server reachable, fetch fresh data from the API\n if (isOnline() && isServerReachable()) {\n console.log('[App] Fetching district boundary from API...');\n const apiResponse = await getDistrictBoundary();\n\n // Convert WKT response to GeoJSON\n const geojson = apiResponseToGeoJSON(apiResponse);\n if (!geojson) {\n console.warn('[App] Could not convert API response to GeoJSON');\n return;\n }\n\n console.log('[App] District boundary:', geojson.features[0]?.properties?.district_name,\n '→', geojson.features[0]?.geometry?.coordinates?.length, 'polygon(s)');\n\n // Save converted GeoJSON to local cache for offline use\n await saveRemoteData(CACHE_KEY, geojson);\n\n // Replace old cached layer if present\n if (cached) {\n removeBoundaryLayer(adminGroup || mapView?.getOverlayGroup());\n }\n\n const layer = mapView?.addGeoJSONLayer(geojson, 'District Boundary', boundaryStyle, adminGroup);\n zoomToBoundary(layer);\n console.log('[App] District boundary loaded from API');\n\n } else if (!cached) {\n console.log('[App] District boundary not available — offline and no local cache');\n }\n\n } catch (error) {\n console.error('[App] Failed to load district boundary:', error);\n }\n}\n\n/**\n * Load collector zones with local-first strategy:\n * 1. Read from local collector_zones table → convert to GeoJSON → display\n * 2. If online, fetch from API → save to local table → convert → display\n *\n * The \"Zones\" layer is added to the Administration LayerGroup (id 1),\n * initially not visible. It becomes visible when toggled in the LayerSwitcher.\n */\nasync function loadCollectorZones() {\n const ADMIN_GROUP_ID = 1;\n const zoneStyle = {\n strokeColor: '#7c3aed',\n strokeWidth: 1.5,\n fillColor: 'rgba(124,58,237,0.12)',\n typeDescription: 'Vector / Polygon',\n };\n\n const adminGroup = mapView?.getLayerGroup(ADMIN_GROUP_ID) || null;\n console.log('[App] loadCollectorZones — adminGroup:', adminGroup ? adminGroup.get('title') : 'null');\n\n // Create the Zones layer immediately (empty) so it always appears\n // in the LayerSwitcher. Features will be added once data is available.\n const emptyGeoJSON = { type: 'FeatureCollection', features: [] };\n const zonesLayer = mapView?.addGeoJSONLayer(emptyGeoJSON, 'Zones', zoneStyle, adminGroup);\n if (!zonesLayer) {\n console.warn('[App] Could not create Zones layer');\n return;\n }\n zonesLayer.setVisible(false);\n\n // Warn when the user enables the layer but it has no data\n zonesLayer.on('change:visible', () => {\n if (zonesLayer.getVisible() && zonesLayer.getSource().getFeatures().length === 0) {\n showError('No collector zones available locally. Connect to the internet to download zone data.');\n }\n });\n\n /**\n * Replace the layer's source features with features parsed from GeoJSON.\n */\n function setZoneFeatures(geojson) {\n const newFeatures = new GeoJSON().readFeatures(geojson, {\n featureProjection: 'EPSG:3857',\n });\n zonesLayer.getSource().clear();\n zonesLayer.getSource().addFeatures(newFeatures);\n }\n\n try {\n // Step 1: Load from local table\n const cached = await getLocalCollectorZones();\n if (cached) {\n const geojson = zonesToGeoJSON(cached);\n if (geojson) {\n console.log('[App] Collector zones loaded from local cache:', geojson.features.length, 'zones');\n setZoneFeatures(geojson);\n }\n }\n\n // Step 2: If online and server reachable, fetch fresh data from the API\n if (isOnline() && isServerReachable()) {\n console.log('[App] Fetching collector zones from API...');\n const apiResponse = await getCollectorZones();\n\n if (!apiResponse?.success || !Array.isArray(apiResponse?.data)) {\n console.warn('[App] getCollectorZones API response invalid:', apiResponse);\n return;\n }\n\n const zones = apiResponse.data;\n console.log('[App] Collector zones from API:', zones.length, 'entries');\n\n // Save to local table\n await saveCollectorZones(zones);\n\n // Convert to GeoJSON and update the existing layer\n const geojson = zonesToGeoJSON(zones);\n if (!geojson) {\n console.warn('[App] Could not convert zones to GeoJSON');\n return;\n }\n\n setZoneFeatures(geojson);\n console.log('[App] Collector zones updated from API:', geojson.features.length, 'zones');\n\n } else if (!cached) {\n console.log('[App] Collector zones not available — offline and no local cache');\n }\n\n } catch (error) {\n console.error('[App] Failed to load collector zones:', error);\n }\n}\n\n/**\n * Convert parcels data to a GeoJSON FeatureCollection.\n * Each parcel feature gets a '_layerType' = 'parcel' property for identification.\n *\n * @param {Array} parcels - Array of parcel objects from the API\n * @returns {Object|null} GeoJSON FeatureCollection or null\n */\nfunction parcelsToGeoJSON(parcels) {\n if (!Array.isArray(parcels) || parcels.length === 0) return null;\n\n // Deduplicate by id — the API may return the same parcel more than once\n const seen = new Set();\n const features = [];\n for (const parcel of parcels) {\n const id = parcel.id || parcel.parcelid || parcel.parcel_id;\n if (id != null) {\n if (seen.has(id)) continue;\n seen.add(id);\n }\n\n // Geometry sources, in order:\n // - API path: `geom` is a GeoJSON object, `boundary` is the WKT string\n // - local cache: `geometry_wkt` is the WKT string\n let geometry = null;\n if (parcel.geom && parcel.geom.type && parcel.geom.coordinates) {\n geometry = { type: parcel.geom.type, coordinates: parcel.geom.coordinates };\n } else if (parcel.sp_boundary && parcel.sp_boundary.type && parcel.sp_boundary.coordinates) {\n geometry = { type: parcel.sp_boundary.type, coordinates: parcel.sp_boundary.coordinates };\n } else {\n const wkt = parcel.boundary || parcel.geometry_wkt || parcel.polygon || parcel.wkt;\n geometry = parseWKT(wkt);\n }\n if (!geometry) continue;\n\n // Collect all properties except bulky geometry fields and local housekeeping.\n const skipKeys = new Set(['polygon', 'boundary', 'geom', 'geometry_wkt', 'wkt', 'textboundary', 'sp_boundary', 'fetched_at']);\n const properties = { _layerType: 'parcel' };\n for (const [key, value] of Object.entries(parcel)) {\n if (skipKeys.has(key)) continue;\n properties[key] = value;\n }\n\n features.push({ type: 'Feature', properties, geometry });\n }\n\n if (features.length === 0) return null;\n return { type: 'FeatureCollection', features };\n}\n\n/**\n * Load parcels with local-first strategy:\n * 1. Read from local parcels table → convert to GeoJSON → display\n * 2. If online, fetch from API → save to local table → convert → display\n *\n * The \"Parcels\" layer is added to the \"Land Use and Land Tenure\" LayerGroup (id 4),\n * initially not visible. It becomes visible when toggled in the LayerSwitcher.\n */\nasync function loadParcels() {\n const LAND_USE_GROUP_ID = 4;\n const parcelStyle = {\n strokeColor: '#0ea5e9',\n strokeWidth: 1.5,\n fillColor: 'rgba(14,165,233,0.12)',\n typeDescription: 'Vector / Polygon',\n };\n\n const landUseGroup = mapView?.getLayerGroup(LAND_USE_GROUP_ID) || null;\n console.log('[App] loadParcels — landUseGroup:', landUseGroup ? landUseGroup.get('title') : 'null');\n\n // Create the Parcels layer immediately (empty) so it always appears\n // in the LayerSwitcher. Features will be added once data is available.\n const emptyGeoJSON = { type: 'FeatureCollection', features: [] };\n // Assigned to the module-level `parcelsLayer` so the iframe embed bridge\n // can pick it up after loadParcels() returns from its sync prologue.\n parcelsLayer = mapView?.addGeoJSONLayer(emptyGeoJSON, 'Parcels', parcelStyle, landUseGroup);\n if (!parcelsLayer) {\n console.warn('[App] Could not create Parcels layer');\n return;\n }\n parcelsLayer.setVisible(false);\n\n // Warn when the user enables the layer but it has no data\n parcelsLayer.on('change:visible', () => {\n if (parcelsLayer.getVisible() && parcelsLayer.getSource().getFeatures().length === 0) {\n showError('No parcels available locally. Connect to the internet to download parcel data.');\n }\n });\n\n /**\n * Replace the layer's source features with features parsed from GeoJSON.\n */\n function setParcelFeatures(geojson) {\n const newFeatures = new GeoJSON().readFeatures(geojson, {\n featureProjection: 'EPSG:3857',\n });\n parcelsLayer.getSource().clear();\n parcelsLayer.getSource().addFeatures(newFeatures);\n }\n\n try {\n // Step 1: Load from local table\n const cached = await getLocalParcels();\n if (cached) {\n const geojson = parcelsToGeoJSON(cached);\n if (geojson) {\n console.log('[App] Parcels loaded from local cache:', geojson.features.length, 'parcels');\n setParcelFeatures(geojson);\n }\n }\n\n // Step 2: If online and server reachable, fetch fresh data from the API\n if (isOnline() && isServerReachable()) {\n console.log('[App] Fetching parcels from API...');\n const apiResponse = await getDistrictParcels();\n\n if (!apiResponse?.success || !Array.isArray(apiResponse?.data)) {\n console.warn('[App] getDistrictParcels API response invalid:', apiResponse);\n return;\n }\n\n const parcels = apiResponse.data;\n console.log('[App] Parcels from API:', parcels.length, 'entries');\n\n // Log first parcel's keys for debugging field names\n if (parcels.length > 0) {\n console.log('[App] First parcel keys:', Object.keys(parcels[0]));\n }\n\n // Save to local table\n await saveParcels(parcels);\n\n // Convert to GeoJSON and update the existing layer\n const geojson = parcelsToGeoJSON(parcels);\n if (!geojson) {\n console.warn('[App] Could not convert parcels to GeoJSON');\n return;\n }\n\n setParcelFeatures(geojson);\n console.log('[App] Parcels updated from API:', geojson.features.length, 'parcels');\n\n } else if (!cached) {\n console.log('[App] Parcels not available — offline and no local cache');\n }\n\n } catch (error) {\n console.error('[App] Failed to load parcels:', error);\n }\n}\n\n/**\n * Convert an array of building footprint objects to a GeoJSON FeatureCollection.\n * Each footprint's WKT geometry field is parsed; all other fields become properties.\n *\n * @param {Array} footprints - Array of footprint objects\n * @returns {Object|null} GeoJSON FeatureCollection, or null if no valid features\n */\nfunction footprintsToGeoJSON(footprints) {\n if (!Array.isArray(footprints) || footprints.length === 0) return null;\n\n const geomKeys = ['polygon', 'boundary', 'geom', 'wkt', 'footprint'];\n\n const features = [];\n for (const fp of footprints) {\n const raw = fp.polygon || fp.boundary || fp.geom || fp.wkt || fp.footprint;\n // Geometry may be WKT string or GeoJSON object\n let geometry;\n if (typeof raw === 'object' && raw !== null && raw.type) {\n // Already a GeoJSON geometry object\n geometry = raw;\n } else {\n geometry = parseWKT(raw);\n }\n if (!geometry) continue;\n\n const properties = { _layerType: 'building_footprint' };\n for (const [key, value] of Object.entries(fp)) {\n if (geomKeys.includes(key)) continue;\n // Skip nested objects that aren't useful as flat properties\n if (typeof value === 'object' && value !== null) continue;\n properties[key] = value;\n }\n\n features.push({ type: 'Feature', properties, geometry });\n }\n\n if (features.length === 0) return null;\n return { type: 'FeatureCollection', features };\n}\n\n/**\n * Load building footprints with local-first strategy:\n * 1. Read from local building_footprints table → convert to GeoJSON → display\n * 2. If online, fetch from API → save to local table → convert → display\n *\n * The \"Building footprints\" layer is added to the \"Physical Infrastructures\" LayerGroup (id 5),\n * initially not visible. It becomes visible when toggled in the LayerSwitcher.\n */\nasync function loadBuildingFootprints() {\n const PHYS_INFRA_GROUP_ID = 5;\n const footprintStyle = {\n strokeColor: '#8b6f47',\n strokeWidth: 1,\n fillColor: 'rgba(139,111,71,0.18)',\n typeDescription: 'Vector / Polygon',\n };\n\n const physInfraGroup = mapView?.getLayerGroup(PHYS_INFRA_GROUP_ID) || null;\n console.log('[App] loadBuildingFootprints — physInfraGroup:', physInfraGroup ? physInfraGroup.get('title') : 'null');\n\n // Create the layer immediately (empty) so it always appears in the LayerSwitcher.\n const emptyGeoJSON = { type: 'FeatureCollection', features: [] };\n const footprintsLayer = mapView?.addGeoJSONLayer(emptyGeoJSON, 'Building footprints', footprintStyle, physInfraGroup);\n if (!footprintsLayer) {\n console.warn('[App] Could not create Building footprints layer');\n return;\n }\n footprintsLayer.setVisible(false);\n\n // Warn when the user enables the layer but it has no data\n footprintsLayer.on('change:visible', () => {\n if (footprintsLayer.getVisible() && footprintsLayer.getSource().getFeatures().length === 0) {\n showError('No building footprints available locally. Connect to the internet to download footprint data.');\n }\n });\n\n /**\n * Replace the layer's source features with features parsed from GeoJSON.\n */\n function setFootprintFeatures(geojson) {\n const newFeatures = new GeoJSON().readFeatures(geojson, {\n featureProjection: 'EPSG:3857',\n });\n footprintsLayer.getSource().clear();\n footprintsLayer.getSource().addFeatures(newFeatures);\n }\n\n try {\n // Step 1: Load from local table\n const cached = await getLocalBuildingFootprints();\n if (cached) {\n const geojson = footprintsToGeoJSON(cached);\n if (geojson) {\n console.log('[App] Building footprints loaded from local cache:', geojson.features.length, 'footprints');\n setFootprintFeatures(geojson);\n }\n }\n\n // Step 2: If online and server reachable, fetch fresh data from the API\n if (isOnline() && isServerReachable()) {\n console.log('[App] Fetching building footprints from API...');\n const apiResponse = await getBuildingFootprints();\n\n if (!apiResponse?.success || !Array.isArray(apiResponse?.data)) {\n console.warn('[App] getBuildingFootprints API response invalid:', apiResponse);\n return;\n }\n\n const footprints = apiResponse.data;\n console.log('[App] Building footprints from API:', footprints.length, 'entries');\n\n // Log first footprint's keys for debugging field names\n if (footprints.length > 0) {\n console.log('[App] First footprint keys:', Object.keys(footprints[0]));\n }\n\n // Save to local table\n await saveBuildingFootprints(footprints);\n\n // Convert to GeoJSON and update the existing layer\n const geojson = footprintsToGeoJSON(footprints);\n if (!geojson) {\n console.warn('[App] Could not convert building footprints to GeoJSON');\n return;\n }\n\n setFootprintFeatures(geojson);\n console.log('[App] Building footprints updated from API:', geojson.features.length, 'footprints');\n\n } else if (!cached) {\n console.log('[App] Building footprints not available — offline and no local cache');\n }\n\n } catch (error) {\n console.error('[App] Failed to load building footprints:', error);\n }\n}\n\n/**\n * Convert an array of DB rows (each with a WKT geom field) to GeoJSON.\n * Uses OpenLayers' WKT parser so LINESTRING, MULTILINESTRING, POLYGON, etc.\n * are all supported out of the box.\n *\n * @param {Array} rows — API rows, each having a WKT-valued geom/geometry/wkt field\n * @param {string} layerType — value to store in each feature's _layerType property\n * @returns {Object|null} GeoJSON FeatureCollection, or null if no valid rows\n */\nfunction wktRowsToGeoJSON(rows, layerType) {\n if (!Array.isArray(rows) || rows.length === 0) return null;\n\n const wktFormat = new WKT();\n const geojsonFormat = new GeoJSON();\n // Field-name fallbacks — different endpoints alias the geometry column\n // differently (e.g. get_osm_roads uses `road`, get_contours_hillshade uses\n // `geom`). The first non-null match wins.\n const geomKeys = ['geom', 'geometry', 'wkt', 'polygon', 'boundary', 'road', 'line'];\n\n const features = [];\n for (const row of rows) {\n const raw = row.geom || row.geometry || row.wkt || row.polygon || row.boundary || row.road || row.line;\n if (!raw) continue;\n\n let olGeom;\n try {\n if (typeof raw === 'object' && raw !== null && raw.type) {\n // Already a GeoJSON geometry — just pass through\n features.push({\n type: 'Feature',\n properties: flattenProps(row, geomKeys, layerType),\n geometry: raw,\n });\n continue;\n }\n olGeom = wktFormat.readGeometry(raw);\n } catch (err) {\n console.warn(`[App] Could not parse WKT for ${layerType}:`, err, raw?.toString().slice(0, 60));\n continue;\n }\n\n const geometry = JSON.parse(geojsonFormat.writeGeometry(olGeom));\n features.push({\n type: 'Feature',\n properties: flattenProps(row, geomKeys, layerType),\n geometry,\n });\n }\n\n if (features.length === 0) return null;\n return { type: 'FeatureCollection', features };\n}\n\n/**\n * Flatten a DB row into properties, skipping geometry fields and nested objects.\n */\nfunction flattenProps(row, skipKeys, layerType) {\n const props = { _layerType: layerType };\n for (const [key, value] of Object.entries(row)) {\n if (skipKeys.includes(key)) continue;\n if (typeof value === 'object' && value !== null) continue;\n props[key] = value;\n }\n return props;\n}\n\n/**\n * Load the \"Contours hillshade\" layer — elevation contours derived from\n * OpenTopography, stored in PostgreSQL as `contours_hillshade`.\n *\n * Added to the \"Biophysical Environment\" LayerGroup, initially not visible.\n * No local caching (the server is the source of truth).\n */\nasync function loadContoursHillshade() {\n const contoursStyle = {\n strokeColor: '#78716c', // warm grey — traditional contour colour\n strokeWidth: 0.8,\n typeDescription: 'Vector / Line',\n fillColor: 'rgba(0,0,0,0)',\n };\n\n const biophysGroup = mapView?.getLayerGroupByTitle('Biophysical Environment');\n console.log('[App] loadContoursHillshade — group:', biophysGroup ? biophysGroup.get('title') : 'null');\n\n // Create empty layer first so it always appears in the LayerSwitcher\n const emptyGeoJSON = { type: 'FeatureCollection', features: [] };\n const contoursLayer = mapView?.addGeoJSONLayer(emptyGeoJSON, 'Contours hillshade', contoursStyle, biophysGroup);\n if (!contoursLayer) {\n console.warn('[App] Could not create Contours hillshade layer');\n return;\n }\n contoursLayer.setVisible(false);\n\n // Warn when the user enables the layer but it has no data\n contoursLayer.on('change:visible', () => {\n if (contoursLayer.getVisible() && contoursLayer.getSource().getFeatures().length === 0) {\n showError('No Contours hillshade data available. Connect to the internet to download it.');\n }\n });\n\n // Fetch from API (only when online and server reachable — no local cache)\n if (!isOnline() || !isServerReachable()) {\n console.log('[App] Contours hillshade not available — offline or server unreachable');\n return;\n }\n\n try {\n console.log('[App] Fetching contours_hillshade from API...');\n const apiResponse = await getContoursHillshade();\n\n if (!apiResponse?.success || !Array.isArray(apiResponse?.data)) {\n console.warn('[App] getContoursHillshade API response invalid:', apiResponse);\n return;\n }\n\n const rows = apiResponse.data;\n console.log('[App] Contours hillshade from API:', rows.length, 'rows');\n if (rows.length > 0) {\n console.log('[App] First row keys:', Object.keys(rows[0]));\n }\n\n const geojson = wktRowsToGeoJSON(rows, 'contours_hillshade');\n if (!geojson) {\n console.warn('[App] Could not convert contours to GeoJSON');\n return;\n }\n\n const features = new GeoJSON().readFeatures(geojson, { featureProjection: 'EPSG:3857' });\n contoursLayer.getSource().clear();\n contoursLayer.getSource().addFeatures(features);\n console.log('[App] Contours hillshade loaded:', features.length, 'features');\n\n } catch (error) {\n console.error('[App] Failed to load contours_hillshade:', error);\n }\n}\n\n/**\n * Load the \"OSM_roads\" layer — OpenStreetMap road network for the district.\n *\n * Added to the \"Physical Infrastructures\" LayerGroup (id 5), initially not\n * visible — becomes visible when the user toggles it in the LayerSwitcher.\n *\n * Local-first caching:\n * 1. Read from the local `osm_roads` table → render immediately if available\n * 2. If online, fetch from the API → overwrite the local table → re-render\n */\nasync function loadOSMRoads() {\n const PHYS_INFRA_GROUP_ID = 5;\n // Cartographic road casing: a black outer stroke makes the light-coloured\n // inner stroke (the \"road body\") readable on every base map.\n const roadsStyle = {\n strokeColor: '#F0F1F0', // inner — road body\n strokeWidth: 1.5,\n lineCasingColor: '#000000', // outer — black casing\n lineCasingWidth: 3.5,\n fillColor: 'rgba(0,0,0,0)',\n typeDescription: 'Vector / Line',\n };\n\n const physInfraGroup = mapView?.getLayerGroup(PHYS_INFRA_GROUP_ID) || null;\n console.log('[App] loadOSMRoads — group:', physInfraGroup ? physInfraGroup.get('title') : 'null');\n\n // Create the layer immediately (empty) so it appears in the LayerSwitcher\n // even when offline.\n const emptyGeoJSON = { type: 'FeatureCollection', features: [] };\n const roadsLayer = mapView?.addGeoJSONLayer(emptyGeoJSON, 'OSM_roads', roadsStyle, physInfraGroup);\n if (!roadsLayer) {\n console.warn('[App] Could not create OSM_roads layer');\n return;\n }\n roadsLayer.setVisible(false);\n\n // Warn only when the layer is enabled AND truly empty AND no source is reachable\n roadsLayer.on('change:visible', () => {\n if (roadsLayer.getVisible() && roadsLayer.getSource().getFeatures().length === 0) {\n showError('No OSM roads available locally. Connect to the internet to download them.');\n }\n });\n\n /** Replace the layer's features with those parsed from the API/cache rows. */\n function setRoadFeatures(rows) {\n const geojson = wktRowsToGeoJSON(rows, 'osm_road');\n if (!geojson) {\n console.warn('[App] Could not convert OSM roads to GeoJSON');\n return 0;\n }\n const features = new GeoJSON().readFeatures(geojson, { featureProjection: 'EPSG:3857' });\n roadsLayer.getSource().clear();\n roadsLayer.getSource().addFeatures(features);\n return features.length;\n }\n\n try {\n // Step 1 — local cache (works offline, instant)\n const cached = await getLocalOSMRoads();\n if (cached) {\n const n = setRoadFeatures(cached);\n console.log('[App] OSM_roads loaded from local cache:', n, 'features');\n }\n\n // Step 2 — fetch fresh from API when online\n if (isOnline() && isServerReachable()) {\n console.log('[App] Fetching OSM_roads from API...');\n const apiResponse = await getOSMRoads();\n\n if (!apiResponse?.success || !Array.isArray(apiResponse?.data)) {\n console.warn('[App] getOSMRoads API response invalid:', apiResponse);\n return;\n }\n\n const rows = apiResponse.data;\n console.log('[App] OSM_roads from API:', rows.length, 'rows');\n if (rows.length > 0) {\n console.log('[App] First row keys:', Object.keys(rows[0]));\n }\n\n // Persist to local table so it's available next time offline\n await saveOSMRoads(rows);\n\n const n = setRoadFeatures(rows);\n console.log('[App] OSM_roads updated from API:', n, 'features');\n\n } else if (!cached) {\n console.log('[App] OSM_roads not available — offline and no local cache');\n }\n\n } catch (error) {\n console.error('[App] Failed to load OSM_roads:', error);\n }\n}\n\n/**\n * Add external WMS/XYZ layers to the map.\n * Called after loadLayers() so the target layer groups already exist.\n */\nfunction loadExternalWMSLayers() {\n // DEAfrica Coastlines v0.4.0 — annual shorelines & rates of change\n // Source: Digital Earth Africa GeoServer\n // Latest available version as of 2026: v0.4.0\n mapView?.addWMSLayer(\n 'Biophysical Environment',\n 'DEAfrica Coastlines v0.4',\n 'https://geoserver.digitalearth.africa/geoserver/wms',\n 'coastlines:DEAfrica_Coastlines',\n { serverType: 'geoserver', visible: false, onlineOnly: true }\n );\n\n // Note: OpenTopoMap is available as the \"Topographic\" base map —\n // no separate overlay in \"Biophysical Environment\" needed.\n\n // Digital Earth Africa — SRTM-derived Slope (30m)\n // Shows terrain steepness as a background overlay — hills and valleys stand\n // out naturally, reading like a traditional shaded-relief topographic map.\n // Service: datacube-ows (not GeoServer).\n // Layer 'srtm_deriv' styles: 'style_slope', 'style_mrvbf' (valley bottoms),\n // 'style_mrrtf' (ridge tops).\n mapView?.addWMSLayer(\n 'Biophysical Environment',\n 'DEAfrica Slope (SRTM 30m)',\n 'https://ows.digitalearth.africa/wms',\n 'srtm_deriv',\n {\n serverType: null,\n style: 'style_slope',\n visible: false,\n opacity: 0.5,\n zIndex: -50,\n onlineOnly: true,\n attributions:\n '© Digital Earth Africa — ' +\n 'SRTM-derived Slope',\n legendUrl: 'https://ows.digitalearth.africa/legend/srtm_deriv/style_slope/legend.png',\n }\n );\n}\n\n/**\n * Load layer categories from the API and create empty VectorLayers on the map.\n * Uses local-first caching — reads from SQLite first, then refreshes from API when online.\n *\n * API response: { success: true, data: [{ id, name, description, createdt, editdt }, ...] }\n */\nasync function loadLayers() {\n const CACHE_KEY = 'layer_categories';\n\n /**\n * Create layer groups on the map from the layer category list.\n * @param {Array} layers - Array of { id, name, description, ... }\n */\n function createLayerGroupsOnMap(layers) {\n // Sort by id descending so that id 1 is pushed last → ends up\n // at the top of the layer stack and at the top of the LayerSwitcher.\n const sorted = [...layers].sort((a, b) => b.id - a.id);\n for (const layer of sorted) {\n mapView?.addLayerGroup(layer.id, layer.name, layer.description || '');\n }\n console.log('[App] Created', layers.length, 'layer groups on map');\n }\n\n try {\n // Step 1: Load from local cache\n const cached = await getRemoteData(CACHE_KEY);\n if (cached) {\n console.log('[App] Layer categories loaded from local cache:', cached.length, 'entries');\n createLayerGroupsOnMap(cached);\n }\n\n // Step 2: If online and server reachable, fetch fresh data from the API\n if (isOnline() && isServerReachable()) {\n console.log('[App] Fetching layer categories from API...');\n const apiResponse = await getLayers();\n\n if (!apiResponse?.success || !Array.isArray(apiResponse?.data)) {\n console.warn('[App] getLayers API response invalid:', apiResponse);\n return;\n }\n\n const layers = apiResponse.data;\n console.log('[App] Layer categories from API:', layers.length, 'entries');\n\n // Save to local cache\n await saveRemoteData(CACHE_KEY, layers);\n\n // Replace layers on map if we already created from cache\n if (cached) {\n // Remove previously created empty layers (keep District Boundary)\n const overlayLayers = mapView?.getOverlayGroup()?.getLayers();\n if (overlayLayers) {\n const toRemove = [];\n overlayLayers.forEach((layer) => {\n if (layer.get('layerId') !== undefined) {\n toRemove.push(layer);\n }\n });\n toRemove.forEach((layer) => overlayLayers.remove(layer));\n }\n }\n\n createLayerGroupsOnMap(layers);\n console.log('[App] Layer categories refreshed from API');\n\n } else if (!cached) {\n console.log('[App] Layer categories not available — offline and no local cache');\n }\n\n } catch (error) {\n console.error('[App] Failed to load layer categories:', error);\n }\n}\n\n// ============================================================================\n// Sync (placeholder - implement based on your backend)\n// ============================================================================\n\nasync function syncData() {\n if (!isOnline()) {\n console.log('[App] Cannot sync - offline');\n return;\n }\n\n // TODO: Implement sync with your backend\n // Example:\n // const unsynced = await getUnsyncedLocations();\n // for (const location of unsynced) {\n // await fetch('/api/locations', {\n // method: 'POST',\n // body: JSON.stringify(location)\n // });\n // await markLocationsSynced([location.id]);\n // }\n\n console.log('[App] Sync placeholder - implement based on your backend');\n}\n\n// ============================================================================\n// File Import (Shapefile, GeoJSON, KML)\n// ============================================================================\n\n/** All layers added by file imports — shared across formats. */\nconst importedFileLayers = [];\n\n/** Default style for imported layers. */\nconst IMPORT_STYLE = {\n strokeColor: '#e11d48',\n strokeWidth: 2,\n fillColor: 'rgba(225,29,72,0.12)',\n};\n\n/**\n * Show an error message inside the left panel's file-import alert area.\n */\nfunction showFileImportError(message) {\n logMessage('error', message);\n const el = document.getElementById('file-import-alert');\n if (el) {\n el.querySelector('.message-text').textContent = message;\n el.classList.remove('d-none');\n setTimeout(() => el.classList.add('d-none'), 8000);\n }\n}\n\n/**\n * Add a GeoJSON FeatureCollection (or array of them) to the map, zoom to\n * the data, and refresh the imported-layers info card.\n *\n * @param {Object|Object[]} geojsonInput - Single FeatureCollection or array\n * @param {string} fallbackName - Layer name when the FC has no fileName\n * @param {string} tag - Log prefix, e.g. 'ShpImport'\n */\nfunction addImportedGeoJSON(geojsonInput, fallbackName, tag) {\n const collections = Array.isArray(geojsonInput) ? geojsonInput : [geojsonInput];\n\n let totalFeatures = 0;\n for (const fc of collections) {\n if (!fc || fc.type !== 'FeatureCollection' || !fc.features?.length) continue;\n\n const layerName = fc.fileName\n ? fc.fileName.replace(/\\.[^/.]+$/, '')\n : fallbackName;\n\n const layer = mapView?.addGeoJSONLayer(fc, layerName, IMPORT_STYLE);\n if (layer) {\n // Imported file layers are not part of the built-in data model;\n // the user can remove them via the LayerSwitcher × button.\n layer.set('removable', true);\n layer.set('typeTag', 'GEO');\n importedFileLayers.push(layer);\n totalFeatures += fc.features.length;\n }\n }\n\n if (totalFeatures === 0) {\n showFileImportError('No features found in the file.');\n return;\n }\n\n console.log(`[${tag}] Added ${totalFeatures} feature(s) from ${collections.length} layer(s)`);\n\n // Zoom to the last imported layer\n const lastLayer = importedFileLayers[importedFileLayers.length - 1];\n if (lastLayer) {\n const extent = lastLayer.getSource().getExtent();\n mapView?.getMap().getView().fit(extent, { padding: [50, 50, 50, 50], maxZoom: 18 });\n }\n\n refreshImportedLayersCard();\n}\n\n/**\n * Rebuild the imported-layers info card in the left panel.\n */\nfunction refreshImportedLayersCard() {\n const infoEl = document.getElementById('imported-layers-info');\n if (!infoEl) return;\n\n if (importedFileLayers.length === 0) {\n infoEl.innerHTML = '';\n infoEl.classList.add('d-none');\n return;\n }\n\n infoEl.innerHTML = `\n
                        \n
                        \n
                        Imported Layers
                        \n \n
                        \n
                          \n
                          `;\n\n const listEl = infoEl.querySelector('#imported-layers-list');\n importedFileLayers.forEach((l, idx) => {\n const li = document.createElement('li');\n li.className = 'list-group-item d-flex justify-content-between align-items-center py-2';\n li.innerHTML = `${escapeHtml(l.get('title'))}\n \n ${l.getSource().getFeatures().length}\n \n `;\n listEl.appendChild(li);\n });\n infoEl.classList.remove('d-none');\n\n // Per-layer remove buttons\n infoEl.querySelectorAll('[data-remove-idx]').forEach(btn => {\n btn.addEventListener('click', () => {\n removeImportedLayer(Number(btn.dataset.removeIdx));\n });\n });\n\n // Remove-all button\n infoEl.querySelector('#remove-imported-layers')?.addEventListener('click', () => {\n removeImportedLayers();\n });\n}\n\n/**\n * Remove a single imported layer by its index in importedFileLayers.\n */\nfunction removeImportedLayer(idx) {\n if (idx < 0 || idx >= importedFileLayers.length) return;\n const layer = importedFileLayers[idx];\n const overlayGroup = mapView?.getOverlayGroup();\n if (overlayGroup) {\n overlayGroup.getLayers().remove(layer);\n }\n importedFileLayers.splice(idx, 1);\n refreshImportedLayersCard();\n console.log('[FileImport] Removed layer:', layer.get('title'));\n}\n\n/**\n * Remove all imported layers from the map and clear the info card.\n */\nfunction removeImportedLayers() {\n const overlayGroup = mapView?.getOverlayGroup();\n if (overlayGroup) {\n for (const layer of importedFileLayers) {\n overlayGroup.getLayers().remove(layer);\n }\n }\n importedFileLayers.length = 0;\n refreshImportedLayersCard();\n console.log('[FileImport] All imported layers removed');\n}\n\n// ---------------------------------------------------------------------------\n// Shapefile (.shp / .zip)\n// ---------------------------------------------------------------------------\n\n/**\n * Build a lookup of selected files keyed by lowercase extension.\n */\nfunction indexFilesByExtension(files) {\n const map = {};\n for (const f of files) {\n const ext = f.name.split('.').pop().toLowerCase();\n map[ext] = f;\n }\n return map;\n}\n\nasync function handleShapefileImport(evt) {\n const files = evt.target.files;\n if (!files || files.length === 0) return;\n\n const MAX_FILE_SIZE = 200 * 1024 * 1024;\n const totalSize = Array.from(files).reduce((s, f) => s + f.size, 0);\n if (totalSize > MAX_FILE_SIZE) {\n const sizeMB = (totalSize / (1024 * 1024)).toFixed(0);\n showFileImportError(\n `Files too large (${sizeMB} MB total). Maximum supported size is 200 MB.`\n );\n evt.target.value = '';\n return;\n }\n\n try {\n let geojson;\n let displayName;\n const byExt = indexFilesByExtension(files);\n\n if (byExt.zip) {\n const file = byExt.zip;\n displayName = file.name.replace(/\\.zip$/i, '');\n console.log('[ShpImport] Parsing zip', file.name, '(' + (file.size / 1024).toFixed(1) + ' KB)');\n const shp = await getShp();\n geojson = await shp(await file.arrayBuffer());\n\n } else if (byExt.shp) {\n displayName = byExt.shp.name.replace(/\\.shp$/i, '');\n\n const required = ['dbf', 'shx', 'prj'];\n const missing = required.filter(ext => !byExt[ext]);\n if (missing.length > 0) {\n showFileImportError('Missing required file(s): ' + missing.map(e => '.' + e).join(', ')\n + '. Please select .shp, .dbf, .shx and .prj together.');\n evt.target.value = '';\n return;\n }\n\n const shpObj = {};\n shpObj.shp = await byExt.shp.arrayBuffer();\n shpObj.dbf = await byExt.dbf.arrayBuffer();\n shpObj.prj = await new Response(byExt.prj).text();\n if (byExt.cpg) shpObj.cpg = await new Response(byExt.cpg).text();\n\n console.log('[ShpImport] Parsing loose files:',\n Object.keys(byExt).map(e => '.' + e).join(', '),\n '(' + (byExt.shp.size / 1024).toFixed(1) + ' KB .shp)');\n\n const shp = await getShp();\n geojson = await shp(shpObj);\n\n } else {\n showFileImportError('Please select a .zip or at least a .shp file.');\n evt.target.value = '';\n return;\n }\n\n addImportedGeoJSON(geojson, displayName, 'ShpImport');\n } catch (error) {\n console.error('[ShpImport] Failed:', error);\n showFileImportError('Failed to parse shapefile: ' + error.message);\n }\n\n evt.target.value = '';\n}\n\n// ---------------------------------------------------------------------------\n// GeoJSON (.geojson / .json)\n// ---------------------------------------------------------------------------\n\nasync function handleGeoJSONImport(evt) {\n const file = evt.target.files?.[0];\n if (!file) return;\n\n // Guard: reject files larger than 200 MB — JSON.parse cannot reliably\n // handle them in a single pass and the browser will freeze or crash.\n const MAX_FILE_SIZE = 200 * 1024 * 1024; // 200 MB\n if (file.size > MAX_FILE_SIZE) {\n const sizeMB = (file.size / (1024 * 1024)).toFixed(0);\n showFileImportError(\n `File too large (${sizeMB} MB). Maximum supported size is 200 MB. `\n + 'Consider splitting the file into smaller tiles with ogr2ogr or QGIS.'\n );\n evt.target.value = '';\n return;\n }\n\n try {\n const text = await file.text();\n console.log('[GeoJSONImport] Parsing', file.name, '(' + (file.size / 1024).toFixed(1) + ' KB)');\n\n const parsed = JSON.parse(text);\n\n // Normalise to a FeatureCollection\n let fc;\n if (parsed.type === 'FeatureCollection') {\n fc = parsed;\n } else if (parsed.type === 'Feature') {\n fc = { type: 'FeatureCollection', features: [parsed] };\n } else if (parsed.type && parsed.coordinates) {\n // Bare geometry object\n fc = { type: 'FeatureCollection', features: [{ type: 'Feature', geometry: parsed, properties: {} }] };\n } else {\n showFileImportError('The file does not contain valid GeoJSON.');\n evt.target.value = '';\n return;\n }\n\n const displayName = file.name.replace(/\\.(geo)?json$/i, '');\n addImportedGeoJSON(fc, displayName, 'GeoJSONImport');\n } catch (error) {\n console.error('[GeoJSONImport] Failed:', error);\n const sizeMB = (file.size / (1024 * 1024)).toFixed(1);\n showFileImportError(\n `Failed to import \"${file.name}\" (${sizeMB} MB): ${error.message}`\n );\n }\n\n evt.target.value = '';\n}\n\n// ---------------------------------------------------------------------------\n// KML (.kml)\n// ---------------------------------------------------------------------------\n\nasync function handleKMLImport(evt) {\n const file = evt.target.files?.[0];\n if (!file) return;\n\n const MAX_FILE_SIZE = 200 * 1024 * 1024;\n if (file.size > MAX_FILE_SIZE) {\n const sizeMB = (file.size / (1024 * 1024)).toFixed(0);\n showFileImportError(\n `File too large (${sizeMB} MB). Maximum supported size is 200 MB.`\n );\n evt.target.value = '';\n return;\n }\n\n try {\n const text = await file.text();\n console.log('[KMLImport] Parsing', file.name, '(' + (file.size / 1024).toFixed(1) + ' KB)');\n\n const kmlFormat = new KML({ extractStyles: false });\n const features = kmlFormat.readFeatures(text, {\n featureProjection: 'EPSG:3857',\n });\n\n if (!features || features.length === 0) {\n showFileImportError('No features found in the KML file.');\n evt.target.value = '';\n return;\n }\n\n // Convert OL features back to GeoJSON so we can use the shared pipeline\n const geojsonFormat = new GeoJSON();\n const fc = JSON.parse(geojsonFormat.writeFeatures(features, {\n featureProjection: 'EPSG:3857',\n dataProjection: 'EPSG:4326',\n }));\n\n const displayName = file.name.replace(/\\.kml$/i, '');\n addImportedGeoJSON(fc, displayName, 'KMLImport');\n } catch (error) {\n console.error('[KMLImport] Failed:', error);\n const sizeMB = (file.size / (1024 * 1024)).toFixed(1);\n showFileImportError(\n `Failed to import \"${file.name}\" (${sizeMB} MB): ${error.message}`\n );\n }\n\n evt.target.value = '';\n}\n\n// ---------------------------------------------------------------------------\n// Drag-and-drop on the map\n// ---------------------------------------------------------------------------\n\n/**\n * Set up the map container as a drop zone for .shp/.zip, .geojson/.json, .kml\n * files. Dragging files over the map shows a visual overlay; dropping them\n * routes to the correct import handler.\n */\nfunction initMapDropZone() {\n const container = document.querySelector('.map-container');\n if (!container) return;\n\n let dragCounter = 0; // track nested enter/leave events\n\n container.addEventListener('dragenter', (e) => {\n e.preventDefault();\n dragCounter++;\n container.classList.add('drag-over');\n });\n\n container.addEventListener('dragover', (e) => {\n e.preventDefault(); // required to allow drop\n });\n\n container.addEventListener('dragleave', (e) => {\n e.preventDefault();\n dragCounter--;\n if (dragCounter <= 0) {\n dragCounter = 0;\n container.classList.remove('drag-over');\n }\n });\n\n container.addEventListener('drop', (e) => {\n e.preventDefault();\n dragCounter = 0;\n container.classList.remove('drag-over');\n\n const files = e.dataTransfer?.files;\n if (!files || files.length === 0) return;\n\n // Build extension lookup to decide which handler to use\n const byExt = indexFilesByExtension(files);\n const exts = Object.keys(byExt);\n\n if (byExt.zip || byExt.shp) {\n // Shapefile import (zip or loose .shp + companions)\n const fakeEvt = { target: { files, value: '' } };\n Object.defineProperty(fakeEvt.target, 'value', { writable: true });\n handleShapefileImport(fakeEvt);\n } else if (byExt.geojson || byExt.json) {\n const file = byExt.geojson || byExt.json;\n const fakeEvt = { target: { files: [file], value: '' } };\n Object.defineProperty(fakeEvt.target, 'value', { writable: true });\n handleGeoJSONImport(fakeEvt);\n } else if (byExt.kml) {\n const fakeEvt = { target: { files: [byExt.kml], value: '' } };\n Object.defineProperty(fakeEvt.target, 'value', { writable: true });\n handleKMLImport(fakeEvt);\n } else {\n showFileImportError(\n 'Unsupported file type(s): ' + exts.map(e => '.' + e).join(', ')\n + '. Drop .zip, .shp, .geojson, .json, or .kml files.'\n );\n }\n });\n\n console.log('[FileImport] Map drop zone initialised');\n}\n\n// ============================================================================\n// Utilities\n// ============================================================================\n\nfunction escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\n// ============================================================================\n// Message Log — persistent stack in the right panel\n// ============================================================================\n\nconst MESSAGE_LOG_MAX = 50;\n\nconst MSG_CONFIG = {\n error: { icon: 'bi-x-circle-fill', color: 'var(--destructive, #dc3545)' },\n warning: { icon: 'bi-exclamation-triangle-fill', color: 'var(--warning, #ffc107)' },\n success: { icon: 'bi-check-circle-fill', color: 'var(--success, #198754)' },\n info: { icon: 'bi-info-circle-fill', color: 'var(--primary, #0d6efd)' },\n};\n\n/**\n * Append a message to the persistent log in the right panel.\n * Also logs to the browser console.\n *\n * @param {'error'|'warning'|'success'|'info'} type\n * @param {string} text\n */\nfunction logMessage(type, text) {\n const cfg = MSG_CONFIG[type] || MSG_CONFIG.info;\n\n // Console mirror\n const consoleFn = type === 'error' ? console.error\n : type === 'warning' ? console.warn\n : console.log;\n consoleFn('[App]', text);\n\n const log = document.getElementById('message-log');\n if (!log) return;\n\n // Remove the \"No messages yet\" placeholder if present\n const placeholder = log.querySelector('.text-muted');\n if (placeholder) placeholder.remove();\n\n // Build the entry\n const entry = document.createElement('div');\n entry.className = 'list-group-item message-log-entry py-2 px-3';\n const now = new Date();\n const time = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });\n entry.innerHTML =\n `
                          ` +\n `` +\n `
                          ${escapeHtml(text)}
                          ` +\n `${time}` +\n `
                          `;\n\n // Prepend (newest first)\n log.prepend(entry);\n\n // Cap the list\n while (log.children.length > MESSAGE_LOG_MAX) {\n log.lastElementChild.remove();\n }\n}\n\n/** Wire up the \"clear\" button */\nfunction initMessageLog() {\n const btn = document.getElementById('clear-message-log');\n if (btn) {\n btn.addEventListener('click', () => {\n const log = document.getElementById('message-log');\n if (log) {\n log.innerHTML = '
                          No messages yet.
                          ';\n }\n });\n }\n}\n\n// ============================================================================\n// GPS live-position + trail recording\n//\n// Wiring only — all GPS logic lives in the reusable src/geotracker/ engine and\n// the LUPMIS adapter in src/geotracker-lupmis.js. Here we connect the engine's\n// events to the navbar readout and the map's render/control hooks.\n// ============================================================================\n\nfunction initGpsTracking() {\n const readout = document.getElementById('gps-readout');\n const coordsEl = document.getElementById('gps-coords');\n const accEl = document.getElementById('gps-accuracy');\n const satsEl = document.getElementById('gps-sats');\n\n if (!geoTracker.isSupported) {\n if (coordsEl) coordsEl.textContent = 'No GPS';\n return;\n }\n\n // Live navbar readout — fires for every fix (one-shot Locate or watch).\n geoTracker.on('position', (fix) => {\n if (coordsEl) coordsEl.textContent = `${formatCoord(fix.lat)}, ${formatCoord(fix.lon)}`;\n if (accEl) accEl.textContent = formatAccuracy(fix.accuracy);\n if (satsEl) satsEl.textContent = `${fix.satellites != null ? fix.satellites : '—'} sat`;\n if (readout) {\n readout.classList.add('active');\n readout.classList.remove('quality-good', 'quality-fair', 'quality-poor');\n readout.classList.add('quality-' + accuracyQuality(fix.accuracy));\n }\n mapView?.showCurrentPosition(fix.lon, fix.lat, fix.accuracy);\n });\n\n // Each recorded waypoint extends the on-map trail line.\n geoTracker.on('point', (evt) => {\n mapView?.appendTrailPoint(evt.point.lon, evt.point.lat);\n });\n\n geoTracker.on('error', (err) => {\n console.warn('[GPS]', err?.message || err);\n if (err && err.code === 1) { // PERMISSION_DENIED\n showError('Location permission denied. Enable location access to use GPS.');\n }\n });\n\n // \"Locate me\" → one-shot position + recenter.\n mapView.onLocateMe(async () => {\n try {\n const fix = await geoTracker.getCurrentPosition();\n mapView.centerOn(fix.lon, fix.lat, 16);\n } catch (err) {\n showError('Could not get your location: ' + (err?.message || err));\n }\n });\n\n // \"Record trail\" → start/stop. Recording persists locally and syncs on stop.\n mapView.onToggleRecording(async (start) => {\n if (start) {\n try {\n await dbReady;\n mapView.startTrailRender();\n mapView.setRecordingState(true);\n readout?.classList.add('recording');\n await geoTracker.startRecording({ name: `Trail ${new Date().toLocaleString()}` });\n showSuccess('GPS trail recording started');\n } catch (err) {\n mapView.setRecordingState(false);\n readout?.classList.remove('recording');\n showError('Could not start recording: ' + (err?.message || err));\n }\n } else {\n try {\n const res = await geoTracker.stopRecording();\n mapView.setRecordingState(false);\n readout?.classList.remove('recording');\n if (res) {\n const msg = `Trail saved: ${res.pointCount} points, ${formatDistance(res.distanceM)}` +\n (res.synced ? ' — synced' : ' — will sync when online');\n showSuccess(msg);\n }\n } catch (err) {\n showError('Error stopping recording: ' + (err?.message || err));\n }\n }\n });\n\n // Retry uploading trails recorded while offline — on load and when back online.\n const trySync = async () => {\n if (!isOnline()) return;\n try {\n await dbReady;\n const r = await geoTracker.syncPending();\n if (r.pushed) console.log(`[GPS] Synced ${r.pushed} pending trail(s)`);\n } catch (e) {\n console.warn('[GPS] pending-sync error', e);\n }\n };\n trySync();\n onOfflineChange((offline) => { if (!offline) trySync(); });\n}\n\n// ============================================================================\n// Toast-style alerts (auto-dismiss) + persistent log\n// ============================================================================\n\nfunction showError(message) {\n logMessage('error', message);\n const el = document.getElementById('error-message');\n if (el) {\n el.querySelector('.message-text').textContent = message;\n el.classList.remove('d-none');\n setTimeout(() => el.classList.add('d-none'), 5000);\n }\n}\n\nfunction showSuccess(message) {\n logMessage('success', message);\n const el = document.getElementById('success-message');\n if (el) {\n el.querySelector('.message-text').textContent = message;\n el.classList.remove('d-none');\n setTimeout(() => el.classList.add('d-none'), 3000);\n }\n}\n\nfunction showWarning(message) {\n logMessage('warning', message);\n const el = document.getElementById('warning-message');\n if (el) {\n el.querySelector('.message-text').textContent = message;\n el.classList.remove('d-none');\n setTimeout(() => el.classList.add('d-none'), 5000);\n }\n}\n\n// ============================================================================\n// Fieldwork Mode\n// ============================================================================\n\nfunction initFieldworkMode() {\n const toggle = document.getElementById('fieldwork-mode-toggle');\n if (!toggle) return;\n\n // Restore saved preference\n const saved = localStorage.getItem('fieldwork-mode');\n if (saved === 'true') {\n document.documentElement.classList.add('fieldwork-mode');\n toggle.checked = true;\n }\n\n toggle.addEventListener('change', () => {\n document.documentElement.classList.toggle('fieldwork-mode', toggle.checked);\n localStorage.setItem('fieldwork-mode', toggle.checked);\n console.log('[Settings] Fieldwork mode', toggle.checked ? 'ON' : 'OFF');\n });\n}\n\n// ============================================================================\n// Dark Mode\n// ============================================================================\n\nfunction initDarkMode() {\n const toggle = document.getElementById('dark-mode-toggle');\n if (!toggle) return;\n\n function applyDark(on) {\n document.documentElement.classList.toggle('dark-mode', on);\n // Bootstrap 5.3 built-in dark mode support\n document.documentElement.setAttribute('data-bs-theme', on ? 'dark' : 'light');\n }\n\n // Restore saved preference\n const saved = localStorage.getItem('dark-mode');\n if (saved === 'true') {\n toggle.checked = true;\n applyDark(true);\n }\n\n toggle.addEventListener('change', () => {\n applyDark(toggle.checked);\n localStorage.setItem('dark-mode', toggle.checked);\n console.log('[Settings] Dark mode', toggle.checked ? 'ON' : 'OFF');\n });\n}\n\n// ============================================================================\n// Measurement System\n// ============================================================================\n\nfunction initMeasurementSystem() {\n const toggle = document.getElementById('measurement-system-toggle');\n const label = document.getElementById('measurement-system-label');\n if (!toggle) return;\n\n function updateLabel() {\n if (label) label.textContent = toggle.checked ? 'Imperial' : 'Metric';\n }\n\n // Restore saved preference\n const saved = localStorage.getItem('measurement-system');\n if (saved === 'imperial') {\n toggle.checked = true;\n }\n updateLabel();\n\n // Apply saved setting to the scale bar on load\n mapView?.setScaleBarUnits(saved || 'metric');\n\n toggle.addEventListener('change', () => {\n const system = toggle.checked ? 'imperial' : 'metric';\n localStorage.setItem('measurement-system', system);\n updateLabel();\n mapView?.setScaleBarUnits(system);\n console.log('[Settings] Measurement system:', system);\n });\n}\n\n/**\n * Default base map selector — persisted in localStorage.\n * Keys must match those handled by MapView.setBaseMap().\n */\nfunction initDefaultBasemap() {\n const select = document.getElementById('default-basemap-select');\n if (!select) return;\n\n // Restore saved preference (default: topo)\n const saved = localStorage.getItem('default-basemap') || 'topo';\n select.value = saved;\n\n select.addEventListener('change', () => {\n const key = select.value;\n localStorage.setItem('default-basemap', key);\n mapView?.setBaseMap(key);\n console.log('[Settings] Default base map:', key);\n });\n\n // Keep the dropdown in sync when the user switches via the floating\n // base-map picker (or any other UI) — MapView fires `basemapchange`\n // from setBaseMap().\n mapView?.getMap()?.on('basemapchange', (evt) => {\n if (evt?.key && select.value !== evt.key) {\n select.value = evt.key;\n try { localStorage.setItem('default-basemap', evt.key); } catch {}\n }\n });\n}\n\n/**\n * Offline Map Tiles card — shows per-provider cache stats and offers a\n * \"Clear cached tiles\" button. Stats refresh whenever the Settings panel\n * is opened so the numbers are always current.\n */\nfunction initOfflineTileCache() {\n const statsEl = document.getElementById('tile-cache-stats');\n const clearBtn = document.getElementById('clear-tiles-btn');\n const offcanvas = document.getElementById('offcanvasBottom');\n if (!statsEl || !clearBtn || !offcanvas) return;\n\n /** Format a byte count into a human-friendly string. */\n function fmtBytes(bytes) {\n if (!bytes) return '0 KB';\n if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(0) + ' KB';\n if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';\n return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';\n }\n\n // Track in-flight refresh so rapid calls don't overlap and to allow a\n // controllerchange handler to know when a refresh is already underway.\n let refreshInFlight = null;\n\n /** Render the stats panel. */\n async function refresh() {\n if (refreshInFlight) return refreshInFlight;\n\n // If the SW hasn't taken control yet, give the user a friendly hint\n // instead of immediately failing. The wait inside getTileCacheStats()\n // will resolve once the SW becomes available, at which point this\n // refresh completes normally — no reload needed.\n const swActive = !!navigator.serviceWorker?.controller;\n statsEl.innerHTML = swActive\n ? '
                          Loading…
                          '\n : '
                          Initialising service worker…
                          ';\n\n refreshInFlight = (async () => {\n try {\n const stats = await getTileCacheStats();\n\n if (!stats) {\n statsEl.innerHTML = `\n
                          \n Tile cache stats unavailable. Try reloading the page if this persists.\n
                          `;\n return;\n }\n\n const total = stats.totals;\n const rows = stats.byProvider\n .filter((p) => p.count > 0)\n .map((p) => `\n \n ${escapeHtml(p.label)}\n ${p.count.toLocaleString()} / ${p.limit.toLocaleString()}\n ${fmtBytes(p.estBytes)}\n \n \n \n `).join('');\n\n let storageNote = '';\n const est = await getStorageEstimate();\n if (est && est.quota > 0) {\n const pct = ((est.usage / est.quota) * 100).toFixed(1);\n storageNote = `\n
                          \n Total app storage: ${fmtBytes(est.usage)} of ${fmtBytes(est.quota)} available (${pct}%)\n
                          `;\n }\n\n if (total.count === 0) {\n statsEl.innerHTML = `\n
                          \n No tiles cached yet. Pan and zoom the map to start caching tiles automatically.\n
                          ${storageNote}`;\n clearBtn.disabled = true;\n return;\n }\n\n statsEl.innerHTML = `\n
                          \n ${total.count.toLocaleString()} tiles cached, ~${fmtBytes(total.estBytes)} on this device\n
                          \n \n \n \n \n \n \n \n ${rows}\n
                          Base mapCached / limitApprox. size
                          ${storageNote}`;\n clearBtn.disabled = false;\n\n // Per-provider Clear — confirm, clear that bucket only, refresh\n statsEl.querySelectorAll('.provider-clear-btn').forEach((btn) => {\n btn.addEventListener('click', async (e) => {\n e.preventDefault();\n const cacheName = btn.dataset.cache;\n const label = btn.dataset.label || cacheName;\n if (!confirm(`Clear cached \"${label}\" tiles?\\n\\nOther providers are not affected. The tiles will re-download as you browse online.`)) {\n return;\n }\n btn.disabled = true;\n const ok = await clearTileCacheForProvider(cacheName);\n if (ok) {\n console.log(`[Settings] Cleared tile cache for ${label}`);\n } else {\n console.warn(`[Settings] Could not clear tile cache for ${label}`);\n }\n await refresh();\n });\n });\n } finally {\n refreshInFlight = null;\n }\n })();\n\n return refreshInFlight;\n }\n\n // Clear button — confirm, then clear, then refresh\n clearBtn.addEventListener('click', async () => {\n if (!confirm('Clear all cached map tiles from this device? You will need to be online to view them again.')) {\n return;\n }\n clearBtn.disabled = true;\n const ok = await clearTileCaches();\n if (ok) {\n console.log('[Settings] Tile caches cleared');\n } else {\n console.warn('[Settings] Tile-cache clear failed');\n }\n await refresh();\n });\n\n // Refresh stats whenever the Settings offcanvas opens\n offcanvas.addEventListener('show.bs.offcanvas', refresh);\n\n // Auto-refresh when a (new) service worker takes control of the page —\n // makes the panel populate as soon as the SW is available, even if the\n // user is staring at it during initial install or during an SW update.\n onServiceWorkerControllerChange(() => {\n console.log('[Settings] SW controller changed → refreshing tile-cache stats');\n refresh();\n });\n\n // Also do an initial render so the card isn't empty if Settings is open\n // immediately on load.\n refresh();\n}\n\n/**\n * Offline-download dialog (Phase 2). Allows users to pre-fetch tiles for a\n * chosen extent and zoom range so they can use the map without connectivity.\n */\nfunction initOfflineDownloadDialog() {\n const triggerBtn = document.getElementById('download-tiles-btn');\n const modalEl = document.getElementById('offline-download-modal');\n if (!triggerBtn || !modalEl) return;\n\n const modal = Modal.getOrCreateInstance(modalEl);\n\n // ----- Element refs -----\n const formView = document.getElementById('offline-download-form-view');\n const progressView = document.getElementById('offline-download-progress-view');\n const doneView = document.getElementById('offline-download-done-view');\n const cancelBtn = document.getElementById('offline-download-cancel-btn');\n const startBtn = document.getElementById('offline-download-start-btn');\n const closeDoneBtn = document.getElementById('offline-download-close-done-btn');\n const headerCloseBtn = document.getElementById('offline-download-close-btn');\n\n const basemapSelect = document.getElementById('offline-basemap-select');\n const minZoomInput = document.getElementById('offline-min-zoom');\n const maxZoomInput = document.getElementById('offline-max-zoom');\n const ackCheck = document.getElementById('offline-ack-check');\n const estimateEl = document.getElementById('offline-estimate-detail');\n const estimateBox = document.getElementById('offline-estimate');\n\n const areaViewRadio = document.getElementById('offline-area-view');\n const areaDistrictRadio = document.getElementById('offline-area-district');\n const areaGhanaRadio = document.getElementById('offline-area-ghana');\n const areaViewInfo = document.getElementById('offline-area-view-info');\n const areaDistrictInfo = document.getElementById('offline-area-district-info');\n\n const progressBar = document.getElementById('offline-progress-bar');\n const progressPercent = document.getElementById('offline-progress-percent');\n const progressCounts = document.getElementById('offline-progress-counts');\n const progressOk = document.getElementById('offline-progress-ok');\n const progressFailed = document.getElementById('offline-progress-failed');\n const progressEta = document.getElementById('offline-progress-eta');\n\n const doneTitle = document.getElementById('offline-done-title');\n const doneDetail = document.getElementById('offline-done-detail');\n\n // ----- State -----\n let currentDownloader = null;\n\n /** Format byte count for display. */\n function fmtBytes(b) {\n if (!b) return '0 KB';\n if (b < 1024 * 1024) return (b / 1024).toFixed(0) + ' KB';\n if (b < 1024 * 1024 * 1024) return (b / (1024 * 1024)).toFixed(1) + ' MB';\n return (b / (1024 * 1024 * 1024)).toFixed(2) + ' GB';\n }\n\n /** Format ms → human-readable duration. */\n function fmtDuration(ms) {\n if (!ms || ms < 1000) return '< 1 s';\n const s = Math.round(ms / 1000);\n if (s < 60) return s + ' s';\n const m = Math.floor(s / 60);\n const r = s % 60;\n if (m < 60) return `${m} min ${r} s`;\n const h = Math.floor(m / 60);\n return `${h} h ${m % 60} min`;\n }\n\n /** Get the chosen extent based on the radio selection. Returns null if invalid. */\n function getSelectedExtent() {\n if (areaViewRadio.checked) {\n return mapView?.getCurrentViewExtent() || null;\n }\n if (areaDistrictRadio.checked) {\n return mapView?.getDistrictBoundaryExtent()?.extent || null;\n }\n if (areaGhanaRadio.checked) {\n return GHANA_EXTENT_3857;\n }\n return null;\n }\n\n /** Recalculate and update the live estimate display. */\n function updateEstimate() {\n const baseMap = basemapSelect.value;\n const minZ = parseInt(minZoomInput.value, 10);\n const maxZ = parseInt(maxZoomInput.value, 10);\n\n if (Number.isNaN(minZ) || Number.isNaN(maxZ) || minZ > maxZ) {\n estimateEl.textContent = 'Invalid zoom range';\n estimateBox.classList.replace('alert-info', 'alert-warning');\n startBtn.disabled = true;\n return;\n }\n\n const extent = getSelectedExtent();\n if (!extent) {\n estimateEl.textContent = 'Selected area is not available.';\n estimateBox.classList.replace('alert-info', 'alert-warning');\n startBtn.disabled = true;\n return;\n }\n\n const tplMaxZoom = BASEMAP_TEMPLATES[baseMap]?.maxZoom ?? 19;\n const effMaxZ = Math.min(maxZ, tplMaxZoom);\n const count = countTiles(extent, minZ, effMaxZ);\n const bytes = estimatedSizeBytes(count);\n\n let warningHTML = '';\n if (effMaxZ < maxZ) {\n warningHTML = `
                          Zoom ${maxZ} is above this provider's max (${tplMaxZoom}); will clamp to ${tplMaxZoom}.`;\n }\n if (count > 8000) {\n warningHTML += `
                          More than 8 000 tiles — exceeds the per-provider cache limit. Earlier tiles will be evicted as new ones arrive.`;\n }\n\n estimateEl.innerHTML =\n `${count.toLocaleString()} tiles · ` +\n `~${fmtBytes(bytes)}` +\n warningHTML;\n\n estimateBox.classList.toggle('alert-warning', !!warningHTML);\n estimateBox.classList.toggle('alert-info', !warningHTML);\n\n startBtn.disabled = !ackCheck.checked || count === 0;\n }\n\n /** Update the area-radio info labels (tile count + size estimate). */\n function updateAreaInfos() {\n const view = mapView?.getCurrentViewExtent();\n if (view) {\n areaViewInfo.textContent = ' · ready';\n } else {\n areaViewInfo.textContent = '';\n }\n\n const dist = mapView?.getDistrictBoundaryExtent();\n if (dist) {\n areaDistrictInfo.textContent = '';\n areaDistrictRadio.disabled = false;\n } else {\n areaDistrictInfo.textContent = ' (not loaded — connect online to fetch)';\n areaDistrictRadio.disabled = true;\n if (areaDistrictRadio.checked) areaViewRadio.checked = true;\n }\n }\n\n /** Reset the modal to its initial form state. */\n function resetModal() {\n formView.classList.remove('d-none');\n progressView.classList.add('d-none');\n doneView.classList.add('d-none');\n\n startBtn.classList.remove('d-none');\n cancelBtn.classList.remove('d-none');\n cancelBtn.textContent = 'Cancel';\n closeDoneBtn.classList.add('d-none');\n headerCloseBtn.disabled = false;\n\n ackCheck.checked = false;\n startBtn.disabled = true;\n\n currentDownloader = null;\n }\n\n // ----- Event wiring -----\n\n triggerBtn.addEventListener('click', () => {\n resetModal();\n updateAreaInfos();\n updateEstimate();\n modal.show();\n });\n\n // Recalculate estimate on any input change\n basemapSelect.addEventListener('change', updateEstimate);\n minZoomInput.addEventListener('input', updateEstimate);\n maxZoomInput.addEventListener('input', updateEstimate);\n areaViewRadio.addEventListener('change', updateEstimate);\n areaDistrictRadio.addEventListener('change', updateEstimate);\n areaGhanaRadio.addEventListener('change', updateEstimate);\n ackCheck.addEventListener('change', updateEstimate);\n\n // Start the download\n startBtn.addEventListener('click', async () => {\n const baseMap = basemapSelect.value;\n const minZ = parseInt(minZoomInput.value, 10);\n const maxZ = parseInt(maxZoomInput.value, 10);\n const extent = getSelectedExtent();\n if (!extent) return;\n\n // Switch UI to progress view\n formView.classList.add('d-none');\n progressView.classList.remove('d-none');\n startBtn.classList.add('d-none');\n cancelBtn.textContent = 'Cancel download';\n headerCloseBtn.disabled = true;\n\n progressBar.style.width = '0%';\n progressBar.setAttribute('aria-valuenow', '0');\n progressPercent.textContent = '0%';\n progressCounts.textContent = '0 of 0 tiles';\n progressOk.textContent = '0';\n progressFailed.textContent = '0';\n progressEta.textContent = '—';\n\n currentDownloader = new OfflineTileDownloader({\n baseMap,\n extent3857: extent,\n minZoom: minZ,\n maxZoom: maxZ,\n onProgress: (s) => {\n if (s.total > 0) {\n const pct = Math.min(100, Math.round((s.done / s.total) * 100));\n progressBar.style.width = pct + '%';\n progressBar.setAttribute('aria-valuenow', String(pct));\n progressPercent.textContent = pct + '%';\n progressCounts.textContent = `${s.done.toLocaleString()} of ${s.total.toLocaleString()} tiles`;\n }\n progressOk.textContent = s.ok.toLocaleString();\n progressFailed.textContent = s.failed.toLocaleString();\n progressEta.textContent = s.etaMs != null ? fmtDuration(s.etaMs) : '—';\n },\n });\n\n let result;\n try {\n result = await currentDownloader.start();\n } catch (err) {\n console.error('[OfflineDownload] failed:', err);\n result = { phase: 'error', done: 0, total: 0, ok: 0, failed: 0 };\n }\n\n // Switch UI to done view\n progressView.classList.add('d-none');\n doneView.classList.remove('d-none');\n cancelBtn.classList.add('d-none');\n closeDoneBtn.classList.remove('d-none');\n headerCloseBtn.disabled = false;\n\n if (result.phase === 'cancelled') {\n doneTitle.textContent = 'Download cancelled';\n doneDetail.innerHTML = `Stopped after ${result.done.toLocaleString()} of ${result.total.toLocaleString()} tiles.
                          ` +\n `${result.ok.toLocaleString()} fetched · ${result.failed.toLocaleString()} failed.`;\n } else if (result.phase === 'error') {\n doneTitle.textContent = 'Download failed';\n doneDetail.textContent = 'See console for details.';\n } else {\n doneTitle.textContent = 'Download complete';\n doneDetail.innerHTML = `${result.ok.toLocaleString()} tiles cached` +\n (result.failed > 0 ? `, ${result.failed.toLocaleString()} failed` : '') +\n `.
                          Took ${fmtDuration(result.elapsedMs)}.`;\n }\n });\n\n // Cancel button — either close modal (form view) or cancel download (progress view)\n cancelBtn.addEventListener('click', () => {\n if (currentDownloader) {\n currentDownloader.cancel();\n }\n });\n\n // When modal is fully hidden, reset for next time\n modalEl.addEventListener('hidden.bs.modal', () => {\n if (currentDownloader) currentDownloader.cancel();\n resetModal();\n });\n}\n\n/**\n * Account card — displays the signed-in user from window.LUPMIS_SESSION\n * (injected by public/index.php) and wires the \"Sign out\" button.\n *\n * In local dev (no PHP), window.LUPMIS_SESSION is absent / empty and the\n * card shows \"Guest (no session)\" without a Sign-out button.\n */\n/**\n * Account UI — populates the right-side Menu offcanvas (id=\"menuOffcanvas\")\n * with the signed-in user's details, and wires the Sign-out button.\n * The Menu is opened from the navbar Menu button (id=\"menu-btn\").\n *\n * Three states:\n * • authenticated — show name, email, district info, and \"Sign out\"\n * • unauthenticated (PHP ran, no SSO cookie) — show \"Sign in\" link\n * • no-session (window.LUPMIS_SESSION undefined → dev mode) — show\n * a warning note that the page wasn't served via index.php\n */\nfunction initAccountCard() {\n const session = getSession();\n const menuBtn = document.getElementById('menu-btn');\n const avatarEl = document.getElementById('menu-user-avatar');\n const nameEl = document.getElementById('menu-user-name');\n const emailEl = document.getElementById('menu-user-email');\n const detailEl = document.getElementById('menu-user-detail');\n const signoutBtn = document.getElementById('menu-signout-btn');\n const signinLink = document.getElementById('menu-signin-link');\n const noSessNote = document.getElementById('menu-no-session-note');\n\n if (!menuBtn || !avatarEl || !nameEl || !emailEl || !detailEl || !signoutBtn) {\n console.warn('[AccountMenu] One or more elements missing — shell may be stale. Hard-refresh.');\n return;\n }\n\n const isAuthenticated = !!session && !!session.user_id;\n\n if (isAuthenticated) {\n // ---------- Authenticated state ----------\n const displayName = [session.title, session.full_name].filter(Boolean).join(' ').trim()\n || session.username || 'Authenticated user';\n const initial = (session.full_name || session.username || '?').trim().charAt(0).toUpperCase();\n avatarEl.textContent = initial;\n avatarEl.style.background = 'var(--brand-navy, #1e1a4b)';\n nameEl.textContent = displayName;\n emailEl.textContent = session.email || '';\n\n const bits = [];\n if (session.district_id != null) bits.push(`District ${escapeHtml(String(session.district_id))}`);\n if (session.region_id != null) bits.push(`Region ${escapeHtml(String(session.region_id))}`);\n if (session.ua_position) bits.push(escapeHtml(session.ua_position));\n detailEl.innerHTML = bits.join(' · ') || 'No district info';\n\n signoutBtn.classList.remove('d-none');\n signoutBtn.addEventListener('click', () => handleSignOut(session), { once: false });\n signinLink?.classList.add('d-none');\n noSessNote?.classList.add('d-none');\n menuBtn.removeAttribute('data-state');\n menuBtn.setAttribute('title', `Menu — ${displayName}`);\n } else if (typeof window.LUPMIS_SESSION === 'undefined') {\n // ---------- Dev mode (no PHP processing) ----------\n avatarEl.innerHTML = '';\n avatarEl.style.background = 'var(--brand-orange-warm, #ff9e1b)';\n nameEl.textContent = 'No session injected';\n emailEl.textContent = '';\n detailEl.textContent = '';\n\n signoutBtn.classList.add('d-none');\n signinLink?.classList.add('d-none');\n noSessNote?.classList.remove('d-none');\n menuBtn.dataset.state = 'no-session';\n menuBtn.setAttribute('title', 'Menu (no session — dev mode)');\n } else {\n // ---------- PHP ran but the user has no valid SSO session ----------\n avatarEl.innerHTML = '';\n avatarEl.style.background = 'var(--brand-gray-medium, #7a7a7a)';\n nameEl.textContent = 'Not signed in';\n emailEl.textContent = '';\n detailEl.textContent = '';\n\n signoutBtn.classList.add('d-none');\n signinLink?.classList.remove('d-none');\n noSessNote?.classList.add('d-none');\n menuBtn.dataset.state = 'unauthenticated';\n menuBtn.setAttribute('title', 'Menu (not signed in)');\n }\n}\n\n// Legacy chip+popover removed — replaced by the navbar Menu button +\n// right-side menuOffcanvas. See initAccountCard above.\n\n/**\n * Sign-out flow:\n * 1. Confirm with the user.\n * 2. Best-effort fire-and-forget call to the SSO logout endpoint so the\n * server-side token is invalidated (no-cors mode tolerates CORS issues).\n * 3. Expire the local sso_auth_token cookie on the parent domain so the\n * browser stops sending it.\n * 4. Redirect to the SSO login page — leaves the user on familiar ground\n * (and on next visit, index.php sees no session and serves a fresh\n * page with no LUPMIS_SESSION).\n */\nasync function handleSignOut(session) {\n if (!confirm(`Return to Landing Page, ${session?.full_name || session?.username || 'user'}?`)) {\n return;\n }\n\n // 1. Best-effort: invalidate the SSO token server-side\n const cookieToken = document.cookie\n .split(';')\n .map((c) => c.trim())\n .find((c) => c.startsWith('sso_auth_token='))\n ?.split('=')[1];\n if (cookieToken) {\n try {\n // no-cors swallows CORS errors; we don't read the response\n await fetch('https://lupmis4luspa.org/sso/logout?token=' + encodeURIComponent(cookieToken), {\n method: 'GET',\n mode: 'no-cors',\n credentials: 'include',\n cache: 'no-store',\n });\n } catch (err) {\n console.warn('[Signout] Best-effort SSO logout call failed:', err);\n }\n }\n\n // 2. Clear the cookie on the shared parent domain\n // Set with both leading-dot and no-dot variants; browsers vary on which sticks.\n const past = 'Thu, 01 Jan 1970 00:00:00 GMT';\n document.cookie = `sso_auth_token=; expires=${past}; path=/; domain=.lupmis4luspa.org`;\n document.cookie = `sso_auth_token=; expires=${past}; path=/; domain=lupmis4luspa.org`;\n document.cookie = `sso_auth_token=; expires=${past}; path=/`;\n\n // 3. Redirect to the central LUSPA login\n window.location.href = 'https://lupmis4luspa.org/';\n}\n\n// ============================================================================\n// Start Application\n// ============================================================================\n\n// Wait for DOM to be ready\nif (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', initApp);\n} else {\n initApp();\n}\n"],"file":"assets/index-YjHYbDyk.js"} \ No newline at end of file diff --git a/dist/embed.php b/dist/embed.php new file mode 100644 index 0000000..5a2d65b --- /dev/null +++ b/dist/embed.php @@ -0,0 +1,157 @@ + $validate_url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "GET", + CURLOPT_HTTPHEADER => [ "Content-Type: application/xml" ], + ]); + $response = curl_exec($curl); + $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + + if ($httpCode === 200) { + $data = json_decode($response, true); + if ( + is_array($data) + && isset($data['valid']) && $data['valid'] === true + && isset($data['logged_in_user']) && is_array($data['logged_in_user']) + ) { + foreach ($data['logged_in_user'] as $key => $value) { + $_SESSION[$key] = $value; + } + } + } else { + setcookie('sso_auth_token', '', time() - 3600, '/', '.lupmis4luspa.org'); + } +} + +// ──────────────────────────────────────────────────────────────────────────── +// Production access guard — same rule as index.php +// ──────────────────────────────────────────────────────────────────────────── +$host = $_SERVER['HTTP_HOST'] ?? ''; +$isProduction = (bool) preg_match('/(^|\.)lupmis4luspa\.org$/i', $host); +if ($isProduction && !isset($_SESSION['user_id'])) { + header('Location: https://lupmis4luspa.org/', true, 302); + exit; +} + +// ──────────────────────────────────────────────────────────────────────────── +// Session payload (same shape as index.php) +// ──────────────────────────────────────────────────────────────────────────── +$payload = []; +if (isset($_SESSION['user_id'])) { + $payload = [ + 'user_id' => $_SESSION['user_id'] ?? null, + 'ua_id' => $_SESSION['ua_id'] ?? null, + 'username' => $_SESSION['username'] ?? null, + 'title' => $_SESSION['title'] ?? null, + 'full_name' => $_SESSION['full_name'] ?? null, + 'email' => $_SESSION['email'] ?? null, + 'user_type' => $_SESSION['user_type'] ?? null, + 'phone' => $_SESSION['phone'] ?? null, + 'ua_position' => $_SESSION['ua_position'] ?? null, + 'region_id' => $_SESSION['region_id'] ?? null, + 'district_id' => $_SESSION['district_id'] ?? null, + ]; +} + +// ──────────────────────────────────────────────────────────────────────────── +// Embed config — parse the contract's URL parameters (see Permit Map +// Integration doc §2.1). Strict whitelisting + type coercion keeps invalid +// input from reaching the PWA. +// ──────────────────────────────────────────────────────────────────────────── +$validBasemaps = ['topo','osm','satellite','googlesat','carto-light','carto-dark','none']; +$validModes = ['permit']; + +$mode = isset($_GET['mode']) ? (string)$_GET['mode'] : 'permit'; +$basemap = isset($_GET['basemap']) ? (string)$_GET['basemap'] : null; +$upn = isset($_GET['upn']) ? (string)$_GET['upn'] : null; +$appCode = isset($_GET['application_code']) ? (string)$_GET['application_code'] : null; + +$lon = isset($_GET['lon']) && is_numeric($_GET['lon']) ? (float)$_GET['lon'] : null; +$lat = isset($_GET['lat']) && is_numeric($_GET['lat']) ? (float)$_GET['lat'] : null; +$zoom = isset($_GET['zoom']) && is_numeric($_GET['zoom']) ? (int)$_GET['zoom'] : null; + +$embed = [ + 'mode' => in_array($mode, $validModes, true) ? $mode : 'permit', + 'lon' => $lon, + 'lat' => $lat, + 'zoom' => $zoom, + 'upn' => $upn, + 'basemap' => ($basemap && in_array($basemap, $validBasemaps, true)) ? $basemap : null, + 'application_code' => $appCode, +]; + +// ──────────────────────────────────────────────────────────────────────────── +// Read the built index.html and inject the session + embed config +// ──────────────────────────────────────────────────────────────────────────── +$indexPath = __DIR__ . '/index.html'; +$html = is_readable($indexPath) + ? file_get_contents($indexPath) + : '

                          LUPMIS2 PWA

                          index.html is missing.

                          '; + +$jsonFlags = JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT; +$sessionJson = json_encode($payload, $jsonFlags); +$embedJson = json_encode($embed, $jsonFlags); +$inject = + ""; + +$html = preg_replace('/]*>/i', '$0' . "\n " . $inject, $html, 1); + +// ──────────────────────────────────────────────────────────────────────────── +// Headers — frame-ancestors restricts who may embed; tighten this list once +// the real permitting host is confirmed. NEVER use `frame-ancestors *`. +// ──────────────────────────────────────────────────────────────────────────── +$EMBED_ALLOWED_PARENTS = [ + 'https://permits.lupmis4luspa.org', + // Add local dev parents here if needed, e.g. 'http://localhost:8000'. +]; +$frameAncestors = "'self' " . implode(' ', $EMBED_ALLOWED_PARENTS); + +header("Content-Security-Policy: frame-ancestors {$frameAncestors}"); +header('Content-Type: text/html; charset=utf-8'); +header('Cache-Control: no-store, must-revalidate'); +header('Pragma: no-cache'); +header('X-Content-Type-Options: nosniff'); +header('Referrer-Policy: strict-origin-when-cross-origin'); + +echo $html; diff --git a/dist/index.html b/dist/index.html index eaeda3b..341e73f 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1525,6 +1525,33 @@ display: none; } + /* ---------------------------------------------------------------- + Iframe embed mode (see public/embed.php + src/embed-bridge.js). + The body class is set by the inline script injected by embed.php + (`embed embed-mode-permit`). In permit mode the iframe is hosted + inside the permitting app, so all LUPMIS2 chrome is hidden — + only the map and its floating controls remain visible. The map + container expands to fill the viewport. + ---------------------------------------------------------------- */ + body.embed-mode-permit .navbar, + body.embed-mode-permit .bottom-dock, + body.embed-mode-permit .offcanvas, + body.embed-mode-permit .offcanvas-toggle, + body.embed-mode-permit #install-btn, + body.embed-mode-permit #offline-indicator, + body.embed-mode-permit .map-tools-bar { + display: none !important; + } + body.embed-mode-permit .main-content, + body.embed-mode-permit .map-container { + flex: 1 1 auto; + height: 100%; + } + body.embed-mode-permit #map { + height: 100svh; + height: 100dvh; + } + @media (max-width: 576px) { .ol-editbar.ol-bar { /* NOTE: display must NOT be !important — ol-ext toggles the bar via an @@ -1571,7 +1598,7 @@ } } - + diff --git a/dist/index.php b/dist/index.php index d6ff0e0..dd6ee37 100644 --- a/dist/index.php +++ b/dist/index.php @@ -65,6 +65,21 @@ if (!isset($_SESSION['user_id']) && isset($_COOKIE['sso_auth_token'])) { } } +// ──────────────────────────────────────────────────────────────────────────── +// Production access guard +// ──────────────────────────────────────────────────────────────────────────── +// On the public production host, calling this file without a valid SSO session +// is not allowed — bounce the visitor to the central LUSPA login portal. On +// local development (any host that is not *.lupmis4luspa.org, e.g. localhost, +// 127.0.0.1, a developer's .local hostname) the guard is bypassed so the PHP +// entry point can still be exercised directly during testing. +$host = $_SERVER['HTTP_HOST'] ?? ''; +$isProduction = (bool) preg_match('/(^|\.)lupmis4luspa\.org$/i', $host); +if ($isProduction && !isset($_SESSION['user_id'])) { + header('Location: https://lupmis4luspa.org/', true, 302); + exit; +} + // ──────────────────────────────────────────────────────────────────────────── // Build the payload exposed to the PWA as window.LUPMIS_SESSION // ──────────────────────────────────────────────────────────────────────────── diff --git a/dist/sw.js b/dist/sw.js index 8536061..4d327ad 100644 --- a/dist/sw.js +++ b/dist/sw.js @@ -29,7 +29,11 @@ // mobile drawing-toolbar wrap, base-map "None" option, and the Safari // 100svh dock fix. New hashed bundle + updated shell — bump to evict the // old module/shell caches. -const CACHE_VERSION = 'v8'; +// v9: Iframe embed endpoint (/embed via public/embed.php) + postMessage bridge +// for the permitting integration; lu_parcels structural refactor in the +// local DB; production access guard + no-district overlay; LayerSwitcher +// ordering fix. New shell + hashed bundle. +const CACHE_VERSION = 'v9'; const SHELL_CACHE = `shell-${CACHE_VERSION}`; const MODULES_CACHE = `modules-${CACHE_VERSION}`; const API_CACHE = `api-${CACHE_VERSION}`; diff --git a/index.html b/index.html index f8a3051..5c38881 100644 --- a/index.html +++ b/index.html @@ -1525,6 +1525,33 @@ display: none; } + /* ---------------------------------------------------------------- + Iframe embed mode (see public/embed.php + src/embed-bridge.js). + The body class is set by the inline script injected by embed.php + (`embed embed-mode-permit`). In permit mode the iframe is hosted + inside the permitting app, so all LUPMIS2 chrome is hidden — + only the map and its floating controls remain visible. The map + container expands to fill the viewport. + ---------------------------------------------------------------- */ + body.embed-mode-permit .navbar, + body.embed-mode-permit .bottom-dock, + body.embed-mode-permit .offcanvas, + body.embed-mode-permit .offcanvas-toggle, + body.embed-mode-permit #install-btn, + body.embed-mode-permit #offline-indicator, + body.embed-mode-permit .map-tools-bar { + display: none !important; + } + body.embed-mode-permit .main-content, + body.embed-mode-permit .map-container { + flex: 1 1 auto; + height: 100%; + } + body.embed-mode-permit #map { + height: 100svh; + height: 100dvh; + } + @media (max-width: 576px) { .ol-editbar.ol-bar { /* NOTE: display must NOT be !important — ol-ext toggles the bar via an diff --git a/main.js b/main.js index 351ee96..cf573fa 100644 --- a/main.js +++ b/main.js @@ -88,20 +88,93 @@ import { checkServerReachable, isServerReachable, getDistrictBoundary, getLayers import { geoTracker } from './src/geotracker-lupmis.js'; import { formatCoord, formatAccuracy, formatDistance, accuracyQuality } from './src/geotracker/geo-utils.js'; +// Iframe embed bridge (see public/embed.php + LUPMIS2_Permit_Map_Integration.docx) +import { createEmbedBridge } from './src/embed-bridge.js'; + // Map instance (global for access across functions) let mapView = null; let mapTools = null; +// Module-level reference so the embed bridge can access the parcels layer +// once loadParcels() has created it. +let parcelsLayer = null; +let embedBridge = null; + +// Iframe embed mode. Set by public/embed.php when serving the /embed route; +// undefined for the normal /index.php entry point. +const EMBED_CONFIG = (typeof window !== 'undefined' && window.LUPMIS_EMBED) || null; +const IS_EMBED_PERMIT = !!(EMBED_CONFIG && EMBED_CONFIG.mode === 'permit'); // Current interaction mode: 'addLocation' | 'measureCircle' | 'measureLine' | 'measureArea' -let currentMode = 'addLocation'; +// In embed permit mode we don't want the default Add-Location click to fire, +// so start the mode in a neutral state. +let currentMode = IS_EMBED_PERMIT ? 'embed-permit' : 'addLocation'; // ============================================================================ // Application Initialization // ============================================================================ +/** + * Pre-flight: when an SSO session is present but the user has no district + * assigned, the app cannot function (every API call is scoped to a district). + * Show a blocking message and halt initialisation so we never silently fall + * back to a default district. + * + * Local dev (no window.LUPMIS_SESSION at all) is *not* affected — that path + * still uses the remotedb FALLBACK_DISTRICT_ID for testing. + * + * @returns {boolean} true if the user is blocked (init should abort) + */ +function showNoDistrictBlockerIfNeeded() { + const session = (typeof window !== 'undefined') ? window.LUPMIS_SESSION : null; + if (!session || typeof session !== 'object') return false; // dev mode + const id = session.district_id; + if (id !== null && id !== undefined && String(id).length > 0) return false; + + // Authenticated but no district — render an overlay and abort init. + console.warn('[App] Authenticated user has no district assigned; halting init.'); + const overlay = document.createElement('div'); + overlay.id = 'no-district-overlay'; + overlay.setAttribute('role', 'alertdialog'); + overlay.setAttribute('aria-modal', 'true'); + overlay.style.cssText = + 'position:fixed;inset:0;z-index:99999;display:flex;align-items:center;' + + 'justify-content:center;background:rgba(255,255,255,0.98);padding:24px;'; + const name = session.full_name || session.username || 'You'; + overlay.innerHTML = ` +
                          +
                          🛑
                          +

                          + No district assigned +

                          +

                          + ${escapeHtml(name)}, your user profile is not associated with any + district. LUPMIS2 cannot load the relevant map data without one. +

                          +

                          + Please contact the system administrator to have a district assigned + to your account. +

                          + +
                          `; + document.body.appendChild(overlay); + overlay.querySelector('#no-district-portal-btn')?.addEventListener('click', () => { + window.location.href = 'https://lupmis4luspa.org/'; + }); + return true; +} + async function initApp() { console.log('[App] Initializing...'); + // Pre-flight: authenticated user must have a district assigned. + if (showNoDistrictBlockerIfNeeded()) return; + // 1. Initialize PWA features (Service Worker, install prompt, offline detection) await initPWA({ installButton: '#install-btn', @@ -143,8 +216,20 @@ async function initApp() { // 'water': '💧', 'school': '🏫', 'health': '🏥', // 'market': '🏪', 'default': '📍', 'other': '📌' + // In iframe embed permit mode, install the postMessage bridge BEFORE the + // regular handlers so its outbound parcel:select / parcel:cleared events + // are wired up; the regular click/dblclick handlers below short-circuit in + // that mode (the bridge owns map interaction in the embed). + if (IS_EMBED_PERMIT) { + embedBridge = createEmbedBridge({ mapView, embedConfig: EMBED_CONFIG }); + } + // Set up map click handler immediately after map creation mapView.onClick((lon, lat, feature, evt) => { + // Embed permit mode: the bridge handles parcel selection itself; the + // normal popup/add-location behaviour does not apply. + if (IS_EMBED_PERMIT) return; + console.log('[MapClick] Clicked at:', lon.toFixed(4), lat.toFixed(4)); console.log('[MapClick] currentMode =', currentMode); @@ -192,6 +277,8 @@ async function initApp() { // Set up double-click handler for overlay feature info // Uses '_layerType' property to distinguish zone features from other layers mapView.onDblClick((lon, lat, feature, evt) => { + // Embed permit mode shows no info popups (the host owns the UI). + if (IS_EMBED_PERMIT) return; if (!feature) return; const layerType = feature.get('_layerType'); @@ -329,6 +416,16 @@ async function initApp() { loadDistrictBoundary(); loadCollectorZones(); loadParcels(); + // In embed permit mode the parcels layer is the user's working surface, + // so make it visible immediately and hand the layer to the bridge so it + // can emit `ready` (and resolve any pending `set:selected` UPN) once the + // features arrive. loadParcels() runs its synchronous prologue (creating + // the layer and assigning the module-level reference) before returning + // its promise, so `parcelsLayer` is already set here. + if (IS_EMBED_PERMIT && embedBridge && parcelsLayer) { + parcelsLayer.setVisible(true); + embedBridge.attachParcelsLayer(parcelsLayer); + } loadBuildingFootprints(); loadContoursHillshade(); loadOSMRoads(); @@ -1359,18 +1456,22 @@ function parcelsToGeoJSON(parcels) { seen.add(id); } - // Prefer the GeoJSON geometry (sp_boundary) if available; fall back to WKT + // Geometry sources, in order: + // - API path: `geom` is a GeoJSON object, `boundary` is the WKT string + // - local cache: `geometry_wkt` is the WKT string let geometry = null; - if (parcel.sp_boundary && parcel.sp_boundary.type && parcel.sp_boundary.coordinates) { + if (parcel.geom && parcel.geom.type && parcel.geom.coordinates) { + geometry = { type: parcel.geom.type, coordinates: parcel.geom.coordinates }; + } else if (parcel.sp_boundary && parcel.sp_boundary.type && parcel.sp_boundary.coordinates) { geometry = { type: parcel.sp_boundary.type, coordinates: parcel.sp_boundary.coordinates }; } else { - const wkt = parcel.boundary || parcel.polygon || parcel.geom || parcel.wkt; + const wkt = parcel.boundary || parcel.geometry_wkt || parcel.polygon || parcel.wkt; geometry = parseWKT(wkt); } if (!geometry) continue; - // Collect all properties except bulky geometry fields - const skipKeys = new Set(['polygon', 'boundary', 'geom', 'wkt', 'textboundary', 'sp_boundary']); + // Collect all properties except bulky geometry fields and local housekeeping. + const skipKeys = new Set(['polygon', 'boundary', 'geom', 'geometry_wkt', 'wkt', 'textboundary', 'sp_boundary', 'fetched_at']); const properties = { _layerType: 'parcel' }; for (const [key, value] of Object.entries(parcel)) { if (skipKeys.has(key)) continue; @@ -1407,7 +1508,9 @@ async function loadParcels() { // Create the Parcels layer immediately (empty) so it always appears // in the LayerSwitcher. Features will be added once data is available. const emptyGeoJSON = { type: 'FeatureCollection', features: [] }; - const parcelsLayer = mapView?.addGeoJSONLayer(emptyGeoJSON, 'Parcels', parcelStyle, landUseGroup); + // Assigned to the module-level `parcelsLayer` so the iframe embed bridge + // can pick it up after loadParcels() returns from its sync prologue. + parcelsLayer = mapView?.addGeoJSONLayer(emptyGeoJSON, 'Parcels', parcelStyle, landUseGroup); if (!parcelsLayer) { console.warn('[App] Could not create Parcels layer'); return; diff --git a/public/.htaccess b/public/.htaccess index 441dfd9..d2e1dad 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -13,11 +13,17 @@ DirectoryIndex index.php index.html SetHandler application/x-httpd-php -# Common single-page-app behaviour: if a route doesn't map to a real file or -# directory, send the request to index.php so the PWA can handle it client-side. -# Comment out the next block if hash-based routing is preferred (no rewrites). RewriteEngine On + + # Clean URL for the iframe embed endpoint: /embed → embed.php + # Must come BEFORE the SPA fallback so /embed doesn't get routed to + # index.php. Query strings (?mode=permit&...) pass through automatically. + RewriteRule ^embed/?$ embed.php [L] + + # Common single-page-app behaviour: if a route doesn't map to a real file + # or directory, send the request to index.php so the PWA can handle it + # client-side. Comment out this block if hash-based routing is preferred. RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^ index.php [L] diff --git a/public/embed.php b/public/embed.php new file mode 100644 index 0000000..5a2d65b --- /dev/null +++ b/public/embed.php @@ -0,0 +1,157 @@ + $validate_url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "GET", + CURLOPT_HTTPHEADER => [ "Content-Type: application/xml" ], + ]); + $response = curl_exec($curl); + $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + + if ($httpCode === 200) { + $data = json_decode($response, true); + if ( + is_array($data) + && isset($data['valid']) && $data['valid'] === true + && isset($data['logged_in_user']) && is_array($data['logged_in_user']) + ) { + foreach ($data['logged_in_user'] as $key => $value) { + $_SESSION[$key] = $value; + } + } + } else { + setcookie('sso_auth_token', '', time() - 3600, '/', '.lupmis4luspa.org'); + } +} + +// ──────────────────────────────────────────────────────────────────────────── +// Production access guard — same rule as index.php +// ──────────────────────────────────────────────────────────────────────────── +$host = $_SERVER['HTTP_HOST'] ?? ''; +$isProduction = (bool) preg_match('/(^|\.)lupmis4luspa\.org$/i', $host); +if ($isProduction && !isset($_SESSION['user_id'])) { + header('Location: https://lupmis4luspa.org/', true, 302); + exit; +} + +// ──────────────────────────────────────────────────────────────────────────── +// Session payload (same shape as index.php) +// ──────────────────────────────────────────────────────────────────────────── +$payload = []; +if (isset($_SESSION['user_id'])) { + $payload = [ + 'user_id' => $_SESSION['user_id'] ?? null, + 'ua_id' => $_SESSION['ua_id'] ?? null, + 'username' => $_SESSION['username'] ?? null, + 'title' => $_SESSION['title'] ?? null, + 'full_name' => $_SESSION['full_name'] ?? null, + 'email' => $_SESSION['email'] ?? null, + 'user_type' => $_SESSION['user_type'] ?? null, + 'phone' => $_SESSION['phone'] ?? null, + 'ua_position' => $_SESSION['ua_position'] ?? null, + 'region_id' => $_SESSION['region_id'] ?? null, + 'district_id' => $_SESSION['district_id'] ?? null, + ]; +} + +// ──────────────────────────────────────────────────────────────────────────── +// Embed config — parse the contract's URL parameters (see Permit Map +// Integration doc §2.1). Strict whitelisting + type coercion keeps invalid +// input from reaching the PWA. +// ──────────────────────────────────────────────────────────────────────────── +$validBasemaps = ['topo','osm','satellite','googlesat','carto-light','carto-dark','none']; +$validModes = ['permit']; + +$mode = isset($_GET['mode']) ? (string)$_GET['mode'] : 'permit'; +$basemap = isset($_GET['basemap']) ? (string)$_GET['basemap'] : null; +$upn = isset($_GET['upn']) ? (string)$_GET['upn'] : null; +$appCode = isset($_GET['application_code']) ? (string)$_GET['application_code'] : null; + +$lon = isset($_GET['lon']) && is_numeric($_GET['lon']) ? (float)$_GET['lon'] : null; +$lat = isset($_GET['lat']) && is_numeric($_GET['lat']) ? (float)$_GET['lat'] : null; +$zoom = isset($_GET['zoom']) && is_numeric($_GET['zoom']) ? (int)$_GET['zoom'] : null; + +$embed = [ + 'mode' => in_array($mode, $validModes, true) ? $mode : 'permit', + 'lon' => $lon, + 'lat' => $lat, + 'zoom' => $zoom, + 'upn' => $upn, + 'basemap' => ($basemap && in_array($basemap, $validBasemaps, true)) ? $basemap : null, + 'application_code' => $appCode, +]; + +// ──────────────────────────────────────────────────────────────────────────── +// Read the built index.html and inject the session + embed config +// ──────────────────────────────────────────────────────────────────────────── +$indexPath = __DIR__ . '/index.html'; +$html = is_readable($indexPath) + ? file_get_contents($indexPath) + : '

                          LUPMIS2 PWA

                          index.html is missing.

                          '; + +$jsonFlags = JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT; +$sessionJson = json_encode($payload, $jsonFlags); +$embedJson = json_encode($embed, $jsonFlags); +$inject = + ""; + +$html = preg_replace('/]*>/i', '$0' . "\n " . $inject, $html, 1); + +// ──────────────────────────────────────────────────────────────────────────── +// Headers — frame-ancestors restricts who may embed; tighten this list once +// the real permitting host is confirmed. NEVER use `frame-ancestors *`. +// ──────────────────────────────────────────────────────────────────────────── +$EMBED_ALLOWED_PARENTS = [ + 'https://permits.lupmis4luspa.org', + // Add local dev parents here if needed, e.g. 'http://localhost:8000'. +]; +$frameAncestors = "'self' " . implode(' ', $EMBED_ALLOWED_PARENTS); + +header("Content-Security-Policy: frame-ancestors {$frameAncestors}"); +header('Content-Type: text/html; charset=utf-8'); +header('Cache-Control: no-store, must-revalidate'); +header('Pragma: no-cache'); +header('X-Content-Type-Options: nosniff'); +header('Referrer-Policy: strict-origin-when-cross-origin'); + +echo $html; diff --git a/public/index.php b/public/index.php index d6ff0e0..dd6ee37 100644 --- a/public/index.php +++ b/public/index.php @@ -65,6 +65,21 @@ if (!isset($_SESSION['user_id']) && isset($_COOKIE['sso_auth_token'])) { } } +// ──────────────────────────────────────────────────────────────────────────── +// Production access guard +// ──────────────────────────────────────────────────────────────────────────── +// On the public production host, calling this file without a valid SSO session +// is not allowed — bounce the visitor to the central LUSPA login portal. On +// local development (any host that is not *.lupmis4luspa.org, e.g. localhost, +// 127.0.0.1, a developer's .local hostname) the guard is bypassed so the PHP +// entry point can still be exercised directly during testing. +$host = $_SERVER['HTTP_HOST'] ?? ''; +$isProduction = (bool) preg_match('/(^|\.)lupmis4luspa\.org$/i', $host); +if ($isProduction && !isset($_SESSION['user_id'])) { + header('Location: https://lupmis4luspa.org/', true, 302); + exit; +} + // ──────────────────────────────────────────────────────────────────────────── // Build the payload exposed to the PWA as window.LUPMIS_SESSION // ──────────────────────────────────────────────────────────────────────────── diff --git a/public/sw.js b/public/sw.js index 8536061..4d327ad 100644 --- a/public/sw.js +++ b/public/sw.js @@ -29,7 +29,11 @@ // mobile drawing-toolbar wrap, base-map "None" option, and the Safari // 100svh dock fix. New hashed bundle + updated shell — bump to evict the // old module/shell caches. -const CACHE_VERSION = 'v8'; +// v9: Iframe embed endpoint (/embed via public/embed.php) + postMessage bridge +// for the permitting integration; lu_parcels structural refactor in the +// local DB; production access guard + no-district overlay; LayerSwitcher +// ordering fix. New shell + hashed bundle. +const CACHE_VERSION = 'v9'; const SHELL_CACHE = `shell-${CACHE_VERSION}`; const MODULES_CACHE = `modules-${CACHE_VERSION}`; const API_CACHE = `api-${CACHE_VERSION}`; diff --git a/src/components/MapTools.js b/src/components/MapTools.js index ce335c4..c926d60 100644 --- a/src/components/MapTools.js +++ b/src/components/MapTools.js @@ -50,10 +50,13 @@ export class MapTools { zIndex: 99, }); - // Insert both layers just before the last layer (Overlays group) - // so the LayerSwitcher order becomes: Overlays > Measurements > Markers > Base Maps + // Insert both layers just before the Overlays group so the LayerSwitcher + // order becomes: Overlays > Measurements > Markers > Base Maps. Locate the + // Overlays group by title rather than assuming it is the last layer — + // other layers (e.g. the GPS trail/position layers) may sit on top of it. const layers = this.map.getLayers(); - const overlayIdx = layers.getLength() - 1; // Overlays is the last layer + let overlayIdx = layers.getArray().findIndex((l) => l.get('title') === 'Overlays'); + if (overlayIdx < 0) overlayIdx = layers.getLength(); layers.insertAt(overlayIdx, this.drawLayer); layers.insertAt(overlayIdx, this.measureLayer); diff --git a/src/components/MapView.js b/src/components/MapView.js index 972549e..7122a14 100644 --- a/src/components/MapView.js +++ b/src/components/MapView.js @@ -370,11 +370,14 @@ export class MapView { title: 'Drawings', layers: [this.drawingsLayer], }); - // Insert as a top-level map layer just before the Overlays group - // so the LayerSwitcher order is: Overlays > Drawings > Measurements > Markers > Base Maps + // Insert as a top-level map layer just before the Overlays group so the + // LayerSwitcher order is: Overlays > Drawings > Measurements > Markers > Base Maps. + // Find Overlays by reference rather than assuming it is the last layer — + // other layers (e.g. the GPS trail/position layers added by + // _initGpsRendering) may sit on top of it. const mapLayers = this.map.getLayers(); - const overlayIdx = mapLayers.getLength() - 1; // Overlays is the last layer - mapLayers.insertAt(overlayIdx, this._drawingsGroup); + const overlayIdx = mapLayers.getArray().indexOf(this.overlayGroup); + mapLayers.insertAt(overlayIdx >= 0 ? overlayIdx : mapLayers.getLength(), this._drawingsGroup); // 2. Create a Select interaction that works on ALL vector layers. // It starts INACTIVE so it doesn't steal clicks from normal handlers. diff --git a/src/database.js b/src/database.js index 4b276b5..5ba7470 100644 --- a/src/database.js +++ b/src/database.js @@ -164,27 +164,58 @@ export async function initSchema() { ) `; - // Create parcels table for caching parcel data - // status: 'verified' = from API, 'new' = drawn locally, needs verification + // Create parcels table — mirrors the server's spatial.lu_parcels structure + // (land-use parcels). Attribute columns match the server 1:1 so the local + // data viewer shows real fields rather than a JSON blob. + // status — local-only flag: 'verified' = downloaded from the API, + // 'new' = drawn locally and pending server verification. + // fetched_at — local-only cache timestamp. + // Migrate older databases that used the previous JSON-blob schema + // (id, geometry_wkt, properties, status, fetched_at): if the new `upn` + // column is missing, drop the cached table so it is recreated with the + // lu_parcels columns. Cached server parcels re-download on next load. console.log('[Database] Creating parcels table...'); + try { + const cols = await sql`PRAGMA table_info(parcels)`; + if (cols.length > 0 && !cols.some((c) => c.name === 'upn')) { + console.log('[Database] Migrating parcels table to lu_parcels structure (dropping old cache)...'); + await sql`DROP TABLE parcels`; + } + } catch (e) { + console.warn('[Database] parcels migration check failed:', e); + } await sql` CREATE TABLE IF NOT EXISTS parcels ( id INTEGER PRIMARY KEY, + upn TEXT, + style INTEGER, + landuse TEXT, + zone_code TEXT, + zone_name TEXT, + sector TEXT, + block TEXT, + parcel_no TEXT, + prop_no TEXT, + st_name TEXT, + prop_add TEXT, + fac_name TEXT, + min_height INTEGER, + max_height INTEGER, + eff_date TEXT, + lp_name TEXT, + locality TEXT, + mmda TEXT, + last_update TEXT, + remarks TEXT, geometry_wkt TEXT, - properties TEXT, + created_at TEXT, + updated_at TEXT, + districtid INTEGER, status TEXT DEFAULT 'verified', fetched_at TEXT DEFAULT CURRENT_TIMESTAMP ) `; - // Migrate: add status column if it doesn't exist (for existing databases) - try { - await sql`SELECT status FROM parcels LIMIT 1`; - } catch { - console.log('[Database] Adding status column to parcels table...'); - await sql`ALTER TABLE parcels ADD COLUMN status TEXT DEFAULT 'verified'`; - } - // Create building_footprints table for caching footprint data console.log('[Database] Creating building_footprints table...'); await sql` @@ -586,30 +617,59 @@ export async function getLocalCollectorZones() { // Parcels // ============================================================================ +/** Coerce a value for an INTEGER column; '' / null / undefined / NaN → null. */ +function numOrNull(v) { + if (v === '' || v === null || v === undefined) return null; + const n = Number(v); + return Number.isNaN(n) ? null : n; +} + /** - * Save parcels to the local table. - * Replaces all existing rows. + * Save parcels to the local table (mirrors spatial.lu_parcels). + * Replaces all server-cached rows. Each API field maps to its own column. + * Geometry is taken from the server `geom` field (WKT, EPSG:4326). * * @param {Array} parcels - Array of parcel objects from the API */ export async function saveParcels(parcels) { try { + // Wrap the bulk load in a single transaction — with ~25k parcels, per-row + // auto-commits on OPFS would be prohibitively slow. + await sql`BEGIN`; await sql`DELETE FROM parcels`; let saved = 0; for (const p of parcels) { - const id = p.id || p.parcelid || p.parcel_id || null; + const id = p.id ?? p.parcelid ?? p.parcel_id ?? null; if (id == null) continue; // skip rows without a usable ID - const props = JSON.stringify(p); - // API field names: 'boundary' (WKT), 'polygon', 'geom', 'wkt' - const wkt = p.boundary || p.polygon || p.geom || p.wkt || ''; + // Geometry must be a WKT *string* for the geometry_wkt TEXT column. + // The API sends WKT in `boundary` and a GeoJSON *object* in `geom`, so + // prefer the string fields and only accept `geom` when it is a string. + const wkt = p.boundary || p.geometry_wkt || p.polygon || p.wkt + || (typeof p.geom === 'string' ? p.geom : ''); await sql` - INSERT OR REPLACE INTO parcels (id, geometry_wkt, properties, fetched_at) - VALUES (${id}, ${wkt}, ${props}, CURRENT_TIMESTAMP) + INSERT OR REPLACE INTO parcels ( + id, upn, style, landuse, zone_code, zone_name, sector, block, parcel_no, + prop_no, st_name, prop_add, fac_name, min_height, max_height, eff_date, + lp_name, locality, mmda, last_update, remarks, geometry_wkt, + created_at, updated_at, districtid, status, fetched_at + ) VALUES ( + ${id}, ${p.upn ?? null}, ${numOrNull(p.style)}, ${p.landuse ?? null}, + ${p.zone_code ?? null}, ${p.zone_name ?? null}, ${p.sector ?? null}, + ${p.block ?? null}, ${p.parcel_no ?? null}, ${p.prop_no ?? null}, + ${p.st_name ?? null}, ${p.prop_add ?? null}, ${p.fac_name ?? null}, + ${numOrNull(p.min_height)}, ${numOrNull(p.max_height)}, ${p.eff_date ?? null}, + ${p.lp_name ?? null}, ${p.locality ?? null}, ${p.mmda ?? null}, + ${p.last_update ?? null}, ${p.remarks ?? null}, ${wkt}, + ${p.created_at ?? null}, ${p.updated_at ?? null}, ${numOrNull(p.districtid)}, + 'verified', CURRENT_TIMESTAMP + ) `; saved++; } - console.log('[Database] ✓ Saved', saved, 'parcels (from', parcels.length, 'rows,', parcels.length - saved, 'duplicates replaced)'); + await sql`COMMIT`; + console.log('[Database] ✓ Saved', saved, 'parcels (from', parcels.length, 'rows,', parcels.length - saved, 'skipped/replaced)'); } catch (error) { + try { await sql`ROLLBACK`; } catch { /* no active txn */ } console.error('[Database] ✗ Failed to save parcels:', error); throw error; } @@ -617,13 +677,14 @@ export async function saveParcels(parcels) { /** * Load all cached parcels from the local table. - * @returns {Promise} Array of parcel objects, or null if empty + * Each row is a plain object keyed by column name (incl. geometry_wkt). + * @returns {Promise} Array of parcel rows, or null if empty */ export async function getLocalParcels() { try { - const rows = await sql`SELECT properties FROM parcels ORDER BY id`; + const rows = await sql`SELECT * FROM parcels ORDER BY id`; if (rows.length === 0) return null; - return rows.map(r => JSON.parse(r.properties)); + return rows; } catch (error) { console.error('[Database] ✗ Failed to read local parcels:', error); return null; @@ -631,16 +692,40 @@ export async function getLocalParcels() { } /** - * Update a single parcel's properties in the local table. - * Only the properties JSON blob is updated; geometry stays unchanged. + * Update a single parcel's attribute columns in the local table. + * Geometry, id, created_at and status are left unchanged; updated_at is bumped. * * @param {number|string} parcelId - The parcel id - * @param {Object} updatedProps - Full updated properties object + * @param {Object} p - Updated attribute values (keys = lu_parcels columns) */ -export async function updateParcel(parcelId, updatedProps) { +export async function updateParcel(parcelId, p) { try { - const props = JSON.stringify(updatedProps); - await sql`UPDATE parcels SET properties = ${props} WHERE id = ${parcelId}`; + await sql` + UPDATE parcels SET + upn = ${p.upn ?? null}, + style = ${numOrNull(p.style)}, + landuse = ${p.landuse ?? null}, + zone_code = ${p.zone_code ?? null}, + zone_name = ${p.zone_name ?? null}, + sector = ${p.sector ?? null}, + block = ${p.block ?? null}, + parcel_no = ${p.parcel_no ?? null}, + prop_no = ${p.prop_no ?? null}, + st_name = ${p.st_name ?? null}, + prop_add = ${p.prop_add ?? null}, + fac_name = ${p.fac_name ?? null}, + min_height = ${numOrNull(p.min_height)}, + max_height = ${numOrNull(p.max_height)}, + eff_date = ${p.eff_date ?? null}, + lp_name = ${p.lp_name ?? null}, + locality = ${p.locality ?? null}, + mmda = ${p.mmda ?? null}, + last_update = ${p.last_update ?? null}, + remarks = ${p.remarks ?? null}, + districtid = ${numOrNull(p.districtid)}, + updated_at = CURRENT_TIMESTAMP + WHERE id = ${parcelId} + `; console.log('[Database] ✓ Parcel updated:', parcelId); broadcastChange('parcels', 'UPDATE', parcelId); } catch (error) { @@ -654,15 +739,28 @@ export async function updateParcel(parcelId, updatedProps) { * The parcel is tagged with status='new' to indicate it needs verification. * * @param {string} geometryWkt - WKT geometry string (EPSG:4326) - * @param {Object} properties - Attribute properties from the form + * @param {Object} p - Attribute values from the form (keys = lu_parcels columns) * @returns {Promise<{id: number}>} The new row id */ -export async function insertNewParcel(geometryWkt, properties) { +export async function insertNewParcel(geometryWkt, p = {}) { try { - const props = JSON.stringify(properties); await sql` - INSERT INTO parcels (id, geometry_wkt, properties, status, fetched_at) - VALUES (NULL, ${geometryWkt}, ${props}, 'new', CURRENT_TIMESTAMP) + INSERT INTO parcels ( + id, upn, style, landuse, zone_code, zone_name, sector, block, parcel_no, + prop_no, st_name, prop_add, fac_name, min_height, max_height, eff_date, + lp_name, locality, mmda, last_update, remarks, geometry_wkt, + created_at, updated_at, districtid, status, fetched_at + ) VALUES ( + NULL, ${p.upn ?? null}, ${numOrNull(p.style)}, ${p.landuse ?? null}, + ${p.zone_code ?? null}, ${p.zone_name ?? null}, ${p.sector ?? null}, + ${p.block ?? null}, ${p.parcel_no ?? null}, ${p.prop_no ?? null}, + ${p.st_name ?? null}, ${p.prop_add ?? null}, ${p.fac_name ?? null}, + ${numOrNull(p.min_height)}, ${numOrNull(p.max_height)}, ${p.eff_date ?? null}, + ${p.lp_name ?? null}, ${p.locality ?? null}, ${p.mmda ?? null}, + ${p.last_update ?? null}, ${p.remarks ?? null}, ${geometryWkt}, + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ${numOrNull(p.districtid)}, + 'new', CURRENT_TIMESTAMP + ) `; const idResult = await sql`SELECT last_insert_rowid() as id`; const newId = idResult[0]?.id; diff --git a/src/embed-bridge.js b/src/embed-bridge.js new file mode 100644 index 0000000..53e79e3 --- /dev/null +++ b/src/embed-bridge.js @@ -0,0 +1,262 @@ +/** + * LUPMIS2 — iframe embed bridge + * + * Implements the postMessage contract defined in + * LUPMIS2_Permit_Map_Integration.docx §2 + * which itself follows the iframe channel from + * LUPMIS2_Reusable_Mapping_Concept.docx §3.2 / §4. + * + * Outbound (embed → host): + * { type: 'ready' } + * { type: 'parcel:select', upn, parcel_id, lon, lat, + * zone_code, zone_name, landuse, + * min_height, max_height } + * { type: 'parcel:cleared' } + * { type: 'error', code, message } + * + * Inbound (host → embed): + * { type: 'set:view', lon, lat, zoom } + * { type: 'set:selected', upn } + * { type: 'clear:selected' } + * { type: 'set:basemap', key } + * + * The bridge is framework-agnostic apart from the OpenLayers imports — it + * lives one level above the proposed `map-core` library so once that library + * is extracted (concept §3.1) this file can be lifted into it unchanged. + * + * Note on security: outbound messages are sent with target origin `*` because + * the embed cannot know its parent's origin in advance and the payload is + * non-sensitive (selection metadata only). The HOST is expected to verify + * `event.origin === ''` before trusting any message, as + * documented in §2.2 of the integration doc. Inbound commands are + * type-checked against a strict whitelist before being acted on. + */ + +import { fromLonLat, toLonLat } from 'ol/proj.js'; +import { getCenter } from 'ol/extent.js'; +import VectorLayer from 'ol/layer/Vector.js'; +import VectorSource from 'ol/source/Vector.js'; +import { Style, Stroke, Fill } from 'ol/style.js'; + +const KNOWN_COMMANDS = new Set([ + 'set:view', + 'set:selected', + 'clear:selected', + 'set:basemap', +]); + +/** + * Create and install the embed bridge. + * + * @param {Object} opts + * @param {Object} opts.mapView — the MapView instance (uses onClick, + * getMap, setBaseMap). + * @param {Object} opts.embedConfig — window.LUPMIS_EMBED contents. + * @returns {{ attachParcelsLayer: (layer) => void, emitError: (code, msg) => void }} + */ +export function createEmbedBridge({ mapView, embedConfig }) { + const map = mapView.getMap(); + const parent = (window.parent && window.parent !== window) ? window.parent : null; + + // ------------------------------------------------------------------------- + // Highlight layer — visual cue for the currently-selected parcel. + // ------------------------------------------------------------------------- + const highlightSource = new VectorSource(); + const highlightLayer = new VectorLayer({ + source: highlightSource, + zIndex: 9999, + style: new Style({ + stroke: new Stroke({ color: '#f97316', width: 3 }), + fill: new Fill({ color: 'rgba(249,115,22,0.18)' }), + }), + properties: { + title: 'Permit selection', + displayInLayerSwitcher: false, + }, + }); + map.addLayer(highlightLayer); + + let parcelsLayer = null; + let pendingSelectUpn = embedConfig?.upn ? String(embedConfig.upn) : null; + let readyEmitted = false; + + // ------------------------------------------------------------------------- + // Outbound + // ------------------------------------------------------------------------- + function send(message) { + if (!parent) { + console.warn('[embed-bridge] No parent window — would have sent:', message); + return; + } + try { + parent.postMessage(message, '*'); + } catch (e) { + console.warn('[embed-bridge] postMessage failed:', e); + } + } + + function emitError(code, message) { + send({ type: 'error', code, message }); + } + + function emitReady() { + if (readyEmitted) return; + readyEmitted = true; + send({ type: 'ready' }); + } + + /** Build a `parcel:select` payload from a feature and (optionally) the click point. */ + function parcelPayload(feature, lon, lat) { + const p = feature.getProperties(); + let outLon = lon, outLat = lat; + if (outLon == null || outLat == null) { + const ext = feature.getGeometry()?.getExtent(); + if (ext) { + const [cx, cy] = toLonLat(getCenter(ext)); + outLon = cx; outLat = cy; + } + } + return { + type: 'parcel:select', + upn: p.upn ?? null, + parcel_id: p.id ?? null, + lon: outLon ?? null, + lat: outLat ?? null, + zone_code: p.zone_code ?? null, + zone_name: p.zone_name ?? null, + landuse: p.landuse ?? null, + min_height: p.min_height ?? null, + max_height: p.max_height ?? null, + }; + } + + function highlightFeature(feature) { + highlightSource.clear(); + if (feature) { + const clone = feature.clone(); + highlightSource.addFeature(clone); + } + } + + // ------------------------------------------------------------------------- + // Click → parcel:select / parcel:cleared + // ------------------------------------------------------------------------- + mapView.onClick((lon, lat, _markerFeature, evt) => { + let parcelFeature = null; + map.forEachFeatureAtPixel(evt.pixel, (f) => { + if (f.get('_layerType') === 'parcel') { + parcelFeature = f; + return true; + } + }); + if (parcelFeature) { + highlightFeature(parcelFeature); + send(parcelPayload(parcelFeature, lon, lat)); + } else { + highlightFeature(null); + send({ type: 'parcel:cleared' }); + } + }); + + // ------------------------------------------------------------------------- + // Inbound commands + // ------------------------------------------------------------------------- + window.addEventListener('message', (event) => { + const msg = event.data; + if (!msg || typeof msg !== 'object' || !KNOWN_COMMANDS.has(msg.type)) return; + try { + switch (msg.type) { + case 'set:view': { + if (typeof msg.lon === 'number' && typeof msg.lat === 'number') { + const view = map.getView(); + view.setCenter(fromLonLat([msg.lon, msg.lat])); + if (typeof msg.zoom === 'number') view.setZoom(msg.zoom); + } + break; + } + case 'set:selected': + if (msg.upn) selectByUpn(String(msg.upn)); + break; + case 'clear:selected': + highlightFeature(null); + pendingSelectUpn = null; + break; + case 'set:basemap': + if (msg.key && typeof mapView.setBaseMap === 'function') { + mapView.setBaseMap(msg.key); + } + break; + } + } catch (e) { + emitError('COMMAND_FAILED', `Failed to handle ${msg.type}: ${e.message}`); + } + }); + + /** + * Find the parcel with the given UPN, highlight it, fit the view to it, + * and emit a synthesized parcel:select so the host receives the metadata. + * If the parcels haven't finished loading yet, the UPN is queued and the + * lookup is retried as features stream in. + */ + function selectByUpn(upn) { + if (!parcelsLayer) { pendingSelectUpn = upn; return; } + const features = parcelsLayer.getSource().getFeatures(); + const feature = features.find((f) => String(f.get('upn') ?? '') === upn); + if (!feature) { pendingSelectUpn = upn; return; } + + pendingSelectUpn = null; + highlightFeature(feature); + const ext = feature.getGeometry()?.getExtent(); + if (ext) { + map.getView().fit(ext, { padding: [50, 50, 50, 50], duration: 400, maxZoom: 17 }); + } + send(parcelPayload(feature, null, null)); + } + + // ------------------------------------------------------------------------- + // Parcels attached → emit `ready`, drain any pending set:selected + // ------------------------------------------------------------------------- + function attachParcelsLayer(layer) { + parcelsLayer = layer; + const source = layer.getSource(); + + const drain = () => { + // Microtask hop so a batch addFeatures() finishes before we react. + queueMicrotask(() => { + if (pendingSelectUpn) selectByUpn(pendingSelectUpn); + emitReady(); + }); + }; + + if (source.getFeatures().length > 0) { + drain(); + } else { + // Stay subscribed: parcels may arrive in waves (cache then API refresh), + // and a pending UPN may only resolve after the second wave. + let scheduled = false; + source.on('addfeature', () => { + if (scheduled) return; + scheduled = true; + queueMicrotask(() => { + scheduled = false; + if (pendingSelectUpn) selectByUpn(pendingSelectUpn); + emitReady(); + }); + }); + } + } + + // ------------------------------------------------------------------------- + // Apply initial config: basemap + view (UPN is handled after parcels load) + // ------------------------------------------------------------------------- + if (embedConfig?.basemap && typeof mapView.setBaseMap === 'function') { + mapView.setBaseMap(embedConfig.basemap); + } + if (typeof embedConfig?.lon === 'number' && typeof embedConfig?.lat === 'number') { + const view = map.getView(); + view.setCenter(fromLonLat([embedConfig.lon, embedConfig.lat])); + view.setZoom(typeof embedConfig?.zoom === 'number' ? embedConfig.zoom : 15); + } + + return { attachParcelsLayer, emitError }; +} diff --git a/src/remotedb.js b/src/remotedb.js index a681561..aec059f 100644 --- a/src/remotedb.js +++ b/src/remotedb.js @@ -32,16 +32,27 @@ const FALLBACK_DISTRICT_ID = '1'; const API_TOKEN = '1c46538c712e9b5b'; /** - * Returns the authenticated user's district_id, or the dev fallback. - * The getter runs on each spread of API_CREDENTIALS, so changing the - * session at runtime (rare but possible) takes effect immediately. + * Returns the authenticated user's district_id. + * + * - No SSO session at all (window.LUPMIS_SESSION undefined): we're in local + * development → fall back to the hard-coded test district. + * - Session present but no district_id: the user is authenticated but not + * assigned to any district → return null. The bootstrap in main.js detects + * this case BEFORE any API call and shows a blocking message; this null + * is defence-in-depth so we never silently send district_id=1 for an + * authenticated user. + * + * The getter runs on each spread of API_CREDENTIALS, so a session change at + * runtime takes effect immediately. */ function resolveDistrictId() { try { - const id = (typeof window !== 'undefined') && window.LUPMIS_SESSION?.district_id; - if (id !== null && id !== undefined && String(id).length > 0) { - return String(id); - } + if (typeof window === 'undefined') return FALLBACK_DISTRICT_ID; + const session = window.LUPMIS_SESSION; + if (!session || typeof session !== 'object') return FALLBACK_DISTRICT_ID; + const id = session.district_id; + if (id === null || id === undefined || String(id).length === 0) return null; + return String(id); } catch { /* no-op */ } return FALLBACK_DISTRICT_ID; }