@motionstudies/web 0.1.0-alpha.12 → 0.1.0-alpha.13
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 +12 -0
- package/package.json +7 -2
- package/use-pattern-network-day.d.ts +17 -0
- package/use-pattern-network-day.js +41 -0
- package/use-progressive-air-day.d.ts +1 -0
- package/use-progressive-chunks.d.ts +3 -0
- package/use-progressive-chunks.js +13 -4
- package/use-progressive-network-day.d.ts +1 -0
- package/use-progressive-road-study.d.ts +1 -0
package/README.md
CHANGED
|
@@ -25,6 +25,14 @@ Build release candidates with `npm run build:packages`. Distribution manifests a
|
|
|
25
25
|
|
|
26
26
|
Source workspace manifests always stay private. `npm run check:release` builds public candidates, tests their packed consumer and records the tested tarball hashes; `npm run release:dry-run` inspects the publication without writing to npm. The manual main-branch `release.yml` workflow publishes those same tarballs with public access and provenance. See [release instructions](https://github.com/emmettl/motionstudies/blob/main/docs/RELEASING.md) for bootstrap-token and trusted-publisher setup. All four shared packages are MIT-licensed; each distribution includes `LICENSE`.
|
|
27
27
|
|
|
28
|
+
## Timetable patterns, station calls and observation windows
|
|
29
|
+
|
|
30
|
+
`@motionstudies/core/domain/network-patterns` provides a transport-neutral repeated-schedule codec. `@motionstudies/web/use-pattern-network-day` lazily decodes current/adjacent chunks after verifying byte length and SHA-256; it exposes retry and drops old-source state when the manifest URL changes. Supply stable options and a stable asset resolver. Format/category options support existing edition wire contracts.
|
|
31
|
+
|
|
32
|
+
`@motionstudies/core/domain/station-calls` supplies station calls, independent source status, namespaced source combination and upcoming-call filtering. Source IDs, repeated visits and acquired windows survive combination. Consumers supply interchange IDs and passenger restrictions; matching display names do not collapse services.
|
|
33
|
+
|
|
34
|
+
`@motionstudies/data/observation-windows` compiles bounded acquired records into hashed windows with explicit time semantics, conflict policy and acquisition-gap reporting. It preserves supplied record IDs, values and source references. It uses elapsed UTC seconds, including 23/25-hour study days, and performs no acquisition or inference. See [contracts, limits and parity evidence](../docs/ALLCHANGE-FOUNDATIONS.md).
|
|
35
|
+
|
|
28
36
|
## Now and browser location
|
|
29
37
|
|
|
30
38
|
`useNowClock` from `@motionstudies/web/use-now-clock` follows the wall clock on each animation frame, so returning from a suspended tab catches up immediately. Call `start()` from a Now button, set the consumer's playback rate to 1, and render its `time` while `active`. With `NationalNetworkScene`, pass `isPlaying={false}` while this external clock owns time. Call `stop()` before pausing, seeking or changing speed, and retain the last clock time for ordinary playback. Clear moving vehicle selections on entry to keep the camera still; panning and zooming remain available.
|
|
@@ -217,3 +225,7 @@ The renderer now shares active GPU upload ranges, paused frame reuse, label and
|
|
|
217
225
|
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.
|
|
218
226
|
|
|
219
227
|
For timetable-backed vehicle selections, `NetworkVehicleHeroCard` from `@motionstudies/web/components/NetworkVehicleHeroCard` accepts `snapshot`, `train`, playback `time`, a required source `note` and the vehicle card's presentation/state/label options. Import `vehicle-hero-card.css`. It resolves the selected train ID against the current snapshot, retaining destination identity but showing no stale calls when that ID is absent. Calls stay in journey order, use stable journey/call IDs and follow backward seeking. During dwell the heading becomes `atStopLabel` (default “At stop”) and the time is the departure; otherwise it is the upcoming arrival. Times are formatted with `formatServiceTime` by default, without normalizing after midnight or adding delay twice. Use `showTimes={false}` for frequency-modelled journeys, and explain the model in `note`. The core `vehicleCalls` helper is also exported from `@motionstudies/core/domain/vehicle-calls` for custom adapters. Selection callback IDs refer to calls, not station indexes.
|
|
228
|
+
|
|
229
|
+
### Railway preparation
|
|
230
|
+
|
|
231
|
+
`@motionstudies/data/rail-wtt`, `rail-journeys`, `rail-public-calls`, `rail-routing` and `rail-geometry` expose typed offline railway preparation. XLSX reading, dated source-column audits, UID/date joins, public-call anchors and connected OSM paths preserve source evidence. Editions supply regional identities, calendar exceptions and clipping policy. Node 24+ and `unzip` are required; no Python runtime is shipped. See `docs/SHARED-RAIL-READERS.md` in the source repository for calendar limits and parity evidence.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motionstudies/web",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Browser controls and loaders for Motion Studies.",
|
|
@@ -87,6 +87,11 @@
|
|
|
87
87
|
"import": "./use-progressive-network-day.js",
|
|
88
88
|
"default": "./use-progressive-network-day.js"
|
|
89
89
|
},
|
|
90
|
+
"./use-pattern-network-day": {
|
|
91
|
+
"types": "./use-pattern-network-day.d.ts",
|
|
92
|
+
"import": "./use-pattern-network-day.js",
|
|
93
|
+
"default": "./use-pattern-network-day.js"
|
|
94
|
+
},
|
|
90
95
|
"./use-progressive-road-study": {
|
|
91
96
|
"types": "./use-progressive-road-study.d.ts",
|
|
92
97
|
"import": "./use-progressive-road-study.js",
|
|
@@ -162,7 +167,7 @@
|
|
|
162
167
|
"**/*.css"
|
|
163
168
|
],
|
|
164
169
|
"dependencies": {
|
|
165
|
-
"@motionstudies/core": "0.1.0-alpha.
|
|
170
|
+
"@motionstudies/core": "0.1.0-alpha.13"
|
|
166
171
|
},
|
|
167
172
|
"peerDependencies": {
|
|
168
173
|
"react": "^19.2.8",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PatternChunkOptions } from '@motionstudies/core/domain/network-patterns';
|
|
2
|
+
import type { NetworkDayChunk, NetworkDayChunkDescriptor, NetworkDayManifest } from '@motionstudies/core/domain/network';
|
|
3
|
+
import type { DataUrlResolver } from './data-url.ts';
|
|
4
|
+
export interface PatternNetworkManifest extends NetworkDayManifest {
|
|
5
|
+
readonly format: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function verifiedPatternNetworkChunk(response: Response, descriptor: NetworkDayChunkDescriptor, options?: PatternChunkOptions): Promise<NetworkDayChunk>;
|
|
8
|
+
/** Fetch and decode only the current and adjacent chunks; keep the manifest independent. */
|
|
9
|
+
export declare function usePatternNetworkDay(manifestFile: string, active: boolean, time: number, resolveAssetUrl: DataUrlResolver, options?: PatternChunkOptions): {
|
|
10
|
+
manifest: PatternNetworkManifest | undefined;
|
|
11
|
+
chunkReady: boolean;
|
|
12
|
+
loading: boolean;
|
|
13
|
+
unavailable: boolean;
|
|
14
|
+
error: boolean;
|
|
15
|
+
retry: () => void;
|
|
16
|
+
network: import("@motionstudies/core/domain/network").NetworkSnapshot | undefined;
|
|
17
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { adjacentDayChunks, dayChunkForTime, networkSnapshotForDayChunk } from '@motionstudies/core/domain/network-day';
|
|
3
|
+
import { useProgressiveChunks } from './use-progressive-chunks.js';
|
|
4
|
+
export async function verifiedPatternNetworkChunk(response, descriptor, options = {}) {
|
|
5
|
+
const bytes = await response.arrayBuffer();
|
|
6
|
+
if (!Number.isSafeInteger(descriptor.bytes) || bytes.byteLength !== descriptor.bytes || !/^[a-f0-9]{64}$/.test(descriptor.sha256 ?? ''))
|
|
7
|
+
throw new Error('Pattern chunk integrity metadata missing or size mismatch');
|
|
8
|
+
const hash = [...new Uint8Array(await crypto.subtle.digest('SHA-256', bytes))].map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
9
|
+
if (hash !== descriptor.sha256)
|
|
10
|
+
throw new Error('Pattern chunk integrity mismatch');
|
|
11
|
+
const { decodeNetworkPatterns } = await import('@motionstudies/core/domain/network-patterns');
|
|
12
|
+
const chunk = decodeNetworkPatterns(JSON.parse(new TextDecoder().decode(bytes)), options);
|
|
13
|
+
if (chunk.windowStart !== descriptor.windowStart || chunk.windowEnd !== descriptor.windowEnd || chunk.trains.length !== descriptor.tripCount)
|
|
14
|
+
throw new Error('Pattern chunk descriptor mismatch');
|
|
15
|
+
return chunk;
|
|
16
|
+
}
|
|
17
|
+
/** Fetch and decode only the current and adjacent chunks; keep the manifest independent. */
|
|
18
|
+
export function usePatternNetworkDay(manifestFile, active, time, resolveAssetUrl, options = {}) {
|
|
19
|
+
const format = options.format ?? 'network-patterns-v1', categories = options.categories;
|
|
20
|
+
const adapter = useMemo(() => ({
|
|
21
|
+
chunkForTime: dayChunkForTime, adjacentChunks: adjacentDayChunks, retainAdjacentOnly: true,
|
|
22
|
+
validateManifest(manifest) {
|
|
23
|
+
if (manifest.format !== format || !manifest.metadata || !Array.isArray(manifest.stops) || !Array.isArray(manifest.edges))
|
|
24
|
+
throw new Error('Invalid pattern manifest');
|
|
25
|
+
const ids = new Set();
|
|
26
|
+
let previous = -Infinity;
|
|
27
|
+
for (const chunk of manifest.chunks) {
|
|
28
|
+
if (!chunk.id || ids.has(chunk.id) || !chunk.path || !Number.isFinite(chunk.windowStart) || !Number.isFinite(chunk.windowEnd) ||
|
|
29
|
+
chunk.windowStart < previous || chunk.windowEnd <= chunk.windowStart || !Number.isSafeInteger(chunk.tripCount) || chunk.tripCount < 0 ||
|
|
30
|
+
!Number.isSafeInteger(chunk.bytes) || chunk.bytes < 1 || !/^[a-f0-9]{64}$/.test(chunk.sha256 ?? ''))
|
|
31
|
+
throw new Error('Invalid pattern descriptor');
|
|
32
|
+
ids.add(chunk.id);
|
|
33
|
+
previous = chunk.windowEnd;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
readChunk: (response, descriptor) => verifiedPatternNetworkChunk(response, descriptor, { format, categories }),
|
|
37
|
+
}), [format, categories]);
|
|
38
|
+
const { chunk, ...state } = useProgressiveChunks(manifestFile, active, time, resolveAssetUrl, adapter);
|
|
39
|
+
const network = useMemo(() => state.manifest ? networkSnapshotForDayChunk(state.manifest, chunk) : undefined, [state.manifest, chunk]);
|
|
40
|
+
return { ...state, network };
|
|
41
|
+
}
|
|
@@ -11,6 +11,8 @@ export interface ProgressiveChunkAdapter<Manifest, Descriptor, Chunk> {
|
|
|
11
11
|
readonly adjacentChunks: (manifest: Manifest, current: Descriptor) => readonly Descriptor[];
|
|
12
12
|
readonly readChunk: (response: Response, descriptor: Descriptor) => Promise<Chunk>;
|
|
13
13
|
readonly optional?: boolean;
|
|
14
|
+
readonly validateManifest?: (manifest: Manifest) => void;
|
|
15
|
+
readonly retainAdjacentOnly?: boolean;
|
|
14
16
|
}
|
|
15
17
|
/** Internal lifecycle shared by the network, aircraft and road loaders. */
|
|
16
18
|
export declare function useProgressiveChunks<Descriptor extends ChunkDescriptor, Manifest extends ChunkManifest<Descriptor>, Chunk>(manifestFile: string, active: boolean, time: number, resolveAssetUrl: DataUrlResolver, adapter: ProgressiveChunkAdapter<Manifest, Descriptor, Chunk>): {
|
|
@@ -20,5 +22,6 @@ export declare function useProgressiveChunks<Descriptor extends ChunkDescriptor,
|
|
|
20
22
|
loading: boolean;
|
|
21
23
|
unavailable: boolean;
|
|
22
24
|
error: boolean;
|
|
25
|
+
retry: () => void;
|
|
23
26
|
};
|
|
24
27
|
export {};
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
2
|
/** Internal lifecycle shared by the network, aircraft and road loaders. */
|
|
3
3
|
export function useProgressiveChunks(manifestFile, active, time, resolveAssetUrl, adapter) {
|
|
4
4
|
const key = resolveAssetUrl(manifestFile);
|
|
5
5
|
const [stored, setStored] = useState({ key, chunks: {} });
|
|
6
|
+
const [attempt, setAttempt] = useState(0);
|
|
7
|
+
const retry = useCallback(() => {
|
|
8
|
+
setStored(current => current.key === key ? { ...current, manifestError: false, failedChunk: undefined, unavailable: false } : current);
|
|
9
|
+
setAttempt(value => value + 1);
|
|
10
|
+
}, [key]);
|
|
6
11
|
// Reset during render so no effect (or consumer) sees another source's data.
|
|
7
12
|
// Late responses are also rejected below, independently of fetch cancellation.
|
|
8
13
|
const state = stored.key === key ? stored : { key, chunks: {} };
|
|
@@ -32,6 +37,7 @@ export function useProgressiveChunks(manifestFile, active, time, resolveAssetUrl
|
|
|
32
37
|
if (!Array.isArray(next.chunks) || !next.chunks.length) {
|
|
33
38
|
throw new Error('Day manifest has no chunks');
|
|
34
39
|
}
|
|
40
|
+
adapter.validateManifest?.(next);
|
|
35
41
|
if (!controller.signal.aborted) {
|
|
36
42
|
setStored((current) => current.key === key
|
|
37
43
|
? { ...current, manifest: next, manifestError: false }
|
|
@@ -45,7 +51,7 @@ export function useProgressiveChunks(manifestFile, active, time, resolveAssetUrl
|
|
|
45
51
|
}
|
|
46
52
|
})();
|
|
47
53
|
return () => controller.abort();
|
|
48
|
-
}, [active, adapter, key, manifest, unavailable]);
|
|
54
|
+
}, [active, adapter, attempt, key, manifest, unavailable]);
|
|
49
55
|
useEffect(() => {
|
|
50
56
|
if (!active || !manifest || !descriptor)
|
|
51
57
|
return;
|
|
@@ -66,7 +72,9 @@ export function useProgressiveChunks(manifestFile, active, time, resolveAssetUrl
|
|
|
66
72
|
const next = await adapter.readChunk(response, candidate);
|
|
67
73
|
if (!controller.signal.aborted) {
|
|
68
74
|
setStored((current) => current.key === key
|
|
69
|
-
? { ...current, chunks: { ...
|
|
75
|
+
? { ...current, chunks: { ...(adapter.retainAdjacentOnly
|
|
76
|
+
? Object.fromEntries(Object.entries(current.chunks).filter(([id]) => adapter.adjacentChunks(manifest, descriptor).some(item => item.id === id)))
|
|
77
|
+
: current.chunks), [candidate.id]: next },
|
|
70
78
|
failedChunk: current.failedChunk === candidate.id ? undefined : current.failedChunk }
|
|
71
79
|
: current);
|
|
72
80
|
}
|
|
@@ -79,7 +87,7 @@ export function useProgressiveChunks(manifestFile, active, time, resolveAssetUrl
|
|
|
79
87
|
})();
|
|
80
88
|
}
|
|
81
89
|
return () => controller.abort();
|
|
82
|
-
}, [active, adapter, chunks, descriptor, key, manifest, resolveAssetUrl]);
|
|
90
|
+
}, [active, adapter, attempt, chunks, descriptor, key, manifest, resolveAssetUrl]);
|
|
83
91
|
return {
|
|
84
92
|
manifest,
|
|
85
93
|
chunk,
|
|
@@ -87,5 +95,6 @@ export function useProgressiveChunks(manifestFile, active, time, resolveAssetUrl
|
|
|
87
95
|
loading: active && !unavailable && !error && (!manifest || !chunkReady),
|
|
88
96
|
unavailable: Boolean(unavailable),
|
|
89
97
|
error,
|
|
98
|
+
retry,
|
|
90
99
|
};
|
|
91
100
|
}
|