Tighten coverage prompt comments and docs

This commit is contained in:
German Khokhlov committed 2026-11-06 20:20:11 +04:00
1 parent 3a78f40024
commit 5c4918affb
12 files changed
+31 -54

No files matched your search

@@ -1,6 +1,6 @@
- Say why Transit and Bike Share are empty outside coverage. Over a city with - Say why Transit and Bike Share are empty outside coverage. Over a city with
no feed or system (London, Tokyo) their Layers rows read no feed or system (London, Tokyo) their Layers rows read
`No feed here yet · 16 regions available` and `No feed here yet · 17 regions available` and
`No bike share here yet · 61 systems available`, or how low to fly, instead `No bike share here yet · 61 systems available`, or how low to fly, instead
of `never`; leaving coverage clears the last-update time (German, @gekh, of `never`; leaving coverage clears the last-update time (German, @gekh,
#1576). #1576).
+10 -12
View File
@@ -1975,18 +1975,16 @@ Mapped-installation zoom guidance appears in the layer row without counting as
a global loading failure. Guidance statuses do not suppress independent refresh a global loading failure. Guidance statuses do not suppress independent refresh
errors. errors.
Transit and Bike Share report the same kind of guidance when no feed or system Transit and Bike Share say the same when no feed or system covers the view,
covers the view, instead of an undated `never`. Above their altitude gates the instead of an undated `never`. Above their altitude gates the rows read
rows read `GTFS-RT · Fly below 2,850 km to a covered region` and `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 `GBFS · Fly below 48 km to a covered city`; lower, over a city neither covers
covers (London, Tokyo) they read `GTFS-RT · No feed here yet · N regions (London, Tokyo), `GTFS-RT · No feed here yet · N regions available` and
available` and `GBFS · No bike share here yet · N systems available`, N being `GBFS · No bike share here yet · N systems available` (covered regions,
the enabled feeds and the catalogued systems. The catalogued systems). The chip stays ON. A loading or failed Bike Share city
chip stays ON. A Bike Share city that is loading or failed keeps its own shows its own text. Leaving coverage clears the last-update time. The voice
loading or error text. Leaving every covered feed or system clears the layer's analyst gets the same English notes; both translate them through
last-update time, so the row never dates data it no longer draws. The same `translateStatusMessage` (`src/ui/statusText.js`).
English notes are the voice analyst's empty-result notes; both surfaces show
them translated through `translateStatusMessage` (`src/ui/statusText.js`).
## Places and CCTV request bounds ## Places and CCTV request bounds
+2 -3
View File
@@ -2393,9 +2393,8 @@ export class LayerLifecycle {
} }
/** /**
* A tick's failure belongs to the feed it polled. Once the layer's own stats * Drop a tick failure the layer's stats no longer report (the camera left
* stop reporting one (the camera left that feed), the row stops showing it * that feed) instead of showing it until the next tick.
* instead of waiting for the next tick.
*/ */
_dropRecoveredRefreshError(layerId) { _dropRecoveredRefreshError(layerId) {
const entry = layerId ? this.layers.get(layerId) : null; const entry = layerId ? this.layers.get(layerId) : null;
+5 -8
View File
@@ -15,8 +15,7 @@ export function createControls({ state: layerState, services, parts, source }) {
updateInterval: STATUS_POLL_MS, updateInterval: STATUS_POLL_MS,
/** /**
* Keep the manager so camera-driven coverage changes can repaint the row * Kept so camera moves can repaint the row before the next status poll.
* before the next status poll.
* @param {object} dataManager DataLayerManager instance. * @param {object} dataManager DataLayerManager instance.
*/ */
attachDataManager(dataManager) { attachDataManager(dataManager) {
@@ -84,18 +83,16 @@ export function createControls({ state: layerState, services, parts, source }) {
lastUpdate: layerState._lastUpdate, lastUpdate: layerState._lastUpdate,
loading, loading,
}; };
// No system in range is guidance, not an empty feed: the camera is too // No system in range: the camera is too high or no system is near. A
// high, or no catalogued system is near the view. A city that failed // failed city also leaves range empty and reports its failure below.
// leaves range empty too, and reports its failure below instead.
if ( if (
layerState._enabled && layerState._enabled &&
!loading && !loading &&
layerState._activeCityIds.size === 0 && layerState._activeCityIds.size === 0 &&
layerState._failedCityIds.size === 0 layerState._failedCityIds.size === 0
) { ) {
// English data (the analyst's empty note to the model, and the Layers // English: the analyst's empty note and the row's prompt, both shown
// row's prompt); both show it through translateStatusMessage // through translateStatusMessage (ui/statusText.js).
// (ui/statusText.js).
const note = layerState._altitudeGateEnabled const note = layerState._altitudeGateEnabled
? `No bike share here yet · ${GBFS_CITY_REGISTRY.length} systems available` ? `No bike share here yet · ${GBFS_CITY_REGISTRY.length} systems available`
: `Fly below ${Math.round(ACTIVATION_ENTER_ALTITUDE_M / 1000).toLocaleString('en-US')} km to a covered city`; : `Fly below ${Math.round(ACTIVATION_ENTER_ALTITUDE_M / 1000).toLocaleString('en-US')} km to a covered city`;
+2 -5
View File
@@ -1,10 +1,7 @@
// BIKESHARE COVERAGE STATUS — a view with no system says so. // BIKESHARE COVERAGE STATUS — a view with no system says so.
// //
// Over a city with no catalogued system (London, Tokyo) the layer drew nothing // With no system in range (London, Tokyo) the layer says why instead of
// and reported nothing, so its row read "ON · — · GBFS · never": the same as a // "never", and keeps no date for data it no longer draws.
// feed that had never answered. These cases pin the rule that replaced it:
// with no system in range the layer reports a guidance status that says why,
// and a view it left keeps no date for data it no longer draws.
// //
// Run with: npm test // Run with: npm test
import { test } from 'node:test'; import { test } from 'node:test';
-2
View File
@@ -1,5 +1,3 @@
// Bike Share row repaints: entering a city, leaving it or nudging the camera
// mid-load, and leaving a failed city.
// Bike Share: after the camera settles, the last row the panel painted must // Bike Share: after the camera settles, the last row the panel painted must
// match what the layer reports now. Each case drives the real viewport, // match what the layer reports now. Each case drives the real viewport,
// ingestion, rendering and controls; the manager is a spy that records the // ingestion, rendering and controls; the manager is a spy that records the
+1 -4
View File
@@ -116,10 +116,7 @@ export function createViewport({ state: layerState, services, parts, source }) {
parts.rendering.removeCityPoints(cityId); parts.rendering.removeCityPoints(cityId);
} }
/** /** Repaint the row now: it follows the camera, the manager tick is 60 s. */
* Repaint the layer's row now. Its text follows the camera (coverage
* prompt, station count), and the manager tick is 60 s.
*/
function refreshRow() { function refreshRow() {
layerState._dataManager?.refreshLayerStats?.('bikeshare'); layerState._dataManager?.refreshLayerStats?.('bikeshare');
} }
+2 -4
View File
@@ -1,7 +1,5 @@
// When the camera changes which coverage prompt a Transit or // A camera move that changes a Transit or Bike Share coverage prompt repaints
// Bike Share row shows, the layer asks the panel to repaint at once, instead // the row at once, not at the next manager tick (15 s, 60 s).
// 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 { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createState as createBikeState } from './bikeshare/state.js'; import { createState as createBikeState } from './bikeshare/state.js';
+2 -4
View File
@@ -1,7 +1,5 @@
// The Transit coverage prompt names the altitude the closed gate // The Transit prompt names the altitude the gate opens at: at 2,900 km over
// actually opens at. Descending over Boston to 2,900 km (below 3,000 km, above // Boston (under 3,000 km, above the 2,850 km enter threshold) nothing loads.
// 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 { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createState } from './state.js'; import { createState } from './state.js';
+2 -3
View File
@@ -245,9 +245,8 @@ export function createQueries({ state, parts }) {
const health = aggregateTransitFeedHealth(statuses, now); const health = aggregateTransitFeedHealth(statuses, now);
const count = state._vehicles.size; const count = state._vehicles.size;
if (state._enabled && active.length === 0) { if (state._enabled && active.length === 0) {
// English data (the analyst's empty note to the model, and the Layers // English: the analyst's empty note and the row's prompt, both shown
// row's prompt); both show it through translateStatusMessage // through translateStatusMessage (ui/statusText.js).
// (ui/statusText.js).
const note = state._altitudeGateOpen const note = state._altitudeGateOpen
? `No feed here yet · ${TRANSIT_REGION_COUNT} regions available` ? `No feed here yet · ${TRANSIT_REGION_COUNT} regions available`
: `Fly below ${Math.round(ACTIVATION_ENTER_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`;
+2 -4
View File
@@ -111,8 +111,7 @@ export function createViewport({ state, services, parts }) {
changed = true; changed = true;
} }
} }
// Leaving coverage leaves nothing on the map to date: a row that read // Nothing is left on the map to date.
// "26s ago" over an empty view would claim a feed that is no longer here.
if (desired.size === 0) state._lastUpdate = null; if (desired.size === 0) state._lastUpdate = null;
for (const [feedId, feed] of desired) { for (const [feedId, feed] of desired) {
if (!state._activeFeeds.has(feedId)) { if (!state._activeFeeds.has(feedId)) {
@@ -124,8 +123,7 @@ export function createViewport({ state, services, parts }) {
parts.ingestion.sweepAgedVehicles(Date.now()); parts.ingestion.sweepAgedVehicles(Date.now());
parts.rendering.syncRenderHold(); parts.rendering.syncRenderHold();
governorRequestRender('transit-proximity'); governorRequestRender('transit-proximity');
// The row's text follows the camera (coverage prompt, feed list), so it // Repaint now: the row follows the camera, the manager tick is 15 s.
// repaints now instead of at the next 15 s manager tick.
if (changed) state._dataManager?.refreshLayerStats?.('transit'); if (changed) state._dataManager?.refreshLayerStats?.('transit');
} }
+2 -4
View File
@@ -269,10 +269,8 @@ export function translateStatusLine(text) {
/** Thousands separators out of an English number ("3,000" → 3000). */ /** Thousands separators out of an English number ("3,000" → 3000). */
const englishNumber = (text) => Number(text.replace(/,/g, '')); const englishNumber = (text) => Number(text.replace(/,/g, ''));
// Coverage notes of a layer whose view has no feed, whole lines with a number // Coverage notes (transit/queries.js, bikeshare/controls.js). Their " · " is
// in them: transit (transit/queries.js) and bikeshare (bikeshare/controls.js). // inside one sentence, so they match whole before the line is split.
// Their " · " is part of one sentence, so they are matched before the line is
// split into pieces. Keep these in step with the producers.
const COVERAGE_NOTE_PATTERNS = [ const COVERAGE_NOTE_PATTERNS = [
[ [
/^Fly below ([\d,]+) km to a covered region$/, /^Fly below ([\d,]+) km to a covered region$/,