mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
keep bent streets, enclosed landmarks and same-name outlines honest
A street ribbon that holds neither its ring mean nor the anchor is centred on the street's nearest point when the street runs within 100 m. Nominatim outline selection ranks an exact name, then one holding every asked word, then an overlap, and refuses an overlap whose name lacks the ask's last word unless that word is in the result's address or type, so "Central Park Zoo" is no longer Central Park. The server caches every accepted outline and answers each ask with the one holding its resolved anchor (sent as alat/alon), read across the antimeridian. A duplicate mark that fails to clean up no longer tears down the new outline it duplicates.
This commit is contained in:
1 parent
d9a460658d
commit
a38a065c5c
16 files changed
+516
-133
No files matched your search
@@ -1128,8 +1128,13 @@ export function createAnnotationEngine({
|
||||
if ((other.footprintKind || null) !== (anno.footprintKind || null))
|
||||
continue;
|
||||
if (!outlinesEqual(other, anno)) continue;
|
||||
removeRendered(other);
|
||||
annotations.delete(other.id);
|
||||
try {
|
||||
removeRendered(other);
|
||||
annotations.delete(other.id);
|
||||
} catch {
|
||||
// The duplicate's failure is its own: leave it addressable for
|
||||
// later cleanup and keep this mark's installed outline.
|
||||
}
|
||||
}
|
||||
}
|
||||
renderer.sync(annotations);
|
||||
|
||||
@@ -120,3 +120,45 @@ test('the engine resolves a bare street highlight with its footprint', async (t)
|
||||
assert.equal(seen[0].footprint, true);
|
||||
assert.equal(seen[0].entityKind, 'street');
|
||||
});
|
||||
|
||||
test('a duplicate that fails to clean up never costs the new outline', async (t) => {
|
||||
installAnimationFrameStubs(t);
|
||||
const polygons = [[outer]];
|
||||
const outline = { ring: outer, polygons, footprintKind: 'area' };
|
||||
const point = { lon: 0.005, lat: 0.005, height: 0, source: 'fake' };
|
||||
// "Old" draws at once; "New" resolves the same polygon later.
|
||||
const resolveTarget = async ({ target }) =>
|
||||
target === 'old'
|
||||
? { ...point, label: target, ...outline }
|
||||
: {
|
||||
...point,
|
||||
label: target,
|
||||
resolveOutline: async () => ({ ...point, ...outline }),
|
||||
};
|
||||
const stuck = new Set();
|
||||
const outcomes = [];
|
||||
const engine = createAnnotationEngine({
|
||||
viewer: {},
|
||||
renderer: {
|
||||
...renderer,
|
||||
add(anno) {
|
||||
if (anno.label === 'Old') stuck.add(anno);
|
||||
},
|
||||
remove(anno) {
|
||||
if (stuck.has(anno)) throw new Error('renderer refused');
|
||||
},
|
||||
},
|
||||
resolveTarget,
|
||||
});
|
||||
engine.onOutlineEvent((event) => outcomes.push(event));
|
||||
await engine.annotate({ type: 'area', target: 'old', label: 'Old' });
|
||||
await engine.annotate({ type: 'area', target: 'new', label: 'New' });
|
||||
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
||||
const fresh = engine.list().find((mark) => mark.label === 'New');
|
||||
assert.equal(fresh?.footprintKind, 'area', 'the new mark keeps its outline');
|
||||
assert.notEqual(fresh.outlineUnavailable, true);
|
||||
assert.deepEqual(
|
||||
outcomes.filter((event) => event.label === 'New').map((e) => e.status),
|
||||
['resolved'],
|
||||
);
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
ringCentroid,
|
||||
} from '../sources/featureGeometry.js';
|
||||
import { isUnavailableCapability } from '../sources/capability.js';
|
||||
import { polygonsContain } from '../data/adminBoundaries.js';
|
||||
import { containsAnchor } from '../data/adminBoundaries.js';
|
||||
import { lonLatToTile } from '../data/tomtomTiles.js';
|
||||
import {
|
||||
BUILDING_MAX_DISTANCE_M,
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
} from '../sources/openFreeMapOutlines.js';
|
||||
import { createNominatimOutlineClient } from '../sources/nominatimOutlines.js';
|
||||
|
||||
export { containsAnchor };
|
||||
|
||||
/**
|
||||
* Outline rungs that need no Overpass, around the resolver's existing ladder:
|
||||
*
|
||||
@@ -82,46 +84,6 @@ export function nominatimKindFor({ scope, groundsLike, pointLike, around }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether polygons contain a point. A part whose outer ring crosses the
|
||||
* antimeridian ([179.99, …], [-179.99, …]) is read contiguously in its own
|
||||
* longitude frame, with the point moved into that frame; every other part,
|
||||
* including one genuinely wider than 180°, is tested as stored. Crossing is
|
||||
* read from an edge jumping more than 180°, which assumes densely noded rings
|
||||
* (OSM, Nominatim, tiles); a ring that is both wider than 180° and crossing, or
|
||||
* a non-crossing ring with a single edge over 180°, is not supported.
|
||||
*/
|
||||
export function containsAnchor(polygons, lat, lon) {
|
||||
if (!Number.isFinite(lon)) return false;
|
||||
for (const part of polygons || []) {
|
||||
const outer = part?.[0];
|
||||
if (!Array.isArray(outer) || !outer.length) continue;
|
||||
const reference = outer[0][0];
|
||||
const shift = (x) => x - 360 * Math.round((x - reference) / 360);
|
||||
const span = (ring, f) => {
|
||||
let west = Infinity;
|
||||
let east = -Infinity;
|
||||
for (const [x] of ring) {
|
||||
west = Math.min(west, f(x));
|
||||
east = Math.max(east, f(x));
|
||||
}
|
||||
return east - west;
|
||||
};
|
||||
// An edge jumping more than 180° wraps the line; a polar ring whose
|
||||
// edges run along ±180 collapses when reframed and is read as stored.
|
||||
const jumps = outer.some(
|
||||
([x], i) => i > 0 && Math.abs(x - outer[i - 1][0]) > 180,
|
||||
);
|
||||
const framedSpan = jumps ? span(outer, shift) : 0;
|
||||
const crosses = framedSpan > 0 && framedSpan < 180;
|
||||
const framed = crosses
|
||||
? part.map((ring) => ring.map(([x, y]) => [shift(x), y]))
|
||||
: part;
|
||||
if (polygonsContain([framed], lat, crosses ? shift(lon) : lon)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parts with the one holding the anchor outside its holes first. A boundary
|
||||
* can be largest at sea (Tokyo's island municipalities), so size alone does
|
||||
@@ -275,7 +237,14 @@ export function createOutlineRungs({
|
||||
if (!isNamedAsk(query)) return null;
|
||||
const bias = ctx.view || { lat: ctx.lat, lon: ctx.lon };
|
||||
const outline = await nominatim.lookup(
|
||||
{ query, kind, lat: bias.lat, lon: bias.lon },
|
||||
{
|
||||
query,
|
||||
kind,
|
||||
lat: bias.lat,
|
||||
lon: bias.lon,
|
||||
anchorLat: ctx.lat,
|
||||
anchorLon: ctx.lon,
|
||||
},
|
||||
{ signal: ctx.signal },
|
||||
);
|
||||
if (!outline?.polygons?.length) return outline; // null | undefined | unavailable | rateLimited
|
||||
|
||||
@@ -420,6 +420,8 @@ test('a city with no configured Overpass is outlined by Nominatim', async () =>
|
||||
kind: 'city',
|
||||
lat: CAPITOL.lat,
|
||||
lon: CAPITOL.lon,
|
||||
anchorLat: 43.6,
|
||||
anchorLon: 1.44,
|
||||
});
|
||||
assert.deepEqual(
|
||||
run.calls.finished,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { readFileSync } from 'node:fs';
|
||||
import { FEATURE_SOURCE_METHODS } from '../sources/featureSource.js';
|
||||
import { createAnnotationResolver } from './resolver.js';
|
||||
import { polygonsContain } from '../data/adminBoundaries.js';
|
||||
import { streetFootprint } from './outlineRungs.js';
|
||||
|
||||
function viewerAt(lat, lon, height = 20_000) {
|
||||
return {
|
||||
@@ -338,6 +339,54 @@ test('a courtyard outline with no filled candidate anchor remains a point', asyn
|
||||
}
|
||||
});
|
||||
|
||||
test('a bent street keeps its outline when the anchor sits beside the centreline', async () => {
|
||||
// 500 m east then 500 m north: the ring mean lands off the 22 m ribbon.
|
||||
const lat0 = 30.27;
|
||||
const lon0 = -97.74;
|
||||
const dLon = 500 / (111320 * Math.cos((lat0 * Math.PI) / 180));
|
||||
const dLat = 500 / 111320;
|
||||
const line = [
|
||||
[lon0, lat0],
|
||||
[lon0 + dLon / 2, lat0],
|
||||
[lon0 + dLon, lat0],
|
||||
[lon0 + dLon, lat0 + dLat / 2],
|
||||
[lon0 + dLon, lat0 + dLat],
|
||||
];
|
||||
const footprint = streetFootprint({ lines: [line] });
|
||||
// A geocoded point 15 m north of the first leg, outside the ribbon.
|
||||
const lat = lat0 + 15 / 111320;
|
||||
const lon = lon0 + dLon / 2;
|
||||
assert.equal(polygonsContain(footprint.polygons, lat, lon), false);
|
||||
const { resolve } = harness(
|
||||
{ lat, lng: lon, name: 'Bent Street', label: 'Bent St', types: ['route'] },
|
||||
{ outlineRungs: rungsAnswering(footprint) },
|
||||
);
|
||||
const pending = await resolve('Bent Street', viewerAt(lat, lon, 3000), {
|
||||
entityKind: 'street',
|
||||
});
|
||||
const outline = await pending.resolveOutline();
|
||||
assert.equal(outline?.ring, footprint.ring, 'the street is still drawn');
|
||||
assert.ok(polygonsContain(outline.polygons, outline.lat, outline.lon));
|
||||
assert.ok(Math.abs(outline.lat - lat0) < 1e-7, 'centred on the street');
|
||||
assert.ok(Math.abs(outline.lon - lon) < 1e-7);
|
||||
// A same-name street that never comes near the anchor is not that street.
|
||||
const far = lat0 + 300 / 111320;
|
||||
const away = harness(
|
||||
{
|
||||
lat: far,
|
||||
lng: lon,
|
||||
name: 'Bent Street',
|
||||
label: 'Bent St',
|
||||
types: ['route'],
|
||||
},
|
||||
{ outlineRungs: rungsAnswering(footprint) },
|
||||
);
|
||||
const distant = await away.resolve('Bent Street', viewerAt(far, lon, 3000), {
|
||||
entityKind: 'street',
|
||||
});
|
||||
assert.equal((await distant.resolveOutline())?.ring ?? null, null);
|
||||
});
|
||||
|
||||
test('local dateline outlines share a frame for area, centering and filled anchors', async () => {
|
||||
const ring = [
|
||||
[179.9995, -0.0005],
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
approximateAreaM2,
|
||||
approximateDistanceM,
|
||||
pointInPolygon,
|
||||
nearestPointOnLines,
|
||||
} from '../sources/featureGeometry.js';
|
||||
import { defaultGeospatial } from '../search/defaults.js';
|
||||
import * as Cesium from 'cesium';
|
||||
@@ -79,6 +80,9 @@ function localOutlineFrame(fp) {
|
||||
/** Ms the analyst region lookup waits on geocode + admin boundary. */
|
||||
const REGION_FALLBACK_BUDGET_MS = 3_000;
|
||||
|
||||
/** How far a street may run from its anchor when the ribbon holds neither. */
|
||||
const STREET_SNAP_MAX_M = 100;
|
||||
|
||||
/**
|
||||
* Names that end like a landmark rather than a district: "Central Park",
|
||||
* "Lake Merritt"-style suffixes are checked on the last word only, so "Park
|
||||
@@ -830,8 +834,18 @@ export function createAnnotationResolver({
|
||||
frame.reference === null
|
||||
? lon
|
||||
: lon - 360 * Math.round((lon - frame.reference) / 360);
|
||||
if (!polygonsContainPoint(frame.polygons, lat, anchorLon)) return null;
|
||||
centroid = { lat, lon };
|
||||
if (polygonsContainPoint(frame.polygons, lat, anchorLon)) {
|
||||
centroid = { lat, lon };
|
||||
} else if (Array.isArray(fp.lines)) {
|
||||
// A bent street's ribbon rarely holds its ring mean, and a geocoded
|
||||
// anchor can sit beside the centreline: centre on the street itself,
|
||||
// only when it runs beside the anchor.
|
||||
const onStreet = nearestPointOnLines(fp.lines, lat, lon);
|
||||
if (!onStreet || onStreet.distanceM > STREET_SNAP_MAX_M) return null;
|
||||
centroid = { lat: onStreet.lat, lon: onStreet.lon };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (
|
||||
frame.reference !== null &&
|
||||
|
||||
@@ -150,6 +150,46 @@ export function polygonsContain(polygons, lat, lon) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether polygons contain a point. A part whose outer ring crosses the
|
||||
* antimeridian ([179.99, …], [-179.99, …]) is read contiguously in its own
|
||||
* longitude frame, with the point moved into that frame; every other part,
|
||||
* including one genuinely wider than 180°, is tested as stored. Crossing is
|
||||
* read from an edge jumping more than 180°, which assumes densely noded rings
|
||||
* (OSM, Nominatim, tiles); a ring that is both wider than 180° and crossing, or
|
||||
* a non-crossing ring with a single edge over 180°, is not supported.
|
||||
*/
|
||||
export function containsAnchor(polygons, lat, lon) {
|
||||
if (!Number.isFinite(lon)) return false;
|
||||
for (const part of polygons || []) {
|
||||
const outer = part?.[0];
|
||||
if (!Array.isArray(outer) || !outer.length) continue;
|
||||
const reference = outer[0][0];
|
||||
const shift = (x) => x - 360 * Math.round((x - reference) / 360);
|
||||
const span = (ring, f) => {
|
||||
let west = Infinity;
|
||||
let east = -Infinity;
|
||||
for (const [x] of ring) {
|
||||
west = Math.min(west, f(x));
|
||||
east = Math.max(east, f(x));
|
||||
}
|
||||
return east - west;
|
||||
};
|
||||
// An edge jumping more than 180° wraps the line; a polar ring whose
|
||||
// edges run along ±180 collapses when reframed and is read as stored.
|
||||
const jumps = outer.some(
|
||||
([x], i) => i > 0 && Math.abs(x - outer[i - 1][0]) > 180,
|
||||
);
|
||||
const framedSpan = jumps ? span(outer, shift) : 0;
|
||||
const crosses = framedSpan > 0 && framedSpan < 180;
|
||||
const framed = crosses
|
||||
? part.map((ring) => ring.map(([x, y]) => [shift(x), y]))
|
||||
: part;
|
||||
if (polygonsContain([framed], lat, crosses ? shift(lon) : lon)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A unit's geometry, decoded on first use. Longitudes are unwrapped around
|
||||
* the largest part for the box so units cut at the antimeridian (Alaska,
|
||||
|
||||
@@ -253,6 +253,37 @@ export function ringCentroid(ring) {
|
||||
return { lat: sumLat / ring.length, lon: sumLon / ring.length };
|
||||
}
|
||||
|
||||
/**
|
||||
* The point on any of `lines` ([lon, lat] chains) nearest `lat, lon`, with
|
||||
* its distance in metres, or null when no line has a segment.
|
||||
*/
|
||||
export function nearestPointOnLines(lines, lat, lon) {
|
||||
const mLat = 111320;
|
||||
const mLon = mLat * Math.cos((lat * Math.PI) / 180);
|
||||
let best = null;
|
||||
let bestD2 = Infinity;
|
||||
for (const line of lines || []) {
|
||||
for (let i = 0; i + 1 < line.length; i++) {
|
||||
const ax = (line[i][0] - lon) * mLon;
|
||||
const ay = (line[i][1] - lat) * mLat;
|
||||
const dx = (line[i + 1][0] - line[i][0]) * mLon;
|
||||
const dy = (line[i + 1][1] - line[i][1]) * mLat;
|
||||
const len2 = dx * dx + dy * dy;
|
||||
const t = len2
|
||||
? Math.max(0, Math.min(1, -(ax * dx + ay * dy) / len2))
|
||||
: 0;
|
||||
const x = ax + t * dx;
|
||||
const y = ay + t * dy;
|
||||
if (x * x + y * y < bestD2) {
|
||||
bestD2 = x * x + y * y;
|
||||
best = { lat: lat + y / mLat, lon: lon + x / mLon, distanceM: 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (best) best.distanceM = Math.sqrt(bestD2);
|
||||
return best;
|
||||
}
|
||||
|
||||
export function approximateAreaM2(coords) {
|
||||
// Shoelace in a local equirectangular projection (good enough for buildings).
|
||||
if (coords.length < 3) return 0;
|
||||
|
||||
@@ -79,10 +79,16 @@ export function createNominatimOutlineClient({
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{query: string, kind: string, lat?: number, lon?: number}} ask
|
||||
* `lat, lon` bias the search; `anchorLat, anchorLon` (the resolved place)
|
||||
* pick which same-name result answers.
|
||||
*
|
||||
* @param {{query: string, kind: string, lat?: number, lon?: number, anchorLat?: number, anchorLon?: number}} ask
|
||||
* @param {{signal?: AbortSignal}} [options]
|
||||
*/
|
||||
async function lookup({ query, kind, lat, lon }, { signal } = {}) {
|
||||
async function lookup(
|
||||
{ query, kind, lat, lon, anchorLat, anchorLon },
|
||||
{ signal } = {},
|
||||
) {
|
||||
if (unavailable) return unavailable;
|
||||
const text = String(query || '').trim();
|
||||
if (!text || text.length > 200 || !NOMINATIM_OUTLINE_KINDS.includes(kind))
|
||||
@@ -92,6 +98,11 @@ export function createNominatimOutlineClient({
|
||||
params.set('lat', lat.toFixed(3));
|
||||
params.set('lon', lon.toFixed(3));
|
||||
}
|
||||
if (Number.isFinite(anchorLat) && Number.isFinite(anchorLon)) {
|
||||
// Full anchor precision: rounding could cross a building's edge.
|
||||
params.set('alat', anchorLat.toFixed(6));
|
||||
params.set('alon', anchorLon.toFixed(6));
|
||||
}
|
||||
const key = params.toString();
|
||||
if (remembered.has(key)) return remembered.get(key);
|
||||
// A cancelled ask spends nothing.
|
||||
|
||||
@@ -210,3 +210,21 @@ test('an ask cancelled while the capability probe is pending spends nothing', as
|
||||
assert.equal(calls.length, 0, `${label}: no outline request`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the resolved anchor is sent at full precision, apart from the bias', async () => {
|
||||
const { calls, lookup } = client(() =>
|
||||
reply(200, { status: 'OK', outline: { polygons: [[ring]] } }),
|
||||
);
|
||||
await lookup({
|
||||
query: 'Paris',
|
||||
kind: 'city',
|
||||
lat: 48.85,
|
||||
lon: 2.35,
|
||||
anchorLat: 48.8566,
|
||||
anchorLon: 2.35221,
|
||||
});
|
||||
assert.match(
|
||||
calls[0],
|
||||
/&lat=48\.850&lon=2\.350&alat=48\.856600&alon=2\.352210$/,
|
||||
);
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ringAreaM2,
|
||||
closeRing,
|
||||
stitchLines,
|
||||
nearestPointOnLines,
|
||||
} from './featureGeometry.js';
|
||||
import { phaseTiming } from './phaseTiming.js';
|
||||
import { normalizePlaceName } from '../data/placeBoundaries.js';
|
||||
@@ -406,21 +407,7 @@ function distanceToPolygonM(lon, lat, rings) {
|
||||
|
||||
/** Distance (m) from a point to the nearest segment of a line or ring. */
|
||||
function distanceToLineM(lon, lat, ring) {
|
||||
const mLat = 111_320;
|
||||
const mLon = mLat * Math.cos((lat * Math.PI) / 180);
|
||||
let best = Infinity;
|
||||
for (let i = 1; i < ring.length; i++) {
|
||||
const ax = (ring[i - 1][0] - lon) * mLon;
|
||||
const ay = (ring[i - 1][1] - lat) * mLat;
|
||||
const bx = (ring[i][0] - lon) * mLon;
|
||||
const by = (ring[i][1] - lat) * mLat;
|
||||
const dx = bx - ax;
|
||||
const dy = by - ay;
|
||||
const len2 = dx * dx + dy * dy;
|
||||
const t = len2 ? Math.max(0, Math.min(1, -(ax * dx + ay * dy) / len2)) : 0;
|
||||
best = Math.min(best, Math.hypot(ax + t * dx, ay + t * dy));
|
||||
}
|
||||
return best;
|
||||
return nearestPointOnLines([ring], lat, lon)?.distanceM ?? Infinity;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import {
|
||||
acceptsOutlineClass,
|
||||
capOutlineVertices,
|
||||
outlineAt,
|
||||
outlineBias,
|
||||
selectOutlineResult,
|
||||
} from '../../server/providers/regional/outlineSelect.js';
|
||||
@@ -1092,6 +1093,16 @@ test('the outline route answers OK with polygons and validates its input', async
|
||||
assert.match(calls[0], /polygon_geojson=1/);
|
||||
assert.match(calls[0], /polygon_threshold=0\.0005/);
|
||||
assert.match(calls[0], /limit=3/);
|
||||
// The resolved anchor picks among cached same-name results.
|
||||
const anchored = await request(
|
||||
'/outline?q=Paris&kind=city&lat=48.85&lon=2.35&alat=10&alon=10',
|
||||
);
|
||||
assert.equal(
|
||||
anchored.body.outline.osm,
|
||||
ok.body.outline.osm,
|
||||
'else the first',
|
||||
);
|
||||
assert.equal(calls.length, 1, 'the anchor is not part of the upstream ask');
|
||||
for (const bad of [
|
||||
'/outline?q=Paris',
|
||||
'/outline?kind=city',
|
||||
@@ -1271,3 +1282,132 @@ test('the outline route reports disabled and capped as permanent, busy as retrya
|
||||
assert.equal(refused.status, 429);
|
||||
assert.equal(refused.headers['Retry-After'], '90');
|
||||
});
|
||||
|
||||
test('a landmark ask is not answered by the place around it', () => {
|
||||
const park = (name) =>
|
||||
row({ category: 'leisure', type: 'park', name, display_name: name });
|
||||
const landmark = (query) => ({ kind: 'landmark', query });
|
||||
for (const query of [
|
||||
'Central Park Zoo',
|
||||
'Washington Square Arch',
|
||||
'Lincoln Park Conservatory',
|
||||
])
|
||||
assert.equal(
|
||||
selectOutlineResult(
|
||||
[park(query.split(' ').slice(0, -1).join(' '))],
|
||||
landmark(query),
|
||||
).outline,
|
||||
null,
|
||||
query,
|
||||
);
|
||||
// The last word may say where the result is, or what it is.
|
||||
const at = (name, display_name, overrides = {}) =>
|
||||
row({
|
||||
category: 'tourism',
|
||||
type: 'attraction',
|
||||
name,
|
||||
display_name,
|
||||
...overrides,
|
||||
});
|
||||
for (const [query, result] of [
|
||||
[
|
||||
'Space Needle Seattle',
|
||||
at('Space Needle', 'Space Needle, Seattle, King County, Washington'),
|
||||
],
|
||||
[
|
||||
'Golden Gate Bridge San Francisco',
|
||||
at('Golden Gate Bridge', 'Golden Gate Bridge, San Francisco, California'),
|
||||
],
|
||||
[
|
||||
'Notre Dame Cathedral',
|
||||
at('Notre-Dame de Paris', 'Notre-Dame de Paris, Paris, France', {
|
||||
category: 'building',
|
||||
type: 'cathedral',
|
||||
}),
|
||||
],
|
||||
])
|
||||
assert.ok(selectOutlineResult([result], landmark(query)).outline, query);
|
||||
// An exact name, then one holding every asked word, outranks earlier
|
||||
// partial matches.
|
||||
const exact = selectOutlineResult(
|
||||
[park('Zilker Metropolitan Park'), park('Zilker Park')],
|
||||
landmark('Zilker Park'),
|
||||
);
|
||||
assert.deepEqual(
|
||||
exact.outlines.map((outline) => outline.name),
|
||||
['Zilker Park', 'Zilker Metropolitan Park'],
|
||||
);
|
||||
const every = selectOutlineResult(
|
||||
[park('Royal Park'), park('Royal Botanic Historic Park')],
|
||||
landmark('Royal Botanic Park'),
|
||||
);
|
||||
assert.equal(every.outline.name, 'Royal Botanic Historic Park');
|
||||
});
|
||||
|
||||
test('outlineAt reads a ring crossing the antimeridian in its own frame', () => {
|
||||
const band = {
|
||||
osm: 'way/1',
|
||||
polygons: [
|
||||
[
|
||||
[
|
||||
[170, -17],
|
||||
[175, -17],
|
||||
[175, -16],
|
||||
[170, -16],
|
||||
[170, -17],
|
||||
],
|
||||
],
|
||||
],
|
||||
};
|
||||
const fiji = {
|
||||
osm: 'way/2',
|
||||
polygons: [
|
||||
[
|
||||
[
|
||||
[179.9, -17],
|
||||
[-179.9, -17],
|
||||
[-179.9, -16],
|
||||
[179.9, -16],
|
||||
[179.9, -17],
|
||||
],
|
||||
],
|
||||
],
|
||||
};
|
||||
assert.equal(outlineAt([band, fiji], -16.5, 179.995).osm, 'way/2');
|
||||
assert.equal(outlineAt([band, fiji], -16.5, 172).osm, 'way/1');
|
||||
assert.equal(
|
||||
outlineAt([fiji, band], -16.5, 0).osm,
|
||||
'way/2',
|
||||
'else the first',
|
||||
);
|
||||
});
|
||||
|
||||
test('a same-name outline answers the ask whose anchor it holds, cached or not', async (t) => {
|
||||
const london = (lon, lat, osm_id) =>
|
||||
row({
|
||||
category: 'place',
|
||||
type: 'city',
|
||||
addresstype: 'city',
|
||||
name: 'London',
|
||||
osm_id,
|
||||
geojson: { type: 'Polygon', coordinates: square(lon, lat, 0.2) },
|
||||
});
|
||||
const { calls, fetchImpl } = stubFetch(async () =>
|
||||
json([london(-0.2, 51.4, 1), london(-81.35, 42.9, 2)]),
|
||||
);
|
||||
const gate = publicGate(t, { fetchImpl, ...clock() });
|
||||
const outline = createNominatimOutlineProvider({ gate });
|
||||
const ask = { query: 'London', kind: 'city', lat: 43, lon: -81 };
|
||||
const ontario = await outline({
|
||||
...ask,
|
||||
anchorLat: 42.98,
|
||||
anchorLon: -81.25,
|
||||
});
|
||||
assert.equal(ontario.outline.osm, 'relation/2');
|
||||
const england = await outline({ ...ask, anchorLat: 51.5, anchorLon: -0.12 });
|
||||
assert.equal(england.outline.osm, 'relation/1');
|
||||
assert.equal(england.cached, true);
|
||||
const unanchored = await outline(ask);
|
||||
assert.equal(unanchored.outline.osm, 'relation/1', 'first without an anchor');
|
||||
assert.equal(calls.length, 1, 'one upstream request for every anchor');
|
||||
});
|
||||
Reference in new issue
Block a user