fix(street-level): keep cones on low ground, and gekh's P3s

From gekh's review of db79027, each with a test that fails before:

- P2: image cones and the photo marker no longer disappear where the
  ground lies below the WGS84 ellipsoid (NYC -22 m, Colombo -95 m). The
  horizon cull's occluder is WGS84 shrunk by 1 km (HORIZON_CULL_DEPTH_M),
  below any real surface, so a camera near or under the ellipsoid no
  longer treats every lower point as behind the horizon; the far side of
  the Earth is still hidden from orbit (tested).
- Mesh range across the date line: the camera's longitude is taken to the
  box's side of +-180 before clamping (meshBoxInRange), so a tile just
  across the line is not measured from its far edge.
- MapillaryJS no longer tracks window resizes (trackResize: false): the
  panel's ResizeObserver resizes the viewer and skips it while hidden, so
  a window resize after closing a photo cannot ask for a tile at z=NaN.
- A key-less install sends no tile requests before the key status is
  known: coverage waits for the first status answer and the provider
  refreshes it once the status lands.
- constructCatalog takes the source method list from MAPILLARY_SOURCE_
  METHODS; sequences.js imports view.js once; the provider's unused
  re-exports are gone.

Verified: unit 5970 pass; strict fixture gate on a production build 28 ok
+ 2 "no Google 3D" skips and the panel gate 6/6 (QA_FAIL_ON_RETRY=1);
live gate with a real token 25 ok + 3 fixture-only skips.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-06 18:28:14 +01:00
1 parent db79027998
commit c65feda4b1
10 files changed
+208 -19

No files matched your search

