Files
gods-eye-view/src/sources/infrastructureData.js
T
Sameh Khamis f752b5eea1 fix: resolve bundled data URLs when used, not at import
The submarine cable source and the infrastructure data URLs built their
asset URLs from import.meta.url while the module loaded. Importing the
tool services therefore failed in runtimes without a usable module URL,
even for tools that never read those files. Build the URLs when they are
read; Vite still rewrites them in browser builds.
2026-10-03 18:47:31 -07:00

167 lines
5.4 KiB
JavaScript

/**
* 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,
// when read, so importing this module never needs a module URL.
export const INFRASTRUCTURE_DATA_URLS = Object.freeze({
get 'local-datacenters'() {
return new URL(
'../data/local_data/datacenters/datacenters.geojsonl',
import.meta.url,
).href;
},
get 'local-dams'() {
return 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,
};
}
/** A representative point for a feature: its point, or its outline's average vertex. */
export function featurePoint(feature) {
const geometry = feature?.geometry;
const outline =
geometry?.type === 'Point'
? [geometry.coordinates]
: geometry?.type === 'LineString'
? geometry.coordinates
: geometry?.type === 'Polygon'
? geometry.coordinates?.[0]
: geometry?.type === 'MultiPolygon'
? geometry.coordinates?.[0]?.[0]
: null;
const points = (outline || []).filter(
(point) => Number.isFinite(point?.[0]) && Number.isFinite(point?.[1]),
);
if (!points.length) return null;
return {
lon: points.reduce((sum, point) => sum + point[0], 0) / points.length,
lat: points.reduce((sum, point) => sum + point[1], 0) / points.length,
};
}
/** Read the bundled layers' analyst records through a supplied transport. */
export function createInfrastructureSource({
fetchImpl = (...args) => globalThis.fetch(...args),
} = {}) {
const cache = new Map();
return {
async getRecords(layerId, { signal } = {}) {
const url = INFRASTRUCTURE_DATA_URLS[layerId];
if (!url) throw new TypeError(`Unknown infrastructure layer: ${layerId}`);
if (!cache.has(layerId)) {
signal?.throwIfAborted();
const response = await fetchImpl(url, { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const records = parseGeojsonLines(await response.text()).flatMap(
(feature) => {
const point = featurePoint(feature);
return point
? [
mapAnalystRecord(
{
id: feature.properties?.osm_id ?? feature.id,
...point,
properties: feature.properties,
},
layerId,
),
]
: [];
},
);
cache.set(layerId, records);
}
return cache.get(layerId);
},
};
}