mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-08 04:14:02 +00:00
Name the real gate altitude and repaint coverage rows at once
The Transit prompt said "Fly below 3,000 km", but the closed gate opens at 2,850 km; it now names the enter threshold, as Bike Share does. When the camera changes coverage, Transit and Bike Share ask the Layers panel to repaint instead of leaving the old prompt up until the next 15 s / 60 s tick.
This commit is contained in:
1 parent
25c33893f6
commit
90db4eb2c9
13 files changed
+233
-7
No files matched your search
@@ -1819,7 +1819,7 @@ errors.
|
||||
|
||||
Transit and Bike Share report the same kind of guidance when no feed or system
|
||||
covers the view, instead of an undated `never`. Above their altitude gates the
|
||||
rows read `GTFS-RT · Fly below 3,000 km to a covered region` and
|
||||
rows read `GTFS-RT · Fly below 2,850 km to a covered region` and
|
||||
`GBFS · Fly below 48 km to a covered city`; lower down over a city neither
|
||||
covers (London, Tokyo) they read `GTFS-RT · No feed here yet · N regions
|
||||
available` and `GBFS · No bike share here yet · N systems available`, N being
|
||||
|
||||
Vendored
+9
@@ -14,6 +14,15 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
|
||||
updateInterval: STATUS_POLL_MS,
|
||||
|
||||
/**
|
||||
* Keep the manager so camera-driven coverage changes can repaint the row
|
||||
* before the next status poll.
|
||||
* @param {object} dataManager DataLayerManager instance.
|
||||
*/
|
||||
attachDataManager(dataManager) {
|
||||
layerState._dataManager = dataManager;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a sampled array of detectable station objects for HUD overlay rendering.
|
||||
* @param {Object} [options] - Sampling options (maxCount, seed).
|
||||
|
||||
@@ -169,6 +169,7 @@ export function createLifecycle({
|
||||
viewer.scene.primitives.remove(layerState._pointCollection);
|
||||
layerState._pointCollection = null;
|
||||
}
|
||||
layerState._dataManager = null;
|
||||
layerState._viewer = null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -19,6 +19,10 @@ export function createState({ services }) {
|
||||
|
||||
layerState._viewer = null;
|
||||
|
||||
/** @type {{ refreshLayerStats?: () => void }|null} Manager that repaints the layer's row. */
|
||||
|
||||
layerState._dataManager = null;
|
||||
|
||||
/** @type {Cesium.PointPrimitiveCollection|null} Primitive collection for station dots. */
|
||||
|
||||
layerState._pointCollection = null;
|
||||
|
||||
@@ -116,6 +116,14 @@ export function createViewport({ state: layerState, services, parts, source }) {
|
||||
parts.rendering.removeCityPoints(cityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Repaint the layer's row now. Its text follows the camera (coverage
|
||||
* prompt, station count), and the manager tick is 60 s.
|
||||
*/
|
||||
function refreshRow() {
|
||||
layerState._dataManager?.refreshLayerStats?.();
|
||||
}
|
||||
|
||||
/** Deactivate all currently active cities and clear the active set. */
|
||||
|
||||
function deactivateAllCities() {
|
||||
@@ -205,9 +213,11 @@ export function createViewport({ state: layerState, services, parts, source }) {
|
||||
|
||||
const generation = ++layerState._proximityGeneration;
|
||||
const altitude = getCameraAltitude(layerState._viewer);
|
||||
const wasGateEnabled = layerState._altitudeGateEnabled;
|
||||
// Altitude gate: disable all cities when camera is too high
|
||||
if (!shouldActivateAtAltitude(altitude)) {
|
||||
deactivateAllCities();
|
||||
if (wasGateEnabled) refreshRow();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -226,12 +236,18 @@ export function createViewport({ state: layerState, services, parts, source }) {
|
||||
if (!nextActive.has(cityId)) layerState._failedCityIds.delete(cityId);
|
||||
}
|
||||
|
||||
const changed =
|
||||
!wasGateEnabled ||
|
||||
nextActive.size !== layerState._activeCityIds.size ||
|
||||
[...nextActive].some((cityId) => !layerState._activeCityIds.has(cityId));
|
||||
layerState._activeCityIds = nextActive;
|
||||
if (layerState._activeCityIds.size === 0) {
|
||||
layerState._count = 0;
|
||||
layerState._lastUpdate = null;
|
||||
if (changed) refreshRow();
|
||||
return;
|
||||
}
|
||||
if (changed) refreshRow();
|
||||
|
||||
// Only activate cities that don't already have rendered points
|
||||
const toActivate = [];
|
||||
@@ -243,6 +259,7 @@ export function createViewport({ state: layerState, services, parts, source }) {
|
||||
await Promise.all(
|
||||
toActivate.map((cityId) => activateCity(cityId, generation)),
|
||||
);
|
||||
if (generation === layerState._proximityGeneration) refreshRow();
|
||||
}
|
||||
|
||||
/** Schedule a debounced proximity check after camera movement. */
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// When the camera changes which coverage prompt a Transit or
|
||||
// Bike Share row shows, the layer asks the panel to repaint at once, instead
|
||||
// of leaving the old prompt up until the manager's next tick (Transit 15 s,
|
||||
// Bike Share 60 s).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createState as createBikeState } from './bikeshare/state.js';
|
||||
import { createControls as createBikeControls } from './bikeshare/controls.js';
|
||||
import { createModel as createBikeModel } from './bikeshare/model.js';
|
||||
import { createViewport as createBikeViewport } from './bikeshare/viewport.js';
|
||||
import { createState as createTransitState } from './transit/state.js';
|
||||
import { createViewport as createTransitViewport } from './transit/viewport.js';
|
||||
import { createQueries as createTransitQueries } from './transit/queries.js';
|
||||
|
||||
const rad = Math.PI / 180;
|
||||
const LONDON = { lat: 51.5072, lon: -0.1276 };
|
||||
const BOSTON = { lat: 42.3601, lon: -71.0589 };
|
||||
|
||||
const services = () => ({
|
||||
overlays: {
|
||||
setOverlayEntries() {},
|
||||
setOverlaySourceVisible() {},
|
||||
clearOverlaySource() {},
|
||||
},
|
||||
render: { governorRequestRender() {} },
|
||||
});
|
||||
|
||||
function viewer(at, height) {
|
||||
return {
|
||||
camera: {
|
||||
positionCartographic: {
|
||||
height,
|
||||
latitude: at.lat * rad,
|
||||
longitude: at.lon * rad,
|
||||
},
|
||||
computeViewRectangle: () => undefined,
|
||||
},
|
||||
scene: { globe: {} },
|
||||
};
|
||||
}
|
||||
|
||||
function spyManager() {
|
||||
const dm = { repaints: 0 };
|
||||
dm.refreshLayerStats = () => {
|
||||
dm.repaints++;
|
||||
};
|
||||
return dm;
|
||||
}
|
||||
|
||||
test('Bike Share: flying from London into Boston repaints the prompt row', async () => {
|
||||
const svc = services();
|
||||
const state = createBikeState({ services: svc });
|
||||
const parts = {
|
||||
queries: { collectDetectableStations: () => [] },
|
||||
model: createBikeModel({ state, services: svc, parts: {}, source: null }),
|
||||
ingestion: {
|
||||
abortInFlight() {},
|
||||
loadCityStationInfo: async () => new Map(),
|
||||
loadCityStationStatus: async () => new Map(),
|
||||
},
|
||||
rendering: {
|
||||
removeCityPoints() {},
|
||||
ensureCityPoints() {},
|
||||
applyStatusToPoints() {},
|
||||
},
|
||||
};
|
||||
parts.viewport = createBikeViewport({
|
||||
state,
|
||||
services: svc,
|
||||
parts,
|
||||
source: null,
|
||||
});
|
||||
const { methods } = createBikeControls({
|
||||
state,
|
||||
services: svc,
|
||||
parts,
|
||||
source: {},
|
||||
});
|
||||
state._enabled = true;
|
||||
const dm = spyManager();
|
||||
methods.attachDataManager(dm);
|
||||
|
||||
state._viewer = viewer(LONDON, 8_000);
|
||||
await parts.viewport.runProximityCheck();
|
||||
assert.equal(methods.getStats().status, 'zoom-in');
|
||||
|
||||
const before = dm.repaints;
|
||||
state._viewer = viewer(BOSTON, 3_000);
|
||||
await parts.viewport.runProximityCheck();
|
||||
assert.equal(methods.getStats().status, undefined, 'Boston is covered');
|
||||
assert.ok(dm.repaints > before, 'the row was asked to repaint');
|
||||
});
|
||||
|
||||
test('Transit: leaving Boston for London repaints the row', () => {
|
||||
const svc = services();
|
||||
const state = createTransitState({ services: svc });
|
||||
const parts = {
|
||||
ingestion: {
|
||||
feedStatus: () => ({ count: 0 }),
|
||||
pollFeed() {},
|
||||
removeFeedVehicles() {},
|
||||
sweepAgedVehicles() {},
|
||||
},
|
||||
rendering: { requestVisibility() {}, syncRenderHold() {} },
|
||||
};
|
||||
parts.viewport = createTransitViewport({ state, services: svc, parts });
|
||||
const queries = createTransitQueries({ state, parts }).methods;
|
||||
state._enabled = true;
|
||||
const dm = spyManager();
|
||||
state._dataManager = dm;
|
||||
|
||||
state._viewer = viewer(BOSTON, 4_000);
|
||||
parts.viewport.runProximityCheck();
|
||||
assert.ok(state._activeFeeds.size > 0, 'Boston has a feed');
|
||||
state._lastUpdate = Date.now();
|
||||
|
||||
const before = dm.repaints;
|
||||
state._viewer = viewer(LONDON, 4_000);
|
||||
parts.viewport.runProximityCheck();
|
||||
assert.equal(queries.getStats().status, 'zoom-in');
|
||||
assert.ok(dm.repaints > before, 'the row was asked to repaint');
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
// The Transit coverage prompt names the altitude the closed gate
|
||||
// actually opens at. Descending over Boston to 2,900 km (below 3,000 km, above
|
||||
// the 2,850 km enter threshold) loads nothing, so a prompt "Fly below 3,000 km"
|
||||
// asks for something the camera has already done.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createState } from './state.js';
|
||||
import { createViewport } from './viewport.js';
|
||||
import { createQueries } from './queries.js';
|
||||
import { ACTIVATION_ENTER_ALTITUDE_M } from './policy.js';
|
||||
|
||||
const BOSTON = { lat: 42.3601, lon: -71.0589 };
|
||||
|
||||
function makeLayer() {
|
||||
const services = {
|
||||
overlays: {
|
||||
setOverlayEntries() {},
|
||||
setOverlaySourceVisible() {},
|
||||
clearOverlaySource() {},
|
||||
},
|
||||
render: { governorRequestRender() {} },
|
||||
};
|
||||
const state = createState({ services });
|
||||
const polled = [];
|
||||
const parts = {
|
||||
ingestion: {
|
||||
feedStatus: () => ({ count: 0 }),
|
||||
pollFeed: (feed) => polled.push(feed.id),
|
||||
removeFeedVehicles() {},
|
||||
sweepAgedVehicles() {},
|
||||
},
|
||||
rendering: { requestVisibility() {}, syncRenderHold() {} },
|
||||
};
|
||||
parts.viewport = createViewport({ state, services, parts });
|
||||
const queries = createQueries({ state, parts }).methods;
|
||||
state._enabled = true;
|
||||
const rad = Math.PI / 180;
|
||||
state._viewer = {
|
||||
camera: {
|
||||
positionCartographic: {
|
||||
height: 0,
|
||||
latitude: BOSTON.lat * rad,
|
||||
longitude: BOSTON.lon * rad,
|
||||
},
|
||||
computeViewRectangle: () => undefined,
|
||||
},
|
||||
scene: { globe: {} },
|
||||
};
|
||||
return { state, queries, viewport: parts.viewport, polled };
|
||||
}
|
||||
|
||||
test('below the prompted altitude but above the gate, the prompt names the gate', () => {
|
||||
const { state, queries, viewport, polled } = makeLayer();
|
||||
state._viewer.camera.positionCartographic.height = 2_900_000;
|
||||
viewport.runProximityCheck();
|
||||
|
||||
// Nothing loads at 2,900 km: the gate opens at the enter threshold.
|
||||
assert.equal(state._altitudeGateOpen, false);
|
||||
assert.deepEqual(polled, []);
|
||||
|
||||
const km = Math.round(ACTIVATION_ENTER_ALTITUDE_M / 1000).toLocaleString(
|
||||
'en-US',
|
||||
);
|
||||
const { statusMessage } = queries.getStats();
|
||||
assert.equal(statusMessage, `Fly below ${km} km to a covered region`);
|
||||
});
|
||||
@@ -4018,6 +4018,6 @@ test('a view with no feed reads its coverage prompt, not an undated feed', async
|
||||
app.advance(1_000);
|
||||
assert.equal(
|
||||
app.layer.getStats().statusMessage,
|
||||
'Fly below 3,000 km to a covered region',
|
||||
'Fly below 2,850 km to a covered region',
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
ACTIVATION_ALTITUDE_M,
|
||||
ACTIVATION_ENTER_ALTITUDE_M,
|
||||
FEED_STALE_AFTER_MS,
|
||||
aggregateTransitFeedHealth,
|
||||
transitDetectionClass,
|
||||
@@ -245,7 +245,7 @@ export function createQueries({ state, parts }) {
|
||||
// (ui/statusText.js).
|
||||
const note = state._altitudeGateOpen
|
||||
? `No feed here yet · ${TRANSIT_ENABLED_FEEDS.length} regions available`
|
||||
: `Fly below ${Math.round(ACTIVATION_ALTITUDE_M / 1000).toLocaleString('en-US')} km to a covered region`;
|
||||
: `Fly below ${Math.round(ACTIVATION_ENTER_ALTITUDE_M / 1000).toLocaleString('en-US')} km to a covered region`;
|
||||
return {
|
||||
count: 0,
|
||||
lastUpdate: state._lastUpdate,
|
||||
|
||||
@@ -83,7 +83,9 @@ export function createViewport({ state, services, parts }) {
|
||||
if (!state._enabled || !state._viewer) return;
|
||||
refreshViewBounds();
|
||||
const altitude = getCameraAltitude();
|
||||
const wasGateOpen = state._altitudeGateOpen;
|
||||
state._altitudeGateOpen = altitudeGateOpen(altitude);
|
||||
let changed = state._altitudeGateOpen !== wasGateOpen;
|
||||
const center = state._altitudeGateOpen ? getCameraCenterLatLon() : null;
|
||||
const desired = new Map();
|
||||
if (center) {
|
||||
@@ -106,6 +108,7 @@ export function createViewport({ state, services, parts }) {
|
||||
state._activeFeeds.delete(feedId);
|
||||
state._feedStatus.delete(feedId);
|
||||
parts.ingestion.removeFeedVehicles(feedId);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
// Leaving coverage leaves nothing on the map to date: a row that read
|
||||
@@ -115,11 +118,15 @@ export function createViewport({ state, services, parts }) {
|
||||
if (!state._activeFeeds.has(feedId)) {
|
||||
state._activeFeeds.set(feedId, feed);
|
||||
void parts.ingestion.pollFeed(feed, state._generation);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
parts.ingestion.sweepAgedVehicles(Date.now());
|
||||
parts.rendering.syncRenderHold();
|
||||
governorRequestRender('transit-proximity');
|
||||
// The row's text follows the camera (coverage prompt, feed list), so it
|
||||
// repaints now instead of at the next 15 s manager tick.
|
||||
if (changed) state._dataManager?.refreshLayerStats?.();
|
||||
}
|
||||
|
||||
function onCameraChanged() {
|
||||
|
||||
@@ -83,7 +83,7 @@ for (const locale of ['es', 'ru']) {
|
||||
// A layer's coverage note when no feed covers the view: one sentence with a
|
||||
// " · " in it (transit/queries.js, bikeshare/controls.js).
|
||||
const COVERAGE_NOTES = [
|
||||
'Fly below 3,000 km to a covered region',
|
||||
'Fly below 2,850 km to a covered region',
|
||||
'No feed here yet · 16 regions available',
|
||||
'Fly below 48 km to a covered city',
|
||||
'No bike share here yet · 61 systems available',
|
||||
|
||||
@@ -70,7 +70,7 @@ const cases = [
|
||||
|
||||
// Transit's coverage text when nothing is in view (transit/queries.js).
|
||||
const transitCoverage = [
|
||||
'transit: Fly below 3,000 km to a covered region',
|
||||
'transit: Fly below 2,850 km to a covered region',
|
||||
'transit: No feed here yet · 12 regions available',
|
||||
];
|
||||
|
||||
|
||||
@@ -423,7 +423,7 @@ function shownNote(text) {
|
||||
const shown = match ? show(match) : null;
|
||||
if (shown) return shown;
|
||||
}
|
||||
// A layer's empty note can be its coverage note ("Fly below 3,000 km to a
|
||||
// A layer's empty note can be its coverage note ("Fly below 2,850 km to a
|
||||
// covered region") or its status line, English pieces joined by " · "
|
||||
// ("feeds 1090, 978 stale · 0 heard", "TTC 0 (95s old)").
|
||||
return translateStatusMessage(text);
|
||||
|
||||
Reference in new issue
Block a user