@@ -95,6 +95,24 @@ export function sequenceIdFromPick(pickId) {
* and tree tops. A tile is drawn draped first and swapped for its cast lines
* once the terrain heights are in, so coverage never waits on the terrain.
*/
/**
* Whether any point of a line's bounding `box` is within the mesh sampler's
* range of the camera ground point `centre`, measured the short way round
* the date line.
*/
export function meshBoxInRange(box, centre) {
// Take the camera's longitude to the box's side of the date line first:
// clamping 179.999 into a box at -179.99 would pick its far edge.
const lon =
centre.lon +
360 * Math.round(((box.west + box.east) / 2 - centre.lon) / 360);
const nearest = {
lon: Math.min(box.east, Math.max(box.west, lon)),
lat: Math.min(box.north, Math.max(box.south, centre.lat)),
};
return metresBetween(centre, nearest) <= MESH_SAMPLE_RADIUS_M;
}
export function createCoverage({ state, source }) {
const { render } = state.services;
@@ -268,13 +286,6 @@ export function createCoverage({ state, source }) {
}
/** Whether any of a {west, south, east, north} box is within the sampler's range. */
function inMeshRange(box, centre) {
const nearest = {
lon: Math.min(box.east, Math.max(box.west, centre.lon)),
lat: Math.min(box.north, Math.max(box.south, centre.lat)),
};
return metresBetween(centre, nearest) <= MESH_SAMPLE_RADIUS_M;
}
/**
* A tile's drawn lines with their bounding boxes, built once per filter;
@@ -310,10 +321,11 @@ export function createCoverage({ state, source }) {
const sampler = state.context.meshSampler;
if (!sampler || entry.kind !== 'sequence' || !terrainMode()) return;
const centre = meshCentre();
if (!centre || !entry.bounds || !inMeshRange(entry.bounds, centre)) return;
if (!centre || !entry.bounds || !meshBoxInRange(entry.bounds, centre))
return;
const points = [];
for (const line of meshParts(entry)) {
if (!inMeshRange(line.box, centre)) continue;
if (!meshBoxInRange(line.box, centre)) continue;
line.points ||= densifyLine(line.part, MESH_DENSIFY_DEG);
for (const point of line.points) points.push(point);
}
@@ -668,6 +680,7 @@ export function createCoverage({ state, source }) {
function refresh() {
const viewer = state.viewer;
if (!viewer || !state.context.isActive() || state.keyRequired) return;
if (state.statusKnown === false) return;
if (state.keyRejected || state.coverage.holdUntil > Date.now()) return;
const ground = groundUnderCamera(viewer, {
groundAt: state.context.groundCaster?.groundAt,
@@ -1,7 +1,11 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import * as Cesium from 'cesium';
import { SWAP_MAX_WAIT_MS, createCoverage } from './coverage.js';
import {
SWAP_MAX_WAIT_MS,
createCoverage,
meshBoxInRange,
} from './coverage.js';
import { encodeCoverageTile } from './coverageFixture.mjs';
import { rayCamera } from '../../../../testSupport/streetLevelFakes.mjs';
import {
@@ -893,3 +897,16 @@ test('a rate limit holds every refresh inside its wait, not only the first (revi
coverage.clear();
coverage.resetErrors();
});
test('a line across the date line from the camera is in mesh range (gekh P3)', () => {
// A z14 tile's width of lines just past -180, the camera 111 m short of it.
const camera = { lon: 179.999, lat: 0 };
const east = { west: -180, east: -179.978, south: -0.001, north: 0.001 };
assert.equal(meshBoxInRange(east, camera), true);
// The mirror case, from the other side.
const west = { west: 179.978, east: 180, south: -0.001, north: 0.001 };
assert.equal(meshBoxInRange(west, { lon: -179.999, lat: 0 }), true);
// ~2.2 km away across the line: out of range either way.
const far = { west: -179.99, east: -179.97, south: -0.001, north: 0.001 };
assert.equal(meshBoxInRange(far, camera), false);
});
@@ -16,8 +16,6 @@ import {
PICK_PREFIX,
} from './policy.js';
export { MAPILLARY_PROVIDER_ID, mapillaryImageUrl } from './policy.js';
/** Per-provider mutable state, created once per `create()`. */
function createProviderState(context) {
return {
@@ -25,6 +23,11 @@ function createProviderState(context) {
services: context.services,
viewer: null,
keyRequired: false,
/**
* False until the first status answer: coverage asks for nothing before
* it, so a key-less install never sends tile requests that get 503.
*/
statusKnown: false,
/** Mapillary refused the configured token: nothing is requested until re-enabled. */
keyRejected: false,
status: null,
@@ -101,6 +104,10 @@ export function createMapillaryProvider({ source }) {
state.status = null;
state.keyRequired = !source.hasToken();
}
const first = !state.statusKnown;
state.statusKnown = true;
// Coverage held back until now: draw it if the layer is on.
if (first) parts.coverage.refresh();
context.notify();
return { configured: !state.keyRequired };
},
@@ -1,11 +1,14 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createMapillaryProvider, mapillaryImageUrl } from './index.js';
import { createMapillaryProvider } from './index.js';
import { mapillaryImageUrl } from './policy.js';
import { sequenceIdFromPick } from './coverage.js';
import { thinImages } from './sequences.js';
import { validateProviders } from '../../registry.js';
import { PROVIDER_COLORS } from '../../policy.js';
import { resolveFilter } from '../../filter.js';
import * as Cesium from 'cesium';
import { rayCamera } from '../../../../testSupport/streetLevelFakes.mjs';
function fakeSource({ nearest = [] } = {}) {
return {
@@ -180,3 +183,70 @@ test('cone thinning across the date line drops images metres apart (review IC8 P
['a', 'c'],
);
});
/** A viewer looking straight down on downtown Sacramento from 900 m. */
function streetViewer() {
const collection = () => ({
add: (item) => item,
remove: () => true,
contains: () => false,
get length() {
return 0;
},
});
const event = () => ({ addEventListener: () => () => {} });
const camera = Object.assign(
rayCamera({
lon: -121.4944,
lat: 38.5816,
altitude: 900,
pitch: -90,
width: 100,
height: 100,
}),
{ changed: event(), moveEnd: event() },
);
return {
camera,
scene: {
canvas: { clientWidth: 100, clientHeight: 100 },
globe: { show: false, ellipsoid: Cesium.Ellipsoid.WGS84 },
primitives: collection(),
groundPrimitives: collection(),
preRender: event(),
postRender: event(),
requestRender() {},
},
};
}
test('coverage asks for no tiles before the key status is known (gekh P3)', async () => {
for (const configured of [false, true]) {
let answer;
const tiles = [];
const source = {
...fakeSource(),
hasToken: () => configured,
getStatus: () => new Promise((resolve) => (answer = resolve)),
getTile: (...args) => {
tiles.push(args);
return new Promise(() => {});
},
};
const instance = createMapillaryProvider({ source }).create(fakeContext());
const viewer = streetViewer();
instance.init(viewer);
const status = instance.status();
instance.activate(viewer);
assert.equal(tiles.length, 0, 'nothing before the status answers');
answer({ configured });
await status;
if (configured)
assert.ok(tiles.length > 0, 'coverage loads as soon as the key is known');
else {
assert.equal(tiles.length, 0, 'a key-less install never asks');
assert.equal(instance.coverageStats().keyRequired, true);
}
instance.destroy(viewer);
}
});
@@ -2,9 +2,8 @@ import * as Cesium from 'cesium';
import { imageConeGlyph } from '../../glyphs.js';
import { passesImageryFilter } from '../../filter.js';
import { refineHeights } from '../../groundCast.js';
import { createHorizonCull } from '../../view.js';
import { createHorizonCull, metresBetween } from '../../view.js';
import { meshCellKey } from '../../meshSampler.js';
import { metresBetween } from '../../view.js';
import {
COLORS,
IMAGE_CONE_MIN_SPACING_M,
@@ -124,7 +124,10 @@ export function createMapillaryViewer({ source, render } = {}) {
zoom: true,
attribution: true,
},
trackResize: true,
// The panel's ResizeObserver resizes the viewer, and skips it while it
// is hidden (0x0); MapillaryJS's own window tracking would resize a
// hidden viewer and ask for a tile at z=NaN.
trackResize: false,
renderMode: libraryRenderMode(renderMode),
});
container = host;