UPN-grid layer: - src/database.js — new upn_grid SQLocal table (id, districtid, upn_prefix, geometry_wkt) + saveUpnGrid / getLocalUpnGrid; cache-once-per-district. - src/remotedb.js — getUpnGrid → get_upn_grid_per_district.php. - main.js loadUpnGrid + upnGridToGeoJSON in the Administration group, with a zoom-aware style: white casing under a bolder violet dashed stroke (visible against parcels) and upn_prefix labels rendered only when resolution ≤ 7 m/px (≈ scale ≤ 1:25,000). - main.js click handler: single click on a UPN-grid cell opens an info popup showing the upn_prefix. External-dataset import → staging → upload (client-side complete): - src/database.js — external_imports + external_import_features tables, plus createExternalImport / addExternalImportFeatures / updateExternalImport / getExternalImport / getExternalImportFeatures / listExternalImports / remapImportedFeatureProperties / deleteExternalImport. Status enum: imported/mapped/other/uploading/ submitted/migrated/failed (aligned with the database team's staged- upload model — lu_parcels_upload_tmp + supervisor review). - src/import-detect.js — pure helpers: detectTargetType(), autoMapFields(), applyFieldMapping(), listSourceFields() + TARGET_TYPES / TARGET_FIELDS registries. - src/import-modal.js — Bootstrap mapping modal: target dropdown, field-rename table, three actions (Cancel / Save / Save + Upload now). - main.js — stageImport hooked into addImportedGeoJSON (the single convergence point for shp/GeoJSON/KML drops); handleImportModalResult applies the mapping in one transaction; runUpload builds the real payload (district_id + api_token from remotePost, user_id_upload from SSO session, per-feature client_uuid/geom/props) and currently logs + toasts — the upload_<target>.php endpoints are not yet live. - index.html — #importMappingModal markup. - MapView._decorateLayerListItem — import-state chip (Upload N / spinner / ✓ submitted / ✓ live / N errors) dispatching lupmis:import-chip-click; src/styles/layerswitcher.css — chip variants. GIS export from Area / Circle Analysis popups: - MapView._showAnalysisPopup now accepts an exportContext (clipGeometry + parcelFeatures + zoneFeatures + otherByLayer) and renders an "Export GIS" button next to "Export PDF". Click dispatches lupmis:export-gis. - index.html — #exportGisModal markup. - src/export-gis-modal.js — Bootstrap modal: format toggle (GeoJSON default / Shapefile / KML), filename, field-rename table with SHP 10-char DBF warning. - src/gis-export.js — writers: GeoJSON via Blob, KML via OL KMLFormat, Shapefile via shp-write (with DBF-safe name sanitiser). - Adds shp-write@0.3.2 dependency. MapView style options: - addGeoJSONLayer now accepts strokeDash for line-dash patterns (used by the UPN-grid layer and available for any future contextual overlay). Service Worker v9 → v10 to evict the stale shell/module caches on the next deploy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
905 lines
264 KiB
JavaScript
905 lines
264 KiB
JavaScript
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/pdf-export-BG6jqfsR.js","assets/jspdf-BTK_8o8D.js","assets/openlayers-D8ReJJOp.js","assets/openlayers-BtPuoxOl.css"])))=>i.map(i=>d[i]);
|
||
import{_ as Ct,h as M,F as A,j as I,k as ue,m as bo,b as G,V as O,L as pe,D as Pe,P as it,Q as st,n as de,U as Te,M as Io,W as Tr,X as Q,Y as Lr,S as kr,G as Ir,Z as Pr,o as ut,O as _e,$ as nt,a0 as Et,a1 as Me,A as Mr,T as ne,a2 as Le,a3 as Po,a4 as ie,a5 as Mo,e as Ar,u as Gt,s as Cr,a6 as kn,a7 as wo}from"./openlayers-D8ReJJOp.js";import{M as ct}from"./bootstrap-D1-uvFxm.js";import{o as Fr,a as Dr,b as Or,c as Rr,d as ao,e as Ao,f as et,g as Nr,h as ve,i as $r,j as Br}from"./ol-ext-P1ircg-B.js";import{r as Gr}from"./shpjs-iyObTF9J.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 a of o)if(a.type==="childList")for(const i of a.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function t(o){const a={};return o.integrity&&(a.integrity=o.integrity),o.referrerPolicy&&(a.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?a.credentials="include":o.crossOrigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function r(o){if(o.ep)return;o.ep=!0;const a=t(o);fetch(o.href,a)}})();const Co="function",$e="64e10b34-2bf7-4616-9668-f99de5aa046e",jr="get",qr="has",zr="set",{isArray:wt}=Array;let{SharedArrayBuffer:xt,window:Ur}=globalThis,{notify:In,wait:Pn,waitAsync:St}=Atomics,Mn=null;St||(St=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 xt(4)}catch{xt=ArrayBuffer;const e=new WeakMap;if(Ur){const t=new Map,{prototype:{postMessage:r}}=Worker,o=a=>{const i=a.data?.[$e];if(!wt(i)){a.stopImmediatePropagation();const{id:s,sb:l}=i;t.get(s)(l)}};Mn=function(a,...i){const s=a?.[$e];if(wt(s)){const[l,c]=s;e.set(c,l),this.addEventListener("message",o)}return r.call(this,a,...i)},St=a=>({value:new Promise(i=>{t.set(e.get(a),i)}).then(i=>{t.delete(e.get(a)),e.delete(a);for(let s=0;s<i.length;s++)a[s]=i[s];return"ok"})})}else{const t=(r,o)=>({[$e]:{id:r,sb:o}});In=r=>{postMessage(t(e.get(r),r))},addEventListener("message",r=>{const o=r.data?.[$e];if(wt(o)){const[a,i]=o;e.set(i,a)}})}}/*! (c) Andrea Giammarchi - ISC */const{Int32Array:io,Map:Fo,Uint16Array:so}=globalThis,{BYTES_PER_ELEMENT:Do}=io,{BYTES_PER_ELEMENT:Hr}=so,Wr=(n,e,t)=>{for(;Pn(n,0,0,e)==="timed-out";)t()},lo=new WeakSet,jt=new WeakMap,Kr={value:{then:n=>n()}};let Xr=0;const _o=(n,{parse:e=JSON.parse,stringify:t=JSON.stringify,transform:r,interrupt:o}=JSON)=>{if(!jt.has(n)){const a=Mn||n.postMessage,i=(p,...h)=>a.call(n,{[$e]:h},{transfer:p}),s=typeof o===Co?o:o?.handler,l=o?.delay||42,c=new TextDecoder("utf-16"),d=(p,h)=>p?St(h,0):(s?Wr(h,l,s):Pn(h,0),Kr);let u=!1;jt.set(n,new Proxy(new Fo,{[qr]:(p,h)=>typeof h=="string"&&!h.startsWith("_"),[jr]:(p,h)=>h==="then"?null:((...f)=>{const y=Xr++;let g=new io(new xt(Do*2)),m=[];lo.has(f.at(-1)||m)&&lo.delete(m=f.pop()),i(m,y,g,h,r?f.map(r):f);const b=n!==globalThis;let _=0;return u&&b&&(_=setTimeout(console.warn,1e3,`💀🔒 - Possible deadlock if proxy.${h}(...args) is awaited`)),d(b,g).value.then(()=>{clearTimeout(_);const S=g[1];if(!S)return;const T=Hr*S;return g=new io(new xt(T+T%Do)),i([],y,g),d(b,g).value.then(()=>e(c.decode(new so(g.buffer).slice(0,S))))})}),[zr](p,h,f){const y=typeof f;if(y!==Co)throw new Error(`Unable to assign ${h} as ${y}`);if(!p.size){const g=new Fo;n.addEventListener("message",async m=>{const b=m.data?.[$e];if(wt(b)){m.stopImmediatePropagation();const[_,S,...T]=b;let v;if(T.length){const[x,P]=T;if(p.has(x)){u=!0;try{const k=await p.get(x)(...P);if(k!==void 0){const q=t(r?r(k):k);g.set(_,q),S[1]=q.length}}catch(k){v=k}finally{u=!1}}else v=new Error(`Unsupported action: ${x}`);S[0]=1}else{const x=g.get(_);g.delete(_);for(let P=new so(S.buffer),k=0;k<x.length;k++)P[k]=x.charCodeAt(k)}if(In(S,0),v)throw v}})}return!!p.set(h,f)}}))}return jt.get(n)};_o.transfer=(...n)=>(lo.add(n),n);function Oo(){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 An(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 a=[];let i=!1;for(;!i;){const d=await o.read();d.value&&a.push(d.value),i=d.done}const s=a.reduce((d,u)=>d+u.length,0),l=new Uint8Array(s);let c=0;return a.forEach(d=>{l.set(d,c),c+=d.length}),l.buffer}}else return t}class Tt{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 Ct(async()=>{const{default:a}=await import("./index-DTMgZTfd.js");return{default:a}},[]);this.sqlite3InitModule=o}this.sqlite3||(this.sqlite3=await this.sqlite3InitModule()),this.db&&await this.destroy(),this.db=new this.sqlite3.oo1.DB(t,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 a of e){let i=o.get(a.sql);if(!i){const c=r.prepare(a.sql);o.set(a.sql,c),i=c}a.params?.length&&i.bind(a.params);let s=[],l=[];for(;i.step();)s=i.getColumnNames([]),l.push(i.get([]));t.push({columns:s,rows:l}),i.reset()}}finally{o.forEach(a=>{a.finalize()})}}),t}async isDatabasePersisted(){return!1}async getDatabaseSizeBytes(){const t=(await this.exec({sql:`SELECT page_count * page_size AS size
|
||
FROM pragma_page_count(), pragma_page_size()`,method:"get"}))?.rows?.[0];if(typeof t!="number")throw new Error("Failed to query database size");return t}async createFunction(e){if(!this.db)throw new Error("Driver not initialized");switch(e.type){case"callback":case"scalar":this.db.createFunction({name:e.name,xFunc:(t,...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 An(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,a,i)=>{this.writeCallbacks.forEach(s=>{s({table:a,rowid:i,operation:e[r]})})},0)}closeDb(){this.db&&(this.db.close(),this.db=void 0)}}function Vr(n,e,t){let r,o,a,i,s,l,c=0,d=!1,u=!1,p=!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,a=u?Math.max(Number(t.maxWait)||0,e):0,p="trailing"in t?!!t.trailing:p);function h(v){const x=r,P=o;return r=o=void 0,c=v,i=n.apply(P,x),i}function f(v){return c=v,s=setTimeout(m,e),d?h(v):i}function y(v){const x=v-(l??0),P=v-c,k=e-x;return u?Math.min(k,a-P):k}function g(v){const x=v-(l??0),P=v-c;return l===void 0||x>=e||x<0||u&&P>=a}function m(){const v=Date.now();if(g(v))return b(v);s=setTimeout(m,y(v))}function b(v){return s=void 0,p&&r?h(v):(r=o=void 0,i)}function _(){s!==void 0&&clearTimeout(s),c=0,r=l=o=s=void 0}function S(){return s===void 0?i:b(Date.now())}function T(){const v=Date.now(),x=g(v);if(r=arguments,o=this,l=v,x){if(s===void 0)return f(l);if(u)return s=setTimeout(m,e),h(l)}return s===void 0&&(s=setTimeout(m,e)),i}return T.cancel=_,T.flush=S,T}function _t(){return crypto.randomUUID()}function Cn(n,e){switch(n){case"session":case":sessionStorage:":let t=sessionStorage._sqlocal_session_key;return t||(t=_t(),sessionStorage._sqlocal_session_key=t),`session:${t}`;case"local":case":localStorage:":return"local";case":memory:":return`memory:${e}`;default:return`path:${n}`}}class pt{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:Oo()}),Object.defineProperty(this,"transactionMutex",{enumerable:!0,configurable:!0,writable:!0,value:Oo()}),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 Tt,await this.driver.init(this.config)}const a=Cn(this.config.databasePath,this.config.clientKey);this.reinitChannel=new BroadcastChannel(`_sqlocal_reinit_(${a})`),this.reinitChannel.onmessage=i=>{const s=i.data;if(this.config.clientKey!==s.clientKey)switch(s.type){case"reinit":this.init(s.reason);break;case"close":this.driver.destroy();break}},this.config.reactive&&(this.effectsChannel=new BroadcastChannel(`_sqlocal_effects_(${a})`),this.driver.onWrite(async i=>{this.dirtyTables.add(i.table),await this.transactionMutex.lock(),this.emitEffectsDebounced(),await this.transactionMutex.unlock()})),await Promise.all(Array.from(this.userFunctions.values()).map(i=>this.initUserFunction(i))),await this.execInitStatements(),this.emitMessage({type:"event",event:"connect",reason:o})}catch(a){this.emitMessage({type:"error",error:a,queryKey:null}),await this.destroy()}finally{await this.initMutex.unlock()}}}}),Object.defineProperty(this,"postMessage",{enumerable:!0,configurable:!0,writable:!0,value:async(o,a)=>{const i=o instanceof MessageEvent?o.data:o;switch(await this.initMutex.lock(),i.type){case"config":this.editConfig(i);break;case"query":case"batch":case"transaction":this.exec(i);break;case"function":this.createUserFunction(i);break;case"getinfo":this.getDatabaseInfo(i);break;case"import":this.importDb(i);break;case"export":this.exportDb(i);break;case"delete":this.deleteDb(i);break;case"destroy":this.destroy(i);break}await this.initMutex.unlock()}}),Object.defineProperty(this,"emitMessage",{enumerable:!0,configurable:!0,writable:!0,value:(o,a=[])=>{this.onmessage&&this.onmessage(o,a)}}),Object.defineProperty(this,"emitEffects",{enumerable:!0,configurable:!0,writable:!0,value:()=>{!this.effectsChannel||this.dirtyTables.size===0||(this.effectsChannel.postMessage({type:"effects",tables:[...this.dirtyTables]}),this.dirtyTables.clear())}}),Object.defineProperty(this,"emitEffectsDebounced",{enumerable:!0,configurable:!0,writable:!0,value:Vr(()=>this.emitEffects(),32,{maxWait:180})}),Object.defineProperty(this,"editConfig",{enumerable:!0,configurable:!0,writable:!0,value:o=>{this.config=o.config,this.init("initial")}}),Object.defineProperty(this,"exec",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{try{const a={type:"data",queryKey:o.queryKey,data:[]};switch(o.type){case"query":const i=this.transactionKey!==null&&this.transactionKey===o.transactionKey;try{i||await this.transactionMutex.lock();const s=await this.driver.exec(o);a.data.push(s)}finally{i||await this.transactionMutex.unlock()}break;case"batch":try{await this.transactionMutex.lock();const s=await this.driver.execBatch(o.statements);a.data.push(...s)}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 s=o.action==="commit"?"COMMIT":"ROLLBACK";await this.driver.exec({sql:s}),this.transactionKey=null,await this.transactionMutex.unlock()}break}this.emitMessage(a)}catch(a){this.emitMessage({type:"error",error:a,queryKey:o.queryKey})}}}),Object.defineProperty(this,"execInitStatements",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{if(this.config.onInitStatements)for(let o of this.config.onInitStatements)await this.driver.exec(o)}}),Object.defineProperty(this,"getDatabaseInfo",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{try{this.emitMessage({type:"info",queryKey:o.queryKey,info:{databasePath:this.config.databasePath,storageType:this.driver.storageType,databaseSizeBytes:await this.driver.getDatabaseSizeBytes(),persisted:await this.driver.isDatabasePersisted()}})}catch(a){this.emitMessage({type:"error",queryKey:o.queryKey,error:a})}}}),Object.defineProperty(this,"createUserFunction",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{functionName:a,functionType:i,queryKey:s}=o;let l;if(this.userFunctions.has(a)){this.emitMessage({type:"error",error:new Error(`A user-defined function with the name "${a}" has already been created for this SQLocal instance.`),queryKey:s});return}switch(i){case"callback":l={type:i,name:a,func:(...c)=>{this.emitMessage({type:"callback",name:a,args:c})}};break;case"scalar":l={type:i,name:a,func:this.proxy[`_sqlocal_func_${a}`]};break;case"aggregate":l={type:i,name:a,func:{step:this.proxy[`_sqlocal_func_${a}_step`],final:this.proxy[`_sqlocal_func_${a}_final`]}};break}try{await this.initUserFunction(l),this.emitMessage({type:"success",queryKey:s})}catch(c){this.emitMessage({type:"error",error:c,queryKey:s})}}}),Object.defineProperty(this,"initUserFunction",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{await this.driver.createFunction(o),this.userFunctions.set(o.name,o)}}),Object.defineProperty(this,"importDb",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{queryKey:a,database:i}=o;let s=!1;try{await this.driver.import(i),this.driver.storageType==="memory"&&await this.execInitStatements()}catch(l){this.emitMessage({type:"error",error:l,queryKey:a}),s=!0}finally{this.driver.storageType!=="memory"&&await this.init("overwrite")}s||this.emitMessage({type:"success",queryKey:a})}}),Object.defineProperty(this,"exportDb",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{queryKey:a}=o;try{const{name:i,data:s}=await this.driver.export();this.emitMessage({type:"buffer",queryKey:a,bufferName:i,buffer:s},[s])}catch(i){this.emitMessage({type:"error",error:i,queryKey:a})}}}),Object.defineProperty(this,"deleteDb",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{const{queryKey:a}=o;let i=!1;try{await this.driver.clear()}catch(s){this.emitMessage({type:"error",error:s,queryKey:a}),i=!0}finally{await this.init("delete")}i||this.emitMessage({type:"success",queryKey:a})}}),Object.defineProperty(this,"destroy",{enumerable:!0,configurable:!0,writable:!0,value:async o=>{await this.driver.exec({sql:"PRAGMA optimize"}),await this.driver.destroy(),this.effectsChannel&&(this.emitEffectsDebounced.flush(),this.effectsChannel.close(),this.effectsChannel=void 0),this.reinitChannel&&(this.reinitChannel.close(),this.reinitChannel=void 0),o&&this.emitMessage({type:"success",queryKey:o.queryKey})}});const r=typeof WorkerGlobalScope<"u"&&globalThis instanceof WorkerGlobalScope?_o(globalThis):globalThis;this.proxy=r,this.driver=e}}function Lt(n,...e){return{sql:n.join("?"),params:e}}function Yr(n){return!n.some(e=>!Array.isArray(e))}function qt(n,e){let t;return Yr(n)?t=n:t=[n],t.map(r=>{const o={};return e.forEach((a,i)=>{o[a]=r[i]}),o})}function Jr(n){return typeof n=="object"&&n!==null&&"getSQL"in n&&typeof n.getSQL=="function"}function Zr(n){return typeof n=="object"&&n!==null&&"sql"in n&&typeof n.sql=="string"&&"params"in n}function Ro(n){if(typeof n=="function"&&(n=n(Lt)),Jr(n))try{if(!("toSQL"in n&&typeof n.toSQL=="function"))throw 1;const r=n.toSQL();if(!Zr(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 No(n,e){let t;return typeof n=="string"?t={sql:n,params:e}:t=Lt(n,...e),t}async function ft(n,e,t,r){return!e&&"locks"in navigator?navigator.locks.request(`_sqlocal_mutation_(${t.databasePath})`,{mode:n},r):r()}class $o extends Tt{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 Ct(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 Tt;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 Fn,Dn;class Qr{constructor(e){Object.defineProperty(this,"config",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"clientKey",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"processor",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"isDestroyed",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"bypassMutationLock",{enumerable:!0,configurable:!0,writable:!0,value:!1}),Object.defineProperty(this,"transactionQueryKeyQueue",{enumerable:!0,configurable:!0,writable:!0,value:[]}),Object.defineProperty(this,"userCallbacks",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"queriesInProgress",{enumerable:!0,configurable:!0,writable:!0,value:new Map}),Object.defineProperty(this,"proxy",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"reinitChannel",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"effectsChannel",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),Object.defineProperty(this,"processMessageEvent",{enumerable:!0,configurable:!0,writable:!0,value:c=>{const d=c instanceof MessageEvent?c.data:c,u=this.queriesInProgress;switch(d.type){case"success":case"data":case"buffer":case"info":case"error":if(d.queryKey&&u.has(d.queryKey)){const[h,f]=u.get(d.queryKey);d.type==="error"?f(d.error):h(d),u.delete(d.queryKey)}else if(d.type==="error")throw d.error;break;case"callback":const p=this.userCallbacks.get(d.name);p&&p(...d.args??[]);break;case"event":this.config.onConnect?.(d.reason);break}}}),Object.defineProperty(this,"createQuery",{enumerable:!0,configurable:!0,writable:!0,value:async c=>ft("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=_t();switch(c.type){case"import":this.processor.postMessage({...c,queryKey:d},[c.database]);break;default:this.processor.postMessage({...c,queryKey:d});break}return new Promise((u,p)=>{this.queriesInProgress.set(d,[u,p])})})}),Object.defineProperty(this,"broadcast",{enumerable:!0,configurable:!0,writable:!0,value:c=>{this.reinitChannel.postMessage(c)}}),Object.defineProperty(this,"exec",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d,u="all",p)=>{const h=await this.createQuery({type:"query",transactionKey:p,sql:c,params:d,method:u}),f={rows:[],columns:[]};return h.type==="data"&&(f.rows=h.data[0]?.rows??[],f.columns=h.data[0]?.columns??[]),f}}),Object.defineProperty(this,"execBatch",{enumerable:!0,configurable:!0,writable:!0,value:async c=>{const d=await this.createQuery({type:"batch",statements:c}),u=new Array(c.length).fill({rows:[],columns:[]});return d.type==="data"&&d.data.forEach((p,h)=>{u[h]=p}),u}}),Object.defineProperty(this,"sql",{enumerable:!0,configurable:!0,writable:!0,value:async(c,...d)=>{const u=No(c,d),{rows:p,columns:h}=await this.exec(u.sql,u.params,"all");return qt(p,h)}}),Object.defineProperty(this,"batch",{enumerable:!0,configurable:!0,writable:!0,value:async c=>{const d=c(Lt);return(await this.execBatch(d)).map(({rows:p,columns:h})=>qt(p,h))}}),Object.defineProperty(this,"beginTransaction",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{const c=_t();await this.createQuery({type:"transaction",transactionKey:c,action:"begin"});const d=async f=>{const y=Ro(f);if(y.exec)return this.transactionQueryKeyQueue.push(c),y.exec();const{rows:g,columns:m}=await this.exec(y.sql,y.params,"all",c);return qt(g,m)};return{query:d,sql:async(f,...y)=>{const g=No(f,y);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=>ft("exclusive",!1,this.config,async()=>{let d;this.bypassMutationLock=!0;try{d=await this.beginTransaction();const u=await c({sql:d.sql,query:d.query});return await d.commit(),u}catch(u){throw await d?.rollback(),u}finally{this.bypassMutationLock=!1}})}),Object.defineProperty(this,"reactiveQuery",{enumerable:!0,configurable:!0,writable:!0,value:c=>{let d=[],u=!1,p=!1,h=0;const f=Ro(c),y=new Set,g=new Set,m=new Set,b=async()=>{try{const S=++h;if(y.size===0){const v=await this.sql("SELECT name, wr FROM tables_used(?) WHERE type = 'table'",f.sql),x=new Set,P=new Set;if(v.forEach(k=>{typeof k.name=="string"&&(k.wr?P.add(k.name):x.add(k.name))}),x.size===0)throw new Error("The passed SQL does not read any tables.");if(Array.from(P).some(k=>x.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.");x.forEach(k=>y.add(k))}const T=f.exec?await f.exec():await this.sql(f.sql,...f.params);S===h&&(d=T,u=!0,g.forEach(v=>v(d)))}catch(S){m.forEach(T=>{T(S instanceof Error?S:new Error(String(S)))})}},_=S=>{S.data.tables.some(T=>y.has(T))&&b()};return{get value(){return d},subscribe:(S,T)=>{if(!this.effectsChannel)throw new Error('This SQLocal instance is not configured for reactive queries. Set the "reactive" option to enable them.');return T||(T=v=>{throw v}),g.add(S),m.add(T),p?u&&S(d):(this.effectsChannel.addEventListener("message",_),p=!0,b()),{unsubscribe:()=>{g.delete(S),m.delete(T),g.size===0&&(this.effectsChannel?.removeEventListener("message",_),p=!1)}}}}}}),Object.defineProperty(this,"createCallbackFunction",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{await this.createQuery({type:"function",functionName:c,functionType:"callback"}),this.userCallbacks.set(c,d)}}),Object.defineProperty(this,"createScalarFunction",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{const u=`_sqlocal_func_${c}`,p=()=>{this.proxy[u]=d};this.proxy===globalThis&&p(),await this.createQuery({type:"function",functionName:c,functionType:"scalar"}),this.proxy!==globalThis&&p()}}),Object.defineProperty(this,"createAggregateFunction",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{const u=`_sqlocal_func_${c}`,p=()=>{this.proxy[`${u}_step`]=d.step,this.proxy[`${u}_final`]=d.final};this.proxy===globalThis&&p(),await this.createQuery({type:"function",functionName:c,functionType:"aggregate"}),this.proxy!==globalThis&&p()}}),Object.defineProperty(this,"getDatabaseInfo",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{const c=await this.createQuery({type:"getinfo"});if(c.type==="info")return c.info;throw new Error("The database failed to return valid information.")}}),Object.defineProperty(this,"getDatabaseFile",{enumerable:!0,configurable:!0,writable:!0,value:async()=>{const c=await this.createQuery({type:"export"});if(c.type==="buffer")return new File([c.buffer],c.bufferName,{type:"application/x-sqlite3"});throw new Error("The database failed to export.")}}),Object.defineProperty(this,"overwriteDatabaseFile",{enumerable:!0,configurable:!0,writable:!0,value:async(c,d)=>{await ft("exclusive",!1,this.config,async()=>{try{this.broadcast({type:"close",clientKey:this.clientKey});const u=await An(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 ft("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,Fn,{enumerable:!0,configurable:!0,writable:!0,value:()=>{this.destroy()}}),Object.defineProperty(this,Dn,{enumerable:!0,configurable:!0,writable:!0,value:async()=>{await this.destroy()}});const t=typeof e=="string"?{databasePath:e}:e,{onInit:r,onConnect:o,processor:a,...i}=t,{databasePath:s}=i;this.config=t,this.clientKey=_t();const l=Cn(s,this.clientKey);if(this.reinitChannel=new BroadcastChannel(`_sqlocal_reinit_(${l})`),i.reactive&&(this.effectsChannel=new BroadcastChannel(`_sqlocal_effects_(${l})`)),typeof a<"u")this.processor=a;else if(s==="local"||s===":localStorage:"){const c=new $o("local");this.processor=new pt(c)}else if(s==="session"||s===":sessionStorage:"){const c=new $o("session");this.processor=new pt(c)}else if(typeof globalThis.Worker<"u"&&s!==":memory:")this.processor=new Worker(new URL("/assets/worker-CuIBOSaM.js",import.meta.url),{type:"module"});else{const c=new Tt;this.processor=new pt(c)}this.processor instanceof pt?(this.processor.onmessage=c=>this.processMessageEvent(c),this.proxy=globalThis):(this.processor.addEventListener("message",this.processMessageEvent),this.proxy=_o(this.processor)),this.processor.postMessage({type:"config",config:{...i,clientKey:this.clientKey,onInitStatements:r?.(Lt)??[]}})}}Fn=Symbol.dispose,Dn=Symbol.asyncDispose;const vo="lupmis2.db",ea="lupmis-db-sync",On=new Qr(vo),{sql:w}=On;console.log("[Database] SQLocal instance created for:",vo);const Rn=new BroadcastChannel(ea);let Nn=!1,$n,Bn;const Bo=new Promise((n,e)=>{$n=n,Bn=e}),kt=new Set;function ta(n){return kt.add(n),()=>kt.delete(n)}Rn.onmessage=n=>{const{type:e,payload:t}=n.data;if(e==="DB_CHANGE")for(const r of kt)try{r(t)}catch(o){console.error("[Database] Change listener error:",o)}};function se(n,e,t=null){Rn.postMessage({type:"DB_CHANGE",payload:{table:n,action:e,id:t,timestamp:Date.now()}});for(const r of kt)try{r({table:n,action:e,id:t,timestamp:Date.now(),local:!0})}catch(o){console.error("[Database] Change listener error:",o)}}async function oa(){try{console.log("[Database] Initializing schema...");const n=await w`SELECT sqlite_version() as version`;console.log("[Database] SQLite version:",n[0]?.version),console.log("[Database] Creating locations table..."),await w`
|
||
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 w`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 w`
|
||
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 w`
|
||
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 w`
|
||
CREATE TABLE IF NOT EXISTS collector_zones (
|
||
id INTEGER PRIMARY KEY,
|
||
zone_name TEXT,
|
||
geometry_wkt TEXT,
|
||
properties TEXT,
|
||
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
`,console.log("[Database] Creating parcels table...");try{const r=await w`PRAGMA table_info(parcels)`;r.length>0&&!r.some(o=>o.name==="upn")&&(console.log("[Database] Migrating parcels table to lu_parcels structure (dropping old cache)..."),await w`DROP TABLE parcels`)}catch(r){console.warn("[Database] parcels migration check failed:",r)}await w`
|
||
CREATE TABLE IF NOT EXISTS parcels (
|
||
id INTEGER PRIMARY KEY,
|
||
upn TEXT,
|
||
style INTEGER,
|
||
landuse TEXT,
|
||
zone_code TEXT,
|
||
zone_name TEXT,
|
||
sector TEXT,
|
||
block TEXT,
|
||
parcel_no TEXT,
|
||
prop_no TEXT,
|
||
st_name TEXT,
|
||
prop_add TEXT,
|
||
fac_name TEXT,
|
||
min_height INTEGER,
|
||
max_height INTEGER,
|
||
eff_date TEXT,
|
||
lp_name TEXT,
|
||
locality TEXT,
|
||
mmda TEXT,
|
||
last_update TEXT,
|
||
remarks TEXT,
|
||
geometry_wkt TEXT,
|
||
created_at TEXT,
|
||
updated_at TEXT,
|
||
districtid INTEGER,
|
||
status TEXT DEFAULT 'verified',
|
||
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
`,console.log("[Database] Creating building_footprints table..."),await w`
|
||
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 w`
|
||
CREATE TABLE IF NOT EXISTS osm_roads (
|
||
osm_id INTEGER PRIMARY KEY,
|
||
geometry_wkt TEXT,
|
||
properties TEXT,
|
||
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
`,console.log("[Database] Creating upn_grid table..."),await w`
|
||
CREATE TABLE IF NOT EXISTS upn_grid (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
districtid INTEGER,
|
||
upn_prefix TEXT,
|
||
geometry_wkt TEXT,
|
||
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
`,console.log("[Database] Creating gps_trails table..."),await w`
|
||
CREATE TABLE IF NOT EXISTS gps_trails (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
client_uuid TEXT UNIQUE,
|
||
name TEXT,
|
||
district_id TEXT,
|
||
started_at TEXT NOT NULL,
|
||
ended_at TEXT,
|
||
status TEXT NOT NULL DEFAULT 'recording',
|
||
point_count INTEGER NOT NULL DEFAULT 0,
|
||
distance_m REAL NOT NULL DEFAULT 0,
|
||
synced INTEGER NOT NULL DEFAULT 0,
|
||
remote_id TEXT,
|
||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
`,console.log("[Database] Creating gps_trail_points table..."),await w`
|
||
CREATE TABLE IF NOT EXISTS gps_trail_points (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
trail_id INTEGER NOT NULL,
|
||
seq INTEGER NOT NULL,
|
||
longitude REAL NOT NULL,
|
||
latitude REAL NOT NULL,
|
||
altitude REAL,
|
||
accuracy REAL,
|
||
altitude_accuracy REAL,
|
||
heading REAL,
|
||
speed REAL,
|
||
satellites INTEGER,
|
||
recorded_at TEXT NOT NULL
|
||
)
|
||
`,console.log("[Database] Creating external_imports table..."),await w`
|
||
CREATE TABLE IF NOT EXISTS external_imports (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
filename TEXT NOT NULL,
|
||
target_type TEXT NOT NULL DEFAULT 'other',
|
||
mapping_json TEXT,
|
||
status TEXT NOT NULL DEFAULT 'imported',
|
||
feature_count INTEGER NOT NULL DEFAULT 0,
|
||
error_count INTEGER NOT NULL DEFAULT 0,
|
||
client_import_id TEXT UNIQUE,
|
||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||
last_uploaded_at TEXT
|
||
)
|
||
`,console.log("[Database] Creating external_import_features table..."),await w`
|
||
CREATE TABLE IF NOT EXISTS external_import_features (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
import_id INTEGER NOT NULL REFERENCES external_imports(id) ON DELETE CASCADE,
|
||
client_uuid TEXT UNIQUE,
|
||
geometry_wkt TEXT NOT NULL,
|
||
properties_json TEXT,
|
||
upload_status TEXT NOT NULL DEFAULT 'pending',
|
||
server_id INTEGER,
|
||
error_message TEXT
|
||
)
|
||
`,await w`CREATE INDEX IF NOT EXISTS idx_locations_category ON locations(category)`,await w`CREATE INDEX IF NOT EXISTS idx_locations_synced ON locations(synced)`,await w`CREATE INDEX IF NOT EXISTS idx_gps_trails_synced ON gps_trails(synced, status)`,await w`CREATE INDEX IF NOT EXISTS idx_gps_trail_points_trail ON gps_trail_points(trail_id, seq)`,await w`CREATE INDEX IF NOT EXISTS idx_external_imports_status ON external_imports(status)`,await w`CREATE INDEX IF NOT EXISTS idx_external_import_features_import ON external_import_features(import_id, upload_status)`;const t=await w`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;console.log("[Database] All tables:",t.map(r=>r.name)),Nn=!0,$n(!0),console.log("[Database] ✓ Schema initialized")}catch(n){throw console.error("[Database] ✗ Schema init failed:",n),Bn(n),n}}async function na(n,e,t,r={}){const{description:o=null,category:a="default"}=r;console.log("[Database] Adding location:",n,e,t,a);try{const i=await w`SELECT name FROM sqlite_master WHERE type='table' AND name='locations'`;if(console.log("[Database] Table check before insert:",i),i.length===0)throw console.error("[Database] ✗ locations table does not exist!"),new Error("locations table does not exist");console.log("[Database] Executing INSERT..."),await w`
|
||
INSERT INTO locations (name, longitude, latitude, description, category)
|
||
VALUES (${n}, ${e}, ${t}, ${o}, ${a})
|
||
`,console.log("[Database] INSERT completed");const l=(await w`SELECT last_insert_rowid() as id`)[0]?.id;console.log("[Database] New ID:",l);const c=await w`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 w`
|
||
INSERT INTO sync_log (table_name, record_id, action)
|
||
VALUES ('locations', ${l}, 'INSERT')
|
||
`,se("locations","INSERT",l),console.log("[Database] ✓ Location added:",l),{id:l}}catch(i){throw console.error("[Database] ✗ Failed to add location:",i),i}}async function Gn(n={}){const{category:e=null,limit:t=1e3}=n;try{const r=await w`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 w`
|
||
SELECT * FROM locations
|
||
WHERE category = ${e}
|
||
ORDER BY created_at DESC
|
||
LIMIT ${t}
|
||
`:o=await w`
|
||
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 ra(){try{return(await w`SELECT COUNT(*) as count FROM locations`)[0]?.count??0}catch(n){return console.error("[Database] getLocationCount error:",n),0}}async function jn(n,e){try{const t=JSON.stringify(e);await w`
|
||
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 qn(n){try{const e=await w`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 aa(n){try{await w`DELETE FROM collector_zones`;for(const e of n){const t=JSON.stringify(e);await w`
|
||
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 ia(){try{const n=await w`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 sa(n,e){try{await w`BEGIN`,await w`DELETE FROM upn_grid`;let t=0;for(const r of n){const o=r.polygon||r.geometry_wkt||r.geom||"";await w`
|
||
INSERT INTO upn_grid (districtid, upn_prefix, geometry_wkt, fetched_at)
|
||
VALUES (${X(e)}, ${r.upn_prefix??null}, ${o}, CURRENT_TIMESTAMP)
|
||
`,t++}await w`COMMIT`,console.log("[Database] ✓ Saved",t,"UPN-grid cells (district",e,")")}catch(t){try{await w`ROLLBACK`}catch{}throw console.error("[Database] ✗ Failed to save UPN grid:",t),t}}async function la(n){try{const e=await w`
|
||
SELECT id, districtid, upn_prefix, geometry_wkt
|
||
FROM upn_grid
|
||
WHERE districtid = ${X(n)}
|
||
ORDER BY id
|
||
`;return e.length===0?null:e}catch(e){return console.error("[Database] ✗ Failed to read UPN grid:",e),null}}function zn(){return typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,n=>{const e=Math.random()*16|0;return(n==="x"?e:e&3|8).toString(16)})}async function ca(n){const{filename:e,targetType:t="other",featureCount:r=0}=n,o=zn();try{await w`
|
||
INSERT INTO external_imports
|
||
(filename, target_type, status, feature_count, client_import_id)
|
||
VALUES
|
||
(${e}, ${t},
|
||
${t==="other"?"other":"imported"},
|
||
${r}, ${o})
|
||
`;const i=(await w`SELECT last_insert_rowid() AS id`)[0]?.id;return se("external_imports","INSERT",i),{id:i,client_import_id:o}}catch(a){throw console.error("[Database] ✗ Failed to create external import:",a),a}}async function da(n,e){if(!Array.isArray(e)||e.length===0)return 0;try{await w`BEGIN`;let t=0;for(const r of e){const o=r.geometry_wkt||"";if(!o)continue;const a=JSON.stringify(r.properties??{}),i=r.client_uuid||zn();await w`
|
||
INSERT INTO external_import_features
|
||
(import_id, client_uuid, geometry_wkt, properties_json, upload_status)
|
||
VALUES
|
||
(${n}, ${i}, ${o}, ${a}, 'pending')
|
||
`,t++}return await w`COMMIT`,se("external_import_features","INSERT",n),t}catch(t){try{await w`ROLLBACK`}catch{}throw console.error("[Database] ✗ Failed to add import features:",t),t}}async function It(n,e={}){try{const t=await w`SELECT * FROM external_imports WHERE id = ${n}`;if(t.length===0)throw new Error(`Import ${n} not found`);const r=t[0],o=e.targetType??r.target_type,a=e.mapping!==void 0?e.mapping?JSON.stringify(e.mapping):null:r.mapping_json,i=e.status??r.status,s=e.errorCount??r.error_count,l=e.lastUploadedAt??r.last_uploaded_at;await w`
|
||
UPDATE external_imports SET
|
||
target_type = ${o},
|
||
mapping_json = ${a},
|
||
status = ${i},
|
||
error_count = ${s},
|
||
last_uploaded_at = ${l}
|
||
WHERE id = ${n}
|
||
`,se("external_imports","UPDATE",n)}catch(t){throw console.error("[Database] ✗ Failed to update external import:",t),t}}async function ua(n){try{const e=await w`SELECT * FROM external_imports WHERE id = ${n}`;if(e.length===0)return null;const t=e[0];return{...t,mapping:t.mapping_json?JSON.parse(t.mapping_json):null}}catch(e){return console.error("[Database] ✗ Failed to read external import:",e),null}}async function pa(n){try{return(await w`
|
||
SELECT id, client_uuid, geometry_wkt, properties_json,
|
||
upload_status, server_id, error_message
|
||
FROM external_import_features
|
||
WHERE import_id = ${n}
|
||
ORDER BY id
|
||
`).map(t=>({...t,properties:t.properties_json?JSON.parse(t.properties_json):{}}))}catch(e){return console.error("[Database] ✗ Failed to read import features:",e),[]}}async function fa(n,e){try{const t=await w`
|
||
SELECT id, properties_json
|
||
FROM external_import_features
|
||
WHERE import_id = ${n}
|
||
`;if(t.length===0)return 0;await w`BEGIN`;let r=0;for(const o of t){const a=o.properties_json?JSON.parse(o.properties_json):{},i=e(a)??{};await w`
|
||
UPDATE external_import_features
|
||
SET properties_json = ${JSON.stringify(i)}
|
||
WHERE id = ${o.id}
|
||
`,r++}return await w`COMMIT`,se("external_import_features","UPDATE",n),r}catch(t){try{await w`ROLLBACK`}catch{}throw console.error("[Database] ✗ Failed to remap import features:",t),t}}function X(n){if(n===""||n===null||n===void 0)return null;const e=Number(n);return Number.isNaN(e)?null:e}async function ha(n){try{await w`BEGIN`,await w`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=t.boundary||t.geometry_wkt||t.polygon||t.wkt||(typeof t.geom=="string"?t.geom:"");await w`
|
||
INSERT OR REPLACE INTO parcels (
|
||
id, upn, style, landuse, zone_code, zone_name, sector, block, parcel_no,
|
||
prop_no, st_name, prop_add, fac_name, min_height, max_height, eff_date,
|
||
lp_name, locality, mmda, last_update, remarks, geometry_wkt,
|
||
created_at, updated_at, districtid, status, fetched_at
|
||
) VALUES (
|
||
${r}, ${t.upn??null}, ${X(t.style)}, ${t.landuse??null},
|
||
${t.zone_code??null}, ${t.zone_name??null}, ${t.sector??null},
|
||
${t.block??null}, ${t.parcel_no??null}, ${t.prop_no??null},
|
||
${t.st_name??null}, ${t.prop_add??null}, ${t.fac_name??null},
|
||
${X(t.min_height)}, ${X(t.max_height)}, ${t.eff_date??null},
|
||
${t.lp_name??null}, ${t.locality??null}, ${t.mmda??null},
|
||
${t.last_update??null}, ${t.remarks??null}, ${o},
|
||
${t.created_at??null}, ${t.updated_at??null}, ${X(t.districtid)},
|
||
'verified', CURRENT_TIMESTAMP
|
||
)
|
||
`,e++}await w`COMMIT`,console.log("[Database] ✓ Saved",e,"parcels (from",n.length,"rows,",n.length-e,"skipped/replaced)")}catch(e){try{await w`ROLLBACK`}catch{}throw console.error("[Database] ✗ Failed to save parcels:",e),e}}async function ga(){try{const n=await w`SELECT * FROM parcels ORDER BY id`;return n.length===0?null:n}catch(n){return console.error("[Database] ✗ Failed to read local parcels:",n),null}}async function ma(n,e){try{await w`
|
||
UPDATE parcels SET
|
||
upn = ${e.upn??null},
|
||
style = ${X(e.style)},
|
||
landuse = ${e.landuse??null},
|
||
zone_code = ${e.zone_code??null},
|
||
zone_name = ${e.zone_name??null},
|
||
sector = ${e.sector??null},
|
||
block = ${e.block??null},
|
||
parcel_no = ${e.parcel_no??null},
|
||
prop_no = ${e.prop_no??null},
|
||
st_name = ${e.st_name??null},
|
||
prop_add = ${e.prop_add??null},
|
||
fac_name = ${e.fac_name??null},
|
||
min_height = ${X(e.min_height)},
|
||
max_height = ${X(e.max_height)},
|
||
eff_date = ${e.eff_date??null},
|
||
lp_name = ${e.lp_name??null},
|
||
locality = ${e.locality??null},
|
||
mmda = ${e.mmda??null},
|
||
last_update = ${e.last_update??null},
|
||
remarks = ${e.remarks??null},
|
||
districtid = ${X(e.districtid)},
|
||
updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = ${n}
|
||
`,console.log("[Database] ✓ Parcel updated:",n),se("parcels","UPDATE",n)}catch(t){throw console.error("[Database] ✗ Failed to update parcel:",n,t),t}}async function ya(n,e={}){try{await w`
|
||
INSERT INTO parcels (
|
||
id, upn, style, landuse, zone_code, zone_name, sector, block, parcel_no,
|
||
prop_no, st_name, prop_add, fac_name, min_height, max_height, eff_date,
|
||
lp_name, locality, mmda, last_update, remarks, geometry_wkt,
|
||
created_at, updated_at, districtid, status, fetched_at
|
||
) VALUES (
|
||
NULL, ${e.upn??null}, ${X(e.style)}, ${e.landuse??null},
|
||
${e.zone_code??null}, ${e.zone_name??null}, ${e.sector??null},
|
||
${e.block??null}, ${e.parcel_no??null}, ${e.prop_no??null},
|
||
${e.st_name??null}, ${e.prop_add??null}, ${e.fac_name??null},
|
||
${X(e.min_height)}, ${X(e.max_height)}, ${e.eff_date??null},
|
||
${e.lp_name??null}, ${e.locality??null}, ${e.mmda??null},
|
||
${e.last_update??null}, ${e.remarks??null}, ${n},
|
||
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ${X(e.districtid)},
|
||
'new', CURRENT_TIMESTAMP
|
||
)
|
||
`;const r=(await w`SELECT last_insert_rowid() as id`)[0]?.id;return console.log("[Database] ✓ New parcel inserted:",r,"(status: new)"),se("parcels","INSERT",r),{id:r}}catch(t){throw console.error("[Database] ✗ Failed to insert new parcel:",t),t}}async function ba(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 w`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 a=e.id||e.footprint_id||e.building_id||null;await w`
|
||
INSERT INTO building_footprints (id, geometry_wkt, properties, fetched_at)
|
||
VALUES (${a!==null&&typeof a=="object"?null:a}, ${o}, ${t}, CURRENT_TIMESTAMP)
|
||
`}console.log("[Database] ✓ Saved",n.length,"building footprints")}catch(e){throw console.error("[Database] ✗ Failed to save building footprints:",e),e}}async function wa(){try{const n=await w`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 _a(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 w`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 a=e.osm_id??e.osmid??e.id??null;await w`
|
||
INSERT OR REPLACE INTO osm_roads (osm_id, geometry_wkt, properties, fetched_at)
|
||
VALUES (${a!==null&&typeof a=="object"?null:a}, ${o}, ${t}, CURRENT_TIMESTAMP)
|
||
`}console.log("[Database] ✓ Saved",n.length,"OSM roads")}catch(e){throw console.error("[Database] ✗ Failed to save OSM roads:",e),e}}async function va(){try{const n=await w`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 Ea(){return On.getDatabaseFile()}async function xa(n="lupmis-backup.sqlite3"){const e=await Ea(),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 Sa(){return{type:"FeatureCollection",features:(await Gn()).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 Eo(){try{const n=await w`
|
||
SELECT name FROM sqlite_master
|
||
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
||
ORDER BY name
|
||
`,e=await ra();return{ready:Nn,databasePath:vo,tables:n.map(t=>t.name),locationCount:e}}catch(n){return{ready:!1,error:n.message}}}const Un=Object.freeze(["parcels","building_footprints","osm_roads","collector_zones","upn_grid","remote_data"]);function Hn(n){return Un.includes(n)}async function Wn(n){if(!Hn(n))throw new Error(`Refusing to clear "${n}" — not a known cached-layer table`);const t=(await w(`SELECT COUNT(*) AS n FROM "${n}"`))[0]?.n??0;return await w(`DELETE FROM "${n}"`),console.log(`[Database] ✓ Cleared "${n}" (${t} rows)`),se(n,"CLEAR",null),t}async function Ta(){const n=await w`
|
||
SELECT name FROM sqlite_master
|
||
WHERE type='table' AND name IN (
|
||
'parcels', 'building_footprints', 'osm_roads', 'collector_zones', 'upn_grid', 'remote_data'
|
||
)
|
||
`,e=new Set(n.map(o=>o.name)),t=[];for(const o of Un)if(e.has(o))try{const a=await Wn(o);t.push({table:o,count:a})}catch(a){console.error(`[Database] Failed to clear ${o}:`,a),t.push({table:o,count:0,error:a.message})}const r=t.reduce((o,a)=>o+a.count,0);return console.log(`[Database] ✓ Cleared all cached layers: ${r} rows across ${t.length} tables`),t}async function La(){const n=await w`
|
||
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 w(e)}async function ka(n,e=200){if((await w`
|
||
SELECT name FROM sqlite_master
|
||
WHERE type='table' AND name = ${n}
|
||
`).length===0)throw new Error(`Table "${n}" does not exist`);const r=await w(`SELECT * FROM "${n}" LIMIT ${e}`);return{columns:r.length>0?Object.keys(r[0]):[],rows:r}}async function Ia(){console.log("=== DATABASE TEST ===");try{const n=await w`SELECT sqlite_version() as v`;console.log("1. SQLite version:",n[0].v);const e=await w`SELECT name FROM sqlite_master WHERE type='table'`;console.log("2. Tables:",e.map(o=>o.name)),console.log("3. Inserting test row..."),await w`INSERT INTO locations (name, longitude, latitude, category) VALUES ('TEST', -1.0, 7.0, 'test')`;const t=await w`SELECT * FROM locations WHERE name = 'TEST'`;console.log("4. Test row:",t);const r=await w`SELECT COUNT(*) as c FROM locations`;return console.log("5. Total rows:",r[0].c),await w`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=Ia,window.dbStatus=Eo);async function Pa(n){const{uuid:e,name:t=null,startedAt:r,districtId:o=null}=n;await w`
|
||
INSERT INTO gps_trails (client_uuid, name, district_id, started_at, status)
|
||
VALUES (${e}, ${t}, ${o}, ${r}, 'recording')
|
||
`;const i=(await w`SELECT last_insert_rowid() as id`)[0]?.id;return se("gps_trails","insert",i),i}async function Ma(n,e){const{seq:t,lon:r,lat:o,altitude:a=null,accuracy:i=null,altitudeAccuracy:s=null,heading:l=null,speed:c=null,satellites:d=null,timestamp:u}=e,p=typeof u=="number"?new Date(u).toISOString():u||new Date().toISOString();await w`
|
||
INSERT INTO gps_trail_points
|
||
(trail_id, seq, longitude, latitude, altitude, accuracy, altitude_accuracy, heading, speed, satellites, recorded_at)
|
||
VALUES
|
||
(${n}, ${t}, ${r}, ${o}, ${a}, ${i}, ${s}, ${l}, ${c}, ${d}, ${p})
|
||
`}async function Aa(n,e){const{endedAt:t,pointCount:r=0,distanceM:o=0}=e;await w`
|
||
UPDATE gps_trails
|
||
SET ended_at = ${t}, point_count = ${r}, distance_m = ${o}, status = 'completed'
|
||
WHERE id = ${n}
|
||
`,se("gps_trails","update",n)}async function Ca(){return w`SELECT * FROM gps_trails WHERE synced = 0 AND status = 'completed' ORDER BY started_at ASC`}async function Fa(n){return w`SELECT * FROM gps_trail_points WHERE trail_id = ${n} ORDER BY seq ASC`}async function Da(n,e=null){await w`UPDATE gps_trails SET synced = 1, remote_id = ${e} WHERE id = ${n}`,se("gps_trails","update",n)}const Kn=3.28084,Xn=621371e-9,Vn=10.7639,Yn=247105e-9,Jn=3861e-10;function Ft(){return localStorage.getItem("measurement-system")||"metric"}function Pt(n){if(Ft()==="imperial"){const e=n*Kn;return e>=5280?Math.round(n*Xn*100)/100+" mi":Math.round(e)+" ft"}return n>1e3?Math.round(n/1e3*100)/100+" km":Math.round(n*100)/100+" m"}function Oa(n){if(Ft()==="imperial"){const e=n*Kn,t=n*Xn;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 rt(n){if(Ft()==="imperial"){const e=n*Yn;return e>=640?Math.round(n*Jn*100)/100+" mi²":e>=1?Math.round(e*100)/100+" acres":Math.round(n*Vn).toLocaleString("en")+" ft²"}return n>1e6?Math.round(n/1e6*100)/100+" km²":Math.round(n*100)/100+" m²"}function Ra(n){if(Ft()==="imperial"){const e=n*Vn,t=n*Yn,r=n*Jn;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 Na(n){return rt(Math.PI*n*n)}function $a(n,e,t,r,o=1e-10){const a=e[0]-n[0],i=e[1]-n[1],s=r[0]-t[0],l=r[1]-t[1],c=a*l-i*s;if(Math.abs(c)<o)return null;const d=t[0]-n[0],u=t[1]-n[1],p=(d*l-u*s)/c,h=(d*i-u*a)/c;return p<-o||p>1+o||h<-o||h>1+o?null:{point:[n[0]+p*a,n[1]+p*i],t:Math.max(0,Math.min(1,p)),u:Math.max(0,Math.min(1,h))}}function Zn(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 co(n,e){let t=!1;for(let r=0,o=e.length-2;r<e.length-1;o=r++){const a=e[r][0],i=e[r][1],s=e[o][0],l=e[o][1];i>n[1]!=l>n[1]&&n[0]<(s-a)*(n[1]-i)/(l-i)+a&&(t=!t)}return t}function lt(n,e){return(n[0]-e[0])**2+(n[1]-e[1])**2}function Ba(n,e){const t=[];for(let o=0;o<e.length-1;o++)for(let a=0;a<n.length-1;a++){const i=$a(n[a],n[a+1],e[o],e[o+1],1e-10);if(!i)continue;const s=i.point;let l=!1;for(const c of t)if(lt(c.point,s)<1e-6){l=!0;break}l||t.push({point:s,ringSegIdx:a,ringT:i.t,lineSegIdx:o,lineT:i.u})}return t.sort((o,a)=>o.lineSegIdx!==a.lineSegIdx?o.lineSegIdx-a.lineSegIdx:o.lineT-a.lineT),t}function Ga(n,e){const t=e.map((a,i)=>({...a,origOrder:i}));t.sort((a,i)=>a.ringSegIdx!==i.ringSegIdx?a.ringSegIdx-i.ringSegIdx:a.ringT-i.ringT);const r=n.slice(),o=new Array(t.length);for(let a=t.length-1;a>=0;a--){const i=t[a],s=i.ringSegIdx+1,l=1e-6;if(lt(i.point,r[i.ringSegIdx])<l){o[i.origOrder]=i.ringSegIdx;continue}if(lt(i.point,r[i.ringSegIdx+1])<l){o[i.origOrder]=i.ringSegIdx+1;continue}r.splice(s,0,i.point),o[i.origOrder]=s;for(let c=a+1;c<t.length;c++)o[t[c].origOrder]>=s&&o[t[c].origOrder]++}return{ring:r,indices:o}}function Go(n,e,t){const r=n.length-1,o=(e%r+r)%r,a=(t%r+r)%r,i=[];let s=o;for(;i.push(n[s]),s!==a;)s=(s+1)%r;return i}function jo(n,e,t){const r=[e.point],o=e.lineSegIdx,a=t.lineSegIdx;for(let i=o+1;i<=a;i++)r.push(n[i]);return lt(r[r.length-1],t.point)>1e-10&&r.push(t.point),r}function qo(n,e){const t=Zn(n);return e&&t<0||!e&&t>0?n.slice().reverse():n}function zo(n){if(n.length<2)return n;const e=n[0],t=n[n.length-1];return lt(e,t)>1e-10?[...n,e.slice()]:n}function ja(n,e){let t=1/0,r=1/0,o=-1/0,a=-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]>a&&(a=c[1]);const i=Math.sqrt((o-t)**2+(a-r)**2)||1,s=n.slice();if(co(s[0],e)){const c=s[0],d=s[1],u=c[0]-d[0],p=c[1]-d[1],h=Math.sqrt(u*u+p*p)||1,f=i*2/h;s[0]=[c[0]+u*f,c[1]+p*f]}const l=s.length-1;if(co(s[l],e)){const c=s[l],d=s[l-1],u=c[0]-d[0],p=c[1]-d[1],h=Math.sqrt(u*u+p*p)||1,f=i*2/h;s[l]=[c[0]+u*f,c[1]+p*f]}return s}function vt(n,e){const t=n[0],r=n.slice(1),o=ja(e,t),a=Ba(t,o);if(a.length!==2)return console.warn(`[polygonSplit] Expected 2 intersections, got ${a.length}`),null;const[i,s]=a,{ring:l,indices:c}=Ga(t,a),d=c[0],u=c[1],[p,h]=d<u?[d,u]:[u,d],f=d<u?jo(o,i,s):jo(o,s,i),y=f.slice().reverse(),g=Go(l,p,h),m=zo([...g,...y.slice(1)]),b=Go(l,h,p),_=zo([...b,...f.slice(1)]),S=Zn(t)>0,T=qo(m,S),v=qo(_,S),x=[T],P=[v];for(const k of r){const q=qa(k);co(q,T)?x.push(k):P.push(k)}return[x,P]}function qa(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 Uo={success:{bg:"#10b981",icon:"✅"},error:{bg:"#ef4444",icon:"❌"},warning:{bg:"#f59e0b",icon:"⚠️"},info:{bg:"#0ea5e9",icon:"ℹ️"}};let Fe=null;function za(){return Fe||(Fe=document.createElement("div"),Fe.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(Fe),Fe)}function B(n,e="info",t=4e3){const r=za(),o=Uo[e]||Uo.info,a=document.createElement("div");a.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;
|
||
`,a.textContent=`${o.icon} ${n}`,r.appendChild(a),requestAnimationFrame(()=>{a.style.opacity="1",a.style.transform="translateY(0)"});const i=()=>{a.style.opacity="0",a.style.transform="translateY(-8px)",setTimeout(()=>a.remove(),300)};a.addEventListener("click",i),setTimeout(i,t)}const ht=[{stroke:"#ef4444",fill:"rgba(239,68,68,0.25)"},{stroke:"#3b82f6",fill:"rgba(59,130,246,0.25)"}],Ua=new M({stroke:new I({color:"#0ea5e9",width:3}),fill:new A({color:"rgba(14,165,233,0.15)"})}),Ha=new M({stroke:new I({color:"#f43f5e",width:2,lineDash:[8,6]}),image:new ue({radius:5,fill:new A({color:"#f43f5e"}),stroke:new I({color:"#fff",width:1.5})})});class Wa extends bo{constructor(e={}){super({handleEvent:t=>this._handleEvent(t)}),this.snapDistance_=e.snapDistance||25,this._sources=e.sources?Array.isArray(e.sources)?e.sources:[e.sources]:null,this._phase="select",this._selectedFeature=null,this._selectedSource=null,this._drawInteraction=null,this._splitFeatures=null,this._overlaySource=new G({useSpatialIndex:!1}),this._overlayLayer=new O({source:this._overlaySource,displayInLayerSwitcher:!1,style:Ua})}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 G?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 a=o.getClosestFeatureToCoordinate(e.coordinate);if(!a)continue;const i=a.getGeometry();if(!i)continue;const s=i.getType();if(s!=="Polygon"&&s!=="MultiPolygon")continue;const l=i.getClosestPoint(e.coordinate),d=new pe([e.coordinate,l]).getLength()/e.frameState.viewState.resolution;d<r&&(r=d,t={feature:a,source:o,coord:l})}return t}_startDrawPhase(){this._phase="draw";const e=this.getMap();e&&(e.getTargetElement().style.cursor="crosshair",this._drawInteraction=new Pe({type:"LineString",style:Ha}),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 a;o.getType()==="Polygon"?a=o.getCoordinates():o.getType()==="MultiPolygon"&&(a=o.getCoordinates()[0]);const i=vt(a,e);if(!i){console.warn("[PolygonSplit] Split failed — line must cross the polygon boundary at exactly 2 points."),this._removeDrawInteraction(),this._startDrawPhase();return}const[s,l]=i,c=t.clone();c.setGeometry(new it(s)),c.setStyle(new M({stroke:new I({color:ht[0].stroke,width:2.5}),fill:new A({color:ht[0].fill})}));const d=t.clone();d.setGeometry(new it(l)),d.setStyle(new M({stroke:new I({color:ht[1].stroke,width:2.5}),fill:new A({color:ht[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 h=this.getMap();h&&(h.getTargetElement().style.cursor=""),B("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 a=o.getGeometry();if(!a)continue;const i=a.getClosestPoint(e.coordinate),l=new pe([e.coordinate,i]).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 qe(n,e){return(n[0]-e[0])**2+(n[1]-e[1])**2}function tt(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 Ka(n,e){let t=!1;for(let r=0,o=e.length-2;r<e.length-1;o=r++){const a=e[r][0],i=e[r][1],s=e[o][0],l=e[o][1];i>n[1]!=l>n[1]&&n[0]<(s-a)*(n[1]-i)/(l-i)+a&&(t=!t)}return t}function Xa(n,e){const t=tt(n);return e&&t<0||!e&&t>0?n.slice().reverse():n}function Va(n){return n.length<2?n:qe(n[0],n[n.length-1])>1e-10?[...n,n[0].slice()]:n}function Be(n,e,t){const r=t[0]-e[0],o=t[1]-e[1],a=r*r+o*o;if(a<1e-20)return qe(n,e);let i=((n[0]-e[0])*r+(n[1]-e[1])*o)/a;i=Math.max(0,Math.min(1,i));const s=e[0]+i*r,l=e[1]+i*o;return(n[0]-s)**2+(n[1]-l)**2}function Ho(n,e){let t=0,r=1/0;const o=n.length-1;for(let a=0;a<o;a++){const i=Be(e,n[a],n[(a+1)%o===0?o:a+1]);i<r&&(r=i,t=a)}return{segIdx:t,distSq:r}}function De(n,e,t){return qe(n,e)<t}function gt(n,e,t){const r=e.length-1;for(let o=0;o<r;o++)if(Be(n,e[o],e[o+1])<t)return!0;return!1}function Ya(n,e,t,r,o){const a=n.length-1,i=e.length-1,s=o*o,l=n[t],c=n[(t+1)%a],d=e[r],u=e[(r+1)%i],p=gt(l,e,s),h=gt(c,e,s),f=gt(d,n,s),y=gt(u,n,s);if(!(p&&h)&&!(f&&y))return console.warn("[polygonMerge] Seed edges are not on the shared boundary"),null;let g;De(l,u,s)&&De(c,d,s)?g=!0:De(l,d,s)&&De(c,u,s)?g=!1:g=qe(l,u)<qe(l,d);let m=t,b=(t+1)%a,_,S;g?(_=(r+1)%i,S=r):(_=r,S=(r+1)%i);let T=a+i;for(;T-- >0;){const v=(b+1)%a,x=g?(S-1+i)%i:(S+1)%i;if(v===m||x===_)break;if(De(n[v],e[x],s)){b=v,S=x;continue}if(Be(n[v],e[S],e[x])<s){b=v;continue}if(Be(e[x],n[b],n[v])<s){S=x;continue}break}for(T=a+i;T-- >0;){const v=(m-1+a)%a,x=g?(_+1)%i:(_-1+i)%i;if(v===b||x===S)break;if(De(n[v],e[x],s)){m=v,_=x;continue}if(Be(n[v],e[_],e[x])<s){m=v;continue}if(Be(e[x],n[m],n[v])<s){_=x;continue}break}return{startA:m,endA:b,startB:_,endB:S,reversed:g}}function zt(n,e,t){const r=n.length-1,o=[];let a=e;for(;o.push(n[a]),!(a===t||(a=(a+1)%r,o.length>r+1)););return o}function Ja(n,e,t,r,o=5){const a=n[0],i=e[0],s=n.slice(1),l=e.slice(1),c=Ho(a,t),d=Ho(i,r),u=Ya(a,i,c.segIdx,d.segIdx,o);if(!u)return console.warn("[polygonMerge] Could not find shared boundary between polygons — seed edges are not near the other ring"),{coords:null,error:"The selected edges are not on a shared boundary. Click edges that lie on the common border between the two polygons."};const{startA:p,endA:h,startB:f,endB:y,reversed:g}=u;a.length-1,i.length-1;const m=zt(a,h,p);let b;g?b=zt(i,f,y):b=zt(i,y,f);const _=[...m,...b.slice(1)],S=o*o;_.length>2&&qe(_[_.length-1],_[0])<S&&(_[_.length-1]=_[0].slice());const T=Va(_),v=Math.abs(tt(a)),x=Math.abs(tt(i)),P=Math.abs(tt(T)),k=v+x;if(P<k*.5||P>k*1.5)return console.warn(`[polygonMerge] Area mismatch: A=${v.toFixed(1)}, B=${x.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 q=tt(a)>0,U=Xa(T,q),N=[...s,...l].filter(le=>{const Z=le.reduce((H,me)=>H+me[0],0)/(le.length-1),ge=le.reduce((H,me)=>H+me[1],0)/(le.length-1);return Ka([Z,ge],U)});return{coords:[U,...N]}}const Wo=new M({stroke:new I({color:"#0ea5e9",width:3}),fill:new A({color:"rgba(14,165,233,0.15)"})}),Za=new M({stroke:new I({color:"#f59e0b",width:3}),fill:new A({color:"rgba(245,158,11,0.15)"})}),Qa=new M({stroke:new I({color:"#0ea5e9",width:3}),fill:new A({color:"rgba(14,165,233,0.15)"}),text:new st({text:"A",font:"bold 22px Exo, sans-serif",fill:new A({color:"#0ea5e9"}),stroke:new I({color:"#fff",width:4}),overflow:!0})}),ei=new M({stroke:new I({color:"#f59e0b",width:3}),fill:new A({color:"rgba(245,158,11,0.15)"}),text:new st({text:"B",font:"bold 22px Exo, sans-serif",fill:new A({color:"#f59e0b"}),stroke:new I({color:"#fff",width:4}),overflow:!0})}),ti=new M({stroke:new I({color:"#ec4899",width:4,lineDash:[10,6]})}),oi=new M({stroke:new I({color:"#10b981",width:2.5}),fill:new A({color:"rgba(16,185,129,0.3)"})});class ni extends bo{constructor(e={}){super({handleEvent:t=>this._handleEvent(t)}),this.snapDistance_=e.snapDistance||25,this.tolerance_=e.tolerance||5,this._phase="select_a",this._featureA=null,this._sourceA=null,this._featureB=null,this._sourceB=null,this._edgeClickA=null,this._edgeClickB=null,this._highlightSource=new G({useSpatialIndex:!1}),this._highlightLayer=new O({source:this._highlightSource,displayInLayerSwitcher:!1,style:t=>t.get("_highlightStyle")||Wo}),this._edgeSource=new G({useSpatialIndex:!1}),this._edgeLayer=new O({source:this._edgeSource,displayInLayerSwitcher:!1,style:ti})}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 G?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 a=this._phase==="select_a"?Wo:Za,i=o.feature.clone();i.set("_highlightStyle",a),this._highlightSource.addFeature(i),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 a of this._getSources()){const i=a.getClosestFeatureToCoordinate(e.coordinate);if(!i||t&&i===t)continue;const s=i.getGeometry();if(!s)continue;const l=s.getType();if(l!=="Polygon"&&l!=="MultiPolygon")continue;const c=s.getClosestPoint(e.coordinate),u=new pe([e.coordinate,c]).getLength()/e.frameState.viewState.resolution;u<o&&(o=u,r={feature:i,source:a,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 a=new de(new pe([o.segStart,o.segEnd]));this._edgeSource.addFeature(a),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 a=t.frameState.viewState.resolution;let i=1/0,s=null;const l=o.length-1;for(let c=0;c<l;c++){const d=o[c],u=o[c+1],p=u[0]-d[0],h=u[1]-d[1],f=p*p+h*h;if(f<1e-20)continue;let y=((t.coordinate[0]-d[0])*p+(t.coordinate[1]-d[1])*h)/f;y=Math.max(0,Math.min(1,y));const g=d[0]+y*p,m=d[1]+y*h,b=Math.sqrt((t.coordinate[0]-g)**2+(t.coordinate[1]-m)**2)/a;b<i&&(i=b,s={segStart:d,segEnd:u})}return i<=this.snapDistance_?s:null}_performMerge(){const e=this._featureA,t=this._featureB,r=this._sourceA,o=this._sourceB,a=e.getGeometry(),i=t.getGeometry(),s=a.getType()==="Polygon"?a.getCoordinates():a.getCoordinates()[0],l=i.getType()==="Polygon"?i.getCoordinates():i.getCoordinates()[0],c=Ja(s,l,this._edgeClickA,this._edgeClickB,this.tolerance_);if(!c.coords){B(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 it(c.coords)),d.setStyle(oi);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 p={type:"aftermerge",original:[e,t],merged:d};this.dispatchEvent(p),r.dispatchEvent({...p}),o!==r&&o.dispatchEvent({...p});const h=e.get("_layerType")==="parcel",f=t.get("_layerType")==="parcel";h&&f?(this.dispatchEvent({type:"mergedparcel",merged:d,propsA:e.getProperties(),propsB:t.getProperties(),coordinate:this._edgeClickA}),B("Polygons merged — choose which identifier to keep.","success")):B("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",Qa),t.set("_permanent",!0),this._highlightSource.addFeature(t)}if(this._featureB){const t=this._featureB.clone();t.set("_highlightStyle",ei),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 ri(n,e){return(n[0]-e[0])**2+(n[1]-e[1])**2}function Ko(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 Xe(n){let e=Math.abs(Ko(n[0]));for(let t=1;t<n.length;t++)e-=Math.abs(Ko(n[t]));return e}function ai(n){const e=n.length-1;let t=-1,r=0;for(let c=0;c<e;c++){const d=ri(n[c],n[c+1]);d>t&&(t=d,r=c)}const o=n[r],a=n[r+1],i=Math.sqrt(t),s=[(a[0]-o[0])/i,(a[1]-o[1])/i],l=[-s[1],s[0]];return{p0:o,p1:a,along:s,perp:l}}function Ut(n,e,t,r,o){const a=n[0]+r*e[0],i=n[1]+r*e[1];return[[a-o*t[0],i-o*t[1]],[a+o*t[0],i+o*t[1]]]}function Oe(n,e,t){const r=n[0],o=r.length-1;let a=0,i=0;for(let c=0;c<o;c++)a+=r[c][0],i+=r[c][1];const s=a/o-e[0],l=i/o-e[1];return s*t[0]+l*t[1]}function ii(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(Xe(n)<1e-6)return{pieces:null,error:"Polygon has no measurable area."};let a,i,s;if(t&&t.length===2){a=t[0];const g=t[1][0]-t[0][0],m=t[1][1]-t[0][1],b=Math.sqrt(g*g+m*m);if(b<1e-10)return{pieces:null,error:"Selected edge has zero length."};i=[g/b,m/b],s=[-i[1],i[0]]}else{const g=ai(r);a=g.p0,i=g.along,s=g.perp}const l=a,c=r.length-1;for(let g=0;g<c;g++){const m=r[g][0]-l[0],b=r[g][1]-l[1];m*i[0]+b*i[1]}let d=1/0,u=-1/0;for(let g=0;g<c;g++){const m=r[g][0]-l[0],b=r[g][1]-l[1],_=m*s[0]+b*s[1];_<d&&(d=_),_>u&&(u=_)}const p=(u-d)*1.5,h=[];let f=n,y=e;for(let g=0;g<e-1;g++){const m=Xe(f),b=m/y,_=f[0],S=_.length-1;let T=1/0,v=-1/0;for(let J=0;J<S;J++){const N=_[J][0]-l[0],le=_[J][1]-l[1],Z=N*i[0]+le*i[1];Z<T&&(T=Z),Z>v&&(v=Z)}let x=T,P=v,k=null,q=null,U=1/0;for(let J=0;J<40;J++){const N=(x+P)/2,le=Ut(l,i,s,N,p),Z=vt(f,le);if(!Z){const D=(P-x)*.01,W=Ut(l,i,s,N+D,p),oe=vt(f,W);if(oe){const[xe,Se]=oe,Ue=Oe(xe,l,i),He=Oe(Se,l,i),We=Ue<He?xe:Se,$t=Ue<He?Se:xe,Bt=Xe(We),Ke=Math.abs(Bt-b);Ke<U&&(U=Ke,k=We,q=$t)}const Nt=Ut(l,i,s,N-D,p),ye=vt(f,Nt);if(ye){const[xe,Se]=ye,Ue=Oe(xe,l,i),He=Oe(Se,l,i),We=Ue<He?xe:Se,$t=Ue<He?Se:xe,Bt=Xe(We),Ke=Math.abs(Bt-b);Ke<U&&(U=Ke,k=We,q=$t)}x=N;continue}const[ge,H]=Z,me=Oe(ge,l,i),ze=Oe(H,l,i),$=me<ze?ge:H,Y=me<ze?H:ge,K=Xe($),te=Math.abs(K-b);if(te<U&&(U=te,k=$,q=Y),te/m<.001)break;K<b?x=N:P=N}if(!k||!q)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.`};h.push(k),f=q,y--}return h.push(f),{pieces:h}}const si=new M({stroke:new I({color:"#0ea5e9",width:3}),fill:new A({color:"rgba(14,165,233,0.15)"})}),li=new M({stroke:new I({color:"#8b5cf6",width:4,lineDash:[10,6]})});function ci(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 di extends bo{constructor(e={}){super({handleEvent:t=>this._handleEvent(t)}),this.snapDistance_=e.snapDistance||25,this._sources=e.sources?Array.isArray(e.sources)?e.sources:[e.sources]:null,this._phase="select",this._selectedFeature=null,this._selectedSource=null,this._selectedEdge=null,this._dividedFeatures=null,this._overlaySource=new G({useSpatialIndex:!1}),this._overlayLayer=new O({source:this._overlaySource,displayInLayerSwitcher:!1,style:si}),this._edgeSource=new G({useSpatialIndex:!1}),this._edgeLayer=new O({source:this._edgeSource,displayInLayerSwitcher:!1,style:li})}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 G?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",B("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 a=o.getClosestFeatureToCoordinate(e.coordinate);if(!a)continue;const i=a.getGeometry();if(!i)continue;const s=i.getType();if(s!=="Polygon"&&s!=="MultiPolygon")continue;const l=i.getClosestPoint(e.coordinate),d=new pe([e.coordinate,l]).getLength()/e.frameState.viewState.resolution;d<r&&(r=d,t={feature:a,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 de(new pe([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(),a=[(o[0]+o[2])/2,(o[1]+o[3])/2];return this.dispatchEvent({type:"divideform",feature:this._selectedFeature,source:this._selectedSource,coordinate:a}),!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 a=t.frameState.viewState.resolution;let i=1/0,s=null;const l=o.length-1;for(let c=0;c<l;c++){const d=o[c],u=o[c+1],p=u[0]-d[0],h=u[1]-d[1],f=p*p+h*h;if(f<1e-20)continue;let y=((t.coordinate[0]-d[0])*p+(t.coordinate[1]-d[1])*h)/f;y=Math.max(0,Math.min(1,y));const g=d[0]+y*p,m=d[1]+y*h,b=Math.sqrt((t.coordinate[0]-g)**2+(t.coordinate[1]-m)**2)/a;b<i&&(i=b,s={segStart:d,segEnd:u})}return i<=this.snapDistance_?s:null}performDivide(e){if(this._phase!=="form"||!this._selectedFeature)return;const t=this._selectedFeature,r=this._selectedSource,o=t.getGeometry();let a;o.getType()==="Polygon"?a=o.getCoordinates():o.getType()==="MultiPolygon"&&(a=o.getCoordinates()[0]);const i=ii(a,e,this._selectedEdge);if(!i.pieces){B(i.error||"Division failed.","error",5e3),this._reset();return}const s=ci(e),l=i.pieces.map((p,h)=>{const f=t.clone();return f.setGeometry(new it(p)),f.setStyle(new M({stroke:new I({color:s[h].stroke,width:2.5}),fill:new A({color:s[h].fill})})),f}),c={type:"beforedivide",original:t,features:l};this.dispatchEvent(c),r.dispatchEvent({...c}),r.removeFeature(t);for(const p of l)r.addFeature(p);const d={type:"afterdivide",original:t,features:l};this.dispatchEvent(d),r.dispatchEvent({...d}),t.get("_layerType")==="parcel"?(this._dividedFeatures=l,this._phase="pick",B("Click the polygon that should keep the original identifier.","info",5e3),this.dispatchEvent({type:"dividedparcel",features:l,originalProps:t.getProperties(),source:r})):(B(`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 a=o.getGeometry();if(!a)continue;const i=a.getClosestPoint(e.coordinate),l=new pe([e.coordinate,i]).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 ui{constructor(e,t={}){this.options=t,this.markerSource=new G,this.clickCallbacks=[],this.categoryEmojis={default:{emoji:"📍",label:"Default"},water:{emoji:"💧",label:"Water Point"},school:{emoji:"🏫",label:"School"},health:{emoji:"🏥",label:"Health Facility"},market:{emoji:"🏪",label:"Market"},other:{emoji:"📌",label:"Other"}},this.getEmoji=s=>{const l=this.categoryEmojis[s];return l?l.emoji:"📍"},this.getCategoryOptionsHtml=()=>Object.entries(this.categoryEmojis).map(([s,{emoji:l,label:c}])=>`<option value="${s}">${l} ${c}</option>`).join(`
|
||
`),this.createEmojiStyle=(s,l=24)=>new M({text:new st({text:s,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[s,{emoji:l}]of Object.entries(this.categoryEmojis))this.categoryStyles[s]=this.createEmojiStyle(l,32);const r=this.createBaseLayers(t.basemap||"topo");this.markersLayer=new O({title:"Markers",source:this.markerSource,style:s=>this.getFeatureStyle(s),visible:!1}),this.overlayGroup=new Te({title:"Overlays"}),this.map=new Io({target:e,layers:[r,this.markersLayer,this.overlayGroup],view:new Tr({center:Q(t.center||[0,0]),zoom:t.zoom||2,minZoom:t.minZoom||2,maxZoom:t.maxZoom||19})});const o=new Fr({collapsed:!0,mouseover:!0,extent:!0,trash:!1,oninfo:null});this.map.addControl(o),queueMicrotask(()=>{const s=o.element?.querySelector(":scope > button");if(s){const l="/".replace(/\/?$/,"/");s.style.backgroundImage=`url('${l}app-icons/luspa-72x72.png')`}});let a=!1;o.on("drawlist",s=>{this._decorateLayerListItem(s.layer,s.li),a||(a=!0,queueMicrotask(()=>{a=!1,this._refreshLayerSwitcherChrome(o)}))}),this.map.getLayers().on("change",()=>{this._refreshLayerSwitcherChrome(o)}),this._wireLayerSwitcherVisibilityHooks(o),this._createAddLayerDialog(),this._createLegendPanel(),this.scaleBar=new Lr({bar:!0,steps:4,text:!0,minWidth:140}),this.map.addControl(this.scaleBar),this._initGpsRendering(),this._createLocationControl(),this._createBaseMapPicker();const i=new Dr({placeholder:"Search location...",typing:300,minLength:3,maxItems:10,collapsed:!0});this.map.addControl(i),i.on("select",s=>{const l=s.search;if(l){const c=parseFloat(l.lon),d=parseFloat(l.lat),u=[c,d],p=Q(u);this.navigateTo(c,d,14);const h={coordinate:p,lonLat:u,name:l.display_name||l.name||"Unknown",searchResult:l};this.searchSelectCallbacks.forEach(f=>f(h))}}),this.searchNominatim=i,this.searchSelectCallbacks=[],this.selectedFeature=null,this.createPopup(),this.createInfoPopup(),this.createAddLocationPopup(),this.createParcelEditPopup(),this.createDrawnPolygonPopup(),this.createMergePopup(),this.createDividePopup(),this.dblClickCallbacks=[],this.editBar=null,this.drawingsSource=null,this.drawingsLayer=null,this.touchCursor=null,this._editBarActive=!1}initEditBar(){this.drawingsSource=new G,this.drawingsLayer=new O({title:"sketches",source:this.drawingsSource,style:new M({stroke:new I({color:"#f59e0b",width:2.5}),fill:new A({color:"rgba(245,158,11,0.15)"}),image:new ue({radius:6,fill:new A({color:"#f59e0b"}),stroke:new I({color:"#fff",width:1.5})})})}),this._drawingsGroup=new Te({title:"Drawings",layers:[this.drawingsLayer]});const e=this.map.getLayers(),t=e.getArray().indexOf(this.overlayGroup);e.insertAt(t>=0?t:e.getLength(),this._drawingsGroup),this._selectInteraction=new kr({condition:Ir,filter:(f,y)=>!!y,layers:f=>f instanceof O}),this._selectInteraction.setActive(!1),this.map.addInteraction(this._selectInteraction),this._modifyInteraction=new Or({features:this._selectInteraction.getFeatures()}),this._modifyInteraction.setActive(!1),this._undoRedo=new Rr,this.map.addInteraction(this._undoRedo),this.editBar=new ao({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 Ao({group:!0,className:"ol-editbar-actions",controls:[new et({html:'<i class="bi bi-arrow-counterclockwise"></i>',className:"ol-undo",title:"Undo",handleClick:()=>{this._undoRedo.hasUndo()&&this._undoRedo.undo()}}),new et({html:'<i class="bi bi-arrow-clockwise"></i>',className:"ol-redo",title:"Redo",handleClick:()=>{this._undoRedo.hasRedo()&&this._undoRedo.redo()}}),new et({html:'<i class="bi bi-floppy"></i>',className:"ol-save",title:"Save drawings",handleClick:()=>{this.dispatchEditEvent("save")}})]});this.editBar.addControl(r),this._lineSplitInteraction=new Nr,this._polygonSplitInteraction=new Wa,this.map.addInteraction(this._lineSplitInteraction),this.map.addInteraction(this._polygonSplitInteraction),this._lineSplitInteraction.setActive(!1),this._polygonSplitInteraction.setActive(!1),this._polygonSplitInteraction.on("splitpick",f=>{const y=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"];for(const g of f.features)if(g!==f.picked)for(const m of y)g.get(m)!==void 0&&g.set(m,"")}),this._polygonDivideInteraction=new di,this.map.addInteraction(this._polygonDivideInteraction),this._polygonDivideInteraction.setActive(!1);const o=new ve({html:'<i class="bi bi-slash-lg"></i>',className:"ol-split-line",title:"Split Lines",name:"SplitLine",interaction:this._lineSplitInteraction,autoActivate:!0}),a=new ve({html:'<i class="bi bi-scissors"></i>',className:"ol-split-polygon",title:"Split Polygons",name:"SplitPolygon",interaction:this._polygonSplitInteraction}),i=new ve({html:'<i class="bi bi-grid-3x3-gap"></i>',className:"ol-split-divide",title:"Divide Polygon",name:"DividePolygon",interaction:this._polygonDivideInteraction}),s=new Ao({toggleOne:!0,autoDeactivate:!0,controls:[o,a,i]}),l=new ve({className:"ol-split",title:"Split",name:"Split",bar:s,onToggle:f=>{f||(this._lineSplitInteraction.setActive(!1),this._polygonSplitInteraction.setActive(!1),this._polygonDivideInteraction.setActive(!1))}});this.editBar.addControl(l),this._polygonDivideInteraction.on("divideform",f=>{this.showDividePopup(f.feature,f.source,f.coordinate)}),this._polygonDivideInteraction.on("dividecancel",()=>{this.hideDividePopup()}),this._polygonDivideInteraction.on("dividepick",f=>{const y=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"];for(const g of f.features)if(g!==f.picked)for(const m of y)g.get(m)!==void 0&&g.set(m,"")}),this._polygonMergeInteraction=new ni,this.map.addInteraction(this._polygonMergeInteraction),this._polygonMergeInteraction.setActive(!1);const c=new ve({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",f=>{this.showMergeIdentifierPopup(f.merged,f.propsA,f.propsB,f.coordinate)});const d=this.editBar.element;if(d&&r.element&&r.element.parentNode===d){const f=document.createElement("div");f.className="ol-editbar-break",d.insertBefore(f,r.element)}this._snapGuidesEnabled=localStorage.getItem("snap-guides-enabled")==="1",this._snapGuides=new $r({pixelTolerance:10,vectorClass:Pr}),this.map.addInteraction(this._snapGuides);const u=["DrawPoint","DrawLine","DrawPolygon","DrawHole","DrawRegular"];for(const f of u){const y=this.editBar.getInteraction(f);y&&y.on("change:active",()=>{y.getActive()&&this._snapGuides.setDrawInteraction(y)})}this._modifyInteraction&&this._snapGuides.setModifyInteraction(this._modifyInteraction);const p=new et({html:'<i class="bi bi-magnet"></i>',className:"ol-snap-toggle"+(this._snapGuidesEnabled?" ol-active":""),title:"Toggle Snap Guides",handleClick:()=>{this._snapGuidesEnabled=!this._snapGuidesEnabled,localStorage.setItem("snap-guides-enabled",this._snapGuidesEnabled?"1":"0"),p.element.classList.toggle("ol-active",this._snapGuidesEnabled),this._snapGuides&&this._snapGuides.setActive(this._snapGuidesEnabled&&this._editBarActive),console.log("[MapView] Snap guides:",this._snapGuidesEnabled?"ON":"OFF")}});this._snapToggleBtn=p,r.addControl(p),this.setEditMode(!1),this._drawingsGroup.on("change:visible",()=>{const f=this._drawingsGroup.getVisible();this.setEditMode(f)}),("ontouchstart"in window||navigator.maxTouchPoints>0||navigator.msMaxTouchPoints>0)&&(this.touchCursor=new Br({className:"ol-editbar-cursor"}),this.map.addInteraction(this.touchCursor),this.touchCursor.setActive(!1),console.log("[MapView] Touch device detected — TouchCursor added")),this.drawingsSource.on("addfeature",f=>{const y=f.feature,g=y.getGeometry();if(!g||g.getType()!=="Polygon")return;const m=g.getInteriorPoint().getCoordinates();this.showDrawnPolygonPopup(y,m)}),console.log("[MapView] EditBar initialised with Drawings group, UndoRedo and SnapGuides (default:",this._snapGuidesEnabled?"ON":"OFF",")")}dispatchEditEvent(e){if(!this._editEventListeners)return;const t=this._editEventListeners[e];t&&t.forEach(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 G,this._vertexOverlayLayer=new O({title:"__vertex_highlight__",source:this._vertexOverlaySource,zIndex:990,style:new M({image:new ue({radius:4,fill:new A({color:"rgba(14,165,233,0.85)"}),stroke:new I({color:"#fff",width:1.2})})})}),this._vertexOverlayLayer.set("displayInLayerSwitcher",!1),this.map.addLayer(this._vertexOverlayLayer),this._onSelectedFeatureGeomChange=()=>this._refreshVertexOverlay(),this._vertexTrackedFeatures=new Set,this._selectInteraction.on("select",()=>this._refreshVertexOverlay())}_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 a=this._collectAllVertices(r);for(const i of a)this._vertexOverlaySource.addFeature(new de(new ut(i)));t.on("change",this._onSelectedFeatureGeomChange),this._vertexTrackedFeatures.add(t)}}_collectAllVertices(e){const t=[],r=s=>Array.isArray(s)&&typeof s[0]=="number",o=(s,l)=>{const c=l&&s.length>1?s.length-1:s.length;for(let d=0;d<c;d++)t.push(s[d])},a=e.getType(),i=e.getCoordinates();switch(a){case"Polygon":for(const l of i)o(l,!0);break;case"MultiPolygon":for(const l of i)for(const c of l)o(c,!0);break;case"LineString":o(i,!1);break;case"MultiLineString":for(const l of i)o(l,!1);break;default:const s=l=>{if(r(l))t.push(l);else if(Array.isArray(l))for(const c of l)s(c)};s(i)}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 _e({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",a=e.get("description"),i=e.get("lon"),s=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>
|
||
`,a&&(c+=`
|
||
<div style="color: var(--muted-foreground, #7a7a7a); font-size: 12px; margin-bottom: 6px; line-height: 1.4;">
|
||
${this.escapeHtml(a)}
|
||
</div>
|
||
`),i!==void 0&&s!==void 0&&(c+=`
|
||
<div style="color: var(--muted-foreground, #7a7a7a); font-size: 11px; font-family: monospace;">
|
||
${Number(i).toFixed(5)}, ${Number(s).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 _e({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:a="#e11d48"}=r,i=e.getProperties(),s=e.getGeometry(),l=s.getType(),c=["geometry","_layerType"];let d="";for(const[p,h]of Object.entries(i))c.includes(p)||h===void 0||h===null||(d+=`
|
||
<tr>
|
||
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">${this.escapeHtml(p)}</td>
|
||
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${this.escapeHtml(String(h))}</td>
|
||
</tr>
|
||
`);if(l==="Polygon"||l==="MultiPolygon"){const p=nt(s,{projection:"EPSG:3857"}),h=Ra(p);d+=`
|
||
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
|
||
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">area</td>
|
||
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${h}</td>
|
||
</tr>
|
||
`}else if(l==="LineString"||l==="MultiLineString"){const p=Et(s,{projection:"EPSG:3857"}),h=Oa(p);d+=`
|
||
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
|
||
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">length</td>
|
||
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${h}</td>
|
||
</tr>
|
||
`}else if(l==="Point"){const p=Me(s.getCoordinates()),h=p[0].toFixed(6),f=p[1].toFixed(6);d+=`
|
||
<tr style="border-top:1px solid var(--border, #1e1a4b1f);">
|
||
<td style="padding:4px 8px;font-weight:600;color:var(--muted-foreground, #7a7a7a);white-space:nowrap;">longitude</td>
|
||
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${h}</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);">${f}</td>
|
||
</tr>
|
||
`}const u=`
|
||
<div style="background:${a};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 a=t.map(i=>i.get("colzonename")||i.get("zone_name")||i.get("name")||"unnamed");o.push({label:"Zones",value:String(t.length),color:"#7c3aed"}),o.push({label:"Zone Names",value:a.map(i=>this.escapeHtml(i)).join(", "),color:"#7c3aed"})}for(const[a,i]of Object.entries(r))o.push({label:this.escapeHtml(a),value:`${i.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 a of r){if(a.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;">${a.value}</td>
|
||
</tr>`;continue}const i=a.color||"var(--muted-foreground, #7a7a7a)",s=a._first?"":"border-top:1px solid var(--border, #1e1a4b1f);";o+=`
|
||
<tr style="${s}">
|
||
<td style="padding:4px 8px;font-weight:600;color:${i};white-space:nowrap;">${a.label}</td>
|
||
<td style="padding:4px 8px;color:var(--foreground, #1e1a4b);">${a.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;display:flex;justify-content:flex-end;gap:6px;flex-shrink:0;border-top:1px solid var(--border, #1e1a4b1f);">
|
||
<button id="info-popup-export-gis"
|
||
style="background:var(--brand-navy,#1e1a4b);color:#fff;border:none;border-radius:6px;padding:5px 12px;font-size:12px;cursor:pointer;font-family:inherit;">
|
||
🗺️ Export GIS
|
||
</button>
|
||
<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,a=null){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(l=>!l.empty).map(l=>({label:l.label,value:l.value.replace(/<[^>]*>/g,"")}));Ct(async()=>{const{exportAnalysisPDF:l}=await import("./pdf-export-BG6jqfsR.js");return{exportAnalysisPDF:l}},__vite__mapDeps([0,1,2,3])).then(({exportAnalysisPDF:l})=>{l({title:t,rows:s})}).catch(l=>{console.error("[MapView] PDF export failed:",l)})});const i=this.infoPopupElement.querySelector("#info-popup-export-gis");if(i){const s=a?a.parcelFeatures.length+a.zoneFeatures.length+Object.values(a.otherByLayer).reduce((l,c)=>l+c.length,0):0;!a||s===0?(i.disabled=!0,i.style.opacity="0.5",i.style.cursor="not-allowed",i.title="No intersecting features to export"):i.addEventListener("click",()=>{window.dispatchEvent(new CustomEvent("lupmis:export-gis",{detail:{title:t,...a}}))})}}showCircleIntersectionPopup(e,t){const r=e.getGeometry();if(!r||typeof r.getCenter!="function")return;const o=Mr(r,64),a=o.getExtent(),i=e.get("_radius")||r.getRadius(),s=[],l=[],c={},d=g=>{const m=g.getGeometry();if(!m)return!1;const b=m.getExtent();return b[2]<a[0]||b[0]>a[2]||b[3]<a[1]||b[1]>a[3]?!1:o.intersectsExtent(b)&&this._geometriesIntersect(o,m)},u=(g,m)=>{g.getLayers().forEach(b=>{if(b instanceof Te)u(b,b.get("title")||m);else if(b instanceof O&&b.getVisible()){const _=b.get("title")||m||"Unknown",S=b.getSource();if(!S)return;const T=S.getFeaturesInExtent(a);for(const v of T){const x=v.get("_layerType");x==="measure_circle"||x==="measure_circle_radius"||d(v)&&(x==="parcel"?s.push(v):x==="collector_zone"?l.push(v):(c[_]||(c[_]=[]),c[_].push(v)))}}})};u(this.overlayGroup,"Overlays");const p=Pt(i),h=Math.PI*i*i,f=rt(h),y=[{label:"Radius",value:p,_first:!0},{label:"Area",value:f},...this._collectIntersectionRows(s,l,c)];this._showAnalysisPopup("⭕","Circle Analysis",y,t,{kind:"circle",clipGeometry:o,parcelFeatures:s,zoneFeatures:l,otherByLayer:c})}showAreaIntersectionPopup(e,t){const r=e.getGeometry();if(!r)return;const o=r.getExtent(),a=nt(r,{projection:"EPSG:3857"}),i=rt(a),s=Et(r,{projection:"EPSG:3857"}),l=Pt(s),c=[],d=[],u={},p=y=>{const g=y.getGeometry();if(!g)return!1;const m=g.getExtent();return m[2]<o[0]||m[0]>o[2]||m[3]<o[1]||m[1]>o[3]?!1:r.intersectsExtent(m)&&this._geometriesIntersect(r,g)},h=(y,g)=>{y.getLayers().forEach(m=>{if(m instanceof Te)h(m,m.get("title")||g);else if(m instanceof O&&m.getVisible()){const b=m.get("title")||g||"Unknown",_=m.getSource();if(!_)return;const S=_.getFeaturesInExtent(o);for(const T of S){const v=T.get("_layerType");v==="measure_area"||v==="measure_circle"||v==="measure_circle_radius"||p(T)&&(v==="parcel"?c.push(T):v==="collector_zone"?d.push(T):(u[b]||(u[b]=[]),u[b].push(T)))}}})};h(this.overlayGroup,"Overlays");const f=[{label:"Area",value:i,_first:!0},{label:"Perimeter",value:l},...this._collectIntersectionRows(c,d,u)];this._showAnalysisPopup("📐","Area Analysis",f,t,{kind:"area",clipGeometry:r,parcelFeatures:c,zoneFeatures:d,otherByLayer:u})}_geometriesIntersect(e,t){const r=t.getType();if(r==="Polygon"||r==="MultiPolygon"){const o=t.getFlatCoordinates(),a=t.getStride();for(let l=0;l<o.length;l+=a)if(e.intersectsCoordinate([o[l],o[l+1]]))return!0;const i=e.getFlatCoordinates(),s=e.getStride();for(let l=0;l<i.length;l+=s)if(t.intersectsCoordinate([i[l],i[l+1]]))return!0;return!1}if(r==="Point")return e.intersectsCoordinate(t.getCoordinates());if(r==="LineString"||r==="MultiLineString"){const o=t.getFlatCoordinates(),a=t.getStride();for(let i=0;i<o.length;i+=a)if(e.intersectsCoordinate([o[i],o[i+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 _e({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 a="";for(const[l,c]of Object.entries(r)){if(o.includes(l))continue;const d=c==null?"":String(c),u=this.escapeHtml(l),p=this.escapeHtml(d);a+=`
|
||
<div style="margin-bottom:8px;">
|
||
<label style="display:block;font-size:11px;font-weight:600;color:var(--muted-foreground, #7a7a7a);margin-bottom:2px;">${u}</label>
|
||
<input type="text" name="${u}" value="${p}"
|
||
style="width:100%;padding:6px 8px;border:1px solid var(--border, #1e1a4b1f);border-radius:4px;font-size:13px;color:var(--foreground, #1e1a4b);background:var(--muted, #f2f4f7);min-height:34px;"
|
||
/>
|
||
</div>
|
||
`}const i=`
|
||
<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;">
|
||
${a}
|
||
<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=i,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 s=this.parcelEditElement.querySelector(".parcel-edit-form");s.addEventListener("submit",l=>{l.preventDefault();const c=new FormData(s),d={};for(const[u,p]of c.entries())d[u]=p;d._layerType="parcel";for(const[u,p]of Object.entries(d))this._parcelEditFeature.set(u,p);for(const u of this._parcelEditCallbacks)u(this._parcelEditFeature,d);this.hideParcelEditPopup()})}hideParcelEditPopup(){this.parcelEditPopup.setPosition(void 0),this._parcelEditFeature=null}onParcelEdit(e){this._parcelEditCallbacks.push(e)}createMergePopup(){this.mergePopupElement=document.createElement("div"),this.mergePopupElement.className="map-merge-popup",this.mergePopupElement.style.cssText=`
|
||
position: absolute;
|
||
background: var(--card, #fff);
|
||
color: var(--card-foreground, #1e1a4b);
|
||
border-radius: 10px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
font-family: var(--font-body, 'Exo', sans-serif);
|
||
font-size: 13px;
|
||
min-width: 280px;
|
||
max-width: 360px;
|
||
z-index: 1002;
|
||
border: 2px solid #10b981;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
`,this.mergePopup=new _e({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 a=["UPN","upn","id","parcelid","parcel_id","PARCELID","PARCEL_ID","ID"],i=f=>{for(const y of a)if(f[y]!==void 0&&f[y]!==null&&String(f[y]).trim())return{field:y,value:String(f[y])};return{field:"id",value:"Unknown"}},s=i(t),l=i(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(s.field)}: ${this.escapeHtml(s.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 y=this.mergePopupElement.querySelector('input[name="merge-choice"]:checked').value==="A"?t:r,g=["geometry"];for(const[m,b]of Object.entries(y))g.includes(m)||e.set(m,b);e.set("_layerType","parcel");for(const m of this._parcelEditCallbacks)m(e,y);d()});const u=this.mergePopupElement.querySelectorAll("label"),p=this.mergePopupElement.querySelectorAll('input[name="merge-choice"]'),h=()=>{u.forEach(f=>{const y=f.querySelector("input");f.style.borderColor=y.checked?y.value==="A"?"#0ea5e9":"#f59e0b":"var(--border, #1e1a4b1f)"})};p.forEach(f=>f.addEventListener("change",h)),h()}createDividePopup(){this.dividePopupElement=document.createElement("div"),this.dividePopupElement.className="map-divide-popup",this.dividePopupElement.style.cssText=`
|
||
position: absolute;
|
||
background: var(--card, #fff);
|
||
color: var(--card-foreground, #1e1a4b);
|
||
border-radius: 10px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||
font-family: var(--font-body, 'Exo', sans-serif);
|
||
font-size: 13px;
|
||
min-width: 260px;
|
||
max-width: 320px;
|
||
z-index: 1002;
|
||
border: 2px solid #8b5cf6;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
`,this.dividePopup=new _e({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 a=this.dividePopupElement.querySelector(".divide-input");a.focus(),a.select();const i=()=>{this.hideDividePopup(),this._polygonDivideInteraction.cancelDivide()};this.dividePopupElement.querySelector(".divide-popup-close").addEventListener("click",i),this.dividePopupElement.querySelector(".divide-popup-cancel").addEventListener("click",i),this.dividePopupElement.querySelector(".divide-popup-confirm").addEventListener("click",()=>{const s=parseInt(a.value,10);if(!s||s<2){a.style.borderColor="#ef4444";return}this.hideDividePopup(),this._polygonDivideInteraction.performDivide(s)}),a.addEventListener("keydown",s=>{s.key==="Enter"&&(s.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 _e({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(a=>{if(!(t.length>0)){if(a instanceof Te)r(a);else if(a instanceof O){const i=a.getSource();if(!i)return;for(const s of i.getFeatures()){if(s.get("_layerType")!=="parcel")continue;const l=s.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 a=e.getGeometry(),i=nt(a,{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>${rt(i)}</strong>
|
||
</div>
|
||
<form class="drawn-polygon-form" style="padding:10px 12px;overflow-y:auto;flex:1;">
|
||
${o}
|
||
<div style="display:flex;gap:8px;margin-top:10px;">
|
||
<button type="submit" style="flex:1;padding:8px 12px;background:var(--success, #006b3f);color:var(--success-foreground, #fff);border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
|
||
💾 Save
|
||
</button>
|
||
<button type="button" class="drawn-polygon-cancel" style="flex:1;padding:8px 12px;background:var(--muted-foreground, #7a7a7a);color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;min-height:38px;">
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</form>
|
||
`;this.drawnPolygonElement.innerHTML=l,this.drawnPolygonPopup.setPosition(t),this.drawnPolygonElement.querySelector(".drawn-polygon-close").addEventListener("click",()=>{this.hideDrawnPolygonPopup()}),this.drawnPolygonElement.querySelector(".drawn-polygon-cancel").addEventListener("click",()=>{this.hideDrawnPolygonPopup()});const c=this.drawnPolygonElement.querySelector(".drawn-polygon-form");c.addEventListener("submit",d=>{d.preventDefault();const u=new FormData(c),p={};for(const[h,f]of u.entries())p[h]=f;for(const[h,f]of Object.entries(p))this._drawnPolygonFeature.set(h,f);this._drawnPolygonFeature.set("_layerType","parcel");for(const h of this._drawnPolygonCallbacks)h(this._drawnPolygonFeature,p);this.hideDrawnPolygonPopup()})}hideDrawnPolygonPopup(){this.drawnPolygonPopup.setPosition(void 0),this._drawnPolygonFeature=null}onDrawnPolygonSave(e){this._drawnPolygonCallbacks.push(e)}onDblClick(e){return this.dblClickCallbacks.push(e),this.dblClickCallbacks.length===1&&this.map.on("dblclick",t=>{const[r,o]=Me(t.coordinate);let a=null;this.map.forEachFeatureAtPixel(t.pixel,i=>(a=i,!0)),a&&(t.preventDefault(),t.stopPropagation());for(const i of this.dblClickCallbacks)i(r,o,a,t);if(a)return!1}),()=>{const t=this.dblClickCallbacks.indexOf(e);t>-1&&this.dblClickCallbacks.splice(t,1)}}escapeHtml(e){if(!e)return"";const t=document.createElement("div");return t.textContent=e,t.innerHTML}createAddLocationPopup(){this.addLocationPopupElement=document.createElement("div"),this.addLocationPopupElement.className="map-add-location-popup",this.addLocationPopupElement.innerHTML=`
|
||
<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 _e({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]=Me(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),a={name:o.get("name"),category:o.get("category"),description:o.get("description"),lon:this.addLocationCoords.lon,lat:this.addLocationCoords.lat};this.addLocationCallbacks.forEach(i=>i(a)),this.hideAddLocationPopup()})}}createBaseLayers(e){const t=new ne({title:"Topographic",type:"base",zIndex:-100,visible:e==="topo",source:new Le({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 ne({title:"Carto Light",type:"base",zIndex:-100,visible:e==="carto-light",source:new Le({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 ne({title:"Carto Dark",type:"base",zIndex:-100,visible:e==="carto-dark",source:new Le({url:"https://{a-c}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",attributions:"© CARTO",maxZoom:19,crossOrigin:"anonymous"})});o.set("basemapKey","carto-dark");const a=new ne({title:"OSM Cycle map",type:"base",zIndex:-100,visible:!1,source:new Po({url:"https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=ae1339c46dd3446b9c491e7336d38760"})});a.set("basemapKey","cycle");const i=new ne({title:"Satellite",type:"base",zIndex:-100,visible:e==="satellite",source:new Le({url:"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",attributions:"Tiles © Esri",maxZoom:19,crossOrigin:"anonymous"})});i.set("basemapKey","satellite");const s=new ne({title:"Google Sat",type:"base",zIndex:-100,visible:e==="googlesat",source:new Le({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"})});s.set("basemapKey","googlesat");const l=new ne({title:"OpenStreetMap",type:"base",zIndex:-100,visible:e==="osm",source:new Po});l.set("basemapKey","osm"),this._baseMapLayers=[r,o,a,i,s,l,t];const c=new Te({title:"Base Maps",layers:[r,o,i,a,s,l,t]});return c.set("displayInLayerSwitcher",!1),c}setBaseMap(e){if(!this._baseMapLayers)return!1;if(e==="none"){for(const r of this._baseMapLayers)r.setVisible(!1);return console.log("[MapView] Base map switched off (none)"),this.map.dispatchEvent({type:"basemapchange",key:"none"}),!0}let t=!1;for(const r of this._baseMapLayers){const o=r.get("basemapKey")===e;r.setVisible(o),o&&(t=!0)}return t&&(console.log("[MapView] Base map switched to:",e),this.map.dispatchEvent({type:"basemapchange",key:e})),t}_createBaseMapPicker(){const e=[{key:"topo",label:"Topographic",grad:"linear-gradient(135deg,#e8d5b7,#a67c52)"},{key:"osm",label:"OpenStreetMap",grad:"linear-gradient(135deg,#d4e6f1,#85c1e9)"},{key:"satellite",label:"Satellite",grad:"linear-gradient(135deg,#1b4332,#40916c)"},{key:"googlesat",label:"Google Sat",grad:"linear-gradient(135deg,#2a5d3d,#4a8c5a)"},{key:"carto-light",label:"Carto Light",grad:"linear-gradient(135deg,#f5f5f5,#d4d4d4)"},{key:"carto-dark",label:"Carto Dark",grad:"linear-gradient(135deg,#1a1a2e,#0f3460)"},{key:"none",label:"None",grad:"repeating-conic-gradient(#e5e7eb 0 25%, #fff 0 50%) 50% / 12px 12px"}],t=this.map.getTargetElement();if(!t)return;const r=document.createElement("button");r.type="button",r.className="ls-basemap-toggle",r.title="Switch base map",r.setAttribute("aria-label","Switch base map"),r.innerHTML='<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true"><path d="M9 2L16 5.8L9 9.6L2 5.8L9 2Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M2 9.2L9 13L16 9.2" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/><path d="M2 12.4L9 16.2L16 12.4" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" stroke-opacity=".4"/></svg>',t.appendChild(r);const o=document.createElement("div");o.className="ls-basemap-panel",o.innerHTML='<div class="ls-basemap-header">Base Map</div><div class="ls-basemap-grid">'+e.map(i=>`
|
||
<label class="ls-bm-chip">
|
||
<input type="radio" name="lupmis-basemap" value="${i.key}">
|
||
<div class="ls-bm-label">
|
||
<div class="ls-bm-thumb" style="background:${i.grad};"></div>
|
||
<div class="ls-bm-name">${i.label}</div>
|
||
</div>
|
||
</label>
|
||
`).join("")+"</div>",t.appendChild(o),this._basemapPanel=o,this._basemapToggle=r;const a=i=>{const s=i||this._baseMapLayers?.find(l=>l.getVisible())?.get("basemapKey");o.querySelectorAll('input[name="lupmis-basemap"]').forEach(l=>{l.checked=l.value===s})};a(),r.addEventListener("click",i=>{i.stopPropagation();const s=!o.classList.contains("open");o.classList.toggle("open",s),r.classList.toggle("active",s),s&&a()}),document.addEventListener("click",i=>{o.classList.contains("open")&&(o.contains(i.target)||r.contains(i.target)||(o.classList.remove("open"),r.classList.remove("active")))}),o.addEventListener("change",i=>{const s=i.target.closest('input[type=radio][name="lupmis-basemap"]');if(!s)return;const l=s.value;this.setBaseMap(l);try{localStorage.setItem("default-basemap",l)}catch{}o.classList.remove("open"),r.classList.remove("active")}),this.map.on("basemapchange",i=>a(i.key))}_initGpsRendering(){this._gpsPositionSource=new G,this._gpsTrailSource=new G,this._gpsTrailCoords=[],this._gpsTrailLayer=new O({source:this._gpsTrailSource,zIndex:940,style:new M({stroke:new I({color:"#ff6d00",width:4,lineCap:"round",lineJoin:"round"})}),properties:{title:"GPS Trail",displayInLayerSwitcher:!1}}),this._gpsPositionLayer=new O({source:this._gpsPositionSource,zIndex:950,style:e=>e.get("_kind")==="accuracy"?new M({fill:new A({color:"rgba(0,94,184,0.12)"}),stroke:new I({color:"rgba(0,94,184,0.35)",width:1})}):new M({image:new ue({radius:7,fill:new A({color:"#005eb8"}),stroke:new I({color:"#ffffff",width:2.5})})}),properties:{title:"GPS Position",displayInLayerSwitcher:!1}}),this.map.addLayer(this._gpsTrailLayer),this.map.addLayer(this._gpsPositionLayer),this._gpsCallbacks={locate:[],record:[]},this._gpsRecording=!1}onLocateMe(e){this._gpsCallbacks.locate.push(e)}onToggleRecording(e){this._gpsCallbacks.record.push(e)}showCurrentPosition(e,t,r=null){if(e==null||t==null)return;const o=Q([e,t]);if(this._gpsPositionSource.clear(),r&&r>0){const i=r/Math.cos(t*Math.PI/180),s=new de({geometry:new it([this._circleRing(o,i)])});s.set("_kind","accuracy"),this._gpsPositionSource.addFeature(s)}const a=new de({geometry:new ut(o)});a.set("_kind","dot"),this._gpsPositionSource.addFeature(a)}_circleRing(e,t,r=48){const o=[],i=t/1;for(let s=0;s<=r;s++){const l=s/r*2*Math.PI;o.push([e[0]+i*Math.cos(l),e[1]+i*Math.sin(l)])}return o}centerOn(e,t,r=16){this.map.getView().animate({center:Q([e,t]),zoom:r,duration:500})}startTrailRender(){this._gpsTrailCoords=[],this._gpsTrailSource.clear()}appendTrailPoint(e,t){e==null||t==null||(this._gpsTrailCoords.push(Q([e,t])),this._gpsTrailSource.clear(),this._gpsTrailCoords.length>=2&&this._gpsTrailSource.addFeature(new de({geometry:new pe(this._gpsTrailCoords)})))}clearTrailRender(){this._gpsTrailCoords=[],this._gpsTrailSource.clear()}setRecordingState(e){this._gpsRecording=!!e,this._recordBtn&&(this._recordBtn.classList.toggle("recording",this._gpsRecording),this._recordBtn.title=this._gpsRecording?"Stop trail recording":"Record GPS trail",this._recordBtn.innerHTML=this._gpsRecording?'<i class="bi bi-stop-fill"></i>':'<i class="bi bi-record-circle"></i>'),this._locateToggle&&this._locateToggle.classList.toggle("recording",this._gpsRecording)}_createLocationControl(){const e=this.map.getTargetElement();if(!e)return;const t=document.createElement("button");t.type="button",t.className="ls-locate-toggle",t.title="My Location",t.setAttribute("aria-label","My Location"),t.innerHTML='<i class="bi bi-geo-alt-fill"></i>',e.appendChild(t);const r=document.createElement("div");r.className="ls-locate-actions",r.innerHTML='<button type="button" class="ls-locate-btn ls-locate-me" title="Locate me"><i class="bi bi-crosshair"></i></button><button type="button" class="ls-locate-btn ls-locate-record" title="Record GPS trail"><i class="bi bi-record-circle"></i></button>',e.appendChild(r),this._locateToggle=t,this._locateActions=r,this._locateMeBtn=r.querySelector(".ls-locate-me"),this._recordBtn=r.querySelector(".ls-locate-record");const o=()=>{r.classList.remove("open"),t.classList.remove("active")},a=()=>{r.classList.add("open"),t.classList.add("active")};t.addEventListener("click",i=>{i.stopPropagation(),r.classList.contains("open")?o():a()}),document.addEventListener("click",i=>{r.classList.contains("open")&&(r.contains(i.target)||t.contains(i.target)||this._gpsRecording||o())}),this._locateMeBtn.addEventListener("click",i=>{i.stopPropagation();for(const s of this._gpsCallbacks.locate)try{s()}catch(l){console.error(l)}this._gpsRecording||o()}),this._recordBtn.addEventListener("click",i=>{i.stopPropagation();const s=!this._gpsRecording;for(const l of this._gpsCallbacks.record)try{l(s)}catch(c){console.error(c)}})}getFeatureStyle(e){const t=e.get("category")||"default",r=this.getEmoji(t);if(e===this.selectedFeature)return[new M({image:new ue({radius:22,fill:new A({color:"rgba(220, 38, 38, 0.25)"}),stroke:new I({color:"#dc2626",width:3})})}),new M({text:new st({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),a=r.fontSize||28;this.categoryStyles[t]=this.createEmojiStyle(o,a)}this.markerSource.changed()}addMarker(e,t,r={}){console.log("[MapView] Adding marker at",e,t,"with properties:",r);const o=new de({geometry:new ut(Q([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 de({geometry:new ut(Q([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:Q([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 Me(e)}getZoom(){return this.map.getView().getZoom()}setCenter(e,t){this.map.getView().setCenter(Q([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,a=null;if(this.map.forEachFeatureAtPixel(t.pixel,l=>{l.get("_layerType")==="parcel"&&(o=!0),l.get("name")&&(a=l),r=!0}),r&&!o&&!a)return;const[i,s]=Me(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(i,s,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]=Me(t.coordinate);let a=null;this.map.forEachFeatureAtPixel(t.pixel,i=>{if(i.get("name"))return a=i,!0}),this.map.getTargetElement().style.cursor=a?"pointer":"",e(r,o,a,t)})}enableHoverCursor(){}addGeoJSONLayer(e,t,r={},o=null){const{strokeColor:a="#3b82f6",strokeWidth:i=2,strokeDash:s=null,fillColor:l="rgba(59,130,246,0.1)",lineCasingColor:c=null,lineCasingWidth:d=null,pointRadius:u=5,pointFillColor:p=null,pointStrokeColor:h="#ffffff",pointStrokeWidth:f=1.5}=r,y=new G({features:new ie().readFeatures(e,{featureProjection:"EPSG:3857"})}),g=new A({color:l}),m=new ue({radius:u,fill:new A({color:p||a}),stroke:new I({color:h,width:f})}),b=new I({color:a,width:i,...s?{lineDash:s}:{}});let _;if(c){const x=d??i+2;_=[new M({stroke:new I({color:c,width:x})}),new M({stroke:b,fill:g,image:m})]}else _=new M({stroke:b,fill:g,image:m});const S=new O({title:t,source:y,style:_});S.set("typeTag",r.typeTag||"VEC");const T=x=>x?x.includes("Polygon")?"Vector / Polygon":x.includes("LineString")?"Vector / Line":x.includes("Point")?"Vector / Point":"Vector":null;if(r.typeDescription)S.set("typeDescription",r.typeDescription);else{const x=y.getFeatures(),P=T(x[0]?.getGeometry?.()?.getType?.());if(P)S.set("typeDescription",P);else{const k=q=>{const U=T(q.feature.getGeometry?.()?.getType?.());U&&S.set("typeDescription",U),y.un("addfeature",k)};y.on("addfeature",k)}}return(o||this.overlayGroup).getLayers().push(S),console.log("[MapView] GeoJSON layer added:",t,"→",y.getFeatures().length,"features",o?`(in group "${o.get("title")}")`:""),S}addLayerGroup(e,t,r=""){const o=new Te({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,a={}){const i=this.getLayerGroupByTitle(e);if(!i)return console.warn(`[MapView] Layer group "${e}" not found — cannot add WMS layer "${t}"`),null;const s={LAYERS:o,TILED:!0,WIDTH:256,HEIGHT:256};a.style!==void 0&&(s.STYLES=a.style);const l=new Mo({url:r,params:s,serverType:a.serverType!==void 0?a.serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1,attributions:a.attributions}),c=new ne({title:t,visible:a.visible!==void 0?a.visible:!0,source:l,opacity:a.opacity!==void 0?a.opacity:1,zIndex:a.zIndex});if(c.set("typeTag","WMS"),c.set("typeDescription","WMS / Raster"),l.on("tileloaderror",()=>{B(`WMS layer "${t}" — tile load error. Check the URL and layer name.`,"warning",5e3)}),i.getLayers().push(c),a.legendUrl)try{this._registerLegend(c,t,a.legendUrl)}catch(d){console.warn(`[MapView] Could not register legend for "${t}":`,d)}return a.onlineOnly&&this._attachOnlineOnlyHandler(c,t),console.log(`[MapView] WMS layer added: "${t}" → group "${e}"`),c}addXYZLayer(e,t,r,o={}){const a=this.getLayerGroupByTitle(e);if(!a)return console.warn(`[MapView] Layer group "${e}" not found — cannot add XYZ layer "${t}"`),null;const i=new Le({url:r,crossOrigin:"anonymous",maxZoom:o.maxZoom!==void 0?o.maxZoom:19,attributions:o.attributions}),s=new ne({title:t,visible:o.visible!==void 0?o.visible:!0,source:i,opacity:o.opacity!==void 0?o.opacity:1,zIndex:o.zIndex});if(s.set("typeTag","XYZ"),s.set("typeDescription","XYZ / Tile"),i.on("tileloaderror",()=>{B(`XYZ layer "${t}" — tile load error. Check the URL.`,"warning",5e3)}),a.getLayers().push(s),o.legendUrl)try{this._registerLegend(s,t,o.legendUrl)}catch(l){console.warn(`[MapView] Could not register legend for "${t}":`,l)}return o.onlineOnly&&this._attachOnlineOnlyHandler(s,t),console.log(`[MapView] XYZ layer added: "${t}" → group "${e}"`),s}_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(i=>{i.addEventListener("change",()=>{const s=i.value;s==="xyz"?(t.style.display="none",o.placeholder="https://example.com/tiles/{z}/{x}/{y}.png"):(t.style.display="",o.placeholder=s==="wms"?"https://example.com/wms":"https://example.com/wfs",r.textContent=s==="wms"?"WMS LAYERS parameter (e.g. workspace:layer)":"WFS typename (e.g. workspace:layer)")})});const a=()=>this._hideAddLayerDialog();e.querySelector(".add-layer-close").addEventListener("click",a),e.querySelector(".add-layer-cancel").addEventListener("click",a),this._addLayerDialog.addEventListener("click",i=>{i.target===this._addLayerDialog&&a()}),e.querySelector(".add-layer-confirm").addEventListener("click",()=>{const i=e.querySelector('input[name="add-layer-type"]:checked').value,s=e.querySelector(".add-layer-url").value.trim(),l=e.querySelector(".add-layer-name").value.trim(),c=e.querySelector(".add-layer-title").value.trim();if(!s){e.querySelector(".add-layer-url").style.borderColor="#ef4444";return}if((i==="wms"||i==="wfs")&&!l){e.querySelector(".add-layer-name").style.borderColor="#ef4444";return}if(!c){e.querySelector(".add-layer-title").style.borderColor="#ef4444";return}this._addExternalLayer(i,s,l,c),this._hideAddLayerDialog()}),e.addEventListener("keydown",i=>{i.key==="Enter"&&(i.preventDefault(),e.querySelector(".add-layer-confirm").click()),i.key==="Escape"&&(i.preventDefault(),a())})}showAddLayerDialog(){const e=this._addLayerDialog;e.querySelector(".add-layer-url").value="",e.querySelector(".add-layer-name").value="",e.querySelector(".add-layer-title").value="",e.querySelectorAll('input[name="add-layer-type"]')[0].checked=!0,e.querySelector(".add-layer-name-row").style.display="",e.querySelector(".add-layer-url").placeholder="https://example.com/wms",e.querySelector(".add-layer-name-hint").textContent="WMS LAYERS parameter (e.g. workspace:layer)",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 a=this._externalSourceGroup;if(!a){B('Layer group "External Source" not found.',"error",4e3);return}let i;switch(e){case"wms":{const s=new Mo({url:t,params:{LAYERS:r,TILED:!0,WIDTH:256,HEIGHT:256},serverType:"geoserver",crossOrigin:"anonymous",hidpi:!1});i=new ne({title:o,visible:!0,source:s}),s.on("tileloaderror",()=>{B(`WMS "${o}" — tile load error. Check URL and layer name.`,"warning",5e3)});break}case"wfs":{const s=`${t}${t.includes("?")?"&":"?"}service=WFS&version=1.1.0&request=GetFeature&typename=${encodeURIComponent(r)}&outputFormat=application/json&srsname=EPSG:3857`,l=new G({url:s,format:new ie});l.on("featuresloaderror",()=>{B(`WFS "${o}" — load error. Check URL and layer name.`,"warning",5e3)}),i=new O({title:o,visible:!0,source:l,style:new M({stroke:new I({color:"#e11d48",width:2}),fill:new A({color:"rgba(225,29,72,0.15)"})})});break}case"xyz":i=new ne({title:o,visible:!0,source:new Le({url:t,crossOrigin:"anonymous"})}),i.getSource().on("tileloaderror",()=>{B(`XYZ "${o}" — tile load error. Check the URL template.`,"warning",5e3)});break;default:B(`Unknown layer type: ${e}`,"error",4e3);return}i.set("typeTag",e.toUpperCase()),i.set("typeDescription",{wms:"WMS / Raster",wfs:"WFS / Vector",xyz:"XYZ / Tile"}[e]||e.toUpperCase()),i.set("removable",!0),a.getLayers().push(i),B(`Layer "${o}" added to External Source.`,"success",3e3),console.log(`[MapView] External ${e.toUpperCase()} layer added: "${o}"`)}_decorateLayerListItem(e,t){const r=e.get("typeTag");if(r){const c=t.querySelector(":scope > .li-content > label > span");if(c&&!c.querySelector(":scope > .ls-type-tag")){const d=document.createElement("span");d.className=`ls-type-tag ls-type-tag-${String(r).toLowerCase()}`,d.textContent=String(r),d.title=`${r} layer`,c.appendChild(d)}}const o=t.querySelector(":scope > .ol-layerswitcher-buttons");if(o){const c=o.querySelector(":scope > .expend-layers, :scope > .collapse-layers");c&&!c.querySelector(":scope > svg.ls-chevron-svg")&&(c.innerHTML='<svg class="ls-chevron-svg" width="11" height="11" viewBox="0 0 11 11" fill="none" aria-hidden="true"><path d="M3 2L7 5.5L3 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"></path></svg>')}const a=t.querySelector(":scope > .li-content"),i=()=>{if(!a)return;const c=e.get("typeDescription");let d=a.querySelector(":scope > .ls-layer-subtitle");if(!c){d&&d.remove();return}if(!d){d=document.createElement("div"),d.className="ls-layer-subtitle";const u=a.querySelector(":scope > label");u&&u.nextSibling?a.insertBefore(d,u.nextSibling):a.appendChild(d)}d.textContent=c};if(i(),e._lsSubtitleHooked||(e._lsSubtitleHooked=!0,e.on("change:typeDescription",()=>{i()})),e.get("removable")===!0&&o&&!o.querySelector(":scope > .ls-remove-btn")){const c=document.createElement("button");c.type="button",c.className="ls-remove-btn",c.title="Remove this layer",c.setAttribute("aria-label","Remove layer"),c.innerHTML='<svg width="11" height="11" viewBox="0 0 11 11" fill="none" aria-hidden="true"><path d="M2 2l7 7M9 2L2 9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"></path></svg>',c.addEventListener("click",d=>{d.stopPropagation(),this._removeLayer(e)}),o.appendChild(c)}const s=e.get("_externalImportId");if(s!=null){const c=t.querySelector(":scope > .li-content > label > span");let d=c?c.querySelector(":scope > .ls-import-chip"):null;const u=e.get("_externalImportStatus")||"mapped",p=e.getSource()?.getFeatures().length??0,h=e.get("_externalImportErrorCount")??0,f=(()=>{switch(u){case"mapped":return{text:`Upload ${p}`,cls:"ls-import-chip-mapped",title:"Upload this dataset to the database",clickable:!0};case"uploading":return{text:"…",cls:"ls-import-chip-uploading",title:"Uploading…",clickable:!1};case"submitted":return{text:"✓ submitted",cls:"ls-import-chip-submitted",title:"Uploaded — awaiting supervisor review",clickable:!1};case"migrated":return{text:"✓ live",cls:"ls-import-chip-migrated",title:"Approved by supervisor and live on the server",clickable:!1};case"failed":return{text:`${h} errors — fix?`,cls:"ls-import-chip-failed",title:"Some rows failed; click to review",clickable:!0};case"other":case null:case void 0:default:return null}})();if(!f)d&&d.remove();else if(c){d||(d=document.createElement("span"),d.className="ls-import-chip",c.appendChild(d)),d.className=`ls-import-chip ${f.cls}`,d.textContent=f.text,d.title=f.title,d.style.cursor=f.clickable?"pointer":"default",d.style.opacity=f.clickable?"1":"0.85";const y=d.cloneNode(!0);d.replaceWith(y),d=y,f.clickable&&d.addEventListener("click",g=>{g.preventDefault(),g.stopPropagation(),window.dispatchEvent(new CustomEvent("lupmis:import-chip-click",{detail:{importId:s,status:u,layer:e}}))})}}if((e.get("title")||"").toLowerCase().includes("external")&&(this._externalSourceGroup=e,o&&!o.querySelector(".ol-add-layer"))){const c=document.createElement("span");c.className="ol-add-layer",c.title="Add external layer",c.textContent="+",c.style.cssText=`
|
||
display:inline-flex !important;align-items:center;justify-content:center;
|
||
width:22px !important;height:22px !important;border-radius:50%;
|
||
background:#41b6a6 !important;color:#fff !important;
|
||
font-size:15px !important;font-weight:700;
|
||
cursor:pointer;line-height:1 !important;
|
||
margin:0 4px 0 0;vertical-align:middle;
|
||
transition:background 0.2s;box-sizing:border-box;border:none;
|
||
`,c.addEventListener("mouseenter",()=>{c.style.background="#329686"}),c.addEventListener("mouseleave",()=>{c.style.background="#41b6a6"}),c.addEventListener("click",d=>{d.stopPropagation(),this.showAddLayerDialog()}),o.prepend(c)}}_removeLayer(e){const t=e.get("title")||"this layer";if(!confirm(`Remove "${t}" from the map?
|
||
|
||
This only affects the current session — built-in layers cannot be removed.`))return;const r=a=>{const i=a.getLayers();if(i.getArray().includes(e))return i.remove(e),!0;let s=!1;return i.forEach(l=>{!s&&l.getLayers&&(s=r(l))}),s};r(this.overlayGroup)?(console.log(`[MapView] Removed layer "${t}"`),B(`Removed "${t}" from the map.`,"info",3e3)):console.warn(`[MapView] Could not find layer "${t}" in any group`)}_refreshLayerSwitcherChrome(e){const t=e.element?.querySelector(".panel-container"),r=e.element?.querySelector("ul.panel");if(!t||!r)return;let o=t.querySelector(":scope > .ls-active-badge");o||(o=document.createElement("div"),o.className="ls-active-badge",o.innerHTML=`
|
||
<span class="ls-active-badge-title">Layers</span>
|
||
<span class="ls-active-badge-count">0 active</span>
|
||
`,t.insertBefore(o,r));let a=t.querySelector(":scope > .ls-footer-row");a||(a=document.createElement("div"),a.className="ls-footer-row",a.innerHTML=`
|
||
<span class="ls-footer-note">— layers total</span>
|
||
<button type="button" class="ls-footer-btn"
|
||
title="Hide every overlay (base map stays on)">
|
||
Reset
|
||
</button>
|
||
`,t.appendChild(a),a.querySelector(".ls-footer-btn").addEventListener("click",s=>{s.stopPropagation(),this._resetAllOverlays()}));const i=this._countLayers();o.querySelector(".ls-active-badge-count").textContent=`${i.activeOverlays} active`,a.querySelector(".ls-footer-note").textContent=`${i.totalOverlays} overlay${i.totalOverlays===1?"":"s"}`}_countLayers(){let e=0,t=0;const r=new Set(["__vertex_highlight__"]),o=a=>{a.getLayers().forEach(i=>{i.get("displayInLayerSwitcher")!==!1&&(r.has(i.get("title"))||(i.getLayers?o(i):(e++,i.getVisible()&&t++)))})};return this.overlayGroup&&o(this.overlayGroup),{totalOverlays:e,activeOverlays:t}}_resetAllOverlays(){const e=new Set(["__vertex_highlight__"]),t=r=>{r.getLayers().forEach(o=>{o.get("displayInLayerSwitcher")!==!1&&(e.has(o.get("title"))||(o.getLayers?t(o):o.setVisible(!1)))})};this.overlayGroup&&t(this.overlayGroup),console.log("[MapView] Reset overlays — all hidden")}_wireLayerSwitcherVisibilityHooks(e){const t=()=>this._refreshLayerSwitcherChrome(e),r=a=>{a._lsVisHooked||(a._lsVisHooked=!0,a.on("change:visible",t))},o=a=>{a.getLayers().forEach(i=>{i.getLayers?(o(i),a._lsAddHooked||(a._lsAddHooked=!0,a.getLayers().on("add",s=>{const l=s.element;l.getLayers?o(l):r(l),t()}))):r(i)})};this.overlayGroup&&o(this.overlayGroup)}_attachOnlineOnlyHandler(e,t){e.set("onlineOnly",!0),e.on("change:visible",()=>{e.getVisible()&&!navigator.onLine&&B(`"${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 Io}_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 a=()=>{try{this._updateLegendPanel()}catch(i){console.warn("[MapView] legend panel update failed:",i)}};e.on("change:visible",a),a()}_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 a=o.getSource&&o.getSource();if(a&&typeof a.getExtent=="function"){const i=a.getExtent();i&&Number.isFinite(i[0])&&(e={extent:i,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 a=Q([e,t]);this.map.getView().animate({center:a,zoom:r,duration:o})}}class pi{constructor(e,t={}){this.map=e,this.options=t,this.measureSource=new G,this.measureLayer=new O({source:this.measureSource,style:this.getMeasureStyle(),title:"Measurements",zIndex:100}),this.drawSource=new G,this.drawLayer=new O({source:this.drawSource,style:this.getDrawStyle(),title:"Draw sketches",displayInLayerSwitcher:!1,zIndex:99});const r=this.map.getLayers();let o=r.getArray().findIndex(a=>a.get("title")==="Overlays");o<0&&(o=r.getLength()),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 M({fill:new A({color:"rgba(255, 233, 106, 0.2)"}),stroke:new I({color:"#8B008B",lineDash:[10,10],width:2}),image:new ue({radius:5,stroke:new I({color:"#8B008B"}),fill:new A({color:"rgba(255, 233, 106, 0.5)"})})})}getDrawStyle(){return new M({fill:new A({color:"rgba(255, 233, 106, 0.3)"}),stroke:new I({color:"#8B008B",width:2}),image:new ue({radius:6,stroke:new I({color:"#8B008B",width:2}),fill:new A({color:"#FFE96A"})})})}createMeasureTooltip(){this.measureTooltipElement&&this.measureTooltipElement.parentNode.removeChild(this.measureTooltipElement),this.measureTooltipElement=document.createElement("div"),this.measureTooltipElement.className="measure-tooltip",this.measureTooltip=new _e({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 Pe({source:this.measureSource,type:"Circle",style:new M({fill:new A({color:"rgba(255, 233, 106, 0.2)"}),stroke:new I({color:"rgba(139, 0, 139, 0.7)",lineDash:[10,10],width:2}),image:new ue({radius:5,stroke:new I({color:"rgba(139, 0, 139, 0.7)"}),fill:new A({color:"rgba(255, 233, 106, 0.5)"})})})});this.activeInteraction=e,this.map.addInteraction(e);let t;return e.on("drawstart",r=>{t=r.feature.getGeometry().on("change",a=>{const i=a.target;if(i instanceof Ar){const s=i.getRadius(),l=Na(s),d=`<strong>${Pt(s)}</strong><br><small>${l}</small>`;this.measureTooltipElement.innerHTML=d,this.measureTooltip.setPosition(i.getLastCoordinate())}})}),e.on("drawend",r=>{const o=r.feature,a=o.getGeometry(),i=a.getCenter(),s=a.getRadius();o.set("_layerType","measure_circle"),o.set("_radius",s),o.set("_center",i);const l=new de({geometry:new pe([i,[i[0]+s,i[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:i,radius:s,area:Math.PI*s*s,feature:o};this.onMeasureCompleteCallbacks.forEach(d=>d(c))}),e}startLineMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new Pe({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",a=>{const i=a.target,s=Et(i),l=Pt(s);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(i.getLastCoordinate())})}),e.on("drawend",r=>{const o=r.feature,a=o.getGeometry(),i=Et(a);this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),Gt(t);const s={type:"line",length:i,feature:o};this.onMeasureCompleteCallbacks.forEach(l=>l(s))}),e}startAreaMeasure(){this.deactivate(),this.createMeasureTooltip();const e=new Pe({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",a=>{const i=a.target,s=nt(i),l=rt(s);this.measureTooltipElement.innerHTML=l,this.measureTooltip.setPosition(i.getInteriorPoint().getCoordinates())})}),e.on("drawend",r=>{const o=r.feature,a=o.getGeometry(),i=nt(a);o.set("_layerType","measure_area"),o.set("_area",i),this.measureTooltipElement.className="measure-tooltip measure-tooltip-static",this.measureTooltipElement=null,this.createMeasureTooltip(),Gt(t);const s={type:"polygon",area:i,feature:o,coordinate:a.getInteriorPoint().getCoordinates()};this.onMeasureCompleteCallbacks.forEach(l=>l(s))}),e}startDrawPoint(){this.deactivate();const e=new Pe({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 Pe({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 Pe({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 ao({group:!0,className:"map-tools-bar"}),r=new ao({toggleOne:!0,group:!0}),o=new ve({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 a=new ve({html:'<span class="tool-icon">📏</span>',title:"Measure Distance",className:"measure-line-btn",onToggle:l=>{l?this.startLineMeasure():this.deactivate()}});r.addControl(a);const i=new ve({html:'<span class="tool-icon">⬛</span>',title:"Measure Area",className:"measure-area-btn",onToggle:l=>{l?this.startAreaMeasure():this.deactivate()}});r.addControl(i);const s=new et({html:'<span class="tool-icon">🗑️</span>',title:"Clear Measurements",className:"clear-measure-btn",handleClick:()=>{this.clearMeasurements(),o.setActive(!1),a.setActive(!1),i.setActive(!1)}});return r.addControl(s),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 Ce=null;async function fi(){if(!("serviceWorker"in navigator))return console.warn("[PWA] Service Workers not supported"),null;try{return Ce=await navigator.serviceWorker.register("/sw.js",{scope:"/"}),console.log("[PWA] Service Worker registered:",Ce.scope),Ce.addEventListener("updatefound",()=>{const n=Ce.installing;n.addEventListener("statechange",()=>{n.state==="installed"&&navigator.serviceWorker.controller&&(console.log("[PWA] New version available"),yi())})}),Ce}catch(n){return console.error("[PWA] Service Worker registration failed:",n),null}}let Re=null,be=null;function hi(n="#install-btn"){if(be=typeof n=="string"?document.querySelector(n):n,!be){console.warn("[PWA] Install button not found:",n);return}be.style.display="none",window.addEventListener("beforeinstallprompt",e=>{e.preventDefault(),Re=e,be.style.display="block",console.log("[PWA] Install prompt ready")}),be.addEventListener("click",async()=>{if(!Re){gi();return}Re.prompt();const{outcome:e}=await Re.userChoice;console.log("[PWA] Install prompt outcome:",e),Re=null,be.style.display="none"}),window.addEventListener("appinstalled",()=>{console.log("[PWA] App installed"),Re=null,be.style.display="none"}),window.matchMedia("(display-mode: standalone)").matches&&(be.style.display="none")}function gi(){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 uo=null;const po=new Set;function mi(n="#offline-indicator"){uo=typeof n=="string"?document.querySelector(n):n,Ht(!navigator.onLine),window.addEventListener("online",()=>{console.log("[PWA] Back online"),Ht(!1),Xo(!1)}),window.addEventListener("offline",()=>{console.log("[PWA] Gone offline"),Ht(!0),Xo(!0)})}function Ht(n){uo&&(uo.style.display=n?"block":"none"),document.body.classList.toggle("is-offline",n)}function Qn(n){return po.add(n),n(!navigator.onLine),()=>po.delete(n)}function Xo(n){for(const e of po)try{e(n)}catch(t){console.error("[PWA] Offline listener error:",t)}}function V(){return navigator.onLine}function yi(){confirm("A new version is available. Reload now?")&&bi()}function bi(){Ce?.waiting&&Ce.waiting.postMessage({type:"SKIP_WAITING"}),window.location.reload()}async function wi({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((a,i)=>setTimeout(()=>i(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 _i(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 xo(n,e,t={},r=5e3,o=1e4){const a=await wi({timeoutMs:o});return new Promise((i,s)=>{const l=new MessageChannel,c=setTimeout(()=>{l.port1.close(),s(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,...p}=d.data;i(p)}},a.postMessage({type:n,...t},[l.port2])})}async function vi(){try{return(await xo("GET_TILE_STATS","TILE_STATS")).stats}catch(n){return console.warn("[PWA] getTileCacheStats failed:",n),null}}async function Ei(){try{return await xo("CLEAR_TILE_CACHES","TILE_CACHES_CLEARED"),!0}catch(n){return console.warn("[PWA] clearTileCaches failed:",n),!1}}async function xi(n){if(!n)return!1;try{return!!(await xo("CLEAR_TILE_CACHE","TILE_CACHE_CLEARED",{cacheName:n})).deleted}catch(e){return console.warn(`[PWA] clearTileCacheForProvider(${n}) failed:`,e),!1}}async function Si(){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 Ti(n={}){const{installButton:e="#install-btn",offlineIndicator:t="#offline-indicator",autoRegisterSW:r=!0}=n;r&&await fi(),hi(e),mi(t),console.log("[PWA] Initialized")}const er={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"}},Li=30*1024,Mt=2*Math.PI*6378137/2;function Vo(n,e){const t=n/Mt*180;let r=e/Mt*180;return r=180/Math.PI*(2*Math.atan(Math.exp(r*Math.PI/180))-Math.PI/2),[t,r]}function Yo(n,e,t){const r=Math.pow(2,t),o=Math.floor((n+180)/360*r),a=e*Math.PI/180,i=Math.floor((1-Math.log(Math.tan(a)+1/Math.cos(a))/Math.PI)/2*r);return{x:o,y:i}}function tr(n,e){const[t,r,o,a]=n,[i,s]=Vo(t,r),[l,c]=Vo(o,a),d=Yo(i,c,e),u=Yo(l,s,e),p=Math.pow(2,e),h=Math.max(0,Math.min(d.x,u.x)),f=Math.min(p-1,Math.max(d.x,u.x)),y=Math.max(0,Math.min(d.y,u.y)),g=Math.min(p-1,Math.max(d.y,u.y));return{z:e,minX:h,maxX:f,minY:y,maxY:g,count:(f-h+1)*(g-y+1)}}function ki(n,e,t){let r=0;for(let o=e;o<=t;o++)r+=tr(n,o).count;return r}function Ii(n,e,t){const r=[];for(let o=e;o<=t;o++){const a=tr(n,o);for(let i=a.minX;i<=a.maxX;i++)for(let s=a.minY;s<=a.maxY;s++)r.push({z:o,x:i,y:s})}return r}function Pi(n,{z:e,x:t,y:r}){return n.replace("{z}",e).replace("{x}",t).replace("{y}",r)}class Mi{constructor({baseMap:e,extent3857:t,minZoom:r,maxZoom:o,concurrency:a=2,interBatchDelayMs:i=50,onProgress:s=()=>{}}){const l=er[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(a,6)),this.interBatchDelayMs=i,this.onProgress=s,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=Ii(this.extent,this.minZoom,this.maxZoom),t=e.length,r=Date.now();let o=0,a=0,i=0,s=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:a,failed:i,cached:s,elapsedMs:d,etaMs:u})};l("running");for(let c=0;c<e.length&&!this._cancelled;c+=this.concurrency){const d=e.slice(c,c+this.concurrency);await Promise.all(d.map(async u=>{if(this._cancelled)return;const p=Pi(this.template,u);try{const h=await fetch(p,{signal:this._abortCtrl.signal,cache:"default"});h.ok?(a++,h.body&&h.body.cancel().catch(()=>{})):(h.status,i++)}catch(h){h.name==="AbortError"||i++}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:a,failed:i,cached:s,elapsedMs:Date.now()-r}}cancel(){this._cancelled=!0,this._abortCtrl&&this._abortCtrl.abort()}}const Ai=(()=>{const n=(r,o)=>{const a=r*Mt/180,i=Math.log(Math.tan((90+o)*Math.PI/360))/(Math.PI/180);return[a,i*Mt/180]},e=n(-3.3,4.5),t=n(1.2,11.2);return[e[0],e[1],t[0],t[1]]})();function Ci(n){return n*Li}const or="https://api.lupmis4luspa.org/api/spatial_planning",Wt="1",Fi="1c46538c712e9b5b";function Di(){try{if(typeof window>"u")return Wt;const n=window.LUPMIS_SESSION;if(!n||typeof n!="object")return Wt;const e=n.district_id;return e==null||String(e).length===0?null:String(e)}catch{}return Wt}const nr={get district_id(){return Di()},api_token:Fi};function Dt(){if(typeof window<"u"&&window.LUPMIS_SESSION&&window.LUPMIS_SESSION.user_id)return window.LUPMIS_SESSION;try{const n=localStorage.getItem("dev-session");if(n){const e=JSON.parse(n);if(e&&e.user_id)return e}}catch{}return null}typeof window<"u"&&(window.lupmisDevSession=n=>{n==null?(localStorage.removeItem("dev-session"),console.log("[Dev] Session override cleared. Reload to apply.")):(localStorage.setItem("dev-session",JSON.stringify(n)),console.log("[Dev] Session override saved. Reload to apply:",n))});const Oi=3e4,Ri=5e3;let Ae=null;async function Ni(n=!1){if(Ae!==null&&!n)return Ae;const e=new AbortController,t=setTimeout(()=>e.abort(),Ri);try{Ae=(await fetch(`${or}/get_layers.php`,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(nr),signal:e.signal})).ok}catch{Ae=!1}finally{clearTimeout(t)}return console.log("[RemoteDB] Server reachable:",Ae),Ae}function Ee(){return Ae}function $i(n,e=Oi){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 he(n,e={},t={}){const r=`${or}/${n}`,o={...nr,...e};console.log("[RemoteDB] POST",r);const a=$i(t);try{const i=await fetch(r,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(o),...t,signal:a.signal});if(!i.ok)throw new Error(`HTTP ${i.status}: ${i.statusText}`);const s=await i.json();return console.log("[RemoteDB] POST response:",n,"→",typeof s=="object"?`${Array.isArray(s)?s.length+" items":"object"}`:s),s}catch(i){throw i.name==="AbortError"?(console.error("[RemoteDB] POST timed out:",n),new Error(`Request timed out: ${n}`)):(console.error("[RemoteDB] POST failed:",n,i),i)}finally{a.clear()}}async function Bi(){return he("get_district_boundary.php")}async function Gi(){return he("get_layers.php")}async function ji(){return he("get_all_collector_zone_per_district.php")}async function qi(){return he("get_parcels_per_district.php")}async function zi(){return he("get_all_footprint_per_district.php")}async function Ui(){return he("get_contours_hillshade.php")}async function Hi(){return he("get_osm_roads.php")}async function Wi(){return he("get_upn_grid_per_district.php")}async function Ki(n,e){const t={client_uuid:n.client_uuid,name:n.name??null,started_at:n.started_at,ended_at:n.ended_at,point_count:n.point_count??e.length,distance_m:n.distance_m??0,points:(e||[]).map(o=>({seq:o.seq,longitude:o.longitude,latitude:o.latitude,altitude:o.altitude??null,accuracy:o.accuracy??null,altitude_accuracy:o.altitude_accuracy??null,heading:o.heading??null,speed:o.speed??null,satellites:o.satellites??null,recorded_at:o.recorded_at}))},r=await he("save_gps_trail.php",t);return{remoteId:r?.id??r?.remote_id??null}}const Xi=63710088e-1,mt=Math.PI/180;function Vi(n,e,t,r){const o=(r-e)*mt,a=(t-n)*mt,i=Math.sin(o/2)**2+Math.cos(e*mt)*Math.cos(r*mt)*Math.sin(a/2)**2;return 2*Xi*Math.asin(Math.min(1,Math.sqrt(i)))}function Jo(n,e=5){return n==null||Number.isNaN(n)?"—":n.toFixed(e)}function Yi(n){return n==null||Number.isNaN(n)?"—":n<1e3?`${Math.round(n)} m`:`${(n/1e3).toFixed(2)} km`}function Ji(n){return n==null||Number.isNaN(n)?"—":`±${Math.round(n)} m`}function Zi(n){return n==null||Number.isNaN(n)?"none":n<=10?"good":n<=30?"fair":"poor"}const Qi={minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0,timeoutMs:15e3,maximumAgeMs:0};class at{constructor(e={}){this.opts={...Qi,...e},this.storage=e.storage||null,this.sync=e.sync||null,this._geo=e.geolocation||(typeof navigator<"u"?navigator.geolocation:null),this._state="idle",this._watchId=null,this._live=!1,this._recording=!1,this._activeTrailId=null,this._activeTrailUuid=null,this._lastRecorded=null,this._lastRecordedAt=0,this._distanceM=0,this._pointCount=0,this._lastFix=null,this._listeners=Object.create(null)}on(e,t){return(this._listeners[e]||(this._listeners[e]=new Set)).add(t),()=>this._listeners[e]?.delete(t)}_emit(e,t){const r=this._listeners[e];if(r)for(const o of r)try{o(t)}catch(a){console.error(`[GeoTracker] listener for "${e}" threw`,a)}}get state(){return this._state}get isRecording(){return this._recording}get lastFix(){return this._lastFix}get isSupported(){return!!this._geo}_setState(e){this._state!==e&&(this._state=e,this._emit("statechange",e))}startLive(){if(!this._geo){this._emit("error",new Error("Geolocation not supported"));return}this._live=!0,this._ensureWatch()}stopLive(){this._live=!1,this._recording||this._teardownWatch()}getCurrentPosition(){return new Promise((e,t)=>{if(!this._geo){t(new Error("Geolocation not supported"));return}this._geo.getCurrentPosition(r=>{const o=at.normalize(r);this._lastFix=o,this._emit("position",o),e(o)},r=>{this._emit("error",r),t(r)},{enableHighAccuracy:this.opts.enableHighAccuracy,timeout:this.opts.timeoutMs,maximumAge:this.opts.maximumAgeMs})})}async startRecording(e={}){if(!this._geo)throw new Error("Geolocation not supported");if(!this.storage)throw new Error("GeoTracker: no storage adapter configured");if(this._recording)return{trailId:this._activeTrailId,uuid:this._activeTrailUuid};const t=at.uuid(),r=new Date().toISOString(),o={uuid:t,name:e.name||null,startedAt:r,...e},a=await this.storage.createTrail(o);return this._activeTrailId=a,this._activeTrailUuid=t,this._lastRecorded=null,this._lastRecordedAt=0,this._distanceM=0,this._pointCount=0,this._recording=!0,this._ensureWatch(),this._setState("recording"),this._emit("trailstart",{trailId:a,uuid:t,startedAt:r}),{trailId:a,uuid:t}}async stopRecording(){if(!this._recording)return null;const e=this._activeTrailId,r={endedAt:new Date().toISOString(),pointCount:this._pointCount,distanceM:this._distanceM};this._recording=!1,this._live||this._teardownWatch(),this._setState(this._live?"watching":"idle");try{await this.storage.finishTrail(e,r)}catch(a){this._emit("error",a)}this._emit("trailstop",{trailId:e,...r});let o=!1;if(this.sync)try{o=await this._syncTrail(e)}catch(a){this._emit("error",a)}return this._activeTrailId=null,this._activeTrailUuid=null,{trailId:e,pointCount:r.pointCount,distanceM:r.distanceM,synced:o}}async syncPending(){if(!this.sync||!this.storage)return{pushed:0,failed:0};if(this.sync.isOnline&&!this.sync.isOnline())return{pushed:0,failed:0};let e=0,t=0;const r=await this.storage.getUnsyncedTrails();for(const o of r)try{await this._syncTrail(o.id??o.trailId,o)?e++:t++}catch(a){t++,this._emit("error",a)}return this._emit("syncstatus",{pushed:e,failed:t}),{pushed:e,failed:t}}async _syncTrail(e,t){const r=await this.storage.getTrailPoints(e),o=t||{id:e},a=await this.sync.pushTrail(o,r),i=a&&(a.remoteId??a.id??null);return await this.storage.markTrailSynced(e,i),!0}_ensureWatch(){if(this._watchId!=null||!this._geo){this._state==="idle"&&this._live&&this._setState("watching");return}this._watchId=this._geo.watchPosition(e=>this._onFix(e),e=>this._emit("error",e),{enableHighAccuracy:this.opts.enableHighAccuracy,timeout:this.opts.timeoutMs,maximumAge:this.opts.maximumAgeMs}),this._recording||this._setState("watching")}_teardownWatch(){this._watchId!=null&&this._geo&&this._geo.clearWatch(this._watchId),this._watchId=null}async _onFix(e){const t=at.normalize(e);if(this._lastFix=t,this._emit("position",t),!this._recording)return;const{minIntervalMs:r,minDistanceM:o,heartbeatMs:a,maxAccuracyM:i}=this.opts,s=t.timestamp;if(this._lastRecordedAt&&s-this._lastRecordedAt<r||i>0&&t.accuracy!=null&&t.accuracy>i&&this._lastRecorded)return;let l=!1,c=0;if(!this._lastRecorded)l=!0;else{c=Vi(this._lastRecorded.lon,this._lastRecorded.lat,t.lon,t.lat);const d=s-this._lastRecordedAt;(c>=o||d>=a)&&(l=!0)}if(l){this._lastRecorded&&(this._distanceM+=c),this._pointCount+=1,this._lastRecorded={lon:t.lon,lat:t.lat,timestamp:s},this._lastRecordedAt=s;try{await this.storage.addPoint(this._activeTrailId,{...t,seq:this._pointCount}),this._emit("point",{trailId:this._activeTrailId,seq:this._pointCount,point:t,distanceM:this._distanceM,pointCount:this._pointCount})}catch(d){this._emit("error",d)}}}static normalize(e){const t=e.coords||{},r=o=>o!=null&&!Number.isNaN(o)?o:null;return{lon:t.longitude,lat:t.latitude,accuracy:r(t.accuracy),altitude:r(t.altitude),altitudeAccuracy:r(t.altitudeAccuracy),heading:r(t.heading),speed:r(t.speed),satellites:null,timestamp:e.timestamp||Date.now()}}static uuid(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,e=>{const t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}}const es={async createTrail(n){const e=n.districtId??Dt()?.district_id??null;return Pa({...n,districtId:e!=null?String(e):null})},addPoint:(n,e)=>Ma(n,e),finishTrail:(n,e)=>Aa(n,e),getUnsyncedTrails:()=>Ca(),getTrailPoints:n=>Fa(n),markTrailSynced:(n,e)=>Da(n,e)},ts={pushTrail:(n,e)=>Ki(n,e),isOnline:()=>V()},we=new at({storage:es,sync:ts,minDistanceM:5,minIntervalMs:1e3,heartbeatMs:2e4,maxAccuracyM:50,enableHighAccuracy:!0}),os=new Set(["set:view","set:selected","clear:selected","set:basemap"]);function ns({mapView:n,embedConfig:e}){const t=n.getMap(),r=window.parent&&window.parent!==window?window.parent:null,o=new G,a=new O({source:o,zIndex:9999,style:new M({stroke:new I({color:"#f97316",width:3}),fill:new A({color:"rgba(249,115,22,0.18)"})}),properties:{title:"Permit selection",displayInLayerSwitcher:!1}});t.addLayer(a);let i=null,s=e?.upn?String(e.upn):null,l=!1;function c(g){if(!r){console.warn("[embed-bridge] No parent window — would have sent:",g);return}try{r.postMessage(g,"*")}catch(m){console.warn("[embed-bridge] postMessage failed:",m)}}function d(g,m){c({type:"error",code:g,message:m})}function u(){l||(l=!0,c({type:"ready"}))}function p(g,m,b){const _=g.getProperties();let S=m,T=b;if(S==null||T==null){const v=g.getGeometry()?.getExtent();if(v){const[x,P]=Me(Cr(v));S=x,T=P}}return{type:"parcel:select",upn:_.upn??null,parcel_id:_.id??null,lon:S??null,lat:T??null,zone_code:_.zone_code??null,zone_name:_.zone_name??null,landuse:_.landuse??null,min_height:_.min_height??null,max_height:_.max_height??null}}function h(g){if(o.clear(),g){const m=g.clone();o.addFeature(m)}}n.onClick((g,m,b,_)=>{let S=null;t.forEachFeatureAtPixel(_.pixel,T=>{if(T.get("_layerType")==="parcel")return S=T,!0}),S?(h(S),c(p(S,g,m))):(h(null),c({type:"parcel:cleared"}))}),window.addEventListener("message",g=>{const m=g.data;if(!(!m||typeof m!="object"||!os.has(m.type)))try{switch(m.type){case"set:view":{if(typeof m.lon=="number"&&typeof m.lat=="number"){const b=t.getView();b.setCenter(Q([m.lon,m.lat])),typeof m.zoom=="number"&&b.setZoom(m.zoom)}break}case"set:selected":m.upn&&f(String(m.upn));break;case"clear:selected":h(null),s=null;break;case"set:basemap":m.key&&typeof n.setBaseMap=="function"&&n.setBaseMap(m.key);break}}catch(b){d("COMMAND_FAILED",`Failed to handle ${m.type}: ${b.message}`)}});function f(g){if(!i){s=g;return}const b=i.getSource().getFeatures().find(S=>String(S.get("upn")??"")===g);if(!b){s=g;return}s=null,h(b);const _=b.getGeometry()?.getExtent();_&&t.getView().fit(_,{padding:[50,50,50,50],duration:400,maxZoom:17}),c(p(b,null,null))}function y(g){i=g;const m=g.getSource(),b=()=>{queueMicrotask(()=>{s&&f(s),u()})};if(m.getFeatures().length>0)b();else{let _=!1;m.on("addfeature",()=>{_||(_=!0,queueMicrotask(()=>{_=!1,s&&f(s),u()}))})}}if(e?.basemap&&typeof n.setBaseMap=="function"&&n.setBaseMap(e.basemap),typeof e?.lon=="number"&&typeof e?.lat=="number"){const g=t.getView();g.setCenter(Q([e.lon,e.lat])),g.setZoom(typeof e?.zoom=="number"?e.zoom:15)}return{attachParcelsLayer:y,emitError:d}}const rs=[{key:"parcels",label:"Parcels",geometryFamily:"polygon"},{key:"collector_zones",label:"Collector Zones",geometryFamily:"polygon"},{key:"building_footprints",label:"Building Footprints",geometryFamily:"polygon"},{key:"osm_roads",label:"OSM Roads",geometryFamily:"line"},{key:"other",label:"Other (view only)",geometryFamily:"any"}],rr={parcels:["upn","landuse","zone_code","zone_name","sector","block","parcel_no","prop_no","st_name","prop_add","fac_name","min_height","max_height","eff_date","lp_name","locality","mmda","last_update","remarks"],collector_zones:["zone_name"],building_footprints:[],osm_roads:["osm_id","name","highway"],other:[]};function as(n){if(!n?.features?.length)return"none";let e=0,t=0,r=0;for(const i of n.features){const s=i?.geometry?.type;s&&(s==="Polygon"||s==="MultiPolygon"?e++:s==="LineString"||s==="MultiLineString"?t++:(s==="Point"||s==="MultiPoint")&&r++)}const o=e+t+r;if(o===0)return"none";const a=Math.max(e,t,r);return a<o*.85?"mixed":e===a?"polygon":t===a?"line":"point"}function ar(n,e=50){const t=new Set,r=Math.min(e,n.features.length);for(let o=0;o<r;o++){const a=n.features[o]?.properties;if(!(!a||typeof a!="object"))for(const i of Object.keys(a))t.add(String(i).toLowerCase())}return t}function Ne(n,...e){for(const t of e)if(n.has(t))return!0;return!1}function is(n){const e=as(n);if(e==="none"||e==="mixed")return"other";const t=ar(n);return e==="line"?Ne(t,"osm_id","highway")?"osm_roads":"other":e==="polygon"?Ne(t,"upn","parcel_no","landuse","lu_code","zone_code")&&Ne(t,"upn","parcel_no","landuse","lu_code")?"parcels":Ne(t,"zone_name","colzonename","colzonenr")?"collector_zones":Ne(t,"building","building:levels","building_levels","height","min_height","max_height")&&!Ne(t,"upn","parcel_no")?"building_footprints":"other":"other"}const ss={upn:["upn","unique_parcel_no","parcel_id","pid"],landuse:["landuse","land_use","lu","lu_code"],zone_code:["zone_code","zonecode","zone"],zone_name:["zone_name","zonename","colzonename"],sector:["sector","sec"],block:["block","blk"],parcel_no:["parcel_no","parcelno","plot_no","plotno"],prop_no:["prop_no","propertyno","property_no"],st_name:["st_name","street","street_name","road"],prop_add:["prop_add","address","addr"],fac_name:["fac_name","facility","facilityname"],min_height:["min_height","minheight","h_min"],max_height:["max_height","maxheight","h_max","height"],eff_date:["eff_date","effectivedate","effdate"],lp_name:["lp_name","lpname","localplan"],locality:["locality","town","settlement"],mmda:["mmda","district","assembly"],last_update:["last_update","lastupdate","updated"],remarks:["remarks","notes","comments"],osm_id:["osm_id","osmid","id"],name:["name","street_name","st_name"],highway:["highway","road_class","class"]};function Zo(n){return String(n).toLowerCase().replace(/[\s_\-]+/g,"")}function ir(n,e){const t=rr[e]||[];if(t.length===0)return{};const r=ar(n,50),o=new Map;for(const i of r)o.set(Zo(i),i);const a={};for(const i of t){const s=ss[i]||[i];let l=null;for(const c of s){const d=Zo(c);if(o.has(d)){l=o.get(d);break}}a[i]=l}return a}function ls(n,e){const t={};for(const[r,o]of Object.entries(e||{}))o!=null&&n&&Object.prototype.hasOwnProperty.call(n,o)&&(t[r]=n[o]);return t}function cs(n){const e=new Map;for(const t of n.features||[]){const r=t?.properties;if(!(!r||typeof r!="object"))for(const o of Object.keys(r))e.has(o)||e.set(o,!0)}return Array.from(e.keys())}const L={};let fo=null,R=null;function ds(){L.root||(L.root=document.getElementById("importMappingModal"),L.filename=document.getElementById("import-modal-filename"),L.summary=document.getElementById("import-modal-summary"),L.target=document.getElementById("import-modal-target"),L.targetHint=document.getElementById("import-modal-target-hint"),L.fieldsWrap=document.getElementById("import-modal-fields-wrap"),L.tbody=document.getElementById("import-modal-fields-tbody"),L.btnSave=document.getElementById("import-modal-save"),L.btnSaveUpload=document.getElementById("import-modal-save-upload"),L.btnCancel=document.getElementById("import-modal-cancel"),L.target&&!L.target.dataset.populated&&(L.target.innerHTML=rs.map(n=>`<option value="${n.key}">${n.label}</option>`).join(""),L.target.dataset.populated="1"),L.target&&!L.target.dataset.wired&&(L.target.dataset.wired="1",L.target.addEventListener("change",sr)),L.btnSave&&!L.btnSave.dataset.wired&&(L.btnSave.dataset.wired="1",L.btnSave.addEventListener("click",()=>Qo("save"))),L.btnSaveUpload&&!L.btnSaveUpload.dataset.wired&&(L.btnSaveUpload.dataset.wired="1",L.btnSaveUpload.addEventListener("click",()=>Qo("upload"))),L.root&&!L.root.dataset.wired&&(L.root.dataset.wired="1",L.root.addEventListener("hidden.bs.modal",()=>{R?.onResult&&!R._resolved&&(R._resolved=!0,R.onResult({action:"cancel"})),R=null})))}function us(){const n=R.targetType,e=rr[n]||[];if(n==="other"||e.length===0){L.fieldsWrap.style.display="none";return}L.fieldsWrap.style.display="";const t=['<option value="">(none)</option>'].concat(R.sourceFields.map(r=>`<option value="${yt(r)}">${ho(r)}</option>`)).join("");L.tbody.innerHTML=e.map(r=>{const o=R.mapping[r]||"",a=t.replace(`<option value="${yt(o)}">`,`<option value="${yt(o)}" selected>`);return`
|
||
<tr>
|
||
<td><code>${ho(r)}</code></td>
|
||
<td>
|
||
<select class="form-select form-select-sm import-field-map"
|
||
data-col="${yt(r)}">
|
||
${a}
|
||
</select>
|
||
</td>
|
||
</tr>
|
||
`}).join(""),L.tbody.querySelectorAll(".import-field-map").forEach(r=>{r.addEventListener("change",o=>{const a=o.target.dataset.col;R.mapping[a]=o.target.value||null})})}function sr(){const n=L.target.value;R.targetType=n,R.mapping=ir(R.fc,n),n==="other"?(L.targetHint.innerHTML="<em>This dataset will be visible on the map but cannot be uploaded to the database. You can change the type later.</em>",L.btnSave.disabled=!1,L.btnSaveUpload.disabled=!0):(L.targetHint.innerHTML="Each LUPMIS2 column is matched to a source field where possible. You can override any choice below.",L.btnSave.disabled=!1,L.btnSaveUpload.disabled=!1),us()}function Qo(n){if(!R||R._resolved)return;R._resolved=!0;const{targetType:e,mapping:t,onResult:r}=R;fo.hide(),r&&r({action:n,targetType:e,mapping:e==="other"?null:{...t}}),R=null}function ps(n){if(ds(),!L.root){console.warn("[ImportModal] Modal element missing — calling onResult with cancel"),n.onResult?.({action:"cancel"});return}const e=n.fc,t=e?.features?.length??0,r=is(e);R={importId:n.importId,filename:n.filename,fc:e,sourceFields:cs(e),targetType:r,mapping:ir(e,r),onResult:n.onResult,_resolved:!1},L.filename.textContent=n.filename||"imported dataset",L.summary.textContent=`— ${t} feature${t===1?"":"s"}`,L.target.value=R.targetType,sr(),fo=ct.getOrCreateInstance(L.root),fo.show()}function ho(n){return String(n).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function yt(n){return ho(n)}var Ve={},Kt={},en;function So(){return en||(en=1,Kt.geometries={NULL:0,POINT:1,POLYLINE:3,POLYGON:5,MULTIPOINT:8,POINTZ:11,POLYLINEZ:13,POLYGONZ:15,MULTIPOINTZ:18,POINTM:21,POLYLINEM:23,POLYGONM:25,MULTIPOINTM:28,MULTIPATCH:31}),Kt}var Xt={},Vt,tn;function lr(){return tn||(tn=1,Vt={C:254,L:1,D:8,N:18,M:18,F:18,B:8}),Vt}var Ye={},on;function fs(){return on||(on=1,Ye.lpad=function(e,t,r){for(;e.length<t;)e=r+e;return e},Ye.rpad=function(e,t,r){for(;e.length<t;)e=e+r;return e},Ye.writeField=function(e,t,r,o){for(var a=0;a<t;a++)e.setUint8(o,r.charCodeAt(a)),o++;return o}),Ye}var Je={},nn;function hs(){if(nn)return Je;nn=1;var n=lr(),e={string:"C",number:"N",boolean:"L",null:"C"};Je.multi=t,Je.bytesPer=a,Je.obj=o;function t(i){var s={};i.forEach(l);function l(c){r(s,c)}return o(s)}function r(i,s){for(var l in s){var c=typeof s[l]<"u"&&s[l]!==null;(typeof i[l]>"u"||c)&&(i[l]=s[l])}return i}function o(i){var s={},l=[];for(var c in i)s[c]=i[c]===null?"null":typeof i[c];for(var d in s){var u=e[s[d]];u&&l.push({name:d,type:u,size:n[u]})}return l}function a(i){return i.reduce(function(s,l){return s+l.size},1)}return Je}var Yt,rn;function gs(){if(rn)return Yt;rn=1,lr();var n=fs(),e=hs();return Yt=function(r,o){var a=o||e.multi(r),i=32*a.length+1,s=e.bytesPer(a),l=new ArrayBuffer(i+32+s*r.length+1),c=new Date,d=new DataView(l);d.setUint8(0,3),d.setUint8(1,c.getFullYear()-1900),d.setUint8(2,c.getMonth()),d.setUint8(3,c.getDate()),d.setUint32(4,r.length,!0);var u=i+32;return d.setUint16(8,u,!0),d.setUint16(10,s,!0),d.setInt8(32+i-1,13),a.forEach(function(p,h){p.name.split("").slice(0,8).forEach(function(f,y){d.setInt8(32+h*32+y,f.charCodeAt(0))}),d.setInt8(32+h*32+11,p.type.charCodeAt(0)),d.setInt8(32+h*32+16,p.size),p.type=="N"&&d.setInt8(32+h*32+17,3)}),offset=i+32,r.forEach(function(p,h){d.setUint8(offset,32),offset++,a.forEach(function(f){var y=p[f.name];switch((y===null||typeof y>"u")&&(y=""),f.type){case"L":d.setUint8(offset,y?84:70),offset++;break;case"D":offset=n.writeField(d,8,n.lpad(y.toString(),8," "),offset);break;case"N":offset=n.writeField(d,f.size,n.lpad(y.toString(),f.size," ").substr(0,18),offset);break;case"C":offset=n.writeField(d,f.size,n.rpad(y.toString(),f.size," "),offset);break;default:throw new Error("Unknown field type")}})}),d.setUint8(offset,26),d},Yt}var an;function ms(){return an||(an=1,Xt.structure=gs()),Xt}var Jt,sn;function cr(){return sn||(sn=1,Jt='GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]'),Jt}var Ze={},ln;function To(){return ln||(ln=1,Ze.enlarge=function(e,t){return t[0]<e.xmin&&(e.xmin=t[0]),t[0]>e.xmax&&(e.xmax=t[0]),t[1]<e.ymin&&(e.ymin=t[1]),t[1]>e.ymax&&(e.ymax=t[1]),e},Ze.enlargeExtent=function(e,t){return t.xmax>e.xmax&&(e.xmax=t.xmax),t.xmin<e.xmin&&(e.xmin=t.xmin),t.ymax>e.ymax&&(e.ymax=t.ymax),t.ymin<e.ymin&&(e.ymin=t.ymin),e},Ze.blank=function(){return{xmin:Number.MAX_VALUE,ymin:Number.MAX_VALUE,xmax:-Number.MAX_VALUE,ymax:-Number.MAX_VALUE}}),Ze}var bt={},cn;function ys(){if(cn)return bt;cn=1;var n=So().jstypes;bt.geojson=e,bt.obj=r;function e(o){var a={};o.forEach(i);function i(s){t(a,s.properties)}return r(a)}function t(o,a){for(var i in a)o[i]=a[i];return o}function r(o){var a={},i=[];for(var s in o)a[s]=typeof o[s];for(var l in a)i.push({name:l,type:n[a[l]]});return i}return bt}var ke={},dn;function bs(){if(dn)return ke;dn=1;var n=To();return ke.write=function(t,r,o,a){var i=28,s=100,l=0,c=0;t.forEach(function(u,p){o.setInt32(l,p),o.setInt32(l+4,10),o.setInt32(l+8,1,!0),o.setFloat64(l+12,u[0],!0),o.setFloat64(l+20,u[1],!0),a.setInt32(c,s/2),a.setInt32(c+4,10),c+=8,l+=i,s+=i})},ke.extent=function(e){return e.reduce(function(t,r){return n.enlarge(t,r)},n.blank())},ke.parts=function(t,r){return t.length},ke.shxLength=function(e){return e.length*8},ke.shpLength=function(e){return e.length*28},ke}var Ie={},un;function ws(){if(un)return Ie;un=1;var n=To(),e=So();Ie.write=function(a,i,s,l,c){var d=0,u=0,p=100;a.forEach(h);function h(f,y){var g=r(f),m=t([f],c),b=g.length*16+48+(m-1)*4,_=g.reduce(function(v,x){return n.enlarge(v,x)},n.blank());l.setInt32(u,p/2),l.setInt32(u+4,b/2),u+=8,p+=b+8,s.setInt32(d,y+1),s.setInt32(d+4,b/2),s.setInt32(d+8,c,!0),s.setFloat64(d+12,_.xmin,!0),s.setFloat64(d+20,_.ymin,!0),s.setFloat64(d+28,_.xmax,!0),s.setFloat64(d+36,_.ymax,!0),s.setInt32(d+44,m,!0),s.setInt32(d+48,g.length,!0),s.setInt32(d+52,0,!0);for(var S=f.reduce(function(v,x){return Array.isArray(x[0][0])?v=v.concat(x):v.push(x),v},[]),T=1;T<m;T++)s.setInt32(d+52+T*4,S.reduce(function(v,x,P){return P<T?v+x.length:v},0),!0);g.forEach(function(x,P){s.setFloat64(d+56+P*16+(m-1)*4,x[0],!0),s.setFloat64(d+56+P*16+(m-1)*4+8,x[1],!0)}),d+=b+8}},Ie.shpLength=function(o){return o.length*56+r(o).length*16},Ie.shxLength=function(o){return o.length*8},Ie.extent=function(o){return r(o).reduce(function(a,i){return n.enlarge(a,i)},n.blank())};function t(o,a){var i=1;return(a===e.geometries.POLYGON||a===e.geometries.POLYLINE)&&(i=o.reduce(function(s,l){return s+=l.length,Array.isArray(l[0][0][0])&&(s+=l.reduce(function(c,d){return c+d.length-1},0)),s},0)),i}Ie.parts=t;function r(o,a){return a===void 0&&(a=[]),typeof o[0][0]=="object"?o.reduce(function(i,s){return i.concat(r(s))},a):o}return Ie}var Zt,pn;function dr(){if(pn)return Zt;pn=1;var n=So(),e=ms(),t=cr();To(),ys();var r=bs(),o=ws(),a={1:r,5:o,3:o};Zt=i;function i(c,d,u,p){var h=n.geometries[d],f=a[h],y=f.parts(u,h),g=100+(y-u.length)*4+f.shpLength(u),m=100+f.shxLength(u),b=new ArrayBuffer(g),_=new DataView(b),S=new ArrayBuffer(m),T=new DataView(S),v=f.extent(u);s(_,h),s(T,h),l(v,_),l(v,T),f.write(u,v,new DataView(b,100),new DataView(S,100),h),_.setInt32(24,g/2),T.setInt32(24,50+u.length*4);var x=e.structure(c);p(null,{shp:_,shx:T,dbf:x,prj:t})}function s(c,d){c.setInt32(0,9994),c.setInt32(28,1e3,!0),c.setInt32(32,d,!0)}function l(c,d){d.setFloat64(36,c.xmin,!0),d.setFloat64(44,c.ymin,!0),d.setFloat64(52,c.xmax,!0),d.setFloat64(60,c.ymax,!0)}return Zt}var Qe={},fn;function _s(){if(fn)return Qe;fn=1,Qe.point=n("Point","POINT"),Qe.line=n("LineString","POLYLINE"),Qe.polygon=n("Polygon","POLYGON");function n(o,a){return function(i){var s=i.features.filter(r(o));return{geometries:a==="POLYGON"||a==="POLYLINE"?[s.map(e)]:s.map(e),properties:s.map(t),type:a}}}function e(o){return o.geometry.coordinates[0]!==void 0&&o.geometry.coordinates[0][0]!==void 0&&o.geometry.coordinates[0][0][0]!==void 0?o.geometry.coordinates[0]:o.geometry.coordinates}function t(o){return o.properties}function r(o){return function(a){return a.geometry.type===o}}return Qe}var Qt,hn;function ur(){if(hn)return Qt;hn=1;var n=dr(),e=_s(),t=cr(),r=Gr();return Qt=function(o,a){var i=new r,s=i.folder(a&&a.folder?a.folder:"layers");[e.point(o),e.line(o),e.polygon(o)].forEach(function(c){c.geometries.length&&c.geometries[0].length&&n(c.properties,c.type,c.geometries,function(d,u){var p=a&&a.types[c.type.toLowerCase()]?a.types[c.type.toLowerCase()]:c.type;s.file(p+".shp",u.shp.buffer,{binary:!0}),s.file(p+".shx",u.shx.buffer,{binary:!0}),s.file(p+".dbf",u.dbf.buffer,{binary:!0}),s.file(p+".prj",t)})});var l={compression:"STORE"};return process.browser||(l.type="nodebuffer"),i.generate(l)},Qt}var eo,gn;function vs(){if(gn)return eo;gn=1;var n=ur();return eo=function(e,t){var r=n(e,t);location.href="data:application/zip;base64,"+r},eo}var mn;function Es(){return mn||(mn=1,Ve.download=vs(),Ve.write=dr(),Ve.zip=ur()),Ve}var xs=Es();const pr="EPSG:3857",fr="EPSG:4326";async function Ss({features:n,rename:e,format:t,filenameBase:r="export"}){if(!Array.isArray(n)||n.length===0)throw new Error("No features to export");const o=Ts(n,e);switch(t){case"geojson":return yn(new Blob([JSON.stringify(o,null,2)],{type:"application/geo+json"}),`${r}.geojson`);case"kml":return yn(new Blob([Ls(n,e)],{type:"application/vnd.google-earth.kml+xml"}),`${r}.kml`);case"shp":return ks(o,r);default:throw new Error(`Unknown export format: ${t}`)}}function Ts(n,e){const t=new ie,r={type:"FeatureCollection",features:[]};for(const o of n){if(!o.getGeometry())continue;const a=t.writeGeometryObject(o.getGeometry(),{dataProjection:fr,featureProjection:pr}),i=hr(o.getProperties()),s={};for(const[l,c]of Object.entries(e||{}))c&&Object.prototype.hasOwnProperty.call(i,l)&&(s[c]=i[l]);r.features.push({type:"Feature",geometry:a,properties:s})}return r}function hr(n){const e={};for(const[t,r]of Object.entries(n||{}))t!=="geometry"&&(e[t]=r);return e}function Ls(n,e){const t=n.filter(r=>r.getGeometry()).map(r=>{const o=r.clone(),a=hr(r.getProperties()),i={};for(const[l,c]of Object.entries(e||{}))c&&Object.prototype.hasOwnProperty.call(a,l)&&(i[c]=a[l]);o.setProperties(i,!0);const s=Object.values(e||{}).find(l=>l&&i[l]!=null&&i[l]!=="");return s&&o.set("name",String(i[s])),o});return new kn({extractStyles:!1}).writeFeatures(t,{dataProjection:fr,featureProjection:pr})}async function ks(n,e){const t=Is(n);return new Promise((r,o)=>{try{xs.download(t,{folder:e,outputType:"blob",compression:"DEFLATE",types:{point:`${e}_point`,polygon:`${e}_polygon`,polyline:`${e}_line`}}),r()}catch(a){o(a)}})}function Is(n){const e=new Set;for(const o of n.features)for(const a of Object.keys(o.properties||{}))e.add(a);const t=new Set,r={};for(const o of e){let a=String(o).replace(/[^A-Za-z0-9_]+/g,"_").slice(0,10)||"field",i=a,s=1;for(;t.has(i);){const l=String(s++);i=a.slice(0,Math.max(1,10-l.length))+l}t.add(i),r[o]=i}return{type:"FeatureCollection",features:n.features.map(o=>{const a={};for(const[i,s]of Object.entries(o.properties||{}))a[r[i]]=s;return{type:"Feature",geometry:o.geometry,properties:a}})}}function yn(n,e){const t=URL.createObjectURL(n),r=document.createElement("a");r.href=t,r.download=e,document.body.appendChild(r),r.click(),document.body.removeChild(r),setTimeout(()=>URL.revokeObjectURL(t),1e3)}const C={};let go=null,Ge=null;function Ps(){C.root||(C.root=document.getElementById("exportGisModal"),C.summary=document.getElementById("export-gis-summary"),C.filename=document.getElementById("export-gis-filename"),C.tbody=document.getElementById("export-gis-fields-tbody"),C.fmtHint=document.getElementById("export-gis-format-hint"),C.btnGo=document.getElementById("export-gis-go"),C.fmtInputs=Array.from(document.querySelectorAll('input[name="export-gis-format"]')),C.root.dataset.wired||(C.root.dataset.wired="1",C.fmtInputs.forEach(n=>n.addEventListener("change",mr)),C.btnGo.addEventListener("click",Cs)))}function Ms(n){const e=[];for(const t of n.parcelFeatures||[])e.push(to(t,"Parcels"));for(const t of n.zoneFeatures||[])e.push(to(t,"Zones"));for(const[t,r]of Object.entries(n.otherByLayer||{}))for(const o of r)e.push(to(o,t));return e}function to(n,e){const t=n.clone();return t.set("_source",e),t}function As(n){const e=new Set(["geometry","_layerType"]),t=new Map;for(const o of n)for(const a of Object.keys(o.getProperties()||{}))e.has(a)||t.has(a)||t.set(a,!0);t.delete("_source");const r=Array.from(t.keys());return r.push("_source"),r}function gr(){const n=At();C.tbody.innerHTML=Ge.keys.map(e=>{const t=Ge.rename[e]??e,o=n==="shp"&&t.length>10?`<div class="form-text text-danger mt-1">
|
||
${mo(t.length)} characters — Shapefile will
|
||
truncate / rename.
|
||
</div>`:"";return`
|
||
<tr>
|
||
<td><code>${mo(e)}</code></td>
|
||
<td>
|
||
<input type="text" class="form-control form-control-sm export-field-rename"
|
||
data-src="${bn(e)}"
|
||
value="${bn(t)}">
|
||
${o}
|
||
</td>
|
||
</tr>
|
||
`}).join(""),C.tbody.querySelectorAll(".export-field-rename").forEach(e=>{e.addEventListener("input",t=>{const r=t.target.dataset.src;Ge.rename[r]=t.target.value,At()==="shp"&&gr()})})}function mr(){const n=At();C.fmtHint.innerHTML={geojson:"GeoJSON keeps all attributes as-is and is the safest default.",shp:"Shapefile attribute names are limited to <strong>10 characters</strong> and alphanumeric/underscore only. Over-length names will be truncated (collisions are auto-numbered). One file per geometry type is written, all zipped into one download.",kml:"KML preserves attribute names; the first non-empty renamed field is used as each feature's <name> in Google Earth."}[n],gr()}function At(){return C.fmtInputs.find(n=>n.checked)?.value||"geojson"}async function Cs(){const n=At(),e=(C.filename.value||"export").replace(/[^A-Za-z0-9_\-]+/g,"_");C.btnGo.disabled=!0;try{await Ss({features:Ge.features,rename:Ge.rename,format:n,filenameBase:e}),go.hide()}catch(t){console.error("[ExportGIS] failed:",t),alert("Export failed: "+t.message)}finally{C.btnGo.disabled=!1}}function Fs(n){if(Ps(),!C.root){console.warn("[ExportGIS] Modal missing from DOM");return}const e=Ms(n);if(e.length===0){alert("No intersecting features to export.");return}const t=As(e),r=Object.fromEntries(t.map(a=>[a,a]));Ge={features:e,keys:t,rename:r},C.summary.textContent=`${e.length} feature${e.length===1?"":"s"} intersecting the ${n.kind==="circle"?"circle":"area"}`,C.filename.value=n.kind==="circle"?"circle_analysis":"area_analysis";const o=document.getElementById("export-gis-fmt-geojson");o&&(o.checked=!0),mr(),go=ct.getOrCreateInstance(C.root),go.show()}function mo(n){return String(n).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function bn(n){return mo(n)}let oo=null;async function wn(){if(!oo){const n=await Ct(()=>import("./shpjs-iyObTF9J.js").then(e=>e.i),[]);oo=n.default||n}return oo}let E=null,re=null,ae=null,no=null;const yo=typeof window<"u"&&window.LUPMIS_EMBED||null,ot=!!(yo&&yo.mode==="permit");let z=ot?"embed-permit":"addLocation";function Ds(){const n=typeof window<"u"?window.LUPMIS_SESSION:null;if(!n||typeof n!="object")return!1;const e=n.district_id;if(e!=null&&String(e).length>0)return!1;console.warn("[App] Authenticated user has no district assigned; halting init.");const t=document.createElement("div");t.id="no-district-overlay",t.setAttribute("role","alertdialog"),t.setAttribute("aria-modal","true"),t.style.cssText="position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.98);padding:24px;";const r=n.full_name||n.username||"You";return t.innerHTML=`
|
||
<div style="max-width:480px;text-align:center;border:1px solid #e5e7eb;
|
||
border-radius:12px;padding:28px 24px;box-shadow:0 8px 24px rgba(0,0,0,0.08);
|
||
background:#fff;font-family:var(--font-body,sans-serif);">
|
||
<div style="font-size:42px;line-height:1;margin-bottom:12px;">🛑</div>
|
||
<h2 style="margin:0 0 12px;color:var(--primary,#005eb8);font-size:1.35rem;">
|
||
No district assigned
|
||
</h2>
|
||
<p style="margin:0 0 10px;color:#333;">
|
||
${j(r)}, your user profile is not associated with any
|
||
district. LUPMIS2 cannot load the relevant map data without one.
|
||
</p>
|
||
<p style="margin:0 0 20px;color:#6b7280;font-size:0.95rem;">
|
||
Please contact the system administrator to have a district assigned
|
||
to your account.
|
||
</p>
|
||
<button type="button" id="no-district-portal-btn"
|
||
style="background:var(--primary,#005eb8);color:#fff;border:0;
|
||
border-radius:8px;padding:10px 18px;font-weight:600;cursor:pointer;">
|
||
Return to LUSPA portal
|
||
</button>
|
||
</div>`,document.body.appendChild(t),t.querySelector("#no-district-portal-btn")?.addEventListener("click",()=>{window.location.href="https://lupmis4luspa.org/"}),!0}async function _n(){if(console.log("[App] Initializing..."),Ds())return;await Ti({installButton:"#install-btn",offlineIndicator:"#offline-indicator",autoRegisterSW:!0});const n=localStorage.getItem("default-basemap")||"topo";E=new ui("map",{center:[-1.5,7.5],zoom:7,basemap:n}),re=new pi(E.getMap()),fl(),re.onMeasureComplete(t=>{console.log("[MapTools] Measurement complete:",t),t.type==="polygon"&&t.coordinate&&t.feature?.get("_layerType")!=="measure_area"&&E?.showDrawnPolygonPopup(t.feature,t.coordinate)}),ot&&(no=ns({mapView:E,embedConfig:yo})),E.onClick((t,r,o,a)=>{if(ot||(console.log("[MapClick] Clicked at:",t.toFixed(4),r.toFixed(4)),console.log("[MapClick] currentMode =",z),z==="draw"||z.startsWith("measure")))return;let i=null;if(E.getMap().forEachFeatureAtPixel(a.pixel,l=>{if(l.get("_layerType")==="parcel")return i=l,!0}),i){console.log("[MapClick] Clicked on parcel → Edit Attributes"),E.showParcelEditPopup(i,a.coordinate);return}let s=null;if(E.getMap().forEachFeatureAtPixel(a.pixel,l=>{if(l.get("_layerType")==="upn_grid")return s=l,!0}),s){console.log("[MapClick] Clicked on UPN-grid cell → Info popup"),E.showInfoPopup(s,a.coordinate,{title:"UPN Grid Cell",color:"#7c3aed"});return}z==="addLocation"&&(o?(console.log("[MapClick] Clicked on marker:",o.getId()),E.selectMarker(o),Rs(o)):(console.log("[MapClick] Empty space → Add Location popup"),E.clearSelection(),E.showAddLocationPopup(a.coordinate)))}),E.onDblClick((t,r,o,a)=>{if(ot||!o)return;const i=o.get("_layerType");if(console.log("[App] Double-click on feature, _layerType:",i||"none"),i==="measure_circle")E.showCircleIntersectionPopup(o,a.coordinate);else{if(i==="measure_circle_radius")return;i==="measure_area"?E.showAreaIntersectionPopup(o,a.coordinate):i==="collector_zone"?E.showInfoPopup(o,a.coordinate,{title:"Zone Info",color:"#7c3aed"}):i==="parcel"?E.showInfoPopup(o,a.coordinate,{title:"Parcel Info",color:"#0ea5e9"}):E.showInfoPopup(o,a.coordinate,{title:"Feature Info",color:"#e11d48"})}}),E.onAddLocation(async t=>{console.log("[App] Add location from map popup:",t);try{const r=await na(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 ro(),E?.zoomTo(t.lon,t.lat,14),r.id&&E?.selectMarker(r.id),fe("Location added successfully")}catch(r){console.error("[App] Failed to add location:",r),F("Failed to add location: "+r.message)}}),E.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 ma(o,r),fe("Parcel updated locally")}catch(a){console.error("[App] Failed to save parcel update:",a),F("Failed to save parcel: "+a.message)}});const e=new wo;E.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"}),a=await ya(o,r);console.log("[App] New parcel inserted with id:",a.id),fe("New parcel saved (pending verification)")}catch(o){console.error("[App] Failed to save new parcel:",o),F("Failed to save parcel: "+o.message)}});try{console.log("[App] Initializing database..."),await oa(),console.log("[App] Database ready");const t=await Eo();console.log("[App] Database status:",t),V()&&(await Ni()||(console.warn("[App] API server unreachable — using local data only"),Sr("Server not responding — loading cached data."))),await el(),E?.initEditBar(),Ws(),Ks(),Xs(),Vs(),ot&&no&&ae&&(ae.setVisible(!0),no.attachParcelsLayer(ae)),Ys(),Js(),Zs(),Qs()}catch(t){console.error("[App] Database initialization failed:",t),F("Failed to initialize database. Please refresh the page.");return}Os(),await ro(),ta(t=>{if(console.log("[App] Database change:",t),t.table==="locations"&&!t.local&&ro(),t.table==="parcels"){const r=document.getElementById("local-data-stats");r&&!r.classList.contains("d-none")&&Ot()}}),Qn(t=>{t?console.log("[App] Working offline - data will sync when back online"):(console.log("[App] Back online - syncing data..."),tl())}),hl(),ml(),gl(),yl(),bl(),wl(),_l(),console.log("[App] Initialized successfully")}function Os(){console.log("[initUI] Starting UI initialization..."),pl();const n=document.getElementById("export-btn");n&&n.addEventListener("click",Gs);const e=document.getElementById("local-data-btn");e&&e.addEventListener("click",()=>Ot());const t=document.getElementById("import-shp-btn"),r=document.getElementById("shp-file-input");t&&r&&(t.addEventListener("click",()=>r.click()),r.addEventListener("change",vr));const o=document.getElementById("import-geojson-btn"),a=document.getElementById("geojson-file-input");o&&a&&(o.addEventListener("click",()=>a.click()),a.addEventListener("change",Er));const i=document.getElementById("import-kml-btn"),s=document.getElementById("kml-file-input");i&&s&&(i.addEventListener("click",()=>s.click()),s.addEventListener("change",xr)),dl();const l=document.getElementById("exportGeoJSON-btn");l&&l.addEventListener("click",js);const c=document.getElementById("status-btn");c&&c.addEventListener("click",qs);const d=document.getElementById("fit-btn");d&&d.addEventListener("click",()=>E?.fitToMarkers());const u=document.getElementById("dock-btn-add-location"),p=document.getElementById("dock-btn-measure-circle"),h=document.getElementById("dock-btn-measure-line"),f=document.getElementById("dock-btn-measure-area"),y=document.getElementById("dock-btn-draw"),g=document.getElementById("dock-btn-clear");console.log("[initUI] Buttons found:",{addLocation:!!u,measureCircle:!!p,measureLine:!!h,measureArea:!!f,draw:!!y,clear:!!g});const m=[u,p,h,f,y],b=(_,S)=>{switch(console.log("[setMode] Changing mode from",z,"to",_),z=_,console.log("[setMode] currentMode is now:",z),m.forEach(T=>{T&&T.classList.toggle("active",T===S)}),re?.deactivate(),_!=="draw"&&E?.setEditMode(!1),_!=="addLocation"&&E?.hideAddLocationPopup(),_){case"measureCircle":re?.startCircleMeasure();break;case"measureLine":re?.startLineMeasure();break;case"measureArea":re?.startAreaMeasure();break;case"draw":E?.setEditMode(!0);break}};u&&u.addEventListener("click",()=>{console.log("[Button] Add Location clicked"),b("addLocation",u)}),p&&p.addEventListener("click",()=>{console.log("[Button] Circle clicked, currentMode is:",z),z==="measureCircle"?b("addLocation",u):b("measureCircle",p)}),h&&h.addEventListener("click",()=>{console.log("[Button] Line clicked, currentMode is:",z),z==="measureLine"?b("addLocation",u):b("measureLine",h)}),f&&f.addEventListener("click",()=>{console.log("[Button] Area clicked, currentMode is:",z),z==="measureArea"?b("addLocation",u):b("measureArea",f)}),y&&y.addEventListener("click",()=>{console.log("[Button] Draw clicked, currentMode is:",z),z==="draw"?b("addLocation",u):b("draw",y)}),g&&g.addEventListener("click",()=>{if(re?.clearMeasurements(),z.startsWith("measure"))switch(re?.deactivate(),z){case"measureCircle":re?.startCircleMeasure();break;case"measureLine":re?.startLineMeasure();break;case"measureArea":re?.startAreaMeasure();break}})}async function ro(){try{console.log("[App] Loading locations...");const n=await Gn();console.log("[App] Locations loaded:",n),Ns(n),E&&(E.clearMarkers(),n.length>0&&(E.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 Rs(n){const e=n.get("name"),t=n.get("description"),r=n.get("category"),o=n.get("lon")||n.get("longitude"),a=n.get("lat")||n.get("latitude");console.log("[App] Selected location:",{name:e,description:t,category:r,lon:o,lat:a})}function Ns(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 a=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">${a} ${j(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">${j(o.description)}</small>`:""}
|
||
</a>
|
||
`}).join(""),e.querySelectorAll(".location-item").forEach(o=>{o.addEventListener("click",a=>{a.preventDefault();const i=parseFloat(o.dataset.lon),s=parseFloat(o.dataset.lat),l=parseInt(o.dataset.id);E?.zoomTo(i,s,14),E?.selectMarker(l)})})}async function Ot(){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 La();e.innerHTML=r.map(o=>{const i=Hn(o.name)?`<button type="button" class="btn btn-sm btn-link text-danger p-0 table-clear-btn"
|
||
data-table="${j(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="${j(o.name)}">${j(o.name)}</a>
|
||
</td>
|
||
<td class="text-end"><span class="badge bg-secondary">${o.count}</span></td>
|
||
<td class="text-end pe-3">${i}</td>
|
||
</tr>
|
||
`}).join(""),n.classList.remove("d-none"),e.querySelectorAll(".table-name-link").forEach(o=>{o.addEventListener("click",a=>{a.preventDefault(),Bs(o.dataset.table)})}),e.querySelectorAll(".table-clear-btn").forEach(o=>{o.addEventListener("click",async a=>{a.preventDefault();const i=o.dataset.table;if(confirm(`Clear local cache for "${i}"?
|
||
|
||
The data will be re-downloaded from the server on the next app start.`))try{const s=await Wn(i);fe(`Cleared ${s} row${s===1?"":"s"} from "${i}". It will re-download on next start.`),await Ot()}catch(s){console.error("[App] Per-table clear failed:",s),F(`Could not clear "${i}": ${s.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",$s))}}async function $s(){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 Ta(),e=n.reduce((t,r)=>t+r.count,0);fe(`Cleared ${e} row${e===1?"":"s"} across ${n.length} table${n.length===1?"":"s"}.`),await Ot(),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),F("Failed to clear cached layers: "+n.message)}}async function Bs(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 ct(document.getElementById("tableContentModal")).show();try{const{columns:a,rows:i}=await ka(n);if(i.length===0){t.innerHTML='<div class="text-center text-muted py-4">Table is empty</div>',r.textContent="0 rows";return}const s=a.map(c=>`<th class="text-nowrap">${j(c)}</th>`).join(""),l=i.map(c=>`<tr>${a.map(u=>{let p=c[u];if(p==null)return'<td class="text-muted fst-italic">NULL</td>';p=String(p);const h=p.length>120?p.substring(0,120)+"...":p;return`<td>${j(h)}</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>${s}</tr>
|
||
</thead>
|
||
<tbody>${l}</tbody>
|
||
</table>
|
||
</div>
|
||
`,r.textContent=`${i.length}${i.length>=200?"+":""} row(s), ${a.length} column(s)`}catch(a){console.error("[App] Failed to load table content:",a),t.innerHTML=`<div class="text-danger text-center py-4">Failed to load: ${j(a.message)}</div>`}}async function Gs(){try{await xa("lupmis-backup.sqlite3"),fe("Database exported successfully")}catch(n){console.error("[App] Export failed:",n),F("Export failed: "+n.message)}}async function js(){try{const n=await Sa(),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),fe(`Exported ${n.features.length} location(s)`)}catch(n){console.error("[App] GeoJSON Export failed:",n),F("GeoJSON Export failed: "+n.message)}}async function qs(){try{const n=await Eo(),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 ${V()?"bg-success":"bg-warning"}">${V()?"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 ct(document.getElementById("statusModal")).show()}catch(n){console.error("[App] Failed to get status:",n),F("Failed to get status")}}function yr(n){return n.replace(/^\(+/,"").replace(/\)+$/,"").split(",").map(e=>{const[t,r]=e.trim().split(/\s+/).map(Number);return[t,r]})}function zs(n){return{type:"Polygon",coordinates:n.trim().replace(/^POLYGON\s*\(\s*/i,"").replace(/\s*\)$/,"").split("),(").map(yr)}}function Us(n){return{type:"MultiPolygon",coordinates:n.trim().replace(/^MULTIPOLYGON\s*\(\s*/i,"").replace(/\s*\)$/,"").split(")),((").map(o=>o.replace(/^\(+/,"").replace(/\)+$/,"").split("),(").map(yr))}}function dt(n){if(!n)return null;const e=n.trim().toUpperCase();return e.startsWith("MULTIPOLYGON")?Us(n):e.startsWith("POLYGON")?zs(n):(console.warn("[App] Unsupported WKT type:",e.substring(0,30)),null)}function Hs(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=dt(e);return{type:"FeatureCollection",features:[{type:"Feature",properties:{districtid:t,district_name:r},geometry:o}]}}function vn(n){if(!Array.isArray(n)||n.length===0)return null;const e=[];for(const t of n){const r=t.polygon||t.boundary,o=dt(r);if(!o)continue;const a={_layerType:"collector_zone"};for(const[i,s]of Object.entries(t))i==="polygon"||i==="boundary"||(a[i]=s);e.push({type:"Feature",properties:a,geometry:o})}return e.length===0?null:{type:"FeatureCollection",features:e}}async function Ws(){const n="district_boundary",t={strokeColor:"#e11d48",strokeWidth:2.5,fillColor:"rgba(225,29,72,0.08)",typeDescription:"Vector / Polygon"},r=E?.getLayerGroup(1)||null;function o(i){if(!i)return;const s=i.getLayers(),l=[];s.forEach(c=>{c.get("title")==="District Boundary"&&l.push(c)}),l.forEach(c=>s.remove(c))}function a(i){if(!i||!E)return;const s=i.getSource().getExtent();s&&s[0]!==1/0&&E.getMap().getView().fit(s,{padding:[40,40,40,40],duration:600})}try{const i=await qn(n);if(i){console.log("[App] District boundary loaded from local cache");const s=E?.addGeoJSONLayer(i,"District Boundary",t,r);a(s)}if(V()&&Ee()){console.log("[App] Fetching district boundary from API...");const s=await Bi(),l=Hs(s);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 jn(n,l),i&&o(r||E?.getOverlayGroup());const c=E?.addGeoJSONLayer(l,"District Boundary",t,r);a(c),console.log("[App] District boundary loaded from API")}else i||console.log("[App] District boundary not available — offline and no local cache")}catch(i){console.error("[App] Failed to load district boundary:",i)}}function En(n){if(!Array.isArray(n)||n.length===0)return null;const e=[];for(const t of n){const r=t.polygon||t.geometry_wkt||t.geom,o=dt(r);o&&e.push({type:"Feature",properties:{_layerType:"upn_grid",upn_prefix:t.upn_prefix??null},geometry:o})}return e.length===0?null:{type:"FeatureCollection",features:e}}async function Ks(){const e={strokeColor:"#5b21b6",strokeWidth:1.5,fillColor:"rgba(124,58,237,0.04)",typeDescription:"Vector / Polygon"},t=E?.getLayerGroup(1)||null,r={type:"FeatureCollection",features:[]},o=E?.addGeoJSONLayer(r,"UPN Grid",e,t);if(!o){console.warn("[App] Could not create UPN Grid layer");return}o.setVisible(!1);const a=7;o.setStyle((s,l)=>{const c=[new M({stroke:new I({color:"rgba(255,255,255,0.95)",width:3.5})}),new M({stroke:new I({color:"#5b21b6",width:1.5,lineDash:[7,4]}),fill:new A({color:"rgba(124,58,237,0.04)"})})];if(l<=a){const d=s.get("upn_prefix");d!=null&&String(d).length>0&&c.push(new M({text:new st({text:String(d),font:"600 12px Arial, sans-serif",fill:new A({color:"#3b0764"}),stroke:new I({color:"rgba(255,255,255,0.95)",width:3}),overflow:!0})}))}return c}),o.on("change:visible",()=>{o.getVisible()&&o.getSource().getFeatures().length===0&&F("No UPN grid available locally. Connect to the internet to download it.")});function i(s){const l=new ie().readFeatures(s,{featureProjection:"EPSG:3857"});o.getSource().clear(),o.getSource().addFeatures(l)}try{const l=Dt()?.district_id??null,c=await la(l);if(c){const h=En(c);h&&i(h),console.log("[App] UPN grid from cache:",c.length,"cells (district",l,")");return}if(!V()||!Ee()){console.log("[App] UPN grid not available — offline and no cache for district",l);return}console.log("[App] Fetching UPN grid from API (district",l,")...");const d=await Wi();if(!d?.success||!Array.isArray(d?.data)){console.warn("[App] getUpnGrid invalid response:",d);return}const u=d.data;console.log("[App] UPN grid from API:",u.length,"cells"),await sa(u,l);const p=En(u);p&&i(p),console.log("[App] UPN grid loaded:",p?.features.length??0,"cells rendered")}catch(s){console.error("[App] Failed to load UPN grid:",s)}}async function Xs(){const e={strokeColor:"#7c3aed",strokeWidth:1.5,fillColor:"rgba(124,58,237,0.12)",typeDescription:"Vector / Polygon"},t=E?.getLayerGroup(1)||null;console.log("[App] loadCollectorZones — adminGroup:",t?t.get("title"):"null");const r={type:"FeatureCollection",features:[]},o=E?.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&&F("No collector zones available locally. Connect to the internet to download zone data.")});function a(i){const s=new ie().readFeatures(i,{featureProjection:"EPSG:3857"});o.getSource().clear(),o.getSource().addFeatures(s)}try{const i=await ia();if(i){const s=vn(i);s&&(console.log("[App] Collector zones loaded from local cache:",s.features.length,"zones"),a(s))}if(V()&&Ee()){console.log("[App] Fetching collector zones from API...");const s=await ji();if(!s?.success||!Array.isArray(s?.data)){console.warn("[App] getCollectorZones API response invalid:",s);return}const l=s.data;console.log("[App] Collector zones from API:",l.length,"entries"),await aa(l);const c=vn(l);if(!c){console.warn("[App] Could not convert zones to GeoJSON");return}a(c),console.log("[App] Collector zones updated from API:",c.features.length,"zones")}else i||console.log("[App] Collector zones not available — offline and no local cache")}catch(i){console.error("[App] Failed to load collector zones:",i)}}function xn(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 a=null;if(r.geom&&r.geom.type&&r.geom.coordinates)a={type:r.geom.type,coordinates:r.geom.coordinates};else if(r.sp_boundary&&r.sp_boundary.type&&r.sp_boundary.coordinates)a={type:r.sp_boundary.type,coordinates:r.sp_boundary.coordinates};else{const l=r.boundary||r.geometry_wkt||r.polygon||r.wkt;a=dt(l)}if(!a)continue;const i=new Set(["polygon","boundary","geom","geometry_wkt","wkt","textboundary","sp_boundary","fetched_at"]),s={_layerType:"parcel"};for(const[l,c]of Object.entries(r))i.has(l)||(s[l]=c);t.push({type:"Feature",properties:s,geometry:a})}return t.length===0?null:{type:"FeatureCollection",features:t}}async function Vs(){const e={strokeColor:"#0ea5e9",strokeWidth:1.5,fillColor:"rgba(14,165,233,0.12)",typeDescription:"Vector / Polygon"},t=E?.getLayerGroup(4)||null;console.log("[App] loadParcels — landUseGroup:",t?t.get("title"):"null");const r={type:"FeatureCollection",features:[]};if(ae=E?.addGeoJSONLayer(r,"Parcels",e,t),!ae){console.warn("[App] Could not create Parcels layer");return}ae.setVisible(!1),ae.on("change:visible",()=>{ae.getVisible()&&ae.getSource().getFeatures().length===0&&F("No parcels available locally. Connect to the internet to download parcel data.")});function o(a){const i=new ie().readFeatures(a,{featureProjection:"EPSG:3857"});ae.getSource().clear(),ae.getSource().addFeatures(i)}try{const a=await ga();if(a){const i=xn(a);i&&(console.log("[App] Parcels loaded from local cache:",i.features.length,"parcels"),o(i))}if(V()&&Ee()){console.log("[App] Fetching parcels from API...");const i=await qi();if(!i?.success||!Array.isArray(i?.data)){console.warn("[App] getDistrictParcels API response invalid:",i);return}const s=i.data;console.log("[App] Parcels from API:",s.length,"entries"),s.length>0&&console.log("[App] First parcel keys:",Object.keys(s[0])),await ha(s);const l=xn(s);if(!l){console.warn("[App] Could not convert parcels to GeoJSON");return}o(l),console.log("[App] Parcels updated from API:",l.features.length,"parcels")}else a||console.log("[App] Parcels not available — offline and no local cache")}catch(a){console.error("[App] Failed to load parcels:",a)}}function Sn(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 a;if(typeof o=="object"&&o!==null&&o.type?a=o:a=dt(o),!a)continue;const i={_layerType:"building_footprint"};for(const[s,l]of Object.entries(r))e.includes(s)||typeof l=="object"&&l!==null||(i[s]=l);t.push({type:"Feature",properties:i,geometry:a})}return t.length===0?null:{type:"FeatureCollection",features:t}}async function Ys(){const e={strokeColor:"#8b6f47",strokeWidth:1,fillColor:"rgba(139,111,71,0.18)",typeDescription:"Vector / Polygon"},t=E?.getLayerGroup(5)||null;console.log("[App] loadBuildingFootprints — physInfraGroup:",t?t.get("title"):"null");const r={type:"FeatureCollection",features:[]},o=E?.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&&F("No building footprints available locally. Connect to the internet to download footprint data.")});function a(i){const s=new ie().readFeatures(i,{featureProjection:"EPSG:3857"});o.getSource().clear(),o.getSource().addFeatures(s)}try{const i=await wa();if(i){const s=Sn(i);s&&(console.log("[App] Building footprints loaded from local cache:",s.features.length,"footprints"),a(s))}if(V()&&Ee()){console.log("[App] Fetching building footprints from API...");const s=await zi();if(!s?.success||!Array.isArray(s?.data)){console.warn("[App] getBuildingFootprints API response invalid:",s);return}const l=s.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 ba(l);const c=Sn(l);if(!c){console.warn("[App] Could not convert building footprints to GeoJSON");return}a(c),console.log("[App] Building footprints updated from API:",c.features.length,"footprints")}else i||console.log("[App] Building footprints not available — offline and no local cache")}catch(i){console.error("[App] Failed to load building footprints:",i)}}function br(n,e){if(!Array.isArray(n)||n.length===0)return null;const t=new wo,r=new ie,o=["geom","geometry","wkt","polygon","boundary","road","line"],a=[];for(const i of n){const s=i.geom||i.geometry||i.wkt||i.polygon||i.boundary||i.road||i.line;if(!s)continue;let l;try{if(typeof s=="object"&&s!==null&&s.type){a.push({type:"Feature",properties:Tn(i,o,e),geometry:s});continue}l=t.readGeometry(s)}catch(d){console.warn(`[App] Could not parse WKT for ${e}:`,d,s?.toString().slice(0,60));continue}const c=JSON.parse(r.writeGeometry(l));a.push({type:"Feature",properties:Tn(i,o,e),geometry:c})}return a.length===0?null:{type:"FeatureCollection",features:a}}function Tn(n,e,t){const r={_layerType:t};for(const[o,a]of Object.entries(n))e.includes(o)||typeof a=="object"&&a!==null||(r[o]=a);return r}async function Js(){const n={strokeColor:"#78716c",strokeWidth:.8,typeDescription:"Vector / Line",fillColor:"rgba(0,0,0,0)"},e=E?.getLayerGroupByTitle("Biophysical Environment");console.log("[App] loadContoursHillshade — group:",e?e.get("title"):"null");const t={type:"FeatureCollection",features:[]},r=E?.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&&F("No Contours hillshade data available. Connect to the internet to download it.")}),!V()||!Ee()){console.log("[App] Contours hillshade not available — offline or server unreachable");return}try{console.log("[App] Fetching contours_hillshade from API...");const o=await Ui();if(!o?.success||!Array.isArray(o?.data)){console.warn("[App] getContoursHillshade API response invalid:",o);return}const a=o.data;console.log("[App] Contours hillshade from API:",a.length,"rows"),a.length>0&&console.log("[App] First row keys:",Object.keys(a[0]));const i=br(a,"contours_hillshade");if(!i){console.warn("[App] Could not convert contours to GeoJSON");return}const s=new ie().readFeatures(i,{featureProjection:"EPSG:3857"});r.getSource().clear(),r.getSource().addFeatures(s),console.log("[App] Contours hillshade loaded:",s.length,"features")}catch(o){console.error("[App] Failed to load contours_hillshade:",o)}}async function Zs(){const e={strokeColor:"#F0F1F0",strokeWidth:1.5,lineCasingColor:"#000000",lineCasingWidth:3.5,fillColor:"rgba(0,0,0,0)",typeDescription:"Vector / Line"},t=E?.getLayerGroup(5)||null;console.log("[App] loadOSMRoads — group:",t?t.get("title"):"null");const r={type:"FeatureCollection",features:[]},o=E?.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&&F("No OSM roads available locally. Connect to the internet to download them.")});function a(i){const s=br(i,"osm_road");if(!s)return console.warn("[App] Could not convert OSM roads to GeoJSON"),0;const l=new ie().readFeatures(s,{featureProjection:"EPSG:3857"});return o.getSource().clear(),o.getSource().addFeatures(l),l.length}try{const i=await va();if(i){const s=a(i);console.log("[App] OSM_roads loaded from local cache:",s,"features")}if(V()&&Ee()){console.log("[App] Fetching OSM_roads from API...");const s=await Hi();if(!s?.success||!Array.isArray(s?.data)){console.warn("[App] getOSMRoads API response invalid:",s);return}const l=s.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 _a(l);const c=a(l);console.log("[App] OSM_roads updated from API:",c,"features")}else i||console.log("[App] OSM_roads not available — offline and no local cache")}catch(i){console.error("[App] Failed to load OSM_roads:",i)}}function Qs(){E?.addWMSLayer("Biophysical Environment","DEAfrica Coastlines v0.4","https://geoserver.digitalearth.africa/geoserver/wms","coastlines:DEAfrica_Coastlines",{serverType:"geoserver",visible:!1,onlineOnly:!0}),E?.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 el(){const n="layer_categories";function e(t){const r=[...t].sort((o,a)=>a.id-o.id);for(const o of r)E?.addLayerGroup(o.id,o.name,o.description||"");console.log("[App] Created",t.length,"layer groups on map")}try{const t=await qn(n);if(t&&(console.log("[App] Layer categories loaded from local cache:",t.length,"entries"),e(t)),V()&&Ee()){console.log("[App] Fetching layer categories from API...");const r=await Gi();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 jn(n,o),t){const a=E?.getOverlayGroup()?.getLayers();if(a){const i=[];a.forEach(s=>{s.get("layerId")!==void 0&&i.push(s)}),i.forEach(s=>a.remove(s))}}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 tl(){if(!V()){console.log("[App] Cannot sync - offline");return}console.log("[App] Sync placeholder - implement based on your backend")}const ce=[],ol={strokeColor:"#e11d48",strokeWidth:2,fillColor:"rgba(225,29,72,0.12)"};function ee(n){Rt("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 Lo(n,e,t){const r=Array.isArray(n)?n:[n];let o=0;for(const i of r){if(!i||i.type!=="FeatureCollection"||!i.features?.length)continue;const s=i.fileName?i.fileName.replace(/\.[^/.]+$/,""):e,l=E?.addGeoJSONLayer(i,s,ol);l&&(l.set("removable",!0),l.set("typeTag","GEO"),ce.push(l),o+=i.features.length,al(i,s,l).catch(c=>console.warn("[FileImport] Staging failed (layer remains view-only):",c)))}if(o===0){ee("No features found in the file.");return}console.log(`[${t}] Added ${o} feature(s) from ${r.length} layer(s)`);const a=ce[ce.length-1];if(a){const i=a.getSource().getExtent();E?.getMap().getView().fit(i,{padding:[50,50,50,50],maxZoom:18})}ko()}const nl=new wo;function rl(n){return nl.writeGeometry(n,{dataProjection:"EPSG:4326",featureProjection:"EPSG:3857"})}async function al(n,e,t){const r=n?.features?.length??0;if(r===0)return;const{id:o}=await ca({filename:e||"imported dataset",targetType:"other",featureCount:r});t.set("_externalImportId",o);const i=t.getSource().getFeatures().map(s=>{const l=s.getGeometry();return{geometry_wkt:l?rl(l):"",properties:il(s.getProperties())}});await da(o,i),ps({importId:o,filename:e,fc:n,onResult:async s=>{try{await sl(o,t,s)}catch(l){console.error("[FileImport] Failed to apply mapping result:",l),F("Could not save the import mapping: "+l.message)}}})}function il(n){const e={};for(const[t,r]of Object.entries(n||{}))t!=="geometry"&&(e[t]=r);return e}async function sl(n,e,t){if(!t||t.action==="cancel"){e?.set("_externalImportStatus","other"),je(e);return}const{action:r,targetType:o,mapping:a}=t;if(!o||o==="other"){await It(n,{targetType:"other",mapping:null,status:"other"}),e?.set("_externalImportStatus","other"),je(e);return}await fa(n,i=>ls(i,a)),await It(n,{targetType:o,mapping:a,status:"mapped"}),e?.set("_externalImportStatus","mapped"),e?.set("_externalImportTargetType",o),je(e),r==="upload"&&await wr(n,e)}async function wr(n,e){e?.set("_externalImportStatus","uploading"),je(e);try{await It(n,{status:"uploading"});const t=await ua(n),r=await pa(n),a={user_id_upload:Dt()?.user_id??null,import:{client_import_id:t.client_import_id,filename:t.filename,feature_count:r.length},features:r.map(i=>({client_uuid:i.client_uuid,geom:i.geometry_wkt,props:i.properties}))};console.log("[Upload]",{endpoint:`upload_${t.target_type}.php (not yet available on the server)`,target_type:t.target_type,body:a}),await It(n,{status:"mapped",lastUploadedAt:new Date().toISOString()}),e?.set("_externalImportStatus","mapped"),je(e),Sr("The server upload endpoint is not yet available. The data stays staged locally — you can upload again later.")}catch(t){console.error("[Upload] Stub failed:",t),e?.set("_externalImportStatus","mapped"),je(e),F("Upload preparation failed: "+t.message)}}window.addEventListener("lupmis:export-gis",n=>{Fs(n.detail||{})});window.addEventListener("lupmis:import-chip-click",n=>{const{importId:e,status:t,layer:r}=n.detail||{};t==="mapped"&&wr(e,r).catch(o=>console.error("[FileImport] runUpload failed:",o))});function je(n){if(!n||!E)return;const e=E.getMap()?.getControls()?.getArray()?.find(t=>t?.constructor?.name==="LayerSwitcher"||t?.element?.classList?.contains("ol-layerswitcher"));e&&typeof e.drawPanel=="function"&&e.drawPanel()}function ko(){const n=document.getElementById("imported-layers-info");if(!n)return;if(ce.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");ce.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">${j(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",()=>{ll(Number(t.dataset.removeIdx))})}),n.querySelector("#remove-imported-layers")?.addEventListener("click",()=>{cl()})}function ll(n){if(n<0||n>=ce.length)return;const e=ce[n],t=E?.getOverlayGroup();t&&t.getLayers().remove(e),ce.splice(n,1),ko(),console.log("[FileImport] Removed layer:",e.get("title"))}function cl(){const n=E?.getOverlayGroup();if(n)for(const e of ce)n.getLayers().remove(e);ce.length=0,ko(),console.log("[FileImport] All imported layers removed")}function _r(n){const e={};for(const t of n){const r=t.name.split(".").pop().toLowerCase();e[r]=t}return e}async function vr(n){const e=n.target.files;if(!e||e.length===0)return;const t=200*1024*1024,r=Array.from(e).reduce((o,a)=>o+a.size,0);if(r>t){const o=(r/1048576).toFixed(0);ee(`Files too large (${o} MB total). Maximum supported size is 200 MB.`),n.target.value="";return}try{let o,a;const i=_r(e);if(i.zip){const s=i.zip;a=s.name.replace(/\.zip$/i,""),console.log("[ShpImport] Parsing zip",s.name,"("+(s.size/1024).toFixed(1)+" KB)"),o=await(await wn())(await s.arrayBuffer())}else if(i.shp){a=i.shp.name.replace(/\.shp$/i,"");const l=["dbf","shx","prj"].filter(u=>!i[u]);if(l.length>0){ee("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 i.shp.arrayBuffer(),c.dbf=await i.dbf.arrayBuffer(),c.prj=await new Response(i.prj).text(),i.cpg&&(c.cpg=await new Response(i.cpg).text()),console.log("[ShpImport] Parsing loose files:",Object.keys(i).map(u=>"."+u).join(", "),"("+(i.shp.size/1024).toFixed(1)+" KB .shp)"),o=await(await wn())(c)}else{ee("Please select a .zip or at least a .shp file."),n.target.value="";return}Lo(o,a,"ShpImport")}catch(o){console.error("[ShpImport] Failed:",o),ee("Failed to parse shapefile: "+o.message)}n.target.value=""}async function Er(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);ee(`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 a;if(o.type==="FeatureCollection")a=o;else if(o.type==="Feature")a={type:"FeatureCollection",features:[o]};else if(o.type&&o.coordinates)a={type:"FeatureCollection",features:[{type:"Feature",geometry:o,properties:{}}]};else{ee("The file does not contain valid GeoJSON."),n.target.value="";return}const i=e.name.replace(/\.(geo)?json$/i,"");Lo(a,i,"GeoJSONImport")}catch(r){console.error("[GeoJSONImport] Failed:",r);const o=(e.size/(1024*1024)).toFixed(1);ee(`Failed to import "${e.name}" (${o} MB): ${r.message}`)}n.target.value=""}async function xr(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);ee(`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 a=new kn({extractStyles:!1}).readFeatures(r,{featureProjection:"EPSG:3857"});if(!a||a.length===0){ee("No features found in the KML file."),n.target.value="";return}const i=new ie,s=JSON.parse(i.writeFeatures(a,{featureProjection:"EPSG:3857",dataProjection:"EPSG:4326"})),l=e.name.replace(/\.kml$/i,"");Lo(s,l,"KMLImport")}catch(r){console.error("[KMLImport] Failed:",r);const o=(e.size/(1024*1024)).toFixed(1);ee(`Failed to import "${e.name}" (${o} MB): ${r.message}`)}n.target.value=""}function dl(){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=_r(r),a=Object.keys(o);if(o.zip||o.shp){const i={target:{files:r,value:""}};Object.defineProperty(i.target,"value",{writable:!0}),vr(i)}else if(o.geojson||o.json){const s={target:{files:[o.geojson||o.json],value:""}};Object.defineProperty(s.target,"value",{writable:!0}),Er(s)}else if(o.kml){const i={target:{files:[o.kml],value:""}};Object.defineProperty(i.target,"value",{writable:!0}),xr(i)}else ee("Unsupported file type(s): "+a.map(i=>"."+i).join(", ")+". Drop .zip, .shp, .geojson, .json, or .kml files.")}),console.log("[FileImport] Map drop zone initialised")}function j(n){const e=document.createElement("div");return e.textContent=n,e.innerHTML}const ul=50,Ln={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 Rt(n,e){const t=Ln[n]||Ln.info;(n==="error"?console.error:n==="warning"?console.warn:console.log)("[App]",e);const o=document.getElementById("message-log");if(!o)return;const a=o.querySelector(".text-muted");a&&a.remove();const i=document.createElement("div");i.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(i.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>${j(e)}</small></div><small class="text-muted flex-shrink-0 ms-1">${l}</small></div>`,o.prepend(i);o.children.length>ul;)o.lastElementChild.remove()}function pl(){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 fl(){const n=document.getElementById("gps-readout"),e=document.getElementById("gps-coords"),t=document.getElementById("gps-accuracy"),r=document.getElementById("gps-sats");if(!we.isSupported){e&&(e.textContent="No GPS");return}we.on("position",a=>{e&&(e.textContent=`${Jo(a.lat)}, ${Jo(a.lon)}`),t&&(t.textContent=Ji(a.accuracy)),r&&(r.textContent=`${a.satellites!=null?a.satellites:"—"} sat`),n&&(n.classList.add("active"),n.classList.remove("quality-good","quality-fair","quality-poor"),n.classList.add("quality-"+Zi(a.accuracy))),E?.showCurrentPosition(a.lon,a.lat,a.accuracy)}),we.on("point",a=>{E?.appendTrailPoint(a.point.lon,a.point.lat)}),we.on("error",a=>{console.warn("[GPS]",a?.message||a),a&&a.code===1&&F("Location permission denied. Enable location access to use GPS.")}),E.onLocateMe(async()=>{try{const a=await we.getCurrentPosition();E.centerOn(a.lon,a.lat,16)}catch(a){F("Could not get your location: "+(a?.message||a))}}),E.onToggleRecording(async a=>{if(a)try{await Bo,E.startTrailRender(),E.setRecordingState(!0),n?.classList.add("recording"),await we.startRecording({name:`Trail ${new Date().toLocaleString()}`}),fe("GPS trail recording started")}catch(i){E.setRecordingState(!1),n?.classList.remove("recording"),F("Could not start recording: "+(i?.message||i))}else try{const i=await we.stopRecording();if(E.setRecordingState(!1),n?.classList.remove("recording"),i){const s=`Trail saved: ${i.pointCount} points, ${Yi(i.distanceM)}`+(i.synced?" — synced":" — will sync when online");fe(s)}}catch(i){F("Error stopping recording: "+(i?.message||i))}});const o=async()=>{if(V())try{await Bo;const a=await we.syncPending();a.pushed&&console.log(`[GPS] Synced ${a.pushed} pending trail(s)`)}catch(a){console.warn("[GPS] pending-sync error",a)}};o(),Qn(a=>{a||o()})}function F(n){Rt("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 fe(n){Rt("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 Sr(n){Rt("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 hl(){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 gl(){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 ml(){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(),E?.setScaleBarUnits(r||"metric"),n.addEventListener("change",()=>{const o=n.checked?"imperial":"metric";localStorage.setItem("measurement-system",o),t(),E?.setScaleBarUnits(o),console.log("[Settings] Measurement system:",o)})}function yl(){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),E?.setBaseMap(t),console.log("[Settings] Default base map:",t)}),E?.getMap()?.on("basemapchange",t=>{if(t?.key&&n.value!==t.key){n.value=t.key;try{localStorage.setItem("default-basemap",t.key)}catch{}}})}function bl(){const n=document.getElementById("tile-cache-stats"),e=document.getElementById("clear-tiles-btn"),t=document.getElementById("offcanvasBottom");if(!n||!e||!t)return;function r(i){return i?i<1024*1024?(i/1024).toFixed(0)+" KB":i<1024*1024*1024?(i/(1024*1024)).toFixed(1)+" MB":(i/(1024*1024*1024)).toFixed(2)+" GB":"0 KB"}let o=null;async function a(){if(o)return o;const i=!!navigator.serviceWorker?.controller;return n.innerHTML=i?'<div class="text-muted fst-italic">Loading…</div>':'<div class="text-muted fst-italic">Initialising service worker…</div>',o=(async()=>{try{const s=await vi();if(!s){n.innerHTML=`
|
||
<div class="text-muted fst-italic">
|
||
Tile cache stats unavailable. Try reloading the page if this persists.
|
||
</div>`;return}const l=s.totals,c=s.byProvider.filter(p=>p.count>0).map(p=>`
|
||
<tr>
|
||
<td>${j(p.label)}</td>
|
||
<td class="text-end">${p.count.toLocaleString()} / ${p.limit.toLocaleString()}</td>
|
||
<td class="text-end">${r(p.estBytes)}</td>
|
||
<td class="text-end pe-0" style="width:2.2rem;">
|
||
<button type="button" class="btn btn-sm btn-link text-danger p-0 provider-clear-btn"
|
||
data-cache="${j(p.key)}" data-label="${j(p.label)}"
|
||
title="Clear ${j(p.label)} tiles only">
|
||
<i class="bi bi-trash3"></i>
|
||
</button>
|
||
</td>
|
||
</tr>`).join("");let d="";const u=await Si();if(u&&u.quota>0){const p=(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 (${p}%)
|
||
</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>
|
||
<th class="text-end pe-0" style="width:2.2rem;"></th>
|
||
</tr></thead>
|
||
<tbody>${c}</tbody>
|
||
</table>${d}`,e.disabled=!1,n.querySelectorAll(".provider-clear-btn").forEach(p=>{p.addEventListener("click",async h=>{h.preventDefault();const f=p.dataset.cache,y=p.dataset.label||f;if(!confirm(`Clear cached "${y}" tiles?
|
||
|
||
Other providers are not affected. The tiles will re-download as you browse online.`))return;p.disabled=!0,await xi(f)?console.log(`[Settings] Cleared tile cache for ${y}`):console.warn(`[Settings] Could not clear tile cache for ${y}`),await a()})})}finally{o=null}})(),o}e.addEventListener("click",async()=>{if(!confirm("Clear all cached map tiles from this device? You will need to be online to view them again."))return;e.disabled=!0,await Ei()?console.log("[Settings] Tile caches cleared"):console.warn("[Settings] Tile-cache clear failed"),await a()}),t.addEventListener("show.bs.offcanvas",a),_i(()=>{console.log("[Settings] SW controller changed → refreshing tile-cache stats"),a()}),a()}function wl(){const n=document.getElementById("download-tiles-btn"),e=document.getElementById("offline-download-modal");if(!n||!e)return;const t=ct.getOrCreateInstance(e),r=document.getElementById("offline-download-form-view"),o=document.getElementById("offline-download-progress-view"),a=document.getElementById("offline-download-done-view"),i=document.getElementById("offline-download-cancel-btn"),s=document.getElementById("offline-download-start-btn"),l=document.getElementById("offline-download-close-done-btn"),c=document.getElementById("offline-download-close-btn"),d=document.getElementById("offline-basemap-select"),u=document.getElementById("offline-min-zoom"),p=document.getElementById("offline-max-zoom"),h=document.getElementById("offline-ack-check"),f=document.getElementById("offline-estimate-detail"),y=document.getElementById("offline-estimate"),g=document.getElementById("offline-area-view"),m=document.getElementById("offline-area-district"),b=document.getElementById("offline-area-ghana"),_=document.getElementById("offline-area-view-info"),S=document.getElementById("offline-area-district-info"),T=document.getElementById("offline-progress-bar"),v=document.getElementById("offline-progress-percent"),x=document.getElementById("offline-progress-counts"),P=document.getElementById("offline-progress-ok"),k=document.getElementById("offline-progress-failed"),q=document.getElementById("offline-progress-eta"),U=document.getElementById("offline-done-title"),J=document.getElementById("offline-done-detail");let N=null;function le($){return $?$<1024*1024?($/1024).toFixed(0)+" KB":$<1024*1024*1024?($/(1024*1024)).toFixed(1)+" MB":($/(1024*1024*1024)).toFixed(2)+" GB":"0 KB"}function Z($){if(!$||$<1e3)return"< 1 s";const Y=Math.round($/1e3);if(Y<60)return Y+" s";const K=Math.floor(Y/60),te=Y%60;return K<60?`${K} min ${te} s`:`${Math.floor(K/60)} h ${K%60} min`}function ge(){return g.checked?E?.getCurrentViewExtent()||null:m.checked?E?.getDistrictBoundaryExtent()?.extent||null:b.checked?Ai:null}function H(){const $=d.value,Y=parseInt(u.value,10),K=parseInt(p.value,10);if(Number.isNaN(Y)||Number.isNaN(K)||Y>K){f.textContent="Invalid zoom range",y.classList.replace("alert-info","alert-warning"),s.disabled=!0;return}const te=ge();if(!te){f.textContent="Selected area is not available.",y.classList.replace("alert-info","alert-warning"),s.disabled=!0;return}const D=er[$]?.maxZoom??19,W=Math.min(K,D),oe=ki(te,Y,W),Nt=Ci(oe);let ye="";W<K&&(ye=`<br><span class="text-warning">Zoom ${K} is above this provider's max (${D}); will clamp to ${D}.</span>`),oe>8e3&&(ye+='<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>'),f.innerHTML=`<strong>${oe.toLocaleString()}</strong> tiles · ~${le(Nt)}`+ye,y.classList.toggle("alert-warning",!!ye),y.classList.toggle("alert-info",!ye),s.disabled=!h.checked||oe===0}function me(){E?.getCurrentViewExtent()?_.textContent=" · ready":_.textContent="",E?.getDistrictBoundaryExtent()?(S.textContent="",m.disabled=!1):(S.textContent=" (not loaded — connect online to fetch)",m.disabled=!0,m.checked&&(g.checked=!0))}function ze(){r.classList.remove("d-none"),o.classList.add("d-none"),a.classList.add("d-none"),s.classList.remove("d-none"),i.classList.remove("d-none"),i.textContent="Cancel",l.classList.add("d-none"),c.disabled=!1,h.checked=!1,s.disabled=!0,N=null}n.addEventListener("click",()=>{ze(),me(),H(),t.show()}),d.addEventListener("change",H),u.addEventListener("input",H),p.addEventListener("input",H),g.addEventListener("change",H),m.addEventListener("change",H),b.addEventListener("change",H),h.addEventListener("change",H),s.addEventListener("click",async()=>{const $=d.value,Y=parseInt(u.value,10),K=parseInt(p.value,10),te=ge();if(!te)return;r.classList.add("d-none"),o.classList.remove("d-none"),s.classList.add("d-none"),i.textContent="Cancel download",c.disabled=!0,T.style.width="0%",T.setAttribute("aria-valuenow","0"),v.textContent="0%",x.textContent="0 of 0 tiles",P.textContent="0",k.textContent="0",q.textContent="—",N=new Mi({baseMap:$,extent3857:te,minZoom:Y,maxZoom:K,onProgress:W=>{if(W.total>0){const oe=Math.min(100,Math.round(W.done/W.total*100));T.style.width=oe+"%",T.setAttribute("aria-valuenow",String(oe)),v.textContent=oe+"%",x.textContent=`${W.done.toLocaleString()} of ${W.total.toLocaleString()} tiles`}P.textContent=W.ok.toLocaleString(),k.textContent=W.failed.toLocaleString(),q.textContent=W.etaMs!=null?Z(W.etaMs):"—"}});let D;try{D=await N.start()}catch(W){console.error("[OfflineDownload] failed:",W),D={phase:"error",done:0,total:0,ok:0,failed:0}}o.classList.add("d-none"),a.classList.remove("d-none"),i.classList.add("d-none"),l.classList.remove("d-none"),c.disabled=!1,D.phase==="cancelled"?(U.textContent="Download cancelled",J.innerHTML=`Stopped after <strong>${D.done.toLocaleString()}</strong> of ${D.total.toLocaleString()} tiles.<br>${D.ok.toLocaleString()} fetched · ${D.failed.toLocaleString()} failed.`):D.phase==="error"?(U.textContent="Download failed",J.textContent="See console for details."):(U.textContent="Download complete",J.innerHTML=`<strong>${D.ok.toLocaleString()}</strong> tiles cached`+(D.failed>0?`, ${D.failed.toLocaleString()} failed`:"")+`.<br>Took ${Z(D.elapsedMs)}.`)}),i.addEventListener("click",()=>{N&&N.cancel()}),e.addEventListener("hidden.bs.modal",()=>{N&&N.cancel(),ze()})}function _l(){const n=Dt(),e=document.getElementById("menu-btn"),t=document.getElementById("menu-user-avatar"),r=document.getElementById("menu-user-name"),o=document.getElementById("menu-user-email"),a=document.getElementById("menu-user-detail"),i=document.getElementById("menu-signout-btn"),s=document.getElementById("menu-signin-link"),l=document.getElementById("menu-no-session-note");if(!e||!t||!r||!o||!a||!i){console.warn("[AccountMenu] One or more elements missing — shell may be stale. Hard-refresh.");return}if(!!n&&!!n.user_id){const d=[n.title,n.full_name].filter(Boolean).join(" ").trim()||n.username||"Authenticated user",u=(n.full_name||n.username||"?").trim().charAt(0).toUpperCase();t.textContent=u,t.style.background="var(--brand-navy, #1e1a4b)",r.textContent=d,o.textContent=n.email||"";const p=[];n.district_id!=null&&p.push(`District ${j(String(n.district_id))}`),n.region_id!=null&&p.push(`Region ${j(String(n.region_id))}`),n.ua_position&&p.push(j(n.ua_position)),a.innerHTML=p.join(" · ")||"No district info",i.classList.remove("d-none"),i.addEventListener("click",()=>vl(n),{once:!1}),s?.classList.add("d-none"),l?.classList.add("d-none"),e.removeAttribute("data-state"),e.setAttribute("title",`Menu — ${d}`)}else typeof window.LUPMIS_SESSION>"u"?(t.innerHTML='<i class="bi bi-exclamation"></i>',t.style.background="var(--brand-orange-warm, #ff9e1b)",r.textContent="No session injected",o.textContent="",a.textContent="",i.classList.add("d-none"),s?.classList.add("d-none"),l?.classList.remove("d-none"),e.dataset.state="no-session",e.setAttribute("title","Menu (no session — dev mode)")):(t.innerHTML='<i class="bi bi-person-fill"></i>',t.style.background="var(--brand-gray-medium, #7a7a7a)",r.textContent="Not signed in",o.textContent="",a.textContent="",i.classList.add("d-none"),s?.classList.remove("d-none"),l?.classList.add("d-none"),e.dataset.state="unauthenticated",e.setAttribute("title","Menu (not signed in)"))}async function vl(n){if(!confirm(`Return to Landing Page, ${n?.full_name||n?.username||"user"}?`))return;const e=document.cookie.split(";").map(r=>r.trim()).find(r=>r.startsWith("sso_auth_token="))?.split("=")[1];if(e)try{await fetch("https://lupmis4luspa.org/sso/logout?token="+encodeURIComponent(e),{method:"GET",mode:"no-cors",credentials:"include",cache:"no-store"})}catch(r){console.warn("[Signout] Best-effort SSO logout call failed:",r)}const t="Thu, 01 Jan 1970 00:00:00 GMT";document.cookie=`sso_auth_token=; expires=${t}; path=/; domain=.lupmis4luspa.org`,document.cookie=`sso_auth_token=; expires=${t}; path=/; domain=lupmis4luspa.org`,document.cookie=`sso_auth_token=; expires=${t}; path=/`,window.location.href="https://lupmis4luspa.org/"}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",_n):_n();
|
||
//# sourceMappingURL=index-DR_U08k-.js.map
|