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>
637 lines
195 KiB
JavaScript
637 lines
195 KiB
JavaScript
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;">×</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;">×</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;">×</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;">×</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;">×</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;">×</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">×</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;">×</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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}getLayerGroup(e){let t=null;return this.overlayGroup.getLayers().forEach(r=>{r.get("layerId")===e&&(t=r)}),t}getLayerGroupByTitle(e){let t=null;return this.overlayGroup.getLayers().forEach(r=>{r.get("title")===e&&(t=r)}),t}getOverlayGroup(){return this.overlayGroup}getMap(){return this.map}getCurrentViewExtent(){const e=this.map.getView(),t=this.map.getSize();return t?e.calculateExtent(t):null}getDistrictBoundaryExtent(){let e=null;const t=r=>{r.getLayers().forEach(o=>{if(o.getLayers)t(o);else if(o.get("title")==="District Boundary"){const 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")&<()}}),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:'© <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
|