pwaLUPMIS2/dist/assets/index-B4XzHtZX.js
ekke ef12e4477b Offline tile cache, polygon Divide, topographic layer integrations
Major feature batch covering drawing-tool improvements, layer additions,
and offline-first capabilities. Largest changes in MapView.js (+1700),
main.js (+1500), public/sw.js (+367), and new modules under src/.

Drawing & editing toolkit
  * Polygon Divide tool — sub-button under Split, divides a polygon into
    N equal-area pieces via binary search; user picks the cutting edge
  * UPN pick phase after Split and Divide — non-picked pieces have their
    identifier fields cleared automatically
  * Improved Merge algorithm — vertex-to-edge proximity (5 m tol.) with
    hybrid lockstep extension; bold A/B labels on selected polygons
  * Persistent vertex highlights — all vertices of the selected polygon
    rendered as dots while edit mode is on, without subclassing ol-ext
  * Toast notifications for merge/split/divide outcomes
  * Shapefile import — addGeoJSONLayer now includes an image style so
    Point features render (previously invisible)

Background & overlay layers
  * DEAfrica Coastlines v0.4 (WMS) in Biophysical Environment
  * DEAfrica Slope (SRTM 30m, style_slope) — semi-transparent background
  * Contours hillshade — get_contours_hillshade.php → local SQLite cache
  * OSM_roads — get_osm_roads.php → local SQLite cache, casing-stroke
    style (black 3.5 px outer, #F0F1F0 1.5 px inner)
  * External Source dialog — green + button in LayerSwitcher lets users
    add WMS / WFS / XYZ layers at runtime
  * Generic addWMSLayer / addXYZLayer with style, opacity, zIndex,
    legendUrl, onlineOnly options
  * TileWMS replaces ImageWMS (fixes 'Width exceeds 512' WMS errors)
  * Legend panel — bottom-right, auto-shown for visible layers that
    register a legendUrl
  * Default base map setting in Settings, persisted in localStorage;
    setBaseMap() on MapView

Offline tile cache (Phase 1 + 2)
  * Service worker: per-host tile caches (osm / topo / satellite /
    carto-light / carto-dark), counter-based eviction to prevent
    iOS Safari memory-pressure reloads, GET_TILE_STATS /
    CLEAR_TILE_CACHES message API
  * pwa.js helpers: getActiveServiceWorker, onServiceWorkerControllerChange,
    getTileCacheStats, clearTileCaches, getStorageEstimate
  * Settings: Offline Map Tiles card with per-provider stats + clear
  * Phase 2 download dialog: form to pick base map, area (current view /
    district / Ghana), zoom range; live tile-count + size estimate;
    progress bar with cancel; OfflineTileDownloader class with
    concurrency + throttling

Local database management
  * osm_roads table + saveOSMRoads / getLocalOSMRoads helpers
  * CACHED_LAYER_TABLES allow-list with clearTable / clearAllCachedLayers
  * Local Database Tables card: per-row Clear button (cached layers
    only) + 'Refresh cached layers' header button with reload prompt

Build & infrastructure
  * Shpjs lazy-loaded via dynamic import (saves ~140 kB from initial JS)
  * chunkSizeWarningLimit raised to 900 kB (openlayers + sqlite3.wasm
    can't be split further)
  * Toast notification module (src/toast.js)
  * Units module (src/units.js) for metric / imperial conversions
  * PDF export module (src/pdf-export.js)

Documentation & SQL
  * Topographic_Background_Layers_for_LUPMIS2.docx — research report
  * OpenTopography_Workflow.svg/.png — ETL pipeline diagram
  * LUPMIS2_Development_Status_Report.docx — April update section
  * sql/create_landuse_parcels.sql — PostgreSQL schema for the LUSPA
    land-use parcel specification (Feb 2026, revised), with PostGIS
    geometry column and standard indices

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 10:55:30 +02:00

637 lines
195 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

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

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

const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/pdf-export-Vpiz8VA4.js","assets/jspdf-Cu-2SCgw.js","assets/openlayers-CUDtI0S3.js","assets/openlayers-BtPuoxOl.css"])))=>i.map(i=>d[i]);
import{_ as at,d as C,F as M,S as T,c as ce,b as Ct,e as z,V as N,L as de,K as me,P as Ye,X as Je,f as Me,Y as fe,M as Rt,Z as Yo,$ as he,a0 as Jo,o as Xo,O as Zo,a1 as Qo,n as ht,U as ie,a2 as $e,a3 as Xe,a4 as xe,k as en,T as J,a5 as ge,a6 as Nt,a7 as oe,a8 as $t,j as tn,u as gt,a9 as fo,aa as on}from"./openlayers-CUDtI0S3.js";import{M as It}from"./bootstrap-D1-uvFxm.js";import{o as nn,a as rn,b as an,c as sn,d as ln,e as _t,f as jt,g as Re,h as cn,i as le,j as dn,k as un}from"./ol-ext-CSk2UikI.js";(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const s of o)if(s.type==="childList")for(const a of s.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&r(a)}).observe(document,{childList:!0,subtree:!0});function t(o){const s={};return o.integrity&&(s.integrity=o.integrity),o.referrerPolicy&&(s.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?s.credentials="include":o.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function r(o){if(o.ep)return;o.ep=!0;const s=t(o);fetch(o.href,s)}})();const zt="function",Te="64e10b34-2bf7-4616-9668-f99de5aa046e",pn="get",fn="has",hn="set",{isArray:We}=Array;let{SharedArrayBuffer:Ze,window:gn}=globalThis,{notify:ho,wait:go,waitAsync:Qe}=Atomics,mo=null;Qe||(Qe=n=>({value:new Promise(e=>{let t=new Worker("data:application/javascript,onmessage%3D(%7Bdata%3Ab%7D)%3D%3E(Atomics.wait(b%2C0)%2CpostMessage(0))");t.onmessage=e,t.postMessage(n)})}));try{new Ze(4)}catch{Ze=ArrayBuffer;const e=new WeakMap;if(gn){const t=new Map,{prototype:{postMessage:r}}=Worker,o=s=>{const a=s.data?.[Te];if(!We(a)){s.stopImmediatePropagation();const{id:i,sb:l}=a;t.get(i)(l)}};mo=function(s,...a){const i=s?.[Te];if(We(i)){const[l,c]=i;e.set(c,l),this.addEventListener("message",o)}return r.call(this,s,...a)},Qe=s=>({value:new Promise(a=>{t.set(e.get(s),a)}).then(a=>{t.delete(e.get(s)),e.delete(s);for(let i=0;i<a.length;i++)s[i]=a[i];return"ok"})})}else{const t=(r,o)=>({[Te]:{id:r,sb:o}});ho=r=>{postMessage(t(e.get(r),r))},addEventListener("message",r=>{const o=r.data?.[Te];if(We(o)){const[s,a]=o;e.set(a,s)}})}}/*! (c) Andrea Giammarchi - ISC */const{Int32Array:St,Map:Gt,Uint16Array:Lt}=globalThis,{BYTES_PER_ELEMENT:qt}=St,{BYTES_PER_ELEMENT:mn}=Lt,yn=(n,e,t)=>{for(;go(n,0,0,e)==="timed-out";)t()},kt=new WeakSet,mt=new WeakMap,bn={value:{then:n=>n()}};let wn=0;const At=(n,{parse:e=JSON.parse,stringify:t=JSON.stringify,transform:r,interrupt:o}=JSON)=>{if(!mt.has(n)){const s=mo||n.postMessage,a=(f,...p)=>s.call(n,{[Te]:p},{transfer:f}),i=typeof o===zt?o:o?.handler,l=o?.delay||42,c=new TextDecoder("utf-16"),d=(f,p)=>f?Qe(p,0):(i?yn(p,l,i):go(p,0),bn);let u=!1;mt.set(n,new Proxy(new Gt,{[fn]:(f,p)=>typeof p=="string"&&!p.startsWith("_"),[pn]:(f,p)=>p==="then"?null:((...h)=>{const m=wn++;let g=new St(new Ze(qt*2)),y=[];kt.has(h.at(-1)||y)&&kt.delete(y=h.pop()),a(y,m,g,p,r?h.map(r):h);const b=n!==globalThis;let E=0;return u&&b&&(E=setTimeout(console.warn,1e3,`💀🔒 - Possible deadlock if proxy.${p}(...args) is awaited`)),d(b,g).value.then(()=>{clearTimeout(E);const S=g[1];if(!S)return;const L=mn*S;return g=new St(new Ze(L+L%qt)),a([],m,g),d(b,g).value.then(()=>e(c.decode(new Lt(g.buffer).slice(0,S))))})}),[hn](f,p,h){const m=typeof h;if(m!==zt)throw new Error(`Unable to assign ${p} as ${m}`);if(!f.size){const g=new Gt;n.addEventListener("message",async y=>{const b=y.data?.[Te];if(We(b)){y.stopImmediatePropagation();const[E,S,...L]=b;let w;if(L.length){const[_,P]=L;if(f.has(_)){u=!0;try{const k=await f.get(_)(...P);if(k!==void 0){const $=t(r?r(k):k);g.set(E,$),S[1]=$.length}}catch(k){w=k}finally{u=!1}}else w=new Error(`Unsupported action: ${_}`);S[0]=1}else{const _=g.get(E);g.delete(E);for(let P=new Lt(S.buffer),k=0;k<_.length;k++)P[k]=_.charCodeAt(k)}if(ho(S,0),w)throw w}})}return!!f.set(p,h)}}))}return mt.get(n)};At.transfer=(...n)=>(kt.add(n),n);function Ht(){let n,e;return{lock:async()=>{for(;n;)await n;n=new Promise(o=>{e=o})},unlock:async()=>{const o=e;n=void 0,e=void 0,o?.()}}}async function yo(n,e){let t;if(n instanceof Blob?t=n.stream():t=n,t instanceof ReadableStream&&e){const o=t.getReader();switch(e){case"callback":return async()=>(await o.read()).value;case"buffer":const s=[];let a=!1;for(;!a;){const d=await o.read();d.value&&s.push(d.value),a=d.done}const i=s.reduce((d,u)=>d+u.length,0),l=new Uint8Array(i);let c=0;return s.forEach(d=>{l.set(d,c),c+=d.length}),l.buffer}}else return t}class et{constructor(e){Object.defineProperty(this,"sqlite3InitModule",{enumerable:!0,configurable:!0,writable:!0,value:e}),Object.defineProperty(this,"sqlite3",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"db",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"config",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"pointers",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"writeCallbacks",{enumerable:!0,configurable:!0,writable:!0,value:new Set}),Object.defineProperty(this,"storageType",{enumerable:!0,configurable:!0,writable:!0,value:"memory"})}async init(e){const{databasePath:t}=e,r=this.getFlags(e);if(!this.sqlite3InitModule){const{default:o}=await at(async()=>{const{default:s}=await import("./index-DTMgZTfd.js");return{default:s}},[]);this.sqlite3InitModule=o}this.sqlite3||(this.sqlite3=await this.sqlite3InitModule()),this.db&&await this.destroy(),this.db=new this.sqlite3.oo1.DB(t,r),this.config=e,this.initWriteHook()}onWrite(e){return this.writeCallbacks.add(e),()=>{this.writeCallbacks.delete(e)}}async exec(e){if(!this.db)throw new Error("Driver not initialized");return this.execOnDb(this.db,e)}async execBatch(e){if(!this.db)throw new Error("Driver not initialized");const t=[];return this.db.transaction(r=>{const o=new Map;try{for(let s of e){let a=o.get(s.sql);if(!a){const c=r.prepare(s.sql);o.set(s.sql,c),a=c}s.params?.length&&a.bind(s.params);let i=[],l=[];for(;a.step();)i=a.getColumnNames([]),l.push(a.get([]));t.push({columns:i,rows:l}),a.reset()}}finally{o.forEach(s=>{s.finalize()})}}),t}async isDatabasePersisted(){return!1}async getDatabaseSizeBytes(){const t=(await this.exec({sql:`SELECT page_count * page_size AS size
FROM pragma_page_count(), pragma_page_size()`,method:"get"}))?.rows?.[0];if(typeof t!="number")throw new Error("Failed to query database size");return t}async createFunction(e){if(!this.db)throw new Error("Driver not initialized");switch(e.type){case"callback":case"scalar":this.db.createFunction({name:e.name,xFunc:(t,...r)=>e.func(...r),arity:-1});break;case"aggregate":this.db.createFunction({name:e.name,xStep:(t,...r)=>e.func.step(...r),xFinal:(t,...r)=>e.func.final(...r),arity:-1});break}}async import(e){if(!this.sqlite3||!this.db||!this.config)throw new Error("Driver not initialized");const t=await yo(e,"buffer"),r=this.sqlite3.wasm.allocFromTypedArray(t);this.pointers.push(r);const o=this.sqlite3.capi.sqlite3_deserialize(this.db,"main",r,t.byteLength,t.byteLength,this.config.readOnly?this.sqlite3.capi.SQLITE_DESERIALIZE_READONLY:this.sqlite3.capi.SQLITE_DESERIALIZE_RESIZEABLE);this.db.checkRc(o)}async export(){if(!this.sqlite3||!this.db)throw new Error("Driver not initialized");return{name:"database.sqlite3",data:this.sqlite3.capi.sqlite3_js_db_export(this.db)}}async clear(){}async destroy(){this.closeDb(),this.pointers.forEach(e=>this.sqlite3?.wasm.dealloc(e)),this.pointers=[],this.writeCallbacks.clear()}getFlags(e){const{readOnly:t,verbose:r}=e;return[t===!0?"r":"cw",r===!0?"t":""].join("")}execOnDb(e,t){const r={rows:[],columns:[]},o=e.exec({sql:t.sql,bind:t.params,returnValue:"resultRows",rowMode:"array",columnNames:r.columns});switch(t.method){case"run":break;case"get":r.rows=o[0]??[];break;case"all":default:r.rows=o;break}return r}initWriteHook(){if(!this.config?.reactive)return;if(!this.sqlite3||!this.db)throw new Error("Driver not initialized");const e={[this.sqlite3.capi.SQLITE_INSERT]:"insert",[this.sqlite3.capi.SQLITE_UPDATE]:"update",[this.sqlite3.capi.SQLITE_DELETE]:"delete"};this.sqlite3.capi.sqlite3_update_hook(this.db,(t,r,o,s,a)=>{this.writeCallbacks.forEach(i=>{i({table:s,rowid:a,operation:e[r]})})},0)}closeDb(){this.db&&(this.db.close(),this.db=void 0)}}function vn(n,e,t){let r,o,s,a,i,l,c=0,d=!1,u=!1,f=!0;if(typeof n!="function")throw new TypeError("Expected a function");e=Number(e)||0,typeof t=="object"&&t!==null&&(d=!!t.leading,u="maxWait"in t,s=u?Math.max(Number(t.maxWait)||0,e):0,f="trailing"in t?!!t.trailing:f);function p(w){const _=r,P=o;return r=o=void 0,c=w,a=n.apply(P,_),a}function h(w){return c=w,i=setTimeout(y,e),d?p(w):a}function m(w){const _=w-(l??0),P=w-c,k=e-_;return u?Math.min(k,s-P):k}function g(w){const _=w-(l??0),P=w-c;return l===void 0||_>=e||_<0||u&&P>=s}function y(){const w=Date.now();if(g(w))return b(w);i=setTimeout(y,m(w))}function b(w){return i=void 0,f&&r?p(w):(r=o=void 0,a)}function E(){i!==void 0&&clearTimeout(i),c=0,r=l=o=i=void 0}function S(){return i===void 0?a:b(Date.now())}function L(){const w=Date.now(),_=g(w);if(r=arguments,o=this,l=w,_){if(i===void 0)return h(l);if(u)return i=setTimeout(y,e),p(l)}return i===void 0&&(i=setTimeout(y,e)),a}return L.cancel=E,L.flush=S,L}function Ke(){return crypto.randomUUID()}function bo(n,e){switch(n){case"session":case":sessionStorage:":let t=sessionStorage._sqlocal_session_key;return t||(t=Ke(),sessionStorage._sqlocal_session_key=t),`session:${t}`;case"local":case":localStorage:":return"local";case":memory:":return`memory:${e}`;default:return`path:${n}`}}class Ge{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:Ht()}),Object.defineProperty(this,"transactionMutex",{enumerable:!0,configurable:!0,writable:!0,value:Ht()}),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 et,await this.driver.init(this.config)}const s=bo(this.config.databasePath,this.config.clientKey);this.reinitChannel=new BroadcastChannel(`_sqlocal_reinit_(${s})`),this.reinitChannel.onmessage=a=>{const i=a.data;if(this.config.clientKey!==i.clientKey)switch(i.type){case"reinit":this.init(i.reason);break;case"close":this.driver.destroy();break}},this.config.reactive&&(this.effectsChannel=new BroadcastChannel(`_sqlocal_effects_(${s})`),this.driver.onWrite(async a=>{this.dirtyTables.add(a.table),await this.transactionMutex.lock(),this.emitEffectsDebounced(),await this.transactionMutex.unlock()})),await Promise.all(Array.from(this.userFunctions.values()).map(a=>this.initUserFunction(a))),await this.execInitStatements(),this.emitMessage({type:"event",event:"connect",reason:o})}catch(s){this.emitMessage({type:"error",error:s,queryKey:null}),await this.destroy()}finally{await this.initMutex.unlock()}}}}),Object.defineProperty(this,"postMessage",{enumerable:!0,configurable:!0,writable:!0,value:async(o,s)=>{const a=o instanceof MessageEvent?o.data:o;switch(await this.initMutex.lock(),a.type){case"config":this.editConfig(a);break;case"query":case"batch":case"transaction":this.exec(a);break;case"function":this.createUserFunction(a);break;case"getinfo":this.getDatabaseInfo(a);break;case"import":this.importDb(a);break;case"export":this.exportDb(a);break;case"delete":this.deleteDb(a);break;case"destroy":this.destroy(a);break}await this.initMutex.unlock()}}),Object.defineProperty(this,"emitMessage",{enumerable:!0,configurable:!0,writable:!0,value:(o,s=[])=>{this.onmessage&&this.onmessage(o,s)}}),Object.defineProperty(this,"emitEffects",{enumerable:!0,configurable:!0,writable:!0,value:()=>{!this.effectsChannel||this.dirtyTables.size===0||(this.effectsChannel.postMessage({type:"effects",tables:[...this.dirtyTables]}),this.dirtyTables.clear())}}),Object.defineProperty(this,"emitEffectsDebounced",{enumerable:!0,configurable:!0,writable:!0,value:vn(()=>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 s={type:"data",queryKey:o.queryKey,data:[]};switch(o.type){case"query":const a=this.transactionKey!==null&&this.transactionKey===o.transactionKey;try{a||await this.transactionMutex.lock();const i=await this.driver.exec(o);s.data.push(i)}finally{a||await this.transactionMutex.unlock()}break;case"batch":try{await this.transactionMutex.lock();const i=await this.driver.execBatch(o.statements);s.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(s)}catch(s){this.emitMessage({type:"error",error:s,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(s){this.emitMessage({type:"error",queryKey:o.queryKey,error:s})}}}),Object.defineProperty(this,"createUserFunction",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{functionName:s,functionType:a,queryKey:i}=o;let l;if(this.userFunctions.has(s)){this.emitMessage({type:"error",error:new Error(`A user-defined function with the name "${s}" has already been created for this SQLocal instance.`),queryKey:i});return}switch(a){case"callback":l={type:a,name:s,func:(...c)=>{this.emitMessage({type:"callback",name:s,args:c})}};break;case"scalar":l={type:a,name:s,func:this.proxy[`_sqlocal_func_${s}`]};break;case"aggregate":l={type:a,name:s,func:{step:this.proxy[`_sqlocal_func_${s}_step`],final:this.proxy[`_sqlocal_func_${s}_final`]}};break}try{await this.initUserFunction(l),this.emitMessage({type:"success",queryKey:i})}catch(c){this.emitMessage({type:"error",error:c,queryKey:i})}}}),Object.defineProperty(this,"initUserFunction",{enumerable:!0,configurable:!0,writable:!0,value:async 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:s,database:a}=o;let i=!1;try{await this.driver.import(a),this.driver.storageType==="memory"&&await this.execInitStatements()}catch(l){this.emitMessage({type:"error",error:l,queryKey:s}),i=!0}finally{this.driver.storageType!=="memory"&&await this.init("overwrite")}i||this.emitMessage({type:"success",queryKey:s})}}),Object.defineProperty(this,"exportDb",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{queryKey:s}=o;try{const{name:a,data:i}=await this.driver.export();this.emitMessage({type:"buffer",queryKey:s,bufferName:a,buffer:i},[i])}catch(a){this.emitMessage({type:"error",error:a,queryKey:s})}}}),Object.defineProperty(this,"deleteDb",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{queryKey:s}=o;let a=!1;try{await this.driver.clear()}catch(i){this.emitMessage({type:"error",error:i,queryKey:s}),a=!0}finally{await this.init("delete")}a||this.emitMessage({type:"success",queryKey:s})}}),Object.defineProperty(this,"destroy",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{await this.driver.exec({sql:"PRAGMA optimize"}),await this.driver.destroy(),this.effectsChannel&&(this.emitEffectsDebounced.flush(),this.effectsChannel.close(),this.effectsChannel=void 0),this.reinitChannel&&(this.reinitChannel.close(),this.reinitChannel=void 0),o&&this.emitMessage({type:"success",queryKey:o.queryKey})}});const r=typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope?At(globalThis):globalThis;this.proxy=r,this.driver=e}}function tt(n,...e){return{sql:n.join("?"),params:e}}function En(n){return!n.some(e=>!Array.isArray(e))}function yt(n,e){let t;return En(n)?t=n:t=[n],t.map(r=>{const o={};return e.forEach((s,a)=>{o[s]=r[a]}),o})}function xn(n){return typeof n=="object"&&n!==null&&"getSQL"in n&&typeof n.getSQL=="function"}function _n(n){return typeof n=="object"&&n!==null&&"sql"in n&&typeof n.sql=="string"&&"params"in n}function Ut(n){if(typeof n=="function"&&(n=n(tt)),xn(n))try{if(!("toSQL"in n&&typeof n.toSQL=="function"))throw 1;const r=n.toSQL();if(!_n(r))throw 2;const o="all"in n&&typeof n.all=="function"?n.all:void 0;return{...r,exec:o?()=>o():void 0}}catch{throw new Error("The passed statement could not be parsed.")}const e=n.sql;let t=[];return"params"in n?t=n.params:"parameters"in n&&(t=n.parameters),{sql:e,params:t}}function Wt(n,e){let t;return typeof n=="string"?t={sql:n,params:e}:t=tt(n,...e),t}async function qe(n,e,t,r){return!e&&"locks"in navigator?navigator.locks.request(`_sqlocal_mutation_(${t.databasePath})`,{mode:n},r):r()}class Kt extends et{constructor(e,t){super(t),Object.defineProperty(this,"storageType",{enumerable:!0,configurable:!0,writable:!0,value:e})}async init(e){const t=this.getFlags(e);if(e.readOnly)throw new Error(`SQLite storage type "${this.storageType}" does not support read-only mode.`);if(!this.sqlite3InitModule){const{default:r}=await at(async()=>{const{default:o}=await import("./index-DTMgZTfd.js");return{default:o}},[]);this.sqlite3InitModule=r}this.sqlite3||(this.sqlite3=await this.sqlite3InitModule()),this.db&&await this.destroy(),this.db=new this.sqlite3.oo1.JsStorageDb({filename:this.storageType,flags:t}),this.config=e,this.initWriteHook()}async isDatabasePersisted(){return navigator.storage?.persisted()}async getDatabaseSizeBytes(){if(!this.db)throw new Error("Driver not initialized");return this.db.storageSize()}async import(e){const t=new et;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 wo,vo;class Sn{constructor(e){Object.defineProperty(this,"config",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"clientKey",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"processor",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"isDestroyed",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"bypassMutationLock",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"transactionQueryKeyQueue",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"userCallbacks",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"queriesInProgress",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"proxy",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"reinitChannel",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"effectsChannel",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"processMessageEvent",{enumerable:!0,configurable:!0,writable:!0,value:c=>{const d=c instanceof MessageEvent?c.data:c,u=this.queriesInProgress;switch(d.type){case"success":case"data":case"buffer":case"info":case"error":if(d.queryKey&&u.has(d.queryKey)){const[p,h]=u.get(d.queryKey);d.type==="error"?h(d.error):p(d),u.delete(d.queryKey)}else if(d.type==="error")throw d.error;break;case"callback":const f=this.userCallbacks.get(d.name);f&&f(...d.args??[]);break;case"event":this.config.onConnect?.(d.reason);break}}}),Object.defineProperty(this,"createQuery",{enumerable:!0,configurable:!0,writable:!0,value:async c=>qe("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=Ke();switch(c.type){case"import":this.processor.postMessage({...c,queryKey:d},[c.database]);break;default:this.processor.postMessage({...c,queryKey:d});break}return new Promise((u,f)=>{this.queriesInProgress.set(d,[u,f])})})}),Object.defineProperty(this,"broadcast",{enumerable:!0,configurable:!0,writable:!0,value:c=>{this.reinitChannel.postMessage(c)}}),Object.defineProperty(this,"exec",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d,u="all",f)=>{const p=await this.createQuery({type:"query",transactionKey:f,sql:c,params:d,method:u}),h={rows:[],columns:[]};return p.type==="data"&&(h.rows=p.data[0]?.rows??[],h.columns=p.data[0]?.columns??[]),h}}),Object.defineProperty(this,"execBatch",{enumerable:!0,configurable:!0,writable:!0,value:async c=>{const d=await this.createQuery({type:"batch",statements:c}),u=new Array(c.length).fill({rows:[],columns:[]});return d.type==="data"&&d.data.forEach((f,p)=>{u[p]=f}),u}}),Object.defineProperty(this,"sql",{enumerable:!0,configurable:!0,writable:!0,value:async(c,...d)=>{const u=Wt(c,d),{rows:f,columns:p}=await this.exec(u.sql,u.params,"all");return yt(f,p)}}),Object.defineProperty(this,"batch",{enumerable:!0,configurable:!0,writable:!0,value:async c=>{const d=c(tt);return(await this.execBatch(d)).map(({rows:f,columns:p})=>yt(f,p))}}),Object.defineProperty(this,"beginTransaction",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{const c=Ke();await this.createQuery({type:"transaction",transactionKey:c,action:"begin"});const d=async h=>{const m=Ut(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 yt(g,y)};return{query:d,sql:async(h,...m)=>{const g=Wt(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=>qe("exclusive",!1,this.config,async()=>{let d;this.bypassMutationLock=!0;try{d=await this.beginTransaction();const u=await c({sql:d.sql,query:d.query});return await d.commit(),u}catch(u){throw await d?.rollback(),u}finally{this.bypassMutationLock=!1}})}),Object.defineProperty(this,"reactiveQuery",{enumerable:!0,configurable:!0,writable:!0,value:c=>{let d=[],u=!1,f=!1,p=0;const h=Ut(c),m=new Set,g=new Set,y=new Set,b=async()=>{try{const S=++p;if(m.size===0){const w=await this.sql("SELECT name, wr FROM tables_used(?) WHERE type = 'table'",h.sql),_=new Set,P=new Set;if(w.forEach(k=>{typeof k.name=="string"&&(k.wr?P.add(k.name):_.add(k.name))}),_.size===0)throw new Error("The passed SQL does not read any tables.");if(Array.from(P).some(k=>_.has(k)))throw new Error("The passed SQL would mutate one or more of the tables that it reads. Doing this in a reactive query would create an infinite loop.");_.forEach(k=>m.add(k))}const L=h.exec?await h.exec():await this.sql(h.sql,...h.params);S===p&&(d=L,u=!0,g.forEach(w=>w(d)))}catch(S){y.forEach(L=>{L(S instanceof Error?S:new Error(String(S)))})}},E=S=>{S.data.tables.some(L=>m.has(L))&&b()};return{get value(){return d},subscribe:(S,L)=>{if(!this.effectsChannel)throw new Error('This SQLocal instance is not configured for reactive queries. Set the "reactive" option to enable them.');return L||(L=w=>{throw w}),g.add(S),y.add(L),f?u&&S(d):(this.effectsChannel.addEventListener("message",E),f=!0,b()),{unsubscribe:()=>{g.delete(S),y.delete(L),g.size===0&&(this.effectsChannel?.removeEventListener("message",E),f=!1)}}}}}}),Object.defineProperty(this,"createCallbackFunction",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{await this.createQuery({type:"function",functionName:c,functionType:"callback"}),this.userCallbacks.set(c,d)}}),Object.defineProperty(this,"createScalarFunction",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{const u=`_sqlocal_func_${c}`,f=()=>{this.proxy[u]=d};this.proxy===globalThis&&f(),await this.createQuery({type:"function",functionName:c,functionType:"scalar"}),this.proxy!==globalThis&&f()}}),Object.defineProperty(this,"createAggregateFunction",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{const u=`_sqlocal_func_${c}`,f=()=>{this.proxy[`${u}_step`]=d.step,this.proxy[`${u}_final`]=d.final};this.proxy===globalThis&&f(),await this.createQuery({type:"function",functionName:c,functionType:"aggregate"}),this.proxy!==globalThis&&f()}}),Object.defineProperty(this,"getDatabaseInfo",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{const c=await this.createQuery({type:"getinfo"});if(c.type==="info")return c.info;throw new Error("The database failed to return valid information.")}}),Object.defineProperty(this,"getDatabaseFile",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{const c=await this.createQuery({type:"export"});if(c.type==="buffer")return new File([c.buffer],c.bufferName,{type:"application/x-sqlite3"});throw new Error("The database failed to export.")}}),Object.defineProperty(this,"overwriteDatabaseFile",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{await qe("exclusive",!1,this.config,async()=>{try{this.broadcast({type:"close",clientKey:this.clientKey});const u=await yo(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 qe("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,wo,{enumerable:!0,configurable:!0,writable:!0,value:()=>{this.destroy()}}),Object.defineProperty(this,vo,{enumerable:!0,configurable:!0,writable:!0,value:async()=>{await this.destroy()}});const t=typeof e=="string"?{databasePath:e}:e,{onInit:r,onConnect:o,processor:s,...a}=t,{databasePath:i}=a;this.config=t,this.clientKey=Ke();const l=bo(i,this.clientKey);if(this.reinitChannel=new BroadcastChannel(`_sqlocal_reinit_(${l})`),a.reactive&&(this.effectsChannel=new BroadcastChannel(`_sqlocal_effects_(${l})`)),typeof s<"u")this.processor=s;else if(i==="local"||i===":localStorage:"){const c=new Kt("local");this.processor=new Ge(c)}else if(i==="session"||i===":sessionStorage:"){const c=new Kt("session");this.processor=new Ge(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 et;this.processor=new Ge(c)}this.processor instanceof Ge?(this.processor.onmessage=c=>this.processMessageEvent(c),this.proxy=globalThis):(this.processor.addEventListener("message",this.processMessageEvent),this.proxy=At(this.processor)),this.processor.postMessage({type:"config",config:{...a,clientKey:this.clientKey,onInitStatements:r?.(tt)??[]}})}}wo=Symbol.dispose,vo=Symbol.asyncDispose;const Dt="lupmis2.db",Ln="lupmis-db-sync",Eo=new Sn(Dt),{sql:x}=Eo;console.log("[Database] SQLocal instance created for:",Dt);const xo=new BroadcastChannel(Ln);let _o=!1,So,Lo;new Promise((n,e)=>{So=n,Lo=e});const ot=new Set;function kn(n){return ot.add(n),()=>ot.delete(n)}xo.onmessage=n=>{const{type:e,payload:t}=n.data;if(e==="DB_CHANGE")for(const r of ot)try{r(t)}catch(o){console.error("[Database] Change listener error:",o)}};function st(n,e,t=null){xo.postMessage({type:"DB_CHANGE",payload:{table:n,action:e,id:t,timestamp:Date.now()}});for(const r of ot)try{r({table:n,action:e,id:t,timestamp:Date.now(),local:!0})}catch(o){console.error("[Database] Change listener error:",o)}}async function Tn(){try{console.log("[Database] Initializing schema...");const n=await x`SELECT sqlite_version() as version`;console.log("[Database] SQLite version:",n[0]?.version),console.log("[Database] Creating locations table..."),await x`
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 x`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 x`
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 x`
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 x`
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 x`
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 x`SELECT status FROM parcels LIMIT 1`}catch{console.log("[Database] Adding status column to parcels table..."),await x`ALTER TABLE parcels ADD COLUMN status TEXT DEFAULT 'verified'`}console.log("[Database] Creating building_footprints table..."),await x`
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 x`
CREATE TABLE IF NOT EXISTS osm_roads (
osm_id INTEGER PRIMARY KEY,
geometry_wkt TEXT,
properties TEXT,
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`,await x`CREATE INDEX IF NOT EXISTS idx_locations_category ON locations(category)`,await x`CREATE INDEX IF NOT EXISTS idx_locations_synced ON locations(synced)`;const t=await x`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;console.log("[Database] All tables:",t.map(r=>r.name)),_o=!0,So(!0),console.log("[Database] ✓ Schema initialized")}catch(n){throw console.error("[Database] ✗ Schema init failed:",n),Lo(n),n}}async function Pn(n,e,t,r={}){const{description:o=null,category:s="default"}=r;console.log("[Database] Adding location:",n,e,t,s);try{const a=await x`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;if(console.log("[Database] Table check before insert:",a),a.length===0)throw console.error("[Database] ✗ locations table does not exist!"),new Error("locations table does not exist");console.log("[Database] Executing INSERT..."),await x`
INSERT INTO locations (name, longitude, latitude, description, category)
VALUES (${n}, ${e}, ${t}, ${o}, ${s})
`,console.log("[Database] INSERT completed");const l=(await x`SELECT last_insert_rowid() as id`)[0]?.id;console.log("[Database] New ID:",l);const c=await x`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 x`
INSERT INTO sync_log (table_name, record_id, action)
VALUES ('locations', ${l}, 'INSERT')
`,st("locations","INSERT",l),console.log("[Database] ✓ Location added:",l),{id:l}}catch(a){throw console.error("[Database] ✗ Failed to add location:",a),a}}async function ko(n={}){const{category:e=null,limit:t=1e3}=n;try{const r=await x`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;if(console.log("[Database] getLocations - table exists:",r.length>0),r.length===0)return console.warn("[Database] locations table does not exist yet"),[];let o;return e?o=await x`
SELECT * FROM locations
WHERE category = ${e}
ORDER BY created_at DESC
LIMIT ${t}
`:o=await x`
SELECT * FROM locations
ORDER BY created_at DESC
LIMIT ${t}
`,console.log("[Database] getLocations returned",o.length,"rows"),o}catch(r){return console.error("[Database] getLocations error:",r),[]}}async function Mn(){try{return(await x`SELECT COUNT(*) as count FROM locations`)[0]?.count??0}catch(n){return console.error("[Database] getLocationCount error:",n),0}}async function To(n,e){try{const t=JSON.stringify(e);await x`
INSERT OR REPLACE INTO remote_data (key, data, fetched_at)
VALUES (${n}, ${t}, CURRENT_TIMESTAMP)
`,console.log("[Database] ✓ Remote data cached:",n)}catch(t){throw console.error("[Database] ✗ Failed to cache remote data:",n,t),t}}async function Po(n){try{const e=await x`SELECT data, fetched_at FROM remote_data WHERE key = ${n}`;if(e.length===0)return null;const t=JSON.parse(e[0].data);return console.log("[Database] ✓ Remote data loaded from cache:",n,"(fetched",e[0].fetched_at+")"),t}catch(e){return console.error("[Database] ✗ Failed to read cached remote data:",n,e),null}}async function Cn(n){try{await x`DELETE FROM collector_zones`;for(const e of n){const t=JSON.stringify(e);await x`
INSERT INTO collector_zones (id, zone_name, geometry_wkt, properties, fetched_at)
VALUES (${e.colzonenr||e.id}, ${e.colzonename||e.zone_name||""}, ${e.polygon||e.boundary||""}, ${t}, CURRENT_TIMESTAMP)
`}console.log("[Database] ✓ Saved",n.length,"collector zones")}catch(e){throw console.error("[Database] ✗ Failed to save collector zones:",e),e}}async function In(){try{const n=await x`SELECT properties FROM collector_zones ORDER BY id`;return n.length===0?null:n.map(e=>JSON.parse(e.properties))}catch(n){return console.error("[Database] ✗ Failed to read local collector zones:",n),null}}async function An(n){try{await x`DELETE FROM parcels`;let e=0;for(const t of n){const r=t.id||t.parcelid||t.parcel_id||null;if(r==null)continue;const o=JSON.stringify(t),s=t.boundary||t.polygon||t.geom||t.wkt||"";await x`
INSERT OR REPLACE INTO parcels (id, geometry_wkt, properties, fetched_at)
VALUES (${r}, ${s}, ${o}, CURRENT_TIMESTAMP)
`,e++}console.log("[Database] ✓ Saved",e,"parcels (from",n.length,"rows,",n.length-e,"duplicates replaced)")}catch(e){throw console.error("[Database] ✗ Failed to save parcels:",e),e}}async function Dn(){try{const n=await x`SELECT properties FROM parcels ORDER BY id`;return n.length===0?null:n.map(e=>JSON.parse(e.properties))}catch(n){return console.error("[Database] ✗ Failed to read local parcels:",n),null}}async function Fn(n,e){try{const t=JSON.stringify(e);await x`UPDATE parcels SET properties = ${t} WHERE id = ${n}`,console.log("[Database] ✓ Parcel updated:",n),st("parcels","UPDATE",n)}catch(t){throw console.error("[Database] ✗ Failed to update parcel:",n,t),t}}async function On(n,e){try{const t=JSON.stringify(e);await x`
INSERT INTO parcels (id, geometry_wkt, properties, status, fetched_at)
VALUES (NULL, ${n}, ${t}, 'new', CURRENT_TIMESTAMP)
`;const o=(await x`SELECT last_insert_rowid() as id`)[0]?.id;return console.log("[Database] ✓ New parcel inserted:",o,"(status: new)"),st("parcels","INSERT",o),{id:o}}catch(t){throw console.error("[Database] ✗ Failed to insert new parcel:",t),t}}async function Bn(n){try{if(n.length>0){const e=n[0],t={};for(const[r,o]of Object.entries(e))t[r]=o===null?"null":typeof o;console.log("[Database] First footprint field types:",t)}await x`DELETE FROM building_footprints`;for(const e of n){const t=JSON.stringify(e);let r=e.polygon||e.boundary||e.geom||e.wkt||e.footprint||"";const o=typeof r=="object"?JSON.stringify(r):String(r);let s=e.id||e.footprint_id||e.building_id||null;await x`
INSERT INTO building_footprints (id, geometry_wkt, properties, fetched_at)
VALUES (${s!==null&&typeof s=="object"?null:s}, ${o}, ${t}, CURRENT_TIMESTAMP)
`}console.log("[Database] ✓ Saved",n.length,"building footprints")}catch(e){throw console.error("[Database] ✗ Failed to save building footprints:",e),e}}async function Rn(){try{const n=await x`SELECT properties FROM building_footprints ORDER BY id`;return n.length===0?null:n.map(e=>JSON.parse(e.properties))}catch(n){return console.error("[Database] ✗ Failed to read local building footprints:",n),null}}async function Nn(n){try{if(n.length>0){const e=n[0],t={};for(const[r,o]of Object.entries(e))t[r]=o===null?"null":typeof o;console.log("[Database] First road field types:",t)}await x`DELETE FROM osm_roads`;for(const e of n){const t=JSON.stringify(e);let r=e.geom||e.geometry||e.wkt||e.road||e.line||"";const o=typeof r=="object"?JSON.stringify(r):String(r);let s=e.osm_id??e.osmid??e.id??null;await x`
INSERT OR REPLACE INTO osm_roads (osm_id, geometry_wkt, properties, fetched_at)
VALUES (${s!==null&&typeof s=="object"?null:s}, ${o}, ${t}, CURRENT_TIMESTAMP)
`}console.log("[Database] ✓ Saved",n.length,"OSM roads")}catch(e){throw console.error("[Database] ✗ Failed to save OSM roads:",e),e}}async function $n(){try{const n=await x`SELECT properties FROM osm_roads ORDER BY osm_id`;return n.length===0?null:n.map(e=>JSON.parse(e.properties))}catch(n){return console.error("[Database] ✗ Failed to read local OSM roads:",n),null}}async function jn(){return Eo.getDatabaseFile()}async function zn(n="lupmis-backup.sqlite3"){const e=await jn(),t=new Blob([e],{type:"application/x-sqlite3"}),r=URL.createObjectURL(t),o=document.createElement("a");o.href=r,o.download=n,o.click(),URL.revokeObjectURL(r)}async function Gn(){return{type:"FeatureCollection",features:(await ko()).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 Ft(){try{const n=await x`
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
ORDER BY name
`,e=await Mn();return{ready:_o,databasePath:Dt,tables:n.map(t=>t.name),locationCount:e}}catch(n){return{ready:!1,error:n.message}}}const Mo=Object.freeze(["parcels","building_footprints","osm_roads","collector_zones","remote_data"]);function Co(n){return Mo.includes(n)}async function Io(n){if(!Co(n))throw new Error(`Refusing to clear "${n}" — not a known cached-layer table`);const t=(await x(`SELECT COUNT(*) AS n FROM "${n}"`))[0]?.n??0;return await x(`DELETE FROM "${n}"`),console.log(`[Database] ✓ Cleared "${n}" (${t} rows)`),st(n,"CLEAR",null),t}async function qn(){const n=await x`
SELECT name FROM sqlite_master
WHERE type='table' AND name IN (
'parcels', 'building_footprints', 'osm_roads', 'collector_zones', 'remote_data'
)
`,e=new Set(n.map(o=>o.name)),t=[];for(const o of Mo)if(e.has(o))try{const s=await Io(o);t.push({table:o,count:s})}catch(s){console.error(`[Database] Failed to clear ${o}:`,s),t.push({table:o,count:0,error:s.message})}const r=t.reduce((o,s)=>o+s.count,0);return console.log(`[Database] ✓ Cleared all cached layers: ${r} rows across ${t.length} tables`),t}async function Hn(){const n=await x`
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%'
ORDER BY name
`;if(n.length===0)return[];const e=n.map(t=>`SELECT '${t.name}' AS name, COUNT(*) AS count FROM "${t.name}"`).join(" UNION ALL ");return x(e)}async function Un(n,e=200){if((await x`
SELECT name FROM sqlite_master
WHERE type='table' AND name = ${n}
`).length===0)throw new Error(`Table "${n}" does not exist`);const r=await x(`SELECT * FROM "${n}" LIMIT ${e}`);return{columns:r.length>0?Object.keys(r[0]):[],rows:r}}async function Wn(){console.log("=== DATABASE TEST ===");try{const n=await x`SELECT sqlite_version() as v`;console.log("1. SQLite version:",n[0].v);const e=await x`SELECT name FROM sqlite_master WHERE type='table'`;console.log("2. Tables:",e.map(o=>o.name)),console.log("3. Inserting test row..."),await x`INSERT INTO locations (name, longitude, latitude, category) VALUES ('TEST', -1.0, 7.0, 'test')`;const t=await x`SELECT * FROM locations WHERE name = 'TEST'`;console.log("4. Test row:",t);const r=await x`SELECT COUNT(*) as c FROM locations`;return console.log("5. Total rows:",r[0].c),await x`DELETE FROM locations WHERE name = 'TEST'`,console.log("6. Test row deleted"),console.log("=== TEST PASSED ==="),!0}catch(n){return console.error("=== TEST FAILED ===",n),!1}}typeof window<"u"&&(window.testDatabase=Wn,window.dbStatus=Ft);const Ao=3.28084,Do=621371e-9,Fo=10.7639,Oo=247105e-9,Bo=3861e-10;function it(){return localStorage.getItem("measurement-system")||"metric"}function nt(n){if(it()==="imperial"){const e=n*Ao;return e>=5280?Math.round(n*Do*100)/100+" mi":Math.round(e)+" ft"}return n>1e3?Math.round(n/1e3*100)/100+" km":Math.round(n*100)/100+" m"}function Kn(n){if(it()==="imperial"){const e=n*Ao,t=n*Do;return e>=5280?`${t.toFixed(2)} mi (${e.toLocaleString("en",{maximumFractionDigits:0})} ft)`:`${e.toLocaleString("en",{maximumFractionDigits:1})} ft`}return n>=1e3?`${(n/1e3).toFixed(2)} km (${n.toLocaleString("en",{maximumFractionDigits:0})} m)`:`${n.toLocaleString("en",{maximumFractionDigits:1})} m`}function je(n){if(it()==="imperial"){const e=n*Oo;return e>=640?Math.round(n*Bo*100)/100+" mi²":e>=1?Math.round(e*100)/100+" acres":Math.round(n*Fo).toLocaleString("en")+" ft²"}return n>1e6?Math.round(n/1e6*100)/100+" km²":Math.round(n*100)/100+" m²"}function Vn(n){if(it()==="imperial"){const e=n*Fo,t=n*Oo,r=n*Bo;return t>=640?`${r.toFixed(2)} mi² (${t.toLocaleString("en",{maximumFractionDigits:0})} acres)`:t>=1?`${t.toLocaleString("en",{maximumFractionDigits:1})} acres (${e.toLocaleString("en",{maximumFractionDigits:0})} ft²)`:`${e.toLocaleString("en",{maximumFractionDigits:0})} ft²`}return n>1e6?`${(n/1e6).toFixed(2)} km² (${n.toLocaleString("en",{maximumFractionDigits:0})} m²)`:`${n.toLocaleString("en",{maximumFractionDigits:0})} m²`}function Yn(n){return je(Math.PI*n*n)}function Jn(n,e,t,r,o=1e-10){const s=e[0]-n[0],a=e[1]-n[1],i=r[0]-t[0],l=r[1]-t[1],c=s*l-a*i;if(Math.abs(c)<o)return null;const d=t[0]-n[0],u=t[1]-n[1],f=(d*l-u*i)/c,p=(d*a-u*s)/c;return f<-o||f>1+o||p<-o||p>1+o?null:{point:[n[0]+f*s,n[1]+f*a],t:Math.max(0,Math.min(1,f)),u:Math.max(0,Math.min(1,p))}}function Ro(n){let e=0;for(let t=0,r=n.length;t<r-1;t++)e+=n[t][0]*n[t+1][1]-n[t+1][0]*n[t][1];return e/2}function Tt(n,e){let t=!1;for(let r=0,o=e.length-2;r<e.length-1;o=r++){const s=e[r][0],a=e[r][1],i=e[o][0],l=e[o][1];a>n[1]!=l>n[1]&&n[0]<(i-s)*(n[1]-a)/(l-a)+s&&(t=!t)}return t}function ze(n,e){return(n[0]-e[0])**2+(n[1]-e[1])**2}function Xn(n,e){const t=[];for(let o=0;o<e.length-1;o++)for(let s=0;s<n.length-1;s++){const a=Jn(n[s],n[s+1],e[o],e[o+1],1e-10);if(!a)continue;const i=a.point;let l=!1;for(const c of t)if(ze(c.point,i)<1e-6){l=!0;break}l||t.push({point:i,ringSegIdx:s,ringT:a.t,lineSegIdx:o,lineT:a.u})}return t.sort((o,s)=>o.lineSegIdx!==s.lineSegIdx?o.lineSegIdx-s.lineSegIdx:o.lineT-s.lineT),t}function Zn(n,e){const t=e.map((s,a)=>({...s,origOrder:a}));t.sort((s,a)=>s.ringSegIdx!==a.ringSegIdx?s.ringSegIdx-a.ringSegIdx:s.ringT-a.ringT);const r=n.slice(),o=new Array(t.length);for(let s=t.length-1;s>=0;s--){const a=t[s],i=a.ringSegIdx+1,l=1e-6;if(ze(a.point,r[a.ringSegIdx])<l){o[a.origOrder]=a.ringSegIdx;continue}if(ze(a.point,r[a.ringSegIdx+1])<l){o[a.origOrder]=a.ringSegIdx+1;continue}r.splice(i,0,a.point),o[a.origOrder]=i;for(let c=s+1;c<t.length;c++)o[t[c].origOrder]>=i&&o[t[c].origOrder]++}return{ring:r,indices:o}}function Vt(n,e,t){const r=n.length-1,o=(e%r+r)%r,s=(t%r+r)%r,a=[];let i=o;for(;a.push(n[i]),i!==s;)i=(i+1)%r;return a}function Yt(n,e,t){const r=[e.point],o=e.lineSegIdx,s=t.lineSegIdx;for(let a=o+1;a<=s;a++)r.push(n[a]);return ze(r[r.length-1],t.point)>1e-10&&r.push(t.point),r}function Jt(n,e){const t=Ro(n);return e&&t<0||!e&&t>0?n.slice().reverse():n}function Xt(n){if(n.length<2)return n;const e=n[0],t=n[n.length-1];return ze(e,t)>1e-10?[...n,e.slice()]:n}function Qn(n,e){let t=1/0,r=1/0,o=-1/0,s=-1/0;for(const c of e)c[0]<t&&(t=c[0]),c[1]<r&&(r=c[1]),c[0]>o&&(o=c[0]),c[1]>s&&(s=c[1]);const a=Math.sqrt((o-t)**2+(s-r)**2)||1,i=n.slice();if(Tt(i[0],e)){const c=i[0],d=i[1],u=c[0]-d[0],f=c[1]-d[1],p=Math.sqrt(u*u+f*f)||1,h=a*2/p;i[0]=[c[0]+u*h,c[1]+f*h]}const l=i.length-1;if(Tt(i[l],e)){const c=i[l],d=i[l-1],u=c[0]-d[0],f=c[1]-d[1],p=Math.sqrt(u*u+f*f)||1,h=a*2/p;i[l]=[c[0]+u*h,c[1]+f*h]}return i}function Ve(n,e){const t=n[0],r=n.slice(1),o=Qn(e,t),s=Xn(t,o);if(s.length!==2)return console.warn(`[polygonSplit] Expected 2 intersections, got ${s.length}`),null;const[a,i]=s,{ring:l,indices:c}=Zn(t,s),d=c[0],u=c[1],[f,p]=d<u?[d,u]:[u,d],h=d<u?Yt(o,a,i):Yt(o,i,a),m=h.slice().reverse(),g=Vt(l,f,p),y=Xt([...g,...m.slice(1)]),b=Vt(l,p,f),E=Xt([...b,...h.slice(1)]),S=Ro(t)>0,L=Jt(y,S),w=Jt(E,S),_=[L],P=[w];for(const k of r){const $=er(k);Tt($,L)?_.push(k):P.push(k)}return[_,P]}function er(n){let e=0,t=0;const r=n.length-1;for(let o=0;o<r;o++)e+=n[o][0],t+=n[o][1];return[e/r,t/r]}const Zt={success:{bg:"#10b981",icon:"✅"},error:{bg:"#ef4444",icon:"❌"},warning:{bg:"#f59e0b",icon:"⚠️"},info:{bg:"#0ea5e9",icon:""}};let _e=null;function tr(){return _e||(_e=document.createElement("div"),_e.style.cssText=`
position: fixed;
top: 16px;
left: 50%;
transform: translateX(-50%);
z-index: 10000;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
pointer-events: none;
`,document.body.appendChild(_e),_e)}function O(n,e="info",t=4e3){const r=tr(),o=Zt[e]||Zt.info,s=document.createElement("div");s.style.cssText=`
background: ${o.bg};
color: #fff;
padding: 10px 18px;
border-radius: 8px;
font-family: var(--font-body, 'Exo', sans-serif);
font-size: 13px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
pointer-events: auto;
cursor: pointer;
opacity: 0;
transition: opacity 0.25s ease, transform 0.25s ease;
transform: translateY(-8px);
max-width: 420px;
text-align: center;
line-height: 1.4;
`,s.textContent=`${o.icon} ${n}`,r.appendChild(s),requestAnimationFrame(()=>{s.style.opacity="1",s.style.transform="translateY(0)"});const a=()=>{s.style.opacity="0",s.style.transform="translateY(-8px)",setTimeout(()=>s.remove(),300)};s.addEventListener("click",a),setTimeout(a,t)}const He=[{stroke:"#ef4444",fill:"rgba(239,68,68,0.25)"},{stroke:"#3b82f6",fill:"rgba(59,130,246,0.25)"}],or=new C({stroke:new T({color:"#0ea5e9",width:3}),fill:new M({color:"rgba(14,165,233,0.15)"})}),nr=new C({stroke:new T({color:"#f43f5e",width:2,lineDash:[8,6]}),image:new ce({radius:5,fill:new M({color:"#f43f5e"}),stroke:new T({color:"#fff",width:1.5})})});class rr extends Ct{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 z({useSpatialIndex:!1}),this._overlayLayer=new N({source:this._overlaySource,displayInLayerSwitcher:!1,style:or})}setMap(e){this.getMap()&&(this.getMap().removeLayer(this._overlayLayer),this._removeDrawInteraction()),super.setMap(e),e&&this._overlayLayer.setMap(e)}setActive(e){super.setActive(e),e||this._reset()}_getSources(){if(this._sources)return this._sources;if(!this.getMap())return[];const e=[],t=r=>{r.forEach(o=>{o.getVisible()&&(o.getSource&&o.getSource()instanceof z?e.push(o.getSource()):o.getLayers&&t(o.getLayers()))})};return t(this.getMap().getLayers()),e}_handleEvent(e){if(!this.getActive())return!0;if(this._phase==="select"){if(e.type==="pointermove")return this._onSelectMove(e);if(e.type==="singleclick")return this._onSelectClick(e)}if(this._phase==="draw"&&e.type==="keydown"&&e.originalEvent?.key==="Escape")return this._cancelDraw(),!1;if(this._phase==="pick"){if(e.type==="pointermove")return this._onPickMove(e);if(e.type==="singleclick")return this._onPickClick(e);if(e.type==="keydown"&&e.originalEvent?.key==="Escape")return this._reset(),!1}return!0}_onSelectMove(e){const t=this.getMap();if(!t)return!0;this._overlaySource.clear();const r=this._closestPolygon(e);if(r){const o=r.feature.clone();this._overlaySource.addFeature(o),t.getTargetElement().style.cursor="pointer"}else t.getTargetElement().style.cursor="";return!0}_onSelectClick(e){const t=this._closestPolygon(e);if(!t)return!0;this._selectedFeature=t.feature,this._selectedSource=t.source,this._overlaySource.clear();const r=t.feature.clone();return this._overlaySource.addFeature(r),this._startDrawPhase(),!1}_closestPolygon(e){let t=null,r=this.snapDistance_+1;for(const o of this._getSources()){const s=o.getClosestFeatureToCoordinate(e.coordinate);if(!s)continue;const a=s.getGeometry();if(!a)continue;const i=a.getType();if(i!=="Polygon"&&i!=="MultiPolygon")continue;const l=a.getClosestPoint(e.coordinate),d=new de([e.coordinate,l]).getLength()/e.frameState.viewState.resolution;d<r&&(r=d,t={feature:s,source:o,coord:l})}return t}_startDrawPhase(){this._phase="draw";const e=this.getMap();e&&(e.getTargetElement().style.cursor="crosshair",this._drawInteraction=new me({type:"LineString",style:nr}),this._drawInteraction.on("drawend",t=>{const r=t.feature.getGeometry().getCoordinates();this._performSplit(r)}),e.addInteraction(this._drawInteraction))}_removeDrawInteraction(){this._drawInteraction&&this.getMap()&&this.getMap().removeInteraction(this._drawInteraction),this._drawInteraction=null}_cancelDraw(){this._removeDrawInteraction(),this._reset()}_performSplit(e){const t=this._selectedFeature,r=this._selectedSource,o=t.getGeometry();let s;o.getType()==="Polygon"?s=o.getCoordinates():o.getType()==="MultiPolygon"&&(s=o.getCoordinates()[0]);const a=Ve(s,e);if(!a){console.warn("[PolygonSplit] Split failed — line must cross the polygon boundary at exactly 2 points."),this._removeDrawInteraction(),this._startDrawPhase();return}const[i,l]=a,c=t.clone();c.setGeometry(new Ye(i)),c.setStyle(new C({stroke:new T({color:He[0].stroke,width:2.5}),fill:new M({color:He[0].fill})}));const d=t.clone();d.setGeometry(new Ye(l)),d.setStyle(new C({stroke:new T({color:He[1].stroke,width:2.5}),fill:new M({color:He[1].fill})}));const u=[c,d];if(this.dispatchEvent({type:"beforesplit",original:t,features:u}),r.dispatchEvent({type:"beforesplit",original:t,features:u}),r.removeFeature(t),r.addFeature(c),r.addFeature(d),this.dispatchEvent({type:"aftersplit",original:t,features:u}),r.dispatchEvent({type:"aftersplit",original:t,features:u}),this._removeDrawInteraction(),t.get("_layerType")==="parcel"){this._splitFeatures=u,this._phase="pick",this._overlaySource.clear();const p=this.getMap();p&&(p.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:r})}else this._reset()}_onPickMove(e){const t=this.getMap();if(!t)return!0;this._overlaySource.clear();const r=this._closestSplitPiece(e);if(r){const o=r.clone();this._overlaySource.addFeature(o),t.getTargetElement().style.cursor="pointer"}else t.getTargetElement().style.cursor="";return!0}_onPickClick(e){const t=this._closestSplitPiece(e);return t?(this.dispatchEvent({type:"splitpick",picked:t,features:this._splitFeatures}),this._reset(),!1):!0}_closestSplitPiece(e){if(!this._splitFeatures)return null;let t=null,r=this.snapDistance_+1;for(const o of this._splitFeatures){const s=o.getGeometry();if(!s)continue;const a=s.getClosestPoint(e.coordinate),l=new de([e.coordinate,a]).getLength()/e.frameState.viewState.resolution;l<r&&(r=l,t=o)}return t}_reset(){this._phase="select",this._selectedFeature=null,this._selectedSource=null,this._splitFeatures=null,this._overlaySource.clear(),this._removeDrawInteraction();const e=this.getMap();e&&(e.getTargetElement().style.cursor="")}}function Ce(n,e){return(n[0]-e[0])**2+(n[1]-e[1])**2}function Ne(n){let e=0;for(let t=0,r=n.length;t<r-1;t++)e+=n[t][0]*n[t+1][1]-n[t+1][0]*n[t][1];return e/2}function ar(n,e){let t=!1;for(let r=0,o=e.length-2;r<e.length-1;o=r++){const s=e[r][0],a=e[r][1],i=e[o][0],l=e[o][1];a>n[1]!=l>n[1]&&n[0]<(i-s)*(n[1]-a)/(l-a)+s&&(t=!t)}return t}function sr(n,e){const t=Ne(n);return e&&t<0||!e&&t>0?n.slice().reverse():n}function ir(n){return n.length<2?n:Ce(n[0],n[n.length-1])>1e-10?[...n,n[0].slice()]:n}function Pe(n,e,t){const r=t[0]-e[0],o=t[1]-e[1],s=r*r+o*o;if(s<1e-20)return Ce(n,e);let a=((n[0]-e[0])*r+(n[1]-e[1])*o)/s;a=Math.max(0,Math.min(1,a));const i=e[0]+a*r,l=e[1]+a*o;return(n[0]-i)**2+(n[1]-l)**2}function Qt(n,e){let t=0,r=1/0;const o=n.length-1;for(let s=0;s<o;s++){const a=Pe(e,n[s],n[(s+1)%o===0?o:s+1]);a<r&&(r=a,t=s)}return{segIdx:t,distSq:r}}function Se(n,e,t){return Ce(n,e)<t}function Ue(n,e,t){const r=e.length-1;for(let o=0;o<r;o++)if(Pe(n,e[o],e[o+1])<t)return!0;return!1}function lr(n,e,t,r,o){const s=n.length-1,a=e.length-1,i=o*o,l=n[t],c=n[(t+1)%s],d=e[r],u=e[(r+1)%a],f=Ue(l,e,i),p=Ue(c,e,i),h=Ue(d,n,i),m=Ue(u,n,i);if(!(f&&p)&&!(h&&m))return console.warn("[polygonMerge] Seed edges are not on the shared boundary"),null;let g;Se(l,u,i)&&Se(c,d,i)?g=!0:Se(l,d,i)&&Se(c,u,i)?g=!1:g=Ce(l,u)<Ce(l,d);let y=t,b=(t+1)%s,E,S;g?(E=(r+1)%a,S=r):(E=r,S=(r+1)%a);let L=s+a;for(;L-- >0;){const w=(b+1)%s,_=g?(S-1+a)%a:(S+1)%a;if(w===y||_===E)break;if(Se(n[w],e[_],i)){b=w,S=_;continue}if(Pe(n[w],e[S],e[_])<i){b=w;continue}if(Pe(e[_],n[b],n[w])<i){S=_;continue}break}for(L=s+a;L-- >0;){const w=(y-1+s)%s,_=g?(E+1)%a:(E-1+a)%a;if(w===b||_===S)break;if(Se(n[w],e[_],i)){y=w,E=_;continue}if(Pe(n[w],e[E],e[_])<i){y=w;continue}if(Pe(e[_],n[y],n[w])<i){E=_;continue}break}return{startA:y,endA:b,startB:E,endB:S,reversed:g}}function bt(n,e,t){const r=n.length-1,o=[];let s=e;for(;o.push(n[s]),!(s===t||(s=(s+1)%r,o.length>r+1)););return o}function cr(n,e,t,r,o=5){const s=n[0],a=e[0],i=n.slice(1),l=e.slice(1),c=Qt(s,t),d=Qt(a,r),u=lr(s,a,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:f,endA:p,startB:h,endB:m,reversed:g}=u;s.length-1,a.length-1;const y=bt(s,p,f);let b;g?b=bt(a,h,m):b=bt(a,m,h);const E=[...y,...b.slice(1)],S=o*o;E.length>2&&Ce(E[E.length-1],E[0])<S&&(E[E.length-1]=E[0].slice());const L=ir(E),w=Math.abs(Ne(s)),_=Math.abs(Ne(a)),P=Math.abs(Ne(L)),k=w+_;if(P<k*.5||P>k*1.5)return console.warn(`[polygonMerge] Area mismatch: A=${w.toFixed(1)}, B=${_.toFixed(1)}, merged=${P.toFixed(1)}, expected≈${k.toFixed(1)}`),{coords:null,error:"Merge produced an invalid polygon (area mismatch). The polygons may not be truly adjacent — try clicking closer to the shared boundary."};const $=Ne(s)>0,q=sr(L,$),A=[...i,...l].filter(Q=>{const W=Q.reduce((B,re)=>B+re[0],0)/(Q.length-1),ne=Q.reduce((B,re)=>B+re[1],0)/(Q.length-1);return ar([W,ne],q)});return{coords:[q,...A]}}const eo=new C({stroke:new T({color:"#0ea5e9",width:3}),fill:new M({color:"rgba(14,165,233,0.15)"})}),dr=new C({stroke:new T({color:"#f59e0b",width:3}),fill:new M({color:"rgba(245,158,11,0.15)"})}),ur=new C({stroke:new T({color:"#0ea5e9",width:3}),fill:new M({color:"rgba(14,165,233,0.15)"}),text:new Je({text:"A",font:"bold 22px Exo, sans-serif",fill:new M({color:"#0ea5e9"}),stroke:new T({color:"#fff",width:4}),overflow:!0})}),pr=new C({stroke:new T({color:"#f59e0b",width:3}),fill:new M({color:"rgba(245,158,11,0.15)"}),text:new Je({text:"B",font:"bold 22px Exo, sans-serif",fill:new M({color:"#f59e0b"}),stroke:new T({color:"#fff",width:4}),overflow:!0})}),fr=new C({stroke:new T({color:"#ec4899",width:4,lineDash:[10,6]})}),hr=new C({stroke:new T({color:"#10b981",width:2.5}),fill:new M({color:"rgba(16,185,129,0.3)"})});class gr extends Ct{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 z({useSpatialIndex:!1}),this._highlightLayer=new N({source:this._highlightSource,displayInLayerSwitcher:!1,style:t=>t.get("_highlightStyle")||eo}),this._edgeSource=new z({useSpatialIndex:!1}),this._edgeLayer=new N({source:this._edgeSource,displayInLayerSwitcher:!1,style:fr})}setMap(e){this.getMap()&&(this.getMap().removeLayer(this._highlightLayer),this.getMap().removeLayer(this._edgeLayer)),super.setMap(e),e&&(this._highlightLayer.setMap(e),this._edgeLayer.setMap(e))}setActive(e){super.setActive(e),e||this._reset()}_getSources(){if(!this.getMap())return[];const e=[],t=r=>{r.forEach(o=>{o.getVisible()&&(o.getSource&&o.getSource()instanceof z?e.push(o.getSource()):o.getLayers&&t(o.getLayers()))})};return t(this.getMap().getLayers()),e}_handleEvent(e){if(!this.getActive())return!0;if(e.type==="keydown"&&e.originalEvent?.key==="Escape")return this._reset(),!1;switch(this._phase){case"select_a":if(e.type==="pointermove")return this._onSelectMove(e,null);if(e.type==="singleclick")return this._onSelectAClick(e);break;case"select_b":if(e.type==="pointermove")return this._onSelectMove(e,this._featureA);if(e.type==="singleclick")return this._onSelectBClick(e);break;case"click_edge_a":if(e.type==="pointermove")return this._onEdgeMove(e,this._featureA);if(e.type==="singleclick")return this._onEdgeAClick(e);break;case"click_edge_b":if(e.type==="pointermove")return this._onEdgeMove(e,this._featureB);if(e.type==="singleclick")return this._onEdgeBClick(e);break}return!0}_onSelectMove(e,t){const r=this.getMap();if(!r)return!0;this._highlightSource.clear(),this._edgeSource.clear(),this._rebuildHighlights();const o=this._closestPolygon(e,t);if(o){const s=this._phase==="select_a"?eo:dr,a=o.feature.clone();a.set("_highlightStyle",s),this._highlightSource.addFeature(a),r.getTargetElement().style.cursor="pointer"}else r.getTargetElement().style.cursor="";return!0}_onSelectAClick(e){const t=this._closestPolygon(e,null);return t?(this._featureA=t.feature,this._sourceA=t.source,this._phase="select_b",this._rebuildHighlights(),!1):!0}_onSelectBClick(e){const t=this._closestPolygon(e,this._featureA);return t?(this._featureB=t.feature,this._sourceB=t.source,this._phase="click_edge_a",this._rebuildHighlights(),this.getMap().getTargetElement().style.cursor="crosshair",!1):!0}_closestPolygon(e,t){let r=null,o=this.snapDistance_+1;for(const s of this._getSources()){const a=s.getClosestFeatureToCoordinate(e.coordinate);if(!a||t&&a===t)continue;const i=a.getGeometry();if(!i)continue;const l=i.getType();if(l!=="Polygon"&&l!=="MultiPolygon")continue;const c=i.getClosestPoint(e.coordinate),u=new de([e.coordinate,c]).getLength()/e.frameState.viewState.resolution;u<o&&(o=u,r={feature:a,source:s,coord:c})}return r}_onEdgeMove(e,t){const r=this.getMap();if(!r)return!0;this._edgeSource.clear();const o=this._closestEdgeSegment(t,e);if(o){const s=new Me(new de([o.segStart,o.segEnd]));this._edgeSource.addFeature(s),r.getTargetElement().style.cursor="crosshair"}return!0}_onEdgeAClick(e){return this._edgeClickA=e.coordinate,this._phase="click_edge_b",this._edgeSource.clear(),!1}_onEdgeBClick(e){return this._edgeClickB=e.coordinate,this._performMerge(),!1}_closestEdgeSegment(e,t){const r=e.getGeometry();let o;if(r.getType()==="Polygon")o=r.getCoordinates()[0];else if(r.getType()==="MultiPolygon")o=r.getCoordinates()[0][0];else return null;const s=t.frameState.viewState.resolution;let a=1/0,i=null;const l=o.length-1;for(let c=0;c<l;c++){const d=o[c],u=o[c+1],f=u[0]-d[0],p=u[1]-d[1],h=f*f+p*p;if(h<1e-20)continue;let m=((t.coordinate[0]-d[0])*f+(t.coordinate[1]-d[1])*p)/h;m=Math.max(0,Math.min(1,m));const g=d[0]+m*f,y=d[1]+m*p,b=Math.sqrt((t.coordinate[0]-g)**2+(t.coordinate[1]-y)**2)/s;b<a&&(a=b,i={segStart:d,segEnd:u})}return a<=this.snapDistance_?i:null}_performMerge(){const e=this._featureA,t=this._featureB,r=this._sourceA,o=this._sourceB,s=e.getGeometry(),a=t.getGeometry(),i=s.getType()==="Polygon"?s.getCoordinates():s.getCoordinates()[0],l=a.getType()==="Polygon"?a.getCoordinates():a.getCoordinates()[0],c=cr(i,l,this._edgeClickA,this._edgeClickB,this.tolerance_);if(!c.coords){O(c.error||"Merge failed — try clicking on the shared boundary.","error",5e3),this._edgeClickA=null,this._edgeClickB=null,this._phase="click_edge_a",this._edgeSource.clear();return}const d=e.clone();d.setGeometry(new Ye(c.coords)),d.setStyle(hr);const u={type:"beforemerge",original:[e,t],merged:d};this.dispatchEvent(u),r.dispatchEvent({...u}),o!==r&&o.dispatchEvent({...u}),r.removeFeature(e),o.removeFeature(t),r.addFeature(d);const f={type:"aftermerge",original:[e,t],merged:d};this.dispatchEvent(f),r.dispatchEvent({...f}),o!==r&&o.dispatchEvent({...f});const p=e.get("_layerType")==="parcel",h=t.get("_layerType")==="parcel";p&&h?(this.dispatchEvent({type:"mergedparcel",merged:d,propsA:e.getProperties(),propsB:t.getProperties(),coordinate:this._edgeClickA}),O("Polygons merged — choose which identifier to keep.","success")):O("Polygons merged successfully.","success"),this._reset()}_rebuildHighlights(){const e=[];if(this._highlightSource.getFeatures().forEach(t=>{t.get("_permanent")&&e.push(t)}),e.forEach(t=>this._highlightSource.removeFeature(t)),this._featureA){const t=this._featureA.clone();t.set("_highlightStyle",ur),t.set("_permanent",!0),this._highlightSource.addFeature(t)}if(this._featureB){const t=this._featureB.clone();t.set("_highlightStyle",pr),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 mr(n,e){return(n[0]-e[0])**2+(n[1]-e[1])**2}function to(n){let e=0;for(let t=0,r=n.length;t<r-1;t++)e+=n[t][0]*n[t+1][1]-n[t+1][0]*n[t][1];return e/2}function Be(n){let e=Math.abs(to(n[0]));for(let t=1;t<n.length;t++)e-=Math.abs(to(n[t]));return e}function yr(n){const e=n.length-1;let t=-1,r=0;for(let c=0;c<e;c++){const d=mr(n[c],n[c+1]);d>t&&(t=d,r=c)}const o=n[r],s=n[r+1],a=Math.sqrt(t),i=[(s[0]-o[0])/a,(s[1]-o[1])/a],l=[-i[1],i[0]];return{p0:o,p1:s,along:i,perp:l}}function wt(n,e,t,r,o){const s=n[0]+r*e[0],a=n[1]+r*e[1];return[[s-o*t[0],a-o*t[1]],[s+o*t[0],a+o*t[1]]]}function Le(n,e,t){const r=n[0],o=r.length-1;let s=0,a=0;for(let c=0;c<o;c++)s+=r[c][0],a+=r[c][1];const i=s/o-e[0],l=a/o-e[1];return i*t[0]+l*t[1]}function br(n,e,t){if(!Number.isInteger(e)||e<1)return{pieces:null,error:"Number of divisions must be a positive integer."};if(e===1)return{pieces:[n]};const r=n[0];if(Be(n)<1e-6)return{pieces:null,error:"Polygon has no measurable area."};let s,a,i;if(t&&t.length===2){s=t[0];const g=t[1][0]-t[0][0],y=t[1][1]-t[0][1],b=Math.sqrt(g*g+y*y);if(b<1e-10)return{pieces:null,error:"Selected edge has zero length."};a=[g/b,y/b],i=[-a[1],a[0]]}else{const g=yr(r);s=g.p0,a=g.along,i=g.perp}const l=s,c=r.length-1;for(let g=0;g<c;g++){const y=r[g][0]-l[0],b=r[g][1]-l[1];y*a[0]+b*a[1]}let d=1/0,u=-1/0;for(let g=0;g<c;g++){const y=r[g][0]-l[0],b=r[g][1]-l[1],E=y*i[0]+b*i[1];E<d&&(d=E),E>u&&(u=E)}const f=(u-d)*1.5,p=[];let h=n,m=e;for(let g=0;g<e-1;g++){const y=Be(h),b=y/m,E=h[0],S=E.length-1;let L=1/0,w=-1/0;for(let U=0;U<S;U++){const A=E[U][0]-l[0],Q=E[U][1]-l[1],W=A*a[0]+Q*a[1];W<L&&(L=W),W>w&&(w=W)}let _=L,P=w,k=null,$=null,q=1/0;for(let U=0;U<40;U++){const A=(_+P)/2,Q=wt(l,a,i,A,f),W=Ve(h,Q);if(!W){const I=(P-_)*.01,R=wt(l,a,i,A+I,f),Y=Ve(h,R);if(Y){const[ue,pe]=Y,Ae=Le(ue,l,a),De=Le(pe,l,a),Fe=Ae<De?ue:pe,pt=Ae<De?pe:ue,ft=Be(Fe),Oe=Math.abs(ft-b);Oe<q&&(q=Oe,k=Fe,$=pt)}const ut=wt(l,a,i,A-I,f),ae=Ve(h,ut);if(ae){const[ue,pe]=ae,Ae=Le(ue,l,a),De=Le(pe,l,a),Fe=Ae<De?ue:pe,pt=Ae<De?pe:ue,ft=Be(Fe),Oe=Math.abs(ft-b);Oe<q&&(q=Oe,k=Fe,$=pt)}_=A;continue}const[ne,B]=W,re=Le(ne,l,a),Ie=Le(B,l,a),D=re<Ie?ne:B,H=re<Ie?B:ne,j=Be(D),V=Math.abs(j-b);if(V<q&&(q=V,k=D,$=H),V/y<.001)break;j<b?_=A:P=A}if(!k||!$)return{pieces:null,error:`Could not find a valid cut for piece ${g+1} of ${e}. The polygon shape may be too irregular for equal division.`};p.push(k),h=$,m--}return p.push(h),{pieces:p}}const wr=new C({stroke:new T({color:"#0ea5e9",width:3}),fill:new M({color:"rgba(14,165,233,0.15)"})}),vr=new C({stroke:new T({color:"#8b5cf6",width:4,lineDash:[10,6]})});function Er(n){const e=[];for(let t=0;t<n;t++){const r=Math.round(t*360/n);e.push({stroke:`hsl(${r}, 70%, 45%)`,fill:`hsla(${r}, 70%, 55%, 0.25)`})}return e}class xr extends Ct{constructor(e={}){super({handleEvent:t=>this._handleEvent(t)}),this.snapDistance_=e.snapDistance||25,this._sources=e.sources?Array.isArray(e.sources)?e.sources:[e.sources]:null,this._phase="select",this._selectedFeature=null,this._selectedSource=null,this._selectedEdge=null,this._dividedFeatures=null,this._overlaySource=new z({useSpatialIndex:!1}),this._overlayLayer=new N({source:this._overlaySource,displayInLayerSwitcher:!1,style:wr}),this._edgeSource=new z({useSpatialIndex:!1}),this._edgeLayer=new N({source:this._edgeSource,displayInLayerSwitcher:!1,style:vr})}setMap(e){this.getMap()&&(this.getMap().removeLayer(this._overlayLayer),this.getMap().removeLayer(this._edgeLayer)),super.setMap(e),e&&(this._overlayLayer.setMap(e),this._edgeLayer.setMap(e))}setActive(e){super.setActive(e),e||this._reset()}_getSources(){if(this._sources)return this._sources;if(!this.getMap())return[];const e=[],t=r=>{r.forEach(o=>{o.getVisible()&&(o.getSource&&o.getSource()instanceof z?e.push(o.getSource()):o.getLayers&&t(o.getLayers()))})};return t(this.getMap().getLayers()),e}_handleEvent(e){if(!this.getActive())return!0;if(e.type==="keydown"&&e.originalEvent?.key==="Escape")return this._phase==="form"?this.cancelDivide():this._reset(),!1;if(this._phase==="select"){if(e.type==="pointermove")return this._onSelectMove(e);if(e.type==="singleclick")return this._onSelectClick(e)}if(this._phase==="edge"){if(e.type==="pointermove")return this._onEdgeMove(e);if(e.type==="singleclick")return this._onEdgeClick(e)}if(this._phase==="pick"){if(e.type==="pointermove")return this._onPickMove(e);if(e.type==="singleclick")return this._onPickClick(e)}return!0}_onSelectMove(e){const t=this.getMap();if(!t)return!0;this._overlaySource.clear();const r=this._closestPolygon(e);if(r){const o=r.feature.clone();this._overlaySource.addFeature(o),t.getTargetElement().style.cursor="pointer"}else t.getTargetElement().style.cursor="";return!0}_onSelectClick(e){const t=this._closestPolygon(e);if(!t)return!0;this._selectedFeature=t.feature,this._selectedSource=t.source,this._overlaySource.clear();const r=t.feature.clone();return r.set("_permanent",!0),this._overlaySource.addFeature(r),this._phase="edge",O("Click the edge to divide along.","info",3e3),!1}_closestPolygon(e){let t=null,r=this.snapDistance_+1;for(const o of this._getSources()){const s=o.getClosestFeatureToCoordinate(e.coordinate);if(!s)continue;const a=s.getGeometry();if(!a)continue;const i=a.getType();if(i!=="Polygon"&&i!=="MultiPolygon")continue;const l=a.getClosestPoint(e.coordinate),d=new de([e.coordinate,l]).getLength()/e.frameState.viewState.resolution;d<r&&(r=d,t={feature:s,source:o})}return t}_onEdgeMove(e){const t=this.getMap();if(!t)return!0;this._edgeSource.clear();const r=this._closestEdgeSegment(this._selectedFeature,e);if(r){const o=new Me(new de([r.segStart,r.segEnd]));this._edgeSource.addFeature(o),t.getTargetElement().style.cursor="crosshair"}else t.getTargetElement().style.cursor="";return!0}_onEdgeClick(e){const t=this._closestEdgeSegment(this._selectedFeature,e);if(!t)return!0;this._selectedEdge=[t.segStart,t.segEnd],this._edgeSource.clear(),this._phase="form";const o=this._selectedFeature.getGeometry().getExtent(),s=[(o[0]+o[2])/2,(o[1]+o[3])/2];return this.dispatchEvent({type:"divideform",feature:this._selectedFeature,source:this._selectedSource,coordinate:s}),!1}_closestEdgeSegment(e,t){const r=e.getGeometry();let o;if(r.getType()==="Polygon")o=r.getCoordinates()[0];else if(r.getType()==="MultiPolygon")o=r.getCoordinates()[0][0];else return null;const s=t.frameState.viewState.resolution;let a=1/0,i=null;const l=o.length-1;for(let c=0;c<l;c++){const d=o[c],u=o[c+1],f=u[0]-d[0],p=u[1]-d[1],h=f*f+p*p;if(h<1e-20)continue;let m=((t.coordinate[0]-d[0])*f+(t.coordinate[1]-d[1])*p)/h;m=Math.max(0,Math.min(1,m));const g=d[0]+m*f,y=d[1]+m*p,b=Math.sqrt((t.coordinate[0]-g)**2+(t.coordinate[1]-y)**2)/s;b<a&&(a=b,i={segStart:d,segEnd:u})}return a<=this.snapDistance_?i:null}performDivide(e){if(this._phase!=="form"||!this._selectedFeature)return;const t=this._selectedFeature,r=this._selectedSource,o=t.getGeometry();let s;o.getType()==="Polygon"?s=o.getCoordinates():o.getType()==="MultiPolygon"&&(s=o.getCoordinates()[0]);const a=br(s,e,this._selectedEdge);if(!a.pieces){O(a.error||"Division failed.","error",5e3),this._reset();return}const i=Er(e),l=a.pieces.map((f,p)=>{const h=t.clone();return h.setGeometry(new Ye(f)),h.setStyle(new C({stroke:new T({color:i[p].stroke,width:2.5}),fill:new M({color:i[p].fill})})),h}),c={type:"beforedivide",original:t,features:l};this.dispatchEvent(c),r.dispatchEvent({...c}),r.removeFeature(t);for(const f of l)r.addFeature(f);const d={type:"afterdivide",original:t,features:l};this.dispatchEvent(d),r.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:r})):(O(`Polygon divided into ${e} equal pieces.`,"success"),this._reset())}_onPickMove(e){const t=this.getMap();if(!t)return!0;this._overlaySource.clear();const r=this._closestDividedPiece(e);if(r){const o=r.clone();this._overlaySource.addFeature(o),t.getTargetElement().style.cursor="pointer"}else t.getTargetElement().style.cursor="";return!0}_onPickClick(e){const t=this._closestDividedPiece(e);return t?(this.dispatchEvent({type:"dividepick",picked:t,features:this._dividedFeatures}),this._reset(),!1):!0}_closestDividedPiece(e){if(!this._dividedFeatures)return null;let t=null,r=this.snapDistance_+1;for(const o of this._dividedFeatures){const s=o.getGeometry();if(!s)continue;const a=s.getClosestPoint(e.coordinate),l=new de([e.coordinate,a]).getLength()/e.frameState.viewState.resolution;l<r&&(r=l,t=o)}return t}cancelDivide(){this.dispatchEvent({type:"dividecancel"}),this._reset()}_reset(){this._phase="select",this._selectedFeature=null,this._selectedSource=null,this._selectedEdge=null,this._dividedFeatures=null,this._overlaySource.clear(),this._edgeSource.clear();const e=this.getMap();e&&(e.getTargetElement().style.cursor="")}}class _r{constructor(e,t={}){this.options=t,this.markerSource=new z,this.clickCallbacks=[],this.categoryEmojis={default:{emoji:"📍",label:"Default"},water:{emoji:"💧",label:"Water Point"},school:{emoji:"🏫",label:"School"},health:{emoji:"🏥",label:"Health Facility"},market:{emoji:"🏪",label:"Market"},other:{emoji:"📌",label:"Other"}},this.getEmoji=i=>{const l=this.categoryEmojis[i];return l?l.emoji:"📍"},this.getCategoryOptionsHtml=()=>Object.entries(this.categoryEmojis).map(([i,{emoji:l,label:c}])=>`<option value="${i}">${l} ${c}</option>`).join(`
`),this.createEmojiStyle=(i,l=24)=>new C({text:new Je({text:i,font:`${l}px sans-serif`,textBaseline:"bottom",textAlign:"center",offsetY:-5})}),this.defaultStyle=this.createEmojiStyle("📍",32),this.selectedStyle=this.createEmojiStyle("📍",42),this.categoryStyles={};for(const[i,{emoji:l}]of Object.entries(this.categoryEmojis))this.categoryStyles[i]=this.createEmojiStyle(l,32);const r=this.createBaseLayers(t.basemap||"topo");this.markersLayer=new N({title:"Markers",source:this.markerSource,style:i=>this.getFeatureStyle(i)}),this.overlayGroup=new fe({title:"Overlays"}),this.map=new Rt({target:e,layers:[r,this.markersLayer,this.overlayGroup],view:new Yo({center:he(t.center||[0,0]),zoom:t.zoom||2,minZoom:t.minZoom||2,maxZoom:t.maxZoom||19})});const o=new nn({collapsed:!0,mouseover:!0,extent:!0,trash:!1,oninfo:null});this.map.addControl(o),o.on("drawlist",i=>{if((i.layer.get("title")||"").toLowerCase().includes("external")){this._externalSourceGroup=i.layer;const c=i.li.querySelector(".ol-layerswitcher-buttons");if(c&&!c.querySelector(".ol-add-layer")){const d=document.createElement("span");d.className="ol-add-layer",d.title="Add external layer",d.textContent="+",d.style.cssText=`
display:inline-flex !important;align-items:center;justify-content:center;
width:20px !important;height:20px !important;border-radius:50%;
background:#10b981 !important;color:#fff !important;
font-size:16px !important;font-weight:700;
cursor:pointer;line-height:1 !important;
margin:2px 4px 2px 2px;vertical-align:middle;
transition:background 0.2s;box-sizing:border-box;
`,d.addEventListener("mouseenter",()=>{d.style.background="#059669"}),d.addEventListener("mouseleave",()=>{d.style.background="#10b981"}),d.addEventListener("click",u=>{u.stopPropagation(),this.showAddLayerDialog()}),c.prepend(d)}}}),this._createAddLayerDialog(),this._createLegendPanel(),this.scaleBar=new Jo({bar:!0,steps:4,text:!0,minWidth:140}),this.map.addControl(this.scaleBar);const s=new rn({title:"My Location",delay:3e3,zoom:16});this.map.addControl(s),this.geolocationButton=s;const a=new an({placeholder:"Search location...",typing:300,minLength:3,maxItems:10,collapsed:!0});this.map.addControl(a),a.on("select",i=>{const l=i.search;if(l){const c=parseFloat(l.lon),d=parseFloat(l.lat),u=[c,d],f=he(u);this.navigateTo(c,d,14);const p={coordinate:f,lonLat:u,name:l.display_name||l.name||"Unknown",searchResult:l};this.searchSelectCallbacks.forEach(h=>h(p))}}),this.searchNominatim=a,this.searchSelectCallbacks=[],this.selectedFeature=null,this.createPopup(),this.createInfoPopup(),this.createAddLocationPopup(),this.createParcelEditPopup(),this.createDrawnPolygonPopup(),this.createMergePopup(),this.createDividePopup(),this.dblClickCallbacks=[],this.editBar=null,this.drawingsSource=null,this.drawingsLayer=null,this.touchCursor=null,this._editBarActive=!1}initEditBar(){this.drawingsSource=new z,this.drawingsLayer=new N({title:"sketches",source:this.drawingsSource,style:new C({stroke:new T({color:"#f59e0b",width:2.5}),fill:new M({color:"rgba(245,158,11,0.15)"}),image:new ce({radius:6,fill:new M({color:"#f59e0b"}),stroke:new T({color:"#fff",width:1.5})})})}),this._drawingsGroup=new fe({title:"Drawings",layers:[this.drawingsLayer]});const e=this.map.getLayers(),t=e.getLength()-1;e.insertAt(t,this._drawingsGroup),this._selectInteraction=new Xo({condition:Zo,filter:(p,h)=>!!h,layers:p=>p instanceof N}),this._selectInteraction.setActive(!1),this.map.addInteraction(this._selectInteraction),this._modifyInteraction=new sn({features:this._selectInteraction.getFeatures()}),this._modifyInteraction.setActive(!1),this._undoRedo=new ln,this.map.addInteraction(this._undoRedo),this.editBar=new _t({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 r=new jt({group:!0,controls:[new Re({html:'<i class="bi bi-arrow-counterclockwise"></i>',className:"ol-undo",title:"Undo",handleClick:()=>{this._undoRedo.hasUndo()&&this._undoRedo.undo()}}),new Re({html:'<i class="bi bi-arrow-clockwise"></i>',className:"ol-redo",title:"Redo",handleClick:()=>{this._undoRedo.hasRedo()&&this._undoRedo.redo()}}),new Re({html:'<i class="bi bi-floppy"></i>',className:"ol-save",title:"Save drawings",handleClick:()=>{this.dispatchEditEvent("save")}})]});this.editBar.addControl(r),this._lineSplitInteraction=new cn,this._polygonSplitInteraction=new rr,this.map.addInteraction(this._lineSplitInteraction),this.map.addInteraction(this._polygonSplitInteraction),this._lineSplitInteraction.setActive(!1),this._polygonSplitInteraction.setActive(!1),this._polygonSplitInteraction.on("splitpick",p=>{const h=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"];for(const m of p.features)if(m!==p.picked)for(const g of h)m.get(g)!==void 0&&m.set(g,"")}),this._polygonDivideInteraction=new xr,this.map.addInteraction(this._polygonDivideInteraction),this._polygonDivideInteraction.setActive(!1);const o=new le({html:'<i class="bi bi-slash-lg"></i>',className:"ol-split-line",title:"Split Lines",name:"SplitLine",interaction:this._lineSplitInteraction,autoActivate:!0}),s=new le({html:'<i class="bi bi-scissors"></i>',className:"ol-split-polygon",title:"Split Polygons",name:"SplitPolygon",interaction:this._polygonSplitInteraction}),a=new le({html:'<i class="bi bi-grid-3x3-gap"></i>',className:"ol-split-divide",title:"Divide Polygon",name:"DividePolygon",interaction:this._polygonDivideInteraction}),i=new jt({toggleOne:!0,autoDeactivate:!0,controls:[o,s,a]}),l=new le({className:"ol-split",title:"Split",name:"Split",bar:i,onToggle:p=>{p||(this._lineSplitInteraction.setActive(!1),this._polygonSplitInteraction.setActive(!1),this._polygonDivideInteraction.setActive(!1))}});this.editBar.addControl(l),this._polygonDivideInteraction.on("divideform",p=>{this.showDividePopup(p.feature,p.source,p.coordinate)}),this._polygonDivideInteraction.on("dividecancel",()=>{this.hideDividePopup()}),this._polygonDivideInteraction.on("dividepick",p=>{const h=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"];for(const m of p.features)if(m!==p.picked)for(const g of h)m.get(g)!==void 0&&m.set(g,"")}),this._polygonMergeInteraction=new gr,this.map.addInteraction(this._polygonMergeInteraction),this._polygonMergeInteraction.setActive(!1);const c=new le({html:'<i class="bi bi-union"></i>',className:"ol-merge",title:"Merge Polygons",name:"Merge",interaction:this._polygonMergeInteraction});this.editBar.addControl(c),this._polygonMergeInteraction.on("mergedparcel",p=>{this.showMergeIdentifierPopup(p.merged,p.propsA,p.propsB,p.coordinate)}),this._snapGuidesEnabled=localStorage.getItem("snap-guides-enabled")==="1",this._snapGuides=new dn({pixelTolerance:10,vectorClass:Qo}),this.map.addInteraction(this._snapGuides);const d=["DrawPoint","DrawLine","DrawPolygon","DrawHole","DrawRegular"];for(const p of d){const h=this.editBar.getInteraction(p);h&&h.on("change:active",()=>{h.getActive()&&this._snapGuides.setDrawInteraction(h)})}this._modifyInteraction&&this._snapGuides.setModifyInteraction(this._modifyInteraction);const u=new Re({html:'<i class="bi bi-magnet"></i>',className:"ol-snap-toggle"+(this._snapGuidesEnabled?" ol-active":""),title:"Toggle Snap Guides",handleClick:()=>{this._snapGuidesEnabled=!this._snapGuidesEnabled,localStorage.setItem("snap-guides-enabled",this._snapGuidesEnabled?"1":"0"),u.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=u,r.addControl(u),this.setEditMode(!1),this._drawingsGroup.on("change:visible",()=>{const p=this._drawingsGroup.getVisible();this.setEditMode(p)}),("ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0)&&(this.touchCursor=new un({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",p=>{const h=p.feature,m=h.getGeometry();if(!m||m.getType()!=="Polygon")return;const g=m.getInteriorPoint().getCoordinates();this.showDrawnPolygonPopup(h,g)}),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(r=>r())}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 z,this._vertexOverlayLayer=new N({title:"__vertex_highlight__",source:this._vertexOverlaySource,zIndex:990,style:new C({image:new ce({radius:4,fill:new M({color:"rgba(14,165,233,0.85)"}),stroke:new T({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 r=t.getGeometry();if(!r)continue;const o=r.getType();if(!["Polygon","MultiPolygon","LineString","MultiLineString"].includes(o))continue;const s=this._collectAllVertices(r);for(const a of s)this._vertexOverlaySource.addFeature(new Me(new ht(a)));t.on("change",this._onSelectedFeatureGeomChange),this._vertexTrackedFeatures.add(t)}}_collectAllVertices(e){const t=[],r=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<c;d++)t.push(i[d])},s=e.getType(),a=e.getCoordinates();switch(s){case"Polygon":for(const l of a)o(l,!0);break;case"MultiPolygon":for(const l of a)for(const c of l)o(c,!0);break;case"LineString":o(a,!1);break;case"MultiLineString":for(const l of a)o(l,!1);break;default:const i=l=>{if(r(l))t.push(l);else if(Array.isArray(l))for(const c of l)i(c)};i(a)}return t}getDrawingsLayer(){return this.drawingsLayer}getDrawingsSource(){return this.drawingsSource}getEditBar(){return this.editBar}setScaleBarUnits(e){this.scaleBar&&this.scaleBar.setUnits(e==="imperial"?"imperial":"metric")}createPopup(){this.popupElement=document.createElement("div"),this.popupElement.className="map-popup",this.popupElement.style.cssText=`
position: absolute;
background: var(--card, #fff);
color: var(--card-foreground, #1e1a4b);
border-radius: 8px;
padding: 10px 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
font-family: var(--font-body, 'Exo', sans-serif);
font-size: 13px;
min-width: 150px;
max-width: 280px;
pointer-events: none;
z-index: 1000;
border: 1px solid var(--border, #1e1a4b1f);
`,this.popup=new ie({element:this.popupElement,positioning:"bottom-center",offset:[0,-15],stopEvent:!1}),this.map.addOverlay(this.popup),this.setupHoverPopup()}setupHoverPopup(){let e=null;this.map.on("pointermove",t=>{if(t.dragging){this.hidePopup();return}const r=this.map.forEachFeatureAtPixel(t.pixel,o=>o.get("name")?o:null);r&&r!==e?(e=r,this.showPopup(r,t.coordinate)):!r&&e&&(e=null,this.hidePopup()),this.map.getTargetElement().style.cursor=r?"pointer":""}),this.map.getTargetElement().addEventListener("mouseleave",()=>{this.hidePopup(),e=null})}showPopup(e,t){const r=e.get("name")||"Unnamed",o=e.get("category")||"default",s=e.get("description"),a=e.get("lon"),i=e.get("lat");let c=`
<div style="font-weight: 600; font-size: 14px; margin-bottom: 6px;">
${this.getEmoji(o)} ${this.escapeHtml(r)}
</div>
`;const u={water:"#3b82f6",school:"#f59e0b",health:"#ef4444",market:"#8b5cf6",default:"#2d5016",other:"#6b7280"}[o]||"#6b7280";c+=`
<div style="margin-bottom: 6px;">
<span style="
background: ${u}20;
color: ${u};
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
">${o}</span>
</div>
`,s&&(c+=`
<div style="color: var(--muted-foreground, #7a7a7a); font-size: 12px; margin-bottom: 6px; line-height: 1.4;">
${this.escapeHtml(s)}
</div>
`),a!==void 0&&i!==void 0&&(c+=`
<div style="color: var(--muted-foreground, #7a7a7a); font-size: 11px; font-family: monospace;">
${Number(a).toFixed(5)}, ${Number(i).toFixed(5)}
</div>
`),this.popupElement.innerHTML=c,this.popup.setPosition(t)}hidePopup(){this.popup.setPosition(void 0)}createInfoPopup(){this.infoPopupElement=document.createElement("div"),this.infoPopupElement.className="map-info-popup",this.infoPopupElement.style.cssText=`
position: absolute;
background: var(--card, #fff);
color: var(--card-foreground, #1e1a4b);
border-radius: 10px;
padding: 0;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
font-family: var(--font-body, 'Exo', sans-serif);
font-size: 13px;
min-width: 220px;
max-width: 320px;
max-height: 70vh;
display: flex;
flex-direction: column;
z-index: 1001;
border: 1px solid var(--border, #1e1a4b1f);
overflow: hidden;
`,this.infoPopup=new ie({element:this.infoPopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.infoPopup)}showInfoPopup(e,t,r={}){const{title:o="Feature Info",color:s="#e11d48"}=r,a=e.getProperties(),i=e.getGeometry(),l=i.getType(),c=["geometry","_layerType"];let d="";for(const[f,p]of Object.entries(a))c.includes(f)||p===void 0||p===null||(d+=`
<tr>
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">${this.escapeHtml(f)}</td>
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${this.escapeHtml(String(p))}</td>
</tr>
`);if(l==="Polygon"||l==="MultiPolygon"){const f=$e(i,{projection:"EPSG:3857"}),p=Vn(f);d+=`
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">area</td>
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${p}</td>
</tr>
`}else if(l==="LineString"||l==="MultiLineString"){const f=Xe(i,{projection:"EPSG:3857"}),p=Kn(f);d+=`
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">length</td>
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${p}</td>
</tr>
`}else if(l==="Point"){const f=xe(i.getCoordinates()),p=f[0].toFixed(6),h=f[1].toFixed(6);d+=`
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">longitude</td>
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${p}</td>
</tr>
<tr>
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">latitude</td>
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${h}</td>
</tr>
`}const u=`
<div style="background:${s};color:#fff;padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;border-radius:10px 10px 0 0;">
<span>${this.escapeHtml(o)}</span>
<button id="info-popup-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">&times;</button>
</div>
<div style="padding:8px 4px;overflow-y:auto;flex:1 1 auto;min-height:0;">
<table style="width:100%;border-collapse:collapse;font-size:13px;">
${d}
</table>
</div>
`;this.infoPopupElement.innerHTML=u,this.infoPopup.setPosition(t),this.infoPopupElement.querySelector("#info-popup-close").addEventListener("click",()=>{this.hideInfoPopup()})}hideInfoPopup(){this.infoPopup.setPosition(void 0)}_collectIntersectionRows(e,t,r){const o=[];if(e.length>0&&o.push({label:"Parcels",value:String(e.length),color:"#0ea5e9"}),t.length>0){const s=t.map(a=>a.get("colzonename")||a.get("zone_name")||a.get("name")||"unnamed");o.push({label:"Zones",value:String(t.length),color:"#7c3aed"}),o.push({label:"Zone Names",value:s.map(a=>this.escapeHtml(a)).join(", "),color:"#7c3aed"})}for(const[s,a]of Object.entries(r))o.push({label:this.escapeHtml(s),value:`${a.length} feature(s)`});return o.length===0&&o.push({label:"",value:"No intersecting features found",empty:!0}),o}_buildAnalysisPopupHtml(e,t,r){let o="";for(const s of r){if(s.empty){o+=`
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
<td colspan="2" style="padding:8px;color:#999;text-align:center;font-style:italic;">${s.value}</td>
</tr>`;continue}const a=s.color||"var(--muted-foreground, #7a7a7a)",i=s._first?"":"border-top:1px solid var(--border, #1e1a4b1f);";o+=`
<tr style="${i}">
<td style="padding:4px 8px;font-weight:600;color:${a};white-space:nowrap;">${s.label}</td>
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${s.value}</td>
</tr>`}return`
<div style="background:var(--brand-navy, #1e1a4b);color:#fff;padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;border-radius:10px 10px 0 0;">
<span>${e} ${t}</span>
<button id="info-popup-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">&times;</button>
</div>
<div style="padding:8px 4px;overflow-y:auto;flex:1 1 auto;min-height:0;">
<table style="width:100%;border-collapse:collapse;font-size:13px;">
${o}
</table>
</div>
<div style="padding:2px 8px 8px;text-align:right;flex-shrink:0;border-top:1px solid var(--border, #1e1a4b1f);">
<button id="info-popup-export-pdf"
style="background:var(--brand-navy,#1e1a4b);color:#fff;border:none;border-radius:6px;padding:5px 12px;font-size:12px;cursor:pointer;font-family:inherit;">
📄 Export PDF
</button>
</div>`}_showAnalysisPopup(e,t,r,o){this.infoPopupElement.innerHTML=this._buildAnalysisPopupHtml(e,t,r),this.infoPopup.setPosition(o),this.infoPopupElement.querySelector("#info-popup-close").addEventListener("click",()=>{this.hideInfoPopup()}),this.infoPopupElement.querySelector("#info-popup-export-pdf")?.addEventListener("click",()=>{const s=r.filter(a=>!a.empty).map(a=>({label:a.label,value:a.value.replace(/<[^>]*>/g,"")}));at(async()=>{const{exportAnalysisPDF:a}=await import("./pdf-export-Vpiz8VA4.js");return{exportAnalysisPDF:a}},__vite__mapDeps([0,1,2,3])).then(({exportAnalysisPDF:a})=>{a({title:t,rows:s})}).catch(a=>{console.error("[MapView] PDF export failed:",a)})})}showCircleIntersectionPopup(e,t){const r=e.getGeometry();if(!r||typeof r.getCenter!="function")return;const o=en(r,64),s=o.getExtent(),a=e.get("_radius")||r.getRadius(),i=[],l=[],c={},d=g=>{const y=g.getGeometry();if(!y)return!1;const b=y.getExtent();return b[2]<s[0]||b[0]>s[2]||b[3]<s[1]||b[1]>s[3]?!1:o.intersectsExtent(b)&&this._geometriesIntersect(o,y)},u=(g,y)=>{g.getLayers().forEach(b=>{if(b instanceof fe)u(b,b.get("title")||y);else if(b instanceof N&&b.getVisible()){const E=b.get("title")||y||"Unknown",S=b.getSource();if(!S)return;const L=S.getFeaturesInExtent(s);for(const w of L){const _=w.get("_layerType");_==="measure_circle"||_==="measure_circle_radius"||d(w)&&(_==="parcel"?i.push(w):_==="collector_zone"?l.push(w):(c[E]||(c[E]=[]),c[E].push(w)))}}})};u(this.overlayGroup,"Overlays");const f=nt(a),p=Math.PI*a*a,h=je(p),m=[{label:"Radius",value:f,_first:!0},{label:"Area",value:h},...this._collectIntersectionRows(i,l,c)];this._showAnalysisPopup("⭕","Circle Analysis",m,t)}showAreaIntersectionPopup(e,t){const r=e.getGeometry();if(!r)return;const o=r.getExtent(),s=$e(r,{projection:"EPSG:3857"}),a=je(s),i=Xe(r,{projection:"EPSG:3857"}),l=nt(i),c=[],d=[],u={},f=m=>{const g=m.getGeometry();if(!g)return!1;const y=g.getExtent();return y[2]<o[0]||y[0]>o[2]||y[3]<o[1]||y[1]>o[3]?!1:r.intersectsExtent(y)&&this._geometriesIntersect(r,g)},p=(m,g)=>{m.getLayers().forEach(y=>{if(y instanceof fe)p(y,y.get("title")||g);else if(y instanceof N&&y.getVisible()){const b=y.get("title")||g||"Unknown",E=y.getSource();if(!E)return;const S=E.getFeaturesInExtent(o);for(const L of S){const w=L.get("_layerType");w==="measure_area"||w==="measure_circle"||w==="measure_circle_radius"||f(L)&&(w==="parcel"?c.push(L):w==="collector_zone"?d.push(L):(u[b]||(u[b]=[]),u[b].push(L)))}}})};p(this.overlayGroup,"Overlays");const h=[{label:"Area",value:a,_first:!0},{label:"Perimeter",value:l},...this._collectIntersectionRows(c,d,u)];this._showAnalysisPopup("📐","Area Analysis",h,t)}_geometriesIntersect(e,t){const r=t.getType();if(r==="Polygon"||r==="MultiPolygon"){const o=t.getFlatCoordinates(),s=t.getStride();for(let l=0;l<o.length;l+=s)if(e.intersectsCoordinate([o[l],o[l+1]]))return!0;const a=e.getFlatCoordinates(),i=e.getStride();for(let l=0;l<a.length;l+=i)if(t.intersectsCoordinate([a[l],a[l+1]]))return!0;return!1}if(r==="Point")return e.intersectsCoordinate(t.getCoordinates());if(r==="LineString"||r==="MultiLineString"){const o=t.getFlatCoordinates(),s=t.getStride();for(let a=0;a<o.length;a+=s)if(e.intersectsCoordinate([o[a],o[a+1]]))return!0;return!1}return!0}createParcelEditPopup(){this.parcelEditElement=document.createElement("div"),this.parcelEditElement.className="map-parcel-edit-popup",this.parcelEditElement.style.cssText=`
position: absolute;
background: var(--card, #fff);
color: var(--card-foreground, #1e1a4b);
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
font-family: var(--font-body, 'Exo', sans-serif);
font-size: 13px;
min-width: 280px;
max-width: 360px;
max-height: 420px;
z-index: 1002;
border: 2px solid var(--primary, #005eb8);
overflow: hidden;
display: flex;
flex-direction: column;
`,this.parcelEditPopup=new ie({element:this.parcelEditElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.parcelEditPopup),this._parcelEditCallbacks=[],this._parcelEditFeature=null}showParcelEditPopup(e,t){this._parcelEditFeature=e;const r=e.getProperties(),o=["geometry","_layerType"];let s="";for(const[l,c]of Object.entries(r)){if(o.includes(l))continue;const d=c==null?"":String(c),u=this.escapeHtml(l),f=this.escapeHtml(d);s+=`
<div style="margin-bottom:8px;">
<label style="display:block;font-size:11px;font-weight:600;color:var(--muted-foreground, #7a7a7a);margin-bottom:2px;">${u}</label>
<input type="text" name="${u}" value="${f}"
style="width:100%;padding:6px 8px;border:1px solid var(--border, #1e1a4b1f);border-radius:4px;font-size:13px;color:var(--foreground, #1e1a4b);background:var(--muted, #f2f4f7);min-height:34px;"
/>
</div>
`}const a=`
<div style="background:var(--primary, #005eb8);color:#fff;padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<span>✏️ Edit Parcel</span>
<button class="parcel-edit-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">&times;</button>
</div>
<form class="parcel-edit-form" style="padding:10px 12px;overflow-y:auto;flex:1;">
${s}
<div style="display:flex;gap:8px;margin-top:10px;">
<button type="submit" style="flex:1;padding:8px 12px;background:var(--primary, #005eb8);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
💾 Save
</button>
<button type="button" class="parcel-edit-cancel" style="flex:1;padding:8px 12px;background:var(--muted-foreground, #7a7a7a);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
Cancel
</button>
</div>
</form>
`;this.parcelEditElement.innerHTML=a,this.parcelEditPopup.setPosition(t),this.parcelEditElement.querySelector(".parcel-edit-close").addEventListener("click",()=>{this.hideParcelEditPopup()}),this.parcelEditElement.querySelector(".parcel-edit-cancel").addEventListener("click",()=>{this.hideParcelEditPopup()});const i=this.parcelEditElement.querySelector(".parcel-edit-form");i.addEventListener("submit",l=>{l.preventDefault();const c=new FormData(i),d={};for(const[u,f]of c.entries())d[u]=f;d._layerType="parcel";for(const[u,f]of Object.entries(d))this._parcelEditFeature.set(u,f);for(const u of this._parcelEditCallbacks)u(this._parcelEditFeature,d);this.hideParcelEditPopup()})}hideParcelEditPopup(){this.parcelEditPopup.setPosition(void 0),this._parcelEditFeature=null}onParcelEdit(e){this._parcelEditCallbacks.push(e)}createMergePopup(){this.mergePopupElement=document.createElement("div"),this.mergePopupElement.className="map-merge-popup",this.mergePopupElement.style.cssText=`
position: absolute;
background: var(--card, #fff);
color: var(--card-foreground, #1e1a4b);
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
font-family: var(--font-body, 'Exo', sans-serif);
font-size: 13px;
min-width: 280px;
max-width: 360px;
z-index: 1002;
border: 2px solid #10b981;
overflow: hidden;
display: flex;
flex-direction: column;
`,this.mergePopup=new ie({element:this.mergePopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.mergePopup)}showMergeIdentifierPopup(e,t,r,o){const s=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"],a=h=>{for(const m of s)if(h[m]!==void 0&&h[m]!==null&&String(h[m]).trim())return{field:m,value:String(h[m])};return{field:"id",value:"Unknown"}},i=a(t),l=a(r),c=`
<div style="background:#10b981;color:#fff;padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<span>🔗 Merged Parcel — Choose Identifier</span>
<button class="merge-popup-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">&times;</button>
</div>
<div style="padding:12px;">
<p style="margin:0 0 10px;color:var(--muted-foreground, #7a7a7a);font-size:12px;">
Select which parcel's attributes the merged polygon should keep:
</p>
<label style="display:flex;align-items:center;padding:10px;border:2px solid var(--border, #1e1a4b1f);border-radius:8px;cursor:pointer;margin-bottom:8px;transition:border-color 0.15s;">
<input type="radio" name="merge-choice" value="A" checked
style="margin-right:10px;accent-color:#0ea5e9;width:16px;height:16px;" />
<div>
<div style="font-weight:600;color:#0ea5e9;">Parcel A</div>
<div style="font-size:12px;color:var(--muted-foreground, #7a7a7a);">${this.escapeHtml(i.field)}: ${this.escapeHtml(i.value)}</div>
</div>
</label>
<label style="display:flex;align-items:center;padding:10px;border:2px solid var(--border, #1e1a4b1f);border-radius:8px;cursor:pointer;margin-bottom:12px;transition:border-color 0.15s;">
<input type="radio" name="merge-choice" value="B"
style="margin-right:10px;accent-color:#f59e0b;width:16px;height:16px;" />
<div>
<div style="font-weight:600;color:#f59e0b;">Parcel B</div>
<div style="font-size:12px;color:var(--muted-foreground, #7a7a7a);">${this.escapeHtml(l.field)}: ${this.escapeHtml(l.value)}</div>
</div>
</label>
<div style="display:flex;gap:8px;">
<button class="merge-popup-confirm" style="flex:1;padding:8px 12px;background:#10b981;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
✅ Confirm
</button>
<button class="merge-popup-cancel" style="flex:1;padding:8px 12px;background:var(--muted-foreground, #7a7a7a);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
Cancel
</button>
</div>
</div>
`;this.mergePopupElement.innerHTML=c,this.mergePopup.setPosition(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:r,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"),f=this.mergePopupElement.querySelectorAll('input[name="merge-choice"]'),p=()=>{u.forEach(h=>{const m=h.querySelector("input");h.style.borderColor=m.checked?m.value==="A"?"#0ea5e9":"#f59e0b":"var(--border, #1e1a4b1f)"})};f.forEach(h=>h.addEventListener("change",p)),p()}createDividePopup(){this.dividePopupElement=document.createElement("div"),this.dividePopupElement.className="map-divide-popup",this.dividePopupElement.style.cssText=`
position: absolute;
background: var(--card, #fff);
color: var(--card-foreground, #1e1a4b);
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
font-family: var(--font-body, 'Exo', sans-serif);
font-size: 13px;
min-width: 260px;
max-width: 320px;
z-index: 1002;
border: 2px solid #8b5cf6;
overflow: hidden;
display: flex;
flex-direction: column;
`,this.dividePopup=new ie({element:this.dividePopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.dividePopup)}showDividePopup(e,t,r){const o=`
<div style="background:#8b5cf6;color:#fff;padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<span>Divide Polygon</span>
<button class="divide-popup-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">&times;</button>
</div>
<div style="padding:12px;">
<p style="margin:0 0 10px;color:var(--muted-foreground, #7a7a7a);font-size:12px;">
Enter the number of equal pieces:
</p>
<input type="number" class="divide-input" min="2" max="50" value="2"
style="width:100%;padding:8px 10px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:16px;font-weight:600;text-align:center;color:var(--foreground, #1e1a4b);background:var(--muted, #f2f4f7);min-height:40px;" />
<div style="display:flex;gap:8px;margin-top:12px;">
<button class="divide-popup-confirm" style="flex:1;padding:8px 12px;background:#8b5cf6;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
Divide
</button>
<button class="divide-popup-cancel" style="flex:1;padding:8px 12px;background:var(--muted-foreground, #7a7a7a);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
Cancel
</button>
</div>
</div>
`;this.dividePopupElement.innerHTML=o,this.dividePopup.setPosition(r);const s=this.dividePopupElement.querySelector(".divide-input");s.focus(),s.select();const a=()=>{this.hideDividePopup(),this._polygonDivideInteraction.cancelDivide()};this.dividePopupElement.querySelector(".divide-popup-close").addEventListener("click",a),this.dividePopupElement.querySelector(".divide-popup-cancel").addEventListener("click",a),this.dividePopupElement.querySelector(".divide-popup-confirm").addEventListener("click",()=>{const i=parseInt(s.value,10);if(!i||i<2){s.style.borderColor="#ef4444";return}this.hideDividePopup(),this._polygonDivideInteraction.performDivide(i)}),s.addEventListener("keydown",i=>{i.key==="Enter"&&(i.preventDefault(),this.dividePopupElement.querySelector(".divide-popup-confirm").click())})}hideDividePopup(){this.dividePopup.setPosition(void 0)}createDrawnPolygonPopup(){this.drawnPolygonElement=document.createElement("div"),this.drawnPolygonElement.className="map-drawn-polygon-popup",this.drawnPolygonElement.style.cssText=`
position: absolute;
background: var(--card, #fff);
border-radius: var(--radius-xl, 0.75rem);
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
font-family: var(--font-body, 'Exo', sans-serif);
font-size: 13px;
min-width: 280px;
max-width: 360px;
max-height: 420px;
z-index: 1002;
border: 2px solid var(--success, #006b3f);
overflow: hidden;
display: flex;
flex-direction: column;
`,this.drawnPolygonPopup=new ie({element:this.drawnPolygonElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.drawnPolygonPopup),this._drawnPolygonCallbacks=[],this._drawnPolygonFeature=null}getParcelAttributeKeys(){const e=["geometry","_layerType"],t=[],r=o=>{t.length>0||o.getLayers().forEach(s=>{if(!(t.length>0)){if(s instanceof fe)r(s);else if(s instanceof N){const a=s.getSource();if(!a)return;for(const i of a.getFeatures()){if(i.get("_layerType")!=="parcel")continue;const l=i.getProperties();for(const c of Object.keys(l))e.includes(c)||t.push(c);return}}}})};return r(this.overlayGroup),t}showDrawnPolygonPopup(e,t){this._drawnPolygonFeature=e;const r=this.getParcelAttributeKeys();if(r.length===0){console.warn("[MapView] No parcel attributes found — cannot build form");return}let o="";for(const d of r){const u=this.escapeHtml(d);o+=`
<div style="margin-bottom:8px;">
<label style="display:block;font-size:11px;font-weight:600;color:var(--muted-foreground, #7a7a7a);margin-bottom:2px;">${u}</label>
<input type="text" name="${u}" value=""
style="width:100%;padding:6px 8px;border:1px solid var(--border, #1e1a4b1f);border-radius:4px;font-size:13px;color:var(--foreground, #1e1a4b);background:var(--muted, #f2f4f7);min-height:34px;"
/>
</div>
`}const s=e.getGeometry(),a=$e(s,{projection:"EPSG:3857"}),l=`
<div style="background:var(--success, #006b3f);color:var(--success-foreground, #fff);padding:8px 12px;font-weight:600;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;">
<span>📐 Polygon Attributes</span>
<button class="drawn-polygon-close" style="background:none;border:none;color:var(--success-foreground, #fff);font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">&times;</button>
</div>
<div style="padding:8px 12px;background:var(--muted, #f2f4f7);border-bottom:1px solid var(--border, #1e1a4b1f);font-size:12px;color:var(--muted-foreground, #7a7a7a);flex-shrink:0;">
Area: <strong>${je(a)}</strong>
</div>
<form class="drawn-polygon-form" style="padding:10px 12px;overflow-y:auto;flex:1;">
${o}
<div style="display:flex;gap:8px;margin-top:10px;">
<button type="submit" style="flex:1;padding:8px 12px;background:var(--success, #006b3f);color:var(--success-foreground, #fff);border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
💾 Save
</button>
<button type="button" class="drawn-polygon-cancel" style="flex:1;padding:8px 12px;background:var(--muted-foreground, #7a7a7a);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
Cancel
</button>
</div>
</form>
`;this.drawnPolygonElement.innerHTML=l,this.drawnPolygonPopup.setPosition(t),this.drawnPolygonElement.querySelector(".drawn-polygon-close").addEventListener("click",()=>{this.hideDrawnPolygonPopup()}),this.drawnPolygonElement.querySelector(".drawn-polygon-cancel").addEventListener("click",()=>{this.hideDrawnPolygonPopup()});const c=this.drawnPolygonElement.querySelector(".drawn-polygon-form");c.addEventListener("submit",d=>{d.preventDefault();const u=new FormData(c),f={};for(const[p,h]of u.entries())f[p]=h;for(const[p,h]of Object.entries(f))this._drawnPolygonFeature.set(p,h);this._drawnPolygonFeature.set("_layerType","parcel");for(const p of this._drawnPolygonCallbacks)p(this._drawnPolygonFeature,f);this.hideDrawnPolygonPopup()})}hideDrawnPolygonPopup(){this.drawnPolygonPopup.setPosition(void 0),this._drawnPolygonFeature=null}onDrawnPolygonSave(e){this._drawnPolygonCallbacks.push(e)}onDblClick(e){return this.dblClickCallbacks.push(e),this.dblClickCallbacks.length===1&&this.map.on("dblclick",t=>{const[r,o]=xe(t.coordinate);let s=null;this.map.forEachFeatureAtPixel(t.pixel,a=>(s=a,!0)),s&&(t.preventDefault(),t.stopPropagation());for(const a of this.dblClickCallbacks)a(r,o,s,t);if(s)return!1}),()=>{const t=this.dblClickCallbacks.indexOf(e);t>-1&&this.dblClickCallbacks.splice(t,1)}}escapeHtml(e){if(!e)return"";const t=document.createElement("div");return t.textContent=e,t.innerHTML}createAddLocationPopup(){this.addLocationPopupElement=document.createElement("div"),this.addLocationPopupElement.className="map-add-location-popup",this.addLocationPopupElement.innerHTML=`
<div class="add-location-popup-header">
<span> Add Location</span>
<button type="button" class="add-location-popup-close" aria-label="Close">&times;</button>
</div>
<form id="map-add-location-form">
<div class="add-location-popup-field">
<label for="map-location-name">Name <span class="text-danger">*</span></label>
<input type="text" id="map-location-name" name="name" required placeholder="e.g., Water Point A">
</div>
<div class="add-location-popup-field">
<label for="map-location-category">Category</label>
<select id="map-location-category" name="category">
${this.getCategoryOptionsHtml()}
</select>
</div>
<div class="add-location-popup-field">
<label for="map-location-description">Description</label>
<textarea id="map-location-description" name="description" rows="2" placeholder="Optional notes..."></textarea>
</div>
<div class="add-location-popup-coords">
<small>📍 <span id="map-location-coords"></span></small>
</div>
<button type="submit" class="add-location-popup-submit"> Add Location</button>
</form>
`,this.addLocationPopup=new ie({element:this.addLocationPopupElement,positioning:"bottom-center",offset:[0,-10],stopEvent:!0,autoPan:!0,autoPanAnimation:{duration:250}}),this.map.addOverlay(this.addLocationPopup),this.addLocationCoords=null,this.addLocationPopupElement.querySelector(".add-location-popup-close").addEventListener("click",()=>{this.hideAddLocationPopup()}),this.addLocationCallbacks=[]}showAddLocationPopup(e){const[t,r]=xe(e);this.addLocationCoords={lon:t,lat:r};const o=this.addLocationPopupElement.querySelector("#map-location-coords");o.textContent=`${t.toFixed(6)}, ${r.toFixed(6)}`,this.addLocationPopupElement.querySelector("#map-add-location-form").reset(),this.addLocationPopup.setPosition(e)}hideAddLocationPopup(){this.addLocationPopup.setPosition(void 0),this.addLocationCoords=null}onAddLocation(e){if(this.addLocationCallbacks.push(e),this.addLocationCallbacks.length===1){const t=this.addLocationPopupElement.querySelector("#map-add-location-form");t.addEventListener("submit",r=>{if(r.preventDefault(),!this.addLocationCoords)return;const o=new FormData(t),s={name:o.get("name"),category:o.get("category"),description:o.get("description"),lon:this.addLocationCoords.lon,lat:this.addLocationCoords.lat};this.addLocationCallbacks.forEach(a=>a(s)),this.hideAddLocationPopup()})}}createBaseLayers(e){const t=new J({title:"Topographic",type:"base",zIndex:-100,visible:e==="topo",source:new ge({url:"https://{a-c}.tile.opentopomap.org/{z}/{x}/{y}.png",attributions:"Map data: © OpenTopoMap",maxZoom:17,crossOrigin:"anonymous"})});t.set("basemapKey","topo");const r=new J({title:"Carto Light",type:"base",zIndex:-100,visible:e==="carto-light",source:new ge({url:"https://{a-c}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});r.set("basemapKey","carto-light");const o=new J({title:"Carto Dark",type:"base",zIndex:-100,visible:e==="carto-dark",source:new ge({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 s=new J({title:"OSM Cycle map",type:"base",zIndex:-100,visible:!1,source:new Nt({url:"https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=ae1339c46dd3446b9c491e7336d38760"})});s.set("basemapKey","cycle");const a=new J({title:"Satellite",type:"base",zIndex:-100,visible:e==="satellite",source:new ge({url:"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",attributions:"Tiles © Esri",maxZoom:19,crossOrigin:"anonymous"})});a.set("basemapKey","satellite");const i=new J({title:"Google Sat",type:"base",zIndex:-100,visible:e==="googlesat",source:new ge({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 J({title:"OpenStreetMap",type:"base",zIndex:-100,visible:e==="osm",source:new Nt});return l.set("basemapKey","osm"),this._baseMapLayers=[r,o,s,a,i,l,t],new fe({title:"Base Maps",layers:[r,o,a,s,i,l,t]})}setBaseMap(e){if(!this._baseMapLayers)return!1;let t=!1;for(const r of this._baseMapLayers){const o=r.get("basemapKey")===e;r.setVisible(o),o&&(t=!0)}return t&&console.log("[MapView] Base map switched to:",e),t}getFeatureStyle(e){const t=e.get("category")||"default",r=this.getEmoji(t);if(e===this.selectedFeature)return[new C({image:new ce({radius:22,fill:new M({color:"rgba(220, 38, 38, 0.25)"}),stroke:new T({color:"#dc2626",width:3})})}),new C({text:new Je({text:r,font:"40px sans-serif",textBaseline:"bottom",textAlign:"center",offsetY:-5})})];const o=e.get("style");return o||(this.categoryStyles[t]?this.categoryStyles[t]:this.defaultStyle)}setCategoryStyles(e){for(const[t,r]of Object.entries(e)){r.emoji&&(this.categoryEmojis[t]?(this.categoryEmojis[t].emoji=r.emoji,r.label&&(this.categoryEmojis[t].label=r.label)):this.categoryEmojis[t]={emoji:r.emoji,label:r.label||t});const o=this.getEmoji(t),s=r.fontSize||28;this.categoryStyles[t]=this.createEmojiStyle(o,s)}this.markerSource.changed()}addMarker(e,t,r={}){console.log("[MapView] Adding marker at",e,t,"with properties:",r);const o=new Me({geometry:new ht(he([e,t])),...r});return o.set("lon",e),o.set("lat",t),this.markerSource.addFeature(o),console.log("[MapView] Marker added, total features:",this.markerSource.getFeatures().length),o}addMarkers(e){console.log("[MapView] Adding",e.length,"markers");const t=e.map(r=>new Me({geometry:new ht(he([r.longitude,r.latitude])),id:r.id,name:r.name,description:r.description,category:r.category,lon:r.longitude,lat:r.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(r=>r.get("id")===e);t&&this.markerSource.removeFeature(t)}}getMarkers(){return this.markerSource.getFeatures()}findMarker(e){return this.markerSource.getFeatures().find(t=>t.get("id")===e)}selectMarker(e){return typeof e=="object"?this.selectedFeature=e:this.selectedFeature=this.findMarker(e),this.markerSource.changed(),this.selectedFeature}clearSelection(){this.selectedFeature=null,this.markerSource.changed()}zoomTo(e,t,r=15){this.map.getView().animate({center:he([e,t]),zoom:r,duration:500})}fitToMarkers(e=50){const t=this.markerSource.getExtent();t&&t[0]!==1/0&&this.map.getView().fit(t,{padding:[e,e,e,e],duration:500,maxZoom:16})}getCenter(){const e=this.map.getView().getCenter();return xe(e)}getZoom(){return this.map.getView().getZoom()}setCenter(e,t){this.map.getView().setCenter(he([e,t]))}setZoom(e){this.map.getView().setZoom(e)}onClick(e){return this.clickCallbacks.push(e),this.clickCallbacks.length===1&&(this._clickTimer=null,this.map.on("dblclick",()=>{this._clickTimer&&(clearTimeout(this._clickTimer),this._clickTimer=null)}),this.map.on("click",t=>{this._clickTimer&&(clearTimeout(this._clickTimer),this._clickTimer=null),!this._editBarActive&&this._selectInteraction&&this._selectInteraction.getFeatures().clear();let r=!1,o=!1,s=null;if(this.map.forEachFeatureAtPixel(t.pixel,l=>{l.get("_layerType")==="parcel"&&(o=!0),l.get("name")&&(s=l),r=!0}),r&&!o&&!s)return;const[a,i]=xe(t.coordinate);this._clickTimer=setTimeout(()=>{this._clickTimer=null;let l=null;this.map.forEachFeatureAtPixel(t.pixel,c=>{if(c.get("name"))return l=c,!0});for(const c of this.clickCallbacks)c(a,i,l,t)},300)})),()=>{const t=this.clickCallbacks.indexOf(e);t>-1&&this.clickCallbacks.splice(t,1)}}onPointerMove(e){this.map.on("pointermove",t=>{if(t.dragging)return;const[r,o]=xe(t.coordinate);let s=null;this.map.forEachFeatureAtPixel(t.pixel,a=>{if(a.get("name"))return s=a,!0}),this.map.getTargetElement().style.cursor=s?"pointer":"",e(r,o,s,t)})}enableHoverCursor(){}addGeoJSONLayer(e,t,r={},o=null){const{strokeColor:s="#3b82f6",strokeWidth:a=2,fillColor:i="rgba(59,130,246,0.1)",lineCasingColor:l=null,lineCasingWidth:c=null,pointRadius:d=5,pointFillColor:u=null,pointStrokeColor:f="#ffffff",pointStrokeWidth:p=1.5}=r,h=new z({features:new oe().readFeatures(e,{featureProjection:"EPSG:3857"})}),m=new M({color:i}),g=new ce({radius:d,fill:new M({color:u||s}),stroke:new T({color:f,width:p})});let y;if(l){const S=c??a+2;y=[new C({stroke:new T({color:l,width:S})}),new C({stroke:new T({color:s,width:a}),fill:m,image:g})]}else y=new C({stroke:new T({color:s,width:a}),fill:m,image:g});const b=new N({title:t,source:h,style:y});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,r=""){const o=new fe({title:t.trim()});return o.set("layerId",e),o.set("description",r),this.overlayGroup.getLayers().push(o),console.log("[MapView] Layer group added:",t.trim(),"(id:",e+")"),o}addWMSLayer(e,t,r,o,s={}){const a=this.getLayerGroupByTitle(e);if(!a)return console.warn(`[MapView] Layer group "${e}" not found — cannot add WMS layer "${t}"`),null;const i={LAYERS:o,TILED:!0,WIDTH:256,HEIGHT:256};s.style!==void 0&&(i.STYLES=s.style);const l=new $t({url:r,params:i,serverType:s.serverType!==void 0?s.serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1,attributions:s.attributions}),c=new J({title:t,visible:s.visible!==void 0?s.visible:!0,source:l,opacity:s.opacity!==void 0?s.opacity:1,zIndex:s.zIndex});if(l.on("tileloaderror",()=>{O(`WMS layer "${t}" — tile load error. Check the URL and layer name.`,"warning",5e3)}),a.getLayers().push(c),s.legendUrl)try{this._registerLegend(c,t,s.legendUrl)}catch(d){console.warn(`[MapView] Could not register legend for "${t}":`,d)}return s.onlineOnly&&this._attachOnlineOnlyHandler(c,t),console.log(`[MapView] WMS layer added: "${t}" → group "${e}"`),c}addXYZLayer(e,t,r,o={}){const s=this.getLayerGroupByTitle(e);if(!s)return console.warn(`[MapView] Layer group "${e}" not found — cannot add XYZ layer "${t}"`),null;const a=new ge({url:r,crossOrigin:"anonymous",maxZoom:o.maxZoom!==void 0?o.maxZoom:19,attributions:o.attributions}),i=new J({title:t,visible:o.visible!==void 0?o.visible:!0,source:a,opacity:o.opacity!==void 0?o.opacity:1,zIndex:o.zIndex});if(a.on("tileloaderror",()=>{O(`XYZ layer "${t}" — tile load error. Check the URL.`,"warning",5e3)}),s.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=`
<div style="background:#10b981;color:#fff;padding:10px 14px;font-weight:600;display:flex;justify-content:space-between;align-items:center;">
<span>Add External Layer</span>
<button class="add-layer-close" style="background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;">&times;</button>
</div>
<div style="padding:14px;display:flex;flex-direction:column;gap:10px;">
<div>
<label style="font-weight:600;font-size:12px;display:block;margin-bottom:4px;">Layer Type</label>
<div class="add-layer-types" style="display:flex;gap:6px;">
<label style="flex:1;display:flex;align-items:center;gap:4px;cursor:pointer;padding:6px 8px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:12px;font-weight:600;">
<input type="radio" name="add-layer-type" value="wms" checked style="accent-color:#10b981;"> WMS
</label>
<label style="flex:1;display:flex;align-items:center;gap:4px;cursor:pointer;padding:6px 8px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:12px;font-weight:600;">
<input type="radio" name="add-layer-type" value="wfs" style="accent-color:#10b981;"> WFS
</label>
<label style="flex:1;display:flex;align-items:center;gap:4px;cursor:pointer;padding:6px 8px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:12px;font-weight:600;">
<input type="radio" name="add-layer-type" value="xyz" style="accent-color:#10b981;"> XYZ
</label>
</div>
</div>
<div>
<label style="font-weight:600;font-size:12px;display:block;margin-bottom:4px;">Server URL</label>
<input type="text" class="add-layer-url" placeholder="https://example.com/wms"
style="width:100%;padding:8px 10px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:13px;background:var(--muted, #f2f4f7);color:var(--foreground, #1e1a4b);box-sizing:border-box;" />
</div>
<div class="add-layer-name-row">
<label style="font-weight:600;font-size:12px;display:block;margin-bottom:4px;">Layer Name</label>
<input type="text" class="add-layer-name" placeholder="workspace:layer_name"
style="width:100%;padding:8px 10px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:13px;background:var(--muted, #f2f4f7);color:var(--foreground, #1e1a4b);box-sizing:border-box;" />
<div style="font-size:11px;color:var(--muted-foreground, #7a7a7a);margin-top:2px;" class="add-layer-name-hint">
WMS LAYERS parameter (e.g. workspace:layer)
</div>
</div>
<div>
<label style="font-weight:600;font-size:12px;display:block;margin-bottom:4px;">Display Title</label>
<input type="text" class="add-layer-title" placeholder="My Layer"
style="width:100%;padding:8px 10px;border:2px solid var(--border, #1e1a4b1f);border-radius:6px;font-size:13px;background:var(--muted, #f2f4f7);color:var(--foreground, #1e1a4b);box-sizing:border-box;" />
</div>
<div style="display:flex;gap:8px;margin-top:4px;">
<button class="add-layer-confirm" style="flex:1;padding:8px 12px;background:#10b981;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
Add Layer
</button>
<button class="add-layer-cancel" style="flex:1;padding:8px 12px;background:var(--muted-foreground, #7a7a7a);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
Cancel
</button>
</div>
</div>
`,this._addLayerDialog.appendChild(e),this.map.getTargetElement().appendChild(this._addLayerDialog);const t=e.querySelector(".add-layer-name-row"),r=e.querySelector(".add-layer-name-hint"),o=e.querySelector(".add-layer-url");e.querySelectorAll('input[name="add-layer-type"]').forEach(a=>{a.addEventListener("change",()=>{const i=a.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",r.textContent=i==="wms"?"WMS LAYERS parameter (e.g. workspace:layer)":"WFS typename (e.g. workspace:layer)")})});const s=()=>this._hideAddLayerDialog();e.querySelector(".add-layer-close").addEventListener("click",s),e.querySelector(".add-layer-cancel").addEventListener("click",s),this._addLayerDialog.addEventListener("click",a=>{a.target===this._addLayerDialog&&s()}),e.querySelector(".add-layer-confirm").addEventListener("click",()=>{const a=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((a==="wms"||a==="wfs")&&!l){e.querySelector(".add-layer-name").style.borderColor="#ef4444";return}if(!c){e.querySelector(".add-layer-title").style.borderColor="#ef4444";return}this._addExternalLayer(a,i,l,c),this._hideAddLayerDialog()}),e.addEventListener("keydown",a=>{a.key==="Enter"&&(a.preventDefault(),e.querySelector(".add-layer-confirm").click()),a.key==="Escape"&&(a.preventDefault(),s())})}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,r,o){const s=this._externalSourceGroup;if(!s){O('Layer group "External Source" not found.',"error",4e3);return}let a;switch(e){case"wms":{const i=new $t({url:t,params:{LAYERS:r,TILED:!0,WIDTH:256,HEIGHT:256},serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1});a=new J({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(r)}&outputFormat=application/json&srsname=EPSG:3857`,l=new z({url:i,format:new oe});l.on("featuresloaderror",()=>{O(`WFS "${o}" — load error. Check URL and layer name.`,"warning",5e3)}),a=new N({title:o,visible:!0,source:l,style:new C({stroke:new T({color:"#e11d48",width:2}),fill:new M({color:"rgba(225,29,72,0.15)"})})});break}case"xyz":a=new J({title:o,visible:!0,source:new ge({url:t,crossOrigin:"anonymous"})}),a.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.getLayers().push(a),O(`Layer "${o}" added to External Source.`,"success",3e3),console.log(`[MapView] External ${e.toUpperCase()} layer added: "${o}"`)}_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 Rt}_registerLegend(e,t,r){if(!this._legendPanel)return;const o=document.createElement("div");o.className="map-legend-entry",o.style.cssText="border-bottom:1px solid var(--border, #1e1a4b1f);padding-bottom:6px;",o.innerHTML=`
<div style="font-weight:600;font-size:11px;margin-bottom:4px;line-height:1.3;">
${this._escapeHtml(t)}
</div>
<img src="${r}" alt="${this._escapeHtml(t)} legend"
style="display:block;max-width:100%;height:auto;border-radius:3px;"
onerror="this.style.display='none'" />
`,this._legendEntries.set(e,o);const s=()=>{try{this._updateLegendPanel()}catch(a){console.warn("[MapView] legend panel update failed:",a)}};e.on("change:visible",s),s()}_updateLegendPanel(){if(!this._legendPanel)return;const e=[];for(const[t,r]of this._legendEntries)t.getVisible()&&e.push(r);this._legendEntries.forEach(t=>{t.style.borderBottom="1px solid var(--border, #1e1a4b1f)",t.style.paddingBottom="6px"}),e.length>0&&(e[e.length-1].style.borderBottom="none",e[e.length-1].style.paddingBottom="0"),this._legendPanel.replaceChildren(...e),this._legendPanel.style.display=e.length>0?"flex":"none"}_escapeHtml(e){return String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}getLayerGroup(e){let t=null;return this.overlayGroup.getLayers().forEach(r=>{r.get("layerId")===e&&(t=r)}),t}getLayerGroupByTitle(e){let t=null;return this.overlayGroup.getLayers().forEach(r=>{r.get("title")===e&&(t=r)}),t}getOverlayGroup(){return this.overlayGroup}getMap(){return this.map}getCurrentViewExtent(){const e=this.map.getView(),t=this.map.getSize();return t?e.calculateExtent(t):null}getDistrictBoundaryExtent(){let e=null;const t=r=>{r.getLayers().forEach(o=>{if(o.getLayers)t(o);else if(o.get("title")==="District Boundary"){const s=o.getSource&&o.getSource();if(s&&typeof s.getExtent=="function"){const a=s.getExtent();a&&Number.isFinite(a[0])&&(e={extent:a,title:o.get("title")})}}})};return t(this.overlayGroup),e}getMarkerSource(){return this.markerSource}getMarkersLayer(){return this.markersLayer}updateSize(){this.map.updateSize()}onSearchSelect(e){this.searchSelectCallbacks.push(e)}navigateTo(e,t,r=14,o=500){const s=he([e,t]);this.map.getView().animate({center:s,zoom:r,duration:o})}}class Sr{constructor(e,t={}){this.map=e,this.options=t,this.measureSource=new z,this.measureLayer=new N({source:this.measureSource,style:this.getMeasureStyle(),title:"Measurements",zIndex:100}),this.drawSource=new z,this.drawLayer=new N({source:this.drawSource,style:this.getDrawStyle(),title:"Draw sketches",displayInLayerSwitcher:!1,zIndex:99});const r=this.map.getLayers(),o=r.getLength()-1;r.insertAt(o,this.drawLayer),r.insertAt(o,this.measureLayer),this.activeInteraction=null,this.measureTooltip=null,this.measureTooltipElement=null,this.onMeasureCompleteCallbacks=[],this.onDrawCompleteCallbacks=[]}getMeasureStyle(){return new C({fill:new M({color:"rgba(255, 233, 106, 0.2)"}),stroke:new T({color:"#8B008B",lineDash:[10,10],width:2}),image:new ce({radius:5,stroke:new T({color:"#8B008B"}),fill:new M({color:"rgba(255, 233, 106, 0.5)"})})})}getDrawStyle(){return new C({fill:new M({color:"rgba(255, 233, 106, 0.3)"}),stroke:new T({color:"#8B008B",width:2}),image:new ce({radius:6,stroke:new T({color:"#8B008B",width:2}),fill:new M({color:"#FFE96A"})})})}createMeasureTooltip(){this.measureTooltipElement&&this.measureTooltipElement.parentNode.removeChild(this.measureTooltipElement),this.measureTooltipElement=document.createElement("div"),this.measureTooltipElement.className="measure-tooltip",this.measureTooltip=new ie({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 me({source:this.measureSource,type:"Circle",style:new C({fill:new M({color:"rgba(255, 233, 106, 0.2)"}),stroke:new T({color:"rgba(139, 0, 139, 0.7)",lineDash:[10,10],width:2}),image:new ce({radius:5,stroke:new T({color:"rgba(139, 0, 139, 0.7)"}),fill:new M({color:"rgba(255, 233, 106, 0.5)"})})})});this.activeInteraction=e,this.map.addInteraction(e);let t;return e.on("drawstart",r=>{t=r.feature.getGeometry().on("change",s=>{const a=s.target;if(a instanceof tn){const i=a.getRadius(),l=Yn(i),d=`<strong>${nt(i)}</strong><br><small>${l}</small>`;this.measureTooltipElement.innerHTML=d,this.measureTooltip.setPosition(a.getLastCoordinate())}})}),e.on("drawend",r=>{const o=r.feature,s=o.getGeometry(),a=s.getCenter(),i=s.getRadius();o.set("_layerType","measure_circle"),o.set("_radius",i),o.set("_center",a);const l=new Me({geometry:new de([a,[a[0]+i,a[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(),gt(t);const c={type:"circle",center:a,radius:i,area:Math.PI*i*i,feature:o};this.onMeasureCompleteCallbacks.forEach(d=>d(c))}),e}startLineMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new me({source:this.measureSource,type:"LineString",style:this.getMeasureStyle()});this.activeInteraction=e,this.map.addInteraction(e);let t;return e.on("drawstart",r=>{t=r.feature.getGeometry().on("change",s=>{const a=s.target,i=Xe(a),l=nt(i);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(a.getLastCoordinate())})}),e.on("drawend",r=>{const o=r.feature,s=o.getGeometry(),a=Xe(s);this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),gt(t);const i={type:"line",length:a,feature:o};this.onMeasureCompleteCallbacks.forEach(l=>l(i))}),e}startAreaMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new me({source:this.measureSource,type:"Polygon",style:this.getMeasureStyle()});this.activeInteraction=e,this.map.addInteraction(e);let t;return e.on("drawstart",r=>{t=r.feature.getGeometry().on("change",s=>{const a=s.target,i=$e(a),l=je(i);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(a.getInteriorPoint().getCoordinates())})}),e.on("drawend",r=>{const o=r.feature,s=o.getGeometry(),a=$e(s);o.set("_layerType","measure_area"),o.set("_area",a),this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),gt(t);const i={type:"polygon",area:a,feature:o,coordinate:s.getInteriorPoint().getCoordinates()};this.onMeasureCompleteCallbacks.forEach(l=>l(i))}),e}startDrawPoint(){this.deactivate();const e=new me({source:this.drawSource,type:"Point",style:this.getDrawStyle()});return this.activeInteraction=e,this.map.addInteraction(e),e.on("drawend",t=>{const r={type:"point",feature:t.feature};this.onDrawCompleteCallbacks.forEach(o=>o(r))}),e}startDrawLine(){this.deactivate();const e=new me({source:this.drawSource,type:"LineString",style:this.getDrawStyle()});return this.activeInteraction=e,this.map.addInteraction(e),e.on("drawend",t=>{const r={type:"line",feature:t.feature};this.onDrawCompleteCallbacks.forEach(o=>o(r))}),e}startDrawPolygon(){this.deactivate();const e=new me({source:this.drawSource,type:"Polygon",style:this.getDrawStyle()});return this.activeInteraction=e,this.map.addInteraction(e),e.on("drawend",t=>{const r={type:"polygon",feature:t.feature};this.onDrawCompleteCallbacks.forEach(o=>o(r))}),e}clearMeasurements(){this.measureSource.clear(),document.querySelectorAll(".measure-tooltip-static").forEach(t=>t.parentNode.removeChild(t))}clearDrawings(){this.drawSource.clear()}clearAll(){this.clearMeasurements(),this.clearDrawings()}onMeasureComplete(e){this.onMeasureCompleteCallbacks.push(e)}onDrawComplete(e){this.onDrawCompleteCallbacks.push(e)}createControlBar(e={}){e.position;const t=new _t({group:!0,className:"map-tools-bar"}),r=new _t({toggleOne:!0,group:!0}),o=new le({html:'<span class="tool-icon">⭕</span>',title:"Measure Circle (radius & area)",className:"measure-circle-btn",onToggle:l=>{l?this.startCircleMeasure():this.deactivate()}});r.addControl(o);const s=new le({html:'<span class="tool-icon">📏</span>',title:"Measure Distance",className:"measure-line-btn",onToggle:l=>{l?this.startLineMeasure():this.deactivate()}});r.addControl(s);const a=new le({html:'<span class="tool-icon">⬛</span>',title:"Measure Area",className:"measure-area-btn",onToggle:l=>{l?this.startAreaMeasure():this.deactivate()}});r.addControl(a);const i=new Re({html:'<span class="tool-icon">🗑️</span>',title:"Clear Measurements",className:"clear-measure-btn",handleClick:()=>{this.clearMeasurements(),o.setActive(!1),s.setActive(!1),a.setActive(!1)}});return r.addControl(i),t.addControl(r),t}getMeasureLayer(){return this.measureLayer}getDrawLayer(){return this.drawLayer}getMeasureSource(){return this.measureSource}getDrawSource(){return this.drawSource}isActive(){return this.activeInteraction!==null}}let be=null;async function Lr(){if(!("serviceWorker"in navigator))return console.warn("[PWA] Service Workers not supported"),null;try{return be=await navigator.serviceWorker.register("/sw.js",{scope:"/"}),console.log("[PWA] Service Worker registered:",be.scope),be.addEventListener("updatefound",()=>{const n=be.installing;n.addEventListener("statechange",()=>{n.state==="installed"&&navigator.serviceWorker.controller&&(console.log("[PWA] New version available"),Cr())})}),be}catch(n){return console.error("[PWA] Service Worker registration failed:",n),null}}let ke=null,se=null;function kr(n="#install-btn"){if(se=typeof n=="string"?document.querySelector(n):n,!se){console.warn("[PWA] Install button not found:",n);return}se.style.display="none",window.addEventListener("beforeinstallprompt",e=>{e.preventDefault(),ke=e,se.style.display="block",console.log("[PWA] Install prompt ready")}),se.addEventListener("click",async()=>{if(!ke){Tr();return}ke.prompt();const{outcome:e}=await ke.userChoice;console.log("[PWA] Install prompt outcome:",e),ke=null,se.style.display="none"}),window.addEventListener("appinstalled",()=>{console.log("[PWA] App installed"),ke=null,se.style.display="none"}),window.matchMedia("(display-mode: standalone)").matches&&(se.style.display="none")}function Tr(){const n=/iPad|iPhone|iPod/.test(navigator.userAgent),e=/^((?!chrome|android).)*safari/i.test(navigator.userAgent);let t=`To install this app:
`;n?(t+=`1. Tap the Share button (square with arrow)
`,t+='2. Scroll down and tap "Add to Home Screen"'):e?(t+=`1. Click File menu
`,t+='2. Click "Add to Dock"'):(t+=`1. Click the menu button (three dots)
`,t+='2. Click "Install" or "Add to Home Screen"'),alert(t)}let Pt=null;const Mt=new Set;function Pr(n="#offline-indicator"){Pt=typeof n=="string"?document.querySelector(n):n,vt(!navigator.onLine),window.addEventListener("online",()=>{console.log("[PWA] Back online"),vt(!1),oo(!1)}),window.addEventListener("offline",()=>{console.log("[PWA] Gone offline"),vt(!0),oo(!0)})}function vt(n){Pt&&(Pt.style.display=n?"block":"none"),document.body.classList.toggle("is-offline",n)}function Mr(n){return Mt.add(n),n(!navigator.onLine),()=>Mt.delete(n)}function oo(n){for(const e of Mt)try{e(n)}catch(t){console.error("[PWA] Offline listener error:",t)}}function Z(){return navigator.onLine}function Cr(){confirm("A new version is available. Reload now?")&&Ir()}function Ir(){be?.waiting&&be.waiting.postMessage({type:"SKIP_WAITING"}),window.location.reload()}async function Ar({timeoutMs:n=1e4}={}){if(!("serviceWorker"in navigator))throw new Error("Service Workers not supported in this browser");if(navigator.serviceWorker.controller)return navigator.serviceWorker.controller;const e=navigator.serviceWorker.ready,t=new Promise((s,a)=>setTimeout(()=>a(new Error("Service-worker readiness timeout")),n)),r=await Promise.race([e,t]),o=navigator.serviceWorker.controller||r.active;if(!o)throw new Error("No active service worker available");return o}function Dr(n){if(!("serviceWorker"in navigator))return()=>{};const e=()=>{try{n()}catch(t){console.error("[PWA] controllerchange handler error:",t)}};return navigator.serviceWorker.addEventListener("controllerchange",e),()=>navigator.serviceWorker.removeEventListener("controllerchange",e)}async function No(n,e,t={},r=5e3,o=1e4){const s=await Ar({timeoutMs:o});return new Promise((a,i)=>{const l=new MessageChannel,c=setTimeout(()=>{l.port1.close(),i(new Error(`Service-worker reply "${e}" timed out`))},r);l.port1.onmessage=d=>{if(d.data?.type===e){clearTimeout(c),l.port1.close();const{type:u,...f}=d.data;a(f)}},s.postMessage({type:n,...t},[l.port2])})}async function Fr(){try{return(await No("GET_TILE_STATS","TILE_STATS")).stats}catch(n){return console.warn("[PWA] getTileCacheStats failed:",n),null}}async function Or(){try{return await No("CLEAR_TILE_CACHES","TILE_CACHES_CLEARED"),!0}catch(n){return console.warn("[PWA] clearTileCaches failed:",n),!1}}async function Br(){if(!navigator.storage?.estimate)return null;try{const{usage:n,quota:e}=await navigator.storage.estimate();return{usage:n||0,quota:e||0}}catch(n){return console.warn("[PWA] getStorageEstimate failed:",n),null}}async function Rr(n={}){const{installButton:e="#install-btn",offlineIndicator:t="#offline-indicator",autoRegisterSW:r=!0}=n;r&&await Lr(),kr(e),Pr(t),console.log("[PWA] Initialized")}const $o={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"}},Nr=30*1024,rt=2*Math.PI*6378137/2;function no(n,e){const t=n/rt*180;let r=e/rt*180;return r=180/Math.PI*(2*Math.atan(Math.exp(r*Math.PI/180))-Math.PI/2),[t,r]}function ro(n,e,t){const r=Math.pow(2,t),o=Math.floor((n+180)/360*r),s=e*Math.PI/180,a=Math.floor((1-Math.log(Math.tan(s)+1/Math.cos(s))/Math.PI)/2*r);return{x:o,y:a}}function jo(n,e){const[t,r,o,s]=n,[a,i]=no(t,r),[l,c]=no(o,s),d=ro(a,c,e),u=ro(l,i,e),f=Math.pow(2,e),p=Math.max(0,Math.min(d.x,u.x)),h=Math.min(f-1,Math.max(d.x,u.x)),m=Math.max(0,Math.min(d.y,u.y)),g=Math.min(f-1,Math.max(d.y,u.y));return{z:e,minX:p,maxX:h,minY:m,maxY:g,count:(h-p+1)*(g-m+1)}}function $r(n,e,t){let r=0;for(let o=e;o<=t;o++)r+=jo(n,o).count;return r}function jr(n,e,t){const r=[];for(let o=e;o<=t;o++){const s=jo(n,o);for(let a=s.minX;a<=s.maxX;a++)for(let i=s.minY;i<=s.maxY;i++)r.push({z:o,x:a,y:i})}return r}function zr(n,{z:e,x:t,y:r}){return n.replace("{z}",e).replace("{x}",t).replace("{y}",r)}class Gr{constructor({baseMap:e,extent3857:t,minZoom:r,maxZoom:o,concurrency:s=2,interBatchDelayMs:a=50,onProgress:i=()=>{}}){const l=$o[e];if(!l)throw new Error(`Unknown base map: ${e}`);o>l.maxZoom&&(console.warn(`[OfflineTiles] ${e}: maxZoom ${o} > supported ${l.maxZoom}; clamping`),o=l.maxZoom),this.baseMap=e,this.template=l.url,this.extent=t,this.minZoom=r,this.maxZoom=o,this.concurrency=Math.max(1,Math.min(s,6)),this.interBatchDelayMs=a,this.onProgress=i,this._abortCtrl=null,this._cancelled=!1}async start(){if(this._abortCtrl)throw new Error("Downloader already started");this._abortCtrl=new AbortController,this._cancelled=!1;const e=jr(this.extent,this.minZoom,this.maxZoom),t=e.length,r=Date.now();let o=0,s=0,a=0,i=0;const l=c=>{const d=Date.now()-r,u=o>0?Math.round(d/o*(t-o)):null;this.onProgress({phase:c,done:o,total:t,ok:s,failed:a,cached:i,elapsedMs:d,etaMs:u})};l("running");for(let c=0;c<e.length&&!this._cancelled;c+=this.concurrency){const d=e.slice(c,c+this.concurrency);await Promise.all(d.map(async u=>{if(this._cancelled)return;const f=zr(this.template,u);try{const p=await fetch(f,{signal:this._abortCtrl.signal,cache:"default"});p.ok?(s++,p.body&&p.body.cancel().catch(()=>{})):(p.status,a++)}catch(p){p.name==="AbortError"||a++}o++})),l("running"),this.interBatchDelayMs>0&&c+this.concurrency<e.length&&await new Promise(u=>setTimeout(u,this.interBatchDelayMs))}return l(this._cancelled?"cancelled":"done"),{phase:this._cancelled?"cancelled":"done",done:o,total:t,ok:s,failed:a,cached:i,elapsedMs:Date.now()-r}}cancel(){this._cancelled=!0,this._abortCtrl&&this._abortCtrl.abort()}}const qr=(()=>{const n=(r,o)=>{const s=r*rt/180,a=Math.log(Math.tan((90+o)*Math.PI/360))/(Math.PI/180);return[s,a*rt/180]},e=n(-3.3,4.5),t=n(1.2,11.2);return[e[0],e[1],t[0],t[1]]})();function Hr(n){return n*Nr}const zo="https://api.lupmis4luspa.org/api/spatial_planning",Go={district_id:"1",api_token:"1c46538c712e9b5b"},Ur=3e4,Wr=5e3;let ye=null;async function Kr(n=!1){if(ye!==null&&!n)return ye;const e=new AbortController,t=setTimeout(()=>e.abort(),Wr);try{ye=(await fetch(`${zo}/get_layers.php`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(Go),signal:e.signal})).ok}catch{ye=!1}finally{clearTimeout(t)}return console.log("[RemoteDB] Server reachable:",ye),ye}function ve(){return ye}function Vr(n,e=Ur){const t=new AbortController,r=setTimeout(()=>t.abort(),e);return n.signal&&n.signal.addEventListener("abort",()=>t.abort()),{signal:t.signal,clear:()=>clearTimeout(r)}}async function Ee(n,e={},t={}){const r=`${zo}/${n}`,o={...Go,...e};console.log("[RemoteDB] POST",r);const s=Vr(t);try{const a=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(o),...t,signal:s.signal});if(!a.ok)throw new Error(`HTTP ${a.status}: ${a.statusText}`);const i=await a.json();return console.log("[RemoteDB] POST response:",n,"→",typeof i=="object"?`${Array.isArray(i)?i.length+" items":"object"}`:i),i}catch(a){throw a.name==="AbortError"?(console.error("[RemoteDB] POST timed out:",n),new Error(`Request timed out: ${n}`)):(console.error("[RemoteDB] POST failed:",n,a),a)}finally{s.clear()}}async function Yr(){return Ee("get_district_boundary.php")}async function Jr(){return Ee("get_layers.php")}async function Xr(){return Ee("get_all_collector_zone_per_district.php")}async function Zr(){return Ee("get_parcels_per_district.php")}async function Qr(){return Ee("get_all_footprint_per_district.php")}async function ea(){return Ee("get_contours_hillshade.php")}async function ta(){return Ee("get_osm_roads.php")}let Et=null;async function ao(){if(!Et){const n=await at(()=>import("./shpjs-CNrRgkgn.js"),[]);Et=n.default||n}return Et}let v=null,X=null,F="addLocation";async function so(){console.log("[App] Initializing..."),await Rr({installButton:"#install-btn",offlineIndicator:"#offline-indicator",autoRegisterSW:!0});const n=localStorage.getItem("default-basemap")||"topo";v=new _r("map",{center:[-1.5,7.5],zoom:7,basemap:n}),X=new Sr(v.getMap()),X.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,r,o,s)=>{if(console.log("[MapClick] Clicked at:",t.toFixed(4),r.toFixed(4)),console.log("[MapClick] currentMode =",F),F==="draw"||F.startsWith("measure"))return;let a=null;if(v.getMap().forEachFeatureAtPixel(s.pixel,i=>{if(i.get("_layerType")==="parcel")return a=i,!0}),a){console.log("[MapClick] Clicked on parcel → Edit Attributes"),v.showParcelEditPopup(a,s.coordinate);return}F==="addLocation"&&(o?(console.log("[MapClick] Clicked on marker:",o.getId()),v.selectMarker(o),na(o)):(console.log("[MapClick] Empty space → Add Location popup"),v.clearSelection(),v.showAddLocationPopup(s.coordinate)))}),v.onDblClick((t,r,o,s)=>{if(!o)return;const a=o.get("_layerType");if(console.log("[App] Double-click on feature, _layerType:",a||"none"),a==="measure_circle")v.showCircleIntersectionPopup(o,s.coordinate);else{if(a==="measure_circle_radius")return;a==="measure_area"?v.showAreaIntersectionPopup(o,s.coordinate):a==="collector_zone"?v.showInfoPopup(o,s.coordinate,{title:"Zone Info",color:"#7c3aed"}):a==="parcel"?v.showInfoPopup(o,s.coordinate,{title:"Parcel Info",color:"#0ea5e9"}):v.showInfoPopup(o,s.coordinate,{title:"Feature Info",color:"#e11d48"})}}),v.onAddLocation(async t=>{console.log("[App] Add location from map popup:",t);try{const r=await Pn(t.name,t.lon,t.lat,{description:t.description||null,category:t.category||"default"});console.log("[App] Location added:",t.name,"id:",r.id),await xt(),v?.zoomTo(t.lon,t.lat,14),r.id&&v?.selectMarker(r.id),we("Location added successfully")}catch(r){console.error("[App] Failed to add location:",r),G("Failed to add location: "+r.message)}}),v.onParcelEdit(async(t,r)=>{const o=r.id||r.parcelid||r.parcel_id;if(console.log("[App] Parcel edit saved:",o,r),!o){console.warn("[App] No parcel ID found in updated properties — skipping local save");return}try{await Fn(o,r),we("Parcel updated locally")}catch(s){console.error("[App] Failed to save parcel update:",s),G("Failed to save parcel: "+s.message)}});const e=new fo;v.onDrawnPolygonSave(async(t,r)=>{console.log("[App] Drawn polygon attributes saved:",r);try{const o=e.writeGeometry(t.getGeometry(),{dataProjection:"EPSG:4326",featureProjection:"EPSG:3857"}),s=await On(o,r);console.log("[App] New parcel inserted with id:",s.id),we("New parcel saved (pending verification)")}catch(o){console.error("[App] Failed to save new parcel:",o),G("Failed to save parcel: "+o.message)}});try{console.log("[App] Initializing database..."),await Tn(),console.log("[App] Database ready");const t=await Ft();console.log("[App] Database status:",t),Z()&&(await Kr()||(console.warn("[App] API server unreachable — using local data only"),Pa("Server not responding — loading cached data."))),await va(),v?.initEditBar(),fa(),ha(),ga(),ma(),ya(),ba(),wa()}catch(t){console.error("[App] Database initialization failed:",t),G("Failed to initialize database. Please refresh the page.");return}oa(),await xt(),kn(t=>{if(console.log("[App] Database change:",t),t.table==="locations"&&!t.local&&xt(),t.table==="parcels"){const r=document.getElementById("local-data-stats");r&&!r.classList.contains("d-none")&&lt()}}),Mr(t=>{t?console.log("[App] Working offline - data will sync when back online"):(console.log("[App] Back online - syncing data..."),Ea())}),Ma(),Ia(),Ca(),Aa(),Da(),Fa(),console.log("[App] Initialized successfully")}function oa(){console.log("[initUI] Starting UI initialization..."),Ta();const n=document.getElementById("export-btn");n&&n.addEventListener("click",ia);const e=document.getElementById("local-data-btn");e&&e.addEventListener("click",()=>lt());const t=document.getElementById("import-shp-btn"),r=document.getElementById("shp-file-input");t&&r&&(t.addEventListener("click",()=>r.click()),r.addEventListener("change",Wo));const o=document.getElementById("import-geojson-btn"),s=document.getElementById("geojson-file-input");o&&s&&(o.addEventListener("click",()=>s.click()),s.addEventListener("change",Ko));const a=document.getElementById("import-kml-btn"),i=document.getElementById("kml-file-input");a&&i&&(a.addEventListener("click",()=>i.click()),i.addEventListener("change",Vo)),La();const l=document.getElementById("exportGeoJSON-btn");l&&l.addEventListener("click",la);const c=document.getElementById("status-btn");c&&c.addEventListener("click",ca);const d=document.getElementById("fit-btn");d&&d.addEventListener("click",()=>v?.fitToMarkers());const u=document.getElementById("dock-btn-add-location"),f=document.getElementById("dock-btn-measure-circle"),p=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:!!f,measureLine:!!p,measureArea:!!h,draw:!!m,clear:!!g});const y=[u,f,p,h,m],b=(E,S)=>{switch(console.log("[setMode] Changing mode from",F,"to",E),F=E,console.log("[setMode] currentMode is now:",F),y.forEach(L=>{L&&L.classList.toggle("active",L===S)}),X?.deactivate(),E!=="draw"&&v?.setEditMode(!1),E!=="addLocation"&&v?.hideAddLocationPopup(),E){case"measureCircle":X?.startCircleMeasure();break;case"measureLine":X?.startLineMeasure();break;case"measureArea":X?.startAreaMeasure();break;case"draw":v?.setEditMode(!0);break}};u&&u.addEventListener("click",()=>{console.log("[Button] Add Location clicked"),b("addLocation",u)}),f&&f.addEventListener("click",()=>{console.log("[Button] Circle clicked, currentMode is:",F),F==="measureCircle"?b("addLocation",u):b("measureCircle",f)}),p&&p.addEventListener("click",()=>{console.log("[Button] Line clicked, currentMode is:",F),F==="measureLine"?b("addLocation",u):b("measureLine",p)}),h&&h.addEventListener("click",()=>{console.log("[Button] Area clicked, currentMode is:",F),F==="measureArea"?b("addLocation",u):b("measureArea",h)}),m&&m.addEventListener("click",()=>{console.log("[Button] Draw clicked, currentMode is:",F),F==="draw"?b("addLocation",u):b("draw",m)}),g&&g.addEventListener("click",()=>{if(X?.clearMeasurements(),F.startsWith("measure"))switch(X?.deactivate(),F){case"measureCircle":X?.startCircleMeasure();break;case"measureLine":X?.startLineMeasure();break;case"measureArea":X?.startAreaMeasure();break}})}async function xt(){try{console.log("[App] Loading locations...");const n=await ko();console.log("[App] Locations loaded:",n),ra(n),v&&(v.clearMarkers(),n.length>0&&(v.addMarkers(n),console.log("[App] Added",n.length,"markers to map")));const e=document.getElementById("location-count");e&&(e.textContent=n.length)}catch(n){console.error("[App] Failed to load locations:",n)}}function na(n){const e=n.get("name"),t=n.get("description"),r=n.get("category"),o=n.get("lon")||n.get("longitude"),s=n.get("lat")||n.get("latitude");console.log("[App] Selected location:",{name:e,description:t,category:r,lon:o,lat:s})}function ra(n){const e=document.getElementById("locations-list");if(!e)return;const t=document.getElementById("location-count-mobile");if(t&&(t.textContent=n.length),n.length===0){e.innerHTML=`
<div class="text-center text-muted py-4">
<p class="mb-0">No locations yet.</p>
<small>Click the map or fill the form above!</small>
</div>
`;return}const r={water:"💧",school:"🏫",health:"🏥",market:"🏪",default:"📍",other:"📌"};e.innerHTML=n.map(o=>{const s=r[o.category]||"📍";return`
<a href="#" class="list-group-item list-group-item-action location-item py-2"
data-id="${o.id}" data-lon="${o.longitude}" data-lat="${o.latitude}">
<div class="d-flex w-100 justify-content-between align-items-start">
<div>
<h6 class="mb-1">${s} ${te(o.name)}</h6>
<small class="text-muted font-monospace">${o.latitude.toFixed(5)}, ${o.longitude.toFixed(5)}</small>
</div>
<span class="badge badge-${o.category}">${o.category}</span>
</div>
${o.description?`<small class="text-secondary d-block mt-1">${te(o.description)}</small>`:""}
</a>
`}).join(""),e.querySelectorAll(".location-item").forEach(o=>{o.addEventListener("click",s=>{s.preventDefault();const a=parseFloat(o.dataset.lon),i=parseFloat(o.dataset.lat),l=parseInt(o.dataset.id);v?.zoomTo(a,i,14),v?.selectMarker(l)})})}async function lt(){const n=document.getElementById("local-data-stats"),e=document.getElementById("local-data-tbody"),t=document.getElementById("clear-all-cached-btn");if(!(!n||!e)){try{const r=await Hn();e.innerHTML=r.map(o=>{const a=Co(o.name)?`<button type="button" class="btn btn-sm btn-link text-danger p-0 table-clear-btn"
data-table="${te(o.name)}"
title="Clear local cache (will re-download from server)">
<i class="bi bi-trash3"></i>
</button>`:"";return`
<tr>
<td class="ps-3">
<a href="#" class="table-name-link" data-table="${te(o.name)}">${te(o.name)}</a>
</td>
<td class="text-end"><span class="badge bg-secondary">${o.count}</span></td>
<td class="text-end pe-3">${a}</td>
</tr>
`}).join(""),n.classList.remove("d-none"),e.querySelectorAll(".table-name-link").forEach(o=>{o.addEventListener("click",s=>{s.preventDefault(),sa(o.dataset.table)})}),e.querySelectorAll(".table-clear-btn").forEach(o=>{o.addEventListener("click",async s=>{s.preventDefault();const a=o.dataset.table;if(confirm(`Clear local cache for "${a}"?
The data will be re-downloaded from the server on the next app start.`))try{const i=await Io(a);we(`Cleared ${i} row${i===1?"":"s"} from "${a}". It will re-download on next start.`),await lt()}catch(i){console.error("[App] Per-table clear failed:",i),G(`Could not clear "${a}": ${i.message}`)}})})}catch(r){console.error("[App] Failed to load table stats:",r),e.innerHTML='<tr><td colspan="3" class="text-danger ps-3">Failed to load</td></tr>',n.classList.remove("d-none")}t&&!t._wired&&(t._wired=!0,t.addEventListener("click",aa))}}async function aa(){if(confirm(`Delete all cached map layers from this device?
The next time the app starts (or after a reload), every layer will be re-downloaded from the server. Your locally drawn data is not affected.`))try{const n=await qn(),e=n.reduce((t,r)=>t+r.count,0);we(`Cleared ${e} row${e===1?"":"s"} across ${n.length} table${n.length===1?"":"s"}.`),await lt(),confirm("Reload the app now to re-download the layers fresh from the server?")&&window.location.reload()}catch(n){console.error("[App] Clear-all failed:",n),G("Failed to clear cached layers: "+n.message)}}async function sa(n){const e=document.getElementById("tableContentModalLabel"),t=document.getElementById("table-content-body"),r=document.getElementById("table-content-info");e.textContent=`Table: ${n}`,t.innerHTML=`
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
`,r.textContent="",new It(document.getElementById("tableContentModal")).show();try{const{columns:s,rows:a}=await Un(n);if(a.length===0){t.innerHTML='<div class="text-center text-muted py-4">Table is empty</div>',r.textContent="0 rows";return}const i=s.map(c=>`<th class="text-nowrap">${te(c)}</th>`).join(""),l=a.map(c=>`<tr>${s.map(u=>{let f=c[u];if(f==null)return'<td class="text-muted fst-italic">NULL</td>';f=String(f);const p=f.length>120?f.substring(0,120)+"...":f;return`<td>${te(p)}</td>`}).join("")}</tr>`).join("");t.innerHTML=`
<div class="table-responsive">
<table class="table table-sm table-striped table-hover mb-0" style="font-size:12px;">
<thead class="table-light">
<tr>${i}</tr>
</thead>
<tbody>${l}</tbody>
</table>
</div>
`,r.textContent=`${a.length}${a.length>=200?"+":""} row(s), ${s.length} column(s)`}catch(s){console.error("[App] Failed to load table content:",s),t.innerHTML=`<div class="text-danger text-center py-4">Failed to load: ${te(s.message)}</div>`}}async function ia(){try{await zn("lupmis-backup.sqlite3"),we("Database exported successfully")}catch(n){console.error("[App] Export failed:",n),G("Export failed: "+n.message)}}async function la(){try{const n=await Gn(),e=new Blob([JSON.stringify(n,null,2)],{type:"application/json"}),t=URL.createObjectURL(e),r=document.createElement("a");r.href=t,r.download="locations.geojson",r.click(),URL.revokeObjectURL(t),we(`Exported ${n.features.length} location(s)`)}catch(n){console.error("[App] GeoJSON Export failed:",n),G("GeoJSON Export failed: "+n.message)}}async function ca(){try{const n=await Ft(),e=document.getElementById("status-content");e&&(e.innerHTML=`
<table class="table table-sm table-borderless mb-0">
<tbody>
<tr>
<td class="fw-semibold">Ready:</td>
<td><span class="badge ${n.ready?"bg-success":"bg-danger"}">${n.ready?"Yes":"No"}</span></td>
</tr>
<tr>
<td class="fw-semibold">Online:</td>
<td><span class="badge ${Z()?"bg-success":"bg-warning"}">${Z()?"Yes":"Offline"}</span></td>
</tr>
<tr>
<td class="fw-semibold">Database:</td>
<td><code>${n.databasePath||"N/A"}</code></td>
</tr>
<tr>
<td class="fw-semibold">Tables:</td>
<td>${n.tables.map(r=>`<span class="badge bg-secondary me-1">${r}</span>`).join("")}</td>
</tr>
<tr>
<td class="fw-semibold">Locations:</td>
<td><span class="badge bg-primary">${n.locationCount}</span></td>
</tr>
</tbody>
</table>
`),new It(document.getElementById("statusModal")).show()}catch(n){console.error("[App] Failed to get status:",n),G("Failed to get status")}}function qo(n){return n.replace(/^\(+/,"").replace(/\)+$/,"").split(",").map(e=>{const[t,r]=e.trim().split(/\s+/).map(Number);return[t,r]})}function da(n){return{type:"Polygon",coordinates:n.trim().replace(/^POLYGON\s*\(\s*/i,"").replace(/\s*\)$/,"").split("),(").map(qo)}}function ua(n){return{type:"MultiPolygon",coordinates:n.trim().replace(/^MULTIPOLYGON\s*\(\s*/i,"").replace(/\s*\)$/,"").split(")),((").map(o=>o.replace(/^\(+/,"").replace(/\)+$/,"").split("),(").map(qo))}}function ct(n){if(!n)return null;const e=n.trim().toUpperCase();return e.startsWith("MULTIPOLYGON")?ua(n):e.startsWith("POLYGON")?da(n):(console.warn("[App] Unsupported WKT type:",e.substring(0,30)),null)}function pa(n){if(!n?.success||!n?.data?.boundary)return console.warn("[App] API response missing success or boundary data"),null;const{boundary:e,districtid:t,district_name:r}=n.data,o=ct(e);return{type:"FeatureCollection",features:[{type:"Feature",properties:{districtid:t,district_name:r},geometry:o}]}}function io(n){if(!Array.isArray(n)||n.length===0)return null;const e=[];for(const t of n){const r=t.polygon||t.boundary,o=ct(r);if(!o)continue;const s={_layerType:"collector_zone"};for(const[a,i]of Object.entries(t))a==="polygon"||a==="boundary"||(s[a]=i);e.push({type:"Feature",properties:s,geometry:o})}return e.length===0?null:{type:"FeatureCollection",features:e}}async function fa(){const n="district_boundary",t={strokeColor:"#e11d48",strokeWidth:2.5,fillColor:"rgba(225,29,72,0.08)"},r=v?.getLayerGroup(1)||null;function o(a){if(!a)return;const i=a.getLayers(),l=[];i.forEach(c=>{c.get("title")==="District Boundary"&&l.push(c)}),l.forEach(c=>i.remove(c))}function s(a){if(!a||!v)return;const i=a.getSource().getExtent();i&&i[0]!==1/0&&v.getMap().getView().fit(i,{padding:[40,40,40,40],duration:600})}try{const a=await Po(n);if(a){console.log("[App] District boundary loaded from local cache");const i=v?.addGeoJSONLayer(a,"District Boundary",t,r);s(i)}if(Z()&&ve()){console.log("[App] Fetching district boundary from API...");const i=await Yr(),l=pa(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 To(n,l),a&&o(r||v?.getOverlayGroup());const c=v?.addGeoJSONLayer(l,"District Boundary",t,r);s(c),console.log("[App] District boundary loaded from API")}else a||console.log("[App] District boundary not available — offline and no local cache")}catch(a){console.error("[App] Failed to load district boundary:",a)}}async function ha(){const e={strokeColor:"#7c3aed",strokeWidth:1.5,fillColor:"rgba(124,58,237,0.12)"},t=v?.getLayerGroup(1)||null;console.log("[App] loadCollectorZones — adminGroup:",t?t.get("title"):"null");const r={type:"FeatureCollection",features:[]},o=v?.addGeoJSONLayer(r,"Zones",e,t);if(!o){console.warn("[App] Could not create Zones layer");return}o.setVisible(!1),o.on("change:visible",()=>{o.getVisible()&&o.getSource().getFeatures().length===0&&G("No collector zones available locally. Connect to the internet to download zone data.")});function s(a){const i=new oe().readFeatures(a,{featureProjection:"EPSG:3857"});o.getSource().clear(),o.getSource().addFeatures(i)}try{const a=await In();if(a){const i=io(a);i&&(console.log("[App] Collector zones loaded from local cache:",i.features.length,"zones"),s(i))}if(Z()&&ve()){console.log("[App] Fetching collector zones from API...");const i=await Xr();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 Cn(l);const c=io(l);if(!c){console.warn("[App] Could not convert zones to GeoJSON");return}s(c),console.log("[App] Collector zones updated from API:",c.features.length,"zones")}else a||console.log("[App] Collector zones not available — offline and no local cache")}catch(a){console.error("[App] Failed to load collector zones:",a)}}function lo(n){if(!Array.isArray(n)||n.length===0)return null;const e=new Set,t=[];for(const r of n){const o=r.id||r.parcelid||r.parcel_id;if(o!=null){if(e.has(o))continue;e.add(o)}let s=null;if(r.sp_boundary&&r.sp_boundary.type&&r.sp_boundary.coordinates)s={type:r.sp_boundary.type,coordinates:r.sp_boundary.coordinates};else{const l=r.boundary||r.polygon||r.geom||r.wkt;s=ct(l)}if(!s)continue;const a=new Set(["polygon","boundary","geom","wkt","textboundary","sp_boundary"]),i={_layerType:"parcel"};for(const[l,c]of Object.entries(r))a.has(l)||(i[l]=c);t.push({type:"Feature",properties:i,geometry:s})}return t.length===0?null:{type:"FeatureCollection",features:t}}async function ga(){const e={strokeColor:"#0ea5e9",strokeWidth:1.5,fillColor:"rgba(14,165,233,0.12)"},t=v?.getLayerGroup(4)||null;console.log("[App] loadParcels — landUseGroup:",t?t.get("title"):"null");const r={type:"FeatureCollection",features:[]},o=v?.addGeoJSONLayer(r,"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&&G("No parcels available locally. Connect to the internet to download parcel data.")});function s(a){const i=new oe().readFeatures(a,{featureProjection:"EPSG:3857"});o.getSource().clear(),o.getSource().addFeatures(i)}try{const a=await Dn();if(a){const i=lo(a);i&&(console.log("[App] Parcels loaded from local cache:",i.features.length,"parcels"),s(i))}if(Z()&&ve()){console.log("[App] Fetching parcels from API...");const i=await Zr();if(!i?.success||!Array.isArray(i?.data)){console.warn("[App] getDistrictParcels API response invalid:",i);return}const l=i.data;console.log("[App] Parcels from API:",l.length,"entries"),l.length>0&&console.log("[App] First parcel keys:",Object.keys(l[0])),await An(l);const c=lo(l);if(!c){console.warn("[App] Could not convert parcels to GeoJSON");return}s(c),console.log("[App] Parcels updated from API:",c.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 co(n){if(!Array.isArray(n)||n.length===0)return null;const e=["polygon","boundary","geom","wkt","footprint"],t=[];for(const r of n){const o=r.polygon||r.boundary||r.geom||r.wkt||r.footprint;let s;if(typeof o=="object"&&o!==null&&o.type?s=o:s=ct(o),!s)continue;const a={_layerType:"building_footprint"};for(const[i,l]of Object.entries(r))e.includes(i)||typeof l=="object"&&l!==null||(a[i]=l);t.push({type:"Feature",properties:a,geometry:s})}return t.length===0?null:{type:"FeatureCollection",features:t}}async function ma(){const e={strokeColor:"#8b6f47",strokeWidth:1,fillColor:"rgba(139,111,71,0.18)"},t=v?.getLayerGroup(5)||null;console.log("[App] loadBuildingFootprints — physInfraGroup:",t?t.get("title"):"null");const r={type:"FeatureCollection",features:[]},o=v?.addGeoJSONLayer(r,"Building footprints",e,t);if(!o){console.warn("[App] Could not create Building footprints layer");return}o.setVisible(!1),o.on("change:visible",()=>{o.getVisible()&&o.getSource().getFeatures().length===0&&G("No building footprints available locally. Connect to the internet to download footprint data.")});function s(a){const i=new oe().readFeatures(a,{featureProjection:"EPSG:3857"});o.getSource().clear(),o.getSource().addFeatures(i)}try{const a=await Rn();if(a){const i=co(a);i&&(console.log("[App] Building footprints loaded from local cache:",i.features.length,"footprints"),s(i))}if(Z()&&ve()){console.log("[App] Fetching building footprints from API...");const i=await Qr();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 Bn(l);const c=co(l);if(!c){console.warn("[App] Could not convert building footprints to GeoJSON");return}s(c),console.log("[App] Building footprints updated from API:",c.features.length,"footprints")}else a||console.log("[App] Building footprints not available — offline and no local cache")}catch(a){console.error("[App] Failed to load building footprints:",a)}}function Ho(n,e){if(!Array.isArray(n)||n.length===0)return null;const t=new fo,r=new oe,o=["geom","geometry","wkt","polygon","boundary","road","line"],s=[];for(const a of n){const i=a.geom||a.geometry||a.wkt||a.polygon||a.boundary||a.road||a.line;if(!i)continue;let l;try{if(typeof i=="object"&&i!==null&&i.type){s.push({type:"Feature",properties:uo(a,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(r.writeGeometry(l));s.push({type:"Feature",properties:uo(a,o,e),geometry:c})}return s.length===0?null:{type:"FeatureCollection",features:s}}function uo(n,e,t){const r={_layerType:t};for(const[o,s]of Object.entries(n))e.includes(o)||typeof s=="object"&&s!==null||(r[o]=s);return r}async function ya(){const n={strokeColor:"#78716c",strokeWidth:.8,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:[]},r=v?.addGeoJSONLayer(t,"Contours hillshade",n,e);if(!r){console.warn("[App] Could not create Contours hillshade layer");return}if(r.setVisible(!1),r.on("change:visible",()=>{r.getVisible()&&r.getSource().getFeatures().length===0&&G("No Contours hillshade data available. Connect to the internet to download it.")}),!Z()||!ve()){console.log("[App] Contours hillshade not available — offline or server unreachable");return}try{console.log("[App] Fetching contours_hillshade from API...");const o=await ea();if(!o?.success||!Array.isArray(o?.data)){console.warn("[App] getContoursHillshade API response invalid:",o);return}const s=o.data;console.log("[App] Contours hillshade from API:",s.length,"rows"),s.length>0&&console.log("[App] First row keys:",Object.keys(s[0]));const a=Ho(s,"contours_hillshade");if(!a){console.warn("[App] Could not convert contours to GeoJSON");return}const i=new oe().readFeatures(a,{featureProjection:"EPSG:3857"});r.getSource().clear(),r.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 ba(){const e={strokeColor:"#F0F1F0",strokeWidth:1.5,lineCasingColor:"#000000",lineCasingWidth:3.5,fillColor:"rgba(0,0,0,0)"},t=v?.getLayerGroup(5)||null;console.log("[App] loadOSMRoads — group:",t?t.get("title"):"null");const r={type:"FeatureCollection",features:[]},o=v?.addGeoJSONLayer(r,"OSM_roads",e,t);if(!o){console.warn("[App] Could not create OSM_roads layer");return}o.setVisible(!1),o.on("change:visible",()=>{o.getVisible()&&o.getSource().getFeatures().length===0&&G("No OSM roads available locally. Connect to the internet to download them.")});function s(a){const i=Ho(a,"osm_road");if(!i)return console.warn("[App] Could not convert OSM roads to GeoJSON"),0;const l=new oe().readFeatures(i,{featureProjection:"EPSG:3857"});return o.getSource().clear(),o.getSource().addFeatures(l),l.length}try{const a=await $n();if(a){const i=s(a);console.log("[App] OSM_roads loaded from local cache:",i,"features")}if(Z()&&ve()){console.log("[App] Fetching OSM_roads from API...");const i=await ta();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 Nn(l);const c=s(l);console.log("[App] OSM_roads updated from API:",c,"features")}else a||console.log("[App] OSM_roads not available — offline and no local cache")}catch(a){console.error("[App] Failed to load OSM_roads:",a)}}function wa(){v?.addWMSLayer("Biophysical Environment","DEAfrica Coastlines v0.4","https://geoserver.digitalearth.africa/geoserver/wms","coastlines:DEAfrica_Coastlines",{serverType:"geoserver",visible:!1,onlineOnly:!0}),v?.addWMSLayer("Biophysical Environment","DEAfrica Slope (SRTM 30m)","https://ows.digitalearth.africa/wms","srtm_deriv",{serverType:null,style:"style_slope",visible:!1,opacity:.5,zIndex:-50,onlineOnly:!0,attributions:'&copy; <a href="https://www.digitalearthafrica.org/">Digital Earth Africa</a> — SRTM-derived Slope',legendUrl:"https://ows.digitalearth.africa/legend/srtm_deriv/style_slope/legend.png"})}async function va(){const n="layer_categories";function e(t){const r=[...t].sort((o,s)=>s.id-o.id);for(const o of r)v?.addLayerGroup(o.id,o.name,o.description||"");console.log("[App] Created",t.length,"layer groups on map")}try{const t=await Po(n);if(t&&(console.log("[App] Layer categories loaded from local cache:",t.length,"entries"),e(t)),Z()&&ve()){console.log("[App] Fetching layer categories from API...");const r=await Jr();if(!r?.success||!Array.isArray(r?.data)){console.warn("[App] getLayers API response invalid:",r);return}const o=r.data;if(console.log("[App] Layer categories from API:",o.length,"entries"),await To(n,o),t){const s=v?.getOverlayGroup()?.getLayers();if(s){const a=[];s.forEach(i=>{i.get("layerId")!==void 0&&a.push(i)}),a.forEach(i=>s.remove(i))}}e(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(!Z()){console.log("[App] Cannot sync - offline");return}console.log("[App] Sync placeholder - implement based on your backend")}const ee=[],xa={strokeColor:"#e11d48",strokeWidth:2,fillColor:"rgba(225,29,72,0.12)"};function K(n){dt("error",n);const e=document.getElementById("file-import-alert");e&&(e.querySelector(".message-text").textContent=n,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),8e3))}function Ot(n,e,t){const r=Array.isArray(n)?n:[n];let o=0;for(const a of r){if(!a||a.type!=="FeatureCollection"||!a.features?.length)continue;const i=a.fileName?a.fileName.replace(/\.[^/.]+$/,""):e,l=v?.addGeoJSONLayer(a,i,xa);l&&(ee.push(l),o+=a.features.length)}if(o===0){K("No features found in the file.");return}console.log(`[${t}] Added ${o} feature(s) from ${r.length} layer(s)`);const s=ee[ee.length-1];if(s){const a=s.getSource().getExtent();v?.getMap().getView().fit(a,{padding:[50,50,50,50],maxZoom:18})}Bt()}function Bt(){const n=document.getElementById("imported-layers-info");if(!n)return;if(ee.length===0){n.innerHTML="",n.classList.add("d-none");return}n.innerHTML=`
<div class="card">
<div class="card-header bg-primary py-2 d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="bi bi-layers me-2"></i>Imported Layers</h6>
<button type="button" class="btn btn-sm btn-outline-light" id="remove-imported-layers" title="Remove all imported layers">
<i class="bi bi-trash"></i>
</button>
</div>
<ul class="list-group list-group-flush" id="imported-layers-list"></ul>
</div>`;const e=n.querySelector("#imported-layers-list");ee.forEach((t,r)=>{const o=document.createElement("li");o.className="list-group-item d-flex justify-content-between align-items-center py-2",o.innerHTML=`<small class="text-truncate me-2">${te(t.get("title"))}</small>
<span class="d-flex align-items-center gap-2 flex-shrink-0">
<span class="badge" style="background-color:var(--primary);color:var(--primary-foreground);">${t.getSource().getFeatures().length}</span>
<button type="button" class="btn btn-sm btn-outline-danger border-0 p-0 lh-1" data-remove-idx="${r}" title="Remove layer">
<i class="bi bi-x-lg" style="font-size:.75rem;"></i>
</button>
</span>`,e.appendChild(o)}),n.classList.remove("d-none"),n.querySelectorAll("[data-remove-idx]").forEach(t=>{t.addEventListener("click",()=>{_a(Number(t.dataset.removeIdx))})}),n.querySelector("#remove-imported-layers")?.addEventListener("click",()=>{Sa()})}function _a(n){if(n<0||n>=ee.length)return;const e=ee[n],t=v?.getOverlayGroup();t&&t.getLayers().remove(e),ee.splice(n,1),Bt(),console.log("[FileImport] Removed layer:",e.get("title"))}function Sa(){const n=v?.getOverlayGroup();if(n)for(const e of ee)n.getLayers().remove(e);ee.length=0,Bt(),console.log("[FileImport] All imported layers removed")}function Uo(n){const e={};for(const t of n){const r=t.name.split(".").pop().toLowerCase();e[r]=t}return e}async function Wo(n){const e=n.target.files;if(!e||e.length===0)return;const t=200*1024*1024,r=Array.from(e).reduce((o,s)=>o+s.size,0);if(r>t){const o=(r/1048576).toFixed(0);K(`Files too large (${o} MB total). Maximum supported size is 200 MB.`),n.target.value="";return}try{let o,s;const a=Uo(e);if(a.zip){const i=a.zip;s=i.name.replace(/\.zip$/i,""),console.log("[ShpImport] Parsing zip",i.name,"("+(i.size/1024).toFixed(1)+" KB)"),o=await(await ao())(await i.arrayBuffer())}else if(a.shp){s=a.shp.name.replace(/\.shp$/i,"");const l=["dbf","shx","prj"].filter(u=>!a[u]);if(l.length>0){K("Missing required file(s): "+l.map(u=>"."+u).join(", ")+". Please select .shp, .dbf, .shx and .prj together."),n.target.value="";return}const c={};c.shp=await a.shp.arrayBuffer(),c.dbf=await a.dbf.arrayBuffer(),c.prj=await new Response(a.prj).text(),a.cpg&&(c.cpg=await new Response(a.cpg).text()),console.log("[ShpImport] Parsing loose files:",Object.keys(a).map(u=>"."+u).join(", "),"("+(a.shp.size/1024).toFixed(1)+" KB .shp)"),o=await(await ao())(c)}else{K("Please select a .zip or at least a .shp file."),n.target.value="";return}Ot(o,s,"ShpImport")}catch(o){console.error("[ShpImport] Failed:",o),K("Failed to parse shapefile: "+o.message)}n.target.value=""}async function Ko(n){const e=n.target.files?.[0];if(!e)return;const t=200*1024*1024;if(e.size>t){const r=(e.size/1048576).toFixed(0);K(`File too large (${r} MB). Maximum supported size is 200 MB. Consider splitting the file into smaller tiles with ogr2ogr or QGIS.`),n.target.value="";return}try{const r=await e.text();console.log("[GeoJSONImport] Parsing",e.name,"("+(e.size/1024).toFixed(1)+" KB)");const o=JSON.parse(r);let s;if(o.type==="FeatureCollection")s=o;else if(o.type==="Feature")s={type:"FeatureCollection",features:[o]};else if(o.type&&o.coordinates)s={type:"FeatureCollection",features:[{type:"Feature",geometry:o,properties:{}}]};else{K("The file does not contain valid GeoJSON."),n.target.value="";return}const a=e.name.replace(/\.(geo)?json$/i,"");Ot(s,a,"GeoJSONImport")}catch(r){console.error("[GeoJSONImport] Failed:",r);const o=(e.size/(1024*1024)).toFixed(1);K(`Failed to import "${e.name}" (${o} MB): ${r.message}`)}n.target.value=""}async function Vo(n){const e=n.target.files?.[0];if(!e)return;const t=200*1024*1024;if(e.size>t){const r=(e.size/1048576).toFixed(0);K(`File too large (${r} MB). Maximum supported size is 200 MB.`),n.target.value="";return}try{const r=await e.text();console.log("[KMLImport] Parsing",e.name,"("+(e.size/1024).toFixed(1)+" KB)");const s=new on({extractStyles:!1}).readFeatures(r,{featureProjection:"EPSG:3857"});if(!s||s.length===0){K("No features found in the KML file."),n.target.value="";return}const a=new oe,i=JSON.parse(a.writeFeatures(s,{featureProjection:"EPSG:3857",dataProjection:"EPSG:4326"})),l=e.name.replace(/\.kml$/i,"");Ot(i,l,"KMLImport")}catch(r){console.error("[KMLImport] Failed:",r);const o=(e.size/(1024*1024)).toFixed(1);K(`Failed to import "${e.name}" (${o} MB): ${r.message}`)}n.target.value=""}function La(){const n=document.querySelector(".map-container");if(!n)return;let e=0;n.addEventListener("dragenter",t=>{t.preventDefault(),e++,n.classList.add("drag-over")}),n.addEventListener("dragover",t=>{t.preventDefault()}),n.addEventListener("dragleave",t=>{t.preventDefault(),e--,e<=0&&(e=0,n.classList.remove("drag-over"))}),n.addEventListener("drop",t=>{t.preventDefault(),e=0,n.classList.remove("drag-over");const r=t.dataTransfer?.files;if(!r||r.length===0)return;const o=Uo(r),s=Object.keys(o);if(o.zip||o.shp){const a={target:{files:r,value:""}};Object.defineProperty(a.target,"value",{writable:!0}),Wo(a)}else if(o.geojson||o.json){const i={target:{files:[o.geojson||o.json],value:""}};Object.defineProperty(i.target,"value",{writable:!0}),Ko(i)}else if(o.kml){const a={target:{files:[o.kml],value:""}};Object.defineProperty(a.target,"value",{writable:!0}),Vo(a)}else K("Unsupported file type(s): "+s.map(a=>"."+a).join(", ")+". Drop .zip, .shp, .geojson, .json, or .kml files.")}),console.log("[FileImport] Map drop zone initialised")}function te(n){const e=document.createElement("div");return e.textContent=n,e.innerHTML}const ka=50,po={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 dt(n,e){const t=po[n]||po.info;(n==="error"?console.error:n==="warning"?console.warn:console.log)("[App]",e);const o=document.getElementById("message-log");if(!o)return;const s=o.querySelector(".text-muted");s&&s.remove();const a=document.createElement("div");a.className="list-group-item message-log-entry py-2 px-3";const l=new Date().toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"});for(a.innerHTML=`<div class="d-flex align-items-start gap-2"><i class="bi ${t.icon} flex-shrink-0 mt-1" style="color:${t.color}"></i><div class="flex-grow-1 text-break"><small>${te(e)}</small></div><small class="text-muted flex-shrink-0 ms-1">${l}</small></div>`,o.prepend(a);o.children.length>ka;)o.lastElementChild.remove()}function Ta(){const n=document.getElementById("clear-message-log");n&&n.addEventListener("click",()=>{const e=document.getElementById("message-log");e&&(e.innerHTML='<div class="text-center text-muted py-3"><small>No messages yet.</small></div>')})}function G(n){dt("error",n);const e=document.getElementById("error-message");e&&(e.querySelector(".message-text").textContent=n,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),5e3))}function we(n){dt("success",n);const e=document.getElementById("success-message");e&&(e.querySelector(".message-text").textContent=n,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),3e3))}function Pa(n){dt("warning",n);const e=document.getElementById("warning-message");e&&(e.querySelector(".message-text").textContent=n,e.classList.remove("d-none"),setTimeout(()=>e.classList.add("d-none"),5e3))}function Ma(){const n=document.getElementById("fieldwork-mode-toggle");if(!n)return;localStorage.getItem("fieldwork-mode")==="true"&&(document.documentElement.classList.add("fieldwork-mode"),n.checked=!0),n.addEventListener("change",()=>{document.documentElement.classList.toggle("fieldwork-mode",n.checked),localStorage.setItem("fieldwork-mode",n.checked),console.log("[Settings] Fieldwork mode",n.checked?"ON":"OFF")})}function Ca(){const n=document.getElementById("dark-mode-toggle");if(!n)return;function e(r){document.documentElement.classList.toggle("dark-mode",r),document.documentElement.setAttribute("data-bs-theme",r?"dark":"light")}localStorage.getItem("dark-mode")==="true"&&(n.checked=!0,e(!0)),n.addEventListener("change",()=>{e(n.checked),localStorage.setItem("dark-mode",n.checked),console.log("[Settings] Dark mode",n.checked?"ON":"OFF")})}function Ia(){const n=document.getElementById("measurement-system-toggle"),e=document.getElementById("measurement-system-label");if(!n)return;function t(){e&&(e.textContent=n.checked?"Imperial":"Metric")}const r=localStorage.getItem("measurement-system");r==="imperial"&&(n.checked=!0),t(),v?.setScaleBarUnits(r||"metric"),n.addEventListener("change",()=>{const o=n.checked?"imperial":"metric";localStorage.setItem("measurement-system",o),t(),v?.setScaleBarUnits(o),console.log("[Settings] Measurement system:",o)})}function Aa(){const n=document.getElementById("default-basemap-select");if(!n)return;const e=localStorage.getItem("default-basemap")||"topo";n.value=e,n.addEventListener("change",()=>{const t=n.value;localStorage.setItem("default-basemap",t),v?.setBaseMap(t),console.log("[Settings] Default base map:",t)})}function Da(){const n=document.getElementById("tile-cache-stats"),e=document.getElementById("clear-tiles-btn"),t=document.getElementById("offcanvasBottom");if(!n||!e||!t)return;function r(a){return a?a<1024*1024?(a/1024).toFixed(0)+" KB":a<1024*1024*1024?(a/(1024*1024)).toFixed(1)+" MB":(a/(1024*1024*1024)).toFixed(2)+" GB":"0 KB"}let o=null;async function s(){if(o)return o;const a=!!navigator.serviceWorker?.controller;return n.innerHTML=a?'<div class="text-muted fst-italic">Loading…</div>':'<div class="text-muted fst-italic">Initialising service worker…</div>',o=(async()=>{try{const i=await Fr();if(!i){n.innerHTML=`
<div class="text-muted fst-italic">
Tile cache stats unavailable. Try reloading the page if this persists.
</div>`;return}const l=i.totals,c=i.byProvider.filter(f=>f.count>0).map(f=>`
<tr>
<td>${f.label}</td>
<td class="text-end">${f.count.toLocaleString()} / ${f.limit.toLocaleString()}</td>
<td class="text-end">${r(f.estBytes)}</td>
</tr>`).join("");let d="";const u=await Br();if(u&&u.quota>0){const f=(u.usage/u.quota*100).toFixed(1);d=`
<div class="mt-2 text-muted">
Total app storage: ${r(u.usage)} of ${r(u.quota)} available (${f}%)
</div>`}if(l.count===0){n.innerHTML=`
<div class="text-muted">
No tiles cached yet. Pan and zoom the map to start caching tiles automatically.
</div>${d}`,e.disabled=!0;return}n.innerHTML=`
<div class="mb-1">
<strong>${l.count.toLocaleString()}</strong> tiles cached, ~${r(l.estBytes)} on this device
</div>
<table class="table table-sm mb-0" style="font-size:0.85em;">
<thead><tr>
<th>Base map</th>
<th class="text-end">Cached / limit</th>
<th class="text-end">Approx. size</th>
</tr></thead>
<tbody>${c}</tbody>
</table>${d}`,e.disabled=!1}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 Or()?console.log("[Settings] Tile caches cleared"):console.warn("[Settings] Tile-cache clear failed"),await s()}),t.addEventListener("show.bs.offcanvas",s),Dr(()=>{console.log("[Settings] SW controller changed → refreshing tile-cache stats"),s()}),s()}function Fa(){const n=document.getElementById("download-tiles-btn"),e=document.getElementById("offline-download-modal");if(!n||!e)return;const t=It.getOrCreateInstance(e),r=document.getElementById("offline-download-form-view"),o=document.getElementById("offline-download-progress-view"),s=document.getElementById("offline-download-done-view"),a=document.getElementById("offline-download-cancel-btn"),i=document.getElementById("offline-download-start-btn"),l=document.getElementById("offline-download-close-done-btn"),c=document.getElementById("offline-download-close-btn"),d=document.getElementById("offline-basemap-select"),u=document.getElementById("offline-min-zoom"),f=document.getElementById("offline-max-zoom"),p=document.getElementById("offline-ack-check"),h=document.getElementById("offline-estimate-detail"),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"),S=document.getElementById("offline-area-district-info"),L=document.getElementById("offline-progress-bar"),w=document.getElementById("offline-progress-percent"),_=document.getElementById("offline-progress-counts"),P=document.getElementById("offline-progress-ok"),k=document.getElementById("offline-progress-failed"),$=document.getElementById("offline-progress-eta"),q=document.getElementById("offline-done-title"),U=document.getElementById("offline-done-detail");let A=null;function Q(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 W(D){if(!D||D<1e3)return"< 1 s";const H=Math.round(D/1e3);if(H<60)return H+" s";const j=Math.floor(H/60),V=H%60;return j<60?`${j} min ${V} s`:`${Math.floor(j/60)} h ${j%60} min`}function ne(){return g.checked?v?.getCurrentViewExtent()||null:y.checked?v?.getDistrictBoundaryExtent()?.extent||null:b.checked?qr:null}function B(){const D=d.value,H=parseInt(u.value,10),j=parseInt(f.value,10);if(Number.isNaN(H)||Number.isNaN(j)||H>j){h.textContent="Invalid zoom range",m.classList.replace("alert-info","alert-warning"),i.disabled=!0;return}const V=ne();if(!V){h.textContent="Selected area is not available.",m.classList.replace("alert-info","alert-warning"),i.disabled=!0;return}const I=$o[D]?.maxZoom??19,R=Math.min(j,I),Y=$r(V,H,R),ut=Hr(Y);let ae="";R<j&&(ae=`<br><span class="text-warning">Zoom ${j} is above this provider's max (${I}); will clamp to ${I}.</span>`),Y>8e3&&(ae+='<br><span class="text-warning">More than 8 000 tiles — exceeds the per-provider cache limit. Earlier tiles will be evicted as new ones arrive.</span>'),h.innerHTML=`<strong>${Y.toLocaleString()}</strong> tiles · ~${Q(ut)}`+ae,m.classList.toggle("alert-warning",!!ae),m.classList.toggle("alert-info",!ae),i.disabled=!p.checked||Y===0}function re(){v?.getCurrentViewExtent()?E.textContent=" · ready":E.textContent="",v?.getDistrictBoundaryExtent()?(S.textContent="",y.disabled=!1):(S.textContent=" (not loaded — connect online to fetch)",y.disabled=!0,y.checked&&(g.checked=!0))}function Ie(){r.classList.remove("d-none"),o.classList.add("d-none"),s.classList.add("d-none"),i.classList.remove("d-none"),a.classList.remove("d-none"),a.textContent="Cancel",l.classList.add("d-none"),c.disabled=!1,p.checked=!1,i.disabled=!0,A=null}n.addEventListener("click",()=>{Ie(),re(),B(),t.show()}),d.addEventListener("change",B),u.addEventListener("input",B),f.addEventListener("input",B),g.addEventListener("change",B),y.addEventListener("change",B),b.addEventListener("change",B),p.addEventListener("change",B),i.addEventListener("click",async()=>{const D=d.value,H=parseInt(u.value,10),j=parseInt(f.value,10),V=ne();if(!V)return;r.classList.add("d-none"),o.classList.remove("d-none"),i.classList.add("d-none"),a.textContent="Cancel download",c.disabled=!0,L.style.width="0%",L.setAttribute("aria-valuenow","0"),w.textContent="0%",_.textContent="0 of 0 tiles",P.textContent="0",k.textContent="0",$.textContent="—",A=new Gr({baseMap:D,extent3857:V,minZoom:H,maxZoom:j,onProgress:R=>{if(R.total>0){const Y=Math.min(100,Math.round(R.done/R.total*100));L.style.width=Y+"%",L.setAttribute("aria-valuenow",String(Y)),w.textContent=Y+"%",_.textContent=`${R.done.toLocaleString()} of ${R.total.toLocaleString()} tiles`}P.textContent=R.ok.toLocaleString(),k.textContent=R.failed.toLocaleString(),$.textContent=R.etaMs!=null?W(R.etaMs):"—"}});let I;try{I=await A.start()}catch(R){console.error("[OfflineDownload] failed:",R),I={phase:"error",done:0,total:0,ok:0,failed:0}}o.classList.add("d-none"),s.classList.remove("d-none"),a.classList.add("d-none"),l.classList.remove("d-none"),c.disabled=!1,I.phase==="cancelled"?(q.textContent="Download cancelled",U.innerHTML=`Stopped after <strong>${I.done.toLocaleString()}</strong> of ${I.total.toLocaleString()} tiles.<br>${I.ok.toLocaleString()} fetched · ${I.failed.toLocaleString()} failed.`):I.phase==="error"?(q.textContent="Download failed",U.textContent="See console for details."):(q.textContent="Download complete",U.innerHTML=`<strong>${I.ok.toLocaleString()}</strong> tiles cached`+(I.failed>0?`, ${I.failed.toLocaleString()} failed`:"")+`.<br>Took ${W(I.elapsedMs)}.`)}),a.addEventListener("click",()=>{A&&A.cancel()}),e.addEventListener("hidden.bs.modal",()=>{A&&A.cancel(),Ie()})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",so):so();
//# sourceMappingURL=index-B4XzHtZX.js.map