mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +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
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user