mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 07:12:21 +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
+303
-141
No files matched your search
@@ -2,12 +2,50 @@ import { pointAtS } from '../../data/railPaths.js';
|
||||
import { getRegisteredRailFeed } from '../../data/railFeeds.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.
|
||||
*
|
||||
* `getSelectedTrain()` and `getCockpitSubject()` describe the selected train
|
||||
* 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
|
||||
* 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
|
||||
@@ -36,26 +74,37 @@ export function createRailCockpitSubject({
|
||||
const finiteOrNull = (value) =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||
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. */
|
||||
function feedOf(feedId) {
|
||||
function feedOf(feedId, previous) {
|
||||
const listed = state._catalog?.find((feed) => feed.id === feedId);
|
||||
const feed = listed ?? getRegisteredRailFeed(feedId);
|
||||
if (!feed) return null;
|
||||
return Object.freeze({
|
||||
id: feed.id,
|
||||
name: feed.name ?? null,
|
||||
operator: feed.operator ?? null,
|
||||
region: feed.region ?? null,
|
||||
positions: listed?.positions ?? feed.positions ?? 'gps',
|
||||
tripUpdates:
|
||||
typeof listed?.tripUpdates === 'boolean'
|
||||
? listed.tripUpdates
|
||||
: typeof feed.tripUpdatesUrl === 'string',
|
||||
attribution: feed.attribution ?? null,
|
||||
license: feed.license ?? null,
|
||||
licenseUrl: feed.licenseUrl ?? null,
|
||||
});
|
||||
const next = draft.feed;
|
||||
next.id = feed.id;
|
||||
next.name = feed.name ?? null;
|
||||
next.operator = feed.operator ?? null;
|
||||
next.region = feed.region ?? null;
|
||||
next.positions = listed?.positions ?? feed.positions ?? 'gps';
|
||||
next.tripUpdates =
|
||||
typeof listed?.tripUpdates === 'boolean'
|
||||
? listed.tripUpdates
|
||||
: typeof feed.tripUpdatesUrl === 'string';
|
||||
next.attribution = feed.attribution ?? null;
|
||||
next.license = feed.license ?? null;
|
||||
next.licenseUrl = feed.licenseUrl ?? null;
|
||||
return reuse(previous, next);
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
function trainView(entry) {
|
||||
if (!entry?.record) return null;
|
||||
const previous = views.get(entry);
|
||||
const record = entry.record;
|
||||
const sample = entry.sample ?? null;
|
||||
const drawn = entry.drawnHeight ?? null;
|
||||
@@ -95,64 +145,68 @@ export function createRailCockpitSubject({
|
||||
const derived = finiteOrNull(sample?.segmentSpeedMps);
|
||||
const trip = state._selectedKey === entry.key ? state._trip : null;
|
||||
const status = state._feedStatus.get(entry.feedId);
|
||||
const latitude =
|
||||
|
||||
const position = draft.position;
|
||||
position.latitude =
|
||||
finiteOrNull(drawn?.lat) ?? finiteOrNull(sample?.lat) ?? record.lat;
|
||||
const longitude =
|
||||
position.longitude =
|
||||
finiteOrNull(drawn?.lon) ?? finiteOrNull(sample?.lon) ?? record.lon;
|
||||
return Object.freeze({
|
||||
key: entry.key,
|
||||
feedId: entry.feedId,
|
||||
selectedAtMs:
|
||||
state._selectedKey === entry.key ? state._selectedAtMs : null,
|
||||
id: String(record.id ?? ''),
|
||||
tripId: record.tripId || null,
|
||||
routeId: record.routeId || null,
|
||||
routeName: routeNameOf(entry),
|
||||
label: record.label || null,
|
||||
trainNumber: record.trainNumber || null,
|
||||
headsign: trip?.headsign || null,
|
||||
category: entry.category ?? null,
|
||||
mode: entry.railMode ?? null,
|
||||
feed: feedOf(entry.feedId),
|
||||
position: Object.freeze({
|
||||
latitude,
|
||||
longitude,
|
||||
railHeightM: Number.isFinite(drawn?.h) ? drawn.h - liftM : null,
|
||||
heightQuality: Number.isFinite(drawn?.q) ? drawn.q : 0,
|
||||
atMs: finiteOrNull(drawn?.at),
|
||||
}),
|
||||
path:
|
||||
binding && binding.path
|
||||
? Object.freeze({
|
||||
shapeIndex: binding.shapeIndex,
|
||||
s: binding.drawS,
|
||||
direction: directionOf(entry),
|
||||
lengthM: binding.path.length,
|
||||
via: binding.via,
|
||||
builtAt:
|
||||
state._pathPacks.get(entry.feedId)?.pack?.builtAt ?? null,
|
||||
line: binding.path,
|
||||
})
|
||||
: null,
|
||||
motion: Object.freeze({
|
||||
reportedSpeedMps: reported,
|
||||
derivedSpeedMps: derived,
|
||||
speedSource:
|
||||
reported !== null ? 'reported' : derived !== null ? 'derived' : null,
|
||||
courseDeg: finiteOrNull(entry.courseDeg),
|
||||
phase: sample?.phase ?? null,
|
||||
word: displayMotion(entry, Date.now()).word,
|
||||
reportAgeMs: finiteOrNull(sample?.latestReportAgeMs),
|
||||
shownBehindMs: finiteOrNull(sample?.actualDelayMs),
|
||||
feedStale: status?.stale === true,
|
||||
}),
|
||||
provenance: Object.freeze({
|
||||
positionSource: record.positionSource ?? 'gps',
|
||||
timestampSource: record.timestampSource ?? null,
|
||||
}),
|
||||
stops: Object.freeze(Array.isArray(trip?.stops) ? [...trip.stops] : []),
|
||||
match: trip?.match ?? null,
|
||||
});
|
||||
position.railHeightM = Number.isFinite(drawn?.h) ? drawn.h - liftM : null;
|
||||
position.heightQuality = Number.isFinite(drawn?.q) ? drawn.q : 0;
|
||||
position.atMs = finiteOrNull(drawn?.at);
|
||||
|
||||
let path = null;
|
||||
if (binding && binding.path) {
|
||||
path = draft.path;
|
||||
path.shapeIndex = binding.shapeIndex;
|
||||
path.s = binding.drawS;
|
||||
path.direction = directionOf(entry);
|
||||
path.lengthM = binding.path.length;
|
||||
path.via = binding.via;
|
||||
path.builtAt = state._pathPacks.get(entry.feedId)?.pack?.builtAt ?? null;
|
||||
path.line = binding.path;
|
||||
}
|
||||
|
||||
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.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 provenance = draft.provenance;
|
||||
provenance.positionSource = record.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.tripId = record.tripId || null;
|
||||
view.routeId = record.routeId || null;
|
||||
view.routeName = routeNameOf(entry);
|
||||
view.label = record.label || null;
|
||||
view.trainNumber = record.trainNumber || null;
|
||||
view.headsign = trip?.headsign || null;
|
||||
view.category = entry.category ?? null;
|
||||
view.mode = entry.railMode ?? null;
|
||||
view.feed = feedOf(entry.feedId, previous?.feed);
|
||||
view.position = reuse(previous?.position, position);
|
||||
view.path = path && reuse(previous?.path, path);
|
||||
view.motion = reuse(previous?.motion, motion);
|
||||
view.provenance = reuse(previous?.provenance, provenance);
|
||||
view.stops = stopsOf(trip?.stops);
|
||||
view.match = trip?.match ?? null;
|
||||
const snapshot = reuse(previous, view);
|
||||
views.set(entry, snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
const methods = {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// hides its marker and card, keeps it selected and ends when it goes.
|
||||
import test from 'node:test';
|
||||
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 {
|
||||
BOSTON,
|
||||
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. */
|
||||
async function onTheLine(t) {
|
||||
const app = harness(t, { paths: { 'mbta-rail': PACK }, at: BOSTON });
|
||||
async function onTheLine(t, options = {}) {
|
||||
const app = harness(t, {
|
||||
paths: { 'mbta-rail': PACK },
|
||||
at: BOSTON,
|
||||
...options,
|
||||
});
|
||||
let step = 0;
|
||||
app.feeds.set('mbta-rail', () => [
|
||||
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) => {
|
||||
const app = await onTheLine(t);
|
||||
assert.equal(app.layer.setCockpitSubject('rail:mbta-rail:nope'), false);
|
||||
|
||||
@@ -4,17 +4,33 @@ import {
|
||||
} from '../../data/transitFeeds.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.
|
||||
*
|
||||
* `getSelectedVehicle()` and `getCockpitSubject()` describe the selected
|
||||
* 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
|
||||
* route shapes or stops, so a snapshot names only the vehicle's last reported
|
||||
* stop. `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
|
||||
* where it is drawn, how fast it moves and how that is known. A call while
|
||||
* nothing in it changed returns the snapshot it returned before, and a part
|
||||
* that did not change (its feed, its stop) is the same object in the next
|
||||
* one, so a view can read it on every frame. Transit has no route shapes or
|
||||
* stops, so a snapshot names only the vehicle's last reported stop.
|
||||
* `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.
|
||||
* `focusVehicle(key)` selects a vehicle, as a click does. A snapshot's
|
||||
* `selectedAtMs` is when the user last selected the selected vehicle (a
|
||||
@@ -31,84 +47,101 @@ export function createTransitCockpitSubject({ state, parts, services }) {
|
||||
: 0;
|
||||
const finiteOrNull = (value) =>
|
||||
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. */
|
||||
function feedEntry(feedId) {
|
||||
return state._activeFeeds.get(feedId) || getRegisteredTransitFeed(feedId);
|
||||
}
|
||||
|
||||
function feedOf(feed) {
|
||||
function feedOf(feed, previous) {
|
||||
if (!feed) return null;
|
||||
return Object.freeze({
|
||||
id: feed.id,
|
||||
name: feed.name ?? null,
|
||||
operator: feed.operator ?? null,
|
||||
region: feed.region ?? null,
|
||||
attribution: feed.attribution ?? null,
|
||||
license: feed.license ?? null,
|
||||
licenseUrl: feed.licenseUrl ?? null,
|
||||
});
|
||||
const next = draft.feed;
|
||||
next.id = feed.id;
|
||||
next.name = feed.name ?? null;
|
||||
next.operator = feed.operator ?? null;
|
||||
next.region = feed.region ?? null;
|
||||
next.attribution = feed.attribution ?? null;
|
||||
next.license = feed.license ?? null;
|
||||
next.licenseUrl = feed.licenseUrl ?? null;
|
||||
return reuse(previous, next);
|
||||
}
|
||||
|
||||
/** The vehicle as a frozen snapshot (see the module comment), or null. */
|
||||
function vehicleView(entry) {
|
||||
if (!entry?.record) return null;
|
||||
const previous = views.get(entry);
|
||||
const record = entry.record;
|
||||
const sample = entry.sample ?? null;
|
||||
const feed = feedEntry(entry.feedId);
|
||||
const reported = finiteOrNull(record.speedMps);
|
||||
const derived = finiteOrNull(sample?.segmentSpeedMps);
|
||||
const status = state._feedStatus.get(entry.feedId);
|
||||
const latitude = finiteOrNull(sample?.lat) ?? record.lat;
|
||||
const longitude = finiteOrNull(sample?.lon) ?? record.lon;
|
||||
return Object.freeze({
|
||||
key: entry.key,
|
||||
feedId: entry.feedId,
|
||||
selectedAtMs:
|
||||
state._selectedKey === entry.key ? state._selectedAtMs : null,
|
||||
id: String(record.id ?? ''),
|
||||
routeId: record.routeId || null,
|
||||
routeLabel:
|
||||
(feed && record.routeId
|
||||
? transitRouteLabel(feed, record.routeId)
|
||||
: null) || null,
|
||||
tripId: record.tripId || null,
|
||||
directionId: record.directionId ?? null,
|
||||
label: record.label || null,
|
||||
mode: entry.mode ?? null,
|
||||
modeInferred: entry.modeInferred === true,
|
||||
feed: feedOf(feed),
|
||||
position: Object.freeze({
|
||||
latitude,
|
||||
longitude,
|
||||
groundHeightM: Number.isFinite(entry.heightM)
|
||||
? entry.heightM - liftM
|
||||
: null,
|
||||
atMs: finiteOrNull(sample?.displayT),
|
||||
}),
|
||||
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,
|
||||
}),
|
||||
});
|
||||
|
||||
const position = draft.position;
|
||||
position.latitude = finiteOrNull(sample?.lat) ?? record.lat;
|
||||
position.longitude = finiteOrNull(sample?.lon) ?? record.lon;
|
||||
position.groundHeightM = Number.isFinite(entry.heightM)
|
||||
? entry.heightM - liftM
|
||||
: null;
|
||||
position.atMs = finiteOrNull(sample?.displayT);
|
||||
|
||||
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
|
||||
? transitRouteLabel(feed, record.routeId)
|
||||
: null) || null;
|
||||
view.tripId = record.tripId || null;
|
||||
view.directionId = record.directionId ?? null;
|
||||
view.label = record.label || null;
|
||||
view.mode = entry.mode ?? null;
|
||||
view.modeInferred = entry.modeInferred === true;
|
||||
view.feed = feedOf(feed, previous?.feed);
|
||||
view.position = reuse(previous?.position, position);
|
||||
view.motion = reuse(previous?.motion, motion);
|
||||
view.stop = reuse(previous?.stop, stop);
|
||||
view.provenance = reuse(previous?.provenance, provenance);
|
||||
const snapshot = reuse(previous, view);
|
||||
views.set(entry, snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/** 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));
|
||||
});
|
||||
|
||||
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) => {
|
||||
const app = harness(t);
|
||||
const key = await busRunningNorth(app);
|
||||
|
||||
Reference in new issue
Block a user