mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
- i18n scanner: lastError sink, no false sinks on defaults and ternaries - shared src/i18n/marker.js for modules that only mark strings - wind field messages move to wind/fields.js; CITY_POIS imports from cityPois.js - drop review IDs, change history and stale references from comments and docs Co-Authored-By: Claude Opus 5.5 <[email protected]>
200 lines
6.7 KiB
JavaScript
200 lines
6.7 KiB
JavaScript
import {
|
|
UNNAMED_INSTALLATION_FALLBACK,
|
|
UNNAMED_INSTALLATION_LABEL_BY_CLASS,
|
|
} from './installationNames.js';
|
|
|
|
/**
|
|
* Normalize the deliberately small, allow-listed installation context returned
|
|
* by `/api/military-installations`. These are mapped features, not assertions
|
|
* about a facility's capability, occupancy, or operational status.
|
|
*/
|
|
|
|
const CLASS_BY_MILITARY_TAG = {
|
|
airfield: 'airfield',
|
|
naval_base: 'naval_base',
|
|
range: 'range',
|
|
barracks: 'military_land',
|
|
base: 'military_land',
|
|
};
|
|
|
|
/**
|
|
* How an UNNAMED feature reads on the map (owner playtest 2026-08-18: the old
|
|
* fallback surfaced "range (10981656305)" — an OSM primary key shown to a human
|
|
* as if it were a place name).
|
|
*
|
|
* Only the classes CLASS_BY_MILITARY_TAG can actually produce need an entry;
|
|
* anything else title-cases its class, which already reads correctly for plain
|
|
* nouns. The OSM id is not lost — it stays on `id` and on `sources[].id`, where
|
|
* attribution and the details panel read it.
|
|
*/
|
|
// The labels stay English in data (the server provider and AI tools read the
|
|
// records too); the map card and Contacts translate them (installationNames.js).
|
|
const LABEL_BY_CLASS = UNNAMED_INSTALLATION_LABEL_BY_CLASS;
|
|
const UNNAMED_FALLBACK = UNNAMED_INSTALLATION_FALLBACK;
|
|
|
|
const MAX_FOOTPRINT_POINTS = 400;
|
|
|
|
/**
|
|
* Human-readable label for an unnamed feature's class.
|
|
* @param {string} klass Installation class.
|
|
* @returns {string} Display label, never empty.
|
|
*/
|
|
export function humanizeInstallationClass(klass) {
|
|
const key = String(klass || '')
|
|
.trim()
|
|
.toLowerCase();
|
|
if (LABEL_BY_CLASS[key]) return LABEL_BY_CLASS[key];
|
|
const words = key.replaceAll('_', ' ').trim();
|
|
return words
|
|
? `${words[0].toUpperCase()}${words.slice(1)}`
|
|
: UNNAMED_FALLBACK;
|
|
}
|
|
|
|
function finiteLatitude(value) {
|
|
return Number.isFinite(value) && value >= -90 && value <= 90;
|
|
}
|
|
|
|
function finiteLongitude(value) {
|
|
return Number.isFinite(value) && value >= -180 && value <= 180;
|
|
}
|
|
|
|
function pointFrom(element) {
|
|
const lat = Number(element?.lat ?? element?.center?.lat);
|
|
const longitude = Number(element?.lon ?? element?.center?.lon);
|
|
if (finiteLatitude(lat) && finiteLongitude(longitude))
|
|
return { latitude: lat, longitude };
|
|
|
|
// Bounds midpoint fallback (field test 2026-08-28, Warendorf: nothing drawn).
|
|
// The proxy asks for `out center tags geom`, but Overpass takes the LAST
|
|
// geometry mode only — `geom` wins and no `center` is ever emitted. Every
|
|
// way and relation therefore arrived point-less and was dropped here, so the
|
|
// layer rendered nodes and nothing else (San Diego: 149 of 228 kept, all the
|
|
// ways and relations gone; Warendorf has no nodes at all, hence an empty
|
|
// screen over a mapped Bundeswehr barracks). `bounds` accompanies exactly
|
|
// those elements — including relations, which carry no `geometry` — so it
|
|
// recovers all of them without touching the query or the footprint path.
|
|
const bounds = element?.bounds;
|
|
// JSON nulls, booleans, and empty strings must not coerce to a false 0,0 site.
|
|
if (
|
|
![bounds?.minlat, bounds?.minlon, bounds?.maxlat, bounds?.maxlon].every(
|
|
(value) => typeof value === 'number' && Number.isFinite(value),
|
|
)
|
|
)
|
|
return null;
|
|
const south = Number(bounds?.minlat);
|
|
const west = Number(bounds?.minlon);
|
|
const north = Number(bounds?.maxlat);
|
|
const east = Number(bounds?.maxlon);
|
|
// Ordering matters as much as range: an inverted or antimeridian-spanning
|
|
// box midpoints to a plausible-looking point in the wrong ocean, so match
|
|
// isValidInstallationBoundingBox and refuse it rather than average it.
|
|
// A single installation footprint is never wider than the request bbox cap
|
|
// (10°): an ascending box that spans the antimeridian (minlon -179, maxlon
|
|
// 179) passes the ordering check yet midpoints to longitude 0.
|
|
if (
|
|
finiteLatitude(south) &&
|
|
finiteLatitude(north) &&
|
|
finiteLongitude(west) &&
|
|
finiteLongitude(east) &&
|
|
south <= north &&
|
|
west <= east &&
|
|
north - south <= 10 &&
|
|
east - west <= 10
|
|
) {
|
|
return { latitude: (south + north) / 2, longitude: (west + east) / 2 };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function footprintFrom(element) {
|
|
if (
|
|
!Array.isArray(element?.geometry) ||
|
|
element.geometry.length < 3 ||
|
|
element.geometry.length > MAX_FOOTPRINT_POINTS
|
|
)
|
|
return null;
|
|
const points = [];
|
|
for (const point of element.geometry) {
|
|
const latitude = Number(point?.lat);
|
|
const longitude = Number(point?.lon);
|
|
if (!finiteLatitude(latitude) || !finiteLongitude(longitude)) return null;
|
|
points.push([longitude, latitude]);
|
|
}
|
|
return points;
|
|
}
|
|
|
|
/**
|
|
* Convert a safe, server-filtered Overpass payload into display records.
|
|
* @param {{elements?: Array}} payload
|
|
* @param {string} [retrievedAt]
|
|
* @returns {{records: Array, droppedCount: number}}
|
|
*/
|
|
export function normalizeMilitaryInstallations(
|
|
payload,
|
|
retrievedAt = new Date().toISOString(),
|
|
) {
|
|
const records = [];
|
|
const ids = new Set();
|
|
let droppedCount = 0;
|
|
for (const element of Array.isArray(payload?.elements)
|
|
? payload.elements
|
|
: []) {
|
|
const type = String(element?.type || '');
|
|
const osmId = Number(element?.id);
|
|
if (
|
|
!['node', 'way', 'relation'].includes(type) ||
|
|
!Number.isSafeInteger(osmId)
|
|
) {
|
|
droppedCount += 1;
|
|
continue;
|
|
}
|
|
const id = `osm:${type}:${osmId}`;
|
|
const tags = element?.tags || {};
|
|
const klass =
|
|
CLASS_BY_MILITARY_TAG[String(tags.military || '').toLowerCase()] ||
|
|
(tags.landuse === 'military' ? 'military_land' : null);
|
|
const point = pointFrom(element);
|
|
if (!klass || !point || ids.has(id)) {
|
|
droppedCount += 1;
|
|
continue;
|
|
}
|
|
ids.add(id);
|
|
records.push({
|
|
id,
|
|
kind: 'installation',
|
|
// Load-bearing for viewport filtering: a node's centre is its whole
|
|
// geometry, while a footprint-less way/relation has unknown extent.
|
|
osmType: type,
|
|
class: klass,
|
|
name:
|
|
String(tags.name || tags['name:en'] || '').trim() ||
|
|
humanizeInstallationClass(klass),
|
|
...point,
|
|
footprint: footprintFrom(element),
|
|
sources: [{ name: 'OpenStreetMap', id: `${type}/${osmId}`, retrievedAt }],
|
|
validation: 'unreviewed',
|
|
retrievedAt,
|
|
});
|
|
}
|
|
return { records, droppedCount };
|
|
}
|
|
|
|
/** @param {unknown} value @returns {boolean} */
|
|
export function isValidInstallationBoundingBox(value) {
|
|
const box = value || {};
|
|
const south = Number(box.south);
|
|
const west = Number(box.west);
|
|
const north = Number(box.north);
|
|
const east = Number(box.east);
|
|
return (
|
|
finiteLatitude(south) &&
|
|
finiteLatitude(north) &&
|
|
finiteLongitude(west) &&
|
|
finiteLongitude(east) &&
|
|
south < north &&
|
|
west < east &&
|
|
north - south <= 10 &&
|
|
east - west <= 10
|
|
);
|
|
}
|