Merge remote-tracking branch 'origin/main' into feat/king-county-camera-pack

# Conflicts:
#	CHANGELOG.md
#	docs/CURRENT-STATE.md
This commit is contained in:
bassem chagra committed 2026-10-01 12:20:32 +02:00
commit 031df60014
201 files changed
+26687 -1996

No files matched your search

+821
View File
@@ -0,0 +1,821 @@
/**
* 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),
importJson: () =>
import('./local_data/natural_earth/countries.json', {
with: { type: 'json' },
}),
},
admin1: {
url: new URL(
'./local_data/natural_earth/states_provinces.json',
import.meta.url,
),
importJson: () =>
import('./local_data/natural_earth/states_provinces.json', {
with: { type: 'json' },
}),
},
counties: {
url: new URL(
'./local_data/us_census_counties/counties.json',
import.meta.url,
),
importJson: () =>
import('./local_data/us_census_counties/counties.json', {
with: { type: 'json' },
}),
},
};
/** 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, PACKS.admin1.importJson);
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,
PACKS.counties.importJson,
);
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,
PACKS.countries.importJson,
);
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',
);
});
+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); },
+3 -2
View File
@@ -19,9 +19,10 @@
* `import('./….json', { with: { type: 'json' } })`, used only under Node.
* @returns {Promise<any>} The parsed JSON.
*/
export async function loadBundledJson(url, importJson) {
export async function loadBundledJson(url, importJson, { signal } = {}) {
if (url.protocol === 'file:') return (await importJson()).default;
const response = await fetch(url);
signal?.throwIfAborted();
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status} for ${url.pathname}`);
}
+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);
};
}
+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',
@@ -221,7 +260,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>',
},
{
@@ -271,19 +309,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',
@@ -316,9 +344,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',
@@ -351,15 +377,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).
@@ -395,10 +430,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
+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]);
});
+2
View File
@@ -22,6 +22,7 @@ export function createInfrastructureLayers(services) {
color: '#00ffff', // Cyan
icon: '▣',
source: 'Local',
osmDerived: true,
labels: true,
labelMax: 700,
labelGridPx: 138,
@@ -37,6 +38,7 @@ export function createInfrastructureLayers(services) {
color: '#0088ff', // Blue
icon: '▰',
source: 'USACE',
osmDerived: true,
labels: true,
labelMax: 900,
labelGridPx: 132,
+9
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,7 @@ test('consumer build includes only infrastructure code and resolves assets under
const sources = Object.keys(entry.modules).filter((id) => id.endsWith('.js'));
assert.deepEqual(sources.map((id) => id.split('/').at(-1)).sort(), [
'infrastructure.js',
'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);
});
+13 -1
View File
@@ -298,6 +298,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',
@@ -637,7 +644,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',
+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
+9 -25
View File
@@ -1,4 +1,5 @@
import * as Cesium from 'cesium';
import { createInfrastructureOverlayEntry } from './infrastructureOverlayEntry.js';
import { isPointerFree } from './inputOwnership.js';
import {
selectInfraLod,
@@ -14,10 +15,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
@@ -182,33 +179,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 +373,7 @@ export function createLocalGeoJsonLayer(
color,
icon = '📍',
source = 'Local JSONL',
osmDerived = false,
labels = true,
labelMax = DEFAULT_LABEL_MAX,
labelGridPx = DEFAULT_LABEL_GRID_PX,
@@ -409,6 +389,8 @@ export function createLocalGeoJsonLayer(
clearSelectedEntityContextForLayer,
removeEntityContextsForLayer,
governorRequestRender,
showOsmCredit,
hideOsmCredit,
},
) {
let _dataSource = null;
@@ -538,6 +520,7 @@ export function createLocalGeoJsonLayer(
};
const disableLayer = (viewer) => {
hideOsmCredit?.(viewer, id);
_enabled = false;
clearGroundRetryRender();
_activeLodIds = new Set();
@@ -942,6 +925,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(() => {
+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
+38
View File
@@ -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();
}
});
+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(); }
});
+115
View File
@@ -0,0 +1,115 @@
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),
() =>
import('./local_data/osm_military_names/names.json', {
with: { type: 'json' },
}),
{ 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. */
export function militaryNamesInView(names, box, cap = MILITARY_POINT_CAP) {
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 (cells.has(cell) || records.length >= cap) continue;
cells.add(cell);
records.push(record);
}
return { records, count };
}
+219
View File
@@ -0,0 +1,219 @@
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,
);
});
+36
View File
@@ -325,3 +325,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;
}
+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');
});
+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)');