@motionstudies/core 0.1.0-alpha.0

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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +26 -0
  3. package/air-search.d.ts +11 -0
  4. package/air-search.js +32 -0
  5. package/domain/air-day.d.ts +34 -0
  6. package/domain/air-day.js +17 -0
  7. package/domain/air.d.ts +48 -0
  8. package/domain/air.js +83 -0
  9. package/domain/airport.d.ts +17 -0
  10. package/domain/airport.js +41 -0
  11. package/domain/boundary.d.ts +14 -0
  12. package/domain/boundary.js +1 -0
  13. package/domain/corridor.d.ts +64 -0
  14. package/domain/corridor.js +1 -0
  15. package/domain/hub.d.ts +57 -0
  16. package/domain/hub.js +137 -0
  17. package/domain/journey.d.ts +19 -0
  18. package/domain/journey.js +23 -0
  19. package/domain/lakes.d.ts +21 -0
  20. package/domain/lakes.js +1 -0
  21. package/domain/map-reference.d.ts +20 -0
  22. package/domain/map-reference.js +1 -0
  23. package/domain/network-day.d.ts +4 -0
  24. package/domain/network-day.js +21 -0
  25. package/domain/network-layers.d.ts +7 -0
  26. package/domain/network-layers.js +131 -0
  27. package/domain/network.d.ts +173 -0
  28. package/domain/network.js +106 -0
  29. package/domain/operations.d.ts +85 -0
  30. package/domain/operations.js +191 -0
  31. package/domain/realtime.d.ts +41 -0
  32. package/domain/realtime.js +116 -0
  33. package/domain/road-day.d.ts +65 -0
  34. package/domain/road-day.js +76 -0
  35. package/domain/road.d.ts +153 -0
  36. package/domain/road.js +78 -0
  37. package/domain/spatial-layout.d.ts +42 -0
  38. package/domain/spatial-layout.js +12 -0
  39. package/domain/train-time-index.d.ts +14 -0
  40. package/domain/train-time-index.js +28 -0
  41. package/edition.d.ts +34 -0
  42. package/edition.js +1 -0
  43. package/package.json +154 -0
  44. package/road-search.d.ts +11 -0
  45. package/road-search.js +24 -0
  46. package/search-navigation.d.ts +2 -0
  47. package/search-navigation.js +14 -0
  48. package/search-text.d.ts +1 -0
  49. package/search-text.js +12 -0
  50. package/theme.d.ts +19 -0
  51. package/theme.js +16 -0
