mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
main grew the Street Level provider registry while this branch removed it (#982–#985: injected providers, an empty composition, the `r` switch field). The registry, params, uiState and pick router come back as main has them and the slimmed layer sits behind the provider contract: - The core (index.js) drives providers as on main, without surface modes, ground casting, FOLLOW, prewarm and nearest-image lookups. A provider's key status is asked with a 10 s timeout and asked again on a switch-on while no key is known. - Mapillary is mapillaryProvider.js over the slim coverage, sequences and viewer modules, which read the filter and the active flag from the provider context. - The viewer host mounts one provider adapter at a time, no prewarm. - The panel has a chip per provider again (the pill switches the layer); no legend, EXPAND stays the Fullscreen API. - The contract drops what no provider implements: groundCaster, meshSampler, getSurface/setSurface, nearestImage, prewarm, reportError, the legend and surface fields of the UI snapshot. Co-Authored-By: Claude Fable 5.1 <[email protected]>
264 lines
7.1 KiB
JavaScript
264 lines
7.1 KiB
JavaScript
/** Shared Street Level test stand-ins: provider snapshot and provider, Mapillary source, photo viewer, Cesium viewer, ray-casting camera. */
|
|
import * as Cesium from 'cesium';
|
|
import { MAPILLARY_CREDIT_HTML } from '../layers/streetLevel/policy.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,
|
|
} = {}) {
|
|
const calls = { activate: 0, deactivate: 0, mount: 0, open: [], unmount: 0 };
|
|
const filters = [];
|
|
const stats = {
|
|
count: 0,
|
|
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() {},
|
|
setFilter: (filter) => filters.push(filter),
|
|
coverageStats: () => ({ ...stats }),
|
|
handlePick: () => false,
|
|
viewer: {
|
|
mount: async () => calls.mount++,
|
|
open: async (imageId) => calls.open.push(imageId),
|
|
close() {},
|
|
unmount: () => calls.unmount++,
|
|
resize() {},
|
|
onPose: () => () => {},
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A Mapillary source for the provider: a configured key, empty tiles, and the
|
|
* given lookups; `calls` records tile requests.
|
|
*/
|
|
export function fakeMapillarySource(overrides = {}) {
|
|
const calls = { tiles: [] };
|
|
return {
|
|
calls,
|
|
hasToken: () => true,
|
|
getStatus: async () => ({ configured: true }),
|
|
getTile: async (...args) => {
|
|
calls.tiles.push(args);
|
|
return new Uint8Array(0);
|
|
},
|
|
getSequenceImages: async () => [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A photo viewer in place of MapillaryJS: `calls` records use, and `open`
|
|
* reports a pose for the image (when `pose` is given) as the real one does.
|
|
*/
|
|
export function fakePhotoViewer({ pose = null } = {}) {
|
|
const calls = { mount: 0, open: [], unmount: 0 };
|
|
let emit = null;
|
|
return {
|
|
calls,
|
|
mount: async () => calls.mount++,
|
|
async open(imageId) {
|
|
calls.open.push(imageId);
|
|
if (pose) emit?.({ imageId, ...pose });
|
|
},
|
|
close() {},
|
|
unmount: () => calls.unmount++,
|
|
resize() {},
|
|
onPose(listener) {
|
|
emit = listener;
|
|
return () => {
|
|
emit = null;
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A stand-in Cesium viewer a layer can be enabled on: a canvas for the click
|
|
* handler, camera events with Cesium's flight bookkeeping (`flights` counts
|
|
* them, lists them and can `land` the current one), primitive lists and a
|
|
* credit display. With `view`, the camera looks straight down on Sacramento
|
|
* from 900 m, so coverage asks for tiles.
|
|
*/
|
|
export function fakeCesiumViewer({ view = false } = {}) {
|
|
const credits = [];
|
|
let flight = null;
|
|
const flights = {
|
|
started: 0,
|
|
cancelled: 0,
|
|
/** Every flight asked for, oldest first: `{ sphere, options }`. */
|
|
list: [],
|
|
land() {
|
|
const current = flight;
|
|
flight = null;
|
|
current?.options.complete?.();
|
|
},
|
|
};
|
|
const stopFlight = () => {
|
|
const current = flight;
|
|
flight = null;
|
|
current?.options.cancel?.();
|
|
return Boolean(current);
|
|
};
|
|
const canvas = Object.assign(new EventTarget(), {
|
|
style: {},
|
|
// Keep Cesium's handler on the canvas; there is no real document here.
|
|
disableRootEvents: true,
|
|
onwheel: null,
|
|
...(view ? { clientWidth: 100, clientHeight: 100 } : {}),
|
|
});
|
|
const camera = Object.assign(
|
|
view
|
|
? rayCamera({
|
|
lon: -121.4944,
|
|
lat: 38.5816,
|
|
altitude: 900,
|
|
pitch: -90,
|
|
width: 100,
|
|
height: 100,
|
|
})
|
|
: {},
|
|
{
|
|
changed: new Cesium.Event(),
|
|
moveStart: new Cesium.Event(),
|
|
moveEnd: new Cesium.Event(),
|
|
flyToBoundingSphere(sphere, options = {}) {
|
|
stopFlight();
|
|
flight = { sphere, options };
|
|
flights.list.push(flight);
|
|
flights.started++;
|
|
},
|
|
cancelFlight() {
|
|
if (stopFlight()) flights.cancelled++;
|
|
},
|
|
},
|
|
);
|
|
return {
|
|
credits,
|
|
flights,
|
|
scene: {
|
|
canvas,
|
|
primitives: { add: (p) => p, remove() {} },
|
|
groundPrimitives: { add: (p) => p, remove() {} },
|
|
// Enough of a scene for the position marker to clamp to the ground.
|
|
frameState: { mode: Cesium.SceneMode.SCENE3D },
|
|
updateHeight: () => () => {},
|
|
getHeight: () => undefined,
|
|
},
|
|
camera,
|
|
creditDisplay: {
|
|
addStaticCredit: (credit) => credits.push(credit),
|
|
removeStaticCredit: (credit) =>
|
|
credits.splice(credits.indexOf(credit), 1),
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
};
|
|
}
|