Files
gods-eye-view/src/locations.test.mjs
T

781 lines
34 KiB
JavaScript

import { createStandalonePlaceSearch } from './standalone/placeSearch.js';
// Camera-framing mode contract for fly_to_location (field test 8 + rootcause doc §3):
// parks/lakes/campuses and streets are NOT precise POIs — flying to "Zilker Park" at
// building range (250 m) lands on a random rooftop. Pure mapping tests, no network.
//
// Run with: npm test (node --test)
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import * as Cesium from 'cesium';
import {
CANCELLED_SEARCH,
placeFramingViewport,
PLACE_VIEWPORT_MAX_SPAN_KM,
PLACE_ANCHOR_OFFSET_RATIO,
flyToGlobeView,
flyToPresetLocation,
geocodeNavigationMode,
regionFramingPlan,
REGION_SWATH_SPAN_KM,
GLOBE_VIEW,
searchAndFlyTo,
} from './locations.js';
function stubViewer() {
const flights = [];
return {
flights,
scene: { globe: null, canvas: { clientWidth: 0, clientHeight: 0 } },
camera: {
positionCartographic: {
longitude: Cesium.Math.toRadians(-97.7431),
latitude: Cesium.Math.toRadians(30.2672),
height: 1200,
},
cancelFlight() {},
flyTo(options) { flights.push(options); },
flyToBoundingSphere(sphere, options) { flights.push({ sphere, ...options }); },
lookAt() {},
lookAtTransform() {},
},
};
}
const AUSTIN_RESULT = {
formatted_address: 'Austin, TX, USA',
types: ['locality', 'political'],
geometry: {
location: { lat: 30.2672, lng: -97.7431 },
viewport: {
southwest: { lat: 30.1, lng: -97.95 },
northeast: { lat: 30.5, lng: -97.55 },
},
},
};
async function runSearch(viewer, options, { result = AUSTIN_RESULT, query = 'austin' } = {}) {
const hadWindow = Object.hasOwn(globalThis, 'window');
const priorWindow = globalThis.window;
const priorFetch = globalThis.fetch;
globalThis.window = { __GOOGLE_MAPS_API_KEY__: 'test-key' };
globalThis.fetch = async () => ({
json: async () => ({ status: 'OK', results: [result] }),
});
try {
return await searchAndFlyTo(viewer, query, { placeSearch: createStandalonePlaceSearch({ resolveApiKey: () => globalThis.window?.__GOOGLE_MAPS_API_KEY__ }), ...options });
} finally {
globalThis.fetch = priorFetch;
if (hadWindow) globalThis.window = priorWindow;
else delete globalThis.window;
}
}
/** Read a recorded viewport flight back as a degrees rectangle. */
function flownRectangleDegrees(viewer, index = 0) {
const rectangle = viewer.flights[index]?.destination;
assert.ok(rectangle instanceof Cesium.Rectangle, 'expected a rectangle (viewport) flight');
return {
south: Cesium.Math.toDegrees(rectangle.south),
north: Cesium.Math.toDegrees(rectangle.north),
west: Cesium.Math.toDegrees(rectangle.west),
east: Cesium.Math.toDegrees(rectangle.east),
// Cesium's own width, which adds a full turn when the box crosses the
// antimeridian — the number the camera actually frames.
widthDeg: Cesium.Math.toDegrees(rectangle.width),
crossesAntimeridian: rectangle.east < rectangle.west,
};
}
test('parks / lakes / campuses frame as area-overview, not precise-place', () => {
// Zilker Park — the field-test rooftop bug.
assert.equal(geocodeNavigationMode(['park', 'tourist_attraction', 'point_of_interest', 'establishment']), 'area-overview');
// Lady Bird Lake.
assert.equal(geocodeNavigationMode(['natural_feature', 'establishment']), 'area-overview');
assert.equal(geocodeNavigationMode(['university', 'point_of_interest']), 'area-overview');
assert.equal(geocodeNavigationMode(['airport']), 'area-overview');
});
test('streets frame as street-corridor (rootcause doc §3 — Sixth Street)', () => {
assert.equal(geocodeNavigationMode(['route']), 'street-corridor');
assert.equal(geocodeNavigationMode(['intersection']), 'street-corridor');
});
test('existing modes unchanged: admin, city, neighborhood, precise POI', () => {
assert.equal(geocodeNavigationMode(['country', 'political']), 'region-overview');
assert.equal(geocodeNavigationMode(['administrative_area_level_1', 'political']), 'region-overview');
assert.equal(geocodeNavigationMode(['locality', 'political']), 'city-overview');
assert.equal(geocodeNavigationMode(['neighborhood', 'political']), 'neighborhood-close');
// A statue / small POI stays precise (tourist_attraction alone must NOT widen it).
assert.equal(geocodeNavigationMode(['tourist_attraction', 'point_of_interest', 'establishment']), 'precise-place');
assert.equal(geocodeNavigationMode(['premise']), 'precise-place');
assert.equal(geocodeNavigationMode([]), 'precise-place');
});
test('admin types win over area types when both present', () => {
// A locality that is also tagged park-ish should still frame as the locality.
assert.equal(geocodeNavigationMode(['locality', 'park', 'political']), 'city-overview');
});
// Natural-region framing heuristic (owner field test 2026-07-23): "take me to the
// Rocky Mountains" geocodes as natural_feature with a ~2,700 km viewport — framing
// the whole box flies the camera to space. Region-scale viewports get a capped
// oblique swath instead; ordinary parks/lakes keep full-viewport framing.
test('regionFramingPlan: Rocky-Mountains-scale viewport gets a capped oblique swath', () => {
// Real-shape geocode bounds for the Rockies: ~25° of latitude, N-S elongated.
const plan = regionFramingPlan({
southwest: { lat: 35.0, lng: -119.0 },
northeast: { lat: 60.0, lng: -105.0 },
});
assert.equal(plan.mode, 'swath');
assert.ok(plan.spanKm > 2000, `span should be huge, got ${plan.spanKm}`);
// Camera range is CAPPED at regional scale — never the multi-thousand-km bbox fit.
assert.ok(plan.rangeM <= 400000, `rangeM must stay regional, got ${plan.rangeM}`);
assert.ok(plan.rangeM >= 150000, `rangeM must still read as a region, got ${plan.rangeM}`);
// Oblique cinematic angle, not straight down.
assert.ok(plan.pitchDeg > -60 && plan.pitchDeg <= -20, `pitch should be oblique, got ${plan.pitchDeg}`);
// N-S elongated range → camera looks along the long axis (north).
assert.equal(plan.headingDeg, 0);
// Swath is centered on the feature.
assert.ok(Math.abs(plan.centerLat - 47.5) < 0.01);
assert.ok(Math.abs(plan.centerLng - (-112)) < 0.01);
});
test('regionFramingPlan: E-W elongated region looks along the east axis', () => {
// Alps-shaped box: ~2.3° lat, ~11° lon.
const plan = regionFramingPlan({
southwest: { lat: 45.5, lng: 5.0 },
northeast: { lat: 47.8, lng: 16.0 },
});
assert.equal(plan.mode, 'swath');
assert.equal(plan.headingDeg, 90);
});
// Compatibility guard for inherited geocode authority, not a newly fixed bug.
test('search retains main geocode authority for Gold Coast, Australia', async () => {
const viewer = stubViewer();
const result = await runSearch(viewer, {}, {
query: 'Gold Coast',
result: {
formatted_address: 'Gold Coast, Queensland, Australia',
types: ['locality', 'political'],
geometry: {
location: { lat: -28, lng: 153.4 },
viewport: boxOf(-28.2, 153.2, -27.8, 153.6),
},
},
});
assert.equal(result.label, 'Gold Coast, Queensland, Australia');
assert.equal(result.navigationMode, 'city-overview');
assert.equal(viewer.flights.length, 1);
const flown = flownRectangleDegrees(viewer);
assert.ok(Math.abs((flown.south + flown.north) / 2 + 28) < 0.000001);
assert.ok(Math.abs((flown.west + flown.east) / 2 - 153.4) < 0.000001);
});
function alpsViewer() {
const viewer = stubViewer();
Object.assign(viewer.camera.positionCartographic, {
latitude: Cesium.Math.toRadians(46.5),
longitude: Cesium.Math.toRadians(10),
height: 248_000,
});
return viewer;
}
test('a successful nearby recovery resolves after a geocoder miss', async () => {
const viewer = alpsViewer();
const result = await searchAndFlyTo(viewer, 'the Alps', {
placeSearch: { async geocode() { return { place: null, answered: true }; } },
recoverNearView: async () => ({
lat: 46.6,
lon: 10.1,
label: 'The Alps local landmark',
types: ['locality', 'political'],
viewport: { low: { latitude: 46.5, longitude: 10 }, high: { latitude: 46.7, longitude: 10.2 } },
}),
});
assert.equal(result.label, 'The Alps local landmark');
assert.equal(result.navigationMode, 'city-overview');
assert.equal(viewer.flights.length, 1);
});
test('a geocoder miss returns null even over a namesake region', async () => {
for (const answered of [true, false]) {
const viewer = alpsViewer();
const result = await searchAndFlyTo(viewer, 'the Alps', {
placeSearch: { async geocode() { return { place: null, answered }; } },
recoverNearView: async () => null,
});
assert.equal(result, null);
assert.equal(viewer.flights.length, 0);
}
});
test('a geocoder rejection propagates without nearby recovery or a flight', async () => {
const viewer = alpsViewer();
const failure = new Error('geocoder failed');
let recoveries = 0;
await assert.rejects(searchAndFlyTo(viewer, 'the Alps', {
placeSearch: { async geocode() { throw failure; } },
recoverNearView: async () => { recoveries += 1; return null; },
}), (error) => error === failure);
assert.equal(recoveries, 0);
assert.equal(viewer.flights.length, 0);
});
test('search cancellation before or during geocoding cannot issue a flight', async () => {
for (const preAborted of [true, false]) {
const viewer = alpsViewer();
const controller = new AbortController();
if (preAborted) controller.abort();
let geocodes = 0;
let recoveries = 0;
await assert.rejects(searchAndFlyTo(viewer, 'the Alps', {
signal: controller.signal,
placeSearch: {
async geocode(_query, { signal }) {
geocodes += 1;
assert.equal(signal, controller.signal);
controller.abort();
return { place: null, answered: true };
},
},
recoverNearView: async () => { recoveries += 1; return null; },
}), { name: 'AbortError' });
assert.equal(geocodes, preAborted ? 0 : 1);
assert.equal(recoveries, 0);
assert.equal(viewer.flights.length, 0);
}
});
test('cancellation during nearby recovery cannot issue a flight', async () => {
const viewer = alpsViewer();
const controller = new AbortController();
let authorityChecks = 0;
await assert.rejects(searchAndFlyTo(viewer, 'the Alps', {
signal: controller.signal,
placeSearch: { async geocode() { return { place: null, answered: true }; } },
recoverNearView: async () => {
controller.abort();
return {
lat: 46.6, lon: 10.1, label: 'Alpine landmark',
types: ['locality'],
};
},
beforeFly: () => { authorityChecks += 1; return true; },
}), { name: 'AbortError' });
assert.equal(authorityChecks, 0);
assert.equal(viewer.flights.length, 0);
});
test('regionFramingPlan: park- and lake-scale viewports keep full framing', () => {
// Zilker Park (~3 km).
assert.equal(regionFramingPlan({
southwest: { lat: 30.26, lng: -97.78 },
northeast: { lat: 30.28, lng: -97.75 },
}).mode, 'full');
// Lake Tahoe (~50 km).
assert.equal(regionFramingPlan({
southwest: { lat: 38.90, lng: -120.20 },
northeast: { lat: 39.25, lng: -119.90 },
}).mode, 'full');
});
test('regionFramingPlan: threshold splits full vs swath', () => {
// ~330 km tall box (3° latitude, negligible width) → still full framing.
const under = regionFramingPlan({
southwest: { lat: 40.0, lng: -100.0 },
northeast: { lat: 43.0, lng: -99.9 },
});
assert.equal(under.mode, 'full');
assert.ok(under.spanKm < REGION_SWATH_SPAN_KM);
// ~560 km tall box (5° latitude) → swath.
const over = regionFramingPlan({
southwest: { lat: 40.0, lng: -100.0 },
northeast: { lat: 45.0, lng: -99.9 },
});
assert.equal(over.mode, 'swath');
assert.ok(over.spanKm > REGION_SWATH_SPAN_KM);
});
test('regionFramingPlan: antimeridian-crossing viewport measured the short way round', () => {
// 20° of longitude across the date line at the equator (~2,200 km), NOT ~340°.
const plan = regionFramingPlan({
southwest: { lat: -5.0, lng: 170.0 },
northeast: { lat: 5.0, lng: -170.0 },
});
assert.equal(plan.mode, 'swath');
assert.ok(plan.spanKm < 3000, `span must use the short arc, got ${plan.spanKm}`);
// Center sits on the antimeridian, normalized into [-180, 180].
assert.ok(Math.abs(Math.abs(plan.centerLng) - 180) < 0.01);
});
// ── Off-centre viewport sanity gate (2026-08-20 QA hunt) ────────────────────
// Free-text "Tokyo" flew the camera ~977 km out over the open Pacific. Tokyo
// geocodes as the PREFECTURE (administrative_area_level_1), and Tokyo Metropolis
// owns the Izu/Ogasawara chains ~1,000 km out to sea — so its bounding box is
// mostly ocean and its centroid is nowhere near the city. "Hawaii" behaves the
// same way via the Northwestern Hawaiian Islands.
//
// EVERY fixture below is SYNTHETIC geometry, constructed here. Google Maps
// content — geocodes included — may not be cached, stored, rehosted, or
// committed (DATA_SOURCES.md), so no captured API response appears in this repo.
// The fixtures reproduce the SHAPES that matter instead: an oversized box whose
// place sits in one corner, an oversized box centred on its place, the same
// crossing the antimeridian, and city-sized boxes.
/** Synthetic geocode bounds from explicit south/west/north/east degrees. */
function boxOf(southLat, westLng, northLat, eastLng) {
return { southwest: { lat: southLat, lng: westLng }, northeast: { lat: northLat, lng: eastLng } };
}
/** Synthetic geocode bounds from a centre and its degree spans. */
function boxAround(centerLat, centerLng, latSpanDeg, lonSpanDeg) {
return boxOf(
centerLat - latSpanDeg / 2,
centerLng - lonSpanDeg / 2,
centerLat + latSpanDeg / 2,
centerLng + lonSpanDeg / 2,
);
}
/** Build a synthetic geocode result around one of the boxes above. */
function resultOf(types, bounds, lat, lng, label = 'Synthetic Place') {
return { formatted_address: label, types, geometry: { location: { lat, lng }, bounds } };
}
// ~3,055 km box whose place sits near the NW corner, ~1,352 km off the centroid
// → ratio ~0.44. Mirrors the Tokyo (0.397) and Hawaii (0.456) failures.
const OFF_CENTRE_ADMIN_BOX = boxOf(10, 100, 30, 120);
const OFF_CENTRE_ADMIN_ANCHOR = { lat: 29, lng: 101 };
const OFF_CENTRE_ADMIN_RESULT = resultOf(
['administrative_area_level_1', 'political'],
OFF_CENTRE_ADMIN_BOX,
OFF_CENTRE_ADMIN_ANCHOR.lat,
OFF_CENTRE_ADMIN_ANCHOR.lng,
'Off-Centre Prefecture',
);
// ~1,439 km box centred on its place (~62 km off → ratio ~0.043). Mirrors a
// state or province: big, but the box IS the place.
const CENTRED_REGION_BOX = boxAround(35, -5, 10, 10);
const CENTRED_REGION_RESULT = resultOf(
['administrative_area_level_1', 'political'],
CENTRED_REGION_BOX,
35.5,
-5.3,
'Centred Province',
);
// Well-centred region crossing the antimeridian: 20 deg tall, 60 deg wide the
// SHORT way round (sw 170E, ne -130). Mirrors Alaska.
const ANTIMERIDIAN_REGION_BOX = boxOf(50, 170, 70, -130);
const ANTIMERIDIAN_REGION_RESULT = resultOf(
['administrative_area_level_1', 'political'],
ANTIMERIDIAN_REGION_BOX,
62.5,
-163,
'Antimeridian Territory',
);
test('placeFramingViewport: an off-centre admin box collapses to a metro box on the RESULT location', () => {
const framed = placeFramingViewport(
OFF_CENTRE_ADMIN_BOX,
OFF_CENTRE_ADMIN_ANCHOR.lat,
OFF_CENTRE_ADMIN_ANCHOR.lng,
OFF_CENTRE_ADMIN_RESULT.types,
);
const centerLat = (framed.southwest.lat + framed.northeast.lat) / 2;
const centerLng = (framed.southwest.lng + framed.northeast.lng) / 2;
// Centred on the place — NOT the box centroid, which is ~1,350 km away.
assert.ok(Math.abs(centerLat - OFF_CENTRE_ADMIN_ANCHOR.lat) < 1e-6, `centred on the place, got ${centerLat}`);
assert.ok(Math.abs(centerLng - OFF_CENTRE_ADMIN_ANCHOR.lng) < 1e-6, `centred on the place, got ${centerLng}`);
// And it is metro-sized: ~40 km on a side, well inside the gate.
const plan = regionFramingPlan(framed);
assert.ok(plan.spanKm < 60, `metro box must stay city-scale, got ${plan.spanKm} km`);
});
test('placeFramingViewport: genuinely large, well-centred regions keep whole-place framing', () => {
// Each is far over the span gate, but its place sits near the box centre, so
// the box IS the place. The last one also exercises the antimeridian path.
const regions = [
['centred province', CENTRED_REGION_BOX, 35.5, -5.3],
['tall province', boxAround(-20, 140, 12, 9), -20.4, 140.3],
['broad territory', boxAround(60, -95, 24, 40), 61.2, -93.5],
['antimeridian territory', ANTIMERIDIAN_REGION_BOX, 62.5, -163],
];
for (const [name, box, lat, lng] of regions) {
assert.ok(
regionFramingPlan(box).spanKm > PLACE_VIEWPORT_MAX_SPAN_KM,
`${name} is over the span gate by construction`,
);
assert.equal(
placeFramingViewport(box, lat, lng, ['administrative_area_level_1', 'political']),
box,
`${name} must still frame its own bounds`,
);
}
});
test('placeFramingViewport: ordinary city viewports never reach the gate', () => {
// City-scale boxes across the latitude range, including a municipality as wide
// as the widest real one measured (~140 km corner to corner).
const cityBoxes = [
['equatorial city', boxAround(0, 100, 0.5, 0.5)],
['mid-latitude metro', boxAround(35, 20, 0.4, 0.5)],
['southern coastal city', boxAround(-23, -43, 0.65, 0.5)],
['high-latitude capital', boxAround(51.5, -0.1, 0.42, 0.84)],
['wide municipality', boxAround(61, -150, 0.87, 1.93)],
];
for (const [name, box] of cityBoxes) {
assert.ok(
regionFramingPlan(box).spanKm < PLACE_VIEWPORT_MAX_SPAN_KM,
`${name} must sit under the gate, got ${regionFramingPlan(box).spanKm} km`,
);
assert.equal(placeFramingViewport(box, 0, 0, ['locality']), box, `${name} must be framed as geocoded`);
}
});
test('placeFramingViewport: the gate needs BOTH an oversized box and an off-centre anchor', () => {
// A 3-degree box near the equator: ~470 km diagonal, over the span gate.
const over = boxOf(8, 18, 11, 21);
const spanKm = regionFramingPlan(over).spanKm;
assert.ok(spanKm > PLACE_VIEWPORT_MAX_SPAN_KM);
// Anchor at the centre → the box is the place, keep it.
assert.equal(placeFramingViewport(over, 9.5, 19.5, ['administrative_area_level_1']), over);
// Anchor pushed past the ratio → collapse to a metro box.
const offCentreLat = 9.5 + (spanKm * PLACE_ANCHOR_OFFSET_RATIO * 1.5) / 111.32;
assert.notEqual(placeFramingViewport(over, offCentreLat, 19.5, ['administrative_area_level_1']), over);
// Under the span gate, even a wildly off-centre anchor is left alone.
const under = boxOf(9, 19, 10.9, 20.9);
assert.ok(regionFramingPlan(under).spanKm < PLACE_VIEWPORT_MAX_SPAN_KM);
assert.equal(placeFramingViewport(under, 9.05, 19.05, ['locality']), under);
});
test('placeFramingViewport: countries are exempt, deliberately and on the record', () => {
// Several countries have the identical pathology from overseas territories and
// today fly to a mid-ocean centroid. Reframing a country is a bigger call than
// fixing a prefecture, so it is left for the owner rather than changed silently.
// Same shape as the gated fixture, differing ONLY in the `country` type.
assert.notEqual(
placeFramingViewport(OFF_CENTRE_ADMIN_BOX, 29, 101, ['administrative_area_level_1', 'political']),
OFF_CENTRE_ADMIN_BOX,
'the shape itself is gated when it is an administrative area',
);
assert.equal(
placeFramingViewport(OFF_CENTRE_ADMIN_BOX, 29, 101, ['country', 'political']),
OFF_CENTRE_ADMIN_BOX,
'country framing must stay exactly as shipped until the owner rules on it',
);
});
test('placeFramingViewport: no usable viewport or anchor leaves framing alone', () => {
assert.equal(placeFramingViewport(null, 29, 101, ['locality']), null);
assert.equal(placeFramingViewport(undefined, 29, 101, ['locality']), undefined);
// Without a finite anchor there is nothing better to fly to than the box.
assert.equal(placeFramingViewport(OFF_CENTRE_ADMIN_BOX, NaN, 101, ['administrative_area_level_1']), OFF_CENTRE_ADMIN_BOX);
assert.equal(placeFramingViewport(OFF_CENTRE_ADMIN_BOX, 29, undefined, ['administrative_area_level_1']), OFF_CENTRE_ADMIN_BOX);
});
test('an off-centre admin search lands over the place, not the box centroid', async () => {
const viewer = stubViewer();
const result = await runSearch(viewer, {}, { result: OFF_CENTRE_ADMIN_RESULT, query: 'Off-Centre Prefecture' });
assert.equal(result.navigationMode, 'region-overview', 'the fixture is a prefecture, not a locality');
const flown = flownRectangleDegrees(viewer);
const centerLat = (flown.south + flown.north) / 2;
const centerLng = (flown.west + flown.east) / 2;
assert.ok(Math.abs(centerLat - 29) < 0.01, `expected the place latitude, got ${centerLat}`);
assert.ok(Math.abs(centerLng - 101) < 0.01, `expected the place longitude, got ${centerLng}`);
// The regression: the old framing centred on the box centroid, ~9 degrees away.
assert.ok(centerLng < 105, 'must not drift toward the far side of the box');
// Metro framing, not an ocean-wide box.
assert.ok(flown.widthDeg < 1, `expected a metro-scale box, got ${flown.widthDeg}°`);
});
test('a well-centred region search still frames its whole bounds', async () => {
const viewer = stubViewer();
const result = await runSearch(viewer, {}, { result: CENTRED_REGION_RESULT, query: 'Centred Province' });
assert.equal(result.navigationMode, 'region-overview');
const flown = flownRectangleDegrees(viewer);
// The region's own bounds, plus flyToViewportBounds' 12% padding — never a metro box.
assert.ok(flown.south <= 30 && flown.north >= 40, `must span the region N-S, got ${flown.south}..${flown.north}`);
assert.ok(flown.west <= -10 && flown.east >= 0, `must span the region E-W, got ${flown.west}..${flown.east}`);
});
// ── Explicit overview intent outranks the sanity gate ───────────────────────
// The gate exists to guess what an ambiguous place name meant. "Show me an
// overview of Hawaii" (voice `viewMode: 'overview'`) leaves nothing to guess, so
// the whole administrative area must be framed even though the gate would fire.
test('an explicit overview ask frames the whole administrative area', async () => {
const gated = stubViewer();
await runSearch(gated, {}, { result: OFF_CENTRE_ADMIN_RESULT, query: 'Off-Centre Prefecture' });
const gatedFlight = flownRectangleDegrees(gated);
assert.ok(gatedFlight.widthDeg < 1, 'without the ask, the gate collapses it to a metro box');
const overview = stubViewer();
await runSearch(
overview,
{ viewMode: 'overview' },
{ result: OFF_CENTRE_ADMIN_RESULT, query: 'Off-Centre Prefecture' },
);
const flown = flownRectangleDegrees(overview);
// The full 20x20 degree box plus 12% padding, not a 0.4 degree metro box.
assert.ok(flown.widthDeg > 20, `overview must frame the whole area, got ${flown.widthDeg}°`);
assert.ok(flown.south <= 10 && flown.north >= 30, `must span the area N-S, got ${flown.south}..${flown.north}`);
});
test('an explicit overview ask still reaches a well-centred region unchanged', async () => {
const viewer = stubViewer();
await runSearch(
viewer,
{ viewMode: 'overview' },
{ result: CENTRED_REGION_RESULT, query: 'Centred Province' },
);
const flown = flownRectangleDegrees(viewer);
assert.ok(flown.south <= 30 && flown.north >= 40);
assert.ok(flown.west <= -10 && flown.east >= 0);
});
// ── Antimeridian framing in the ACTUAL flight ───────────────────────────────
// flyToViewportBounds padded from a raw longitude subtraction, so any box
// crossing the antimeridian inflated catastrophically: a 0.41 degree metro box
// straddling the dateline subtracts to -359.6, whose 12% padding alone is 43
// degrees, and the camera framed ~86.7 degrees of ocean.
test('a metro fallback straddling the antimeridian frames a metro box, not a hemisphere', async () => {
// Oversized box crossing the dateline whose place sits at 179.9 — the gate
// fires and produces a metro box that straddles +/-180.
const bounds = boxOf(10, 161, 30, -159);
const result = resultOf(['administrative_area_level_1', 'political'], bounds, 29, 179.9, 'Dateline Prefecture');
const viewer = stubViewer();
await runSearch(viewer, {}, { result, query: 'Dateline Prefecture' });
const flown = flownRectangleDegrees(viewer);
assert.ok(flown.crossesAntimeridian, 'the framed box must be a real east<west crossing rectangle');
assert.ok(
flown.widthDeg > 0.3 && flown.widthDeg < 1,
`expected a metro-scale width across the dateline, got ${flown.widthDeg}°`,
);
assert.ok(flown.west >= -180 && flown.west <= 180, `west must be normalized, got ${flown.west}`);
assert.ok(flown.east >= -180 && flown.east <= 180, `east must be normalized, got ${flown.east}`);
});
test('an antimeridian REGION frames its own span, not four times it', async () => {
const viewer = stubViewer();
await runSearch(viewer, {}, { result: ANTIMERIDIAN_REGION_RESULT, query: 'Antimeridian Territory' });
const flown = flownRectangleDegrees(viewer);
// 60 deg short-way span + 12% padding on each side = 74.4 deg. Raw subtraction
// produced ~132 deg instead.
assert.ok(flown.crossesAntimeridian, 'the territory box crosses the antimeridian');
assert.ok(
Math.abs(flown.widthDeg - 74.4) < 0.5,
`expected the padded 60° span (74.4°), got ${flown.widthDeg}°`,
);
assert.ok(flown.west >= -180 && flown.west <= 180, `west must be normalized, got ${flown.west}`);
assert.ok(flown.east >= -180 && flown.east <= 180, `east must be normalized, got ${flown.east}`);
});
test('ordinary boxes are framed exactly as before the antimeridian fix', async () => {
const viewer = stubViewer();
await runSearch(viewer, {}, { result: CENTRED_REGION_RESULT, query: 'Centred Province' });
const flown = flownRectangleDegrees(viewer);
// 10 deg span + 12% padding each side = 12.4 deg, and no crossing.
assert.equal(flown.crossesAntimeridian, false);
assert.ok(Math.abs(flown.widthDeg - 12.4) < 0.01, `expected 12.4°, got ${flown.widthDeg}°`);
});
test('regionFramingPlan: invalid viewports return null', () => {
assert.equal(regionFramingPlan(null), null);
assert.equal(regionFramingPlan({}), null);
assert.equal(regionFramingPlan({ southwest: { lat: NaN, lng: 0 }, northeast: { lat: 1, lng: 1 } }), null);
});
// Globe-view preset (owner field test 2026-07-23): "zoom out to a globe view" needs an
// ABSOLUTE full-earth framing — the relative zoom tool can never reach it. The preset
// must sit inside the app's own 'global' view-scale band (>12,000 km camera height,
// classifyViewScale in gevActions.js) and under the fly_to rangeM ceiling (20,000 km).
test('GLOBE_VIEW preset height sits in the global view band', () => {
assert.ok(GLOBE_VIEW.heightM > 12000000, `must classify as global band, got ${GLOBE_VIEW.heightM}`);
assert.ok(GLOBE_VIEW.heightM <= 20000000, `must stay under the 20,000 km ceiling, got ${GLOBE_VIEW.heightM}`);
// Straight-down framing so the planet reads as a globe, not a horizon shot.
assert.equal(GLOBE_VIEW.pitchDeg, -90);
});
// The globe flight's callbacks are what resolves Reset Globe / zoom_to_globe.
// They were published under this module's OWN option names (`onComplete` /
// `onCancel`), which Cesium's Camera.flyTo ignores — so neither ever fired and
// every caller resolved off its ~4.2 s watchdog timeout instead of the flight.
test('the globe flight publishes its callbacks under Cesium\'s own option names', () => {
const viewer = stubViewer();
const fired = [];
flyToGlobeView(viewer, {
onComplete: () => fired.push('complete'),
onCancel: () => fired.push('cancel'),
});
const flight = viewer.flights[0];
assert.equal(typeof flight.complete, 'function', 'Cesium resolves arrival through `complete`');
assert.equal(typeof flight.cancel, 'function', 'Cesium reports supersession through `cancel`');
// Not merely present under both spellings: the ignored names must be gone,
// or a later reader can "fix" the wrong one back.
assert.equal('onComplete' in flight, false, 'Cesium ignores onComplete — do not publish it');
assert.equal('onCancel' in flight, false, 'Cesium ignores onCancel — do not publish it');
flight.complete();
flight.cancel();
assert.deepEqual(fired, ['complete', 'cancel'], 'each hook reaches the caller it belongs to');
});
test('a globe flight without callbacks still flies (both hooks are optional)', () => {
const viewer = stubViewer();
const target = flyToGlobeView(viewer);
assert.equal(viewer.flights.length, 1);
assert.equal(target.heightM, GLOBE_VIEW.heightM);
assert.equal(viewer.flights[0].complete, undefined);
assert.equal(viewer.flights[0].cancel, undefined);
});
test('geocoded Location branches forward the resolved-navigation ownership hook', () => {
const source = fs.readFileSync(new URL('./locations.js', import.meta.url), 'utf8');
const searchStart = source.indexOf('export async function searchAndFlyTo');
const searchEnd = source.indexOf('function placesViewportToBounds', searchStart);
const search = source.slice(searchStart, searchEnd);
assert.equal(
(search.match(/onStart: options\.onStart/g) || []).length,
3,
'swath, viewport, and landmark flights must each forward onStart',
);
assert.equal(
(search.match(/onCancel: options\.onCancel/g) || []).length,
3,
'swath, viewport, and landmark flights must each forward onCancel',
);
assert.ok(search.indexOf('return null;') < search.indexOf('onStart: options.onStart'));
});
test('globe and city-overview flights name the world frame explicitly', () => {
const globeViewer = stubViewer();
flyToGlobeView(globeViewer);
assert.equal(globeViewer.flights[0].endTransform, Cesium.Matrix4.IDENTITY);
const cityViewer = stubViewer();
flyToPresetLocation(cityViewer, 'austin', { viewMode: 'overview' });
assert.equal(cityViewer.flights[0].endTransform, Cesium.Matrix4.IDENTITY);
});
test('city and landmark flights expose completion and cancellation hooks', () => {
const overviewViewer = stubViewer();
const overviewEvents = [];
flyToPresetLocation(overviewViewer, 'austin', {
viewMode: 'overview',
onComplete: () => overviewEvents.push('complete'),
onCancel: () => overviewEvents.push('cancel'),
});
overviewViewer.flights[0].complete();
overviewViewer.flights[0].cancel();
assert.deepEqual(overviewEvents, ['complete', 'cancel']);
const landmarkViewer = stubViewer();
const landmarkEvents = [];
flyToPresetLocation(landmarkViewer, 'austin', {
onComplete: () => landmarkEvents.push('complete'),
onCancel: () => landmarkEvents.push('cancel'),
});
landmarkViewer.flights[0].complete();
landmarkViewer.flights[0].cancel();
assert.deepEqual(landmarkEvents, ['complete', 'cancel']);
});
test('beforeFly runs once after resolution and immediately before the flight', async () => {
const viewer = stubViewer();
const order = [];
const result = await runSearch(viewer, {
beforeFly: () => {
order.push(`before:${viewer.flights.length}`);
return true;
},
});
assert.equal(result.navigationMode, 'city-overview');
assert.deepEqual(order, ['before:0']);
assert.equal(viewer.flights.length, 1);
});
test('a final authority veto returns cancellation without issuing a flight', async () => {
const viewer = stubViewer();
const result = await runSearch(viewer, { beforeFly: () => false });
assert.equal(result, CANCELLED_SEARCH);
assert.equal(viewer.flights.length, 0);
});
test('search without an authority hook preserves the existing caller contract', async () => {
const viewer = stubViewer();
const result = await runSearch(viewer, {});
assert.equal(result.navigationMode, 'city-overview');
assert.equal(viewer.flights.length, 1);
});
test('near-view recovery searches through the application place search', async () => {
// Recovery must use the place search the application configured, not the module default.
const viewer = stubViewer();
const placeSearch = createStandalonePlaceSearch({ resolveApiKey: () => 'test-key' });
let received = null;
await runSearch(viewer, {
placeSearch,
recoverNearView: async (...args) => {
received = args[4];
return null;
},
});
assert.equal(received, placeSearch);
});
test('a precise search without an outline frames against the resolved ground, not sea level', async () => {
// Camp Mabry field report: with no detailed outline (no configured Overpass)
// framing used the 250 m landmark range from a sea-level target, and the eye
// landed about 1 m above the 171 m mesh. The ground service now anchors it.
const viewer = stubViewer();
const resolved = [];
const warmed = [];
const ground = {
async resolveGroundFloorCellsBounded(points) {
resolved.push(...points);
},
cachedGroundFloor: () => 171,
warmGroundFloor: (points) => warmed.push(...points),
};
const result = await runSearch(
viewer,
{
ground,
features: {
getFocusFootprints: async () => ({
unavailable: true,
retryable: false,
code: 'OVERPASS_NOT_CONFIGURED',
}),
},
recoverNearView: async () => null,
},
{
query: 'Camp Mabry',
result: {
formatted_address: 'Camp Mabry, Austin, TX',
types: ['point_of_interest', 'establishment'],
geometry: { location: { lat: 30.3125, lng: -97.765 } },
},
},
);
assert.equal(result.navigationMode, 'precise-place');
assert.equal(result.outlineUnavailable, true);
assert.equal(resolved.length, 1);
assert.equal(warmed.length, 1);
const flight = viewer.flights[0];
const target = Cesium.Cartographic.fromCartesian(flight.sphere.center);
assert.ok(Math.abs(target.height - (171 + 30)) < 0.5, `target ${target.height}`);
const eye = target.height + flight.offset.range * Math.sin(-flight.offset.pitch);
assert.ok(eye > 171 + 100, `eye ${eye} must clear the 171 m surface`);
});