@motionstudies/core 0.1.0-alpha.8 → 0.1.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/domain/network.js +37 -1
- package/domain/vehicle-counts.d.ts +13 -0
- package/domain/vehicle-counts.js +33 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -179,3 +179,17 @@ See [service architecture and operations](../docs/LIVE-AIRPORTS.md). The Worker
|
|
|
179
179
|
The same `decodeAdsbHeatmap` now powers `enrichAirEndpoints`; endpoint inference retains full coordinate precision, while playback compilation retains the existing five-decimal coordinates. `transportAirTracks` and `chunkAirSnapshot` are also available for consumers that assemble their own pipeline. All functions have public TypeScript declarations and work in the packed Node package.
|
|
180
180
|
|
|
181
181
|
Editions supply geographic bounds, service date, explicit UTC offset, optional timezone, input files and output paths. Flight IDs and chunk overlap retain the existing contracts. The default splits known callsign changes and gaps over 30 minutes. Set `splitTracks: false` only when reproducing a legacy opening snapshot with one ID per aircraft. See [adoption and compatibility](../docs/AIR-DATA.md).
|
|
182
|
+
|
|
183
|
+
## Shared edition controllers and performance
|
|
184
|
+
|
|
185
|
+
`positionForTrain` now indexes chronological stop times with binary search and retains sequential behavior for unordered observations. Stop arrays are immutable: replace the array when a timetable changes. Arrival/departure boundaries, dwell, cancellation and backward seeking retain the existing contract.
|
|
186
|
+
|
|
187
|
+
`countableVehicleTrains(network, stations, selection)` and `createActiveTimetableVehicleCounter(trains, options)` from `@motionstudies/core/domain/vehicle-counts` separate station/route/category membership from clock updates. Build the selector and counter with `useMemo` when data or selection changes, then call the counter at the displayed time. It includes both interval endpoints and excludes cancellations and inverted intervals. Missing stations yield no matches. By default it counts timetable intervals even if a journey lacks enough stops to position; `{ requirePositionable: true }` excludes journeys with fewer than two stops. This distinction is explicit so an edition can retain its established metric.
|
|
188
|
+
|
|
189
|
+
The renderer now shares active GPU upload ranges, paused frame reuse, label and trail frame budgets, cached text comparators and batched hub lines. Custom layers can import the low-level helpers from `@motionstudies/three/render-performance`. Recreate frame trackers when their geometry/data/selection inputs change; `batchHubLines` takes ownership of two-vertex source line resources. Edition-specific worker transfer, picking and cartographic adapters remain consumer-owned.
|
|
190
|
+
|
|
191
|
+
`useJsonAsset<T>(url, enabled, parse?, optional?)` from `@motionstudies/web/use-json-asset` loads a single asset lazily and exposes `data`, `loading`, `error`, `unavailable`, and `retry`. Keep the parser stable and perform edition-specific schema/source compatibility checks there. Successful data remains cached while disabled; consumers decide whether to display it. Changing the URL or parser immediately discards prior-source state, and disabling/unmounting cancels requests. `retry()` discards cached state and requests again when enabled. Optional HTTP 404 responses are unavailable; other failures are errors. No source fallback or freshness policy is inferred.
|
|
192
|
+
|
|
193
|
+
`useTransitionValue(target, { durationMs, easing, steps })` from `@motionstudies/web/use-transition-value` animates a numeric value, returning `value` and `transitioning`. It reverses from the current frame, cancels on teardown, and settles immediately when reduced motion becomes active. `smoothTransition` is the default easing; `cosineTransition` and stepped progress support existing edition rhythms. Keep custom easing functions stable. Camera actions and lazy layout loading stay in the edition.
|
|
194
|
+
|
|
195
|
+
Edition chunk scripts can call `runNetworkChunkCli()` from `@motionstudies/data/network-chunk-cli`. It accepts the existing `--input`, `--manifest`, `--opening`, `--chunk-hours`, `--opening-start`, `--opening-end`, and `--focus` arguments. Source acquisition, provenance, output paths and command invocation remain edition-owned.
|
package/domain/network.js
CHANGED
|
@@ -69,7 +69,7 @@ export function buildStationIndex(snapshot) {
|
|
|
69
69
|
routes: [...record.routes.values()].sort((first, second) => first.name.localeCompare(second.name, 'de-CH')),
|
|
70
70
|
}));
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
function linearPositionForTrain(train, time) {
|
|
73
73
|
if (train.realtime?.status === 'cancelled' ||
|
|
74
74
|
time < train.start ||
|
|
75
75
|
time > train.end ||
|
|
@@ -98,6 +98,42 @@ export function positionForTrain(train, time) {
|
|
|
98
98
|
const last = train.stops.at(-1);
|
|
99
99
|
return { fromStop: last[0], toStop: last[0], progress: 0 };
|
|
100
100
|
}
|
|
101
|
+
// Timetables are immutable. A replacement stop array (including realtime edits)
|
|
102
|
+
// gets its own validation; old schedules can be garbage collected.
|
|
103
|
+
const chronologicalSchedules = new WeakMap();
|
|
104
|
+
export function positionForTrain(train, time) {
|
|
105
|
+
if (train.realtime?.status === 'cancelled' || time < train.start || time > train.end || train.stops.length < 2)
|
|
106
|
+
return;
|
|
107
|
+
const stops = train.stops;
|
|
108
|
+
let chronological = chronologicalSchedules.get(stops);
|
|
109
|
+
if (chronological === undefined) {
|
|
110
|
+
chronological = stops.every((stop, index) => Number.isFinite(stop[1]) && Number.isFinite(stop[2]) &&
|
|
111
|
+
stop[2] >= stop[1] && (index === 0 || stop[1] >= stops[index - 1][2]));
|
|
112
|
+
chronologicalSchedules.set(stops, chronological);
|
|
113
|
+
}
|
|
114
|
+
// Preserve sequential semantics for unordered observations and non-finite clocks.
|
|
115
|
+
if (!chronological || !Number.isFinite(time))
|
|
116
|
+
return linearPositionForTrain(train, time);
|
|
117
|
+
let lower = 0;
|
|
118
|
+
let upper = stops.length;
|
|
119
|
+
while (lower < upper) {
|
|
120
|
+
const middle = (lower + upper) >>> 1;
|
|
121
|
+
if (stops[middle][2] < time)
|
|
122
|
+
lower = middle + 1;
|
|
123
|
+
else
|
|
124
|
+
upper = middle;
|
|
125
|
+
}
|
|
126
|
+
const next = stops[Math.min(lower, stops.length - 1)];
|
|
127
|
+
if (lower === 0 || lower === stops.length || time > next[1]) {
|
|
128
|
+
return { fromStop: next[0], toStop: next[0], progress: 0 };
|
|
129
|
+
}
|
|
130
|
+
const previous = stops[lower - 1];
|
|
131
|
+
return {
|
|
132
|
+
fromStop: previous[0], toStop: next[0],
|
|
133
|
+
progress: Math.min(1, Math.max(0, (time - previous[2]) / Math.max(1, next[1] - previous[2]))),
|
|
134
|
+
segmentIndex: lower - 1,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
101
137
|
export function formatServiceTime(totalSeconds) {
|
|
102
138
|
const normalized = ((Math.round(totalSeconds) % 86400) + 86400) % 86400;
|
|
103
139
|
const hours = Math.floor(normalized / 3600);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NetworkSnapshot, NetworkTrain, ServiceCategory, StationIndexEntry } from './network.ts';
|
|
2
|
+
export declare function countableVehicleTrains(network: NetworkSnapshot | undefined, stations: readonly StationIndexEntry[], selection: {
|
|
3
|
+
category?: ServiceCategory;
|
|
4
|
+
station?: Pick<StationIndexEntry, 'name'>;
|
|
5
|
+
route?: {
|
|
6
|
+
name: string;
|
|
7
|
+
category: ServiceCategory;
|
|
8
|
+
};
|
|
9
|
+
}): readonly NetworkTrain[];
|
|
10
|
+
/** Build once per immutable selection; clock updates need only two binary searches. */
|
|
11
|
+
export declare function createActiveTimetableVehicleCounter(trains: readonly NetworkTrain[], options?: {
|
|
12
|
+
readonly requirePositionable?: boolean;
|
|
13
|
+
}): (time: number) => number;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function countableVehicleTrains(network, stations, selection) {
|
|
2
|
+
const stationTrainIds = selection.station
|
|
3
|
+
? new Set(stations.find(station => station.name === selection.station?.name)?.trainIds ?? [])
|
|
4
|
+
: undefined;
|
|
5
|
+
return network?.trains.filter(train => (!selection.category || train.category === selection.category) &&
|
|
6
|
+
(!stationTrainIds || stationTrainIds.has(train.id)) &&
|
|
7
|
+
(!selection.route || (train.route === selection.route.name && train.category === selection.route.category))) ?? [];
|
|
8
|
+
}
|
|
9
|
+
/** Build once per immutable selection; clock updates need only two binary searches. */
|
|
10
|
+
export function createActiveTimetableVehicleCounter(trains, options = {}) {
|
|
11
|
+
const starts = [], ends = [];
|
|
12
|
+
for (const train of trains) {
|
|
13
|
+
if (train.realtime?.status === 'cancelled' || (options.requirePositionable && train.stops.length < 2) || !(train.start <= train.end))
|
|
14
|
+
continue;
|
|
15
|
+
starts.push(train.start);
|
|
16
|
+
ends.push(train.end);
|
|
17
|
+
}
|
|
18
|
+
starts.sort((a, b) => a - b);
|
|
19
|
+
ends.sort((a, b) => a - b);
|
|
20
|
+
const before = (values, time, inclusive) => {
|
|
21
|
+
let low = 0, high = values.length;
|
|
22
|
+
while (low < high) {
|
|
23
|
+
const middle = (low + high) >>> 1;
|
|
24
|
+
if (values[middle] < time || inclusive && values[middle] === time)
|
|
25
|
+
low = middle + 1;
|
|
26
|
+
else
|
|
27
|
+
high = middle;
|
|
28
|
+
}
|
|
29
|
+
return low;
|
|
30
|
+
};
|
|
31
|
+
// Include both departure and arrival instants, including zero-length trips.
|
|
32
|
+
return time => Number.isNaN(time) ? 0 : before(starts, time, true) - before(ends, time, false);
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motionstudies/core",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Transport contracts and motion primitives for Motion Studies.",
|
|
@@ -154,6 +154,11 @@
|
|
|
154
154
|
"types": "./domain/station-departures.d.ts",
|
|
155
155
|
"import": "./domain/station-departures.js",
|
|
156
156
|
"default": "./domain/station-departures.js"
|
|
157
|
+
},
|
|
158
|
+
"./domain/vehicle-counts": {
|
|
159
|
+
"types": "./domain/vehicle-counts.d.ts",
|
|
160
|
+
"import": "./domain/vehicle-counts.js",
|
|
161
|
+
"default": "./domain/vehicle-counts.js"
|
|
157
162
|
}
|
|
158
163
|
},
|
|
159
164
|
"files": [
|