mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
perf(cockpit): a Rail or Transit snapshot read again while nothing in it changed is the one read before
getSelectedTrain, getSelectedVehicle and getCockpitSubject fill drafts and freeze a new object only for a part whose values changed, so a view that reads them on every frame builds nothing while the train or vehicle stands still, and a moving one keeps its feed, stop and provenance objects. A train's stops are copied once per list the stops provider returns, each stop a frozen copy, instead of on every call.
This commit is contained in:
1 parent
a4f321a9ef
commit
294ccc0ee8
4 files changed
+299
-137
No files matched your search
@@ -2,12 +2,50 @@ import { pointAtS } from '../../data/railPaths.js';
|
|||||||
import { getRegisteredRailFeed } from '../../data/railFeeds.js';
|
import { getRegisteredRailFeed } from '../../data/railFeeds.js';
|
||||||
import { displayMotion } from '../transit/movement.js';
|
import { displayMotion } from '../transit/movement.js';
|
||||||
|
|
||||||
|
const NO_STOPS = Object.freeze([]);
|
||||||
|
/** Each stops list's frozen copy for a snapshot's `stops` (stopsOf). */
|
||||||
|
const stopLists = new WeakMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A trip's stops as a snapshot hands them out: a frozen array of frozen
|
||||||
|
* copies, made once per list the stops provider returns.
|
||||||
|
*/
|
||||||
|
function stopsOf(list) {
|
||||||
|
if (!Array.isArray(list)) return NO_STOPS;
|
||||||
|
let stops = stopLists.get(list);
|
||||||
|
if (!stops) {
|
||||||
|
stops = Object.freeze(
|
||||||
|
list.map((stop) =>
|
||||||
|
stop && typeof stop === 'object' ? Object.freeze({ ...stop }) : stop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
stopLists.set(list, stops);
|
||||||
|
}
|
||||||
|
return stops;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `previous` when it holds each of `next`'s values, else a frozen copy of
|
||||||
|
* `next`: a part of a snapshot that did not change is the one read before.
|
||||||
|
* `next` is a draft, filled again on every read.
|
||||||
|
*/
|
||||||
|
function reuse(previous, next) {
|
||||||
|
if (!previous) return Object.freeze({ ...next });
|
||||||
|
for (const name in next)
|
||||||
|
if (!Object.is(previous[name], next[name]))
|
||||||
|
return Object.freeze({ ...next });
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read access to one train, and a hold on it for a first-person view.
|
* Read access to one train, and a hold on it for a first-person view.
|
||||||
*
|
*
|
||||||
* `getSelectedTrain()` and `getCockpitSubject()` describe the selected train
|
* `getSelectedTrain()` and `getCockpitSubject()` describe the selected train
|
||||||
* and the held one as a frozen snapshot, read again on each call: where it is
|
* and the held one as a frozen snapshot, read again on each call: where it is
|
||||||
* along its line, how fast it moves and how that is known, and its stops.
|
* along its line, how fast it moves and how that is known, and its stops. A
|
||||||
|
* call while nothing in it changed returns the snapshot it returned before,
|
||||||
|
* and a part that did not change (its feed, its stops) is the same object in
|
||||||
|
* the next one, so a view can read it on every frame.
|
||||||
* `setCockpitSubject(key)` holds a train: its marker, card and selected line
|
* `setCockpitSubject(key)` holds a train: its marker, card and selected line
|
||||||
* are hidden, it stays selected (so its stops stay fresh and its feed stays
|
* are hidden, it stays selected (so its stops stay fresh and its feed stays
|
||||||
* polled at any distance) and clicks and Escape no longer change the
|
* polled at any distance) and clicks and Escape no longer change the
|
||||||
@@ -36,26 +74,37 @@ export function createRailCockpitSubject({
|
|||||||
const finiteOrNull = (value) =>
|
const finiteOrNull = (value) =>
|
||||||
typeof value === 'number' && Number.isFinite(value) ? value : null;
|
typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||||
const scratch = {};
|
const scratch = {};
|
||||||
|
/** Each train's last snapshot, returned again while nothing in it changed. */
|
||||||
|
const views = new WeakMap();
|
||||||
|
/** The drafts trainView fills on every read (reuse). */
|
||||||
|
const draft = {
|
||||||
|
view: {},
|
||||||
|
feed: {},
|
||||||
|
position: {},
|
||||||
|
path: {},
|
||||||
|
motion: {},
|
||||||
|
provenance: {},
|
||||||
|
};
|
||||||
|
|
||||||
/** The catalog's public entry for a feed, else the registry's. */
|
/** The catalog's public entry for a feed, else the registry's. */
|
||||||
function feedOf(feedId) {
|
function feedOf(feedId, previous) {
|
||||||
const listed = state._catalog?.find((feed) => feed.id === feedId);
|
const listed = state._catalog?.find((feed) => feed.id === feedId);
|
||||||
const feed = listed ?? getRegisteredRailFeed(feedId);
|
const feed = listed ?? getRegisteredRailFeed(feedId);
|
||||||
if (!feed) return null;
|
if (!feed) return null;
|
||||||
return Object.freeze({
|
const next = draft.feed;
|
||||||
id: feed.id,
|
next.id = feed.id;
|
||||||
name: feed.name ?? null,
|
next.name = feed.name ?? null;
|
||||||
operator: feed.operator ?? null,
|
next.operator = feed.operator ?? null;
|
||||||
region: feed.region ?? null,
|
next.region = feed.region ?? null;
|
||||||
positions: listed?.positions ?? feed.positions ?? 'gps',
|
next.positions = listed?.positions ?? feed.positions ?? 'gps';
|
||||||
tripUpdates:
|
next.tripUpdates =
|
||||||
typeof listed?.tripUpdates === 'boolean'
|
typeof listed?.tripUpdates === 'boolean'
|
||||||
? listed.tripUpdates
|
? listed.tripUpdates
|
||||||
: typeof feed.tripUpdatesUrl === 'string',
|
: typeof feed.tripUpdatesUrl === 'string';
|
||||||
attribution: feed.attribution ?? null,
|
next.attribution = feed.attribution ?? null;
|
||||||
license: feed.license ?? null,
|
next.license = feed.license ?? null;
|
||||||
licenseUrl: feed.licenseUrl ?? null,
|
next.licenseUrl = feed.licenseUrl ?? null;
|
||||||
});
|
return reuse(previous, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The route's short (else long) name in the feed's timetable, or null. */
|
/** The route's short (else long) name in the feed's timetable, or null. */
|
||||||
@@ -87,6 +136,7 @@ export function createRailCockpitSubject({
|
|||||||
/** The train as a frozen snapshot (see the module comment), or null. */
|
/** The train as a frozen snapshot (see the module comment), or null. */
|
||||||
function trainView(entry) {
|
function trainView(entry) {
|
||||||
if (!entry?.record) return null;
|
if (!entry?.record) return null;
|
||||||
|
const previous = views.get(entry);
|
||||||
const record = entry.record;
|
const record = entry.record;
|
||||||
const sample = entry.sample ?? null;
|
const sample = entry.sample ?? null;
|
||||||
const drawn = entry.drawnHeight ?? null;
|
const drawn = entry.drawnHeight ?? null;
|
||||||
@@ -95,64 +145,68 @@ export function createRailCockpitSubject({
|
|||||||
const derived = finiteOrNull(sample?.segmentSpeedMps);
|
const derived = finiteOrNull(sample?.segmentSpeedMps);
|
||||||
const trip = state._selectedKey === entry.key ? state._trip : null;
|
const trip = state._selectedKey === entry.key ? state._trip : null;
|
||||||
const status = state._feedStatus.get(entry.feedId);
|
const status = state._feedStatus.get(entry.feedId);
|
||||||
const latitude =
|
|
||||||
|
const position = draft.position;
|
||||||
|
position.latitude =
|
||||||
finiteOrNull(drawn?.lat) ?? finiteOrNull(sample?.lat) ?? record.lat;
|
finiteOrNull(drawn?.lat) ?? finiteOrNull(sample?.lat) ?? record.lat;
|
||||||
const longitude =
|
position.longitude =
|
||||||
finiteOrNull(drawn?.lon) ?? finiteOrNull(sample?.lon) ?? record.lon;
|
finiteOrNull(drawn?.lon) ?? finiteOrNull(sample?.lon) ?? record.lon;
|
||||||
return Object.freeze({
|
position.railHeightM = Number.isFinite(drawn?.h) ? drawn.h - liftM : null;
|
||||||
key: entry.key,
|
position.heightQuality = Number.isFinite(drawn?.q) ? drawn.q : 0;
|
||||||
feedId: entry.feedId,
|
position.atMs = finiteOrNull(drawn?.at);
|
||||||
selectedAtMs:
|
|
||||||
state._selectedKey === entry.key ? state._selectedAtMs : null,
|
let path = null;
|
||||||
id: String(record.id ?? ''),
|
if (binding && binding.path) {
|
||||||
tripId: record.tripId || null,
|
path = draft.path;
|
||||||
routeId: record.routeId || null,
|
path.shapeIndex = binding.shapeIndex;
|
||||||
routeName: routeNameOf(entry),
|
path.s = binding.drawS;
|
||||||
label: record.label || null,
|
path.direction = directionOf(entry);
|
||||||
trainNumber: record.trainNumber || null,
|
path.lengthM = binding.path.length;
|
||||||
headsign: trip?.headsign || null,
|
path.via = binding.via;
|
||||||
category: entry.category ?? null,
|
path.builtAt = state._pathPacks.get(entry.feedId)?.pack?.builtAt ?? null;
|
||||||
mode: entry.railMode ?? null,
|
path.line = binding.path;
|
||||||
feed: feedOf(entry.feedId),
|
}
|
||||||
position: Object.freeze({
|
|
||||||
latitude,
|
const motion = draft.motion;
|
||||||
longitude,
|
motion.reportedSpeedMps = reported;
|
||||||
railHeightM: Number.isFinite(drawn?.h) ? drawn.h - liftM : null,
|
motion.derivedSpeedMps = derived;
|
||||||
heightQuality: Number.isFinite(drawn?.q) ? drawn.q : 0,
|
motion.speedSource =
|
||||||
atMs: finiteOrNull(drawn?.at),
|
reported !== null ? 'reported' : derived !== null ? 'derived' : null;
|
||||||
}),
|
motion.courseDeg = finiteOrNull(entry.courseDeg);
|
||||||
path:
|
motion.phase = sample?.phase ?? null;
|
||||||
binding && binding.path
|
motion.word = displayMotion(entry, Date.now()).word;
|
||||||
? Object.freeze({
|
motion.reportAgeMs = finiteOrNull(sample?.latestReportAgeMs);
|
||||||
shapeIndex: binding.shapeIndex,
|
motion.shownBehindMs = finiteOrNull(sample?.actualDelayMs);
|
||||||
s: binding.drawS,
|
motion.feedStale = status?.stale === true;
|
||||||
direction: directionOf(entry),
|
|
||||||
lengthM: binding.path.length,
|
const provenance = draft.provenance;
|
||||||
via: binding.via,
|
provenance.positionSource = record.positionSource ?? 'gps';
|
||||||
builtAt:
|
provenance.timestampSource = record.timestampSource ?? null;
|
||||||
state._pathPacks.get(entry.feedId)?.pack?.builtAt ?? null,
|
|
||||||
line: binding.path,
|
const view = draft.view;
|
||||||
})
|
view.key = entry.key;
|
||||||
: null,
|
view.feedId = entry.feedId;
|
||||||
motion: Object.freeze({
|
view.selectedAtMs =
|
||||||
reportedSpeedMps: reported,
|
state._selectedKey === entry.key ? state._selectedAtMs : null;
|
||||||
derivedSpeedMps: derived,
|
view.id = String(record.id ?? '');
|
||||||
speedSource:
|
view.tripId = record.tripId || null;
|
||||||
reported !== null ? 'reported' : derived !== null ? 'derived' : null,
|
view.routeId = record.routeId || null;
|
||||||
courseDeg: finiteOrNull(entry.courseDeg),
|
view.routeName = routeNameOf(entry);
|
||||||
phase: sample?.phase ?? null,
|
view.label = record.label || null;
|
||||||
word: displayMotion(entry, Date.now()).word,
|
view.trainNumber = record.trainNumber || null;
|
||||||
reportAgeMs: finiteOrNull(sample?.latestReportAgeMs),
|
view.headsign = trip?.headsign || null;
|
||||||
shownBehindMs: finiteOrNull(sample?.actualDelayMs),
|
view.category = entry.category ?? null;
|
||||||
feedStale: status?.stale === true,
|
view.mode = entry.railMode ?? null;
|
||||||
}),
|
view.feed = feedOf(entry.feedId, previous?.feed);
|
||||||
provenance: Object.freeze({
|
view.position = reuse(previous?.position, position);
|
||||||
positionSource: record.positionSource ?? 'gps',
|
view.path = path && reuse(previous?.path, path);
|
||||||
timestampSource: record.timestampSource ?? null,
|
view.motion = reuse(previous?.motion, motion);
|
||||||
}),
|
view.provenance = reuse(previous?.provenance, provenance);
|
||||||
stops: Object.freeze(Array.isArray(trip?.stops) ? [...trip.stops] : []),
|
view.stops = stopsOf(trip?.stops);
|
||||||
match: trip?.match ?? null,
|
view.match = trip?.match ?? null;
|
||||||
});
|
const snapshot = reuse(previous, view);
|
||||||
|
views.set(entry, snapshot);
|
||||||
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
const methods = {
|
const methods = {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// hides its marker and card, keeps it selected and ends when it goes.
|
// hides its marker and card, keeps it selected and ends when it goes.
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { RAIL_POLL_MS, railTrainKey } from './policy.js';
|
import { RAIL_POLL_MS, STOP_REFRESH_MS, railTrainKey } from './policy.js';
|
||||||
import {
|
import {
|
||||||
BOSTON,
|
BOSTON,
|
||||||
railLayerHarness as harness,
|
railLayerHarness as harness,
|
||||||
@@ -37,8 +37,12 @@ const PACK = Object.freeze({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** Two trains on the line, T1 running east and T2 west, a step a poll. */
|
/** Two trains on the line, T1 running east and T2 west, a step a poll. */
|
||||||
async function onTheLine(t) {
|
async function onTheLine(t, options = {}) {
|
||||||
const app = harness(t, { paths: { 'mbta-rail': PACK }, at: BOSTON });
|
const app = harness(t, {
|
||||||
|
paths: { 'mbta-rail': PACK },
|
||||||
|
at: BOSTON,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
let step = 0;
|
let step = 0;
|
||||||
app.feeds.set('mbta-rail', () => [
|
app.feeds.set('mbta-rail', () => [
|
||||||
train('T1', LAT, LON0 + (1000 + step * 150) / KX, { speedMps: 15 }),
|
train('T1', LAT, LON0 + (1000 + step * 150) / KX, { speedMps: 15 }),
|
||||||
@@ -100,6 +104,56 @@ test('the selected train reads as a snapshot of its place, motion and sources',
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** A trip's stops as a stops provider lists them. */
|
||||||
|
const stopList = (...names) =>
|
||||||
|
names.map((name, i) => ({
|
||||||
|
stopId: name,
|
||||||
|
name,
|
||||||
|
lat: LAT,
|
||||||
|
lon: LON0 + (i * 2000) / KX,
|
||||||
|
state: 'upcoming',
|
||||||
|
}));
|
||||||
|
|
||||||
|
test('a snapshot read again while nothing changed is the one read before', async (t) => {
|
||||||
|
let list = stopList('Alpha', 'Bravo', 'Charlie');
|
||||||
|
const app = await onTheLine(t, {
|
||||||
|
stops: { stopsFor: () => ({ stops: list, headsign: 'Charlie' }) },
|
||||||
|
});
|
||||||
|
app.parts().selection.selectTrain(T1);
|
||||||
|
const first = app.layer.getSelectedTrain();
|
||||||
|
assert.equal(app.layer.getSelectedTrain(), first, 'the same snapshot');
|
||||||
|
// Its stops are frozen copies of the provider's.
|
||||||
|
assert.equal(first.stops.length, 3);
|
||||||
|
assert.ok(Object.isFrozen(first.stops));
|
||||||
|
assert.ok(Object.isFrozen(first.stops[0]));
|
||||||
|
assert.notEqual(first.stops[0], list[0]);
|
||||||
|
assert.equal(first.stops[0].name, 'Alpha');
|
||||||
|
|
||||||
|
// It moves on: a new snapshot at its new place, what did not change kept.
|
||||||
|
app.run(RAIL_POLL_MS);
|
||||||
|
await app.poll();
|
||||||
|
app.run(RAIL_POLL_MS);
|
||||||
|
const moved = app.layer.getSelectedTrain();
|
||||||
|
assert.notEqual(moved, first, 'a new snapshot');
|
||||||
|
assert.ok(Object.isFrozen(moved) && Object.isFrozen(moved.path));
|
||||||
|
assert.ok(moved.path.s > first.path.s, 'at its new place');
|
||||||
|
assert.equal(moved.stops, first.stops, 'the same stops');
|
||||||
|
assert.equal(moved.feed, first.feed);
|
||||||
|
assert.equal(moved.provenance, first.provenance);
|
||||||
|
assert.equal(moved.path.line, first.path.line);
|
||||||
|
assert.equal(app.layer.getSelectedTrain(), moved);
|
||||||
|
|
||||||
|
// The provider lists other stops: new ones.
|
||||||
|
list = stopList('Delta', 'Echo');
|
||||||
|
app.run(STOP_REFRESH_MS);
|
||||||
|
const later = app.layer.getSelectedTrain();
|
||||||
|
assert.notEqual(later.stops, first.stops);
|
||||||
|
assert.deepEqual(
|
||||||
|
later.stops.map((stop) => stop.name),
|
||||||
|
['Delta', 'Echo'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('a held train hides its marker and card and keeps the selection', async (t) => {
|
test('a held train hides its marker and card and keeps the selection', async (t) => {
|
||||||
const app = await onTheLine(t);
|
const app = await onTheLine(t);
|
||||||
assert.equal(app.layer.setCockpitSubject('rail:mbta-rail:nope'), false);
|
assert.equal(app.layer.setCockpitSubject('rail:mbta-rail:nope'), false);
|
||||||
|
|||||||
@@ -4,17 +4,33 @@ import {
|
|||||||
} from '../../data/transitFeeds.js';
|
} from '../../data/transitFeeds.js';
|
||||||
import { displayMotion } from './movement.js';
|
import { displayMotion } from './movement.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `previous` when it holds each of `next`'s values, else a frozen copy of
|
||||||
|
* `next`: a part of a snapshot that did not change is the one read before.
|
||||||
|
* `next` is a draft, filled again on every read.
|
||||||
|
*/
|
||||||
|
function reuse(previous, next) {
|
||||||
|
if (!previous) return Object.freeze({ ...next });
|
||||||
|
for (const name in next)
|
||||||
|
if (!Object.is(previous[name], next[name]))
|
||||||
|
return Object.freeze({ ...next });
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read access to one vehicle, and a hold on it for a first-person view.
|
* Read access to one vehicle, and a hold on it for a first-person view.
|
||||||
*
|
*
|
||||||
* `getSelectedVehicle()` and `getCockpitSubject()` describe the selected
|
* `getSelectedVehicle()` and `getCockpitSubject()` describe the selected
|
||||||
* vehicle and the held one as a frozen snapshot, read again on each call:
|
* vehicle and the held one as a frozen snapshot, read again on each call:
|
||||||
* where it is drawn, how fast it moves and how that is known. Transit has no
|
* where it is drawn, how fast it moves and how that is known. A call while
|
||||||
* route shapes or stops, so a snapshot names only the vehicle's last reported
|
* nothing in it changed returns the snapshot it returned before, and a part
|
||||||
* stop. `setCockpitSubject(key)` holds a vehicle: its sprite, card, trail
|
* that did not change (its feed, its stop) is the same object in the next
|
||||||
* and detection box are hidden, it keeps moving when it is out of the
|
* one, so a view can read it on every frame. Transit has no route shapes or
|
||||||
* camera's view, its feed stays polled, and clicks and Escape no longer
|
* stops, so a snapshot names only the vehicle's last reported stop.
|
||||||
* change the selection. The hold ends with `setCockpitSubject(null)`, and on
|
* `setCockpitSubject(key)` holds a vehicle: its sprite, card, trail and
|
||||||
|
* detection box are hidden, it keeps moving when it is out of the camera's
|
||||||
|
* view, its feed stays polled, and clicks and Escape no longer change the
|
||||||
|
* selection. The hold ends with `setCockpitSubject(null)`, and on
|
||||||
* its own when the vehicle is removed or the layer is turned off.
|
* its own when the vehicle is removed or the layer is turned off.
|
||||||
* `focusVehicle(key)` selects a vehicle, as a click does. A snapshot's
|
* `focusVehicle(key)` selects a vehicle, as a click does. A snapshot's
|
||||||
* `selectedAtMs` is when the user last selected the selected vehicle (a
|
* `selectedAtMs` is when the user last selected the selected vehicle (a
|
||||||
@@ -31,84 +47,101 @@ export function createTransitCockpitSubject({ state, parts, services }) {
|
|||||||
: 0;
|
: 0;
|
||||||
const finiteOrNull = (value) =>
|
const finiteOrNull = (value) =>
|
||||||
typeof value === 'number' && Number.isFinite(value) ? value : null;
|
typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||||
|
/** Each vehicle's last snapshot, returned again while nothing changed. */
|
||||||
|
const views = new WeakMap();
|
||||||
|
/** The drafts vehicleView fills on every read (reuse). */
|
||||||
|
const draft = {
|
||||||
|
view: {},
|
||||||
|
feed: {},
|
||||||
|
position: {},
|
||||||
|
motion: {},
|
||||||
|
stop: {},
|
||||||
|
provenance: {},
|
||||||
|
};
|
||||||
|
|
||||||
/** The feed in use, else the registry's entry for it. */
|
/** The feed in use, else the registry's entry for it. */
|
||||||
function feedEntry(feedId) {
|
function feedEntry(feedId) {
|
||||||
return state._activeFeeds.get(feedId) || getRegisteredTransitFeed(feedId);
|
return state._activeFeeds.get(feedId) || getRegisteredTransitFeed(feedId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function feedOf(feed) {
|
function feedOf(feed, previous) {
|
||||||
if (!feed) return null;
|
if (!feed) return null;
|
||||||
return Object.freeze({
|
const next = draft.feed;
|
||||||
id: feed.id,
|
next.id = feed.id;
|
||||||
name: feed.name ?? null,
|
next.name = feed.name ?? null;
|
||||||
operator: feed.operator ?? null,
|
next.operator = feed.operator ?? null;
|
||||||
region: feed.region ?? null,
|
next.region = feed.region ?? null;
|
||||||
attribution: feed.attribution ?? null,
|
next.attribution = feed.attribution ?? null;
|
||||||
license: feed.license ?? null,
|
next.license = feed.license ?? null;
|
||||||
licenseUrl: feed.licenseUrl ?? null,
|
next.licenseUrl = feed.licenseUrl ?? null;
|
||||||
});
|
return reuse(previous, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The vehicle as a frozen snapshot (see the module comment), or null. */
|
/** The vehicle as a frozen snapshot (see the module comment), or null. */
|
||||||
function vehicleView(entry) {
|
function vehicleView(entry) {
|
||||||
if (!entry?.record) return null;
|
if (!entry?.record) return null;
|
||||||
|
const previous = views.get(entry);
|
||||||
const record = entry.record;
|
const record = entry.record;
|
||||||
const sample = entry.sample ?? null;
|
const sample = entry.sample ?? null;
|
||||||
const feed = feedEntry(entry.feedId);
|
const feed = feedEntry(entry.feedId);
|
||||||
const reported = finiteOrNull(record.speedMps);
|
const reported = finiteOrNull(record.speedMps);
|
||||||
const derived = finiteOrNull(sample?.segmentSpeedMps);
|
const derived = finiteOrNull(sample?.segmentSpeedMps);
|
||||||
const status = state._feedStatus.get(entry.feedId);
|
const status = state._feedStatus.get(entry.feedId);
|
||||||
const latitude = finiteOrNull(sample?.lat) ?? record.lat;
|
|
||||||
const longitude = finiteOrNull(sample?.lon) ?? record.lon;
|
const position = draft.position;
|
||||||
return Object.freeze({
|
position.latitude = finiteOrNull(sample?.lat) ?? record.lat;
|
||||||
key: entry.key,
|
position.longitude = finiteOrNull(sample?.lon) ?? record.lon;
|
||||||
feedId: entry.feedId,
|
position.groundHeightM = Number.isFinite(entry.heightM)
|
||||||
selectedAtMs:
|
? entry.heightM - liftM
|
||||||
state._selectedKey === entry.key ? state._selectedAtMs : null,
|
: null;
|
||||||
id: String(record.id ?? ''),
|
position.atMs = finiteOrNull(sample?.displayT);
|
||||||
routeId: record.routeId || null,
|
|
||||||
routeLabel:
|
const motion = draft.motion;
|
||||||
|
motion.reportedSpeedMps = reported;
|
||||||
|
motion.derivedSpeedMps = derived;
|
||||||
|
motion.speedSource =
|
||||||
|
reported !== null ? 'reported' : derived !== null ? 'derived' : null;
|
||||||
|
motion.courseDeg = finiteOrNull(entry.courseDeg);
|
||||||
|
motion.reportedBearingDeg = finiteOrNull(record.bearing);
|
||||||
|
motion.segmentCourseDeg = finiteOrNull(sample?.segmentCourseDeg);
|
||||||
|
motion.phase = sample?.phase ?? null;
|
||||||
|
motion.word = displayMotion(entry, Date.now()).word;
|
||||||
|
motion.reportAgeMs = finiteOrNull(sample?.latestReportAgeMs);
|
||||||
|
motion.shownBehindMs = finiteOrNull(sample?.actualDelayMs);
|
||||||
|
motion.feedStale = status?.stale === true;
|
||||||
|
|
||||||
|
const stop = draft.stop;
|
||||||
|
stop.stopId = record.stopId || null;
|
||||||
|
stop.status = record.status || null;
|
||||||
|
|
||||||
|
const provenance = draft.provenance;
|
||||||
|
provenance.positionSource = 'gps';
|
||||||
|
provenance.timestampSource = record.timestampSource ?? null;
|
||||||
|
|
||||||
|
const view = draft.view;
|
||||||
|
view.key = entry.key;
|
||||||
|
view.feedId = entry.feedId;
|
||||||
|
view.selectedAtMs =
|
||||||
|
state._selectedKey === entry.key ? state._selectedAtMs : null;
|
||||||
|
view.id = String(record.id ?? '');
|
||||||
|
view.routeId = record.routeId || null;
|
||||||
|
view.routeLabel =
|
||||||
(feed && record.routeId
|
(feed && record.routeId
|
||||||
? transitRouteLabel(feed, record.routeId)
|
? transitRouteLabel(feed, record.routeId)
|
||||||
: null) || null,
|
: null) || null;
|
||||||
tripId: record.tripId || null,
|
view.tripId = record.tripId || null;
|
||||||
directionId: record.directionId ?? null,
|
view.directionId = record.directionId ?? null;
|
||||||
label: record.label || null,
|
view.label = record.label || null;
|
||||||
mode: entry.mode ?? null,
|
view.mode = entry.mode ?? null;
|
||||||
modeInferred: entry.modeInferred === true,
|
view.modeInferred = entry.modeInferred === true;
|
||||||
feed: feedOf(feed),
|
view.feed = feedOf(feed, previous?.feed);
|
||||||
position: Object.freeze({
|
view.position = reuse(previous?.position, position);
|
||||||
latitude,
|
view.motion = reuse(previous?.motion, motion);
|
||||||
longitude,
|
view.stop = reuse(previous?.stop, stop);
|
||||||
groundHeightM: Number.isFinite(entry.heightM)
|
view.provenance = reuse(previous?.provenance, provenance);
|
||||||
? entry.heightM - liftM
|
const snapshot = reuse(previous, view);
|
||||||
: null,
|
views.set(entry, snapshot);
|
||||||
atMs: finiteOrNull(sample?.displayT),
|
return snapshot;
|
||||||
}),
|
|
||||||
motion: Object.freeze({
|
|
||||||
reportedSpeedMps: reported,
|
|
||||||
derivedSpeedMps: derived,
|
|
||||||
speedSource:
|
|
||||||
reported !== null ? 'reported' : derived !== null ? 'derived' : null,
|
|
||||||
courseDeg: finiteOrNull(entry.courseDeg),
|
|
||||||
reportedBearingDeg: finiteOrNull(record.bearing),
|
|
||||||
segmentCourseDeg: finiteOrNull(sample?.segmentCourseDeg),
|
|
||||||
phase: sample?.phase ?? null,
|
|
||||||
word: displayMotion(entry, Date.now()).word,
|
|
||||||
reportAgeMs: finiteOrNull(sample?.latestReportAgeMs),
|
|
||||||
shownBehindMs: finiteOrNull(sample?.actualDelayMs),
|
|
||||||
feedStale: status?.stale === true,
|
|
||||||
}),
|
|
||||||
stop: Object.freeze({
|
|
||||||
stopId: record.stopId || null,
|
|
||||||
status: record.status || null,
|
|
||||||
}),
|
|
||||||
provenance: Object.freeze({
|
|
||||||
positionSource: 'gps',
|
|
||||||
timestampSource: record.timestampSource ?? null,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Show or hide a vehicle's sprite and trail for the hold. */
|
/** Show or hide a vehicle's sprite and trail for the hold. */
|
||||||
|
|||||||
@@ -4196,6 +4196,27 @@ test('the selected vehicle reads as a snapshot of its place, motion and source',
|
|||||||
assert.ok(Number.isFinite(view.position.latitude));
|
assert.ok(Number.isFinite(view.position.latitude));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a vehicle read again while nothing changed is the snapshot read before', async (t) => {
|
||||||
|
const app = harness(t);
|
||||||
|
const key = await busRunningNorth(app);
|
||||||
|
app.layer._transitPartsForTest().selection.selectVehicle(key);
|
||||||
|
const first = app.layer.getSelectedVehicle();
|
||||||
|
assert.equal(app.layer.getSelectedVehicle(), first, 'the same snapshot');
|
||||||
|
// It moves on: a new snapshot at its new place, what did not change kept.
|
||||||
|
app.run(TRANSIT_POLL_MS);
|
||||||
|
await app.layer.update();
|
||||||
|
app.settle();
|
||||||
|
app.run(TRANSIT_POLL_MS);
|
||||||
|
const moved = app.layer.getSelectedVehicle();
|
||||||
|
assert.notEqual(moved, first, 'a new snapshot');
|
||||||
|
assert.ok(Object.isFrozen(moved) && Object.isFrozen(moved.position));
|
||||||
|
assert.ok(moved.position.latitude > first.position.latitude, 'moved');
|
||||||
|
assert.equal(moved.feed, first.feed);
|
||||||
|
assert.equal(moved.stop, first.stop);
|
||||||
|
assert.equal(moved.provenance, first.provenance);
|
||||||
|
assert.equal(app.layer.getSelectedVehicle(), moved);
|
||||||
|
});
|
||||||
|
|
||||||
test('selectedAtMs is the user’s selection time; a hold and a deselection leave none', async (t) => {
|
test('selectedAtMs is the user’s selection time; a hold and a deselection leave none', async (t) => {
|
||||||
const app = harness(t);
|
const app = harness(t);
|
||||||
const key = await busRunningNorth(app);
|
const key = await busRunningNorth(app);
|
||||||
|
|||||||
Reference in new issue
Block a user