keep tile outlines honest when the anchor tile fails to load

A building or grounds outline from tiles now needs the tile holding the
anchor: when that tile fails and only neighbours load, no neighbouring
building or area stands in. Nominatim name tags holding several
";"-separated names match each name. Document recovery from a stuck
public-Nominatim dispatch marker, correct the phone credit comment,
drive the slow-device queue test with a mocked performance clock, and
credit GitHub handles in the changelog.
This commit is contained in:
manjunath committed 2026-10-31 02:23:43 +05:30
1 parent c61ddb38c2
commit 62a36b9609
10 files changed
+134 -32

No files matched your search

@@ -723,6 +723,10 @@ test('a building queued behind others gets its full budget on a slow device', as
installBrowserGlobals(t);
t.mock.timers.enable({ apis: ['setTimeout'] });
t.after(() => t.mock.timers.reset());
// The renderers' per-frame budget checks read performance.now(); it follows
// the same mocked clock as the timers.
let now = 0;
t.mock.method(globalThis.performance, 'now', () => now);
const { viewer, frames } = fakeViewer(-99, 31);
const renderer = createHybridAnnotationRenderer(viewer);
viewer.scene.clampToHeight = (position) => position;
@@ -732,17 +736,22 @@ test('a building queued behind others gets its full budget on a slow device', as
pendingVisible: false,
workTrace: traces[index],
}));
// Eight probes per building at one probe per frame; 1.5 s passes while
// each of the first two buildings samples, so the third waits 3 s in line.
// Eight probes per building at one probe per frame, at about 5 fps: each
// building samples for 1.5 s, so the third waits 3 s in line. Pending
// promise work runs between frames, as it does in a browser.
for (let building = 0; building < 3; building += 1) {
for (let frame = 0; frame < 8; frame += 1) frames.emit();
if (building < 2) t.mock.timers.tick(1500);
for (let frame = 0; frame < 8; frame += 1) {
now += 187.5;
t.mock.timers.tick(187.5);
frames.emit();
await new Promise((resolve) => setImmediate(resolve));
}
}
const results = await Promise.all(completions);
assert.deepEqual(results.map((result) => result.status), ['installed', 'installed', 'installed']);
assert.deepEqual(traces.map((trace) => trace.samplerStatus), ['installed', 'installed', 'installed']);
assert.ok(traces[2].samplerHeadAt - traces[2].samplerStart >= 2000, 'the third waited longer than one budget');
assert.ok(traces[2].samplerHeadAt >= traces[1].samplerHeadAt);
assert.ok(traces[2].samplerHeadAt >= traces[2].samplerStart);
});
test('building cancellation, queued deadline and destroy prevent saved callbacks from committing', async (t) => {
+23 -7
View File
@@ -5,6 +5,7 @@ import {
} from '../sources/featureGeometry.js';
import { isUnavailableCapability } from '../sources/capability.js';
import { polygonsContain } from '../data/adminBoundaries.js';
import { lonLatToTile } from '../data/tomtomTiles.js';
import {
BUILDING_MAX_DISTANCE_M,
OUTLINE_TILE_ZOOM,
@@ -173,13 +174,27 @@ export function createOutlineRungs({
tiles = sharedOutlineTiles(),
nominatim = createNominatimOutlineClient(),
} = {}) {
/** Decoded tiles around a point, or null when the tiles cannot be read. */
async function tilesAround(lat, lon, radiusM, signal) {
/**
* Decoded tiles around a point, or null when the tiles cannot be read. With
* `needAnchor`, a partial load that lost the point's own tile is null too: a
* neighbouring tile alone cannot show what is at the point, so a nearby
* building or area would stand in for it.
*/
async function tilesAround(lat, lon, radiusM, signal, needAnchor = false) {
try {
const { tiles: decoded } = await tiles.fetchBounds(
boxAround(lat, lon, radiusM),
{ zoom: OUTLINE_TILE_ZOOM, signal },
);
const {
tiles: decoded,
partial,
loadedTiles,
} = await tiles.fetchBounds(boxAround(lat, lon, radiusM), {
zoom: OUTLINE_TILE_ZOOM,
signal,
});
if (needAnchor && partial && Array.isArray(loadedTiles)) {
const anchor = lonLatToTile(lon, lat, OUTLINE_TILE_ZOOM);
if (!loadedTiles.some((t) => t.x === anchor.x && t.y === anchor.y))
return null;
}
return decoded;
} catch (error) {
signal?.throwIfAborted();
@@ -212,6 +227,7 @@ export function createOutlineRungs({
ctx.lon,
BUILDING_MAX_DISTANCE_M * 2,
ctx.signal,
true,
);
const building = decoded && buildingFromTiles(decoded, ctx);
if (!building || (requireContainment && building.contains !== true))
@@ -228,7 +244,7 @@ export function createOutlineRungs({
/** The open area enclosing `seed` (the named feature) or the anchor. */
async function groundsRung(ctx, seed = null) {
const at = seed || { lat: ctx.lat, lon: ctx.lon };
const decoded = await tilesAround(at.lat, at.lon, 600, ctx.signal);
const decoded = await tilesAround(at.lat, at.lon, 600, ctx.signal, true);
const area =
decoded &&
enclosingAreaFromTiles(decoded, {
+54
View File
@@ -340,6 +340,60 @@ test('named building tile fallbacks require the anchor in filled geometry', asyn
assert.equal(pointed.outlineCredit, 'tiles');
});
test('a partial tile load that lost the point tile lets nothing stand in', async () => {
// The point (z14 tile 8192/8192) is ~28 m from a building in the tile that
// did load; without the point's own tile, that neighbour must not answer.
const anchor = { lat: 0, lon: 0.00035 };
const school = square(CAPITOL.lat, CAPITOL.lon, 0.002);
const load = (loadedTiles) => ({
fetchBounds: async () => ({
tiles: [
{
buildings: [{ rings: [square(0, 0, 0.0001)], heightM: null }],
areas: [
{ name: null, rings: [school], class: 'school', clipped: false },
],
},
],
loadedTiles,
partial: true,
}),
});
const pointAt = (tiles) =>
createOutlineRungs({ tiles, nominatim: fakeNominatim(null) }).resolve(
ctx({
...anchor,
view: anchor,
scope: 'building',
target: 'this building',
fromName: false,
}),
ladder(UNAVAILABLE),
);
assert.equal(await pointAt(load([{ z: 14, x: 8191, y: 8192 }])), UNAVAILABLE);
const kept = await pointAt(load([{ z: 14, x: 8192, y: 8192 }]));
assert.equal(kept.footprintKind, 'building', 'the point tile loaded');
const grounds = (loaded) =>
createOutlineRungs({
tiles: load(loaded),
nominatim: fakeNominatim(null),
}).resolve(
ctx({
scope: 'building',
target: 'Lincoln High School grounds',
groundsLike: true,
}),
ladder(UNAVAILABLE),
);
// The Capitol fixture point lies in tile 3743/6745.
assert.equal(await grounds([{ z: 14, x: 3744, y: 6745 }]), UNAVAILABLE);
assert.equal(
(await grounds([{ z: 14, x: 3743, y: 6745 }])).footprintKind,
'area',
);
});
test('a city with no configured Overpass is outlined by Nominatim', async () => {
const ring = square(43.6, 1.44, 0.05);
const nominatim = fakeNominatim({ polygons: [[ring]], class: 'boundary' });
@@ -884,6 +884,18 @@ test('a result whose name does not answer the ask is passed over', () => {
selectOutlineResult([city('City of York', { short_name: 'York' })], york)
.outline,
);
// A name tag holding several names answers for each of them, not the joint text.
assert.ok(
selectOutlineResult([city('Eboracum', { alt_name: 'Jorvik;York' })], york)
.outline,
);
assert.equal(
selectOutlineResult(
[city('Eboracum', { alt_name: 'Jorvik;New York' })],
york,
).outline,
null,
);
assert.equal(
selectOutlineResult([city('新東京')], { kind: 'city', query: '東京' })
.outline,
+2 -2
View File
@@ -500,8 +500,8 @@ test('the phone shell keeps the required attribution visible', () => {
/display:\s*none|visibility:\s*hidden|opacity:\s*0/,
);
// A long credit line (map plus outline credits) wraps inside the screen:
// the box needs a real width for Cesium's absolutely positioned credit
// block, set before the 0.8 scale, so it is divided by it.
// it wraps, and the box gets a real width, set before the 0.8 scale, so it
// is divided by it.
assert.match(idleCredit, /white-space: normal;/);
assert.match(
idleCredit,
+6 -6
View File
@@ -378,12 +378,12 @@ body.mobile-shell:not(.recording-mode):not(.cockpit-mode):not(.ui-clean-view)
right: auto;
bottom: calc(var(--mobile-tab-space) + 106px) !important;
left: 10px;
/* Cesium's credit block is absolutely positioned inside this box, so the
box needs a real width (a max-width alone leaves it 0 wide) for the
credits to wrap at the screen width rather than one word per line. The
width applies before the 0.8 scale, keeping the scaled line 10px from the
right edge, so every credit and "Data attribution" stay on screen. Like
the offset above, it must beat the dock's width: auto !important. */
/* With the base rule's nowrap the credits run off the right edge, and a
max-width alone wrapped them into a narrow column: the line needs wrapping
and a real width. The width applies before the 0.8 scale, keeping the
scaled line 10px from the right edge, so every credit and "Data
attribution" stay on screen. Like the offset above, it must beat the
dock's width: auto !important. */
width: calc((100vw - 20px) / 0.8) !important;
max-width: none !important;
white-space: normal;