mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge remote-tracking branch 'origin/main' into HEAD
This commit is contained in:
commit
3748e9c23a
85 files changed
+3409
-195
No files matched your search
@@ -97,12 +97,23 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
|
||||
/**
|
||||
* Coerces a value to a finite number or returns the fallback.
|
||||
*
|
||||
* Absent, blank and boolean values return the fallback as well. The source
|
||||
* list serializes an unknown number (NaN) as JSON null, and `Number(null)`
|
||||
* is 0, so without this an unknown pose field arrived as zero and was
|
||||
* clamped to the field's floor instead of taking its default.
|
||||
* @param {*} value
|
||||
* @param {number} [fallback=NaN]
|
||||
* @returns {number}
|
||||
*/
|
||||
|
||||
function safeNumber(value, fallback = NaN) {
|
||||
if (
|
||||
value == null ||
|
||||
typeof value === 'boolean' ||
|
||||
(typeof value === 'string' && !value.trim())
|
||||
)
|
||||
return fallback;
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@ import {
|
||||
FRAME_ENDPOINT,
|
||||
MEDIA_ENDPOINT,
|
||||
} from './sourcePolicy.js';
|
||||
const FRAME_SOURCE_KINDS = new Set([
|
||||
'upstream-image',
|
||||
'hls-pull',
|
||||
'streetview',
|
||||
'synthetic',
|
||||
]);
|
||||
function safeNumber(value, fallback = NaN) {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
@@ -51,7 +57,7 @@ export function createCctvSource({
|
||||
},
|
||||
getFrameUrl: frameUrlFor,
|
||||
getMediaUrl: mediaUrlFor,
|
||||
/** Read one current frame through the registered frame endpoint. */
|
||||
/** Read a registered frame and its provenance, including fallback frames. */
|
||||
async getFrame(camera, { signal } = {}) {
|
||||
signal?.throwIfAborted();
|
||||
const response = await fetchImpl(frameUrlFor(camera), {
|
||||
@@ -62,7 +68,12 @@ export function createCctvSource({
|
||||
const bytes = new Uint8Array(await response.arrayBuffer());
|
||||
signal?.throwIfAborted();
|
||||
const type = response.headers.get('content-type') || '';
|
||||
return { contentType: type.split(';')[0].trim().toLowerCase(), bytes };
|
||||
const sourceKind = response.headers.get('x-cctv-source');
|
||||
return {
|
||||
contentType: type.split(';')[0].trim().toLowerCase(),
|
||||
bytes,
|
||||
sourceKind: FRAME_SOURCE_KINDS.has(sourceKind) ? sourceKind : null,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -173,3 +173,26 @@ test('frames are read through the registered frame endpoint', async () => {
|
||||
});
|
||||
await assert.rejects(failing.getFrame(camera), /Camera frame HTTP 502/);
|
||||
});
|
||||
|
||||
test('frame provenance admits the proxy kinds and leaves missing or unknown headers unknown', async () => {
|
||||
for (const kind of [
|
||||
'upstream-image',
|
||||
'hls-pull',
|
||||
'streetview',
|
||||
'synthetic',
|
||||
'other',
|
||||
null,
|
||||
]) {
|
||||
const source = createCctvSource({
|
||||
fetchImpl: async () =>
|
||||
new Response(new Uint8Array([1]), {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
...(kind ? { 'X-CCTV-Source': kind } : {}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
const frame = await source.getFrame(camera);
|
||||
assert.equal(frame.sourceKind, kind === 'other' ? null : kind);
|
||||
}
|
||||
});
|
||||
@@ -114,7 +114,7 @@ export function createQueries({
|
||||
* Build a public descriptor for one aircraft using its best current position
|
||||
* (dead-reckoned when history exists, billboard position otherwise).
|
||||
* @param {string} icao24 - ICAO 24-bit transponder address.
|
||||
* @returns {{icao24: string, callsign: string|null, position: Cesium.Cartesian3, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null}|null}
|
||||
* @returns {{icao24: string, callsign: string|null, position: Cesium.Cartesian3, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null, verticalRateMps: number|null}|null}
|
||||
* Descriptor with a cloned position, or null if the aircraft is unknown.
|
||||
*/
|
||||
|
||||
@@ -164,6 +164,15 @@ export function createQueries({
|
||||
onGround: info?.onGround === true,
|
||||
velocityMps: displayed.speedMps,
|
||||
track: displayed.trackDeg,
|
||||
// Climb/descent rate, the one kinematic field `mapAnalystRecord` already
|
||||
// published (as `verticalRateMps`) but this descriptor did not, so a
|
||||
// consumer reading the tracked seam had to reach back into `_flightData`
|
||||
// for it. Reported, never derived: `stickyNumber` in records.js keeps the
|
||||
// last value across a blank poll, and an absent rate stays null rather
|
||||
// than becoming a level-flight 0 the aircraft never transmitted.
|
||||
verticalRateMps: Number.isFinite(info?.verticalRate)
|
||||
? info.verticalRate
|
||||
: null,
|
||||
stale: Boolean(
|
||||
flightState.records.missingPolls.get(icao24) ||
|
||||
flightState.feed._backoff,
|
||||
@@ -856,7 +865,7 @@ export function createQueries({
|
||||
|
||||
/**
|
||||
* Describe the currently tracked aircraft at its dead-reckoned position.
|
||||
* @returns {{icao24: string, callsign: string|null, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null}|null}
|
||||
* @returns {{icao24: string, callsign: string|null, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null, verticalRateMps: number|null}|null}
|
||||
* Tracked aircraft info, or null when nothing is tracked.
|
||||
*/
|
||||
getTrackedInfo() {
|
||||
|
||||
@@ -13,9 +13,4 @@ export const MISSING_POLL_LIMIT = 3;
|
||||
export const ERROR_BACKOFF_INTERVAL = 20000;
|
||||
|
||||
/** Local equirectangular distance used for viewer-proximate floor sampling. */
|
||||
export function approxDistanceKm(lat1, lon1, lat2, lon2) {
|
||||
const dLat = (lat2 - lat1) * 111.32;
|
||||
const dLon =
|
||||
(lon2 - lon1) * 111.32 * Math.cos((((lat1 + lat2) / 2) * Math.PI) / 180);
|
||||
return Math.hypot(dLat, dLon);
|
||||
}
|
||||
export { approxDistanceKm } from '../../data/approxDistance.js';
|
||||
@@ -0,0 +1,18 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
approxDistanceKm,
|
||||
GROUND_FLOOR_CLAMP_RADIUS_KM,
|
||||
} from './recordPolicy.js';
|
||||
|
||||
test('approximate distance takes the short way across the antimeridian', () => {
|
||||
// 0.2° of longitude at Taveuni, Fiji (16.8° S) is about 21 km.
|
||||
for (const [lon1, lon2] of [
|
||||
[179.9, -179.9],
|
||||
[-179.9, 179.9],
|
||||
]) {
|
||||
const km = approxDistanceKm(-16.8, lon1, -16.8, lon2);
|
||||
assert.ok(Math.abs(km - 21.31) < 0.01, `${lon1} → ${lon2}: ${km} km`);
|
||||
assert.ok(km <= GROUND_FLOOR_CLAMP_RADIUS_KM);
|
||||
}
|
||||
});
|
||||
@@ -134,15 +134,16 @@ export function createIngestion({
|
||||
let placesError = null;
|
||||
if (layerState.googleSearchRequested) {
|
||||
layerState.googleSearchRequested = false;
|
||||
// A view across 180° has west > east; its width runs through 180°.
|
||||
const lonSpan = box.east - box.west + (box.east < box.west ? 360 : 0);
|
||||
const latitude = (box.south + box.north) / 2;
|
||||
const longitude = (box.west + box.east) / 2;
|
||||
let longitude = box.west + lonSpan / 2;
|
||||
if (longitude > 180) longitude -= 360;
|
||||
const radiusM = Math.min(
|
||||
50000,
|
||||
Math.max(
|
||||
1000,
|
||||
Math.round(
|
||||
Math.max(box.north - box.south, box.east - box.west) * 55_000,
|
||||
),
|
||||
Math.round(Math.max(box.north - box.south, lonSpan) * 55_000),
|
||||
),
|
||||
);
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createIngestion } from './ingestion.js';
|
||||
|
||||
// Taveuni, Fiji: Cesium reports a view across 180° with west > east. Each view
|
||||
// is 0.4° wide and 0.2° tall; the last one does not cross 180°.
|
||||
for (const [west, east, centre] of [
|
||||
[179.7, -179.9, 179.9],
|
||||
[179.9, -179.7, -179.9],
|
||||
[179.5, 179.9, 179.7],
|
||||
])
|
||||
test(`a nearby search is centred on the view at ${centre}`, async () => {
|
||||
const box = { south: -17, west, north: -16.8, east };
|
||||
const searches = [];
|
||||
const state = {
|
||||
enabled: true,
|
||||
viewer: {},
|
||||
records: [],
|
||||
googleSearchRequested: true,
|
||||
};
|
||||
const ingestion = createIngestion({
|
||||
state,
|
||||
source: {
|
||||
getMappedSites: async () => ({ records: [], wide: true }),
|
||||
searchNearby: async (query) => {
|
||||
searches.push(query);
|
||||
return { places: [] };
|
||||
},
|
||||
},
|
||||
services: {
|
||||
render: { governorRequestRender() {} },
|
||||
ground: { resolveGroundFloorCellsBounded: async () => {} },
|
||||
},
|
||||
parts: {
|
||||
viewport: {
|
||||
loadArea: () => ({ box, coverage: {} }),
|
||||
clearUnavailableRetry() {},
|
||||
scheduleUnavailableRetry() {},
|
||||
},
|
||||
model: { installationWithinViewport: () => true },
|
||||
rendering: { renderRecords() {}, warmInstallationFloors() {} },
|
||||
},
|
||||
});
|
||||
await ingestion.loadInstallations();
|
||||
assert.equal(searches.length, 1);
|
||||
const { latitude, longitude, radiusM } = searches[0];
|
||||
assert.ok(Math.abs(latitude + 16.9) < 1e-9);
|
||||
assert.ok(Math.abs(longitude - centre) < 1e-9, `longitude ${longitude}`);
|
||||
// The width, not the height, sets the radius.
|
||||
assert.equal(radiusM, 22000);
|
||||
});
|
||||
@@ -88,7 +88,7 @@ export function createQueries({
|
||||
* Build a public descriptor for one aircraft using its best current position
|
||||
* (dead-reckoned when history exists, billboard position otherwise).
|
||||
* @param {string} icao24 - ICAO hex identifier of the aircraft.
|
||||
* @returns {{icao24: string, callsign: string|null, position: Cesium.Cartesian3, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null}|null}
|
||||
* @returns {{icao24: string, callsign: string|null, position: Cesium.Cartesian3, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null, verticalRateMps: number|null}|null}
|
||||
* Descriptor with a cloned position, or null if the aircraft is unknown.
|
||||
*/
|
||||
|
||||
@@ -131,6 +131,13 @@ export function createQueries({
|
||||
onGround: info?.onGround === true,
|
||||
velocityMps: displayed.speedMps,
|
||||
track: displayed.trackDeg,
|
||||
// Same field, same name, same null-when-absent rule as the civil
|
||||
// descriptor — the two layers store the record under different keys
|
||||
// (`verticalRateMps` here, `verticalRate` there), so only the mapping
|
||||
// makes `resolveTrackedAircraftInfo` hand back one shape for both.
|
||||
verticalRateMps: Number.isFinite(info?.verticalRateMps)
|
||||
? info.verticalRateMps
|
||||
: null,
|
||||
stale: Boolean(
|
||||
flightState.records.missingPolls.get(icao24) ||
|
||||
flightState.feed._backoff,
|
||||
@@ -777,7 +784,7 @@ export function createQueries({
|
||||
|
||||
/**
|
||||
* Describe the currently tracked aircraft at its dead-reckoned position.
|
||||
* @returns {{icao24: string, callsign: string|null, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null}|null}
|
||||
* @returns {{icao24: string, callsign: string|null, latitude: number, longitude: number, altitudeM: number, velocityMps: number|null, track: number|null, verticalRateMps: number|null}|null}
|
||||
* Tracked aircraft info, or null when nothing is tracked.
|
||||
*/
|
||||
getTrackedInfo() {
|
||||
|
||||
@@ -39,8 +39,6 @@ export class MilitaryFlightRecords {
|
||||
? prevMeta.altitudeFt * 0.3048
|
||||
: 0
|
||||
: 3048);
|
||||
const track = aircraft.courseDeg || 0;
|
||||
const speedMps = aircraft.speedMps ?? 0;
|
||||
const verticalRateMps = aircraft.verticalRateMps;
|
||||
const callsign = aircraft.callsign;
|
||||
const type = aircraft.typeCode;
|
||||
@@ -148,8 +146,15 @@ export class MilitaryFlightRecords {
|
||||
// altitudeFt/baro).
|
||||
geoAltitudeM,
|
||||
renderAltitudeM,
|
||||
speedMps: stickyNumber(speedMps, prevMeta?.speedMps, null),
|
||||
track: stickyNumber(track, prevMeta?.track, null),
|
||||
// A poll that omits track/speed holds the last values, like the civil
|
||||
// records; 0 only until one is known. A grounded row without a speed
|
||||
// reads 0, so a parked plane never coasts at its landing speed.
|
||||
speedMps: stickyNumber(
|
||||
aircraft.speedMps,
|
||||
onGround ? 0 : prevMeta?.speedMps,
|
||||
0,
|
||||
),
|
||||
track: stickyNumber(aircraft.courseDeg, prevMeta?.track, 0),
|
||||
// Analyst seam (additive): sticky like the other kinematics.
|
||||
verticalRateMps: stickyNumber(
|
||||
verticalRateMps,
|
||||
|
||||
@@ -114,3 +114,39 @@ test('military emergency status follows the latest poll instead of sticking', ()
|
||||
);
|
||||
assert.equal(next.meta.emergency, null);
|
||||
});
|
||||
|
||||
test('military records hold the last track and speed when a poll omits them', () => {
|
||||
const records = store();
|
||||
records.receive(observation({ courseDeg: 270, speedMps: 200 }), context());
|
||||
const missing = records.receive(
|
||||
observation({ courseDeg: null, speedMps: null }),
|
||||
context(),
|
||||
);
|
||||
assert.equal(missing.meta.track, 270);
|
||||
assert.equal(missing.meta.speedMps, 200);
|
||||
const north = records.receive(
|
||||
observation({ courseDeg: 0, speedMps: 0 }),
|
||||
context(),
|
||||
);
|
||||
assert.equal(north.meta.track, 0);
|
||||
assert.equal(north.meta.speedMps, 0);
|
||||
const unseen = store().receive(
|
||||
observation({ courseDeg: undefined, speedMps: undefined }),
|
||||
context(),
|
||||
);
|
||||
assert.equal(unseen.meta.track, 0);
|
||||
assert.equal(unseen.meta.speedMps, 0);
|
||||
const landing = store();
|
||||
landing.receive(observation({ courseDeg: 90, speedMps: 70 }), context());
|
||||
const parked = landing.receive(
|
||||
observation({
|
||||
onGround: true,
|
||||
baroAltitudeM: null,
|
||||
courseDeg: null,
|
||||
speedMps: null,
|
||||
}),
|
||||
context(),
|
||||
);
|
||||
assert.equal(parked.meta.speedMps, 0);
|
||||
assert.equal(parked.meta.track, 90);
|
||||
});
|
||||
@@ -11,20 +11,26 @@ export function containmentAccent(containedPct) {
|
||||
return '#8bc34a';
|
||||
}
|
||||
|
||||
/** Shoelace area (degree², sign dropped) — relative sizes only. */
|
||||
/** Shoelace area along short longitude edges (degree², relative sizes only). */
|
||||
function ringArea(ring) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < ring.length - 1; i++) {
|
||||
const [x1, y1] = ring[i];
|
||||
const [x2, y2] = ring[i + 1];
|
||||
let [x1, y1] = ring[0];
|
||||
for (let i = 1; i < ring.length; i++) {
|
||||
let delta = ring[i][0] - ring[i - 1][0];
|
||||
if (delta > 180) delta -= 360;
|
||||
else if (delta < -180) delta += 360;
|
||||
const x2 = x1 + delta;
|
||||
const y2 = ring[i][1];
|
||||
sum += x1 * y2 - x2 * y1;
|
||||
x1 = x2;
|
||||
y1 = y2;
|
||||
}
|
||||
return Math.abs(sum / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Anchor point for the incident card: the centroid of the largest polygon's
|
||||
* outer ring. Degrees, so callers own the Cartesian conversion.
|
||||
* Anchor point for the incident card: the vertex mean of the largest polygon's
|
||||
* outer ring, with circular longitude. Callers own the Cartesian conversion.
|
||||
* @param {Array<Array<Array<[number, number]>>>} polygons - Normalized rows' polygons.
|
||||
* @returns {{lon: number, lat: number}}
|
||||
*/
|
||||
@@ -41,14 +47,17 @@ export function perimeterAnchorDegrees(polygons) {
|
||||
}
|
||||
// Vertex mean over the closed ring (first == last point dropped): perimeters
|
||||
// are compact enough that this stays inside or near the polygon.
|
||||
let lon = 0;
|
||||
let sinLon = 0;
|
||||
let cosLon = 0;
|
||||
let lat = 0;
|
||||
const count = best.length - 1;
|
||||
const toRad = Math.PI / 180;
|
||||
for (let i = 0; i < count; i++) {
|
||||
lon += best[i][0];
|
||||
sinLon += Math.sin(best[i][0] * toRad);
|
||||
cosLon += Math.cos(best[i][0] * toRad);
|
||||
lat += best[i][1];
|
||||
}
|
||||
return { lon: lon / count, lat: lat / count };
|
||||
return { lon: Math.atan2(sinLon, cosLon) / toRad, lat: lat / count };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,12 +16,79 @@ const tiny = [
|
||||
[-107.01, 34.0],
|
||||
];
|
||||
|
||||
test('the card anchors at the centroid of the largest polygon', () => {
|
||||
test('the card anchors at the vertex mean of the largest polygon', () => {
|
||||
const anchor = perimeterAnchorDegrees([[tiny], [square]]);
|
||||
assert.ok(Math.abs(anchor.lon - -108.1) < 1e-9);
|
||||
assert.ok(Math.abs(anchor.lat - 35.1) < 1e-9);
|
||||
});
|
||||
|
||||
test('incident cards stay at the dateline for closed, reversed and rotated outlines', () => {
|
||||
const vertices = [
|
||||
[179, 10],
|
||||
[-179, 10],
|
||||
[-179, 12],
|
||||
[179, 12],
|
||||
];
|
||||
for (let start = 0; start < vertices.length; start++) {
|
||||
const rotated = [...vertices.slice(start), ...vertices.slice(0, start)];
|
||||
for (const points of [rotated, [...rotated].reverse()]) {
|
||||
const anchor = perimeterAnchorDegrees([[points.concat([points[0]])]]);
|
||||
assert.ok(Math.abs(Math.abs(anchor.lon) - 180) < 1e-9);
|
||||
assert.ok(Math.abs(anchor.lat - 11) < 1e-9);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('incident card longitude treats vertices at either sign of 180 as the same meridian', () => {
|
||||
for (const boundary of [-180, 180]) {
|
||||
const ring = [
|
||||
[179, 10],
|
||||
[boundary, 10],
|
||||
[-179, 10],
|
||||
[-179, 12],
|
||||
[179, 12],
|
||||
[179, 10],
|
||||
];
|
||||
const anchor = perimeterAnchorDegrees([[ring]]);
|
||||
assert.ok(Math.abs(Math.abs(anchor.lon) - 180) < 1e-9);
|
||||
assert.ok(Math.abs(anchor.lat - 10.8) < 1e-9);
|
||||
}
|
||||
});
|
||||
|
||||
test('multipart cards compare polygon areas along short dateline edges', () => {
|
||||
const ordinary = [
|
||||
[10, 20],
|
||||
[11, 20],
|
||||
[11, 21],
|
||||
[10, 21],
|
||||
[10, 20],
|
||||
];
|
||||
for (const width of [0.2, 2]) {
|
||||
const crossing = [
|
||||
[180 - width / 2, 10],
|
||||
[-180 + width / 2, 10],
|
||||
[-180 + width / 2, 10 + width],
|
||||
[180 - width / 2, 10 + width],
|
||||
[180 - width / 2, 10],
|
||||
];
|
||||
for (const ring of [crossing, [...crossing].reverse()]) {
|
||||
for (const polygons of [
|
||||
[[ring], [ordinary]],
|
||||
[[ordinary], [ring]],
|
||||
]) {
|
||||
const anchor = perimeterAnchorDegrees(polygons);
|
||||
if (width < 1) {
|
||||
assert.ok(Math.abs(anchor.lon - 10.5) < 1e-9);
|
||||
assert.ok(Math.abs(anchor.lat - 20.5) < 1e-9);
|
||||
} else {
|
||||
assert.ok(Math.abs(Math.abs(anchor.lon) - 180) < 1e-9);
|
||||
assert.ok(Math.abs(anchor.lat - 11) < 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('a full incident row renders name, size, containment, and ages', () => {
|
||||
const now = 1758000000000;
|
||||
const card = buildIncidentCard(
|
||||
|
||||
@@ -97,6 +97,8 @@ export function createGoogleViewer({ loader, getService, render } = {}) {
|
||||
service ||= getService
|
||||
? await getService()
|
||||
: new (await ensureLibrary()).StreetViewService();
|
||||
if (!current())
|
||||
throw new Error('Street View details lookup was released');
|
||||
return service.getPanorama({ pano: panoId });
|
||||
})().then(resolve, reject);
|
||||
});
|
||||
@@ -236,6 +238,7 @@ export function createGoogleViewer({ loader, getService, render } = {}) {
|
||||
const panoId = String(imageId);
|
||||
if (!host) throw new Error('Street View is not mounted');
|
||||
if (loader.authFailed()) throw new Error(KEY_REJECTED_MESSAGE);
|
||||
cancelShown?.();
|
||||
pendingOpen = panoId;
|
||||
const instance = await ensurePanorama(host);
|
||||
if (pendingOpen !== panoId) return;
|
||||
|
||||
@@ -403,3 +403,69 @@ test('a remount looks up again a panorama whose lookup hung', async () => {
|
||||
await settle();
|
||||
assert.equal(poses.length, count);
|
||||
});
|
||||
|
||||
test('superseded panorama waits cannot steal the current close cleanup', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||||
const { OPEN_TIMEOUT_MS } = await import('./policy.js');
|
||||
const flush = async () => {
|
||||
for (let i = 0; i < 10; i++) await Promise.resolve();
|
||||
};
|
||||
const { fake, loader, viewer, host } = setup();
|
||||
await viewer.mount(host);
|
||||
const [panorama] = fake.built;
|
||||
panorama.setPano = (id) => {
|
||||
panorama.pano = id;
|
||||
};
|
||||
const first = viewer.open('pano-a');
|
||||
await flush();
|
||||
const second = viewer.open('pano-b');
|
||||
await flush();
|
||||
panorama.fire('position_changed'); // new panorama has not reached OK
|
||||
await flush();
|
||||
viewer.close();
|
||||
const remaining = loader.listenerCount();
|
||||
t.mock.timers.tick(OPEN_TIMEOUT_MS);
|
||||
await Promise.allSettled([first, second]);
|
||||
assert.equal(
|
||||
remaining,
|
||||
0,
|
||||
'close disposes every pending open without waiting 20 seconds',
|
||||
);
|
||||
assert.equal(panorama.visible, false);
|
||||
});
|
||||
|
||||
test('unmount during details-service acquisition makes no later billed lookup', async () => {
|
||||
const fake = fakeStreetViewLibrary({ panoramas: PANORAMAS });
|
||||
const loader = fakeMapsLoader(fake.library);
|
||||
const { createGoogleProvider } = await import('./index.js');
|
||||
let release;
|
||||
const acquiring = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const importLibrary = loader.importLibrary.bind(loader);
|
||||
let imports = 0;
|
||||
loader.importLibrary = async (name) => {
|
||||
if (++imports > 1) await acquiring;
|
||||
return importLibrary(name);
|
||||
};
|
||||
const provider = createGoogleProvider({
|
||||
getApiKey: () => 'fixture-key',
|
||||
loader,
|
||||
});
|
||||
const instance = provider.create({
|
||||
notify() {},
|
||||
actions: { reportError() {} },
|
||||
});
|
||||
const viewer = instance.viewer;
|
||||
await viewer.mount(fakeHost());
|
||||
await viewer.open('pano-a');
|
||||
viewer.unmount();
|
||||
release();
|
||||
for (let i = 0; i < 10; i++) await Promise.resolve();
|
||||
assert.equal(
|
||||
fake.lookups.length,
|
||||
0,
|
||||
'teardown invalidates the request before calling getPanorama',
|
||||
);
|
||||
instance.destroy();
|
||||
});
|
||||
Reference in new issue
Block a user