From 2b64fd2c1ea73a1c4be44a3fdd66533a343eb6f0 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Fri, 12 Jun 2026 18:05:41 +0000 Subject: [PATCH] perf(map): skip /info round-trip on small viewports + optimistic revalidate Panning and zooming used to block on a fresh /info round-trip before the GeoJSON layers could start loading. With ETag revalidation the gate returns 304 on most pans, but the round-trip itself is what was making the map feel laggy on slow links. The frontend now uses a three-band strategy (load-strategy.ts): zoom < MIN_DATA_ZOOM (11): render nothing. MIN_DATA_ZOOM <= zoom < SKIP_INFO_ZOOM (default 17): if a recent /info is cached, render that decision immediately and revalidate in the background with If-None-Match. A 304 leaves the screen untouched; a 200 reconciles only when the per-layer decision actually changes. First load with no cache still waits one round-trip. zoom >= SKIP_INFO_ZOOM: skip /info entirely. The viewport is too small to plausibly cross any hide/cluster threshold, so the gate is unhelpful. Configurable via PLUGINS_CONFIG['netbox_pathways']['map_skip_info_zoom'] if a deployment has unusual feature density. fetchMapInfo's callback now also signals whether the response was a 200 (fresh) or 304 (unchanged), so callers can skip the reconciliation render in the common case. The pure decision logic is covered by vitest (load-strategy.test.ts and fetch-info.test.ts, 15 new tests, 217/217 passing total). --- CHANGELOG.md | 1 + docs/user-guide/interactive-map.md | 12 ++ .../netbox_pathways/dist/load-strategy.min.js | 2 + .../dist/load-strategy.min.js.map | 7 + .../netbox_pathways/dist/pathways-map.min.js | 2 +- .../dist/pathways-map.min.js.map | 8 +- .../static/netbox_pathways/package-lock.json | 30 ---- .../static/netbox_pathways/src/data-layers.ts | 52 +++++- .../netbox_pathways/src/fetch-info.test.ts | 102 +++++++++++ .../netbox_pathways/src/load-strategy.test.ts | 166 ++++++++++++++++++ .../netbox_pathways/src/load-strategy.ts | 89 ++++++++++ .../netbox_pathways/src/pathways-map.ts | 51 +++++- .../netbox_pathways/src/types/netbox.d.ts | 6 + netbox_pathways/template_content.py | 1 + netbox_pathways/views.py | 1 + 15 files changed, 484 insertions(+), 46 deletions(-) create mode 100644 netbox_pathways/static/netbox_pathways/dist/load-strategy.min.js create mode 100644 netbox_pathways/static/netbox_pathways/dist/load-strategy.min.js.map create mode 100644 netbox_pathways/static/netbox_pathways/src/fetch-info.test.ts create mode 100644 netbox_pathways/static/netbox_pathways/src/load-strategy.test.ts create mode 100644 netbox_pathways/static/netbox_pathways/src/load-strategy.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 71fb837..23605f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Skip-info band + optimistic `/info` revalidation on the map.** Panning and zooming no longer block on a fresh `/info` round-trip before the GeoJSON layers start loading. The frontend now uses a three-band strategy: below `MIN_DATA_ZOOM` (11) nothing renders; in the gated band, if a recent `/info` is cached the cached decision drives the immediate render and `/info` revalidates in the background with `If-None-Match` (a 304 leaves the screen untouched, a 200 reconciles only if the decision actually changed); at or above `SKIP_INFO_ZOOM` (default 17) `/info` is skipped entirely because the viewport is too small to plausibly cross any hide/cluster threshold. Configurable via `PLUGINS_CONFIG['netbox_pathways']['map_skip_info_zoom']` if a deployment hits the edge case. The pure decision logic lives in `netbox_pathways/static/netbox_pathways/src/load-strategy.ts` (`chooseLoadStrategy`, `decideSkipInfo`, `decisionsDiffer`) and is covered by vitest. `fetchMapInfo`'s callback now also signals whether the response was a 200 (fresh) or 304 (unchanged), so callers can skip the reconciliation render in the common case. - **Computed `geo_length` on Pathway and subclasses** -- the drawn length of a pathway's LineString, in metres, is now exposed as a read-only `geo_length` property computed by PostGIS (`ST_Length`) rather than entered manually. The existing `length` field stays for as-built / field-measured lengths (slack, sag, riser drops) and is now labelled "Length (m, as-built)" in detail panels alongside the new "Geo length (m, drawn)". A custom `PathwayQuerySet.with_geo_length()` adds an `_geo_length` annotation that the list views (`Pathway`, `Conduit`, `AerialSpan`, `DirectBuried`, `Innerduct`, `ConduitBank`) already apply so the new sortable "Geo length (m)" table column hits PostGIS, not Python. REST and GeoJSON serializers emit `geo_length`; `PathwayFilterSet` (and the per-subclass filtersets) gain `geo_length__gte` / `geo_length__lte` URL range filters via a `GeoLengthFilterMixin`. Requires a projected, metre-based SRID (`PLUGINS_CONFIG['netbox_pathways']['srid']`) -- which is already required for the rest of the plugin's geometry support. - **`/info` map endpoint and count-based layer gating** -- new `GET /api/plugins/pathways/geo/info/?bbox=...` returns per-layer feature counts (`structures`, `conduit_banks`, `conduits`, `aerial_spans`, `direct_buried`, `circuits`, and an `external` map for reference-mode registered layers) plus the per-layer thresholds the frontend uses to decide whether to render, client-cluster, or hide each layer. Thresholds default to `{structures: {cluster: 200, hide: 5000}, ...others: {hide: 500}}` and are overridable per-layer via `PLUGINS_CONFIG['netbox_pathways']['map_thresholds']`. The map frontend now consults `/info` on every pan/zoom and applies a single "structures clustered -> no supports" rule: whenever structures cross either threshold (client or server cluster), every pathway and reference-mode external layer is suppressed for that viewport. The hardcoded `MIN_BANK_ZOOM = 18` heuristic is removed; banks become visible whenever their viewport count is below the configured threshold. Over-budget layer toggles in the sidebar dim and display a count chip. `MapLayerRegistration` gains an optional `max_features` (default 500) for reference-mode external layers. - **Geometry on CSV bulk import** -- `StructureImportForm` (Point) and the LineString import forms (`ConduitImportForm`, `AerialSpanImportForm`, `ConduitBankImportForm`) now expose a `location` / `path` column. Values pass through the same forgiving parser as the interactive map widget, so spreadsheets can carry GeoJSON, WKT, DMS (hemispheres optional), or Google-Maps-style decimal `lat,lon` pairs. The parser produces WGS84 and Django GIS reprojects to the configured storage SRID at save time. New helper `netbox_pathways.coord_parser.parse_geometry_input` plus `ForgivingGeometryField` are also importable by downstream code that wants the same lenient parsing. diff --git a/docs/user-guide/interactive-map.md b/docs/user-guide/interactive-map.md index f13f7bd..0c39a06 100644 --- a/docs/user-guide/interactive-map.md +++ b/docs/user-guide/interactive-map.md @@ -54,6 +54,18 @@ Defaults (override in `PLUGINS_CONFIG['netbox_pathways']['map_thresholds']`): When the structures layer crosses either threshold (client cluster or server cluster), the supporting infrastructure layers are suppressed regardless of their own counts: at that density a single highlighted line cannot be matched back to a clustered structure marker, so the whole set is hidden until you zoom in. Reference-mode external layers participate in the same gating, using their `max_features` registration value (default 500). +### How the gating performs during panning + +To keep the gating logic from making the map feel laggy, the frontend uses three zoom bands: + +| Zoom band | Behaviour | `/info` round-trip | +| --- | --- | --- | +| Below `MIN_DATA_ZOOM` (11) | Nothing renders | None | +| `MIN_DATA_ZOOM` to `map_skip_info_zoom` -- 1 (default 16) | Render from the most recent cached `/info` immediately; `/info` revalidates in the background with `If-None-Match`. A 304 leaves the screen untouched; a 200 only triggers a reconcile if the per-layer decision actually changes. First load with an empty cache still waits one round-trip. | Conditional, in the background | +| At or above `map_skip_info_zoom` (default 17) | Render every enabled layer directly. The viewport is too small to plausibly cross any hide/cluster threshold, so the gate is skipped. | None | + +Set `PLUGINS_CONFIG['netbox_pathways']['map_skip_info_zoom']` to raise or lower the skip-info threshold for deployments with unusual feature density. + ### Sidebar Clicking any feature opens a sidebar panel with two views: diff --git a/netbox_pathways/static/netbox_pathways/dist/load-strategy.min.js b/netbox_pathways/static/netbox_pathways/dist/load-strategy.min.js new file mode 100644 index 0000000..31b1069 --- /dev/null +++ b/netbox_pathways/static/netbox_pathways/dist/load-strategy.min.js @@ -0,0 +1,2 @@ +"use strict";(()=>{var p=window.PATHWAYS_CONFIG||{},P=p.apiBase||"/api/plugins/pathways/geo/";var y=11,f,h=(f=p.skipInfoZoom)!=null?f:17;function g(e,t){var d;let r=e.counts.structures,o=e.thresholds.structures,i="off";r>o.hide?i="server":o.cluster!=null&&r>o.cluster&&(i="client");let s={},u=i!=="off";t.has("structures")&&(s.structures="render");let x=["conduit_banks","conduits","aerial_spans","direct_buried","circuits"];for(let n of x){if(!t.has(n))continue;if(u){s[n]="hide";continue}let a=(d=e.counts[n])!=null?d:0,c=e.thresholds[n];s[n]=c&&a>c.hide?"hide":"render"}let l=e.counts.external||{},b=e.thresholds.external||{};for(let n of Object.keys(l)){let a=`external:${n}`;if(!t.has(a))continue;if(u){s[a]="hide";continue}let c=b[n];s[a]=c&&l[n]>c.hide?"hide":"render"}return{clusterMode:i,layers:s}}function m(e){let t={};for(let r of e)t[r]="render";return{clusterMode:"off",layers:t}}function N(e,t,r,o=h){return e=o?{kind:"skip-info",decision:m(r)}:t?{kind:"optimistic",decision:g(t,r),revalidate:!0,cachedInfo:t}:{kind:"gated"}}function I(e,t){if(e.clusterMode!==t.clusterMode)return!0;let r=Object.keys(e.layers),o=Object.keys(t.layers);if(r.length!==o.length)return!0;for(let i of r)if(e.layers[i]!==t.layers[i])return!0;return!1}})(); +//# sourceMappingURL=load-strategy.min.js.map diff --git a/netbox_pathways/static/netbox_pathways/dist/load-strategy.min.js.map b/netbox_pathways/static/netbox_pathways/dist/load-strategy.min.js.map new file mode 100644 index 0000000..04131b0 --- /dev/null +++ b/netbox_pathways/static/netbox_pathways/dist/load-strategy.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../src/data-layers.ts", "../src/load-strategy.ts"], + "sourcesContent": ["/**\n * GeoJSON data layer loading and viewport caching.\n *\n * Fetches structures and pathways from the GeoJSON API, manages an\n * overfetch cache for smooth panning, and renders features on the map.\n *\n * Shared between the full-page infrastructure map (pathways-map.ts)\n * and the route planner map (route-planner-map.ts).\n */\n\nimport type { FeatureEntry, FeatureType, GeoJSONProperties, PathwayStyle } from './types/features';\nimport {\n structureIcon as _structureIcon,\n clusterIcon as _clusterIcon,\n esc as _esc,\n getCookie as _getCookie,\n haversine as _haversine,\n} from './map-utils';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst CFG: Partial = window.PATHWAYS_CONFIG || {};\nconst API_BASE: string = CFG.apiBase || '/api/plugins/pathways/geo/';\n\nexport { API_BASE };\n\nexport const MIN_DATA_ZOOM = 11;\n\n/**\n * Zoom at or above which `/info` is skipped entirely. The viewport at this\n * zoom is small enough that hide/cluster thresholds are effectively\n * unreachable, so blocking the render on an `/info` round-trip just adds\n * latency. Overridable by server config (`PATHWAYS_CONFIG.skipInfoZoom`)\n * and by callers of `chooseLoadStrategy` for tests.\n */\nexport const SKIP_INFO_ZOOM: number = CFG.skipInfoZoom ?? 17;\n\n// ---------------------------------------------------------------------------\n// /info endpoint: per-layer counts + thresholds for the current viewport\n// ---------------------------------------------------------------------------\n\nexport interface LayerThresholds {\n hide: number;\n cluster?: number;\n}\n\nexport interface MapInfo {\n bbox: [number, number, number, number] | null;\n counts: {\n structures: number;\n conduit_banks: number;\n conduits: number;\n aerial_spans: number;\n direct_buried: number;\n circuits: number;\n external?: Record;\n };\n thresholds: {\n structures: LayerThresholds;\n conduit_banks: LayerThresholds;\n conduits: LayerThresholds;\n aerial_spans: LayerThresholds;\n direct_buried: LayerThresholds;\n circuits: LayerThresholds;\n external?: Record;\n };\n}\n\nexport type ClusterMode = 'off' | 'client' | 'server';\nexport type LayerDecision = 'render' | 'hide';\n\nexport interface RenderingDecision {\n clusterMode: ClusterMode;\n layers: Record;\n}\n\n/**\n * Pure mapping from /info counts + thresholds + currently-enabled layers to\n * a per-layer render decision plus a global cluster mode.\n *\n * Rule of thumb: structures drive cluster mode. When structures are clustered\n * (client or server), every non-structure layer is hidden because the\n * supporting topology no longer makes sense at that density.\n *\n * Layer keys: native layers use their counts/thresholds keys directly\n * (e.g. ``'conduit_banks'``); external layers use ``'external:'``.\n */\nexport function decideLayerRendering(info: MapInfo, enabled: Set): RenderingDecision {\n const structuresCount = info.counts.structures;\n const sThresh = info.thresholds.structures;\n let clusterMode: ClusterMode = 'off';\n if (structuresCount > sThresh.hide) {\n clusterMode = 'server';\n } else if (sThresh.cluster != null && structuresCount > sThresh.cluster) {\n clusterMode = 'client';\n }\n\n const layers: Record = {};\n const suppress = clusterMode !== 'off';\n\n if (enabled.has('structures')) {\n layers.structures = 'render';\n }\n\n const nativeKeys: (keyof MapInfo['counts'])[] = [\n 'conduit_banks', 'conduits', 'aerial_spans', 'direct_buried', 'circuits',\n ];\n for (const key of nativeKeys) {\n if (!enabled.has(key)) continue;\n if (suppress) {\n layers[key] = 'hide';\n continue;\n }\n const count = (info.counts[key] as number) ?? 0;\n const threshold = info.thresholds[key as keyof MapInfo['thresholds']] as LayerThresholds | undefined;\n layers[key] = threshold && count > threshold.hide ? 'hide' : 'render';\n }\n\n const extCounts = info.counts.external || {};\n const extThresholds = info.thresholds.external || {};\n for (const name of Object.keys(extCounts)) {\n const key = `external:${name}`;\n if (!enabled.has(key)) continue;\n if (suppress) {\n layers[key] = 'hide';\n continue;\n }\n const t = extThresholds[name];\n layers[key] = t && extCounts[name] > t.hide ? 'hide' : 'render';\n }\n\n return { clusterMode, layers };\n}\n\n/**\n * Synthetic \"all enabled keys render\" decision for the skip-info band.\n *\n * At these zooms we deliberately skip `/info`, so we have no counts.\n * The premise is that any viewport at that zoom holds too few features to\n * cross hide/cluster thresholds in practice; if a deployment hits that\n * edge case it can raise `PATHWAYS_CONFIG.skipInfoZoom`.\n */\nexport function decideSkipInfo(enabled: Set): RenderingDecision {\n const layers: Record = {};\n for (const key of enabled) {\n layers[key] = 'render';\n }\n return { clusterMode: 'off', layers };\n}\n\n// ---------------------------------------------------------------------------\n// /info fetch helper\n// ---------------------------------------------------------------------------\n\nlet _infoController: AbortController | null = null;\nlet _lastInfoEtag = '';\nlet _lastInfo: MapInfo | null = null;\n\n/** Returns the most recent /info response, or null if none cached yet. */\nexport function getLastInfo(): MapInfo | null {\n return _lastInfo;\n}\n\n/** Test-only: clear cached /info state so each test starts deterministically. */\nexport function _resetInfoCache(): void {\n if (_infoController) _infoController.abort();\n _infoController = null;\n _lastInfoEtag = '';\n _lastInfo = null;\n}\n\n/**\n * Fetch `/info` with conditional revalidation.\n *\n * `callback(info, changed)` -- `changed` is `true` when the server returned\n * fresh data (200) and `false` when it returned 304 Not Modified, in which\n * case `info` is the cached value. Callers can short-circuit on `!changed`\n * to avoid re-rendering when the previous decision is still valid.\n */\nexport async function fetchMapInfo(\n bbox: string,\n callback: (info: MapInfo, changed: boolean) => void,\n): Promise {\n if (_infoController) _infoController.abort();\n const controller = new AbortController();\n _infoController = controller;\n\n const url = API_BASE + 'info/?bbox=' + bbox;\n const headers: Record = { 'Accept': 'application/json' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n if (_lastInfoEtag) headers['If-None-Match'] = _lastInfoEtag;\n\n try {\n const response = await fetch(url, { headers, signal: controller.signal });\n if (_infoController === controller) _infoController = null;\n if (response.status === 304 && _lastInfo) {\n callback(_lastInfo, false);\n return;\n }\n if (response.ok) {\n const data = await response.json() as MapInfo;\n _lastInfoEtag = response.headers.get('ETag') || '';\n _lastInfo = data;\n callback(data, true);\n }\n } catch {\n if (_infoController === controller) _infoController = null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// GeoJSON fetching with AbortController\n// ---------------------------------------------------------------------------\n\nconst _inflightControllers: Record = {};\n\ninterface FetchResult {\n data: GeoJSON.FeatureCollection | null; // null on 304\n etag: string;\n}\n\nexport async function fetchGeoJSON(\n endpoint: string,\n bbox: string,\n callback: (result: FetchResult) => void,\n extraParams?: Record,\n ifNoneMatch?: string,\n): Promise {\n // Abort any in-flight request for this endpoint\n if (_inflightControllers[endpoint]) {\n _inflightControllers[endpoint].abort();\n }\n\n let url = API_BASE + endpoint + '?format=json&bbox=' + bbox;\n if (extraParams) {\n for (const key in extraParams) {\n url += '&' + key + '=' + encodeURIComponent(String(extraParams[key]));\n }\n }\n\n const controller = new AbortController();\n _inflightControllers[endpoint] = controller;\n\n const headers: Record = { 'Accept': 'application/json' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n if (ifNoneMatch) headers['If-None-Match'] = ifNoneMatch;\n\n try {\n const response = await fetch(url, { headers, signal: controller.signal });\n _inflightControllers[endpoint] = undefined!;\n const etag = response.headers.get('ETag') || '';\n if (response.status === 304) {\n callback({ data: null, etag });\n } else if (response.ok) {\n const data = await response.json() as GeoJSON.FeatureCollection;\n callback({ data, etag });\n }\n } catch (e) {\n _inflightControllers[endpoint] = undefined!;\n // Silently ignore AbortError and network errors\n }\n}\n\n// ---------------------------------------------------------------------------\n// Server-side search\n// ---------------------------------------------------------------------------\n\nimport type { ServerSearchResult } from './types/features';\n\nlet _searchController: AbortController | null = null;\n\nexport async function serverSearch(\n query: string,\n onResults: (results: ServerSearchResult[]) => void,\n): Promise {\n if (_searchController) _searchController.abort();\n const controller = new AbortController();\n _searchController = controller;\n\n const headers: Record = { 'Accept': 'application/json' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n\n // Search all layer endpoints in parallel, without bbox\n const endpoints: [string, FeatureType, string][] = [\n ['structures/', 'structure', 'structure_type'],\n ['conduit-banks/', 'conduit_bank', 'pathway_type'],\n ['conduits/', 'conduit', 'pathway_type'],\n ['aerial-spans/', 'aerial', 'pathway_type'],\n ['direct-buried/', 'direct_buried', 'pathway_type'],\n ['circuits/', 'circuit', 'pathway_type'],\n ];\n\n const results: ServerSearchResult[] = [];\n\n const fetches = endpoints.map(function (cfg) {\n const [endpoint, featureType, typeField] = cfg;\n const url = API_BASE + endpoint + '?format=json&q=' + encodeURIComponent(query);\n return fetch(url, { headers, signal: controller.signal })\n .then(function (resp) { return resp.ok ? resp.json() : null; })\n .then(function (data: GeoJSON.FeatureCollection | null) {\n if (!data || !data.features) return;\n data.features.forEach(function (f: GeoJSON.Feature) {\n if (!f.geometry) return;\n const props = f.properties || {};\n let latlng: L.LatLng;\n if (f.geometry.type === 'Point') {\n const coords = (f.geometry as GeoJSON.Point).coordinates;\n latlng = L.latLng(coords[1], coords[0]);\n } else if (f.geometry.type === 'LineString') {\n const coords = (f.geometry as GeoJSON.LineString).coordinates;\n const mid = coords[Math.floor(coords.length / 2)];\n latlng = L.latLng(mid[1], mid[0]);\n } else {\n return;\n }\n results.push({\n name: props.name || props.cid || 'Unnamed',\n featureType: featureType,\n typeKey: (props[typeField] as string) || featureType,\n latlng: latlng,\n url: props.url as string | undefined,\n });\n });\n })\n .catch(function () { /* ignore aborted / failed */ });\n });\n\n try {\n await Promise.all(fetches);\n _searchController = null;\n onResults(results);\n } catch {\n _searchController = null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Line labels\n// ---------------------------------------------------------------------------\n\nexport function addLineLabels(geoJsonLayer: L.GeoJSON, layerGroup: L.LayerGroup, map: L.Map): void {\n if (map.getZoom() < 20) return;\n\n geoJsonLayer.eachLayer(function (layer: L.Layer) {\n const polyline = layer as L.Polyline;\n const coords = polyline.getLatLngs() as L.LatLng[];\n if (!coords || coords.length < 2) return;\n const feature = (polyline as any).feature as GeoJSON.Feature;\n const name = feature?.properties?.name as string | undefined;\n if (!name) return;\n\n const midIdx = Math.floor(coords.length / 2);\n const p1 = coords[midIdx - 1] || coords[0];\n const p2 = coords[midIdx];\n const midLat = (p1.lat + p2.lat) / 2;\n const midLng = (p1.lng + p2.lng) / 2;\n\n // Use screen pixel coordinates so the angle matches the visual line\n const px1 = map.latLngToContainerPoint(p1);\n const px2 = map.latLngToContainerPoint(p2);\n const dx = px2.x - px1.x;\n const dy = px2.y - px1.y;\n // Angle in screen space (0 deg = right, positive = clockwise)\n let angle = Math.atan2(dy, dx) * 180 / Math.PI;\n // Keep text readable (not upside-down)\n if (angle > 90) angle -= 180;\n if (angle < -90) angle += 180;\n\n const icon = L.divIcon({\n className: 'pw-line-label',\n html: '
' + _esc(name) + '
',\n iconSize: [0, 0] as [number, number],\n iconAnchor: [0, 0] as [number, number],\n });\n\n layerGroup.addLayer(L.marker([midLat, midLng], { icon, interactive: false }));\n });\n}\n\n// ---------------------------------------------------------------------------\n// Data layer groups\n// ---------------------------------------------------------------------------\n\nexport interface DataLayerGroups {\n structures: L.LayerGroup;\n conduitBanks: L.LayerGroup;\n conduits: L.LayerGroup;\n aerialSpans: L.LayerGroup;\n directBuried: L.LayerGroup;\n circuits: L.LayerGroup;\n}\n\nexport function createDataLayers(): DataLayerGroups {\n return {\n structures: L.layerGroup(),\n conduitBanks: L.layerGroup(),\n conduits: L.layerGroup(),\n aerialSpans: L.layerGroup(),\n directBuried: L.layerGroup(),\n circuits: L.layerGroup(),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Pathway config\n// ---------------------------------------------------------------------------\n\n/** Pathway layer descriptor.\n *\n * - ``endpoint`` -- GeoJSON endpoint path relative to API_BASE\n * - ``layerKey`` -- key into DataLayerGroups\n * - ``featureType`` -- canonical type label used by the sidebar/popover\n * - ``style`` -- Leaflet polyline style\n * - ``infoKey`` -- corresponding key in MapInfo.counts/thresholds; this is\n * what the gating decision uses\n */\nexport type PathwayInfoKey = 'conduit_banks' | 'conduits' | 'aerial_spans' | 'direct_buried' | 'circuits';\n\nexport interface PathwayConfig {\n endpoint: string;\n layerKey: keyof DataLayerGroups;\n featureType: FeatureType;\n style: PathwayStyle;\n infoKey: PathwayInfoKey;\n}\n\nexport const PATHWAY_CONFIGS: PathwayConfig[] = [\n { endpoint: 'conduit-banks/', layerKey: 'conduitBanks', featureType: 'conduit_bank',\n style: { color: '#ad1457', weight: 5, opacity: 0.8, dashArray: '' }, infoKey: 'conduit_banks' },\n { endpoint: 'conduits/', layerKey: 'conduits', featureType: 'conduit',\n style: { color: '#f57c00', weight: 3, opacity: 0.7, dashArray: '5 5' }, infoKey: 'conduits' },\n { endpoint: 'aerial-spans/', layerKey: 'aerialSpans', featureType: 'aerial',\n style: { color: '#1565c0', weight: 3, opacity: 0.7, dashArray: '10 5' }, infoKey: 'aerial_spans' },\n { endpoint: 'direct-buried/', layerKey: 'directBuried', featureType: 'direct_buried',\n style: { color: '#616161', weight: 3, opacity: 0.7, dashArray: '2 4' }, infoKey: 'direct_buried' },\n { endpoint: 'circuits/', layerKey: 'circuits', featureType: 'circuit',\n style: { color: '#d32f2f', weight: 3, opacity: 0.8, dashArray: '8 6' }, infoKey: 'circuits' },\n];\n\n// ---------------------------------------------------------------------------\n// Viewport cache\n// ---------------------------------------------------------------------------\n\nconst GEO_CACHE_SIZE = 12;\nconst OVERFETCH = 0.5; // fraction of viewport width/height to pad each side\n\ninterface GeoCacheEntry {\n west: number; south: number; east: number; north: number;\n zoom: number;\n extraKey: string;\n etag: string;\n data: GeoJSON.FeatureCollection;\n}\n\nconst _geoCache: Record = {};\n\n/** Find a cache entry that covers a given viewport at a given zoom. */\nfunction _findCovering(\n endpoint: string, zoom: number, extraKey: string,\n west: number, south: number, east: number, north: number,\n): GeoCacheEntry | null {\n const entries = _geoCache[endpoint];\n if (!entries) return null;\n for (let i = entries.length - 1; i >= 0; i--) {\n const e = entries[i];\n if (e.zoom === zoom && e.extraKey === extraKey &&\n west >= e.west && south >= e.south &&\n east <= e.east && north <= e.north) {\n return e;\n }\n }\n return null;\n}\n\nfunction _storeInCache(\n endpoint: string, west: number, south: number, east: number, north: number,\n zoom: number, extraKey: string, etag: string, data: GeoJSON.FeatureCollection,\n): void {\n if (!_geoCache[endpoint]) _geoCache[endpoint] = [];\n const cache = _geoCache[endpoint];\n cache.push({ west, south, east, north, zoom, extraKey, etag, data });\n if (cache.length > GEO_CACHE_SIZE) cache.shift();\n}\n\nexport function cachedFetch(\n map: L.Map,\n endpoint: string,\n callback: (data: GeoJSON.FeatureCollection) => void,\n extraParams?: Record,\n): void {\n const b = map.getBounds();\n const west = b.getWest(), south = b.getSouth();\n const east = b.getEast(), north = b.getNorth();\n const zoom = map.getZoom();\n const extraKey = extraParams ? JSON.stringify(extraParams) : '';\n\n const cached = _findCovering(endpoint, zoom, extraKey, west, south, east, north);\n if (cached) {\n callback(cached.data);\n return;\n }\n\n // Cache miss -- expand bbox and fetch\n const dw = (east - west) * OVERFETCH;\n const dh = (north - south) * OVERFETCH;\n const fw = west - dw, fs = south - dh, fe = east + dw, fn = north + dh;\n const fetchBbox = fw + ',' + fs + ',' + fe + ',' + fn;\n\n fetchGeoJSON(endpoint, fetchBbox, function (result: FetchResult) {\n if (result.data) {\n _storeInCache(endpoint, fw, fs, fe, fn, zoom, extraKey, result.etag, result.data);\n callback(result.data);\n }\n }, extraParams);\n}\n\n// ---------------------------------------------------------------------------\n// Neighbor preloading\n// ---------------------------------------------------------------------------\n\nlet _preloadTimer: ReturnType | null = null;\n\nexport interface PreloadSpec {\n endpoint: string;\n extraParams?: Record;\n}\n\nconst MIN_PRELOAD_ZOOM = 14; // don't preload at low zoom -- user is likely to zoom, not pan\n\nexport function preloadNeighbors(map: L.Map, specs: PreloadSpec[]): void {\n if (_preloadTimer) clearTimeout(_preloadTimer);\n\n const zoom = map.getZoom();\n if (zoom < MIN_PRELOAD_ZOOM) return;\n\n const b = map.getBounds();\n const vw = b.getEast() - b.getWest();\n const vh = b.getNorth() - b.getSouth();\n\n // Cardinal offsets: right, left, down, up\n const offsets: [number, number][] = [[vw, 0], [-vw, 0], [0, -vh], [0, vh]];\n\n // Build a queue of {endpoint, bbox} pairs, skipping already-cached regions\n const queue: { endpoint: string; bbox: string; fw: number; fs: number; fe: number; fn: number; zoom: number; extraKey: string; extraParams?: Record }[] = [];\n for (const spec of specs) {\n const extraKey = spec.extraParams ? JSON.stringify(spec.extraParams) : '';\n for (const [dx, dy] of offsets) {\n const cw = b.getWest() + dx, cs = b.getSouth() + dy;\n const ce = b.getEast() + dx, cn = b.getNorth() + dy;\n if (_findCovering(spec.endpoint, zoom, extraKey, cw, cs, ce, cn)) continue;\n const dw = vw * OVERFETCH, dh = vh * OVERFETCH;\n const fw = cw - dw, fs = cs - dh, fe = ce + dw, fn = cn + dh;\n queue.push({\n endpoint: spec.endpoint,\n bbox: fw + ',' + fs + ',' + fe + ',' + fn,\n fw, fs, fe, fn, zoom, extraKey,\n extraParams: spec.extraParams,\n });\n }\n }\n\n // Drain the queue one at a time to avoid flooding the server\n let idx = 0;\n function _next(): void {\n if (idx >= queue.length) return;\n // Abort if the user has moved (zoom changed or panned significantly)\n if (map.getZoom() !== zoom) return;\n const q = queue[idx++];\n fetchGeoJSON(q.endpoint, q.bbox, function (result: FetchResult) {\n if (result.data) {\n _storeInCache(q.endpoint, q.fw, q.fs, q.fe, q.fn, q.zoom, q.extraKey, result.etag, result.data);\n }\n _preloadTimer = setTimeout(_next, 50);\n }, q.extraParams);\n }\n\n // Start after a short idle delay so visible data renders first\n _preloadTimer = setTimeout(_next, 200);\n}\n\n// ---------------------------------------------------------------------------\n// Data loading callbacks\n// ---------------------------------------------------------------------------\n\nexport interface LoadCallbacks {\n onFeatureClick?: (entry: FeatureEntry, e: L.LeafletMouseEvent) => void;\n onFeatureMouseOver?: (entry: FeatureEntry, e: L.LeafletMouseEvent, feature: GeoJSON.Feature) => void;\n onFeatureMouseOut?: () => void;\n onFeatureCreated?: (entry: FeatureEntry) => void;\n onStructuresLoaded?: (count: number) => void;\n onPathwayLoaded?: (data: GeoJSON.FeatureCollection) => void;\n onAllLoaded?: (allFeatures: FeatureEntry[]) => void;\n}\n\n/**\n * Load all data layers for the current viewport.\n *\n * Decision-driven: the caller supplies a ``RenderingDecision`` (from\n * /info + ``decideLayerRendering``) that says which layers to render and\n * whether to client-cluster structures. Layers marked ``'hide'`` are cleared\n * without a network call; layers not present in the decision are also\n * cleared (treated as disabled).\n *\n * The structures fetch always passes ``zoom`` so the server-side grid\n * cluster fallback still kicks in for any installation that doesn't have a\n * fresh /info result.\n */\nexport function loadDataLayers(\n map: L.Map,\n dataLayers: DataLayerGroups,\n decision: RenderingDecision | null,\n zoomHint: HTMLDivElement | null,\n callbacks: LoadCallbacks,\n): void {\n const zoom = map.getZoom();\n\n if (zoom < MIN_DATA_ZOOM || decision == null) {\n dataLayers.structures.clearLayers();\n dataLayers.conduitBanks.clearLayers();\n dataLayers.conduits.clearLayers();\n dataLayers.aerialSpans.clearLayers();\n dataLayers.directBuried.clearLayers();\n dataLayers.circuits.clearLayers();\n if (zoomHint) zoomHint.style.display = zoom < MIN_DATA_ZOOM ? '' : 'none';\n if (callbacks.onAllLoaded) callbacks.onAllLoaded([]);\n return;\n }\n // From here on ``decision`` is non-null; the local alias lets nested\n // closures benefit from the narrowing.\n const live: RenderingDecision = decision;\n\n if (zoomHint) zoomHint.style.display = 'none';\n\n // Clear any pathway layer whose decision is 'hide' (or absent because\n // the toggle is off). Counts that don't make the cut never fetch.\n PATHWAY_CONFIGS.forEach(function (cfg) {\n if (live.layers[cfg.infoKey] !== 'render') {\n dataLayers[cfg.layerKey].clearLayers();\n }\n });\n\n const allFeatures: FeatureEntry[] = [];\n let pendingLoads = 0;\n let totalExpectedLoads = 0;\n\n const renderStructures = live.layers.structures === 'render' && map.hasLayer(dataLayers.structures);\n if (renderStructures) totalExpectedLoads++;\n\n let pendingPathway = 0;\n PATHWAY_CONFIGS.forEach(function (cfg) {\n if (live.layers[cfg.infoKey] === 'render' && map.hasLayer(dataLayers[cfg.layerKey])) {\n totalExpectedLoads++;\n pendingPathway++;\n }\n });\n\n function _checkAllLoaded(): void {\n pendingLoads++;\n if (pendingLoads === totalExpectedLoads) {\n if (callbacks.onAllLoaded) callbacks.onAllLoaded(allFeatures);\n // Prefetch cardinal neighbors for all active endpoints\n const specs: PreloadSpec[] = [];\n if (renderStructures) {\n specs.push({ endpoint: 'structures/', extraParams: { zoom: zoom } });\n }\n PATHWAY_CONFIGS.forEach(function (cfg) {\n if (live.layers[cfg.infoKey] === 'render' && map.hasLayer(dataLayers[cfg.layerKey])) {\n specs.push({ endpoint: cfg.endpoint });\n }\n });\n preloadNeighbors(map, specs);\n }\n }\n\n function _pathwayLoaded(data: GeoJSON.FeatureCollection): void {\n if (callbacks.onPathwayLoaded) callbacks.onPathwayLoaded(data);\n pendingPathway--;\n }\n\n // Structures\n if (renderStructures) {\n // Client clustering is only used when the decision says 'client'; in\n // 'off' mode markers render plain, in 'server' mode the response\n // already contains pre-aggregated cluster centroids.\n const hasClusterPlugin = typeof L.markerClusterGroup === 'function';\n if (!hasClusterPlugin && live.clusterMode === 'client') {\n console.info('[pathways] MarkerCluster plugin not loaded -- client-side clustering disabled');\n }\n const useClientCluster = live.clusterMode === 'client' && hasClusterPlugin;\n const clusterGroup = useClientCluster\n ? L.markerClusterGroup({ maxClusterRadius: 35, spiderfyOnMaxZoom: true })\n : null;\n\n cachedFetch(map, 'structures/', function (data: GeoJSON.FeatureCollection) {\n dataLayers.structures.clearLayers();\n\n const isServerClustered = data.features && data.features.length > 0 &&\n (data.features[0].properties as GeoJSONProperties)?.cluster;\n\n if (isServerClustered) {\n let total = 0;\n data.features.forEach(function (f: GeoJSON.Feature) {\n const props = f.properties as GeoJSONProperties;\n const count = props.point_count || 0;\n total += count;\n const geom = f.geometry as GeoJSON.Point;\n const latlng = L.latLng(geom.coordinates[1], geom.coordinates[0]);\n const marker = L.marker(latlng, { icon: _clusterIcon(count) });\n marker.on('click', function () {\n const nextZoom = Math.min(map.getZoom() + 3, map.getMaxZoom());\n map.setView(latlng, nextZoom);\n });\n dataLayers.structures.addLayer(marker);\n });\n if (callbacks.onStructuresLoaded) callbacks.onStructuresLoaded(total);\n } else {\n const geoLayer = L.geoJSON(data, {\n pointToLayer: function (feature: GeoJSON.Feature, latlng: L.LatLng) {\n return L.marker(latlng, {\n icon: _structureIcon((feature.properties as GeoJSONProperties).structure_type || ''),\n });\n },\n onEachFeature: function (feature: GeoJSON.Feature, layer: L.Layer) {\n if (feature.id != null && (feature.properties as GeoJSONProperties).id == null) {\n (feature.properties as GeoJSONProperties).id = feature.id as number;\n }\n const entry: FeatureEntry = {\n props: feature.properties as GeoJSONProperties,\n featureType: 'structure',\n layer: layer,\n latlng: (layer as L.Marker).getLatLng(),\n };\n allFeatures.push(entry);\n if (callbacks.onFeatureCreated) callbacks.onFeatureCreated(entry);\n layer.on('click', function (e: L.LeafletMouseEvent) {\n if (callbacks.onFeatureClick) callbacks.onFeatureClick(entry, e);\n });\n layer.on('mouseover', function (e: L.LeafletMouseEvent) {\n if (callbacks.onFeatureMouseOver) callbacks.onFeatureMouseOver(entry, e, feature);\n });\n layer.on('mouseout', function () {\n if (callbacks.onFeatureMouseOut) callbacks.onFeatureMouseOut();\n });\n },\n });\n if (clusterGroup) {\n clusterGroup.addLayers(geoLayer.getLayers());\n dataLayers.structures.addLayer(clusterGroup);\n } else {\n geoLayer.addTo(dataLayers.structures);\n }\n if (callbacks.onStructuresLoaded) {\n callbacks.onStructuresLoaded(data.features ? data.features.length : 0);\n }\n }\n _checkAllLoaded();\n }, { zoom: zoom });\n }\n\n if (pendingPathway === 0 && callbacks.onPathwayLoaded) {\n // No pathway layers active -- signal with empty data\n }\n\n // Shared pathway handler factory\n function _makePathwayOpts(featureType: FeatureType, styleObj: PathwayStyle): L.GeoJSONOptions {\n return {\n style: function () { return styleObj; },\n onEachFeature: function (feature: GeoJSON.Feature, layer: L.Layer) {\n const props = feature.properties as GeoJSONProperties;\n if (feature.id != null && props.id == null) {\n props.id = feature.id as number;\n }\n // Normalise: pathways use \"label\", map UI expects \"name\"\n if (!props.name && props.label) props.name = props.label as string;\n const entry: FeatureEntry = {\n props: props,\n featureType: featureType,\n layer: layer,\n latlng: (layer as L.Polyline).getBounds().getCenter(),\n };\n allFeatures.push(entry);\n if (callbacks.onFeatureCreated) callbacks.onFeatureCreated(entry);\n layer.on('click', function (e: L.LeafletMouseEvent) {\n if (callbacks.onFeatureClick) callbacks.onFeatureClick(entry, e);\n });\n layer.on('mouseover', function (e: L.LeafletMouseEvent) {\n if (callbacks.onFeatureMouseOver) callbacks.onFeatureMouseOver(entry, e, feature);\n });\n layer.on('mouseout', function () {\n if (callbacks.onFeatureMouseOut) callbacks.onFeatureMouseOut();\n });\n },\n };\n }\n\n PATHWAY_CONFIGS.forEach(function (cfg) {\n const layer = dataLayers[cfg.layerKey];\n if (live.layers[cfg.infoKey] !== 'render') return;\n if (!map.hasLayer(layer)) return;\n cachedFetch(map, cfg.endpoint, function (data: GeoJSON.FeatureCollection) {\n layer.clearLayers();\n const geoLayer = L.geoJSON(data, _makePathwayOpts(cfg.featureType, cfg.style));\n geoLayer.addTo(layer);\n addLineLabels(geoLayer, layer, map);\n _pathwayLoaded(data);\n _checkAllLoaded();\n });\n });\n\n // If no layers active, still signal completion\n if (totalExpectedLoads === 0) {\n if (callbacks.onAllLoaded) callbacks.onAllLoaded([]);\n }\n}\n\n/**\n * Calculate total pathway length from a GeoJSON FeatureCollection.\n * Returns length in meters.\n */\nexport function calcPathwayLength(data: GeoJSON.FeatureCollection): number {\n let totalLength = 0;\n if (data.features) {\n data.features.forEach(function (f: GeoJSON.Feature) {\n if (f.geometry && f.geometry.type === 'LineString') {\n const coords = (f.geometry as GeoJSON.LineString).coordinates;\n for (let i = 0; i < coords.length - 1; i++) {\n totalLength += _haversine(\n coords[i][1], coords[i][0],\n coords[i + 1][1], coords[i + 1][0],\n );\n }\n }\n });\n }\n return totalLength;\n}\n", "/**\n * Decides how to render the map at a given zoom + cache state.\n *\n * The map used to round-trip `/info` on every pan/zoom and only start the\n * GeoJSON fetches once the response came back. Even with conditional\n * revalidation (ETag + 304), that adds one full RTT per move which feels\n * noticeably laggy on a slow link.\n *\n * The strategy here cuts that latency in two ways:\n *\n * 1. Skip-info band (zoom >= SKIP_INFO_ZOOM): the viewport is small\n * enough that hide/cluster thresholds are effectively unreachable.\n * Skip `/info` entirely and synthesize a \"render every enabled layer\"\n * decision.\n *\n * 2. Optimistic + revalidate (MIN_DATA_ZOOM <= zoom < SKIP_INFO_ZOOM\n * with a cached `/info`): render the cached decision immediately and\n * fire `/info` in the background with `If-None-Match`. A 304 means\n * the optimistic render was correct -- nothing to do. A 200 with a\n * meaningfully different decision triggers a single reconciliation\n * reload.\n *\n * 3. Cold gated path (no cache): the original \"wait for /info, then\n * load\" behaviour, used only on the very first viewport.\n */\n\nimport {\n MIN_DATA_ZOOM,\n SKIP_INFO_ZOOM,\n decideLayerRendering,\n decideSkipInfo,\n} from './data-layers';\nimport type { MapInfo, RenderingDecision } from './data-layers';\n\nexport { MIN_DATA_ZOOM, SKIP_INFO_ZOOM, decideSkipInfo };\n\nexport type LoadStrategy =\n | { kind: 'below-min-zoom' }\n | { kind: 'skip-info'; decision: RenderingDecision }\n | { kind: 'optimistic'; decision: RenderingDecision; revalidate: true; cachedInfo: MapInfo }\n | { kind: 'gated' };\n\n/**\n * Picks the strategy for one pan/zoom event.\n *\n * `skipInfoZoom` defaults to `SKIP_INFO_ZOOM` so it tracks server config;\n * the argument is mainly there for tests.\n */\nexport function chooseLoadStrategy(\n zoom: number,\n cachedInfo: MapInfo | null,\n enabled: Set,\n skipInfoZoom: number = SKIP_INFO_ZOOM,\n): LoadStrategy {\n if (zoom < MIN_DATA_ZOOM) {\n return { kind: 'below-min-zoom' };\n }\n if (zoom >= skipInfoZoom) {\n return { kind: 'skip-info', decision: decideSkipInfo(enabled) };\n }\n if (cachedInfo) {\n return {\n kind: 'optimistic',\n decision: decideLayerRendering(cachedInfo, enabled),\n revalidate: true,\n cachedInfo,\n };\n }\n return { kind: 'gated' };\n}\n\n/**\n * True when two decisions would render differently.\n *\n * Used by the optimistic path: after revalidation returns a fresh /info,\n * we only re-run the (expensive) GeoJSON fetches if the new decision flips\n * a layer's visibility or the cluster mode. Identical decisions short-\n * circuit to a chip refresh.\n */\nexport function decisionsDiffer(a: RenderingDecision, b: RenderingDecision): boolean {\n if (a.clusterMode !== b.clusterMode) return true;\n const aKeys = Object.keys(a.layers);\n const bKeys = Object.keys(b.layers);\n if (aKeys.length !== bKeys.length) return true;\n for (const key of aKeys) {\n if (a.layers[key] !== b.layers[key]) return true;\n }\n return false;\n}\n"], + "mappings": "mBAuBA,IAAMA,EAA+B,OAAO,iBAAmB,CAAC,EAC1DC,EAAmBD,EAAI,SAAW,6BAIjC,IAAME,EAAgB,GA5B7BC,EAqCaC,GAAyBD,EAAAE,EAAI,eAAJ,KAAAF,EAAoB,GAoDnD,SAASG,EAAqBC,EAAeC,EAAyC,CAzF7F,IAAAL,EA0FI,IAAMM,EAAkBF,EAAK,OAAO,WAC9BG,EAAUH,EAAK,WAAW,WAC5BI,EAA2B,MAC3BF,EAAkBC,EAAQ,KAC1BC,EAAc,SACPD,EAAQ,SAAW,MAAQD,EAAkBC,EAAQ,UAC5DC,EAAc,UAGlB,IAAMC,EAAwC,CAAC,EACzCC,EAAWF,IAAgB,MAE7BH,EAAQ,IAAI,YAAY,IACxBI,EAAO,WAAa,UAGxB,IAAME,EAA0C,CAC5C,gBAAiB,WAAY,eAAgB,gBAAiB,UAClE,EACA,QAAWC,KAAOD,EAAY,CAC1B,GAAI,CAACN,EAAQ,IAAIO,CAAG,EAAG,SACvB,GAAIF,EAAU,CACVD,EAAOG,CAAG,EAAI,OACd,QACJ,CACA,IAAMC,GAASb,EAAAI,EAAK,OAAOQ,CAAG,IAAf,KAAAZ,EAA+B,EACxCc,EAAYV,EAAK,WAAWQ,CAAkC,EACpEH,EAAOG,CAAG,EAAIE,GAAaD,EAAQC,EAAU,KAAO,OAAS,QACjE,CAEA,IAAMC,EAAYX,EAAK,OAAO,UAAY,CAAC,EACrCY,EAAgBZ,EAAK,WAAW,UAAY,CAAC,EACnD,QAAWa,KAAQ,OAAO,KAAKF,CAAS,EAAG,CACvC,IAAMH,EAAM,YAAYK,CAAI,GAC5B,GAAI,CAACZ,EAAQ,IAAIO,CAAG,EAAG,SACvB,GAAIF,EAAU,CACVD,EAAOG,CAAG,EAAI,OACd,QACJ,CACA,IAAMM,EAAIF,EAAcC,CAAI,EAC5BR,EAAOG,CAAG,EAAIM,GAAKH,EAAUE,CAAI,EAAIC,EAAE,KAAO,OAAS,QAC3D,CAEA,MAAO,CAAE,YAAAV,EAAa,OAAAC,CAAO,CACjC,CAUO,SAASU,EAAed,EAAyC,CACpE,IAAMI,EAAwC,CAAC,EAC/C,QAAWG,KAAOP,EACdI,EAAOG,CAAG,EAAI,SAElB,MAAO,CAAE,YAAa,MAAO,OAAAH,CAAO,CACxC,CCtGO,SAASW,EACZC,EACAC,EACAC,EACAC,EAAuBC,EACX,CACZ,OAAIJ,EAAOK,EACA,CAAE,KAAM,gBAAiB,EAEhCL,GAAQG,EACD,CAAE,KAAM,YAAa,SAAUG,EAAeJ,CAAO,CAAE,EAE9DD,EACO,CACH,KAAM,aACN,SAAUM,EAAqBN,EAAYC,CAAO,EAClD,WAAY,GACZ,WAAAD,CACJ,EAEG,CAAE,KAAM,OAAQ,CAC3B,CAUO,SAASO,EAAgBC,EAAsBC,EAA+B,CACjF,GAAID,EAAE,cAAgBC,EAAE,YAAa,MAAO,GAC5C,IAAMC,EAAQ,OAAO,KAAKF,EAAE,MAAM,EAC5BG,EAAQ,OAAO,KAAKF,EAAE,MAAM,EAClC,GAAIC,EAAM,SAAWC,EAAM,OAAQ,MAAO,GAC1C,QAAWC,KAAOF,EACd,GAAIF,EAAE,OAAOI,CAAG,IAAMH,EAAE,OAAOG,CAAG,EAAG,MAAO,GAEhD,MAAO,EACX", + "names": ["CFG", "API_BASE", "MIN_DATA_ZOOM", "_a", "SKIP_INFO_ZOOM", "CFG", "decideLayerRendering", "info", "enabled", "structuresCount", "sThresh", "clusterMode", "layers", "suppress", "nativeKeys", "key", "count", "threshold", "extCounts", "extThresholds", "name", "t", "decideSkipInfo", "chooseLoadStrategy", "zoom", "cachedInfo", "enabled", "skipInfoZoom", "SKIP_INFO_ZOOM", "MIN_DATA_ZOOM", "decideSkipInfo", "decideLayerRendering", "decisionsDiffer", "a", "b", "aKeys", "bKeys", "key"] +} diff --git a/netbox_pathways/static/netbox_pathways/dist/pathways-map.min.js b/netbox_pathways/static/netbox_pathways/dist/pathways-map.min.js index 1899d04..bb0dc79 100644 --- a/netbox_pathways/static/netbox_pathways/dist/pathways-map.min.js +++ b/netbox_pathways/static/netbox_pathways/dist/pathways-map.min.js @@ -1,2 +1,2 @@ -"use strict";(()=>{var $t=Object.defineProperty,Xt=Object.defineProperties;var Qt=Object.getOwnPropertyDescriptors;var rt=Object.getOwnPropertySymbols;var en=Object.prototype.hasOwnProperty,tn=Object.prototype.propertyIsEnumerable;var ot=(e,t,n)=>t in e?$t(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,it=(e,t)=>{for(var n in t||(t={}))en.call(t,n)&&ot(e,n,t[n]);if(rt)for(var n of rt(t))tn.call(t,n)&&ot(e,n,t[n]);return e},st=(e,t)=>Xt(e,Qt(t));var K=(e,t,n)=>new Promise((r,o)=>{var i=l=>{try{s(n.next(l))}catch(c){o(c)}},a=l=>{try{s(n.throw(l))}catch(c){o(c)}},s=l=>l.done?r(l.value):Promise.resolve(l.value).then(i,a);s((n=n.apply(e,t)).next())});var at=["structure","conduit_bank","conduit","aerial","direct_buried","circuit"];var Le=new Map;function nn(e,t){var n,r;if(t.colorField&&t.colorMap){let o=String((n=e[t.colorField])!=null?n:"");return(r=t.colorMap[o])!=null?r:t.defaultColor}return t.color}function rn(e,t){return L.circleMarker(e,{radius:7,fillColor:t,color:"#fff",weight:2,opacity:1,fillOpacity:.85})}function on(e,t,n){var r;return L.polyline(e,{color:t,weight:n.weight,opacity:n.opacity,dashArray:(r=n.dash)!=null?r:void 0})}function sn(e,t,n){var r;return L.polygon(e,{color:t,fillColor:t,fillOpacity:.2,weight:n.weight,opacity:n.opacity,dashArray:(r=n.dash)!=null?r:void 0})}function lt(e,t){Le.clear();let n=new Map,r=[...e].sort((o,i)=>o.sortOrder-i.sortOrder);for(let o of r){let i=L.layerGroup();Le.set(o.name,{config:o,layerGroup:i,abortController:null}),n.set(o.name,i),o.defaultVisible&&i.addTo(t)}return n}function ct(e,t,n,r){return K(this,null,function*(){let o=[],i=[];for(let[a,s]of Le){if(!n.has(a)||ts.config.maxZoom)continue;s.abortController&&s.abortController.abort(),s.abortController=new AbortController;let l=an(s,e,t,o,r);i.push(l)}return yield Promise.all(i.map(a=>a.catch(()=>{}))),o})}function an(e,t,n,r,o){return K(this,null,function*(){var C,d;let{config:i,layerGroup:a,abortController:s}=e,l=i.url.includes("?")?"&":"?",c=`${i.url}${l}format=json&bbox=${t}&zoom=${n}`,u=document.cookie.match(/csrftoken=([^;]+)/),m={Accept:"application/json"};u&&(m["X-CSRFToken"]=u[1]);try{let y=yield fetch(c,{headers:m,signal:s==null?void 0:s.signal});if(!y.ok){console.warn(`External layer '${i.name}' fetch failed: ${y.status}`);return}let f=yield y.json();a.clearLayers();for(let g of f.features){if(!g.geometry)continue;let p=(C=g.properties)!=null?C:{};g.id!=null&&p.id==null&&(p.id=g.id);let h=nn(p,i.style),v=null,E;if(g.geometry.type==="Point"){let[_,T]=g.geometry.coordinates;E=L.latLng(T,_),v=rn(E,h)}else if(g.geometry.type==="LineString"){let _=g.geometry.coordinates.map(k=>L.latLng(k[1],k[0])),T=on(_,h,i.style);E=T.getBounds().getCenter(),v=T}else if(g.geometry.type==="Polygon"){let _=g.geometry.coordinates.map(k=>k.map(F=>L.latLng(F[1],F[0]))),T=sn(_,h,i.style);E=T.getBounds().getCenter(),v=T}if(v&&E){a.addLayer(v);let _={props:st(it({},p),{name:(d=p.name)!=null?d:`${i.label} #${p.id}`}),featureType:i.name,layer:v,latlng:E};r.push(_),o(_,i)}}}catch(y){if(y instanceof DOMException&&y.name==="AbortError")return;console.warn(`External layer '${i.name}' error:`,y)}})}function Z(e){var t;return(t=Le.get(e))==null?void 0:t.config}var J,ln,mt,ue,Se,gt,_e,ze,U=null,H=[],ie=[],R=null,W={},z={},se=null,V=null,Ge=null,be="",Ce=null,ve="",j=!1,Y=null,le=[],we=null,ut=.15;function yt(e){return at.indexOf(e)!==-1}function ht(e){let t=Z(e);return t?t.label:J(e.replace(/_/g," "))}function Ze(e){return e.featureType==="structure"?Se[e.props.structure_type||""]||"#616161":e.featureType==="circuit"?"#d32f2f":_e[e.props.pathway_type||""]||"#616161"}function Te(e){return e.featureType==="structure"?e.props.structure_type||"unknown":e.props.pathway_type||"unknown"}function D(e){return e.featureType+"-"+(e.props.id||"")}function cn(e,t){let n=parseInt(e.replace("#",""),16),r=Math.round((n>>16&255)+(255-(n>>16&255))*t),o=Math.round((n>>8&255)+(255-(n>>8&255))*t),i=Math.round((n&255)+(255-(n&255))*t);return"#"+(1<<24|r<<16|o<<8|i).toString(16).slice(1)}function Ue(){if(V&&(V.remove(),V=null),se){let e=se;e._origIcon&&(e.setIcon(e._origIcon),delete e._origIcon),e._origStyle&&typeof e.setStyle=="function"&&(e.setStyle(e._origStyle),delete e._origStyle),se=null}}function Lt(e){let t=e.layer;if(t)if(se=t,e.featureType==="structure"){let n=t;n._origIcon=n.getIcon();let r=e.props.structure_type||"",o=Se[r]||"#616161",i=gt[r]||'',a=i.includes('fill="none"');n.setIcon(L.divIcon({className:"pw-marker pw-marker-selected",html:''+i+"",iconSize:[26,26],iconAnchor:[13,13],popupAnchor:[0,-14]}))}else{let n=t,r=n.options||{};n._origStyle={weight:r.weight||3,opacity:r.opacity||.7,color:r.color,dashArray:r.dashArray};let o=n.getLatLngs();o&&o.length>0&&U&&(V=L.polyline(o,{color:cn(r.color||"#888",.55),weight:12,opacity:.5,interactive:!1}).addTo(U)),n.setStyle({weight:6,opacity:1,dashArray:""})}}function un(e){Ue(),Lt(e)}function dn(e){V&&(V.remove(),V=null),se=null,Lt(e)}function bt(e){le=[],we=e,Ct()}function Ct(){we&&(le=[],H.forEach(function(e){let t=D(e);we.has(t)||R&&t===D(R)||e.layer&&(le.push(e),e.featureType==="structure"?e.layer.setOpacity(ut):typeof e.layer.setStyle=="function"&&e.layer.setStyle({opacity:ut}))}))}function xe(){we=null,le.forEach(function(e){if(e.layer){if(e.featureType==="structure")e.layer.setOpacity(1);else if(typeof e.layer.setStyle=="function"){let t=e.layer._origStyle;e.layer.setStyle({opacity:t?t.opacity:.7})}}}),le=[]}function He(e){let t=document.getElementById("pw-feature-list");if(!t)return;let n=t.querySelectorAll(".pw-list-item"),r=e?D(e):null;for(let o=0;o0?"":"none"),ie.forEach(function(n){let r=document.createElement("div");r.className="pw-list-item",r.setAttribute("data-feature-id",D(n)),R&&D(R)===D(n)&&r.classList.add("active");let o=document.createElement("span");o.className="pw-list-dot",o.style.background=Ze(n),r.appendChild(o);let i=document.createElement("span");i.className="pw-list-label",i.textContent=n.props.name||"Unnamed",i.title=n.props.name||"Unnamed",r.appendChild(i);let a=document.createElement("span");a.className="pw-list-type",a.textContent=J(Te(n)),r.appendChild(a),r.addEventListener("click",function(){te(n)}),e.appendChild(r)}))}function dt(){let e=document.getElementById("pw-type-filters");if(!e)return;e.textContent="";let t={};H.forEach(function(r){let o=Te(r);t[o]||(t[o]=Ze(r))});let n=Object.keys(t).sort();n.length<=1||(n.forEach(function(r){W[r]===void 0&&(W[r]=!0)}),n.forEach(function(r){let o=document.createElement("button");o.className="pw-filter-btn"+(W[r]?" active":""),o.type="button";let i=document.createElement("span");i.className="pw-filter-dot",i.style.background=t[r],o.appendChild(i);let a=document.createTextNode(ht(r));o.appendChild(a),o.addEventListener("click",function(){W[r]=!W[r],o.classList.toggle("active",W[r]),ee()}),e.appendChild(o)}))}function ee(){let e=document.getElementById("pw-search"),t=(e?e.value:"").toLowerCase().trim();ie=H.filter(function(n){let r=Te(n);if(W[r]===!1)return!1;if(t){let o=(n.props.name||"").toLowerCase(),i=J(r).toLowerCase();if(o.indexOf(t)===-1&&i.indexOf(t)===-1)return!1}return!0}),pn(),ie.length===0&&t.length>=2&&Ge?t!==be&&(be=t,En(),Ge(t)):(be="",wt())}function fn(e){let t=e.props.id;if(!yt(e.featureType)||t==null)return"";let n="/plugins/pathways/";switch(e.featureType){case"structure":return n+"structures/"+t+"/";case"conduit_bank":return n+"conduit-banks/"+t+"/";case"conduit":return n+"conduits/"+t+"/";case"aerial":return n+"aerial-spans/"+t+"/";case"direct_buried":return n+"direct-buried/"+t+"/";case"circuit":return n+"circuit-geometries/"+t+"/";default:return n+"pathways/"+t+"/"}}function vt(e){var r;if(e.props.url)return"/api"+e.props.url;let t=e.props.id;if(yt(e.featureType)){let o=ze.replace(/geo\/?$/,"");switch(e.featureType){case"structure":return o+"structures/"+t+"/";case"conduit_bank":return o+"conduit-banks/"+t+"/";case"conduit":return o+"conduits/"+t+"/";case"aerial":return o+"aerial-spans/"+t+"/";case"direct_buried":return o+"direct-buried/"+t+"/";case"circuit":return o+"circuit-geometries/"+t+"/";default:return o+"pathways/"+t+"/"}}let n=Z(e.featureType);return(r=n==null?void 0:n.detail)!=null&&r.urlTemplate?n.detail.urlTemplate.replace("{id}",String(t)):""}function je(e){if(e==null||e==="")return null;if(Array.isArray(e)){if(e.length===0)return null;let t=[];for(let n=0;n0?{text:t.join(", ")}:null}if(typeof e=="object"&&e!==null&&"label"in e){let t=e;return{text:t.label||J(t.value||"")}}if(typeof e=="object"&&e!==null){let t=e;if(t.display||t.name||t.id!==void 0)return{text:t.display||t.name||String(t.id),url:t.display_url||t.url||null}}return e===!0?{text:"Yes"}:e===!1?{text:"No"}:{text:String(e)}}function Je(e,t,n,r){let o=je(n);if(!o)return;let i=o.text+(r||""),a=document.createElement("tr"),s=document.createElement("td");s.textContent=t;let l=document.createElement("td");if(o.url){let c=document.createElement("a");c.href=o.url,c.textContent=i,l.appendChild(c)}else l.textContent=i;a.appendChild(s),a.appendChild(l),e.appendChild(a)}function mn(e,t){if(!t||!t.length)return;let n=document.createElement("tr"),r=document.createElement("td");r.textContent="Tags";let o=document.createElement("td");t.forEach(function(i){let a=document.createElement("span");a.className="badge",a.style.cssText="margin-right:4px;margin-bottom:2px;",i.color?(a.style.background="#"+i.color,a.style.color="#fff"):a.style.background="var(--tblr-border-color-translucent, rgba(0,0,0,0.1))",a.textContent=i.display||i.name||String(i),o.appendChild(a)}),n.appendChild(r),n.appendChild(o),e.appendChild(n)}var pt={structure:[["Type","structure_type"],["Site","site"],["Elevation","elevation"," m"],["Height","height"," m"],["Width","width"," m"],["Length","length"," m"],["Depth","depth"," m"],["Tenant","tenant"],["Installation Date","installation_date"],["Access Notes","access_notes"],["Comments","comments"]],conduit_bank:[["Start Structure","start_structure"],["End Structure","end_structure"],["Start Face","start_face"],["End Face","end_face"],["Configuration","configuration"],["Total Conduits","total_conduits"],["Encasement","encasement_type"],["Length","length"," m"],["Tenant","tenant"],["Installation Date","installation_date"],["Comments","comments"]],conduit:[["Start Structure","start_structure"],["End Structure","end_structure"],["Start Location","start_location"],["End Location","end_location"],["Material","material"],["Inner Diameter","inner_diameter"," mm"],["Outer Diameter","outer_diameter"," mm"],["Depth","depth"," m"],["Length","length"," m"],["Conduit Bank","conduit_bank"],["Bank Position","bank_position"],["Start Junction","start_junction"],["End Junction","end_junction"],["Cables Routed","cables_routed"],["Tenant","tenant"],["Installation Date","installation_date"],["Comments","comments"]],aerial:[["Start Structure","start_structure"],["End Structure","end_structure"],["Start Location","start_location"],["End Location","end_location"],["Aerial Type","aerial_type"],["Start Attachment Height","start_attachment_height"," m"],["End Attachment Height","end_attachment_height"," m"],["Attachment Height (mean)","attachment_height"," m"],["Sag","sag"," m"],["Messenger Size","messenger_size"],["Wind Loading","wind_loading"],["Ice Loading","ice_loading"],["Length","length"," m"],["Cables Routed","cables_routed"],["Tenant","tenant"],["Installation Date","installation_date"],["Comments","comments"]],direct_buried:[["Start Structure","start_structure"],["End Structure","end_structure"],["Start Location","start_location"],["End Location","end_location"],["Burial Depth","burial_depth"," m"],["Warning Tape","warning_tape"],["Tracer Wire","tracer_wire"],["Armor Type","armor_type"],["Length","length"," m"],["Cables Routed","cables_routed"],["Tenant","tenant"],["Installation Date","installation_date"],["Comments","comments"]],circuit:[["Circuit","circuit"],["Provider Reference","provider_reference"],["Comments","comments"]],default:[["Start Structure","start_structure"],["End Structure","end_structure"],["Start Location","start_location"],["End Location","end_location"],["Length","length"," m"],["Cables Routed","cables_routed"],["Tenant","tenant"],["Installation Date","installation_date"],["Comments","comments"]]};function ce(e,t){let n=document.createElement("div");n.className="pw-section-header";let r=document.createElement("i");r.className="mdi mdi-chevron-down",n.appendChild(r);let o=document.createElement("span");o.textContent=e,n.appendChild(o),t.appendChild(n);let i=document.createElement("div");return i.className="pw-section-body",t.appendChild(i),n.addEventListener("click",function(){let a=i.classList.toggle("collapsed");n.classList.toggle("collapsed",a)}),i}function gn(e,t){let n=document.querySelector(".pw-metric-row");if(!n)return;let r=[];t.featureType==="structure"?e.elevation&&r.push(["Elev","elevation"," m"]):e.length&&r.push(["Length","length"," m"]),t.featureType==="conduit"&&e.inner_diameter&&r.push(["ID","inner_diameter"," mm"]),r.forEach(function(o){let i=je(e[o[1]]);if(!i)return;let a=document.createElement("span");a.className="pw-metric-badge pw-metric-muted",a.textContent=o[0]+" "+i.text+o[2],n.appendChild(a)})}function ft(e,t,n){gn(e,t);let r=Z(t.featureType);if(r!=null&&r.detail){let a=ce("Details",n),s=document.createElement("table");s.className="pw-detail-table";for(let l of r.detail.fields){let c=e[l];c!=null&&Je(s,J(l.replace(/_/g," ")),c)}a.appendChild(s);return}let o=pt[t.featureType]||pt.default,i=document.createElement("table");if(i.className="pw-detail-table",o.forEach(function(a){let s=e[a[1]];Je(i,a[0],s,a[2]||"")}),mn(i,e.tags),i.childNodes.length>0&&ce("Details",n).appendChild(i),e.created||e.last_updated){let a=document.createElement("div");a.style.cssText="font-size:0.72em;color:var(--tblr-muted-color,#667382);margin-top:8px;";let s=[];e.created&&s.push("Created "+e.created.split("T")[0]),e.last_updated&&s.push("Updated "+e.last_updated.split("T")[0]),a.textContent=s.join(" \xB7 "),n.appendChild(a)}t.featureType==="structure"&&yn(t,n),t.featureType!=="structure"&&hn(e,t,n)}function Ke(e,t,n,r,o,i,a){let s=document.createElement("div");s.className="pw-list-item",s.style.padding="6px 0",s.style.cursor="pointer";let l=document.createElement("span");l.className="pw-list-dot",l.style.background=n,s.appendChild(l);let c=document.createElement("span");c.className="pw-list-label",c.textContent=t||"Unnamed",s.appendChild(c);let u=document.createElement("span");u.className="pw-metric-badge pw-metric-muted",u.style.fontSize="0.65em",u.style.padding="1px 6px",u.textContent=r,s.appendChild(u),s.addEventListener("click",function(){let m=H.find(function(C){return D(C)===i});m?te(m):a&&U?(ve=i,U.flyTo(a,18,{duration:.5})):window.location.href=window.location.pathname+"?select="+encodeURIComponent(i)}),e.appendChild(s)}function yn(e,t){let n=e.props.id,o=ze.replace(/geo\/?$/,"")+"pathways/?structure_id="+n,i={Accept:"application/json"},a=ue("csrftoken");a&&(i["X-CSRFToken"]=a),fetch(o,{headers:i}).then(function(s){return s.ok?s.json():null}).then(function(s){if(!s||!s.results||s.results.length===0)return;let l={},c=[];s.results.forEach(function(d){let y=d.pathway_type||"",f=typeof y=="object"?y.value||"":y,g=typeof y=="object"&&y.label||J(f),p=_e[f]||"#616161",h=d.display||d.label||"Unnamed",v=d.id,E=H.find(function(I){return I.featureType!=="structure"&&I.props.id===v});c.push({display:h,color:p,typeLabel:g,mapFeature:E,selectId:f+"-"+v,hint:E?E.latlng:void 0});let _,T;if(E&&E.layer)try{let I=E.layer.getLatLngs();I&&I.length>=2&&(_={lat:I[0].lat,lng:I[0].lng},T={lat:I[I.length-1].lat,lng:I[I.length-1].lng})}catch(I){}let k=d.start_structure,F=d.end_structure;k&&k.id&&k.id!==n&&!l[k.id]&&(l[k.id]={id:k.id,display:k.display||String(k.id),hint:_}),F&&F.id&&F.id!==n&&!l[F.id]&&(l[F.id]={id:F.id,display:F.display||String(F.id),hint:T})});let u=Object.values(l);if(u.length>0){let d=ce("Connected Structures ("+u.length+")",t);u.forEach(function(y){let f=H.find(function(g){return g.featureType==="structure"&&g.props.id===y.id});Ke(d,y.display,"#2e7d32","Structure",f,"structure-"+y.id,y.hint)})}let m=ce("Connected Pathways ("+c.length+")",t);c.forEach(function(d){Ke(m,d.display,d.color,d.typeLabel,d.mapFeature,d.selectId,d.hint)});let C=new Set;u.forEach(function(d){C.add("structure-"+d.id)}),c.forEach(function(d){C.add(d.selectId)}),bt(C)})}function hn(e,t,n){let r=[],o=e.start_structure,i=e.end_structure;if(o&&o.id&&r.push({id:o.id,display:o.display||String(o.id),url:o.url}),i&&i.id&&(!o||i.id!==o.id)&&r.push({id:i.id,display:i.display||String(i.id),url:i.url}),r.length===0)return;let a=ce("Connected Structures ("+r.length+")",n),s,l;if(t.layer)try{let u=t.layer.getLatLngs();u&&u.length>=2&&(s={lat:u[0].lat,lng:u[0].lng},l={lat:u[u.length-1].lat,lng:u[u.length-1].lng})}catch(u){}r.forEach(function(u,m){let C=H.find(function(y){return y.featureType==="structure"&&y.props.id===u.id}),d=m===0?s:l;Ke(a,u.display,"#2e7d32","Structure",C,"structure-"+u.id,d)});let c=new Set;r.forEach(function(u){c.add("structure-"+u.id)}),bt(c)}function Ln(e,t){return K(this,null,function*(){let n=D(e),r=Z(e.featureType),o=bn(r,e);if(o){let c="html:"+n;if(z[c]){Et(t,z[c]);return}yield Cn(o,c,t);return}if(z[n]){ft(z[n],e,t);return}let i=vt(e);if(!i){let c=document.createElement("table");c.className="pw-detail-table";let u=e.props;for(let m in u)u.hasOwnProperty(m)&&(m==="id"||m==="url"||Je(c,J(m.replace(/_/g," ")),u[m]));t.appendChild(c);return}let a=document.createElement("div");a.className="pw-detail-loading",a.textContent="Loading details...",t.appendChild(a);let s={Accept:"application/json"},l=ue("csrftoken");l&&(s["X-CSRFToken"]=l);try{let c=yield fetch(i,{headers:s});if(t.textContent="",c.ok){let u=yield c.json();z[n]=u,ft(u,e,t)}else{let u=document.createElement("div");u.className="pw-detail-loading",u.textContent="Could not load details (HTTP "+c.status+")",t.appendChild(u)}}catch(c){t.textContent="";let u=document.createElement("div");u.className="pw-detail-loading",u.textContent="Network error",t.appendChild(u)}})}function bn(e,t){var n;return(n=e==null?void 0:e.detail)!=null&&n.detailUrl?e.detail.detailUrl.replace("{id}",String(t.props.id)):""}function Et(e,t){e.innerHTML=t}function Cn(e,t,n){return K(this,null,function*(){let r=document.createElement("div");r.className="pw-detail-loading",r.textContent="Loading details...",n.appendChild(r);let o={Accept:"text/html"},i=ue("csrftoken");i&&(o["X-CSRFToken"]=i);try{let a=yield fetch(e,{headers:o});if(n.textContent="",a.ok){let s=yield a.text();z[t]=s,Et(n,s)}else{let s=document.createElement("div");s.className="pw-detail-loading",s.textContent="Could not load details (HTTP "+a.status+")",n.appendChild(s)}}catch(a){n.textContent="";let s=document.createElement("div");s.className="pw-detail-loading",s.textContent="Network error",n.appendChild(s)}})}function vn(e){let t=document.getElementById("pw-detail-body");if(!t)return;t.textContent="";let n=e.props,r=document.createElement("div");r.style.cssText="display:flex;align-items:center;gap:6px;margin-bottom:8px;";let o=document.createElement("div");o.className="pw-detail-title",o.style.marginBottom="0",o.textContent=n.name||"Unnamed",r.appendChild(o);let i=document.createElement("button");i.className="pw-edit-btn",i.title="Edit name";let a=document.createElement("i");a.className="mdi mdi-pencil",i.appendChild(a),r.appendChild(i),t.appendChild(r);let s=document.createElement("div");s.className="pw-inline-edit";let l=document.createElement("input");l.type="text",l.className="form-control form-control-sm",l.value=n.name||"",l.style.flex="1",s.appendChild(l);let c=document.createElement("button");c.className="btn btn-sm btn-primary",c.textContent="Save",s.appendChild(c);let u=document.createElement("button");u.className="btn btn-sm btn-outline-secondary",u.textContent="\xD7",s.appendChild(u),t.appendChild(s),i.addEventListener("click",function(){s.classList.add("active"),r.style.display="none",l.focus(),l.select()}),u.addEventListener("click",function(){s.classList.remove("active"),r.style.display=""}),c.addEventListener("click",function(){let p=l.value.trim();if(!p||p===n.name){u.click();return}let h=vt(e),v={"Content-Type":"application/json",Accept:"application/json"},E=ue("csrftoken");E&&(v["X-CSRFToken"]=E),fetch(h,{method:"PATCH",headers:v,body:JSON.stringify({name:p})}).then(function(_){_.ok&&(n.name=p,o.textContent=p,delete z[D(e)],ee()),s.classList.remove("active"),r.style.display=""})}),l.addEventListener("keydown",function(p){p.key==="Enter"&&c.click(),p.key==="Escape"&&u.click()});let m=Ze(e),C=Te(e),d=document.createElement("div");d.className="pw-metric-row";let y=document.createElement("span");if(y.className="pw-metric-badge",y.style.background=m,y.style.color="#fff",y.textContent=J(C),d.appendChild(y),e.featureType==="structure"&&n.site_name){let p=document.createElement("span");p.className="pw-metric-badge pw-metric-muted",p.textContent=n.site_name,d.appendChild(p)}t.appendChild(d);let f=fn(e);if(f){let p=document.createElement("a");p.href=f,p.className="btn btn-sm btn-primary w-100 mb-3";let h=document.createElement("i");h.className="mdi mdi-open-in-new",p.appendChild(h),p.appendChild(document.createTextNode(" View Details ")),t.appendChild(p)}if(e.featureType==="structure"){let p=document.createElement("a");p.href="/plugins/pathways/route-planner/?start="+n.id,p.className="btn btn-sm btn-outline-primary w-100 mb-3";let h=document.createElement("i");h.className="mdi mdi-map-search-outline",p.appendChild(h),p.appendChild(document.createTextNode(" Plan Route From Here")),t.appendChild(p)}let g=document.createElement("div");t.appendChild(g),Ln(e,g)}function En(){let e=document.getElementById("pw-feature-list");if(!e)return;let t=document.createElement("div");t.className="pw-server-search-status",t.id="pw-server-searching";let n=document.createElement("i");n.className="mdi mdi-magnify mdi-spin",t.appendChild(n),t.appendChild(document.createTextNode(" Searching all features\u2026")),e.appendChild(t)}function wt(){let e=document.getElementById("pw-server-searching");e&&e.remove();let t=document.getElementById("pw-server-results-header");t&&t.remove();let n=document.querySelectorAll(".pw-server-result");for(let r=0;r200&&t.slice(0,100).forEach(function(n){delete z[n]}),R){let n=D(R),r=null;for(let o=0;o0){let n=Ce;for(let r=0;r0){let n=ve;ve="";for(let r=0;rn&&(r=t.x-200),o<0&&(o=t.y+20),G.style.left=r+"px",G.style.top=o+"px"}function On(e){Fe=e,G=document.createElement("div"),G.className="pw-popover",G.style.display="none",e.getContainer().appendChild(G)}function An(e,t,n){var i,a;if(!G)return;G.textContent="";let r=document.createElement("span");r.className="pw-popover-name",n&&n.length>0?r.textContent=String((a=(i=t[n[0]])!=null?i:t.name)!=null?a:`#${t.id}`):r.textContent=t.name||"Unnamed",G.appendChild(r);let o="";if(n&&n.length>1)o=n.slice(1).map(s=>{var l;return String((l=t[s])!=null?l:"")}).filter(Boolean).join(" / ");else{let s=t.structure_type||t.pathway_type||"";o=s?St(s):""}if(o){let s=document.createElement("span");s.className="pw-popover-type",s.textContent=o,G.appendChild(s)}Dn(e),G.style.display=""}function Bn(){G&&(G.style.display="none")}function Gn(e){St=e.titleCase}var q={init:On,show:An,hide:Bn,setDeps:Gn};var de={pole:"#2e7d32",manhole:"#1565c0",handhole:"#00838f",cabinet:"#e65100",vault:"#6a1b9a",pedestal:"#f9a825",building_entrance:"#c62828",splice_closure:"#795548",tower:"#b71c1c",roof:"#616161",equipment_room:"#00796b",telecom_closet:"#283593",riser_room:"#ad1457"},pe={pole:'',manhole:'',handhole:'',cabinet:'',vault:'',pedestal:'',building_entrance:'',splice_closure:'',tower:'',roof:'',equipment_room:'',telecom_closet:'',riser_room:''},Ie={conduit:"#f57c00",conduit_bank:"#ad1457",aerial:"#1565c0",direct_buried:"#616161",innerduct:"#e65100",microduct:"#6a1b9a",tray:"#2e7d32",raceway:"#00838f",submarine:"#1a237e"},_t={conduit:"5,5",conduit_bank:"",aerial:"10,5",direct_buried:"2,4",innerduct:"8,3",microduct:"1,3",tray:"",raceway:"12,4",submarine:"6,2,2,2"};function Tt(e,t=20){let n=de[e]||"#616161",r=pe[e]||'',o=r.includes('fill="none"'),i=t/2;return L.divIcon({className:"pw-marker",html:''+r+"",iconSize:[t,t],iconAnchor:[i,i],popupAnchor:[0,-(i+2)]})}function kt(e){let t,n;return e<10?(t="pw-cluster-small",n=34):e<100?(t="pw-cluster-medium",n=40):(t="pw-cluster-large",n=46),L.divIcon({className:"pw-server-cluster",html:'
'+e+"
",iconSize:[n,n],iconAnchor:[n/2,n/2]})}function Me(e){let t=document.createElement("span");return t.textContent=e,t.innerHTML}function ne(e){return(e||"").replace(/_/g," ").replace(/\b\w/g,function(t){return t.toUpperCase()})}function re(e){let n=("; "+document.cookie).split("; "+e+"=");return n.length===2&&n.pop().split(";").shift()||null}function Ye(e){let t=e.getBounds();return t.getWest()+","+t.getSouth()+","+t.getEast()+","+t.getNorth()}function Ve(e,t){let n;return function(){clearTimeout(n),n=setTimeout(e,t)}}function Ft(e,t,n,r){let i=e*Math.PI/180,a=n*Math.PI/180,s=(n-e)*Math.PI/180,l=(r-t)*Math.PI/180,c=Math.sin(s/2)*Math.sin(s/2)+Math.cos(i)*Math.cos(a)*Math.sin(l/2)*Math.sin(l/2);return 6371e3*2*Math.atan2(Math.sqrt(c),Math.sqrt(1-c))}var qe=window.PATHWAYS_CONFIG||{},Mt=qe.maxNativeZoom||19,Un=[{name:"Street",url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",attribution:"© OpenStreetMap contributors",maxNativeZoom:Mt},{name:"Satellite",url:"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",attribution:"Esri World Imagery",maxNativeZoom:19}];function Hn(){let e=(qe.baseLayers||[]).filter(function(r){return!!r.url}),t=e.length?e:Un,n={};return t.forEach(function(r){n[r.name]=L.tileLayer(r.url,{attribution:r.attribution||"",maxNativeZoom:r.maxNativeZoom||Mt,maxZoom:22,tileSize:r.tileSize||256,zoomOffset:r.zoomOffset||0})}),n}function Jn(){let e=qe.overlays||[],t={};return e.forEach(function(n){let r;n.type==="wms"?r=L.tileLayer.wms(n.url,{layers:n.layers||"",format:n.format||"image/png",transparent:n.transparent!==!1,attribution:n.attribution||"",maxZoom:22}):r=L.tileLayer(n.url,{attribution:n.attribution||"",maxZoom:n.maxZoom||22,maxNativeZoom:n.maxNativeZoom||void 0}),t[n.name]=r}),t}function Nt(e){let t=L.DomUtil.create("div","pathways-zoom-hint");return t.style.cssText="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:800;padding:12px 24px;border-radius:8px;font-size:14px;pointer-events:none;text-align:center;background:rgba(0,0,0,0.7);color:#fff;",t.textContent="Zoom in to see infrastructure data",e.getContainer().appendChild(t),t}function It(e,t){e.innerHTML=t}function Pt(e){let t=L.Control.extend({options:{position:"bottomleft"},onAdd:function(){let n=L.DomUtil.create("div","pw-legend leaflet-bar");L.DomEvent.disableClickPropagation(n),L.DomEvent.disableScrollPropagation(n);let r=L.DomUtil.create("div","pw-legend-header collapsed",n),o=document.createElement("i");o.className="mdi mdi-chevron-down",r.appendChild(o);let i=document.createElement("span");i.textContent="Legend",r.appendChild(i);let a=L.DomUtil.create("div","pw-legend-body collapsed",n),s=L.DomUtil.create("div","pw-legend-section",a),l=L.DomUtil.create("div","pw-legend-section-title",s);l.textContent="Structures";let c=["pole","manhole","handhole","cabinet","vault","pedestal","building_entrance","splice_closure","tower","equipment_room","telecom_closet","riser_room"];for(let d=0;d',p=g.includes('fill="none"'),h=L.DomUtil.create("div","pw-legend-item",s),v=L.DomUtil.create("span","pw-legend-swatch",h);It(v,''+g+"");let E=L.DomUtil.create("span","pw-legend-label",h);E.textContent=ne(y)}let u=L.DomUtil.create("div","pw-legend-section",a),m=L.DomUtil.create("div","pw-legend-section-title",u);m.textContent="Pathways";let C=["conduit","conduit_bank","aerial","direct_buried","innerduct","microduct","tray","raceway","submarine"];for(let d=0;d");let v=L.DomUtil.create("span","pw-legend-label",p);v.textContent=ne(y)}return r.addEventListener("click",function(){let d=a.classList.toggle("collapsed");r.classList.toggle("collapsed",d)}),n}});new t().addTo(e)}function Rt(e){let t=L.Control.extend({options:{position:"bottomleft"},onAdd:function(){let n=L.DomUtil.create("div","pw-stats-overlay"),r=L.DomUtil.create("span","",n);r.id="structure-count",r.textContent="0",n.appendChild(document.createTextNode(" structures \xB7 "));let o=L.DomUtil.create("span","",n);o.id="pathway-count",o.textContent="0",n.appendChild(document.createTextNode(" pathways \xB7 "));let i=L.DomUtil.create("span","",n);return i.id="total-length",i.textContent="0",n.appendChild(document.createTextNode(" km")),n}});new t().addTo(e)}function Dt(e){let t=L.Control.extend({options:{position:"topleft"},onAdd:function(){let n=L.DomUtil.create("div","leaflet-control-zoom leaflet-bar"),r=L.DomUtil.create("a","",n);return r.href="#",r.title="Go to my location",L.DomUtil.create("i","mdi mdi-crosshairs-gps",r),r.style.display="flex",r.style.alignItems="center",r.style.justifyContent="center",r.style.fontSize="18px",L.DomEvent.disableClickPropagation(n),L.DomEvent.on(r,"click",function(o){L.DomEvent.preventDefault(o),navigator.geolocation&&navigator.geolocation.getCurrentPosition(function(i){e.flyTo([i.coords.latitude,i.coords.longitude],17,{duration:1})},function(){},{enableHighAccuracy:!0,timeout:1e4})}),n}});return new t}function Ot(e,t){let n=L.Control.extend({options:{position:"topleft"},onAdd:function(){let r=L.DomUtil.create("div","leaflet-control-zoom leaflet-bar"),o=L.DomUtil.create("a","",r);return o.href="#",o.title=t?"Exit kiosk mode":"Kiosk mode",L.DomUtil.create("i","mdi "+(t?"mdi-fullscreen-exit":"mdi-fullscreen"),o),o.style.display="flex",o.style.alignItems="center",o.style.justifyContent="center",o.style.fontSize="18px",L.DomEvent.disableClickPropagation(r),L.DomEvent.on(o,"click",function(i){L.DomEvent.preventDefault(i);let a=e.getCenter(),s=e.getZoom(),l=new URLSearchParams;l.set("lat",a.lat.toFixed(6)),l.set("lon",a.lng.toFixed(6)),l.set("zoom",String(s)),t||l.set("kiosk","true"),window.location.search=l.toString()}),r}});return new n}function At(e,t,n){let r=L.Control.extend({options:{position:"topright"},onAdd:function(){let o=L.DomUtil.create("div","leaflet-control-zoom leaflet-bar pw-sidebar-toggle-ctrl"),i=L.DomUtil.create("a","",o);i.href="#",i.title="Show sidebar",L.DomUtil.create("i","mdi mdi-chevron-left",i),i.style.display="flex",i.style.alignItems="center",i.style.justifyContent="center",i.style.fontSize="18px",L.DomEvent.disableClickPropagation(o),L.DomEvent.on(i,"click",function(l){L.DomEvent.preventDefault(l),n()});let a=new MutationObserver(function(){let l=document.getElementById("pw-sidebar");if(!l)return;let c=t?l.classList.contains("pw-sidebar-open"):!l.classList.contains("pw-sidebar-hidden");o.style.display=c?"none":""}),s=document.getElementById("pw-sidebar");if(s){a.observe(s,{attributes:!0,attributeFilter:["class"]});let l=t?s.classList.contains("pw-sidebar-open"):!s.classList.contains("pw-sidebar-hidden");o.style.display=l?"none":""}return o}});return new r}function Bt(e,t){let n=Hn(),r=n[Object.keys(n)[0]],o=L.map(e,{layers:[r]});if(t.bounds){let l=t.select?20:17;o.fitBounds(t.bounds,{padding:[50,50],maxZoom:l})}else o.setView(t.center||[0,0],t.zoom||2);let i={},a=Jn();for(let l in a)i[l]=a[l];let s=L.control.layers(n,i,{position:"bottomright",collapsed:!0}).addTo(o);return{map:o,baseLayers:n,layerControl:s}}var Kn=window.PATHWAYS_CONFIG||{},ye=Kn.apiBase||"/api/plugins/pathways/geo/";var ge=11;function Ut(e,t){var u;let n=e.counts.structures,r=e.thresholds.structures,o="off";n>r.hide?o="server":r.cluster!=null&&n>r.cluster&&(o="client");let i={},a=o!=="off";t.has("structures")&&(i.structures="render");let s=["conduit_banks","conduits","aerial_spans","direct_buried","circuits"];for(let m of s){if(!t.has(m))continue;if(a){i[m]="hide";continue}let C=(u=e.counts[m])!=null?u:0,d=e.thresholds[m];i[m]=d&&C>d.hide?"hide":"render"}let l=e.counts.external||{},c=e.thresholds.external||{};for(let m of Object.keys(l)){let C=`external:${m}`;if(!t.has(C))continue;if(a){i[C]="hide";continue}let d=c[m];i[C]=d&&l[m]>d.hide?"hide":"render"}return{clusterMode:o,layers:i}}var $=null,$e="",Xe=null;function Ht(e,t){return K(this,null,function*(){$&&$.abort();let n=new AbortController;$=n;let r=ye+"info/?bbox="+e,o={Accept:"application/json"},i=re("csrftoken");i&&(o["X-CSRFToken"]=i),$e&&(o["If-None-Match"]=$e);try{let a=yield fetch(r,{headers:o,signal:n.signal});if($===n&&($=null),a.status===304&&Xe){t(Xe);return}if(a.ok){let s=yield a.json();$e=a.headers.get("ETag")||"",Xe=s,t(s)}}catch(a){$===n&&($=null)}})}var fe={};function Jt(e,t,n,r,o){return K(this,null,function*(){fe[e]&&fe[e].abort();let i=ye+e+"?format=json&bbox="+t;if(r)for(let c in r)i+="&"+c+"="+encodeURIComponent(String(r[c]));let a=new AbortController;fe[e]=a;let s={Accept:"application/json"},l=re("csrftoken");l&&(s["X-CSRFToken"]=l),o&&(s["If-None-Match"]=o);try{let c=yield fetch(i,{headers:s,signal:a.signal});fe[e]=void 0;let u=c.headers.get("ETag")||"";if(c.status===304)n({data:null,etag:u});else if(c.ok){let m=yield c.json();n({data:m,etag:u})}}catch(c){fe[e]=void 0}})}var me=null;function Kt(e,t){return K(this,null,function*(){me&&me.abort();let n=new AbortController;me=n;let r={Accept:"application/json"},o=re("csrftoken");o&&(r["X-CSRFToken"]=o);let i=[["structures/","structure","structure_type"],["conduit-banks/","conduit_bank","pathway_type"],["conduits/","conduit","pathway_type"],["aerial-spans/","aerial","pathway_type"],["direct-buried/","direct_buried","pathway_type"],["circuits/","circuit","pathway_type"]],a=[],s=i.map(function(l){let[c,u,m]=l,C=ye+c+"?format=json&q="+encodeURIComponent(e);return fetch(C,{headers:r,signal:n.signal}).then(function(d){return d.ok?d.json():null}).then(function(d){!d||!d.features||d.features.forEach(function(y){if(!y.geometry)return;let f=y.properties||{},g;if(y.geometry.type==="Point"){let p=y.geometry.coordinates;g=L.latLng(p[1],p[0])}else if(y.geometry.type==="LineString"){let p=y.geometry.coordinates,h=p[Math.floor(p.length/2)];g=L.latLng(h[1],h[0])}else return;a.push({name:f.name||f.cid||"Unnamed",featureType:u,typeKey:f[m]||u,latlng:g,url:f.url})})}).catch(function(){})});try{yield Promise.all(s),me=null,t(a)}catch(l){me=null}})}function zn(e,t,n){n.getZoom()<20||e.eachLayer(function(r){var v;let o=r,i=o.getLatLngs();if(!i||i.length<2)return;let a=o.feature,s=(v=a==null?void 0:a.properties)==null?void 0:v.name;if(!s)return;let l=Math.floor(i.length/2),c=i[l-1]||i[0],u=i[l],m=(c.lat+u.lat)/2,C=(c.lng+u.lng)/2,d=n.latLngToContainerPoint(c),y=n.latLngToContainerPoint(u),f=y.x-d.x,g=y.y-d.y,p=Math.atan2(g,f)*180/Math.PI;p>90&&(p-=180),p<-90&&(p+=180);let h=L.divIcon({className:"pw-line-label",html:'
'+Me(s)+"
",iconSize:[0,0],iconAnchor:[0,0]});t.addLayer(L.marker([m,C],{icon:h,interactive:!1}))})}function zt(){return{structures:L.layerGroup(),conduitBanks:L.layerGroup(),conduits:L.layerGroup(),aerialSpans:L.layerGroup(),directBuried:L.layerGroup(),circuits:L.layerGroup()}}var Ne=[{endpoint:"conduit-banks/",layerKey:"conduitBanks",featureType:"conduit_bank",style:{color:"#ad1457",weight:5,opacity:.8,dashArray:""},infoKey:"conduit_banks"},{endpoint:"conduits/",layerKey:"conduits",featureType:"conduit",style:{color:"#f57c00",weight:3,opacity:.7,dashArray:"5 5"},infoKey:"conduits"},{endpoint:"aerial-spans/",layerKey:"aerialSpans",featureType:"aerial",style:{color:"#1565c0",weight:3,opacity:.7,dashArray:"10 5"},infoKey:"aerial_spans"},{endpoint:"direct-buried/",layerKey:"directBuried",featureType:"direct_buried",style:{color:"#616161",weight:3,opacity:.7,dashArray:"2 4"},infoKey:"direct_buried"},{endpoint:"circuits/",layerKey:"circuits",featureType:"circuit",style:{color:"#d32f2f",weight:3,opacity:.8,dashArray:"8 6"},infoKey:"circuits"}],Zn=12,De=.5,Re={};function Zt(e,t,n,r,o,i,a){let s=Re[e];if(!s)return null;for(let l=s.length-1;l>=0;l--){let c=s[l];if(c.zoom===t&&c.extraKey===n&&r>=c.west&&o>=c.south&&i<=c.east&&a<=c.north)return c}return null}function jt(e,t,n,r,o,i,a,s,l){Re[e]||(Re[e]=[]);let c=Re[e];c.push({west:t,south:n,east:r,north:o,zoom:i,extraKey:a,etag:s,data:l}),c.length>Zn&&c.shift()}function Gt(e,t,n,r){let o=e.getBounds(),i=o.getWest(),a=o.getSouth(),s=o.getEast(),l=o.getNorth(),c=e.getZoom(),u=r?JSON.stringify(r):"",m=Zt(t,c,u,i,a,s,l);if(m){n(m.data);return}let C=(s-i)*De,d=(l-a)*De,y=i-C,f=a-d,g=s+C,p=l+d,h=y+","+f+","+g+","+p;Jt(t,h,function(v){v.data&&(jt(t,y,f,g,p,c,u,v.etag,v.data),n(v.data))},r)}var Pe=null,jn=14;function Wn(e,t){Pe&&clearTimeout(Pe);let n=e.getZoom();if(n=s.length||e.getZoom()!==n)return;let u=s[l++];Jt(u.endpoint,u.bbox,function(m){m.data&&jt(u.endpoint,u.fw,u.fs,u.fe,u.fn,u.zoom,u.extraKey,m.etag,m.data),Pe=setTimeout(c,50)},u.extraParams)}Pe=setTimeout(c,200)}function Wt(e,t,n,r,o){let i=e.getZoom();if(i0&&((E=h.features[0].properties)==null?void 0:E.cluster)){let _=0;h.features.forEach(function(T){let F=T.properties.point_count||0;_+=F;let I=T.geometry,X=L.latLng(I.coordinates[1],I.coordinates[0]),oe=L.marker(X,{icon:kt(F)});oe.on("click",function(){let Oe=Math.min(e.getZoom()+3,e.getMaxZoom());e.setView(X,Oe)}),t.structures.addLayer(oe)}),o.onStructuresLoaded&&o.onStructuresLoaded(_)}else{let _=L.geoJSON(h,{pointToLayer:function(T,k){return L.marker(k,{icon:Tt(T.properties.structure_type||"")})},onEachFeature:function(T,k){T.id!=null&&T.properties.id==null&&(T.properties.id=T.id);let F={props:T.properties,featureType:"structure",layer:k,latlng:k.getLatLng()};s.push(F),o.onFeatureCreated&&o.onFeatureCreated(F),k.on("click",function(I){o.onFeatureClick&&o.onFeatureClick(F,I)}),k.on("mouseover",function(I){o.onFeatureMouseOver&&o.onFeatureMouseOver(F,I,T)}),k.on("mouseout",function(){o.onFeatureMouseOut&&o.onFeatureMouseOut()})}});p?(p.addLayers(_.getLayers()),t.structures.addLayer(p)):_.addTo(t.structures),o.onStructuresLoaded&&o.onStructuresLoaded(h.features?h.features.length:0)}C()},{zoom:i})}m===0&&o.onPathwayLoaded;function y(f,g){return{style:function(){return g},onEachFeature:function(p,h){let v=p.properties;p.id!=null&&v.id==null&&(v.id=p.id),!v.name&&v.label&&(v.name=v.label);let E={props:v,featureType:f,layer:h,latlng:h.getBounds().getCenter()};s.push(E),o.onFeatureCreated&&o.onFeatureCreated(E),h.on("click",function(_){o.onFeatureClick&&o.onFeatureClick(E,_)}),h.on("mouseover",function(_){o.onFeatureMouseOver&&o.onFeatureMouseOver(E,_,p)}),h.on("mouseout",function(){o.onFeatureMouseOut&&o.onFeatureMouseOut()})}}}Ne.forEach(function(f){let g=t[f.layerKey];a.layers[f.infoKey]==="render"&&e.hasLayer(g)&&Gt(e,f.endpoint,function(p){g.clearLayers();let h=L.geoJSON(p,y(f.featureType,f.style));h.addTo(g),zn(h,g,e),d(p),C()})}),c===0&&o.onAllLoaded&&o.onAllLoaded([])}function Yt(e){let t=0;return e.features&&e.features.forEach(function(n){if(n.geometry&&n.geometry.type==="LineString"){let r=n.geometry.coordinates;for(let o=0;o',"Conduit Banks":'',Conduits:'',"Aerial Spans":'',"Direct Buried":'',"Circuit Routes":''};function k(){let b=document.getElementById("pw-layer-toggles");if(b){b.textContent="";for(let x in g){let w=document.createElement("button");w.type="button";let S=r.hasLayer(g[x]);w.className="pw-layer-toggle"+(S?" pw-layer-active":"");let M=T[x]||"",A=document.createElement("span");A.textContent=x;let P=document.createElement("span");for(P.innerHTML=M;P.firstChild;)w.appendChild(P.firstChild);w.appendChild(A);let O=document.createElement("input");O.type="checkbox",O.checked=S,O.style.display="none",E[x]=O,w.appendChild(O),(function(B,he,Be){Be.addEventListener("click",function(){he.checked=!he.checked,he.checked?(r.addLayer(g[B]),Be.classList.add("pw-layer-active")):(r.removeLayer(g[B]),Be.classList.remove("pw-layer-active"));let nt=C()||m;nt[B]=he.checked,d(nt),Ae()})})(x,O,w),b.appendChild(w)}}}k();let F=0,I=0;function X(){let b=r.getCenter(),x=r.getZoom(),w=new URLSearchParams;w.set("lat",b.lat.toFixed(6)),w.set("lon",b.lng.toFixed(6)),w.set("zoom",String(x));let S=N.getSelectedId();S&&w.set("select",S),t.kiosk&&w.set("kiosk","true");let M=window.location.pathname+"?"+w.toString();history.replaceState(null,"",M)}function oe(b,x){if(!E[b])return;let w=E[b].closest(".pw-layer-toggle");if(!w)return;let S=w.querySelector(".pw-layer-count-chip");if(x==null){S&&S.remove();return}S||(S=document.createElement("span"),S.className="pw-layer-count-chip",w.appendChild(S)),S.textContent=x.toLocaleString()}function Oe(b,x){var w;for(let S in v){if(!E[S])continue;let M=E[S].closest(".pw-layer-toggle");if(!M)continue;let A=v[S],O=E[S].checked&&b.layers[A]==="hide";if(M.classList.toggle("pw-layer-unavailable",O),O){let B;A.startsWith("external:")?B=(w=x.counts.external)==null?void 0:w[A.slice(9)]:B=x.counts[A],oe(S,B!=null?B:0)}else oe(S,null)}}function Qe(b,x){let w=r.getZoom();b&&x&&Oe(b,x),F=0,I=0,Wt(r,f,b,c,{onFeatureClick:function(S,M){M.originalEvent&&(M.originalEvent._sidebarClick=!0),N.selectFeature(S),X()},onFeatureMouseOver:function(S,M,A){q.show(M.latlng||S.layer.getLatLng(),A.properties)},onFeatureMouseOut:function(){q.hide()},onFeatureCreated:function(S){N.onFeatureCreated(S)},onStructuresLoaded:function(S){a&&(a.textContent=String(S))},onPathwayLoaded:function(S){let M=S.features?S.features.length:0;F+=M,I+=Yt(S),s&&(s.textContent=String(F)),l&&(l.textContent=(I/1e3).toFixed(2))},onAllLoaded:function(S){if(w0){let A=Ye(r);ct(A,w,M,function(P,O){N.onFeatureCreated(P),P.layer.on("click",function(B){B.originalEvent&&(B.originalEvent._sidebarClick=!0),N.selectFeature(P)}),P.layer.on("mouseover",function(B){q.show(B.latlng,P.props,O.popoverFields)}),P.layer.on("mouseout",function(){q.hide()})}).then(function(P){for(let O=0;O{var on=Object.defineProperty,sn=Object.defineProperties;var an=Object.getOwnPropertyDescriptors;var st=Object.getOwnPropertySymbols;var cn=Object.prototype.hasOwnProperty,ln=Object.prototype.propertyIsEnumerable;var at=(e,t,n)=>t in e?on(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,ct=(e,t)=>{for(var n in t||(t={}))cn.call(t,n)&&at(e,n,t[n]);if(st)for(var n of st(t))ln.call(t,n)&&at(e,n,t[n]);return e},lt=(e,t)=>sn(e,an(t));var K=(e,t,n)=>new Promise((r,o)=>{var i=c=>{try{s(n.next(c))}catch(l){o(l)}},a=c=>{try{s(n.throw(c))}catch(l){o(l)}},s=c=>c.done?r(c.value):Promise.resolve(c.value).then(i,a);s((n=n.apply(e,t)).next())});var ut=["structure","conduit_bank","conduit","aerial","direct_buried","circuit"];var ve=new Map;function un(e,t){var n,r;if(t.colorField&&t.colorMap){let o=String((n=e[t.colorField])!=null?n:"");return(r=t.colorMap[o])!=null?r:t.defaultColor}return t.color}function dn(e,t){return L.circleMarker(e,{radius:7,fillColor:t,color:"#fff",weight:2,opacity:1,fillOpacity:.85})}function pn(e,t,n){var r;return L.polyline(e,{color:t,weight:n.weight,opacity:n.opacity,dashArray:(r=n.dash)!=null?r:void 0})}function fn(e,t,n){var r;return L.polygon(e,{color:t,fillColor:t,fillOpacity:.2,weight:n.weight,opacity:n.opacity,dashArray:(r=n.dash)!=null?r:void 0})}function dt(e,t){ve.clear();let n=new Map,r=[...e].sort((o,i)=>o.sortOrder-i.sortOrder);for(let o of r){let i=L.layerGroup();ve.set(o.name,{config:o,layerGroup:i,abortController:null}),n.set(o.name,i),o.defaultVisible&&i.addTo(t)}return n}function pt(e,t,n,r){return K(this,null,function*(){let o=[],i=[];for(let[a,s]of ve){if(!n.has(a)||ts.config.maxZoom)continue;s.abortController&&s.abortController.abort(),s.abortController=new AbortController;let c=mn(s,e,t,o,r);i.push(c)}return yield Promise.all(i.map(a=>a.catch(()=>{}))),o})}function mn(e,t,n,r,o){return K(this,null,function*(){var C,d;let{config:i,layerGroup:a,abortController:s}=e,c=i.url.includes("?")?"&":"?",l=`${i.url}${c}format=json&bbox=${t}&zoom=${n}`,u=document.cookie.match(/csrftoken=([^;]+)/),m={Accept:"application/json"};u&&(m["X-CSRFToken"]=u[1]);try{let y=yield fetch(l,{headers:m,signal:s==null?void 0:s.signal});if(!y.ok){console.warn(`External layer '${i.name}' fetch failed: ${y.status}`);return}let f=yield y.json();a.clearLayers();for(let g of f.features){if(!g.geometry)continue;let p=(C=g.properties)!=null?C:{};g.id!=null&&p.id==null&&(p.id=g.id);let h=un(p,i.style),v=null,E;if(g.geometry.type==="Point"){let[_,T]=g.geometry.coordinates;E=L.latLng(T,_),v=dn(E,h)}else if(g.geometry.type==="LineString"){let _=g.geometry.coordinates.map(k=>L.latLng(k[1],k[0])),T=pn(_,h,i.style);E=T.getBounds().getCenter(),v=T}else if(g.geometry.type==="Polygon"){let _=g.geometry.coordinates.map(k=>k.map(F=>L.latLng(F[1],F[0]))),T=fn(_,h,i.style);E=T.getBounds().getCenter(),v=T}if(v&&E){a.addLayer(v);let _={props:lt(ct({},p),{name:(d=p.name)!=null?d:`${i.label} #${p.id}`}),featureType:i.name,layer:v,latlng:E};r.push(_),o(_,i)}}}catch(y){if(y instanceof DOMException&&y.name==="AbortError")return;console.warn(`External layer '${i.name}' error:`,y)}})}function Z(e){var t;return(t=ve.get(e))==null?void 0:t.config}var J,gn,ht,pe,ke,Lt,Ie,Ve,U=null,H=[],ae=[],D=null,W={},z={},ce=null,V=null,ze=null,we="",Ee=null,xe="",j=!1,Y=null,ue=[],_e=null,ft=.15;function bt(e){return ut.indexOf(e)!==-1}function Ct(e){let t=Z(e);return t?t.label:J(e.replace(/_/g," "))}function qe(e){return e.featureType==="structure"?ke[e.props.structure_type||""]||"#616161":e.featureType==="circuit"?"#d32f2f":Ie[e.props.pathway_type||""]||"#616161"}function Fe(e){return e.featureType==="structure"?e.props.structure_type||"unknown":e.props.pathway_type||"unknown"}function O(e){return e.featureType+"-"+(e.props.id||"")}function yn(e,t){let n=parseInt(e.replace("#",""),16),r=Math.round((n>>16&255)+(255-(n>>16&255))*t),o=Math.round((n>>8&255)+(255-(n>>8&255))*t),i=Math.round((n&255)+(255-(n&255))*t);return"#"+(1<<24|r<<16|o<<8|i).toString(16).slice(1)}function Ze(){if(V&&(V.remove(),V=null),ce){let e=ce;e._origIcon&&(e.setIcon(e._origIcon),delete e._origIcon),e._origStyle&&typeof e.setStyle=="function"&&(e.setStyle(e._origStyle),delete e._origStyle),ce=null}}function vt(e){let t=e.layer;if(t)if(ce=t,e.featureType==="structure"){let n=t;n._origIcon=n.getIcon();let r=e.props.structure_type||"",o=ke[r]||"#616161",i=Lt[r]||'',a=i.includes('fill="none"');n.setIcon(L.divIcon({className:"pw-marker pw-marker-selected",html:''+i+"",iconSize:[26,26],iconAnchor:[13,13],popupAnchor:[0,-14]}))}else{let n=t,r=n.options||{};n._origStyle={weight:r.weight||3,opacity:r.opacity||.7,color:r.color,dashArray:r.dashArray};let o=n.getLatLngs();o&&o.length>0&&U&&(V=L.polyline(o,{color:yn(r.color||"#888",.55),weight:12,opacity:.5,interactive:!1}).addTo(U)),n.setStyle({weight:6,opacity:1,dashArray:""})}}function hn(e){Ze(),vt(e)}function Ln(e){V&&(V.remove(),V=null),ce=null,vt(e)}function wt(e){ue=[],_e=e,Et()}function Et(){_e&&(ue=[],H.forEach(function(e){let t=O(e);_e.has(t)||D&&t===O(D)||e.layer&&(ue.push(e),e.featureType==="structure"?e.layer.setOpacity(ft):typeof e.layer.setStyle=="function"&&e.layer.setStyle({opacity:ft}))}))}function Te(){_e=null,ue.forEach(function(e){if(e.layer){if(e.featureType==="structure")e.layer.setOpacity(1);else if(typeof e.layer.setStyle=="function"){let t=e.layer._origStyle;e.layer.setStyle({opacity:t?t.opacity:.7})}}}),ue=[]}function je(e){let t=document.getElementById("pw-feature-list");if(!t)return;let n=t.querySelectorAll(".pw-list-item"),r=e?O(e):null;for(let o=0;o0?"":"none"),ae.forEach(function(n){let r=document.createElement("div");r.className="pw-list-item",r.setAttribute("data-feature-id",O(n)),D&&O(D)===O(n)&&r.classList.add("active");let o=document.createElement("span");o.className="pw-list-dot",o.style.background=qe(n),r.appendChild(o);let i=document.createElement("span");i.className="pw-list-label",i.textContent=n.props.name||"Unnamed",i.title=n.props.name||"Unnamed",r.appendChild(i);let a=document.createElement("span");a.className="pw-list-type",a.textContent=J(Fe(n)),r.appendChild(a),r.addEventListener("click",function(){te(n)}),e.appendChild(r)}))}function mt(){let e=document.getElementById("pw-type-filters");if(!e)return;e.textContent="";let t={};H.forEach(function(r){let o=Fe(r);t[o]||(t[o]=qe(r))});let n=Object.keys(t).sort();n.length<=1||(n.forEach(function(r){W[r]===void 0&&(W[r]=!0)}),n.forEach(function(r){let o=document.createElement("button");o.className="pw-filter-btn"+(W[r]?" active":""),o.type="button";let i=document.createElement("span");i.className="pw-filter-dot",i.style.background=t[r],o.appendChild(i);let a=document.createTextNode(Ct(r));o.appendChild(a),o.addEventListener("click",function(){W[r]=!W[r],o.classList.toggle("active",W[r]),ee()}),e.appendChild(o)}))}function ee(){let e=document.getElementById("pw-search"),t=(e?e.value:"").toLowerCase().trim();ae=H.filter(function(n){let r=Fe(n);if(W[r]===!1)return!1;if(t){let o=(n.props.name||"").toLowerCase(),i=J(r).toLowerCase();if(o.indexOf(t)===-1&&i.indexOf(t)===-1)return!1}return!0}),bn(),ae.length===0&&t.length>=2&&ze?t!==we&&(we=t,In(),ze(t)):(we="",_t())}function Cn(e){let t=e.props.id;if(!bt(e.featureType)||t==null)return"";let n="/plugins/pathways/";switch(e.featureType){case"structure":return n+"structures/"+t+"/";case"conduit_bank":return n+"conduit-banks/"+t+"/";case"conduit":return n+"conduits/"+t+"/";case"aerial":return n+"aerial-spans/"+t+"/";case"direct_buried":return n+"direct-buried/"+t+"/";case"circuit":return n+"circuit-geometries/"+t+"/";default:return n+"pathways/"+t+"/"}}function xt(e){var r;if(e.props.url)return"/api"+e.props.url;let t=e.props.id;if(bt(e.featureType)){let o=Ve.replace(/geo\/?$/,"");switch(e.featureType){case"structure":return o+"structures/"+t+"/";case"conduit_bank":return o+"conduit-banks/"+t+"/";case"conduit":return o+"conduits/"+t+"/";case"aerial":return o+"aerial-spans/"+t+"/";case"direct_buried":return o+"direct-buried/"+t+"/";case"circuit":return o+"circuit-geometries/"+t+"/";default:return o+"pathways/"+t+"/"}}let n=Z(e.featureType);return(r=n==null?void 0:n.detail)!=null&&r.urlTemplate?n.detail.urlTemplate.replace("{id}",String(t)):""}function $e(e){if(e==null||e==="")return null;if(Array.isArray(e)){if(e.length===0)return null;let t=[];for(let n=0;n0?{text:t.join(", ")}:null}if(typeof e=="object"&&e!==null&&"label"in e){let t=e;return{text:t.label||J(t.value||"")}}if(typeof e=="object"&&e!==null){let t=e;if(t.display||t.name||t.id!==void 0)return{text:t.display||t.name||String(t.id),url:t.display_url||t.url||null}}return e===!0?{text:"Yes"}:e===!1?{text:"No"}:{text:String(e)}}function We(e,t,n,r){let o=$e(n);if(!o)return;let i=o.text+(r||""),a=document.createElement("tr"),s=document.createElement("td");s.textContent=t;let c=document.createElement("td");if(o.url){let l=document.createElement("a");l.href=o.url,l.textContent=i,c.appendChild(l)}else c.textContent=i;a.appendChild(s),a.appendChild(c),e.appendChild(a)}function vn(e,t){if(!t||!t.length)return;let n=document.createElement("tr"),r=document.createElement("td");r.textContent="Tags";let o=document.createElement("td");t.forEach(function(i){let a=document.createElement("span");a.className="badge",a.style.cssText="margin-right:4px;margin-bottom:2px;",i.color?(a.style.background="#"+i.color,a.style.color="#fff"):a.style.background="var(--tblr-border-color-translucent, rgba(0,0,0,0.1))",a.textContent=i.display||i.name||String(i),o.appendChild(a)}),n.appendChild(r),n.appendChild(o),e.appendChild(n)}var gt={structure:[["Type","structure_type"],["Site","site"],["Elevation","elevation"," m"],["Height","height"," m"],["Width","width"," m"],["Length","length"," m"],["Depth","depth"," m"],["Tenant","tenant"],["Installation Date","installation_date"],["Access Notes","access_notes"],["Comments","comments"]],conduit_bank:[["Start Structure","start_structure"],["End Structure","end_structure"],["Start Face","start_face"],["End Face","end_face"],["Configuration","configuration"],["Total Conduits","total_conduits"],["Encasement","encasement_type"],["Length","length"," m"],["Tenant","tenant"],["Installation Date","installation_date"],["Comments","comments"]],conduit:[["Start Structure","start_structure"],["End Structure","end_structure"],["Start Location","start_location"],["End Location","end_location"],["Material","material"],["Inner Diameter","inner_diameter"," mm"],["Outer Diameter","outer_diameter"," mm"],["Depth","depth"," m"],["Length","length"," m"],["Conduit Bank","conduit_bank"],["Bank Position","bank_position"],["Start Junction","start_junction"],["End Junction","end_junction"],["Cables Routed","cables_routed"],["Tenant","tenant"],["Installation Date","installation_date"],["Comments","comments"]],aerial:[["Start Structure","start_structure"],["End Structure","end_structure"],["Start Location","start_location"],["End Location","end_location"],["Aerial Type","aerial_type"],["Start Attachment Height","start_attachment_height"," m"],["End Attachment Height","end_attachment_height"," m"],["Attachment Height (mean)","attachment_height"," m"],["Sag","sag"," m"],["Messenger Size","messenger_size"],["Wind Loading","wind_loading"],["Ice Loading","ice_loading"],["Length","length"," m"],["Cables Routed","cables_routed"],["Tenant","tenant"],["Installation Date","installation_date"],["Comments","comments"]],direct_buried:[["Start Structure","start_structure"],["End Structure","end_structure"],["Start Location","start_location"],["End Location","end_location"],["Burial Depth","burial_depth"," m"],["Warning Tape","warning_tape"],["Tracer Wire","tracer_wire"],["Armor Type","armor_type"],["Length","length"," m"],["Cables Routed","cables_routed"],["Tenant","tenant"],["Installation Date","installation_date"],["Comments","comments"]],circuit:[["Circuit","circuit"],["Provider Reference","provider_reference"],["Comments","comments"]],default:[["Start Structure","start_structure"],["End Structure","end_structure"],["Start Location","start_location"],["End Location","end_location"],["Length","length"," m"],["Cables Routed","cables_routed"],["Tenant","tenant"],["Installation Date","installation_date"],["Comments","comments"]]};function de(e,t){let n=document.createElement("div");n.className="pw-section-header";let r=document.createElement("i");r.className="mdi mdi-chevron-down",n.appendChild(r);let o=document.createElement("span");o.textContent=e,n.appendChild(o),t.appendChild(n);let i=document.createElement("div");return i.className="pw-section-body",t.appendChild(i),n.addEventListener("click",function(){let a=i.classList.toggle("collapsed");n.classList.toggle("collapsed",a)}),i}function wn(e,t){let n=document.querySelector(".pw-metric-row");if(!n)return;let r=[];t.featureType==="structure"?e.elevation&&r.push(["Elev","elevation"," m"]):e.length&&r.push(["Length","length"," m"]),t.featureType==="conduit"&&e.inner_diameter&&r.push(["ID","inner_diameter"," mm"]),r.forEach(function(o){let i=$e(e[o[1]]);if(!i)return;let a=document.createElement("span");a.className="pw-metric-badge pw-metric-muted",a.textContent=o[0]+" "+i.text+o[2],n.appendChild(a)})}function yt(e,t,n){wn(e,t);let r=Z(t.featureType);if(r!=null&&r.detail){let a=de("Details",n),s=document.createElement("table");s.className="pw-detail-table";for(let c of r.detail.fields){let l=e[c];l!=null&&We(s,J(c.replace(/_/g," ")),l)}a.appendChild(s);return}let o=gt[t.featureType]||gt.default,i=document.createElement("table");if(i.className="pw-detail-table",o.forEach(function(a){let s=e[a[1]];We(i,a[0],s,a[2]||"")}),vn(i,e.tags),i.childNodes.length>0&&de("Details",n).appendChild(i),e.created||e.last_updated){let a=document.createElement("div");a.style.cssText="font-size:0.72em;color:var(--tblr-muted-color,#667382);margin-top:8px;";let s=[];e.created&&s.push("Created "+e.created.split("T")[0]),e.last_updated&&s.push("Updated "+e.last_updated.split("T")[0]),a.textContent=s.join(" \xB7 "),n.appendChild(a)}t.featureType==="structure"&&En(t,n),t.featureType!=="structure"&&xn(e,t,n)}function Ye(e,t,n,r,o,i,a){let s=document.createElement("div");s.className="pw-list-item",s.style.padding="6px 0",s.style.cursor="pointer";let c=document.createElement("span");c.className="pw-list-dot",c.style.background=n,s.appendChild(c);let l=document.createElement("span");l.className="pw-list-label",l.textContent=t||"Unnamed",s.appendChild(l);let u=document.createElement("span");u.className="pw-metric-badge pw-metric-muted",u.style.fontSize="0.65em",u.style.padding="1px 6px",u.textContent=r,s.appendChild(u),s.addEventListener("click",function(){let m=H.find(function(C){return O(C)===i});m?te(m):a&&U?(xe=i,U.flyTo(a,18,{duration:.5})):window.location.href=window.location.pathname+"?select="+encodeURIComponent(i)}),e.appendChild(s)}function En(e,t){let n=e.props.id,o=Ve.replace(/geo\/?$/,"")+"pathways/?structure_id="+n,i={Accept:"application/json"},a=pe("csrftoken");a&&(i["X-CSRFToken"]=a),fetch(o,{headers:i}).then(function(s){return s.ok?s.json():null}).then(function(s){if(!s||!s.results||s.results.length===0)return;let c={},l=[];s.results.forEach(function(d){let y=d.pathway_type||"",f=typeof y=="object"?y.value||"":y,g=typeof y=="object"&&y.label||J(f),p=Ie[f]||"#616161",h=d.display||d.label||"Unnamed",v=d.id,E=H.find(function(M){return M.featureType!=="structure"&&M.props.id===v});l.push({display:h,color:p,typeLabel:g,mapFeature:E,selectId:f+"-"+v,hint:E?E.latlng:void 0});let _,T;if(E&&E.layer)try{let M=E.layer.getLatLngs();M&&M.length>=2&&(_={lat:M[0].lat,lng:M[0].lng},T={lat:M[M.length-1].lat,lng:M[M.length-1].lng})}catch(M){}let k=d.start_structure,F=d.end_structure;k&&k.id&&k.id!==n&&!c[k.id]&&(c[k.id]={id:k.id,display:k.display||String(k.id),hint:_}),F&&F.id&&F.id!==n&&!c[F.id]&&(c[F.id]={id:F.id,display:F.display||String(F.id),hint:T})});let u=Object.values(c);if(u.length>0){let d=de("Connected Structures ("+u.length+")",t);u.forEach(function(y){let f=H.find(function(g){return g.featureType==="structure"&&g.props.id===y.id});Ye(d,y.display,"#2e7d32","Structure",f,"structure-"+y.id,y.hint)})}let m=de("Connected Pathways ("+l.length+")",t);l.forEach(function(d){Ye(m,d.display,d.color,d.typeLabel,d.mapFeature,d.selectId,d.hint)});let C=new Set;u.forEach(function(d){C.add("structure-"+d.id)}),l.forEach(function(d){C.add(d.selectId)}),wt(C)})}function xn(e,t,n){let r=[],o=e.start_structure,i=e.end_structure;if(o&&o.id&&r.push({id:o.id,display:o.display||String(o.id),url:o.url}),i&&i.id&&(!o||i.id!==o.id)&&r.push({id:i.id,display:i.display||String(i.id),url:i.url}),r.length===0)return;let a=de("Connected Structures ("+r.length+")",n),s,c;if(t.layer)try{let u=t.layer.getLatLngs();u&&u.length>=2&&(s={lat:u[0].lat,lng:u[0].lng},c={lat:u[u.length-1].lat,lng:u[u.length-1].lng})}catch(u){}r.forEach(function(u,m){let C=H.find(function(y){return y.featureType==="structure"&&y.props.id===u.id}),d=m===0?s:c;Ye(a,u.display,"#2e7d32","Structure",C,"structure-"+u.id,d)});let l=new Set;r.forEach(function(u){l.add("structure-"+u.id)}),wt(l)}function Sn(e,t){return K(this,null,function*(){let n=O(e),r=Z(e.featureType),o=_n(r,e);if(o){let l="html:"+n;if(z[l]){St(t,z[l]);return}yield Tn(o,l,t);return}if(z[n]){yt(z[n],e,t);return}let i=xt(e);if(!i){let l=document.createElement("table");l.className="pw-detail-table";let u=e.props;for(let m in u)u.hasOwnProperty(m)&&(m==="id"||m==="url"||We(l,J(m.replace(/_/g," ")),u[m]));t.appendChild(l);return}let a=document.createElement("div");a.className="pw-detail-loading",a.textContent="Loading details...",t.appendChild(a);let s={Accept:"application/json"},c=pe("csrftoken");c&&(s["X-CSRFToken"]=c);try{let l=yield fetch(i,{headers:s});if(t.textContent="",l.ok){let u=yield l.json();z[n]=u,yt(u,e,t)}else{let u=document.createElement("div");u.className="pw-detail-loading",u.textContent="Could not load details (HTTP "+l.status+")",t.appendChild(u)}}catch(l){t.textContent="";let u=document.createElement("div");u.className="pw-detail-loading",u.textContent="Network error",t.appendChild(u)}})}function _n(e,t){var n;return(n=e==null?void 0:e.detail)!=null&&n.detailUrl?e.detail.detailUrl.replace("{id}",String(t.props.id)):""}function St(e,t){e.innerHTML=t}function Tn(e,t,n){return K(this,null,function*(){let r=document.createElement("div");r.className="pw-detail-loading",r.textContent="Loading details...",n.appendChild(r);let o={Accept:"text/html"},i=pe("csrftoken");i&&(o["X-CSRFToken"]=i);try{let a=yield fetch(e,{headers:o});if(n.textContent="",a.ok){let s=yield a.text();z[t]=s,St(n,s)}else{let s=document.createElement("div");s.className="pw-detail-loading",s.textContent="Could not load details (HTTP "+a.status+")",n.appendChild(s)}}catch(a){n.textContent="";let s=document.createElement("div");s.className="pw-detail-loading",s.textContent="Network error",n.appendChild(s)}})}function kn(e){let t=document.getElementById("pw-detail-body");if(!t)return;t.textContent="";let n=e.props,r=document.createElement("div");r.style.cssText="display:flex;align-items:center;gap:6px;margin-bottom:8px;";let o=document.createElement("div");o.className="pw-detail-title",o.style.marginBottom="0",o.textContent=n.name||"Unnamed",r.appendChild(o);let i=document.createElement("button");i.className="pw-edit-btn",i.title="Edit name";let a=document.createElement("i");a.className="mdi mdi-pencil",i.appendChild(a),r.appendChild(i),t.appendChild(r);let s=document.createElement("div");s.className="pw-inline-edit";let c=document.createElement("input");c.type="text",c.className="form-control form-control-sm",c.value=n.name||"",c.style.flex="1",s.appendChild(c);let l=document.createElement("button");l.className="btn btn-sm btn-primary",l.textContent="Save",s.appendChild(l);let u=document.createElement("button");u.className="btn btn-sm btn-outline-secondary",u.textContent="\xD7",s.appendChild(u),t.appendChild(s),i.addEventListener("click",function(){s.classList.add("active"),r.style.display="none",c.focus(),c.select()}),u.addEventListener("click",function(){s.classList.remove("active"),r.style.display=""}),l.addEventListener("click",function(){let p=c.value.trim();if(!p||p===n.name){u.click();return}let h=xt(e),v={"Content-Type":"application/json",Accept:"application/json"},E=pe("csrftoken");E&&(v["X-CSRFToken"]=E),fetch(h,{method:"PATCH",headers:v,body:JSON.stringify({name:p})}).then(function(_){_.ok&&(n.name=p,o.textContent=p,delete z[O(e)],ee()),s.classList.remove("active"),r.style.display=""})}),c.addEventListener("keydown",function(p){p.key==="Enter"&&l.click(),p.key==="Escape"&&u.click()});let m=qe(e),C=Fe(e),d=document.createElement("div");d.className="pw-metric-row";let y=document.createElement("span");if(y.className="pw-metric-badge",y.style.background=m,y.style.color="#fff",y.textContent=J(C),d.appendChild(y),e.featureType==="structure"&&n.site_name){let p=document.createElement("span");p.className="pw-metric-badge pw-metric-muted",p.textContent=n.site_name,d.appendChild(p)}t.appendChild(d);let f=Cn(e);if(f){let p=document.createElement("a");p.href=f,p.className="btn btn-sm btn-primary w-100 mb-3";let h=document.createElement("i");h.className="mdi mdi-open-in-new",p.appendChild(h),p.appendChild(document.createTextNode(" View Details ")),t.appendChild(p)}if(e.featureType==="structure"){let p=document.createElement("a");p.href="/plugins/pathways/route-planner/?start="+n.id,p.className="btn btn-sm btn-outline-primary w-100 mb-3";let h=document.createElement("i");h.className="mdi mdi-map-search-outline",p.appendChild(h),p.appendChild(document.createTextNode(" Plan Route From Here")),t.appendChild(p)}let g=document.createElement("div");t.appendChild(g),Sn(e,g)}function In(){let e=document.getElementById("pw-feature-list");if(!e)return;let t=document.createElement("div");t.className="pw-server-search-status",t.id="pw-server-searching";let n=document.createElement("i");n.className="mdi mdi-magnify mdi-spin",t.appendChild(n),t.appendChild(document.createTextNode(" Searching all features\u2026")),e.appendChild(t)}function _t(){let e=document.getElementById("pw-server-searching");e&&e.remove();let t=document.getElementById("pw-server-results-header");t&&t.remove();let n=document.querySelectorAll(".pw-server-result");for(let r=0;r200&&t.slice(0,100).forEach(function(n){delete z[n]}),D){let n=O(D),r=null;for(let o=0;o0){let n=Ee;for(let r=0;r0){let n=xe;xe="";for(let r=0;rn&&(r=t.x-200),o<0&&(o=t.y+20),G.style.left=r+"px",G.style.top=o+"px"}function Kn(e){Ne=e,G=document.createElement("div"),G.className="pw-popover",G.style.display="none",e.getContainer().appendChild(G)}function zn(e,t,n){var i,a;if(!G)return;G.textContent="";let r=document.createElement("span");r.className="pw-popover-name",n&&n.length>0?r.textContent=String((a=(i=t[n[0]])!=null?i:t.name)!=null?a:`#${t.id}`):r.textContent=t.name||"Unnamed",G.appendChild(r);let o="";if(n&&n.length>1)o=n.slice(1).map(s=>{var c;return String((c=t[s])!=null?c:"")}).filter(Boolean).join(" / ");else{let s=t.structure_type||t.pathway_type||"";o=s?kt(s):""}if(o){let s=document.createElement("span");s.className="pw-popover-type",s.textContent=o,G.appendChild(s)}Jn(e),G.style.display=""}function Zn(){G&&(G.style.display="none")}function jn(e){kt=e.titleCase}var q={init:Kn,show:zn,hide:Zn,setDeps:jn};var fe={pole:"#2e7d32",manhole:"#1565c0",handhole:"#00838f",cabinet:"#e65100",vault:"#6a1b9a",pedestal:"#f9a825",building_entrance:"#c62828",splice_closure:"#795548",tower:"#b71c1c",roof:"#616161",equipment_room:"#00796b",telecom_closet:"#283593",riser_room:"#ad1457"},me={pole:'',manhole:'',handhole:'',cabinet:'',vault:'',pedestal:'',building_entrance:'',splice_closure:'',tower:'',roof:'',equipment_room:'',telecom_closet:'',riser_room:''},Pe={conduit:"#f57c00",conduit_bank:"#ad1457",aerial:"#1565c0",direct_buried:"#616161",innerduct:"#e65100",microduct:"#6a1b9a",tray:"#2e7d32",raceway:"#00838f",submarine:"#1a237e"},It={conduit:"5,5",conduit_bank:"",aerial:"10,5",direct_buried:"2,4",innerduct:"8,3",microduct:"1,3",tray:"",raceway:"12,4",submarine:"6,2,2,2"};function Ft(e,t=20){let n=fe[e]||"#616161",r=me[e]||'',o=r.includes('fill="none"'),i=t/2;return L.divIcon({className:"pw-marker",html:''+r+"",iconSize:[t,t],iconAnchor:[i,i],popupAnchor:[0,-(i+2)]})}function Mt(e){let t,n;return e<10?(t="pw-cluster-small",n=34):e<100?(t="pw-cluster-medium",n=40):(t="pw-cluster-large",n=46),L.divIcon({className:"pw-server-cluster",html:'
'+e+"
",iconSize:[n,n],iconAnchor:[n/2,n/2]})}function Re(e){let t=document.createElement("span");return t.textContent=e,t.innerHTML}function ne(e){return(e||"").replace(/_/g," ").replace(/\b\w/g,function(t){return t.toUpperCase()})}function re(e){let n=("; "+document.cookie).split("; "+e+"=");return n.length===2&&n.pop().split(";").shift()||null}function De(e){let t=e.getBounds();return t.getWest()+","+t.getSouth()+","+t.getEast()+","+t.getNorth()}function Qe(e,t){let n;return function(){clearTimeout(n),n=setTimeout(e,t)}}function Nt(e,t,n,r){let i=e*Math.PI/180,a=n*Math.PI/180,s=(n-e)*Math.PI/180,c=(r-t)*Math.PI/180,l=Math.sin(s/2)*Math.sin(s/2)+Math.cos(i)*Math.cos(a)*Math.sin(c/2)*Math.sin(c/2);return 6371e3*2*Math.atan2(Math.sqrt(l),Math.sqrt(1-l))}var et=window.PATHWAYS_CONFIG||{},Rt=et.maxNativeZoom||19,Wn=[{name:"Street",url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",attribution:"© OpenStreetMap contributors",maxNativeZoom:Rt},{name:"Satellite",url:"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",attribution:"Esri World Imagery",maxNativeZoom:19}];function Yn(){let e=(et.baseLayers||[]).filter(function(r){return!!r.url}),t=e.length?e:Wn,n={};return t.forEach(function(r){n[r.name]=L.tileLayer(r.url,{attribution:r.attribution||"",maxNativeZoom:r.maxNativeZoom||Rt,maxZoom:22,tileSize:r.tileSize||256,zoomOffset:r.zoomOffset||0})}),n}function Vn(){let e=et.overlays||[],t={};return e.forEach(function(n){let r;n.type==="wms"?r=L.tileLayer.wms(n.url,{layers:n.layers||"",format:n.format||"image/png",transparent:n.transparent!==!1,attribution:n.attribution||"",maxZoom:22}):r=L.tileLayer(n.url,{attribution:n.attribution||"",maxZoom:n.maxZoom||22,maxNativeZoom:n.maxNativeZoom||void 0}),t[n.name]=r}),t}function Dt(e){let t=L.DomUtil.create("div","pathways-zoom-hint");return t.style.cssText="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:800;padding:12px 24px;border-radius:8px;font-size:14px;pointer-events:none;text-align:center;background:rgba(0,0,0,0.7);color:#fff;",t.textContent="Zoom in to see infrastructure data",e.getContainer().appendChild(t),t}function Pt(e,t){e.innerHTML=t}function Ot(e){let t=L.Control.extend({options:{position:"bottomleft"},onAdd:function(){let n=L.DomUtil.create("div","pw-legend leaflet-bar");L.DomEvent.disableClickPropagation(n),L.DomEvent.disableScrollPropagation(n);let r=L.DomUtil.create("div","pw-legend-header collapsed",n),o=document.createElement("i");o.className="mdi mdi-chevron-down",r.appendChild(o);let i=document.createElement("span");i.textContent="Legend",r.appendChild(i);let a=L.DomUtil.create("div","pw-legend-body collapsed",n),s=L.DomUtil.create("div","pw-legend-section",a),c=L.DomUtil.create("div","pw-legend-section-title",s);c.textContent="Structures";let l=["pole","manhole","handhole","cabinet","vault","pedestal","building_entrance","splice_closure","tower","equipment_room","telecom_closet","riser_room"];for(let d=0;d',p=g.includes('fill="none"'),h=L.DomUtil.create("div","pw-legend-item",s),v=L.DomUtil.create("span","pw-legend-swatch",h);Pt(v,''+g+"");let E=L.DomUtil.create("span","pw-legend-label",h);E.textContent=ne(y)}let u=L.DomUtil.create("div","pw-legend-section",a),m=L.DomUtil.create("div","pw-legend-section-title",u);m.textContent="Pathways";let C=["conduit","conduit_bank","aerial","direct_buried","innerduct","microduct","tray","raceway","submarine"];for(let d=0;d");let v=L.DomUtil.create("span","pw-legend-label",p);v.textContent=ne(y)}return r.addEventListener("click",function(){let d=a.classList.toggle("collapsed");r.classList.toggle("collapsed",d)}),n}});new t().addTo(e)}function At(e){let t=L.Control.extend({options:{position:"bottomleft"},onAdd:function(){let n=L.DomUtil.create("div","pw-stats-overlay"),r=L.DomUtil.create("span","",n);r.id="structure-count",r.textContent="0",n.appendChild(document.createTextNode(" structures \xB7 "));let o=L.DomUtil.create("span","",n);o.id="pathway-count",o.textContent="0",n.appendChild(document.createTextNode(" pathways \xB7 "));let i=L.DomUtil.create("span","",n);return i.id="total-length",i.textContent="0",n.appendChild(document.createTextNode(" km")),n}});new t().addTo(e)}function Bt(e){let t=L.Control.extend({options:{position:"topleft"},onAdd:function(){let n=L.DomUtil.create("div","leaflet-control-zoom leaflet-bar"),r=L.DomUtil.create("a","",n);return r.href="#",r.title="Go to my location",L.DomUtil.create("i","mdi mdi-crosshairs-gps",r),r.style.display="flex",r.style.alignItems="center",r.style.justifyContent="center",r.style.fontSize="18px",L.DomEvent.disableClickPropagation(n),L.DomEvent.on(r,"click",function(o){L.DomEvent.preventDefault(o),navigator.geolocation&&navigator.geolocation.getCurrentPosition(function(i){e.flyTo([i.coords.latitude,i.coords.longitude],17,{duration:1})},function(){},{enableHighAccuracy:!0,timeout:1e4})}),n}});return new t}function Gt(e,t){let n=L.Control.extend({options:{position:"topleft"},onAdd:function(){let r=L.DomUtil.create("div","leaflet-control-zoom leaflet-bar"),o=L.DomUtil.create("a","",r);return o.href="#",o.title=t?"Exit kiosk mode":"Kiosk mode",L.DomUtil.create("i","mdi "+(t?"mdi-fullscreen-exit":"mdi-fullscreen"),o),o.style.display="flex",o.style.alignItems="center",o.style.justifyContent="center",o.style.fontSize="18px",L.DomEvent.disableClickPropagation(r),L.DomEvent.on(o,"click",function(i){L.DomEvent.preventDefault(i);let a=e.getCenter(),s=e.getZoom(),c=new URLSearchParams;c.set("lat",a.lat.toFixed(6)),c.set("lon",a.lng.toFixed(6)),c.set("zoom",String(s)),t||c.set("kiosk","true"),window.location.search=c.toString()}),r}});return new n}function Ut(e,t,n){let r=L.Control.extend({options:{position:"topright"},onAdd:function(){let o=L.DomUtil.create("div","leaflet-control-zoom leaflet-bar pw-sidebar-toggle-ctrl"),i=L.DomUtil.create("a","",o);i.href="#",i.title="Show sidebar",L.DomUtil.create("i","mdi mdi-chevron-left",i),i.style.display="flex",i.style.alignItems="center",i.style.justifyContent="center",i.style.fontSize="18px",L.DomEvent.disableClickPropagation(o),L.DomEvent.on(i,"click",function(c){L.DomEvent.preventDefault(c),n()});let a=new MutationObserver(function(){let c=document.getElementById("pw-sidebar");if(!c)return;let l=t?c.classList.contains("pw-sidebar-open"):!c.classList.contains("pw-sidebar-hidden");o.style.display=l?"none":""}),s=document.getElementById("pw-sidebar");if(s){a.observe(s,{attributes:!0,attributeFilter:["class"]});let c=t?s.classList.contains("pw-sidebar-open"):!s.classList.contains("pw-sidebar-hidden");o.style.display=c?"none":""}return o}});return new r}function Ht(e,t){let n=Yn(),r=n[Object.keys(n)[0]],o=L.map(e,{layers:[r]});if(t.bounds){let c=t.select?20:17;o.fitBounds(t.bounds,{padding:[50,50],maxZoom:c})}else o.setView(t.center||[0,0],t.zoom||2);let i={},a=Vn();for(let c in a)i[c]=a[c];let s=L.control.layers(n,i,{position:"bottomright",collapsed:!0}).addTo(o);return{map:o,baseLayers:n,layerControl:s}}var zt=window.PATHWAYS_CONFIG||{},he=zt.apiBase||"/api/plugins/pathways/geo/";var oe=11,Kt,Zt=(Kt=zt.skipInfoZoom)!=null?Kt:17;function Le(e,t){var u;let n=e.counts.structures,r=e.thresholds.structures,o="off";n>r.hide?o="server":r.cluster!=null&&n>r.cluster&&(o="client");let i={},a=o!=="off";t.has("structures")&&(i.structures="render");let s=["conduit_banks","conduits","aerial_spans","direct_buried","circuits"];for(let m of s){if(!t.has(m))continue;if(a){i[m]="hide";continue}let C=(u=e.counts[m])!=null?u:0,d=e.thresholds[m];i[m]=d&&C>d.hide?"hide":"render"}let c=e.counts.external||{},l=e.thresholds.external||{};for(let m of Object.keys(c)){let C=`external:${m}`;if(!t.has(C))continue;if(a){i[C]="hide";continue}let d=l[m];i[C]=d&&c[m]>d.hide?"hide":"render"}return{clusterMode:o,layers:i}}function jt(e){let t={};for(let n of e)t[n]="render";return{clusterMode:"off",layers:t}}var $=null,tt="",Be=null;function Wt(){return Be}function nt(e,t){return K(this,null,function*(){$&&$.abort();let n=new AbortController;$=n;let r=he+"info/?bbox="+e,o={Accept:"application/json"},i=re("csrftoken");i&&(o["X-CSRFToken"]=i),tt&&(o["If-None-Match"]=tt);try{let a=yield fetch(r,{headers:o,signal:n.signal});if($===n&&($=null),a.status===304&&Be){t(Be,!1);return}if(a.ok){let s=yield a.json();tt=a.headers.get("ETag")||"",Be=s,t(s,!0)}}catch(a){$===n&&($=null)}})}var ge={};function Yt(e,t,n,r,o){return K(this,null,function*(){ge[e]&&ge[e].abort();let i=he+e+"?format=json&bbox="+t;if(r)for(let l in r)i+="&"+l+"="+encodeURIComponent(String(r[l]));let a=new AbortController;ge[e]=a;let s={Accept:"application/json"},c=re("csrftoken");c&&(s["X-CSRFToken"]=c),o&&(s["If-None-Match"]=o);try{let l=yield fetch(i,{headers:s,signal:a.signal});ge[e]=void 0;let u=l.headers.get("ETag")||"";if(l.status===304)n({data:null,etag:u});else if(l.ok){let m=yield l.json();n({data:m,etag:u})}}catch(l){ge[e]=void 0}})}var ye=null;function Vt(e,t){return K(this,null,function*(){ye&&ye.abort();let n=new AbortController;ye=n;let r={Accept:"application/json"},o=re("csrftoken");o&&(r["X-CSRFToken"]=o);let i=[["structures/","structure","structure_type"],["conduit-banks/","conduit_bank","pathway_type"],["conduits/","conduit","pathway_type"],["aerial-spans/","aerial","pathway_type"],["direct-buried/","direct_buried","pathway_type"],["circuits/","circuit","pathway_type"]],a=[],s=i.map(function(c){let[l,u,m]=c,C=he+l+"?format=json&q="+encodeURIComponent(e);return fetch(C,{headers:r,signal:n.signal}).then(function(d){return d.ok?d.json():null}).then(function(d){!d||!d.features||d.features.forEach(function(y){if(!y.geometry)return;let f=y.properties||{},g;if(y.geometry.type==="Point"){let p=y.geometry.coordinates;g=L.latLng(p[1],p[0])}else if(y.geometry.type==="LineString"){let p=y.geometry.coordinates,h=p[Math.floor(p.length/2)];g=L.latLng(h[1],h[0])}else return;a.push({name:f.name||f.cid||"Unnamed",featureType:u,typeKey:f[m]||u,latlng:g,url:f.url})})}).catch(function(){})});try{yield Promise.all(s),ye=null,t(a)}catch(c){ye=null}})}function qn(e,t,n){n.getZoom()<20||e.eachLayer(function(r){var v;let o=r,i=o.getLatLngs();if(!i||i.length<2)return;let a=o.feature,s=(v=a==null?void 0:a.properties)==null?void 0:v.name;if(!s)return;let c=Math.floor(i.length/2),l=i[c-1]||i[0],u=i[c],m=(l.lat+u.lat)/2,C=(l.lng+u.lng)/2,d=n.latLngToContainerPoint(l),y=n.latLngToContainerPoint(u),f=y.x-d.x,g=y.y-d.y,p=Math.atan2(g,f)*180/Math.PI;p>90&&(p-=180),p<-90&&(p+=180);let h=L.divIcon({className:"pw-line-label",html:'
'+Re(s)+"
",iconSize:[0,0],iconAnchor:[0,0]});t.addLayer(L.marker([m,C],{icon:h,interactive:!1}))})}function qt(){return{structures:L.layerGroup(),conduitBanks:L.layerGroup(),conduits:L.layerGroup(),aerialSpans:L.layerGroup(),directBuried:L.layerGroup(),circuits:L.layerGroup()}}var Oe=[{endpoint:"conduit-banks/",layerKey:"conduitBanks",featureType:"conduit_bank",style:{color:"#ad1457",weight:5,opacity:.8,dashArray:""},infoKey:"conduit_banks"},{endpoint:"conduits/",layerKey:"conduits",featureType:"conduit",style:{color:"#f57c00",weight:3,opacity:.7,dashArray:"5 5"},infoKey:"conduits"},{endpoint:"aerial-spans/",layerKey:"aerialSpans",featureType:"aerial",style:{color:"#1565c0",weight:3,opacity:.7,dashArray:"10 5"},infoKey:"aerial_spans"},{endpoint:"direct-buried/",layerKey:"directBuried",featureType:"direct_buried",style:{color:"#616161",weight:3,opacity:.7,dashArray:"2 4"},infoKey:"direct_buried"},{endpoint:"circuits/",layerKey:"circuits",featureType:"circuit",style:{color:"#d32f2f",weight:3,opacity:.8,dashArray:"8 6"},infoKey:"circuits"}],$n=12,Ue=.5,Ge={};function $t(e,t,n,r,o,i,a){let s=Ge[e];if(!s)return null;for(let c=s.length-1;c>=0;c--){let l=s[c];if(l.zoom===t&&l.extraKey===n&&r>=l.west&&o>=l.south&&i<=l.east&&a<=l.north)return l}return null}function Xt(e,t,n,r,o,i,a,s,c){Ge[e]||(Ge[e]=[]);let l=Ge[e];l.push({west:t,south:n,east:r,north:o,zoom:i,extraKey:a,etag:s,data:c}),l.length>$n&&l.shift()}function Jt(e,t,n,r){let o=e.getBounds(),i=o.getWest(),a=o.getSouth(),s=o.getEast(),c=o.getNorth(),l=e.getZoom(),u=r?JSON.stringify(r):"",m=$t(t,l,u,i,a,s,c);if(m){n(m.data);return}let C=(s-i)*Ue,d=(c-a)*Ue,y=i-C,f=a-d,g=s+C,p=c+d,h=y+","+f+","+g+","+p;Yt(t,h,function(v){v.data&&(Xt(t,y,f,g,p,l,u,v.etag,v.data),n(v.data))},r)}var Ae=null,Xn=14;function Qn(e,t){Ae&&clearTimeout(Ae);let n=e.getZoom();if(n=s.length||e.getZoom()!==n)return;let u=s[c++];Yt(u.endpoint,u.bbox,function(m){m.data&&Xt(u.endpoint,u.fw,u.fs,u.fe,u.fn,u.zoom,u.extraKey,m.etag,m.data),Ae=setTimeout(l,50)},u.extraParams)}Ae=setTimeout(l,200)}function Qt(e,t,n,r,o){let i=e.getZoom();if(i0&&((E=h.features[0].properties)==null?void 0:E.cluster)){let _=0;h.features.forEach(function(T){let F=T.properties.point_count||0;_+=F;let M=T.geometry,X=L.latLng(M.coordinates[1],M.coordinates[0]),ie=L.marker(X,{icon:Mt(F)});ie.on("click",function(){let be=Math.min(e.getZoom()+3,e.getMaxZoom());e.setView(X,be)}),t.structures.addLayer(ie)}),o.onStructuresLoaded&&o.onStructuresLoaded(_)}else{let _=L.geoJSON(h,{pointToLayer:function(T,k){return L.marker(k,{icon:Ft(T.properties.structure_type||"")})},onEachFeature:function(T,k){T.id!=null&&T.properties.id==null&&(T.properties.id=T.id);let F={props:T.properties,featureType:"structure",layer:k,latlng:k.getLatLng()};s.push(F),o.onFeatureCreated&&o.onFeatureCreated(F),k.on("click",function(M){o.onFeatureClick&&o.onFeatureClick(F,M)}),k.on("mouseover",function(M){o.onFeatureMouseOver&&o.onFeatureMouseOver(F,M,T)}),k.on("mouseout",function(){o.onFeatureMouseOut&&o.onFeatureMouseOut()})}});p?(p.addLayers(_.getLayers()),t.structures.addLayer(p)):_.addTo(t.structures),o.onStructuresLoaded&&o.onStructuresLoaded(h.features?h.features.length:0)}C()},{zoom:i})}m===0&&o.onPathwayLoaded;function y(f,g){return{style:function(){return g},onEachFeature:function(p,h){let v=p.properties;p.id!=null&&v.id==null&&(v.id=p.id),!v.name&&v.label&&(v.name=v.label);let E={props:v,featureType:f,layer:h,latlng:h.getBounds().getCenter()};s.push(E),o.onFeatureCreated&&o.onFeatureCreated(E),h.on("click",function(_){o.onFeatureClick&&o.onFeatureClick(E,_)}),h.on("mouseover",function(_){o.onFeatureMouseOver&&o.onFeatureMouseOver(E,_,p)}),h.on("mouseout",function(){o.onFeatureMouseOut&&o.onFeatureMouseOut()})}}}Oe.forEach(function(f){let g=t[f.layerKey];a.layers[f.infoKey]==="render"&&e.hasLayer(g)&&Jt(e,f.endpoint,function(p){g.clearLayers();let h=L.geoJSON(p,y(f.featureType,f.style));h.addTo(g),qn(h,g,e),d(p),C()})}),l===0&&o.onAllLoaded&&o.onAllLoaded([])}function en(e){let t=0;return e.features&&e.features.forEach(function(n){if(n.geometry&&n.geometry.type==="LineString"){let r=n.geometry.coordinates;for(let o=0;o=r?{kind:"skip-info",decision:jt(n)}:t?{kind:"optimistic",decision:Le(t,n),revalidate:!0,cachedInfo:t}:{kind:"gated"}}function nn(e,t){if(e.clusterMode!==t.clusterMode)return!0;let n=Object.keys(e.layers),r=Object.keys(t.layers);if(n.length!==r.length)return!0;for(let o of n)if(e.layers[o]!==t.layers[o])return!0;return!1}var er=window.PATHWAYS_CONFIG||{};function tr(e,t){var ot;let n=document.getElementById(e);R.setDeps({titleCase:ne,esc:Re,debounce:Qe,getCookie:re,structureColors:fe,structureShapes:me,pathwayColors:Pe,apiBase:he}),q.setDeps({titleCase:ne});let{map:r,baseLayers:o,layerControl:i}=Ht(e,t);At(r),Ot(r),Gt(r,!!t.kiosk).addTo(r),Ut(r,!!t.kiosk,function(){R.show()}).addTo(r),Bt(r).addTo(r);let a=document.getElementById("structure-count"),s=document.getElementById("pathway-count"),c=document.getElementById("total-length"),l=Dt(r),u="pathways_map_layers",m={Structures:!0,"Conduit Banks":!0,Conduits:!0,"Aerial Spans":!1,"Direct Buried":!1,"Circuit Routes":!1};function C(){try{let b=localStorage.getItem(u);return b?JSON.parse(b):null}catch(b){return null}}function d(b){try{localStorage.setItem(u,JSON.stringify(b))}catch(S){}}let y=C()||m,f=qt(),g={Structures:f.structures,"Conduit Banks":f.conduitBanks,Conduits:f.conduits,"Aerial Spans":f.aerialSpans,"Direct Buried":f.directBuried,"Circuit Routes":f.circuits},p=(ot=er.externalLayers)!=null?ot:[],h=dt(p,r);for(let[b,S]of h){let w=Z(b);w&&(g[w.label]=S)}for(let b in g)y[b]!==!1&&g[b].addTo(r);let v={Structures:"structures","Conduit Banks":"conduit_banks",Conduits:"conduits","Aerial Spans":"aerial_spans","Direct Buried":"direct_buried","Circuit Routes":"circuits"};for(let[b]of h){let S=Z(b);S&&(v[S.label]="external:"+b)}let E={};function _(b,S){if(E[b]){E[b].checked=S;let w=E[b].closest(".pw-layer-toggle");w&&w.classList.toggle("pw-layer-active",S)}}r.on("overlayadd",function(b){let S=C()||m;S[b.name]=!0,d(S),_(b.name,!0)}),r.on("overlayremove",function(b){let S=C()||m;S[b.name]=!1,d(S),_(b.name,!1)});let T={Structures:'',"Conduit Banks":'',Conduits:'',"Aerial Spans":'',"Direct Buried":'',"Circuit Routes":''};function k(){let b=document.getElementById("pw-layer-toggles");if(b){b.textContent="";for(let S in g){let w=document.createElement("button");w.type="button";let x=r.hasLayer(g[S]);w.className="pw-layer-toggle"+(x?" pw-layer-active":"");let I=T[S]||"",P=document.createElement("span");P.textContent=S;let N=document.createElement("span");for(N.innerHTML=I;N.firstChild;)w.appendChild(N.firstChild);w.appendChild(P);let A=document.createElement("input");A.type="checkbox",A.checked=x,A.style.display="none",E[S]=A,w.appendChild(A),(function(B,Ce,Ke){Ke.addEventListener("click",function(){Ce.checked=!Ce.checked,Ce.checked?(r.addLayer(g[B]),Ke.classList.add("pw-layer-active")):(r.removeLayer(g[B]),Ke.classList.remove("pw-layer-active"));let it=C()||m;it[B]=Ce.checked,d(it),Je()})})(S,A,w),b.appendChild(w)}}}k();let F=0,M=0;function X(){let b=r.getCenter(),S=r.getZoom(),w=new URLSearchParams;w.set("lat",b.lat.toFixed(6)),w.set("lon",b.lng.toFixed(6)),w.set("zoom",String(S));let x=R.getSelectedId();x&&w.set("select",x),t.kiosk&&w.set("kiosk","true");let I=window.location.pathname+"?"+w.toString();history.replaceState(null,"",I)}function ie(b,S){if(!E[b])return;let w=E[b].closest(".pw-layer-toggle");if(!w)return;let x=w.querySelector(".pw-layer-count-chip");if(S==null){x&&x.remove();return}x||(x=document.createElement("span"),x.className="pw-layer-count-chip",w.appendChild(x)),x.textContent=S.toLocaleString()}function be(b,S){var w;for(let x in v){if(!E[x])continue;let I=E[x].closest(".pw-layer-toggle");if(!I)continue;let P=v[x],A=E[x].checked&&b.layers[P]==="hide";if(I.classList.toggle("pw-layer-unavailable",A),A){let B;P.startsWith("external:")?B=(w=S.counts.external)==null?void 0:w[P.slice(9)]:B=S.counts[P],ie(x,B!=null?B:0)}else ie(x,null)}}function se(b,S){let w=r.getZoom();b&&S&&be(b,S),F=0,M=0,Qt(r,f,b,l,{onFeatureClick:function(x,I){I.originalEvent&&(I.originalEvent._sidebarClick=!0),R.selectFeature(x),X()},onFeatureMouseOver:function(x,I,P){q.show(I.latlng||x.layer.getLatLng(),P.properties)},onFeatureMouseOut:function(){q.hide()},onFeatureCreated:function(x){R.onFeatureCreated(x)},onStructuresLoaded:function(x){a&&(a.textContent=String(x))},onPathwayLoaded:function(x){let I=x.features?x.features.length:0;F+=I,M+=en(x),s&&(s.textContent=String(F)),c&&(c.textContent=(M/1e3).toFixed(2))},onAllLoaded:function(x){if(w0){let P=De(r);pt(P,w,I,function(N,A){R.onFeatureCreated(N),N.layer.on("click",function(B){B.originalEvent&&(B.originalEvent._sidebarClick=!0),R.selectFeature(N)}),N.layer.on("mouseover",function(B){q.show(B.latlng,N.props,A.popoverFields)}),N.layer.on("mouseout",function(){q.hide()})}).then(function(N){for(let A=0;A = new Map();\n\n/** Resolve the color for a feature based on the layer style config. */\nfunction _resolveColor(\n props: GeoJSONProperties,\n style: ExternalLayerConfig['style'],\n): string {\n if (style.colorField && style.colorMap) {\n const val = String(props[style.colorField] ?? '');\n return style.colorMap[val] ?? style.defaultColor;\n }\n return style.color;\n}\n\n/** Create a Leaflet marker for a point feature.\n * TODO: Use iconClass to render MDI icons instead of plain circles. */\nfunction _createPointMarker(\n latlng: L.LatLng,\n color: string,\n): L.CircleMarker {\n return L.circleMarker(latlng, {\n radius: 7,\n fillColor: color,\n color: '#fff',\n weight: 2,\n opacity: 1,\n fillOpacity: 0.85,\n });\n}\n\n/** Create a Leaflet polyline for a line feature. */\nfunction _createLine(\n coords: L.LatLngExpression[],\n color: string,\n style: ExternalLayerConfig['style'],\n): L.Polyline {\n return L.polyline(coords, {\n color,\n weight: style.weight,\n opacity: style.opacity,\n dashArray: style.dash ?? undefined,\n });\n}\n\n/** Create a Leaflet polygon for a polygon feature. */\nfunction _createPolygon(\n coords: L.LatLngExpression[][],\n color: string,\n style: ExternalLayerConfig['style'],\n): L.Polygon {\n return L.polygon(coords, {\n color,\n fillColor: color,\n fillOpacity: 0.2,\n weight: style.weight,\n opacity: style.opacity,\n dashArray: style.dash ?? undefined,\n });\n}\n\n/**\n * Initialize layer groups for all external layers.\n * Returns a map of layer name -> L.LayerGroup for the layer control.\n */\nexport function initExternalLayers(\n configs: ExternalLayerConfig[],\n map: L.Map,\n): Map {\n _layerStates.clear();\n const groups = new Map();\n\n // Sort by sortOrder for consistent z-ordering\n const sorted = [...configs].sort((a, b) => a.sortOrder - b.sortOrder);\n\n for (const cfg of sorted) {\n const group = L.layerGroup();\n _layerStates.set(cfg.name, {\n config: cfg,\n layerGroup: group,\n abortController: null,\n });\n groups.set(cfg.name, group);\n\n if (cfg.defaultVisible) {\n group.addTo(map);\n }\n }\n return groups;\n}\n\n/**\n * Fetch and render features for all visible external layers.\n * Returns FeatureEntry[] for sidebar integration.\n *\n * @param bbox - \"W,S,E,N\" string\n * @param zoom - current zoom level\n * @param visibleLayers - set of layer names currently toggled on\n * @param onFeature - callback for each created feature (for sidebar/popover wiring)\n */\nexport async function loadExternalLayers(\n bbox: string,\n zoom: number,\n visibleLayers: Set,\n onFeature: (entry: FeatureEntry, config: ExternalLayerConfig) => void,\n): Promise {\n const allEntries: FeatureEntry[] = [];\n const fetchPromises: Promise[] = [];\n\n for (const [name, state] of _layerStates) {\n if (!visibleLayers.has(name)) continue;\n if (zoom < state.config.minZoom) continue;\n if (state.config.maxZoom !== null && zoom > state.config.maxZoom) continue;\n\n // Abort any in-flight request for this layer\n if (state.abortController) {\n state.abortController.abort();\n }\n state.abortController = new AbortController();\n\n const promise = _fetchLayer(state, bbox, zoom, allEntries, onFeature);\n fetchPromises.push(promise);\n }\n\n // Wait for all fetches; individual errors are caught inside _fetchLayer\n await Promise.all(fetchPromises.map(p => p.catch(() => {})));\n return allEntries;\n}\n\nasync function _fetchLayer(\n state: ExternalLayerState,\n bbox: string,\n zoom: number,\n entries: FeatureEntry[],\n onFeature: (entry: FeatureEntry, config: ExternalLayerConfig) => void,\n): Promise {\n const { config, layerGroup, abortController } = state;\n const sep = config.url.includes('?') ? '&' : '?';\n const url = `${config.url}${sep}format=json&bbox=${bbox}&zoom=${zoom}`;\n\n // Read CSRF token from cookie\n const csrfMatch = document.cookie.match(/csrftoken=([^;]+)/);\n const headers: Record = {\n Accept: 'application/json',\n };\n if (csrfMatch) {\n headers['X-CSRFToken'] = csrfMatch[1];\n }\n\n try {\n const resp = await fetch(url, {\n headers,\n signal: abortController?.signal,\n });\n if (!resp.ok) {\n console.warn(`External layer '${config.name}' fetch failed: ${resp.status}`);\n return;\n }\n const data = await resp.json() as GeoJSON.FeatureCollection;\n layerGroup.clearLayers();\n\n for (const feature of data.features) {\n if (!feature.geometry) continue;\n const props = (feature.properties ?? {}) as GeoJSONProperties;\n // Copy top-level feature.id to properties if not already present\n if (feature.id != null && props.id == null) {\n props.id = feature.id as number;\n }\n const color = _resolveColor(props, config.style);\n let layer: L.Layer | null = null;\n let latlng: L.LatLng | undefined;\n\n if (feature.geometry.type === 'Point') {\n const [lng, lat] = (feature.geometry as GeoJSON.Point).coordinates as [number, number];\n latlng = L.latLng(lat, lng);\n layer = _createPointMarker(latlng, color);\n } else if (feature.geometry.type === 'LineString') {\n const coords = (feature.geometry as GeoJSON.LineString).coordinates.map(\n (c: number[]) => L.latLng(c[1], c[0]),\n );\n const line = _createLine(coords, color, config.style);\n latlng = line.getBounds().getCenter();\n layer = line;\n } else if (feature.geometry.type === 'Polygon') {\n const rings = (feature.geometry as GeoJSON.Polygon).coordinates.map(\n (ring: number[][]) => ring.map((c: number[]) => L.latLng(c[1], c[0])),\n );\n const poly = _createPolygon(rings, color, config.style);\n latlng = poly.getBounds().getCenter();\n layer = poly;\n }\n\n if (layer && latlng) {\n layerGroup.addLayer(layer);\n const entry: FeatureEntry = {\n props: { ...props, name: props.name ?? `${config.label} #${props.id}` },\n featureType: config.name,\n layer,\n latlng,\n };\n entries.push(entry);\n onFeature(entry, config);\n }\n }\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') return;\n console.warn(`External layer '${config.name}' error:`, err);\n }\n}\n\n/** Get the ExternalLayerConfig for a layer name, if it exists. */\nexport function getLayerConfig(name: string): ExternalLayerConfig | undefined {\n return _layerStates.get(name)?.config;\n}\n\n/** Get all layer configs. */\nexport function getAllLayerConfigs(): ExternalLayerConfig[] {\n return Array.from(_layerStates.values()).map(s => s.config);\n}\n", "/**\n * Sidebar module for the full-page infrastructure map.\n *\n * Provides feature list, search/filter, detail panel with enriched\n * REST API data, inline name editing, and map feature highlighting.\n */\n\nimport type { FeatureEntry, FeatureType, DetailFieldDef, ResolvedValue, ServerSearchResult } from './types/features';\nimport { NATIVE_TYPES } from './types/features';\nimport { getLayerConfig } from './external-layers';\n\n// ---------------------------------------------------------------------------\n// Module-level helpers imported from the outer scope at init time\n// ---------------------------------------------------------------------------\n\n/** Set externally by pathways-map before Sidebar.init(). */\nlet _titleCase: (s: string) => string;\nlet _esc: (s: string) => string;\nlet _debounce: (fn: () => void, delay: number) => () => void;\nlet _getCookie: (name: string) => string | null;\n\nlet STRUCTURE_COLORS: Record;\nlet STRUCTURE_SHAPES: Record;\nlet PATHWAY_COLORS: Record;\nlet API_BASE: string;\n\n// ---------------------------------------------------------------------------\n// Internal state\n// ---------------------------------------------------------------------------\n\nlet _map: L.Map | null = null;\nlet _features: FeatureEntry[] = [];\nlet _filtered: FeatureEntry[] = [];\nlet _selected: FeatureEntry | null = null;\nconst _activeTypes: Record = {};\nconst _detailCache: Record> = {};\nlet _highlightedLayer: (L.Marker & { _origIcon?: L.Icon | L.DivIcon }) | (L.Polyline & { _origStyle?: L.PathOptions }) | null = null;\nlet _highlightOutline: L.Polyline | null = null;\nlet _serverSearchCallback: ((query: string) => void) | null = null;\nlet _lastServerQuery = '';\nlet _pendingSelect: { url: string; featureType: string } | null = null;\nlet _pendingFlySelectId = '';\nlet _isKiosk = false;\nlet _onSelectionChange: (() => void) | null = null;\nlet _dimmedFeatures: FeatureEntry[] = [];\nlet _activeConnectedIds: Set | null = null;\nconst DIM_OPACITY = 0.15;\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction _isNativeType(featureType: string): boolean {\n return (NATIVE_TYPES as readonly string[]).indexOf(featureType) !== -1;\n}\n\nfunction _typeLabel(featureType: string): string {\n const extCfg = getLayerConfig(featureType);\n if (extCfg) return extCfg.label;\n return _titleCase(featureType.replace(/_/g, ' '));\n}\n\nfunction _colorForFeature(entry: FeatureEntry): string {\n if (entry.featureType === 'structure') {\n return STRUCTURE_COLORS[entry.props.structure_type || ''] || '#616161';\n }\n if (entry.featureType === 'circuit') {\n return '#d32f2f';\n }\n return PATHWAY_COLORS[entry.props.pathway_type || ''] || '#616161';\n}\n\nfunction _typeKeyForFeature(entry: FeatureEntry): string {\n if (entry.featureType === 'structure') {\n return (entry.props.structure_type as string) || 'unknown';\n }\n return (entry.props.pathway_type as string) || 'unknown';\n}\n\nfunction _featureId(entry: FeatureEntry): string {\n return entry.featureType + '-' + (entry.props.id || '');\n}\n\n/** Mix a hex colour toward white by `amount` (0 = original, 1 = white). */\nfunction _lighten(hex: string, amount: number): string {\n const n = parseInt(hex.replace('#', ''), 16);\n const r = Math.round(((n >> 16) & 0xff) + (255 - ((n >> 16) & 0xff)) * amount);\n const g = Math.round(((n >> 8) & 0xff) + (255 - ((n >> 8) & 0xff)) * amount);\n const b = Math.round((n & 0xff) + (255 - (n & 0xff)) * amount);\n return '#' + ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1);\n}\n\n// ---------------------------------------------------------------------------\n// Highlight logic\n// ---------------------------------------------------------------------------\n\nfunction _unhighlightMapFeature(): void {\n if (_highlightOutline) {\n _highlightOutline.remove();\n _highlightOutline = null;\n }\n if (_highlightedLayer) {\n const layer = _highlightedLayer as Record;\n if (layer._origIcon) {\n (layer as any).setIcon(layer._origIcon);\n delete layer._origIcon;\n }\n if (layer._origStyle && typeof (layer as any).setStyle === 'function') {\n (layer as any).setStyle(layer._origStyle);\n delete layer._origStyle;\n }\n _highlightedLayer = null;\n }\n}\n\nfunction _applyHighlightVisuals(entry: FeatureEntry): void {\n const layer = entry.layer;\n if (!layer) return;\n _highlightedLayer = layer as any;\n\n if (entry.featureType === 'structure') {\n const marker = layer as L.Marker & { _origIcon?: L.Icon | L.DivIcon };\n marker._origIcon = (marker as any).getIcon();\n const type = entry.props.structure_type || '';\n const color = STRUCTURE_COLORS[type] || '#616161';\n const shape = STRUCTURE_SHAPES[type] || '';\n const isOutline = shape.includes('fill=\"none\"');\n marker.setIcon(L.divIcon({\n className: 'pw-marker pw-marker-selected',\n html: '' + shape + '',\n iconSize: [26, 26] as [number, number],\n iconAnchor: [13, 13] as [number, number],\n popupAnchor: [0, -14] as [number, number],\n }));\n } else {\n const polyline = layer as L.Polyline & { _origStyle?: L.PathOptions };\n const origStyle = (polyline as any).options || {};\n polyline._origStyle = {\n weight: origStyle.weight || 3,\n opacity: origStyle.opacity || 0.7,\n color: origStyle.color,\n dashArray: origStyle.dashArray,\n };\n const latlngs = polyline.getLatLngs() as L.LatLng[];\n if (latlngs && latlngs.length > 0 && _map) {\n // Glow: lighten the feature's own color toward white\n _highlightOutline = L.polyline(latlngs, {\n color: _lighten(origStyle.color || '#888', 0.55),\n weight: 12,\n opacity: 0.5,\n interactive: false,\n }).addTo(_map);\n }\n polyline.setStyle({ weight: 6, opacity: 1, dashArray: '' });\n }\n}\n\nfunction _highlightMapFeature(entry: FeatureEntry): void {\n _unhighlightMapFeature();\n _applyHighlightVisuals(entry);\n}\n\nfunction _reapplyHighlight(entry: FeatureEntry): void {\n if (_highlightOutline) {\n _highlightOutline.remove();\n _highlightOutline = null;\n }\n _highlightedLayer = null;\n _applyHighlightVisuals(entry);\n}\n\n// ---------------------------------------------------------------------------\n// Focus dimming \u2014 dim features not connected to the selected one\n// ---------------------------------------------------------------------------\n\nfunction _dimFeatures(connectedIds: Set): void {\n _dimmedFeatures = [];\n _activeConnectedIds = connectedIds;\n _applyDim();\n}\n\nfunction _applyDim(): void {\n if (!_activeConnectedIds) return;\n _dimmedFeatures = [];\n _features.forEach(function (f: FeatureEntry) {\n const fid = _featureId(f);\n if (_activeConnectedIds!.has(fid)) return;\n if (_selected && fid === _featureId(_selected)) return;\n if (!f.layer) return;\n\n _dimmedFeatures.push(f);\n if (f.featureType === 'structure') {\n (f.layer as L.Marker).setOpacity(DIM_OPACITY);\n } else if (typeof (f.layer as L.Polyline).setStyle === 'function') {\n (f.layer as L.Polyline).setStyle({ opacity: DIM_OPACITY });\n }\n });\n}\n\nfunction _restoreDim(): void {\n _activeConnectedIds = null;\n _dimmedFeatures.forEach(function (f: FeatureEntry) {\n if (!f.layer) return;\n if (f.featureType === 'structure') {\n (f.layer as L.Marker).setOpacity(1);\n } else if (typeof (f.layer as L.Polyline).setStyle === 'function') {\n const orig = (f.layer as any)._origStyle;\n (f.layer as L.Polyline).setStyle({ opacity: orig ? orig.opacity : 0.7 });\n }\n });\n _dimmedFeatures = [];\n}\n\n// ---------------------------------------------------------------------------\n// List rendering\n// ---------------------------------------------------------------------------\n\nfunction _highlightListItem(entry: FeatureEntry | null): void {\n const listEl = document.getElementById('pw-feature-list');\n if (!listEl) return;\n const items = listEl.querySelectorAll('.pw-list-item');\n const targetId = entry ? _featureId(entry) : null;\n for (let i = 0; i < items.length; i++) {\n items[i].classList.toggle('active', items[i].getAttribute('data-feature-id') === targetId);\n }\n}\n\nfunction _renderList(): void {\n const listEl = document.getElementById('pw-feature-list');\n const countEl = document.getElementById('pw-list-count');\n if (!listEl) return;\n\n listEl.textContent = '';\n if (countEl) {\n countEl.textContent = String(_filtered.length);\n countEl.style.display = _filtered.length > 0 ? '' : 'none';\n }\n\n _filtered.forEach(function (entry: FeatureEntry) {\n const item = document.createElement('div');\n item.className = 'pw-list-item';\n item.setAttribute('data-feature-id', _featureId(entry));\n\n if (_selected && _featureId(_selected) === _featureId(entry)) {\n item.classList.add('active');\n }\n\n const dot = document.createElement('span');\n dot.className = 'pw-list-dot';\n dot.style.background = _colorForFeature(entry);\n item.appendChild(dot);\n\n const label = document.createElement('span');\n label.className = 'pw-list-label';\n label.textContent = entry.props.name || 'Unnamed';\n label.title = entry.props.name || 'Unnamed';\n item.appendChild(label);\n\n const typeBadge = document.createElement('span');\n typeBadge.className = 'pw-list-type';\n typeBadge.textContent = _titleCase(_typeKeyForFeature(entry));\n item.appendChild(typeBadge);\n\n item.addEventListener('click', function () {\n selectFeature(entry);\n });\n\n listEl.appendChild(item);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Filters\n// ---------------------------------------------------------------------------\n\nfunction _buildTypeFilters(): void {\n const container = document.getElementById('pw-type-filters');\n if (!container) return;\n container.textContent = '';\n\n const typeMap: Record = {};\n _features.forEach(function (entry: FeatureEntry) {\n const key = _typeKeyForFeature(entry);\n if (!typeMap[key]) {\n typeMap[key] = _colorForFeature(entry);\n }\n });\n\n const types = Object.keys(typeMap).sort();\n if (types.length <= 1) return;\n\n types.forEach(function (t: string) {\n if (_activeTypes[t] === undefined) {\n _activeTypes[t] = true;\n }\n });\n\n types.forEach(function (type: string) {\n const btn = document.createElement('button');\n btn.className = 'pw-filter-btn' + (_activeTypes[type] ? ' active' : '');\n btn.type = 'button';\n\n const dot = document.createElement('span');\n dot.className = 'pw-filter-dot';\n dot.style.background = typeMap[type];\n btn.appendChild(dot);\n\n const label = document.createTextNode(_typeLabel(type));\n btn.appendChild(label);\n\n btn.addEventListener('click', function () {\n _activeTypes[type] = !_activeTypes[type];\n btn.classList.toggle('active', _activeTypes[type]);\n _applyFilters();\n });\n\n container.appendChild(btn);\n });\n}\n\nfunction _applyFilters(): void {\n const searchInput = document.getElementById('pw-search') as HTMLInputElement | null;\n const query = (searchInput ? searchInput.value : '').toLowerCase().trim();\n\n _filtered = _features.filter(function (entry: FeatureEntry) {\n const typeKey = _typeKeyForFeature(entry);\n if (_activeTypes[typeKey] === false) return false;\n\n if (query) {\n const name = (entry.props.name || '').toLowerCase();\n const type = _titleCase(typeKey).toLowerCase();\n if (name.indexOf(query) === -1 && type.indexOf(query) === -1) {\n return false;\n }\n }\n\n return true;\n });\n\n _renderList();\n\n // If no client-side results and there's a query, fall back to server search\n if (_filtered.length === 0 && query.length >= 2 && _serverSearchCallback) {\n if (query !== _lastServerQuery) {\n _lastServerQuery = query;\n _showSearchingIndicator();\n _serverSearchCallback(query);\n }\n } else {\n _lastServerQuery = '';\n _clearServerResults();\n }\n}\n\n// ---------------------------------------------------------------------------\n// API helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Build the UI detail-page URL for a native feature client-side.\n *\n * The GeoJSON geo endpoints intentionally omit a ``url`` property because\n * computing get_absolute_url() per row calls django.urls.reverse(), which\n * triggers lazy URL-pattern population (including the Strawberry GraphQL\n * schema). That single import adds ~12 s per 1000 features, making the\n * geo API unusably slow. Building the URL here costs nothing.\n */\nfunction _detailPageUrl(entry: FeatureEntry): string {\n const id = entry.props.id;\n if (!_isNativeType(entry.featureType) || id == null) return '';\n const base = '/plugins/pathways/';\n switch (entry.featureType) {\n case 'structure': return base + 'structures/' + id + '/';\n case 'conduit_bank': return base + 'conduit-banks/' + id + '/';\n case 'conduit': return base + 'conduits/' + id + '/';\n case 'aerial': return base + 'aerial-spans/' + id + '/';\n case 'direct_buried': return base + 'direct-buried/' + id + '/';\n case 'circuit': return base + 'circuit-geometries/' + id + '/';\n default: return base + 'pathways/' + id + '/';\n }\n}\n\nfunction _apiUrlForFeature(entry: FeatureEntry): string {\n if (entry.props.url) {\n return '/api' + entry.props.url;\n }\n const id = entry.props.id;\n\n // Native types use the pathways API\n if (_isNativeType(entry.featureType)) {\n const base = API_BASE.replace(/geo\\/?$/, '');\n switch (entry.featureType) {\n case 'structure':\n return base + 'structures/' + id + '/';\n case 'conduit_bank':\n return base + 'conduit-banks/' + id + '/';\n case 'conduit':\n return base + 'conduits/' + id + '/';\n case 'aerial':\n return base + 'aerial-spans/' + id + '/';\n case 'direct_buried':\n return base + 'direct-buried/' + id + '/';\n case 'circuit':\n return base + 'circuit-geometries/' + id + '/';\n default:\n return base + 'pathways/' + id + '/';\n }\n }\n\n // Check external layer config for detail URL\n const extCfg = getLayerConfig(entry.featureType);\n if (extCfg?.detail?.urlTemplate) {\n return extCfg.detail.urlTemplate.replace('{id}', String(id));\n }\n return '';\n}\n\n// ---------------------------------------------------------------------------\n// Value resolver\n// ---------------------------------------------------------------------------\n\nfunction _resolveValue(val: unknown): ResolvedValue | null {\n if (val === null || val === undefined || val === '') return null;\n\n // Array\n if (Array.isArray(val)) {\n if (val.length === 0) return null;\n const texts: string[] = [];\n for (let i = 0; i < val.length; i++) {\n const r = _resolveValue(val[i]);\n if (r) texts.push(r.text);\n }\n return texts.length > 0 ? { text: texts.join(', ') } : null;\n }\n\n // Choice field: {value, label}\n if (typeof val === 'object' && val !== null && 'label' in val) {\n const choice = val as { value?: string; label?: string };\n return { text: choice.label || _titleCase(choice.value || '') };\n }\n\n // Nested FK: {id, url, display_url, display, ...}\n if (typeof val === 'object' && val !== null) {\n const fk = val as { id?: number; url?: string; display_url?: string; display?: string; name?: string };\n if (fk.display || fk.name || fk.id !== undefined) {\n return { text: fk.display || fk.name || String(fk.id), url: fk.display_url || fk.url || null };\n }\n }\n\n // Boolean\n if (val === true) return { text: 'Yes' };\n if (val === false) return { text: 'No' };\n\n // Primitive\n return { text: String(val) };\n}\n\nfunction _addFieldRow(table: HTMLTableElement, label: string, val: unknown, suffix?: string): void {\n const resolved = _resolveValue(val);\n if (!resolved) return;\n const text = resolved.text + (suffix || '');\n const tr = document.createElement('tr');\n const tdLabel = document.createElement('td');\n tdLabel.textContent = label;\n const tdVal = document.createElement('td');\n if (resolved.url) {\n const a = document.createElement('a');\n a.href = resolved.url;\n a.textContent = text;\n tdVal.appendChild(a);\n } else {\n tdVal.textContent = text;\n }\n tr.appendChild(tdLabel);\n tr.appendChild(tdVal);\n table.appendChild(tr);\n}\n\ninterface TagData {\n color?: string;\n display?: string;\n name?: string;\n}\n\nfunction _addTagsRow(table: HTMLTableElement, tags: TagData[] | undefined): void {\n if (!tags || !tags.length) return;\n const tr = document.createElement('tr');\n const tdLabel = document.createElement('td');\n tdLabel.textContent = 'Tags';\n const tdVal = document.createElement('td');\n tags.forEach(function (tag: TagData) {\n const badge = document.createElement('span');\n badge.className = 'badge';\n badge.style.cssText = 'margin-right:4px;margin-bottom:2px;';\n if (tag.color) {\n badge.style.background = '#' + tag.color;\n badge.style.color = '#fff';\n } else {\n badge.style.background = 'var(--tblr-border-color-translucent, rgba(0,0,0,0.1))';\n }\n badge.textContent = tag.display || tag.name || String(tag);\n tdVal.appendChild(badge);\n });\n tr.appendChild(tdLabel);\n tr.appendChild(tdVal);\n table.appendChild(tr);\n}\n\n// ---------------------------------------------------------------------------\n// Detail field definitions\n// ---------------------------------------------------------------------------\n\nconst DETAIL_FIELDS: Record = {\n structure: [\n ['Type', 'structure_type'],\n ['Site', 'site'],\n ['Elevation', 'elevation', ' m'],\n ['Height', 'height', ' m'],\n ['Width', 'width', ' m'],\n ['Length', 'length', ' m'],\n ['Depth', 'depth', ' m'],\n ['Tenant', 'tenant'],\n ['Installation Date', 'installation_date'],\n ['Access Notes', 'access_notes'],\n ['Comments', 'comments'],\n ],\n conduit_bank: [\n ['Start Structure', 'start_structure'],\n ['End Structure', 'end_structure'],\n ['Start Face', 'start_face'],\n ['End Face', 'end_face'],\n ['Configuration', 'configuration'],\n ['Total Conduits', 'total_conduits'],\n ['Encasement', 'encasement_type'],\n ['Length', 'length', ' m'],\n ['Tenant', 'tenant'],\n ['Installation Date', 'installation_date'],\n ['Comments', 'comments'],\n ],\n conduit: [\n ['Start Structure', 'start_structure'],\n ['End Structure', 'end_structure'],\n ['Start Location', 'start_location'],\n ['End Location', 'end_location'],\n ['Material', 'material'],\n ['Inner Diameter', 'inner_diameter', ' mm'],\n ['Outer Diameter', 'outer_diameter', ' mm'],\n ['Depth', 'depth', ' m'],\n ['Length', 'length', ' m'],\n ['Conduit Bank', 'conduit_bank'],\n ['Bank Position', 'bank_position'],\n ['Start Junction', 'start_junction'],\n ['End Junction', 'end_junction'],\n ['Cables Routed', 'cables_routed'],\n ['Tenant', 'tenant'],\n ['Installation Date', 'installation_date'],\n ['Comments', 'comments'],\n ],\n aerial: [\n ['Start Structure', 'start_structure'],\n ['End Structure', 'end_structure'],\n ['Start Location', 'start_location'],\n ['End Location', 'end_location'],\n ['Aerial Type', 'aerial_type'],\n ['Start Attachment Height', 'start_attachment_height', ' m'],\n ['End Attachment Height', 'end_attachment_height', ' m'],\n ['Attachment Height (mean)', 'attachment_height', ' m'],\n ['Sag', 'sag', ' m'],\n ['Messenger Size', 'messenger_size'],\n ['Wind Loading', 'wind_loading'],\n ['Ice Loading', 'ice_loading'],\n ['Length', 'length', ' m'],\n ['Cables Routed', 'cables_routed'],\n ['Tenant', 'tenant'],\n ['Installation Date', 'installation_date'],\n ['Comments', 'comments'],\n ],\n direct_buried: [\n ['Start Structure', 'start_structure'],\n ['End Structure', 'end_structure'],\n ['Start Location', 'start_location'],\n ['End Location', 'end_location'],\n ['Burial Depth', 'burial_depth', ' m'],\n ['Warning Tape', 'warning_tape'],\n ['Tracer Wire', 'tracer_wire'],\n ['Armor Type', 'armor_type'],\n ['Length', 'length', ' m'],\n ['Cables Routed', 'cables_routed'],\n ['Tenant', 'tenant'],\n ['Installation Date', 'installation_date'],\n ['Comments', 'comments'],\n ],\n circuit: [\n ['Circuit', 'circuit'],\n ['Provider Reference', 'provider_reference'],\n ['Comments', 'comments'],\n ],\n default: [\n ['Start Structure', 'start_structure'],\n ['End Structure', 'end_structure'],\n ['Start Location', 'start_location'],\n ['End Location', 'end_location'],\n ['Length', 'length', ' m'],\n ['Cables Routed', 'cables_routed'],\n ['Tenant', 'tenant'],\n ['Installation Date', 'installation_date'],\n ['Comments', 'comments'],\n ],\n};\n\n// ---------------------------------------------------------------------------\n// Enriched detail rendering\n// ---------------------------------------------------------------------------\n\n/** Create a collapsible section with chevron toggle. */\nfunction _createSection(title: string, parent: HTMLElement): HTMLElement {\n const header = document.createElement('div');\n header.className = 'pw-section-header';\n const chevron = document.createElement('i');\n chevron.className = 'mdi mdi-chevron-down';\n header.appendChild(chevron);\n const label = document.createElement('span');\n label.textContent = title;\n header.appendChild(label);\n parent.appendChild(header);\n\n const body = document.createElement('div');\n body.className = 'pw-section-body';\n parent.appendChild(body);\n\n header.addEventListener('click', function () {\n const collapsed = body.classList.toggle('collapsed');\n header.classList.toggle('collapsed', collapsed);\n });\n\n return body;\n}\n\n/** Add enrichment metric badges from API data to the badge row. */\nfunction _addEnrichmentBadges(data: Record, entry: FeatureEntry): void {\n const badgeRow = document.querySelector('.pw-metric-row');\n if (!badgeRow) return;\n\n // Add measurement badges based on feature type\n const metrics: [string, string, string][] = []; // [label, field, suffix]\n\n if (entry.featureType === 'structure') {\n if (data.elevation) metrics.push(['Elev', 'elevation', ' m']);\n } else {\n if (data.length) metrics.push(['Length', 'length', ' m']);\n }\n\n if (entry.featureType === 'conduit') {\n if (data.inner_diameter) metrics.push(['ID', 'inner_diameter', ' mm']);\n }\n\n metrics.forEach(function (m) {\n const resolved = _resolveValue(data[m[1]]);\n if (!resolved) return;\n const badge = document.createElement('span');\n badge.className = 'pw-metric-badge pw-metric-muted';\n badge.textContent = m[0] + ' ' + resolved.text + m[2];\n badgeRow.appendChild(badge);\n });\n}\n\nfunction _renderEnrichedDetail(data: Record, entry: FeatureEntry, container: HTMLElement): void {\n // Add metric badges from enriched data\n _addEnrichmentBadges(data, entry);\n\n // Check if this is an external layer feature\n const extCfg = getLayerConfig(entry.featureType);\n if (extCfg?.detail) {\n const sectionBody = _createSection('Details', container);\n const table = document.createElement('table');\n table.className = 'pw-detail-table';\n for (const fieldName of extCfg.detail.fields) {\n const val = data[fieldName];\n if (val !== undefined && val !== null) {\n _addFieldRow(table, _titleCase(fieldName.replace(/_/g, ' ')), val);\n }\n }\n sectionBody.appendChild(table);\n return;\n }\n\n const fields = DETAIL_FIELDS[entry.featureType] || DETAIL_FIELDS['default'];\n const table = document.createElement('table');\n table.className = 'pw-detail-table';\n\n fields.forEach(function (f: DetailFieldDef) {\n const val = data[f[1]];\n _addFieldRow(table, f[0], val, f[2] || '');\n });\n\n _addTagsRow(table, data.tags as TagData[] | undefined);\n\n if (table.childNodes.length > 0) {\n const sectionBody = _createSection('Details', container);\n sectionBody.appendChild(table);\n }\n\n // Timestamps\n if (data.created || data.last_updated) {\n const tsDiv = document.createElement('div');\n tsDiv.style.cssText = 'font-size:0.72em;color:var(--tblr-muted-color,#667382);margin-top:8px;';\n const parts: string[] = [];\n if (data.created) parts.push('Created ' + (data.created as string).split('T')[0]);\n if (data.last_updated) parts.push('Updated ' + (data.last_updated as string).split('T')[0]);\n tsDiv.textContent = parts.join(' \\u00b7 ');\n container.appendChild(tsDiv);\n }\n\n // For structures: show connected pathways\n if (entry.featureType === 'structure') {\n _fetchConnectedPathways(entry, container);\n }\n // For pathways: show connected structures\n if (entry.featureType !== 'structure') {\n _renderConnectedStructures(data, entry, container);\n }\n}\n\n/**\n * Render a clickable list item for a connected object.\n *\n * @param selectId Feature ID (e.g. \"structure-123\") used to look up in\n * viewport or navigate to if not found.\n * @param hintLatLng Approximate location for flyTo when feature is off-screen.\n */\nfunction _renderConnectedItem(\n parent: HTMLElement,\n name: string,\n color: string,\n typeLabel: string,\n mapFeature: FeatureEntry | undefined,\n selectId: string,\n hintLatLng?: { lat: number; lng: number },\n): void {\n const item = document.createElement('div');\n item.className = 'pw-list-item';\n item.style.padding = '6px 0';\n item.style.cursor = 'pointer';\n\n const dot = document.createElement('span');\n dot.className = 'pw-list-dot';\n dot.style.background = color;\n item.appendChild(dot);\n\n const label = document.createElement('span');\n label.className = 'pw-list-label';\n label.textContent = name || 'Unnamed';\n item.appendChild(label);\n\n const badge = document.createElement('span');\n badge.className = 'pw-metric-badge pw-metric-muted';\n badge.style.fontSize = '0.65em';\n badge.style.padding = '1px 6px';\n badge.textContent = typeLabel;\n item.appendChild(badge);\n\n item.addEventListener('click', function () {\n // Re-check viewport \u2014 features may have been loaded since render\n const current = _features.find(function (f: FeatureEntry) {\n return _featureId(f) === selectId;\n });\n if (current) {\n selectFeature(current);\n } else if (hintLatLng && _map) {\n // Fly to approximate location; data reload will auto-select\n _pendingFlySelectId = selectId;\n _map.flyTo(hintLatLng as L.LatLngExpression, 18, { duration: 0.5 });\n } else {\n // No cached location \u2014 full page navigation\n window.location.href = window.location.pathname + '?select=' + encodeURIComponent(selectId);\n }\n });\n\n parent.appendChild(item);\n}\n\n/** Fetch connected pathways and neighbor structures for a structure. */\nfunction _fetchConnectedPathways(entry: FeatureEntry, container: HTMLElement): void {\n const structId = entry.props.id;\n const base = API_BASE.replace(/geo\\/?$/, '');\n const url = base + 'pathways/?structure_id=' + structId;\n\n const headers: Record = { 'Accept': 'application/json' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n\n fetch(url, { headers }).then(function (resp) {\n return resp.ok ? resp.json() : null;\n }).then(function (data: { results?: Array> } | null) {\n if (!data || !data.results || data.results.length === 0) return;\n\n // Collect neighbor structures from pathway endpoints\n interface NeighborInfo { id: number; display: string; hint?: { lat: number; lng: number } }\n const neighborMap: Record = {};\n const pwItems: Array<{ display: string; color: string; typeLabel: string; mapFeature: FeatureEntry | undefined; selectId: string; hint?: { lat: number; lng: number } }> = [];\n\n data.results.forEach(function (pw: Record) {\n const pwType = pw.pathway_type as { value?: string; label?: string } | string || '';\n const typeKey = typeof pwType === 'object' ? (pwType.value || '') : pwType;\n const typeLabel = typeof pwType === 'object' ? (pwType.label || _titleCase(typeKey)) : _titleCase(typeKey);\n const color = PATHWAY_COLORS[typeKey] || '#616161';\n const display = (pw.display || pw.label || 'Unnamed') as string;\n\n const pwId = pw.id as number;\n const mapFeature = _features.find(function (f: FeatureEntry) {\n return f.featureType !== 'structure' && f.props.id === pwId;\n });\n pwItems.push({\n display, color, typeLabel, mapFeature,\n selectId: typeKey + '-' + pwId,\n hint: mapFeature ? mapFeature.latlng : undefined,\n });\n\n // Extract polyline endpoints for neighbor location hints\n let startHint: { lat: number; lng: number } | undefined;\n let endHint: { lat: number; lng: number } | undefined;\n if (mapFeature && mapFeature.layer) {\n try {\n const latlngs = (mapFeature.layer as L.Polyline).getLatLngs() as L.LatLng[];\n if (latlngs && latlngs.length >= 2) {\n startHint = { lat: latlngs[0].lat, lng: latlngs[0].lng };\n endHint = { lat: latlngs[latlngs.length - 1].lat, lng: latlngs[latlngs.length - 1].lng };\n }\n } catch (_e) { /* not a polyline */ }\n }\n\n // Extract the \"other\" structure as a neighbor\n const startS = pw.start_structure as { id?: number; display?: string } | null;\n const endS = pw.end_structure as { id?: number; display?: string } | null;\n if (startS && startS.id && startS.id !== structId && !neighborMap[startS.id]) {\n neighborMap[startS.id] = { id: startS.id, display: startS.display || String(startS.id), hint: startHint };\n }\n if (endS && endS.id && endS.id !== structId && !neighborMap[endS.id]) {\n neighborMap[endS.id] = { id: endS.id, display: endS.display || String(endS.id), hint: endHint };\n }\n });\n\n // --- Connected Structures (neighbors) \u2014 listed first ---\n const neighbors = Object.values(neighborMap);\n if (neighbors.length > 0) {\n const structSection = _createSection(\n 'Connected Structures (' + neighbors.length + ')',\n container,\n );\n neighbors.forEach(function (s) {\n const mf = _features.find(function (f: FeatureEntry) {\n return f.featureType === 'structure' && f.props.id === s.id;\n });\n _renderConnectedItem(structSection, s.display, '#2e7d32', 'Structure',\n mf, 'structure-' + s.id, s.hint,\n );\n });\n }\n\n // --- Connected Pathways ---\n const pwSection = _createSection(\n 'Connected Pathways (' + pwItems.length + ')',\n container,\n );\n pwItems.forEach(function (item) {\n _renderConnectedItem(pwSection, item.display, item.color, item.typeLabel,\n item.mapFeature, item.selectId, item.hint,\n );\n });\n\n // Dim unrelated features\n const connectedIds = new Set();\n neighbors.forEach(function (s) { connectedIds.add('structure-' + s.id); });\n pwItems.forEach(function (item) { connectedIds.add(item.selectId); });\n _dimFeatures(connectedIds);\n });\n}\n\n/** Render connected structures from the pathway's detail data. */\nfunction _renderConnectedStructures(data: Record, entry: FeatureEntry, container: HTMLElement): void {\n const structs: Array<{ id: number; display: string; url?: string }> = [];\n\n const startStruct = data.start_structure as { id?: number; display?: string; url?: string } | null;\n const endStruct = data.end_structure as { id?: number; display?: string; url?: string } | null;\n if (startStruct && startStruct.id) structs.push({\n id: startStruct.id,\n display: startStruct.display || String(startStruct.id),\n url: startStruct.url,\n });\n if (endStruct && endStruct.id && (!startStruct || endStruct.id !== startStruct.id)) structs.push({\n id: endStruct.id,\n display: endStruct.display || String(endStruct.id),\n url: endStruct.url,\n });\n\n if (structs.length === 0) return;\n\n const sectionBody = _createSection(\n 'Connected Structures (' + structs.length + ')',\n container,\n );\n\n // Extract hint coordinates from the current pathway's endpoints\n let startHint: { lat: number; lng: number } | undefined;\n let endHint: { lat: number; lng: number } | undefined;\n if (entry.layer) {\n try {\n const latlngs = (entry.layer as L.Polyline).getLatLngs() as L.LatLng[];\n if (latlngs && latlngs.length >= 2) {\n startHint = { lat: latlngs[0].lat, lng: latlngs[0].lng };\n endHint = { lat: latlngs[latlngs.length - 1].lat, lng: latlngs[latlngs.length - 1].lng };\n }\n } catch (_e) { /* not a polyline */ }\n }\n\n structs.forEach(function (s, idx) {\n const mf = _features.find(function (f: FeatureEntry) {\n return f.featureType === 'structure' && f.props.id === s.id;\n });\n const hint = idx === 0 ? startHint : endHint;\n\n _renderConnectedItem(sectionBody, s.display, '#2e7d32', 'Structure',\n mf, 'structure-' + s.id, hint,\n );\n });\n\n // Dim unrelated features\n const connectedIds = new Set();\n structs.forEach(function (s) { connectedIds.add('structure-' + s.id); });\n _dimFeatures(connectedIds);\n}\n\n// ---------------------------------------------------------------------------\n// Detail fetch (async/await with fetch)\n// ---------------------------------------------------------------------------\n\nasync function _fetchDetail(entry: FeatureEntry, container: HTMLElement): Promise {\n const cacheKey = _featureId(entry);\n\n // Check for HTML fragment URL from external layer config\n const extCfg = getLayerConfig(entry.featureType);\n const htmlUrl = _resolveDetailUrl(extCfg, entry);\n\n if (htmlUrl) {\n // HTML fragment mode \u2014 check cache (stored as string under a prefixed key)\n const htmlCacheKey = 'html:' + cacheKey;\n if (_detailCache[htmlCacheKey]) {\n _setTrustedHtml(container, _detailCache[htmlCacheKey] as unknown as string);\n return;\n }\n await _fetchHtmlDetail(htmlUrl, htmlCacheKey, container);\n return;\n }\n\n // JSON mode \u2014 existing behavior\n if (_detailCache[cacheKey]) {\n _renderEnrichedDetail(_detailCache[cacheKey], entry, container);\n return;\n }\n\n const url = _apiUrlForFeature(entry);\n\n // No detail URL available \u2014 render GeoJSON properties directly\n if (!url) {\n const table = document.createElement('table');\n table.className = 'pw-detail-table';\n const props = entry.props as Record;\n for (const key in props) {\n if (!props.hasOwnProperty(key)) continue;\n if (key === 'id' || key === 'url') continue;\n _addFieldRow(table, _titleCase(key.replace(/_/g, ' ')), props[key]);\n }\n container.appendChild(table);\n return;\n }\n\n const loadingDiv = document.createElement('div');\n loadingDiv.className = 'pw-detail-loading';\n loadingDiv.textContent = 'Loading details...';\n container.appendChild(loadingDiv);\n const headers: Record = { 'Accept': 'application/json' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n\n try {\n const response = await fetch(url, { headers });\n container.textContent = '';\n if (response.ok) {\n const data = await response.json() as Record;\n _detailCache[cacheKey] = data;\n _renderEnrichedDetail(data, entry, container);\n } else {\n const errDiv = document.createElement('div');\n errDiv.className = 'pw-detail-loading';\n errDiv.textContent = 'Could not load details (HTTP ' + response.status + ')';\n container.appendChild(errDiv);\n }\n } catch (_e) {\n container.textContent = '';\n const errDiv = document.createElement('div');\n errDiv.className = 'pw-detail-loading';\n errDiv.textContent = 'Network error';\n container.appendChild(errDiv);\n }\n}\n\n/** Resolve HTML detail URL from external layer config, if available. */\nfunction _resolveDetailUrl(\n extCfg: import('./types/external').ExternalLayerConfig | undefined,\n entry: FeatureEntry,\n): string {\n if (!extCfg?.detail?.detailUrl) return '';\n return extCfg.detail.detailUrl.replace('{id}', String(entry.props.id));\n}\n\n/**\n * Inject trusted HTML into a container element.\n *\n * SECURITY: This HTML is fetched from same-origin endpoints served by\n * registered NetBox plugins \u2014 the same trust model as Django's\n * PluginTemplateExtension. Only installed plugin code can register\n * detail_url endpoints via the map layer registry.\n */\nfunction _setTrustedHtml(container: HTMLElement, html: string): void {\n container.innerHTML = html; // eslint-disable-line no-unsanitized/property\n}\n\n/** Fetch an HTML fragment and inject it into the container. */\nasync function _fetchHtmlDetail(\n url: string,\n cacheKey: string,\n container: HTMLElement,\n): Promise {\n const loadingDiv = document.createElement('div');\n loadingDiv.className = 'pw-detail-loading';\n loadingDiv.textContent = 'Loading details...';\n container.appendChild(loadingDiv);\n\n const headers: Record = { 'Accept': 'text/html' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n\n try {\n const response = await fetch(url, { headers });\n container.textContent = '';\n if (response.ok) {\n const html = await response.text();\n _detailCache[cacheKey] = html as unknown as Record;\n _setTrustedHtml(container, html);\n } else {\n const errDiv = document.createElement('div');\n errDiv.className = 'pw-detail-loading';\n errDiv.textContent = 'Could not load details (HTTP ' + response.status + ')';\n container.appendChild(errDiv);\n }\n } catch (_e) {\n container.textContent = '';\n const errDiv = document.createElement('div');\n errDiv.className = 'pw-detail-loading';\n errDiv.textContent = 'Network error';\n container.appendChild(errDiv);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Detail panel rendering\n// ---------------------------------------------------------------------------\n\nfunction _renderDetail(entry: FeatureEntry): void {\n const body = document.getElementById('pw-detail-body');\n if (!body) return;\n body.textContent = '';\n const p = entry.props;\n\n // Title with inline edit\n const titleRow = document.createElement('div');\n titleRow.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:8px;';\n\n const title = document.createElement('div');\n title.className = 'pw-detail-title';\n title.style.marginBottom = '0';\n title.textContent = p.name || 'Unnamed';\n titleRow.appendChild(title);\n\n const editBtn = document.createElement('button');\n editBtn.className = 'pw-edit-btn';\n editBtn.title = 'Edit name';\n const pencilIcon = document.createElement('i');\n pencilIcon.className = 'mdi mdi-pencil';\n editBtn.appendChild(pencilIcon);\n titleRow.appendChild(editBtn);\n\n body.appendChild(titleRow);\n\n // Inline edit form (hidden)\n const editForm = document.createElement('div');\n editForm.className = 'pw-inline-edit';\n const editInput = document.createElement('input');\n editInput.type = 'text';\n editInput.className = 'form-control form-control-sm';\n editInput.value = p.name || '';\n editInput.style.flex = '1';\n editForm.appendChild(editInput);\n\n const saveBtn = document.createElement('button');\n saveBtn.className = 'btn btn-sm btn-primary';\n saveBtn.textContent = 'Save';\n editForm.appendChild(saveBtn);\n\n const cancelEditBtn = document.createElement('button');\n cancelEditBtn.className = 'btn btn-sm btn-outline-secondary';\n cancelEditBtn.textContent = '\\u00d7';\n editForm.appendChild(cancelEditBtn);\n\n body.appendChild(editForm);\n\n editBtn.addEventListener('click', function () {\n editForm.classList.add('active');\n titleRow.style.display = 'none';\n editInput.focus();\n editInput.select();\n });\n\n cancelEditBtn.addEventListener('click', function () {\n editForm.classList.remove('active');\n titleRow.style.display = '';\n });\n\n saveBtn.addEventListener('click', function () {\n const newName = editInput.value.trim();\n if (!newName || newName === p.name) {\n cancelEditBtn.click();\n return;\n }\n const url = _apiUrlForFeature(entry);\n const patchHeaders: Record = {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) patchHeaders['X-CSRFToken'] = csrfToken;\n\n fetch(url, {\n method: 'PATCH',\n headers: patchHeaders,\n body: JSON.stringify({ name: newName }),\n }).then(function (response) {\n if (response.ok) {\n p.name = newName;\n title.textContent = newName;\n delete _detailCache[_featureId(entry)];\n _applyFilters();\n }\n editForm.classList.remove('active');\n titleRow.style.display = '';\n });\n });\n\n editInput.addEventListener('keydown', function (e: KeyboardEvent) {\n if (e.key === 'Enter') saveBtn.click();\n if (e.key === 'Escape') cancelEditBtn.click();\n });\n\n // Metric badge row\n const color = _colorForFeature(entry);\n const typeKey = _typeKeyForFeature(entry);\n const badgeRow = document.createElement('div');\n badgeRow.className = 'pw-metric-row';\n\n const typeBadge = document.createElement('span');\n typeBadge.className = 'pw-metric-badge';\n typeBadge.style.background = color;\n typeBadge.style.color = '#fff';\n typeBadge.textContent = _titleCase(typeKey);\n badgeRow.appendChild(typeBadge);\n\n if (entry.featureType === 'structure' && p.site_name) {\n const siteBadge = document.createElement('span');\n siteBadge.className = 'pw-metric-badge pw-metric-muted';\n siteBadge.textContent = p.site_name as string;\n badgeRow.appendChild(siteBadge);\n }\n\n body.appendChild(badgeRow);\n\n // View Details button\n const detailUrl = _detailPageUrl(entry);\n if (detailUrl) {\n const link = document.createElement('a');\n link.href = detailUrl;\n link.className = 'btn btn-sm btn-primary w-100 mb-3';\n const icon = document.createElement('i');\n icon.className = 'mdi mdi-open-in-new';\n link.appendChild(icon);\n link.appendChild(document.createTextNode(' View Details '));\n body.appendChild(link);\n }\n\n // Plan Route button (structures only)\n if (entry.featureType === 'structure') {\n const planLink = document.createElement('a');\n planLink.href = '/plugins/pathways/route-planner/?start=' + p.id;\n planLink.className = 'btn btn-sm btn-outline-primary w-100 mb-3';\n const planIcon = document.createElement('i');\n planIcon.className = 'mdi mdi-map-search-outline';\n planLink.appendChild(planIcon);\n planLink.appendChild(document.createTextNode(' Plan Route From Here'));\n body.appendChild(planLink);\n }\n\n // Fetch enriched detail from REST API\n const detailContainer = document.createElement('div');\n body.appendChild(detailContainer);\n _fetchDetail(entry, detailContainer);\n}\n\n// ---------------------------------------------------------------------------\n// Server-side search fallback\n// ---------------------------------------------------------------------------\n\nfunction _showSearchingIndicator(): void {\n const listEl = document.getElementById('pw-feature-list');\n if (!listEl) return;\n const indicator = document.createElement('div');\n indicator.className = 'pw-server-search-status';\n indicator.id = 'pw-server-searching';\n const icon = document.createElement('i');\n icon.className = 'mdi mdi-magnify mdi-spin';\n indicator.appendChild(icon);\n indicator.appendChild(document.createTextNode(' Searching all features\\u2026'));\n listEl.appendChild(indicator);\n}\n\nfunction _clearServerResults(): void {\n const el = document.getElementById('pw-server-searching');\n if (el) el.remove();\n const hdr = document.getElementById('pw-server-results-header');\n if (hdr) hdr.remove();\n const items = document.querySelectorAll('.pw-server-result');\n for (let i = 0; i < items.length; i++) items[i].remove();\n}\n\nfunction setServerResults(results: ServerSearchResult[]): void {\n // Remove the \"searching\" indicator\n const searching = document.getElementById('pw-server-searching');\n if (searching) searching.remove();\n\n const listEl = document.getElementById('pw-feature-list');\n if (!listEl) return;\n\n // Remove any previous server results\n const oldHdr = document.getElementById('pw-server-results-header');\n if (oldHdr) oldHdr.remove();\n const oldItems = document.querySelectorAll('.pw-server-result');\n for (let i = 0; i < oldItems.length; i++) oldItems[i].remove();\n\n if (results.length === 0) {\n const noResults = document.createElement('div');\n noResults.className = 'pw-server-search-status pw-server-result';\n noResults.textContent = 'No matching features found.';\n listEl.appendChild(noResults);\n return;\n }\n\n const header = document.createElement('div');\n header.id = 'pw-server-results-header';\n header.className = 'pw-server-search-status';\n const hdrIcon = document.createElement('i');\n hdrIcon.className = 'mdi mdi-earth';\n header.appendChild(hdrIcon);\n header.appendChild(document.createTextNode(\n ' ' + results.length + ' result' + (results.length !== 1 ? 's' : '') + ' outside current view'\n ));\n listEl.appendChild(header);\n\n results.forEach(function (result: ServerSearchResult) {\n const item = document.createElement('div');\n item.className = 'pw-list-item pw-server-result';\n\n const dot = document.createElement('span');\n dot.className = 'pw-list-dot';\n const color = result.featureType === 'structure'\n ? (STRUCTURE_COLORS[result.typeKey] || '#616161')\n : (PATHWAY_COLORS[result.typeKey] || '#616161');\n dot.style.background = color;\n item.appendChild(dot);\n\n const label = document.createElement('span');\n label.className = 'pw-list-label';\n label.textContent = result.name || 'Unnamed';\n label.title = result.name || 'Unnamed';\n item.appendChild(label);\n\n const typeBadge = document.createElement('span');\n typeBadge.className = 'pw-list-type';\n typeBadge.textContent = _titleCase(result.typeKey);\n item.appendChild(typeBadge);\n\n const goIcon = document.createElement('i');\n goIcon.className = 'mdi mdi-crosshairs-gps';\n goIcon.style.cssText = 'color:var(--tblr-muted-color,#667382);flex-shrink:0;';\n goIcon.title = 'Go to location';\n item.appendChild(goIcon);\n\n item.addEventListener('click', function () {\n if (_map) {\n if (result.url) {\n _pendingSelect = { url: result.url, featureType: result.featureType };\n }\n _map.flyTo(result.latlng, 18, { duration: 0.8 });\n }\n });\n\n listEl.appendChild(item);\n });\n}\n\nfunction onServerSearch(cb: (query: string) => void): void {\n _serverSearchCallback = cb;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nfunction init(map: L.Map, kiosk?: boolean): void {\n _isKiosk = !!kiosk;\n _map = map;\n\n const toggleBtn = document.getElementById('pw-sidebar-toggle');\n if (toggleBtn) {\n toggleBtn.addEventListener('click', function () {\n _setListBodyVisible(_isCollapsed());\n });\n }\n\n const backBtn = document.getElementById('pw-detail-back');\n if (backBtn) {\n backBtn.addEventListener('click', function () {\n showList();\n });\n }\n\n document.addEventListener('keydown', function (e: KeyboardEvent) {\n if (e.key === 'Escape') {\n const detailPanel = document.getElementById('pw-panel-detail');\n if (detailPanel && detailPanel.style.display !== 'none') {\n showList();\n }\n _unhighlightMapFeature();\n _restoreDim();\n _selected = null;\n _highlightListItem(null);\n if (_isKiosk) _kioskSidebarClose();\n if (_onSelectionChange) _onSelectionChange();\n }\n });\n\n const searchInput = document.getElementById('pw-search');\n if (searchInput) {\n searchInput.addEventListener('input', _debounce(function () {\n _applyFilters();\n }, 150));\n }\n\n map.on('click', function (e: L.LeafletMouseEvent) {\n if (!(e.originalEvent as any)._sidebarClick) {\n _unhighlightMapFeature();\n _restoreDim();\n _selected = null;\n _highlightListItem(null);\n const detailPanel = document.getElementById('pw-panel-detail');\n if (detailPanel && detailPanel.style.display !== 'none') {\n showList();\n }\n if (_onSelectionChange) _onSelectionChange();\n }\n });\n\n if (_isKiosk) {\n const kioskCloseBtn = document.getElementById('pw-kiosk-close');\n if (kioskCloseBtn) {\n kioskCloseBtn.addEventListener('click', function () {\n _kioskSidebarClose();\n const detailPanel = document.getElementById('pw-panel-detail');\n if (detailPanel) detailPanel.style.display = 'none';\n });\n }\n document.addEventListener('keydown', function (e: KeyboardEvent) {\n if (e.key === 'Escape') {\n _kioskSidebarClose();\n const detailPanel = document.getElementById('pw-panel-detail');\n if (detailPanel) detailPanel.style.display = 'none';\n }\n });\n }\n}\n\nfunction _setListBodyVisible(visible: boolean): void {\n const body = document.getElementById('pw-panel-list-body');\n const chevron = document.getElementById('pw-sidebar-chevron');\n if (body) body.classList.toggle('collapsed', !visible);\n if (chevron) chevron.classList.toggle('collapsed', !visible);\n}\n\nfunction _kioskSidebarOpen(): void {\n const sidebar = document.getElementById('pw-sidebar');\n if (!sidebar) return;\n sidebar.classList.add('pw-sidebar-open');\n}\n\nfunction _kioskSidebarClose(): void {\n const sidebar = document.getElementById('pw-sidebar');\n if (!sidebar) return;\n sidebar.classList.remove('pw-sidebar-open');\n}\n\nfunction _isCollapsed(): boolean {\n const body = document.getElementById('pw-panel-list-body');\n return body ? body.classList.contains('collapsed') : false;\n}\n\nfunction show(): void {\n if (_isKiosk) {\n _kioskSidebarOpen();\n return;\n }\n const sidebar = document.getElementById('pw-sidebar');\n if (sidebar) sidebar.classList.remove('pw-sidebar-hidden');\n _setListBodyVisible(true);\n}\n\nfunction hide(): void {\n if (_isKiosk) { _kioskSidebarClose(); return; }\n const sidebar = document.getElementById('pw-sidebar');\n if (sidebar) sidebar.classList.add('pw-sidebar-hidden');\n}\n\nfunction setFeatures(features: FeatureEntry[]): void {\n _features = features;\n // Cap cache size instead of clearing on every pan/zoom\n const cacheKeys = Object.keys(_detailCache);\n if (cacheKeys.length > 200) {\n cacheKeys.slice(0, 100).forEach(function (k: string) { delete _detailCache[k]; });\n }\n\n // Preserve selection across data reloads (e.g. panTo triggers moveend)\n if (_selected) {\n const selId = _featureId(_selected);\n let found: FeatureEntry | null = null;\n for (let i = 0; i < features.length; i++) {\n if (_featureId(features[i]) === selId) {\n found = features[i];\n break;\n }\n }\n if (found) {\n _selected = found;\n _buildTypeFilters();\n _applyFilters();\n _applyDim();\n return;\n }\n _restoreDim();\n _selected = null;\n }\n\n _buildTypeFilters();\n _applyFilters();\n if (!_isKiosk) showList();\n\n // Resolve pending selection from server search result click\n if (_pendingSelect && features.length > 0) {\n const pending = _pendingSelect;\n for (let i = 0; i < features.length; i++) {\n if (features[i].props.url === pending.url) {\n _pendingSelect = null;\n // Clear the search input so client filter doesn't hide the match\n const searchInput = document.getElementById('pw-search') as HTMLInputElement | null;\n if (searchInput) searchInput.value = '';\n _lastServerQuery = '';\n _clearServerResults();\n _applyFilters();\n selectFeature(features[i]);\n return;\n }\n }\n }\n\n // Resolve pending fly-to-select (from connected item click)\n if (_pendingFlySelectId && features.length > 0) {\n const id = _pendingFlySelectId;\n _pendingFlySelectId = '';\n for (let i = 0; i < features.length; i++) {\n if (_featureId(features[i]) === id) {\n selectFeature(features[i]);\n return;\n }\n }\n }\n}\n\nfunction showList(): void {\n if (_isKiosk) _kioskSidebarOpen();\n _setListBodyVisible(true);\n const detailPanel = document.getElementById('pw-panel-detail');\n if (detailPanel) detailPanel.style.display = 'none';\n}\n\nfunction showDetail(entry: FeatureEntry): void {\n if (_isKiosk) _kioskSidebarOpen();\n _setListBodyVisible(false);\n const detailPanel = document.getElementById('pw-panel-detail');\n if (detailPanel) detailPanel.style.display = '';\n\n // Set panel heading (e.g. \"Structure Details\")\n const heading = document.getElementById('pw-detail-heading');\n if (heading) {\n heading.textContent = _typeLabel(entry.featureType) + ' Details';\n }\n\n _renderDetail(entry);\n}\n\nfunction selectFeature(entry: FeatureEntry): void {\n _restoreDim();\n _selected = entry;\n _highlightListItem(entry);\n _highlightMapFeature(entry);\n showDetail(entry);\n if (_map && entry.latlng) {\n const zoom = _map.getZoom();\n // Zoom past disableClusteringAtZoom (18) for structures so the\n // individual marker is visible, not hidden inside a cluster.\n const minZoom = entry.featureType === 'structure' ? 18 : 16;\n if (zoom < minZoom) {\n _map.flyTo(entry.latlng, minZoom, { duration: 0.5 });\n } else {\n _map.panTo(entry.latlng);\n }\n }\n if (_onSelectionChange) _onSelectionChange();\n}\n\nfunction onFeatureCreated(entry: FeatureEntry): void {\n if (!_selected) return;\n if (_featureId(entry) === _featureId(_selected)) {\n _selected = entry;\n _reapplyHighlight(entry);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Dependency injection\n// ---------------------------------------------------------------------------\n\nexport interface SidebarDeps {\n titleCase: (s: string) => string;\n esc: (s: string) => string;\n debounce: (fn: () => void, delay: number) => () => void;\n getCookie: (name: string) => string | null;\n structureColors: Record;\n structureShapes: Record;\n pathwayColors: Record;\n apiBase: string;\n}\n\nfunction setDeps(deps: SidebarDeps): void {\n _titleCase = deps.titleCase;\n _esc = deps.esc;\n _debounce = deps.debounce;\n _getCookie = deps.getCookie;\n STRUCTURE_COLORS = deps.structureColors;\n STRUCTURE_SHAPES = deps.structureShapes;\n PATHWAY_COLORS = deps.pathwayColors;\n API_BASE = deps.apiBase;\n}\n\n// ---------------------------------------------------------------------------\n// Export\n// ---------------------------------------------------------------------------\n\n/** Return the ID string of the selected feature, or empty string. */\nfunction getSelectedId(): string {\n return _selected ? _featureId(_selected) : '';\n}\n\n/** Select a feature by its ID string (e.g. \"structure-123\"). */\nfunction selectById(id: string): boolean {\n if (!id) return false;\n for (let i = 0; i < _features.length; i++) {\n if (_featureId(_features[i]) === id) {\n selectFeature(_features[i]);\n return true;\n }\n }\n return false;\n}\n\nfunction onSelectionChange(cb: () => void): void {\n _onSelectionChange = cb;\n}\n\nexport const Sidebar = {\n init,\n show,\n hide,\n setFeatures,\n showList,\n showDetail,\n selectFeature,\n selectById,\n getSelectedId,\n onFeatureCreated,\n onSelectionChange,\n setDeps,\n onServerSearch,\n setServerResults,\n};\n", "/**\n * Hover popover module for the full-page infrastructure map.\n *\n * Shows a lightweight tooltip with feature name and type on hover,\n * positioned near the cursor within the map container.\n */\n\nimport type { GeoJSONProperties } from './types/features';\n\n// ---------------------------------------------------------------------------\n// Module-level helpers injected via setDeps\n// ---------------------------------------------------------------------------\n\nlet _titleCase: (s: string) => string;\n\n// ---------------------------------------------------------------------------\n// Internal state\n// ---------------------------------------------------------------------------\n\nlet _el: HTMLDivElement | null = null;\nlet _map: L.Map | null = null;\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction _position(latlng: L.LatLng): void {\n if (!_map || !_el) return;\n const pt = _map.latLngToContainerPoint(latlng);\n const cw = _map.getContainer().clientWidth;\n let x = pt.x + 14;\n let y = pt.y - 10;\n if (x + 200 > cw) x = pt.x - 200;\n if (y < 0) y = pt.y + 20;\n _el.style.left = x + 'px';\n _el.style.top = y + 'px';\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nfunction init(map: L.Map): void {\n _map = map;\n _el = document.createElement('div');\n _el.className = 'pw-popover';\n _el.style.display = 'none';\n map.getContainer().appendChild(_el);\n}\n\nfunction show(latlng: L.LatLng, props: GeoJSONProperties, popoverFields?: string[]): void {\n if (!_el) return;\n _el.textContent = '';\n\n // Name line\n const name = document.createElement('span');\n name.className = 'pw-popover-name';\n if (popoverFields && popoverFields.length > 0) {\n name.textContent = String(props[popoverFields[0]] ?? props.name ?? `#${props.id}`);\n } else {\n name.textContent = props.name || 'Unnamed';\n }\n _el.appendChild(name);\n\n // Type line\n let typeText = '';\n if (popoverFields && popoverFields.length > 1) {\n typeText = popoverFields.slice(1)\n .map(f => String(props[f] ?? ''))\n .filter(Boolean)\n .join(' / ');\n } else {\n const t = props.structure_type || props.pathway_type || '';\n typeText = t ? _titleCase(t) : '';\n }\n if (typeText) {\n const type = document.createElement('span');\n type.className = 'pw-popover-type';\n type.textContent = typeText;\n _el.appendChild(type);\n }\n\n _position(latlng);\n _el.style.display = '';\n}\n\nfunction hide(): void {\n if (_el) _el.style.display = 'none';\n}\n\n// ---------------------------------------------------------------------------\n// Dependency injection\n// ---------------------------------------------------------------------------\n\nexport interface PopoverDeps {\n titleCase: (s: string) => string;\n}\n\nfunction setDeps(deps: PopoverDeps): void {\n _titleCase = deps.titleCase;\n}\n\n// ---------------------------------------------------------------------------\n// Export\n// ---------------------------------------------------------------------------\n\nexport const Popover = {\n init,\n show,\n hide,\n setDeps,\n};\n", "/**\n * Pure utility functions and constants extracted from pathways-map.ts.\n *\n * These are used by pathways-map, sidebar, popover, and tests.\n */\n\n// ---------------------------------------------------------------------------\n// Color & Icon Maps\n// ---------------------------------------------------------------------------\n\nexport const STRUCTURE_COLORS: Record = {\n 'pole': '#2e7d32', 'manhole': '#1565c0', 'handhole': '#00838f',\n 'cabinet': '#e65100', 'vault': '#6a1b9a', 'pedestal': '#f9a825',\n 'building_entrance': '#c62828', 'splice_closure': '#795548',\n 'tower': '#b71c1c', 'roof': '#616161', 'equipment_room': '#00796b',\n 'telecom_closet': '#283593', 'riser_room': '#ad1457',\n};\n\nexport const STRUCTURE_SHAPES: Record = {\n 'pole': '',\n 'manhole': '',\n 'handhole': '',\n 'cabinet': '',\n 'vault': '',\n 'pedestal': '',\n 'building_entrance': '',\n 'splice_closure': '',\n 'tower': '',\n 'roof': '',\n 'equipment_room': '',\n 'telecom_closet': '',\n 'riser_room': '',\n};\n\nexport const PATHWAY_COLORS: Record = {\n 'conduit': '#f57c00', 'conduit_bank': '#ad1457', 'aerial': '#1565c0',\n 'direct_buried': '#616161', 'innerduct': '#e65100', 'microduct': '#6a1b9a',\n 'tray': '#2e7d32', 'raceway': '#00838f', 'submarine': '#1a237e',\n};\n\nexport const PATHWAY_DASH: Record = {\n 'conduit': '5,5', 'conduit_bank': '', 'aerial': '10,5',\n 'direct_buried': '2,4', 'innerduct': '8,3', 'microduct': '1,3',\n 'tray': '', 'raceway': '12,4', 'submarine': '6,2,2,2',\n};\n\nexport interface PathwayStyleDef {\n color: string;\n weight: number;\n opacity: number;\n dashArray: string;\n}\n\n/** Return the polyline style for a given pathway_type key. */\nexport function pathwayStyle(pathwayType: string): PathwayStyleDef {\n const color = PATHWAY_COLORS[pathwayType] || '#888';\n const dash = PATHWAY_DASH[pathwayType] || '';\n const isBanks = pathwayType === 'conduit_bank';\n return {\n color,\n weight: isBanks ? 5 : 3,\n opacity: isBanks ? 0.8 : 0.7,\n dashArray: dash,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Marker helpers\n// ---------------------------------------------------------------------------\n\nexport function structureIcon(type: string, size = 20): L.DivIcon {\n const color = STRUCTURE_COLORS[type] || '#616161';\n const shape = STRUCTURE_SHAPES[type] || '';\n const isOutline = shape.includes('fill=\"none\"');\n const half = size / 2;\n return L.divIcon({\n className: 'pw-marker',\n html: '' + shape + '',\n iconSize: [size, size] as [number, number],\n iconAnchor: [half, half] as [number, number],\n popupAnchor: [0, -(half + 2)] as [number, number],\n });\n}\n\nexport function clusterIcon(count: number): L.DivIcon {\n let cls: string;\n let size: number;\n if (count < 10) {\n cls = 'pw-cluster-small'; size = 34;\n } else if (count < 100) {\n cls = 'pw-cluster-medium'; size = 40;\n } else {\n cls = 'pw-cluster-large'; size = 46;\n }\n return L.divIcon({\n className: 'pw-server-cluster',\n html: '
' +\n count + '
',\n iconSize: [size, size] as [number, number],\n iconAnchor: [size / 2, size / 2] as [number, number],\n });\n}\n\n// ---------------------------------------------------------------------------\n// Utility helpers\n// ---------------------------------------------------------------------------\n\nexport function esc(text: string): string {\n const el = document.createElement('span');\n el.textContent = text;\n return el.innerHTML;\n}\n\nexport function titleCase(str: string): string {\n return (str || '').replace(/_/g, ' ').replace(/\\b\\w/g, function (c: string) { return c.toUpperCase(); });\n}\n\nexport function getCookie(name: string): string | null {\n const value = '; ' + document.cookie;\n const parts = value.split('; ' + name + '=');\n if (parts.length === 2) return parts.pop()!.split(';').shift() || null;\n return null;\n}\n\nexport function bboxParam(map: L.Map): string {\n const b = map.getBounds();\n return b.getWest() + ',' + b.getSouth() + ',' + b.getEast() + ',' + b.getNorth();\n}\n\nexport function debounce(fn: () => void, delay: number): () => void {\n let timer: ReturnType;\n return function () {\n clearTimeout(timer);\n timer = setTimeout(fn, delay);\n };\n}\n\nexport function haversine(lat1: number, lon1: number, lat2: number, lon2: number): number {\n const R = 6371000;\n const p1 = lat1 * Math.PI / 180;\n const p2 = lat2 * Math.PI / 180;\n const dp = (lat2 - lat1) * Math.PI / 180;\n const dl = (lon2 - lon1) * Math.PI / 180;\n const a = Math.sin(dp / 2) * Math.sin(dp / 2) +\n Math.cos(p1) * Math.cos(p2) * Math.sin(dl / 2) * Math.sin(dl / 2);\n return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n}\n", "/**\n * Map core: map creation, base layers, overlays, and controls.\n *\n * Shared between the full-page infrastructure map (pathways-map.ts)\n * and the route planner map (route-planner-map.ts).\n */\n\nimport {\n STRUCTURE_COLORS,\n STRUCTURE_SHAPES,\n PATHWAY_COLORS,\n PATHWAY_DASH,\n titleCase as _titleCase,\n} from './map-utils';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst CFG: Partial = window.PATHWAYS_CONFIG || {};\nconst MAX_NATIVE_ZOOM: number = CFG.maxNativeZoom || 19;\n\n// ---------------------------------------------------------------------------\n// Base layers\n// ---------------------------------------------------------------------------\n\ninterface BaseLayerDef {\n name: string;\n url: string;\n attribution?: string;\n maxNativeZoom?: number;\n tileSize?: number;\n zoomOffset?: number;\n}\n\nconst DEFAULT_BASE_LAYERS: BaseLayerDef[] = [\n {\n name: 'Street',\n url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n attribution: '© OpenStreetMap contributors',\n maxNativeZoom: MAX_NATIVE_ZOOM,\n },\n {\n name: 'Satellite',\n url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',\n attribution: 'Esri World Imagery',\n maxNativeZoom: 19,\n },\n];\n\nexport function createBaseLayers(): Record {\n const configured = (CFG.baseLayers || []).filter(function (c: BaseLayerConfig) { return !!c.url; });\n const configs: BaseLayerDef[] = configured.length ? configured : DEFAULT_BASE_LAYERS;\n const layers: Record = {};\n configs.forEach(function (cfg: BaseLayerDef) {\n layers[cfg.name] = L.tileLayer(cfg.url, {\n attribution: cfg.attribution || '',\n maxNativeZoom: cfg.maxNativeZoom || MAX_NATIVE_ZOOM,\n maxZoom: 22,\n tileSize: cfg.tileSize || 256,\n zoomOffset: cfg.zoomOffset || 0,\n });\n });\n return layers;\n}\n\n// ---------------------------------------------------------------------------\n// User-configured overlays\n// ---------------------------------------------------------------------------\n\nexport function createUserOverlays(): Record {\n const userOverlays = CFG.overlays || [];\n const overlays: Record = {};\n userOverlays.forEach(function (cfg: OverlayConfig) {\n let layer: L.TileLayer | L.TileLayer.WMS;\n if (cfg.type === 'wms') {\n layer = L.tileLayer.wms(cfg.url, {\n layers: (cfg['layers'] as string) || '',\n format: (cfg['format'] as string) || 'image/png',\n transparent: cfg['transparent'] !== false,\n attribution: (cfg['attribution'] as string) || '',\n maxZoom: 22,\n });\n } else {\n layer = L.tileLayer(cfg.url, {\n attribution: (cfg['attribution'] as string) || '',\n maxZoom: (cfg['maxZoom'] as number) || 22,\n maxNativeZoom: (cfg['maxNativeZoom'] as number) || undefined,\n });\n }\n overlays[cfg.name] = layer;\n });\n return overlays;\n}\n\n// ---------------------------------------------------------------------------\n// Zoom hint overlay\n// ---------------------------------------------------------------------------\n\nexport function createZoomHint(map: L.Map): HTMLDivElement {\n const div = L.DomUtil.create('div', 'pathways-zoom-hint') as HTMLDivElement;\n div.style.cssText =\n 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);' +\n 'z-index:800;padding:12px 24px;border-radius:8px;font-size:14px;' +\n 'pointer-events:none;text-align:center;' +\n 'background:rgba(0,0,0,0.7);color:#fff;';\n div.textContent = 'Zoom in to see infrastructure data';\n map.getContainer().appendChild(div);\n return div;\n}\n\n// ---------------------------------------------------------------------------\n// Legend control\n// ---------------------------------------------------------------------------\n\n/**\n * Inject trusted static SVG markup into an element.\n *\n * Security: All SVG strings passed to this helper originate from compile-time\n * constants (STRUCTURE_SHAPES, PATHWAY_COLORS, PATHWAY_DASH) defined in this\n * file -- no user/network input is involved. This is the same trust model as\n * structureIcon() which also builds innerHTML from these constants.\n */\nfunction _setStaticSvg(el: HTMLElement, svg: string): void {\n el.innerHTML = svg; // eslint-disable-line no-unsanitized/property -- trusted compile-time SVG constants only\n}\n\nexport function createLegend(map: L.Map): void {\n const LegendControl = L.Control.extend({\n options: { position: 'bottomleft' },\n onAdd: function () {\n const container = L.DomUtil.create('div', 'pw-legend leaflet-bar');\n L.DomEvent.disableClickPropagation(container);\n L.DomEvent.disableScrollPropagation(container);\n\n // Header -- collapsed by default\n const header = L.DomUtil.create('div', 'pw-legend-header collapsed', container);\n const chevron = document.createElement('i');\n chevron.className = 'mdi mdi-chevron-down';\n header.appendChild(chevron);\n const titleSpan = document.createElement('span');\n titleSpan.textContent = 'Legend';\n header.appendChild(titleSpan);\n\n // Body -- collapsed by default\n const body = L.DomUtil.create('div', 'pw-legend-body collapsed', container);\n\n // Structures section\n const structSec = L.DomUtil.create('div', 'pw-legend-section', body);\n const structTitle = L.DomUtil.create('div', 'pw-legend-section-title', structSec);\n structTitle.textContent = 'Structures';\n\n const structTypes = ['pole', 'manhole', 'handhole', 'cabinet', 'vault',\n 'pedestal', 'building_entrance', 'splice_closure', 'tower',\n 'equipment_room', 'telecom_closet', 'riser_room'];\n for (let i = 0; i < structTypes.length; i++) {\n const stype = structTypes[i];\n const color = STRUCTURE_COLORS[stype] || '#616161';\n const shape = STRUCTURE_SHAPES[stype] || '';\n const isOutline = shape.includes('fill=\"none\"');\n const item = L.DomUtil.create('div', 'pw-legend-item', structSec);\n const swatch = L.DomUtil.create('span', 'pw-legend-swatch', item);\n _setStaticSvg(swatch,\n '' +\n shape + '');\n const label = L.DomUtil.create('span', 'pw-legend-label', item);\n label.textContent = _titleCase(stype);\n }\n\n // Pathways section\n const pathSec = L.DomUtil.create('div', 'pw-legend-section', body);\n const pathTitle = L.DomUtil.create('div', 'pw-legend-section-title', pathSec);\n pathTitle.textContent = 'Pathways';\n\n const pathTypes = ['conduit', 'conduit_bank', 'aerial', 'direct_buried',\n 'innerduct', 'microduct', 'tray', 'raceway', 'submarine'];\n for (let i = 0; i < pathTypes.length; i++) {\n const ptype = pathTypes[i];\n const color = PATHWAY_COLORS[ptype] || '#616161';\n const dash = PATHWAY_DASH[ptype] || '';\n const item = L.DomUtil.create('div', 'pw-legend-item', pathSec);\n const swatch = L.DomUtil.create('span', 'pw-legend-swatch', item);\n _setStaticSvg(swatch,\n '' +\n '');\n const label = L.DomUtil.create('span', 'pw-legend-label', item);\n label.textContent = _titleCase(ptype);\n }\n\n // Toggle collapse\n header.addEventListener('click', function () {\n const isCollapsed = body.classList.toggle('collapsed');\n header.classList.toggle('collapsed', isCollapsed);\n });\n\n return container;\n },\n });\n\n new LegendControl().addTo(map);\n}\n\n// ---------------------------------------------------------------------------\n// Stats control\n// ---------------------------------------------------------------------------\n\nexport function createStatsControl(map: L.Map): void {\n const StatsControl = L.Control.extend({\n options: { position: 'bottomleft' },\n onAdd: function (): HTMLElement {\n const container = L.DomUtil.create('div', 'pw-stats-overlay');\n const sc = L.DomUtil.create('span', '', container);\n sc.id = 'structure-count';\n sc.textContent = '0';\n container.appendChild(document.createTextNode(' structures \\u00b7 '));\n const pc = L.DomUtil.create('span', '', container);\n pc.id = 'pathway-count';\n pc.textContent = '0';\n container.appendChild(document.createTextNode(' pathways \\u00b7 '));\n const tl = L.DomUtil.create('span', '', container);\n tl.id = 'total-length';\n tl.textContent = '0';\n container.appendChild(document.createTextNode(' km'));\n return container;\n },\n });\n new StatsControl().addTo(map);\n}\n\n// ---------------------------------------------------------------------------\n// Locate control\n// ---------------------------------------------------------------------------\n\nexport function createLocateControl(map: L.Map): L.Control {\n const LocateControl = L.Control.extend({\n options: { position: 'topleft' },\n onAdd: function (): HTMLElement {\n const container = L.DomUtil.create('div', 'leaflet-control-zoom leaflet-bar');\n const link = L.DomUtil.create('a', '', container) as HTMLAnchorElement;\n link.href = '#';\n link.title = 'Go to my location';\n L.DomUtil.create('i', 'mdi mdi-crosshairs-gps', link);\n link.style.display = 'flex';\n link.style.alignItems = 'center';\n link.style.justifyContent = 'center';\n link.style.fontSize = '18px';\n\n L.DomEvent.disableClickPropagation(container);\n L.DomEvent.on(link, 'click', function (e: Event) {\n L.DomEvent.preventDefault(e);\n if (!navigator.geolocation) return;\n navigator.geolocation.getCurrentPosition(\n function (pos: GeolocationPosition) {\n map.flyTo([pos.coords.latitude, pos.coords.longitude], 17, { duration: 1 });\n },\n function () { /* silently ignore denial */ },\n { enableHighAccuracy: true, timeout: 10000 },\n );\n });\n return container;\n },\n });\n return new LocateControl();\n}\n\n// ---------------------------------------------------------------------------\n// Kiosk control\n// ---------------------------------------------------------------------------\n\nexport function createKioskControl(map: L.Map, isKiosk: boolean): L.Control {\n const KioskControl = L.Control.extend({\n options: { position: 'topleft' },\n onAdd: function (): HTMLElement {\n const container = L.DomUtil.create('div', 'leaflet-control-zoom leaflet-bar');\n const link = L.DomUtil.create('a', '', container) as HTMLAnchorElement;\n link.href = '#';\n link.title = isKiosk ? 'Exit kiosk mode' : 'Kiosk mode';\n L.DomUtil.create('i', 'mdi ' + (isKiosk ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'), link);\n link.style.display = 'flex';\n link.style.alignItems = 'center';\n link.style.justifyContent = 'center';\n link.style.fontSize = '18px';\n\n L.DomEvent.disableClickPropagation(container);\n L.DomEvent.on(link, 'click', function (e: Event) {\n L.DomEvent.preventDefault(e);\n const center = map.getCenter();\n const zoom = map.getZoom();\n const params = new URLSearchParams();\n params.set('lat', center.lat.toFixed(6));\n params.set('lon', center.lng.toFixed(6));\n params.set('zoom', String(zoom));\n if (!isKiosk) {\n params.set('kiosk', 'true');\n }\n window.location.search = params.toString();\n });\n return container;\n },\n });\n return new KioskControl();\n}\n\n// ---------------------------------------------------------------------------\n// Sidebar toggle control\n// ---------------------------------------------------------------------------\n\nexport function createSidebarToggleControl(map: L.Map, isKiosk: boolean, showCallback: () => void): L.Control {\n const SidebarToggle = L.Control.extend({\n options: { position: 'topright' },\n onAdd: function (): HTMLElement {\n const container = L.DomUtil.create('div', 'leaflet-control-zoom leaflet-bar pw-sidebar-toggle-ctrl');\n const link = L.DomUtil.create('a', '', container) as HTMLAnchorElement;\n link.href = '#';\n link.title = 'Show sidebar';\n L.DomUtil.create('i', 'mdi mdi-chevron-left', link);\n link.style.display = 'flex';\n link.style.alignItems = 'center';\n link.style.justifyContent = 'center';\n link.style.fontSize = '18px';\n\n L.DomEvent.disableClickPropagation(container);\n L.DomEvent.on(link, 'click', function (e: Event) {\n L.DomEvent.preventDefault(e);\n showCallback();\n });\n\n // Watch sidebar visibility to show/hide this button\n const observer = new MutationObserver(function () {\n const sidebar = document.getElementById('pw-sidebar');\n if (!sidebar) return;\n const sidebarVisible = isKiosk\n ? sidebar.classList.contains('pw-sidebar-open')\n : !sidebar.classList.contains('pw-sidebar-hidden');\n container.style.display = sidebarVisible ? 'none' : '';\n });\n const sidebar = document.getElementById('pw-sidebar');\n if (sidebar) {\n observer.observe(sidebar, { attributes: true, attributeFilter: ['class'] });\n // Initial state\n const sidebarVisible = isKiosk\n ? sidebar.classList.contains('pw-sidebar-open')\n : !sidebar.classList.contains('pw-sidebar-hidden');\n container.style.display = sidebarVisible ? 'none' : '';\n }\n\n return container;\n },\n });\n return new SidebarToggle();\n}\n\n// ---------------------------------------------------------------------------\n// Map factory\n// ---------------------------------------------------------------------------\n\nexport interface MapInitConfig {\n center?: [number, number];\n zoom?: number;\n bounds?: L.LatLngBoundsExpression;\n kiosk?: boolean;\n select?: string; // feature ID to auto-select, e.g. \"structure-123\"\n routeData?: unknown; // Pre-loaded route geometry for static route display\n}\n\nexport interface MapInstance {\n map: L.Map;\n baseLayers: Record;\n layerControl: L.Control.Layers;\n}\n\nexport function createMap(elementId: string, config: MapInitConfig): MapInstance {\n const baseLayers = createBaseLayers();\n const firstLayer = baseLayers[Object.keys(baseLayers)[0]];\n const map = L.map(elementId, {\n layers: [firstLayer],\n });\n\n // Fit to bounds if provided, otherwise use center/zoom\n if (config.bounds) {\n // Allow higher zoom when selecting a specific feature vs. viewing full data extent\n const boundsMaxZoom = config.select ? 20 : 17;\n map.fitBounds(config.bounds, { padding: [50, 50] as [number, number], maxZoom: boundsMaxZoom });\n } else {\n map.setView(config.center || [0, 0], config.zoom || 2);\n }\n\n // Overlay layers\n const overlayLayers: Record = {};\n\n // User-configured WMS/WMTS/tile overlays\n const userOverlays = createUserOverlays();\n for (const name in userOverlays) {\n overlayLayers[name] = userOverlays[name];\n }\n\n // Layer control\n const layerControl = L.control.layers(baseLayers, overlayLayers, {\n position: 'bottomright', collapsed: true,\n }).addTo(map);\n\n return { map, baseLayers, layerControl };\n}\n", "/**\n * GeoJSON data layer loading and viewport caching.\n *\n * Fetches structures and pathways from the GeoJSON API, manages an\n * overfetch cache for smooth panning, and renders features on the map.\n *\n * Shared between the full-page infrastructure map (pathways-map.ts)\n * and the route planner map (route-planner-map.ts).\n */\n\nimport type { FeatureEntry, FeatureType, GeoJSONProperties, PathwayStyle } from './types/features';\nimport {\n structureIcon as _structureIcon,\n clusterIcon as _clusterIcon,\n esc as _esc,\n getCookie as _getCookie,\n haversine as _haversine,\n} from './map-utils';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst CFG: Partial = window.PATHWAYS_CONFIG || {};\nconst API_BASE: string = CFG.apiBase || '/api/plugins/pathways/geo/';\n\nexport { API_BASE };\n\nexport const MIN_DATA_ZOOM = 11;\n\n// ---------------------------------------------------------------------------\n// /info endpoint: per-layer counts + thresholds for the current viewport\n// ---------------------------------------------------------------------------\n\nexport interface LayerThresholds {\n hide: number;\n cluster?: number;\n}\n\nexport interface MapInfo {\n bbox: [number, number, number, number] | null;\n counts: {\n structures: number;\n conduit_banks: number;\n conduits: number;\n aerial_spans: number;\n direct_buried: number;\n circuits: number;\n external?: Record;\n };\n thresholds: {\n structures: LayerThresholds;\n conduit_banks: LayerThresholds;\n conduits: LayerThresholds;\n aerial_spans: LayerThresholds;\n direct_buried: LayerThresholds;\n circuits: LayerThresholds;\n external?: Record;\n };\n}\n\nexport type ClusterMode = 'off' | 'client' | 'server';\nexport type LayerDecision = 'render' | 'hide';\n\nexport interface RenderingDecision {\n clusterMode: ClusterMode;\n layers: Record;\n}\n\n/**\n * Pure mapping from /info counts + thresholds + currently-enabled layers to\n * a per-layer render decision plus a global cluster mode.\n *\n * Rule of thumb: structures drive cluster mode. When structures are clustered\n * (client or server), every non-structure layer is hidden because the\n * supporting topology no longer makes sense at that density.\n *\n * Layer keys: native layers use their counts/thresholds keys directly\n * (e.g. ``'conduit_banks'``); external layers use ``'external:'``.\n */\nexport function decideLayerRendering(info: MapInfo, enabled: Set): RenderingDecision {\n const structuresCount = info.counts.structures;\n const sThresh = info.thresholds.structures;\n let clusterMode: ClusterMode = 'off';\n if (structuresCount > sThresh.hide) {\n clusterMode = 'server';\n } else if (sThresh.cluster != null && structuresCount > sThresh.cluster) {\n clusterMode = 'client';\n }\n\n const layers: Record = {};\n const suppress = clusterMode !== 'off';\n\n if (enabled.has('structures')) {\n layers.structures = 'render';\n }\n\n const nativeKeys: (keyof MapInfo['counts'])[] = [\n 'conduit_banks', 'conduits', 'aerial_spans', 'direct_buried', 'circuits',\n ];\n for (const key of nativeKeys) {\n if (!enabled.has(key)) continue;\n if (suppress) {\n layers[key] = 'hide';\n continue;\n }\n const count = (info.counts[key] as number) ?? 0;\n const threshold = info.thresholds[key as keyof MapInfo['thresholds']] as LayerThresholds | undefined;\n layers[key] = threshold && count > threshold.hide ? 'hide' : 'render';\n }\n\n const extCounts = info.counts.external || {};\n const extThresholds = info.thresholds.external || {};\n for (const name of Object.keys(extCounts)) {\n const key = `external:${name}`;\n if (!enabled.has(key)) continue;\n if (suppress) {\n layers[key] = 'hide';\n continue;\n }\n const t = extThresholds[name];\n layers[key] = t && extCounts[name] > t.hide ? 'hide' : 'render';\n }\n\n return { clusterMode, layers };\n}\n\n// ---------------------------------------------------------------------------\n// /info fetch helper\n// ---------------------------------------------------------------------------\n\nlet _infoController: AbortController | null = null;\nlet _lastInfoEtag = '';\nlet _lastInfo: MapInfo | null = null;\n\nexport async function fetchMapInfo(\n bbox: string,\n callback: (info: MapInfo) => void,\n): Promise {\n if (_infoController) _infoController.abort();\n const controller = new AbortController();\n _infoController = controller;\n\n const url = API_BASE + 'info/?bbox=' + bbox;\n const headers: Record = { 'Accept': 'application/json' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n if (_lastInfoEtag) headers['If-None-Match'] = _lastInfoEtag;\n\n try {\n const response = await fetch(url, { headers, signal: controller.signal });\n if (_infoController === controller) _infoController = null;\n if (response.status === 304 && _lastInfo) {\n callback(_lastInfo);\n return;\n }\n if (response.ok) {\n const data = await response.json() as MapInfo;\n _lastInfoEtag = response.headers.get('ETag') || '';\n _lastInfo = data;\n callback(data);\n }\n } catch {\n if (_infoController === controller) _infoController = null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// GeoJSON fetching with AbortController\n// ---------------------------------------------------------------------------\n\nconst _inflightControllers: Record = {};\n\ninterface FetchResult {\n data: GeoJSON.FeatureCollection | null; // null on 304\n etag: string;\n}\n\nexport async function fetchGeoJSON(\n endpoint: string,\n bbox: string,\n callback: (result: FetchResult) => void,\n extraParams?: Record,\n ifNoneMatch?: string,\n): Promise {\n // Abort any in-flight request for this endpoint\n if (_inflightControllers[endpoint]) {\n _inflightControllers[endpoint].abort();\n }\n\n let url = API_BASE + endpoint + '?format=json&bbox=' + bbox;\n if (extraParams) {\n for (const key in extraParams) {\n url += '&' + key + '=' + encodeURIComponent(String(extraParams[key]));\n }\n }\n\n const controller = new AbortController();\n _inflightControllers[endpoint] = controller;\n\n const headers: Record = { 'Accept': 'application/json' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n if (ifNoneMatch) headers['If-None-Match'] = ifNoneMatch;\n\n try {\n const response = await fetch(url, { headers, signal: controller.signal });\n _inflightControllers[endpoint] = undefined!;\n const etag = response.headers.get('ETag') || '';\n if (response.status === 304) {\n callback({ data: null, etag });\n } else if (response.ok) {\n const data = await response.json() as GeoJSON.FeatureCollection;\n callback({ data, etag });\n }\n } catch (e) {\n _inflightControllers[endpoint] = undefined!;\n // Silently ignore AbortError and network errors\n }\n}\n\n// ---------------------------------------------------------------------------\n// Server-side search\n// ---------------------------------------------------------------------------\n\nimport type { ServerSearchResult } from './types/features';\n\nlet _searchController: AbortController | null = null;\n\nexport async function serverSearch(\n query: string,\n onResults: (results: ServerSearchResult[]) => void,\n): Promise {\n if (_searchController) _searchController.abort();\n const controller = new AbortController();\n _searchController = controller;\n\n const headers: Record = { 'Accept': 'application/json' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n\n // Search all layer endpoints in parallel, without bbox\n const endpoints: [string, FeatureType, string][] = [\n ['structures/', 'structure', 'structure_type'],\n ['conduit-banks/', 'conduit_bank', 'pathway_type'],\n ['conduits/', 'conduit', 'pathway_type'],\n ['aerial-spans/', 'aerial', 'pathway_type'],\n ['direct-buried/', 'direct_buried', 'pathway_type'],\n ['circuits/', 'circuit', 'pathway_type'],\n ];\n\n const results: ServerSearchResult[] = [];\n\n const fetches = endpoints.map(function (cfg) {\n const [endpoint, featureType, typeField] = cfg;\n const url = API_BASE + endpoint + '?format=json&q=' + encodeURIComponent(query);\n return fetch(url, { headers, signal: controller.signal })\n .then(function (resp) { return resp.ok ? resp.json() : null; })\n .then(function (data: GeoJSON.FeatureCollection | null) {\n if (!data || !data.features) return;\n data.features.forEach(function (f: GeoJSON.Feature) {\n if (!f.geometry) return;\n const props = f.properties || {};\n let latlng: L.LatLng;\n if (f.geometry.type === 'Point') {\n const coords = (f.geometry as GeoJSON.Point).coordinates;\n latlng = L.latLng(coords[1], coords[0]);\n } else if (f.geometry.type === 'LineString') {\n const coords = (f.geometry as GeoJSON.LineString).coordinates;\n const mid = coords[Math.floor(coords.length / 2)];\n latlng = L.latLng(mid[1], mid[0]);\n } else {\n return;\n }\n results.push({\n name: props.name || props.cid || 'Unnamed',\n featureType: featureType,\n typeKey: (props[typeField] as string) || featureType,\n latlng: latlng,\n url: props.url as string | undefined,\n });\n });\n })\n .catch(function () { /* ignore aborted / failed */ });\n });\n\n try {\n await Promise.all(fetches);\n _searchController = null;\n onResults(results);\n } catch {\n _searchController = null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Line labels\n// ---------------------------------------------------------------------------\n\nexport function addLineLabels(geoJsonLayer: L.GeoJSON, layerGroup: L.LayerGroup, map: L.Map): void {\n if (map.getZoom() < 20) return;\n\n geoJsonLayer.eachLayer(function (layer: L.Layer) {\n const polyline = layer as L.Polyline;\n const coords = polyline.getLatLngs() as L.LatLng[];\n if (!coords || coords.length < 2) return;\n const feature = (polyline as any).feature as GeoJSON.Feature;\n const name = feature?.properties?.name as string | undefined;\n if (!name) return;\n\n const midIdx = Math.floor(coords.length / 2);\n const p1 = coords[midIdx - 1] || coords[0];\n const p2 = coords[midIdx];\n const midLat = (p1.lat + p2.lat) / 2;\n const midLng = (p1.lng + p2.lng) / 2;\n\n // Use screen pixel coordinates so the angle matches the visual line\n const px1 = map.latLngToContainerPoint(p1);\n const px2 = map.latLngToContainerPoint(p2);\n const dx = px2.x - px1.x;\n const dy = px2.y - px1.y;\n // Angle in screen space (0 deg = right, positive = clockwise)\n let angle = Math.atan2(dy, dx) * 180 / Math.PI;\n // Keep text readable (not upside-down)\n if (angle > 90) angle -= 180;\n if (angle < -90) angle += 180;\n\n const icon = L.divIcon({\n className: 'pw-line-label',\n html: '
' + _esc(name) + '
',\n iconSize: [0, 0] as [number, number],\n iconAnchor: [0, 0] as [number, number],\n });\n\n layerGroup.addLayer(L.marker([midLat, midLng], { icon, interactive: false }));\n });\n}\n\n// ---------------------------------------------------------------------------\n// Data layer groups\n// ---------------------------------------------------------------------------\n\nexport interface DataLayerGroups {\n structures: L.LayerGroup;\n conduitBanks: L.LayerGroup;\n conduits: L.LayerGroup;\n aerialSpans: L.LayerGroup;\n directBuried: L.LayerGroup;\n circuits: L.LayerGroup;\n}\n\nexport function createDataLayers(): DataLayerGroups {\n return {\n structures: L.layerGroup(),\n conduitBanks: L.layerGroup(),\n conduits: L.layerGroup(),\n aerialSpans: L.layerGroup(),\n directBuried: L.layerGroup(),\n circuits: L.layerGroup(),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Pathway config\n// ---------------------------------------------------------------------------\n\n/** Pathway layer descriptor.\n *\n * - ``endpoint`` -- GeoJSON endpoint path relative to API_BASE\n * - ``layerKey`` -- key into DataLayerGroups\n * - ``featureType`` -- canonical type label used by the sidebar/popover\n * - ``style`` -- Leaflet polyline style\n * - ``infoKey`` -- corresponding key in MapInfo.counts/thresholds; this is\n * what the gating decision uses\n */\nexport type PathwayInfoKey = 'conduit_banks' | 'conduits' | 'aerial_spans' | 'direct_buried' | 'circuits';\n\nexport interface PathwayConfig {\n endpoint: string;\n layerKey: keyof DataLayerGroups;\n featureType: FeatureType;\n style: PathwayStyle;\n infoKey: PathwayInfoKey;\n}\n\nexport const PATHWAY_CONFIGS: PathwayConfig[] = [\n { endpoint: 'conduit-banks/', layerKey: 'conduitBanks', featureType: 'conduit_bank',\n style: { color: '#ad1457', weight: 5, opacity: 0.8, dashArray: '' }, infoKey: 'conduit_banks' },\n { endpoint: 'conduits/', layerKey: 'conduits', featureType: 'conduit',\n style: { color: '#f57c00', weight: 3, opacity: 0.7, dashArray: '5 5' }, infoKey: 'conduits' },\n { endpoint: 'aerial-spans/', layerKey: 'aerialSpans', featureType: 'aerial',\n style: { color: '#1565c0', weight: 3, opacity: 0.7, dashArray: '10 5' }, infoKey: 'aerial_spans' },\n { endpoint: 'direct-buried/', layerKey: 'directBuried', featureType: 'direct_buried',\n style: { color: '#616161', weight: 3, opacity: 0.7, dashArray: '2 4' }, infoKey: 'direct_buried' },\n { endpoint: 'circuits/', layerKey: 'circuits', featureType: 'circuit',\n style: { color: '#d32f2f', weight: 3, opacity: 0.8, dashArray: '8 6' }, infoKey: 'circuits' },\n];\n\n// ---------------------------------------------------------------------------\n// Viewport cache\n// ---------------------------------------------------------------------------\n\nconst GEO_CACHE_SIZE = 12;\nconst OVERFETCH = 0.5; // fraction of viewport width/height to pad each side\n\ninterface GeoCacheEntry {\n west: number; south: number; east: number; north: number;\n zoom: number;\n extraKey: string;\n etag: string;\n data: GeoJSON.FeatureCollection;\n}\n\nconst _geoCache: Record = {};\n\n/** Find a cache entry that covers a given viewport at a given zoom. */\nfunction _findCovering(\n endpoint: string, zoom: number, extraKey: string,\n west: number, south: number, east: number, north: number,\n): GeoCacheEntry | null {\n const entries = _geoCache[endpoint];\n if (!entries) return null;\n for (let i = entries.length - 1; i >= 0; i--) {\n const e = entries[i];\n if (e.zoom === zoom && e.extraKey === extraKey &&\n west >= e.west && south >= e.south &&\n east <= e.east && north <= e.north) {\n return e;\n }\n }\n return null;\n}\n\nfunction _storeInCache(\n endpoint: string, west: number, south: number, east: number, north: number,\n zoom: number, extraKey: string, etag: string, data: GeoJSON.FeatureCollection,\n): void {\n if (!_geoCache[endpoint]) _geoCache[endpoint] = [];\n const cache = _geoCache[endpoint];\n cache.push({ west, south, east, north, zoom, extraKey, etag, data });\n if (cache.length > GEO_CACHE_SIZE) cache.shift();\n}\n\nexport function cachedFetch(\n map: L.Map,\n endpoint: string,\n callback: (data: GeoJSON.FeatureCollection) => void,\n extraParams?: Record,\n): void {\n const b = map.getBounds();\n const west = b.getWest(), south = b.getSouth();\n const east = b.getEast(), north = b.getNorth();\n const zoom = map.getZoom();\n const extraKey = extraParams ? JSON.stringify(extraParams) : '';\n\n const cached = _findCovering(endpoint, zoom, extraKey, west, south, east, north);\n if (cached) {\n callback(cached.data);\n return;\n }\n\n // Cache miss -- expand bbox and fetch\n const dw = (east - west) * OVERFETCH;\n const dh = (north - south) * OVERFETCH;\n const fw = west - dw, fs = south - dh, fe = east + dw, fn = north + dh;\n const fetchBbox = fw + ',' + fs + ',' + fe + ',' + fn;\n\n fetchGeoJSON(endpoint, fetchBbox, function (result: FetchResult) {\n if (result.data) {\n _storeInCache(endpoint, fw, fs, fe, fn, zoom, extraKey, result.etag, result.data);\n callback(result.data);\n }\n }, extraParams);\n}\n\n// ---------------------------------------------------------------------------\n// Neighbor preloading\n// ---------------------------------------------------------------------------\n\nlet _preloadTimer: ReturnType | null = null;\n\nexport interface PreloadSpec {\n endpoint: string;\n extraParams?: Record;\n}\n\nconst MIN_PRELOAD_ZOOM = 14; // don't preload at low zoom -- user is likely to zoom, not pan\n\nexport function preloadNeighbors(map: L.Map, specs: PreloadSpec[]): void {\n if (_preloadTimer) clearTimeout(_preloadTimer);\n\n const zoom = map.getZoom();\n if (zoom < MIN_PRELOAD_ZOOM) return;\n\n const b = map.getBounds();\n const vw = b.getEast() - b.getWest();\n const vh = b.getNorth() - b.getSouth();\n\n // Cardinal offsets: right, left, down, up\n const offsets: [number, number][] = [[vw, 0], [-vw, 0], [0, -vh], [0, vh]];\n\n // Build a queue of {endpoint, bbox} pairs, skipping already-cached regions\n const queue: { endpoint: string; bbox: string; fw: number; fs: number; fe: number; fn: number; zoom: number; extraKey: string; extraParams?: Record }[] = [];\n for (const spec of specs) {\n const extraKey = spec.extraParams ? JSON.stringify(spec.extraParams) : '';\n for (const [dx, dy] of offsets) {\n const cw = b.getWest() + dx, cs = b.getSouth() + dy;\n const ce = b.getEast() + dx, cn = b.getNorth() + dy;\n if (_findCovering(spec.endpoint, zoom, extraKey, cw, cs, ce, cn)) continue;\n const dw = vw * OVERFETCH, dh = vh * OVERFETCH;\n const fw = cw - dw, fs = cs - dh, fe = ce + dw, fn = cn + dh;\n queue.push({\n endpoint: spec.endpoint,\n bbox: fw + ',' + fs + ',' + fe + ',' + fn,\n fw, fs, fe, fn, zoom, extraKey,\n extraParams: spec.extraParams,\n });\n }\n }\n\n // Drain the queue one at a time to avoid flooding the server\n let idx = 0;\n function _next(): void {\n if (idx >= queue.length) return;\n // Abort if the user has moved (zoom changed or panned significantly)\n if (map.getZoom() !== zoom) return;\n const q = queue[idx++];\n fetchGeoJSON(q.endpoint, q.bbox, function (result: FetchResult) {\n if (result.data) {\n _storeInCache(q.endpoint, q.fw, q.fs, q.fe, q.fn, q.zoom, q.extraKey, result.etag, result.data);\n }\n _preloadTimer = setTimeout(_next, 50);\n }, q.extraParams);\n }\n\n // Start after a short idle delay so visible data renders first\n _preloadTimer = setTimeout(_next, 200);\n}\n\n// ---------------------------------------------------------------------------\n// Data loading callbacks\n// ---------------------------------------------------------------------------\n\nexport interface LoadCallbacks {\n onFeatureClick?: (entry: FeatureEntry, e: L.LeafletMouseEvent) => void;\n onFeatureMouseOver?: (entry: FeatureEntry, e: L.LeafletMouseEvent, feature: GeoJSON.Feature) => void;\n onFeatureMouseOut?: () => void;\n onFeatureCreated?: (entry: FeatureEntry) => void;\n onStructuresLoaded?: (count: number) => void;\n onPathwayLoaded?: (data: GeoJSON.FeatureCollection) => void;\n onAllLoaded?: (allFeatures: FeatureEntry[]) => void;\n}\n\n/**\n * Load all data layers for the current viewport.\n *\n * Decision-driven: the caller supplies a ``RenderingDecision`` (from\n * /info + ``decideLayerRendering``) that says which layers to render and\n * whether to client-cluster structures. Layers marked ``'hide'`` are cleared\n * without a network call; layers not present in the decision are also\n * cleared (treated as disabled).\n *\n * The structures fetch always passes ``zoom`` so the server-side grid\n * cluster fallback still kicks in for any installation that doesn't have a\n * fresh /info result.\n */\nexport function loadDataLayers(\n map: L.Map,\n dataLayers: DataLayerGroups,\n decision: RenderingDecision | null,\n zoomHint: HTMLDivElement | null,\n callbacks: LoadCallbacks,\n): void {\n const zoom = map.getZoom();\n\n if (zoom < MIN_DATA_ZOOM || decision == null) {\n dataLayers.structures.clearLayers();\n dataLayers.conduitBanks.clearLayers();\n dataLayers.conduits.clearLayers();\n dataLayers.aerialSpans.clearLayers();\n dataLayers.directBuried.clearLayers();\n dataLayers.circuits.clearLayers();\n if (zoomHint) zoomHint.style.display = zoom < MIN_DATA_ZOOM ? '' : 'none';\n if (callbacks.onAllLoaded) callbacks.onAllLoaded([]);\n return;\n }\n // From here on ``decision`` is non-null; the local alias lets nested\n // closures benefit from the narrowing.\n const live: RenderingDecision = decision;\n\n if (zoomHint) zoomHint.style.display = 'none';\n\n // Clear any pathway layer whose decision is 'hide' (or absent because\n // the toggle is off). Counts that don't make the cut never fetch.\n PATHWAY_CONFIGS.forEach(function (cfg) {\n if (live.layers[cfg.infoKey] !== 'render') {\n dataLayers[cfg.layerKey].clearLayers();\n }\n });\n\n const allFeatures: FeatureEntry[] = [];\n let pendingLoads = 0;\n let totalExpectedLoads = 0;\n\n const renderStructures = live.layers.structures === 'render' && map.hasLayer(dataLayers.structures);\n if (renderStructures) totalExpectedLoads++;\n\n let pendingPathway = 0;\n PATHWAY_CONFIGS.forEach(function (cfg) {\n if (live.layers[cfg.infoKey] === 'render' && map.hasLayer(dataLayers[cfg.layerKey])) {\n totalExpectedLoads++;\n pendingPathway++;\n }\n });\n\n function _checkAllLoaded(): void {\n pendingLoads++;\n if (pendingLoads === totalExpectedLoads) {\n if (callbacks.onAllLoaded) callbacks.onAllLoaded(allFeatures);\n // Prefetch cardinal neighbors for all active endpoints\n const specs: PreloadSpec[] = [];\n if (renderStructures) {\n specs.push({ endpoint: 'structures/', extraParams: { zoom: zoom } });\n }\n PATHWAY_CONFIGS.forEach(function (cfg) {\n if (live.layers[cfg.infoKey] === 'render' && map.hasLayer(dataLayers[cfg.layerKey])) {\n specs.push({ endpoint: cfg.endpoint });\n }\n });\n preloadNeighbors(map, specs);\n }\n }\n\n function _pathwayLoaded(data: GeoJSON.FeatureCollection): void {\n if (callbacks.onPathwayLoaded) callbacks.onPathwayLoaded(data);\n pendingPathway--;\n }\n\n // Structures\n if (renderStructures) {\n // Client clustering is only used when the decision says 'client'; in\n // 'off' mode markers render plain, in 'server' mode the response\n // already contains pre-aggregated cluster centroids.\n const hasClusterPlugin = typeof L.markerClusterGroup === 'function';\n if (!hasClusterPlugin && live.clusterMode === 'client') {\n console.info('[pathways] MarkerCluster plugin not loaded -- client-side clustering disabled');\n }\n const useClientCluster = live.clusterMode === 'client' && hasClusterPlugin;\n const clusterGroup = useClientCluster\n ? L.markerClusterGroup({ maxClusterRadius: 35, spiderfyOnMaxZoom: true })\n : null;\n\n cachedFetch(map, 'structures/', function (data: GeoJSON.FeatureCollection) {\n dataLayers.structures.clearLayers();\n\n const isServerClustered = data.features && data.features.length > 0 &&\n (data.features[0].properties as GeoJSONProperties)?.cluster;\n\n if (isServerClustered) {\n let total = 0;\n data.features.forEach(function (f: GeoJSON.Feature) {\n const props = f.properties as GeoJSONProperties;\n const count = props.point_count || 0;\n total += count;\n const geom = f.geometry as GeoJSON.Point;\n const latlng = L.latLng(geom.coordinates[1], geom.coordinates[0]);\n const marker = L.marker(latlng, { icon: _clusterIcon(count) });\n marker.on('click', function () {\n const nextZoom = Math.min(map.getZoom() + 3, map.getMaxZoom());\n map.setView(latlng, nextZoom);\n });\n dataLayers.structures.addLayer(marker);\n });\n if (callbacks.onStructuresLoaded) callbacks.onStructuresLoaded(total);\n } else {\n const geoLayer = L.geoJSON(data, {\n pointToLayer: function (feature: GeoJSON.Feature, latlng: L.LatLng) {\n return L.marker(latlng, {\n icon: _structureIcon((feature.properties as GeoJSONProperties).structure_type || ''),\n });\n },\n onEachFeature: function (feature: GeoJSON.Feature, layer: L.Layer) {\n if (feature.id != null && (feature.properties as GeoJSONProperties).id == null) {\n (feature.properties as GeoJSONProperties).id = feature.id as number;\n }\n const entry: FeatureEntry = {\n props: feature.properties as GeoJSONProperties,\n featureType: 'structure',\n layer: layer,\n latlng: (layer as L.Marker).getLatLng(),\n };\n allFeatures.push(entry);\n if (callbacks.onFeatureCreated) callbacks.onFeatureCreated(entry);\n layer.on('click', function (e: L.LeafletMouseEvent) {\n if (callbacks.onFeatureClick) callbacks.onFeatureClick(entry, e);\n });\n layer.on('mouseover', function (e: L.LeafletMouseEvent) {\n if (callbacks.onFeatureMouseOver) callbacks.onFeatureMouseOver(entry, e, feature);\n });\n layer.on('mouseout', function () {\n if (callbacks.onFeatureMouseOut) callbacks.onFeatureMouseOut();\n });\n },\n });\n if (clusterGroup) {\n clusterGroup.addLayers(geoLayer.getLayers());\n dataLayers.structures.addLayer(clusterGroup);\n } else {\n geoLayer.addTo(dataLayers.structures);\n }\n if (callbacks.onStructuresLoaded) {\n callbacks.onStructuresLoaded(data.features ? data.features.length : 0);\n }\n }\n _checkAllLoaded();\n }, { zoom: zoom });\n }\n\n if (pendingPathway === 0 && callbacks.onPathwayLoaded) {\n // No pathway layers active -- signal with empty data\n }\n\n // Shared pathway handler factory\n function _makePathwayOpts(featureType: FeatureType, styleObj: PathwayStyle): L.GeoJSONOptions {\n return {\n style: function () { return styleObj; },\n onEachFeature: function (feature: GeoJSON.Feature, layer: L.Layer) {\n const props = feature.properties as GeoJSONProperties;\n if (feature.id != null && props.id == null) {\n props.id = feature.id as number;\n }\n // Normalise: pathways use \"label\", map UI expects \"name\"\n if (!props.name && props.label) props.name = props.label as string;\n const entry: FeatureEntry = {\n props: props,\n featureType: featureType,\n layer: layer,\n latlng: (layer as L.Polyline).getBounds().getCenter(),\n };\n allFeatures.push(entry);\n if (callbacks.onFeatureCreated) callbacks.onFeatureCreated(entry);\n layer.on('click', function (e: L.LeafletMouseEvent) {\n if (callbacks.onFeatureClick) callbacks.onFeatureClick(entry, e);\n });\n layer.on('mouseover', function (e: L.LeafletMouseEvent) {\n if (callbacks.onFeatureMouseOver) callbacks.onFeatureMouseOver(entry, e, feature);\n });\n layer.on('mouseout', function () {\n if (callbacks.onFeatureMouseOut) callbacks.onFeatureMouseOut();\n });\n },\n };\n }\n\n PATHWAY_CONFIGS.forEach(function (cfg) {\n const layer = dataLayers[cfg.layerKey];\n if (live.layers[cfg.infoKey] !== 'render') return;\n if (!map.hasLayer(layer)) return;\n cachedFetch(map, cfg.endpoint, function (data: GeoJSON.FeatureCollection) {\n layer.clearLayers();\n const geoLayer = L.geoJSON(data, _makePathwayOpts(cfg.featureType, cfg.style));\n geoLayer.addTo(layer);\n addLineLabels(geoLayer, layer, map);\n _pathwayLoaded(data);\n _checkAllLoaded();\n });\n });\n\n // If no layers active, still signal completion\n if (totalExpectedLoads === 0) {\n if (callbacks.onAllLoaded) callbacks.onAllLoaded([]);\n }\n}\n\n/**\n * Calculate total pathway length from a GeoJSON FeatureCollection.\n * Returns length in meters.\n */\nexport function calcPathwayLength(data: GeoJSON.FeatureCollection): number {\n let totalLength = 0;\n if (data.features) {\n data.features.forEach(function (f: GeoJSON.Feature) {\n if (f.geometry && f.geometry.type === 'LineString') {\n const coords = (f.geometry as GeoJSON.LineString).coordinates;\n for (let i = 0; i < coords.length - 1; i++) {\n totalLength += _haversine(\n coords[i][1], coords[i][0],\n coords[i + 1][1], coords[i + 1][0],\n );\n }\n }\n });\n }\n return totalLength;\n}\n", "/**\n * Full-page infrastructure map.\n *\n * Fetches structures and pathways from the GeoJSON API within the visible\n * bounding box, but only when zoomed in past a minimum threshold.\n * Re-fetches on pan/zoom with debouncing.\n *\n * Structures use server-side grid clustering at low zoom levels (11-14)\n * to avoid transferring thousands of features just for client-side clustering.\n * At zoom 15+ individual features are returned and optionally client-clustered.\n */\n\nimport { Sidebar } from './sidebar';\nimport { Popover } from './popover';\nimport type { FeatureEntry, FeatureType, GeoJSONProperties, PathwayStyle } from './types/features';\nimport {\n initExternalLayers,\n loadExternalLayers,\n getLayerConfig,\n} from './external-layers';\nimport type { ExternalLayerConfig } from './types/external';\nimport {\n STRUCTURE_COLORS,\n STRUCTURE_SHAPES,\n PATHWAY_COLORS,\n esc as _esc,\n titleCase as _titleCase,\n getCookie as _getCookie,\n bboxParam as _bboxParam,\n debounce as _debounce,\n} from './map-utils';\nimport {\n createMap,\n createZoomHint,\n createLegend,\n createStatsControl,\n createKioskControl,\n createSidebarToggleControl,\n createLocateControl,\n} from './map-core';\nimport type { MapInitConfig } from './map-core';\nimport {\n API_BASE,\n MIN_DATA_ZOOM,\n createDataLayers,\n loadDataLayers,\n calcPathwayLength,\n serverSearch,\n fetchMapInfo,\n decideLayerRendering,\n} from './data-layers';\nimport type { MapInfo, RenderingDecision } from './data-layers';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst CFG: Partial = window.PATHWAYS_CONFIG || {};\n\n// ---------------------------------------------------------------------------\n// Main initialization\n// ---------------------------------------------------------------------------\n\nfunction initializePathwaysMap(elementId: string, config: MapInitConfig): void {\n const container = document.getElementById(elementId);\n\n // Inject dependencies into sub-modules\n Sidebar.setDeps({\n titleCase: _titleCase,\n esc: _esc,\n debounce: _debounce,\n getCookie: _getCookie,\n structureColors: STRUCTURE_COLORS,\n structureShapes: STRUCTURE_SHAPES,\n pathwayColors: PATHWAY_COLORS,\n apiBase: API_BASE,\n });\n Popover.setDeps({ titleCase: _titleCase });\n\n const { map, baseLayers, layerControl } = createMap(elementId, config);\n\n // Stats + legend (stats first so legend stacks above it)\n createStatsControl(map);\n createLegend(map);\n\n // Map controls (topleft, below zoom)\n createKioskControl(map, !!config.kiosk).addTo(map);\n createSidebarToggleControl(map, !!config.kiosk, function () { Sidebar.show(); }).addTo(map);\n createLocateControl(map).addTo(map);\n\n // Counters\n const structureCountEl = document.getElementById('structure-count');\n const pathwayCountEl = document.getElementById('pathway-count');\n const totalLengthEl = document.getElementById('total-length');\n\n // Zoom hint\n const zoomHint = createZoomHint(map);\n\n // --- Layer visibility persistence (localStorage) ---\n\n const PREFS_KEY = 'pathways_map_layers';\n const DEFAULT_LAYERS: Record = {\n 'Structures': true, 'Conduit Banks': true, 'Conduits': true, 'Aerial Spans': false, 'Direct Buried': false, 'Circuit Routes': false,\n };\n\n function _loadPrefs(): Record | null {\n try {\n const saved = localStorage.getItem(PREFS_KEY);\n return saved ? JSON.parse(saved) as Record : null;\n } catch (_e) { return null; }\n }\n\n function _savePrefs(layers: Record): void {\n try { localStorage.setItem(PREFS_KEY, JSON.stringify(layers)); } catch (_e) { /* ignore */ }\n }\n\n const layerPrefs = _loadPrefs() || DEFAULT_LAYERS;\n\n // --- Dynamic data layers ---\n\n const dataLayers = createDataLayers();\n\n const layerNames: Record = {\n 'Structures': dataLayers.structures,\n 'Conduit Banks': dataLayers.conduitBanks,\n 'Conduits': dataLayers.conduits,\n 'Aerial Spans': dataLayers.aerialSpans,\n 'Direct Buried': dataLayers.directBuried,\n 'Circuit Routes': dataLayers.circuits,\n };\n\n // --- External plugin layers ---\n const externalConfigs: ExternalLayerConfig[] = CFG.externalLayers ?? [];\n const externalGroups = initExternalLayers(externalConfigs, map);\n\n // Add external layers to layerNames for the layer control\n for (const [name, group] of externalGroups) {\n const cfg = getLayerConfig(name);\n if (cfg) {\n layerNames[cfg.label] = group;\n }\n }\n\n // Add layers based on saved prefs\n for (const lname in layerNames) {\n if (layerPrefs[lname] !== false) {\n layerNames[lname].addTo(map);\n }\n }\n\n // Map between sidebar display name and MapInfo key. Used to translate\n // the /info decision (which keys layers by their snake_case info key)\n // back into the friendly labels used for the sidebar toggles.\n const LAYER_INFO_KEY: Record = {\n 'Structures': 'structures',\n 'Conduit Banks': 'conduit_banks',\n 'Conduits': 'conduits',\n 'Aerial Spans': 'aerial_spans',\n 'Direct Buried': 'direct_buried',\n 'Circuit Routes': 'circuits',\n };\n for (const [extName] of externalGroups) {\n const cfg = getLayerConfig(extName);\n if (cfg) LAYER_INFO_KEY[cfg.label] = 'external:' + extName;\n }\n\n // --- Sidebar layer toggle sync ---\n\n const _layerCheckboxes: Record = {};\n\n function _syncSidebarCheckbox(name: string, checked: boolean): void {\n if (_layerCheckboxes[name]) {\n _layerCheckboxes[name].checked = checked;\n const btn = _layerCheckboxes[name].closest('.pw-layer-toggle');\n if (btn) {\n btn.classList.toggle('pw-layer-active', checked);\n }\n }\n }\n\n // Persist layer toggles\n map.on('overlayadd', function (e: L.LayersControlEvent) {\n const prefs = _loadPrefs() || DEFAULT_LAYERS;\n prefs[e.name] = true;\n _savePrefs(prefs);\n _syncSidebarCheckbox(e.name, true);\n });\n map.on('overlayremove', function (e: L.LayersControlEvent) {\n const prefs = _loadPrefs() || DEFAULT_LAYERS;\n prefs[e.name] = false;\n _savePrefs(prefs);\n _syncSidebarCheckbox(e.name, false);\n });\n\n // --- Sidebar layer toggles ---\n\n // SVG icons for layer toggle buttons (trusted compile-time constants)\n const LAYER_ICONS: Record = {\n 'Structures': '',\n 'Conduit Banks': '',\n 'Conduits': '',\n 'Aerial Spans': '',\n 'Direct Buried': '',\n 'Circuit Routes': '',\n };\n\n function _buildSidebarLayerToggles(): void {\n const toggleContainer = document.getElementById('pw-layer-toggles');\n if (!toggleContainer) return;\n toggleContainer.textContent = '';\n\n for (const lname in layerNames) {\n const btn = document.createElement('button');\n btn.type = 'button';\n const active = map.hasLayer(layerNames[lname]);\n btn.className = 'pw-layer-toggle' + (active ? ' pw-layer-active' : '');\n\n // Build button content using DOM methods\n // iconSvg is from LAYER_ICONS constant -- trusted compile-time SVG\n const iconSvg = LAYER_ICONS[lname] || '';\n const span = document.createElement('span');\n span.textContent = lname;\n const wrapper = document.createElement('span');\n wrapper.innerHTML = iconSvg; // eslint-disable-line no-unsanitized/property -- trusted compile-time SVG constants only\n while (wrapper.firstChild) btn.appendChild(wrapper.firstChild);\n btn.appendChild(span);\n\n const cb = document.createElement('input');\n cb.type = 'checkbox';\n cb.checked = active;\n cb.style.display = 'none';\n _layerCheckboxes[lname] = cb;\n btn.appendChild(cb);\n\n (function (cbName: string, checkbox: HTMLInputElement, button: HTMLButtonElement) {\n button.addEventListener('click', function () {\n checkbox.checked = !checkbox.checked;\n if (checkbox.checked) {\n map.addLayer(layerNames[cbName]);\n button.classList.add('pw-layer-active');\n } else {\n map.removeLayer(layerNames[cbName]);\n button.classList.remove('pw-layer-active');\n }\n const prefs = _loadPrefs() || DEFAULT_LAYERS;\n prefs[cbName] = checkbox.checked;\n _savePrefs(prefs);\n _loadData();\n });\n })(lname, cb, btn);\n\n toggleContainer.appendChild(btn);\n }\n }\n _buildSidebarLayerToggles();\n\n // --- Data loading ---\n\n let pathwayCount = 0;\n let totalLength = 0;\n\n function _updateUrl(): void {\n const center = map.getCenter();\n const zoom = map.getZoom();\n const params = new URLSearchParams();\n params.set('lat', center.lat.toFixed(6));\n params.set('lon', center.lng.toFixed(6));\n params.set('zoom', String(zoom));\n const selId = Sidebar.getSelectedId();\n if (selId) params.set('select', selId);\n if (config.kiosk) params.set('kiosk', 'true');\n const newUrl = window.location.pathname + '?' + params.toString();\n history.replaceState(null, '', newUrl);\n }\n\n function _setLayerChip(name: string, count: number | null): void {\n if (!_layerCheckboxes[name]) return;\n const btn = _layerCheckboxes[name].closest('.pw-layer-toggle') as HTMLElement | null;\n if (!btn) return;\n let chip = btn.querySelector('.pw-layer-count-chip');\n if (count == null) {\n if (chip) chip.remove();\n return;\n }\n if (!chip) {\n chip = document.createElement('span');\n chip.className = 'pw-layer-count-chip';\n btn.appendChild(chip);\n }\n chip.textContent = count.toLocaleString();\n }\n\n function _applyDecisionToToggles(decision: RenderingDecision, info: MapInfo): void {\n for (const lname in LAYER_INFO_KEY) {\n if (!_layerCheckboxes[lname]) continue;\n const btn = _layerCheckboxes[lname].closest('.pw-layer-toggle');\n if (!btn) continue;\n const infoKey = LAYER_INFO_KEY[lname];\n // Hidden layers (decision said 'hide') get dimmed with a count chip.\n // Only show the chip while the layer is enabled in the toggle.\n const enabled = _layerCheckboxes[lname].checked;\n const isHidden = enabled && decision.layers[infoKey] === 'hide';\n btn.classList.toggle('pw-layer-unavailable', isHidden);\n if (isHidden) {\n let count: number | undefined;\n if (infoKey.startsWith('external:')) {\n count = info.counts.external?.[infoKey.slice(9)];\n } else {\n count = (info.counts as unknown as Record)[infoKey];\n }\n _setLayerChip(lname, count ?? 0);\n } else {\n _setLayerChip(lname, null);\n }\n }\n }\n\n function _runLoad(decision: RenderingDecision | null, info: MapInfo | null): void {\n const zoom = map.getZoom();\n if (decision && info) _applyDecisionToToggles(decision, info);\n\n // Reset pathway stats\n pathwayCount = 0;\n totalLength = 0;\n\n loadDataLayers(map, dataLayers, decision, zoomHint, {\n onFeatureClick: function (entry: FeatureEntry, e: L.LeafletMouseEvent) {\n if (e.originalEvent) (e.originalEvent as any)._sidebarClick = true;\n Sidebar.selectFeature(entry);\n _updateUrl();\n },\n onFeatureMouseOver: function (entry: FeatureEntry, e: L.LeafletMouseEvent, feature: GeoJSON.Feature) {\n Popover.show(\n e.latlng || (entry.layer as L.Marker).getLatLng(),\n feature.properties as GeoJSONProperties,\n );\n },\n onFeatureMouseOut: function () {\n Popover.hide();\n },\n onFeatureCreated: function (entry: FeatureEntry) {\n Sidebar.onFeatureCreated(entry);\n },\n onStructuresLoaded: function (count: number) {\n if (structureCountEl) structureCountEl.textContent = String(count);\n },\n onPathwayLoaded: function (data: GeoJSON.FeatureCollection) {\n const count = data.features ? data.features.length : 0;\n pathwayCount += count;\n totalLength += calcPathwayLength(data);\n if (pathwayCountEl) pathwayCountEl.textContent = String(pathwayCount);\n if (totalLengthEl) totalLengthEl.textContent = (totalLength / 1000).toFixed(2);\n },\n onAllLoaded: function (allFeatures: FeatureEntry[]) {\n if (zoom < MIN_DATA_ZOOM) {\n if (structureCountEl) structureCountEl.textContent = '-';\n if (pathwayCountEl) pathwayCountEl.textContent = '-';\n if (totalLengthEl) totalLengthEl.textContent = '-';\n Sidebar.setFeatures([]);\n return;\n }\n\n // --- External plugin layers ---\n // Suppress externals when the decision says hide (e.g. structures\n // are clustered or the external layer is over its own threshold).\n const visibleExternal = new Set();\n for (const [name, group] of externalGroups) {\n if (!map.hasLayer(group)) continue;\n if (decision && decision.layers['external:' + name] === 'hide') continue;\n visibleExternal.add(name);\n }\n if (visibleExternal.size > 0) {\n const bbox = _bboxParam(map);\n loadExternalLayers(bbox, zoom, visibleExternal, function (entry: FeatureEntry, extCfg: ExternalLayerConfig) {\n Sidebar.onFeatureCreated(entry);\n entry.layer.on('click', function (e: L.LeafletMouseEvent) {\n if (e.originalEvent) (e.originalEvent as any)._sidebarClick = true;\n Sidebar.selectFeature(entry);\n });\n entry.layer.on('mouseover', function (e: L.LeafletMouseEvent) {\n Popover.show(e.latlng, entry.props, extCfg.popoverFields);\n });\n entry.layer.on('mouseout', function () { Popover.hide(); });\n }).then(function (extEntries: FeatureEntry[]) {\n for (let i = 0; i < extEntries.length; i++) {\n allFeatures.push(extEntries[i]);\n }\n Sidebar.setFeatures(allFeatures);\n // Auto-select feature from URL on first load\n if (_pendingSelectId) {\n Sidebar.selectById(_pendingSelectId);\n _pendingSelectId = '';\n }\n });\n } else {\n Sidebar.setFeatures(allFeatures);\n // Auto-select feature from URL on first load\n if (_pendingSelectId) {\n Sidebar.selectById(_pendingSelectId);\n _pendingSelectId = '';\n }\n }\n },\n });\n }\n\n function _enabledInfoKeys(): Set {\n const set = new Set();\n for (const lname in LAYER_INFO_KEY) {\n if (_layerCheckboxes[lname]?.checked) {\n set.add(LAYER_INFO_KEY[lname]);\n } else if (!_layerCheckboxes[lname] && map.hasLayer(layerNames[lname])) {\n // Toggle not yet built (initial load): fall back to layer state\n set.add(LAYER_INFO_KEY[lname]);\n }\n }\n return set;\n }\n\n function _loadData(): void {\n const zoom = map.getZoom();\n if (zoom < MIN_DATA_ZOOM) {\n _runLoad(null, null);\n return;\n }\n const bbox = _bboxParam(map);\n fetchMapInfo(bbox, function (info: MapInfo) {\n const decision = decideLayerRendering(info, _enabledInfoKeys());\n _runLoad(decision, info);\n });\n }\n\n // --- URL state management ---\n\n let _pendingSelectId = config.select || '';\n\n const debouncedUrlUpdate = _debounce(_updateUrl, 300);\n\n // Load data on every moveend -- AbortController in fetchGeoJSON cancels\n // stale in-flight requests, so no debounce needed.\n map.on('moveend', function () {\n _loadData();\n debouncedUrlUpdate();\n });\n\n // Initialize sidebar and popover\n Sidebar.init(map, config.kiosk);\n Sidebar.onServerSearch(function (query: string) {\n serverSearch(query, function (results) {\n Sidebar.setServerResults(results);\n });\n });\n Sidebar.onSelectionChange(_updateUrl);\n Popover.init(map);\n\n // Initial load\n _loadData();\n\n // Reset view button\n const resetBtn = document.getElementById('reset-view');\n if (resetBtn) {\n resetBtn.addEventListener('click', function () {\n if (config.bounds) {\n map.fitBounds(config.bounds, { padding: [30, 30] as [number, number], maxZoom: 17 });\n } else {\n map.setView(config.center || [0, 0], config.zoom || 2);\n }\n });\n }\n\n // Store reference\n (window as any).PathwaysMap = {\n map: map,\n layerControl: layerControl,\n };\n\n // Leaflet calculates size at init; force a recheck after layout settles\n setTimeout(function () { map.invalidateSize(); }, 100);\n window.addEventListener('resize', function () { map.invalidateSize(); });\n}\n\n// Expose globally\nwindow.initializePathwaysMap = initializePathwaysMap;\n"], - "mappings": "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", - "names": ["NATIVE_TYPES", "_layerStates", "_resolveColor", "props", "style", "_a", "_b", "val", "_createPointMarker", "latlng", "color", "_createLine", "coords", "_createPolygon", "initExternalLayers", "configs", "map", "groups", "sorted", "a", "b", "cfg", "group", "loadExternalLayers", "bbox", "zoom", "visibleLayers", "onFeature", "__async", "allEntries", "fetchPromises", "name", "state", "promise", "_fetchLayer", "p", "entries", "config", "layerGroup", "abortController", "sep", "url", "csrfMatch", "headers", "resp", "data", "feature", "layer", "lng", "lat", "c", "line", "rings", "ring", "poly", "entry", "__spreadProps", "__spreadValues", "err", "getLayerConfig", "_titleCase", "_esc", "_debounce", "_getCookie", "STRUCTURE_COLORS", "STRUCTURE_SHAPES", "PATHWAY_COLORS", "API_BASE", "_map", "_features", "_filtered", "_selected", "_activeTypes", "_detailCache", "_highlightedLayer", "_highlightOutline", "_serverSearchCallback", "_lastServerQuery", "_pendingSelect", "_pendingFlySelectId", "_isKiosk", "_onSelectionChange", "_dimmedFeatures", "_activeConnectedIds", "DIM_OPACITY", "_isNativeType", "featureType", "NATIVE_TYPES", "_typeLabel", "extCfg", "getLayerConfig", "_colorForFeature", "entry", "_typeKeyForFeature", "_featureId", "_lighten", "hex", "amount", "g", "b", "_unhighlightMapFeature", "layer", "_applyHighlightVisuals", "marker", "type", "color", "shape", "isOutline", "polyline", "origStyle", "latlngs", "_highlightMapFeature", "_reapplyHighlight", "_dimFeatures", "connectedIds", "_applyDim", "f", "fid", "_restoreDim", "orig", "_highlightListItem", "listEl", "items", "targetId", "i", "_renderList", "countEl", "item", "dot", "label", "typeBadge", "selectFeature", "_buildTypeFilters", "container", "typeMap", "key", "types", "t", "btn", "_applyFilters", "searchInput", "query", "typeKey", "name", "_showSearchingIndicator", "_clearServerResults", "_detailPageUrl", "id", "base", "_apiUrlForFeature", "_a", "_resolveValue", "val", "texts", "choice", "fk", "_addFieldRow", "table", "suffix", "resolved", "text", "tr", "tdLabel", "tdVal", "a", "_addTagsRow", "tags", "tag", "badge", "DETAIL_FIELDS", "_createSection", "title", "parent", "header", "chevron", "body", "collapsed", "_addEnrichmentBadges", "data", "badgeRow", "metrics", "m", "_renderEnrichedDetail", "sectionBody", "fieldName", "fields", "tsDiv", "parts", "_fetchConnectedPathways", "_renderConnectedStructures", "_renderConnectedItem", "typeLabel", "mapFeature", "selectId", "hintLatLng", "current", "structId", "url", "headers", "csrfToken", "resp", "neighborMap", "pwItems", "pw", "pwType", "display", "pwId", "startHint", "endHint", "_e", "startS", "endS", "neighbors", "structSection", "s", "mf", "pwSection", "structs", "startStruct", "endStruct", "idx", "hint", "_fetchDetail", "__async", "cacheKey", "htmlUrl", "_resolveDetailUrl", "htmlCacheKey", "_setTrustedHtml", "_fetchHtmlDetail", "props", "loadingDiv", "response", "errDiv", "html", "_renderDetail", "p", "titleRow", "editBtn", "pencilIcon", "editForm", "editInput", "saveBtn", "cancelEditBtn", "newName", "patchHeaders", "e", "siteBadge", "detailUrl", "link", "icon", "planLink", "planIcon", "detailContainer", "indicator", "el", "hdr", "setServerResults", "results", "searching", "oldHdr", "oldItems", "noResults", "hdrIcon", "result", "goIcon", "onServerSearch", "cb", "init", "map", "kiosk", "toggleBtn", "_setListBodyVisible", "_isCollapsed", "backBtn", "showList", "detailPanel", "_kioskSidebarClose", "kioskCloseBtn", "visible", "_kioskSidebarOpen", "sidebar", "show", "hide", "setFeatures", "features", "cacheKeys", "k", "selId", "found", "pending", "showDetail", "heading", "zoom", "minZoom", "onFeatureCreated", "setDeps", "deps", "getSelectedId", "selectById", "onSelectionChange", "Sidebar", "_titleCase", "_el", "_map", "_position", "latlng", "pt", "cw", "x", "y", "init", "map", "show", "props", "popoverFields", "_a", "_b", "name", "typeText", "f", "t", "type", "hide", "setDeps", "deps", "Popover", "STRUCTURE_COLORS", "STRUCTURE_SHAPES", "PATHWAY_COLORS", "PATHWAY_DASH", "structureIcon", "type", "size", "color", "STRUCTURE_COLORS", "shape", "STRUCTURE_SHAPES", "isOutline", "half", "clusterIcon", "count", "cls", "esc", "text", "el", "titleCase", "str", "c", "getCookie", "name", "parts", "bboxParam", "map", "b", "debounce", "fn", "delay", "timer", "haversine", "lat1", "lon1", "lat2", "lon2", "p1", "p2", "dp", "dl", "a", "CFG", "MAX_NATIVE_ZOOM", "DEFAULT_BASE_LAYERS", "createBaseLayers", "configured", "c", "configs", "layers", "cfg", "createUserOverlays", "userOverlays", "overlays", "layer", "createZoomHint", "map", "div", "_setStaticSvg", "el", "svg", "createLegend", "LegendControl", "container", "header", "chevron", "titleSpan", "body", "structSec", "structTitle", "structTypes", "i", "stype", "color", "STRUCTURE_COLORS", "shape", "STRUCTURE_SHAPES", "isOutline", "item", "swatch", "label", "titleCase", "pathSec", "pathTitle", "pathTypes", "ptype", "PATHWAY_COLORS", "dash", "PATHWAY_DASH", "isCollapsed", "createStatsControl", "StatsControl", "sc", "pc", "tl", "createLocateControl", "LocateControl", "link", "e", "pos", "createKioskControl", "isKiosk", "KioskControl", "center", "zoom", "params", "createSidebarToggleControl", "showCallback", "SidebarToggle", "observer", "sidebar", "sidebarVisible", "createMap", "elementId", "config", "baseLayers", "firstLayer", "boundsMaxZoom", "overlayLayers", "name", "layerControl", "CFG", "API_BASE", "MIN_DATA_ZOOM", "decideLayerRendering", "info", "enabled", "_a", "structuresCount", "sThresh", "clusterMode", "layers", "suppress", "nativeKeys", "key", "count", "threshold", "extCounts", "extThresholds", "name", "t", "_infoController", "_lastInfoEtag", "_lastInfo", "fetchMapInfo", "bbox", "callback", "__async", "controller", "url", "API_BASE", "headers", "csrfToken", "getCookie", "response", "data", "e", "_inflightControllers", "fetchGeoJSON", "endpoint", "extraParams", "ifNoneMatch", "etag", "_searchController", "serverSearch", "query", "onResults", "endpoints", "results", "fetches", "cfg", "featureType", "typeField", "resp", "f", "props", "latlng", "coords", "mid", "addLineLabels", "geoJsonLayer", "layerGroup", "map", "layer", "polyline", "feature", "midIdx", "p1", "p2", "midLat", "midLng", "px1", "px2", "dx", "dy", "angle", "icon", "esc", "createDataLayers", "PATHWAY_CONFIGS", "GEO_CACHE_SIZE", "OVERFETCH", "_geoCache", "_findCovering", "zoom", "extraKey", "west", "south", "east", "north", "entries", "i", "_storeInCache", "cache", "cachedFetch", "b", "cached", "dw", "dh", "fw", "fs", "fe", "fn", "fetchBbox", "result", "_preloadTimer", "MIN_PRELOAD_ZOOM", "preloadNeighbors", "specs", "vw", "vh", "offsets", "queue", "spec", "cw", "cs", "ce", "cn", "idx", "_next", "q", "loadDataLayers", "dataLayers", "decision", "zoomHint", "callbacks", "live", "allFeatures", "pendingLoads", "totalExpectedLoads", "renderStructures", "pendingPathway", "_checkAllLoaded", "_pathwayLoaded", "hasClusterPlugin", "clusterGroup", "total", "geom", "marker", "clusterIcon", "nextZoom", "geoLayer", "structureIcon", "entry", "_makePathwayOpts", "styleObj", "calcPathwayLength", "totalLength", "haversine", "CFG", "initializePathwaysMap", "elementId", "config", "_a", "container", "Sidebar", "titleCase", "esc", "debounce", "getCookie", "STRUCTURE_COLORS", "STRUCTURE_SHAPES", "PATHWAY_COLORS", "API_BASE", "Popover", "map", "baseLayers", "layerControl", "createMap", "createStatsControl", "createLegend", "createKioskControl", "createSidebarToggleControl", "createLocateControl", "structureCountEl", "pathwayCountEl", "totalLengthEl", "zoomHint", "createZoomHint", "PREFS_KEY", "DEFAULT_LAYERS", "_loadPrefs", "saved", "_e", "_savePrefs", "layers", "layerPrefs", "dataLayers", "createDataLayers", "layerNames", "externalConfigs", "externalGroups", "initExternalLayers", "name", "group", "cfg", "getLayerConfig", "lname", "LAYER_INFO_KEY", "extName", "_layerCheckboxes", "_syncSidebarCheckbox", "checked", "btn", "e", "prefs", "LAYER_ICONS", "_buildSidebarLayerToggles", "toggleContainer", "active", "iconSvg", "span", "wrapper", "cb", "cbName", "checkbox", "button", "_loadData", "pathwayCount", "totalLength", "_updateUrl", "center", "zoom", "params", "selId", "newUrl", "_setLayerChip", "count", "chip", "_applyDecisionToToggles", "decision", "info", "infoKey", "isHidden", "_runLoad", "loadDataLayers", "entry", "feature", "data", "calcPathwayLength", "allFeatures", "MIN_DATA_ZOOM", "visibleExternal", "bbox", "bboxParam", "loadExternalLayers", "extCfg", "extEntries", "i", "_pendingSelectId", "_enabledInfoKeys", "set", "fetchMapInfo", "decideLayerRendering", "debouncedUrlUpdate", "query", "serverSearch", "results", "resetBtn"] + "sources": ["../src/types/features.ts", "../src/external-layers.ts", "../src/sidebar.ts", "../src/popover.ts", "../src/map-utils.ts", "../src/map-core.ts", "../src/data-layers.ts", "../src/load-strategy.ts", "../src/pathways-map.ts"], + "sourcesContent": ["/** Shared types for map features and sidebar entries. */\n\nexport interface GeoJSONProperties {\n id: number;\n name: string;\n url: string;\n structure_type?: string;\n pathway_type?: string;\n site_name?: string;\n cluster?: boolean;\n point_count?: number;\n [key: string]: unknown;\n}\n\n// Native layer types (used internally for styling/detail lookups)\nexport const NATIVE_TYPES = ['structure', 'conduit_bank', 'conduit', 'aerial', 'direct_buried', 'circuit'] as const;\nexport type NativeFeatureType = typeof NATIVE_TYPES[number];\n\n// Any feature type \u2014 includes external layer names\nexport type FeatureType = string;\n\nexport interface FeatureEntry {\n props: GeoJSONProperties;\n featureType: FeatureType;\n layer: L.Layer;\n latlng: L.LatLng;\n}\n\nexport interface ServerSearchResult {\n name: string;\n featureType: FeatureType;\n typeKey: string;\n latlng: L.LatLng;\n url?: string;\n}\n\nexport type DetailFieldDef = [string, string] | [string, string, string];\n\nexport interface ResolvedValue {\n text: string;\n url?: string | null;\n}\n\nexport interface PathwayStyle {\n color: string;\n weight: number;\n opacity: number;\n dashArray: string;\n}\n\nexport interface PathwayLayerConfig {\n endpoint: string;\n layer: L.LayerGroup;\n featureType: FeatureType;\n style: PathwayStyle;\n}\n", "/**\n * External layer rendering module.\n *\n * Fetches GeoJSON from registered external layers, applies configured\n * styles, and returns FeatureEntry objects for sidebar/popover integration.\n */\n\nimport type { FeatureEntry, GeoJSONProperties } from './types/features';\nimport type { ExternalLayerConfig } from './types/external';\n\ninterface ExternalLayerState {\n config: ExternalLayerConfig;\n layerGroup: L.LayerGroup;\n abortController: AbortController | null;\n}\n\nconst _layerStates: Map = new Map();\n\n/** Resolve the color for a feature based on the layer style config. */\nfunction _resolveColor(\n props: GeoJSONProperties,\n style: ExternalLayerConfig['style'],\n): string {\n if (style.colorField && style.colorMap) {\n const val = String(props[style.colorField] ?? '');\n return style.colorMap[val] ?? style.defaultColor;\n }\n return style.color;\n}\n\n/** Create a Leaflet marker for a point feature.\n * TODO: Use iconClass to render MDI icons instead of plain circles. */\nfunction _createPointMarker(\n latlng: L.LatLng,\n color: string,\n): L.CircleMarker {\n return L.circleMarker(latlng, {\n radius: 7,\n fillColor: color,\n color: '#fff',\n weight: 2,\n opacity: 1,\n fillOpacity: 0.85,\n });\n}\n\n/** Create a Leaflet polyline for a line feature. */\nfunction _createLine(\n coords: L.LatLngExpression[],\n color: string,\n style: ExternalLayerConfig['style'],\n): L.Polyline {\n return L.polyline(coords, {\n color,\n weight: style.weight,\n opacity: style.opacity,\n dashArray: style.dash ?? undefined,\n });\n}\n\n/** Create a Leaflet polygon for a polygon feature. */\nfunction _createPolygon(\n coords: L.LatLngExpression[][],\n color: string,\n style: ExternalLayerConfig['style'],\n): L.Polygon {\n return L.polygon(coords, {\n color,\n fillColor: color,\n fillOpacity: 0.2,\n weight: style.weight,\n opacity: style.opacity,\n dashArray: style.dash ?? undefined,\n });\n}\n\n/**\n * Initialize layer groups for all external layers.\n * Returns a map of layer name -> L.LayerGroup for the layer control.\n */\nexport function initExternalLayers(\n configs: ExternalLayerConfig[],\n map: L.Map,\n): Map {\n _layerStates.clear();\n const groups = new Map();\n\n // Sort by sortOrder for consistent z-ordering\n const sorted = [...configs].sort((a, b) => a.sortOrder - b.sortOrder);\n\n for (const cfg of sorted) {\n const group = L.layerGroup();\n _layerStates.set(cfg.name, {\n config: cfg,\n layerGroup: group,\n abortController: null,\n });\n groups.set(cfg.name, group);\n\n if (cfg.defaultVisible) {\n group.addTo(map);\n }\n }\n return groups;\n}\n\n/**\n * Fetch and render features for all visible external layers.\n * Returns FeatureEntry[] for sidebar integration.\n *\n * @param bbox - \"W,S,E,N\" string\n * @param zoom - current zoom level\n * @param visibleLayers - set of layer names currently toggled on\n * @param onFeature - callback for each created feature (for sidebar/popover wiring)\n */\nexport async function loadExternalLayers(\n bbox: string,\n zoom: number,\n visibleLayers: Set,\n onFeature: (entry: FeatureEntry, config: ExternalLayerConfig) => void,\n): Promise {\n const allEntries: FeatureEntry[] = [];\n const fetchPromises: Promise[] = [];\n\n for (const [name, state] of _layerStates) {\n if (!visibleLayers.has(name)) continue;\n if (zoom < state.config.minZoom) continue;\n if (state.config.maxZoom !== null && zoom > state.config.maxZoom) continue;\n\n // Abort any in-flight request for this layer\n if (state.abortController) {\n state.abortController.abort();\n }\n state.abortController = new AbortController();\n\n const promise = _fetchLayer(state, bbox, zoom, allEntries, onFeature);\n fetchPromises.push(promise);\n }\n\n // Wait for all fetches; individual errors are caught inside _fetchLayer\n await Promise.all(fetchPromises.map(p => p.catch(() => {})));\n return allEntries;\n}\n\nasync function _fetchLayer(\n state: ExternalLayerState,\n bbox: string,\n zoom: number,\n entries: FeatureEntry[],\n onFeature: (entry: FeatureEntry, config: ExternalLayerConfig) => void,\n): Promise {\n const { config, layerGroup, abortController } = state;\n const sep = config.url.includes('?') ? '&' : '?';\n const url = `${config.url}${sep}format=json&bbox=${bbox}&zoom=${zoom}`;\n\n // Read CSRF token from cookie\n const csrfMatch = document.cookie.match(/csrftoken=([^;]+)/);\n const headers: Record = {\n Accept: 'application/json',\n };\n if (csrfMatch) {\n headers['X-CSRFToken'] = csrfMatch[1];\n }\n\n try {\n const resp = await fetch(url, {\n headers,\n signal: abortController?.signal,\n });\n if (!resp.ok) {\n console.warn(`External layer '${config.name}' fetch failed: ${resp.status}`);\n return;\n }\n const data = await resp.json() as GeoJSON.FeatureCollection;\n layerGroup.clearLayers();\n\n for (const feature of data.features) {\n if (!feature.geometry) continue;\n const props = (feature.properties ?? {}) as GeoJSONProperties;\n // Copy top-level feature.id to properties if not already present\n if (feature.id != null && props.id == null) {\n props.id = feature.id as number;\n }\n const color = _resolveColor(props, config.style);\n let layer: L.Layer | null = null;\n let latlng: L.LatLng | undefined;\n\n if (feature.geometry.type === 'Point') {\n const [lng, lat] = (feature.geometry as GeoJSON.Point).coordinates as [number, number];\n latlng = L.latLng(lat, lng);\n layer = _createPointMarker(latlng, color);\n } else if (feature.geometry.type === 'LineString') {\n const coords = (feature.geometry as GeoJSON.LineString).coordinates.map(\n (c: number[]) => L.latLng(c[1], c[0]),\n );\n const line = _createLine(coords, color, config.style);\n latlng = line.getBounds().getCenter();\n layer = line;\n } else if (feature.geometry.type === 'Polygon') {\n const rings = (feature.geometry as GeoJSON.Polygon).coordinates.map(\n (ring: number[][]) => ring.map((c: number[]) => L.latLng(c[1], c[0])),\n );\n const poly = _createPolygon(rings, color, config.style);\n latlng = poly.getBounds().getCenter();\n layer = poly;\n }\n\n if (layer && latlng) {\n layerGroup.addLayer(layer);\n const entry: FeatureEntry = {\n props: { ...props, name: props.name ?? `${config.label} #${props.id}` },\n featureType: config.name,\n layer,\n latlng,\n };\n entries.push(entry);\n onFeature(entry, config);\n }\n }\n } catch (err) {\n if (err instanceof DOMException && err.name === 'AbortError') return;\n console.warn(`External layer '${config.name}' error:`, err);\n }\n}\n\n/** Get the ExternalLayerConfig for a layer name, if it exists. */\nexport function getLayerConfig(name: string): ExternalLayerConfig | undefined {\n return _layerStates.get(name)?.config;\n}\n\n/** Get all layer configs. */\nexport function getAllLayerConfigs(): ExternalLayerConfig[] {\n return Array.from(_layerStates.values()).map(s => s.config);\n}\n", "/**\n * Sidebar module for the full-page infrastructure map.\n *\n * Provides feature list, search/filter, detail panel with enriched\n * REST API data, inline name editing, and map feature highlighting.\n */\n\nimport type { FeatureEntry, FeatureType, DetailFieldDef, ResolvedValue, ServerSearchResult } from './types/features';\nimport { NATIVE_TYPES } from './types/features';\nimport { getLayerConfig } from './external-layers';\n\n// ---------------------------------------------------------------------------\n// Module-level helpers imported from the outer scope at init time\n// ---------------------------------------------------------------------------\n\n/** Set externally by pathways-map before Sidebar.init(). */\nlet _titleCase: (s: string) => string;\nlet _esc: (s: string) => string;\nlet _debounce: (fn: () => void, delay: number) => () => void;\nlet _getCookie: (name: string) => string | null;\n\nlet STRUCTURE_COLORS: Record;\nlet STRUCTURE_SHAPES: Record;\nlet PATHWAY_COLORS: Record;\nlet API_BASE: string;\n\n// ---------------------------------------------------------------------------\n// Internal state\n// ---------------------------------------------------------------------------\n\nlet _map: L.Map | null = null;\nlet _features: FeatureEntry[] = [];\nlet _filtered: FeatureEntry[] = [];\nlet _selected: FeatureEntry | null = null;\nconst _activeTypes: Record = {};\nconst _detailCache: Record> = {};\nlet _highlightedLayer: (L.Marker & { _origIcon?: L.Icon | L.DivIcon }) | (L.Polyline & { _origStyle?: L.PathOptions }) | null = null;\nlet _highlightOutline: L.Polyline | null = null;\nlet _serverSearchCallback: ((query: string) => void) | null = null;\nlet _lastServerQuery = '';\nlet _pendingSelect: { url: string; featureType: string } | null = null;\nlet _pendingFlySelectId = '';\nlet _isKiosk = false;\nlet _onSelectionChange: (() => void) | null = null;\nlet _dimmedFeatures: FeatureEntry[] = [];\nlet _activeConnectedIds: Set | null = null;\nconst DIM_OPACITY = 0.15;\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction _isNativeType(featureType: string): boolean {\n return (NATIVE_TYPES as readonly string[]).indexOf(featureType) !== -1;\n}\n\nfunction _typeLabel(featureType: string): string {\n const extCfg = getLayerConfig(featureType);\n if (extCfg) return extCfg.label;\n return _titleCase(featureType.replace(/_/g, ' '));\n}\n\nfunction _colorForFeature(entry: FeatureEntry): string {\n if (entry.featureType === 'structure') {\n return STRUCTURE_COLORS[entry.props.structure_type || ''] || '#616161';\n }\n if (entry.featureType === 'circuit') {\n return '#d32f2f';\n }\n return PATHWAY_COLORS[entry.props.pathway_type || ''] || '#616161';\n}\n\nfunction _typeKeyForFeature(entry: FeatureEntry): string {\n if (entry.featureType === 'structure') {\n return (entry.props.structure_type as string) || 'unknown';\n }\n return (entry.props.pathway_type as string) || 'unknown';\n}\n\nfunction _featureId(entry: FeatureEntry): string {\n return entry.featureType + '-' + (entry.props.id || '');\n}\n\n/** Mix a hex colour toward white by `amount` (0 = original, 1 = white). */\nfunction _lighten(hex: string, amount: number): string {\n const n = parseInt(hex.replace('#', ''), 16);\n const r = Math.round(((n >> 16) & 0xff) + (255 - ((n >> 16) & 0xff)) * amount);\n const g = Math.round(((n >> 8) & 0xff) + (255 - ((n >> 8) & 0xff)) * amount);\n const b = Math.round((n & 0xff) + (255 - (n & 0xff)) * amount);\n return '#' + ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1);\n}\n\n// ---------------------------------------------------------------------------\n// Highlight logic\n// ---------------------------------------------------------------------------\n\nfunction _unhighlightMapFeature(): void {\n if (_highlightOutline) {\n _highlightOutline.remove();\n _highlightOutline = null;\n }\n if (_highlightedLayer) {\n const layer = _highlightedLayer as Record;\n if (layer._origIcon) {\n (layer as any).setIcon(layer._origIcon);\n delete layer._origIcon;\n }\n if (layer._origStyle && typeof (layer as any).setStyle === 'function') {\n (layer as any).setStyle(layer._origStyle);\n delete layer._origStyle;\n }\n _highlightedLayer = null;\n }\n}\n\nfunction _applyHighlightVisuals(entry: FeatureEntry): void {\n const layer = entry.layer;\n if (!layer) return;\n _highlightedLayer = layer as any;\n\n if (entry.featureType === 'structure') {\n const marker = layer as L.Marker & { _origIcon?: L.Icon | L.DivIcon };\n marker._origIcon = (marker as any).getIcon();\n const type = entry.props.structure_type || '';\n const color = STRUCTURE_COLORS[type] || '#616161';\n const shape = STRUCTURE_SHAPES[type] || '';\n const isOutline = shape.includes('fill=\"none\"');\n marker.setIcon(L.divIcon({\n className: 'pw-marker pw-marker-selected',\n html: '' + shape + '',\n iconSize: [26, 26] as [number, number],\n iconAnchor: [13, 13] as [number, number],\n popupAnchor: [0, -14] as [number, number],\n }));\n } else {\n const polyline = layer as L.Polyline & { _origStyle?: L.PathOptions };\n const origStyle = (polyline as any).options || {};\n polyline._origStyle = {\n weight: origStyle.weight || 3,\n opacity: origStyle.opacity || 0.7,\n color: origStyle.color,\n dashArray: origStyle.dashArray,\n };\n const latlngs = polyline.getLatLngs() as L.LatLng[];\n if (latlngs && latlngs.length > 0 && _map) {\n // Glow: lighten the feature's own color toward white\n _highlightOutline = L.polyline(latlngs, {\n color: _lighten(origStyle.color || '#888', 0.55),\n weight: 12,\n opacity: 0.5,\n interactive: false,\n }).addTo(_map);\n }\n polyline.setStyle({ weight: 6, opacity: 1, dashArray: '' });\n }\n}\n\nfunction _highlightMapFeature(entry: FeatureEntry): void {\n _unhighlightMapFeature();\n _applyHighlightVisuals(entry);\n}\n\nfunction _reapplyHighlight(entry: FeatureEntry): void {\n if (_highlightOutline) {\n _highlightOutline.remove();\n _highlightOutline = null;\n }\n _highlightedLayer = null;\n _applyHighlightVisuals(entry);\n}\n\n// ---------------------------------------------------------------------------\n// Focus dimming \u2014 dim features not connected to the selected one\n// ---------------------------------------------------------------------------\n\nfunction _dimFeatures(connectedIds: Set): void {\n _dimmedFeatures = [];\n _activeConnectedIds = connectedIds;\n _applyDim();\n}\n\nfunction _applyDim(): void {\n if (!_activeConnectedIds) return;\n _dimmedFeatures = [];\n _features.forEach(function (f: FeatureEntry) {\n const fid = _featureId(f);\n if (_activeConnectedIds!.has(fid)) return;\n if (_selected && fid === _featureId(_selected)) return;\n if (!f.layer) return;\n\n _dimmedFeatures.push(f);\n if (f.featureType === 'structure') {\n (f.layer as L.Marker).setOpacity(DIM_OPACITY);\n } else if (typeof (f.layer as L.Polyline).setStyle === 'function') {\n (f.layer as L.Polyline).setStyle({ opacity: DIM_OPACITY });\n }\n });\n}\n\nfunction _restoreDim(): void {\n _activeConnectedIds = null;\n _dimmedFeatures.forEach(function (f: FeatureEntry) {\n if (!f.layer) return;\n if (f.featureType === 'structure') {\n (f.layer as L.Marker).setOpacity(1);\n } else if (typeof (f.layer as L.Polyline).setStyle === 'function') {\n const orig = (f.layer as any)._origStyle;\n (f.layer as L.Polyline).setStyle({ opacity: orig ? orig.opacity : 0.7 });\n }\n });\n _dimmedFeatures = [];\n}\n\n// ---------------------------------------------------------------------------\n// List rendering\n// ---------------------------------------------------------------------------\n\nfunction _highlightListItem(entry: FeatureEntry | null): void {\n const listEl = document.getElementById('pw-feature-list');\n if (!listEl) return;\n const items = listEl.querySelectorAll('.pw-list-item');\n const targetId = entry ? _featureId(entry) : null;\n for (let i = 0; i < items.length; i++) {\n items[i].classList.toggle('active', items[i].getAttribute('data-feature-id') === targetId);\n }\n}\n\nfunction _renderList(): void {\n const listEl = document.getElementById('pw-feature-list');\n const countEl = document.getElementById('pw-list-count');\n if (!listEl) return;\n\n listEl.textContent = '';\n if (countEl) {\n countEl.textContent = String(_filtered.length);\n countEl.style.display = _filtered.length > 0 ? '' : 'none';\n }\n\n _filtered.forEach(function (entry: FeatureEntry) {\n const item = document.createElement('div');\n item.className = 'pw-list-item';\n item.setAttribute('data-feature-id', _featureId(entry));\n\n if (_selected && _featureId(_selected) === _featureId(entry)) {\n item.classList.add('active');\n }\n\n const dot = document.createElement('span');\n dot.className = 'pw-list-dot';\n dot.style.background = _colorForFeature(entry);\n item.appendChild(dot);\n\n const label = document.createElement('span');\n label.className = 'pw-list-label';\n label.textContent = entry.props.name || 'Unnamed';\n label.title = entry.props.name || 'Unnamed';\n item.appendChild(label);\n\n const typeBadge = document.createElement('span');\n typeBadge.className = 'pw-list-type';\n typeBadge.textContent = _titleCase(_typeKeyForFeature(entry));\n item.appendChild(typeBadge);\n\n item.addEventListener('click', function () {\n selectFeature(entry);\n });\n\n listEl.appendChild(item);\n });\n}\n\n// ---------------------------------------------------------------------------\n// Filters\n// ---------------------------------------------------------------------------\n\nfunction _buildTypeFilters(): void {\n const container = document.getElementById('pw-type-filters');\n if (!container) return;\n container.textContent = '';\n\n const typeMap: Record = {};\n _features.forEach(function (entry: FeatureEntry) {\n const key = _typeKeyForFeature(entry);\n if (!typeMap[key]) {\n typeMap[key] = _colorForFeature(entry);\n }\n });\n\n const types = Object.keys(typeMap).sort();\n if (types.length <= 1) return;\n\n types.forEach(function (t: string) {\n if (_activeTypes[t] === undefined) {\n _activeTypes[t] = true;\n }\n });\n\n types.forEach(function (type: string) {\n const btn = document.createElement('button');\n btn.className = 'pw-filter-btn' + (_activeTypes[type] ? ' active' : '');\n btn.type = 'button';\n\n const dot = document.createElement('span');\n dot.className = 'pw-filter-dot';\n dot.style.background = typeMap[type];\n btn.appendChild(dot);\n\n const label = document.createTextNode(_typeLabel(type));\n btn.appendChild(label);\n\n btn.addEventListener('click', function () {\n _activeTypes[type] = !_activeTypes[type];\n btn.classList.toggle('active', _activeTypes[type]);\n _applyFilters();\n });\n\n container.appendChild(btn);\n });\n}\n\nfunction _applyFilters(): void {\n const searchInput = document.getElementById('pw-search') as HTMLInputElement | null;\n const query = (searchInput ? searchInput.value : '').toLowerCase().trim();\n\n _filtered = _features.filter(function (entry: FeatureEntry) {\n const typeKey = _typeKeyForFeature(entry);\n if (_activeTypes[typeKey] === false) return false;\n\n if (query) {\n const name = (entry.props.name || '').toLowerCase();\n const type = _titleCase(typeKey).toLowerCase();\n if (name.indexOf(query) === -1 && type.indexOf(query) === -1) {\n return false;\n }\n }\n\n return true;\n });\n\n _renderList();\n\n // If no client-side results and there's a query, fall back to server search\n if (_filtered.length === 0 && query.length >= 2 && _serverSearchCallback) {\n if (query !== _lastServerQuery) {\n _lastServerQuery = query;\n _showSearchingIndicator();\n _serverSearchCallback(query);\n }\n } else {\n _lastServerQuery = '';\n _clearServerResults();\n }\n}\n\n// ---------------------------------------------------------------------------\n// API helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Build the UI detail-page URL for a native feature client-side.\n *\n * The GeoJSON geo endpoints intentionally omit a ``url`` property because\n * computing get_absolute_url() per row calls django.urls.reverse(), which\n * triggers lazy URL-pattern population (including the Strawberry GraphQL\n * schema). That single import adds ~12 s per 1000 features, making the\n * geo API unusably slow. Building the URL here costs nothing.\n */\nfunction _detailPageUrl(entry: FeatureEntry): string {\n const id = entry.props.id;\n if (!_isNativeType(entry.featureType) || id == null) return '';\n const base = '/plugins/pathways/';\n switch (entry.featureType) {\n case 'structure': return base + 'structures/' + id + '/';\n case 'conduit_bank': return base + 'conduit-banks/' + id + '/';\n case 'conduit': return base + 'conduits/' + id + '/';\n case 'aerial': return base + 'aerial-spans/' + id + '/';\n case 'direct_buried': return base + 'direct-buried/' + id + '/';\n case 'circuit': return base + 'circuit-geometries/' + id + '/';\n default: return base + 'pathways/' + id + '/';\n }\n}\n\nfunction _apiUrlForFeature(entry: FeatureEntry): string {\n if (entry.props.url) {\n return '/api' + entry.props.url;\n }\n const id = entry.props.id;\n\n // Native types use the pathways API\n if (_isNativeType(entry.featureType)) {\n const base = API_BASE.replace(/geo\\/?$/, '');\n switch (entry.featureType) {\n case 'structure':\n return base + 'structures/' + id + '/';\n case 'conduit_bank':\n return base + 'conduit-banks/' + id + '/';\n case 'conduit':\n return base + 'conduits/' + id + '/';\n case 'aerial':\n return base + 'aerial-spans/' + id + '/';\n case 'direct_buried':\n return base + 'direct-buried/' + id + '/';\n case 'circuit':\n return base + 'circuit-geometries/' + id + '/';\n default:\n return base + 'pathways/' + id + '/';\n }\n }\n\n // Check external layer config for detail URL\n const extCfg = getLayerConfig(entry.featureType);\n if (extCfg?.detail?.urlTemplate) {\n return extCfg.detail.urlTemplate.replace('{id}', String(id));\n }\n return '';\n}\n\n// ---------------------------------------------------------------------------\n// Value resolver\n// ---------------------------------------------------------------------------\n\nfunction _resolveValue(val: unknown): ResolvedValue | null {\n if (val === null || val === undefined || val === '') return null;\n\n // Array\n if (Array.isArray(val)) {\n if (val.length === 0) return null;\n const texts: string[] = [];\n for (let i = 0; i < val.length; i++) {\n const r = _resolveValue(val[i]);\n if (r) texts.push(r.text);\n }\n return texts.length > 0 ? { text: texts.join(', ') } : null;\n }\n\n // Choice field: {value, label}\n if (typeof val === 'object' && val !== null && 'label' in val) {\n const choice = val as { value?: string; label?: string };\n return { text: choice.label || _titleCase(choice.value || '') };\n }\n\n // Nested FK: {id, url, display_url, display, ...}\n if (typeof val === 'object' && val !== null) {\n const fk = val as { id?: number; url?: string; display_url?: string; display?: string; name?: string };\n if (fk.display || fk.name || fk.id !== undefined) {\n return { text: fk.display || fk.name || String(fk.id), url: fk.display_url || fk.url || null };\n }\n }\n\n // Boolean\n if (val === true) return { text: 'Yes' };\n if (val === false) return { text: 'No' };\n\n // Primitive\n return { text: String(val) };\n}\n\nfunction _addFieldRow(table: HTMLTableElement, label: string, val: unknown, suffix?: string): void {\n const resolved = _resolveValue(val);\n if (!resolved) return;\n const text = resolved.text + (suffix || '');\n const tr = document.createElement('tr');\n const tdLabel = document.createElement('td');\n tdLabel.textContent = label;\n const tdVal = document.createElement('td');\n if (resolved.url) {\n const a = document.createElement('a');\n a.href = resolved.url;\n a.textContent = text;\n tdVal.appendChild(a);\n } else {\n tdVal.textContent = text;\n }\n tr.appendChild(tdLabel);\n tr.appendChild(tdVal);\n table.appendChild(tr);\n}\n\ninterface TagData {\n color?: string;\n display?: string;\n name?: string;\n}\n\nfunction _addTagsRow(table: HTMLTableElement, tags: TagData[] | undefined): void {\n if (!tags || !tags.length) return;\n const tr = document.createElement('tr');\n const tdLabel = document.createElement('td');\n tdLabel.textContent = 'Tags';\n const tdVal = document.createElement('td');\n tags.forEach(function (tag: TagData) {\n const badge = document.createElement('span');\n badge.className = 'badge';\n badge.style.cssText = 'margin-right:4px;margin-bottom:2px;';\n if (tag.color) {\n badge.style.background = '#' + tag.color;\n badge.style.color = '#fff';\n } else {\n badge.style.background = 'var(--tblr-border-color-translucent, rgba(0,0,0,0.1))';\n }\n badge.textContent = tag.display || tag.name || String(tag);\n tdVal.appendChild(badge);\n });\n tr.appendChild(tdLabel);\n tr.appendChild(tdVal);\n table.appendChild(tr);\n}\n\n// ---------------------------------------------------------------------------\n// Detail field definitions\n// ---------------------------------------------------------------------------\n\nconst DETAIL_FIELDS: Record = {\n structure: [\n ['Type', 'structure_type'],\n ['Site', 'site'],\n ['Elevation', 'elevation', ' m'],\n ['Height', 'height', ' m'],\n ['Width', 'width', ' m'],\n ['Length', 'length', ' m'],\n ['Depth', 'depth', ' m'],\n ['Tenant', 'tenant'],\n ['Installation Date', 'installation_date'],\n ['Access Notes', 'access_notes'],\n ['Comments', 'comments'],\n ],\n conduit_bank: [\n ['Start Structure', 'start_structure'],\n ['End Structure', 'end_structure'],\n ['Start Face', 'start_face'],\n ['End Face', 'end_face'],\n ['Configuration', 'configuration'],\n ['Total Conduits', 'total_conduits'],\n ['Encasement', 'encasement_type'],\n ['Length', 'length', ' m'],\n ['Tenant', 'tenant'],\n ['Installation Date', 'installation_date'],\n ['Comments', 'comments'],\n ],\n conduit: [\n ['Start Structure', 'start_structure'],\n ['End Structure', 'end_structure'],\n ['Start Location', 'start_location'],\n ['End Location', 'end_location'],\n ['Material', 'material'],\n ['Inner Diameter', 'inner_diameter', ' mm'],\n ['Outer Diameter', 'outer_diameter', ' mm'],\n ['Depth', 'depth', ' m'],\n ['Length', 'length', ' m'],\n ['Conduit Bank', 'conduit_bank'],\n ['Bank Position', 'bank_position'],\n ['Start Junction', 'start_junction'],\n ['End Junction', 'end_junction'],\n ['Cables Routed', 'cables_routed'],\n ['Tenant', 'tenant'],\n ['Installation Date', 'installation_date'],\n ['Comments', 'comments'],\n ],\n aerial: [\n ['Start Structure', 'start_structure'],\n ['End Structure', 'end_structure'],\n ['Start Location', 'start_location'],\n ['End Location', 'end_location'],\n ['Aerial Type', 'aerial_type'],\n ['Start Attachment Height', 'start_attachment_height', ' m'],\n ['End Attachment Height', 'end_attachment_height', ' m'],\n ['Attachment Height (mean)', 'attachment_height', ' m'],\n ['Sag', 'sag', ' m'],\n ['Messenger Size', 'messenger_size'],\n ['Wind Loading', 'wind_loading'],\n ['Ice Loading', 'ice_loading'],\n ['Length', 'length', ' m'],\n ['Cables Routed', 'cables_routed'],\n ['Tenant', 'tenant'],\n ['Installation Date', 'installation_date'],\n ['Comments', 'comments'],\n ],\n direct_buried: [\n ['Start Structure', 'start_structure'],\n ['End Structure', 'end_structure'],\n ['Start Location', 'start_location'],\n ['End Location', 'end_location'],\n ['Burial Depth', 'burial_depth', ' m'],\n ['Warning Tape', 'warning_tape'],\n ['Tracer Wire', 'tracer_wire'],\n ['Armor Type', 'armor_type'],\n ['Length', 'length', ' m'],\n ['Cables Routed', 'cables_routed'],\n ['Tenant', 'tenant'],\n ['Installation Date', 'installation_date'],\n ['Comments', 'comments'],\n ],\n circuit: [\n ['Circuit', 'circuit'],\n ['Provider Reference', 'provider_reference'],\n ['Comments', 'comments'],\n ],\n default: [\n ['Start Structure', 'start_structure'],\n ['End Structure', 'end_structure'],\n ['Start Location', 'start_location'],\n ['End Location', 'end_location'],\n ['Length', 'length', ' m'],\n ['Cables Routed', 'cables_routed'],\n ['Tenant', 'tenant'],\n ['Installation Date', 'installation_date'],\n ['Comments', 'comments'],\n ],\n};\n\n// ---------------------------------------------------------------------------\n// Enriched detail rendering\n// ---------------------------------------------------------------------------\n\n/** Create a collapsible section with chevron toggle. */\nfunction _createSection(title: string, parent: HTMLElement): HTMLElement {\n const header = document.createElement('div');\n header.className = 'pw-section-header';\n const chevron = document.createElement('i');\n chevron.className = 'mdi mdi-chevron-down';\n header.appendChild(chevron);\n const label = document.createElement('span');\n label.textContent = title;\n header.appendChild(label);\n parent.appendChild(header);\n\n const body = document.createElement('div');\n body.className = 'pw-section-body';\n parent.appendChild(body);\n\n header.addEventListener('click', function () {\n const collapsed = body.classList.toggle('collapsed');\n header.classList.toggle('collapsed', collapsed);\n });\n\n return body;\n}\n\n/** Add enrichment metric badges from API data to the badge row. */\nfunction _addEnrichmentBadges(data: Record, entry: FeatureEntry): void {\n const badgeRow = document.querySelector('.pw-metric-row');\n if (!badgeRow) return;\n\n // Add measurement badges based on feature type\n const metrics: [string, string, string][] = []; // [label, field, suffix]\n\n if (entry.featureType === 'structure') {\n if (data.elevation) metrics.push(['Elev', 'elevation', ' m']);\n } else {\n if (data.length) metrics.push(['Length', 'length', ' m']);\n }\n\n if (entry.featureType === 'conduit') {\n if (data.inner_diameter) metrics.push(['ID', 'inner_diameter', ' mm']);\n }\n\n metrics.forEach(function (m) {\n const resolved = _resolveValue(data[m[1]]);\n if (!resolved) return;\n const badge = document.createElement('span');\n badge.className = 'pw-metric-badge pw-metric-muted';\n badge.textContent = m[0] + ' ' + resolved.text + m[2];\n badgeRow.appendChild(badge);\n });\n}\n\nfunction _renderEnrichedDetail(data: Record, entry: FeatureEntry, container: HTMLElement): void {\n // Add metric badges from enriched data\n _addEnrichmentBadges(data, entry);\n\n // Check if this is an external layer feature\n const extCfg = getLayerConfig(entry.featureType);\n if (extCfg?.detail) {\n const sectionBody = _createSection('Details', container);\n const table = document.createElement('table');\n table.className = 'pw-detail-table';\n for (const fieldName of extCfg.detail.fields) {\n const val = data[fieldName];\n if (val !== undefined && val !== null) {\n _addFieldRow(table, _titleCase(fieldName.replace(/_/g, ' ')), val);\n }\n }\n sectionBody.appendChild(table);\n return;\n }\n\n const fields = DETAIL_FIELDS[entry.featureType] || DETAIL_FIELDS['default'];\n const table = document.createElement('table');\n table.className = 'pw-detail-table';\n\n fields.forEach(function (f: DetailFieldDef) {\n const val = data[f[1]];\n _addFieldRow(table, f[0], val, f[2] || '');\n });\n\n _addTagsRow(table, data.tags as TagData[] | undefined);\n\n if (table.childNodes.length > 0) {\n const sectionBody = _createSection('Details', container);\n sectionBody.appendChild(table);\n }\n\n // Timestamps\n if (data.created || data.last_updated) {\n const tsDiv = document.createElement('div');\n tsDiv.style.cssText = 'font-size:0.72em;color:var(--tblr-muted-color,#667382);margin-top:8px;';\n const parts: string[] = [];\n if (data.created) parts.push('Created ' + (data.created as string).split('T')[0]);\n if (data.last_updated) parts.push('Updated ' + (data.last_updated as string).split('T')[0]);\n tsDiv.textContent = parts.join(' \\u00b7 ');\n container.appendChild(tsDiv);\n }\n\n // For structures: show connected pathways\n if (entry.featureType === 'structure') {\n _fetchConnectedPathways(entry, container);\n }\n // For pathways: show connected structures\n if (entry.featureType !== 'structure') {\n _renderConnectedStructures(data, entry, container);\n }\n}\n\n/**\n * Render a clickable list item for a connected object.\n *\n * @param selectId Feature ID (e.g. \"structure-123\") used to look up in\n * viewport or navigate to if not found.\n * @param hintLatLng Approximate location for flyTo when feature is off-screen.\n */\nfunction _renderConnectedItem(\n parent: HTMLElement,\n name: string,\n color: string,\n typeLabel: string,\n mapFeature: FeatureEntry | undefined,\n selectId: string,\n hintLatLng?: { lat: number; lng: number },\n): void {\n const item = document.createElement('div');\n item.className = 'pw-list-item';\n item.style.padding = '6px 0';\n item.style.cursor = 'pointer';\n\n const dot = document.createElement('span');\n dot.className = 'pw-list-dot';\n dot.style.background = color;\n item.appendChild(dot);\n\n const label = document.createElement('span');\n label.className = 'pw-list-label';\n label.textContent = name || 'Unnamed';\n item.appendChild(label);\n\n const badge = document.createElement('span');\n badge.className = 'pw-metric-badge pw-metric-muted';\n badge.style.fontSize = '0.65em';\n badge.style.padding = '1px 6px';\n badge.textContent = typeLabel;\n item.appendChild(badge);\n\n item.addEventListener('click', function () {\n // Re-check viewport \u2014 features may have been loaded since render\n const current = _features.find(function (f: FeatureEntry) {\n return _featureId(f) === selectId;\n });\n if (current) {\n selectFeature(current);\n } else if (hintLatLng && _map) {\n // Fly to approximate location; data reload will auto-select\n _pendingFlySelectId = selectId;\n _map.flyTo(hintLatLng as L.LatLngExpression, 18, { duration: 0.5 });\n } else {\n // No cached location \u2014 full page navigation\n window.location.href = window.location.pathname + '?select=' + encodeURIComponent(selectId);\n }\n });\n\n parent.appendChild(item);\n}\n\n/** Fetch connected pathways and neighbor structures for a structure. */\nfunction _fetchConnectedPathways(entry: FeatureEntry, container: HTMLElement): void {\n const structId = entry.props.id;\n const base = API_BASE.replace(/geo\\/?$/, '');\n const url = base + 'pathways/?structure_id=' + structId;\n\n const headers: Record = { 'Accept': 'application/json' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n\n fetch(url, { headers }).then(function (resp) {\n return resp.ok ? resp.json() : null;\n }).then(function (data: { results?: Array> } | null) {\n if (!data || !data.results || data.results.length === 0) return;\n\n // Collect neighbor structures from pathway endpoints\n interface NeighborInfo { id: number; display: string; hint?: { lat: number; lng: number } }\n const neighborMap: Record = {};\n const pwItems: Array<{ display: string; color: string; typeLabel: string; mapFeature: FeatureEntry | undefined; selectId: string; hint?: { lat: number; lng: number } }> = [];\n\n data.results.forEach(function (pw: Record) {\n const pwType = pw.pathway_type as { value?: string; label?: string } | string || '';\n const typeKey = typeof pwType === 'object' ? (pwType.value || '') : pwType;\n const typeLabel = typeof pwType === 'object' ? (pwType.label || _titleCase(typeKey)) : _titleCase(typeKey);\n const color = PATHWAY_COLORS[typeKey] || '#616161';\n const display = (pw.display || pw.label || 'Unnamed') as string;\n\n const pwId = pw.id as number;\n const mapFeature = _features.find(function (f: FeatureEntry) {\n return f.featureType !== 'structure' && f.props.id === pwId;\n });\n pwItems.push({\n display, color, typeLabel, mapFeature,\n selectId: typeKey + '-' + pwId,\n hint: mapFeature ? mapFeature.latlng : undefined,\n });\n\n // Extract polyline endpoints for neighbor location hints\n let startHint: { lat: number; lng: number } | undefined;\n let endHint: { lat: number; lng: number } | undefined;\n if (mapFeature && mapFeature.layer) {\n try {\n const latlngs = (mapFeature.layer as L.Polyline).getLatLngs() as L.LatLng[];\n if (latlngs && latlngs.length >= 2) {\n startHint = { lat: latlngs[0].lat, lng: latlngs[0].lng };\n endHint = { lat: latlngs[latlngs.length - 1].lat, lng: latlngs[latlngs.length - 1].lng };\n }\n } catch (_e) { /* not a polyline */ }\n }\n\n // Extract the \"other\" structure as a neighbor\n const startS = pw.start_structure as { id?: number; display?: string } | null;\n const endS = pw.end_structure as { id?: number; display?: string } | null;\n if (startS && startS.id && startS.id !== structId && !neighborMap[startS.id]) {\n neighborMap[startS.id] = { id: startS.id, display: startS.display || String(startS.id), hint: startHint };\n }\n if (endS && endS.id && endS.id !== structId && !neighborMap[endS.id]) {\n neighborMap[endS.id] = { id: endS.id, display: endS.display || String(endS.id), hint: endHint };\n }\n });\n\n // --- Connected Structures (neighbors) \u2014 listed first ---\n const neighbors = Object.values(neighborMap);\n if (neighbors.length > 0) {\n const structSection = _createSection(\n 'Connected Structures (' + neighbors.length + ')',\n container,\n );\n neighbors.forEach(function (s) {\n const mf = _features.find(function (f: FeatureEntry) {\n return f.featureType === 'structure' && f.props.id === s.id;\n });\n _renderConnectedItem(structSection, s.display, '#2e7d32', 'Structure',\n mf, 'structure-' + s.id, s.hint,\n );\n });\n }\n\n // --- Connected Pathways ---\n const pwSection = _createSection(\n 'Connected Pathways (' + pwItems.length + ')',\n container,\n );\n pwItems.forEach(function (item) {\n _renderConnectedItem(pwSection, item.display, item.color, item.typeLabel,\n item.mapFeature, item.selectId, item.hint,\n );\n });\n\n // Dim unrelated features\n const connectedIds = new Set();\n neighbors.forEach(function (s) { connectedIds.add('structure-' + s.id); });\n pwItems.forEach(function (item) { connectedIds.add(item.selectId); });\n _dimFeatures(connectedIds);\n });\n}\n\n/** Render connected structures from the pathway's detail data. */\nfunction _renderConnectedStructures(data: Record, entry: FeatureEntry, container: HTMLElement): void {\n const structs: Array<{ id: number; display: string; url?: string }> = [];\n\n const startStruct = data.start_structure as { id?: number; display?: string; url?: string } | null;\n const endStruct = data.end_structure as { id?: number; display?: string; url?: string } | null;\n if (startStruct && startStruct.id) structs.push({\n id: startStruct.id,\n display: startStruct.display || String(startStruct.id),\n url: startStruct.url,\n });\n if (endStruct && endStruct.id && (!startStruct || endStruct.id !== startStruct.id)) structs.push({\n id: endStruct.id,\n display: endStruct.display || String(endStruct.id),\n url: endStruct.url,\n });\n\n if (structs.length === 0) return;\n\n const sectionBody = _createSection(\n 'Connected Structures (' + structs.length + ')',\n container,\n );\n\n // Extract hint coordinates from the current pathway's endpoints\n let startHint: { lat: number; lng: number } | undefined;\n let endHint: { lat: number; lng: number } | undefined;\n if (entry.layer) {\n try {\n const latlngs = (entry.layer as L.Polyline).getLatLngs() as L.LatLng[];\n if (latlngs && latlngs.length >= 2) {\n startHint = { lat: latlngs[0].lat, lng: latlngs[0].lng };\n endHint = { lat: latlngs[latlngs.length - 1].lat, lng: latlngs[latlngs.length - 1].lng };\n }\n } catch (_e) { /* not a polyline */ }\n }\n\n structs.forEach(function (s, idx) {\n const mf = _features.find(function (f: FeatureEntry) {\n return f.featureType === 'structure' && f.props.id === s.id;\n });\n const hint = idx === 0 ? startHint : endHint;\n\n _renderConnectedItem(sectionBody, s.display, '#2e7d32', 'Structure',\n mf, 'structure-' + s.id, hint,\n );\n });\n\n // Dim unrelated features\n const connectedIds = new Set();\n structs.forEach(function (s) { connectedIds.add('structure-' + s.id); });\n _dimFeatures(connectedIds);\n}\n\n// ---------------------------------------------------------------------------\n// Detail fetch (async/await with fetch)\n// ---------------------------------------------------------------------------\n\nasync function _fetchDetail(entry: FeatureEntry, container: HTMLElement): Promise {\n const cacheKey = _featureId(entry);\n\n // Check for HTML fragment URL from external layer config\n const extCfg = getLayerConfig(entry.featureType);\n const htmlUrl = _resolveDetailUrl(extCfg, entry);\n\n if (htmlUrl) {\n // HTML fragment mode \u2014 check cache (stored as string under a prefixed key)\n const htmlCacheKey = 'html:' + cacheKey;\n if (_detailCache[htmlCacheKey]) {\n _setTrustedHtml(container, _detailCache[htmlCacheKey] as unknown as string);\n return;\n }\n await _fetchHtmlDetail(htmlUrl, htmlCacheKey, container);\n return;\n }\n\n // JSON mode \u2014 existing behavior\n if (_detailCache[cacheKey]) {\n _renderEnrichedDetail(_detailCache[cacheKey], entry, container);\n return;\n }\n\n const url = _apiUrlForFeature(entry);\n\n // No detail URL available \u2014 render GeoJSON properties directly\n if (!url) {\n const table = document.createElement('table');\n table.className = 'pw-detail-table';\n const props = entry.props as Record;\n for (const key in props) {\n if (!props.hasOwnProperty(key)) continue;\n if (key === 'id' || key === 'url') continue;\n _addFieldRow(table, _titleCase(key.replace(/_/g, ' ')), props[key]);\n }\n container.appendChild(table);\n return;\n }\n\n const loadingDiv = document.createElement('div');\n loadingDiv.className = 'pw-detail-loading';\n loadingDiv.textContent = 'Loading details...';\n container.appendChild(loadingDiv);\n const headers: Record = { 'Accept': 'application/json' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n\n try {\n const response = await fetch(url, { headers });\n container.textContent = '';\n if (response.ok) {\n const data = await response.json() as Record;\n _detailCache[cacheKey] = data;\n _renderEnrichedDetail(data, entry, container);\n } else {\n const errDiv = document.createElement('div');\n errDiv.className = 'pw-detail-loading';\n errDiv.textContent = 'Could not load details (HTTP ' + response.status + ')';\n container.appendChild(errDiv);\n }\n } catch (_e) {\n container.textContent = '';\n const errDiv = document.createElement('div');\n errDiv.className = 'pw-detail-loading';\n errDiv.textContent = 'Network error';\n container.appendChild(errDiv);\n }\n}\n\n/** Resolve HTML detail URL from external layer config, if available. */\nfunction _resolveDetailUrl(\n extCfg: import('./types/external').ExternalLayerConfig | undefined,\n entry: FeatureEntry,\n): string {\n if (!extCfg?.detail?.detailUrl) return '';\n return extCfg.detail.detailUrl.replace('{id}', String(entry.props.id));\n}\n\n/**\n * Inject trusted HTML into a container element.\n *\n * SECURITY: This HTML is fetched from same-origin endpoints served by\n * registered NetBox plugins \u2014 the same trust model as Django's\n * PluginTemplateExtension. Only installed plugin code can register\n * detail_url endpoints via the map layer registry.\n */\nfunction _setTrustedHtml(container: HTMLElement, html: string): void {\n container.innerHTML = html; // eslint-disable-line no-unsanitized/property\n}\n\n/** Fetch an HTML fragment and inject it into the container. */\nasync function _fetchHtmlDetail(\n url: string,\n cacheKey: string,\n container: HTMLElement,\n): Promise {\n const loadingDiv = document.createElement('div');\n loadingDiv.className = 'pw-detail-loading';\n loadingDiv.textContent = 'Loading details...';\n container.appendChild(loadingDiv);\n\n const headers: Record = { 'Accept': 'text/html' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n\n try {\n const response = await fetch(url, { headers });\n container.textContent = '';\n if (response.ok) {\n const html = await response.text();\n _detailCache[cacheKey] = html as unknown as Record;\n _setTrustedHtml(container, html);\n } else {\n const errDiv = document.createElement('div');\n errDiv.className = 'pw-detail-loading';\n errDiv.textContent = 'Could not load details (HTTP ' + response.status + ')';\n container.appendChild(errDiv);\n }\n } catch (_e) {\n container.textContent = '';\n const errDiv = document.createElement('div');\n errDiv.className = 'pw-detail-loading';\n errDiv.textContent = 'Network error';\n container.appendChild(errDiv);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Detail panel rendering\n// ---------------------------------------------------------------------------\n\nfunction _renderDetail(entry: FeatureEntry): void {\n const body = document.getElementById('pw-detail-body');\n if (!body) return;\n body.textContent = '';\n const p = entry.props;\n\n // Title with inline edit\n const titleRow = document.createElement('div');\n titleRow.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:8px;';\n\n const title = document.createElement('div');\n title.className = 'pw-detail-title';\n title.style.marginBottom = '0';\n title.textContent = p.name || 'Unnamed';\n titleRow.appendChild(title);\n\n const editBtn = document.createElement('button');\n editBtn.className = 'pw-edit-btn';\n editBtn.title = 'Edit name';\n const pencilIcon = document.createElement('i');\n pencilIcon.className = 'mdi mdi-pencil';\n editBtn.appendChild(pencilIcon);\n titleRow.appendChild(editBtn);\n\n body.appendChild(titleRow);\n\n // Inline edit form (hidden)\n const editForm = document.createElement('div');\n editForm.className = 'pw-inline-edit';\n const editInput = document.createElement('input');\n editInput.type = 'text';\n editInput.className = 'form-control form-control-sm';\n editInput.value = p.name || '';\n editInput.style.flex = '1';\n editForm.appendChild(editInput);\n\n const saveBtn = document.createElement('button');\n saveBtn.className = 'btn btn-sm btn-primary';\n saveBtn.textContent = 'Save';\n editForm.appendChild(saveBtn);\n\n const cancelEditBtn = document.createElement('button');\n cancelEditBtn.className = 'btn btn-sm btn-outline-secondary';\n cancelEditBtn.textContent = '\\u00d7';\n editForm.appendChild(cancelEditBtn);\n\n body.appendChild(editForm);\n\n editBtn.addEventListener('click', function () {\n editForm.classList.add('active');\n titleRow.style.display = 'none';\n editInput.focus();\n editInput.select();\n });\n\n cancelEditBtn.addEventListener('click', function () {\n editForm.classList.remove('active');\n titleRow.style.display = '';\n });\n\n saveBtn.addEventListener('click', function () {\n const newName = editInput.value.trim();\n if (!newName || newName === p.name) {\n cancelEditBtn.click();\n return;\n }\n const url = _apiUrlForFeature(entry);\n const patchHeaders: Record = {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) patchHeaders['X-CSRFToken'] = csrfToken;\n\n fetch(url, {\n method: 'PATCH',\n headers: patchHeaders,\n body: JSON.stringify({ name: newName }),\n }).then(function (response) {\n if (response.ok) {\n p.name = newName;\n title.textContent = newName;\n delete _detailCache[_featureId(entry)];\n _applyFilters();\n }\n editForm.classList.remove('active');\n titleRow.style.display = '';\n });\n });\n\n editInput.addEventListener('keydown', function (e: KeyboardEvent) {\n if (e.key === 'Enter') saveBtn.click();\n if (e.key === 'Escape') cancelEditBtn.click();\n });\n\n // Metric badge row\n const color = _colorForFeature(entry);\n const typeKey = _typeKeyForFeature(entry);\n const badgeRow = document.createElement('div');\n badgeRow.className = 'pw-metric-row';\n\n const typeBadge = document.createElement('span');\n typeBadge.className = 'pw-metric-badge';\n typeBadge.style.background = color;\n typeBadge.style.color = '#fff';\n typeBadge.textContent = _titleCase(typeKey);\n badgeRow.appendChild(typeBadge);\n\n if (entry.featureType === 'structure' && p.site_name) {\n const siteBadge = document.createElement('span');\n siteBadge.className = 'pw-metric-badge pw-metric-muted';\n siteBadge.textContent = p.site_name as string;\n badgeRow.appendChild(siteBadge);\n }\n\n body.appendChild(badgeRow);\n\n // View Details button\n const detailUrl = _detailPageUrl(entry);\n if (detailUrl) {\n const link = document.createElement('a');\n link.href = detailUrl;\n link.className = 'btn btn-sm btn-primary w-100 mb-3';\n const icon = document.createElement('i');\n icon.className = 'mdi mdi-open-in-new';\n link.appendChild(icon);\n link.appendChild(document.createTextNode(' View Details '));\n body.appendChild(link);\n }\n\n // Plan Route button (structures only)\n if (entry.featureType === 'structure') {\n const planLink = document.createElement('a');\n planLink.href = '/plugins/pathways/route-planner/?start=' + p.id;\n planLink.className = 'btn btn-sm btn-outline-primary w-100 mb-3';\n const planIcon = document.createElement('i');\n planIcon.className = 'mdi mdi-map-search-outline';\n planLink.appendChild(planIcon);\n planLink.appendChild(document.createTextNode(' Plan Route From Here'));\n body.appendChild(planLink);\n }\n\n // Fetch enriched detail from REST API\n const detailContainer = document.createElement('div');\n body.appendChild(detailContainer);\n _fetchDetail(entry, detailContainer);\n}\n\n// ---------------------------------------------------------------------------\n// Server-side search fallback\n// ---------------------------------------------------------------------------\n\nfunction _showSearchingIndicator(): void {\n const listEl = document.getElementById('pw-feature-list');\n if (!listEl) return;\n const indicator = document.createElement('div');\n indicator.className = 'pw-server-search-status';\n indicator.id = 'pw-server-searching';\n const icon = document.createElement('i');\n icon.className = 'mdi mdi-magnify mdi-spin';\n indicator.appendChild(icon);\n indicator.appendChild(document.createTextNode(' Searching all features\\u2026'));\n listEl.appendChild(indicator);\n}\n\nfunction _clearServerResults(): void {\n const el = document.getElementById('pw-server-searching');\n if (el) el.remove();\n const hdr = document.getElementById('pw-server-results-header');\n if (hdr) hdr.remove();\n const items = document.querySelectorAll('.pw-server-result');\n for (let i = 0; i < items.length; i++) items[i].remove();\n}\n\nfunction setServerResults(results: ServerSearchResult[]): void {\n // Remove the \"searching\" indicator\n const searching = document.getElementById('pw-server-searching');\n if (searching) searching.remove();\n\n const listEl = document.getElementById('pw-feature-list');\n if (!listEl) return;\n\n // Remove any previous server results\n const oldHdr = document.getElementById('pw-server-results-header');\n if (oldHdr) oldHdr.remove();\n const oldItems = document.querySelectorAll('.pw-server-result');\n for (let i = 0; i < oldItems.length; i++) oldItems[i].remove();\n\n if (results.length === 0) {\n const noResults = document.createElement('div');\n noResults.className = 'pw-server-search-status pw-server-result';\n noResults.textContent = 'No matching features found.';\n listEl.appendChild(noResults);\n return;\n }\n\n const header = document.createElement('div');\n header.id = 'pw-server-results-header';\n header.className = 'pw-server-search-status';\n const hdrIcon = document.createElement('i');\n hdrIcon.className = 'mdi mdi-earth';\n header.appendChild(hdrIcon);\n header.appendChild(document.createTextNode(\n ' ' + results.length + ' result' + (results.length !== 1 ? 's' : '') + ' outside current view'\n ));\n listEl.appendChild(header);\n\n results.forEach(function (result: ServerSearchResult) {\n const item = document.createElement('div');\n item.className = 'pw-list-item pw-server-result';\n\n const dot = document.createElement('span');\n dot.className = 'pw-list-dot';\n const color = result.featureType === 'structure'\n ? (STRUCTURE_COLORS[result.typeKey] || '#616161')\n : (PATHWAY_COLORS[result.typeKey] || '#616161');\n dot.style.background = color;\n item.appendChild(dot);\n\n const label = document.createElement('span');\n label.className = 'pw-list-label';\n label.textContent = result.name || 'Unnamed';\n label.title = result.name || 'Unnamed';\n item.appendChild(label);\n\n const typeBadge = document.createElement('span');\n typeBadge.className = 'pw-list-type';\n typeBadge.textContent = _titleCase(result.typeKey);\n item.appendChild(typeBadge);\n\n const goIcon = document.createElement('i');\n goIcon.className = 'mdi mdi-crosshairs-gps';\n goIcon.style.cssText = 'color:var(--tblr-muted-color,#667382);flex-shrink:0;';\n goIcon.title = 'Go to location';\n item.appendChild(goIcon);\n\n item.addEventListener('click', function () {\n if (_map) {\n if (result.url) {\n _pendingSelect = { url: result.url, featureType: result.featureType };\n }\n _map.flyTo(result.latlng, 18, { duration: 0.8 });\n }\n });\n\n listEl.appendChild(item);\n });\n}\n\nfunction onServerSearch(cb: (query: string) => void): void {\n _serverSearchCallback = cb;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nfunction init(map: L.Map, kiosk?: boolean): void {\n _isKiosk = !!kiosk;\n _map = map;\n\n const toggleBtn = document.getElementById('pw-sidebar-toggle');\n if (toggleBtn) {\n toggleBtn.addEventListener('click', function () {\n _setListBodyVisible(_isCollapsed());\n });\n }\n\n const backBtn = document.getElementById('pw-detail-back');\n if (backBtn) {\n backBtn.addEventListener('click', function () {\n showList();\n });\n }\n\n document.addEventListener('keydown', function (e: KeyboardEvent) {\n if (e.key === 'Escape') {\n const detailPanel = document.getElementById('pw-panel-detail');\n if (detailPanel && detailPanel.style.display !== 'none') {\n showList();\n }\n _unhighlightMapFeature();\n _restoreDim();\n _selected = null;\n _highlightListItem(null);\n if (_isKiosk) _kioskSidebarClose();\n if (_onSelectionChange) _onSelectionChange();\n }\n });\n\n const searchInput = document.getElementById('pw-search');\n if (searchInput) {\n searchInput.addEventListener('input', _debounce(function () {\n _applyFilters();\n }, 150));\n }\n\n map.on('click', function (e: L.LeafletMouseEvent) {\n if (!(e.originalEvent as any)._sidebarClick) {\n _unhighlightMapFeature();\n _restoreDim();\n _selected = null;\n _highlightListItem(null);\n const detailPanel = document.getElementById('pw-panel-detail');\n if (detailPanel && detailPanel.style.display !== 'none') {\n showList();\n }\n if (_onSelectionChange) _onSelectionChange();\n }\n });\n\n if (_isKiosk) {\n const kioskCloseBtn = document.getElementById('pw-kiosk-close');\n if (kioskCloseBtn) {\n kioskCloseBtn.addEventListener('click', function () {\n _kioskSidebarClose();\n const detailPanel = document.getElementById('pw-panel-detail');\n if (detailPanel) detailPanel.style.display = 'none';\n });\n }\n document.addEventListener('keydown', function (e: KeyboardEvent) {\n if (e.key === 'Escape') {\n _kioskSidebarClose();\n const detailPanel = document.getElementById('pw-panel-detail');\n if (detailPanel) detailPanel.style.display = 'none';\n }\n });\n }\n}\n\nfunction _setListBodyVisible(visible: boolean): void {\n const body = document.getElementById('pw-panel-list-body');\n const chevron = document.getElementById('pw-sidebar-chevron');\n if (body) body.classList.toggle('collapsed', !visible);\n if (chevron) chevron.classList.toggle('collapsed', !visible);\n}\n\nfunction _kioskSidebarOpen(): void {\n const sidebar = document.getElementById('pw-sidebar');\n if (!sidebar) return;\n sidebar.classList.add('pw-sidebar-open');\n}\n\nfunction _kioskSidebarClose(): void {\n const sidebar = document.getElementById('pw-sidebar');\n if (!sidebar) return;\n sidebar.classList.remove('pw-sidebar-open');\n}\n\nfunction _isCollapsed(): boolean {\n const body = document.getElementById('pw-panel-list-body');\n return body ? body.classList.contains('collapsed') : false;\n}\n\nfunction show(): void {\n if (_isKiosk) {\n _kioskSidebarOpen();\n return;\n }\n const sidebar = document.getElementById('pw-sidebar');\n if (sidebar) sidebar.classList.remove('pw-sidebar-hidden');\n _setListBodyVisible(true);\n}\n\nfunction hide(): void {\n if (_isKiosk) { _kioskSidebarClose(); return; }\n const sidebar = document.getElementById('pw-sidebar');\n if (sidebar) sidebar.classList.add('pw-sidebar-hidden');\n}\n\nfunction setFeatures(features: FeatureEntry[]): void {\n _features = features;\n // Cap cache size instead of clearing on every pan/zoom\n const cacheKeys = Object.keys(_detailCache);\n if (cacheKeys.length > 200) {\n cacheKeys.slice(0, 100).forEach(function (k: string) { delete _detailCache[k]; });\n }\n\n // Preserve selection across data reloads (e.g. panTo triggers moveend)\n if (_selected) {\n const selId = _featureId(_selected);\n let found: FeatureEntry | null = null;\n for (let i = 0; i < features.length; i++) {\n if (_featureId(features[i]) === selId) {\n found = features[i];\n break;\n }\n }\n if (found) {\n _selected = found;\n _buildTypeFilters();\n _applyFilters();\n _applyDim();\n return;\n }\n _restoreDim();\n _selected = null;\n }\n\n _buildTypeFilters();\n _applyFilters();\n if (!_isKiosk) showList();\n\n // Resolve pending selection from server search result click\n if (_pendingSelect && features.length > 0) {\n const pending = _pendingSelect;\n for (let i = 0; i < features.length; i++) {\n if (features[i].props.url === pending.url) {\n _pendingSelect = null;\n // Clear the search input so client filter doesn't hide the match\n const searchInput = document.getElementById('pw-search') as HTMLInputElement | null;\n if (searchInput) searchInput.value = '';\n _lastServerQuery = '';\n _clearServerResults();\n _applyFilters();\n selectFeature(features[i]);\n return;\n }\n }\n }\n\n // Resolve pending fly-to-select (from connected item click)\n if (_pendingFlySelectId && features.length > 0) {\n const id = _pendingFlySelectId;\n _pendingFlySelectId = '';\n for (let i = 0; i < features.length; i++) {\n if (_featureId(features[i]) === id) {\n selectFeature(features[i]);\n return;\n }\n }\n }\n}\n\nfunction showList(): void {\n if (_isKiosk) _kioskSidebarOpen();\n _setListBodyVisible(true);\n const detailPanel = document.getElementById('pw-panel-detail');\n if (detailPanel) detailPanel.style.display = 'none';\n}\n\nfunction showDetail(entry: FeatureEntry): void {\n if (_isKiosk) _kioskSidebarOpen();\n _setListBodyVisible(false);\n const detailPanel = document.getElementById('pw-panel-detail');\n if (detailPanel) detailPanel.style.display = '';\n\n // Set panel heading (e.g. \"Structure Details\")\n const heading = document.getElementById('pw-detail-heading');\n if (heading) {\n heading.textContent = _typeLabel(entry.featureType) + ' Details';\n }\n\n _renderDetail(entry);\n}\n\nfunction selectFeature(entry: FeatureEntry): void {\n _restoreDim();\n _selected = entry;\n _highlightListItem(entry);\n _highlightMapFeature(entry);\n showDetail(entry);\n if (_map && entry.latlng) {\n const zoom = _map.getZoom();\n // Zoom past disableClusteringAtZoom (18) for structures so the\n // individual marker is visible, not hidden inside a cluster.\n const minZoom = entry.featureType === 'structure' ? 18 : 16;\n if (zoom < minZoom) {\n _map.flyTo(entry.latlng, minZoom, { duration: 0.5 });\n } else {\n _map.panTo(entry.latlng);\n }\n }\n if (_onSelectionChange) _onSelectionChange();\n}\n\nfunction onFeatureCreated(entry: FeatureEntry): void {\n if (!_selected) return;\n if (_featureId(entry) === _featureId(_selected)) {\n _selected = entry;\n _reapplyHighlight(entry);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Dependency injection\n// ---------------------------------------------------------------------------\n\nexport interface SidebarDeps {\n titleCase: (s: string) => string;\n esc: (s: string) => string;\n debounce: (fn: () => void, delay: number) => () => void;\n getCookie: (name: string) => string | null;\n structureColors: Record;\n structureShapes: Record;\n pathwayColors: Record;\n apiBase: string;\n}\n\nfunction setDeps(deps: SidebarDeps): void {\n _titleCase = deps.titleCase;\n _esc = deps.esc;\n _debounce = deps.debounce;\n _getCookie = deps.getCookie;\n STRUCTURE_COLORS = deps.structureColors;\n STRUCTURE_SHAPES = deps.structureShapes;\n PATHWAY_COLORS = deps.pathwayColors;\n API_BASE = deps.apiBase;\n}\n\n// ---------------------------------------------------------------------------\n// Export\n// ---------------------------------------------------------------------------\n\n/** Return the ID string of the selected feature, or empty string. */\nfunction getSelectedId(): string {\n return _selected ? _featureId(_selected) : '';\n}\n\n/** Select a feature by its ID string (e.g. \"structure-123\"). */\nfunction selectById(id: string): boolean {\n if (!id) return false;\n for (let i = 0; i < _features.length; i++) {\n if (_featureId(_features[i]) === id) {\n selectFeature(_features[i]);\n return true;\n }\n }\n return false;\n}\n\nfunction onSelectionChange(cb: () => void): void {\n _onSelectionChange = cb;\n}\n\nexport const Sidebar = {\n init,\n show,\n hide,\n setFeatures,\n showList,\n showDetail,\n selectFeature,\n selectById,\n getSelectedId,\n onFeatureCreated,\n onSelectionChange,\n setDeps,\n onServerSearch,\n setServerResults,\n};\n", "/**\n * Hover popover module for the full-page infrastructure map.\n *\n * Shows a lightweight tooltip with feature name and type on hover,\n * positioned near the cursor within the map container.\n */\n\nimport type { GeoJSONProperties } from './types/features';\n\n// ---------------------------------------------------------------------------\n// Module-level helpers injected via setDeps\n// ---------------------------------------------------------------------------\n\nlet _titleCase: (s: string) => string;\n\n// ---------------------------------------------------------------------------\n// Internal state\n// ---------------------------------------------------------------------------\n\nlet _el: HTMLDivElement | null = null;\nlet _map: L.Map | null = null;\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction _position(latlng: L.LatLng): void {\n if (!_map || !_el) return;\n const pt = _map.latLngToContainerPoint(latlng);\n const cw = _map.getContainer().clientWidth;\n let x = pt.x + 14;\n let y = pt.y - 10;\n if (x + 200 > cw) x = pt.x - 200;\n if (y < 0) y = pt.y + 20;\n _el.style.left = x + 'px';\n _el.style.top = y + 'px';\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nfunction init(map: L.Map): void {\n _map = map;\n _el = document.createElement('div');\n _el.className = 'pw-popover';\n _el.style.display = 'none';\n map.getContainer().appendChild(_el);\n}\n\nfunction show(latlng: L.LatLng, props: GeoJSONProperties, popoverFields?: string[]): void {\n if (!_el) return;\n _el.textContent = '';\n\n // Name line\n const name = document.createElement('span');\n name.className = 'pw-popover-name';\n if (popoverFields && popoverFields.length > 0) {\n name.textContent = String(props[popoverFields[0]] ?? props.name ?? `#${props.id}`);\n } else {\n name.textContent = props.name || 'Unnamed';\n }\n _el.appendChild(name);\n\n // Type line\n let typeText = '';\n if (popoverFields && popoverFields.length > 1) {\n typeText = popoverFields.slice(1)\n .map(f => String(props[f] ?? ''))\n .filter(Boolean)\n .join(' / ');\n } else {\n const t = props.structure_type || props.pathway_type || '';\n typeText = t ? _titleCase(t) : '';\n }\n if (typeText) {\n const type = document.createElement('span');\n type.className = 'pw-popover-type';\n type.textContent = typeText;\n _el.appendChild(type);\n }\n\n _position(latlng);\n _el.style.display = '';\n}\n\nfunction hide(): void {\n if (_el) _el.style.display = 'none';\n}\n\n// ---------------------------------------------------------------------------\n// Dependency injection\n// ---------------------------------------------------------------------------\n\nexport interface PopoverDeps {\n titleCase: (s: string) => string;\n}\n\nfunction setDeps(deps: PopoverDeps): void {\n _titleCase = deps.titleCase;\n}\n\n// ---------------------------------------------------------------------------\n// Export\n// ---------------------------------------------------------------------------\n\nexport const Popover = {\n init,\n show,\n hide,\n setDeps,\n};\n", "/**\n * Pure utility functions and constants extracted from pathways-map.ts.\n *\n * These are used by pathways-map, sidebar, popover, and tests.\n */\n\n// ---------------------------------------------------------------------------\n// Color & Icon Maps\n// ---------------------------------------------------------------------------\n\nexport const STRUCTURE_COLORS: Record = {\n 'pole': '#2e7d32', 'manhole': '#1565c0', 'handhole': '#00838f',\n 'cabinet': '#e65100', 'vault': '#6a1b9a', 'pedestal': '#f9a825',\n 'building_entrance': '#c62828', 'splice_closure': '#795548',\n 'tower': '#b71c1c', 'roof': '#616161', 'equipment_room': '#00796b',\n 'telecom_closet': '#283593', 'riser_room': '#ad1457',\n};\n\nexport const STRUCTURE_SHAPES: Record = {\n 'pole': '',\n 'manhole': '',\n 'handhole': '',\n 'cabinet': '',\n 'vault': '',\n 'pedestal': '',\n 'building_entrance': '',\n 'splice_closure': '',\n 'tower': '',\n 'roof': '',\n 'equipment_room': '',\n 'telecom_closet': '',\n 'riser_room': '',\n};\n\nexport const PATHWAY_COLORS: Record = {\n 'conduit': '#f57c00', 'conduit_bank': '#ad1457', 'aerial': '#1565c0',\n 'direct_buried': '#616161', 'innerduct': '#e65100', 'microduct': '#6a1b9a',\n 'tray': '#2e7d32', 'raceway': '#00838f', 'submarine': '#1a237e',\n};\n\nexport const PATHWAY_DASH: Record = {\n 'conduit': '5,5', 'conduit_bank': '', 'aerial': '10,5',\n 'direct_buried': '2,4', 'innerduct': '8,3', 'microduct': '1,3',\n 'tray': '', 'raceway': '12,4', 'submarine': '6,2,2,2',\n};\n\nexport interface PathwayStyleDef {\n color: string;\n weight: number;\n opacity: number;\n dashArray: string;\n}\n\n/** Return the polyline style for a given pathway_type key. */\nexport function pathwayStyle(pathwayType: string): PathwayStyleDef {\n const color = PATHWAY_COLORS[pathwayType] || '#888';\n const dash = PATHWAY_DASH[pathwayType] || '';\n const isBanks = pathwayType === 'conduit_bank';\n return {\n color,\n weight: isBanks ? 5 : 3,\n opacity: isBanks ? 0.8 : 0.7,\n dashArray: dash,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Marker helpers\n// ---------------------------------------------------------------------------\n\nexport function structureIcon(type: string, size = 20): L.DivIcon {\n const color = STRUCTURE_COLORS[type] || '#616161';\n const shape = STRUCTURE_SHAPES[type] || '';\n const isOutline = shape.includes('fill=\"none\"');\n const half = size / 2;\n return L.divIcon({\n className: 'pw-marker',\n html: '' + shape + '',\n iconSize: [size, size] as [number, number],\n iconAnchor: [half, half] as [number, number],\n popupAnchor: [0, -(half + 2)] as [number, number],\n });\n}\n\nexport function clusterIcon(count: number): L.DivIcon {\n let cls: string;\n let size: number;\n if (count < 10) {\n cls = 'pw-cluster-small'; size = 34;\n } else if (count < 100) {\n cls = 'pw-cluster-medium'; size = 40;\n } else {\n cls = 'pw-cluster-large'; size = 46;\n }\n return L.divIcon({\n className: 'pw-server-cluster',\n html: '
' +\n count + '
',\n iconSize: [size, size] as [number, number],\n iconAnchor: [size / 2, size / 2] as [number, number],\n });\n}\n\n// ---------------------------------------------------------------------------\n// Utility helpers\n// ---------------------------------------------------------------------------\n\nexport function esc(text: string): string {\n const el = document.createElement('span');\n el.textContent = text;\n return el.innerHTML;\n}\n\nexport function titleCase(str: string): string {\n return (str || '').replace(/_/g, ' ').replace(/\\b\\w/g, function (c: string) { return c.toUpperCase(); });\n}\n\nexport function getCookie(name: string): string | null {\n const value = '; ' + document.cookie;\n const parts = value.split('; ' + name + '=');\n if (parts.length === 2) return parts.pop()!.split(';').shift() || null;\n return null;\n}\n\nexport function bboxParam(map: L.Map): string {\n const b = map.getBounds();\n return b.getWest() + ',' + b.getSouth() + ',' + b.getEast() + ',' + b.getNorth();\n}\n\nexport function debounce(fn: () => void, delay: number): () => void {\n let timer: ReturnType;\n return function () {\n clearTimeout(timer);\n timer = setTimeout(fn, delay);\n };\n}\n\nexport function haversine(lat1: number, lon1: number, lat2: number, lon2: number): number {\n const R = 6371000;\n const p1 = lat1 * Math.PI / 180;\n const p2 = lat2 * Math.PI / 180;\n const dp = (lat2 - lat1) * Math.PI / 180;\n const dl = (lon2 - lon1) * Math.PI / 180;\n const a = Math.sin(dp / 2) * Math.sin(dp / 2) +\n Math.cos(p1) * Math.cos(p2) * Math.sin(dl / 2) * Math.sin(dl / 2);\n return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n}\n", "/**\n * Map core: map creation, base layers, overlays, and controls.\n *\n * Shared between the full-page infrastructure map (pathways-map.ts)\n * and the route planner map (route-planner-map.ts).\n */\n\nimport {\n STRUCTURE_COLORS,\n STRUCTURE_SHAPES,\n PATHWAY_COLORS,\n PATHWAY_DASH,\n titleCase as _titleCase,\n} from './map-utils';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst CFG: Partial = window.PATHWAYS_CONFIG || {};\nconst MAX_NATIVE_ZOOM: number = CFG.maxNativeZoom || 19;\n\n// ---------------------------------------------------------------------------\n// Base layers\n// ---------------------------------------------------------------------------\n\ninterface BaseLayerDef {\n name: string;\n url: string;\n attribution?: string;\n maxNativeZoom?: number;\n tileSize?: number;\n zoomOffset?: number;\n}\n\nconst DEFAULT_BASE_LAYERS: BaseLayerDef[] = [\n {\n name: 'Street',\n url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n attribution: '© OpenStreetMap contributors',\n maxNativeZoom: MAX_NATIVE_ZOOM,\n },\n {\n name: 'Satellite',\n url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',\n attribution: 'Esri World Imagery',\n maxNativeZoom: 19,\n },\n];\n\nexport function createBaseLayers(): Record {\n const configured = (CFG.baseLayers || []).filter(function (c: BaseLayerConfig) { return !!c.url; });\n const configs: BaseLayerDef[] = configured.length ? configured : DEFAULT_BASE_LAYERS;\n const layers: Record = {};\n configs.forEach(function (cfg: BaseLayerDef) {\n layers[cfg.name] = L.tileLayer(cfg.url, {\n attribution: cfg.attribution || '',\n maxNativeZoom: cfg.maxNativeZoom || MAX_NATIVE_ZOOM,\n maxZoom: 22,\n tileSize: cfg.tileSize || 256,\n zoomOffset: cfg.zoomOffset || 0,\n });\n });\n return layers;\n}\n\n// ---------------------------------------------------------------------------\n// User-configured overlays\n// ---------------------------------------------------------------------------\n\nexport function createUserOverlays(): Record {\n const userOverlays = CFG.overlays || [];\n const overlays: Record = {};\n userOverlays.forEach(function (cfg: OverlayConfig) {\n let layer: L.TileLayer | L.TileLayer.WMS;\n if (cfg.type === 'wms') {\n layer = L.tileLayer.wms(cfg.url, {\n layers: (cfg['layers'] as string) || '',\n format: (cfg['format'] as string) || 'image/png',\n transparent: cfg['transparent'] !== false,\n attribution: (cfg['attribution'] as string) || '',\n maxZoom: 22,\n });\n } else {\n layer = L.tileLayer(cfg.url, {\n attribution: (cfg['attribution'] as string) || '',\n maxZoom: (cfg['maxZoom'] as number) || 22,\n maxNativeZoom: (cfg['maxNativeZoom'] as number) || undefined,\n });\n }\n overlays[cfg.name] = layer;\n });\n return overlays;\n}\n\n// ---------------------------------------------------------------------------\n// Zoom hint overlay\n// ---------------------------------------------------------------------------\n\nexport function createZoomHint(map: L.Map): HTMLDivElement {\n const div = L.DomUtil.create('div', 'pathways-zoom-hint') as HTMLDivElement;\n div.style.cssText =\n 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);' +\n 'z-index:800;padding:12px 24px;border-radius:8px;font-size:14px;' +\n 'pointer-events:none;text-align:center;' +\n 'background:rgba(0,0,0,0.7);color:#fff;';\n div.textContent = 'Zoom in to see infrastructure data';\n map.getContainer().appendChild(div);\n return div;\n}\n\n// ---------------------------------------------------------------------------\n// Legend control\n// ---------------------------------------------------------------------------\n\n/**\n * Inject trusted static SVG markup into an element.\n *\n * Security: All SVG strings passed to this helper originate from compile-time\n * constants (STRUCTURE_SHAPES, PATHWAY_COLORS, PATHWAY_DASH) defined in this\n * file -- no user/network input is involved. This is the same trust model as\n * structureIcon() which also builds innerHTML from these constants.\n */\nfunction _setStaticSvg(el: HTMLElement, svg: string): void {\n el.innerHTML = svg; // eslint-disable-line no-unsanitized/property -- trusted compile-time SVG constants only\n}\n\nexport function createLegend(map: L.Map): void {\n const LegendControl = L.Control.extend({\n options: { position: 'bottomleft' },\n onAdd: function () {\n const container = L.DomUtil.create('div', 'pw-legend leaflet-bar');\n L.DomEvent.disableClickPropagation(container);\n L.DomEvent.disableScrollPropagation(container);\n\n // Header -- collapsed by default\n const header = L.DomUtil.create('div', 'pw-legend-header collapsed', container);\n const chevron = document.createElement('i');\n chevron.className = 'mdi mdi-chevron-down';\n header.appendChild(chevron);\n const titleSpan = document.createElement('span');\n titleSpan.textContent = 'Legend';\n header.appendChild(titleSpan);\n\n // Body -- collapsed by default\n const body = L.DomUtil.create('div', 'pw-legend-body collapsed', container);\n\n // Structures section\n const structSec = L.DomUtil.create('div', 'pw-legend-section', body);\n const structTitle = L.DomUtil.create('div', 'pw-legend-section-title', structSec);\n structTitle.textContent = 'Structures';\n\n const structTypes = ['pole', 'manhole', 'handhole', 'cabinet', 'vault',\n 'pedestal', 'building_entrance', 'splice_closure', 'tower',\n 'equipment_room', 'telecom_closet', 'riser_room'];\n for (let i = 0; i < structTypes.length; i++) {\n const stype = structTypes[i];\n const color = STRUCTURE_COLORS[stype] || '#616161';\n const shape = STRUCTURE_SHAPES[stype] || '';\n const isOutline = shape.includes('fill=\"none\"');\n const item = L.DomUtil.create('div', 'pw-legend-item', structSec);\n const swatch = L.DomUtil.create('span', 'pw-legend-swatch', item);\n _setStaticSvg(swatch,\n '' +\n shape + '');\n const label = L.DomUtil.create('span', 'pw-legend-label', item);\n label.textContent = _titleCase(stype);\n }\n\n // Pathways section\n const pathSec = L.DomUtil.create('div', 'pw-legend-section', body);\n const pathTitle = L.DomUtil.create('div', 'pw-legend-section-title', pathSec);\n pathTitle.textContent = 'Pathways';\n\n const pathTypes = ['conduit', 'conduit_bank', 'aerial', 'direct_buried',\n 'innerduct', 'microduct', 'tray', 'raceway', 'submarine'];\n for (let i = 0; i < pathTypes.length; i++) {\n const ptype = pathTypes[i];\n const color = PATHWAY_COLORS[ptype] || '#616161';\n const dash = PATHWAY_DASH[ptype] || '';\n const item = L.DomUtil.create('div', 'pw-legend-item', pathSec);\n const swatch = L.DomUtil.create('span', 'pw-legend-swatch', item);\n _setStaticSvg(swatch,\n '' +\n '');\n const label = L.DomUtil.create('span', 'pw-legend-label', item);\n label.textContent = _titleCase(ptype);\n }\n\n // Toggle collapse\n header.addEventListener('click', function () {\n const isCollapsed = body.classList.toggle('collapsed');\n header.classList.toggle('collapsed', isCollapsed);\n });\n\n return container;\n },\n });\n\n new LegendControl().addTo(map);\n}\n\n// ---------------------------------------------------------------------------\n// Stats control\n// ---------------------------------------------------------------------------\n\nexport function createStatsControl(map: L.Map): void {\n const StatsControl = L.Control.extend({\n options: { position: 'bottomleft' },\n onAdd: function (): HTMLElement {\n const container = L.DomUtil.create('div', 'pw-stats-overlay');\n const sc = L.DomUtil.create('span', '', container);\n sc.id = 'structure-count';\n sc.textContent = '0';\n container.appendChild(document.createTextNode(' structures \\u00b7 '));\n const pc = L.DomUtil.create('span', '', container);\n pc.id = 'pathway-count';\n pc.textContent = '0';\n container.appendChild(document.createTextNode(' pathways \\u00b7 '));\n const tl = L.DomUtil.create('span', '', container);\n tl.id = 'total-length';\n tl.textContent = '0';\n container.appendChild(document.createTextNode(' km'));\n return container;\n },\n });\n new StatsControl().addTo(map);\n}\n\n// ---------------------------------------------------------------------------\n// Locate control\n// ---------------------------------------------------------------------------\n\nexport function createLocateControl(map: L.Map): L.Control {\n const LocateControl = L.Control.extend({\n options: { position: 'topleft' },\n onAdd: function (): HTMLElement {\n const container = L.DomUtil.create('div', 'leaflet-control-zoom leaflet-bar');\n const link = L.DomUtil.create('a', '', container) as HTMLAnchorElement;\n link.href = '#';\n link.title = 'Go to my location';\n L.DomUtil.create('i', 'mdi mdi-crosshairs-gps', link);\n link.style.display = 'flex';\n link.style.alignItems = 'center';\n link.style.justifyContent = 'center';\n link.style.fontSize = '18px';\n\n L.DomEvent.disableClickPropagation(container);\n L.DomEvent.on(link, 'click', function (e: Event) {\n L.DomEvent.preventDefault(e);\n if (!navigator.geolocation) return;\n navigator.geolocation.getCurrentPosition(\n function (pos: GeolocationPosition) {\n map.flyTo([pos.coords.latitude, pos.coords.longitude], 17, { duration: 1 });\n },\n function () { /* silently ignore denial */ },\n { enableHighAccuracy: true, timeout: 10000 },\n );\n });\n return container;\n },\n });\n return new LocateControl();\n}\n\n// ---------------------------------------------------------------------------\n// Kiosk control\n// ---------------------------------------------------------------------------\n\nexport function createKioskControl(map: L.Map, isKiosk: boolean): L.Control {\n const KioskControl = L.Control.extend({\n options: { position: 'topleft' },\n onAdd: function (): HTMLElement {\n const container = L.DomUtil.create('div', 'leaflet-control-zoom leaflet-bar');\n const link = L.DomUtil.create('a', '', container) as HTMLAnchorElement;\n link.href = '#';\n link.title = isKiosk ? 'Exit kiosk mode' : 'Kiosk mode';\n L.DomUtil.create('i', 'mdi ' + (isKiosk ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'), link);\n link.style.display = 'flex';\n link.style.alignItems = 'center';\n link.style.justifyContent = 'center';\n link.style.fontSize = '18px';\n\n L.DomEvent.disableClickPropagation(container);\n L.DomEvent.on(link, 'click', function (e: Event) {\n L.DomEvent.preventDefault(e);\n const center = map.getCenter();\n const zoom = map.getZoom();\n const params = new URLSearchParams();\n params.set('lat', center.lat.toFixed(6));\n params.set('lon', center.lng.toFixed(6));\n params.set('zoom', String(zoom));\n if (!isKiosk) {\n params.set('kiosk', 'true');\n }\n window.location.search = params.toString();\n });\n return container;\n },\n });\n return new KioskControl();\n}\n\n// ---------------------------------------------------------------------------\n// Sidebar toggle control\n// ---------------------------------------------------------------------------\n\nexport function createSidebarToggleControl(map: L.Map, isKiosk: boolean, showCallback: () => void): L.Control {\n const SidebarToggle = L.Control.extend({\n options: { position: 'topright' },\n onAdd: function (): HTMLElement {\n const container = L.DomUtil.create('div', 'leaflet-control-zoom leaflet-bar pw-sidebar-toggle-ctrl');\n const link = L.DomUtil.create('a', '', container) as HTMLAnchorElement;\n link.href = '#';\n link.title = 'Show sidebar';\n L.DomUtil.create('i', 'mdi mdi-chevron-left', link);\n link.style.display = 'flex';\n link.style.alignItems = 'center';\n link.style.justifyContent = 'center';\n link.style.fontSize = '18px';\n\n L.DomEvent.disableClickPropagation(container);\n L.DomEvent.on(link, 'click', function (e: Event) {\n L.DomEvent.preventDefault(e);\n showCallback();\n });\n\n // Watch sidebar visibility to show/hide this button\n const observer = new MutationObserver(function () {\n const sidebar = document.getElementById('pw-sidebar');\n if (!sidebar) return;\n const sidebarVisible = isKiosk\n ? sidebar.classList.contains('pw-sidebar-open')\n : !sidebar.classList.contains('pw-sidebar-hidden');\n container.style.display = sidebarVisible ? 'none' : '';\n });\n const sidebar = document.getElementById('pw-sidebar');\n if (sidebar) {\n observer.observe(sidebar, { attributes: true, attributeFilter: ['class'] });\n // Initial state\n const sidebarVisible = isKiosk\n ? sidebar.classList.contains('pw-sidebar-open')\n : !sidebar.classList.contains('pw-sidebar-hidden');\n container.style.display = sidebarVisible ? 'none' : '';\n }\n\n return container;\n },\n });\n return new SidebarToggle();\n}\n\n// ---------------------------------------------------------------------------\n// Map factory\n// ---------------------------------------------------------------------------\n\nexport interface MapInitConfig {\n center?: [number, number];\n zoom?: number;\n bounds?: L.LatLngBoundsExpression;\n kiosk?: boolean;\n select?: string; // feature ID to auto-select, e.g. \"structure-123\"\n routeData?: unknown; // Pre-loaded route geometry for static route display\n}\n\nexport interface MapInstance {\n map: L.Map;\n baseLayers: Record;\n layerControl: L.Control.Layers;\n}\n\nexport function createMap(elementId: string, config: MapInitConfig): MapInstance {\n const baseLayers = createBaseLayers();\n const firstLayer = baseLayers[Object.keys(baseLayers)[0]];\n const map = L.map(elementId, {\n layers: [firstLayer],\n });\n\n // Fit to bounds if provided, otherwise use center/zoom\n if (config.bounds) {\n // Allow higher zoom when selecting a specific feature vs. viewing full data extent\n const boundsMaxZoom = config.select ? 20 : 17;\n map.fitBounds(config.bounds, { padding: [50, 50] as [number, number], maxZoom: boundsMaxZoom });\n } else {\n map.setView(config.center || [0, 0], config.zoom || 2);\n }\n\n // Overlay layers\n const overlayLayers: Record = {};\n\n // User-configured WMS/WMTS/tile overlays\n const userOverlays = createUserOverlays();\n for (const name in userOverlays) {\n overlayLayers[name] = userOverlays[name];\n }\n\n // Layer control\n const layerControl = L.control.layers(baseLayers, overlayLayers, {\n position: 'bottomright', collapsed: true,\n }).addTo(map);\n\n return { map, baseLayers, layerControl };\n}\n", "/**\n * GeoJSON data layer loading and viewport caching.\n *\n * Fetches structures and pathways from the GeoJSON API, manages an\n * overfetch cache for smooth panning, and renders features on the map.\n *\n * Shared between the full-page infrastructure map (pathways-map.ts)\n * and the route planner map (route-planner-map.ts).\n */\n\nimport type { FeatureEntry, FeatureType, GeoJSONProperties, PathwayStyle } from './types/features';\nimport {\n structureIcon as _structureIcon,\n clusterIcon as _clusterIcon,\n esc as _esc,\n getCookie as _getCookie,\n haversine as _haversine,\n} from './map-utils';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst CFG: Partial = window.PATHWAYS_CONFIG || {};\nconst API_BASE: string = CFG.apiBase || '/api/plugins/pathways/geo/';\n\nexport { API_BASE };\n\nexport const MIN_DATA_ZOOM = 11;\n\n/**\n * Zoom at or above which `/info` is skipped entirely. The viewport at this\n * zoom is small enough that hide/cluster thresholds are effectively\n * unreachable, so blocking the render on an `/info` round-trip just adds\n * latency. Overridable by server config (`PATHWAYS_CONFIG.skipInfoZoom`)\n * and by callers of `chooseLoadStrategy` for tests.\n */\nexport const SKIP_INFO_ZOOM: number = CFG.skipInfoZoom ?? 17;\n\n// ---------------------------------------------------------------------------\n// /info endpoint: per-layer counts + thresholds for the current viewport\n// ---------------------------------------------------------------------------\n\nexport interface LayerThresholds {\n hide: number;\n cluster?: number;\n}\n\nexport interface MapInfo {\n bbox: [number, number, number, number] | null;\n counts: {\n structures: number;\n conduit_banks: number;\n conduits: number;\n aerial_spans: number;\n direct_buried: number;\n circuits: number;\n external?: Record;\n };\n thresholds: {\n structures: LayerThresholds;\n conduit_banks: LayerThresholds;\n conduits: LayerThresholds;\n aerial_spans: LayerThresholds;\n direct_buried: LayerThresholds;\n circuits: LayerThresholds;\n external?: Record;\n };\n}\n\nexport type ClusterMode = 'off' | 'client' | 'server';\nexport type LayerDecision = 'render' | 'hide';\n\nexport interface RenderingDecision {\n clusterMode: ClusterMode;\n layers: Record;\n}\n\n/**\n * Pure mapping from /info counts + thresholds + currently-enabled layers to\n * a per-layer render decision plus a global cluster mode.\n *\n * Rule of thumb: structures drive cluster mode. When structures are clustered\n * (client or server), every non-structure layer is hidden because the\n * supporting topology no longer makes sense at that density.\n *\n * Layer keys: native layers use their counts/thresholds keys directly\n * (e.g. ``'conduit_banks'``); external layers use ``'external:'``.\n */\nexport function decideLayerRendering(info: MapInfo, enabled: Set): RenderingDecision {\n const structuresCount = info.counts.structures;\n const sThresh = info.thresholds.structures;\n let clusterMode: ClusterMode = 'off';\n if (structuresCount > sThresh.hide) {\n clusterMode = 'server';\n } else if (sThresh.cluster != null && structuresCount > sThresh.cluster) {\n clusterMode = 'client';\n }\n\n const layers: Record = {};\n const suppress = clusterMode !== 'off';\n\n if (enabled.has('structures')) {\n layers.structures = 'render';\n }\n\n const nativeKeys: (keyof MapInfo['counts'])[] = [\n 'conduit_banks', 'conduits', 'aerial_spans', 'direct_buried', 'circuits',\n ];\n for (const key of nativeKeys) {\n if (!enabled.has(key)) continue;\n if (suppress) {\n layers[key] = 'hide';\n continue;\n }\n const count = (info.counts[key] as number) ?? 0;\n const threshold = info.thresholds[key as keyof MapInfo['thresholds']] as LayerThresholds | undefined;\n layers[key] = threshold && count > threshold.hide ? 'hide' : 'render';\n }\n\n const extCounts = info.counts.external || {};\n const extThresholds = info.thresholds.external || {};\n for (const name of Object.keys(extCounts)) {\n const key = `external:${name}`;\n if (!enabled.has(key)) continue;\n if (suppress) {\n layers[key] = 'hide';\n continue;\n }\n const t = extThresholds[name];\n layers[key] = t && extCounts[name] > t.hide ? 'hide' : 'render';\n }\n\n return { clusterMode, layers };\n}\n\n/**\n * Synthetic \"all enabled keys render\" decision for the skip-info band.\n *\n * At these zooms we deliberately skip `/info`, so we have no counts.\n * The premise is that any viewport at that zoom holds too few features to\n * cross hide/cluster thresholds in practice; if a deployment hits that\n * edge case it can raise `PATHWAYS_CONFIG.skipInfoZoom`.\n */\nexport function decideSkipInfo(enabled: Set): RenderingDecision {\n const layers: Record = {};\n for (const key of enabled) {\n layers[key] = 'render';\n }\n return { clusterMode: 'off', layers };\n}\n\n// ---------------------------------------------------------------------------\n// /info fetch helper\n// ---------------------------------------------------------------------------\n\nlet _infoController: AbortController | null = null;\nlet _lastInfoEtag = '';\nlet _lastInfo: MapInfo | null = null;\n\n/** Returns the most recent /info response, or null if none cached yet. */\nexport function getLastInfo(): MapInfo | null {\n return _lastInfo;\n}\n\n/** Test-only: clear cached /info state so each test starts deterministically. */\nexport function _resetInfoCache(): void {\n if (_infoController) _infoController.abort();\n _infoController = null;\n _lastInfoEtag = '';\n _lastInfo = null;\n}\n\n/**\n * Fetch `/info` with conditional revalidation.\n *\n * `callback(info, changed)` -- `changed` is `true` when the server returned\n * fresh data (200) and `false` when it returned 304 Not Modified, in which\n * case `info` is the cached value. Callers can short-circuit on `!changed`\n * to avoid re-rendering when the previous decision is still valid.\n */\nexport async function fetchMapInfo(\n bbox: string,\n callback: (info: MapInfo, changed: boolean) => void,\n): Promise {\n if (_infoController) _infoController.abort();\n const controller = new AbortController();\n _infoController = controller;\n\n const url = API_BASE + 'info/?bbox=' + bbox;\n const headers: Record = { 'Accept': 'application/json' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n if (_lastInfoEtag) headers['If-None-Match'] = _lastInfoEtag;\n\n try {\n const response = await fetch(url, { headers, signal: controller.signal });\n if (_infoController === controller) _infoController = null;\n if (response.status === 304 && _lastInfo) {\n callback(_lastInfo, false);\n return;\n }\n if (response.ok) {\n const data = await response.json() as MapInfo;\n _lastInfoEtag = response.headers.get('ETag') || '';\n _lastInfo = data;\n callback(data, true);\n }\n } catch {\n if (_infoController === controller) _infoController = null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// GeoJSON fetching with AbortController\n// ---------------------------------------------------------------------------\n\nconst _inflightControllers: Record = {};\n\ninterface FetchResult {\n data: GeoJSON.FeatureCollection | null; // null on 304\n etag: string;\n}\n\nexport async function fetchGeoJSON(\n endpoint: string,\n bbox: string,\n callback: (result: FetchResult) => void,\n extraParams?: Record,\n ifNoneMatch?: string,\n): Promise {\n // Abort any in-flight request for this endpoint\n if (_inflightControllers[endpoint]) {\n _inflightControllers[endpoint].abort();\n }\n\n let url = API_BASE + endpoint + '?format=json&bbox=' + bbox;\n if (extraParams) {\n for (const key in extraParams) {\n url += '&' + key + '=' + encodeURIComponent(String(extraParams[key]));\n }\n }\n\n const controller = new AbortController();\n _inflightControllers[endpoint] = controller;\n\n const headers: Record = { 'Accept': 'application/json' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n if (ifNoneMatch) headers['If-None-Match'] = ifNoneMatch;\n\n try {\n const response = await fetch(url, { headers, signal: controller.signal });\n _inflightControllers[endpoint] = undefined!;\n const etag = response.headers.get('ETag') || '';\n if (response.status === 304) {\n callback({ data: null, etag });\n } else if (response.ok) {\n const data = await response.json() as GeoJSON.FeatureCollection;\n callback({ data, etag });\n }\n } catch (e) {\n _inflightControllers[endpoint] = undefined!;\n // Silently ignore AbortError and network errors\n }\n}\n\n// ---------------------------------------------------------------------------\n// Server-side search\n// ---------------------------------------------------------------------------\n\nimport type { ServerSearchResult } from './types/features';\n\nlet _searchController: AbortController | null = null;\n\nexport async function serverSearch(\n query: string,\n onResults: (results: ServerSearchResult[]) => void,\n): Promise {\n if (_searchController) _searchController.abort();\n const controller = new AbortController();\n _searchController = controller;\n\n const headers: Record = { 'Accept': 'application/json' };\n const csrfToken = _getCookie('csrftoken');\n if (csrfToken) headers['X-CSRFToken'] = csrfToken;\n\n // Search all layer endpoints in parallel, without bbox\n const endpoints: [string, FeatureType, string][] = [\n ['structures/', 'structure', 'structure_type'],\n ['conduit-banks/', 'conduit_bank', 'pathway_type'],\n ['conduits/', 'conduit', 'pathway_type'],\n ['aerial-spans/', 'aerial', 'pathway_type'],\n ['direct-buried/', 'direct_buried', 'pathway_type'],\n ['circuits/', 'circuit', 'pathway_type'],\n ];\n\n const results: ServerSearchResult[] = [];\n\n const fetches = endpoints.map(function (cfg) {\n const [endpoint, featureType, typeField] = cfg;\n const url = API_BASE + endpoint + '?format=json&q=' + encodeURIComponent(query);\n return fetch(url, { headers, signal: controller.signal })\n .then(function (resp) { return resp.ok ? resp.json() : null; })\n .then(function (data: GeoJSON.FeatureCollection | null) {\n if (!data || !data.features) return;\n data.features.forEach(function (f: GeoJSON.Feature) {\n if (!f.geometry) return;\n const props = f.properties || {};\n let latlng: L.LatLng;\n if (f.geometry.type === 'Point') {\n const coords = (f.geometry as GeoJSON.Point).coordinates;\n latlng = L.latLng(coords[1], coords[0]);\n } else if (f.geometry.type === 'LineString') {\n const coords = (f.geometry as GeoJSON.LineString).coordinates;\n const mid = coords[Math.floor(coords.length / 2)];\n latlng = L.latLng(mid[1], mid[0]);\n } else {\n return;\n }\n results.push({\n name: props.name || props.cid || 'Unnamed',\n featureType: featureType,\n typeKey: (props[typeField] as string) || featureType,\n latlng: latlng,\n url: props.url as string | undefined,\n });\n });\n })\n .catch(function () { /* ignore aborted / failed */ });\n });\n\n try {\n await Promise.all(fetches);\n _searchController = null;\n onResults(results);\n } catch {\n _searchController = null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Line labels\n// ---------------------------------------------------------------------------\n\nexport function addLineLabels(geoJsonLayer: L.GeoJSON, layerGroup: L.LayerGroup, map: L.Map): void {\n if (map.getZoom() < 20) return;\n\n geoJsonLayer.eachLayer(function (layer: L.Layer) {\n const polyline = layer as L.Polyline;\n const coords = polyline.getLatLngs() as L.LatLng[];\n if (!coords || coords.length < 2) return;\n const feature = (polyline as any).feature as GeoJSON.Feature;\n const name = feature?.properties?.name as string | undefined;\n if (!name) return;\n\n const midIdx = Math.floor(coords.length / 2);\n const p1 = coords[midIdx - 1] || coords[0];\n const p2 = coords[midIdx];\n const midLat = (p1.lat + p2.lat) / 2;\n const midLng = (p1.lng + p2.lng) / 2;\n\n // Use screen pixel coordinates so the angle matches the visual line\n const px1 = map.latLngToContainerPoint(p1);\n const px2 = map.latLngToContainerPoint(p2);\n const dx = px2.x - px1.x;\n const dy = px2.y - px1.y;\n // Angle in screen space (0 deg = right, positive = clockwise)\n let angle = Math.atan2(dy, dx) * 180 / Math.PI;\n // Keep text readable (not upside-down)\n if (angle > 90) angle -= 180;\n if (angle < -90) angle += 180;\n\n const icon = L.divIcon({\n className: 'pw-line-label',\n html: '
' + _esc(name) + '
',\n iconSize: [0, 0] as [number, number],\n iconAnchor: [0, 0] as [number, number],\n });\n\n layerGroup.addLayer(L.marker([midLat, midLng], { icon, interactive: false }));\n });\n}\n\n// ---------------------------------------------------------------------------\n// Data layer groups\n// ---------------------------------------------------------------------------\n\nexport interface DataLayerGroups {\n structures: L.LayerGroup;\n conduitBanks: L.LayerGroup;\n conduits: L.LayerGroup;\n aerialSpans: L.LayerGroup;\n directBuried: L.LayerGroup;\n circuits: L.LayerGroup;\n}\n\nexport function createDataLayers(): DataLayerGroups {\n return {\n structures: L.layerGroup(),\n conduitBanks: L.layerGroup(),\n conduits: L.layerGroup(),\n aerialSpans: L.layerGroup(),\n directBuried: L.layerGroup(),\n circuits: L.layerGroup(),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Pathway config\n// ---------------------------------------------------------------------------\n\n/** Pathway layer descriptor.\n *\n * - ``endpoint`` -- GeoJSON endpoint path relative to API_BASE\n * - ``layerKey`` -- key into DataLayerGroups\n * - ``featureType`` -- canonical type label used by the sidebar/popover\n * - ``style`` -- Leaflet polyline style\n * - ``infoKey`` -- corresponding key in MapInfo.counts/thresholds; this is\n * what the gating decision uses\n */\nexport type PathwayInfoKey = 'conduit_banks' | 'conduits' | 'aerial_spans' | 'direct_buried' | 'circuits';\n\nexport interface PathwayConfig {\n endpoint: string;\n layerKey: keyof DataLayerGroups;\n featureType: FeatureType;\n style: PathwayStyle;\n infoKey: PathwayInfoKey;\n}\n\nexport const PATHWAY_CONFIGS: PathwayConfig[] = [\n { endpoint: 'conduit-banks/', layerKey: 'conduitBanks', featureType: 'conduit_bank',\n style: { color: '#ad1457', weight: 5, opacity: 0.8, dashArray: '' }, infoKey: 'conduit_banks' },\n { endpoint: 'conduits/', layerKey: 'conduits', featureType: 'conduit',\n style: { color: '#f57c00', weight: 3, opacity: 0.7, dashArray: '5 5' }, infoKey: 'conduits' },\n { endpoint: 'aerial-spans/', layerKey: 'aerialSpans', featureType: 'aerial',\n style: { color: '#1565c0', weight: 3, opacity: 0.7, dashArray: '10 5' }, infoKey: 'aerial_spans' },\n { endpoint: 'direct-buried/', layerKey: 'directBuried', featureType: 'direct_buried',\n style: { color: '#616161', weight: 3, opacity: 0.7, dashArray: '2 4' }, infoKey: 'direct_buried' },\n { endpoint: 'circuits/', layerKey: 'circuits', featureType: 'circuit',\n style: { color: '#d32f2f', weight: 3, opacity: 0.8, dashArray: '8 6' }, infoKey: 'circuits' },\n];\n\n// ---------------------------------------------------------------------------\n// Viewport cache\n// ---------------------------------------------------------------------------\n\nconst GEO_CACHE_SIZE = 12;\nconst OVERFETCH = 0.5; // fraction of viewport width/height to pad each side\n\ninterface GeoCacheEntry {\n west: number; south: number; east: number; north: number;\n zoom: number;\n extraKey: string;\n etag: string;\n data: GeoJSON.FeatureCollection;\n}\n\nconst _geoCache: Record = {};\n\n/** Find a cache entry that covers a given viewport at a given zoom. */\nfunction _findCovering(\n endpoint: string, zoom: number, extraKey: string,\n west: number, south: number, east: number, north: number,\n): GeoCacheEntry | null {\n const entries = _geoCache[endpoint];\n if (!entries) return null;\n for (let i = entries.length - 1; i >= 0; i--) {\n const e = entries[i];\n if (e.zoom === zoom && e.extraKey === extraKey &&\n west >= e.west && south >= e.south &&\n east <= e.east && north <= e.north) {\n return e;\n }\n }\n return null;\n}\n\nfunction _storeInCache(\n endpoint: string, west: number, south: number, east: number, north: number,\n zoom: number, extraKey: string, etag: string, data: GeoJSON.FeatureCollection,\n): void {\n if (!_geoCache[endpoint]) _geoCache[endpoint] = [];\n const cache = _geoCache[endpoint];\n cache.push({ west, south, east, north, zoom, extraKey, etag, data });\n if (cache.length > GEO_CACHE_SIZE) cache.shift();\n}\n\nexport function cachedFetch(\n map: L.Map,\n endpoint: string,\n callback: (data: GeoJSON.FeatureCollection) => void,\n extraParams?: Record,\n): void {\n const b = map.getBounds();\n const west = b.getWest(), south = b.getSouth();\n const east = b.getEast(), north = b.getNorth();\n const zoom = map.getZoom();\n const extraKey = extraParams ? JSON.stringify(extraParams) : '';\n\n const cached = _findCovering(endpoint, zoom, extraKey, west, south, east, north);\n if (cached) {\n callback(cached.data);\n return;\n }\n\n // Cache miss -- expand bbox and fetch\n const dw = (east - west) * OVERFETCH;\n const dh = (north - south) * OVERFETCH;\n const fw = west - dw, fs = south - dh, fe = east + dw, fn = north + dh;\n const fetchBbox = fw + ',' + fs + ',' + fe + ',' + fn;\n\n fetchGeoJSON(endpoint, fetchBbox, function (result: FetchResult) {\n if (result.data) {\n _storeInCache(endpoint, fw, fs, fe, fn, zoom, extraKey, result.etag, result.data);\n callback(result.data);\n }\n }, extraParams);\n}\n\n// ---------------------------------------------------------------------------\n// Neighbor preloading\n// ---------------------------------------------------------------------------\n\nlet _preloadTimer: ReturnType | null = null;\n\nexport interface PreloadSpec {\n endpoint: string;\n extraParams?: Record;\n}\n\nconst MIN_PRELOAD_ZOOM = 14; // don't preload at low zoom -- user is likely to zoom, not pan\n\nexport function preloadNeighbors(map: L.Map, specs: PreloadSpec[]): void {\n if (_preloadTimer) clearTimeout(_preloadTimer);\n\n const zoom = map.getZoom();\n if (zoom < MIN_PRELOAD_ZOOM) return;\n\n const b = map.getBounds();\n const vw = b.getEast() - b.getWest();\n const vh = b.getNorth() - b.getSouth();\n\n // Cardinal offsets: right, left, down, up\n const offsets: [number, number][] = [[vw, 0], [-vw, 0], [0, -vh], [0, vh]];\n\n // Build a queue of {endpoint, bbox} pairs, skipping already-cached regions\n const queue: { endpoint: string; bbox: string; fw: number; fs: number; fe: number; fn: number; zoom: number; extraKey: string; extraParams?: Record }[] = [];\n for (const spec of specs) {\n const extraKey = spec.extraParams ? JSON.stringify(spec.extraParams) : '';\n for (const [dx, dy] of offsets) {\n const cw = b.getWest() + dx, cs = b.getSouth() + dy;\n const ce = b.getEast() + dx, cn = b.getNorth() + dy;\n if (_findCovering(spec.endpoint, zoom, extraKey, cw, cs, ce, cn)) continue;\n const dw = vw * OVERFETCH, dh = vh * OVERFETCH;\n const fw = cw - dw, fs = cs - dh, fe = ce + dw, fn = cn + dh;\n queue.push({\n endpoint: spec.endpoint,\n bbox: fw + ',' + fs + ',' + fe + ',' + fn,\n fw, fs, fe, fn, zoom, extraKey,\n extraParams: spec.extraParams,\n });\n }\n }\n\n // Drain the queue one at a time to avoid flooding the server\n let idx = 0;\n function _next(): void {\n if (idx >= queue.length) return;\n // Abort if the user has moved (zoom changed or panned significantly)\n if (map.getZoom() !== zoom) return;\n const q = queue[idx++];\n fetchGeoJSON(q.endpoint, q.bbox, function (result: FetchResult) {\n if (result.data) {\n _storeInCache(q.endpoint, q.fw, q.fs, q.fe, q.fn, q.zoom, q.extraKey, result.etag, result.data);\n }\n _preloadTimer = setTimeout(_next, 50);\n }, q.extraParams);\n }\n\n // Start after a short idle delay so visible data renders first\n _preloadTimer = setTimeout(_next, 200);\n}\n\n// ---------------------------------------------------------------------------\n// Data loading callbacks\n// ---------------------------------------------------------------------------\n\nexport interface LoadCallbacks {\n onFeatureClick?: (entry: FeatureEntry, e: L.LeafletMouseEvent) => void;\n onFeatureMouseOver?: (entry: FeatureEntry, e: L.LeafletMouseEvent, feature: GeoJSON.Feature) => void;\n onFeatureMouseOut?: () => void;\n onFeatureCreated?: (entry: FeatureEntry) => void;\n onStructuresLoaded?: (count: number) => void;\n onPathwayLoaded?: (data: GeoJSON.FeatureCollection) => void;\n onAllLoaded?: (allFeatures: FeatureEntry[]) => void;\n}\n\n/**\n * Load all data layers for the current viewport.\n *\n * Decision-driven: the caller supplies a ``RenderingDecision`` (from\n * /info + ``decideLayerRendering``) that says which layers to render and\n * whether to client-cluster structures. Layers marked ``'hide'`` are cleared\n * without a network call; layers not present in the decision are also\n * cleared (treated as disabled).\n *\n * The structures fetch always passes ``zoom`` so the server-side grid\n * cluster fallback still kicks in for any installation that doesn't have a\n * fresh /info result.\n */\nexport function loadDataLayers(\n map: L.Map,\n dataLayers: DataLayerGroups,\n decision: RenderingDecision | null,\n zoomHint: HTMLDivElement | null,\n callbacks: LoadCallbacks,\n): void {\n const zoom = map.getZoom();\n\n if (zoom < MIN_DATA_ZOOM || decision == null) {\n dataLayers.structures.clearLayers();\n dataLayers.conduitBanks.clearLayers();\n dataLayers.conduits.clearLayers();\n dataLayers.aerialSpans.clearLayers();\n dataLayers.directBuried.clearLayers();\n dataLayers.circuits.clearLayers();\n if (zoomHint) zoomHint.style.display = zoom < MIN_DATA_ZOOM ? '' : 'none';\n if (callbacks.onAllLoaded) callbacks.onAllLoaded([]);\n return;\n }\n // From here on ``decision`` is non-null; the local alias lets nested\n // closures benefit from the narrowing.\n const live: RenderingDecision = decision;\n\n if (zoomHint) zoomHint.style.display = 'none';\n\n // Clear any pathway layer whose decision is 'hide' (or absent because\n // the toggle is off). Counts that don't make the cut never fetch.\n PATHWAY_CONFIGS.forEach(function (cfg) {\n if (live.layers[cfg.infoKey] !== 'render') {\n dataLayers[cfg.layerKey].clearLayers();\n }\n });\n\n const allFeatures: FeatureEntry[] = [];\n let pendingLoads = 0;\n let totalExpectedLoads = 0;\n\n const renderStructures = live.layers.structures === 'render' && map.hasLayer(dataLayers.structures);\n if (renderStructures) totalExpectedLoads++;\n\n let pendingPathway = 0;\n PATHWAY_CONFIGS.forEach(function (cfg) {\n if (live.layers[cfg.infoKey] === 'render' && map.hasLayer(dataLayers[cfg.layerKey])) {\n totalExpectedLoads++;\n pendingPathway++;\n }\n });\n\n function _checkAllLoaded(): void {\n pendingLoads++;\n if (pendingLoads === totalExpectedLoads) {\n if (callbacks.onAllLoaded) callbacks.onAllLoaded(allFeatures);\n // Prefetch cardinal neighbors for all active endpoints\n const specs: PreloadSpec[] = [];\n if (renderStructures) {\n specs.push({ endpoint: 'structures/', extraParams: { zoom: zoom } });\n }\n PATHWAY_CONFIGS.forEach(function (cfg) {\n if (live.layers[cfg.infoKey] === 'render' && map.hasLayer(dataLayers[cfg.layerKey])) {\n specs.push({ endpoint: cfg.endpoint });\n }\n });\n preloadNeighbors(map, specs);\n }\n }\n\n function _pathwayLoaded(data: GeoJSON.FeatureCollection): void {\n if (callbacks.onPathwayLoaded) callbacks.onPathwayLoaded(data);\n pendingPathway--;\n }\n\n // Structures\n if (renderStructures) {\n // Client clustering is only used when the decision says 'client'; in\n // 'off' mode markers render plain, in 'server' mode the response\n // already contains pre-aggregated cluster centroids.\n const hasClusterPlugin = typeof L.markerClusterGroup === 'function';\n if (!hasClusterPlugin && live.clusterMode === 'client') {\n console.info('[pathways] MarkerCluster plugin not loaded -- client-side clustering disabled');\n }\n const useClientCluster = live.clusterMode === 'client' && hasClusterPlugin;\n const clusterGroup = useClientCluster\n ? L.markerClusterGroup({ maxClusterRadius: 35, spiderfyOnMaxZoom: true })\n : null;\n\n cachedFetch(map, 'structures/', function (data: GeoJSON.FeatureCollection) {\n dataLayers.structures.clearLayers();\n\n const isServerClustered = data.features && data.features.length > 0 &&\n (data.features[0].properties as GeoJSONProperties)?.cluster;\n\n if (isServerClustered) {\n let total = 0;\n data.features.forEach(function (f: GeoJSON.Feature) {\n const props = f.properties as GeoJSONProperties;\n const count = props.point_count || 0;\n total += count;\n const geom = f.geometry as GeoJSON.Point;\n const latlng = L.latLng(geom.coordinates[1], geom.coordinates[0]);\n const marker = L.marker(latlng, { icon: _clusterIcon(count) });\n marker.on('click', function () {\n const nextZoom = Math.min(map.getZoom() + 3, map.getMaxZoom());\n map.setView(latlng, nextZoom);\n });\n dataLayers.structures.addLayer(marker);\n });\n if (callbacks.onStructuresLoaded) callbacks.onStructuresLoaded(total);\n } else {\n const geoLayer = L.geoJSON(data, {\n pointToLayer: function (feature: GeoJSON.Feature, latlng: L.LatLng) {\n return L.marker(latlng, {\n icon: _structureIcon((feature.properties as GeoJSONProperties).structure_type || ''),\n });\n },\n onEachFeature: function (feature: GeoJSON.Feature, layer: L.Layer) {\n if (feature.id != null && (feature.properties as GeoJSONProperties).id == null) {\n (feature.properties as GeoJSONProperties).id = feature.id as number;\n }\n const entry: FeatureEntry = {\n props: feature.properties as GeoJSONProperties,\n featureType: 'structure',\n layer: layer,\n latlng: (layer as L.Marker).getLatLng(),\n };\n allFeatures.push(entry);\n if (callbacks.onFeatureCreated) callbacks.onFeatureCreated(entry);\n layer.on('click', function (e: L.LeafletMouseEvent) {\n if (callbacks.onFeatureClick) callbacks.onFeatureClick(entry, e);\n });\n layer.on('mouseover', function (e: L.LeafletMouseEvent) {\n if (callbacks.onFeatureMouseOver) callbacks.onFeatureMouseOver(entry, e, feature);\n });\n layer.on('mouseout', function () {\n if (callbacks.onFeatureMouseOut) callbacks.onFeatureMouseOut();\n });\n },\n });\n if (clusterGroup) {\n clusterGroup.addLayers(geoLayer.getLayers());\n dataLayers.structures.addLayer(clusterGroup);\n } else {\n geoLayer.addTo(dataLayers.structures);\n }\n if (callbacks.onStructuresLoaded) {\n callbacks.onStructuresLoaded(data.features ? data.features.length : 0);\n }\n }\n _checkAllLoaded();\n }, { zoom: zoom });\n }\n\n if (pendingPathway === 0 && callbacks.onPathwayLoaded) {\n // No pathway layers active -- signal with empty data\n }\n\n // Shared pathway handler factory\n function _makePathwayOpts(featureType: FeatureType, styleObj: PathwayStyle): L.GeoJSONOptions {\n return {\n style: function () { return styleObj; },\n onEachFeature: function (feature: GeoJSON.Feature, layer: L.Layer) {\n const props = feature.properties as GeoJSONProperties;\n if (feature.id != null && props.id == null) {\n props.id = feature.id as number;\n }\n // Normalise: pathways use \"label\", map UI expects \"name\"\n if (!props.name && props.label) props.name = props.label as string;\n const entry: FeatureEntry = {\n props: props,\n featureType: featureType,\n layer: layer,\n latlng: (layer as L.Polyline).getBounds().getCenter(),\n };\n allFeatures.push(entry);\n if (callbacks.onFeatureCreated) callbacks.onFeatureCreated(entry);\n layer.on('click', function (e: L.LeafletMouseEvent) {\n if (callbacks.onFeatureClick) callbacks.onFeatureClick(entry, e);\n });\n layer.on('mouseover', function (e: L.LeafletMouseEvent) {\n if (callbacks.onFeatureMouseOver) callbacks.onFeatureMouseOver(entry, e, feature);\n });\n layer.on('mouseout', function () {\n if (callbacks.onFeatureMouseOut) callbacks.onFeatureMouseOut();\n });\n },\n };\n }\n\n PATHWAY_CONFIGS.forEach(function (cfg) {\n const layer = dataLayers[cfg.layerKey];\n if (live.layers[cfg.infoKey] !== 'render') return;\n if (!map.hasLayer(layer)) return;\n cachedFetch(map, cfg.endpoint, function (data: GeoJSON.FeatureCollection) {\n layer.clearLayers();\n const geoLayer = L.geoJSON(data, _makePathwayOpts(cfg.featureType, cfg.style));\n geoLayer.addTo(layer);\n addLineLabels(geoLayer, layer, map);\n _pathwayLoaded(data);\n _checkAllLoaded();\n });\n });\n\n // If no layers active, still signal completion\n if (totalExpectedLoads === 0) {\n if (callbacks.onAllLoaded) callbacks.onAllLoaded([]);\n }\n}\n\n/**\n * Calculate total pathway length from a GeoJSON FeatureCollection.\n * Returns length in meters.\n */\nexport function calcPathwayLength(data: GeoJSON.FeatureCollection): number {\n let totalLength = 0;\n if (data.features) {\n data.features.forEach(function (f: GeoJSON.Feature) {\n if (f.geometry && f.geometry.type === 'LineString') {\n const coords = (f.geometry as GeoJSON.LineString).coordinates;\n for (let i = 0; i < coords.length - 1; i++) {\n totalLength += _haversine(\n coords[i][1], coords[i][0],\n coords[i + 1][1], coords[i + 1][0],\n );\n }\n }\n });\n }\n return totalLength;\n}\n", "/**\n * Decides how to render the map at a given zoom + cache state.\n *\n * The map used to round-trip `/info` on every pan/zoom and only start the\n * GeoJSON fetches once the response came back. Even with conditional\n * revalidation (ETag + 304), that adds one full RTT per move which feels\n * noticeably laggy on a slow link.\n *\n * The strategy here cuts that latency in two ways:\n *\n * 1. Skip-info band (zoom >= SKIP_INFO_ZOOM): the viewport is small\n * enough that hide/cluster thresholds are effectively unreachable.\n * Skip `/info` entirely and synthesize a \"render every enabled layer\"\n * decision.\n *\n * 2. Optimistic + revalidate (MIN_DATA_ZOOM <= zoom < SKIP_INFO_ZOOM\n * with a cached `/info`): render the cached decision immediately and\n * fire `/info` in the background with `If-None-Match`. A 304 means\n * the optimistic render was correct -- nothing to do. A 200 with a\n * meaningfully different decision triggers a single reconciliation\n * reload.\n *\n * 3. Cold gated path (no cache): the original \"wait for /info, then\n * load\" behaviour, used only on the very first viewport.\n */\n\nimport {\n MIN_DATA_ZOOM,\n SKIP_INFO_ZOOM,\n decideLayerRendering,\n decideSkipInfo,\n} from './data-layers';\nimport type { MapInfo, RenderingDecision } from './data-layers';\n\nexport { MIN_DATA_ZOOM, SKIP_INFO_ZOOM, decideSkipInfo };\n\nexport type LoadStrategy =\n | { kind: 'below-min-zoom' }\n | { kind: 'skip-info'; decision: RenderingDecision }\n | { kind: 'optimistic'; decision: RenderingDecision; revalidate: true; cachedInfo: MapInfo }\n | { kind: 'gated' };\n\n/**\n * Picks the strategy for one pan/zoom event.\n *\n * `skipInfoZoom` defaults to `SKIP_INFO_ZOOM` so it tracks server config;\n * the argument is mainly there for tests.\n */\nexport function chooseLoadStrategy(\n zoom: number,\n cachedInfo: MapInfo | null,\n enabled: Set,\n skipInfoZoom: number = SKIP_INFO_ZOOM,\n): LoadStrategy {\n if (zoom < MIN_DATA_ZOOM) {\n return { kind: 'below-min-zoom' };\n }\n if (zoom >= skipInfoZoom) {\n return { kind: 'skip-info', decision: decideSkipInfo(enabled) };\n }\n if (cachedInfo) {\n return {\n kind: 'optimistic',\n decision: decideLayerRendering(cachedInfo, enabled),\n revalidate: true,\n cachedInfo,\n };\n }\n return { kind: 'gated' };\n}\n\n/**\n * True when two decisions would render differently.\n *\n * Used by the optimistic path: after revalidation returns a fresh /info,\n * we only re-run the (expensive) GeoJSON fetches if the new decision flips\n * a layer's visibility or the cluster mode. Identical decisions short-\n * circuit to a chip refresh.\n */\nexport function decisionsDiffer(a: RenderingDecision, b: RenderingDecision): boolean {\n if (a.clusterMode !== b.clusterMode) return true;\n const aKeys = Object.keys(a.layers);\n const bKeys = Object.keys(b.layers);\n if (aKeys.length !== bKeys.length) return true;\n for (const key of aKeys) {\n if (a.layers[key] !== b.layers[key]) return true;\n }\n return false;\n}\n", "/**\n * Full-page infrastructure map.\n *\n * Fetches structures and pathways from the GeoJSON API within the visible\n * bounding box, but only when zoomed in past a minimum threshold.\n * Re-fetches on pan/zoom with debouncing.\n *\n * Structures use server-side grid clustering at low zoom levels (11-14)\n * to avoid transferring thousands of features just for client-side clustering.\n * At zoom 15+ individual features are returned and optionally client-clustered.\n */\n\nimport { Sidebar } from './sidebar';\nimport { Popover } from './popover';\nimport type { FeatureEntry, FeatureType, GeoJSONProperties, PathwayStyle } from './types/features';\nimport {\n initExternalLayers,\n loadExternalLayers,\n getLayerConfig,\n} from './external-layers';\nimport type { ExternalLayerConfig } from './types/external';\nimport {\n STRUCTURE_COLORS,\n STRUCTURE_SHAPES,\n PATHWAY_COLORS,\n esc as _esc,\n titleCase as _titleCase,\n getCookie as _getCookie,\n bboxParam as _bboxParam,\n debounce as _debounce,\n} from './map-utils';\nimport {\n createMap,\n createZoomHint,\n createLegend,\n createStatsControl,\n createKioskControl,\n createSidebarToggleControl,\n createLocateControl,\n} from './map-core';\nimport type { MapInitConfig } from './map-core';\nimport {\n API_BASE,\n MIN_DATA_ZOOM,\n createDataLayers,\n loadDataLayers,\n calcPathwayLength,\n serverSearch,\n fetchMapInfo,\n decideLayerRendering,\n getLastInfo,\n} from './data-layers';\nimport type { MapInfo, RenderingDecision } from './data-layers';\nimport { chooseLoadStrategy, decisionsDiffer } from './load-strategy';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst CFG: Partial = window.PATHWAYS_CONFIG || {};\n\n// ---------------------------------------------------------------------------\n// Main initialization\n// ---------------------------------------------------------------------------\n\nfunction initializePathwaysMap(elementId: string, config: MapInitConfig): void {\n const container = document.getElementById(elementId);\n\n // Inject dependencies into sub-modules\n Sidebar.setDeps({\n titleCase: _titleCase,\n esc: _esc,\n debounce: _debounce,\n getCookie: _getCookie,\n structureColors: STRUCTURE_COLORS,\n structureShapes: STRUCTURE_SHAPES,\n pathwayColors: PATHWAY_COLORS,\n apiBase: API_BASE,\n });\n Popover.setDeps({ titleCase: _titleCase });\n\n const { map, baseLayers, layerControl } = createMap(elementId, config);\n\n // Stats + legend (stats first so legend stacks above it)\n createStatsControl(map);\n createLegend(map);\n\n // Map controls (topleft, below zoom)\n createKioskControl(map, !!config.kiosk).addTo(map);\n createSidebarToggleControl(map, !!config.kiosk, function () { Sidebar.show(); }).addTo(map);\n createLocateControl(map).addTo(map);\n\n // Counters\n const structureCountEl = document.getElementById('structure-count');\n const pathwayCountEl = document.getElementById('pathway-count');\n const totalLengthEl = document.getElementById('total-length');\n\n // Zoom hint\n const zoomHint = createZoomHint(map);\n\n // --- Layer visibility persistence (localStorage) ---\n\n const PREFS_KEY = 'pathways_map_layers';\n const DEFAULT_LAYERS: Record = {\n 'Structures': true, 'Conduit Banks': true, 'Conduits': true, 'Aerial Spans': false, 'Direct Buried': false, 'Circuit Routes': false,\n };\n\n function _loadPrefs(): Record | null {\n try {\n const saved = localStorage.getItem(PREFS_KEY);\n return saved ? JSON.parse(saved) as Record : null;\n } catch (_e) { return null; }\n }\n\n function _savePrefs(layers: Record): void {\n try { localStorage.setItem(PREFS_KEY, JSON.stringify(layers)); } catch (_e) { /* ignore */ }\n }\n\n const layerPrefs = _loadPrefs() || DEFAULT_LAYERS;\n\n // --- Dynamic data layers ---\n\n const dataLayers = createDataLayers();\n\n const layerNames: Record = {\n 'Structures': dataLayers.structures,\n 'Conduit Banks': dataLayers.conduitBanks,\n 'Conduits': dataLayers.conduits,\n 'Aerial Spans': dataLayers.aerialSpans,\n 'Direct Buried': dataLayers.directBuried,\n 'Circuit Routes': dataLayers.circuits,\n };\n\n // --- External plugin layers ---\n const externalConfigs: ExternalLayerConfig[] = CFG.externalLayers ?? [];\n const externalGroups = initExternalLayers(externalConfigs, map);\n\n // Add external layers to layerNames for the layer control\n for (const [name, group] of externalGroups) {\n const cfg = getLayerConfig(name);\n if (cfg) {\n layerNames[cfg.label] = group;\n }\n }\n\n // Add layers based on saved prefs\n for (const lname in layerNames) {\n if (layerPrefs[lname] !== false) {\n layerNames[lname].addTo(map);\n }\n }\n\n // Map between sidebar display name and MapInfo key. Used to translate\n // the /info decision (which keys layers by their snake_case info key)\n // back into the friendly labels used for the sidebar toggles.\n const LAYER_INFO_KEY: Record = {\n 'Structures': 'structures',\n 'Conduit Banks': 'conduit_banks',\n 'Conduits': 'conduits',\n 'Aerial Spans': 'aerial_spans',\n 'Direct Buried': 'direct_buried',\n 'Circuit Routes': 'circuits',\n };\n for (const [extName] of externalGroups) {\n const cfg = getLayerConfig(extName);\n if (cfg) LAYER_INFO_KEY[cfg.label] = 'external:' + extName;\n }\n\n // --- Sidebar layer toggle sync ---\n\n const _layerCheckboxes: Record = {};\n\n function _syncSidebarCheckbox(name: string, checked: boolean): void {\n if (_layerCheckboxes[name]) {\n _layerCheckboxes[name].checked = checked;\n const btn = _layerCheckboxes[name].closest('.pw-layer-toggle');\n if (btn) {\n btn.classList.toggle('pw-layer-active', checked);\n }\n }\n }\n\n // Persist layer toggles\n map.on('overlayadd', function (e: L.LayersControlEvent) {\n const prefs = _loadPrefs() || DEFAULT_LAYERS;\n prefs[e.name] = true;\n _savePrefs(prefs);\n _syncSidebarCheckbox(e.name, true);\n });\n map.on('overlayremove', function (e: L.LayersControlEvent) {\n const prefs = _loadPrefs() || DEFAULT_LAYERS;\n prefs[e.name] = false;\n _savePrefs(prefs);\n _syncSidebarCheckbox(e.name, false);\n });\n\n // --- Sidebar layer toggles ---\n\n // SVG icons for layer toggle buttons (trusted compile-time constants)\n const LAYER_ICONS: Record = {\n 'Structures': '',\n 'Conduit Banks': '',\n 'Conduits': '',\n 'Aerial Spans': '',\n 'Direct Buried': '',\n 'Circuit Routes': '',\n };\n\n function _buildSidebarLayerToggles(): void {\n const toggleContainer = document.getElementById('pw-layer-toggles');\n if (!toggleContainer) return;\n toggleContainer.textContent = '';\n\n for (const lname in layerNames) {\n const btn = document.createElement('button');\n btn.type = 'button';\n const active = map.hasLayer(layerNames[lname]);\n btn.className = 'pw-layer-toggle' + (active ? ' pw-layer-active' : '');\n\n // Build button content using DOM methods\n // iconSvg is from LAYER_ICONS constant -- trusted compile-time SVG\n const iconSvg = LAYER_ICONS[lname] || '';\n const span = document.createElement('span');\n span.textContent = lname;\n const wrapper = document.createElement('span');\n wrapper.innerHTML = iconSvg; // eslint-disable-line no-unsanitized/property -- trusted compile-time SVG constants only\n while (wrapper.firstChild) btn.appendChild(wrapper.firstChild);\n btn.appendChild(span);\n\n const cb = document.createElement('input');\n cb.type = 'checkbox';\n cb.checked = active;\n cb.style.display = 'none';\n _layerCheckboxes[lname] = cb;\n btn.appendChild(cb);\n\n (function (cbName: string, checkbox: HTMLInputElement, button: HTMLButtonElement) {\n button.addEventListener('click', function () {\n checkbox.checked = !checkbox.checked;\n if (checkbox.checked) {\n map.addLayer(layerNames[cbName]);\n button.classList.add('pw-layer-active');\n } else {\n map.removeLayer(layerNames[cbName]);\n button.classList.remove('pw-layer-active');\n }\n const prefs = _loadPrefs() || DEFAULT_LAYERS;\n prefs[cbName] = checkbox.checked;\n _savePrefs(prefs);\n _loadData();\n });\n })(lname, cb, btn);\n\n toggleContainer.appendChild(btn);\n }\n }\n _buildSidebarLayerToggles();\n\n // --- Data loading ---\n\n let pathwayCount = 0;\n let totalLength = 0;\n\n function _updateUrl(): void {\n const center = map.getCenter();\n const zoom = map.getZoom();\n const params = new URLSearchParams();\n params.set('lat', center.lat.toFixed(6));\n params.set('lon', center.lng.toFixed(6));\n params.set('zoom', String(zoom));\n const selId = Sidebar.getSelectedId();\n if (selId) params.set('select', selId);\n if (config.kiosk) params.set('kiosk', 'true');\n const newUrl = window.location.pathname + '?' + params.toString();\n history.replaceState(null, '', newUrl);\n }\n\n function _setLayerChip(name: string, count: number | null): void {\n if (!_layerCheckboxes[name]) return;\n const btn = _layerCheckboxes[name].closest('.pw-layer-toggle') as HTMLElement | null;\n if (!btn) return;\n let chip = btn.querySelector('.pw-layer-count-chip');\n if (count == null) {\n if (chip) chip.remove();\n return;\n }\n if (!chip) {\n chip = document.createElement('span');\n chip.className = 'pw-layer-count-chip';\n btn.appendChild(chip);\n }\n chip.textContent = count.toLocaleString();\n }\n\n function _applyDecisionToToggles(decision: RenderingDecision, info: MapInfo): void {\n for (const lname in LAYER_INFO_KEY) {\n if (!_layerCheckboxes[lname]) continue;\n const btn = _layerCheckboxes[lname].closest('.pw-layer-toggle');\n if (!btn) continue;\n const infoKey = LAYER_INFO_KEY[lname];\n // Hidden layers (decision said 'hide') get dimmed with a count chip.\n // Only show the chip while the layer is enabled in the toggle.\n const enabled = _layerCheckboxes[lname].checked;\n const isHidden = enabled && decision.layers[infoKey] === 'hide';\n btn.classList.toggle('pw-layer-unavailable', isHidden);\n if (isHidden) {\n let count: number | undefined;\n if (infoKey.startsWith('external:')) {\n count = info.counts.external?.[infoKey.slice(9)];\n } else {\n count = (info.counts as unknown as Record)[infoKey];\n }\n _setLayerChip(lname, count ?? 0);\n } else {\n _setLayerChip(lname, null);\n }\n }\n }\n\n function _runLoad(decision: RenderingDecision | null, info: MapInfo | null): void {\n const zoom = map.getZoom();\n if (decision && info) _applyDecisionToToggles(decision, info);\n\n // Reset pathway stats\n pathwayCount = 0;\n totalLength = 0;\n\n loadDataLayers(map, dataLayers, decision, zoomHint, {\n onFeatureClick: function (entry: FeatureEntry, e: L.LeafletMouseEvent) {\n if (e.originalEvent) (e.originalEvent as any)._sidebarClick = true;\n Sidebar.selectFeature(entry);\n _updateUrl();\n },\n onFeatureMouseOver: function (entry: FeatureEntry, e: L.LeafletMouseEvent, feature: GeoJSON.Feature) {\n Popover.show(\n e.latlng || (entry.layer as L.Marker).getLatLng(),\n feature.properties as GeoJSONProperties,\n );\n },\n onFeatureMouseOut: function () {\n Popover.hide();\n },\n onFeatureCreated: function (entry: FeatureEntry) {\n Sidebar.onFeatureCreated(entry);\n },\n onStructuresLoaded: function (count: number) {\n if (structureCountEl) structureCountEl.textContent = String(count);\n },\n onPathwayLoaded: function (data: GeoJSON.FeatureCollection) {\n const count = data.features ? data.features.length : 0;\n pathwayCount += count;\n totalLength += calcPathwayLength(data);\n if (pathwayCountEl) pathwayCountEl.textContent = String(pathwayCount);\n if (totalLengthEl) totalLengthEl.textContent = (totalLength / 1000).toFixed(2);\n },\n onAllLoaded: function (allFeatures: FeatureEntry[]) {\n if (zoom < MIN_DATA_ZOOM) {\n if (structureCountEl) structureCountEl.textContent = '-';\n if (pathwayCountEl) pathwayCountEl.textContent = '-';\n if (totalLengthEl) totalLengthEl.textContent = '-';\n Sidebar.setFeatures([]);\n return;\n }\n\n // --- External plugin layers ---\n // Suppress externals when the decision says hide (e.g. structures\n // are clustered or the external layer is over its own threshold).\n const visibleExternal = new Set();\n for (const [name, group] of externalGroups) {\n if (!map.hasLayer(group)) continue;\n if (decision && decision.layers['external:' + name] === 'hide') continue;\n visibleExternal.add(name);\n }\n if (visibleExternal.size > 0) {\n const bbox = _bboxParam(map);\n loadExternalLayers(bbox, zoom, visibleExternal, function (entry: FeatureEntry, extCfg: ExternalLayerConfig) {\n Sidebar.onFeatureCreated(entry);\n entry.layer.on('click', function (e: L.LeafletMouseEvent) {\n if (e.originalEvent) (e.originalEvent as any)._sidebarClick = true;\n Sidebar.selectFeature(entry);\n });\n entry.layer.on('mouseover', function (e: L.LeafletMouseEvent) {\n Popover.show(e.latlng, entry.props, extCfg.popoverFields);\n });\n entry.layer.on('mouseout', function () { Popover.hide(); });\n }).then(function (extEntries: FeatureEntry[]) {\n for (let i = 0; i < extEntries.length; i++) {\n allFeatures.push(extEntries[i]);\n }\n Sidebar.setFeatures(allFeatures);\n // Auto-select feature from URL on first load\n if (_pendingSelectId) {\n Sidebar.selectById(_pendingSelectId);\n _pendingSelectId = '';\n }\n });\n } else {\n Sidebar.setFeatures(allFeatures);\n // Auto-select feature from URL on first load\n if (_pendingSelectId) {\n Sidebar.selectById(_pendingSelectId);\n _pendingSelectId = '';\n }\n }\n },\n });\n }\n\n function _enabledInfoKeys(): Set {\n const set = new Set();\n for (const lname in LAYER_INFO_KEY) {\n if (_layerCheckboxes[lname]?.checked) {\n set.add(LAYER_INFO_KEY[lname]);\n } else if (!_layerCheckboxes[lname] && map.hasLayer(layerNames[lname])) {\n // Toggle not yet built (initial load): fall back to layer state\n set.add(LAYER_INFO_KEY[lname]);\n }\n }\n return set;\n }\n\n function _loadData(): void {\n const zoom = map.getZoom();\n const enabled = _enabledInfoKeys();\n const strategy = chooseLoadStrategy(zoom, getLastInfo(), enabled);\n\n switch (strategy.kind) {\n case 'below-min-zoom':\n _runLoad(null, null);\n return;\n\n case 'skip-info':\n // Skip /info entirely -- viewport is too small to plausibly\n // cross any hide/cluster threshold. Render now, no round-trip.\n _runLoad(strategy.decision, null);\n return;\n\n case 'optimistic': {\n // Render the cached decision immediately, then revalidate /info\n // in the background. A 304 leaves the screen untouched. A 200\n // with a materially different decision triggers a reconcile.\n _runLoad(strategy.decision, strategy.cachedInfo);\n const bbox = _bboxParam(map);\n fetchMapInfo(bbox, function (info: MapInfo, changed: boolean) {\n if (!changed) return;\n const fresh = decideLayerRendering(info, _enabledInfoKeys());\n if (decisionsDiffer(strategy.decision, fresh)) {\n _runLoad(fresh, info);\n } else {\n _applyDecisionToToggles(fresh, info);\n }\n });\n return;\n }\n\n case 'gated': {\n // First load: no cache yet, so we still wait on /info.\n const bbox = _bboxParam(map);\n fetchMapInfo(bbox, function (info: MapInfo) {\n const decision = decideLayerRendering(info, _enabledInfoKeys());\n _runLoad(decision, info);\n });\n return;\n }\n }\n }\n\n // --- URL state management ---\n\n let _pendingSelectId = config.select || '';\n\n const debouncedUrlUpdate = _debounce(_updateUrl, 300);\n\n // Load data on every moveend -- AbortController in fetchGeoJSON cancels\n // stale in-flight requests, so no debounce needed.\n map.on('moveend', function () {\n _loadData();\n debouncedUrlUpdate();\n });\n\n // Initialize sidebar and popover\n Sidebar.init(map, config.kiosk);\n Sidebar.onServerSearch(function (query: string) {\n serverSearch(query, function (results) {\n Sidebar.setServerResults(results);\n });\n });\n Sidebar.onSelectionChange(_updateUrl);\n Popover.init(map);\n\n // Initial load\n _loadData();\n\n // Reset view button\n const resetBtn = document.getElementById('reset-view');\n if (resetBtn) {\n resetBtn.addEventListener('click', function () {\n if (config.bounds) {\n map.fitBounds(config.bounds, { padding: [30, 30] as [number, number], maxZoom: 17 });\n } else {\n map.setView(config.center || [0, 0], config.zoom || 2);\n }\n });\n }\n\n // Store reference\n (window as any).PathwaysMap = {\n map: map,\n layerControl: layerControl,\n };\n\n // Leaflet calculates size at init; force a recheck after layout settles\n setTimeout(function () { map.invalidateSize(); }, 100);\n window.addEventListener('resize', function () { map.invalidateSize(); });\n}\n\n// Expose globally\nwindow.initializePathwaysMap = initializePathwaysMap;\n"], + "mappings": "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", + "names": ["NATIVE_TYPES", "_layerStates", "_resolveColor", "props", "style", "_a", "_b", "val", "_createPointMarker", "latlng", "color", "_createLine", "coords", "_createPolygon", "initExternalLayers", "configs", "map", "groups", "sorted", "a", "b", "cfg", "group", "loadExternalLayers", "bbox", "zoom", "visibleLayers", "onFeature", "__async", "allEntries", "fetchPromises", "name", "state", "promise", "_fetchLayer", "p", "entries", "config", "layerGroup", "abortController", "sep", "url", "csrfMatch", "headers", "resp", "data", "feature", "layer", "lng", "lat", "c", "line", "rings", "ring", "poly", "entry", "__spreadProps", "__spreadValues", "err", "getLayerConfig", "_titleCase", "_esc", "_debounce", "_getCookie", "STRUCTURE_COLORS", "STRUCTURE_SHAPES", "PATHWAY_COLORS", "API_BASE", "_map", "_features", "_filtered", "_selected", "_activeTypes", "_detailCache", "_highlightedLayer", "_highlightOutline", "_serverSearchCallback", "_lastServerQuery", "_pendingSelect", "_pendingFlySelectId", "_isKiosk", "_onSelectionChange", "_dimmedFeatures", "_activeConnectedIds", "DIM_OPACITY", "_isNativeType", "featureType", "NATIVE_TYPES", "_typeLabel", "extCfg", "getLayerConfig", "_colorForFeature", "entry", "_typeKeyForFeature", "_featureId", "_lighten", "hex", "amount", "g", "b", "_unhighlightMapFeature", "layer", "_applyHighlightVisuals", "marker", "type", "color", "shape", "isOutline", "polyline", "origStyle", "latlngs", "_highlightMapFeature", "_reapplyHighlight", "_dimFeatures", "connectedIds", "_applyDim", "f", "fid", "_restoreDim", "orig", "_highlightListItem", "listEl", "items", "targetId", "i", "_renderList", "countEl", "item", "dot", "label", "typeBadge", "selectFeature", "_buildTypeFilters", "container", "typeMap", "key", "types", "t", "btn", "_applyFilters", "searchInput", "query", "typeKey", "name", "_showSearchingIndicator", "_clearServerResults", "_detailPageUrl", "id", "base", "_apiUrlForFeature", "_a", "_resolveValue", "val", "texts", "choice", "fk", "_addFieldRow", "table", "suffix", "resolved", "text", "tr", "tdLabel", "tdVal", "a", "_addTagsRow", "tags", "tag", "badge", "DETAIL_FIELDS", "_createSection", "title", "parent", "header", "chevron", "body", "collapsed", "_addEnrichmentBadges", "data", "badgeRow", "metrics", "m", "_renderEnrichedDetail", "sectionBody", "fieldName", "fields", "tsDiv", "parts", "_fetchConnectedPathways", "_renderConnectedStructures", "_renderConnectedItem", "typeLabel", "mapFeature", "selectId", "hintLatLng", "current", "structId", "url", "headers", "csrfToken", "resp", "neighborMap", "pwItems", "pw", "pwType", "display", "pwId", "startHint", "endHint", "_e", "startS", "endS", "neighbors", "structSection", "s", "mf", "pwSection", "structs", "startStruct", "endStruct", "idx", "hint", "_fetchDetail", "__async", "cacheKey", "htmlUrl", "_resolveDetailUrl", "htmlCacheKey", "_setTrustedHtml", "_fetchHtmlDetail", "props", "loadingDiv", "response", "errDiv", "html", "_renderDetail", "p", "titleRow", "editBtn", "pencilIcon", "editForm", "editInput", "saveBtn", "cancelEditBtn", "newName", "patchHeaders", "e", "siteBadge", "detailUrl", "link", "icon", "planLink", "planIcon", "detailContainer", "indicator", "el", "hdr", "setServerResults", "results", "searching", "oldHdr", "oldItems", "noResults", "hdrIcon", "result", "goIcon", "onServerSearch", "cb", "init", "map", "kiosk", "toggleBtn", "_setListBodyVisible", "_isCollapsed", "backBtn", "showList", "detailPanel", "_kioskSidebarClose", "kioskCloseBtn", "visible", "_kioskSidebarOpen", "sidebar", "show", "hide", "setFeatures", "features", "cacheKeys", "k", "selId", "found", "pending", "showDetail", "heading", "zoom", "minZoom", "onFeatureCreated", "setDeps", "deps", "getSelectedId", "selectById", "onSelectionChange", "Sidebar", "_titleCase", "_el", "_map", "_position", "latlng", "pt", "cw", "x", "y", "init", "map", "show", "props", "popoverFields", "_a", "_b", "name", "typeText", "f", "t", "type", "hide", "setDeps", "deps", "Popover", "STRUCTURE_COLORS", "STRUCTURE_SHAPES", "PATHWAY_COLORS", "PATHWAY_DASH", "structureIcon", "type", "size", "color", "STRUCTURE_COLORS", "shape", "STRUCTURE_SHAPES", "isOutline", "half", "clusterIcon", "count", "cls", "esc", "text", "el", "titleCase", "str", "c", "getCookie", "name", "parts", "bboxParam", "map", "b", "debounce", "fn", "delay", "timer", "haversine", "lat1", "lon1", "lat2", "lon2", "p1", "p2", "dp", "dl", "a", "CFG", "MAX_NATIVE_ZOOM", "DEFAULT_BASE_LAYERS", "createBaseLayers", "configured", "c", "configs", "layers", "cfg", "createUserOverlays", "userOverlays", "overlays", "layer", "createZoomHint", "map", "div", "_setStaticSvg", "el", "svg", "createLegend", "LegendControl", "container", "header", "chevron", "titleSpan", "body", "structSec", "structTitle", "structTypes", "i", "stype", "color", "STRUCTURE_COLORS", "shape", "STRUCTURE_SHAPES", "isOutline", "item", "swatch", "label", "titleCase", "pathSec", "pathTitle", "pathTypes", "ptype", "PATHWAY_COLORS", "dash", "PATHWAY_DASH", "isCollapsed", "createStatsControl", "StatsControl", "sc", "pc", "tl", "createLocateControl", "LocateControl", "link", "e", "pos", "createKioskControl", "isKiosk", "KioskControl", "center", "zoom", "params", "createSidebarToggleControl", "showCallback", "SidebarToggle", "observer", "sidebar", "sidebarVisible", "createMap", "elementId", "config", "baseLayers", "firstLayer", "boundsMaxZoom", "overlayLayers", "name", "layerControl", "CFG", "API_BASE", "MIN_DATA_ZOOM", "_a", "SKIP_INFO_ZOOM", "CFG", "decideLayerRendering", "info", "enabled", "structuresCount", "sThresh", "clusterMode", "layers", "suppress", "nativeKeys", "key", "count", "threshold", "extCounts", "extThresholds", "name", "t", "decideSkipInfo", "_infoController", "_lastInfoEtag", "_lastInfo", "getLastInfo", "fetchMapInfo", "bbox", "callback", "__async", "_infoController", "controller", "url", "API_BASE", "headers", "csrfToken", "getCookie", "_lastInfoEtag", "response", "_lastInfo", "data", "e", "_inflightControllers", "fetchGeoJSON", "endpoint", "extraParams", "ifNoneMatch", "key", "etag", "_searchController", "serverSearch", "query", "onResults", "endpoints", "results", "fetches", "cfg", "featureType", "typeField", "resp", "f", "props", "latlng", "coords", "mid", "addLineLabels", "geoJsonLayer", "layerGroup", "map", "layer", "_a", "polyline", "feature", "name", "midIdx", "p1", "p2", "midLat", "midLng", "px1", "px2", "dx", "dy", "angle", "icon", "esc", "createDataLayers", "PATHWAY_CONFIGS", "GEO_CACHE_SIZE", "OVERFETCH", "_geoCache", "_findCovering", "zoom", "extraKey", "west", "south", "east", "north", "entries", "i", "_storeInCache", "cache", "cachedFetch", "b", "cached", "dw", "dh", "fw", "fs", "fe", "fn", "fetchBbox", "result", "_preloadTimer", "MIN_PRELOAD_ZOOM", "preloadNeighbors", "specs", "vw", "vh", "offsets", "queue", "spec", "cw", "cs", "ce", "cn", "idx", "_next", "q", "loadDataLayers", "dataLayers", "decision", "zoomHint", "callbacks", "MIN_DATA_ZOOM", "live", "allFeatures", "pendingLoads", "totalExpectedLoads", "renderStructures", "pendingPathway", "_checkAllLoaded", "_pathwayLoaded", "hasClusterPlugin", "clusterGroup", "total", "count", "geom", "marker", "clusterIcon", "nextZoom", "geoLayer", "structureIcon", "entry", "_makePathwayOpts", "styleObj", "calcPathwayLength", "totalLength", "haversine", "chooseLoadStrategy", "zoom", "cachedInfo", "enabled", "skipInfoZoom", "SKIP_INFO_ZOOM", "MIN_DATA_ZOOM", "decideSkipInfo", "decideLayerRendering", "decisionsDiffer", "a", "b", "aKeys", "bKeys", "key", "CFG", "initializePathwaysMap", "elementId", "config", "_a", "container", "Sidebar", "titleCase", "esc", "debounce", "getCookie", "STRUCTURE_COLORS", "STRUCTURE_SHAPES", "PATHWAY_COLORS", "API_BASE", "Popover", "map", "baseLayers", "layerControl", "createMap", "createStatsControl", "createLegend", "createKioskControl", "createSidebarToggleControl", "createLocateControl", "structureCountEl", "pathwayCountEl", "totalLengthEl", "zoomHint", "createZoomHint", "PREFS_KEY", "DEFAULT_LAYERS", "_loadPrefs", "saved", "_e", "_savePrefs", "layers", "layerPrefs", "dataLayers", "createDataLayers", "layerNames", "externalConfigs", "externalGroups", "initExternalLayers", "name", "group", "cfg", "getLayerConfig", "lname", "LAYER_INFO_KEY", "extName", "_layerCheckboxes", "_syncSidebarCheckbox", "checked", "btn", "e", "prefs", "LAYER_ICONS", "_buildSidebarLayerToggles", "toggleContainer", "active", "iconSvg", "span", "wrapper", "cb", "cbName", "checkbox", "button", "_loadData", "pathwayCount", "totalLength", "_updateUrl", "center", "zoom", "params", "selId", "newUrl", "_setLayerChip", "count", "chip", "_applyDecisionToToggles", "decision", "info", "infoKey", "isHidden", "_runLoad", "loadDataLayers", "entry", "feature", "data", "calcPathwayLength", "allFeatures", "MIN_DATA_ZOOM", "visibleExternal", "bbox", "bboxParam", "loadExternalLayers", "extCfg", "extEntries", "i", "_pendingSelectId", "_enabledInfoKeys", "set", "enabled", "strategy", "chooseLoadStrategy", "getLastInfo", "fetchMapInfo", "changed", "fresh", "decideLayerRendering", "decisionsDiffer", "debouncedUrlUpdate", "query", "serverSearch", "results", "resetBtn"] } diff --git a/netbox_pathways/static/netbox_pathways/package-lock.json b/netbox_pathways/static/netbox_pathways/package-lock.json index 9ef80d8..b9b39de 100644 --- a/netbox_pathways/static/netbox_pathways/package-lock.json +++ b/netbox_pathways/static/netbox_pathways/package-lock.json @@ -804,9 +804,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -824,9 +821,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -844,9 +838,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -864,9 +855,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -884,9 +872,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -904,9 +889,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2064,9 +2046,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2088,9 +2067,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2112,9 +2088,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2136,9 +2109,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/netbox_pathways/static/netbox_pathways/src/data-layers.ts b/netbox_pathways/static/netbox_pathways/src/data-layers.ts index dc94dbe..1da0cc7 100644 --- a/netbox_pathways/static/netbox_pathways/src/data-layers.ts +++ b/netbox_pathways/static/netbox_pathways/src/data-layers.ts @@ -28,6 +28,15 @@ export { API_BASE }; export const MIN_DATA_ZOOM = 11; +/** + * Zoom at or above which `/info` is skipped entirely. The viewport at this + * zoom is small enough that hide/cluster thresholds are effectively + * unreachable, so blocking the render on an `/info` round-trip just adds + * latency. Overridable by server config (`PATHWAYS_CONFIG.skipInfoZoom`) + * and by callers of `chooseLoadStrategy` for tests. + */ +export const SKIP_INFO_ZOOM: number = CFG.skipInfoZoom ?? 17; + // --------------------------------------------------------------------------- // /info endpoint: per-layer counts + thresholds for the current viewport // --------------------------------------------------------------------------- @@ -125,6 +134,22 @@ export function decideLayerRendering(info: MapInfo, enabled: Set): Rende return { clusterMode, layers }; } +/** + * Synthetic "all enabled keys render" decision for the skip-info band. + * + * At these zooms we deliberately skip `/info`, so we have no counts. + * The premise is that any viewport at that zoom holds too few features to + * cross hide/cluster thresholds in practice; if a deployment hits that + * edge case it can raise `PATHWAYS_CONFIG.skipInfoZoom`. + */ +export function decideSkipInfo(enabled: Set): RenderingDecision { + const layers: Record = {}; + for (const key of enabled) { + layers[key] = 'render'; + } + return { clusterMode: 'off', layers }; +} + // --------------------------------------------------------------------------- // /info fetch helper // --------------------------------------------------------------------------- @@ -133,9 +158,30 @@ let _infoController: AbortController | null = null; let _lastInfoEtag = ''; let _lastInfo: MapInfo | null = null; +/** Returns the most recent /info response, or null if none cached yet. */ +export function getLastInfo(): MapInfo | null { + return _lastInfo; +} + +/** Test-only: clear cached /info state so each test starts deterministically. */ +export function _resetInfoCache(): void { + if (_infoController) _infoController.abort(); + _infoController = null; + _lastInfoEtag = ''; + _lastInfo = null; +} + +/** + * Fetch `/info` with conditional revalidation. + * + * `callback(info, changed)` -- `changed` is `true` when the server returned + * fresh data (200) and `false` when it returned 304 Not Modified, in which + * case `info` is the cached value. Callers can short-circuit on `!changed` + * to avoid re-rendering when the previous decision is still valid. + */ export async function fetchMapInfo( bbox: string, - callback: (info: MapInfo) => void, + callback: (info: MapInfo, changed: boolean) => void, ): Promise { if (_infoController) _infoController.abort(); const controller = new AbortController(); @@ -151,14 +197,14 @@ export async function fetchMapInfo( const response = await fetch(url, { headers, signal: controller.signal }); if (_infoController === controller) _infoController = null; if (response.status === 304 && _lastInfo) { - callback(_lastInfo); + callback(_lastInfo, false); return; } if (response.ok) { const data = await response.json() as MapInfo; _lastInfoEtag = response.headers.get('ETag') || ''; _lastInfo = data; - callback(data); + callback(data, true); } } catch { if (_infoController === controller) _infoController = null; diff --git a/netbox_pathways/static/netbox_pathways/src/fetch-info.test.ts b/netbox_pathways/static/netbox_pathways/src/fetch-info.test.ts new file mode 100644 index 0000000..2e06a16 --- /dev/null +++ b/netbox_pathways/static/netbox_pathways/src/fetch-info.test.ts @@ -0,0 +1,102 @@ +/** + * Tests for `fetchMapInfo`'s callback contract. + * + * Callers need to distinguish "new info" (200 response, may change render + * decision) from "unchanged" (304 Not Modified, cached info is still valid) + * so they can avoid a needless re-render. The flag must be carried on the + * callback so the cost of revalidation drops to a single round-trip plus + * no DOM churn when nothing changed -- which is the common case during a + * gentle pan. + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { fetchMapInfo, _resetInfoCache } from './data-layers'; +import type { MapInfo } from './data-layers'; + +function makeInfoResponse(): MapInfo { + return { + bbox: null, + counts: { + structures: 50, + conduit_banks: 0, + conduits: 0, + aerial_spans: 0, + direct_buried: 0, + circuits: 0, + }, + thresholds: { + structures: { cluster: 200, hide: 5000 }, + conduit_banks: { hide: 500 }, + conduits: { hide: 500 }, + aerial_spans: { hide: 500 }, + direct_buried: { hide: 500 }, + circuits: { hide: 500 }, + }, + }; +} + +describe('fetchMapInfo signals changed vs unchanged', () => { + let fetchSpy: ReturnType; + + beforeEach(() => { + _resetInfoCache(); + fetchSpy = vi.spyOn(globalThis, 'fetch'); + }); + + afterEach(() => { + fetchSpy.mockRestore(); + }); + + it('calls callback with changed=true on a 200 response with fresh data', async () => { + const body = makeInfoResponse(); + fetchSpy.mockResolvedValueOnce( + new Response(JSON.stringify(body), { + status: 200, + headers: { 'Content-Type': 'application/json', ETag: '"abc"' }, + }), + ); + const cb = vi.fn(); + await fetchMapInfo('1,2,3,4', cb); + expect(cb).toHaveBeenCalledTimes(1); + const [info, changed] = cb.mock.calls[0]; + expect(changed).toBe(true); + expect(info.counts.structures).toBe(50); + }); + + it('calls callback with changed=false on a 304 response, using cached info', async () => { + // First call: 200 populates the cache. + fetchSpy.mockResolvedValueOnce( + new Response(JSON.stringify(makeInfoResponse()), { + status: 200, + headers: { 'Content-Type': 'application/json', ETag: '"abc"' }, + }), + ); + await fetchMapInfo('1,2,3,4', vi.fn()); + + // Second call: 304 must reuse the cached MapInfo and signal unchanged. + fetchSpy.mockResolvedValueOnce(new Response(null, { status: 304 })); + const cb = vi.fn(); + await fetchMapInfo('1,2,3,4', cb); + expect(cb).toHaveBeenCalledTimes(1); + const [info, changed] = cb.mock.calls[0]; + expect(changed).toBe(false); + expect(info.counts.structures).toBe(50); + }); + + it('sends If-None-Match on the second call so 304 is reachable', async () => { + fetchSpy.mockResolvedValueOnce( + new Response(JSON.stringify(makeInfoResponse()), { + status: 200, + headers: { 'Content-Type': 'application/json', ETag: '"abc"' }, + }), + ); + await fetchMapInfo('1,2,3,4', vi.fn()); + + fetchSpy.mockResolvedValueOnce(new Response(null, { status: 304 })); + await fetchMapInfo('1,2,3,4', vi.fn()); + + const [, init] = fetchSpy.mock.calls[1]; + const headers = (init as RequestInit | undefined)?.headers as Record | undefined; + expect(headers && headers['If-None-Match']).toBe('"abc"'); + }); +}); diff --git a/netbox_pathways/static/netbox_pathways/src/load-strategy.test.ts b/netbox_pathways/static/netbox_pathways/src/load-strategy.test.ts new file mode 100644 index 0000000..80a97a5 --- /dev/null +++ b/netbox_pathways/static/netbox_pathways/src/load-strategy.test.ts @@ -0,0 +1,166 @@ +/** + * Tests for the load-strategy decision the map uses on every pan/zoom. + * + * The strategy splits the zoom axis into three bands: + * + * zoom < MIN_DATA_ZOOM -> render nothing + * MIN_DATA_ZOOM <= z < SKIP_INFO -> use cached /info optimistically if + * any, else gated /info round-trip + * zoom >= SKIP_INFO_ZOOM -> skip /info entirely + * + * The whole point is to avoid blocking on /info when the viewport is small + * enough that thresholds are unreachable, or when a recent /info result is + * already cached. The revalidation that runs in the background reconciles + * the optimistic render if counts shifted enough to flip a decision. + */ + +import { describe, it, expect } from 'vitest'; +import { + chooseLoadStrategy, + decideSkipInfo, + decisionsDiffer, + MIN_DATA_ZOOM, + SKIP_INFO_ZOOM, +} from './load-strategy'; +import type { MapInfo, RenderingDecision } from './data-layers'; + +function makeInfo(overrides: Partial = {}): MapInfo { + return { + bbox: null, + counts: { + structures: 50, + conduit_banks: 10, + conduits: 20, + aerial_spans: 5, + direct_buried: 0, + circuits: 0, + }, + thresholds: { + structures: { cluster: 200, hide: 5000 }, + conduit_banks: { hide: 500 }, + conduits: { hide: 500 }, + aerial_spans: { hide: 500 }, + direct_buried: { hide: 500 }, + circuits: { hide: 500 }, + }, + ...overrides, + }; +} + +describe('chooseLoadStrategy', () => { + const enabled = new Set(['structures', 'conduits', 'aerial_spans']); + + it('below MIN_DATA_ZOOM, returns below-min-zoom strategy', () => { + const s = chooseLoadStrategy(MIN_DATA_ZOOM - 1, null, enabled); + expect(s.kind).toBe('below-min-zoom'); + }); + + it('at or above SKIP_INFO_ZOOM, skips /info entirely', () => { + const s = chooseLoadStrategy(SKIP_INFO_ZOOM, null, enabled); + expect(s.kind).toBe('skip-info'); + // No info round-trip needed; caller renders the skip-info decision now. + if (s.kind !== 'skip-info') throw new Error('strategy kind regression'); + expect(s.decision.clusterMode).toBe('off'); + expect(s.decision.layers.structures).toBe('render'); + expect(s.decision.layers.conduits).toBe('render'); + }); + + it('above SKIP_INFO_ZOOM, even a stale cached info does not gate rendering', () => { + // Cached info from a denser, lower-zoom viewport saying "hide everything" + // would otherwise suppress; the skip-info band overrides because the + // small viewport at high zoom cannot plausibly carry that many features. + const cachedDense = makeInfo({ + counts: { + structures: 10000, + conduit_banks: 10, + conduits: 20, + aerial_spans: 5, + direct_buried: 0, + circuits: 0, + }, + }); + const s = chooseLoadStrategy(SKIP_INFO_ZOOM + 2, cachedDense, enabled); + expect(s.kind).toBe('skip-info'); + if (s.kind !== 'skip-info') throw new Error('strategy kind regression'); + expect(s.decision.clusterMode).toBe('off'); + expect(s.decision.layers.structures).toBe('render'); + expect(s.decision.layers.conduits).toBe('render'); + }); + + it('inside the gated band with a cached info, returns optimistic + revalidate', () => { + const cached = makeInfo(); + const s = chooseLoadStrategy(13, cached, enabled); + expect(s.kind).toBe('optimistic'); + if (s.kind !== 'optimistic') throw new Error('strategy kind regression'); + expect(s.cachedInfo).toBe(cached); + expect(s.revalidate).toBe(true); + expect(s.decision.layers.structures).toBe('render'); + }); + + it('inside the gated band without a cached info, returns gated', () => { + const s = chooseLoadStrategy(13, null, enabled); + expect(s.kind).toBe('gated'); + }); + + it('lets the caller override SKIP_INFO_ZOOM (server-side config)', () => { + // If a deployment configures map_skip_info_zoom: 19, zoom 17 is no + // longer in the skip-info band -- gated/optimistic flow applies. + const s = chooseLoadStrategy(17, null, enabled, 19); + expect(s.kind).toBe('gated'); + const s2 = chooseLoadStrategy(19, null, enabled, 19); + expect(s2.kind).toBe('skip-info'); + }); +}); + +describe('decideSkipInfo', () => { + it('renders every enabled native key and no others', () => { + const d: RenderingDecision = decideSkipInfo(new Set(['structures', 'conduits'])); + expect(d.clusterMode).toBe('off'); + expect(d.layers.structures).toBe('render'); + expect(d.layers.conduits).toBe('render'); + expect(d.layers.conduit_banks).toBeUndefined(); + expect(d.layers.aerial_spans).toBeUndefined(); + }); + + it('passes external layer keys through unchanged', () => { + const d = decideSkipInfo(new Set(['structures', 'external:splices'])); + expect(d.layers['external:splices']).toBe('render'); + }); +}); + +describe('decisionsDiffer', () => { + const base: RenderingDecision = { + clusterMode: 'off', + layers: { structures: 'render', conduits: 'render' }, + }; + + it('returns false for identical decisions', () => { + const a = { ...base, layers: { ...base.layers } }; + const b = { ...base, layers: { ...base.layers } }; + expect(decisionsDiffer(a, b)).toBe(false); + }); + + it('returns true when cluster mode flips', () => { + const a = base; + const b: RenderingDecision = { ...base, clusterMode: 'client' }; + expect(decisionsDiffer(a, b)).toBe(true); + }); + + it('returns true when a layer flips render -> hide', () => { + const a = base; + const b: RenderingDecision = { + clusterMode: 'off', + layers: { structures: 'render', conduits: 'hide' }, + }; + expect(decisionsDiffer(a, b)).toBe(true); + }); + + it('returns true when the set of layers changes', () => { + const a = base; + const b: RenderingDecision = { + clusterMode: 'off', + layers: { structures: 'render', conduits: 'render', aerial_spans: 'render' }, + }; + expect(decisionsDiffer(a, b)).toBe(true); + }); +}); diff --git a/netbox_pathways/static/netbox_pathways/src/load-strategy.ts b/netbox_pathways/static/netbox_pathways/src/load-strategy.ts new file mode 100644 index 0000000..e03350d --- /dev/null +++ b/netbox_pathways/static/netbox_pathways/src/load-strategy.ts @@ -0,0 +1,89 @@ +/** + * Decides how to render the map at a given zoom + cache state. + * + * The map used to round-trip `/info` on every pan/zoom and only start the + * GeoJSON fetches once the response came back. Even with conditional + * revalidation (ETag + 304), that adds one full RTT per move which feels + * noticeably laggy on a slow link. + * + * The strategy here cuts that latency in two ways: + * + * 1. Skip-info band (zoom >= SKIP_INFO_ZOOM): the viewport is small + * enough that hide/cluster thresholds are effectively unreachable. + * Skip `/info` entirely and synthesize a "render every enabled layer" + * decision. + * + * 2. Optimistic + revalidate (MIN_DATA_ZOOM <= zoom < SKIP_INFO_ZOOM + * with a cached `/info`): render the cached decision immediately and + * fire `/info` in the background with `If-None-Match`. A 304 means + * the optimistic render was correct -- nothing to do. A 200 with a + * meaningfully different decision triggers a single reconciliation + * reload. + * + * 3. Cold gated path (no cache): the original "wait for /info, then + * load" behaviour, used only on the very first viewport. + */ + +import { + MIN_DATA_ZOOM, + SKIP_INFO_ZOOM, + decideLayerRendering, + decideSkipInfo, +} from './data-layers'; +import type { MapInfo, RenderingDecision } from './data-layers'; + +export { MIN_DATA_ZOOM, SKIP_INFO_ZOOM, decideSkipInfo }; + +export type LoadStrategy = + | { kind: 'below-min-zoom' } + | { kind: 'skip-info'; decision: RenderingDecision } + | { kind: 'optimistic'; decision: RenderingDecision; revalidate: true; cachedInfo: MapInfo } + | { kind: 'gated' }; + +/** + * Picks the strategy for one pan/zoom event. + * + * `skipInfoZoom` defaults to `SKIP_INFO_ZOOM` so it tracks server config; + * the argument is mainly there for tests. + */ +export function chooseLoadStrategy( + zoom: number, + cachedInfo: MapInfo | null, + enabled: Set, + skipInfoZoom: number = SKIP_INFO_ZOOM, +): LoadStrategy { + if (zoom < MIN_DATA_ZOOM) { + return { kind: 'below-min-zoom' }; + } + if (zoom >= skipInfoZoom) { + return { kind: 'skip-info', decision: decideSkipInfo(enabled) }; + } + if (cachedInfo) { + return { + kind: 'optimistic', + decision: decideLayerRendering(cachedInfo, enabled), + revalidate: true, + cachedInfo, + }; + } + return { kind: 'gated' }; +} + +/** + * True when two decisions would render differently. + * + * Used by the optimistic path: after revalidation returns a fresh /info, + * we only re-run the (expensive) GeoJSON fetches if the new decision flips + * a layer's visibility or the cluster mode. Identical decisions short- + * circuit to a chip refresh. + */ +export function decisionsDiffer(a: RenderingDecision, b: RenderingDecision): boolean { + if (a.clusterMode !== b.clusterMode) return true; + const aKeys = Object.keys(a.layers); + const bKeys = Object.keys(b.layers); + if (aKeys.length !== bKeys.length) return true; + for (const key of aKeys) { + if (a.layers[key] !== b.layers[key]) return true; + } + return false; +} diff --git a/netbox_pathways/static/netbox_pathways/src/pathways-map.ts b/netbox_pathways/static/netbox_pathways/src/pathways-map.ts index 91ca491..14a6649 100644 --- a/netbox_pathways/static/netbox_pathways/src/pathways-map.ts +++ b/netbox_pathways/static/netbox_pathways/src/pathways-map.ts @@ -48,8 +48,10 @@ import { serverSearch, fetchMapInfo, decideLayerRendering, + getLastInfo, } from './data-layers'; import type { MapInfo, RenderingDecision } from './data-layers'; +import { chooseLoadStrategy, decisionsDiffer } from './load-strategy'; // --------------------------------------------------------------------------- // Config @@ -419,15 +421,48 @@ function initializePathwaysMap(elementId: string, config: MapInitConfig): void { function _loadData(): void { const zoom = map.getZoom(); - if (zoom < MIN_DATA_ZOOM) { - _runLoad(null, null); - return; + const enabled = _enabledInfoKeys(); + const strategy = chooseLoadStrategy(zoom, getLastInfo(), enabled); + + switch (strategy.kind) { + case 'below-min-zoom': + _runLoad(null, null); + return; + + case 'skip-info': + // Skip /info entirely -- viewport is too small to plausibly + // cross any hide/cluster threshold. Render now, no round-trip. + _runLoad(strategy.decision, null); + return; + + case 'optimistic': { + // Render the cached decision immediately, then revalidate /info + // in the background. A 304 leaves the screen untouched. A 200 + // with a materially different decision triggers a reconcile. + _runLoad(strategy.decision, strategy.cachedInfo); + const bbox = _bboxParam(map); + fetchMapInfo(bbox, function (info: MapInfo, changed: boolean) { + if (!changed) return; + const fresh = decideLayerRendering(info, _enabledInfoKeys()); + if (decisionsDiffer(strategy.decision, fresh)) { + _runLoad(fresh, info); + } else { + _applyDecisionToToggles(fresh, info); + } + }); + return; + } + + case 'gated': { + // First load: no cache yet, so we still wait on /info. + const bbox = _bboxParam(map); + fetchMapInfo(bbox, function (info: MapInfo) { + const decision = decideLayerRendering(info, _enabledInfoKeys()); + _runLoad(decision, info); + }); + return; + } } - const bbox = _bboxParam(map); - fetchMapInfo(bbox, function (info: MapInfo) { - const decision = decideLayerRendering(info, _enabledInfoKeys()); - _runLoad(decision, info); - }); } // --- URL state management --- diff --git a/netbox_pathways/static/netbox_pathways/src/types/netbox.d.ts b/netbox_pathways/static/netbox_pathways/src/types/netbox.d.ts index 57714a8..d78ce08 100644 --- a/netbox_pathways/static/netbox_pathways/src/types/netbox.d.ts +++ b/netbox_pathways/static/netbox_pathways/src/types/netbox.d.ts @@ -8,6 +8,12 @@ declare global { zoom?: number; minZoom?: number; maxZoom?: number; + /** + * Zoom at or above which the map skips `/info` and renders every + * enabled layer immediately. Defaults to 17 in the client; overridable + * via `PLUGINS_CONFIG['netbox_pathways']['map_skip_info_zoom']`. + */ + skipInfoZoom?: number; overlays?: OverlayConfig[]; baseLayers?: BaseLayerConfig[]; externalLayers?: import('./external').ExternalLayerConfig[]; diff --git a/netbox_pathways/template_content.py b/netbox_pathways/template_content.py index 6b4cc59..eb11872 100644 --- a/netbox_pathways/template_content.py +++ b/netbox_pathways/template_content.py @@ -107,6 +107,7 @@ def head(self): config = { **NetBoxPathwaysConfig._map_config, "maxNativeZoom": plugin_cfg.get("map_max_native_zoom", 19), + "skipInfoZoom": plugin_cfg.get("map_skip_info_zoom", 17), "apiBase": f"{api_base}geo/", "overlays": plugin_cfg.get("map_overlays", []), } diff --git a/netbox_pathways/views.py b/netbox_pathways/views.py index 99969f8..1b2db21 100644 --- a/netbox_pathways/views.py +++ b/netbox_pathways/views.py @@ -1760,6 +1760,7 @@ def get(self, request): pathways_config = { "maxNativeZoom": plugin_cfg.get("map_max_native_zoom", 19), + "skipInfoZoom": plugin_cfg.get("map_skip_info_zoom", 17), "apiBase": geo_base, "overlays": plugin_cfg.get("map_overlays", []), "baseLayers": plugin_cfg.get("map_base_layers", []),