@faclon-labs/app-sdk 0.1.0 → 0.2.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.
- package/README.md +34 -1
- package/dist/SignIn.js +2 -2
- package/dist/config.d.ts +5 -0
- package/dist/config.js +1 -0
- package/dist/map/globe.d.ts +5 -0
- package/dist/map/globe.js +5 -0
- package/dist/map/index.d.ts +21 -0
- package/dist/map/index.js +21 -0
- package/dist/map/source.d.ts +46 -0
- package/dist/map/source.js +54 -0
- package/dist/map/testing.d.ts +5 -0
- package/dist/map/testing.js +5 -0
- package/dist/session.d.ts +1 -1
- package/dist/session.js +1 -1
- package/package.json +55 -5
- package/vendor/SOURCE.txt +1 -0
- package/vendor/map-engine/cesium/camera.d.ts +10 -0
- package/vendor/map-engine/cesium/camera.js +25 -0
- package/vendor/map-engine/cesium/index.d.ts +2 -0
- package/vendor/map-engine/cesium/index.js +2 -0
- package/vendor/map-engine/cesium/renderer.d.ts +149 -0
- package/vendor/map-engine/cesium/renderer.js +837 -0
- package/vendor/map-engine/commands/index.d.ts +2 -0
- package/vendor/map-engine/commands/index.js +2 -0
- package/vendor/map-engine/commands/post-message.d.ts +32 -0
- package/vendor/map-engine/commands/post-message.js +77 -0
- package/vendor/map-engine/commands/webmcp.d.ts +17 -0
- package/vendor/map-engine/commands/webmcp.js +59 -0
- package/vendor/map-engine/core/commands.d.ts +82 -0
- package/vendor/map-engine/core/commands.js +46 -0
- package/vendor/map-engine/core/emitter.d.ts +9 -0
- package/vendor/map-engine/core/emitter.js +38 -0
- package/vendor/map-engine/core/equal.d.ts +2 -0
- package/vendor/map-engine/core/equal.js +28 -0
- package/vendor/map-engine/core/errors.d.ts +20 -0
- package/vendor/map-engine/core/errors.js +34 -0
- package/vendor/map-engine/core/geometry-family.d.ts +3 -0
- package/vendor/map-engine/core/geometry-family.js +13 -0
- package/vendor/map-engine/core/index.d.ts +4 -0
- package/vendor/map-engine/core/index.js +3 -0
- package/vendor/map-engine/core/types.d.ts +709 -0
- package/vendor/map-engine/core/types.js +7 -0
- package/vendor/map-engine/data/annotations.d.ts +3 -0
- package/vendor/map-engine/data/annotations.js +63 -0
- package/vendor/map-engine/data/contracts.d.ts +172 -0
- package/vendor/map-engine/data/contracts.js +12 -0
- package/vendor/map-engine/data/index.d.ts +4 -0
- package/vendor/map-engine/data/index.js +3 -0
- package/vendor/map-engine/data/schema.d.ts +7 -0
- package/vendor/map-engine/data/schema.js +40 -0
- package/vendor/map-engine/geofence/evaluator.d.ts +35 -0
- package/vendor/map-engine/geofence/evaluator.js +172 -0
- package/vendor/map-engine/geofence/history.d.ts +9 -0
- package/vendor/map-engine/geofence/history.js +19 -0
- package/vendor/map-engine/geofence/index.d.ts +2 -0
- package/vendor/map-engine/geofence/index.js +2 -0
- package/vendor/map-engine/index.d.ts +27 -0
- package/vendor/map-engine/index.js +14 -0
- package/vendor/map-engine/layers/index.d.ts +1 -0
- package/vendor/map-engine/layers/index.js +1 -0
- package/vendor/map-engine/layers/layers.d.ts +49 -0
- package/vendor/map-engine/layers/layers.js +316 -0
- package/vendor/map-engine/maplibre/deck-host.d.ts +105 -0
- package/vendor/map-engine/maplibre/deck-host.js +401 -0
- package/vendor/map-engine/maplibre/icons.d.ts +17 -0
- package/vendor/map-engine/maplibre/icons.js +65 -0
- package/vendor/map-engine/maplibre/index.d.ts +3 -0
- package/vendor/map-engine/maplibre/index.js +2 -0
- package/vendor/map-engine/maplibre/map-like.d.ts +56 -0
- package/vendor/map-engine/maplibre/map-like.js +1 -0
- package/vendor/map-engine/maplibre/renderer.d.ts +173 -0
- package/vendor/map-engine/maplibre/renderer.js +889 -0
- package/vendor/map-engine/maplibre/style.d.ts +84 -0
- package/vendor/map-engine/maplibre/style.js +493 -0
- package/vendor/map-engine/present/decorations.d.ts +25 -0
- package/vendor/map-engine/present/decorations.js +115 -0
- package/vendor/map-engine/present/index.d.ts +2 -0
- package/vendor/map-engine/present/index.js +2 -0
- package/vendor/map-engine/present/present.d.ts +47 -0
- package/vendor/map-engine/present/present.js +149 -0
- package/vendor/map-engine/query/index.d.ts +1 -0
- package/vendor/map-engine/query/index.js +1 -0
- package/vendor/map-engine/query/planner.d.ts +26 -0
- package/vendor/map-engine/query/planner.js +47 -0
- package/vendor/map-engine/react/MapView.d.ts +87 -0
- package/vendor/map-engine/react/MapView.js +270 -0
- package/vendor/map-engine/react/context.d.ts +39 -0
- package/vendor/map-engine/react/context.js +94 -0
- package/vendor/map-engine/react/index.d.ts +2 -0
- package/vendor/map-engine/react/index.js +2 -0
- package/vendor/map-engine/react/store.d.ts +19 -0
- package/vendor/map-engine/react/store.js +18 -0
- package/vendor/map-engine/realtime/apply.d.ts +74 -0
- package/vendor/map-engine/realtime/apply.js +155 -0
- package/vendor/map-engine/realtime/index.d.ts +1 -0
- package/vendor/map-engine/realtime/index.js +1 -0
- package/vendor/map-engine/render/index.d.ts +1 -0
- package/vendor/map-engine/render/index.js +1 -0
- package/vendor/map-engine/render/offline.d.ts +8 -0
- package/vendor/map-engine/render/offline.js +47 -0
- package/vendor/map-engine/render/renderer.d.ts +74 -0
- package/vendor/map-engine/render/renderer.js +1 -0
- package/vendor/map-engine/render/terrarium.d.ts +48 -0
- package/vendor/map-engine/render/terrarium.js +110 -0
- package/vendor/map-engine/runtime/drawing.d.ts +28 -0
- package/vendor/map-engine/runtime/drawing.js +58 -0
- package/vendor/map-engine/runtime/index.d.ts +5 -0
- package/vendor/map-engine/runtime/index.js +2 -0
- package/vendor/map-engine/runtime/offline.d.ts +24 -0
- package/vendor/map-engine/runtime/offline.js +37 -0
- package/vendor/map-engine/runtime/overlays.d.ts +25 -0
- package/vendor/map-engine/runtime/overlays.js +65 -0
- package/vendor/map-engine/runtime/runtime.d.ts +4 -0
- package/vendor/map-engine/runtime/runtime.js +2105 -0
- package/vendor/map-engine/runtime/types.d.ts +407 -0
- package/vendor/map-engine/runtime/types.js +1 -0
- package/vendor/map-engine/scene/index.d.ts +1 -0
- package/vendor/map-engine/scene/index.js +1 -0
- package/vendor/map-engine/scene/scene.d.ts +31 -0
- package/vendor/map-engine/scene/scene.js +216 -0
- package/vendor/map-engine/selection/index.d.ts +1 -0
- package/vendor/map-engine/selection/index.js +1 -0
- package/vendor/map-engine/selection/selection.d.ts +12 -0
- package/vendor/map-engine/selection/selection.js +28 -0
- package/vendor/map-engine/spatial/geojson.d.ts +15 -0
- package/vendor/map-engine/spatial/geojson.js +83 -0
- package/vendor/map-engine/spatial/geometry.d.ts +13 -0
- package/vendor/map-engine/spatial/geometry.js +206 -0
- package/vendor/map-engine/spatial/import.d.ts +27 -0
- package/vendor/map-engine/spatial/import.js +96 -0
- package/vendor/map-engine/spatial/index.d.ts +38 -0
- package/vendor/map-engine/spatial/index.js +37 -0
- package/vendor/map-engine/spatial/line-of-sight.d.ts +35 -0
- package/vendor/map-engine/spatial/line-of-sight.js +43 -0
- package/vendor/map-engine/spatial/measure.d.ts +28 -0
- package/vendor/map-engine/spatial/measure.js +90 -0
- package/vendor/map-engine/spatial/operations.d.ts +21 -0
- package/vendor/map-engine/spatial/operations.js +171 -0
- package/vendor/map-engine/spatial/query.d.ts +17 -0
- package/vendor/map-engine/spatial/query.js +88 -0
- package/vendor/map-engine/spatial/spatial-index.d.ts +24 -0
- package/vendor/map-engine/spatial/spatial-index.js +36 -0
- package/vendor/map-engine/spatial/volume-mesh.d.ts +19 -0
- package/vendor/map-engine/spatial/volume-mesh.js +56 -0
- package/vendor/map-engine/temporal/index.d.ts +3 -0
- package/vendor/map-engine/temporal/index.js +3 -0
- package/vendor/map-engine/temporal/journal.d.ts +20 -0
- package/vendor/map-engine/temporal/journal.js +34 -0
- package/vendor/map-engine/temporal/playback.d.ts +41 -0
- package/vendor/map-engine/temporal/playback.js +80 -0
- package/vendor/map-engine/temporal/tracks.d.ts +33 -0
- package/vendor/map-engine/temporal/tracks.js +119 -0
- package/vendor/map-engine/testing/annotation-store.d.ts +24 -0
- package/vendor/map-engine/testing/annotation-store.js +43 -0
- package/vendor/map-engine/testing/contracts.d.ts +41 -0
- package/vendor/map-engine/testing/contracts.js +328 -0
- package/vendor/map-engine/testing/fixtures-monitoring.d.ts +22 -0
- package/vendor/map-engine/testing/fixtures-monitoring.js +103 -0
- package/vendor/map-engine/testing/fixtures.d.ts +18 -0
- package/vendor/map-engine/testing/fixtures.js +28 -0
- package/vendor/map-engine/testing/index.d.ts +7 -0
- package/vendor/map-engine/testing/index.js +7 -0
- package/vendor/map-engine/testing/mock-ontology.d.ts +13 -0
- package/vendor/map-engine/testing/mock-ontology.js +24 -0
- package/vendor/map-engine/testing/mock-provider.d.ts +97 -0
- package/vendor/map-engine/testing/mock-provider.js +369 -0
- package/vendor/map-engine/testing/test-renderer.d.ts +72 -0
- package/vendor/map-engine/testing/test-renderer.js +153 -0
- package/vendor/map-engine/viewport/index.d.ts +1 -0
- package/vendor/map-engine/viewport/index.js +1 -0
- package/vendor/map-engine/viewport/viewport.d.ts +19 -0
- package/vendor/map-engine/viewport/viewport.js +102 -0
- package/vendor/map-provider-uns/client.d.ts +75 -0
- package/vendor/map-provider-uns/client.js +111 -0
- package/vendor/map-provider-uns/index.d.ts +2 -0
- package/vendor/map-provider-uns/index.js +2 -0
- package/vendor/map-provider-uns/provider.d.ts +131 -0
- package/vendor/map-provider-uns/provider.js +561 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The UNS as a Map Engine data source (requirement §26–28, §52). UNS ONLY: nodes come from the workspace tree,
|
|
3
|
+
* live values from `/timeseries/latest`, history from `/timeseries/range` — always by UNS node id. Legacy device /
|
|
4
|
+
* sensor identifiers in metadata (deviceId, sensorId, latitudeSensor, …) are deprecated and never used.
|
|
5
|
+
*
|
|
6
|
+
* Spatiality is a capability (§9), declared on the node as `metadata.spatial`:
|
|
7
|
+
* - `SpatialGeometry` → an area: `spatial.geometry` (GeoJSON, EPSG:4326)
|
|
8
|
+
* - `SpatialPoint` → a point: `spatial.geometry` or `spatial.latitude/longitude`
|
|
9
|
+
* - `MovingSpatial` → a point whose position comes from the node's child tags whose `metadata.spatialRole` is
|
|
10
|
+
* `latitude` / `longitude` (heading, speed, altitude roles enrich it)
|
|
11
|
+
* - `SpatialTrack` → history; served through getTracks() from the moving nodes' position tags
|
|
12
|
+
* Every other child tag becomes a live property named by its `metadata.semantic` (else its name).
|
|
13
|
+
*
|
|
14
|
+
* The visual plane (docs 28/29) from what the UNS states, never guessed: provenance (workspace, node path, times,
|
|
15
|
+
* version and marking from metadata, a time per property), confidence and an accuracy radius from tags with those
|
|
16
|
+
* spatialRoles, a validity window from metadata, links from `metadata.links` and the spatial parent/child tree, and
|
|
17
|
+
* an event journal from each node's tag history.
|
|
18
|
+
*/
|
|
19
|
+
import { MapEngineError, describeProperties, matchesQuery, rankByRelation, spatial } from '../map-engine/index.js';
|
|
20
|
+
/** A UNS datum as an engine altitude reference (ADR-028); unknown datums give none — never guessed. */
|
|
21
|
+
export function referenceOf(datum) {
|
|
22
|
+
const d = (datum ?? '').trim().toUpperCase();
|
|
23
|
+
if (d === 'MSL' || d === 'AMSL')
|
|
24
|
+
return 'msl';
|
|
25
|
+
if (d === 'AGL')
|
|
26
|
+
return 'agl';
|
|
27
|
+
if (d === 'HAE' || d === 'WGS84' || d === 'ELLIPSOID' || d === 'WGS-84')
|
|
28
|
+
return 'ellipsoid';
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Metadata that identifies legacy wiring or is internal — never shown as an object property. */
|
|
32
|
+
const HIDDEN_META = new Set([
|
|
33
|
+
'spatial', 'telemetryBinding', 'deviceId', 'deviceType', 'sensorId', 'legacyDevID', 'legacyDevTypeID', 'legacySensorID', 'legacySensorCount',
|
|
34
|
+
'commissioning', 'source', 'simulated', 'classChain', 'isDevice', 'timeOperators', 'clusterOperators', 'example', 'dataBindings', 'm', 'c', 'enabled',
|
|
35
|
+
]);
|
|
36
|
+
const POSITION_ROLES = new Set(['latitude', 'longitude']);
|
|
37
|
+
/** Tags read into the object's own fields, not its properties. */
|
|
38
|
+
const QUALITY_ROLES = new Set(['confidence', 'accuracy']);
|
|
39
|
+
const CHUNK = 100;
|
|
40
|
+
/** "AirPlatform" → "Air Platform", "RFStation" → "RF Station". */
|
|
41
|
+
const camelWords = (s) => s.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2').replace(/([a-z\d])([A-Z])/g, '$1 $2');
|
|
42
|
+
/** "AirPlatform" → "Air platforms", "RFStation" → "RF stations" (acronyms keep their capitals). */
|
|
43
|
+
export const pluralLabel = (cls) => {
|
|
44
|
+
const words = camelWords(cls)
|
|
45
|
+
.split(' ')
|
|
46
|
+
.map((w, i) => (w.length > 1 && w === w.toUpperCase() ? w : i === 0 ? w : w.toLowerCase()))
|
|
47
|
+
.join(' ');
|
|
48
|
+
const plural = /(s|x|ch|sh)$/.test(words) ? `${words}es` : /[^aeiou]y$/.test(words) ? `${words.slice(0, -1)}ies` : `${words}s`;
|
|
49
|
+
return plural.charAt(0).toUpperCase() + plural.slice(1).replace(/^(\w+)/, (w) => (w === w.toUpperCase() ? w : w.toLowerCase()));
|
|
50
|
+
};
|
|
51
|
+
function toGeometry(raw) {
|
|
52
|
+
if (!raw || typeof raw !== 'object')
|
|
53
|
+
return undefined;
|
|
54
|
+
try {
|
|
55
|
+
return spatial.fromGeoJSON(raw);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Builds the spatial index of a workspace from its full node rows. Exported for tests. */
|
|
62
|
+
export function indexSpatialNodes(rows) {
|
|
63
|
+
const byParent = new Map();
|
|
64
|
+
for (const r of rows)
|
|
65
|
+
if (r.parentId)
|
|
66
|
+
byParent.set(r.parentId, [...(byParent.get(r.parentId) ?? []), r]);
|
|
67
|
+
const tagsUnder = (id, depth = 0) => (byParent.get(id) ?? []).flatMap((c) => (c.type === 'Tag' ? [c] : depth < 1 && !c.metadata.spatial ? tagsUnder(c.id, depth + 1) : []));
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const n of rows) {
|
|
70
|
+
const s = n.metadata.spatial;
|
|
71
|
+
if (!s || typeof s !== 'object' || n.type === 'Tag' || n.type === 'Class' || n.type === 'Capability')
|
|
72
|
+
continue;
|
|
73
|
+
const capability = String(s.capability ?? '');
|
|
74
|
+
if (capability === 'SpatialTrack')
|
|
75
|
+
continue; // history, served by getTracks
|
|
76
|
+
const tags = tagsUnder(n.id).map((t) => {
|
|
77
|
+
const m = t.metadata;
|
|
78
|
+
const key = typeof m.semantic === 'string' && m.semantic ? m.semantic : t.name;
|
|
79
|
+
return {
|
|
80
|
+
id: t.id,
|
|
81
|
+
key,
|
|
82
|
+
path: t.path,
|
|
83
|
+
...(typeof m.unit === 'string' && m.unit && m.unit !== 'code' ? { unit: m.unit } : {}),
|
|
84
|
+
...(typeof m.spatialRole === 'string' ? { role: m.spatialRole } : {}),
|
|
85
|
+
...(Array.isArray(m.enumValues) ? { enumValues: m.enumValues.map(String) } : {}),
|
|
86
|
+
...(typeof m.valueType === 'string' ? { valueType: m.valueType } : {}),
|
|
87
|
+
...(typeof m.datum === 'string' ? { datum: m.datum } : {}),
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
const latTag = tags.find((t) => t.role === 'latitude');
|
|
91
|
+
const lngTag = tags.find((t) => t.role === 'longitude');
|
|
92
|
+
const altTag = tags.find((t) => t.role === 'altitude');
|
|
93
|
+
const altitudeReference = referenceOf(altTag?.datum);
|
|
94
|
+
let geometry = toGeometry(s.geometry);
|
|
95
|
+
if (!geometry && typeof s.latitude === 'number' && typeof s.longitude === 'number')
|
|
96
|
+
geometry = { type: 'Point', coordinates: { latitude: s.latitude, longitude: s.longitude } };
|
|
97
|
+
const moving = capability === 'MovingSpatial' && latTag !== undefined && lngTag !== undefined;
|
|
98
|
+
if (!moving && !geometry)
|
|
99
|
+
continue; // declares space but says nowhere: not drawable
|
|
100
|
+
const kind = moving ? 'moving' : geometry && geometry.type !== 'Point' && geometry.type !== 'MultiPoint' ? 'area' : s.mobility === 'fixed' ? 'fixed' : 'point';
|
|
101
|
+
const props = { label: n.name };
|
|
102
|
+
for (const [k, v] of Object.entries(n.metadata)) {
|
|
103
|
+
if (HIDDEN_META.has(k) || v === null || v === undefined)
|
|
104
|
+
continue;
|
|
105
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')
|
|
106
|
+
props[k === 'id' ? 'code' : k] = v;
|
|
107
|
+
}
|
|
108
|
+
const ts = n.metadata.timestamp ?? n.metadata.lastSeen;
|
|
109
|
+
out.push({
|
|
110
|
+
node: n,
|
|
111
|
+
ontologyType: typeof n.metadata.ontologyClass === 'string' && n.metadata.ontologyClass ? n.metadata.ontologyClass : n.type,
|
|
112
|
+
kind,
|
|
113
|
+
...(geometry && !moving ? { geometry } : {}),
|
|
114
|
+
tags,
|
|
115
|
+
...(latTag ? { latTag } : {}),
|
|
116
|
+
...(lngTag ? { lngTag } : {}),
|
|
117
|
+
...(altTag && altitudeReference ? { altTag, altitudeReference } : {}),
|
|
118
|
+
props,
|
|
119
|
+
...(typeof ts === 'string' && Number.isFinite(Date.parse(ts)) ? { timestamp: new Date(Date.parse(ts)).toISOString() } : {}),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/** An enum tag's numeric code as its name, when the tag declares the names. */
|
|
125
|
+
function tagValue(tag, v) {
|
|
126
|
+
if (tag.enumValues && Number.isInteger(v) && v >= 0 && v < tag.enumValues.length)
|
|
127
|
+
return tag.enumValues[v];
|
|
128
|
+
if (tag.valueType === 'datetime' && v > 1e11)
|
|
129
|
+
return new Date(v).toISOString();
|
|
130
|
+
return v;
|
|
131
|
+
}
|
|
132
|
+
const isoOf = (v) => (typeof v === 'string' && Number.isFinite(Date.parse(v)) ? new Date(Date.parse(v)).toISOString() : undefined);
|
|
133
|
+
export class UnsSpatialProvider {
|
|
134
|
+
opts;
|
|
135
|
+
capabilities = { supportsSpatialQuery: true, supportsHistoricalQuery: true, supportsRealtime: true, supportsWrite: false };
|
|
136
|
+
nodes = null;
|
|
137
|
+
nodesAt = 0;
|
|
138
|
+
latestCache = new Map();
|
|
139
|
+
/** A read in flight, shared by every caller that needs any of its tags (the UNS answers in seconds). */
|
|
140
|
+
latestInFlight = null;
|
|
141
|
+
subs = new Set();
|
|
142
|
+
timer = null;
|
|
143
|
+
now;
|
|
144
|
+
constructor(opts) {
|
|
145
|
+
this.opts = opts;
|
|
146
|
+
this.now = opts.now ?? (() => Date.now());
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Who is looking (doc 28 G13): the token goes with every UNS call and everything cached is read again, since
|
|
150
|
+
* another user may see other nodes and values. A refused read (HTTP 403) reaches the map as a denied layer.
|
|
151
|
+
*/
|
|
152
|
+
setAuth(auth) {
|
|
153
|
+
this.opts.client.setToken?.(auth?.token ?? null);
|
|
154
|
+
this.nodes = null;
|
|
155
|
+
this.latestCache.clear();
|
|
156
|
+
}
|
|
157
|
+
/** The spatial nodes of the workspace (cached; re-read after discoveryTtlMs). */
|
|
158
|
+
discover(signal) {
|
|
159
|
+
if (!this.nodes || this.now() - this.nodesAt > (this.opts.discoveryTtlMs ?? 300_000)) {
|
|
160
|
+
this.nodesAt = this.now();
|
|
161
|
+
this.nodes = this.readTree(signal).catch((e) => {
|
|
162
|
+
this.nodes = null;
|
|
163
|
+
throw e;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return this.nodes;
|
|
167
|
+
}
|
|
168
|
+
async readTree(signal) {
|
|
169
|
+
const { client, workspace } = this.opts;
|
|
170
|
+
const roots = await client.children(workspace, undefined, signal);
|
|
171
|
+
const rows = (await Promise.all(roots.map((r) => client.subtree(workspace, r.id, signal)))).flat();
|
|
172
|
+
return indexSpatialNodes(rows);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Latest values for tags. The UNS answers a batch in seconds, so a cold read fetches EVERY live tag of the
|
|
176
|
+
* workspace at once (in parallel chunks) and every layer's query is served from that one round; a read already
|
|
177
|
+
* in flight is shared rather than repeated.
|
|
178
|
+
*/
|
|
179
|
+
async latest(tagIds) {
|
|
180
|
+
const ttl = this.opts.latestTtlMs ?? 2500;
|
|
181
|
+
const fresh = (id) => {
|
|
182
|
+
const c = this.latestCache.get(id);
|
|
183
|
+
return c !== undefined && ttl > 0 && this.now() - c.at < ttl;
|
|
184
|
+
};
|
|
185
|
+
const all = this.nodes ? this.liveTagIds(await this.nodes.catch(() => [])) : [];
|
|
186
|
+
// Wait for a round already running; its answers are stamped when they arrive, so they count as fresh here.
|
|
187
|
+
while (this.latestInFlight && tagIds.some((id) => !fresh(id)))
|
|
188
|
+
await this.latestInFlight.catch(() => { });
|
|
189
|
+
// No await from here until the round is registered, so callers arriving together share it.
|
|
190
|
+
if (tagIds.some((id) => !fresh(id))) {
|
|
191
|
+
const need = [...new Set([...tagIds, ...all])].filter((id) => !fresh(id));
|
|
192
|
+
const round = (async () => {
|
|
193
|
+
const chunks = [];
|
|
194
|
+
for (let i = 0; i < need.length; i += CHUNK)
|
|
195
|
+
chunks.push(need.slice(i, i + CHUNK));
|
|
196
|
+
const answers = await Promise.all(chunks.map((c) => this.opts.client.latest(this.opts.workspace, c)));
|
|
197
|
+
const at = this.now();
|
|
198
|
+
for (const v of answers.flat())
|
|
199
|
+
this.latestCache.set(v.nodeId, { at, value: v });
|
|
200
|
+
})();
|
|
201
|
+
this.latestInFlight = round;
|
|
202
|
+
try {
|
|
203
|
+
await round;
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
if (this.latestInFlight === round)
|
|
207
|
+
this.latestInFlight = null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return new Map(tagIds.flatMap((id) => (this.latestCache.has(id) ? [[id, this.latestCache.get(id).value]] : [])));
|
|
211
|
+
}
|
|
212
|
+
/** One spatial node as a SpatialObject, with its live values. Null when a moving node has no position yet. */
|
|
213
|
+
toObject(n, values) {
|
|
214
|
+
const props = { ...n.props, ontologyClass: n.ontologyType };
|
|
215
|
+
let newest = n.timestamp ? Date.parse(n.timestamp) : 0;
|
|
216
|
+
const perProperty = {};
|
|
217
|
+
let confidence;
|
|
218
|
+
let accuracy;
|
|
219
|
+
for (const t of n.tags) {
|
|
220
|
+
if (POSITION_ROLES.has(t.role ?? ''))
|
|
221
|
+
continue;
|
|
222
|
+
const v = values.get(t.id);
|
|
223
|
+
if (!(v?.ok && typeof v.value === 'number'))
|
|
224
|
+
continue;
|
|
225
|
+
if (v.time && v.time > newest)
|
|
226
|
+
newest = v.time;
|
|
227
|
+
// Quality tags feed the object's own fields: confidence as 0…1 (a percentage is scaled), accuracy in metres.
|
|
228
|
+
if (t.role === 'confidence')
|
|
229
|
+
confidence = v.value > 1 ? v.value / 100 : v.value;
|
|
230
|
+
else if (t.role === 'accuracy')
|
|
231
|
+
accuracy = v.value;
|
|
232
|
+
if (QUALITY_ROLES.has(t.role ?? ''))
|
|
233
|
+
continue;
|
|
234
|
+
props[t.key] = tagValue(t, v.value);
|
|
235
|
+
perProperty[t.key] = { ...(t.path ? { source: t.path } : {}), ...(v.time ? { observedAt: new Date(v.time).toISOString() } : {}) };
|
|
236
|
+
}
|
|
237
|
+
let geometry = n.geometry;
|
|
238
|
+
if (n.kind === 'moving') {
|
|
239
|
+
const la = values.get(n.latTag.id);
|
|
240
|
+
const lo = values.get(n.lngTag.id);
|
|
241
|
+
if (!la?.ok || !lo?.ok || typeof la.value !== 'number' || typeof lo.value !== 'number')
|
|
242
|
+
return null;
|
|
243
|
+
const position = { latitude: la.value, longitude: lo.value };
|
|
244
|
+
// Height only with a stated reference (ADR-028).
|
|
245
|
+
const al = n.altTag ? values.get(n.altTag.id) : undefined;
|
|
246
|
+
if (al?.ok && typeof al.value === 'number' && Number.isFinite(al.value))
|
|
247
|
+
position.altitude = al.value;
|
|
248
|
+
if (!spatial.validatePosition(position).valid)
|
|
249
|
+
return null;
|
|
250
|
+
geometry = { type: 'Point', coordinates: position };
|
|
251
|
+
newest = Math.max(newest, la.time ?? 0, lo.time ?? 0);
|
|
252
|
+
}
|
|
253
|
+
if (!geometry)
|
|
254
|
+
return null;
|
|
255
|
+
const m = n.node.metadata;
|
|
256
|
+
const provenance = {
|
|
257
|
+
source: `UNS ${this.opts.workspaceName ?? this.opts.workspace}: ${n.node.path}`,
|
|
258
|
+
...(newest > 0 ? { observedAt: new Date(newest).toISOString() } : {}),
|
|
259
|
+
...(typeof m.version === 'string' || typeof m.version === 'number' ? { version: m.version } : {}),
|
|
260
|
+
...(typeof m.classification === 'string' ? { classification: m.classification } : {}),
|
|
261
|
+
...(Object.keys(perProperty).length ? { properties: perProperty } : {}),
|
|
262
|
+
};
|
|
263
|
+
const validFrom = isoOf(m.validFrom);
|
|
264
|
+
const validUntil = isoOf(m.validUntil ?? m.expiresAt);
|
|
265
|
+
const radius = accuracy ?? (typeof m.spatial?.accuracyMeters === 'number' ? (m.spatial.accuracyMeters) : undefined);
|
|
266
|
+
return {
|
|
267
|
+
objectId: n.node.id,
|
|
268
|
+
geometry,
|
|
269
|
+
properties: props,
|
|
270
|
+
...(newest > 0 ? { observedAt: new Date(newest).toISOString() } : {}),
|
|
271
|
+
...(validFrom ? { validFrom } : {}),
|
|
272
|
+
...(validUntil ? { validUntil } : {}),
|
|
273
|
+
...(confidence !== undefined && Number.isFinite(confidence) ? { confidence: Math.max(0, Math.min(1, confidence)) } : {}),
|
|
274
|
+
...(radius !== undefined && radius > 0 ? { uncertainty: { radiusMeters: radius } } : {}),
|
|
275
|
+
provenance,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
liveTagIds(nodes) {
|
|
279
|
+
return nodes.flatMap((n) => n.tags.map((t) => t.id));
|
|
280
|
+
}
|
|
281
|
+
select(nodes, q) {
|
|
282
|
+
return nodes.filter((n) => (!q.ontologyType || n.ontologyType === q.ontologyType) && (!q.objectIds || q.objectIds.includes(n.node.id)));
|
|
283
|
+
}
|
|
284
|
+
async query(query, signal) {
|
|
285
|
+
// A superseded request stops even when the answer is cached (engine provider contract).
|
|
286
|
+
signal?.throwIfAborted();
|
|
287
|
+
const nodes = this.select(await this.discover(signal), query);
|
|
288
|
+
const values = await this.latest(this.liveTagIds(nodes));
|
|
289
|
+
signal?.throwIfAborted();
|
|
290
|
+
const objects = nodes.map((n) => this.toObject(n, values)).filter((o) => o !== null && matchesQuery(o, query));
|
|
291
|
+
return rankByRelation(objects, query);
|
|
292
|
+
}
|
|
293
|
+
async getCurrentState(objectIds, signal) {
|
|
294
|
+
return this.query({ objectIds }, signal);
|
|
295
|
+
}
|
|
296
|
+
// ---------------------------------------------------------------- realtime (polled; the UNS exposes no stream yet)
|
|
297
|
+
subscribe(query, onPatch, onError) {
|
|
298
|
+
const sub = { query: structuredClone(query), onPatch, onError, last: new Map() };
|
|
299
|
+
this.subs.add(sub);
|
|
300
|
+
// Seed with what the subscriber has: the snapshot it just loaded.
|
|
301
|
+
void this.query(query).then((objs) => objs.forEach((o) => sub.last.set(o.objectId, JSON.stringify(o))), () => { });
|
|
302
|
+
if (!this.timer) {
|
|
303
|
+
const every = this.opts.setInterval ?? ((fn, ms) => setInterval(fn, ms));
|
|
304
|
+
this.timer = every(() => void this.poll(), this.opts.pollMs ?? 3000);
|
|
305
|
+
}
|
|
306
|
+
return () => {
|
|
307
|
+
this.subs.delete(sub);
|
|
308
|
+
if (!this.subs.size && this.timer) {
|
|
309
|
+
(this.opts.clearInterval ?? ((h) => clearInterval(h)))(this.timer);
|
|
310
|
+
this.timer = null;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/** One polling round for every subscriber. Exposed for tests. */
|
|
315
|
+
async poll() {
|
|
316
|
+
if (!this.subs.size)
|
|
317
|
+
return;
|
|
318
|
+
let nodes;
|
|
319
|
+
try {
|
|
320
|
+
nodes = await this.discover();
|
|
321
|
+
}
|
|
322
|
+
catch (e) {
|
|
323
|
+
for (const s of this.subs)
|
|
324
|
+
s.onError?.(e);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const wanted = nodes.filter((n) => [...this.subs].some((s) => this.select([n], s.query).length));
|
|
328
|
+
let values;
|
|
329
|
+
try {
|
|
330
|
+
values = await this.latest(this.liveTagIds(wanted));
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
for (const s of this.subs)
|
|
334
|
+
s.onError?.(e);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
for (const s of [...this.subs]) {
|
|
338
|
+
const upserted = [];
|
|
339
|
+
const removed = [];
|
|
340
|
+
const seen = new Set();
|
|
341
|
+
for (const n of this.select(wanted, s.query)) {
|
|
342
|
+
const o = this.toObject(n, values);
|
|
343
|
+
if (!o || !matchesQuery(o, s.query))
|
|
344
|
+
continue;
|
|
345
|
+
seen.add(o.objectId);
|
|
346
|
+
const key = JSON.stringify(o);
|
|
347
|
+
if (s.last.get(o.objectId) !== key) {
|
|
348
|
+
s.last.set(o.objectId, key);
|
|
349
|
+
upserted.push(o);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const id of [...s.last.keys()]) {
|
|
353
|
+
if (seen.has(id))
|
|
354
|
+
continue;
|
|
355
|
+
s.last.delete(id);
|
|
356
|
+
removed.push(id);
|
|
357
|
+
}
|
|
358
|
+
if (upserted.length || removed.length)
|
|
359
|
+
s.onPatch({ ...(upserted.length ? { upserted } : {}), ...(removed.length ? { removed } : {}) });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// ---------------------------------------------------------------- history
|
|
363
|
+
async getTracks(query, range, signal) {
|
|
364
|
+
signal?.throwIfAborted();
|
|
365
|
+
const from = Date.parse(range.start);
|
|
366
|
+
const to = Date.parse(range.end);
|
|
367
|
+
const moving = this.select(await this.discover(signal), query).filter((n) => n.kind === 'moving');
|
|
368
|
+
const read = (id) => (id ? this.opts.client.range(this.opts.workspace, id, from, to, signal) : Promise.resolve({ nodeId: '', points: [] }));
|
|
369
|
+
const tracks = [];
|
|
370
|
+
// A few nodes at a time: each is two to four range reads.
|
|
371
|
+
for (let i = 0; i < moving.length; i += 4) {
|
|
372
|
+
const batch = await Promise.all(moving.slice(i, i + 4).map(async (n) => {
|
|
373
|
+
const heading = n.tags.find((t) => t.role === 'heading');
|
|
374
|
+
const speed = n.tags.find((t) => t.role === 'speed');
|
|
375
|
+
const [la, lo, hd, sp, al] = await Promise.all([read(n.latTag.id), read(n.lngTag.id), read(heading?.id), read(speed?.id), read(n.altTag?.id)]);
|
|
376
|
+
const lng = new Map(lo.points.map((p) => [p.time, p.value]));
|
|
377
|
+
const alt = new Map(al.points.map((p) => [p.time, p.value]));
|
|
378
|
+
const hdg = new Map(hd.points.map((p) => [p.time, p.value]));
|
|
379
|
+
const spd = new Map(sp.points.map((p) => [p.time, p.value]));
|
|
380
|
+
const points = [];
|
|
381
|
+
for (const p of la.points) {
|
|
382
|
+
const longitude = lng.get(p.time);
|
|
383
|
+
if (longitude === undefined)
|
|
384
|
+
continue;
|
|
385
|
+
if (!spatial.validatePosition({ latitude: p.value, longitude }).valid)
|
|
386
|
+
continue;
|
|
387
|
+
points.push({ timestamp: new Date(p.time).toISOString(), latitude: p.value, longitude, ...(alt.has(p.time) ? { altitude: alt.get(p.time) } : {}), ...(hdg.has(p.time) ? { heading: hdg.get(p.time) } : {}), ...(spd.has(p.time) ? { speed: spd.get(p.time) } : {}) });
|
|
388
|
+
}
|
|
389
|
+
return points.length ? { objectId: n.node.id, points, properties: { ...n.props, ontologyClass: n.ontologyType } } : null;
|
|
390
|
+
}));
|
|
391
|
+
tracks.push(...batch.filter((t) => t !== null));
|
|
392
|
+
}
|
|
393
|
+
return tracks;
|
|
394
|
+
}
|
|
395
|
+
// ---------------------------------------------------------------- journal (doc 29 §7)
|
|
396
|
+
/**
|
|
397
|
+
* The event journal from the UNS's own history: each node's property tags over the range, merged by time into
|
|
398
|
+
* a snapshot at the range start and one entry per change. Positions are left to tracks; areas and fixed points
|
|
399
|
+
* without tags have no history (they are static). A node created inside the range (metadata.createdAt) is not
|
|
400
|
+
* there before it.
|
|
401
|
+
*/
|
|
402
|
+
async getJournal(query, range, signal) {
|
|
403
|
+
signal?.throwIfAborted();
|
|
404
|
+
const from = Date.parse(range.start);
|
|
405
|
+
const to = Date.parse(range.end);
|
|
406
|
+
const nodes = this.select(await this.discover(signal), query).filter((n) => n.tags.some((t) => !POSITION_ROLES.has(t.role ?? '')));
|
|
407
|
+
const values = await this.latest(this.liveTagIds(nodes));
|
|
408
|
+
const out = [];
|
|
409
|
+
for (let i = 0; i < nodes.length; i += 4) {
|
|
410
|
+
const parts = await Promise.all(nodes.slice(i, i + 4).map(async (n) => {
|
|
411
|
+
const tags = n.tags.filter((t) => !POSITION_ROLES.has(t.role ?? ''));
|
|
412
|
+
const ranges = await Promise.all(tags.map((t) => this.opts.client.range(this.opts.workspace, t.id, from, to, signal)));
|
|
413
|
+
const now = this.toObject(n, values);
|
|
414
|
+
if (!now)
|
|
415
|
+
return [];
|
|
416
|
+
// State at each time a tag changed; values carry forward. Start from the first value each tag had.
|
|
417
|
+
const times = [...new Set(ranges.flatMap((r) => r.points.map((p) => p.time)))].sort((a, b) => a - b);
|
|
418
|
+
const current = {};
|
|
419
|
+
tags.forEach((t, k) => {
|
|
420
|
+
const first = ranges[k].points[0];
|
|
421
|
+
if (first)
|
|
422
|
+
current[t.key] = tagValue(t, first.value);
|
|
423
|
+
});
|
|
424
|
+
const created = isoOf(n.node.metadata.createdAt);
|
|
425
|
+
const entries = [];
|
|
426
|
+
const state = (at) => ({ ...now, properties: { ...now.properties, ...current }, observedAt: new Date(at).toISOString() });
|
|
427
|
+
if (!created || Date.parse(created) <= from)
|
|
428
|
+
entries.push({ at: range.start, objectId: n.node.id, kind: 'snapshot', object: state(from) });
|
|
429
|
+
else
|
|
430
|
+
entries.push({ at: created, objectId: n.node.id, kind: 'created', object: state(Date.parse(created)) });
|
|
431
|
+
let last = JSON.stringify(current);
|
|
432
|
+
for (const time of times.slice(0, 2000)) {
|
|
433
|
+
tags.forEach((t, k) => {
|
|
434
|
+
const p = ranges[k].points.find((x) => x.time === time);
|
|
435
|
+
if (p)
|
|
436
|
+
current[t.key] = tagValue(t, p.value);
|
|
437
|
+
});
|
|
438
|
+
const key = JSON.stringify(current);
|
|
439
|
+
if (key === last)
|
|
440
|
+
continue;
|
|
441
|
+
const before = JSON.parse(last);
|
|
442
|
+
const changes = {};
|
|
443
|
+
for (const k of Object.keys(current))
|
|
444
|
+
if (JSON.stringify(before[k]) !== JSON.stringify(current[k]))
|
|
445
|
+
changes[k] = [before[k], current[k]];
|
|
446
|
+
last = key;
|
|
447
|
+
entries.push({ at: new Date(time).toISOString(), objectId: n.node.id, kind: 'updated', object: state(time), changes, source: 'UNS history' });
|
|
448
|
+
}
|
|
449
|
+
return entries;
|
|
450
|
+
}));
|
|
451
|
+
out.push(...parts.flat());
|
|
452
|
+
}
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
// ---------------------------------------------------------------- links (doc 28 G4)
|
|
456
|
+
/**
|
|
457
|
+
* Links the UNS states: `metadata.links` (or `relations`) entries `{ to, type }` naming another node by id or path,
|
|
458
|
+
* and the tree itself — a spatial node under another spatial node is `part-of` it.
|
|
459
|
+
*/
|
|
460
|
+
async getLinks(objectIds, types, signal) {
|
|
461
|
+
signal?.throwIfAborted();
|
|
462
|
+
const nodes = await this.discover(signal);
|
|
463
|
+
const byId = new Map(nodes.map((n) => [n.node.id, n]));
|
|
464
|
+
const byPath = new Map(nodes.map((n) => [n.node.path, n]));
|
|
465
|
+
const wanted = new Set(objectIds);
|
|
466
|
+
const out = [];
|
|
467
|
+
const add = (from, to, type) => {
|
|
468
|
+
if (!to || to === from || (!wanted.has(from.node.id) && !wanted.has(to.node.id)))
|
|
469
|
+
return;
|
|
470
|
+
if (types && !types.includes(type))
|
|
471
|
+
return;
|
|
472
|
+
out.push({ id: `${from.node.id}->${to.node.id}:${type}`, fromId: from.node.id, toId: to.node.id, type });
|
|
473
|
+
};
|
|
474
|
+
for (const n of nodes) {
|
|
475
|
+
const declared = (n.node.metadata.links ?? n.node.metadata.relations);
|
|
476
|
+
if (Array.isArray(declared)) {
|
|
477
|
+
for (const l of declared) {
|
|
478
|
+
const ref = typeof l?.to === 'string' ? l.to : undefined;
|
|
479
|
+
if (ref)
|
|
480
|
+
add(n, byId.get(ref) ?? byPath.get(ref), typeof l.type === 'string' ? l.type : 'related');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// The nearest spatial ancestor (UNS paths nest).
|
|
484
|
+
const parts = n.node.path.split('/');
|
|
485
|
+
for (let k = parts.length - 1; k > 0; k--) {
|
|
486
|
+
const up = byPath.get(parts.slice(0, k).join('/'));
|
|
487
|
+
if (up) {
|
|
488
|
+
add(n, up, 'part-of');
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return out;
|
|
494
|
+
}
|
|
495
|
+
// ---------------------------------------------------------------- catalog + ontology
|
|
496
|
+
async listDatasets(signal) {
|
|
497
|
+
signal?.throwIfAborted();
|
|
498
|
+
const nodes = await this.discover(signal);
|
|
499
|
+
const values = await this.latest(this.liveTagIds(nodes));
|
|
500
|
+
const groups = new Map();
|
|
501
|
+
for (const n of nodes)
|
|
502
|
+
groups.set(n.ontologyType, [...(groups.get(n.ontologyType) ?? []), n]);
|
|
503
|
+
return [...groups.entries()]
|
|
504
|
+
.map(([ontologyType, list]) => {
|
|
505
|
+
const objects = list.map((n) => this.toObject(n, values)).filter((o) => o !== null);
|
|
506
|
+
const kinds = new Set(objects.map((o) => o.geometry.type));
|
|
507
|
+
const area = list.every((n) => n.kind === 'area');
|
|
508
|
+
const units = {};
|
|
509
|
+
for (const n of list)
|
|
510
|
+
for (const t of n.tags)
|
|
511
|
+
if (t.unit && !POSITION_ROLES.has(t.role ?? ''))
|
|
512
|
+
units[t.key] = t.unit;
|
|
513
|
+
const lastDataAt = Math.max(0, ...objects.map((o) => (o.observedAt ? Date.parse(o.observedAt) : 0)));
|
|
514
|
+
const bounds = spatial.unionBounds(objects.map((o) => spatial.bounds(o.geometry)));
|
|
515
|
+
// One reference for the whole class, or none (mixed references must not be drawn as one).
|
|
516
|
+
const refs = new Set(list.filter((n) => n.altTag).map((n) => n.altitudeReference));
|
|
517
|
+
const altitude = refs.size === 1 ? [...refs][0] : undefined;
|
|
518
|
+
const info = {
|
|
519
|
+
ontologyType,
|
|
520
|
+
label: pluralLabel(ontologyType),
|
|
521
|
+
source: `UNS · ${this.opts.workspaceName ?? this.opts.workspace}`,
|
|
522
|
+
geometryType: kinds.size === 1 ? [...kinds][0] : 'Mixed',
|
|
523
|
+
geometryFamily: area ? 'area' : 'point',
|
|
524
|
+
count: list.length,
|
|
525
|
+
properties: describeProperties(objects),
|
|
526
|
+
hasHistory: list.some((n) => n.kind === 'moving'),
|
|
527
|
+
live: list.some((n) => n.tags.length > 0),
|
|
528
|
+
...(Object.keys(units).length ? { units } : {}),
|
|
529
|
+
...(lastDataAt > 0 ? { lastDataAt } : {}),
|
|
530
|
+
...(bounds ? { bounds } : {}),
|
|
531
|
+
...(altitude ? { altitude: { reference: altitude } } : {}),
|
|
532
|
+
};
|
|
533
|
+
return info;
|
|
534
|
+
})
|
|
535
|
+
.sort((a, b) => a.ontologyType.localeCompare(b.ontologyType));
|
|
536
|
+
}
|
|
537
|
+
/** Ontology binding from the declared capabilities (§9, §12): what each class is drawn as. */
|
|
538
|
+
ontology = {
|
|
539
|
+
resolve: async (ontologyType, signal) => {
|
|
540
|
+
const list = (await this.discover(signal)).filter((n) => n.ontologyType === ontologyType);
|
|
541
|
+
if (!list.length)
|
|
542
|
+
return { ontologyType, missing: 'spatial', reason: 'no node of this class declares a spatial capability in the UNS' };
|
|
543
|
+
const area = list.every((n) => n.kind === 'area');
|
|
544
|
+
return {
|
|
545
|
+
ontologyType,
|
|
546
|
+
geometryProperty: 'metadata.spatial',
|
|
547
|
+
geometryType: area ? 'Polygon' : 'Point',
|
|
548
|
+
capabilities: { ...this.capabilities, supportsHistoricalQuery: list.some((n) => n.kind === 'moving') },
|
|
549
|
+
};
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
/** Stops polling. */
|
|
553
|
+
dispose() {
|
|
554
|
+
this.subs.clear();
|
|
555
|
+
if (this.timer)
|
|
556
|
+
(this.opts.clearInterval ?? ((h) => clearInterval(h)))(this.timer);
|
|
557
|
+
this.timer = null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/** Thrown shape for callers that want a typed failure. */
|
|
561
|
+
export const unsError = (message, cause) => new MapEngineError('PROVIDER_ERROR', message, undefined, { cause });
|