Merge remote-tracking branch 'upstream/main' into q690

# Conflicts:
#	CHANGELOG.md
#	scripts/format-scope.json
This commit is contained in:
daikaginza committed 2026-10-05 17:30:28 +07:00
commit 1b58fda366
401 files changed
+47310 -3614

No files matched your search

+803
View File
@@ -0,0 +1,803 @@
/**
* Bundled administrative boundaries — countries and states/provinces (Natural
* Earth admin-0/admin-1) and US counties (US Census Bureau cartographic boundaries),
* so "outline Texas", "outline Bavaria" or "outline Travis County" draws the
* real boundary without a network lookup.
*
* Data: `local_data/natural_earth/{countries,states_provinces}.json` and
* `local_data/us_census_counties/counties.json`, public domain and built
* by `scripts/build-admin-packs.mjs` (provenance in each folder's README).
* Rings are delta-encoded integers; see `decodeRing`.
*
* PURE data module — no Cesium, node-testable. Each pack is fetched through
* `loadBundledJson` on the first lookup that needs it (never at app start),
* indexed once, and kept; a failed load is retried later rather than cached
* (`createRetryableLoader`). The county pack is only loaded for asks that
* name a county (or a county-equivalent city) or that a geocoder typed as one.
*
* Two entry points:
* - `findAdminArea(query, { near })` — name-only. Resolves asks that are
* unambiguous from the words alone ("Texas", "TX", "State of Texas",
* "Bavaria", "Travis County, Texas", "Orleans Parish"); duplicates are
* settled by a state/country qualifier, then by the camera (`near`).
* Country/state homonyms such as Georgia use the qualifier or camera;
* city/state homonyms such as New York still defer to the geocoder.
* - `findAdminAreaAt(names, lat, lon, scope)` — geocoder-confirmed. The
* geocoder already typed the place as a country, state or county; the unit must
* carry one of the names AND contain the geocoded point.
*/
import { loadBundledJson } from './bundledJson.js';
import { createRetryableLoader } from './retryableLoad.js';
const FOLD = {
ß: 'ss',
ø: 'o',
æ: 'ae',
ł: 'l',
đ: 'd',
ı: 'i',
œ: 'oe',
þ: 'th',
};
/** Fold a name for matching: case, accents, punctuation, "the", "saint". */
export function normalizeAdminName(value) {
return String(value || '')
.toLowerCase()
.replace(/[ßøæłđıœþ]/g, (c) => FOLD[c])
.normalize('NFD')
.replace(/\p{M}/gu, '')
.replace(/[^a-z0-9]+/g, ' ')
.trim()
.replace(/^the /, '')
.replace(/\bsaint\b/g, 'st')
.replace(/\bsainte\b/g, 'ste');
}
const PACKS = {
countries: {
url: new URL('./local_data/natural_earth/countries.json', import.meta.url),
},
admin1: {
url: new URL(
'./local_data/natural_earth/states_provinces.json',
import.meta.url,
),
},
counties: {
url: new URL(
'./local_data/us_census_counties/counties.json',
import.meta.url,
),
},
};
/** Test seam: count pack loads (lazy-loading contract). */
export const packLoads = { admin1: 0, counties: 0, countries: 0 };
const EARTH_RADIUS_KM = 6371;
const toRad = (d) => (d * Math.PI) / 180;
/** Spherical ring area (km²) — same formula as naturalEarthRegions.js. */
function ringAreaKm2(ring) {
const n = ring.length;
if (n < 3) return 0;
let sum = 0;
for (let i = 0; i < n; i++) {
const [lon1, lat1] = ring[i];
const [lon2, lat2] = ring[(i + 1) % n];
sum +=
toRad(lon2 - lon1) * (2 + Math.sin(toRad(lat1)) + Math.sin(toRad(lat2)));
}
return Math.abs((sum * EARTH_RADIUS_KM * EARTH_RADIUS_KM) / 2);
}
function haversineKm(lat1, lon1, lat2, lon2) {
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const h =
Math.sin(dLat / 2) ** 2 +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
return 2 * EARTH_RADIUS_KM * Math.asin(Math.min(1, Math.sqrt(h)));
}
/**
* Decode one packed ring: integers in units of 10^-decimals degrees, the
* first vertex absolute and each later one a delta from the previous.
* @param {number[]} encoded
* @param {number} decimals
* @returns {Array<[number, number]>} open ring of [lon, lat]
*/
export function decodeRing(encoded, decimals) {
const factor = 10 ** decimals;
const ring = [];
let x = 0;
let y = 0;
for (let i = 0; i + 1 < encoded.length; i += 2) {
x += encoded[i];
y += encoded[i + 1];
ring.push([x / factor, y / factor]);
}
return ring;
}
/** Even-odd point-in-ring test; ring is [[lon, lat], …], open or closed. */
export function pointInRing(ring, lat, lon) {
if (!Array.isArray(ring) || ring.length < 3) return false;
let inside = false;
for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {
const [xi, yi] = ring[i];
const [xj, yj] = ring[j];
if (
yi > lat !== yj > lat &&
lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi
)
inside = !inside;
}
return inside;
}
/**
* Whether polygons ([outer, ...holes][]) contain the point: inside an outer
* ring and not inside any of that part's holes.
*/
export function polygonsContain(polygons, lat, lon) {
for (const [outer, ...holes] of polygons || []) {
if (!pointInRing(outer, lat, lon)) continue;
if (!holes.some((hole) => pointInRing(hole, lat, lon))) return true;
}
return false;
}
/**
* A unit's geometry, decoded on first use. Longitudes are unwrapped around
* the largest part for the box so units cut at the antimeridian (Alaska,
* Chukotka) get a box on the right side of the globe.
*/
function geometryOf(entry) {
if (entry.geometry) return entry.geometry;
const decimals = entry.feature.d ?? entry.decimals;
const polygons = entry.feature.polygons.map((poly) =>
poly.map((ring) => decodeRing(ring, decimals)),
);
const ref = polygons[0]?.[0]?.[0]?.[0] ?? 0;
let west = Infinity;
let south = Infinity;
let east = -Infinity;
let north = -Infinity;
let areaKm2 = 0;
for (const [outer, ...holes] of polygons) {
areaKm2 += ringAreaKm2(outer);
for (const hole of holes) areaKm2 -= ringAreaKm2(hole);
for (const [lon, lat] of outer) {
const x = lon - 360 * Math.round((lon - ref) / 360);
if (x < west) west = x;
if (x > east) east = x;
if (lat < south) south = lat;
if (lat > north) north = lat;
}
}
const label = entry.feature.label
? { lon: entry.feature.label[0], lat: entry.feature.label[1] }
: ringCentroid(polygons[0][0]);
entry.geometry = {
polygons,
bbox: [west, south, east, north],
areaKm2: Math.max(0, areaKm2),
label,
};
return entry.geometry;
}
/** Area-weighted centroid of an open ring (planar; fine at unit scale). */
function ringCentroid(ring) {
let a = 0;
let cx = 0;
let cy = 0;
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
const [x0, y0] = ring[j];
const [x1, y1] = ring[i];
const f = x0 * y1 - x1 * y0;
a += f;
cx += (x0 + x1) * f;
cy += (y0 + y1) * f;
}
if (!a) return { lon: ring[0][0], lat: ring[0][1] };
return { lon: cx / (3 * a), lat: cy / (3 * a) };
}
/** Km from a point to a unit's box (0 inside it). */
function boxDistanceKm(bbox, lat, lon) {
const [west, south, east, north] = bbox;
const mid = (west + east) / 2;
const x = lon - 360 * Math.round((lon - mid) / 360);
const clampedLon = Math.min(east, Math.max(west, x));
const clampedLat = Math.min(north, Math.max(south, lat));
return haversineKm(lat, x, clampedLat, clampedLon);
}
/**
* Natural Earth `min_label` (the zoom at which a unit is labelled) up to which
* a unit may be named alone: every US state, Canadian province, Mexican and
* German state; not English counties, French départements or small districts.
*/
const BARE_NAME_MAX_RANK = 7;
/**
* Words that name a unit somewhere (Cameroon's "North", Paraguay's
* "Central") but, said alone, name a direction or a feature.
*/
const GENERIC_NAMES = new Set([
'north',
'south',
'east',
'west',
'northern',
'southern',
'eastern',
'western',
'north east',
'north west',
'south east',
'south west',
'northeast',
'northwest',
'southeast',
'southwest',
'far north',
'far west',
'central',
'centre',
'center',
'capital',
'coast',
'island',
'islands',
'lakes',
'highland',
'highlands',
'interior',
'upper',
'lower',
'valley',
'plateau',
'plateaux',
'littoral',
'oriental',
'occidental',
]);
/** State codes that, alone, more often mean a city ("LA", "NY"). */
const CITY_ABBREVIATIONS = new Set(['la', 'ny']);
/** Postal abbreviations accepted alone ("TX", "ON"): US states and Canada. */
const POSTAL_COUNTRIES = new Set(['US', 'CA']);
function addKey(index, key, entry, tier) {
if (!key) return;
entry.keys?.add(key);
const list = index.get(key);
const item = { entry, tier };
if (!list) index.set(key, [item]);
else if (!list.some((i) => i.entry === entry)) list.push(item);
}
const loadAdmin1 = createRetryableLoader(async () => {
packLoads.admin1 += 1;
const pack = await loadBundledJson(PACKS.admin1.url);
const decimals = pack.meta?.decimals ?? 3;
const index = new Map();
for (const feature of pack.features || []) {
const entry = {
kind: 'state',
feature,
decimals,
amb: new Set(feature.amb || []),
ambAbroad: new Set(feature.ambAbroad || []),
keys: new Set(),
country: normalizeAdminName(feature.country),
iso2: String(feature.iso2 || '').toLowerCase(),
};
addKey(index, normalizeAdminName(feature.name), entry, 0);
addKey(index, normalizeAdminName(feature.nameEn), entry, 1);
for (const alt of feature.alt || [])
addKey(index, normalizeAdminName(alt), entry, 2);
if (feature.postal && POSTAL_COUNTRIES.has(feature.iso2))
addKey(index, `postal:${feature.postal.toLowerCase()}`, entry, 2);
}
return { index };
});
const loadCounties = createRetryableLoader(async () => {
packLoads.counties += 1;
const pack = await loadBundledJson(PACKS.counties.url);
const decimals = pack.meta?.decimals ?? 4;
const byFull = new Map();
const byName = new Map();
const states = new Map();
for (const feature of pack.features || []) {
const entry = {
kind: 'county',
feature,
decimals,
state: normalizeAdminName(feature.state),
st: String(feature.st || '').toLowerCase(),
};
addKey(byFull, normalizeAdminName(feature.full), entry, 0);
addKey(byName, normalizeAdminName(feature.name), entry, 1);
states.set(entry.state, entry.st);
}
return { byFull, byName, states };
});
const loadCountries = createRetryableLoader(async () => {
packLoads.countries += 1;
const pack = await loadBundledJson(PACKS.countries.url);
const index = new Map();
for (const feature of pack.features || []) {
const entry = {
kind: 'country',
feature,
decimals: pack.meta.decimals,
keys: new Set(),
country: normalizeAdminName(feature.country),
iso2: feature.iso2?.toLowerCase(),
};
for (const name of [feature.name, ...(feature.alt || [])])
addKey(index, normalizeAdminName(name), entry, 0);
}
return { index };
});
async function countryCandidates(parsed) {
if (!['any', 'country'].includes(parsed.kind)) return [];
const { index } = await loadCountries();
const key = COUNTRY_TARGET_ALIASES.get(parsed.name) || parsed.name;
return (index.get(key) || []).filter(({ entry }) =>
parsed.qualifiers.every((q) => qualifierMatches(entry, q)),
);
}
/** Country target aliases never broaden constituent-country requests. */
const COUNTRY_TARGET_ALIASES = new Map([
['usa', 'united states of america'],
['us', 'united states of america'],
['u s', 'united states of america'],
['u s a', 'united states of america'],
['united states', 'united states of america'],
['america', 'united states of america'],
['uk', 'united kingdom'],
['u k', 'united kingdom'],
['great britain', 'united kingdom'],
['britain', 'united kingdom'],
['russian federation', 'russia'],
['korea', 'south korea'],
['czechia', 'czech republic'],
['tanzania', 'united republic of tanzania'],
['drc', 'democratic republic of the congo'],
]);
const COUNTRY_ALIASES = new Map([
...COUNTRY_TARGET_ALIASES,
['england', 'united kingdom'],
['scotland', 'united kingdom'],
['wales', 'united kingdom'],
['northern ireland', 'united kingdom'],
]);
/** County-equivalent words, longest first so "city and borough" wins. */
const COUNTY_WORDS = [
'city and borough',
'planning region',
'census area',
'municipality',
'municipio',
'borough',
'parish',
'county',
];
const COUNTY_WORD_RE = new RegExp(`\\b(${COUNTY_WORDS.join('|')})\\b`);
const STATE_PREFIX_RE =
/^(?:free state|state|commonwealth|province|territory|prefecture|region|land|canton|department) of (.+)$/;
const STATE_SUFFIX_RE = /^(.+) (?:state|province|prefecture|territory)$/;
/**
* Split an ask into what it names and how.
* @param {string} text
* @returns {{kind:'country'|'county'|'state'|'any', name:string, qualifiers:string[],
* postal:string|null, countyWord:string|null}|null}
*/
export function parseAdminQuery(text) {
const raw = String(text || '').trim();
if (!raw) return null;
const segments = raw.split(',').map(normalizeAdminName).filter(Boolean);
if (!segments.length) return null;
const head = segments[0];
const country =
/^(?:country|nation) of (.+)$/.exec(head) ||
/^(.+) (?:country|nation)$/.exec(head);
if (country)
return {
kind: 'country',
name: country[1],
qualifiers: segments.slice(1),
postal: null,
countyWord: null,
full: head,
};
const qualifiers = segments.slice(1);
const postal = /^[A-Za-z]{2}$/.test(raw) ? raw.toLowerCase() : null;
// "TX", "DC", "D.C.": written as an abbreviation, not a short word.
const abbreviation = /^[A-Z]{2,3}$/.test(raw.replace(/[.\s]/g, ''));
// "County Cork", "County of Los Angeles"
const countyPrefix = /^county (?:of )?(.+)$/.exec(head);
if (countyPrefix)
return {
kind: 'county',
name: countyPrefix[1],
qualifiers,
postal: null,
countyWord: 'county',
full: head,
};
// "Travis County", "Travis County Texas", "Orleans Parish, LA"
const county = COUNTY_WORD_RE.exec(head);
if (county && county.index > 0) {
const name = head.slice(0, county.index).trim();
const rest = head.slice(county.index + county[1].length).trim();
if (rest) qualifiers.unshift(rest);
return {
kind: 'county',
name,
qualifiers,
postal: null,
countyWord: county[1],
full: head.slice(0, county.index + county[1].length),
};
}
const prefix = STATE_PREFIX_RE.exec(head);
if (prefix)
return {
kind: 'state',
name: prefix[1],
qualifiers,
postal: null,
countyWord: null,
full: head,
form: 'prefix',
};
const suffix = STATE_SUFFIX_RE.exec(head);
if (suffix)
return {
kind: 'state',
name: suffix[1],
qualifiers,
postal: null,
countyWord: null,
full: head,
form: 'suffix',
};
return {
kind: 'any',
name: head,
qualifiers,
postal,
abbreviation,
countyWord: null,
};
}
/** A qualifier names the unit's country ("Bavaria, Germany") or the unit
* itself ("Washington, DC"). */
function qualifierMatches(entry, qualifier) {
const q = COUNTRY_ALIASES.get(qualifier) || qualifier;
return (
entry.country === q ||
entry.iso2 === q ||
entry.keys.has(qualifier) ||
entry.keys.has(`postal:${qualifier}`)
);
}
/**
* Rank candidates: a unit containing `near` first, then the match tier,
* then distance from `near`, then larger area. Returns the ranked list.
*/
function rank(candidates, near) {
const hasNear = Number.isFinite(near?.lat) && Number.isFinite(near?.lon);
const scored = candidates.map(({ entry, tier }) => {
const geometry = geometryOf(entry);
const contains = hasNear
? polygonsContain(geometry.polygons, near.lat, near.lon)
: false;
const distanceKm = hasNear
? contains
? 0
: boxDistanceKm(geometry.bbox, near.lat, near.lon)
: 0;
return { entry, tier, contains, distanceKm, areaKm2: geometry.areaKm2 };
});
scored.sort(
(a, b) =>
Number(b.contains) - Number(a.contains) ||
a.tier - b.tier ||
a.distanceKm - b.distanceKm ||
b.areaKm2 - a.areaKm2,
);
return scored;
}
function toResult(scored, candidateCount) {
const { entry } = scored;
const { feature } = entry;
const geometry = geometryOf(entry);
// Parts are stored largest first, so polygons[0][0] is the main outline.
const polygons = geometry.polygons;
if (entry.kind === 'county') {
return {
kind: 'county',
name: feature.full,
region: feature.state,
regionCode: feature.st,
country: 'United States of America',
id: feature.geoid,
source: 'us-census',
polygons,
ring: polygons[0][0],
bbox: geometry.bbox,
areaKm2: geometry.areaKm2,
label: geometry.label,
candidates: candidateCount,
};
}
return {
kind: entry.kind,
name: feature.nameEn || feature.name,
localName: feature.name,
type: feature.type,
region: null,
regionCode: feature.postal || null,
country: feature.country,
id: feature.iso || null,
source: 'natural-earth',
polygons,
ring: polygons[0][0],
bbox: geometry.bbox,
areaKm2: geometry.areaKm2,
label: geometry.label,
candidates: candidateCount,
};
}
async function countyCandidates(parsed) {
const { byFull, byName, states } = await loadCounties();
// An exact full name ("Orleans Parish") wins outright; without one, the
// name with ANY county-equivalent word ("Anchorage Borough" finds
// Anchorage Municipality). An independent city is only ever its full name
// ("Fairfax city", "Carson City").
let list = parsed.countyWord
? [
...(byFull.get(parsed.full || `${parsed.name} ${parsed.countyWord}`) ||
byName.get(parsed.name) ||
[]),
]
: [...(byFull.get(parsed.name) || [])];
if (!list.length) return [];
// Every qualifier must be a state or the US: "Travis County, TX, USA"
// qualifies, "Travis County Courthouse, Texas" names something else.
const stateByCode = new Map([...states].map(([name, st]) => [st, name]));
const stateQualifiers = [];
for (const q of parsed.qualifiers) {
const state = states.has(q) ? q : stateByCode.get(q);
if (state) stateQualifiers.push(state);
else if (!isUnitedStates(q)) return [];
}
if (stateQualifiers.length)
list = list.filter((i) => stateQualifiers.includes(i.entry.state));
return list;
}
function isUnitedStates(qualifier) {
return (
(COUNTRY_ALIASES.get(qualifier) || qualifier) === 'united states of america'
);
}
async function stateCandidates(parsed, { allowAmbiguous = false } = {}) {
const { index } = await loadAdmin1();
const keys = [];
if (parsed.full) keys.push(parsed.full);
keys.push(parsed.name);
if (parsed.postal) keys.push(`postal:${parsed.postal}`);
const seen = new Set();
let list = [];
for (const key of keys) {
for (const item of index.get(key) || []) {
if (seen.has(item.entry)) continue;
seen.add(item.entry);
list.push({ ...item, key, tier: key === parsed.full ? 0 : item.tier });
}
}
// Every qualifier must place the unit: "Bavaria, Germany", "Washington,
// DC" — not "Santa Barbara, California" (a city the pack does not hold).
if (parsed.qualifiers.length)
list = list.filter((i) =>
parsed.qualifiers.every((q) => qualifierMatches(i.entry, q)),
);
if (allowAmbiguous) return list;
// A bare name answers alone only for a prominent unit — a US state, a
// Canadian province, Bavaria — never a French département, an English
// county or a Bahamas district ("Long Island", "Santa Barbara", "Kent" are
// likelier the places a geocoder knows). It must not also be a country or
// a large city ("Georgia", "New York"), and a short code must be written
// as one ("TX", not "tx"). Otherwise a qualifier, admin wording or the
// geocoder decides.
// A country qualifier places a name without making it a state: "New York,
// USA" and "Madrid, Spain" stay the geocoder's; "Washington, DC" names the
// unit itself.
if (parsed.kind === 'any') {
const short = parsed.name.replace(/ /g, '').length <= 3;
if (short && (!parsed.abbreviation || CITY_ABBREVIATIONS.has(parsed.name)))
return [];
const namesUnit = (entry) =>
parsed.qualifiers.some(
(q) => entry.keys.has(q) || entry.keys.has(`postal:${q}`),
);
if (!parsed.qualifiers.length) {
if (GENERIC_NAMES.has(parsed.name)) return [];
return list.filter(
(i) =>
!i.entry.amb.has(i.key) &&
!i.entry.ambAbroad.has(i.key) &&
((i.entry.feature.rank ?? Infinity) <= BARE_NAME_MAX_RANK ||
i.entry.feature.cityState),
);
}
// Qualified: a shared name abroad is settled by the country ("Victoria,
// Australia"); one shared at home only by the unit's own name.
return list.filter((i) => !i.entry.amb.has(i.key) || namesUnit(i.entry));
}
// "X State" only settles a name that is ambiguous on its own ("New York
// State"); otherwise it is as likely a university ("Ohio State") and a
// geocoder decides. A unit literally named so ("Free State") matches.
if (parsed.form === 'suffix')
return list.filter((i) => i.key === parsed.full || i.entry.amb.has(i.key));
return list;
}
/**
* Resolve an administrative unit from the ask's words alone.
*
* @param {string} query e.g. "Texas", "TX", "State of Texas", "Bavaria",
* "Bavaria, Germany", "Travis County", "Travis County, TX", "Orleans Parish"
* @param {{near?: {lat:number, lon:number}|null}} [options] the camera's
* view centre — the tie-break between same-named units
* @returns {Promise<AdminArea|null>}
*
* @typedef {{kind:'country'|'state'|'county', name:string, region:string|null,
* regionCode:string|null, country:string, id:string|null,
* source:'natural-earth'|'us-census',
* polygons:Array<Array<Array<[number,number]>>>, ring:Array<[number,number]>,
* bbox:number[], areaKm2:number, label:{lat:number, lon:number},
* candidates:number}} AdminArea
*/
export async function findAdminArea(query, { near = null } = {}) {
const parsed = parseAdminQuery(query);
if (!parsed || !parsed.name) return null;
let candidates = [];
if (parsed.kind === 'country') {
candidates = await countryCandidates(parsed);
} else if (parsed.kind === 'county') {
// County wording also names admin-1 units abroad. Neither pack gets
// first refusal: qualifiers exclude ineligible countries before ranking.
const [counties, admin1] = await Promise.all([
countyCandidates(parsed),
stateCandidates({ ...parsed, kind: 'state' }),
]);
candidates = [...counties, ...admin1];
} else {
candidates = await stateCandidates(parsed);
if (!candidates.length || COUNTRY_TARGET_ALIASES.has(parsed.name)) {
const countries = await countryCandidates(parsed);
if (countries.length) {
const states = await stateCandidates(parsed, { allowAmbiguous: true });
if (
states.length &&
!Number.isFinite(near?.lat) &&
!parsed.qualifiers.length
)
return null;
candidates = [...countries, ...states];
}
}
// A county-equivalent city: "Baltimore city", "Fairfax city, Virginia".
if (!candidates.length && / city$/.test(parsed.name))
candidates = await countyCandidates(parsed);
}
if (!candidates.length) return null;
const ranked = rank(candidates, near);
return toResult(ranked[0], candidates.length);
}
/**
* Geocoder-confirmed lookup: the geocoder typed a country, state or
* county, so the unit must carry one of `names` AND contain the geocoded
* point. Ambiguity marks do not apply — the geocoder already chose.
*
* @param {string[]} names the ask and the geocoder's own name for it
* @param {number} lat
* @param {number} lon
* @param {'country'|'state'|'county'} scope
* @returns {Promise<AdminArea|null>}
*/
export async function findAdminAreaAt(names, lat, lon, scope) {
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
const parsedList = [...new Set((names || []).filter(Boolean))]
.map(parseAdminQuery)
.filter((p) => p?.name);
if (!parsedList.length) return null;
const containing = (items) => {
const out = [];
for (const item of items)
if (
!out.some((c) => c.entry === item.entry) &&
polygonsContain(geometryOf(item.entry).polygons, lat, lon)
)
out.push(item);
return out;
};
if (scope === 'country') {
const countries = [];
for (const parsed of parsedList)
countries.push(
...(await countryCandidates({
...parsed,
kind: 'country',
qualifiers: [],
})),
);
const found = containing(countries);
return found.length
? toResult(rank(found, { lat, lon })[0], found.length)
: null;
}
// The geocoder's scope says what kind of unit this is; the ask's wording
// may omit it ("Travis" typed as a county). A US county that matches wins
// outright — "New York County" is Manhattan, not the state.
if (scope === 'county' && inUnitedStatesBox(lat, lon)) {
const counties = [];
for (const parsed of parsedList)
counties.push(
...(await countyCandidates({
...parsed,
kind: 'county',
countyWord: parsed.countyWord || 'county',
qualifiers: [],
})),
);
const found = containing(counties);
if (found.length)
return toResult(rank(found, { lat, lon })[0], found.length);
}
const states = [];
for (const parsed of parsedList)
states.push(
...(await stateCandidates(
{ ...parsed, qualifiers: [] },
{ allowAmbiguous: true },
)),
);
const found = containing(states);
if (!found.length) return null;
return toResult(rank(found, { lat, lon })[0], found.length);
}
/** Rough box around the US and its territories (skips the county pack elsewhere). */
function inUnitedStatesBox(lat, lon) {
return (
(lat >= 13 && lat <= 72 && (lon <= -64 || lon >= 144)) ||
(lat >= -15 && lat <= -10 && lon <= -168)
);
}
+481
View File
@@ -0,0 +1,481 @@
// Bundled state/province and US county boundaries: name matching,
// disambiguation, multipolygons and holes, and lazy loading.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, statSync } from 'node:fs';
import {
decodeRing,
findAdminArea,
findAdminAreaAt,
normalizeAdminName,
packLoads,
parseAdminQuery,
polygonsContain,
} from './adminBoundaries.js';
const ADMIN1 = new URL(
'./local_data/natural_earth/states_provinces.json',
import.meta.url,
);
const COUNTIES = new URL(
'./local_data/us_census_counties/counties.json',
import.meta.url,
);
const AUSTIN = { lat: 30.2672, lon: -97.7431 };
const PORTLAND = { lat: 45.5152, lon: -122.6784 };
// Runs first: nothing loads until a lookup needs it, and a state ask never
// pulls the county pack.
test('packs load lazily, once, and only the pack an ask needs', async () => {
assert.deepEqual(packLoads, { admin1: 0, counties: 0, countries: 0 });
assert.equal(parseAdminQuery('Texas').kind, 'any');
assert.deepEqual(
packLoads,
{ admin1: 0, counties: 0, countries: 0 },
'parsing is free',
);
await findAdminArea('Texas');
await findAdminArea('Bavaria');
assert.deepEqual(packLoads, { admin1: 1, counties: 0, countries: 0 });
await findAdminArea('Travis County');
await findAdminArea('Orleans Parish');
assert.deepEqual(packLoads, { admin1: 1, counties: 1, countries: 0 });
});
test('Texas, TX and "State of Texas" name the same unit', async () => {
for (const query of [
'Texas',
'texas',
'TX',
'State of Texas',
'the State of Texas',
'Texas, USA',
'Texas, United States',
]) {
const unit = await findAdminArea(query, { near: AUSTIN });
assert.equal(unit?.id, 'US-TX', query);
assert.equal(unit.name, 'Texas');
assert.equal(unit.kind, 'state');
assert.equal(unit.source, 'natural-earth');
}
const texas = await findAdminArea('Texas');
assert.ok(
texas.areaKm2 > 600_000 && texas.areaKm2 < 750_000,
`Texas area ${texas.areaKm2}`,
);
assert.ok(polygonsContain(texas.polygons, AUSTIN.lat, AUSTIN.lon));
});
test('provinces and states abroad resolve by English and local names', async () => {
for (const query of [
'Bavaria',
'Bayern',
'Bavaria, Germany',
'Free State of Bavaria',
]) {
const unit = await findAdminArea(query);
assert.equal(unit?.id, 'DE-BY', query);
assert.equal(unit.name, 'Bavaria');
assert.equal(unit.country, 'Germany');
}
assert.equal((await findAdminArea('Ontario'))?.id, 'CA-ON');
assert.equal((await findAdminArea('ON'))?.id, 'CA-ON');
assert.equal((await findAdminArea('Province of Ontario'))?.id, 'CA-ON');
assert.equal((await findAdminArea('Free State'))?.country, 'South Africa');
// Same name in two countries: the qualifier decides.
assert.equal((await findAdminArea('Punjab, India'))?.id, 'IN-PB');
assert.equal((await findAdminArea('Punjab, Pakistan'))?.id, 'PK-PB');
// A qualifier may name the unit itself.
assert.equal((await findAdminArea('Washington, DC'))?.id, 'US-DC');
});
test('"Washington" is the state; "Washington County" is a county near the camera', async () => {
const state = await findAdminArea('Washington', { near: AUSTIN });
assert.equal(state?.id, 'US-WA');
const fromAustin = await findAdminArea('Washington County', { near: AUSTIN });
assert.equal(fromAustin.kind, 'county');
assert.equal(fromAustin.name, 'Washington County');
assert.equal(fromAustin.region, 'Texas');
assert.ok(fromAustin.candidates > 20, 'dozens of Washington Counties');
const fromPortland = await findAdminArea('Washington County', {
near: PORTLAND,
});
assert.equal(fromPortland.region, 'Oregon');
const arkansas = await findAdminArea('Washington County, Arkansas', {
near: PORTLAND,
});
assert.equal(arkansas.region, 'Arkansas');
assert.equal(arkansas.candidates, 1);
assert.equal(
(await findAdminArea('Washington County, AR'))?.region,
'Arkansas',
);
});
test('Travis County with and without its state', async () => {
for (const query of [
'Travis County',
'Travis County, Texas',
'Travis County, TX',
'Travis County Texas',
'travis county, texas, usa',
]) {
const unit = await findAdminArea(query, { near: PORTLAND });
assert.equal(unit?.id, '48453', query);
assert.equal(unit.name, 'Travis County');
assert.equal(unit.region, 'Texas');
assert.equal(unit.source, 'us-census');
}
assert.equal(await findAdminArea('Travis County, Ohio'), null);
assert.equal(await findAdminArea('Travis County Courthouse'), null);
// Every qualifier must place the county, not name something in it.
assert.equal(await findAdminArea('Travis County Courthouse, Texas'), null);
assert.equal(
await findAdminArea('Orange County Great Park, California'),
null,
);
});
test('county equivalents: parishes, boroughs, independent cities, and non-US counties', async () => {
const orleans = await findAdminArea('Orleans Parish', { near: AUSTIN });
assert.equal(orleans?.id, '22071');
assert.equal(orleans.region, 'Louisiana');
assert.equal((await findAdminArea('Orleans Parish, LA'))?.id, '22071');
// The wrong county word still finds the only unit with that name.
assert.equal(
(await findAdminArea('Anchorage Borough'))?.name,
'Anchorage Municipality',
);
// Outside the US a county is an admin-1 unit.
assert.equal((await findAdminArea('County Cork'))?.country, 'Ireland');
assert.equal(
(await findAdminArea('Kent County, England'))?.country,
'United Kingdom',
);
assert.equal(
(await findAdminArea('Kent County', { near: AUSTIN }))?.region,
'Texas',
);
});
test('a bare name that is also a country or a large city is left to a geocoder', async () => {
for (const query of ['New York', 'Georgia', 'Madrid']) {
assert.equal(await findAdminArea(query), null, query);
}
// Admin wording settles it.
assert.equal((await findAdminArea('New York State'))?.id, 'US-NY');
assert.equal((await findAdminArea('State of New York'))?.id, 'US-NY');
assert.equal((await findAdminArea('State of Mexico'))?.country, 'Mexico');
// City-states are the city itself.
assert.equal((await findAdminArea('Berlin'))?.id, 'DE-BE');
assert.equal((await findAdminArea('Vienna'))?.country, 'Austria');
// A country qualifier places a name; it does not make a city a state.
assert.equal(await findAdminArea('New York, USA'), null);
assert.equal(await findAdminArea('Madrid, Spain'), null);
// A namesake city abroad: ambiguous alone, settled by naming the country.
const santaFe = { lat: 35.687, lon: -105.938 };
assert.equal(await findAdminArea('Santa Fe', { near: santaFe }), null);
assert.equal(await findAdminArea('Victoria'), null);
assert.equal((await findAdminArea('Victoria, Australia'))?.id, 'AU-VIC');
});
test('bare names: only prominent units, no generic words, codes as codes', async () => {
// Places a geocoder knows better than a small admin unit of that name.
for (const query of [
'Long Island',
'Santa Barbara',
'Kent',
'Hamilton',
'Milan',
])
assert.equal(await findAdminArea(query, { near: AUSTIN }), null, query);
for (const query of ['North', 'Central', 'Capital', 'Lakes'])
assert.equal(await findAdminArea(query), null, query);
// Two-letter codes must be written as codes, and not the city ones.
assert.equal(await findAdminArea('tx'), null);
assert.equal(
await findAdminArea('LA', { near: { lat: 34.05, lon: -118.24 } }),
null,
);
assert.equal(await findAdminArea('NY'), null);
assert.equal((await findAdminArea('OK'))?.id, 'US-OK');
});
test('non-admin asks never match', async () => {
for (const query of [
'Texas Capitol',
'the Texas State Capitol',
'Zilker Park',
'Ohio State',
'Texas State',
'Alps',
'',
null,
]) {
assert.equal(await findAdminArea(query), null, String(query));
}
});
test('geocoder-confirmed lookup: the named unit that contains the point', async () => {
// Atlanta — the geocoder said "state", so the US state wins over the country.
const georgia = await findAdminAreaAt(['Georgia'], 33.749, -84.388, 'state');
assert.equal(georgia?.id, 'US-GA');
// Tbilisi — Georgia the country has no admin-1 unit of that name.
assert.equal(await findAdminAreaAt(['Georgia'], 41.7, 44.8, 'state'), null);
const newYork = await findAdminAreaAt(['New York'], 42.9, -75.5, 'state');
assert.equal(newYork?.id, 'US-NY');
// A county the geocoder typed, named without the county word.
const travis = await findAdminAreaAt(
['Travis', 'Travis County'],
30.33,
-97.78,
'county',
);
assert.equal(travis?.id, '48453');
// A county the geocoder typed stays a county even where a state shares
// its name: Manhattan, the Big Island, Arkansas County.
assert.equal(
(await findAdminAreaAt(['New York County'], 40.783, -73.966, 'county'))?.id,
'36061',
);
assert.equal(
(await findAdminAreaAt(['Hawaii County'], 19.6, -155.5, 'county'))?.name,
'Hawaii County',
);
// A geocoded point outside the named unit is not that unit.
assert.equal(await findAdminAreaAt(['Texas'], 35.0, -106.6, 'state'), null);
// Outside the US a geocoder "county" is an admin-1 unit (Kent).
assert.equal(
(await findAdminAreaAt(['Kent'], 51.2, 0.7, 'county'))?.country,
'United Kingdom',
);
});
test('multipolygons: every island is a part, and containment follows the parts', async () => {
const hawaii = await findAdminArea('Hawaii');
assert.ok(
hawaii.polygons.length >= 7,
`Hawaii parts ${hawaii.polygons.length}`,
);
assert.ok(polygonsContain(hawaii.polygons, 19.6, -155.5), 'Big Island');
assert.ok(polygonsContain(hawaii.polygons, 20.8, -156.3), 'Maui');
assert.ok(polygonsContain(hawaii.polygons, 21.45, -158.0), 'Oahu');
assert.ok(
!polygonsContain(hawaii.polygons, 21.0, -157.3),
'the channel between',
);
// The main ring is the largest part (the Big Island).
assert.equal(hawaii.ring, hawaii.polygons[0][0]);
// Alaska is cut at the antimeridian: its box stays on one side of the globe.
const alaska = await findAdminArea('Alaska');
const [west, , east] = alaska.bbox;
assert.ok(east - west < 60, `Alaska box width ${east - west}°`);
assert.ok(alaska.polygons.length > 10, 'Aleutians and the southeast islands');
});
test('holes: Berlin is cut out of Brandenburg', async () => {
const brandenburg = await findAdminArea('Brandenburg');
assert.ok(
brandenburg.polygons.some((part) => part.length > 1),
'Brandenburg has a hole',
);
assert.ok(!polygonsContain(brandenburg.polygons, 52.52, 13.405), 'Berlin');
assert.ok(polygonsContain(brandenburg.polygons, 52.39, 13.06), 'Potsdam');
const berlin = await findAdminArea('Berlin');
assert.ok(polygonsContain(berlin.polygons, 52.52, 13.405));
});
test('holes: an independent city is cut out of its county', async () => {
const fairfax = await findAdminArea('Fairfax County, Virginia');
assert.ok(fairfax.polygons[0].length > 1, 'Fairfax County has holes');
assert.ok(
!polygonsContain(fairfax.polygons, 38.846, -77.306),
'Fairfax city',
);
assert.ok(polygonsContain(fairfax.polygons, 38.9, -77.2), 'Fairfax County');
const city = await findAdminArea('Fairfax city, Virginia');
assert.ok(polygonsContain(city.polygons, 38.846, -77.306));
assert.equal((await findAdminArea('Carson City'))?.region, 'Nevada');
for (const query of ['New York City', 'Mexico City', 'Quebec City, Canada'])
assert.equal(await findAdminArea(query), null, query);
});
test('rings decode from deltas and are open', () => {
assert.deepEqual(decodeRing([-97743, 30267, 10, -5, -3, 8], 3), [
[-97.743, 30.267],
[-97.733, 30.262],
[-97.736, 30.27],
]);
});
test('names normalize across accents, punctuation and "Saint"', () => {
assert.equal(normalizeAdminName('Québec'), 'quebec');
assert.equal(normalizeAdminName(' The State of TEXAS '), 'state of texas');
assert.equal(normalizeAdminName('Saint Louis County'), 'st louis county');
assert.equal(normalizeAdminName('St. Louis County'), 'st louis county');
assert.equal(normalizeAdminName('Schleswig-Holstein'), 'schleswig holstein');
});
test('packs: public-domain provenance, schema, and size budgets', () => {
const admin1 = JSON.parse(readFileSync(ADMIN1, 'utf8'));
const counties = JSON.parse(readFileSync(COUNTIES, 'utf8'));
for (const pack of [admin1, counties]) {
assert.match(pack.meta.license, /public domain/i);
assert.match(pack.meta.sha256, /^[0-9a-f]{64}$/);
assert.equal(pack.meta.script, 'scripts/build-admin-packs.mjs');
for (const feature of pack.features) {
assert.ok(feature.name, 'named');
assert.ok(feature.polygons.length >= 1, `${feature.name} has a part`);
for (const poly of feature.polygons)
for (const ring of poly) {
assert.equal(ring.length % 2, 0);
assert.ok(ring.length >= 8, `${feature.name}: ring has 4+ vertices`);
}
}
}
assert.ok(
admin1.features.length > 4500,
`${admin1.features.length} states/provinces`,
);
assert.ok(
counties.features.length > 3200,
`${counties.features.length} counties`,
);
assert.ok(statSync(ADMIN1).size <= 4 * 1024 * 1024, 'admin-1 pack ≤ 4 MB');
assert.ok(statSync(COUNTIES).size <= 2 * 1024 * 1024, 'county pack ≤ 2 MB');
});
test('county queries compare US and international names before choosing a geography', async () => {
const england = { lat: 54.78, lon: -1.58 };
const nc = { lat: 36.0, lon: -78.9 };
assert.equal(
(await findAdminArea('County Durham', { near: england }))?.country,
'United Kingdom',
);
assert.equal(
(await findAdminArea('County Durham'))?.country,
'United Kingdom',
'exact alias survives parsing',
);
assert.equal(
(await findAdminArea('Durham County', { near: nc }))?.region,
'North Carolina',
);
assert.equal(
(await findAdminArea('County Durham, England', { near: nc }))?.country,
'United Kingdom',
);
assert.equal(
(await findAdminArea('County Durham, USA', { near: england }))?.region,
'North Carolina',
);
assert.equal(
(await findAdminArea('Durham County, NC', { near: england }))?.region,
'North Carolina',
);
// Durham is also a city: without county wording leave city geocoding intact.
assert.equal(await findAdminArea('Durham, NC', { near: england }), null);
assert.equal(
(await findAdminAreaAt(['Durham, NC'], nc.lat, nc.lon, 'county'))?.region,
'North Carolina',
);
});
test('countries resolve aliases, keep substantial islands and disambiguate Georgia by scope and geography', async () => {
for (const names of [
['USA', 'United States', 'America'],
['UK', 'United Kingdom', 'Britain'],
['Iran', 'Islamic Republic of Iran'],
['Czechia', 'Czech Republic'],
['Ivory Coast', "Côte d'Ivoire"],
['South Korea', 'Korea'],
['Russia', 'Russian Federation'],
['Switzerland'],
['France'],
]) {
const resolved = await Promise.all(
names.map((name) => findAdminArea(name, { near: AUSTIN })),
);
assert.ok(
resolved.every((r) => r?.kind === 'country'),
names.join('/'),
);
assert.equal(new Set(resolved.map((r) => r.id)).size, 1);
}
assert.equal(
(await findAdminArea('Georgia', { near: AUSTIN })).kind,
'state',
);
assert.equal(
(await findAdminArea('Georgia', { near: { lat: 41.7, lon: 44.8 } })).kind,
'country',
);
assert.equal(
(await findAdminArea('the country of Georgia', { near: AUSTIN })).kind,
'country',
);
assert.equal(await findAdminArea('New York', { near: AUSTIN }), null);
assert.equal(
(await findAdminAreaAt(['Iran'], 35.69, 51.39, 'country')).kind,
'country',
);
assert.equal(
await findAdminAreaAt(['Iran'], 46.9, 7.4, 'country'),
null,
'name alone cannot override a mismatched geocoded point',
);
const japan = await findAdminArea('Japan');
for (const [lat, lon] of [
[35.68, 139.69],
[43.06, 141.35],
[33.59, 130.4],
[33.56, 133.53],
])
assert.ok(polygonsContain(japan.polygons, lat, lon));
const usa = await findAdminArea('USA');
for (const [lat, lon] of [
[30.27, -97.74],
[61.22, -149.9],
[19.7, -155.1],
])
assert.ok(
polygonsContain(usa.polygons, lat, lon),
'USA includes mainland, Alaska and Hawaii',
);
const bytes = statSync(
new URL('./local_data/natural_earth/countries.json', import.meta.url),
).size;
assert.ok(bytes < 1_000_000);
});
test('UK constituent countries retain their own identity and extent from any camera', async () => {
const targets = [
['Scotland', 'SCT', [55.95, -3.19], [54.6, 61]],
['England', 'ENG', [51.5, -0.12], [49.8, 55.9]],
['Wales', 'WLS', [51.48, -3.18], [51.3, 53.6]],
['Northern Ireland', 'NIR', [54.6, -5.93], [53.9, 55.4]],
];
for (const [name, id, point, extent] of targets) {
const unit = await findAdminArea(name, { near: AUSTIN });
assert.equal(unit.id, id);
assert.equal(unit.name, name);
assert.equal(unit.kind, 'country');
assert.ok(
unit.bbox[1] >= extent[0] && unit.bbox[3] <= extent[1],
`${name}: ${unit.bbox}`,
);
assert.ok(polygonsContain(unit.polygons, ...point));
for (const [other, , otherPoint] of targets)
if (other !== name)
assert.equal(polygonsContain(unit.polygons, ...otherPoint), false);
assert.equal((await findAdminAreaAt([name], ...point, 'country')).id, id);
assert.equal(await findAdminAreaAt([name], 30.27, -97.74, 'country'), null);
}
assert.equal((await findAdminArea('UK', { near: AUSTIN })).id, 'GBR');
assert.equal(
(await findAdminArea('County Durham, England', { near: AUSTIN })).country,
'United Kingdom',
);
});
+1 -1
View File
@@ -28,7 +28,7 @@ test('ais analyst record: full record maps every contract field', () => {
courseDeg: 214.0,
shipType: 'Cargo',
destination: 'OAKLAND',
navStatus: null, // /api/ais-live does not surface NavigationalStatus
navStatus: null, // /api/vessels does not surface NavigationalStatus
});
});
+6 -3
View File
@@ -1,10 +1,13 @@
import { createApplicationVessels } from '../app/layers/aisLiveVessels.js';
import { createAisStreamSource } from '../sources/live/standalone.js';
import { createVesselSource } from '../sources/live/standalone.js';
const aisLiveVesselsLayer = createApplicationVessels({
source: createAisStreamSource({
apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/ais-live',
source: createVesselSource({
apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/vessels',
// Resolved against the document's address, which a panel host may serve
// from its own scheme.
origin: () => globalThis.document?.baseURI ?? 'http://localhost',
}),
options: {
maxRows: import.meta.env?.VITE_AIS_LIVE_MAX_ROWS,
+2 -2
View File
@@ -1668,7 +1668,7 @@ test('a vessel analyst record carries the MMSI the tracker keys on', () => {
});
test('vessel selection passes the opaque source reference to optional history', async () => {
const { createAisStreamSource } = await import('../sources/live/standalone.js');
const { createVesselSource } = await import('../sources/live/standalone.js');
_setVesselStateForTest({ enabled: false });
const requests = [];
aisLiveVesselsLayer.setSource({
@@ -1688,6 +1688,6 @@ test('vessel selection passes the opaque source reference to optional history',
} finally {
harness.cleanup();
_setVesselStateForTest({ enabled: false });
aisLiveVesselsLayer.setSource(createAisStreamSource());
aisLiveVesselsLayer.setSource(createVesselSource());
}
});
+1 -1
View File
@@ -450,7 +450,7 @@ export function createAisWatchdog(options = {}) {
return generation;
}
/** Status metadata for /api/ais-live. */
/** Status metadata for /api/vessels. */
function snapshot() {
const monoNow = clock.mono();
const wallNow = clock.wall();
+3 -2
View File
@@ -1,12 +1,13 @@
import { defaultSurface } from './surfaceServices.js';
import { createApplicationAlpr } from '../app/layers/alprCameras.js';
import { createSourceSlot } from '../sources/sourceSlot.js';
import { createOverpassAlprSource } from '../layers/alpr/index.js';
import { createAlprTileSource } from '../layers/alpr/index.js';
export * from '../layers/alpr/index.js';
const slot = createSourceSlot(
createOverpassAlprSource(),
createAlprTileSource(),
['fetch'],
'ALPR source',
{ destroy: () => {}, detailZoom: () => null },
);
export const configureAlprSource = slot.configure;
export default createApplicationAlpr({
+437 -70
View File
@@ -3,7 +3,11 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import alprCamerasLayer, { createAlprCamerasLayer } from './alprCameras.js';
import alprCamerasLayer, {
createAlprCamerasLayer,
configureAlprSource,
} from './alprCameras.js';
import { createAlprTileSource } from '../layers/alpr/source.js';
import { DATA_CREDITS } from './dataCredits.js';
import {
clearSelectedEntityContextForLayer,
@@ -262,9 +266,45 @@ function cameraHarness(layer = alprCamerasLayer) {
let response = async () => cameraResponse();
const requests = [];
globalThis.fetch = (...args) => {
// Marker floors resolve through the shared terrain service; these tests
// count camera requests only.
if (String(args[0]).includes('/api/terrain/heights'))
return Promise.resolve(Response.json({ results: [] }));
requests.push(args);
return response(...args);
};
// Rendering tests use normalized records; tile transport has its own fixture suite.
if (layer === alprCamerasLayer)
configureAlprSource({
...createAlprTileSource(),
async fetch(box, signal) {
const response = await globalThis.fetch('/test/cameras', {
signal,
body: `data=${encodeURIComponent(buildOverpassQuery(box.south, box.west, box.north, box.east))}`,
});
if (!response.ok)
throw new Error('Camera source temporarily unavailable');
const body = await response.json();
if (!Array.isArray(body.elements) || body.remark)
throw new Error('Incomplete fixture snapshot');
signal?.throwIfAborted();
return {
records: [
...new Map(
body.elements
.slice(0, QUERY_LIMIT)
.map(normalizeAlprNode)
.filter(Boolean)
.map((r) => [r.id, r]),
).values(),
],
stale: response.headers.get('x-overpass-cache') === 'STALE',
saturated: body.elements.length >= QUERY_LIMIT,
noCoverage: body.noCoverage,
zoomIn: body.zoomIn,
};
},
});
let box = { south: 30.26, west: -97.75, north: 30.28, east: -97.73 };
let source, click, picked;
const credits = new Set();
@@ -348,88 +388,28 @@ function cameraHarness(layer = alprCamerasLayer) {
};
}
test('OSM attribution introduces displayed data for five seconds, then stays discoverable in the overflow', async (t) => {
test('OSM attribution stays inline while ALPR displays and follows toggles', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const h = cameraHarness();
try {
assert.equal(
h.credits.size,
0,
'do not use up the introduction while data is still loading',
);
assert.equal(h.credits.size, 0);
await alprCamerasLayer.update();
assert.equal(h.credits.size, 1);
const [credit] = h.credits;
assert.equal(credit.showOnScreen, true);
assert.match(credit.html, /class="gev-alpr-credit">ALPR:/);
assert.match(
credit.html,
/href="https:\/\/www.openstreetmap.org\/copyright"/,
);
assert.match(credit.html, /© OpenStreetMap<\/a>/);
t.mock.timers.tick(4999);
assert.match(credit.html, />© OpenStreetMap</);
assert.doesNotMatch(credit.html, /OpenMapTiles|Map and place data/);
t.mock.timers.tick(6000);
assert.equal(h.credits.size, 1);
t.mock.timers.tick(1);
assert.equal(h.credits.size, 0);
const overflow = DATA_CREDITS.find((entry) => entry.key === 'alpr-osm');
assert.match(
overflow.html,
/ALPR camera locations \(automatic license plate readers\)/,
);
assert.match(
overflow.html,
/href="https:\/\/www.openstreetmap.org\/copyright"/,
);
assert.match(overflow.html, /ODbL 1\.0/);
await alprCamerasLayer.update();
assert.equal(
h.credits.size,
0,
'refresh must not keep reintroducing the credit',
);
alprCamerasLayer.disable();
assert.equal(h.credits.size, 0);
alprCamerasLayer.enable();
await alprCamerasLayer.update();
t.mock.timers.tick(4000);
alprCamerasLayer.enable();
assert.equal(h.credits.size, 1);
t.mock.timers.tick(1000);
assert.equal(
h.credits.size,
0,
'duplicate enable must not restart the timer',
assert.match(
DATA_CREDITS.find((entry) => entry.key === 'openstreetmap').html,
/OpenStreetMap contributors.*ODbL/,
);
alprCamerasLayer.disable();
alprCamerasLayer.enable();
await alprCamerasLayer.update();
assert.equal(h.credits.size, 1);
} finally {
h.restore();
}
assert.equal(h.credits.size, 0);
t.mock.timers.tick(5000);
assert.equal(h.credits.size, 0, 'destroy cancels the presentation timer');
});
test('disabling midway through the introduction cancels its timer before a new enable', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const h = cameraHarness();
try {
await alprCamerasLayer.update();
t.mock.timers.tick(3000);
alprCamerasLayer.disable();
assert.equal(h.credits.size, 0);
alprCamerasLayer.enable();
await alprCamerasLayer.update();
t.mock.timers.tick(2000);
assert.equal(
h.credits.size,
1,
'old deadline must not hide the new presentation',
);
t.mock.timers.tick(3000);
assert.equal(h.credits.size, 0);
} finally {
h.restore();
}
@@ -765,7 +745,7 @@ test('empty mapped coverage, stale data and a saturated response remain distinct
assert.equal(alprCamerasLayer.getStats().status, 'empty');
assert.match(
alprCamerasLayer.getStats().loadingLabel,
/coverage is incomplete/,
/No ALPR data for this area/,
);
h.expire();
h.setFetch(async () =>
@@ -928,6 +908,122 @@ test('two factories keep their requests, records and destruction independent', a
}
});
test('markers sit on the cached floor; unknown floors clamp only until they resolve', async () => {
// Ground clamping re-samples every marker whenever a photoreal tile streams
// in; a whole-city view (about 1,000 cameras) stuttered while zooming.
const floors = new Map([['30.2672', 150]]);
let resolveCalls = 0;
let releaseResolve;
const services = {
render: { governorRequestRender() {} },
picking: { registerPickOwner() {}, unregisterPickOwner() {} },
groundFloor: {
cachedGroundFloor: (lat) => floors.get(lat.toFixed(4)) ?? null,
resolveGroundFloorCells: async () => {
resolveCalls++;
await new Promise((resolve) => {
releaseResolve = resolve;
});
floors.set('30.2682', 160);
},
},
context: {
clearSelectedEntityContextForLayer() {},
getSelectedEntityContext() {
return null;
},
registerEntityContext() {},
removeEntityContextsForLayer() {},
selectEntityContext() {},
},
};
const layer = createAlprCamerasLayer({
services,
source: {
fetch: async () => ({
records: [
{ id: 'directory:1', latitude: 30.2672, longitude: -97.7431 },
{ id: 'directory:2', latitude: 30.2682, longitude: -97.7431 },
],
stale: false,
saturated: false,
}),
},
});
const h = cameraHarness(layer);
try {
await layer.update();
const known = h.source.entities.getById('directory:1');
const unknown = h.source.entities.getById('directory:2');
assert.equal(
known.billboard.heightReference.getValue(),
Cesium.HeightReference.NONE,
);
assert.ok(
Math.abs(
Cesium.Cartographic.fromCartesian(known.position.getValue()).height -
151.5,
) < 0.01,
);
assert.equal(
unknown.billboard.heightReference.getValue(),
Cesium.HeightReference.CLAMP_TO_GROUND,
);
assert.equal(resolveCalls, 1);
releaseResolve();
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(
unknown.billboard.heightReference.getValue(),
Cesium.HeightReference.NONE,
);
assert.ok(
Math.abs(
Cesium.Cartographic.fromCartesian(unknown.position.getValue()).height -
161.5,
) < 0.01,
);
assert.equal(
h.source.entities.getById('directory:2'),
unknown,
'placed in place, not rebuilt',
);
} finally {
h.restore();
}
});
test('loaded cameras outside the view say so instead of looking unloaded', async () => {
// Field review: "nearby" spans twice the camera-to-ground range, so a view
// could hold cameras with none on screen and read as a failed load.
const h = cameraHarness();
const project = Cesium.SceneTransforms.worldToWindowCoordinates;
let inView = false;
Cesium.SceneTransforms.worldToWindowCoordinates = () =>
inView ? new Cesium.Cartesian2(100, 100) : new Cesium.Cartesian2(-500, 100);
try {
h.viewer.camera.positionWC = Cesium.Cartesian3.fromDegrees(
-97.7431,
30.2672,
2000,
);
h.viewer.scene.canvas.clientWidth = 800;
h.viewer.scene.canvas.clientHeight = 600;
await alprCamerasLayer.update();
let stats = alprCamerasLayer.getStats();
assert.equal(stats.onScreen, 0);
assert.match(stats.countLabel, /1 nearby · 0 on screen/);
assert.match(stats.loadingLabel, /None on screen/);
inView = true;
h.viewer.camera.moveEnd.raiseEvent();
stats = alprCamerasLayer.getStats();
assert.equal(stats.onScreen, 1);
assert.doesNotMatch(stats.loadingLabel, /None on screen/);
} finally {
Cesium.SceneTransforms.worldToWindowCoordinates = project;
h.restore();
}
});
test('orbit cache hits and metadata refreshes preserve marker geometry and selection events', async () => {
const h = cameraHarness();
try {
@@ -943,6 +1039,10 @@ test('orbit cache hits and metadata refreshes preserve marker geometry and selec
const entity = h.source.entities.getById('alpr:42');
const position = entity.position,
line = entity.polyline;
// Ground-clamped native wedges cost ~40 ms per frame for a downtown view;
// bearings are painted by the overlay for the nearest cameras instead.
assert.equal(line, undefined);
assert.equal(entity.polygon, undefined);
let selections = 0,
clears = 0,
collectionChanges = 0;
@@ -1126,3 +1226,270 @@ test('nearby count and discovery control frame a real loaded camera without fetc
h.restore();
}
});
test('a non-retryable capability error does not arm the ALPR retry timer', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const h = cameraHarness();
try {
h.setFetch(async () => {
throw Object.assign(new Error('Unavailable'), {
code: 'OVERPASS_NOT_CONFIGURED',
retryable: false,
});
});
await alprCamerasLayer.update();
assert.equal(alprCamerasLayer.getStats().retryAt, 0);
const requests = h.requests.length;
t.mock.timers.tick(300_000);
await Promise.resolve();
assert.equal(h.requests.length, requests);
} finally {
h.restore();
}
});
test('unsupported ALPR coverage suppresses the nearby count and names the extract region', async () => {
const h = cameraHarness();
try {
h.setFetch(async () => ({
...cameraResponse([]),
json: async () => ({ elements: [], noCoverage: true }),
}));
await alprCamerasLayer.update();
const stats = alprCamerasLayer.getStats();
assert.equal(stats.noCoverage, true);
assert.equal(stats.countLabel, '');
assert.equal(
stats.loadingLabel,
'No ALPR data for this area — US and Canada only',
);
} finally {
h.restore();
}
});
import { createGroundFloor } from '../services/groundFloor.js';
import { createTerrainHeights } from '../services/terrainHeights.js';
import { createApplicationRequestServices } from '../services/requests.js';
test('slow ALPR floors share real terrain requests across preparation, render and pans; disable aborts', async () => {
const requests = [];
const sourceServices = createApplicationRequestServices({
fetchImpl: (url, { signal }) =>
new Promise((resolve, reject) => {
assert.match(url, /^\/api\/terrain\/heights\?/);
const points = new URL(url, 'http://localhost').searchParams
.get('points')
.split(';');
requests.push({
signal,
points,
release: () =>
resolve(
Response.json({
results: points.map(() => ({ ellipsoid: 160 })),
}),
),
});
signal.addEventListener('abort', () => reject(signal.reason), {
once: true,
});
}),
});
const terrain = createTerrainHeights({ source: sourceServices.terrain });
const ground = createGroundFloor({ terrain });
ground.FLOOR_RESOLVE_DEADLINE_MS = 1;
const records = Array.from({ length: 130 }, (_, i) => ({
id: `slow:${i}`,
latitude: 30.261 + Math.floor(i / 15) * 0.001,
longitude: -97.749 + (i % 15) * 0.001,
}));
const layer = createAlprCamerasLayer({
source: {
fetch: async () => ({ records, stale: false, saturated: false }),
},
services: {
groundFloor: ground,
render: { governorRequestRender() {} },
picking: { registerPickOwner() {}, unregisterPickOwner() {} },
context: {
clearSelectedEntityContextForLayer() {},
getSelectedEntityContext() {},
registerEntityContext() {},
removeEntityContextsForLayer() {},
selectEntityContext() {},
},
},
});
const h = cameraHarness(layer);
try {
await layer.update();
assert.equal(
requests.length,
1,
'render joins the outstanding preparation batch',
);
assert.equal(
requests[0].points.length,
64,
'one bounded batch runs at a time',
);
h.expire();
await layer.update();
await layer.update();
assert.equal(
requests.length,
1,
'further preparation and render reuse cold cell promises',
);
requests[0].release();
await new Promise((done) => setTimeout(done, 0));
assert.equal(requests.length, 2);
const requested = requests.flatMap((request) => request.points);
assert.equal(
new Set(requested).size,
requested.length,
'no duplicate cells over HTTP',
);
layer.disable();
assert.equal(
requests[1].signal.aborted,
true,
'disable cancels the active HTTP request',
);
requests[1].release();
await new Promise((done) => setTimeout(done, 0));
assert.equal(requests.length, 2, 'queued final cells are discarded');
assert.equal(h.source.entities.values.length, 0);
layer.enable();
await layer.update();
assert.equal(
requests.length,
3,
'a fresh consumer can resume unresolved cells',
);
layer.destroy();
assert.equal(
requests[2].signal.aborted,
true,
'destroy also aborts its consumer',
);
} finally {
h.restore();
}
});
test('map-source changes reposition all ALPR entities and saved native appearances in place', async () => {
let floor = 180;
let listener;
let removed = 0;
const records = Array.from({ length: 70 }, (_, i) => ({
id: `surface:${i}`,
latitude: 30.2672 + i * 0.00001,
longitude: -97.7431,
directionDeg: i === 0 ? null : 90,
}));
const layer = createAlprCamerasLayer({
source: {
fetch: async () => ({ records, stale: false, saturated: false }),
},
services: {
groundFloor: { cachedGroundFloor: () => floor },
overlays: {
subscribeMapStack(fn) {
listener = fn;
return () => {
removed++;
};
},
},
render: { governorRequestRender() {} },
picking: { registerPickOwner() {}, unregisterPickOwner() {} },
context: {
clearSelectedEntityContextForLayer() {},
getSelectedEntityContext() {},
registerEntityContext() {},
removeEntityContextsForLayer() {},
selectEntityContext() {},
},
},
});
const h = cameraHarness(layer);
try {
await layer.update();
const entities = [...h.source.entities.values];
const saved = entities[1];
saved.gevAlprNativeAppearance = {
position: saved.position,
heightReference: saved.billboard.heightReference,
};
saved.position = Cesium.Cartesian3.fromDegrees(-97.7431, 30.26721, 190);
saved.gevAlprPickPosition = saved.position;
for (const next of [150, 120, null, 180]) {
floor = next;
// Includes a provider switch with the same globe.show value.
h.viewer.scene.globe.show = next !== 180;
listener({ detail: { status: 'ready' } });
for (const entity of entities) {
assert.equal(h.source.entities.getById(entity.id), entity);
assert.ok(
Math.abs(
Cesium.Cartographic.fromCartesian(entity.position.getValue())
.height - (next == null ? 0 : next + 1.5),
) < 0.01,
`${entity.id} refreshes at ${next}`,
);
assert.equal(
entity.billboard.heightReference.getValue(),
next == null
? Cesium.HeightReference.CLAMP_TO_GROUND
: Cesium.HeightReference.NONE,
);
assert.equal(entity.gevAlprCanvasPosition, null);
}
assert.equal(
saved.gevAlprNativeAppearance,
null,
'saved appearance restores at the new floor',
);
}
} finally {
h.restore();
}
assert.equal(removed, 1, 'map listener is disposed');
});
test('a repeated move inside an outstanding query keeps the displayed marker set until commit', async () => {
const h = cameraHarness();
try {
await alprCamerasLayer.update();
const previous = h.source.entities.getById('alpr:42');
const revision = alprCamerasLayer.getStats().renderRevision;
let release;
h.setFetch(
() =>
new Promise((resolve) => {
release = resolve;
}),
);
h.setBox({ south: 31, west: -98, north: 31.02, east: -97.98 });
const pending = alprCamerasLayer.update();
await alprCamerasLayer.update();
assert.equal(
h.source.entities.getById('alpr:42'),
previous,
'do not show an intermediate filtered snapshot while the new query prepares',
);
assert.equal(alprCamerasLayer.getStats().renderRevision, revision);
assert.equal(h.requests.length, 2, 'both moves share one new query');
release(cameraResponse([cameraNode(43, { lat: 31.01, lon: -97.99 })]));
await pending;
assert.equal(alprCamerasLayer.getStats().renderRevision, revision + 1);
assert.deepEqual(
h.source.entities.values.map((e) => e.id),
['alpr:43'],
);
} finally {
h.restore();
}
});
+4
View File
@@ -7,6 +7,8 @@ import { governorRequestRender } from '../renderGovernor.js';
import {
BHOTE_KOSHI_LOCATOR_CREDIT,
registerDynamicCredit,
showOsmCredit,
hideOsmCredit,
} from './dataCredits.js';
import { BHOTE_KOSHI_FLOOD_PATH } from './bhoteKoshiFloodPath.js';
import { INCIDENT_OVERVIEW_PLACES } from './bhoteKoshiIncidentPlaces.js';
@@ -1035,6 +1037,7 @@ export function createBhoteKoshiLocatorLayer({
renderPresentation();
viewer.dataSources.add(dataSource);
registerDynamicCredit(viewer, BHOTE_KOSHI_LOCATOR_CREDIT);
showOsmCredit(viewer, BHOTE_KOSHI_LOCATOR_LAYER_ID);
requestRender('bhote-koshi-locator-callout');
try {
@@ -1070,6 +1073,7 @@ export function createBhoteKoshiLocatorLayer({
}
async function disable() {
hideOsmCredit(_viewer, BHOTE_KOSHI_LOCATOR_LAYER_ID);
_enabled = false;
_boundaryReady = false;
_boundaryResolved = false;
+1 -1
View File
@@ -27,7 +27,7 @@ function viewerFixture() {
const added = [];
const removed = [];
return {
creditDisplay: { addStaticCredit() {} },
creditDisplay: { addStaticCredit() {}, removeStaticCredit() {} },
dataSources: {
add(source) { added.push(source); },
remove(source) { removed.push(source); },
+24 -9
View File
@@ -4,24 +4,39 @@
* The browser fetches the file from `url` — a `new URL('./….json',
* import.meta.url)` written in the calling module, which Vite serves as-is in
* dev and emits as an asset in the production build. Under Node (node:test)
* that URL is a `file:` URL, which `fetch` cannot read, so the caller's
* JSON-attributed import runs instead.
* that URL is a `file:` URL, which `fetch` cannot read, so the file is read
* from disk instead.
*
* The attributed import cannot serve the browser: the Vite dev server answers
* it with a JavaScript module, which the browser rejects for a JSON import.
* Nothing here imports the JSON as a module. A JSON-attributed import cannot
* serve the browser (the Vite dev server answers it with a JavaScript module,
* which the browser rejects), and a literal `import('./….json')` makes the
* production build emit a JavaScript copy of every pack that the browser
* never loads.
*
* @module data/bundledJson
*/
/**
* @param {URL} url - Resolved against the calling module's `import.meta.url`.
* @param {() => Promise<{default: any}>} importJson - The caller's literal
* `import('./….json', { with: { type: 'json' } })`, used only under Node.
* @param {{signal?: AbortSignal}} [options]
* @returns {Promise<any>} The parsed JSON.
*/
export async function loadBundledJson(url, importJson) {
if (url.protocol === 'file:') return (await importJson()).default;
const response = await fetch(url);
export async function loadBundledJson(url, { signal } = {}) {
signal?.throwIfAborted();
if (url.protocol === 'file:') {
// Node's built-in, reached without an import, so the browser build never
// bundles or rewrites it; only Node, reading `file:` URLs, gets here. The
// read is synchronous so the result settles as promptly as the cached
// module import it replaces; callers already memoize each pack.
const getBuiltinModule = globalThis.process?.getBuiltinModule;
if (typeof getBuiltinModule !== 'function')
throw new Error(
`God's Eye View needs Node 24.14 or newer to read ${url.pathname.split('/').pop()} (running ${globalThis.process?.version ?? 'an unknown runtime'})`,
);
const { readFileSync } = getBuiltinModule('node:fs');
return JSON.parse(readFileSync(url, 'utf8'));
}
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status} for ${url.pathname}`);
}
+57 -10
View File
@@ -2,13 +2,14 @@ import { test } from 'node:test';
import assert from 'node:assert/strict';
import { loadBundledJson } from './bundledJson.js';
const MARINE = new URL('./local_data/natural_earth/marine.json', import.meta.url);
const importMarine = () =>
import('./local_data/natural_earth/marine.json', { with: { type: 'json' } });
const MARINE = new URL(
'./local_data/natural_earth/marine.json',
import.meta.url,
);
test('bundled JSON: a file URL uses the JSON import under Node', async (t) => {
test('bundled JSON: a file URL is read from disk under Node', async (t) => {
const fetchMock = t.mock.method(globalThis, 'fetch');
const pack = await loadBundledJson(MARINE, importMarine);
const pack = await loadBundledJson(MARINE);
assert.ok(pack.features.some((ft) => ft.name === 'Gulf of Mexico'));
assert.equal(fetchMock.mock.callCount(), 0);
});
@@ -21,16 +22,62 @@ test('bundled JSON: a served URL is fetched as plain JSON', async (t) => {
headers: { 'content-type': 'application/json' },
});
});
const url = new URL('http://localhost/src/data/local_data/natural_earth/marine.json');
const importJson = () => assert.fail('the browser path must not import JSON');
assert.deepEqual(await loadBundledJson(url, importJson), { features: [] });
const url = new URL(
'http://localhost/src/data/local_data/natural_earth/marine.json',
);
assert.deepEqual(await loadBundledJson(url), { features: [] });
assert.deepEqual(requests, [url.href]);
});
test('bundled JSON: an HTTP error rejects so the retryable loader can retry', async (t) => {
t.mock.method(globalThis, 'fetch', async () => new Response('', { status: 404 }));
t.mock.method(
globalThis,
'fetch',
async () => new Response('', { status: 404 }),
);
await assert.rejects(
loadBundledJson(new URL('http://localhost/missing.json'), importMarine),
loadBundledJson(new URL('http://localhost/missing.json')),
/HTTP 404/,
);
});
test('bundled JSON: an aborted load rejects before reading', async () => {
const controller = new AbortController();
controller.abort();
await assert.rejects(loadBundledJson(MARINE, { signal: controller.signal }), {
name: 'AbortError',
});
});
test('bundled JSON: no data pack is imported as a module', async () => {
// A literal JSON import in app code makes the production build emit a
// JavaScript copy of the pack beside the fetched JSON, which the browser
// never loads. Tests are not bundled, so they may still import JSON.
const { spawnSync } = await import('node:child_process');
const result = spawnSync(
'git',
[
'grep',
'-nE',
String.raw`import\(\s*['"][^'"]*local_data/[^'"]*\.json`,
'--',
'src',
':!*.test.mjs',
],
{ cwd: new URL('../../', import.meta.url), encoding: 'utf8' },
);
// git grep exits 1 when nothing matches.
assert.equal(result.status, 1, result.stdout);
});
test('bundled JSON: an older Node says which version is needed', async (t) => {
// Node before 20.16 / 22.3 has no process.getBuiltinModule.
const original = process.getBuiltinModule;
t.after(() => {
process.getBuiltinModule = original;
});
process.getBuiltinModule = undefined;
await assert.rejects(loadBundledJson(MARINE), {
message: /needs Node 24\.14 or newer to read marine\.json/,
});
});
+16
View File
@@ -0,0 +1,16 @@
/** Arrival corrections belong to one viewer and yield to its next camera owner. */
const arrivals = new WeakMap();
/** Cancel an outstanding arrival correction before navigation or teardown. */
export function cancelCameraArrival(viewer) {
if (viewer) arrivals.get(viewer)?.();
}
/** Register a correction and return an idempotent ownership release. */
export function ownCameraArrival(viewer, cancel) {
cancelCameraArrival(viewer);
arrivals.set(viewer, cancel);
return () => {
if (arrivals.get(viewer) === cancel) arrivals.delete(viewer);
};
}
+25
View File
@@ -128,6 +128,14 @@ test('the catalog applies the fair cap to configured packs end to end', async ()
sources.slice(0, 3).map((s) => s.id),
['env-0', 'env-1', 'env-2'],
);
assert.equal(sources[0].pack, 'env');
const [trimmed] = getSources.trimmedPacks();
assert.deepEqual(
{ ...trimmed, region: undefined },
{ pack: 'env', available: 12, served: 9, region: undefined },
);
assert.equal(trimmed.region.south, 30);
assert.ok(Math.abs(trimmed.region.north - 30.11) < 1e-9);
} finally {
for (const key of Object.keys(process.env)) {
if (!(key in saved)) delete process.env[key];
@@ -135,3 +143,20 @@ test('the catalog applies the fair cap to configured packs end to end', async ()
Object.assign(process.env, saved);
}
});
test('a pack trimmed to its nearest cameras reports how many it offered', async () => {
const { prioritizeSources } =
await import('../../server/providers/cctv/normalize.js');
const cameras = Array.from({ length: 5 }, (_, i) => ({
id: `c${i}`,
lat: i,
lon: 0,
}));
const kept = prioritizeSources(cameras, 2, [{ lat: 0, lon: 0 }]);
assert.deepEqual(
kept.map((camera) => camera.id),
['c0', 'c1'],
);
assert.equal(kept.available, 5);
assert.deepEqual(kept.region, { west: 0, south: 0, east: 0, north: 4 });
});
+86 -54
View File
@@ -25,6 +25,72 @@ import * as Cesium from 'cesium';
* follows DATA_SOURCES.md (live sources, then bundled snapshots).
* @type {{ key: string, html: string }[]}
*/
export const OSM_CREDIT = {
key: 'openstreetmap',
html: 'Map and place data <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap contributors</a> (<a href="https://opendatacommons.org/licenses/odbl/1-0/" target="_blank" rel="noopener">ODbL</a>)',
};
export const OPENMAPTILES_CREDIT = {
key: 'openfreemap',
html: 'Vector tiles: <a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a> © <a href="https://openmaptiles.org" target="_blank" rel="noopener">OpenMapTiles</a>',
};
const osmDisplays = new WeakMap();
function updateOsmDisplay(viewer, state) {
const osm = state.owners.size > 0;
let tiles = false;
for (const usesTiles of state.owners.values()) tiles ||= usesTiles;
for (const [key, visible] of [
['osm', osm],
['tiles', tiles],
]) {
if (state[key].visible === visible) continue;
state[key].visible = visible;
viewer.creditDisplay[visible ? 'addStaticCredit' : 'removeStaticCredit'](
state[key].credit,
);
viewer.scene?.requestRender?.();
}
}
/** Keep one short inline credit while an OSM display owner has visible data. */
export function showOsmCredit(viewer, owner, { openMapTiles = false } = {}) {
if (!viewer?.creditDisplay?.addStaticCredit || !owner) return false;
let state = osmDisplays.get(viewer);
if (!state) {
state = {
owners: new Map(),
osm: {
visible: false,
credit: new Cesium.Credit(
'<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap</a>',
true,
),
},
tiles: {
visible: false,
credit: new Cesium.Credit(
'<a href="https://openmaptiles.org" target="_blank" rel="noopener">© OpenMapTiles</a>',
true,
),
},
};
osmDisplays.set(viewer, state);
}
if (state.owners.get(owner) === openMapTiles) return false;
state.owners.set(owner, openMapTiles);
updateOsmDisplay(viewer, state);
return true;
}
/** Release only this owner's credit; other displayed OSM sources retain theirs. */
export function hideOsmCredit(viewer, owner) {
const state = osmDisplays.get(viewer);
if (!state?.owners.delete(owner)) return false;
updateOsmDisplay(viewer, state);
return true;
}
export const DATA_CREDITS = [
// ── Live sources ────────────────────────────────────────────────
{
@@ -94,42 +160,15 @@ export const DATA_CREDITS = [
' · Incident information: ' +
'<a href="https://inciweb.wildfire.gov/" target="_blank" rel="noopener">InciWeb</a>',
},
OSM_CREDIT,
OPENMAPTILES_CREDIT,
{
key: 'overpass',
html:
'Road geometry (traffic): ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap contributors</a> ' +
'(ODbL 1.0)',
key: 'overture-military-names',
html: 'Military area names: <a href="https://overturemaps.org" target="_blank" rel="noopener">Overture Maps Foundation</a> (ODbL)',
},
{
key: 'photon-geocoder',
html:
'Keyless place search: ' +
'<a href="https://photon.komoot.io" target="_blank" rel="noopener">Photon</a> (komoot) over ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap contributors</a> ' +
'(ODbL 1.0)',
},
{
key: 'alpr-osm',
html:
'ALPR camera locations (automatic license plate readers): ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap contributors</a> ' +
'(<a href="https://opendatacommons.org/licenses/odbl/1-0/" target="_blank" rel="noopener">ODbL 1.0</a>); ' +
'community mapping includes <a href="https://deflock.org" target="_blank" rel="noopener">DeFlock</a>',
},
{
key: 'military-installations-osm',
html:
'Mapped installation context: ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap contributors</a> ' +
'(ODbL 1.0; incomplete mapped context)',
},
{
key: 'cockpit-place-osm',
html:
'Cockpit place context and last-resort place search: ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap contributors</a> ' +
'via Nominatim (ODbL 1.0)',
html: 'Keyless place search: <a href="https://photon.komoot.io" target="_blank" rel="noopener">Photon</a> (komoot)',
},
{
key: 'open-meteo',
@@ -212,7 +251,6 @@ export const DATA_CREDITS = [
html:
'Routing (voice routes and Directions): OSRM on the FOSSGIS servers — ' +
'<a href="https://routing.openstreetmap.de/about.html" target="_blank" rel="noopener">routing.openstreetmap.de</a> · ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap contributors</a> (ODbL) · ' +
'<a href="https://www.openstreetmap.org/fixthemap" target="_blank" rel="noopener">fix the map</a>',
},
{
@@ -262,19 +300,9 @@ export const DATA_CREDITS = [
'ECMWF does not accept any liability whatsoever for any error or omission in the data, their availability, or for any loss or damage arising from their use.',
},
// ── Bundled snapshots ───────────────────────────────────────────
{
key: 'datacenters',
html:
'Datacenters: ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap contributors</a> ' +
'(ODbL 1.0)',
},
{
key: 'dams',
html:
'Dams: ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap contributors</a> ' +
'(ODbL 1.0) + Open Infrastructure Map',
html: 'Dams: Open Infrastructure Map',
},
{
key: 'firms',
@@ -307,9 +335,7 @@ export const DATA_CREDITS = [
},
{
key: 'warendorf-cctv',
html:
'Webcam (Warendorf): <a href="https://www.warendorf.de/" target="_blank" rel="noopener">Stadt Warendorf</a> (courtesy); ' +
'camera poses derived from OpenStreetMap geometry, © OpenStreetMap contributors (ODbL)',
html: 'Webcam (Warendorf): <a href="https://www.warendorf.de/" target="_blank" rel="noopener">Stadt Warendorf</a> (courtesy)',
},
{
key: 'nsw-cctv',
@@ -342,15 +368,24 @@ export const TOMTOM_CREDIT = {
'<a href="https://www.tomtom.com" target="_blank" rel="noopener">TomTom</a>',
};
/** Registered when the first Natural Earth region outline resolves (public
* domain — no attribution required; credited as a courtesy). */
/** Registered when the first Natural Earth region or state/province outline
* resolves (public domain — no attribution required; credited as a courtesy). */
export const NATURAL_EARTH_CREDIT = {
key: 'natural-earth',
html:
'Physical region boundaries from ' +
'Country, state/province and physical region boundaries from ' +
'<a href="https://www.naturalearthdata.com" target="_blank" rel="noopener">Natural Earth</a> (public domain)',
};
/** Registered when the first bundled US county outline resolves (public
* domain — credited as a courtesy). */
export const US_CENSUS_CREDIT = {
key: 'us-census-counties',
html:
'US county boundaries from the ' +
'<a href="https://www.census.gov/geographies/mapping-files/time-series/geo/cartographic-boundary.html" target="_blank" rel="noopener">U.S. Census Bureau</a> (public domain)',
};
/**
* Per-feed transit credit, registered the first time that feed's vehicles
* render (see `src/data/transitFeeds.js` for the license of each).
@@ -386,10 +421,7 @@ export const BHOTE_KOSHI_CREDIT = {
export const BHOTE_KOSHI_LOCATOR_CREDIT = {
key: 'bhote-koshi-locator',
html:
'Nepal administrative boundary and nearby-city locations: ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">© OpenStreetMap contributors</a> ' +
'(ODbL 1.0); incident-place anchors use an owner-curated, source-verified geolocation union; ' +
'flood corridor derived from the ' +
'Flood corridor derived from the ' +
'<a href="https://github.com/geo-pera/bhotekoshi-2026-reconstruction/blob/main/vectors/river_centerline.geojson" target="_blank" rel="noopener">GeoPera river centerline</a> ' +
'(CC BY-NC 4.0)',
};
+83
View File
@@ -37,3 +37,86 @@ test('adsbdb is credited and carries its published route-data restriction', () =
assert.match(credit.html, /Guillaume Michel/);
assert.match(credit.html, /href="https:\/\/www\.adsbdb\.com"/);
});
test('OpenStreetMap has one generic data credit with separate tile and names distributors', () => {
const osm = DATA_CREDITS.filter((entry) =>
entry.html.includes('openstreetmap.org/copyright'),
);
assert.equal(osm.length, 1);
assert.equal(osm[0].key, 'openstreetmap');
assert.match(osm[0].html, /Map and place data/);
assert.match(osm[0].html, /© OpenStreetMap contributors/);
assert.match(
DATA_CREDITS.find((entry) => entry.key === 'openfreemap').html,
/Vector tiles:/,
);
assert.match(
DATA_CREDITS.find((entry) => entry.key === 'overture-military-names').html,
/Overture Maps Foundation/,
);
});
test('inline OSM attribution persists until its last display owner leaves', async (t) => {
const { showOsmCredit, hideOsmCredit } = await import('./dataCredits.js');
t.mock.timers.enable({ apis: ['setTimeout'] });
const visible = new Set();
const viewer = {
creditDisplay: {
addStaticCredit: (c) => visible.add(c),
removeStaticCredit: (c) => visible.delete(c),
},
scene: { requestRender() {} },
};
assert.equal(showOsmCredit(viewer, 'alpr'), true);
assert.equal(showOsmCredit(viewer, 'alpr'), false);
assert.equal(visible.size, 1);
assert.match(
[...visible][0].html,
/href="https:\/\/www.openstreetmap.org\/copyright"/,
);
assert.match([...visible][0].html, />© OpenStreetMap</);
assert.ok([...visible][0].showOnScreen);
t.mock.timers.tick(60000);
assert.equal(visible.size, 1);
showOsmCredit(viewer, 'traffic', { openMapTiles: true });
assert.equal(visible.size, 2);
hideOsmCredit(viewer, 'alpr');
assert.equal(visible.size, 2, 'traffic still owns both credits');
showOsmCredit(viewer, 'datacenters');
hideOsmCredit(viewer, 'traffic');
assert.equal(
visible.size,
1,
'bundled data does not retain the tile distributor',
);
hideOsmCredit(viewer, 'datacenters');
assert.equal(visible.size, 0);
assert.equal(hideOsmCredit(viewer, 'datacenters'), false);
showOsmCredit(viewer, 'alpr');
assert.equal(visible.size, 1, 'reenabling restores the short credit');
});
test('tile attribution follows source changes and viewers have independent ownership', async () => {
const { showOsmCredit, hideOsmCredit } = await import('./dataCredits.js');
const create = () => {
const credits = new Set();
return {
credits,
creditDisplay: {
addStaticCredit: (c) => credits.add(c),
removeStaticCredit: (c) => credits.delete(c),
},
};
};
const a = create(),
b = create();
showOsmCredit(a, 'installations', { openMapTiles: true });
showOsmCredit(b, 'installations');
assert.equal(a.credits.size, 2);
assert.equal(b.credits.size, 1);
showOsmCredit(a, 'installations');
assert.equal(a.credits.size, 1, 'wide names no longer use tiles');
hideOsmCredit(a, 'installations');
assert.equal(a.credits.size, 0);
assert.equal(b.credits.size, 1);
});
+2
View File
@@ -10,6 +10,7 @@
* the layer's pure helpers for the tests and the share-link code.
*/
import * as credits from './dataCredits.js';
import * as render from '../renderGovernor.js';
import * as sprites from './spriteOrder.js';
import * as picking from './pickRegistry.js';
@@ -23,6 +24,7 @@ import { createDirectionsLayer } from '../layers/directions/index.js';
/** The shared scene owners every Directions instance runs on. */
export const directionsServices = Object.freeze({
credits,
render,
sprites,
picking,
+21
View File
@@ -20,3 +20,24 @@
time is interpolated between the frames whose decoded position equals a
dump1090 fix, timed as dump1090's `now − seen_pos`. Used by the decoder's
category, speed-check and track-accuracy tests; never loaded at runtime.
- `ofm-austin-{14-3743-6745,12-935-1686}.pbf` and
`ofm-camp-mabry-12-935-1685.pbf` — OpenFreeMap / OpenMapTiles tiles from
`https://tiles.openfreemap.org/planet/20260913_164504_pt/{z}/{x}/{y}.pbf`,
retrieved 2026-09-23. Trimmed to one transportation feature per class/direction
and one military landuse feature, preserving original geometry and dictionaries.
OpenFreeMap © OpenMapTiles Data from OpenStreetMap; © OpenStreetMap
contributors, ODbL 1.0 (https://www.openstreetmap.org/copyright).
These modified data fixtures retain ODbL attribution and database share-alike;
commercial use and redistribution are permitted under that license. Test-only.
- `osm-alpr-austin-11-467-843.pbf` — one OSM camera feature (original geometry
and attribute dictionary) from the community-hosted hourly US extract,
`https://tiles.dontgetflocked.com/cameras-us-hourly/11/467/843.mvt`,
retrieved 2026-09-23. © OpenStreetMap contributors, ODbL 1.0; modified by
trimming to one feature, test-only. Same attribution/share-alike terms above.
- `traffic-road-access.json` — representative transportation properties and two
original vertices per feature near the Texas Capitol, Camp Mabry greenbelt,
and Austin commercial car parks, from OpenFreeMap z14 tiles, retrieved
2026-09-25. Each record names its XYZ tile and expected traffic eligibility.
Modified, test-only OpenStreetMap data; same ODbL attribution and database
share-alike terms as the OpenFreeMap fixtures above.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,12 +1,12 @@
import { defaultSurface } from './surfaceServices.js';
import { createApplicationFlights } from '../app/layers/flights.js';
import { createOpenSkySource } from '../sources/live/standalone.js';
import { createFlightSource } from '../sources/live/standalone.js';
import * as militaryRegistry from './militaryRegistry.js';
const flightsLayer = createApplicationFlights({
surface: defaultSurface,
source: createOpenSkySource(),
source: createFlightSource(),
militaryRegistry,
});
export { TRACKED_MODEL_MAX_PX } from '../layers/flights/policy.js';
+1 -1
View File
@@ -203,7 +203,7 @@ test('flights poll refreshes tracked callsign/FL/kts and marks a missed poll STA
const nowSec = Math.floor(Date.now() / 1000);
let openskyPoll = 0;
globalThis.fetch = async (url) => {
if (!String(url).startsWith('/api/opensky')) {
if (!String(url).startsWith('/api/flights')) {
return { ok: true, status: 200, json: async () => ({ ac: [] }) };
}
const states = openskyPoll++ === 0
+125 -82
View File
@@ -1,7 +1,7 @@
/**
* @file Flow→road matching: assign TomTom congestion levels to Overpass roads.
* @file Flow→road matching: assign TomTom congestion levels to OpenStreetMap roads.
*
* The traffic layer renders dots along OSM road polylines (Overpass), while
* In OpenStreetMap road mode the layer renders OpenFreeMap polylines, while
* TomTom flow tiles carry their own (differently segmented) polylines with
* `traffic_level`. This module snaps flow onto roads geometrically:
*
@@ -11,8 +11,10 @@
* projected to local meters (degree cell size cos(lat)-adjusted).
* 2. Each road is sampled at up to 7 evenly-spaced points along its length;
* each sample looks for the nearest flow segment within 35 m whose
* bearing agrees within 30° (folded mod 180° so two-way roads match
* opposite-direction flow lines).
* bearing agrees with the road travel direction within 30°. Two-way
* roads are represented as separate travel directions by the layer. A
* flow line whose `coverage` is `full` describes both directions of its
* road, so either travel bearing may match it.
* 3. A road matches when at least half its samples (minimum 2) matched; its
* level is the MEDIAN of the matched samples' trafficLevels, and closure
* is true if ANY matched segment is closed.
@@ -29,7 +31,7 @@ const M_PER_DEG_LAT = 111320;
const CELL_SIZE_M = 100;
/** @const {number} Max snap distance from a road sample to a flow segment. */
const MATCH_RADIUS_M = 35;
/** @const {number} Max bearing disagreement (degrees, folded mod 180). */
/** @const {number} Max travel-bearing disagreement (degrees). */
const BEARING_TOLERANCE_DEG = 30;
/** @const {number} Evenly-spaced samples per road. */
const ROAD_SAMPLES = 7;
@@ -56,15 +58,14 @@ function bearingDeg(dx, dy) {
}
/**
* Bearing disagreement folded mod 180° — a flow line drawn in the opposite
* direction of travel still describes the same two-way road.
* Travel-bearing disagreement; opposing flow never matches.
* @param {number} a - Bearing (degrees). @param {number} b - Bearing (degrees).
* @returns {number} min(|Δ|, 180 − |Δ|) in [0, 90].
* @returns {number} Angular difference in [0, 180].
*/
function bearingDiffDeg(a, b) {
let d = Math.abs(a - b) % 360;
if (d > 180) d = 360 - d;
return Math.min(d, 180 - d);
return d;
}
/** Squared distance from point (px,py) to segment (ax,ay)-(bx,by), meters². */
@@ -80,61 +81,45 @@ function pointSegDist2(px, py, ax, ay, bx, by) {
}
/**
* Match flow segments onto roads.
* Index flow polylines for nearest-segment queries in local meters.
*
* @param {Array<{coords:number[][], type:string}>} roads
* Parsed Overpass road objects ([[lon,lat],…] polylines).
* @param {Array<{coords:number[][], trafficLevel:number, roadType:string, closure:boolean}>} flowSegments
* Decoded flow polylines from `flowTiles.js`.
* @returns {{
* matches: Array<{level:number, closure:boolean}|null>,
* matchedCount: number,
* candidateCount: number,
* }}
* `matches` is PARALLEL to `roads` (index i describes roads[i]; null = no
* flow data for that road, render it exactly as today). `candidateCount` is
* the number of roads with at least one flow segment inside the 35 m search
* radius regardless of bearing — the denominator for coverage stats.
* Shared by congestion matching and the Hybrid road-source dedupe, so both
* use one distance, bearing and direction rule.
*
* @param {Array<{coords:number[][], trafficLevel:number, closure:boolean, coverage?:string}>} flowSegments
* @returns {null|{
* project: (lon:number, lat:number) => [number, number],
* nearest: (x:number, y:number, bearing:number, radiusFor?:(segment:object) => number) => {best:object|null, ambiguous:boolean, candidate:boolean},
* }} Null when no usable segment exists.
*/
export function matchFlowToRoads(roads, flowSegments) {
const roadCount = Array.isArray(roads) ? roads.length : 0;
const matches = new Array(roadCount).fill(null);
const empty = { matches, matchedCount: 0, candidateCount: 0 };
if (
roadCount === 0 ||
!Array.isArray(flowSegments) ||
flowSegments.length === 0
) {
return empty;
}
export function createFlowSegmentIndex(flowSegments) {
if (!Array.isArray(flowSegments)) return null;
// Local equirectangular projection anchored at the first flow coordinate —
// over a ≤0.05° fetch box the distortion is negligible.
const anchor = flowSegments.find(
(f) => Array.isArray(f?.coords) && f.coords.length >= 2,
);
if (!anchor) return empty;
if (!anchor) return null;
const [refLon, refLat] = anchor.coords[0];
const mPerDegLon = M_PER_DEG_LAT * Math.cos((refLat * Math.PI) / 180);
const projX = (lon) => (lon - refLon) * mPerDegLon;
const projY = (lat) => (lat - refLat) * M_PER_DEG_LAT;
const project = (lon, lat) => [
(lon - refLon) * mPerDegLon,
(lat - refLat) * M_PER_DEG_LAT,
];
// ── 1. Spatial hash of flow segments (midpoint-keyed 100 m cells) ──
/** @type {Map<string, Array<{ax:number,ay:number,bx:number,by:number,bearing:number,level:number,closure:boolean}>>} */
// Spatial hash of flow segments (midpoint-keyed 100 m cells).
const grid = new Map();
const cellOf = (x, y) =>
`${Math.floor(x / CELL_SIZE_M)},${Math.floor(y / CELL_SIZE_M)}`;
const cellKey = (cx, cy) => (cx + 0x8000) * 0x10000 + (cy + 0x8000);
for (const flow of flowSegments) {
const coords = flow?.coords;
if (!Array.isArray(coords) || coords.length < 2) continue;
const level = flow.trafficLevel;
const closure = flow.closure === true;
const bothDirections = flow.coverage === 'full';
const roadType = flow.roadType;
for (let i = 0; i < coords.length - 1; i++) {
const ax = projX(coords[i][0]);
const ay = projY(coords[i][1]);
const bx = projX(coords[i + 1][0]);
const by = projY(coords[i + 1][1]);
const [ax, ay] = project(coords[i][0], coords[i][1]);
const [bx, by] = project(coords[i + 1][0], coords[i + 1][1]);
const segLen = Math.hypot(bx - ax, by - ay);
if (!(segLen > 0)) continue;
const bearing = bearingDeg(bx - ax, by - ay);
@@ -150,10 +135,15 @@ export function matchFlowToRoads(roads, flowSegments) {
bx: ax + (bx - ax) * t1,
by: ay + (by - ay) * t1,
bearing,
bothDirections,
roadType,
level,
closure,
};
const key = cellOf((seg.ax + seg.bx) / 2, (seg.ay + seg.by) / 2);
const key = cellKey(
Math.floor((seg.ax + seg.bx) / 2 / CELL_SIZE_M),
Math.floor((seg.ay + seg.by) / 2 / CELL_SIZE_M),
);
let bucket = grid.get(key);
if (!bucket) {
bucket = [];
@@ -163,13 +153,87 @@ export function matchFlowToRoads(roads, flowSegments) {
}
}
}
if (grid.size === 0) return empty;
if (grid.size === 0) return null;
const radius2 = MATCH_RADIUS_M * MATCH_RADIUS_M;
/**
* Nearest direction-compatible segment within 35 m of a projected point.
* `radiusFor(segment)` may tighten the radius per segment (never widen it).
*/
function nearest(px, py, bearing, radiusFor = null) {
const cx = Math.floor(px / CELL_SIZE_M);
const cy = Math.floor(py / CELL_SIZE_M);
let best = null;
let bestDist2 = radius2;
let ambiguous = false;
let candidate = false;
for (let gy = cy - 1; gy <= cy + 1; gy++) {
for (let gx = cx - 1; gx <= cx + 1; gx++) {
const bucket = grid.get(cellKey(gx, gy));
if (!bucket) continue;
for (const seg of bucket) {
const d2 = pointSegDist2(px, py, seg.ax, seg.ay, seg.bx, seg.by);
if (d2 > radius2) continue;
candidate = true; // within radius, bearing not yet checked
if (radiusFor) {
const radius = Math.min(MATCH_RADIUS_M, radiusFor(seg));
if (d2 > radius * radius) continue;
}
const diff = bearingDiffDeg(seg.bearing, bearing);
if (
diff >= BEARING_TOLERANCE_DEG &&
!(seg.bothDirections && 180 - diff < BEARING_TOLERANCE_DEG)
)
continue;
if (d2 < bestDist2 - 1) {
bestDist2 = d2;
best = seg;
ambiguous = false;
} else if (Math.abs(d2 - bestDist2) <= 1) {
if (
best &&
(best.level !== seg.level || best.closure !== seg.closure)
)
ambiguous = true;
else if (!best) best = seg;
}
}
}
}
return { best, ambiguous, candidate };
}
return { project, nearest };
}
/**
* Match flow segments onto roads.
*
* @param {Array<{coords:number[][], type:string, oneway?:number}>} roads
* Parsed OpenStreetMap road objects ([[lon,lat],…] polylines).
* @param {Array<{coords:number[][], trafficLevel:number, roadType:string, closure:boolean, coverage?:string}>} flowSegments
* Decoded flow polylines from `flowDecode.js`.
* @returns {{
* matches: Array<{level:number, closure:boolean}|null>,
* matchedCount: number,
* candidateCount: number,
* }}
* `matches` is PARALLEL to `roads` (index i describes roads[i]; null = no
* flow data for that road, render it exactly as today). `candidateCount` is
* the number of roads with at least one flow segment inside the 35 m search
* radius regardless of bearing before bearing rejection.
*/
export function matchFlowToRoads(roads, flowSegments) {
const roadCount = Array.isArray(roads) ? roads.length : 0;
const matches = new Array(roadCount).fill(null);
const empty = { matches, matchedCount: 0, candidateCount: 0 };
if (roadCount === 0) return empty;
const index = createFlowSegmentIndex(flowSegments);
if (!index) return empty;
let matchedCount = 0;
let candidateCount = 0;
// ── 2+3. Sample each road and vote ──
// Sample each road and vote.
for (let r = 0; r < roadCount; r++) {
const coords = roads[r]?.coords;
if (!Array.isArray(coords) || coords.length < 2) continue;
@@ -180,8 +244,7 @@ export function matchFlowToRoads(roads, flowSegments) {
const cum = new Array(coords.length);
cum[0] = 0;
for (let i = 0; i < coords.length; i++) {
xs[i] = projX(coords[i][0]);
ys[i] = projY(coords[i][1]);
[xs[i], ys[i]] = index.project(coords[i][0], coords[i][1]);
if (i > 0)
cum[i] = cum[i - 1] + Math.hypot(xs[i] - xs[i - 1], ys[i] - ys[i - 1]);
}
@@ -202,37 +265,17 @@ export function matchFlowToRoads(roads, flowSegments) {
(target - cum[cursor - 1]) / (cum[cursor] - cum[cursor - 1] || 1);
const px = xs[cursor - 1] + (xs[cursor] - xs[cursor - 1]) * segT;
const py = ys[cursor - 1] + (ys[cursor] - ys[cursor - 1]) * segT;
const sampleBearing = bearingDeg(
xs[cursor] - xs[cursor - 1],
ys[cursor] - ys[cursor - 1],
);
const sampleBearing =
(roads[r].oneway === -1 ? 180 : 0) +
bearingDeg(xs[cursor] - xs[cursor - 1], ys[cursor] - ys[cursor - 1]);
// 3×3 cell probe around the sample.
const cx = Math.floor(px / CELL_SIZE_M);
const cy = Math.floor(py / CELL_SIZE_M);
let best = null;
let bestDist2 = radius2;
for (let gy = cy - 1; gy <= cy + 1; gy++) {
for (let gx = cx - 1; gx <= cx + 1; gx++) {
const bucket = grid.get(`${gx},${gy}`);
if (!bucket) continue;
for (const seg of bucket) {
const d2 = pointSegDist2(px, py, seg.ax, seg.ay, seg.bx, seg.by);
if (d2 > radius2) continue;
hadCandidate = true; // within radius, bearing not yet checked
if (
bearingDiffDeg(seg.bearing, sampleBearing) >=
BEARING_TOLERANCE_DEG
)
continue;
if (d2 <= bestDist2) {
bestDist2 = d2;
best = seg;
}
}
}
}
if (best) {
const { best, ambiguous, candidate } = index.nearest(
px,
py,
sampleBearing,
);
if (candidate) hadCandidate = true;
if (best && !ambiguous) {
matchedLevels.push(best.level);
if (best.closure) matchedClosure = true;
}
+27 -2
View File
@@ -44,10 +44,10 @@ test('coincident parallel road matches with the flow level', () => {
assert.equal(matches[0].closure, false);
});
test('opposite-direction flow still matches (two-way bearing fold)', () => {
test('opposite-direction flow matches only the reverse travel direction)', () => {
// Same line drawn south->north vs road north->south equivalence.
const reversed = line({ lengthM: 500, bearingDeg: 0, eastM: 4 }).reverse();
const { matches } = matchFlowToRoads([ROAD_NS], [flow(reversed, 0.3)]);
const { matches } = matchFlowToRoads([{...ROAD_NS, oneway:-1}], [flow(reversed, 0.3)]);
assert.ok(matches[0], 'reversed flow should still match');
assert.ok(Math.abs(matches[0].level - 0.3) < 1e-9);
});
@@ -159,3 +159,28 @@ test('median: single value and empty', () => {
assert.equal(median([0.7]), 0.7);
assert.equal(median([]), null);
});
test('one-way rejects opposite closure and two travel directions retain separate congestion', () => {
const coords = [[0,0],[0.002,0]];
const forward = {coords, oneway:1};
const reverse = {coords, oneway:-1};
const east = {coords, trafficLevel:1, closure:false};
const west = {coords:[...coords].reverse(), trafficLevel:0, closure:true};
assert.equal(matchFlowToRoads([forward],[west]).matches[0], null);
for (const flows of [[east,west],[west,east]]) {
assert.deepEqual(matchFlowToRoads([forward,reverse],flows).matches,
[{level:1,closure:false},{level:0,closure:true}]);
}
assert.equal(matchFlowToRoads([forward],[east,{...east,trafficLevel:0,closure:true}]).matches[0], null);
});
test('a full-coverage flow line matches both travel directions; one-side does not', () => {
const coords = [[0,0],[0.002,0]];
const directions = [{coords, oneway:1}, {coords, oneway:-1}];
const full = {coords, trafficLevel:0.4, closure:false, coverage:'full'};
assert.deepEqual(matchFlowToRoads(directions, [full]).matches,
[{level:0.4,closure:false},{level:0.4,closure:false}]);
const oneSide = {...full, coverage:'one_side'};
assert.deepEqual(matchFlowToRoads(directions, [oneSide]).matches,
[{level:0.4,closure:false}, null]);
});
+5 -8
View File
@@ -1,12 +1,5 @@
import { createLocalGeoJsonLayer } from './localGeojsonCore.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;
import { INFRASTRUCTURE_DATA_URLS } from '../sources/infrastructureData.js';
/**
* Create fresh datacenter and dam layers without starting or loading them.
@@ -14,6 +7,8 @@ const damsUrl = new URL('./local_data/dams/dams.geojsonl', import.meta.url)
* @returns {object[]} Datacenters then dams, with stable standalone identities.
*/
export function createInfrastructureLayers(services) {
const datacentersUrl = INFRASTRUCTURE_DATA_URLS['local-datacenters'];
const damsUrl = INFRASTRUCTURE_DATA_URLS['local-dams'];
const datacenters = createLocalGeoJsonLayer(
{
id: 'local-datacenters',
@@ -22,6 +17,7 @@ export function createInfrastructureLayers(services) {
color: '#00ffff', // Cyan
icon: '▣',
source: 'Local',
osmDerived: true,
labels: true,
labelMax: 700,
labelGridPx: 138,
@@ -37,6 +33,7 @@ export function createInfrastructureLayers(services) {
color: '#0088ff', // Blue
icon: '▰',
source: 'USACE',
osmDerived: true,
labels: true,
labelMax: 900,
labelGridPx: 132,
+10
View File
@@ -8,9 +8,12 @@ import { createLocalGeoJsonLayer } from 'gods-eye-view/infrastructure/geojson';
function services() {
const records = new Map();
const credited = [];
let selection;
return {
records,
credited,
showOsmCredit: (viewer) => credited.push(viewer),
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
registerEntityContext(entity, metadata) {
records.set(metadata.id, { entity, ...metadata });
@@ -121,6 +124,7 @@ test('two viewers use their supplied contexts and dispose independently', async
{
id: 'local-dams',
name: 'Dams',
osmDerived: true,
color: '#0088ff',
url: '/dams.geojsonl',
screenSpaceEventHandlerFactory: () => ({
@@ -150,6 +154,10 @@ test('two viewers use their supplied contexts and dispose independently', async
);
await Promise.all(instances.map(({ layer, viewer }) => layer.enable(viewer)));
assert.equal(hosts[0].records.size, 1);
assert.deepEqual(
hosts.map((host) => host.credited),
instances.map(({ viewer }) => [viewer]),
);
assert.equal(hosts[1].records.size, 1);
instances[0].click();
assert.ok(hosts[0].selection());
@@ -184,6 +192,8 @@ 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',
]);
+43
View File
@@ -0,0 +1,43 @@
const LOCAL_OVERLAY_MAX_DISTANCE_M = 14000000;
const LOCAL_OVERLAY_FADE_START_M = 250000;
const LOCAL_OVERLAY_FADE_START_RATIO =
LOCAL_OVERLAY_FADE_START_M / LOCAL_OVERLAY_MAX_DISTANCE_M;
/** Shared ambient infrastructure card contract; the host owns projection and persistence. */
export function createInfrastructureOverlayEntry({
id,
source,
position,
title,
details = [],
accent,
priority,
}) {
return {
id: String(id),
source: source,
position,
variant: 'card',
title: title,
details: details,
accent,
priority,
collisionGroup: 'ambient-card',
zIndex: 30,
interactive: false,
minDistance: 0,
maxDistance: LOCAL_OVERLAY_MAX_DISTANCE_M,
distanceFadeStartRatio: LOCAL_OVERLAY_FADE_START_RATIO,
distanceScale: {
near: 250000,
nearValue: 1,
far: 9000000,
farValue: 0.62,
},
edgeFade: 'keyhole',
horizonCull: true,
terrainOcclusion: false,
gapPx: 15,
placement: 'above',
};
}
+13
View File
@@ -4,6 +4,8 @@ export function installationFeedback(stats = {}, now = Date.now()) {
rate_limited: 'Overpass rate-limited',
timeout: 'Overpass timed out',
query_failed: 'Overpass could not complete the query',
tiles_unavailable: 'Map tiles temporarily unavailable',
names_unavailable: 'Mapped names temporarily unavailable',
};
const reason =
reasons[stats.failureReason] || 'Overpass temporarily unavailable';
@@ -18,5 +20,16 @@ export function installationFeedback(stats = {}, now = Date.now()) {
return 'Zoom in to search mapped installations';
if (stats.stale) return 'Showing cached mapped sites';
if (stats.status === 'idle') return 'Mapped sites not loaded';
// With a count, say what was found and where; an empty area is not "loaded".
if (Number.isFinite(stats.count)) {
const km =
stats.coverage?.kind === 'subject' &&
Number.isFinite(stats.coverage.radiusM)
? Math.round(stats.coverage.radiusM / 1000)
: null;
const where = km ? ` within ${km} km of the contact` : ' in view';
if (stats.count === 0) return `No mapped sites${where}`;
return `${stats.count} mapped site${stats.count === 1 ? '' : 's'}${where}`;
}
return 'Mapped sites loaded';
}
+13
View File
@@ -19,3 +19,16 @@ test('first fetch, retry, cached data and success have distinct copy', () => {
assert.equal(installationFeedback({ status: 'ready' }), 'Mapped sites loaded');
assert.equal(installationFeedback({ stale: true }), 'Showing cached mapped sites');
});
test('a counted result names what was found and where', () => {
assert.equal(installationFeedback({ status: 'ready', count: 3 }), '3 mapped sites in view');
assert.equal(installationFeedback({ status: 'empty', count: 0 }), 'No mapped sites in view');
const coverage = { kind: 'subject', radiusM: 100_000 };
assert.equal(
installationFeedback({ status: 'ready', count: 1, coverage }),
'1 mapped site within 100 km of the contact',
);
assert.equal(
installationFeedback({ status: 'unavailable', failureReason: 'tiles_unavailable' }),
'Map tiles temporarily unavailable',
);
});
+46 -3
View File
@@ -10,6 +10,9 @@ const FADE_IN_MS = 150;
const FADE_OUT_MS = 300;
const MIN_LIFETIME_MS = 2500;
const COOLDOWN_MS = 1200;
function zeroCount(_value, key, map) {
map.set(key, 0);
}
function clampInt(value, min, max) {
return Math.max(min, Math.min(max, Math.floor(Number(value) || 0)));
@@ -757,6 +760,8 @@ export class LabelArbiter {
this._layerBuckets = new Map();
this._demand = new Map();
this._quotas = new Map();
this._acceptedByLayer = new Map();
this._paintedByLayer = new Map();
this._weightedQuotaScratch = [];
this._quotaPriorityScratch = [];
this._quotaRemainderScratch = [];
@@ -770,6 +775,12 @@ export class LabelArbiter {
this._queue = new SpatialCandidateQueue();
}
/** Release an identity when its source changes between ambient and protected paint. */
remove(key) {
if (this.states.delete(key)) this._stateListDirty = true;
this.selectedKeys.delete(key);
}
clear() {
this.states.clear();
this.selectedKeys.clear();
@@ -781,6 +792,8 @@ export class LabelArbiter {
this._candidates.length = 0;
this._layerBuckets.clear();
this._quotas.clear();
this._acceptedByLayer.clear();
this._paintedByLayer.clear();
this._selectedCandidates.length = 0;
this._selectedPlacements.length = 0;
this._droppedKeys.length = 0;
@@ -889,6 +902,12 @@ export class LabelArbiter {
demand.set(layerId, bucket.count);
});
const limits = options.limitsByLayer;
demand.forEach((count, layerId) => {
demand.set(layerId, Math.min(count, limits?.get(layerId) ?? Infinity));
});
const acceptedByLayer = this._acceptedByLayer;
acceptedByLayer.forEach(zeroCount);
const activeLayers = this._activeLayersScratch;
let activeCount = 0;
let totalDemand = 0;
@@ -937,6 +956,9 @@ export class LabelArbiter {
)
return false;
attemptStamps.set(candidate.key, stamp);
const layerCount = acceptedByLayer.get(candidate.layerId) || 0;
if (layerCount >= (limits?.get(candidate.layerId) ?? Infinity))
return false;
const previous = this.states.get(candidate.key);
const stateless = candidate.stateless === true;
// Stateless candidates ignore the re-entry cooldown (they return on the
@@ -955,6 +977,7 @@ export class LabelArbiter {
selectedCandidates[selectedCount] = candidate;
selectedPlacements[selectedCount] = placement;
selectedCount++;
acceptedByLayer.set(candidate.layerId, layerCount + 1);
return true;
};
@@ -968,7 +991,8 @@ export class LabelArbiter {
if (preserveIncumbents) {
for (let i = 0; i < layerCount && accepted < target; i++) {
const candidate = layerList[i];
if (!this.states.get(candidate.key)?.selected) continue;
if (candidate.stateless || !this.states.get(candidate.key)?.selected)
continue;
if (attempt(candidate, true)) accepted++;
}
}
@@ -1139,7 +1163,7 @@ export class LabelArbiter {
* Reproject accepted/fading identities from the current frame candidate map.
* A caller-owned output array enables allocation-free per-frame rendering.
*/
renderEntries(currentCandidates, now = Date.now(), out = []) {
renderEntries(currentCandidates, now = Date.now(), out = [], limits = null) {
const current =
currentCandidates instanceof Map
? currentCandidates
@@ -1151,6 +1175,20 @@ export class LabelArbiter {
);
const states = this._refreshStateList();
let outIndex = 0;
const painted = this._paintedByLayer;
painted.forEach(zeroCount);
// Reserve the live winners before spending any remaining slots on exit fades.
if (limits)
for (let i = 0; i < states.length; i++) {
const state = states[i];
if (!state.selected) continue;
const candidate = current.get(state.key) || state.lastCandidate;
if (candidate)
painted.set(
candidate.layerId,
(painted.get(candidate.layerId) || 0) + 1,
);
}
for (let i = 0; i < states.length; i++) {
const state = states[i];
let temporalAlpha;
@@ -1171,9 +1209,14 @@ export class LabelArbiter {
if (temporalAlpha <= 0) continue;
const candidate = current.get(state.key) || state.lastCandidate;
if (!candidate) continue;
if (limits && !state.selected) {
const count = painted.get(candidate.layerId) || 0;
if (count >= (limits.get(candidate.layerId) ?? Infinity)) continue;
painted.set(candidate.layerId, count + 1);
}
const placement = renderPlacement(
candidate,
state.stateless ? undefined : state.corner,
state.corner,
state.lastPlacement,
);
if (!placement) continue;
+44
View File
@@ -345,6 +345,9 @@ test('a stateless candidate keeps no corner, no cooldown, and no fades', () => {
arbiter.solve([blocker, mover], { capacity: 2, now: 1000 });
assert.equal(arbiter.states.get('cctv:mover').corner, 'below',
'it took the free corner while the preferred one was blocked');
const paint = arbiter.renderEntries(new Map([[blocker.key, blocker], [mover.key, mover]]), 1000, []);
assert.equal(paint.find((entry) => entry.candidate.key === mover.key).placement.corner, 'below',
'painting honors this solve’s collision-free corner even without sticky placement');
// Corner is re-decided from geometry, NOT remembered: with the blocker gone the
// card returns to its preferred placement immediately.
@@ -504,3 +507,44 @@ test('live identities expose selected and temporal fading membership by layer/so
]));
assert.equal(arbiter.liveIdentities({ includeFading: true, now: 1501 }).size, 0);
});
test('stateless labels yield occupied slots and re-rank by area on each solve', () => {
const make = (key, x, priority) => ({ key, layerId: 'installations', sourceId: key,
priority, keyholeAlpha: 1, stateless: true, screenX: x, screenY: 100,
placements: [{ corner: 'above', rect: { x, y: 100, w: 100, h: 20 } }],
});
const large = make('large', 100, 100), small = make('small', 300, 10);
const arbiter = new LabelArbiter();
arbiter.solve([large, small], { capacity: 2, now: 1000 });
assert.equal(arbiter.selectedKeys.size, 2);
small.placements[0].rect.x = 100;
arbiter.solve([large, small], { capacity: 2, now: 1016 });
assert.deepEqual([...arbiter.selectedKeys], ['large'], 'incumbency never forces a stateless overlap');
const larger = make('larger', 100, 1000);
arbiter.solve([large, small, larger], { capacity: 2, now: 1032 });
assert.deepEqual([...arbiter.selectedKeys], ['larger'], 'area priority outranks a smaller incumbent');
});
test('a source ceiling bounds shared-domain winners and exit fades without evicting incumbents', () => {
const arbiter = new LabelArbiter();
const sites = Array.from({ length: 40 }, (_, i) => candidate('installations', i));
const centers = Array.from({ length: 30 }, (_, i) => candidate('flights', i));
const limits = new Map([['installations', 24]]);
const solve = (list, now) => arbiter.solve(list, { capacity: 54, limitsByLayer: limits, now });
solve([...sites, ...centers], 0);
const first = new Set(arbiter.selectedKeys);
assert.equal([...first].filter(key => key.startsWith('installations:')).length, 24);
for (let i = 1; i <= 10; i++) {
for (const site of sites) site.placements[0].rect.x += 0.25;
solve([...sites, ...centers], i * 16);
assert.deepEqual(arbiter.selectedKeys, first);
}
const replacements = sites.map((site, i) => ({ ...site, key: `installations:new${i}` }));
const current = [...replacements, ...centers];
solve(current, 2000);
const rendered = arbiter.renderEntries(new Map(current.map(c => [c.key, c])), 2100, [], limits);
assert.equal(rendered.filter(r => r.candidate.layerId === 'installations').length, 24);
assert.equal(rendered.filter(r => r.candidate.layerId === 'flights').length, 30);
});
+15 -724
View File
@@ -11,8 +11,6 @@ export const LAYER_STATE_TOKEN_ALPHABET =
'0123456789abcdefghijklmnopqrstuvwxyz';
const LAYER_STATE_TOKEN_PATTERN = /^[a-z0-9]{1,2}$/;
const NEW_SINGLE_CHARACTER_TOKEN_PATTERN = /^[0-9]$/;
/** Re-check cadence while a shared subject waits for its feed row to arrive. */
const PENDING_TRACKING_POLL_MS = 1_000;
/**
* A tracking ID is a transponder address, not free text: 6 hex digits for an
* ICAO24, with slack for TIS-B (`~abc123`) and similar prefixed forms. Bounding
@@ -298,6 +296,13 @@ function imageryPinOption(key, token) {
}
const OPTION_GROUPS = Object.freeze({
traffic: Object.freeze([
enumOption('roadMode', 'r', null, ['tomtom', 'osm', 'hybrid'], {
tomtom: 't',
osm: 'o',
hybrid: 'h',
}),
]),
'weather-lightning': Object.freeze([
enumOption('opacity', 'o', 'strong', ['light', 'strong'], {
light: 'l',
@@ -426,12 +431,6 @@ const OPTION_GROUPS = Object.freeze({
]),
});
const TRACKING_OPTION_KEY_BY_LAYER = Object.freeze({
flights: 'selectedFlightsTrackingId',
military: 'selectedMilitaryTrackingId',
satellites: 'selectedSatTrackingId',
});
export const SHARE_TRACKING_RESTORE_POLICIES = Object.freeze({
flights: Object.freeze({
optionOwner: 'flights',
@@ -637,7 +636,12 @@ export const LAYER_STATE_REGISTRY = Object.freeze([
token: 'u',
disposition: 'enabled-only',
}),
Object.freeze({ id: 'traffic', token: 't', disposition: 'enabled-only' }),
Object.freeze({
id: 'traffic',
token: 't',
disposition: 'enabled+options',
optionOwner: 'traffic',
}),
Object.freeze({ id: 'transit', token: 'j', disposition: 'enabled-only' }),
Object.freeze({
id: 'weather-cyclones',
@@ -728,7 +732,7 @@ export function validateLayerStateAllocations(
return true;
}
const REGISTRY_BY_ID = new Map(
export const REGISTRY_BY_ID = new Map(
LAYER_STATE_REGISTRY.map((entry) => [entry.id, entry]),
);
const REGISTRY_BY_TOKEN = new Map(
@@ -740,7 +744,7 @@ const OPTION_OWNER_IDS = Object.freeze([
),
]);
function optionSpecs(ownerId) {
export function optionSpecs(ownerId) {
return OPTION_GROUPS[ownerId] || [];
}
@@ -1043,716 +1047,3 @@ export function layerOptionsForRestore(state, layerId) {
if (!entry?.optionOwner) return null;
return { ...normalizeLayerState(state).options[entry.optionOwner] };
}
function safeStorage() {
try {
return globalThis.localStorage || null;
} catch {
return null;
}
}
function currentLayerOutcome(dataManager, layerId) {
const state = dataManager.getLayerLifecycleState?.(layerId);
return {
settledEnabled: Boolean(state?.enabled),
lifecycleState: state?.lifecycleState || 'missing',
lifecycleUncertain: Boolean(state?.uncertain),
};
}
/**
* Owns durable user layer preferences independently from transient runtime
* choreography, and coordinates passive post-registration restoration.
*/
export class LayerStateCoordinator {
constructor(
dataManager,
shareLinkManager,
{
storage = safeStorage(),
restoreGate = null,
onDurableStateChange = null,
onTrackingRestoreStatus = null,
now = () => Date.now(),
// Injectable so the pending-window behavior is deterministically testable
// without sleeping out a 90 s expiry.
setTimer = (fn, ms) => setTimeout(fn, ms),
clearTimer = (handle) => clearTimeout(handle),
} = {},
) {
if (!dataManager?.registrationsFinalized) {
throw new Error(
'Layer state requires finalized data-layer registrations',
);
}
this.dataManager = dataManager;
this.shareLinkManager = shareLinkManager || null;
this.storage = storage;
this.restoreGate = restoreGate;
this.onDurableStateChange = onDurableStateChange;
this.onTrackingRestoreStatus = onTrackingRestoreStatus;
this.now = now;
this.setTimer = setTimer;
this.clearTimer = clearTimer;
this._durableState = createDefaultLayerState();
this._source = 'defaults';
this._destroyed = false;
this._restoreControllers = new Map();
this._shareCreatedAtMs = null;
this._trackingRestoreController = null;
this._trackingRestoreGeneration = 0;
this._pendingTrackingTimer = null;
this._pendingTrackingContext = null;
this._unsubscribe = this.dataManager.subscribe((change) =>
this._handleManagerChange(change),
);
this._unsubscribeVisibilityRequests =
this.dataManager.subscribeVisibilityRequests((change) =>
this._handleVisibilityRequest(change),
);
this.restorePromise = Promise.resolve([]);
this.lastRestoreResults = [];
}
start({
shareLayerState = null,
allowLocalState = true,
shareCreatedAtMs = null,
} = {}) {
if (this._destroyed)
throw new Error('Layer-state coordinator is destroyed');
let selected = shareLayerState
? normalizeLayerState(shareLayerState)
: null;
if (selected) {
this._source = 'share';
this._shareCreatedAtMs = Number.isFinite(shareCreatedAtMs)
? shareCreatedAtMs
: null;
} else if (allowLocalState) {
let stored = null;
try {
stored = parseStoredLayerState(
this.storage?.getItem?.(LAYER_STATE_STORAGE_KEY),
);
} catch {
/* best effort */
}
if (stored) {
selected = stored;
this._source = 'local';
}
} else {
// A valid historical camera/style share with no v2 layer payload keeps
// the exact legacy default-layer behavior. It must not inherit an
// unrelated recipient's saved local layer preferences.
this._source = 'legacy-share';
}
this._durableState = selected || createDefaultLayerState();
this.shareLinkManager?.setLayerStateProvider?.(() =>
this.getDurableState(),
);
this.shareLinkManager?.onLayerStateChange?.();
this._notifyDurableState();
if (!selected) return this.restorePromise;
this.restorePromise = this._restoreSelectedState(
this._source === 'share'
? LAYER_RESTORE_ORIGINS.share
: LAYER_RESTORE_ORIGINS.local,
);
return this.restorePromise;
}
get source() {
return this._source;
}
getDurableState() {
return cloneLayerState(this._durableState);
}
_notifyDurableState() {
try {
this.onDurableStateChange?.(this.getDurableState());
} catch {
/* UI sync is best effort */
}
}
_handleVisibilityRequest(change) {
if (!isExplicitLayerStateOrigin(change?.origin)) return;
this._restoreControllers
.get(change.layerId)
?.abort('superseded-by-explicit-visibility');
if (SHARE_TRACKING_RESTORE_POLICIES[change.layerId]) {
this._revokePendingTrackingWatch('superseded-by-explicit-visibility');
}
}
/** Revoke every passive restore before explicit navigation can be reclaimed. */
cancelPendingRestores(reason = 'superseded-by-explicit-navigation') {
for (const controller of this._restoreControllers.values())
controller.abort(reason);
this._revokePendingTrackingWatch(reason);
}
/**
* Revoke a pending shared Follow. Physical navigation may also clear only
* the exact passive selection, without writing recipient preferences.
*/
cancelPendingShareTracking(
reason = 'superseded-by-explicit-navigation',
{ clearSelection = false } = {},
) {
this._revokePendingTrackingWatch(reason);
if (!clearSelection) return false;
const selected = this._selectedShareTrackingTarget();
return selected ? this._passivelyClearTrackingSelection(selected) : false;
}
_handleManagerChange(change) {
if (!change || this._destroyed) return;
// Parameter and visibility ownership are independent. A newer explicit
// option request may replace passive share options, but it must not abort
// the same layer's visibility lifecycle.
if (change.type === 'params-requested') {
if (
isExplicitLayerStateOrigin(change.origin) &&
SHARE_TRACKING_RESTORE_POLICIES[change.layerId]
) {
this._revokePendingTrackingWatch('superseded-by-explicit-params');
}
return;
}
// A layer that goes away takes its latch with it, at ANY origin — a
// programmatic disable or teardown never reaches the explicit-intent path
// below, so revoke here before that early return.
if (
change.type === 'visibility' &&
change.enabled === false &&
SHARE_TRACKING_RESTORE_POLICIES[change.layerId]
) {
this._revokePendingTrackingWatch('owner-layer-disabled');
}
if (!isExplicitLayerStateOrigin(change.origin)) return;
if (change.type === 'visibility') {
this._restoreControllers
.get(change.layerId)
?.abort('superseded-by-explicit-visibility');
if (SHARE_TRACKING_RESTORE_POLICIES[change.layerId]) {
this._revokePendingTrackingWatch('superseded-by-explicit-visibility');
}
const enabled = new Set(this._durableState.enabledLayerIds);
if (change.enabled) enabled.add(change.layerId);
else enabled.delete(change.layerId);
this._commitExplicit({
...this._durableState,
enabledLayerIds: [...enabled],
});
return;
}
if (change.type !== 'params') return;
const entry = REGISTRY_BY_ID.get(change.layerId);
if (!entry?.optionOwner) return;
const ownerId = entry.optionOwner;
const nextOwnerOptions = { ...this._durableState.options[ownerId] };
const requestedParams = change.requestedParams || {};
const trackingOptionKey =
TRACKING_OPTION_KEY_BY_LAYER[change.layerId] || null;
let changed = false;
for (const spec of optionSpecs(ownerId)) {
// Only persist keys present in this explicit request. getLayerParams()
// can return a wider live snapshot containing transient or passively
// changed values that this user action did not own. Tracking is the one
// exception: an unrelated explicit option cancels a pending restoration
// in that family, so its wider live value (active ID or null) must replace
// the formerly durable pending ID instead of allowing reload resurrection.
const explicitlyRequested = Object.hasOwn(requestedParams, spec.key);
const implicitTrackingSync =
!explicitlyRequested && spec.key === trackingOptionKey;
if (!explicitlyRequested && !implicitTrackingSync) continue;
const value = spec.normalize(change.params[spec.key]);
if (value === null) {
if (implicitTrackingSync) {
nextOwnerOptions[spec.key] = null;
changed = true;
continue;
}
if (spec.defaultValue !== null || change.params[spec.key] !== null)
continue;
}
nextOwnerOptions[spec.key] = value;
changed = true;
}
if (!changed) return;
this._commitExplicit({
...this._durableState,
options: { ...this._durableState.options, [ownerId]: nextOwnerOptions },
});
}
_commitExplicit(candidate) {
this._durableState = normalizeLayerState(candidate);
const serialized = serializeStoredLayerState(this._durableState);
try {
if (this.storage?.getItem?.(LAYER_STATE_STORAGE_KEY) !== serialized) {
this.storage?.setItem?.(LAYER_STATE_STORAGE_KEY, serialized);
}
} catch {
/* storage can be unavailable or quota-limited */
}
this.shareLinkManager?.onLayerStateChange?.();
this._notifyDurableState();
}
async _waitForRestoreGate() {
if (!this.restoreGate) return;
await (typeof this.restoreGate === 'function'
? this.restoreGate()
: this.restoreGate);
}
async _restoreSelectedState(origin) {
for (const entry of LAYER_STATE_REGISTRY) {
this._restoreControllers.set(entry.id, new AbortController());
}
try {
await this._waitForRestoreGate();
const enabled = new Set(this._durableState.enabledLayerIds);
const settled = await Promise.allSettled(
LAYER_STATE_REGISTRY.map(async (entry) => {
const controller = this._restoreControllers.get(entry.id);
const targetEnabled = enabled.has(entry.id);
const options = layerOptionsForRestore(this._durableState, entry.id);
if (origin === LAYER_RESTORE_ORIGINS.share && options) {
for (const trackingKey of Object.values(
TRACKING_OPTION_KEY_BY_LAYER,
)) {
delete options[trackingKey];
}
}
if (this._destroyed || controller?.signal.aborted) {
return {
layerId: entry.id,
targetEnabled,
origin,
phase: 'reserved',
...currentLayerOutcome(this.dataManager, entry.id),
appliedOptions: {},
cancellationReason: this._destroyed ? 'destroyed' : 'superseded',
errorClass: 'cancelled',
persistenceWrite: false,
succeeded: false,
};
}
// Reserve passive option state before any asynchronous lifecycle work.
// A later explicit params intent then wins on its own lane without
// cancelling or being overwritten by the visibility restore.
const paramsSucceeded =
!options ||
Object.keys(options).length === 0 ||
this.dataManager.setLayerParams(entry.id, options, { origin });
return this.dataManager
.restoreLayerState(
entry.id,
{
enabled: targetEnabled,
params: null,
},
{ origin, signal: controller.signal },
)
.then((result) => ({
...result,
appliedOptions: paramsSucceeded && options ? options : {},
errorClass: paramsSucceeded
? result.errorClass
: 'ParamsRejected',
succeeded: paramsSucceeded && result.succeeded,
}));
}),
);
this.lastRestoreResults = settled.map((result, index) => {
if (result.status === 'fulfilled') return result.value;
const entry = LAYER_STATE_REGISTRY[index];
return {
layerId: entry.id,
targetEnabled: enabled.has(entry.id),
origin,
phase: 'coordinator',
...currentLayerOutcome(this.dataManager, entry.id),
appliedOptions: {},
cancellationReason: null,
errorClass: result.reason?.name || 'Error',
error: String(result.reason?.message || result.reason),
persistenceWrite: false,
succeeded: false,
};
});
return this.lastRestoreResults.map((result) => ({ ...result }));
} finally {
this._restoreControllers.clear();
this._notifyDurableState();
}
}
_selectedShareTrackingTarget() {
if (this._source !== 'share') return null;
for (const [layerId, policy] of Object.entries(
SHARE_TRACKING_RESTORE_POLICIES,
)) {
const targetId =
this._durableState.options?.[policy.optionOwner]?.[policy.optionKey];
if (targetId !== null && targetId !== undefined && targetId !== '') {
return { layerId, targetId, ...policy };
}
}
return null;
}
_passivelyClearTrackingSelection(selected) {
const current =
this._durableState.options?.[selected.optionOwner]?.[selected.optionKey];
if (String(current) !== String(selected.targetId)) return false;
const ownerOptions = {
...this._durableState.options[selected.optionOwner],
[selected.optionKey]: null,
};
this._durableState = normalizeLayerState({
...this._durableState,
options: {
...this._durableState.options,
[selected.optionOwner]: ownerOptions,
},
});
this.dataManager.setLayerParams?.(
selected.layerId,
{ [selected.optionKey]: null },
{ origin: LAYER_RESTORE_ORIGINS.share },
);
this.shareLinkManager?.onLayerStateChange?.();
this._notifyDurableState();
return true;
}
/**
* `atMs` is the moment the subject was first found ABSENT, not the moment the
* verdict is delivered. Waiting out the pending window must not by itself
* push a fresh link into the "expired" wording — that word describes the
* SHARE's age, not how long this client watched for the subject.
*/
_classifyMissingTrackingTarget(selected, atMs = this.now()) {
const copiedAt = this._shareCreatedAtMs;
if (!Number.isFinite(copiedAt)) return 'unavailable';
const ageMs = atMs - copiedAt;
return ageMs > selected.expiryWindowMs ? 'expired' : 'unavailable';
}
/** Whether the owning layer currently follows the shared subject. */
_trackingTargetLatched(selected) {
const params = this.dataManager.getLayerParams?.(selected.layerId);
const active = params?.[selected.optionKey];
return (
active !== null &&
active !== undefined &&
String(active) === String(selected.targetId)
);
}
/** Stop watching a pending shared subject without deciding its fate. */
_cancelPendingTrackingWatch() {
if (this._pendingTrackingTimer !== null)
this.clearTimer(this._pendingTrackingTimer);
this._pendingTrackingTimer = null;
const pending = this._pendingTrackingContext;
if (pending?.signal && pending.abortHandler) {
pending.signal.removeEventListener('abort', pending.abortHandler);
pending.abortHandler = null;
}
}
/** Publish a share-follow lifecycle update without allowing UI errors to own state. */
_publishTrackingRestoreStatus(status) {
try {
this.onTrackingRestoreStatus?.(status);
} catch {
/* status UI is best effort */
}
}
/**
* Revoke a pending shared Follow wholesale.
*
* The watch and the LAYER's own deferred-restore latch are two halves of one
* mechanism, so they must die together. Aborting only the restore controller
* left the timer alive: the controller has already settled by the time the
* watch exists, so the abort was a no-op and the orphaned timer went on to
* announce "Shared … unavailable" for a subject whose latch had been
* cancelled — a notice about work no longer being attempted.
*/
_revokePendingTrackingWatch(reason) {
this._trackingRestoreController?.abort(reason);
this._trackingRestoreGeneration += 1;
this._cancelPendingTrackingWatch();
const pending = this._pendingTrackingContext;
this._pendingTrackingContext = null;
if (pending) {
this.dataManager.cancelPendingLayerRestore?.(pending.selected.layerId, {
origin: LAYER_RESTORE_ORIGINS.share,
reason: String(reason || 'cancelled'),
});
this._publishTrackingRestoreStatus({
...pending.probe,
...pending.selected,
status: 'cancelled',
classification: 'cancelled',
reason: String(reason || 'cancelled'),
cleared: false,
});
}
}
/**
* Hold a not-yet-arrived shared subject PENDING instead of declaring it gone.
*
* A recipient's first authoritative refresh routinely lands without a given
* contact — the feed is polled, coverage is partial, and rendering trails the
* snapshot by a poll. Reload-from-local already survives this: its restore
* arms the layer's own deferred-restore latch, which re-attempts on every
* later poll. The shared path used to decide on that single refresh, clear
* the subject from durable state AND from the URL, then post a failure notice
* seconds into startup — so the same link healed on reload but never on the
* share.
*
* Arm the SAME latch, then watch it in the background: the caller is never
* blocked (startup must not wait out a 90 s window before it may write the
* URL again), and the terminal verdict is deferred until the source-specific
* window has genuinely expired. The existing wordings are unchanged.
*/
async _beginPendingTrackingRestore(selected, probe, signal = null) {
const generation = this._trackingRestoreGeneration;
const absentAtMs = this.now();
// Arm the layer's deferred-restore latch under the passive share origin, so
// it re-attempts each poll and never rewrites recipient preferences.
let armed = false;
let armError = null;
try {
armed =
(await this.dataManager.setLayerParams?.(
selected.layerId,
{ [selected.optionKey]: selected.targetId },
{ origin: LAYER_RESTORE_ORIGINS.share },
)) === true;
} catch (error) {
armError = error;
}
if (
this._destroyed ||
generation !== this._trackingRestoreGeneration ||
signal?.aborted
) {
if (armed) {
this.dataManager.cancelPendingLayerRestore?.(selected.layerId, {
origin: LAYER_RESTORE_ORIGINS.share,
reason: String(signal?.reason || 'superseded'),
});
}
return {
...probe,
...selected,
status: 'cancelled',
classification: 'cancelled',
reason: String(signal?.reason || 'superseded'),
cleared: false,
};
}
if (!armed) {
const terminal = {
...probe,
...selected,
status: 'source-unavailable',
classification: 'source-unavailable',
reason: String(
armError?.message || armError || 'tracking restore latch rejected',
),
cleared: this._passivelyClearTrackingSelection(selected),
};
this._publishTrackingRestoreStatus(terminal);
return terminal;
}
const deadline = this.now() + selected.expiryWindowMs;
const settle = (terminal) => {
if (this._pendingTrackingContext?.generation !== generation) return;
this._cancelPendingTrackingWatch();
this._pendingTrackingContext = null;
this._publishTrackingRestoreStatus(terminal);
};
const poll = () => {
this._pendingTrackingTimer = null;
if (this._destroyed || generation !== this._trackingRestoreGeneration)
return;
// The layer that owns the latch may have gone away since the last tick
// (disable, teardown, replacement). There is nothing left attempting this
// restore, so abandon it silently rather than announcing a verdict.
if (this.dataManager.isEffectivelyEnabled?.(selected.layerId) === false) {
this._revokePendingTrackingWatch('owner-layer-disabled');
return;
}
if (this._trackingTargetLatched(selected)) {
settle({
...probe,
...selected,
status: 'found',
classification: 'followed',
cleared: false,
});
return;
}
if (this.now() >= deadline) {
// The window really has elapsed — only now does the verdict apply.
const classification =
probe.status === 'missing'
? this._classifyMissingTrackingTarget(selected, absentAtMs)
: 'source-unavailable';
const cleared = this._passivelyClearTrackingSelection(selected);
settle({ ...probe, ...selected, classification, cleared });
return;
}
this._pendingTrackingTimer = this.setTimer(
poll,
PENDING_TRACKING_POLL_MS,
);
this._pendingTrackingTimer?.unref?.();
};
this._cancelPendingTrackingWatch();
const pending = {
...probe,
...selected,
status: 'pending',
classification: 'pending',
cleared: false,
};
const pendingContext = {
generation,
selected,
probe,
signal,
abortHandler: null,
};
if (signal) {
pendingContext.abortHandler = () => {
if (this._pendingTrackingContext?.generation !== generation) return;
this._revokePendingTrackingWatch(signal.reason || 'aborted');
};
signal.addEventListener('abort', pendingContext.abortHandler, {
once: true,
});
}
this._pendingTrackingContext = pendingContext;
this._publishTrackingRestoreStatus(pending);
this._pendingTrackingTimer = this.setTimer(poll, PENDING_TRACKING_POLL_MS);
this._pendingTrackingTimer?.unref?.();
return pending;
}
/**
* Refresh and restore the one shareable tracked target after destination
* camera and ordinary layer restoration have settled.
*/
async restoreShareTrackingSelection({ signal = null } = {}) {
const selected = this._selectedShareTrackingTarget();
if (!selected || this._destroyed)
return { status: 'skipped', reason: 'no-shared-target' };
this._revokePendingTrackingWatch('superseded-by-newer-restore');
const controller = new AbortController();
const combinedSignal = signal
? AbortSignal.any([signal, controller.signal])
: controller.signal;
this._trackingRestoreController = controller;
const generation = ++this._trackingRestoreGeneration;
let result;
try {
result = await this.dataManager.resolveLayerTrackingTarget(
selected.layerId,
selected.targetId,
{ signal: combinedSignal, origin: LAYER_RESTORE_ORIGINS.share },
);
} catch (error) {
result = combinedSignal.aborted
? {
status: 'cancelled',
reason: String(combinedSignal.reason || 'aborted'),
}
: {
status: 'source-unavailable',
reason: String(error?.message || error),
};
}
if (
generation !== this._trackingRestoreGeneration ||
this._destroyed ||
combinedSignal.aborted
) {
if (this._trackingRestoreController === controller)
this._trackingRestoreController = null;
return {
...result,
status: 'cancelled',
reason: String(combinedSignal.reason || 'superseded'),
};
}
if (this._trackingRestoreController === controller)
this._trackingRestoreController = null;
if (result.status === 'found') {
const terminal = {
...result,
...selected,
classification: 'followed',
cleared: false,
};
this._publishTrackingRestoreStatus(terminal);
return terminal;
}
if (['cancelled', 'superseded', 'destroyed'].includes(result.status))
return result;
// A subject that is simply not here YET is not a subject that is gone. Hold
// it on the layer's own deferred-restore latch for its source-specific
// window before any verdict is reached or shown. `unsupported` layers have
// no latch to arm, so they still decide immediately.
if (result.status === 'missing' || result.status === 'source-unavailable') {
return this._beginPendingTrackingRestore(
selected,
result,
combinedSignal,
);
}
const classification =
result.status === 'missing'
? this._classifyMissingTrackingTarget(selected)
: 'source-unavailable';
const cleared = this._passivelyClearTrackingSelection(selected);
const terminal = { ...result, ...selected, classification, cleared };
this._publishTrackingRestoreStatus(terminal);
return terminal;
}
destroy() {
if (this._destroyed) return;
this._destroyed = true;
for (const controller of this._restoreControllers.values())
controller.abort('coordinator-destroyed');
this._restoreControllers.clear();
this._revokePendingTrackingWatch('coordinator-destroyed');
this._trackingRestoreController = null;
this._unsubscribe?.();
this._unsubscribe = null;
this._unsubscribeVisibilityRequests?.();
this._unsubscribeVisibilityRequests = null;
this.shareLinkManager?.setLayerStateProvider?.(null);
this.onDurableStateChange = null;
this.onTrackingRestoreStatus = null;
}
}
+1 -1
View File
@@ -11,7 +11,6 @@ import {
LAYER_STATE_STORAGE_KEY,
LAYER_STATE_TOKEN_ALPHABET,
LAYER_STATE_TOKEN_RESERVATIONS,
LayerStateCoordinator,
REGISTERED_LAYER_IDS,
SHARE_TRACKING_RESTORE_POLICIES,
createDefaultLayerState,
@@ -24,6 +23,7 @@ import {
validateLayerStateAllocations,
validateLayerStateRegistry,
} from './layerState.js';
import { LayerStateCoordinator } from './layerStateCoordinator.js';
import radioLayer from './radio.js';
import { stampInitialShareGesture } from '../navigationPolicy.js';
+742
View File
@@ -0,0 +1,742 @@
/**
* Applies layer state to the running application: restores shared and stored
* layer choices and persists changes to browser storage.
*/
import {
LAYER_RESTORE_ORIGINS,
LAYER_STATE_REGISTRY,
LAYER_STATE_STORAGE_KEY,
REGISTRY_BY_ID,
SHARE_TRACKING_RESTORE_POLICIES,
cloneLayerState,
createDefaultLayerState,
isExplicitLayerStateOrigin,
layerOptionsForRestore,
normalizeLayerState,
optionSpecs,
parseStoredLayerState,
serializeStoredLayerState,
} from './layerState.js';
/** Re-check cadence while a shared subject waits for its feed row to arrive. */
const PENDING_TRACKING_POLL_MS = 1_000;
const TRACKING_OPTION_KEY_BY_LAYER = Object.freeze({
flights: 'selectedFlightsTrackingId',
military: 'selectedMilitaryTrackingId',
satellites: 'selectedSatTrackingId',
});
function safeStorage() {
try {
return globalThis.localStorage || null;
} catch {
return null;
}
}
function currentLayerOutcome(dataManager, layerId) {
const state = dataManager.getLayerLifecycleState?.(layerId);
return {
settledEnabled: Boolean(state?.enabled),
lifecycleState: state?.lifecycleState || 'missing',
lifecycleUncertain: Boolean(state?.uncertain),
};
}
/**
* Owns durable user layer preferences independently from transient runtime
* choreography, and coordinates passive post-registration restoration.
*/
export class LayerStateCoordinator {
constructor(
dataManager,
shareLinkManager,
{
storage = safeStorage(),
restoreGate = null,
onDurableStateChange = null,
onTrackingRestoreStatus = null,
now = () => Date.now(),
// Injectable so the pending-window behavior is deterministically testable
// without sleeping out a 90 s expiry.
setTimer = (fn, ms) => setTimeout(fn, ms),
clearTimer = (handle) => clearTimeout(handle),
} = {},
) {
if (!dataManager?.registrationsFinalized) {
throw new Error(
'Layer state requires finalized data-layer registrations',
);
}
this.dataManager = dataManager;
this.shareLinkManager = shareLinkManager || null;
this.storage = storage;
this.restoreGate = restoreGate;
this.onDurableStateChange = onDurableStateChange;
this.onTrackingRestoreStatus = onTrackingRestoreStatus;
this.now = now;
this.setTimer = setTimer;
this.clearTimer = clearTimer;
this._durableState = createDefaultLayerState();
this._source = 'defaults';
this._destroyed = false;
this._restoreControllers = new Map();
this._shareCreatedAtMs = null;
this._trackingRestoreController = null;
this._trackingRestoreGeneration = 0;
this._pendingTrackingTimer = null;
this._pendingTrackingContext = null;
this._unsubscribe = this.dataManager.subscribe((change) =>
this._handleManagerChange(change),
);
this._unsubscribeVisibilityRequests =
this.dataManager.subscribeVisibilityRequests((change) =>
this._handleVisibilityRequest(change),
);
this.restorePromise = Promise.resolve([]);
this.lastRestoreResults = [];
}
start({
shareLayerState = null,
allowLocalState = true,
shareCreatedAtMs = null,
} = {}) {
if (this._destroyed)
throw new Error('Layer-state coordinator is destroyed');
let selected = shareLayerState
? normalizeLayerState(shareLayerState)
: null;
if (selected) {
this._source = 'share';
this._shareCreatedAtMs = Number.isFinite(shareCreatedAtMs)
? shareCreatedAtMs
: null;
} else if (allowLocalState) {
let stored = null;
try {
stored = parseStoredLayerState(
this.storage?.getItem?.(LAYER_STATE_STORAGE_KEY),
);
} catch {
/* best effort */
}
if (stored) {
selected = stored;
this._source = 'local';
}
} else {
// A valid historical camera/style share with no v2 layer payload keeps
// the exact legacy default-layer behavior. It must not inherit an
// unrelated recipient's saved local layer preferences.
this._source = 'legacy-share';
}
this._durableState = selected || createDefaultLayerState();
this.shareLinkManager?.setLayerStateProvider?.(() =>
this.getDurableState(),
);
this.shareLinkManager?.onLayerStateChange?.();
this._notifyDurableState();
if (!selected) return this.restorePromise;
this.restorePromise = this._restoreSelectedState(
this._source === 'share'
? LAYER_RESTORE_ORIGINS.share
: LAYER_RESTORE_ORIGINS.local,
);
return this.restorePromise;
}
get source() {
return this._source;
}
getDurableState() {
return cloneLayerState(this._durableState);
}
_notifyDurableState() {
try {
this.onDurableStateChange?.(this.getDurableState());
} catch {
/* UI sync is best effort */
}
}
_handleVisibilityRequest(change) {
if (!isExplicitLayerStateOrigin(change?.origin)) return;
this._restoreControllers
.get(change.layerId)
?.abort('superseded-by-explicit-visibility');
if (SHARE_TRACKING_RESTORE_POLICIES[change.layerId]) {
this._revokePendingTrackingWatch('superseded-by-explicit-visibility');
}
}
/** Revoke every passive restore before explicit navigation can be reclaimed. */
cancelPendingRestores(reason = 'superseded-by-explicit-navigation') {
for (const controller of this._restoreControllers.values())
controller.abort(reason);
this._revokePendingTrackingWatch(reason);
}
/**
* Revoke a pending shared Follow. Physical navigation may also clear only
* the exact passive selection, without writing recipient preferences.
*/
cancelPendingShareTracking(
reason = 'superseded-by-explicit-navigation',
{ clearSelection = false } = {},
) {
this._revokePendingTrackingWatch(reason);
if (!clearSelection) return false;
const selected = this._selectedShareTrackingTarget();
return selected ? this._passivelyClearTrackingSelection(selected) : false;
}
_handleManagerChange(change) {
if (!change || this._destroyed) return;
// Parameter and visibility ownership are independent. A newer explicit
// option request may replace passive share options, but it must not abort
// the same layer's visibility lifecycle.
if (change.type === 'params-requested') {
if (
isExplicitLayerStateOrigin(change.origin) &&
SHARE_TRACKING_RESTORE_POLICIES[change.layerId]
) {
this._revokePendingTrackingWatch('superseded-by-explicit-params');
}
return;
}
// A layer that goes away takes its latch with it, at ANY origin — a
// programmatic disable or teardown never reaches the explicit-intent path
// below, so revoke here before that early return.
if (
change.type === 'visibility' &&
change.enabled === false &&
SHARE_TRACKING_RESTORE_POLICIES[change.layerId]
) {
this._revokePendingTrackingWatch('owner-layer-disabled');
}
if (!isExplicitLayerStateOrigin(change.origin)) return;
if (change.type === 'visibility') {
this._restoreControllers
.get(change.layerId)
?.abort('superseded-by-explicit-visibility');
if (SHARE_TRACKING_RESTORE_POLICIES[change.layerId]) {
this._revokePendingTrackingWatch('superseded-by-explicit-visibility');
}
const enabled = new Set(this._durableState.enabledLayerIds);
if (change.enabled) enabled.add(change.layerId);
else enabled.delete(change.layerId);
this._commitExplicit({
...this._durableState,
enabledLayerIds: [...enabled],
});
return;
}
if (change.type !== 'params') return;
const entry = REGISTRY_BY_ID.get(change.layerId);
if (!entry?.optionOwner) return;
const ownerId = entry.optionOwner;
const nextOwnerOptions = { ...this._durableState.options[ownerId] };
const requestedParams = change.requestedParams || {};
const trackingOptionKey =
TRACKING_OPTION_KEY_BY_LAYER[change.layerId] || null;
let changed = false;
for (const spec of optionSpecs(ownerId)) {
// Only persist keys present in this explicit request. getLayerParams()
// can return a wider live snapshot containing transient or passively
// changed values that this user action did not own. Tracking is the one
// exception: an unrelated explicit option cancels a pending restoration
// in that family, so its wider live value (active ID or null) must replace
// the formerly durable pending ID instead of allowing reload resurrection.
const explicitlyRequested = Object.hasOwn(requestedParams, spec.key);
const implicitTrackingSync =
!explicitlyRequested && spec.key === trackingOptionKey;
if (!explicitlyRequested && !implicitTrackingSync) continue;
const value = spec.normalize(change.params[spec.key]);
if (value === null) {
if (implicitTrackingSync) {
nextOwnerOptions[spec.key] = null;
changed = true;
continue;
}
if (spec.defaultValue !== null || change.params[spec.key] !== null)
continue;
}
nextOwnerOptions[spec.key] = value;
changed = true;
}
if (!changed) return;
this._commitExplicit({
...this._durableState,
options: { ...this._durableState.options, [ownerId]: nextOwnerOptions },
});
}
_commitExplicit(candidate) {
this._durableState = normalizeLayerState(candidate);
const serialized = serializeStoredLayerState(this._durableState);
try {
if (this.storage?.getItem?.(LAYER_STATE_STORAGE_KEY) !== serialized) {
this.storage?.setItem?.(LAYER_STATE_STORAGE_KEY, serialized);
}
} catch {
/* storage can be unavailable or quota-limited */
}
this.shareLinkManager?.onLayerStateChange?.();
this._notifyDurableState();
}
async _waitForRestoreGate() {
if (!this.restoreGate) return;
await (typeof this.restoreGate === 'function'
? this.restoreGate()
: this.restoreGate);
}
async _restoreSelectedState(origin) {
for (const entry of LAYER_STATE_REGISTRY) {
this._restoreControllers.set(entry.id, new AbortController());
}
try {
await this._waitForRestoreGate();
const enabled = new Set(this._durableState.enabledLayerIds);
const settled = await Promise.allSettled(
LAYER_STATE_REGISTRY.map(async (entry) => {
const controller = this._restoreControllers.get(entry.id);
const targetEnabled = enabled.has(entry.id);
const options = layerOptionsForRestore(this._durableState, entry.id);
if (origin === LAYER_RESTORE_ORIGINS.share && options) {
for (const trackingKey of Object.values(
TRACKING_OPTION_KEY_BY_LAYER,
)) {
delete options[trackingKey];
}
}
if (this._destroyed || controller?.signal.aborted) {
return {
layerId: entry.id,
targetEnabled,
origin,
phase: 'reserved',
...currentLayerOutcome(this.dataManager, entry.id),
appliedOptions: {},
cancellationReason: this._destroyed ? 'destroyed' : 'superseded',
errorClass: 'cancelled',
persistenceWrite: false,
succeeded: false,
};
}
// Reserve passive option state before any asynchronous lifecycle work.
// A later explicit params intent then wins on its own lane without
// cancelling or being overwritten by the visibility restore.
const paramsSucceeded =
!options ||
Object.keys(options).length === 0 ||
this.dataManager.setLayerParams(entry.id, options, { origin });
return this.dataManager
.restoreLayerState(
entry.id,
{
enabled: targetEnabled,
params: null,
},
{ origin, signal: controller.signal },
)
.then((result) => ({
...result,
appliedOptions: paramsSucceeded && options ? options : {},
errorClass: paramsSucceeded
? result.errorClass
: 'ParamsRejected',
succeeded: paramsSucceeded && result.succeeded,
}));
}),
);
this.lastRestoreResults = settled.map((result, index) => {
if (result.status === 'fulfilled') return result.value;
const entry = LAYER_STATE_REGISTRY[index];
return {
layerId: entry.id,
targetEnabled: enabled.has(entry.id),
origin,
phase: 'coordinator',
...currentLayerOutcome(this.dataManager, entry.id),
appliedOptions: {},
cancellationReason: null,
errorClass: result.reason?.name || 'Error',
error: String(result.reason?.message || result.reason),
persistenceWrite: false,
succeeded: false,
};
});
return this.lastRestoreResults.map((result) => ({ ...result }));
} finally {
this._restoreControllers.clear();
this._notifyDurableState();
}
}
_selectedShareTrackingTarget() {
if (this._source !== 'share') return null;
for (const [layerId, policy] of Object.entries(
SHARE_TRACKING_RESTORE_POLICIES,
)) {
const targetId =
this._durableState.options?.[policy.optionOwner]?.[policy.optionKey];
if (targetId !== null && targetId !== undefined && targetId !== '') {
return { layerId, targetId, ...policy };
}
}
return null;
}
_passivelyClearTrackingSelection(selected) {
const current =
this._durableState.options?.[selected.optionOwner]?.[selected.optionKey];
if (String(current) !== String(selected.targetId)) return false;
const ownerOptions = {
...this._durableState.options[selected.optionOwner],
[selected.optionKey]: null,
};
this._durableState = normalizeLayerState({
...this._durableState,
options: {
...this._durableState.options,
[selected.optionOwner]: ownerOptions,
},
});
this.dataManager.setLayerParams?.(
selected.layerId,
{ [selected.optionKey]: null },
{ origin: LAYER_RESTORE_ORIGINS.share },
);
this.shareLinkManager?.onLayerStateChange?.();
this._notifyDurableState();
return true;
}
/**
* `atMs` is the moment the subject was first found ABSENT, not the moment the
* verdict is delivered. Waiting out the pending window must not by itself
* push a fresh link into the "expired" wording — that word describes the
* SHARE's age, not how long this client watched for the subject.
*/
_classifyMissingTrackingTarget(selected, atMs = this.now()) {
const copiedAt = this._shareCreatedAtMs;
if (!Number.isFinite(copiedAt)) return 'unavailable';
const ageMs = atMs - copiedAt;
return ageMs > selected.expiryWindowMs ? 'expired' : 'unavailable';
}
/** Whether the owning layer currently follows the shared subject. */
_trackingTargetLatched(selected) {
const params = this.dataManager.getLayerParams?.(selected.layerId);
const active = params?.[selected.optionKey];
return (
active !== null &&
active !== undefined &&
String(active) === String(selected.targetId)
);
}
/** Stop watching a pending shared subject without deciding its fate. */
_cancelPendingTrackingWatch() {
if (this._pendingTrackingTimer !== null)
this.clearTimer(this._pendingTrackingTimer);
this._pendingTrackingTimer = null;
const pending = this._pendingTrackingContext;
if (pending?.signal && pending.abortHandler) {
pending.signal.removeEventListener('abort', pending.abortHandler);
pending.abortHandler = null;
}
}
/** Publish a share-follow lifecycle update without allowing UI errors to own state. */
_publishTrackingRestoreStatus(status) {
try {
this.onTrackingRestoreStatus?.(status);
} catch {
/* status UI is best effort */
}
}
/**
* Revoke a pending shared Follow wholesale.
*
* The watch and the LAYER's own deferred-restore latch are two halves of one
* mechanism, so they must die together. Aborting only the restore controller
* left the timer alive: the controller has already settled by the time the
* watch exists, so the abort was a no-op and the orphaned timer went on to
* announce "Shared … unavailable" for a subject whose latch had been
* cancelled — a notice about work no longer being attempted.
*/
_revokePendingTrackingWatch(reason) {
this._trackingRestoreController?.abort(reason);
this._trackingRestoreGeneration += 1;
this._cancelPendingTrackingWatch();
const pending = this._pendingTrackingContext;
this._pendingTrackingContext = null;
if (pending) {
this.dataManager.cancelPendingLayerRestore?.(pending.selected.layerId, {
origin: LAYER_RESTORE_ORIGINS.share,
reason: String(reason || 'cancelled'),
});
this._publishTrackingRestoreStatus({
...pending.probe,
...pending.selected,
status: 'cancelled',
classification: 'cancelled',
reason: String(reason || 'cancelled'),
cleared: false,
});
}
}
/**
* Hold a not-yet-arrived shared subject PENDING instead of declaring it gone.
*
* A recipient's first authoritative refresh routinely lands without a given
* contact — the feed is polled, coverage is partial, and rendering trails the
* snapshot by a poll. Reload-from-local already survives this: its restore
* arms the layer's own deferred-restore latch, which re-attempts on every
* later poll. The shared path used to decide on that single refresh, clear
* the subject from durable state AND from the URL, then post a failure notice
* seconds into startup — so the same link healed on reload but never on the
* share.
*
* Arm the SAME latch, then watch it in the background: the caller is never
* blocked (startup must not wait out a 90 s window before it may write the
* URL again), and the terminal verdict is deferred until the source-specific
* window has genuinely expired. The existing wordings are unchanged.
*/
async _beginPendingTrackingRestore(selected, probe, signal = null) {
const generation = this._trackingRestoreGeneration;
const absentAtMs = this.now();
// Arm the layer's deferred-restore latch under the passive share origin, so
// it re-attempts each poll and never rewrites recipient preferences.
let armed = false;
let armError = null;
try {
armed =
(await this.dataManager.setLayerParams?.(
selected.layerId,
{ [selected.optionKey]: selected.targetId },
{ origin: LAYER_RESTORE_ORIGINS.share },
)) === true;
} catch (error) {
armError = error;
}
if (
this._destroyed ||
generation !== this._trackingRestoreGeneration ||
signal?.aborted
) {
if (armed) {
this.dataManager.cancelPendingLayerRestore?.(selected.layerId, {
origin: LAYER_RESTORE_ORIGINS.share,
reason: String(signal?.reason || 'superseded'),
});
}
return {
...probe,
...selected,
status: 'cancelled',
classification: 'cancelled',
reason: String(signal?.reason || 'superseded'),
cleared: false,
};
}
if (!armed) {
const terminal = {
...probe,
...selected,
status: 'source-unavailable',
classification: 'source-unavailable',
reason: String(
armError?.message || armError || 'tracking restore latch rejected',
),
cleared: this._passivelyClearTrackingSelection(selected),
};
this._publishTrackingRestoreStatus(terminal);
return terminal;
}
const deadline = this.now() + selected.expiryWindowMs;
const settle = (terminal) => {
if (this._pendingTrackingContext?.generation !== generation) return;
this._cancelPendingTrackingWatch();
this._pendingTrackingContext = null;
this._publishTrackingRestoreStatus(terminal);
};
const poll = () => {
this._pendingTrackingTimer = null;
if (this._destroyed || generation !== this._trackingRestoreGeneration)
return;
// The layer that owns the latch may have gone away since the last tick
// (disable, teardown, replacement). There is nothing left attempting this
// restore, so abandon it silently rather than announcing a verdict.
if (this.dataManager.isEffectivelyEnabled?.(selected.layerId) === false) {
this._revokePendingTrackingWatch('owner-layer-disabled');
return;
}
if (this._trackingTargetLatched(selected)) {
settle({
...probe,
...selected,
status: 'found',
classification: 'followed',
cleared: false,
});
return;
}
if (this.now() >= deadline) {
// The window really has elapsed — only now does the verdict apply.
const classification =
probe.status === 'missing'
? this._classifyMissingTrackingTarget(selected, absentAtMs)
: 'source-unavailable';
const cleared = this._passivelyClearTrackingSelection(selected);
settle({ ...probe, ...selected, classification, cleared });
return;
}
this._pendingTrackingTimer = this.setTimer(
poll,
PENDING_TRACKING_POLL_MS,
);
this._pendingTrackingTimer?.unref?.();
};
this._cancelPendingTrackingWatch();
const pending = {
...probe,
...selected,
status: 'pending',
classification: 'pending',
cleared: false,
};
const pendingContext = {
generation,
selected,
probe,
signal,
abortHandler: null,
};
if (signal) {
pendingContext.abortHandler = () => {
if (this._pendingTrackingContext?.generation !== generation) return;
this._revokePendingTrackingWatch(signal.reason || 'aborted');
};
signal.addEventListener('abort', pendingContext.abortHandler, {
once: true,
});
}
this._pendingTrackingContext = pendingContext;
this._publishTrackingRestoreStatus(pending);
this._pendingTrackingTimer = this.setTimer(poll, PENDING_TRACKING_POLL_MS);
this._pendingTrackingTimer?.unref?.();
return pending;
}
/**
* Refresh and restore the one shareable tracked target after destination
* camera and ordinary layer restoration have settled.
*/
async restoreShareTrackingSelection({ signal = null } = {}) {
const selected = this._selectedShareTrackingTarget();
if (!selected || this._destroyed)
return { status: 'skipped', reason: 'no-shared-target' };
this._revokePendingTrackingWatch('superseded-by-newer-restore');
const controller = new AbortController();
const combinedSignal = signal
? AbortSignal.any([signal, controller.signal])
: controller.signal;
this._trackingRestoreController = controller;
const generation = ++this._trackingRestoreGeneration;
let result;
try {
result = await this.dataManager.resolveLayerTrackingTarget(
selected.layerId,
selected.targetId,
{ signal: combinedSignal, origin: LAYER_RESTORE_ORIGINS.share },
);
} catch (error) {
result = combinedSignal.aborted
? {
status: 'cancelled',
reason: String(combinedSignal.reason || 'aborted'),
}
: {
status: 'source-unavailable',
reason: String(error?.message || error),
};
}
if (
generation !== this._trackingRestoreGeneration ||
this._destroyed ||
combinedSignal.aborted
) {
if (this._trackingRestoreController === controller)
this._trackingRestoreController = null;
return {
...result,
status: 'cancelled',
reason: String(combinedSignal.reason || 'superseded'),
};
}
if (this._trackingRestoreController === controller)
this._trackingRestoreController = null;
if (result.status === 'found') {
const terminal = {
...result,
...selected,
classification: 'followed',
cleared: false,
};
this._publishTrackingRestoreStatus(terminal);
return terminal;
}
if (['cancelled', 'superseded', 'destroyed'].includes(result.status))
return result;
// A subject that is simply not here YET is not a subject that is gone. Hold
// it on the layer's own deferred-restore latch for its source-specific
// window before any verdict is reached or shown. `unsupported` layers have
// no latch to arm, so they still decide immediately.
if (result.status === 'missing' || result.status === 'source-unavailable') {
return this._beginPendingTrackingRestore(
selected,
result,
combinedSignal,
);
}
const classification =
result.status === 'missing'
? this._classifyMissingTrackingTarget(selected)
: 'source-unavailable';
const cleared = this._passivelyClearTrackingSelection(selected);
const terminal = { ...result, ...selected, classification, cleared };
this._publishTrackingRestoreStatus(terminal);
return terminal;
}
destroy() {
if (this._destroyed) return;
this._destroyed = true;
for (const controller of this._restoreControllers.values())
controller.abort('coordinator-destroyed');
this._restoreControllers.clear();
this._revokePendingTrackingWatch('coordinator-destroyed');
this._trackingRestoreController = null;
this._unsubscribe?.();
this._unsubscribe = null;
this._unsubscribeVisibilityRequests?.();
this._unsubscribeVisibilityRequests = null;
this.shareLinkManager?.setLayerStateProvider?.(null);
this.onDurableStateChange = null;
this.onTrackingRestoreStatus = null;
}
}
+4
View File
@@ -1,3 +1,5 @@
import { cancelCameraArrival } from './cameraArrival.js';
function cloneLayerParams(value) {
if (Array.isArray(value)) return value.map(cloneLayerParams);
if (value && typeof value === 'object') {
@@ -1074,6 +1076,7 @@ export class LayerLifecycle {
const entry = this.layers.get(layerId);
if (!entry) return { intentEpoch: null, promise: Promise.resolve() };
const desiredState = Boolean(shouldEnable);
if (!desiredState) cancelCameraArrival(this.viewer);
if (entry.destroying) {
return {
intentEpoch: null,
@@ -2095,6 +2098,7 @@ export class LayerLifecycle {
async destroyLayer(layerId) {
const entry = this.layers.get(layerId);
if (!entry || entry.destroying) return false;
cancelCameraArrival(this.viewer);
entry.destroying = true;
this._invalidateRefresh(layerId, entry, 'layer-destroyed');
// Teardown becomes authoritative before the first await. Advancing the
+16 -107
View File
@@ -1,4 +1,10 @@
import * as Cesium from 'cesium';
import {
layerTitle,
mapAnalystRecord,
parseGeojsonLines,
} from '../sources/infrastructureData.js';
import { createInfrastructureOverlayEntry } from './infrastructureOverlayEntry.js';
import { isPointerFree } from './inputOwnership.js';
import {
selectInfraLod,
@@ -14,10 +20,6 @@ const VISIBILITY_UPDATE_MS = 450;
export const LOCAL_OVERLAY_COHORT_LIMIT = 160;
const LOCAL_OVERLAY_COLLISION_CAPACITY = 96;
const LOCAL_OVERLAY_CELL_SURPLUS = 2;
const LOCAL_OVERLAY_MAX_DISTANCE_M = 14000000;
const LOCAL_OVERLAY_FADE_START_M = 250000;
const LOCAL_OVERLAY_FADE_START_RATIO =
LOCAL_OVERLAY_FADE_START_M / LOCAL_OVERLAY_MAX_DISTANCE_M;
// Stems are anchored at ellipsoid height 0, but high-elevation features
// (e.g. dams in river canyons) sit hundreds of meters above the ellipsoid,
// burying the short close-in stem inside the photoreal mesh. Once the
@@ -89,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.
@@ -182,33 +112,15 @@ export function createLocalInfrastructureOverlayEntry({
accent,
}) {
const copy = localInfrastructureOverlayCopy(properties, layerId);
return {
id: String(id),
return createInfrastructureOverlayEntry({
id,
source: layerId,
position,
variant: 'card',
title: copy.title,
details: copy.details,
accent,
priority,
collisionGroup: 'ambient-card',
zIndex: 30,
interactive: false,
minDistance: 0,
maxDistance: LOCAL_OVERLAY_MAX_DISTANCE_M,
distanceFadeStartRatio: LOCAL_OVERLAY_FADE_START_RATIO,
distanceScale: {
near: 250000,
nearValue: 1,
far: 9000000,
farValue: 0.62,
},
edgeFade: 'keyhole',
horizonCull: true,
terrainOcclusion: false,
gapPx: 15,
placement: 'above',
};
});
}
/**
@@ -394,6 +306,7 @@ export function createLocalGeoJsonLayer(
color,
icon = '📍',
source = 'Local JSONL',
osmDerived = false,
labels = true,
labelMax = DEFAULT_LABEL_MAX,
labelGridPx = DEFAULT_LABEL_GRID_PX,
@@ -409,6 +322,8 @@ export function createLocalGeoJsonLayer(
clearSelectedEntityContextForLayer,
removeEntityContextsForLayer,
governorRequestRender,
showOsmCredit,
hideOsmCredit,
},
) {
let _dataSource = null;
@@ -538,6 +453,7 @@ export function createLocalGeoJsonLayer(
};
const disableLayer = (viewer) => {
hideOsmCredit?.(viewer, id);
_enabled = false;
clearGroundRetryRender();
_activeLodIds = new Set();
@@ -681,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;
}
@@ -942,6 +854,7 @@ export function createLocalGeoJsonLayer(
}
if (_destroyed) return;
if (_enabled && _count > 0 && osmDerived) showOsmCredit?.(viewer, id);
// 3. Add an incredibly fast pre-render occluder to hide points behind the globe
if (_enabled && !_preRenderRemover) {
_preRenderRemover = viewer.scene.preRender.addEventListener(() => {
@@ -1376,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 };
+70 -1
View File
@@ -1,4 +1,4 @@
# Natural Earth physical regions pack
# Natural Earth packs
Offline named-region polygons for the voice-annotation resolver
(`src/data/naturalEarthRegions.js`) — "outline the Alps" resolves to the real
@@ -8,6 +8,8 @@ range geometry with no network dependency.
|------|----------------|----------|
| `regions.json` | `ne_10m_geography_regions_polys` (ranges, deserts, plateaus, peninsulas, islands, …) | 1,046 named |
| `marine.json` | `ne_10m_geography_marine_polys` (seas, gulfs, straits, bays, …) | 292 named |
| `countries.json` | `ne_10m_admin_0_countries` (countries) | 256 |
| `states_provinces.json` | `ne_10m_admin_1_states_provinces` (states, provinces and other first-level units) | 4,587 named |
**Source:** Natural Earth 10m physical vectors, via the canonical
[nvkelso/natural-earth-vector](https://github.com/nvkelso/natural-earth-vector)
@@ -34,3 +36,70 @@ See DATA_SOURCES.md.
Duplicate names exist upstream (two "Cordillera Oriental", a sliver + real
"Canadian Shield", …); the lookup module resolves ties by largest area.
## States and provinces (`states_provinces.json`)
Worldwide first-level administrative units for area annotations ("outline
Texas", "outline Bavaria"), read by `src/data/adminBoundaries.js`.
**Source:** `geojson/ne_10m_admin_1_states_provinces.geojson` from
[nvkelso/natural-earth-vector](https://github.com/nvkelso/natural-earth-vector)
at commit `ca96624a56bd078437bca8184e78163e5039ad19` (SHA-256
`22d0e3ad85eb3e27f17cabf8ba2d50e554fbc27a87796ff891d958185da62fb5`), fetched
2026-09-25. `geojson/ne_10m_populated_places_simple.geojson` from the same
commit (SHA-256
`fd3fa867a320cbd5c5b6bb5bc550afeec2939fb2cef688e508007282a55ac42f`) is read
only to mark ambiguous names; none of it is bundled.
**License:** public domain, as above.
**Regenerate:** `node scripts/build-admin-packs.mjs --only admin1` downloads
the pinned files (to the system temp directory, or `--cache <dir>`), checks
their SHA-256 and rewrites the pack byte for byte. Parameters are in the
script's `PARAMS` and in the pack's `meta.curation`.
**Schema:** `meta` (source, URLs, hashes, license, curation) and `features[]`
with `name`, `nameEn` (when it differs), `alt[]` (Latin-script `name_alt`
variants and abbreviations), `postal`, `type` (`type_en`), `rank`
(`min_label`), `country` (`admin`), `iso2`, `iso` (ISO 3166-2), `amb[]`,
`ambAbroad[]`, `cityState`, `label` ([lon, lat] label point) and
`polygons`. `polygons` is a list of parts, largest first, each
`[outer, ...holes]`; every ring is open and stored as integers in units of
10^-3 degrees (10^-`d` when a feature carries `d`), the first vertex absolute
and each later one as a `[dLon, dLat]` delta.
**Curation:**
- The 7 unnamed features and 2 degenerate Nauru districts are dropped
(4,596 → 4,587).
- Douglas-Peucker per ring at 0.004 × √(unit area in km²) km, clamped to
0.005°–0.02° (Texas ≈ 0.02°, Bavaria ≈ 0.01°); coordinates rounded to
3 decimals. The 75 units too small to survive (Maldives atolls, Malta's
councils) keep 4 decimals at 0.001° (`d: 4`); 3 smaller still (Vatican
City) keep their source rings at 5 decimals (`d: 5`).
- Parts under 20 km² or under 0.03% of the unit's largest part are dropped;
holes under 20 km² are dropped. Outers are counter-clockwise, holes
clockwise.
- `amb` lists names that are also a country (`admin`/`geonunit` of any
unit), a populated place of 200,000+ (`pop_max`) inside the unit's box
when the unit is larger than 1,000 km², or a populated place of 50,000+
elsewhere in the same country; `ambAbroad` lists names shared only with a
populated place of 50,000+ in another country. `cityState` marks a unit of
at most 1,000 km² that contains its namesake city (Berlin, Vienna, Paris).
548 units carry `amb`, 108 `ambAbroad`, 138 `cityState`.
- Result: 3,648,916 bytes (1,260,545 with gzip -9); 6,492 parts, 80 holes,
407,339 vertices.
## Countries (`countries.json`)
Natural Earth 10m admin-0 countries, public domain under the terms above.
Source: `geojson/ne_10m_admin_0_countries.geojson` at the same pinned commit;
SHA-256 `239eec57ac17f100a11e2536cffc56752c318b50ae765b0918ff7aab4ce8f255`.
Regenerate with `node scripts/build-admin-packs.mjs --only countries`.
Uses the state/province encoding and simplification, retaining parts of at least
20 km² and 0.003% of the largest part (including Alaska and Hawaii); the largest
part always stays. Holes under 20 km² are dropped.
Includes England, Scotland, Wales and Northern Ireland from
`ne_10m_admin_0_map_units.geojson` at the same commit (SHA-256
`57da82be755f4afccd8f3b14251bb2752f5df1395f47d2d86f817470c4a48862`).
Result: 945,843 bytes; 260 features, 2,088 parts, 122,788 vertices.
@@ -0,0 +1 @@
{"meta":{"title":"Natural Earth admin-0 countries","mapUnits":{"url":"https://raw.githubusercontent.com/nvkelso/natural-earth-vector/ca96624a56bd078437bca8184e78163e5039ad19/geojson/ne_10m_admin_0_map_units.geojson","sha256":"57da82be755f4afccd8f3b14251bb2752f5df1395f47d2d86f817470c4a48862"},"source":"ne_10m_admin_0_countries","url":"https://raw.githubusercontent.com/nvkelso/natural-earth-vector/ca96624a56bd078437bca8184e78163e5039ad19/geojson/ne_10m_admin_0_countries.geojson","sha256":"239eec57ac17f100a11e2536cffc56752c318b50ae765b0918ff7aab4ce8f255","commit":"ca96624a56bd078437bca8184e78163e5039ad19","license":"Public domain (https://www.naturalearthdata.com/about/terms-of-use/)","decimals":3,"curation":{"toleranceFactor":0.004,"minToleranceDeg":0.005,"maxToleranceDeg":0.02,"decimals":3,"minPartKm2":20,"minPartShare":0.00003,"minHoleKm2":20},"script":"scripts/build-admin-packs.mjs"},"features":[{"name":"Afghanistan","alt":["Islamic State of Afghanistan"],"country":"Afghanistan","iso":"AFG","iso2":"AF","type":"Country","polygons":[[[74383,37127,-15,40,89,10,43,55,123,-1,98,67,73,-84,98,17,-103,100,-129,63,-139,-18,-205,51,-12,-27,-80,3,-36,-65,-479,-121,-112,45,142,76,14,90,-268,53,-189,-16,-164,-81,-64,-69,-277,-95,-133,-191,-398,-62,-423,-268,-184,-12,-100,83,-122,297,63,241,36,453,60,55,8,82,-61,46,-196,-51,-83,33,101,258,-1,67,-407,222,-15,-40,-159,13,-112,-41,20,-18,-85,-40,-17,-72,-36,-12,-77,-142,-216,-148,8,-34,-90,7,-7,-56,82,-71,36,-114,-35,-82,-90,-82,-150,8,-89,68,-117,-35,-293,-3,-131,-142,32,-191,32,-22,-130,-107,-66,-13,-122,65,-139,151,-97,18,28,-59,-80,50,-26,-17,10,-61,-151,27,-42,-72,-223,-54,-12,-47,-104,-2,-10,-93,-92,8,-166,-92,-142,138,-105,46,8,79,-34,40,-114,25,-72,-34,-58,62,-244,-97,-136,97,-26,59,-91,43,-712,-53,-151,54,-63,56,-225,67,-94,70,-22,-49,-80,-19,-38,-80,3,-85,-87,-88,-474,-24,-284,-115,-23,-64,23,-116,-174,-277,-11,-237,-116,-152,-174,-60,-38,-60,-107,8,-102,-55,-12,-106,-155,31,-124,-53,-184,-18,-152,-88,-326,-15,-20,-30,28,-56,102,-71,-137,-74,24,-100,-21,-88,-162,-49,-224,-140,-159,-21,-103,54,-146,-140,-52,153,-227,145,-402,-8,-212,123,-41,74,-82,-10,20,-70,-95,-158,-8,-90,-85,-43,-6,-64,51,-91,-72,-169,-10,-118,-105,-92,-12,-85,-248,-122,190,-197,-246,-12,-151,-168,35,-298,-41,-130,25,-73,144,-78,190,-4,75,-42,-89,-30,-3,-68,-261,-265,-5,-93,267,-809,-15,-161,-40,-61,47,-532,865,-122,55,-52,1,-81,83,-225,-24,-168,-958,-989,1634,-450,1091,89,517,-110,87,97,305,86,558,-30,1160,295,145,122,-122,101,86,187,17,192,-54,164,98,322,184,54,113,106,34,113,246,119,74,-6,18,-73,311,-28,419,126,5,61,-159,16,-42,119,127,-9,147,102,123,15,81,50,92,137,116,-59,243,-41,52,29,-131,29,108,40,149,-48,29,-74,99,-98,236,70,259,265,-48,193,17,170,-35,162,193,192,-50,117,101,85,11,171,60,47,224,40,109,-26,114,38,53,67,77,5,169,120,-8,64,-131,124,19,125,-56,85,-160,36,-111,174,26,90,130,27,325,-87,194,-18,340,26,22,42,56,-6,123,53,45,111,14,192,-130,86,-35,89,109,26,15,115,122,75,86,127,181,75,41,59,-2,63,129,118,-108,111,91,109,-39,83,11,56,-100,61,22,91,-159,232,-185,94,52,98,335,203,-11,44,63,86,173,-61,-10,83,102,19,164,81,20,47,113,27,-19,48,61,24,240,32,176,75,238,-3,324,47,581,15,262,-76,59,15,36,67,106,4,158,92,63,10,80,-42,5,60]]]},{"name":"Akrotiri Sovereign Base Area","alt":["Akrotiri"],"country":"Akrotiri Sovereign Base Area","iso":"WSB","iso2":"-99","type":"Country","polygons":[[[32970,34632,-7,-7,-9,18,12,9,24,-6,-2,27,-33,4,-19,-19,-19,9,-3,-7,-48,19,-6,22,-28,0,-13,-13,-13,-2,-7,-15,-32,5,-7,-7,1,-16,73,18,80,-14,2,-18,29,-45,-4,-25,89,0,-21,28,7,37]]]},{"name":"Aland","alt":["Åland Islands"],"country":"Aland","iso":"ALD","iso2":"AX","type":"Country","polygons":[[[20236,60232,42,47,-76,14,-21,-7,11,26,-14,11,-52,11,-33,21,-21,-5,-48,11,13,-27,-41,22,-68,0,-22,46,-24,4,-44,-6,-61,-45,42,-8,39,-34,31,32,35,-31,10,-32,-28,-10,-13,35,-13,-48,-21,-14,-7,-28,-34,15,5,-24,-9,-17,-22,9,-27,49,32,51,-48,7,22,-21,-62,-17,-21,-31,3,19,-8,7,-36,2,24,-49,11,-5,13,-42,68,-96,26,22,18,4,17,-6,-1,-10,9,-5,13,0,23,7,5,10,14,5,23,0,14,-9,17,-32,-3,-10,10,-2,10,6,-9,30,57,10,39,-7,5,11,-19,17,13,61,-34,-20,-23,17,-43,13,-27,22,-2,10,40,7,-7,-21,68,-20,-1,18,16,15,-37,24,-18,38,27,-12,22,9,9,18,-10,19,37,-3,-4,-17,-33,-38,8,-19,29,-21,-3,-24,7,0,17,32,29,16,77,6,-16,-12,-49,-2,-21,-10,-13,-25,-2,-14,8,-6,42,0,25,-9,13,3,26,29,29,5,-21,21]],[[20025,60042,79,-14,17,-11,-16,-21,18,-7,48,10,13,-6,-9,-22,50,34,-32,61,-30,22,-62,7,-33,-22,-34,-9]],[[19634,60240,-26,17,-40,-2,-31,-23,0,-31,-24,-17,14,-35,38,-19,20,5,18,29,-11,33,55,-41,24,2,0,33,-32,30]],[[20284,59958,23,-4,15,9,3,14,74,4,48,14,-10,10,-98,15,-37,-21,-21,-22]],[[20256,60145,8,-27,-6,-10,-36,42,-24,-46,4,-16,64,-28,12,6,-9,20,29,36,-20,20]],[[20411,60260,-31,10,-21,-4,-12,-13,-2,-15,8,-9,30,12,5,-19,-7,-23,12,-2,48,48,-2,13,-11,8]],[[20583,60059,-38,-3,-44,-43,26,-8,23,14,37,-11,19,15,2,13,-7,3,17,3,-8,3]]]},{"name":"Albania","alt":["Republic of Albania"],"country":"Albania","iso":"ALB","iso2":"AL","type":"Country","polygons":[[[2Line truncated
@@ -0,0 +1 @@
{"meta":{"title":"Natural Earth admin-1 states and provinces","source":"ne_10m_admin_1_states_provinces","url":"https://raw.githubusercontent.com/nvkelso/natural-earth-vector/ca96624a56bd078437bca8184e78163e5039ad19/geojson/ne_10m_admin_1_states_provinces.geojson","sha256":"22d0e3ad85eb3e27f17cabf8ba2d50e554fbc27a87796ff891d958185da62fb5","ambiguitySource":"https://raw.githubusercontent.com/nvkelso/natural-earth-vector/ca96624a56bd078437bca8184e78163e5039ad19/geojson/ne_10m_populated_places_simple.geojson","ambiguitySha256":"fd3fa867a320cbd5c5b6bb5bc550afeec2939fb2cef688e508007282a55ac42f","commit":"ca96624a56bd078437bca8184e78163e5039ad19","license":"Public domain (https://www.naturalearthdata.com/about/terms-of-use/)","schema":"features[]: name, nameEn?, alt?[], postal?, type, rank? (min_label: the zoom at which Natural Earth labels the unit; lower is more prominent), country, iso2, iso (ISO 3166-2), amb?[] (normalized names that are also a country or a city — alone or with the country named), ambAbroad?[] (names shared only with a city in another country — ambiguous alone, settled by naming the country), cityState? (the unit is its own city), label? [lon, lat] label point, d? (decimals when not meta.decimals), polygons[][ring] (first ring outer, rest holes; each ring open and encoded as integers in 10^-d degrees, first vertex absolute then [dLon, dLat] deltas)","decimals":3,"curation":{"toleranceFactor":0.004,"minToleranceDeg":0.005,"maxToleranceDeg":0.02,"decimals":3,"minPartKm2":20,"minPartShare":0.0003,"minHoleKm2":20,"ambiguity":{"insideMinPopulation":200000,"elsewhereMinPopulation":50000,"cityStateMaxKm2":1000}},"script":"scripts/build-admin-packs.mjs"},"features":[{"name":"Badakhshan","alt":["Badahšan"],"postal":"BD","type":"Province","rank":6.7,"country":"Afghanistan","iso2":"AF","iso":"AF-BDS","label":[70.804,36.781],"polygons":[[[74892,37231,-29,14,-74,86,-129,63,-29,-13,-110,-5,-66,18,-77,0,-34,27,-23,-7,-5,13,-12,-27,-80,3,-16,-13,-2,-34,-18,-18,-352,-81,-38,-28,-52,2,-37,-14,-29,25,-62,-9,-16,8,-5,21,34,34,59,9,49,33,14,90,-78,3,-190,50,-44,-1,-63,-27,-82,12,-36,-15,-49,-42,-41,0,-38,-24,-64,-69,-72,-6,-94,-55,-111,-34,-133,-191,-149,-18,-34,-14,-69,11,-146,-41,-133,-94,-290,-174,-88,-20,-96,8,-42,18,-58,65,-25,86,-65,92,-3,63,-29,56,19,150,37,50,7,41,-7,101,24,77,-14,81,5,43,20,28,21,82,-13,41,8,18,52,37,8,82,-61,46,-36,2,-160,-53,-83,33,15,72,29,44,57,142,-1,67,-24,30,-116,45,-101,73,-67,10,-52,57,-47,7,-15,-40,-65,20,-28,-13,-66,6,-35,-27,-77,-14,20,-18,-44,-33,-41,-7,-17,-72,-36,-12,-77,-142,-55,-26,-131,-113,-30,-9,-4,-17,12,-17,-47,-10,-43,17,-11,-25,4,-31,31,-50,51,-21,28,-45,8,-69,-35,-82,-32,-6,-17,-38,-41,-38,-29,-9,-125,19,-5,-20,6,-123,37,-21,-24,-28,-14,-83,30,-88,-1,-53,91,-92,-28,-47,-40,-16,-19,-39,-30,-19,-3,-307,33,-100,1,-53,102,-57,84,-78,137,-23,80,0,31,-20,3,-134,-46,-112,33,-67,-161,-91,-28,-30,-19,-45,-23,-135,63,-9,90,37,95,-31,20,-45,56,-55,-19,-69,2,-39,6,-13,33,-4,58,24,16,45,36,26,43,-40,28,-8,106,-5,19,32,-17,84,12,26,82,16,54,57,-45,41,4,35,47,24,42,56,23,87,85,89,-4,15,42,42,15,37,79,38,17,38,84,30,76,70,79,27,-16,28,5,16,33,30,30,56,39,-5,87,-57,47,1,11,11,-19,40,-2,32,102,19,102,64,62,17,16,13,-5,26,9,8,113,27,-22,35,20,22,132,32,152,15,63,43,113,32,238,-3,78,22,45,-10,51,22,150,13,75,-10,65,15,177,-3,131,18,133,-5,92,-19,82,-42,88,-15,59,15,21,59,15,8,106,4,158,92,63,10,64,-46,16,4,10,54,-28,14,-42,53,-94,44,-17,21,2,19,89,10,43,55,90,11,33,-12,28,29,25,4,45,34,16,-2,57,-82]]]},{"name":"Badghis","alt":["Badghes","Badghisat","Badgis"],"postal":"BG","type":"Province","rank":6.7,"country":"Afghanistan","iso2":"AF","iso":"AF-BDG","label":[63.85,35.27],"polygons":[[[63125,35860,-21,-4,-15,-13,-5,-17,28,-56,72,-58,25,-8,5,-12,-34,-26,-77,-20,-26,-21,0,-66,24,-34,-21,-88,-13,-11,-82,-11,-67,-27,-95,-60,-39,-101,-84,-115,-28,-70,-20,-148,16,-51,49,-17,34,-1,69,-50,46,-14,47,-29,82,-6,31,-35,8,-45,77,-11,62,-24,71,34,116,-10,73,-22,99,-52,76,-13,42,10,77,-19,58,-30,227,53,67,45,45,10,26,39,52,24,107,0,57,-15,110,6,20,29,33,11,123,-4,57,-12,78,30,179,40,48,29,21,56,10,100,-46,76,-74,50,-57,58,-22,9,-32,-1,-49,25,-35,46,-116,-14,-102,12,-101,29,-5,17,6,79,13,53,-1,22,-8,7,-118,-20,-47,21,11,27,-15,22,-220,19,-24,15,-2,107,-12,37,-43,46,-51,18,-8,11,28,54,3,44,26,86,3,83,-11,30,-30,14,-16,-2,-124,-53,-184,-18,-71,-57,-81,-31,-87,-15]]]},{"name":"Baghlan","alt":["Baglan","Bughlan"],"postal":"BL","type":"Province","rank":6.7,"country":"Afghanistan","iso2":"AF","iso":"AF-BGL","amb":["baghlan"],"label":[68.983,35.859],"polygons":[[[69956,35887,-32,41,-3,19,-20,20,-32,67,-81,65,-10,115,-12,19,-54,8,-11,61,-20,48,-10,3,-56,-32,-92,-26,-79,-48,-99,-17,-23,9,-21,-11,-33,47,-35,8,-35,41,-117,31,-82,62,-84,16,-53,-20,-20,7,-43,28,-64,64,-12,22,-88,-13,-96,20,-217,7,-82,23,-38,-14,-19,-14,-10,-28,152,-201,46,-50,120,-67,31,-26,9,-18,-64,-80,-129,-85,-14,-26,-12,-108,-156,-177,-4,-35,42,-29,-5,-30,-36,-42,-10,-34,-54,-46,-3,-25,20,-60,-12,-32,7,-36,-40,-99,-31,-16,-20,-28,-5,Line truncated
@@ -0,0 +1,23 @@
# Military area names
This database is distributed under [ODbL 1.0](https://opendatacommons.org/licenses/odbl/1-0/), separately from the MIT code.
© [OpenStreetMap contributors](https://www.openstreetmap.org/copyright).
Distribution: [Overture Maps Foundation](https://docs.overturemaps.org/attribution/), base/land_use release **2026-09-23.1**.
OSM snapshots: **2026-09-06**, with two named records from **2026-09-12**.
Selected named military areas; geometry simplified to interior label points,
bounds and area, coordinates rounded and records re-encoded. Names are unchanged.
Rows: `[typedOsmId, name, lon, lat, west, south, east, north, classIndex, areaM2]`.
Labels use five decimal places; bounds use four. Area is measured on the WGS84
ellipsoid. Class indices refer to `classes`. Output SHA-256: [`names.sha256`](names.sha256).
Rebuild (maintainer tool; requires Python and `duckdb==1.5.5`):
```sh
python scripts/build-military-names.py --cache /tmp/military-names
python scripts/build-military-names.py --cache /tmp/military-names --verify
```
The pinned parquet file list and query are in `scripts/military-names-files.json`
and `scripts/military-names.sql`. The first build downloads DuckDB extensions and
scans source columns; subsequent builds reuse the cached selected rows.
@@ -0,0 +1 @@
{"release":"2026-09-23.1","snapshots":["2026-09-06","2026-09-12"],"classes":["military_land","airfield","naval_base","range","barracks","base","training_area"],"records":[["r122949","Scott Air Force Base",-89.85277,38.54384,-89.8799,38.5292,-89.8041,38.5588,1,12413875],["r130448","Fort Campbell",-87.61569,36.63134,-87.8047,36.5355,-87.4231,36.7268,0,419957267],["r133445","Fort George G Meade",-76.74626,39.10227,-76.7766,39.0732,-76.7092,39.131,0,18817854],["r133514","Joint Base Andrews",-76.87745,38.80571,-76.9034,38.7823,-76.8491,38.829,5,17812387],["r139794","Whiteman AFB",-93.56134,38.72992,-93.5959,38.7024,-93.5308,38.7575,1,19580209],["r139848","Fort Leonard Wood",-92.16933,37.70114,-92.2496,37.6052,-92.0411,37.798,0,248751778],["r147157","Camp du Valdahon",6.32589,47.18508,6.2731,47.151,6.375,47.2189,0,34786523],["r169792","Offutt Air Force Base",-95.90817,41.1172,-95.9664,41.0986,-95.8781,41.1364,1,11200098],["r170440","McGuire Air Force Base",-74.57822,40.03444,-74.6026,40.0267,-74.5593,40.0586,5,5388371],["r171293","Cannon AFB",-103.31633,34.38639,-103.3371,34.3653,-103.2955,34.4054,1,13976465],["r171528","White Sands",-106.49662,32.38378,-106.5139,32.3657,-106.4793,32.4031,0,8047394],["r175474","West Point",-74.00783,41.35584,-74.1043,41.3163,-73.9467,41.413,0,64474104],["r176921","Pope Field",-79.01404,35.17337,-79.0319,35.1587,-78.9888,35.1886,1,7327036],["r176940","Fort Bragg",-79.15415,35.14031,-79.3814,35.0306,-78.9447,35.2512,5,554150015],["r206824","Fort Belvoir",-77.14369,38.7108,-77.1739,38.6729,-77.1208,38.7486,0,23156083],["r237502","Joint Base Lewis-McChord",-122.50741,47.03083,-122.802,46.9015,-122.3698,47.1595,0,358289411],["r252043","Francis E. Warren Air Force Base",-104.86328,41.16587,-104.8859,41.1317,-104.8394,41.2012,1,23635348],["r253786","Naval Base Point Loma",-117.24939,32.6941,-117.2562,32.6648,-117.2341,32.7131,5,4741248],["r253814","Marine Corps Air Station Miramar",-117.08249,32.87638,-117.1986,32.8331,-116.9871,32.9208,0,87161025],["r308744","Schietveld",5.44866,51.04934,5.4361,51.0439,5.4672,51.0548,0,1200107],["r309689","Ādažu poligons",24.46976,57.15377,24.3461,57.0837,24.563,57.2226,6,133266211],["r309926","Roberts Army Heliport",-120.74307,35.81448,-120.7467,35.8091,-120.7404,35.8197,1,331999],["r317709","Naval Reservation San Clemente Island",-118.51541,32.92142,-118.6206,32.8004,-118.3487,33.0422,0,163093405],["r317710","Naval Air Warfare Center China Lake",-117.62946,35.75329,-117.8897,35.272,-116.9246,36.2318,0,4495435805],["r317712","Marine Corps Base Camp Pendleton",-117.42402,33.35453,-117.5963,33.2056,-117.2554,33.5036,0,507618719],["r317716","Camp Roberts",-120.77817,35.79079,-120.8594,35.6884,-120.6887,35.8934,6,173959948],["r317718","Camp San Luis Obispo",-120.69818,35.33796,-120.7635,35.3048,-120.6642,35.3716,0,23703563],["r379564","US Coast Guard - Base Portsmouth",-76.35878,36.88311,-76.3678,36.8801,-76.3518,36.8859,0,636553],["r383103","Patuxent River NAS (Trapnell Field)",-76.40977,38.28455,-76.4423,38.2641,-76.3817,38.3042,1,9064162],["r389913","Helmut-Schmidt-Universität – Universität der Bundeswehr Hamburg",10.11122,53.56877,10.1059,53.5663,10.1165,53.5709,4,255487],["r392208","Joint Expeditionary Base Little Creek - Fort Story East East",-76.01769,36.92062,-76.0464,36.9092,-75.9918,36.9323,0,6066899],["r392665","Naval Station Norfolk",-76.29734,36.94167,-76.3307,36.9193,-76.2646,36.964,0,13143152],["r416869","Naval Base Kitsap-Bremerton",-122.64202,47.5574,-122.6622,47.5511,-122.6257,47.5649,5,2487880],["r417668","Camp Murray",-122.56814,47.11741,-122.5809,47.111,-122.5577,47.1229,0,1105917],["r420625","Yakima Training Center",-120.18138,46.75205,-120.4667,46.5473,-119.9159,46.9563,0,1322423534],["r437075","Vojenský újezd Libavá",17.54728,49.67197,17.4068,49.5838,17.6984,49.7602,6,235507050],["r438525","Vojenský újezd Boletice",14.07264,48.8478,13.947,48.7872,14.2431,48.9085,6,165484736],["r439469","Vojenský újezd Hradiště",13.13,50.26224,13.0045,50.1629,13.2555,50.3615,6,280845474],["r440731","Vojenský újezd Březina",16.96557,49.40648,16.8378,49.313,17.0541,49.5,6,149643828],["r451539","Naval Magazine Indian Island",-122.72867,48.05303,-122.7481,48.0187,-122.7014,48.0872,0,10831143],["r451698","Naval Radio Station Jim Creek",-121.92018,48.2028,-121.9584,48.167,-121.8873,48.2249,0,19804554],["r454382","National Security Investigative Service",31.31037,30.05187,31.31,30.0508,31.3117,30.0528,0,14562],["r533411","Naval Outlying Landing Field Coupeville",-122.63203,48.18948,-122.6479,48.171,-122.6152,48.2067,1,2626295],["r533644","Eglin Air Force Base",-86.51396,30.5777,-86.9829,30.3814,-86.1073,30.7498,5,1873152593],["r534267","Saufley Field",-87.34183,30.46763,-87.3512,30.4582,-87.3321,30.4773,0,2666728],["r541791","Whidbey Island Naval Air Station Seaplane Base",-122.56089,48.28799,-122.6453,48.2676,-122.5559,48.3083,0,9647535],["r543980","U.S. Coast Guard - Base Seattle",-122.34035,47.58933,-122.343,47.5879,-122.3375,47.5908,Line truncated
@@ -0,0 +1 @@
74cc07f12c496a622f2f6c5a81922c293bca5eb44e0901c967bbf94e15ee514f names.json
@@ -0,0 +1,45 @@
# US Census Bureau counties pack
County and county-equivalent outlines for area annotations ("outline Travis
County", "outline Orleans Parish"), read by `src/data/adminBoundaries.js`.
| File | Source dataset | Features |
|------|----------------|----------|
| `counties.json` | `cb_2025_us_county_5m` (cartographic boundary file, 1:5,000,000) | 3,235 |
**Source:** U.S. Census Bureau cartographic boundary files,
<https://www2.census.gov/geo/tiger/GENZ2025/shp/cb_2025_us_county_5m.zip>
(SHA-256 `faec522080681e79be5be435c981009a77891206ff8a7f1d142f3bf5da9ebd74`),
fetched 2026-09-25. Covers the 50 states, the District of Columbia, Puerto
Rico and the Island Areas (American Samoa, Guam, the Northern Mariana Islands,
the US Virgin Islands).
**License:** public domain — a work of the U.S. Government (17 U.S.C. § 105).
No attribution is required; the app credits the U.S. Census Bureau. See
DATA_SOURCES.md.
**Regenerate:** `node scripts/build-admin-packs.mjs --only counties`
downloads the pinned zip (to the system temp directory, or `--cache <dir>`),
checks its SHA-256, reads the shapefile and rewrites the pack byte for byte.
Parameters are in the script's `PARAMS` and in the pack's `meta.curation`.
**Schema:** `meta` (source, URL, hash, license, curation) and `features[]`
with `name` (`NAME`), `full` (`NAMELSAD`, e.g. "Travis County"), `lsad` (the
county word: County, Parish, Borough, Census Area, Municipality, city, …),
`state` (`STATE_NAME`), `st` (`STUSPS`), `geoid` (`GEOID`) and `polygons`.
`polygons` is a list of parts, largest first, each `[outer, ...holes]`; every
ring is open and stored as integers in units of 10^-4 degrees, the first
vertex absolute and each later one as a `[dLon, dLat]` delta.
**Curation:**
- Shapefile rings are grouped into parts (clockwise outers) and holes
(counter-clockwise rings, assigned to the outer that contains them), so an
independent city inside a county (Fairfax city in Fairfax County) is a hole.
- Douglas-Peucker per ring at 0.003 × √(county area in km²) km, clamped to
0.0005°–0.01° (Travis County ≈ 0.0014°); coordinates rounded to 4 decimals.
- Parts under 1 km² or under 0.1% of the county's largest part are dropped;
holes under 1 km² are dropped. Outers are counter-clockwise, holes
clockwise.
- Result: 1,541,464 bytes (533,326 with gzip -9); 3,524 parts, 18 holes,
139,904 vertices.
@@ -0,0 +1 @@
{"meta":{"title":"US Census Bureau cartographic boundary counties","source":"cb_2025_us_county_5m","url":"https://www2.census.gov/geo/tiger/GENZ2025/shp/cb_2025_us_county_5m.zip","sha256":"faec522080681e79be5be435c981009a77891206ff8a7f1d142f3bf5da9ebd74","license":"Public domain (U.S. Government work, 17 U.S.C. § 105)","schema":"features[]: name, full (NAMELSAD), lsad? (County, Parish, Borough, …), state, st (USPS), geoid, d? (decimals when not meta.decimals), polygons[][ring] (first ring outer, rest holes; each ring open and encoded as integers in 10^-d degrees, first vertex absolute then [dLon, dLat] deltas)","decimals":4,"curation":{"toleranceFactor":0.003,"minToleranceDeg":0.0005,"maxToleranceDeg":0.01,"decimals":4,"minPartKm2":1,"minPartShare":0.001,"minHoleKm2":1},"script":"scripts/build-admin-packs.mjs"},"features":[{"name":"Autauga","full":"Autauga County","lsad":"County","state":"Alabama","st":"AL","geoid":"01001","polygons":[[[-869212,326575,103,-284,72,-457,50,-60,-44,-368,241,-533,189,-364,-66,-131,114,7,302,-128,-203,-103,16,-150,284,-296,80,-164,-135,-212,48,-232,178,-14,251,321,-90,296,8,222,314,-34,228,154,93,-17,-11,-307,77,-113,278,-74,279,227,22,211,354,85,45,-71,-13,-243,195,-130,134,137,340,-58,122,-288,156,-1,233,41,43,191,158,9,142,137,62,277,116,-60,335,101,-19,2975,-3011,-17,8,-440,-1032,-16,-1010,41]]]},{"name":"Baldwin","full":"Baldwin County","lsad":"County","state":"Alabama","st":"AL","geoid":"01003","polygons":[[[-880146,302224,523,71,1238,-23,379,22,651,113,786,90,988,247,398,60,-1,35,90,83,425,101,28,95,-216,-29,-72,29,-33,62,56,52,276,108,107,101,9,231,127,136,-20,107,111,150,69,36,192,1,78,74,245,129,46,61,-42,103,-291,41,-80,54,-68,9,-104,83,-45,50,-10,60,-50,39,50,106,-6,47,-85,108,-80,38,27,45,-16,122,112,220,75,50,19,60,73,17,20,45,-17,37,57,32,40,105,21,162,73,57,43,97,14,156,-26,195,-97,162,-11,85,-125,49,-109,86,-94,19,-116,93,-56,-23,-93,33,-81,58,-68,99,-211,51,-94,119,-209,100,-39,179,-97,106,11,66,-269,219,-99,119,-186,82,-53,107,-101,34,-14,56,-100,76,11,92,-87,88,54,142,87,74,-14,100,62,39,82,231,157,270,29,129,-96,330,-166,0,-5,2471,-163,-16,-442,191,-379,401,-509,-48,-64,-36,-240,90,-79,163,-55,-124,232,-237,3,-71,-160,-60,-167,88,-101,-33,-54,-162,-156,9,122,-127,-230,-119,-35,-93,151,-177,-114,-38,-99,112,-94,-178,-104,63,-166,-51,-74,-118,-330,55,53,-298,-316,-4,66,-144,262,-11,-65,-255,-296,-322,124,-212,151,-4,75,-197,-162,-188,69,-73,-143,-139,82,-184,-86,-220,216,-60,193,-264,-280,-105,-65,-209,-247,-204,-29,-117,54,-100,265,-125,96,-124,-18,-134,-404,-122,-35,-172,-261,-131,-55,-89,-41,-188,253,-551,-74,-133,272,-99,252,-162,193,-15,174,-213,68,-203,-25,-299,108,-199,25,-151,-94,-251,-223,-384,15,-63,119,-105,137,-612,413,-259,199,-61,79,-81,73,-155,206,-211,365,-210,173,-205,2,-139,-113,-149,-211,-82,-300,1,-424,-140,-192,-17,-212,29,-355,200,-130,-49,-178,-210,-572,-52,-53,-77]]]},{"name":"Barbour","full":"Barbour County","lsad":"County","state":"Alabama","st":"AL","geoid":"01005","polygons":[[[-857483,316180,1629,-4,1184,30,506,-11,4,872,1829,-36,236,74,35,313,406,182,111,139,256,-100,-78,251,53,263,-106,240,120,442,99,98,378,514,283,688,45,489,-1358,-7,-69,31,-5,112,-187,183,-179,84,-51,169,-256,189,21,98,-1525,-16,-172,-73,-24,-934,13,-310,-340,-6,-86,79,5,-81,-420,-1,2,-144,-749,-23,-55,-35,-120,-330,-155,-154,-285,-339,-90,-231,1,-82,-204,-243,98,-215,-58,-264,116,-235,-87,-237,-301,-199,-77,-293,33,-29,-122,-108,18,-375,-253,-409]]]},{"name":"Bibb","full":"Bibb County","lsad":"County","state":"Alabama","st":"AL","geoid":"01007","polygons":[[[-874212,328745,1017,6,3,-436,859,7,208,30,1933,18,1431,-7,-8,2136,-216,-1,-178,98,-177,45,0,147,-177,-1,8,199,-184,-2,-3,597,-560,77,0,133,43,123,-55,551,-498,3,-275,-253,-174,2,-4,141,-336,-60,-437,-332,-1,-659,-826,24,-297,-392,-69,-877,-1034,-28]]]},{"name":"Blount","full":"Blount County","lsad":"County","state":"Alabama","st":"AL","geoid":"01009","polygons":[[[-869634,338582,91,-133,6,-296,172,0,526,279,1248,-26,1138,-676,-25,-71,700,-6,3,367,557,-1,93,89,637,132,186,-45,898,712,-75,205,256,210,-33,79,-446,-3,309,317,66,146,3,604,97,257,192,269,-102,127,-953,932,-361,445,10,96,-418,11,-138,-112,-152,54,-85,-271,-160,-13,-126,-200,67,-199,-287,-207,-207,-300,-64,-164,-230,32,-293,-150,-34,-156,-298,-120,-18,130,-158,-111,200,-260,-27,-35,-91,96,-135,-139,78,-121,-189,-36,-23,-123,-263,-221,-109,-17,-39,-217,-277,-106,-3,-112,-281,51,-12,-160,-149,91,-319,-179,-181,3,2,-156,-393,-93,16,-359,-392,-46]]]},{"name":"Bullock","full":"Bullock County","lsad":"County","state":"Alabama","st":"AL","geoid":"01011","polygons":[[[-859969,320510,466,36,-43,72,83,-72,345,2,181,-74,10,-583,84,-218,943,0,-10,-869,1333,-1,-1,82,90,231,285,339,155,154,120,330,55,35,749,23,-2,144,420,1,-5,81,86,-79,340,6,-13,310,24,934,172,73,-172,-1,-61,880,-4215,-26,-211,143,-18,452,-185,137,-23,-302,-879,-27,1,-217,-127,-1]]]},{"name":"Butler","full":"Butler County","lsad":"County","state":"Alabama","st":"AL","geoid":"01013","polygonLine truncated
+39 -1
View File
@@ -476,7 +476,7 @@ test('pending mapped installations render as unknown instead of a false zero', a
// the dependencies reach it by different routes:
//
// - ais-live-vessels is its REACHABLE producer. enable()/update() both resolve
// as soon as the first /api/ais-live poll answers, so the manager's
// as soon as the first /api/vessels poll answers, so the manager's
// lifecycle settles to `enabled` — but until the server-side socket delivers
// a position, firstConnectPhase stays 'loading' and the module reports busy,
// no lastUpdate, count 0, and an UNDEFINED status, so the status list alone
@@ -1631,6 +1631,20 @@ test('installation summaries disclose viewport-scoped coverage', () => {
assert.equal(retrying.reason, 'Overpass temporarily unavailable — retrying in 30s');
});
test('installation summaries name a subject window instead of the viewport', () => {
const stats = { coverage: { kind: 'subject', radiusM: 100_000 } };
const empty = summarizeInstallationViewport([], { available: true, stale: false, stats });
assert.equal(empty.count, 0);
assert.match(empty.reason, /none mapped within 100 km/);
assert.match(empty.reason, /not a complete 250 km survey/);
const found = summarizeInstallationViewport(
[{ id: 'ofm:installation:1', label: 'Military area', distanceM: 1200 }],
{ available: true, stale: false, stats },
);
assert.equal(found.count, 1);
assert.equal(found.reason, 'mapped matches within 100 km of the subject');
});
test('compact Context snapshots retain installation coverage', () => {
const snapshot = buildAwarenessContextSnapshot({
subject: { layerId: 'flights', id: 'abc' },
@@ -3060,3 +3074,27 @@ test('a moving camera refreshes Contacts more than once inside one parked interv
restoreCollections();
}
});
test('a selected installation card keeps every member name and escapes source text', () => {
const runtime = installAwarenessRuntime();
const restores = [];
try {
for (const layer of [aisLiveVesselsLayer, flightsLayer, militaryFlightsLayer, militaryInstallationsLayer])
replaceMethod(layer, 'getNearby', () => [], restores);
militaryAwarenessLayer.setParams({ passive: false });
runtime.dispatch('gev:entity-selected', {
layerId: 'military-installations', id: 'osm:military:w1', label: 'Parent base',
latitude: 31.13, longitude: -97.78,
properties: { memberNames: ['Area one', 'Area two', 'Area three', '<Area four>'] },
});
militaryAwarenessLayer.update();
const html = runtime.panel().innerHTML;
assert.match(html, /Named areas \(4\)/);
for (const name of ['Area one', 'Area two', 'Area three', '&lt;Area four&gt;']) assert.ok(html.includes(name));
assert.ok(!html.includes('<Area four>'));
} finally {
restores.reverse().forEach((restore) => restore());
runtime.restore();
}
});
+2 -2
View File
@@ -1,12 +1,12 @@
import { defaultSurface } from './surfaceServices.js';
import { createApplicationMilitary } from '../app/layers/militaryFlights.js';
import { createAdsbLolSource } from '../sources/live/standalone.js';
import { createMilitarySource } from '../sources/live/standalone.js';
import * as militaryRegistry from './militaryRegistry.js';
const militaryFlightsLayer = createApplicationMilitary({
surface: defaultSurface,
source: createAdsbLolSource(),
source: createMilitarySource(),
militaryRegistry,
});
export { TRACKED_MODEL_MAX_PX } from '../layers/military/policy.js';
+327 -102
View File
@@ -24,8 +24,12 @@ test('share-Follow absence requires an accepted adsb.lol snapshot', async () =>
(await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234')).status,
'source-unavailable',
);
_setMilitaryTrackingRefreshOutcomeForTest({ status: 'accepted', ids: ['different'] });
const missing = await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234');
_setMilitaryTrackingRefreshOutcomeForTest({
status: 'accepted',
ids: ['different'],
});
const missing =
await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234');
assert.equal(missing.status, 'missing');
assert.equal(missing.reason, 'target-absent-from-snapshot');
});
@@ -44,9 +48,9 @@ const FULL_INFO = {
operator: 'United States Air Force',
};
test('military stats identify adsb.lol as the primary feed, not a fallback', () => {
test('military stats name the feed until the server names its provider, not a fallback', () => {
const stats = militaryFlightsLayer.getStats();
assert.equal(stats.source, 'adsb.lol');
assert.equal(stats.source, 'Military aircraft');
assert.equal(stats.fallback, false);
});
@@ -81,8 +85,14 @@ test('military analyst record: military is ALWAYS true, routes/country always nu
});
test('military analyst record: no callsign falls back to registration, then icao24', () => {
assert.equal(mapAnalystRecord('ae01ce', { ...FULL_INFO, callsign: '' }).id, '05-8152');
assert.equal(mapAnalystRecord('ae01ce', { callsign: '', registration: ' ' }).id, 'ae01ce');
assert.equal(
mapAnalystRecord('ae01ce', { ...FULL_INFO, callsign: '' }).id,
'05-8152',
);
assert.equal(
mapAnalystRecord('ae01ce', { callsign: '', registration: ' ' }).id,
'ae01ce',
);
});
test('military analyst record: empty info yields nulls, never NaN/undefined', () => {
@@ -91,7 +101,8 @@ test('military analyst record: empty info yields nulls, never NaN/undefined', ()
assert.equal(r.onGround, false);
for (const [key, value] of Object.entries(r)) {
assert.notEqual(value, undefined, `${key} must not be undefined`);
if (typeof value === 'number') assert.ok(Number.isFinite(value), `${key} must not be NaN`);
if (typeof value === 'number')
assert.ok(Number.isFinite(value), `${key} must not be NaN`);
}
});
@@ -103,12 +114,17 @@ test('military analyst record: output is JSON-safe (no Cesium types)', () => {
test('military first update forwards caller cancellation into the feed request', async () => {
const realFetch = globalThis.fetch;
let observedSignal = null;
globalThis.fetch = (_url, options = {}) => new Promise((_resolve, reject) => {
observedSignal = options.signal;
options.signal?.addEventListener('abort', () => {
reject(new DOMException('aborted', 'AbortError'));
}, { once: true });
});
globalThis.fetch = (_url, options = {}) =>
new Promise((_resolve, reject) => {
observedSignal = options.signal;
options.signal?.addEventListener(
'abort',
() => {
reject(new DOMException('aborted', 'AbortError'));
},
{ once: true },
);
});
try {
const controller = new AbortController();
const work = militaryFlightsLayer.update({}, { signal: controller.signal });
@@ -138,11 +154,22 @@ test('nonempty adsb.lol payload with zero usable rows cannot prove share target
json: async () => ({ ac: [null, {}, { hex: 'ae1234' }] }),
});
try {
await militaryFlightsLayer.update({ camera: { positionCartographic: null }, scene: {} });
const resolution = await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234');
await militaryFlightsLayer.update({
camera: { positionCartographic: null },
scene: {},
});
const resolution =
await militaryFlightsLayer.resolveTrackingRestoreTarget('ae1234');
assert.equal(resolution.status, 'source-unavailable');
assert.match(militaryFlightsLayer.getStats().error, /Malformed adsb\.lol aircraft rows/);
assert.equal(militaryFlightsLayer.getAnalystRecords().length, 1, 'warm aircraft data is preserved');
assert.match(
militaryFlightsLayer.getStats().error,
/Malformed adsb\.lol aircraft rows/,
);
assert.equal(
militaryFlightsLayer.getAnalystRecords().length,
1,
'warm aircraft data is preserved',
);
} finally {
globalThis.fetch = realFetch;
}
@@ -188,21 +215,26 @@ test('military poll refreshes tracked callsign/altitude/kts and marks a missed p
ok: true,
status: 200,
json: async () => ({
ac: poll++ === 0 ? [{
hex: icao24,
lon: -96.9,
lat: 31.1,
alt_baro: 28_000,
alt_geom: 28_100,
track: 95,
gs: 400,
seen: 0,
seen_pos: 0,
flight: 'RCH451 ',
t: 'C17',
r: '05-8152',
ownOp: 'United States Air Force',
}] : [],
ac:
poll++ === 0
? [
{
hex: icao24,
lon: -96.9,
lat: 31.1,
alt_baro: 28_000,
alt_geom: 28_100,
track: 95,
gs: 400,
seen: 0,
seen_pos: 0,
flight: 'RCH451 ',
t: 'C17',
r: '05-8152',
ownOp: 'United States Air Force',
},
]
: [],
}),
});
@@ -227,7 +259,10 @@ test('cached military snapshots retain their observation time and expose stale f
_setTrackedMilitaryRefreshStateForTest({
icao24: 'ae01ce',
entity: { gevLabelModel: { title: 'RCH451', details: [] } },
billboard: { position: Cesium.Cartesian3.fromDegrees(-97, 31, 8000), show: false },
billboard: {
position: Cesium.Cartesian3.fromDegrees(-97, 31, 8000),
show: false,
},
billboardCollection: { show: false, remove() {} },
viewer,
history,
@@ -235,21 +270,48 @@ test('cached military snapshots retain their observation time and expose stale f
});
let cache = 'MISS';
t.mock.method(Date, 'now', () => now);
t.mock.method(globalThis, 'fetch', async () => Response.json({
ac: [{ hex: 'ae01ce', lon: -97, lat: 31, alt_baro: 28000,
track: 95, gs: 400, seen: 1, seen_pos: 2, flight: 'RCH451' }],
}, { headers: {
'X-ADS-B-Cache': cache,
...(cache === 'MISS' ? {} : { 'X-ADS-B-Cache-Age-Ms': String(now - receivedAt) }),
} }));
t.mock.method(globalThis, 'fetch', async () =>
Response.json(
{
ac: [
{
hex: 'ae01ce',
lon: -97,
lat: 31,
alt_baro: 28000,
track: 95,
gs: 400,
seen: 1,
seen_pos: 2,
flight: 'RCH451',
},
],
},
{
headers: {
'X-Feed-Cache': cache,
...(cache === 'MISS'
? {}
: { 'X-Feed-Age-Ms': String(now - receivedAt) }),
},
},
),
);
await militaryFlightsLayer.update(viewer);
assert.equal(history.length, 1);
assert.equal(history[0].epochMs, receivedAt - 2000);
for (const [status, age] of [['HIT', 5000], ['STALE', 45000]]) {
for (const [status, age] of [
['HIT', 5000],
['STALE', 45000],
]) {
cache = status;
now = receivedAt + age;
await militaryFlightsLayer.update(viewer);
assert.equal(history.length, 1, 'replayed positions cannot create a new fix');
assert.equal(
history.length,
1,
'replayed positions cannot create a new fix',
);
assert.equal(militaryFlightsLayer.getStats().lastUpdate, receivedAt);
assert.equal(militaryFlightsLayer.getStats().stale, status === 'STALE');
assert.equal(militaryFlightsLayer.getStats().error, null);
@@ -276,14 +338,18 @@ test('real military track path creates no native label and publishes every cache
trackedEntityChanged,
clock: { currentTime: now },
camera: {
cancelFlight() { cancelledFlights += 1; },
cancelFlight() {
cancelledFlights += 1;
},
position: new Cesium.Cartesian3(0, -10_000, 7_000),
positionWC: new Cesium.Cartesian3(0, -10_000, 7_000),
direction: Cesium.Cartesian3.UNIT_Y,
transform: Cesium.Matrix4.IDENTITY,
viewMatrix: Cesium.Matrix4.IDENTITY,
frustum: { projectionMatrix: Cesium.Matrix4.IDENTITY },
lookAtTransform() { appliedFrames += 1; },
lookAtTransform() {
appliedFrames += 1;
},
},
scene: {
canvas: { clientWidth: 1600, clientHeight: 900 },
@@ -322,9 +388,12 @@ test('real military track path creates no native label and publishes every cache
const realFetch = globalThis.fetch;
globalThis.window = new EventTarget();
const selectionEvents = [];
globalThis.window.addEventListener('gev:awareness-subject-selected', (event) => {
selectionEvents.push(event.detail);
});
globalThis.window.addEventListener(
'gev:awareness-subject-selected',
(event) => {
selectionEvents.push(event.detail);
},
);
globalThis.fetch = async () => ({ ok: false });
_setTrackedOverlayHostForTest(host);
try {
@@ -336,13 +405,15 @@ test('real military track path creates no native label and publishes every cache
billboardCollection: { show: true, remove() {} },
viewer,
tracked: false,
history: [{
time: now,
epochMs: Date.now(),
position,
velocity: 231.5,
track: 92.1,
}],
history: [
{
time: now,
epochMs: Date.now(),
position,
velocity: 231.5,
track: 92.1,
},
],
meta: {
...FULL_INFO,
type: 'C17',
@@ -352,34 +423,72 @@ test('real military track path creates no native label and publishes every cache
},
});
assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }), true);
assert.equal(
militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }),
true,
);
const entity = viewer.trackedEntity;
assert.ok(entity instanceof Cesium.Entity, 'trackById must create the real Cesium entity');
assert.ok(
entity instanceof Cesium.Entity,
'trackById must create the real Cesium entity',
);
assert.equal(entity.label, undefined);
assert.ok(entities.values.every((candidate) => candidate.label === undefined));
assert.ok(
entities.values.every((candidate) => candidate.label === undefined),
);
assert.deepEqual(entity.gevLabelModel, {
title: 'RCH451',
details: [
'C17 · 05-8152',
'United States Air Force · 28000 ft · 450 kt',
],
details: ['C17 · 05-8152', 'United States Air Force · 28000 ft · 450 kt'],
accent: '#ffd166',
});
viewer.scene.preUpdate.raiseEvent();
const initialAppliedFrames = appliedFrames;
const initialCancelledFlights = cancelledFlights;
assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'user' }), true);
assert.equal(cancelledFlights, initialCancelledFlights, 'ordinary repeated tracking stays camera-idempotent');
assert.equal(selectionEvents.at(-1)?.origin, 'user', 'same-target selection upgrades durable authority');
assert.equal(
militaryFlightsLayer.trackById(icao24, { origin: 'user' }),
true,
);
assert.equal(
cancelledFlights,
initialCancelledFlights,
'ordinary repeated tracking stays camera-idempotent',
);
assert.equal(
selectionEvents.at(-1)?.origin,
'user',
'same-target selection upgrades durable authority',
);
assert.equal(entity.gevSelectionOrigin, 'user');
assert.equal(militaryFlightsLayer.refocusTrackedById('different-flight'), false);
assert.equal(militaryFlightsLayer.refocusTrackedById(icao24, { origin: 'voice' }), true);
assert.equal(selectionEvents.at(-1)?.origin, 'voice', 'same-target refocus forwards explicit authority');
assert.equal(
militaryFlightsLayer.refocusTrackedById('different-flight'),
false,
);
assert.equal(
militaryFlightsLayer.refocusTrackedById(icao24, { origin: 'voice' }),
true,
);
assert.equal(
selectionEvents.at(-1)?.origin,
'voice',
'same-target refocus forwards explicit authority',
);
assert.equal(militaryFlightsLayer.refocusTrackedById(icao24), true);
viewer.scene.preUpdate.raiseEvent();
assert.equal(viewer.trackedEntity, entity, 'refocus must retain the exact tracked entity');
assert.equal(appliedFrames, initialAppliedFrames + 1, 'repeated refocus keeps one camera-frame owner');
assert.equal(cancelledFlights, initialCancelledFlights + 2, 'only explicit refocus requests cancel camera flights');
assert.equal(
viewer.trackedEntity,
entity,
'refocus must retain the exact tracked entity',
);
assert.equal(
appliedFrames,
initialAppliedFrames + 1,
'repeated refocus keeps one camera-frame owner',
);
assert.equal(
cancelledFlights,
initialCancelledFlights + 2,
'only explicit refocus requests cancel camera flights',
);
const publication = publications.at(-1);
assert.equal(publication.sourceId, 'tracked');
const entry = publication.entries[0];
@@ -390,23 +499,40 @@ test('real military track path creates no native label and publishes every cache
const display = entity.position.getValue(now);
assert.ok(display, 'tracked position callback must seed its frame cache');
assert.equal(entry.position(), display, 'host must reuse the exact cached display Cartesian');
assert.equal(
entry.position(),
display,
'host must reuse the exact cached display Cartesian',
);
militaryFlightsLayer.setParams(
{ selectedMilitaryTrackingId: 'late001' },
{ origin: 'share-restore' },
);
assert.equal(_pendingMilitaryTrackingRestoreForTest(), 'late001');
assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'user' }), true);
assert.equal(_pendingMilitaryTrackingRestoreForTest(), null, 'new user selection cancels stale restore');
assert.equal(
militaryFlightsLayer.trackById(icao24, { origin: 'user' }),
true,
);
assert.equal(
_pendingMilitaryTrackingRestoreForTest(),
null,
'new user selection cancels stale restore',
);
_addMilitaryTrackingCandidateForTest({
icao24: 'late001',
billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-96.9, 31.1, 8_000) },
billboard: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-96.9, 31.1, 8_000),
},
meta: { ...FULL_INFO, callsign: 'LATE1', rawLat: 31.1, rawLon: -96.9 },
history: [],
});
_applyPendingMilitaryTrackingRestoreForTest();
assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, icao24);
assert.equal(
militaryFlightsLayer.getParams().selectedMilitaryTrackingId,
icao24,
);
militaryFlightsLayer.stopTracking();
militaryFlightsLayer.setParams(
@@ -415,15 +541,25 @@ test('real military track path creates no native label and publishes every cache
);
assert.equal(_pendingMilitaryTrackingRestoreForTest(), 'late002');
militaryFlightsLayer.stopTracking({ origin: 'user' });
assert.equal(_pendingMilitaryTrackingRestoreForTest(), null, 'explicit clear cancels stale restore');
assert.equal(
_pendingMilitaryTrackingRestoreForTest(),
null,
'explicit clear cancels stale restore',
);
_addMilitaryTrackingCandidateForTest({
icao24: 'late002',
billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-96.8, 31.2, 7_000) },
billboard: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-96.8, 31.2, 7_000),
},
meta: { ...FULL_INFO, callsign: 'LATE2', rawLat: 31.2, rawLon: -96.8 },
history: [],
});
_applyPendingMilitaryTrackingRestoreForTest();
assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, null);
assert.equal(
militaryFlightsLayer.getParams().selectedMilitaryTrackingId,
null,
);
militaryFlightsLayer.setParams(
{ selectedMilitaryTrackingId: 'late003' },
@@ -438,12 +574,18 @@ test('real military track path creates no native label and publishes every cache
);
_addMilitaryTrackingCandidateForTest({
icao24: 'late003',
billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.3, 30.6, 7_000) },
billboard: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-97.3, 30.6, 7_000),
},
meta: { ...FULL_INFO, callsign: 'LATE3', rawLat: 30.6, rawLon: -97.3 },
history: [],
});
_applyPendingMilitaryTrackingRestoreForTest();
assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, null);
assert.equal(
militaryFlightsLayer.getParams().selectedMilitaryTrackingId,
null,
);
militaryFlightsLayer.setParams(
{ selectedMilitaryTrackingId: 'late004' },
@@ -452,18 +594,27 @@ test('real military track path creates no native label and publishes every cache
assert.equal(_pendingMilitaryTrackingRestoreForTest(), 'late004');
_addMilitaryTrackingCandidateForTest({
icao24: 'late004',
billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-97.2, 30.7, 6_000) },
billboard: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-97.2, 30.7, 6_000),
},
meta: { ...FULL_INFO, callsign: 'LATE4', rawLat: 30.7, rawLon: -97.2 },
history: [],
});
assert.equal(_applyPendingMilitaryTrackingRestoreForTest(), true);
assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, 'late004');
assert.equal(
militaryFlightsLayer.getParams().selectedMilitaryTrackingId,
'late004',
);
militaryFlightsLayer.setParams(
{ selectedMilitaryTrackingId: 'late005' },
{ origin: 'local-restore' },
);
assert.equal(militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }), true);
assert.equal(
militaryFlightsLayer.trackById(icao24, { origin: 'programmatic' }),
true,
);
assert.equal(
_pendingMilitaryTrackingRestoreForTest(),
'late005',
@@ -471,12 +622,18 @@ test('real military track path creates no native label and publishes every cache
);
_addMilitaryTrackingCandidateForTest({
icao24: 'late005',
billboard: { ...billboard, position: Cesium.Cartesian3.fromDegrees(-96.6, 31.4, 5_000) },
billboard: {
...billboard,
position: Cesium.Cartesian3.fromDegrees(-96.6, 31.4, 5_000),
},
meta: { ...FULL_INFO, callsign: 'LATE5', rawLat: 31.4, rawLon: -96.6 },
history: [],
});
assert.equal(_applyPendingMilitaryTrackingRestoreForTest(), true);
assert.equal(militaryFlightsLayer.getParams().selectedMilitaryTrackingId, 'late005');
assert.equal(
militaryFlightsLayer.getParams().selectedMilitaryTrackingId,
'late005',
);
} finally {
militaryFlightsLayer.stopTracking();
viewer.scene.preUpdate.raiseEvent();
@@ -521,21 +678,47 @@ test('a callsign-less contact is findable by the tail number the app displays',
// The identity analyst_query hands back for this contact.
const analystId = mapAnalystRecord(icao24, {
callsign: null, registration: '6606', rawLat: 40.71, rawLon: -74.01,
callsign: null,
registration: '6606',
rawLat: 40.71,
rawLon: -74.01,
}).id;
assert.equal(analystId, '6606', 'the analyst names a callsign-less contact by its tail');
assert.equal(
analystId,
'6606',
'the analyst names a callsign-less contact by its tail',
);
const found = militaryFlightsLayer.findByQuery(analystId);
assert.equal(found?.icao24, icao24, 'and the tracker must resolve that same identity');
assert.equal(militaryFlightsLayer.findByQuery('660')?.icao24, icao24, 'prefix match too');
assert.equal(militaryFlightsLayer.findByQuery(icao24)?.icao24, icao24, 'hex still wins outright');
assert.equal(
found?.icao24,
icao24,
'and the tracker must resolve that same identity',
);
assert.equal(
militaryFlightsLayer.findByQuery('660')?.icao24,
icao24,
'prefix match too',
);
assert.equal(
militaryFlightsLayer.findByQuery(icao24)?.icao24,
icao24,
'hex still wins outright',
);
});
test('the analyst record carries the hex key the tracker keys on', () => {
const record = mapAnalystRecord('ae7f01', {
callsign: null, registration: '6606', rawLat: 40.71, rawLon: -74.01,
callsign: null,
registration: '6606',
rawLat: 40.71,
rawLon: -74.01,
});
assert.equal(record.icao24, 'ae7f01', 'a resolvable key must ride along with the display label');
assert.equal(
record.icao24,
'ae7f01',
'a resolvable key must ride along with the display label',
);
});
test('a registration never outranks another contact’s exact callsign', () => {
@@ -548,18 +731,35 @@ test('a registration never outranks another contact’s exact callsign', () => {
show: true,
});
const base = {
type: 'C17', operator: 'USAF', altitudeFt: 25000, speedMps: 180,
track: 90, klass: 'widebody', onGround: false,
type: 'C17',
operator: 'USAF',
altitudeFt: 25000,
speedMps: 180,
track: 90,
klass: 'widebody',
onGround: false,
};
// Registered FIRST, so insertion order favours it.
_addMilitaryTrackingCandidateForTest({
icao24: 'ae0aa1',
meta: { ...base, callsign: null, registration: 'ZULU777', rawLat: 31.0, rawLon: -97.0 },
meta: {
...base,
callsign: null,
registration: 'ZULU777',
rawLat: 31.0,
rawLon: -97.0,
},
billboard: billboardAt(-97.0, 31.0),
});
_addMilitaryTrackingCandidateForTest({
icao24: 'ae0bb2',
meta: { ...base, callsign: 'ZULU777', registration: '05-9999', rawLat: 31.2, rawLon: -97.2 },
meta: {
...base,
callsign: 'ZULU777',
registration: '05-9999',
rawLat: 31.2,
rawLon: -97.2,
},
billboard: billboardAt(-97.2, 31.2),
});
@@ -582,24 +782,49 @@ test('two contacts matching at the same strength resolve deterministically', ()
show: true,
});
const base = {
type: 'C130', operator: 'USAF', altitudeFt: 21000, speedMps: 150,
track: 45, klass: 'turboprop', onGround: false,
type: 'C130',
operator: 'USAF',
altitudeFt: 21000,
speedMps: 150,
track: 45,
klass: 'turboprop',
onGround: false,
};
// Both registrations begin with QQ7 — a same-tier prefix tie. Registered
// highest-hex first so insertion order would pick the wrong one.
_addMilitaryTrackingCandidateForTest({
icao24: 'aef002',
meta: { ...base, callsign: null, registration: 'QQ7-222', rawLat: 32.0, rawLon: -98.0 },
meta: {
...base,
callsign: null,
registration: 'QQ7-222',
rawLat: 32.0,
rawLon: -98.0,
},
billboard: billboardAt(-98.0, 32.0),
});
_addMilitaryTrackingCandidateForTest({
icao24: 'aef001',
meta: { ...base, callsign: null, registration: 'QQ7-111', rawLat: 32.1, rawLon: -98.1 },
meta: {
...base,
callsign: null,
registration: 'QQ7-111',
rawLat: 32.1,
rawLon: -98.1,
},
billboard: billboardAt(-98.1, 32.1),
});
const first = militaryFlightsLayer.findByQuery('QQ7')?.icao24;
const second = militaryFlightsLayer.findByQuery('qq7')?.icao24;
assert.equal(first, 'aef001', 'the stable key (lowest hex) wins, not the feed order');
assert.equal(second, first, 'and the same query resolves the same way every time');
assert.equal(
first,
'aef001',
'the stable key (lowest hex) wins, not the feed order',
);
assert.equal(
second,
first,
'and the same query resolves the same way every time',
);
});
+1
View File
@@ -7,6 +7,7 @@ const sourceSlot = createSourceSlot(
createInstallationSource(),
['getMappedSites', 'searchNearby'],
'Installation source',
{ destroy: () => {} },
);
export const configureInstallationSource = sourceSlot.configure;
const layer = createApplicationInstallations({
+76 -2
View File
@@ -24,7 +24,7 @@ import {
installRenderGovernor,
} from '../renderGovernor.js';
import * as Cesium from 'cesium';
import { registerEntityContext, selectEntityContext, getSelectedEntityContext } from './contextStore.js';
import { registerEntityContext, selectEntityContext, getSelectedEntityContext, clearSelectedEntityContextForLayer } from './contextStore.js';
test('clicking a selected installation again or empty map clears it through refresh', async () => {
const run = await runInstallationLoad({ elements: [{ type: 'node', id: 42,
@@ -42,6 +42,23 @@ test('clicking a selected installation again or empty map clears it through refr
} finally { run.restore(); }
});
test('background installation paints neither reopen Contacts nor resurrect a cleared selection', async () => {
const run = await runInstallationLoad({ elements: [{ type:'node',id:42,lat:30.2,lon:-97.7,tags:{military:'base',name:'Site'} }] });
try {
const selected=()=>run.contextLabels().length;
run.click('osm:node:42');
assert.equal(selected(),1);
await militaryInstallationsLayer.update();
assert.equal(getSelectedEntityContext()?.id,'osm:node:42');
assert.equal(selected(),1,'data and floor refreshes must not publish a fresh selection');
clearSelectedEntityContextForLayer('military-installations');
await militaryInstallationsLayer.update();
assert.equal(getSelectedEntityContext(),null);
assert.equal(selected(),1,'an external clear must survive the next paint');
assert.equal(run.entities()[0].point.pixelSize.getValue(),9);
} finally { run.restore(); }
});
test('clearing a stale installation highlight does not clear or reclaim another layer selection', async () => {
const run = await runInstallationLoad({ elements: [{ type: 'node', id: 42,
lat: 30.2, lon: -97.7, tags: { military: 'base', name: 'Site' } }] });
@@ -798,7 +815,7 @@ test('the unavailable retry backs off 30s to a 240s ceiling and restarts clean',
test('the retry is wired to every lifecycle edge, not just declared', () => {
assert.match(installationsSource,
/setInstallationStatus\(\s*'unavailable',[^]*?\);\n\s*parts\.viewport\.scheduleUnavailableRetry\(\);/,
/setInstallationStatus\(\s*'unavailable',[^]*?\);\n\s*if \(!isUnavailableCapability\(error\)\)\s*parts\.viewport\.scheduleUnavailableRetry\(error\?\.retryAfterMs\);/,
'a failed load schedules the retry immediately after reporting unavailable');
assert.match(installationsSource,
/clearUnavailableRetry\(\);\n\s*setInstallationStatus\(\n?\s*layerState\.records\.length/,
@@ -815,3 +832,60 @@ test('the retry is wired to every lifecycle edge, not just declared', () => {
/layerState\.enabled && !layerState\.loading\)\s*parts\.ingestion\.loadInstallations\(\)/,
'the fired retry re-checks enablement and never races an in-flight load');
});
test('a merged installation stays visible when only its second fragment intersects', () => {
const far = [[-96.6,30.4],[-96.4,30.4],[-96.4,30.6],[-96.6,30.6]];
const near = [[-97.5,30.4],[-97.4,30.4],[-97.4,30.6],[-97.5,30.6]];
assert.equal(installationWithinViewport({latitude:30.5,longitude:-96.5,footprint:far,footprints:[[far],[near]]},VIEWPORT), true);
});
test('a Contacts anchor replaces the viewport and ignores small subject moves', async () => {
// Field report: a follow/Cockpit camera never fires moveEnd and often looks at
// the horizon, so the Context row read "?" for the whole track.
const run = await runInstallationLoad({ elements: [{ type: 'node', id: 7,
lat: 30.3125, lon: -97.765, tags: { military: 'base', name: 'Camp Mabry' } }] });
try {
const box = (href) => {
const url = new URL(href, 'https://example.test');
return Object.fromEntries(['south', 'west', 'north', 'east']
.map((key) => [key, Number(url.searchParams.get(key))]));
};
const centre = (href) => (box(href).south + box(href).north) / 2;
assert.equal(run.stats().coverageLabel, 'CURRENT VIEWPORT ONLY');
assert.equal(militaryInstallationsLayer.setContextAnchor({ latitude: 30.3125, longitude: -97.765 }), true);
await new Promise((resolve) => setTimeout(resolve, 650));
const anchored = box(run.requests.at(-1));
assert.ok(Math.abs(centre(run.requests.at(-1)) - 30.3125) < 0.01);
assert.ok(anchored.north - anchored.south > 1.5, 'window reaches ~100 km each way');
assert.equal(run.stats().coverageLabel, 'WITHIN 100 KM');
assert.equal(run.stats().coverage.kind, 'subject');
const count = run.requests.length;
assert.equal(militaryInstallationsLayer.setContextAnchor({ latitude: 30.35, longitude: -97.765 }), false,
'a 4 km move keeps the window');
assert.equal(militaryInstallationsLayer.setContextAnchor({ latitude: 30.6, longitude: -97.765 }), true,
'a 32 km move shifts the window');
await new Promise((resolve) => setTimeout(resolve, 650));
assert.equal(run.requests.length, count + 1);
assert.ok(Math.abs(centre(run.requests.at(-1)) - 30.6) < 0.01);
assert.equal(militaryInstallationsLayer.setContextAnchor(null), true);
await new Promise((resolve) => setTimeout(resolve, 650));
assert.deepEqual(box(run.requests.at(-1)), { south: 30, west: -98, north: 31, east: -97 },
'clearing the subject returns to the viewport');
assert.equal(run.stats().coverageLabel, 'CURRENT VIEWPORT ONLY');
} finally { run.restore(); }
});
test('unchanged installations keep their entities across reloads', async () => {
const run = await runInstallationLoad({ elements: [{ type: 'node', id: 42,
lat: 30.2, lon: -97.7, tags: { military: 'base', name: 'Site' } }] });
try {
const before = run.entities().slice();
assert.ok(before.length > 0);
await militaryInstallationsLayer.update();
const after = run.entities();
assert.equal(after.length, before.length);
assert.ok(after.every((entity, index) => entity === before[index]),
'a reload with the same geometry must not recreate entities');
} finally { run.restore(); }
});
+119
View File
@@ -0,0 +1,119 @@
import { militaryOsmKey } from '../sources/militaryTileGeometry.js';
export { militaryOsmKey } from '../sources/militaryTileGeometry.js';
import { requestWithDeadline } from '../sources/requestDeadline.js';
import { loadBundledJson } from './bundledJson.js';
import { createRetryableLoader } from './retryableLoad.js';
export const MILITARY_POINT_CAP = 512;
export const MILITARY_LABEL_CAP = 24;
export const MILITARY_NAMES_TIMEOUT_MS = 10_000;
/** Share a bounded acquisition; failed assets become retryable after cooldown. */
export function createMilitaryNamesLoader({
loadPack = (signal) =>
loadBundledJson(
new URL('./local_data/osm_military_names/names.json', import.meta.url),
{ signal },
),
timeoutMs = MILITARY_NAMES_TIMEOUT_MS,
...retryOptions
} = {}) {
return createRetryableLoader(async () => {
const pack = await requestWithDeadline(loadPack, { timeoutMs });
const records = pack.records.map(
([
osmKey,
name,
longitude,
latitude,
west,
south,
east,
north,
kind,
areaM2,
]) => ({
id: `osm:military:${osmKey}`,
osmKey,
name,
longitude,
latitude,
bbox: [west, south, east, north],
class: pack.classes[kind],
areaM2,
kind: 'installation',
namedArea: true,
pointOnly: true,
validation: 'unreviewed',
sources: [
{ name: 'OpenStreetMap', id: osmKey },
{ name: 'Overture Maps Foundation', id: osmKey },
],
}),
);
records.sort(
(a, b) => b.areaM2 - a.areaM2 || a.osmKey.localeCompare(b.osmKey),
);
return { records, byId: new Map(records.map((r) => [r.osmKey, r])) };
}, retryOptions);
}
/** Load and index the separate data asset only when installation context needs it. */
export const loadMilitaryNames = createMilitaryNamesLoader();
/** Join exact source identity before parcel merging; reject geographically inconsistent ids. */
export function nameMilitaryFragment(fragment, names) {
const osmKey = militaryOsmKey(fragment.featureKey);
const named = names?.byId.get(osmKey);
if (!named || !fragment.footprint?.length) return fragment;
const [west, south, east, north] = named.bbox;
const epsilon = (fragment.tileEpsilon || 0) * 2 + 0.0001;
const xs = fragment.footprint.map((p) => p[0]);
const ys = fragment.footprint.map((p) => p[1]);
if (
Math.max(...xs) < west - epsilon ||
Math.min(...xs) > east + epsilon ||
Math.max(...ys) < south - epsilon ||
Math.min(...ys) > north + epsilon
)
return fragment;
return {
...fragment,
name: named.name,
class: named.class,
osmKey,
namedArea: true,
areaM2: named.areaM2,
nameRecord: named,
};
}
/**
* Area-ranked spatial thinning of visible label points, including dateline
* views. With `thinned` false every point in the box is kept, up to `cap`.
*/
export function militaryNamesInView(
names,
box,
cap = MILITARY_POINT_CAP,
thinned = true,
) {
const width =
box.east >= box.west ? box.east - box.west : 360 + box.east - box.west;
const height = box.north - box.south;
const cells = new Set(),
records = [];
let count = 0;
for (const record of names?.records || []) {
const x = (record.longitude - box.west + 360) % 360;
if (x > width || record.latitude < box.south || record.latitude > box.north)
continue;
count++;
const cell = `${Math.min(31, Math.floor((x / width) * 32))}:${Math.min(15, Math.floor(((record.latitude - box.south) / height) * 16))}`;
if ((thinned && cells.has(cell)) || records.length >= cap) continue;
cells.add(cell);
records.push(record);
}
return { records, count };
}
+229
View File
@@ -0,0 +1,229 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { gzipSync } from 'node:zlib';
import {
loadMilitaryNames,
militaryOsmKey,
nameMilitaryFragment,
militaryNamesInView,
MILITARY_POINT_CAP,
} from './militaryNames.js';
import { decodeOpenFreeMapMilitaryTile } from '../sources/openFreeMap.js';
import { createControls } from '../layers/installations/controls.js';
import {
mergeMilitaryFragments,
militaryNameInView,
} from '../sources/militaryTileGeometry.js';
const sites = [
['w27894251', 'Camp Mabry'],
['r317712', 'Marine Corps Base Camp Pendleton'],
['r253814', 'Marine Corps Air Station Miramar'],
['w60546523', 'Ramstein Air Base'],
['w102826331', 'RAF Lakenheath'],
['r13529728', 'Fort Hood'],
];
test('wide-view counts report visible points rather than retained off-screen candidates', () => {
const state = {
wide: true,
records: Array.from({ length: 8 }, () => ({ kind: 'installation' })),
lastUpdate: 1,
status: 'ready',
};
const { methods } = createControls({
state,
services: {},
source: {},
parts: {
namedMarkers: {
stats: () => ({
namedMarkers: 8,
pointsOnScreen: 3,
labelsOnScreen: 2,
labelCap: 24,
}),
},
},
});
const stats = methods.getStats();
assert.equal(stats.count, 3);
assert.equal(stats.namedMarkers, 8);
assert.equal(stats.statusMessage, '3 mapped sites in view');
});
test('the separately licensed pack has unique typed identities, areas, names and a pinned lean output', async () => {
const raw = readFileSync(
new URL('./local_data/osm_military_names/names.json', import.meta.url),
);
const hash = readFileSync(
new URL('./local_data/osm_military_names/names.sha256', import.meta.url),
'utf8',
).split(' ')[0];
assert.equal(createHash('sha256').update(raw).digest('hex'), hash);
assert.ok(gzipSync(raw, { level: 9 }).length <= 2_000_000);
const names = await loadMilitaryNames();
assert.equal(names.records.length, 36466);
assert.equal(names.byId.size, names.records.length);
assert.ok(
names.records.every(
(r) => r.areaM2 > 0 && Number.isFinite(r.latitude + r.longitude),
),
);
for (const [id, name] of sites) assert.equal(names.byId.get(id).name, name);
});
test('checked-in OpenFreeMap fixtures retain typed OSM feature ids and geographical identity', async () => {
const names = await loadMilitaryNames();
const bytes = readFileSync(
new URL('./fixtures/ofm-camp-mabry-12-935-1685.pbf', import.meta.url),
);
const tile = decodeOpenFreeMapMilitaryTile(bytes, 12, 935, 1685);
const mabry = tile.military.find((r) => r.featureKey === '278942512');
assert.ok(mabry);
assert.equal(militaryOsmKey(mabry.featureKey), 'w27894251');
assert.equal(nameMilitaryFragment(mabry, names).name, 'Camp Mabry');
assert.equal(militaryOsmKey('3177123'), 'r317712');
for (const value of [
'tile:12/935/1685:0',
'278942510',
'3177127',
'bad',
1e20,
])
assert.equal(militaryOsmKey(value), null);
assert.equal(
nameMilitaryFragment({ ...mabry, featureKey: '3177123' }, names).name,
'Military area',
'a valid but distant id fails soft',
);
assert.equal(
nameMilitaryFragment({ ...mabry, featureKey: '278942510' }, names).name,
'Military area',
);
});
test('a merged title uses the largest named member and exposes all other member names', async () => {
const names = await loadMilitaryNames();
const bytes = readFileSync(
new URL('./fixtures/ofm-camp-mabry-12-935-1685.pbf', import.meta.url),
);
const fragment = decodeOpenFreeMapMilitaryTile(bytes, 12, 935, 1685)
.military[0];
const parent = nameMilitaryFragment(fragment, names);
const child = {
...parent,
featureKey: '12',
osmKey: 'w1',
nameRecord: {
...parent.nameRecord,
id: 'osm:military:w1',
osmKey: 'w1',
name: 'Small barracks',
areaM2: 1,
},
};
for (const records of [
[child, parent],
[parent, child],
]) {
const merged = mergeMilitaryFragments(records)[0];
assert.equal(merged.name, 'Camp Mabry');
assert.equal(merged.id, 'osm:military:w27894251');
assert.deepEqual(
[merged.longitude, merged.latitude],
[parent.nameRecord.longitude, parent.nameRecord.latitude],
);
assert.deepEqual(merged.memberNames, ['Small barracks']);
}
});
test('wide views rank and thin names within a bounded viewport, including the dateline', async () => {
const names = await loadMilitaryNames();
for (const box of [
{ west: -130, south: 20, east: -60, north: 60 },
{ west: -15, south: 35, east: 35, north: 65 },
{ west: 170, south: -60, east: -150, north: 70 },
]) {
const view = militaryNamesInView(names, box);
assert.ok(view.records.length > 0);
assert.ok(view.records.length <= MILITARY_POINT_CAP);
assert.ok(view.count >= view.records.length);
assert.equal(
new Set(view.records.map((r) => r.id)).size,
view.records.length,
);
for (const r of view.records) {
assert.ok(r.latitude >= box.south && r.latitude <= box.north);
assert.ok(
box.east < box.west
? r.longitude >= box.west || r.longitude <= box.east
: r.longitude >= box.west && r.longitude <= box.east,
);
}
}
});
test('a close view keeps the parent name inside its polygon when the usual label is off-screen', () => {
const record = {
id: 'osm:military:w1',
namedArea: true,
name: 'Parent base',
longitude: 1,
latitude: 1,
footprints: [
[
[
[0, 0],
[10, 0],
[10, 10],
[0, 10],
[0, 0],
],
],
],
};
const visible = militaryNameInView(record, {
west: 5,
south: 5,
east: 6,
north: 6,
});
assert.equal(visible.id, record.id);
assert.deepEqual([visible.longitude, visible.latitude], [5.5, 5.5]);
assert.equal(
militaryNameInView(record, { west: 0, south: 0, east: 2, north: 2 }),
record,
);
const hole = {
...record,
footprints: [
[
record.footprints[0][0],
[
[4, 4],
[7, 4],
[7, 7],
[4, 7],
[4, 4],
],
],
],
};
assert.equal(
militaryNameInView(hole, { west: 5, south: 5, east: 6, north: 6 }),
hole,
);
});
test('queries can take every named site in a box instead of one per display cell', async () => {
const names = await loadMilitaryNames();
const box = { west: -2, south: 50, east: 2, north: 54 };
const thinned = militaryNamesInView(names, box);
const all = militaryNamesInView(names, box, Infinity, false);
assert.equal(all.count, thinned.count);
assert.equal(all.records.length, all.count);
assert.ok(all.records.length > thinned.records.length);
});
+2 -2
View File
@@ -1,7 +1,7 @@
import { createMilitaryRegistry } from '../layers/aircraft/classification.js';
import { createAdsbLolSource } from '../sources/live/standalone.js';
import { createMilitarySource } from '../sources/live/standalone.js';
const registry = createMilitaryRegistry({ source: createAdsbLolSource() });
const registry = createMilitaryRegistry({ source: createMilitarySource() });
export const isMilitaryLayerActive = registry.isMilitaryLayerActive;
export const setMilitaryLayerActive = registry.setMilitaryLayerActive;
export const onMilitaryLayerActiveChange = registry.onMilitaryLayerActiveChange;
+38 -10
View File
@@ -124,17 +124,9 @@ let _entries = null;
const PACKS = {
regions: {
url: new URL('./local_data/natural_earth/regions.json', import.meta.url),
importJson: () =>
import('./local_data/natural_earth/regions.json', {
with: { type: 'json' },
}),
},
marine: {
url: new URL('./local_data/natural_earth/marine.json', import.meta.url),
importJson: () =>
import('./local_data/natural_earth/marine.json', {
with: { type: 'json' },
}),
},
};
@@ -178,8 +170,8 @@ function buildEntries(pack, kind) {
*/
const loadIndex = createRetryableLoader(async () => {
const [regions, marine] = await Promise.all([
loadBundledJson(PACKS.regions.url, PACKS.regions.importJson),
loadBundledJson(PACKS.marine.url, PACKS.marine.importJson),
loadBundledJson(PACKS.regions.url),
loadBundledJson(PACKS.marine.url),
]);
_entries = [
...buildEntries(regions, 'natural'),
@@ -325,3 +317,39 @@ export async function lookupNaturalRegionOutline(query, lat, lon) {
}
return null;
}
/** Resolve the smallest containing bundled physical region without a network geocoder. */
export async function naturalRegionAtPoint(latitude, longitude) {
if (
![latitude, longitude].every(Number.isFinite) ||
Math.abs(latitude) > 90 ||
Math.abs(longitude) > 180
)
return null;
await loadIndex();
let best = null;
for (const entry of _entries) {
const [west, south, east, north] = entry.bbox;
if (
longitude < west ||
longitude > east ||
latitude < south ||
latitude > north
)
continue;
if (best && entry.areaKm2 >= best.areaKm2) continue;
if (entry.polygons.some((ring) => pointInRing(ring, latitude, longitude)))
best = entry;
}
return best
? {
label: best.name,
locality: null,
region: best.name,
country: null,
countryCode: null,
source: 'Natural Earth',
kind: best.kind,
}
: null;
}
+1 -5
View File
@@ -22,10 +22,6 @@ const CITY_FILES = [
'./local_data/neighborhoods/san-francisco.json',
import.meta.url,
),
importJson: () =>
import('./local_data/neighborhoods/san-francisco.json', {
with: { type: 'json' },
}),
},
];
@@ -112,7 +108,7 @@ function cityLoader(city) {
let loader = _cityLoaders.get(city.id);
if (!loader) {
loader = createRetryableLoader(async () => {
const fc = await loadBundledJson(city.url, city.importJson);
const fc = await loadBundledJson(city.url);
return Array.isArray(fc.features) ? fc.features : [];
});
_cityLoaders.set(city.id, loader);
+94
View File
@@ -0,0 +1,94 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as C from 'cesium';
import { showOsmCredit, hideOsmCredit } from './dataCredits.js';
import { createInfrastructureLayers } from './infrastructure.js';
import { createBhoteKoshiLocatorLayer } from './bhoteKoshiLocator.js';
import { createWorldAnnotationRenderer } from '../annotations/worldAnnotationRenderer.js';
import * as annotations from '../annotations/worldAnnotationRenderer.js';
import { createDirectionsLayer } from '../layers/directions/index.js';
import { createLifecycle as createCctvLifecycle } from '../layers/cctv/lifecycle.js';
import * as input from './inputOwnership.js';
import { createOverpassAlprSource } from '../layers/alpr/source.js';
const noop = () => {};
const overlayHost = { setEntries: noop, setVisible: noop, clearSource: noop };
function viewer(t) {
t.mock.timers.enable({ apis: ['setTimeout'] });
for (const name of ['HTMLCanvasElement', 'HTMLImageElement', 'ImageBitmap', 'OffscreenCanvas']) {
const original = globalThis[name];
globalThis[name] = class {};
t.after(() => { if (original === undefined) delete globalThis[name]; else globalThis[name] = original; });
}
const before = globalThis.document;
globalThis.document = { addEventListener: noop, removeEventListener: noop };
t.after(() => { if (before === undefined) delete globalThis.document; else globalThis.document = before; });
const credits = [];
const v = { credits, creditDisplay: { addStaticCredit: (c) => credits.push(c), removeStaticCredit: c => { const i = credits.indexOf(c); if (i >= 0) credits.splice(i, 1); } },
entities: new C.EntityCollection(), dataSources: new C.DataSourceCollection(),
camera: { moveEnd: new C.Event(), positionCartographic: { height: 1000 } },
scene: { requestRender: noop, canvas: { addEventListener: noop, removeEventListener: noop }, preRender: new C.Event(),
primitives: { add: (p) => p, remove: noop }, screenSpaceCameraController: { enableInputs: true } },
};
return v;
}
function assertCredit(v) {
assert.ok(v.credits.some((credit) => credit.showOnScreen && credit.html.includes('>© OpenStreetMap<')));
}
for (const index of [0, 1]) test(`${index ? 'dams' : 'datacenters'} introduce the shared OSM credit when data displays`, async (t) => {
const v = viewer(t);
t.mock.method(globalThis, 'fetch', async () => ({ ok: true, text: async () => JSON.stringify({ type: 'Feature', id: 'test', properties: { name: 'Site' }, geometry: { type: 'Polygon', coordinates: [[[0, 0], [0.01, 0], [0, 0.01], [0, 0]]] } }) }));
const layer = createInfrastructureLayers({ overlayHost, registerEntityContext: noop, selectEntityContext: noop, clearSelectedEntityContextForLayer: noop, removeEntityContextsForLayer: noop, governorRequestRender: noop, showOsmCredit, hideOsmCredit })[index];
await layer.enable(v);
assert.equal(layer.getStats().count, 1);
assertCredit(v);
layer.destroy(v);
assert.equal(v.credits.filter(c => c.showOnScreen).length, 0);
});
test('Nepal locator introduces the shared OSM credit', async (t) => {
const v = viewer(t);
const layer = createBhoteKoshiLocatorLayer({ boundaryResolver: async () => ({ name: 'Nepal', ring: [[80, 26], [88, 26], [88, 30], [80, 30]] }), overlayHost, requestRender: noop, scheduleFrame: () => 1, cancelFrame: noop });
await layer.init(v); await layer.enable(v, { origin: 'scene' });
assertCredit(v); await layer.destroy(v);
assert.equal(v.credits.filter(c => c.showOnScreen).length, 0);
});
test('voice route geometry introduces the shared OSM credit', (t) => {
const v = viewer(t), renderer = createWorldAnnotationRenderer(v);
renderer.add({ id: 'route', type: 'route', color: 'primary', anchor: { lat: 0, lon: 0 }, path: [{ lat: 0, lon: 0 }, { lat: 0.01, lon: 0.01 }] });
assertCredit(v); renderer.destroy();
assert.equal(v.credits.filter(c => c.showOnScreen).length, 0);
});
test('Directions introduces the shared OSM credit when the requested route draws', async (t) => {
const v = viewer(t);
t.mock.method(globalThis, 'fetch', async () => Response.json({ ok: true, geometry: [[0, 0], [0.01, 0.01]], steps: [] }));
const layer = createDirectionsLayer({ services: {
credits: { showOsmCredit, hideOsmCredit }, annotations, input, scenePick: {}, camera: {},
overlays: { setOverlayEntries: noop, setOverlaySourceVisible: noop, clearOverlaySource: noop },
render: { governorRequestRender: noop, holdContinuousRender: noop, releaseContinuousRender: noop },
sprites: { registerSpriteCollection: noop, unregisterSpriteCollection: noop, restoreSpriteOrder: noop },
picking: { registerPickOwner: noop, unregisterPickOwner: noop },
ground: { warmGroundFloor: noop },
} });
layer.init(v); layer.enable(v);
layer.placeEndpoint('a', { lat: 0, lon: 0 }); layer.placeEndpoint('b', { lat: 0.01, lon: 0.01 });
for (let i = 0; i < 30; i++) await Promise.resolve();
assert.equal(layer.getStats().error, null);
assertCredit(v); await layer.destroy(v);
assert.equal(v.credits.filter(c => c.showOnScreen).length, 0);
});
test('Warendorf webcam display introduces the shared OSM credit', (t) => {
const v = viewer(t), empty = new Proxy({}, { get: () => noop });
const lifecycle = createCctvLifecycle({ state: { _viewer: v, _records: [{ camera: { id: 'warendorf-marktplatz-rathaus', cityId: 'warendorf' } }], _recordById: new Map(), _cctvOverlayHost: overlayHost },
services: { credits: { showOsmCredit, hideOsmCredit }, sprites: empty, activation: empty, picking: empty, focus: empty, render: empty },
parts: { selection: empty, geometryQueue: empty, rendering: empty, projection: empty, cards: empty, presentation: empty }, source: {} });
lifecycle.methods.enable(); assertCredit(v); lifecycle.methods.disable();
assert.equal(v.credits.filter(c => c.showOnScreen).length, 0);
});
test('optional Overpass ALPR adapter uses the shared contributor wording', () => {
assert.equal(createOverpassAlprSource().attribution.text, '© OpenStreetMap contributors');
});
+2 -2
View File
@@ -189,7 +189,7 @@ test('a conversion survives a poll refresh, in both the billboard and the tracke
const realFetch = globalThis.fetch;
const nowSec = Math.floor(Date.now() / 1000);
globalThis.fetch = async (url) => {
if (!String(url).startsWith('/api/opensky')) {
if (!String(url).startsWith('/api/flights')) {
return { ok: true, status: 200, json: async () => ({ ac: [] }) };
}
return {
@@ -309,7 +309,7 @@ test('conversions are session-scoped and no lifecycle path clears them', async (
meta: { callsign: 'OLD1', altitude: 9_000, klass: 'airliner', rawLat: 30.2, rawLon: -97.7 },
});
const realFetch = globalThis.fetch;
globalThis.fetch = async (url) => (String(url).startsWith('/api/opensky')
globalThis.fetch = async (url) => (String(url).startsWith('/api/flights')
? { ok: true, status: 200, headers: { get: () => null }, json: async () => ({ time: 0, states: [] }) }
: { ok: true, status: 200, json: async () => ({ ac: [] }) });
try {
+1 -5
View File
@@ -219,11 +219,7 @@ export function getActiveTrackedReadoutId() {
}
/** Static-context layers whose click selection publishes the readout card. */
const READOUT_CONTEXT_LAYERS = new Set([
'military-installations',
'alpr-cameras',
'local-adsb',
]);
const READOUT_CONTEXT_LAYERS = new Set(['alpr-cameras', 'local-adsb']);
/**
* Initialize the model bridge and selection listeners. No render listener is
+3 -3
View File
@@ -191,7 +191,7 @@ test('tracked entity publishes a protected host entry backed by the frame cache'
}
});
test('selection lifecycle ignores vessels, accepts installations, and clears without stale cards', () => {
test('selection lifecycle leaves installations to their own entry and retains ALPR cards', () => {
const originalWindow = globalThis.window;
const fakeWindow = new EventTarget();
const changed = makeCesiumEvent();
@@ -215,8 +215,8 @@ test('selection lifecycle ignores vessels, accepts installations, and clears wit
fakeWindow.dispatchEvent(new CustomEvent('gev:entity-selected', {
detail: { layerId: 'military-installations', entity: installation },
}));
assert.equal(getActiveTrackedReadoutId(), 'installations:fort-test');
assert.equal(recorder.calls.filter(({ op }) => op === 'set').at(-1).entries[0].title, 'FORT TEST');
assert.equal(getActiveTrackedReadoutId(), null);
assert.equal(recorder.calls.filter(({ op }) => op === 'set').length, setsBefore, 'installation selection cannot create a second overlay');
// ALPR cameras are static context too: a click publishes the same card.
const camera = {
+10 -2
View File
@@ -76,11 +76,11 @@ test('a healthy keyed layer reports live flow with its real coverage', () => {
assert.deepEqual(idle, {
mode: 'live',
error: null,
loadingLabel: 'LIVE · TomTom flow · 87% cov',
loadingLabel: 'LIVE · Roads: OpenStreetMap · Flow: TomTom · 87% cov · Unmatched: simulated',
});
assert.equal(
trafficFeedPresentation({ liveMode: true, fetching: true }).loadingLabel,
'syncing LIVE traffic flow',
'Syncing flow · Roads: OpenStreetMap · Flow: TomTom · Unmatched: simulated',
);
});
@@ -159,3 +159,11 @@ test('traffic can be destroyed before its first enable and destroyed repeatedly'
assert.doesNotThrow(() => traffic.destroy(viewer));
assert.equal(traffic.getStats().count, 0);
});
test('zero matched dots never claims live coverage', () => {
const feed = trafficFeedPresentation({ liveMode: true, coveragePct: 0 });
assert.equal(feed.mode, 'live');
assert.match(feed.loadingLabel, /^SIMULATED/);
assert.match(feed.loadingLabel, /TomTom \(no matches\)/);
assert.ok(!LIVE_CLAIM.test(feed.loadingLabel));
});
+1 -1
View File
@@ -366,7 +366,7 @@ test('the timed parser stays source-equivalent to the production parser', () =>
assert.doesNotMatch(SOURCE, /function fetchRoadsTimed\s*\(/);
assert.doesNotMatch(SOURCE, /function renderRoadsForAltitudeTimed\s*\(/);
const fetchBody = canonicalSemanticBody(functionBody('fetchRoads'));
const fetchBody = canonicalSemanticBody(functionBody('readRoads'));
assert.equal(fetchBody.includes('response.text()'), false);
assert.equal(fetchBody.includes('JSON.parse('), false);
const okCheck = fetchBody.indexOf('if(!response.ok)');