mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
A mutation review (91, then 130 mutants) found tests that could not fail
(quick presses with synchronous fakes, a no-flight check with a provider
that never posed), and untested paths. New and rewritten tests kill 123 of
130 mutants; the 7 survivors are equivalent or harmless redraws.
- viewer: held-back loads for quick presses, stale sharp/flat loads,
close mid-load, ends checks for an older photo, WebGL retried per mount,
focus handed back, look direction kept on ›.
- provider: id-ordered pages with out-of-order indexes, page and probe
failures retried, overtaken lookups, cones re-centred on ‹ ›, leftover
imagery filters hide no line or cone, coverage hints, tile cache.
- source: 429 backoff and Retry-After under mock timers, hang timeouts,
the nearest query's parameters. policy: storage-host injection, FOV 360,
longitudes past ±180. panorama: a stub WebGL context (angles, lost
context, re-measuring). lineCoverage.test.mjs: tooHigh hint, passes().
- core: openImage({frame:false}) and openNearest leave the camera
(with a posing provider and a default-open control), KartaView's legend
and wording end to end; viewerHost supportsRenderModes.
- Fixed tick-count waits replaced by an until() poll.
- Browser gate fixture: odd photos archived (409 at storage2, served by
the proxy), the first lookup rate-limited (429, Retry-After: 0); the
line-click step asserts the camera stays and › skips the archived photo.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
238 lines
6.6 KiB
JavaScript
238 lines
6.6 KiB
JavaScript
/** Shared Street Level test stand-ins: provider snapshot, minimal provider, ray-casting camera. */
|
|
import * as Cesium from 'cesium';
|
|
import { MAPILLARY_CREDIT_HTML } from '../layers/streetLevel/providers/mapillary/policy.js';
|
|
import {
|
|
latToTileY,
|
|
lonToTileX,
|
|
tileBounds,
|
|
} from '../layers/streetLevel/tileMath.js';
|
|
|
|
const RAD = Math.PI / 180;
|
|
|
|
/** One provider as `getUIState().providers` lists it. */
|
|
export function providerSnapshot(overrides = {}) {
|
|
return {
|
|
id: 'mapillary',
|
|
name: 'Mapillary',
|
|
label: 'MAPILLARY',
|
|
on: true,
|
|
configured: true,
|
|
keyRequired: false,
|
|
requiresKeyId: 'mapillary',
|
|
loading: false,
|
|
count: 0,
|
|
hint: '',
|
|
error: null,
|
|
color: '#05cb63',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The smallest provider the core accepts. Mutate `stats` to change what
|
|
* coverageStats() answers; `calls`, `filters` and `context()` record use.
|
|
*/
|
|
export function fakeStreetLevelProvider({
|
|
id = 'mapillary',
|
|
pickPrefix = 'mly:',
|
|
html = MAPILLARY_CREDIT_HTML,
|
|
nearestImage = async () => null,
|
|
} = {}) {
|
|
const calls = { activate: 0, deactivate: 0, mount: 0, open: [], unmount: 0 };
|
|
const filters = [];
|
|
const stats = {
|
|
count: 0,
|
|
zoom: null,
|
|
kind: null,
|
|
loading: false,
|
|
hint: '',
|
|
error: null,
|
|
keyRequired: false,
|
|
};
|
|
let context = null;
|
|
return {
|
|
calls,
|
|
filters,
|
|
stats,
|
|
context: () => context,
|
|
id,
|
|
name: 'Mapillary',
|
|
label: 'MAPILLARY',
|
|
requiresKeyId: null,
|
|
pickPrefix,
|
|
colors: { coverage: '#05cb63' },
|
|
credit: { html },
|
|
create: (providerContext) => {
|
|
context = providerContext;
|
|
return {
|
|
status: async () => ({ configured: true }),
|
|
init() {},
|
|
activate: () => calls.activate++,
|
|
deactivate: () => calls.deactivate++,
|
|
destroy() {},
|
|
refreshCoverage() {},
|
|
setFilter: (filter) => filters.push(filter),
|
|
coverageStats: () => ({ ...stats }),
|
|
handlePick: () => false,
|
|
nearestImage,
|
|
viewer: {
|
|
mount: async () => calls.mount++,
|
|
open: async (imageId) => calls.open.push(imageId),
|
|
close() {},
|
|
unmount: () => calls.unmount++,
|
|
resize() {},
|
|
onPose: () => () => {},
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A pinhole camera `altitude` m above WGS84, at `heading` (deg from north) and
|
|
* `pitch` (deg, negative down); `pickEllipsoid` hits whatever ellipsoid it gets.
|
|
*/
|
|
export function rayCamera({
|
|
lon,
|
|
lat,
|
|
altitude,
|
|
heading = 0,
|
|
pitch,
|
|
width = 1600,
|
|
height = 900,
|
|
fovY = 60,
|
|
fovX = null,
|
|
}) {
|
|
const position = Cesium.Cartesian3.fromDegrees(lon, lat, altitude);
|
|
const frame = Cesium.Transforms.eastNorthUpToFixedFrame(position);
|
|
const h = heading * RAD;
|
|
const p = pitch * RAD;
|
|
// East-north-up axes of the view: forward, right and up.
|
|
const forward = [
|
|
Math.sin(h) * Math.cos(p),
|
|
Math.cos(h) * Math.cos(p),
|
|
Math.sin(p),
|
|
];
|
|
const right = [Math.cos(h), -Math.sin(h), 0];
|
|
const up = [
|
|
-Math.sin(h) * Math.sin(p),
|
|
-Math.cos(h) * Math.sin(p),
|
|
Math.cos(p),
|
|
];
|
|
const halfY = Math.tan((fovY / 2) * RAD);
|
|
const halfX =
|
|
fovX == null ? halfY * (width / height) : Math.tan((fovX / 2) * RAD);
|
|
return {
|
|
positionWC: position,
|
|
positionCartographic: Cesium.Cartographic.fromDegrees(lon, lat, altitude),
|
|
pickEllipsoid(point, ellipsoid = Cesium.Ellipsoid.WGS84) {
|
|
const sx = ((2 * point.x) / width - 1) * halfX;
|
|
const sy = (1 - (2 * point.y) / height) * halfY;
|
|
const local = [0, 1, 2].map(
|
|
(i) => forward[i] + sx * right[i] + sy * up[i],
|
|
);
|
|
const direction = Cesium.Cartesian3.normalize(
|
|
Cesium.Matrix4.multiplyByPointAsVector(
|
|
frame,
|
|
new Cesium.Cartesian3(...local),
|
|
new Cesium.Cartesian3(),
|
|
),
|
|
new Cesium.Cartesian3(),
|
|
);
|
|
const ray = new Cesium.Ray(position, direction);
|
|
const hit = Cesium.IntersectionTests.rayEllipsoid(ray, ellipsoid);
|
|
return hit ? Cesium.Ray.getPoint(ray, hit.start) : undefined;
|
|
},
|
|
computeViewRectangle: () => undefined,
|
|
};
|
|
}
|
|
|
|
/** The centre of the tile holding a point at zoom `z`, so a small view is one tile. */
|
|
export function tileCentre(lon, lat, z = 14) {
|
|
const x = lonToTileX(lon, z);
|
|
const y = latToTileY(lat, z);
|
|
const { west, east, south, north } = tileBounds(x, y, z);
|
|
return {
|
|
tile: { x, y, z },
|
|
lon: (west + east) / 2,
|
|
lat: (south + north) / 2,
|
|
};
|
|
}
|
|
|
|
/** A scene collection that remembers what is on the globe. */
|
|
function sceneCollection() {
|
|
const items = new Set();
|
|
return {
|
|
items,
|
|
add(primitive) {
|
|
items.add(primitive);
|
|
return primitive;
|
|
},
|
|
remove(primitive) {
|
|
return items.delete(primitive);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A viewer looking straight down on a 0.004° square around `view`
|
|
* (`{lon, lat, height}`, mutable), enough for line coverage to attach,
|
|
* refresh and draw. Cesium's one-time terrain-height download is skipped.
|
|
*/
|
|
export function coverageViewer(view, t) {
|
|
const initialize = Cesium.GroundPolylinePrimitive.initializeTerrainHeights;
|
|
Cesium.GroundPolylinePrimitive.initializeTerrainHeights = () =>
|
|
Promise.resolve();
|
|
t?.after(() => {
|
|
Cesium.GroundPolylinePrimitive.initializeTerrainHeights = initialize;
|
|
});
|
|
// Node has no WebGL context to report line-width limits.
|
|
Cesium.ContextLimits._minimumAliasedLineWidth = 1;
|
|
Cesium.ContextLimits._maximumAliasedLineWidth = 10;
|
|
const listeners = () => {
|
|
const set = new Set();
|
|
return {
|
|
set,
|
|
addEventListener(listener) {
|
|
set.add(listener);
|
|
return () => set.delete(listener);
|
|
},
|
|
};
|
|
};
|
|
return {
|
|
view,
|
|
scene: {
|
|
canvas: { clientWidth: 100, clientHeight: 100 },
|
|
globe: { show: false, ellipsoid: Cesium.Ellipsoid.WGS84 },
|
|
frameState: { mode: Cesium.SceneMode.SCENE3D },
|
|
updateHeight: () => () => {},
|
|
getHeight: () => undefined,
|
|
groundPrimitives: sceneCollection(),
|
|
primitives: sceneCollection(),
|
|
postRender: listeners(),
|
|
preRender: listeners(),
|
|
},
|
|
camera: {
|
|
changed: new Cesium.Event(),
|
|
moveEnd: new Cesium.Event(),
|
|
get positionWC() {
|
|
return Cesium.Cartesian3.fromDegrees(view.lon, view.lat, view.height);
|
|
},
|
|
get positionCartographic() {
|
|
return {
|
|
longitude: view.lon * RAD,
|
|
latitude: view.lat * RAD,
|
|
height: view.height,
|
|
};
|
|
},
|
|
pickEllipsoid(point) {
|
|
return Cesium.Cartesian3.fromDegrees(
|
|
view.lon - 0.002 + (point.x / 100) * 0.004,
|
|
view.lat + 0.002 - (point.y / 100) * 0.004,
|
|
);
|
|
},
|
|
computeViewRectangle: () => undefined,
|
|
},
|
|
};
|
|
}
|