refactor: separate map feature queries and source records

This commit is contained in:
Sameh Khamis committed 2026-09-15 08:10:32 -07:00
1 parent a8125ad917
commit bf1d50f29b
28 files changed
+1128 -675

No files matched your search

+192
View File
@@ -0,0 +1,192 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
createOverpassFeatureSource,
FEATURE_SOURCE_METHODS,
} from './overpassFeatures.js';
import { normalizeOverpassFeatures } from './overpassFeaturesRecords.js';
import { createAnnotationResolver } from '../annotations/resolver.js';
const geometry = [
{ lat: 30, lon: -97 },
{ lat: 30, lon: -96.999 },
{ lat: 30.001, lon: -96.999 },
{ lat: 30.001, lon: -97 },
{ lat: 30, lon: -97 },
];
const raw = {
type: 'way',
id: 42,
tags: { name: 'Fixture Tower', building: 'yes', height: '100 ft' },
geometry,
};
const candidate = {
id: 42,
category: 'feature',
names: { primary: 'Fixture Tower' },
building: true,
heightM: 30.48,
coordinates: geometry,
};
const featureSource = (overrides) =>
Object.assign(
Object.fromEntries(
FEATURE_SOURCE_METHODS.map((method) => [method, async () => []]),
),
overrides,
);
test('Overpass adapter owns all feature query expressions and normalizes returned records', async () => {
const calls = [];
const source = createOverpassFeatureSource({
boundarySource: {
query: async (query, options) => {
calls.push({ query, options });
return [raw, { ...raw, type: 'relation' }];
},
},
});
for (const method of FEATURE_SOURCE_METHODS) {
const records = await source[method](
method === 'getAreaGeometry' ? 3600000042 : { lat: 30, lon: -97 },
);
assert.equal(records[0].names.primary, 'Fixture Tower');
assert.equal(records[0].heightM, 30.48);
assert.deepEqual(records[0].coordinates, geometry);
assert.equal('tags' in records[0], false);
assert.equal(records.length, method === 'getAreaGeometry' ? 1 : 2);
}
assert.equal(calls.length, 9);
assert.ok(
calls.every(
({ query, options }) =>
query.includes('[out:json]') && options.signal instanceof AbortSignal,
),
);
assert.match(calls[1].query, /rel\(pivot.x\)/);
assert.match(calls[4].query, /\["highway"\]/);
assert.match(calls[7].query, /memorial\|monument/);
assert.match(calls[8].query, /around:180,/);
assert.throws(
() => source.getFootprints({ lat: '30);out;', lon: -97 }),
/coordinates/,
);
assert.throws(() => source.getAreaGeometry('1);out;'), /reference/);
assert.equal(calls.length, 9);
});
test('provider preserves empty, transient and throttled responses and rejects late canceled data', async () => {
const lifetime = new AbortController();
let value = [];
const source = createOverpassFeatureSource({
signal: lifetime.signal,
boundarySource: { query: async () => value },
});
assert.deepEqual(await source.getFootprints({ lat: 30, lon: -97 }), []);
value = null;
assert.equal(await source.getFootprints({ lat: 30, lon: -97 }), null);
value = { rateLimited: true, retryAfterMs: 1500 };
assert.deepEqual(await source.getFootprints({ lat: 30, lon: -97 }), value);
let respond, signal;
const pendingSource = createOverpassFeatureSource({
signal: lifetime.signal,
boundarySource: {
query: async (_q, options) => {
signal = options.signal;
return new Promise((resolve) => {
respond = resolve;
});
},
},
});
const pending = pendingSource.getFootprints({ lat: 30, lon: -97 });
lifetime.abort();
respond([raw]);
assert.equal(signal.aborted, true);
assert.equal(await pending, null);
});
test('relation normalization keeps the largest closed outer and rejects a missing boundary', () => {
const relation = {
type: 'relation',
id: 1,
tags: { name: 'Region' },
members: [
{ role: 'outer', geometry: geometry.slice(0, 3) },
{ role: 'outer', geometry: geometry.slice(2) },
{
role: 'inner',
geometry: geometry.map((p) => ({ lat: p.lat + 1, lon: p.lon + 1 })),
},
],
};
assert.deepEqual(
normalizeOverpassFeatures([relation])[0].coordinates,
geometry,
);
relation.members = [{ role: 'outer', geometry: geometry.slice(0, 3) }];
assert.deepEqual(normalizeOverpassFeatures([relation])[0].coordinates, []);
assert.deepEqual(
normalizeOverpassFeatures([{ ...raw, geometry: undefined, members: {} }], {
focus: true,
})[0].coordinates,
[],
);
});
test('annotation selection accepts another feature provider with no query transport', async () => {
let calls = 0;
const resolver = createAnnotationResolver({
featureSource: featureSource({
getFootprints: async () => {
calls++;
return [candidate];
},
}),
});
const args = {
latitude: 30.0005,
longitude: -96.9995,
target: 'Fixture Tower',
entityKind: 'building',
footprint: true,
};
const first = await resolver.resolveAnnotationTarget(args);
const second = await resolver.resolveAnnotationTarget(args);
assert.equal(first.footprintKind, 'building');
assert.equal(first.buildingHeight, 30.48);
assert.deepEqual(
first.ring,
geometry.map((p) => [p.lon, p.lat]),
);
assert.deepEqual(second.ring, first.ring);
assert.equal(
calls,
1,
'ranking cache is independent of the provider implementation',
);
});
test('annotation retries transient feature failures but caches definitive misses', async () => {
let calls = 0;
const resolver = createAnnotationResolver({
featureSource: featureSource({
getFootprints: async () => {
calls++;
return calls === 1 ? null : [];
},
}),
});
const anchor = await resolver.resolveAnnotationTarget({
latitude: 30.0005,
longitude: -96.9995,
target: 'Missing Tower',
entityKind: 'building',
footprint: true,
deferFootprint: true,
});
assert.equal(await anchor.resolveOutline(), undefined);
assert.equal(await anchor.resolveOutline(), null);
assert.equal(await anchor.resolveOutline(), null);
assert.equal(calls, 2);
});