From a8e5ff939085dc3cd89ded918418f040e60a4163 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 1 Oct 2026 13:34:26 -0700 Subject: [PATCH] refactor: move infrastructure data parsing into a portable source module The datacenter and dam layers parsed their bundled GeoJSON Lines and built analyst records inside the Cesium-backed layer module. Move the data URLs, the line parser, mapAnalystRecord and layerTitle unchanged to src/sources/infrastructureData.js; the layer module imports them and still exports mapAnalystRecord. The consumer-build pin lists the new module. --- scripts/package-boundaries.json | 9 ++- src/data/infrastructure.js | 10 +-- src/data/infrastructure.test.mjs | 1 + src/data/localGeojsonCore.js | 89 ++------------------------ src/sources/infrastructureData.js | 103 ++++++++++++++++++++++++++++++ 5 files changed, 120 insertions(+), 92 deletions(-) create mode 100644 src/sources/infrastructureData.js diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 158624b8a..1b7e90f56 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -10,7 +10,8 @@ "src/data/localGeojsonCore.js", "src/data/localGeojsonLod.js", "src/data/inputOwnership.js", - "src/data/infrastructureOverlayEntry.js" + "src/data/infrastructureOverlayEntry.js", + "src/sources/infrastructureData.js" ], "external": ["cesium"] }, @@ -2034,7 +2035,8 @@ "src/sources/requestDeadline.js", "src/data/local_data/osm_military_names/names.json", "src/layers/installations/namedMarkers.js", - "src/data/infrastructureOverlayEntry.js" + "src/data/infrastructureOverlayEntry.js", + "src/sources/infrastructureData.js" ], "external": [ "hls.js", @@ -2520,7 +2522,8 @@ "src/sources/requestDeadline.js", "src/data/local_data/osm_military_names/names.json", "src/layers/installations/namedMarkers.js", - "src/data/infrastructureOverlayEntry.js" + "src/data/infrastructureOverlayEntry.js", + "src/sources/infrastructureData.js" ], "external": [ "hls.js", diff --git a/src/data/infrastructure.js b/src/data/infrastructure.js index 59cc09893..ea78494c8 100644 --- a/src/data/infrastructure.js +++ b/src/data/infrastructure.js @@ -1,12 +1,8 @@ import { createLocalGeoJsonLayer } from './localGeojsonCore.js'; +import { INFRASTRUCTURE_DATA_URLS } from '../sources/infrastructureData.js'; -// Resolved by Vite in builds and relative to this module in other consumers. -const datacentersUrl = new URL( - './local_data/datacenters/datacenters.geojsonl', - import.meta.url, -).href; -const damsUrl = new URL('./local_data/dams/dams.geojsonl', import.meta.url) - .href; +const datacentersUrl = INFRASTRUCTURE_DATA_URLS['local-datacenters']; +const damsUrl = INFRASTRUCTURE_DATA_URLS['local-dams']; /** * Create fresh datacenter and dam layers without starting or loading them. diff --git a/src/data/infrastructure.test.mjs b/src/data/infrastructure.test.mjs index 2baf5e580..45fb3d317 100644 --- a/src/data/infrastructure.test.mjs +++ b/src/data/infrastructure.test.mjs @@ -192,6 +192,7 @@ test('consumer build includes only infrastructure code and resolves assets under const sources = Object.keys(entry.modules).filter((id) => id.endsWith('.js')); assert.deepEqual(sources.map((id) => id.split('/').at(-1)).sort(), [ 'infrastructure.js', + 'infrastructureData.js', 'infrastructureOverlayEntry.js', 'localGeojsonCore.js', 'localGeojsonLod.js', diff --git a/src/data/localGeojsonCore.js b/src/data/localGeojsonCore.js index 834361c8c..1d04fa46c 100644 --- a/src/data/localGeojsonCore.js +++ b/src/data/localGeojsonCore.js @@ -1,4 +1,9 @@ import * as Cesium from 'cesium'; +import { + layerTitle, + mapAnalystRecord, + parseGeojsonLines, +} from '../sources/infrastructureData.js'; import { createInfrastructureOverlayEntry } from './infrastructureOverlayEntry.js'; import { isPointerFree } from './inputOwnership.js'; import { @@ -86,78 +91,6 @@ export function localInfrastructureOverlayCopy(properties, layerId) { return { title, details }; } -/** - * Map one local infrastructure feature to a JSON-safe analyst record - * (analyst query engine seam). Pure — no Cesium types. Missing/unknown - * fields are null, never NaN/undefined. Layer-specific fields that do not - * apply (river/output on datacenters, capacity on dams) stay null so a - * shared compact payload can copy them without inventing values. - * Names are unclamped — overlay cards shorten for paint; queries need the - * full source string ("Usina Hidrelétrica de Itaipu"). - * @param {Object|null|undefined} raw - {id, lat, lon, properties}. - * @param {string} [layerId] Local layer id (`local-datacenters` / `local-dams`). - * @returns {{id: string, name: string|null, lat: number|null, lon: number|null, - * operator: string|null, capacity: string|null, river: string|null, - * output: string|null}} - */ -export function mapAnalystRecord(raw, layerId = '') { - const num = (v) => (Number.isFinite(v) ? v : null); - const text = (v) => { - const t = String(v ?? '').trim(); - return t && t !== 'undefined' && t !== 'null' ? t : null; - }; - const props = - raw?.properties && - typeof raw.properties === 'object' && - !Array.isArray(raw.properties) - ? raw.properties - : {}; - const tags = - props.tags && typeof props.tags === 'object' && !Array.isArray(props.tags) - ? props.tags - : {}; - const name = - text(props.name) || - text(tags.name) || - text(tags['name:en']) || - text(tags.official_name) || - null; - const operator = - text(tags.operator) || - text(props.operator) || - text(tags['operator:short']) || - null; - const capacity = - layerId === 'local-datacenters' - ? text(tags['capacity:it_load']) || - text(tags.it_load) || - text(tags.capacity) || - text(props.capacity) - : null; - const river = - layerId === 'local-dams' - ? text(tags.associated_river) || - text(props.associated_river) || - text(tags.river) || - text(props.river) || - text(tags['river:name']) - : null; - const output = - layerId === 'local-dams' - ? text(props.output) || text(tags['plant:output:electricity']) - : null; - return { - id: name || text(raw?.id) || layerTitle(layerId), - name, - lat: num(raw?.lat), - lon: num(raw?.lon), - operator, - capacity, - river, - output, - }; -} - /** * Produce one normalized-contract input owned by a local infrastructure layer. * The host revalidates the authoritative `source` value while normalizing it. @@ -664,11 +597,7 @@ export function createLocalGeoJsonLayer( } const text = await response.text(); if (_destroyed) return; - const lines = text - .split('\n') - .filter((l) => l.trim().length > 0); - - features = lines.map((line) => JSON.parse(line)); + features = parseGeojsonLines(text); _cachedFeatures = features; } @@ -1360,8 +1289,4 @@ function clampCardLine(value) { return text.length > 48 ? `${text.slice(0, 45)}...` : text; } -function layerTitle(layerId) { - if (layerId === 'local-datacenters') return 'Datacenter'; - if (layerId === 'local-dams') return 'Dam'; - return 'Feature'; -} +export { mapAnalystRecord }; diff --git a/src/sources/infrastructureData.js b/src/sources/infrastructureData.js new file mode 100644 index 000000000..9c19fdc87 --- /dev/null +++ b/src/sources/infrastructureData.js @@ -0,0 +1,103 @@ +/** + * Bundled OpenStreetMap infrastructure layers (datacenters and dams): their + * data files, line-delimited GeoJSON parsing and analyst records. + */ + +// Resolved by Vite in builds and relative to this module in other consumers. +export const INFRASTRUCTURE_DATA_URLS = Object.freeze({ + 'local-datacenters': new URL( + '../data/local_data/datacenters/datacenters.geojsonl', + import.meta.url, + ).href, + 'local-dams': new URL( + '../data/local_data/dams/dams.geojsonl', + import.meta.url, + ).href, +}); + +/** Parse GeoJSON Lines: one Feature per non-empty line. */ +export function parseGeojsonLines(text) { + return String(text) + .split('\n') + .filter((line) => line.trim().length > 0) + .map((line) => JSON.parse(line)); +} + +/** The singular label for a local infrastructure layer. */ +export function layerTitle(layerId) { + if (layerId === 'local-datacenters') return 'Datacenter'; + if (layerId === 'local-dams') return 'Dam'; + return 'Feature'; +} + +/** + * Map one local infrastructure feature to a JSON-safe analyst record + * (analyst query engine seam). Pure — no Cesium types. Missing/unknown + * fields are null, never NaN/undefined. Layer-specific fields that do not + * apply (river/output on datacenters, capacity on dams) stay null so a + * shared compact payload can copy them without inventing values. + * Names are unclamped — overlay cards shorten for paint; queries need the + * full source string ("Usina Hidrelétrica de Itaipu"). + * @param {Object|null|undefined} raw - {id, lat, lon, properties}. + * @param {string} [layerId] Local layer id (`local-datacenters` / `local-dams`). + * @returns {{id: string, name: string|null, lat: number|null, lon: number|null, + * operator: string|null, capacity: string|null, river: string|null, + * output: string|null}} + */ +export function mapAnalystRecord(raw, layerId = '') { + const num = (v) => (Number.isFinite(v) ? v : null); + const text = (v) => { + const t = String(v ?? '').trim(); + return t && t !== 'undefined' && t !== 'null' ? t : null; + }; + const props = + raw?.properties && + typeof raw.properties === 'object' && + !Array.isArray(raw.properties) + ? raw.properties + : {}; + const tags = + props.tags && typeof props.tags === 'object' && !Array.isArray(props.tags) + ? props.tags + : {}; + const name = + text(props.name) || + text(tags.name) || + text(tags['name:en']) || + text(tags.official_name) || + null; + const operator = + text(tags.operator) || + text(props.operator) || + text(tags['operator:short']) || + null; + const capacity = + layerId === 'local-datacenters' + ? text(tags['capacity:it_load']) || + text(tags.it_load) || + text(tags.capacity) || + text(props.capacity) + : null; + const river = + layerId === 'local-dams' + ? text(tags.associated_river) || + text(props.associated_river) || + text(tags.river) || + text(props.river) || + text(tags['river:name']) + : null; + const output = + layerId === 'local-dams' + ? text(props.output) || text(tags['plant:output:electricity']) + : null; + return { + id: name || text(raw?.id) || layerTitle(layerId), + name, + lat: num(raw?.lat), + lon: num(raw?.lon), + operator, + capacity, + river, + output, + }; +}