@motionstudies/data 0.1.0-alpha.3 → 0.1.0-alpha.30
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 +247 -2
- package/adsb-heatmap.d.mts +56 -0
- package/adsb-heatmap.mjs +223 -0
- package/air-endpoints.d.mts +7 -0
- package/air-endpoints.mjs +148 -0
- package/feed-expectations.d.mts +3 -0
- package/feed-expectations.mjs +33 -0
- package/feed-incidents.d.mts +44 -0
- package/feed-incidents.mjs +148 -0
- package/feed-observability.d.mts +68 -0
- package/feed-observability.mjs +154 -0
- package/feed-observer.d.mts +30 -0
- package/feed-observer.mjs +192 -0
- package/network-chunk-cli.d.mts +1 -0
- package/network-chunk-cli.mjs +65 -0
- package/observation-windows.d.mts +38 -0
- package/observation-windows.mjs +76 -0
- package/operational-files.mjs +21 -0
- package/package.json +85 -2
- package/rail-geometry.d.mts +7 -0
- package/rail-geometry.mjs +108 -0
- package/rail-journeys.d.mts +14 -0
- package/rail-journeys.mjs +118 -0
- package/rail-public-calls.d.mts +4 -0
- package/rail-public-calls.mjs +48 -0
- package/rail-routing.d.mts +16 -0
- package/rail-routing.mjs +119 -0
- package/rail-wtt.d.mts +12 -0
- package/rail-wtt.mjs +135 -0
- package/rail-xml.mjs +12 -0
- package/recorder-evidence.d.mts +14 -0
- package/recorder-evidence.mjs +136 -0
- package/recorder-observability.d.mts +3 -0
- package/recorder-observability.mjs +106 -0
- package/release-files.d.mts +14 -0
- package/release-files.mjs +52 -0
- package/release.d.mts +10 -0
- package/release.mjs +62 -0
- package/source-store.d.mts +15 -0
- package/source-store.mjs +160 -0
- package/uk-service-day.d.mts +7 -0
- package/uk-service-day.mjs +49 -0
- package/webtris.d.mts +14 -0
- package/webtris.mjs +67 -0
package/webtris.d.mts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface WebtrisSite {id:string;description:string}
|
|
2
|
+
export interface WebtrisSample {interval:number;periodEnding:string;totalVolume:number|null;averageSpeedMph:number|null;lengthClassCounts:(number|null)[];classTotalMatches:boolean|null}
|
|
3
|
+
export interface WebtrisReport<S extends WebtrisSite=WebtrisSite> {
|
|
4
|
+
schemaVersion:1;kind:'observed-detector-report-audit';serviceDate:string;
|
|
5
|
+
clock:{basis:string;timezone:null;utcMapping:string;nominalIntervals:96;completeness:string};
|
|
6
|
+
lengthClasses:string[];representation:string;
|
|
7
|
+
audit:{inputRows:number;foreignSite:number;foreignDate:number;invalidInterval:number;exactDuplicates:number;conflictingIntervals:number};
|
|
8
|
+
observations:{site:S;samples:WebtrisSample[];reportIntervals:number;validVolumeIntervals:number;validSpeedIntervals:number;missingNominalIntervals:number[];conflictingIntervals:number[];classTotalMismatches:number}[];
|
|
9
|
+
}
|
|
10
|
+
export function reportUrl(siteIds:readonly string[],serviceDate:string,page:number,pageSize?:number):string
|
|
11
|
+
/** Source slots and labels are retained. No UTC placement, flow-rate conversion or class inference. */
|
|
12
|
+
export function normalizeReports<S extends WebtrisSite>(rows:readonly Record<string,unknown>[],sites:readonly S[],serviceDate:string):WebtrisReport<S>
|
|
13
|
+
/** Pass stable source-record references (for example capture SHA-256 identities). */
|
|
14
|
+
export function aggregateRoadSeries(observation:WebtrisReport['observations'][number],serviceDate:string,dataset:string,sourceRecordIds:readonly string[]):import('@motionstudies/core/domain/aggregate-road').AggregateRoadSeries
|
package/webtris.mjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { checkedDate } from './uk-service-day.mjs'
|
|
2
|
+
|
|
3
|
+
const ENDPOINT = 'https://webtris.nationalhighways.co.uk/api/v1.0/reports/daily'
|
|
4
|
+
const CLASSES = ['0 - 520 cm', '521 - 660 cm', '661 - 1160 cm', '1160+ cm']
|
|
5
|
+
const number = (v, integer = false) => {
|
|
6
|
+
if (!['number', 'string'].includes(typeof v) || String(v).trim() === '') return null
|
|
7
|
+
const n = Number(v)
|
|
8
|
+
return Number.isFinite(n) && n >= 0 && (!integer || Number.isInteger(n)) ? n : null
|
|
9
|
+
}
|
|
10
|
+
export function reportUrl(siteIds, serviceDate, page, pageSize = 1000) {
|
|
11
|
+
checkedDate(serviceDate)
|
|
12
|
+
if (!siteIds.length || siteIds.length > 10 || siteIds.some(id => !/^\d+$/.test(id)) || new Set(siteIds).size !== siteIds.length) throw new Error('Select 1..10 distinct numeric site IDs')
|
|
13
|
+
if (!Number.isSafeInteger(page) || page < 1 || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 1000) throw new Error('Expected a positive page and page size of 1..1000')
|
|
14
|
+
const date = serviceDate.split('-').reverse().join('')
|
|
15
|
+
return `${ENDPOINT}?${new URLSearchParams({ sites: siteIds.join(','), start_date: date, end_date: date, page: String(page), page_size: String(pageSize) }).toString().replaceAll('%2C', ',')}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeReports(rows, sites, serviceDate) {
|
|
19
|
+
checkedDate(serviceDate)
|
|
20
|
+
if (!Array.isArray(rows) || !Array.isArray(sites) || sites.some(s => !s || typeof s.id !== 'string' || typeof s.description !== 'string') || new Set(sites.map(s => s.id)).size !== sites.length) throw new Error('Expected rows and unique named sites')
|
|
21
|
+
const names = new Map(sites.map(site => [site.description, site]))
|
|
22
|
+
if (names.size !== sites.length) throw new Error('Selected site descriptions are ambiguous')
|
|
23
|
+
const grouped = new Map(sites.map(site => [site.id, new Map()]))
|
|
24
|
+
const audit = { inputRows: rows.length, foreignSite: 0, foreignDate: 0, invalidInterval: 0, exactDuplicates: 0, conflictingIntervals: 0 }
|
|
25
|
+
for (const row of rows) {
|
|
26
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) throw new Error('Invalid WebTRIS row')
|
|
27
|
+
const site = names.get(row['Site Name'])
|
|
28
|
+
if (!site) { audit.foreignSite++; continue }
|
|
29
|
+
if (row['Report Date'] !== `${serviceDate}T00:00:00`) { audit.foreignDate++; continue }
|
|
30
|
+
const interval = number(row['Time Interval'], true)
|
|
31
|
+
const ending = row['Time Period Ending']
|
|
32
|
+
// Preserve the source's minute-inclusive ending label (00:14 etc). No invented UTC offset.
|
|
33
|
+
if (interval === null || interval > 99 || !/^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/.test(ending ?? '')) { audit.invalidInterval++; continue }
|
|
34
|
+
const sample = { interval, periodEnding: ending, totalVolume: number(row['Total Volume'], true), averageSpeedMph: number(row['Avg mph']), lengthClassCounts: CLASSES.map(key => number(row[key], true)) }
|
|
35
|
+
sample.classTotalMatches = sample.totalVolume === null || sample.lengthClassCounts.includes(null) ? null : sample.lengthClassCounts.reduce((a,b) => a+b,0) === sample.totalVolume
|
|
36
|
+
const map = grouped.get(site.id)
|
|
37
|
+
if (map.has(interval)) {
|
|
38
|
+
if (JSON.stringify(map.get(interval)) === JSON.stringify(sample)) audit.exactDuplicates++
|
|
39
|
+
else { if (map.get(interval) !== null) audit.conflictingIntervals++; map.set(interval, null) }
|
|
40
|
+
} else map.set(interval, sample)
|
|
41
|
+
}
|
|
42
|
+
const observations = sites.map(site => {
|
|
43
|
+
const map = grouped.get(site.id)
|
|
44
|
+
const samples = [...map.values()].filter(Boolean).sort((a,b) => a.interval-b.interval)
|
|
45
|
+
return { site, samples, reportIntervals: samples.length, validVolumeIntervals: samples.filter(x => x.totalVolume !== null).length, validSpeedIntervals: samples.filter(x => x.averageSpeedMph !== null).length,
|
|
46
|
+
missingNominalIntervals: Array.from({ length: 96 }, (_,i) => i).filter(i => !map.get(i)),
|
|
47
|
+
conflictingIntervals: [...map].filter(([,v]) => v === null).map(([i])=>i).sort((a,b)=>a-b),
|
|
48
|
+
classTotalMismatches: samples.filter(x => x.classTotalMatches === false).length }
|
|
49
|
+
})
|
|
50
|
+
return { schemaVersion: 1, kind: 'observed-detector-report-audit', serviceDate, clock: { basis: 'Provider report date, interval index and period-ending label', timezone: null, utcMapping: 'Unverified; not placed on the scene clock', nominalIntervals: 96, completeness: '96-slot reference only; DST and source time semantics require verification' }, lengthClasses: CLASSES, audit, observations,
|
|
51
|
+
representation: 'Point detector counts and reported mph; no reconstructed vehicles, light/heavy inference or gap interpolation' }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
/** Translate source observations without placing their unverified clock onto UTC. */
|
|
56
|
+
export function aggregateRoadSeries(observation, serviceDate, dataset, sourceRecordIds) {
|
|
57
|
+
checkedDate(serviceDate)
|
|
58
|
+
if (!dataset || !observation?.site?.id || !Array.isArray(sourceRecordIds) || !sourceRecordIds.length || sourceRecordIds.some(id => typeof id !== 'string' || !id)) throw new Error('Aggregate evidence requires dataset, site and source records')
|
|
59
|
+
return {
|
|
60
|
+
id: JSON.stringify(['webtris', dataset, observation.site.id]), provider: 'webtris', dataset, siteId: observation.site.id,
|
|
61
|
+
samples: observation.samples.map(s => ({
|
|
62
|
+
interval: { kind: 'provider-slot', serviceDate, index: s.interval, endingLabel: s.periodEnding },
|
|
63
|
+
vehicleCount: s.totalVolume, averageSpeed: s.averageSpeedMph === null ? null : { value: s.averageSpeedMph, unit: 'mph' },
|
|
64
|
+
sourceRecordIds: [...sourceRecordIds],
|
|
65
|
+
})),
|
|
66
|
+
}
|
|
67
|
+
}
|