Files
gods-eye-view/src/voice/areaActions.test.mjs
T
Sameh Khamis 68667e2e3c fix(area): accept "State of Texas" and "USA" as the units they name
resolve_area compared the spoken name with the unit's name literally, so
"State of Texas" (admin1) and "USA" or "UK" (country) were refused with
AREA_IDENTITY_MISMATCH although the bundled matcher found exactly those
units. Both identity checks, the bundled one and the outline ladder's, now
also accept the prefix forms and same-unit country aliases the matcher
reads, at the level they imply, so the level check still applies.
"Great Britain", "Britain", "America" and "Korea" still need their own
names, and the natural-region check stays literal.
2026-11-03 09:12:28 -08:00

878 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// resolve_area and the analyst area scopes as the voice runner sees them:
// what reaches the annotation engine, what reaches the model, and which
// outline a "drawn" or "outlined" scope counts over.
//
// Run with: npm test
import test from 'node:test';
import assert from 'node:assert/strict';
import { createVoiceAreas, spokenArea } from './areaActions.js';
import { pointInPreparedArea } from '../data/areaGeometry.js';
import { presentResult } from './resultDisplay.js';
import { findAdminCandidates as findBundledAdminCandidates } from '../data/adminBoundaries.js';
const square = (w, s, e, n) => [
[w, s],
[e, s],
[e, n],
[w, n],
[w, s],
];
const BAGMATI_POLYGONS = [
[square(84.4, 26.9, 86.6, 28.4), square(85.3, 27.6, 85.4, 27.7)],
];
/**
* The annotation outline ladder as the area actions see it: an anchor, then
* a deferred outline (here Bagmati from the guarded Nominatim route).
*/
function fakeLadder({
outline,
calls = [],
delay = null,
label = 'Bagmati Province',
match = /bagmati/i,
} = {}) {
return {
calls,
async resolveAnnotationTarget(request) {
calls.push(request);
if (!match.test(request.target)) return null;
return {
lat: 27.7,
lon: 85.3,
label,
ring: null,
resolveOutline: async () => {
if (delay) await delay;
if (outline !== undefined) return outline;
return {
ring: BAGMATI_POLYGONS[0][0],
polygons: BAGMATI_POLYGONS,
adminArea: 'Bagmati Province',
adminLevel: 'admin1',
synthesized: false,
outlineSource: 'nominatim',
};
},
};
},
};
}
const PUNJABS = [
{
kind: 'state',
name: 'Punjab',
country: 'India',
source: 'natural-earth',
polygons: [[square(73.8, 29.5, 77, 32.5)]],
},
{
kind: 'state',
name: 'Punjab',
country: 'Pakistan',
source: 'natural-earth',
polygons: [[square(69.3, 27.7, 73.7, 34)]],
},
];
function fakeAnnotations() {
const marks = [];
let seq = 0;
return {
marks,
calls: [],
async annotate(specs) {
this.calls.push(specs);
const results = specs.map((spec) => {
const mark = {
id: `anno-${(seq += 1)}`,
ring: spec.ring,
label: spec.label,
areaId: spec.areaId || null,
origin: spec.manual
? spec.origin === 'area'
? 'area'
: 'drawn'
: 'voice',
synthesized: spec.approximate === true,
createdAt: seq,
};
marks.push(mark);
return { ok: true, id: mark.id };
});
return { drawn: results.length, results };
},
list() {
return marks;
},
};
}
function makeAreas(overrides = {}) {
const annotations = overrides.annotations || fakeAnnotations();
const areas = createVoiceAreas({
viewer: {},
annotations,
annotationResolver: fakeLadder(),
findRegion: async () => null,
findAdminCandidates: async (q) => (/^punjab$/i.test(q) ? PUNJABS : []),
...overrides,
});
return { areas, annotations };
}
test('resolve_area outlines every part and hands the model a handle, not coordinates', async () => {
const { areas, annotations } = makeAreas();
const result = await areas.resolveAreaAction({
query: 'Bagmati province',
draw: true,
});
assert.equal(result.ok, true);
assert.equal(result.level, 'admin1');
assert.equal(result.source, 'osm');
assert.equal(result.sourceIdentity, 'Bagmati Province');
assert.equal(areas.store.get(result.areaId).meta.adminLevel, 'admin1');
assert.equal(result.display.source, 'OpenStreetMap');
assert.equal(result.holes, 1);
assert.equal(result.drawn, true);
assert.match(result.say, /^Bagmati Province — about [\d,]+ km²$/);
assert.equal(JSON.stringify(result).includes('coordinates'), false);
const [spec] = annotations.calls[0];
assert.equal(spec.manual, true);
assert.equal(spec.origin, 'area');
assert.equal(spec.areaId, result.areaId);
assert.equal(spec.polygons[0].length, 2, 'the hole is drawn too');
});
test('draw failures stay honest in the result, spoken line, and card', async (t) => {
const cases = [
{
name: 'annotation cap',
annotations: {
annotate: async () => ({
ok: false,
capped: true,
results: [{ ok: false, error: 'annotation limit reached' }],
}),
list: () => [],
},
reason: /annotation limit reached/,
},
{
name: 'renderer failure',
annotations: {
annotate: async () => ({
ok: false,
results: [{ ok: false, error: 'renderer unavailable' }],
}),
list: () => [],
},
reason: /renderer unavailable/,
},
{
name: 'missing annotation engine',
annotations: null,
reason: /unavailable/,
},
];
for (const entry of cases) {
await t.test(entry.name, async () => {
const areas = createVoiceAreas({
viewer: {},
annotations: entry.annotations,
annotationResolver: fakeLadder(),
findRegion: async () => null,
findAdminCandidates: async () => [],
});
const result = await areas.resolveAreaAction({
query: 'Bagmati province',
draw: true,
});
assert.equal(result.ok, true, 'the area itself resolved');
assert.equal(result.partial, true);
assert.equal(result.drawn, false);
assert.match(result.notDrawn, entry.reason);
assert.match(result.say, /resolved the area, but couldn't draw/i);
assert.match(result.say, entry.reason);
const card = presentResult('resolve_area', result, {
query: 'Bagmati province',
});
assert.ok(card.display.chips.some((chip) => chip.label === 'partial'));
assert.ok(card.display.lines.some((line) => /Not drawn:/i.test(line)));
});
}
});
test('a newer turn cancels area drawing before any annotation or flight commits', async () => {
let release;
let entered;
const enteredDraw = new Promise((resolve) => {
entered = resolve;
});
let committed = false;
const annotations = {
generation: () => 0,
async annotate(_specs, options) {
entered();
await new Promise((resolve) => {
release = resolve;
});
if (options.signal?.aborted || !options.isCurrent?.())
return { aborted: true, drawn: 0, results: [] };
committed = true;
return { drawn: 1, results: [{ ok: true, id: 'late-area' }] };
},
list: () => [],
};
const { areas } = makeAreas({ annotations });
const controller = new AbortController();
const pending = areas.resolveAreaAction(
{ query: 'Bagmati province', draw: true },
{
signal: controller.signal,
isCurrent: () => !controller.signal.aborted,
},
);
await enteredDraw;
controller.abort();
release();
const result = await pending;
assert.equal(result.code, 'CANCELLED');
assert.equal(result.cancelled, true);
assert.equal(committed, false);
});
test('an outline counts over the stored geometry, not the drawn copy', async () => {
const { areas } = makeAreas();
const resolved = await areas.resolveAreaAction({
query: 'Bagmati',
draw: true,
});
const scope = await areas.resolveAreaScope({ kind: 'annotation' });
assert.equal(scope.ok, true);
assert.equal(
scope.area.areaId,
resolved.areaId,
'the outline resolves back to its area',
);
assert.equal(
pointInPreparedArea(scope.area.prepared, 27.65, 85.35),
false,
'inside the hole',
);
assert.equal(pointInPreparedArea(scope.area.prepared, 27.0, 85.0), true);
});
test('"the area I drew" is the newest hand-drawn outline, never a voice outline', async () => {
const { areas, annotations } = makeAreas();
const none = await areas.resolveAreaScope({ kind: 'drawn' });
assert.equal(none.ok, false);
assert.equal(none.code, 'NO_DRAWN_AREA');
await annotations.annotate([
{ type: 'area', manual: true, ring: square(0, 0, 1, 1), label: null },
]);
await areas.resolveAreaAction({ query: 'Bagmati', draw: true });
const drawn = await areas.resolveAreaScope({ kind: 'drawn' });
assert.equal(drawn.ok, true);
assert.equal(drawn.area.name, 'the drawn area');
assert.equal(pointInPreparedArea(drawn.area.prepared, 0.5, 0.5), true);
// By id, and an unknown id refuses.
const byId = await areas.resolveAreaScope({
kind: 'annotation',
id: 'anno-1',
});
assert.equal(byId.area.name, 'the drawn area');
const unknown = await areas.resolveAreaScope({
kind: 'annotation',
id: 'anno-99',
});
assert.equal(unknown.ok, false);
const expired = await areas.resolveAreaScope({
kind: 'area',
areaId: 'area-99',
});
assert.equal(expired.code, 'AREA_UNKNOWN');
});
test('an ambiguous name returns candidates for the model to ask about', async () => {
const { areas } = makeAreas();
const result = await areas.resolveAreaAction({ query: 'Punjab', draw: true });
assert.equal(result.ok, false);
assert.equal(result.needsClarification, true);
assert.deepEqual(
result.candidates.map((c) => `${c.name}/${c.country}`),
['Punjab/India', 'Punjab/Pakistan'],
);
const picked = await areas.resolveAreaAction({
candidateId: result.candidates[1].candidateId,
});
assert.equal(picked.ok, true);
assert.equal(picked.country, 'Pakistan');
assert.equal(picked.display.source, 'Natural Earth');
});
test('names reach the annotation outline ladder with the level as entity kind', async () => {
const ladder = fakeLadder();
const { areas } = makeAreas({ annotationResolver: ladder });
await areas.resolveAreaAction({ query: 'Bagmati province' });
const [request] = ladder.calls;
assert.equal(request.target, 'Bagmati province');
assert.equal(request.entityKind, 'state');
assert.equal(request.footprint, true);
assert.equal(request.allowDistant, true);
});
test('a verified province accepts a bare query with a separate level', async () => {
const { areas } = makeAreas({ annotationResolver: fakeLadder() });
const result = await areas.resolveAreaAction({
query: 'Bagmati',
level: 'admin1',
});
assert.equal(result.ok, true);
assert.equal(result.name, 'Bagmati Province');
assert.equal(result.level, 'admin1');
assert.equal(result.sourceIdentity, 'Bagmati Province');
const unverified = makeAreas({
annotationResolver: fakeLadder({
outline: {
ring: BAGMATI_POLYGONS[0][0],
polygons: BAGMATI_POLYGONS,
adminArea: 'Bagmati',
synthesized: false,
outlineSource: 'nominatim',
},
}),
});
const refused = await unverified.areas.resolveAreaAction({
query: 'Bagmati',
level: 'admin1',
});
assert.equal(refused.ok, false);
assert.equal(refused.code, 'AREA_IDENTITY_UNVERIFIED');
assert.equal(unverified.areas.store.size, 0);
const implicit = makeAreas({
annotationResolver: fakeLadder({
outline: {
ring: BAGMATI_POLYGONS[0][0],
polygons: BAGMATI_POLYGONS,
adminArea: 'Bagmati',
synthesized: false,
outlineSource: 'nominatim',
},
}),
});
const implicitRefusal = await implicit.areas.resolveAreaAction({
query: 'Bagmati',
});
assert.equal(implicitRefusal.ok, false);
assert.equal(implicitRefusal.code, 'AREA_IDENTITY_UNVERIFIED');
assert.equal(implicit.areas.store.size, 0);
const retired = makeAreas({
annotationResolver: fakeLadder({
label: 'Bagmati',
outline: {
ring: BAGMATI_POLYGONS[0][0],
polygons: BAGMATI_POLYGONS,
adminArea: 'Bagmati Zone',
synthesized: false,
outlineSource: 'nominatim',
},
}),
});
const retiredRefusal = await retired.areas.resolveAreaAction({
query: 'Bagmati',
});
assert.equal(retiredRefusal.ok, false);
assert.equal(retiredRefusal.code, 'AREA_IDENTITY_MISMATCH');
assert.equal(retired.areas.store.size, 0);
});
test('an explicit level accepts the names the administrative matcher reads as that unit', async () => {
const bundled = () =>
makeAreas({ findAdminCandidates: findBundledAdminCandidates }).areas;
for (const [query, level, name] of [
['State of Texas', 'admin1', 'Texas'],
['State of Texas', undefined, 'Texas'],
['USA', 'country', 'United States of America'],
['UK', 'country', 'United Kingdom'],
]) {
const result = await bundled().resolveAreaAction({ query, level });
assert.equal(result.ok, true, `${query} (${level}): ${result.error}`);
assert.equal(result.name, name);
assert.equal(result.level, level || 'admin1');
}
// The level still has to match the unit.
for (const [query, level] of [
['State of Texas', 'country'],
['USA', 'admin1'],
]) {
const areas = bundled();
const result = await areas.resolveAreaAction({ query, level });
assert.equal(result.ok, false, `${query} is not a ${level}`);
assert.equal(areas.store.size, 0);
}
// The outline ladder's identity check reads the same forms.
const prefixed = makeAreas().areas;
const bagmati = await prefixed.resolveAreaAction({
query: 'Province of Bagmati',
level: 'admin1',
});
assert.equal(bagmati.ok, true, bagmati.error);
assert.equal(bagmati.name, 'Bagmati Province');
const wrongLevel = makeAreas().areas;
const county = await wrongLevel.resolveAreaAction({
query: 'Province of Bagmati',
level: 'admin2',
});
assert.equal(county.ok, false);
assert.equal(county.code, 'AREA_IDENTITY_MISMATCH');
assert.equal(wrongLevel.store.size, 0);
});
test('a current Zone the source verified is not refused as retired', async () => {
const gamo = makeAreas({
annotationResolver: fakeLadder({
label: 'Gamo Zone',
match: /gamo/i,
outline: {
ring: BAGMATI_POLYGONS[0][0],
polygons: BAGMATI_POLYGONS,
adminArea: 'Gamo Zone',
adminLevel: 'admin2',
synthesized: false,
outlineSource: 'nominatim',
},
}),
});
const result = await gamo.areas.resolveAreaAction({ query: 'Gamo Zone' });
assert.equal(result.ok, true, result.error);
assert.equal(gamo.areas.store.size, 1);
});
test('an outline named with accents is accepted for the same name spoken without them', async () => {
const quebec = makeAreas({
annotationResolver: fakeLadder({
label: 'Québec',
match: /quebec/i,
outline: {
ring: BAGMATI_POLYGONS[0][0],
polygons: BAGMATI_POLYGONS,
adminArea: 'Québec',
adminLevel: 'admin1',
synthesized: false,
outlineSource: 'nominatim',
},
}),
});
const result = await quebec.areas.resolveAreaAction({
query: 'Quebec province',
});
assert.equal(result.ok, true, result.error);
});
test('a place no outline source can trace is refused, not guessed', async () => {
const miss = makeAreas({
annotationResolver: fakeLadder({ outline: null }),
});
const none = await miss.areas.resolveAreaAction({ query: 'Bagmati' });
assert.equal(none.code, 'AREA_NOT_FOUND');
const off = makeAreas({
annotationResolver: fakeLadder({
outline: { unavailable: true, retryable: false },
}),
});
const refused = await off.areas.resolveAreaAction({ query: 'Bagmati' });
assert.equal(refused.code, 'AREA_UNAVAILABLE');
assert.match(refused.error, /stays a point/);
const unknown = await off.areas.resolveAreaAction({ query: 'Atlantis' });
assert.equal(unknown.code, 'AREA_NOT_FOUND');
});
test('an old or unverified administrative outline is refused and never stored', async () => {
for (const [outline, code] of [
[
{
ring: BAGMATI_POLYGONS[0][0],
polygons: BAGMATI_POLYGONS,
adminArea: 'Bagmati Zone',
outlineSource: 'nominatim',
},
'AREA_IDENTITY_MISMATCH',
],
[
{
ring: BAGMATI_POLYGONS[0][0],
polygons: BAGMATI_POLYGONS,
adminArea: 'Bagmati Province',
adminLevel: 'admin2',
outlineSource: 'nominatim',
},
'AREA_IDENTITY_MISMATCH',
],
[
{
ring: BAGMATI_POLYGONS[0][0],
polygons: BAGMATI_POLYGONS,
adminArea: 'Bagmati Province',
outlineSource: 'nominatim',
},
'AREA_IDENTITY_UNVERIFIED',
],
[
{
ring: BAGMATI_POLYGONS[0][0],
polygons: BAGMATI_POLYGONS,
outlineSource: 'nominatim',
},
'AREA_IDENTITY_UNVERIFIED',
],
[
{
ring: BAGMATI_POLYGONS[0][0],
polygons: BAGMATI_POLYGONS,
synthesized: true,
outlineSource: 'nominatim',
},
'AREA_IDENTITY_UNVERIFIED',
],
]) {
const { areas } = makeAreas({
annotationResolver: fakeLadder({ outline }),
});
const result = await areas.resolveAreaAction({
query: 'Bagmati province',
draw: true,
});
assert.equal(result.ok, false);
assert.equal(result.code, code);
assert.equal(areas.store.size, 0, 'a refused outline is not reusable');
const repeated = await areas.resolveAreaAction({
query: 'Bagmati province',
});
assert.equal(repeated.code, code);
assert.equal(repeated.areaId, undefined);
assert.equal(areas.store.size, 0);
}
});
test('a region scope reuses the ladder and asks on ambiguity', async () => {
const { areas } = makeAreas();
const region = await areas.resolveRegion('Bagmati');
assert.equal(region.name, 'Bagmati Province');
assert.equal(region.sourceLabel, 'OpenStreetMap');
assert.ok(region.prepared);
const asked = await areas.resolveRegion('Punjab');
assert.equal(asked.needsClarification, true);
assert.equal(await areas.resolveRegion('Atlantis'), null);
});
test('an area around a landmark uses its enclosing outline, else a labelled buffer', async () => {
const around = (outline, extra = {}) => ({
resolveAnnotationTarget: async (request) => {
assert.equal(request.entityKind, 'compound', 'grounds, not the building');
return {
lat: 37.7955,
lon: -122.3937,
label: 'Ferry Building',
ring: null,
...(outline === undefined
? {}
: { resolveOutline: async () => outline }),
...extra,
};
},
});
const grounds = makeAreas({
annotationResolver: around({
ring: square(-122.395, 37.794, -122.392, 37.797),
polygons: [[square(-122.395, 37.794, -122.392, 37.797)]],
synthesized: false,
outlineSource: 'openfreemap',
}),
});
const real = await grounds.areas.resolveAreaAction({
query: 'Ferry Building, San Francisco',
around: true,
});
assert.equal(real.ok, true);
assert.equal(real.source, 'openfreemap');
assert.equal(real.approximate, undefined);
assert.equal(real.drawn, true, 'an around-area is always drawn');
assert.equal(real.display.basis, 'enclosing mapped area');
assert.equal(real.name, 'Ferry Building');
// No enclosing outline (Nominatim off, nothing in the tiles): a buffer.
const bare = makeAreas({
annotationResolver: around({ unavailable: true, retryable: false }),
});
const approx = await bare.areas.resolveAreaAction({
query: 'Ferry Building',
around: true,
});
assert.equal(approx.ok, true);
assert.equal(approx.approximate, true);
assert.equal(approx.source, 'approximate');
assert.match(approx.say, /^Rough area around Ferry Building/);
assert.equal(approx.display.basis, '250 m around the landmark');
assert.equal(bare.annotations.calls[0][0].approximate, true, 'drawn dashed');
assert.ok(approx.areaKm2 > 0.15 && approx.areaKm2 < 0.25);
// A buffer the ladder sized itself is still a buffer.
const sized = makeAreas({
annotationResolver: around({
ring: square(-122.395, 37.794, -122.392, 37.797),
synthesized: true,
}),
});
const disc = await sized.areas.resolveAreaAction({
query: 'Ferry Building',
around: true,
});
assert.equal(disc.approximate, true);
// A slow outline gives way to the buffer instead of holding the answer.
const slow = makeAreas({
aroundOutlineMs: 20,
annotationResolver: {
resolveAnnotationTarget: async () => ({
lat: 37.7955,
lon: -122.3937,
ring: null,
resolveOutline: () => new Promise(() => {}),
}),
},
});
const started = Date.now();
const buffered = await slow.areas.resolveAreaAction({
query: 'Ferry Building',
around: true,
});
assert.ok(Date.now() - started < 1000);
assert.equal(buffered.approximate, true);
assert.equal(buffered.display.basis, '250 m around the landmark');
// A radius the user gave never needs the outline, so it is never looked up.
let outlineLookups = 0;
const radius = makeAreas({
annotationResolver: {
resolveAnnotationTarget: async () => ({
lat: 37.7955,
lon: -122.3937,
ring: null,
resolveOutline: async () => {
outlineLookups += 1;
return null;
},
}),
},
});
const sizedBuffer = await radius.areas.resolveAreaAction({
query: 'Ferry Building',
around: true,
radiusM: 400,
});
assert.equal(sizedBuffer.display.basis, '400 m around the landmark');
assert.equal(outlineLookups, 0, 'no outline request for a sized buffer');
const missing = makeAreas({
annotationResolver: { resolveAnnotationTarget: async () => null },
});
const lost = await missing.areas.resolveAreaAction({
query: 'Nowhere Hall',
around: true,
});
assert.equal(lost.code, 'AREA_NOT_FOUND');
});
test('a superseded resolve returns cancelled and draws nothing', async () => {
const { areas, annotations } = makeAreas();
let current = true;
const pending = areas.resolveAreaAction(
{ query: 'Bagmati', draw: true },
{ isCurrent: () => current },
);
current = false;
const result = await pending;
assert.equal(result.code, 'CANCELLED');
assert.equal(annotations.calls.length, 0);
});
test('spoken areas round to two significant figures', () => {
assert.equal(spokenArea(20_312.4), 'about 20,000 km²');
assert.equal(spokenArea(0.196), 'about 0.2 km²');
assert.equal(spokenArea(1234), 'about 1,200 km²');
assert.equal(spokenArea(0), null);
});
// ── Review regressions: board ownership, eviction, runner replacement ──
/** Annotations fake with the engine's board generation and clear(). */
function boardAnnotations() {
const fake = fakeAnnotations();
let generation = 0;
fake.generation = () => generation;
fake.clear = () => {
generation += 1;
fake.marks.length = 0;
};
return fake;
}
test('a clear during a slow lookup is not undone by a late outline', async () => {
let release;
const annotations = boardAnnotations();
const gate = new Promise((resolve) => {
release = resolve;
});
const areas = createVoiceAreas({
viewer: {},
annotations,
findRegion: async () => null,
findAdminCandidates: async () => [],
annotationResolver: fakeLadder({ delay: gate }),
});
const pending = areas.resolveAreaAction({ query: 'Bagmati', draw: true });
await new Promise((resolve) => setTimeout(resolve, 5));
annotations.clear(); // the person clears the map while the lookup runs
release();
const result = await pending;
assert.equal(result.ok, true, 'the area itself still resolves');
assert.equal(result.drawn, false);
assert.match(result.notDrawn, /cleared/);
assert.equal(result.partial, true);
assert.match(result.say, /couldn't draw the outline:.*cleared/i);
assert.equal(annotations.calls.length, 0, 'nothing drawn, no camera flight');
// Asked again after the clear, it draws.
const again = await areas.resolveAreaAction({ query: 'Bagmati', draw: true });
assert.equal(again.drawn, true);
});
test('an outlined area keeps its full geometry past store eviction', async () => {
const { areas } = makeAreas();
const resolved = await areas.resolveAreaAction({
query: 'Bagmati',
draw: true,
});
// Far more areas than the store holds, none of them on the map.
for (let i = 0; i < 40; i += 1)
areas.store.put({
geometry: [[square(i, 0, i + 0.5, 0.5)]],
name: `filler ${i}`,
source: 'osm',
});
const scope = await areas.resolveAreaScope({ kind: 'annotation' });
assert.equal(scope.ok, true);
assert.equal(scope.area.areaId, resolved.areaId);
assert.equal(
pointInPreparedArea(scope.area.prepared, 27.65, 85.35),
false,
'the hole is still a hole',
);
assert.equal(areas.store.has(resolved.areaId), true);
});
test('an outline from before a page reload asks to resolve again, never counts its drawing', async () => {
const annotations = fakeAnnotations();
const first = createVoiceAreas({
viewer: {},
annotations,
annotationResolver: fakeLadder(),
findRegion: async () => null,
findAdminCandidates: async () => [],
});
await first.resolveAreaAction({ query: 'Bagmati', draw: true });
// A new runner means a reloaded page: the store is new while the persisted
// outline stays on the map. A mic restart keeps the runner (and handles);
// see integrationContract.test.mjs.
const second = createVoiceAreas({
viewer: {},
annotations,
findRegion: async () => null,
findAdminCandidates: async () => [],
});
const scope = await second.resolveAreaScope({ kind: 'annotation' });
assert.equal(scope.ok, false);
assert.equal(scope.code, 'AREA_EXPIRED');
});
test('a voice outline without an area handle counts over every part it drew', async () => {
const { areas, annotations } = makeAreas();
annotations.marks.push({
id: 'anno-9',
ring: square(0, 0, 1, 1),
polygons: [[square(0, 0, 1, 1)], [square(2, 2, 3, 3)]],
label: 'Presidio',
origin: 'voice',
createdAt: 99,
});
const scope = await areas.resolveAreaScope({ kind: 'annotation' });
assert.equal(scope.ok, true);
assert.equal(scope.area.approximate, false);
assert.equal(pointInPreparedArea(scope.area.prepared, 2.5, 2.5), true);
annotations.marks.push({
id: 'anno-10',
ring: square(5, 5, 6, 6),
label: 'Around the pier',
origin: 'voice',
synthesized: true,
createdAt: 100,
});
const buffer = await areas.resolveAreaScope({ kind: 'annotation' });
assert.equal(buffer.area.approximate, true, 'a buffer says approximate');
});
test('an area around a landmark keeps the enclosing feature’s parts and holes', async () => {
const { areas, annotations } = makeAreas({
annotationResolver: {
resolveAnnotationTarget: async () => ({
lat: 37.7955,
lon: -122.3937,
ring: null,
resolveOutline: async () => ({
ring: square(-122.395, 37.794, -122.392, 37.797),
polygons: [
[
square(-122.395, 37.794, -122.392, 37.797),
square(-122.394, 37.795, -122.393, 37.796),
],
[square(-122.39, 37.79, -122.389, 37.791)],
],
adminArea: 'Pier Complex',
synthesized: false,
outlineSource: 'nominatim',
}),
}),
},
});
const result = await areas.resolveAreaAction({
query: 'Pier Complex',
around: true,
});
assert.equal(result.source, 'osm');
assert.equal(result.parts, 2);
assert.equal(result.holes, 1);
assert.equal(result.approximate, undefined);
assert.equal(annotations.calls[0][0].polygons.length, 2, 'both parts drawn');
const scope = await areas.resolveAreaScope({
kind: 'area',
areaId: result.areaId,
});
assert.equal(
pointInPreparedArea(scope.area.prepared, 37.7955, -122.3935),
false,
'the courtyard hole',
);
assert.equal(
pointInPreparedArea(scope.area.prepared, 37.7905, -122.3895),
true,
'the second part',
);
});