@@ -0,0 +1,191 @@
1
+ function canonicalLineName(value) {
2
+ return value
3
+ .toLocaleLowerCase('en-GB')
4
+ .replace(/\bline\b/g, '')
5
+ .replace(/[^a-z0-9]+/g, '');
6
+ }
7
+ function canonicalDestination(value) {
8
+ return value
9
+ .toLocaleLowerCase('en-GB')
10
+ .replace(/\b(?:underground|rail|dlr|tram) station\b/g, '')
11
+ .replace(/[^a-z0-9]+/g, '');
12
+ }
13
+ export function operationsServiceTime(snapshot, timeZone) {
14
+ const parts = new Intl.DateTimeFormat('en-GB', {
15
+ timeZone,
16
+ hourCycle: 'h23',
17
+ hour: '2-digit',
18
+ minute: '2-digit',
19
+ second: '2-digit',
20
+ }).formatToParts(new Date(snapshot.metadata.scheduledAt));
21
+ const value = (type) => Number(parts.find((part) => part.type === type)?.value ?? 0);
22
+ return value('hour') * 3600 + value('minute') * 60 + value('second');
23
+ }
24
+ export function operationsAgeSeconds(snapshot, now = Date.now()) {
25
+ return Math.max(0, Math.round((now - Date.parse(snapshot.metadata.collectedAt)) / 1000));
26
+ }
27
+ function templateMatch(train, vehicle, stopIndexes) {
28
+ if (canonicalLineName(train.route) !== canonicalLineName(vehicle.lineId) &&
29
+ canonicalLineName(train.route) !== canonicalLineName(vehicle.lineName)) {
30
+ return undefined;
31
+ }
32
+ const anchors = [];
33
+ let afterPosition = -1;
34
+ for (const prediction of vehicle.predictions) {
35
+ const stopIndex = stopIndexes.get(prediction.stopId.toLocaleUpperCase('en-GB'));
36
+ if (stopIndex === undefined)
37
+ continue;
38
+ const position = train.stops.findIndex(([candidate], index) => index > afterPosition && candidate === stopIndex);
39
+ if (position < 0)
40
+ continue;
41
+ anchors.push({ position, secondsToStop: prediction.secondsToStop });
42
+ afterPosition = position;
43
+ }
44
+ if (!anchors.length)
45
+ return undefined;
46
+ const destination = canonicalDestination(vehicle.destinationName ?? '');
47
+ const headsign = canonicalDestination(train.headsign);
48
+ const destinationScore = destination && headsign &&
49
+ (destination === headsign || destination.includes(headsign) || headsign.includes(destination))
50
+ ? 25
51
+ : 0;
52
+ return {
53
+ train,
54
+ anchors,
55
+ score: anchors.length * 100 + destinationScore,
56
+ };
57
+ }
58
+ function timeBetweenAnchors(train, from, to, position, serviceTime) {
59
+ if (from.position === to.position)
60
+ return serviceTime + from.secondsToStop;
61
+ const scheduledFrom = train.stops[from.position]?.[1] ?? from.position;
62
+ const scheduledTo = train.stops[to.position]?.[1] ?? to.position;
63
+ const scheduledAt = train.stops[position]?.[1] ?? position;
64
+ const scheduledDuration = scheduledTo - scheduledFrom;
65
+ const ratio = scheduledDuration > 0
66
+ ? (scheduledAt - scheduledFrom) / scheduledDuration
67
+ : (position - from.position) / (to.position - from.position);
68
+ return Math.round(serviceTime +
69
+ from.secondsToStop +
70
+ (to.secondsToStop - from.secondsToStop) * Math.max(0, Math.min(1, ratio)));
71
+ }
72
+ function observedTrain(match, vehicle, serviceTime) {
73
+ const { train, anchors } = match;
74
+ const first = anchors[0];
75
+ const last = anchors.at(-1);
76
+ const startPosition = Math.max(0, first.position - 1);
77
+ let endPosition = last.position;
78
+ if (endPosition === startPosition) {
79
+ endPosition = Math.min(train.stops.length - 1, startPosition + 1);
80
+ }
81
+ if (endPosition <= startPosition)
82
+ return undefined;
83
+ const anchorByPosition = new Map(anchors.map((anchor) => [anchor.position, anchor]));
84
+ if (startPosition < first.position) {
85
+ const scheduledDuration = Math.max(30, (train.stops[first.position]?.[1] ?? 0) -
86
+ (train.stops[startPosition]?.[2] ?? 0));
87
+ anchorByPosition.set(startPosition, {
88
+ position: startPosition,
89
+ secondsToStop: Math.min(0, first.secondsToStop - scheduledDuration),
90
+ });
91
+ }
92
+ if (!anchorByPosition.has(endPosition)) {
93
+ const scheduledDuration = Math.max(30, (train.stops[endPosition]?.[1] ?? 0) -
94
+ (train.stops[first.position]?.[1] ?? 0));
95
+ anchorByPosition.set(endPosition, {
96
+ position: endPosition,
97
+ secondsToStop: first.secondsToStop + scheduledDuration,
98
+ });
99
+ }
100
+ const sortedAnchors = [...anchorByPosition.values()].sort((left, right) => left.position - right.position);
101
+ const stops = train.stops
102
+ .slice(startPosition, endPosition + 1)
103
+ .map(([stopIndex], offset) => {
104
+ const position = startPosition + offset;
105
+ const exact = anchorByPosition.get(position);
106
+ let seconds = exact ? serviceTime + exact.secondsToStop : serviceTime;
107
+ if (!exact) {
108
+ const nextAnchorIndex = sortedAnchors.findIndex((anchor) => anchor.position > position);
109
+ const nextAnchor = sortedAnchors[nextAnchorIndex];
110
+ const previousAnchor = sortedAnchors[nextAnchorIndex - 1];
111
+ if (previousAnchor && nextAnchor) {
112
+ seconds = timeBetweenAnchors(train, previousAnchor, nextAnchor, position, serviceTime);
113
+ }
114
+ }
115
+ return [stopIndex, seconds, seconds];
116
+ });
117
+ if (stops.length < 2)
118
+ return undefined;
119
+ return {
120
+ id: `observed:${vehicle.id}`,
121
+ route: train.route,
122
+ headsign: vehicle.destinationName?.trim() || train.headsign,
123
+ shortName: vehicle.lineName,
124
+ category: train.category,
125
+ mode: vehicle.modeName || train.mode,
126
+ start: stops[0][1],
127
+ end: stops.at(-1)[2],
128
+ stops,
129
+ pathSegments: train.pathSegments?.slice(startPosition, endPosition),
130
+ operations: {
131
+ kind: 'prediction-derived',
132
+ observedAt: vehicle.observedAt,
133
+ vehicleId: vehicle.id,
134
+ },
135
+ };
136
+ }
137
+ export function projectOperationsOntoNetwork(reference, operations, serviceTime) {
138
+ const stopIndexes = new Map();
139
+ reference.stops.forEach((stop, index) => {
140
+ if (stop[4])
141
+ stopIndexes.set(stop[4].toLocaleUpperCase('en-GB'), index);
142
+ });
143
+ const trainsByLine = new Map();
144
+ for (const train of reference.trains) {
145
+ const line = canonicalLineName(train.route);
146
+ const candidates = trainsByLine.get(line);
147
+ if (candidates)
148
+ candidates.push(train);
149
+ else
150
+ trainsByLine.set(line, [train]);
151
+ }
152
+ const trains = [];
153
+ for (const vehicle of operations.vehicles) {
154
+ const lineKeys = new Set([
155
+ canonicalLineName(vehicle.lineId),
156
+ canonicalLineName(vehicle.lineName),
157
+ ]);
158
+ let match;
159
+ for (const line of lineKeys) {
160
+ for (const train of trainsByLine.get(line) ?? []) {
161
+ const candidate = templateMatch(train, vehicle, stopIndexes);
162
+ if (candidate && (!match || candidate.score > match.score)) {
163
+ match = candidate;
164
+ }
165
+ }
166
+ }
167
+ if (!match)
168
+ continue;
169
+ const train = observedTrain(match, vehicle, serviceTime);
170
+ if (train)
171
+ trains.push(train);
172
+ }
173
+ return {
174
+ serviceTime,
175
+ matchedVehicleCount: trains.length,
176
+ unmatchedVehicleCount: operations.vehicles.length - trains.length,
177
+ snapshot: {
178
+ ...reference,
179
+ metadata: {
180
+ ...reference.metadata,
181
+ windowStart: 0,
182
+ windowEnd: 86_400,
183
+ focusTime: serviceTime,
184
+ retrievedAt: operations.metadata.collectedAt,
185
+ model: operations.metadata.model,
186
+ note: 'Observed arrival predictions projected between matched stops on the static route geometry; not GPS.',
187
+ },
188
+ trains,
189
+ },
190
+ };
191
+ }
@@ -0,0 +1,41 @@
1
+ import type { NetworkSnapshot } from './network.ts';
2
+ export type RealtimeTripRelationship = 'scheduled' | 'cancelled' | 'deleted' | 'added';
3
+ export interface RealtimeStopUpdate {
4
+ readonly stopPosition?: number;
5
+ readonly stopId?: string;
6
+ readonly stopSequence?: number;
7
+ readonly scheduleRelationship?: 'scheduled' | 'skipped' | 'no-data';
8
+ readonly arrivalDelay?: number;
9
+ readonly departureDelay?: number;
10
+ }
11
+ export interface RealtimeTripUpdate {
12
+ readonly tripId: string;
13
+ readonly startDate?: string;
14
+ readonly scheduleRelationship?: RealtimeTripRelationship;
15
+ readonly delaySeconds?: number;
16
+ readonly stopTimeUpdates?: readonly RealtimeStopUpdate[];
17
+ }
18
+ export interface RealtimeSnapshot {
19
+ readonly metadata: {
20
+ readonly kind: 'fixture' | 'live';
21
+ readonly generatedAt: string;
22
+ readonly receivedAt?: string;
23
+ readonly staticFeedVersion: string;
24
+ readonly serviceDate: string;
25
+ readonly sourceUrl: string;
26
+ readonly model: string;
27
+ };
28
+ readonly updates: readonly RealtimeTripUpdate[];
29
+ }
30
+ export interface RealtimeApplication {
31
+ readonly network: NetworkSnapshot;
32
+ readonly compatible: boolean;
33
+ readonly reason?: 'feed-version' | 'service-date';
34
+ readonly summary: {
35
+ readonly adjusted: number;
36
+ readonly cancelled: number;
37
+ readonly skippedStops: number;
38
+ readonly unmatched: number;
39
+ };
40
+ }
41
+ export declare function applyRealtimeSnapshot(network: NetworkSnapshot, realtime: RealtimeSnapshot): RealtimeApplication;
@@ -0,0 +1,116 @@
1
+ const emptySummary = () => ({
2
+ adjusted: 0,
3
+ cancelled: 0,
4
+ skippedStops: 0,
5
+ unmatched: 0,
6
+ });
7
+ function exactStopPosition(network, train, update) {
8
+ if (update.stopPosition !== undefined &&
9
+ Number.isInteger(update.stopPosition) &&
10
+ update.stopPosition >= 0 &&
11
+ update.stopPosition < train.stops.length) {
12
+ return update.stopPosition;
13
+ }
14
+ if (!update.stopId)
15
+ return undefined;
16
+ const position = train.stops.findIndex(([stopIndex]) => network.stops[stopIndex]?.[4] === update.stopId);
17
+ return position < 0 ? undefined : position;
18
+ }
19
+ function adjustTrain(network, train, update, generatedAt) {
20
+ if (update.scheduleRelationship === 'cancelled' ||
21
+ update.scheduleRelationship === 'deleted') {
22
+ return {
23
+ ...train,
24
+ realtime: {
25
+ status: 'cancelled',
26
+ delaySeconds: update.delaySeconds ?? 0,
27
+ skippedStops: 0,
28
+ generatedAt,
29
+ },
30
+ };
31
+ }
32
+ const updatesByPosition = new Map();
33
+ for (const stopUpdate of update.stopTimeUpdates ?? []) {
34
+ const position = exactStopPosition(network, train, stopUpdate);
35
+ if (position !== undefined)
36
+ updatesByPosition.set(position, stopUpdate);
37
+ }
38
+ let carriedDelay = update.delaySeconds ?? 0;
39
+ const retainedOriginalPositions = [];
40
+ const stops = [];
41
+ let skippedStops = 0;
42
+ train.stops.forEach((stop, position) => {
43
+ const stopUpdate = updatesByPosition.get(position);
44
+ if (stopUpdate?.scheduleRelationship === 'skipped') {
45
+ skippedStops += 1;
46
+ return;
47
+ }
48
+ const arrivalDelay = stopUpdate?.arrivalDelay ?? carriedDelay;
49
+ const departureDelay = stopUpdate?.departureDelay ?? arrivalDelay;
50
+ carriedDelay = departureDelay;
51
+ retainedOriginalPositions.push(position);
52
+ stops.push([stop[0], stop[1] + arrivalDelay, stop[2] + departureDelay]);
53
+ });
54
+ const pathSegments = train.pathSegments
55
+ ? retainedOriginalPositions.slice(1).map((position, index) => {
56
+ const previousPosition = retainedOriginalPositions[index];
57
+ return position === previousPosition + 1
58
+ ? (train.pathSegments?.[previousPosition] ?? null)
59
+ : null;
60
+ })
61
+ : undefined;
62
+ const delaySeconds = carriedDelay;
63
+ return {
64
+ ...train,
65
+ start: stops[0]?.[1] ?? train.start + delaySeconds,
66
+ end: stops.at(-1)?.[2] ?? train.end + delaySeconds,
67
+ stops,
68
+ pathSegments,
69
+ realtime: {
70
+ status: 'adjusted',
71
+ delaySeconds,
72
+ skippedStops,
73
+ generatedAt,
74
+ },
75
+ };
76
+ }
77
+ export function applyRealtimeSnapshot(network, realtime) {
78
+ if (realtime.metadata.staticFeedVersion !== network.metadata.feedVersion) {
79
+ return {
80
+ network,
81
+ compatible: false,
82
+ reason: 'feed-version',
83
+ summary: emptySummary(),
84
+ };
85
+ }
86
+ if (realtime.metadata.serviceDate !== network.metadata.serviceDate) {
87
+ return {
88
+ network,
89
+ compatible: false,
90
+ reason: 'service-date',
91
+ summary: emptySummary(),
92
+ };
93
+ }
94
+ const updates = new Map(realtime.updates.map((update) => [update.tripId, update]));
95
+ const matched = new Set();
96
+ const summary = emptySummary();
97
+ const trains = network.trains.map((train) => {
98
+ const update = updates.get(train.id);
99
+ if (!update || update.scheduleRelationship === 'added')
100
+ return train;
101
+ matched.add(train.id);
102
+ const adjusted = adjustTrain(network, train, update, realtime.metadata.generatedAt);
103
+ if (adjusted.realtime?.status === 'cancelled')
104
+ summary.cancelled += 1;
105
+ else
106
+ summary.adjusted += 1;
107
+ summary.skippedStops += adjusted.realtime?.skippedStops ?? 0;
108
+ return adjusted;
109
+ });
110
+ summary.unmatched = realtime.updates.length - matched.size;
111
+ return {
112
+ network: { ...network, trains },
113
+ compatible: true,
114
+ summary,
115
+ };
116
+ }
@@ -0,0 +1,65 @@
1
+ import { type RoadTrafficConditions } from './road.ts';
2
+ export type NationalRoadSiteValue = readonly [
3
+ siteIndex: number,
4
+ lightFlowPerHour: number,
5
+ lightSpeedKmh: number,
6
+ heavyFlowPerHour: number,
7
+ heavySpeedKmh: number
8
+ ];
9
+ export interface NationalRoadSection {
10
+ readonly id: string;
11
+ readonly road: string;
12
+ readonly direction: 'positive' | 'negative';
13
+ readonly fromSiteIndex: number;
14
+ readonly toSiteIndex: number;
15
+ readonly distanceKm: number;
16
+ }
17
+ export interface NationalRoadMinuteChunk {
18
+ readonly windowStart: number;
19
+ readonly windowEnd: number;
20
+ readonly minutes: readonly (readonly [
21
+ time: number,
22
+ values: readonly NationalRoadSiteValue[]
23
+ ])[];
24
+ }
25
+ export interface NationalRoadChunkDescriptor {
26
+ readonly id: string;
27
+ readonly windowStart: number;
28
+ readonly windowEnd: number;
29
+ readonly path: string;
30
+ readonly minuteCount: number;
31
+ readonly valueCount: number;
32
+ }
33
+ export interface NationalRoadStudyManifest {
34
+ readonly metadata: {
35
+ readonly publisher: string;
36
+ readonly serviceDate: string;
37
+ readonly windowStart: number;
38
+ readonly windowEnd: number;
39
+ readonly sourceUrl: string;
40
+ readonly measurementSiteTableVersion: number;
41
+ readonly measurementKind: 'recorded';
42
+ readonly model: string;
43
+ readonly sampleIntervalSeconds: number;
44
+ readonly acceptedSites: number;
45
+ readonly sections: number;
46
+ readonly minimumSiteCoverage: number;
47
+ readonly firstMeasurementTime: string;
48
+ readonly lastMeasurementTime: string;
49
+ readonly completeMinutes: number;
50
+ };
51
+ readonly siteIds: readonly string[];
52
+ readonly sections: readonly NationalRoadSection[];
53
+ readonly chunks: readonly NationalRoadChunkDescriptor[];
54
+ }
55
+ export interface NationalRoadStudySnapshot {
56
+ readonly metadata: NationalRoadStudyManifest['metadata'];
57
+ readonly siteIds: readonly string[];
58
+ readonly sections: readonly NationalRoadSection[];
59
+ readonly minutes: NationalRoadMinuteChunk['minutes'];
60
+ }
61
+ export declare function roadChunkForTime(manifest: NationalRoadStudyManifest, time: number): NationalRoadChunkDescriptor;
62
+ export declare function adjacentRoadChunks(manifest: NationalRoadStudyManifest, current: NationalRoadChunkDescriptor): readonly NationalRoadChunkDescriptor[];
63
+ export declare function roadSnapshotForChunk(manifest: NationalRoadStudyManifest, chunk?: NationalRoadMinuteChunk): NationalRoadStudySnapshot;
64
+ export declare function nationalRoadConditionsAtTime(snapshot: NationalRoadStudySnapshot, siteIndex: number, time: number): RoadTrafficConditions;
65
+ export declare function reconstructedNationalVehicleCount(snapshot: NationalRoadStudySnapshot, time: number, road?: string): number;
@@ -0,0 +1,76 @@
1
+ import { trafficDensity } from './road.js';
2
+ export function roadChunkForTime(manifest, time) {
3
+ const last = manifest.chunks.at(-1);
4
+ const match = manifest.chunks.find((chunk) => time >= chunk.windowStart &&
5
+ (time < chunk.windowEnd || (chunk === last && time <= chunk.windowEnd)));
6
+ return match ?? (time < manifest.metadata.windowStart ? manifest.chunks[0] : last);
7
+ }
8
+ export function adjacentRoadChunks(manifest, current) {
9
+ const index = manifest.chunks.findIndex((chunk) => chunk.id === current.id);
10
+ return manifest.chunks.slice(Math.max(0, index - 1), index + 2);
11
+ }
12
+ export function roadSnapshotForChunk(manifest, chunk) {
13
+ return {
14
+ metadata: manifest.metadata,
15
+ siteIds: manifest.siteIds,
16
+ sections: manifest.sections,
17
+ minutes: chunk?.minutes ?? [],
18
+ };
19
+ }
20
+ function valueConditions(value) {
21
+ return value
22
+ ? {
23
+ lightFlowPerHour: value[1],
24
+ lightSpeedKmh: value[2],
25
+ heavyFlowPerHour: value[3],
26
+ heavySpeedKmh: value[4],
27
+ }
28
+ : {
29
+ lightFlowPerHour: 0,
30
+ lightSpeedKmh: 0,
31
+ heavyFlowPerHour: 0,
32
+ heavySpeedKmh: 0,
33
+ };
34
+ }
35
+ function interpolate(from, to, progress) {
36
+ return from + (to - from) * progress;
37
+ }
38
+ export function nationalRoadConditionsAtTime(snapshot, siteIndex, time) {
39
+ const minutes = snapshot.minutes;
40
+ if (!minutes.length)
41
+ return valueConditions();
42
+ const conditionsAt = (minuteIndex) => valueConditions(minutes[minuteIndex][1].find((value) => value[0] === siteIndex));
43
+ if (time <= minutes[0][0])
44
+ return conditionsAt(0);
45
+ if (time >= minutes.at(-1)[0])
46
+ return conditionsAt(minutes.length - 1);
47
+ let high = 1;
48
+ while (high < minutes.length && minutes[high][0] < time)
49
+ high += 1;
50
+ const low = high - 1;
51
+ const from = conditionsAt(low);
52
+ const to = conditionsAt(high);
53
+ const progress = (time - minutes[low][0]) / (minutes[high][0] - minutes[low][0]);
54
+ return {
55
+ lightFlowPerHour: interpolate(from.lightFlowPerHour, to.lightFlowPerHour, progress),
56
+ lightSpeedKmh: interpolate(from.lightSpeedKmh, to.lightSpeedKmh, progress),
57
+ heavyFlowPerHour: interpolate(from.heavyFlowPerHour, to.heavyFlowPerHour, progress),
58
+ heavySpeedKmh: interpolate(from.heavySpeedKmh, to.heavySpeedKmh, progress),
59
+ };
60
+ }
61
+ export function reconstructedNationalVehicleCount(snapshot, time, road) {
62
+ return Math.round(snapshot.sections.reduce((total, section) => {
63
+ if (road && section.road !== road)
64
+ return total;
65
+ const from = nationalRoadConditionsAtTime(snapshot, section.fromSiteIndex, time);
66
+ const to = nationalRoadConditionsAtTime(snapshot, section.toSiteIndex, time);
67
+ const lightFlow = (from.lightFlowPerHour + to.lightFlowPerHour) / 2;
68
+ const lightSpeed = (from.lightSpeedKmh + to.lightSpeedKmh) / 2;
69
+ const heavyFlow = (from.heavyFlowPerHour + to.heavyFlowPerHour) / 2;
70
+ const heavySpeed = (from.heavySpeedKmh + to.heavySpeedKmh) / 2;
71
+ return (total +
72
+ (trafficDensity(lightFlow, lightSpeed) +
73
+ trafficDensity(heavyFlow, heavySpeed)) *
74
+ section.distanceKm);
75
+ }, 0));
76
+ }
@@ -0,0 +1,153 @@
1
+ export type RoadTrafficSample = readonly [
2
+ time: number,
3
+ lightFlowPerHour: number,
4
+ lightSpeedKmh: number,
5
+ heavyFlowPerHour: number,
6
+ heavySpeedKmh: number
7
+ ];
8
+ export interface RoadTrafficDirection {
9
+ readonly id: string;
10
+ readonly label: string;
11
+ readonly reverse: boolean;
12
+ readonly detectorIds: readonly string[];
13
+ readonly samples: readonly RoadTrafficSample[];
14
+ }
15
+ export interface RoadTrafficCorridor {
16
+ readonly id: string;
17
+ readonly name: string;
18
+ readonly road: string;
19
+ readonly distanceKm: number;
20
+ readonly path: readonly (readonly [longitude: number, latitude: number])[];
21
+ readonly directions: readonly RoadTrafficDirection[];
22
+ }
23
+ export interface RoadTrafficSnapshot {
24
+ readonly metadata: {
25
+ readonly publisher: string;
26
+ readonly serviceDate: string;
27
+ readonly windowStart: number;
28
+ readonly windowEnd: number;
29
+ readonly sourceUrl: string;
30
+ readonly measurementSiteUrl: string;
31
+ readonly measurementSiteTableVersion: number;
32
+ readonly measurementSitePublishedAt: string;
33
+ readonly measurementKind: 'representative-calibration' | 'recorded';
34
+ readonly model: string;
35
+ readonly note: string;
36
+ readonly sampleIntervalSeconds: number;
37
+ readonly visualSampleRate: number;
38
+ readonly recording?: {
39
+ readonly firstMeasurementTime: string;
40
+ readonly lastMeasurementTime: string;
41
+ readonly completeMinutes: number;
42
+ readonly minimumDirectionCoverage: number;
43
+ };
44
+ };
45
+ readonly corridors: readonly RoadTrafficCorridor[];
46
+ }
47
+ export interface RoadTopologyPath {
48
+ readonly id: string;
49
+ readonly road: string;
50
+ readonly axisName: string;
51
+ readonly position: string;
52
+ readonly mainline: boolean;
53
+ readonly points: readonly (readonly [longitude: number, latitude: number])[];
54
+ }
55
+ export interface RoadTopologySite {
56
+ readonly id: string;
57
+ readonly stationId: string;
58
+ readonly direction: 'positive' | 'negative';
59
+ readonly detectorIds: readonly string[];
60
+ readonly carriageways: readonly string[];
61
+ readonly coordinate: readonly [longitude: number, latitude: number];
62
+ readonly match: {
63
+ readonly confidence: 'high' | 'continuity' | 'authoritative' | 'review' | 'unmatched';
64
+ readonly method?: 'neighbouring-counters' | 'federal-tmc';
65
+ readonly tmcLocationCodes?: readonly string[];
66
+ readonly tmcLocationTableVersions?: readonly string[];
67
+ readonly distanceMetres?: number;
68
+ readonly road?: string;
69
+ readonly axisName?: string;
70
+ readonly axisPosition?: string;
71
+ readonly segmentId?: string;
72
+ readonly mainline?: boolean;
73
+ readonly projectedCoordinate?: readonly [longitude: number, latitude: number];
74
+ readonly competingRoad?: string;
75
+ readonly competingDistanceMetres?: number;
76
+ };
77
+ }
78
+ export interface RoadTopologyRoad {
79
+ readonly id: string;
80
+ readonly label: string;
81
+ readonly officialLabel: string;
82
+ readonly description?: string;
83
+ readonly bounds: {
84
+ readonly minLongitude: number;
85
+ readonly maxLongitude: number;
86
+ readonly minLatitude: number;
87
+ readonly maxLatitude: number;
88
+ };
89
+ readonly focus: readonly [longitude: number, latitude: number];
90
+ readonly cameraScale: number;
91
+ readonly pathCount: number;
92
+ readonly stationCount: number;
93
+ readonly directionalSiteCount: number;
94
+ readonly sectionCount: number;
95
+ }
96
+ export interface RoadTopologySection {
97
+ readonly id: string;
98
+ readonly road: string;
99
+ readonly direction: 'positive' | 'negative';
100
+ readonly fromSiteId: string;
101
+ readonly toSiteId: string;
102
+ readonly fromCoordinate: readonly [longitude: number, latitude: number];
103
+ readonly toCoordinate: readonly [longitude: number, latitude: number];
104
+ readonly path?: readonly (readonly [longitude: number, latitude: number])[];
105
+ readonly distanceKm: number;
106
+ }
107
+ export interface RoadTopologySnapshot {
108
+ readonly metadata: {
109
+ readonly publisher: string;
110
+ readonly sourceUrl: string;
111
+ readonly sourceAsset: string;
112
+ readonly sourceCrs: string;
113
+ readonly sourceDate: string;
114
+ readonly sourceUpdated: string;
115
+ readonly measurementSiteUrl: string;
116
+ readonly measurementSiteTableVersion: number;
117
+ readonly measurementSitePublishedAt: string;
118
+ readonly tmcLocationUrl?: string;
119
+ readonly tmcLocationTableVersion?: string;
120
+ readonly model: string;
121
+ readonly coverage: {
122
+ readonly federalStations: number;
123
+ readonly federalDetectorRecords: number;
124
+ readonly usableDirectionalGroups: number;
125
+ readonly matchedDirectionalGroups: number;
126
+ readonly highConfidenceDirectionalGroups: number;
127
+ readonly continuityResolvedDirectionalGroups: number;
128
+ readonly authoritativeResolvedDirectionalGroups: number;
129
+ readonly reviewDirectionalGroups: number;
130
+ readonly unmatchedDirectionalGroups: number;
131
+ readonly matchedStations: number;
132
+ readonly medianMatchDistanceMetres: number;
133
+ readonly p95MatchDistanceMetres: number;
134
+ readonly roads: number;
135
+ readonly axisSegments: number;
136
+ };
137
+ };
138
+ readonly roads: readonly RoadTopologyRoad[];
139
+ readonly paths: readonly RoadTopologyPath[];
140
+ readonly sections: readonly RoadTopologySection[];
141
+ readonly sites: readonly RoadTopologySite[];
142
+ }
143
+ export interface RoadTrafficConditions {
144
+ readonly lightFlowPerHour: number;
145
+ readonly lightSpeedKmh: number;
146
+ readonly heavyFlowPerHour: number;
147
+ readonly heavySpeedKmh: number;
148
+ }
149
+ export declare function roadConditionsAtTime(direction: RoadTrafficDirection, time: number): RoadTrafficConditions;
150
+ export declare function trafficDensity(flowPerHour: number, speedKmh: number): number;
151
+ export declare function reconstructedVehicleCount(snapshot: RoadTrafficSnapshot, time: number): number;
152
+ export declare function visualVehicleCount(flowPerHour: number, speedKmh: number, distanceKm: number, sampleRate: number, maximum: number): number;
153
+ export declare function roadDistanceTravelledKm(direction: RoadTrafficDirection, time: number, vehicle: 'light' | 'heavy'): number;