Files
gods-eye-view/src/annotations/adminOutline.test.mjs
T

324 lines
10 KiB
JavaScript

// Area annotations draw bundled state/province/county outlines with no
// network lookup, and multi-part outlines reach the renderer intact.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { FEATURE_SOURCE_METHODS } from '../sources/featureSource.js';
import { createAnnotationResolver } from './resolver.js';
import { createAnnotationEngine } from './annotationEngine.js';
import { _resetRenderGovernorForTest } from '../renderGovernor.js';
function viewerAt(lat, lon, height = 20_000) {
return {
camera: {
positionCartographic: {
latitude: (lat * Math.PI) / 180,
longitude: (lon * Math.PI) / 180,
height,
},
},
};
}
/** A resolver whose boundary lookups and geocoder record every call. */
function harness(place = null) {
const lookups = [];
const featureSource = {};
for (const method of FEATURE_SOURCE_METHODS)
featureSource[method] = async () => {
lookups.push(method);
return null;
};
const geocodes = [];
const placeSearch = {
async geocode(query) {
geocodes.push(query);
return { place };
},
async textSearch() {
geocodes.push('text-search');
return [];
},
};
const { resolveAnnotationTarget } = createAnnotationResolver({
featureSource,
});
const resolve = (target, viewer, extra = {}) =>
resolveAnnotationTarget({
placeSearch,
viewer,
target,
footprint: true,
deferFootprint: true,
...extra,
});
return { resolve, lookups, geocodes };
}
const AUSTIN = viewerAt(30.2672, -97.7431, 3000);
test('"Texas", "Travis County, Texas" and "Bavaria" outline offline', async () => {
const { resolve, lookups, geocodes } = harness();
for (const [target, label] of [
['Texas', 'Texas'],
['Travis County, Texas', 'Travis County'],
// Far from the camera, but named outright: no proximity gate applies.
['Bavaria', 'Bavaria'],
]) {
const resolved = await resolve(target, AUSTIN);
assert.ok(resolved, target);
assert.equal(resolved.source, 'bundled', target);
assert.equal(resolved.label, label);
assert.equal(resolved.footprintKind, 'area');
assert.equal(resolved.synthesized, false);
assert.equal(resolved.resolveOutline, undefined, 'drawn at once');
assert.ok(resolved.ring.length >= 9, `${target} ring`);
assert.deepEqual(resolved.ring[0], resolved.ring.at(-1), 'ring closed');
assert.ok(resolved.polygons.length >= 1, `${target} parts`);
assert.equal(resolved.polygons[0][0].length + 1, resolved.ring.length);
assert.ok(resolved.viewport?.low && resolved.viewport?.high);
}
assert.deepEqual(geocodes, [], 'no geocoding');
assert.deepEqual(lookups, [], 'no boundary lookups');
});
test('duplicate county names go to the one near the camera', async () => {
const { resolve } = harness();
const nearPortland = await resolve(
'Washington County',
viewerAt(45.52, -122.68),
);
assert.ok(nearPortland.lat > 45 && nearPortland.lon < -122, 'Oregon');
const nearAustin = await resolve('Washington County', AUSTIN);
assert.ok(nearAustin.lat > 29.9 && nearAustin.lat < 30.5, 'Texas');
assert.ok(nearAustin.lon > -96.8 && nearAustin.lon < -96, 'Texas');
});
test('multi-part outlines carry every part and hole', async () => {
const { resolve } = harness();
const hawaii = await resolve('Hawaii', AUSTIN);
assert.ok(hawaii.polygons.length >= 7, 'islands');
const brandenburg = await resolve('Brandenburg', AUSTIN);
assert.ok(
brandenburg.polygons.some((part) => part.length === 2),
'Berlin hole',
);
});
test('Georgia resolves from camera geography without a geocoder', async () => {
const { resolve, lookups, geocodes } = harness();
for (const [query, viewer, latitude] of [
['Georgia', viewerAt(33.75, -84.39, 400_000), 32],
['the country of Georgia', AUSTIN, 42],
['Georgia', viewerAt(41.7, 44.8), 42],
]) {
const resolved = await resolve(query, viewer);
assert.equal(resolved.source, 'bundled');
assert.ok(Math.abs(resolved.lat - latitude) < 2);
assert.ok(resolved.ring.length > 20);
}
assert.deepEqual(lookups, []);
assert.deepEqual(geocodes, []);
});
test('Gold Coast in Australia cannot acquire its West African namesake outline', async () => {
for (const types of [['locality', 'political'], ['natural_feature']]) {
const { resolve, geocodes } = harness({
lat: -28.0167,
lng: 153.4,
label: 'Gold Coast, Australia',
name: 'Gold Coast',
types,
});
const resolved = await resolve('Gold Coast', viewerAt(-28.0167, 153.4));
assert.equal(resolved?.source, 'geocode');
assert.equal(resolved.lat, -28.0167);
assert.equal(resolved.lon, 153.4);
assert.equal(resolved.naturalRegion, undefined);
const outline = await resolved.resolveOutline();
assert.equal(outline?.naturalRegion, undefined);
assert.deepEqual(geocodes, ['Gold Coast']);
}
});
test('natural annotation outlines require a geocoded anchor', async () => {
for (const viewer of [AUSTIN, {}, viewerAt(46.5, 10, 248_000)]) {
const miss = harness();
assert.equal(await miss.resolve('the Alps', viewer), null);
assert.ok(miss.geocodes.includes('the Alps'));
}
const anchored = harness({
lat: 46.5,
lng: 10,
label: 'Alps',
name: 'Alps',
types: ['natural_feature'],
});
const resolved = await anchored.resolve('the Alps', {});
assert.equal(resolved.source, 'geocode');
const outline = await resolved.resolveOutline();
assert.equal(outline.naturalRegion, 'Alps');
assert.ok(outline.ring.length >= 8);
});
test('overlapping natural candidates continue through geocoding without choosing a ring', async () => {
const { resolve, geocodes } = harness({
lat: 45.6336,
lng: -74.86493,
label: 'Canadian Shield',
name: 'Canadian Shield',
types: ['natural_feature'],
});
const resolved = await resolve(
'Canadian Shield',
viewerAt(45.6336, -74.86493),
);
assert.equal(resolved?.source, 'geocode');
assert.equal((await resolved.resolveOutline())?.naturalRegion, undefined);
assert.deepEqual(geocodes, ['Canadian Shield']);
});
test('geocoded natural outlines respect district and around intent', async () => {
for (const extra of [
{ entityKind: 'district' },
{ intent: 'around_the_thing' },
]) {
const { resolve, geocodes } = harness({
lat: 46.5,
lng: 10,
label: 'Alps',
name: 'Alps',
types:
extra.entityKind === 'district' ? ['locality'] : ['natural_feature'],
});
const resolved = await resolve('the Alps', viewerAt(46.5, 10), extra);
assert.ok(geocodes.includes('the Alps'));
assert.equal((await resolved.resolveOutline())?.naturalRegion, undefined);
}
});
test('cancellation while a bundled admin lookup is pending cannot publish an annotation', async () => {
const controller = new AbortController();
const { resolve, geocodes } = harness();
const pending = resolve('the Alps', viewerAt(46.5, 10), {
signal: controller.signal,
});
controller.abort();
await assert.rejects(pending, { name: 'AbortError' });
assert.deepEqual(geocodes, []);
});
test('country asks and geocoded country scopes use the bundled outline', async () => {
const direct = harness();
for (const name of ['Switzerland', 'Iran', 'France', 'Japan']) {
const resolved = await direct.resolve(name, AUSTIN);
assert.equal(resolved.source, 'bundled');
assert.ok(resolved.polygons.length);
if (name === 'Japan') assert.ok(resolved.polygons.length > 3);
}
assert.deepEqual(direct.lookups, []);
assert.deepEqual(direct.geocodes, []);
const geocoded = harness({
lat: 46.95,
lng: 7.45,
label: 'Switzerland',
name: 'Switzerland',
types: ['country', 'political'],
});
const resolved = await geocoded.resolve(
'Swiss country outline',
viewerAt(46.95, 7.45),
);
assert.equal(resolved.source, 'geocode');
const outline = await resolved.resolveOutline();
assert.ok(outline.polygons.length);
assert.deepEqual(geocoded.lookups, []);
});
test('non-admin asks and entity facts keep the existing path', async () => {
const { resolve, geocodes } = harness();
await resolve('Texas Capitol', AUSTIN);
await resolve('Texas', AUSTIN, { entityKind: 'building' });
await resolve('Zilker Park', AUSTIN);
assert.equal(
geocodes.filter((q) => q !== 'text-search').length,
3,
'each ask geocoded',
);
// Points and explicit coordinates never use the pack.
const point = await resolve('Texas', AUSTIN, { footprint: false });
assert.notEqual(point?.source, 'bundled');
});
test('the engine keeps every part on the mark, drawn at once or upgraded', async (t) => {
globalThis.requestAnimationFrame ??= () => 1;
globalThis.cancelAnimationFrame ??= () => {};
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const polygons = [
[
[
[0, 0],
[2, 0],
[2, 2],
[0, 2],
],
[
[0.5, 0.5],
[0.5, 1],
[1, 1],
[1, 0.5],
],
],
[
[
[3, 3],
[4, 3],
[4, 4],
],
],
];
const ring = [...polygons[0][0], polygons[0][0][0]];
const seen = [];
const renderer = {
add: (anno) => seen.push(['add', anno.polygons]),
update: (anno) => seen.push(['update', anno.polygons]),
remove() {},
sync() {},
destroy() {},
};
const engine = createAnnotationEngine({
viewer: {},
renderer,
resolveTarget: async ({ target }) =>
target === 'now'
? { lon: 1, lat: 1, height: 0, ring, polygons, footprintKind: 'area' }
: {
lon: 1,
lat: 1,
height: 0,
ring: null,
resolveOutline: async () => ({
ring,
polygons,
footprintKind: 'area',
lat: 1,
lon: 1,
height: 0,
}),
},
});
await engine.annotate([{ type: 'area', target: 'now' }]);
await engine.annotate([{ type: 'area', target: 'later', label: 'later' }]);
for (let i = 0; i < 20; i += 1) await Promise.resolve();
const marks = engine.list();
assert.equal(marks[0].polygons, polygons);
assert.ok(
marks.some((m) => m.label === 'later' && m.polygons === polygons),
'upgrade carries the parts',
);
assert.ok(
seen.some(([kind, parts]) => kind === 'update' && parts === polygons),
);
engine.destroy();
});