mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
tighten the remaining voice outline review items
- One place-name key for server and tiles, so "St. Paul" and "Saint Paul"
agree in Nominatim name checks.
- Tile street names: a lone letter is a direction only beside another name
word ("E Street" is not "East Street"), and a trailing quadrant ("E St NW")
may go unsaid in the ask.
- A building cut at the tile edge that is nearer the point than any whole
one leaves the point instead of a farther neighbour standing in; an edge
without a tile buffer counts as a cut, so a cut area never answers grounds.
- A batch building install re-judges only results that installed; a late
timeout during caption conversion keeps the pending point drawn; a failed
plain recaption keeps the old mark addressable.
- An interrupted Nominatim dispatch is reclaimed by the next lock owner ten
minutes after the start its marker names; node:sqlite loads on first use
without its experimental warning.
- PHOTON_URL accepts only https, which the page's connect-src allows.
- Shared OpenFreeMap tiles decode each layer when a consumer first selects
it; outline selection reuses the shared ring area and simplification, and
ground probes the shared ring centroid; the grounds word list lives once.
This commit is contained in:
1 parent
a38a065c5c
commit
a61d0f4c2a
24 files changed
+422
-128
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { foldPlaceName } from '../../../src/data/placeNames.js';
|
||||
import { normalizePlaceName as foldName } from '../../../src/data/placeNames.js';
|
||||
|
||||
const DAY_MS = 86_400_000;
|
||||
|
||||
@@ -17,13 +17,14 @@ export const NOMINATIM_CACHE_TTL_MS = Object.freeze({
|
||||
|
||||
/**
|
||||
* Comparable form of a place name for matching results to the ask: the
|
||||
* shared fold (see `foldPlaceName`), bounded in length.
|
||||
* shared place-name key (see `normalizePlaceName` in placeNames.js, so
|
||||
* "St. Paul" and "Saint Paul" agree), bounded in length.
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
export function normalizePlaceName(value) {
|
||||
return foldPlaceName(value).slice(0, 200);
|
||||
return foldName(value).slice(0, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import { readResponseJsonCapped } from '../common/http.js';
|
||||
|
||||
/**
|
||||
@@ -50,9 +49,34 @@ const DEFAULT_BACKOFF_MS = 60_000;
|
||||
const BLOCKED_BACKOFF_MS = 3_600_000;
|
||||
/** Pause after a server error or a failed retry. */
|
||||
const OUTAGE_BACKOFF_MS = 30_000;
|
||||
/** How old an unfinished dispatch must be before the next lock owner reclaims it. */
|
||||
const DISPATCH_RECLAIM_MS = 10 * 60_000;
|
||||
|
||||
const DISABLED_VALUES = new Set(['', 'off', 'none', 'disabled', '0']);
|
||||
|
||||
/**
|
||||
* node:sqlite, loaded on first public-instance use. Node 24 still flags it
|
||||
* experimental and prints that once per process; only that one warning is
|
||||
* dropped, and only while the module loads.
|
||||
*/
|
||||
let sqlite = null;
|
||||
function sqliteModule() {
|
||||
if (sqlite) return sqlite;
|
||||
const emitWarning = process.emitWarning;
|
||||
process.emitWarning = function (warning, ...rest) {
|
||||
const type = typeof rest[0] === 'string' ? rest[0] : rest[0]?.type;
|
||||
if (type === 'ExperimentalWarning' && /SQLite/.test(String(warning)))
|
||||
return;
|
||||
return emitWarning.call(this, warning, ...rest);
|
||||
};
|
||||
try {
|
||||
sqlite = process.getBuiltinModule('node:sqlite');
|
||||
} finally {
|
||||
process.emitWarning = emitWarning;
|
||||
}
|
||||
return sqlite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the search endpoint setting.
|
||||
*
|
||||
@@ -257,8 +281,11 @@ export function createGateStateStore({
|
||||
const dispatchMarkerPrefix = file ? `${path.basename(file)}.dispatch.` : null;
|
||||
|
||||
// A crash or persistence failure during dispatch must never reopen admission
|
||||
// using an older timestamp. Only this dispatch removes its own marker after
|
||||
// persisting the post-invocation deadline; there is no timed stale reclaim.
|
||||
// using an older timestamp. A dispatch removes its own marker after
|
||||
// persisting the post-invocation deadline. A marker is only ever seen under
|
||||
// the lock after its owner left the lock, so one that names a start older
|
||||
// than DISPATCH_RECLAIM_MS (far beyond any request's timeout) is reclaimed
|
||||
// by the next lock owner, which restarts spacing from its own clock.
|
||||
function pendingDispatches({ strict = false } = {}) {
|
||||
if (!file) return [];
|
||||
try {
|
||||
@@ -273,6 +300,26 @@ export function createGateStateStore({
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Under the lock: remove dispatch markers whose start is older than
|
||||
* DISPATCH_RECLAIM_MS and report whether any were. Any other marker (newer,
|
||||
* or naming no start) still refuses admission.
|
||||
*/
|
||||
function reclaimDispatches(at) {
|
||||
const names = pendingDispatches({ strict: true });
|
||||
for (const name of names) {
|
||||
const since = /^since-(\d+)$/.exec(
|
||||
name.slice(dispatchMarkerPrefix.length).split('.', 1)[0],
|
||||
);
|
||||
const started = since ? Number(since[1]) : NaN;
|
||||
if (!Number.isSafeInteger(started) || at - started < DISPATCH_RECLAIM_MS)
|
||||
throw new Error('unfinished durable Nominatim dispatch');
|
||||
}
|
||||
for (const name of names)
|
||||
fs.unlinkSync(path.join(path.dirname(file), name));
|
||||
return names.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* A refusal must remain visible even while another process owns the SQLite
|
||||
* transaction. Immutable marker names make that write atomic and monotonic:
|
||||
@@ -411,7 +458,7 @@ export function createGateStateStore({
|
||||
const waitMs = Number.isFinite(lockWaitMs)
|
||||
? Math.max(0, Math.floor(lockWaitMs))
|
||||
: 500;
|
||||
const database = new DatabaseSync(`${file}.lock.sqlite`, {
|
||||
const database = new (sqliteModule().DatabaseSync)(`${file}.lock.sqlite`, {
|
||||
timeout: waitMs,
|
||||
});
|
||||
let transactionOpen = false;
|
||||
@@ -505,8 +552,7 @@ export function createGateStateStore({
|
||||
try {
|
||||
let result;
|
||||
withLock(() => {
|
||||
if (pendingDispatches({ strict: true }).length)
|
||||
throw new Error('unfinished durable Nominatim dispatch');
|
||||
const reclaimed = reclaimDispatches(clock());
|
||||
const disk = readDisk({ strict: true });
|
||||
if (disk) memory = merge(memory, disk);
|
||||
const pauses = pendingPauses({ strict: true });
|
||||
@@ -518,6 +564,11 @@ export function createGateStateStore({
|
||||
),
|
||||
});
|
||||
const at = clock();
|
||||
if (reclaimed) {
|
||||
state.nextStartAt = Math.max(state.nextStartAt, at + minSpacingMs);
|
||||
writeDisk(state);
|
||||
memory = state;
|
||||
}
|
||||
if (at > latestStartAt) {
|
||||
result = { status: 'abandoned' };
|
||||
return;
|
||||
@@ -538,7 +589,7 @@ export function createGateStateStore({
|
||||
: { status: 'wait', waitMs: state.nextStartAt - at };
|
||||
return;
|
||||
}
|
||||
const marker = `${file}.dispatch.${process.pid}.${process.hrtime.bigint()}`;
|
||||
const marker = `${file}.dispatch.since-${at}.${process.pid}.${process.hrtime.bigint()}`;
|
||||
fs.writeFileSync(marker, '', { flag: 'wx' });
|
||||
const next = {
|
||||
day,
|
||||
|
||||
@@ -3,6 +3,10 @@ import {
|
||||
containsAnchor,
|
||||
pointInRing,
|
||||
} from '../../../src/data/adminBoundaries.js';
|
||||
import {
|
||||
douglasPeucker,
|
||||
ringAreaM2,
|
||||
} from '../../../src/sources/featureGeometry.js';
|
||||
|
||||
/**
|
||||
* Which Nominatim results may answer which voice ask, and how their polygons
|
||||
@@ -231,11 +235,6 @@ function adminLevelFits(kind, row) {
|
||||
const BUILDING_MAX_AREA_M2 = 150_000;
|
||||
|
||||
/** Approximate area (m²) of a [lon, lat] ring. */
|
||||
function ringAreaM2(ring) {
|
||||
const lat0 = ring[0][1];
|
||||
const k = 111_320 * 111_320 * Math.cos((lat0 * Math.PI) / 180);
|
||||
return ringArea(ring) * k;
|
||||
}
|
||||
|
||||
/** Whether a result's class/type suits the ask kind. */
|
||||
export function acceptsOutlineClass(kind, osmClass, osmType) {
|
||||
@@ -358,12 +357,6 @@ function cleanRing(ring) {
|
||||
}
|
||||
|
||||
/** Planar ring area in squared degrees (only used to rank parts). */
|
||||
function ringArea(ring) {
|
||||
let sum = 0;
|
||||
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++)
|
||||
sum += (ring[j][0] - ring[i][0]) * (ring[j][1] + ring[i][1]);
|
||||
return Math.abs(sum / 2);
|
||||
}
|
||||
|
||||
/** Polygon/MultiPolygon GeoJSON → `[[outer, ...holes], …]`, largest first. */
|
||||
function polygonsFromGeoJson(geometry) {
|
||||
@@ -387,7 +380,7 @@ function polygonsFromGeoJson(geometry) {
|
||||
if (!outer) return null;
|
||||
const holes = polygon.slice(1).map(cleanRing);
|
||||
if (holes.some((hole) => !hole)) return null;
|
||||
polygons.push({ rings: [outer, ...holes], area: ringArea(outer) });
|
||||
polygons.push({ rings: [outer, ...holes], area: ringAreaM2(outer) });
|
||||
}
|
||||
polygons.sort((a, b) => b.area - a.area);
|
||||
return polygons.map((entry) => entry.rings);
|
||||
@@ -396,37 +389,7 @@ function polygonsFromGeoJson(geometry) {
|
||||
/** Iterative Douglas–Peucker on a closed ring (keeps it closed, ≥ 4 points). */
|
||||
function simplifyClosedRing(ring, tolerance) {
|
||||
if (ring.length <= 8 || tolerance <= 0) return ring;
|
||||
const keep = new Uint8Array(ring.length);
|
||||
keep[0] = keep[ring.length - 1] = 1;
|
||||
const stack = [[0, ring.length - 1]];
|
||||
const tol2 = tolerance * tolerance;
|
||||
while (stack.length) {
|
||||
const [start, end] = stack.pop();
|
||||
const [ax, ay] = ring[start];
|
||||
const [bx, by] = ring[end];
|
||||
const dx = bx - ax;
|
||||
const dy = by - ay;
|
||||
const len2 = dx * dx + dy * dy;
|
||||
let best = -1;
|
||||
let bestD = tol2;
|
||||
for (let i = start + 1; i < end; i++) {
|
||||
const [px, py] = ring[i];
|
||||
let t = len2 ? ((px - ax) * dx + (py - ay) * dy) / len2 : 0;
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
const ex = ax + t * dx - px;
|
||||
const ey = ay + t * dy - py;
|
||||
const d = ex * ex + ey * ey;
|
||||
if (d > bestD) {
|
||||
bestD = d;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
if (best > 0) {
|
||||
keep[best] = 1;
|
||||
stack.push([start, best], [best, end]);
|
||||
}
|
||||
}
|
||||
const out = ring.filter((_point, index) => keep[index]);
|
||||
const out = douglasPeucker(ring, tolerance);
|
||||
return out.length >= 4 ? out : ring;
|
||||
}
|
||||
|
||||
@@ -480,7 +443,7 @@ function ringsIntersect(a, b) {
|
||||
function simplifiedTopologyFits(polygons) {
|
||||
for (const [outer, ...holes] of polygons) {
|
||||
for (const ring of [outer, ...holes]) {
|
||||
if (ringArea(ring) === 0) return false;
|
||||
if (ringAreaM2(ring) === 0) return false;
|
||||
for (let i = 1; i < ring.length; i++)
|
||||
for (let j = i + 2; j < ring.length; j++) {
|
||||
if (i === 1 && j === ring.length - 1) continue;
|
||||
|
||||
Reference in new issue
Block a user