mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
refactor(street-level): trim dead paths, duplicated labels and test-only API
- visibleBbox: no view-rectangle fallback or grid option; its one caller always ranges the rays - tilesForBbox returns the tiles; normalizeBbox takes arrays only - viewerHost: no try/catch around adapter calls that catch for themselves - coverage: counts come from the drawn sequences; no sequenceCount export - one keyStatusLabel for the pill and the layer list - no constructor source check, double-init guard or selectSequence / clearSequence on the layer Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
777aa62586
commit
d59b4d05bb
14 files changed
+73
-116
No files matched your search
@@ -660,6 +660,7 @@
|
||||
"src/ui/shellFacade.js",
|
||||
"src/ui/streetLevelControls.js",
|
||||
"src/ui/streetLevelPresentation.js",
|
||||
"src/layers/streetLevel/policy.js",
|
||||
"src/ui/localSdrControls.js",
|
||||
"src/layers/localAdsb/feedNames.js",
|
||||
"src/ui/localSdrPresentation.js",
|
||||
|
||||
@@ -5,14 +5,13 @@ import { createCredit } from './credits.js';
|
||||
import { createSelection } from './selection.js';
|
||||
import { createViewerHost } from './viewerHost.js';
|
||||
import { normalizeFilter, sameFilter } from './filter.js';
|
||||
import { STREET_LEVEL_LAYER_ID } from './policy.js';
|
||||
import { keyStatusLabel, STREET_LEVEL_LAYER_ID } from './policy.js';
|
||||
import { createCoverage } from './providers/mapillary/coverage.js';
|
||||
import { createSequences } from './providers/mapillary/sequences.js';
|
||||
import { createMapillaryViewer } from './providers/mapillary/viewer.js';
|
||||
import {
|
||||
MAPILLARY_CREDIT_HTML,
|
||||
MAPILLARY_KEY_ID,
|
||||
MAPILLARY_SOURCE_METHODS,
|
||||
} from './providers/mapillary/policy.js';
|
||||
|
||||
export { STREET_LEVEL_LAYER_ID } from './policy.js';
|
||||
@@ -39,12 +38,6 @@ export function createStreetLevelLayer({
|
||||
services = {},
|
||||
photoViewer = null,
|
||||
}) {
|
||||
if (
|
||||
!MAPILLARY_SOURCE_METHODS.every(
|
||||
(method) => typeof source?.[method] === 'function',
|
||||
)
|
||||
)
|
||||
throw new TypeError('A Mapillary source is required');
|
||||
const state = createState({ services });
|
||||
const parts = {};
|
||||
const context = { state, parts };
|
||||
@@ -169,10 +162,7 @@ export function createStreetLevelLayer({
|
||||
requiresKeyId: MAPILLARY_KEY_ID,
|
||||
|
||||
init(viewer) {
|
||||
if (state.initialized)
|
||||
throw new Error('Street Level layer is already initialized');
|
||||
state.viewer = viewer;
|
||||
state.initialized = true;
|
||||
parts.marker.ensure(viewer);
|
||||
parts.marker.setVisible(false);
|
||||
parts.sequences.ensureCollections(viewer);
|
||||
@@ -209,14 +199,13 @@ export function createStreetLevelLayer({
|
||||
parts.framing.attachNavigation(null);
|
||||
state.listeners.clear();
|
||||
state.viewer = null;
|
||||
state.initialized = false;
|
||||
},
|
||||
|
||||
getStats() {
|
||||
const coverage = coverageStats();
|
||||
const keyLabel = coverage.keyRejected ? 'KEY REJECTED' : 'KEY REQUIRED';
|
||||
const keyLabel = keyStatusLabel(coverage);
|
||||
let loadingLabel = '';
|
||||
if (coverage.keyRequired) loadingLabel = keyLabel;
|
||||
if (keyLabel) loadingLabel = keyLabel;
|
||||
else if (coverage.loading) loadingLabel = 'loading coverage...';
|
||||
else if (coverage.hint && state.enabled) loadingLabel = coverage.hint;
|
||||
return {
|
||||
@@ -224,10 +213,7 @@ export function createStreetLevelLayer({
|
||||
loading: coverage.loading,
|
||||
keyRequired: coverage.keyRequired,
|
||||
// A rejected key's message names the fix; a missing one says so.
|
||||
error:
|
||||
coverage.keyRequired && !coverage.keyRejected
|
||||
? keyLabel
|
||||
: coverage.error,
|
||||
error: keyLabel && !coverage.keyRejected ? keyLabel : coverage.error,
|
||||
loadingLabel,
|
||||
};
|
||||
},
|
||||
@@ -283,8 +269,6 @@ export function createStreetLevelLayer({
|
||||
},
|
||||
setViewerRenderMode: (mode) => parts.viewerHost.setRenderMode(mode),
|
||||
resizeViewer: () => parts.viewerHost.resize(),
|
||||
selectSequence: (sequenceId) => parts.sequences.select(sequenceId),
|
||||
clearSequence: () => parts.sequences.clearSelection(),
|
||||
};
|
||||
return layer;
|
||||
}
|
||||
@@ -205,23 +205,26 @@ test('switching Mapillary off clears coverage and closes the image it shows', as
|
||||
assert.equal(layer.getUIState().providerOn, false);
|
||||
});
|
||||
|
||||
test('a sequence error goes with the sequence and leaves the viewer’s own', async (t) => {
|
||||
test('a sequence that fails to load reports it apart from the photo, and the error goes with it', async (t) => {
|
||||
const source = fakeMapillarySource({
|
||||
getSequenceImages: async () => {
|
||||
throw new Error('Sequence images unavailable');
|
||||
},
|
||||
});
|
||||
const { layer } = await startLayer(t, { source });
|
||||
await layer.selectSequence('seq-1');
|
||||
layer.attachViewerHost(null);
|
||||
await layer.openImage('img1');
|
||||
// The photo's pose names its sequence, which opening selects.
|
||||
const photoViewer = fakePhotoViewer({
|
||||
pose: { position: { lon: -121.49, lat: 38.58 }, sequenceId: 'seq-1' },
|
||||
});
|
||||
const { layer } = await startLayer(t, { source, photoViewer });
|
||||
layer.attachViewerHost({});
|
||||
assert.equal(await layer.openImage('img1'), true);
|
||||
await settle();
|
||||
let ui = layer.getUIState();
|
||||
assert.equal(ui.sequence.error, 'Sequence images unavailable');
|
||||
assert.match(ui.street.error, /Open the Street Level panel/);
|
||||
layer.clearSequence();
|
||||
assert.equal(ui.street.error, null, 'the photo itself is fine');
|
||||
layer.closeViewer();
|
||||
ui = layer.getUIState();
|
||||
assert.equal(ui.sequence.error, null, 'withdrawn with the sequence');
|
||||
assert.match(ui.street.error, /Open the Street Level panel/, 'kept');
|
||||
});
|
||||
|
||||
test('closing the photo, switching the layer off or destroying it stops the framing flight', async (t) => {
|
||||
@@ -245,7 +248,6 @@ test('closing the photo, switching the layer off or destroying it stops the fram
|
||||
});
|
||||
|
||||
test('setParams takes "any date" (0 days) and the switch over the current values (share-link defaults)', async (t) => {
|
||||
assert.throws(() => createStreetLevelLayer({ source: {} }), /source/);
|
||||
const { layer } = await startLayer(t);
|
||||
assert.deepEqual(layer.getParams(), {
|
||||
mapillary: true,
|
||||
|
||||
@@ -18,3 +18,9 @@ export const MAX_SINCE_DAYS = 36_500;
|
||||
|
||||
/** Filter the layer starts with: all imagery, any date. */
|
||||
export const FILTER_DEFAULT = Object.freeze({ pano: 'all', sinceDays: 0 });
|
||||
|
||||
/** The key gate's label for the pill and the layer list, or null when the key is fine. */
|
||||
export function keyStatusLabel({ keyRequired, keyRejected }) {
|
||||
if (keyRejected) return 'KEY REJECTED';
|
||||
return keyRequired ? 'KEY REQUIRED' : null;
|
||||
}
|
||||
@@ -162,7 +162,6 @@ export function createCoverage({ state, source }) {
|
||||
const sequences = drawnSequences(entry);
|
||||
entry.sequences = new Map(sequences.map((s) => [s.id, s]));
|
||||
entry.primitives = buildSequencePrimitives(sequences);
|
||||
entry.count = sequences.length;
|
||||
for (const primitive of entry.primitives)
|
||||
scene.groundPrimitives.add(primitive);
|
||||
}
|
||||
@@ -212,7 +211,7 @@ export function createCoverage({ state, source }) {
|
||||
)
|
||||
return;
|
||||
const decoded = decodeCoverageTile(bytes, tile);
|
||||
const entry = { primitives: [], count: 0, sequences: new Map() };
|
||||
const entry = { primitives: [], sequences: new Map() };
|
||||
// Newest first; the per-tile cap applies to what passes the filter.
|
||||
entry.sequenceList = decoded.sequences.sort(
|
||||
(a, b) => (b.capturedAt || 0) - (a.capturedAt || 0),
|
||||
@@ -311,7 +310,7 @@ export function createCoverage({ state, source }) {
|
||||
if (!bbox) return { hint: NO_GROUND_HINT };
|
||||
// Ranked from between the camera's ground point and the screen centre's:
|
||||
// a tilted view's box centre can sit kilometres past both.
|
||||
const { tiles } = tilesForBbox(bbox, zoom, {
|
||||
const tiles = tilesForBbox(bbox, zoom, {
|
||||
limit: COVERAGE_MAX_TILES,
|
||||
from: viewFocus(viewer, ranged),
|
||||
});
|
||||
@@ -430,7 +429,6 @@ export function createCoverage({ state, source }) {
|
||||
setKeyStatus,
|
||||
rebuild,
|
||||
highlight,
|
||||
sequenceCount,
|
||||
stats,
|
||||
};
|
||||
}
|
||||
@@ -66,7 +66,6 @@ function fakeViewer(view) {
|
||||
view.lat + 0.002 - (point.y / 100) * 0.004,
|
||||
);
|
||||
},
|
||||
computeViewRectangle: () => undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -318,14 +317,18 @@ test('the per-tile cap applies after the imagery filter, so a dense tile keeps i
|
||||
await settle();
|
||||
const [entry] = state.coverage.tiles.values();
|
||||
const cap = Math.floor(COVERAGE_MAX_SEQUENCES / COVERAGE_MAX_TILES);
|
||||
assert.equal(entry.count, Math.min(300, cap), 'every 360° line is drawn');
|
||||
assert.equal(coverage.sequenceCount(), Math.min(300, cap));
|
||||
assert.equal(
|
||||
entry.sequences.size,
|
||||
Math.min(300, cap),
|
||||
'every 360° line is drawn',
|
||||
);
|
||||
assert.equal(coverage.stats().count, Math.min(300, cap));
|
||||
// Picking and the highlight look sequences up among the drawn lines.
|
||||
assert.ok(entry.sequences.has('pano-0'), 'drawn lines can be looked up');
|
||||
assert.equal(entry.sequences.has('flat-699'), false);
|
||||
state.filter = { pano: 'all', sinceDays: 0 };
|
||||
coverage.rebuild();
|
||||
assert.equal(entry.count, cap, 'all imagery is capped as before');
|
||||
assert.equal(entry.sequences.size, cap, 'all imagery is capped as before');
|
||||
assert.ok(entry.sequences.has('flat-699'), 'the newest flat line is drawn');
|
||||
assert.equal(entry.sequences.has('pano-299'), false, 'over the cap');
|
||||
coverage.clear();
|
||||
@@ -359,7 +362,7 @@ test('a sequence that crosses a tile edge is counted once', async () => {
|
||||
);
|
||||
}
|
||||
await settle();
|
||||
assert.equal(coverage.sequenceCount(), 3, 'across, plus one per tile');
|
||||
assert.equal(coverage.stats().count, 3, 'across, plus one per tile');
|
||||
coverage.clear();
|
||||
});
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ export function createState({ services }) {
|
||||
services,
|
||||
viewer: null,
|
||||
enabled: false,
|
||||
initialized: false,
|
||||
listeners: new Set(),
|
||||
notify: null,
|
||||
/** The share link's `mapillary` switch: off draws nothing. */
|
||||
|
||||
@@ -58,15 +58,10 @@ export function tileBounds(x, y, z) {
|
||||
};
|
||||
}
|
||||
|
||||
/** A [west, south, east, north] array or object as finite, ordered bounds; null if unusable. */
|
||||
/** A [west, south, east, north] array as finite, ordered bounds; null if unusable. */
|
||||
export function normalizeBbox(input) {
|
||||
const values = Array.isArray(input)
|
||||
? input
|
||||
: input && typeof input === 'object'
|
||||
? [input.west, input.south, input.east, input.north]
|
||||
: null;
|
||||
if (!values || values.length !== 4) return null;
|
||||
const [w, s, e, n] = values.map(Number);
|
||||
if (!Array.isArray(input) || input.length !== 4) return null;
|
||||
const [w, s, e, n] = input.map(Number);
|
||||
if (![w, s, e, n].every(Number.isFinite)) return null;
|
||||
const west = Math.max(-180, Math.min(w, e));
|
||||
const east = Math.min(180, Math.max(w, e));
|
||||
@@ -81,7 +76,7 @@ export function normalizeBbox(input) {
|
||||
* crosses the date line), nearest to `from` first (default: the box centre),
|
||||
* capped at `limit`. Distances are measured the short way round ±180°.
|
||||
* @param {{limit?: number, from?: {lon: number, lat: number}|null}} [options]
|
||||
* @returns {{tiles: Array<{x:number,y:number,z:number}>, truncated: boolean, total: number}}
|
||||
* @returns {Array<{x:number,y:number,z:number}>}
|
||||
*/
|
||||
export function tilesForBbox(bbox, z, { limit = Infinity, from = null } = {}) {
|
||||
const [west, south, east, north] = Array.isArray(bbox)
|
||||
@@ -95,7 +90,7 @@ export function tilesForBbox(bbox, z, { limit = Infinity, from = null } = {}) {
|
||||
[-180, south, east, north],
|
||||
]
|
||||
: [bbox];
|
||||
const tiles = halves
|
||||
const grid = halves
|
||||
.map(normalizeBbox)
|
||||
.filter(Boolean)
|
||||
.flatMap((box) => tileGrid(box, z));
|
||||
@@ -107,7 +102,7 @@ export function tilesForBbox(bbox, z, { limit = Infinity, from = null } = {}) {
|
||||
const cx = tileXAt(wrapLon(centre.lon), z);
|
||||
const cy = tileYAt(centre.lat, z);
|
||||
// Each tile's distance is computed once; ties keep their row order.
|
||||
const ranked = tiles
|
||||
const ranked = grid
|
||||
.map((tile) => {
|
||||
const dx = Math.abs(tile.x + 0.5 - cx);
|
||||
const d = Math.min(dx, n - dx) ** 2 + (tile.y + 0.5 - cy) ** 2;
|
||||
@@ -115,12 +110,7 @@ export function tilesForBbox(bbox, z, { limit = Infinity, from = null } = {}) {
|
||||
})
|
||||
.sort((a, b) => a.d - b.d)
|
||||
.map(({ tile }) => tile);
|
||||
const truncated = ranked.length > limit;
|
||||
return {
|
||||
tiles: truncated ? ranked.slice(0, limit) : ranked,
|
||||
truncated,
|
||||
total: ranked.length,
|
||||
};
|
||||
return ranked.slice(0, limit);
|
||||
}
|
||||
|
||||
/** The tiles at zoom z in a normalized box, row by row. */
|
||||
|
||||
@@ -37,24 +37,21 @@ test('normalizeBbox orders and clamps coordinates', () => {
|
||||
north: 38.69,
|
||||
});
|
||||
assert.equal(normalizeBbox([0, 0, 0, 1]), null);
|
||||
assert.equal(normalizeBbox('nope'), null);
|
||||
assert.equal(normalizeBbox([1, 2, 3]), null);
|
||||
assert.equal(normalizeBbox([1, 2, Number.NaN, 3]), null);
|
||||
});
|
||||
|
||||
test('tilesForBbox orders from the centre outwards and honours the cap', () => {
|
||||
const result = tilesForBbox([-121.56, 38.44, -121.36, 38.69], 14, {
|
||||
const tiles = tilesForBbox([-121.56, 38.44, -121.36, 38.69], 14, {
|
||||
limit: 5,
|
||||
});
|
||||
assert.equal(result.total, 160);
|
||||
assert.equal(result.truncated, true);
|
||||
assert.equal(result.tiles.length, 5);
|
||||
assert.equal(tiles.length, 5);
|
||||
const centreX = lonToTileX(-121.46, 14);
|
||||
const centreY = latToTileY(38.565, 14);
|
||||
assert.ok(Math.abs(result.tiles[0].x - centreX) <= 1);
|
||||
assert.ok(Math.abs(result.tiles[0].y - centreY) <= 1);
|
||||
assert.ok(Math.abs(tiles[0].x - centreX) <= 1);
|
||||
assert.ok(Math.abs(tiles[0].y - centreY) <= 1);
|
||||
const full = tilesForBbox([-121.56, 38.44, -121.36, 38.69], 14);
|
||||
assert.equal(full.truncated, false);
|
||||
assert.equal(full.tiles.length, 160);
|
||||
assert.equal(full.length, 160);
|
||||
});
|
||||
|
||||
test('coverageZoomForHeight steps from coarse to z14 as the camera descends', () => {
|
||||
@@ -71,13 +68,13 @@ test('longitude 180 is the last column, so a world view keeps the eastern hemisp
|
||||
assert.equal(lonToTileX(179.99, 1), 1);
|
||||
assert.equal(lonToTileX(-180, 1), 0);
|
||||
const columns = new Set(
|
||||
tilesForBbox([-180, -85, 180, 85], 1).tiles.map((tile) => tile.x),
|
||||
tilesForBbox([-180, -85, 180, 85], 1).map((tile) => tile.x),
|
||||
);
|
||||
assert.deepEqual([...columns].sort(), [0, 1]);
|
||||
});
|
||||
|
||||
test('a box across the date line takes tiles on both sides of it, not the far side of the globe', () => {
|
||||
const { tiles } = tilesForBbox([179, -1, -179, 1], 8);
|
||||
const tiles = tilesForBbox([179, -1, -179, 1], 8);
|
||||
const columns = new Set(tiles.map((tile) => tile.x));
|
||||
assert.ok(columns.has(255), 'west of the line');
|
||||
assert.ok(columns.has(0), 'east of the line');
|
||||
@@ -86,7 +83,7 @@ test('a box across the date line takes tiles on both sides of it, not the far si
|
||||
'nothing from the middle of the map',
|
||||
);
|
||||
// The limit keeps the tiles nearest the line.
|
||||
const limited = tilesForBbox([170, -1, -170, 1], 8, { limit: 4 }).tiles;
|
||||
const limited = tilesForBbox([170, -1, -170, 1], 8, { limit: 4 });
|
||||
assert.ok(limited.every((tile) => tile.x <= 8 || tile.x >= 247));
|
||||
});
|
||||
|
||||
@@ -97,12 +94,12 @@ test('a box across the date line ranks from the line, or from the camera across
|
||||
const key = (t) => `${t.x}/${t.y}`;
|
||||
const [north, south] = [latToTileY(0.1, z), latToTileY(0, z)];
|
||||
const plain = tilesForBbox(bbox, z);
|
||||
assert.equal(plain.total, 10 * (south - north + 1), '5 columns either side');
|
||||
assert.equal(plain.length, 10 * (south - north + 1), '5 columns either side');
|
||||
// The box centre is the date line itself, not the middle of the map: the
|
||||
// columns touching it come first, then the ones beside them.
|
||||
for (const tile of plain.tiles.slice(0, 4))
|
||||
for (const tile of plain.slice(0, 4))
|
||||
assert.ok([n - 1, 0].includes(tile.x), `${key(tile)} at the line`);
|
||||
for (const tile of plain.tiles.slice(4, 12))
|
||||
for (const tile of plain.slice(4, 12))
|
||||
assert.ok(
|
||||
[n - 2, n - 1, 0, 1].includes(tile.x),
|
||||
`${key(tile)} near the line`,
|
||||
@@ -112,15 +109,9 @@ test('a box across the date line ranks from the line, or from the camera across
|
||||
const { south: rowSouth, north: rowNorth } = tileBounds(0, row, z);
|
||||
const lat = (rowSouth + rowNorth) / 2;
|
||||
const west = tilesForBbox(bbox, z, { from: { lon: 179.999, lat } });
|
||||
assert.deepEqual(west.tiles.slice(0, 2).map(key), [
|
||||
`${n - 1}/${row}`,
|
||||
`0/${row}`,
|
||||
]);
|
||||
assert.deepEqual(west.slice(0, 2).map(key), [`${n - 1}/${row}`, `0/${row}`]);
|
||||
const east = tilesForBbox(bbox, z, { from: { lon: -179.999, lat } });
|
||||
assert.deepEqual(east.tiles.slice(0, 2).map(key), [
|
||||
`0/${row}`,
|
||||
`${n - 1}/${row}`,
|
||||
]);
|
||||
assert.deepEqual(east.slice(0, 2).map(key), [`0/${row}`, `${n - 1}/${row}`]);
|
||||
});
|
||||
|
||||
test('ranking from the camera keeps the ground under it, not the box centre', () => {
|
||||
@@ -131,11 +122,10 @@ test('ranking from the camera keeps the ground under it, not the box centre', ()
|
||||
const z = 14;
|
||||
const key = (t) => `${t.x}/${t.y}`;
|
||||
const under = `${lonToTileX(from.lon, z)}/${latToTileY(from.lat, z)}`;
|
||||
const plain = tilesForBbox(bbox, z, { limit: 9 }).tiles.map(key);
|
||||
const plain = tilesForBbox(bbox, z, { limit: 9 }).map(key);
|
||||
assert.equal(plain.includes(under), false, 'the box centre drops it');
|
||||
const ranked = tilesForBbox(bbox, z, { limit: 9, from });
|
||||
assert.equal(key(ranked.tiles[0]), under, 'the tile under the camera first');
|
||||
assert.equal(ranked.total > 9, true);
|
||||
assert.equal(key(ranked[0]), under, 'the tile under the camera first');
|
||||
});
|
||||
|
||||
test('wrapLon wraps longitudes and their differences into [-180, 180]', () => {
|
||||
|
||||
@@ -77,17 +77,19 @@ function groundHit(camera, point, ellipsoid, maxRange) {
|
||||
return Cesium.Cartographic.fromCartesian(cartesian, ellipsoid) || null;
|
||||
}
|
||||
|
||||
/** Screen rays per side, less one, that `visibleBbox` casts. */
|
||||
const BBOX_GRID = 5;
|
||||
|
||||
/**
|
||||
* Visible [west, south, east, north] degrees, or null. Only screen rays that
|
||||
* hit count, so a horizon cannot inflate the box to the world.
|
||||
* Visible [west, south, east, north] degrees, or null when fewer than four
|
||||
* rays hit. Only hits count, so a horizon cannot inflate the box to the world.
|
||||
* `groundHeight` raises the ellipsoid to the ground (rays to the bare
|
||||
* ellipsoid under Denver land kilometres too far out) and `maxRange` (m)
|
||||
* drops near-horizon hits; with it, too few hits give null rather than
|
||||
* Cesium's view rectangle, which can span the horizon.
|
||||
* drops near-horizon hits.
|
||||
*/
|
||||
export function visibleBbox(
|
||||
viewer,
|
||||
{ grid = 5, groundHeight = null, maxRange = null } = {},
|
||||
{ groundHeight = null, maxRange = null } = {},
|
||||
) {
|
||||
const scene = viewer?.scene;
|
||||
const camera = viewer?.camera;
|
||||
@@ -101,10 +103,10 @@ export function visibleBbox(
|
||||
groundHeight,
|
||||
);
|
||||
const point = new Cesium.Cartesian2();
|
||||
for (let i = 0; i <= grid; i++) {
|
||||
for (let j = 0; j <= grid; j++) {
|
||||
point.x = (width * i) / grid;
|
||||
point.y = (height * j) / grid;
|
||||
for (let i = 0; i <= BBOX_GRID; i++) {
|
||||
for (let j = 0; j <= BBOX_GRID; j++) {
|
||||
point.x = (width * i) / BBOX_GRID;
|
||||
point.y = (height * j) / BBOX_GRID;
|
||||
const carto = groundHit(camera, point, ellipsoid, maxRange);
|
||||
if (carto) hits.push(carto);
|
||||
}
|
||||
@@ -142,15 +144,7 @@ export function visibleBbox(
|
||||
}
|
||||
if (west !== east && north - south > 0) return [west, south, east, north];
|
||||
}
|
||||
if (Number.isFinite(maxRange)) return null;
|
||||
const rectangle = camera.computeViewRectangle?.(scene.globe?.ellipsoid);
|
||||
if (!rectangle) return null;
|
||||
return [
|
||||
Cesium.Math.toDegrees(rectangle.west),
|
||||
Cesium.Math.toDegrees(rectangle.south),
|
||||
Cesium.Math.toDegrees(rectangle.east),
|
||||
Cesium.Math.toDegrees(rectangle.north),
|
||||
];
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The point on the ground under the camera, as {lon, lat} degrees. */
|
||||
|
||||
@@ -155,7 +155,7 @@ test('a tilted view from 3 km loads the tile at the centre of the screen', () =>
|
||||
const ahead = lat + 3000 / Math.tan((20 * Math.PI) / 180) / 110_540;
|
||||
const centreTile = `${lonToTileX(lon, 13)}/${latToTileY(ahead, 13)}`;
|
||||
const chosen = (from) =>
|
||||
tilesForBbox(bbox, 13, { limit: 9, from }).tiles.map(
|
||||
tilesForBbox(bbox, 13, { limit: 9, from }).map(
|
||||
(tile) => `${tile.x}/${tile.y}`,
|
||||
);
|
||||
assert.ok(
|
||||
|
||||
@@ -121,20 +121,12 @@ export function createViewerHost({ state, parts, adapter }) {
|
||||
}
|
||||
|
||||
function resize() {
|
||||
try {
|
||||
if (unsubscribe) adapter.resize();
|
||||
} catch {
|
||||
/* no-op */
|
||||
}
|
||||
if (unsubscribe) adapter.resize();
|
||||
}
|
||||
|
||||
function setRenderMode(mode) {
|
||||
state.street.renderMode = mode === 'fill' ? 'fill' : 'letterbox';
|
||||
try {
|
||||
if (unsubscribe) adapter.setRenderMode?.(state.street.renderMode);
|
||||
} catch {
|
||||
/* adapter not ready */
|
||||
}
|
||||
if (unsubscribe) adapter.setRenderMode?.(state.street.renderMode);
|
||||
notify();
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,5 @@ export function rayCamera({
|
||||
const hit = Cesium.IntersectionTests.rayEllipsoid(ray, ellipsoid);
|
||||
return hit ? Cesium.Ray.getPoint(ray, hit.start) : undefined;
|
||||
},
|
||||
computeViewRectangle: () => undefined,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { keySetupRequirement } from '../keySetupCore.mjs';
|
||||
import { keyStatusLabel } from '../layers/streetLevel/policy.js';
|
||||
|
||||
// Street Level UI state to panel strings and flags. Pure, so it is testable.
|
||||
|
||||
@@ -59,10 +60,8 @@ const drawing = (state) => state.enabled === true && state.providerOn !== false;
|
||||
function presentStatus(state) {
|
||||
const pressed = drawing(state);
|
||||
const title = pressed ? 'Turn Street Level off' : 'Turn Street Level on';
|
||||
if (state.keyRejected)
|
||||
return { text: 'KEY REJECTED', tone: 'warn', pressed, title };
|
||||
if (state.keyRequired)
|
||||
return { text: 'KEY REQUIRED', tone: 'warn', pressed, title };
|
||||
const key = keyStatusLabel(state);
|
||||
if (key) return { text: key, tone: 'warn', pressed, title };
|
||||
if (state.coverage.loading)
|
||||
return { text: 'LOADING', tone: 'busy', pressed, title };
|
||||
return pressed
|
||||
|
||||
Reference in new issue
Block a user