mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
324 lines
10 KiB
JavaScript
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();
|
|
});
|