@faclon-labs/app-sdk 0.1.0 → 0.3.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 +39 -1
- package/dist/IOSenseApp.js +26 -6
- package/dist/SignIn.js +2 -2
- package/dist/auth.d.ts +7 -0
- package/dist/auth.js +35 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- 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 +48 -0
- package/dist/map/source.js +73 -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,97 @@
|
|
|
1
|
+
import type { Position, SpatialObject, SpatialQuery, TimeRange, Track, TrackPoint, Unsubscribe } from '../core/types.js';
|
|
2
|
+
import type { AuthAware, AuthContext, DataCapabilities, DatasetCatalog, DatasetInfo, JournalEntry, JournalProvider, Link, LinkProvider, RealtimeProvider, SpatialPatch, TrackProvider, WritableSpatialProvider } from '../data/contracts.js';
|
|
3
|
+
export interface MockRecord {
|
|
4
|
+
object: SpatialObject;
|
|
5
|
+
ontologyType?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface MockProviderOptions {
|
|
8
|
+
/** Simulated network latency per call. */
|
|
9
|
+
latencyMs?: number;
|
|
10
|
+
capabilities?: Partial<DataCapabilities>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* In-memory provider for tests and io-maps. It plays the role of an already-authorized server runtime
|
|
14
|
+
* (it does no authorization itself), including realtime: every write notifies the subscriptions it affects,
|
|
15
|
+
* asynchronously, like a stream would.
|
|
16
|
+
*/
|
|
17
|
+
export declare class MockSpatialProvider implements WritableSpatialProvider, RealtimeProvider, TrackProvider, DatasetCatalog, LinkProvider, JournalProvider, AuthAware {
|
|
18
|
+
readonly capabilities: DataCapabilities;
|
|
19
|
+
private readonly records;
|
|
20
|
+
private latencyMs;
|
|
21
|
+
private failures;
|
|
22
|
+
/** Every query received, for assertions. */
|
|
23
|
+
readonly queries: SpatialQuery[];
|
|
24
|
+
private readonly subscriptions;
|
|
25
|
+
private readonly datasetMeta;
|
|
26
|
+
/**
|
|
27
|
+
* Every write as a journal entry (doc 29 §7), so replay can rebuild whole objects — per object, the newest
|
|
28
|
+
* JOURNAL_CAP kept. Writing is cheap (the stored object is shared, changes are worked out when read): live
|
|
29
|
+
* feeds write thousands of times a second.
|
|
30
|
+
*/
|
|
31
|
+
private readonly journal;
|
|
32
|
+
private readonly linkList;
|
|
33
|
+
/** The identity the engine handed over (doc 28 G13). */
|
|
34
|
+
auth: AuthContext | null;
|
|
35
|
+
private deniedTypes;
|
|
36
|
+
private redaction;
|
|
37
|
+
/** Position history per object (kept after removal, so replay can still show it). */
|
|
38
|
+
private readonly history;
|
|
39
|
+
constructor(records?: Array<MockRecord | SpatialObject>, options?: MockProviderOptions);
|
|
40
|
+
setAuth(auth: AuthContext | null): void;
|
|
41
|
+
/** Test and demo hook: queries for these types are refused (ACCESS_DENIED), as a server would for this user. */
|
|
42
|
+
denyTypes(types: string[]): void;
|
|
43
|
+
/** Test and demo hook: these properties are removed (and listed in `redacted`) unless the user has the role. */
|
|
44
|
+
redactFields(fields: string[], unlessRole?: string): void;
|
|
45
|
+
private visible;
|
|
46
|
+
private refuse;
|
|
47
|
+
setLatency(ms: number): void;
|
|
48
|
+
/** The next query (or getCurrentState) rejects with this error. */
|
|
49
|
+
failNext(error?: Error): void;
|
|
50
|
+
query(query: SpatialQuery, signal?: AbortSignal): Promise<SpatialObject[]>;
|
|
51
|
+
private matches;
|
|
52
|
+
getCurrentState(objectIds: string[], signal?: AbortSignal): Promise<SpatialObject[]>;
|
|
53
|
+
upsert(object: SpatialObject, meta?: {
|
|
54
|
+
ontologyType?: string;
|
|
55
|
+
}): Promise<void>;
|
|
56
|
+
remove(objectId: string): Promise<void>;
|
|
57
|
+
/** The stored record is a private copy and is replaced (never changed) on the next write, so it is shared here. */
|
|
58
|
+
private write;
|
|
59
|
+
private addEntry;
|
|
60
|
+
/** Adds journal entries (demos and tests): past states and changes, merged with what writes recorded. */
|
|
61
|
+
seedJournal(entries: Array<JournalEntry & {
|
|
62
|
+
ontologyType?: string;
|
|
63
|
+
}>): void;
|
|
64
|
+
getJournal(query: SpatialQuery, range: TimeRange, signal?: AbortSignal): Promise<JournalEntry[]>;
|
|
65
|
+
/** Adds links between objects (demos and tests). */
|
|
66
|
+
seedLinks(links: Link[]): void;
|
|
67
|
+
getLinks(objectIds: string[], types: string[] | undefined, signal?: AbortSignal): Promise<Link[]>;
|
|
68
|
+
/** Labels a dataset for the catalog, or hides it (e.g. app-internal draft types). */
|
|
69
|
+
describeDataset(ontologyType: string, meta: {
|
|
70
|
+
label?: string;
|
|
71
|
+
source?: string;
|
|
72
|
+
hidden?: boolean;
|
|
73
|
+
altitude?: DatasetInfo['altitude'];
|
|
74
|
+
}): void;
|
|
75
|
+
/** Every ontology type held, with geometry, count and property schema — discovered from the records. */
|
|
76
|
+
listDatasets(signal?: AbortSignal): Promise<DatasetInfo[]>;
|
|
77
|
+
/** Replaces every object of a type (an import, a re-sync). Subscribers see the removals and upserts. */
|
|
78
|
+
setDataset(ontologyType: string, objects: SpatialObject[]): Promise<void>;
|
|
79
|
+
removeDataset(ontologyType: string): Promise<void>;
|
|
80
|
+
/** Convenience for simulations: move a point and stamp the observation time. */
|
|
81
|
+
move(objectId: string, position: Position, observedAt?: string): Promise<void>;
|
|
82
|
+
subscribe(query: SpatialQuery, onPatch: (p: SpatialPatch) => void, onError?: (e: Error) => void): Unsubscribe;
|
|
83
|
+
/** Test hook: break every open stream (the engine should reconnect with a fresh snapshot). */
|
|
84
|
+
failStreams(error?: Error): void;
|
|
85
|
+
subscriptionCount(): number;
|
|
86
|
+
private notify;
|
|
87
|
+
/** Adds past positions for an object (demos and tests). Points merge with recorded history by time. */
|
|
88
|
+
seedHistory(objectId: string, points: TrackPoint[], meta?: {
|
|
89
|
+
ontologyType?: string;
|
|
90
|
+
properties?: Record<string, unknown>;
|
|
91
|
+
}): void;
|
|
92
|
+
getTracks(query: SpatialQuery, range: TimeRange, signal?: AbortSignal): Promise<Track[]>;
|
|
93
|
+
/** Every point position with a timestamp becomes history. */
|
|
94
|
+
private record;
|
|
95
|
+
/** Synchronous view for tests. Not part of any provider contract. */
|
|
96
|
+
size(): number;
|
|
97
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { MapEngineError } from '../core/errors.js';
|
|
2
|
+
import { describeProperties } from '../data/schema.js';
|
|
3
|
+
import { geometryFamily } from '../core/geometry-family.js';
|
|
4
|
+
import { validateGeometry } from '../spatial/geometry.js';
|
|
5
|
+
import { boundsContainPosition, geometryBounds, unionBounds } from '../spatial/geometry.js';
|
|
6
|
+
import { matchesQuery, rankByRelation } from '../spatial/query.js';
|
|
7
|
+
/** Journal entries kept per object (the newest). */
|
|
8
|
+
const JOURNAL_CAP = 2000;
|
|
9
|
+
function diffProps(a, b) {
|
|
10
|
+
const out = {};
|
|
11
|
+
const x = a ?? {};
|
|
12
|
+
const y = b ?? {};
|
|
13
|
+
for (const k of new Set([...Object.keys(x), ...Object.keys(y)]))
|
|
14
|
+
if (JSON.stringify(x[k]) !== JSON.stringify(y[k]))
|
|
15
|
+
out[k] = [x[k], y[k]];
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
const abortError = () => Object.assign(new Error('The operation was aborted.'), { name: 'AbortError' });
|
|
19
|
+
function delay(ms, signal) {
|
|
20
|
+
if (signal?.aborted)
|
|
21
|
+
return Promise.reject(abortError());
|
|
22
|
+
if (ms <= 0)
|
|
23
|
+
return Promise.resolve();
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const t = setTimeout(resolve, ms);
|
|
26
|
+
signal?.addEventListener('abort', () => {
|
|
27
|
+
clearTimeout(t);
|
|
28
|
+
reject(abortError());
|
|
29
|
+
}, { once: true });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* In-memory provider for tests and io-maps. It plays the role of an already-authorized server runtime
|
|
34
|
+
* (it does no authorization itself), including realtime: every write notifies the subscriptions it affects,
|
|
35
|
+
* asynchronously, like a stream would.
|
|
36
|
+
*/
|
|
37
|
+
export class MockSpatialProvider {
|
|
38
|
+
capabilities;
|
|
39
|
+
records = new Map();
|
|
40
|
+
latencyMs;
|
|
41
|
+
failures = [];
|
|
42
|
+
/** Every query received, for assertions. */
|
|
43
|
+
queries = [];
|
|
44
|
+
subscriptions = new Set();
|
|
45
|
+
datasetMeta = new Map();
|
|
46
|
+
/**
|
|
47
|
+
* Every write as a journal entry (doc 29 §7), so replay can rebuild whole objects — per object, the newest
|
|
48
|
+
* JOURNAL_CAP kept. Writing is cheap (the stored object is shared, changes are worked out when read): live
|
|
49
|
+
* feeds write thousands of times a second.
|
|
50
|
+
*/
|
|
51
|
+
journal = new Map();
|
|
52
|
+
linkList = [];
|
|
53
|
+
/** The identity the engine handed over (doc 28 G13). */
|
|
54
|
+
auth = null;
|
|
55
|
+
deniedTypes = new Set();
|
|
56
|
+
redaction = null;
|
|
57
|
+
/** Position history per object (kept after removal, so replay can still show it). */
|
|
58
|
+
history = new Map();
|
|
59
|
+
constructor(records = [], options = {}) {
|
|
60
|
+
this.latencyMs = options.latencyMs ?? 0;
|
|
61
|
+
this.capabilities = { supportsSpatialQuery: true, supportsHistoricalQuery: true, supportsRealtime: true, supportsWrite: true, ...options.capabilities };
|
|
62
|
+
for (const r of records) {
|
|
63
|
+
const rec = 'object' in r ? r : { object: r };
|
|
64
|
+
this.records.set(rec.object.objectId, structuredClone(rec));
|
|
65
|
+
this.record(rec);
|
|
66
|
+
this.write(rec, undefined, rec.object.observedAt ?? '1970-01-01T00:00:00.000Z');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ---------------------------------------------------------------- identity (doc 28 G13)
|
|
70
|
+
setAuth(auth) {
|
|
71
|
+
this.auth = auth ? structuredClone(auth) : null;
|
|
72
|
+
}
|
|
73
|
+
/** Test and demo hook: queries for these types are refused (ACCESS_DENIED), as a server would for this user. */
|
|
74
|
+
denyTypes(types) {
|
|
75
|
+
this.deniedTypes = new Set(types);
|
|
76
|
+
}
|
|
77
|
+
/** Test and demo hook: these properties are removed (and listed in `redacted`) unless the user has the role. */
|
|
78
|
+
redactFields(fields, unlessRole) {
|
|
79
|
+
this.redaction = fields.length ? { fields: [...fields], ...(unlessRole ? { unlessRole } : {}) } : null;
|
|
80
|
+
}
|
|
81
|
+
visible(o) {
|
|
82
|
+
const r = this.redaction;
|
|
83
|
+
if (!r || (r.unlessRole && this.auth?.roles?.includes(r.unlessRole)))
|
|
84
|
+
return o;
|
|
85
|
+
const hidden = r.fields.filter((f) => o.properties && f in o.properties);
|
|
86
|
+
if (!hidden.length)
|
|
87
|
+
return o;
|
|
88
|
+
const properties = { ...o.properties };
|
|
89
|
+
for (const f of hidden)
|
|
90
|
+
delete properties[f];
|
|
91
|
+
return { ...o, properties, redacted: [...new Set([...(o.redacted ?? []), ...hidden])] };
|
|
92
|
+
}
|
|
93
|
+
refuse(ontologyType) {
|
|
94
|
+
if (ontologyType && this.deniedTypes.has(ontologyType))
|
|
95
|
+
throw new MapEngineError('ACCESS_DENIED', `${ontologyType} is not available to this user`, { ontologyType });
|
|
96
|
+
}
|
|
97
|
+
setLatency(ms) {
|
|
98
|
+
this.latencyMs = ms;
|
|
99
|
+
}
|
|
100
|
+
/** The next query (or getCurrentState) rejects with this error. */
|
|
101
|
+
failNext(error = new Error('mock provider failure')) {
|
|
102
|
+
this.failures.push(error);
|
|
103
|
+
}
|
|
104
|
+
async query(query, signal) {
|
|
105
|
+
this.queries.push(structuredClone(query));
|
|
106
|
+
await delay(this.latencyMs, signal);
|
|
107
|
+
const failure = this.failures.shift();
|
|
108
|
+
if (failure)
|
|
109
|
+
throw failure;
|
|
110
|
+
if (!this.capabilities.supportsSpatialQuery && (query.spatial || query.bounds)) {
|
|
111
|
+
throw new Error('this provider cannot evaluate spatial predicates (supportsSpatialQuery: false)');
|
|
112
|
+
}
|
|
113
|
+
this.refuse(query.ontologyType);
|
|
114
|
+
const hits = [...this.records.values()].filter((r) => this.matches(r, query) && !(r.ontologyType && this.deniedTypes.has(r.ontologyType))).map((r) => r.object);
|
|
115
|
+
return rankByRelation(hits, query).map((o) => structuredClone(this.visible(o)));
|
|
116
|
+
}
|
|
117
|
+
matches(r, query) {
|
|
118
|
+
return (!query.ontologyType || r.ontologyType === query.ontologyType) && matchesQuery(r.object, query);
|
|
119
|
+
}
|
|
120
|
+
async getCurrentState(objectIds, signal) {
|
|
121
|
+
await delay(this.latencyMs, signal);
|
|
122
|
+
const failure = this.failures.shift();
|
|
123
|
+
if (failure)
|
|
124
|
+
throw failure;
|
|
125
|
+
return objectIds.flatMap((id) => {
|
|
126
|
+
const r = this.records.get(id);
|
|
127
|
+
return r && !(r.ontologyType && this.deniedTypes.has(r.ontologyType)) ? [structuredClone(this.visible(r.object))] : [];
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async upsert(object, meta) {
|
|
131
|
+
const v = validateGeometry(object.geometry);
|
|
132
|
+
if (!v.valid)
|
|
133
|
+
throw new MapEngineError('INVALID_GEOMETRY', `${object.objectId}: ${v.reason}`, { objectId: object.objectId });
|
|
134
|
+
const prev = this.records.get(object.objectId);
|
|
135
|
+
const ontologyType = meta?.ontologyType ?? prev?.ontologyType;
|
|
136
|
+
const record = structuredClone(ontologyType !== undefined ? { object, ontologyType } : { object });
|
|
137
|
+
this.records.set(object.objectId, record);
|
|
138
|
+
this.record(record);
|
|
139
|
+
this.write(record, prev, object.observedAt ?? new Date().toISOString());
|
|
140
|
+
this.notify(object.objectId, record);
|
|
141
|
+
}
|
|
142
|
+
async remove(objectId) {
|
|
143
|
+
const prev = this.records.get(objectId);
|
|
144
|
+
if (!prev)
|
|
145
|
+
return;
|
|
146
|
+
this.records.delete(objectId);
|
|
147
|
+
this.addEntry({ at: new Date().toISOString(), objectId, kind: 'removed', ...(prev.ontologyType ? { ontologyType: prev.ontologyType } : {}) });
|
|
148
|
+
this.notify(objectId, undefined);
|
|
149
|
+
}
|
|
150
|
+
/** The stored record is a private copy and is replaced (never changed) on the next write, so it is shared here. */
|
|
151
|
+
write(record, prev, at) {
|
|
152
|
+
this.addEntry({ at, objectId: record.object.objectId, kind: prev ? 'updated' : 'created', object: record.object, ...(record.ontologyType ? { ontologyType: record.ontologyType } : {}) });
|
|
153
|
+
}
|
|
154
|
+
addEntry(e) {
|
|
155
|
+
let list = this.journal.get(e.objectId);
|
|
156
|
+
if (!list)
|
|
157
|
+
this.journal.set(e.objectId, (list = []));
|
|
158
|
+
list.push(e);
|
|
159
|
+
if (list.length > JOURNAL_CAP)
|
|
160
|
+
list.splice(0, list.length - JOURNAL_CAP);
|
|
161
|
+
}
|
|
162
|
+
// ---------------------------------------------------------------- journal (doc 29 §7) and links (doc 28 G4)
|
|
163
|
+
/** Adds journal entries (demos and tests): past states and changes, merged with what writes recorded. */
|
|
164
|
+
seedJournal(entries) {
|
|
165
|
+
for (const e of entries)
|
|
166
|
+
this.addEntry(structuredClone({ ...e, ontologyType: e.ontologyType ?? this.records.get(e.objectId)?.ontologyType }));
|
|
167
|
+
}
|
|
168
|
+
async getJournal(query, range, signal) {
|
|
169
|
+
await delay(this.latencyMs, signal);
|
|
170
|
+
this.refuse(query.ontologyType);
|
|
171
|
+
const from = Date.parse(range.start);
|
|
172
|
+
const to = Date.parse(range.end);
|
|
173
|
+
const out = [];
|
|
174
|
+
for (const [objectId, all] of this.journal) {
|
|
175
|
+
if (query.objectIds && !query.objectIds.includes(objectId))
|
|
176
|
+
continue;
|
|
177
|
+
const list = all.filter((e) => !query.ontologyType || e.ontologyType === query.ontologyType).sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
|
|
178
|
+
if (!list.length)
|
|
179
|
+
continue;
|
|
180
|
+
const before = [...list].reverse().find((e) => Date.parse(e.at) < from);
|
|
181
|
+
if (before && before.kind !== 'removed' && before.object)
|
|
182
|
+
out.push({ at: range.start, objectId, kind: 'snapshot', object: structuredClone(this.visible(before.object)) });
|
|
183
|
+
let prev = before?.object;
|
|
184
|
+
for (const e of list) {
|
|
185
|
+
const t = Date.parse(e.at);
|
|
186
|
+
if (t < from)
|
|
187
|
+
continue;
|
|
188
|
+
if (t > to)
|
|
189
|
+
break;
|
|
190
|
+
const { ontologyType: _t, ...entry } = e;
|
|
191
|
+
// What changed since the entry before, worked out now (writes stay cheap).
|
|
192
|
+
const changes = entry.changes ?? (prev && entry.object ? diffProps(prev.properties, entry.object.properties) : undefined);
|
|
193
|
+
out.push(structuredClone({ ...entry, ...(entry.object ? { object: this.visible(entry.object) } : {}), ...(changes && Object.keys(changes).length ? { changes } : {}) }));
|
|
194
|
+
if (entry.object)
|
|
195
|
+
prev = entry.object;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
/** Adds links between objects (demos and tests). */
|
|
201
|
+
seedLinks(links) {
|
|
202
|
+
this.linkList.push(...structuredClone(links));
|
|
203
|
+
}
|
|
204
|
+
async getLinks(objectIds, types, signal) {
|
|
205
|
+
await delay(this.latencyMs, signal);
|
|
206
|
+
const ids = new Set(objectIds);
|
|
207
|
+
return this.linkList.filter((k) => (ids.has(k.fromId) || ids.has(k.toId)) && (!types || (k.type !== undefined && types.includes(k.type)))).map((k) => structuredClone(k));
|
|
208
|
+
}
|
|
209
|
+
/** Labels a dataset for the catalog, or hides it (e.g. app-internal draft types). */
|
|
210
|
+
describeDataset(ontologyType, meta) {
|
|
211
|
+
this.datasetMeta.set(ontologyType, { ...this.datasetMeta.get(ontologyType), ...meta });
|
|
212
|
+
}
|
|
213
|
+
/** Every ontology type held, with geometry, count and property schema — discovered from the records. */
|
|
214
|
+
async listDatasets(signal) {
|
|
215
|
+
await delay(this.latencyMs, signal);
|
|
216
|
+
const groups = new Map();
|
|
217
|
+
for (const r of this.records.values()) {
|
|
218
|
+
if (r.ontologyType === undefined || this.datasetMeta.get(r.ontologyType)?.hidden)
|
|
219
|
+
continue;
|
|
220
|
+
let list = groups.get(r.ontologyType);
|
|
221
|
+
if (!list)
|
|
222
|
+
groups.set(r.ontologyType, (list = []));
|
|
223
|
+
list.push(r.object);
|
|
224
|
+
}
|
|
225
|
+
// History means movement over time: at least two observations of one object.
|
|
226
|
+
const historyTypes = new Set([...this.history.values()].filter((h) => h.points.length >= 2).map((h) => h.ontologyType));
|
|
227
|
+
return [...groups.entries()]
|
|
228
|
+
.map(([ontologyType, objects]) => {
|
|
229
|
+
const kinds = new Set(objects.map((o) => o.geometry.type));
|
|
230
|
+
const families = new Set(objects.map((o) => geometryFamily(o.geometry.type)));
|
|
231
|
+
const meta = this.datasetMeta.get(ontologyType);
|
|
232
|
+
const info = {
|
|
233
|
+
ontologyType,
|
|
234
|
+
geometryType: kinds.size === 1 ? [...kinds][0] : 'Mixed',
|
|
235
|
+
geometryFamily: families.size === 1 ? [...families][0] : 'mixed',
|
|
236
|
+
count: objects.length,
|
|
237
|
+
properties: describeProperties(objects),
|
|
238
|
+
hasHistory: historyTypes.has(ontologyType),
|
|
239
|
+
live: this.capabilities.supportsRealtime,
|
|
240
|
+
};
|
|
241
|
+
const bounds = unionBounds(objects.map((o) => geometryBounds(o.geometry)));
|
|
242
|
+
if (bounds)
|
|
243
|
+
info.bounds = bounds;
|
|
244
|
+
if (meta?.label)
|
|
245
|
+
info.label = meta.label;
|
|
246
|
+
if (meta?.source)
|
|
247
|
+
info.source = meta.source;
|
|
248
|
+
if (meta?.altitude)
|
|
249
|
+
info.altitude = meta.altitude;
|
|
250
|
+
return info;
|
|
251
|
+
})
|
|
252
|
+
.sort((a, b) => a.ontologyType.localeCompare(b.ontologyType));
|
|
253
|
+
}
|
|
254
|
+
/** Replaces every object of a type (an import, a re-sync). Subscribers see the removals and upserts. */
|
|
255
|
+
async setDataset(ontologyType, objects) {
|
|
256
|
+
const keep = new Set(objects.map((o) => o.objectId));
|
|
257
|
+
for (const [id, r] of [...this.records])
|
|
258
|
+
if (r.ontologyType === ontologyType && !keep.has(id))
|
|
259
|
+
await this.remove(id);
|
|
260
|
+
for (const o of objects)
|
|
261
|
+
await this.upsert(o, { ontologyType });
|
|
262
|
+
}
|
|
263
|
+
async removeDataset(ontologyType) {
|
|
264
|
+
for (const [id, r] of [...this.records])
|
|
265
|
+
if (r.ontologyType === ontologyType)
|
|
266
|
+
await this.remove(id);
|
|
267
|
+
this.datasetMeta.delete(ontologyType);
|
|
268
|
+
}
|
|
269
|
+
/** Convenience for simulations: move a point and stamp the observation time. */
|
|
270
|
+
async move(objectId, position, observedAt = new Date().toISOString()) {
|
|
271
|
+
const r = this.records.get(objectId);
|
|
272
|
+
if (!r)
|
|
273
|
+
throw new MapEngineError('PROVIDER_ERROR', `unknown object ${objectId}`);
|
|
274
|
+
await this.upsert({ ...r.object, geometry: { type: 'Point', coordinates: position }, observedAt });
|
|
275
|
+
}
|
|
276
|
+
subscribe(query, onPatch, onError) {
|
|
277
|
+
const sub = { query: structuredClone(query), onPatch, onError, members: new Set() };
|
|
278
|
+
for (const r of this.records.values())
|
|
279
|
+
if (this.matches(r, sub.query))
|
|
280
|
+
sub.members.add(r.object.objectId);
|
|
281
|
+
this.subscriptions.add(sub);
|
|
282
|
+
return () => {
|
|
283
|
+
this.subscriptions.delete(sub);
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/** Test hook: break every open stream (the engine should reconnect with a fresh snapshot). */
|
|
287
|
+
failStreams(error = new Error('stream disconnected')) {
|
|
288
|
+
for (const sub of [...this.subscriptions])
|
|
289
|
+
queueMicrotask(() => sub.onError?.(error));
|
|
290
|
+
}
|
|
291
|
+
subscriptionCount() {
|
|
292
|
+
return this.subscriptions.size;
|
|
293
|
+
}
|
|
294
|
+
notify(objectId, record) {
|
|
295
|
+
for (const sub of this.subscriptions) {
|
|
296
|
+
const now = record !== undefined && this.matches(record, sub.query);
|
|
297
|
+
const had = sub.members.has(objectId);
|
|
298
|
+
let patch = null;
|
|
299
|
+
if (now) {
|
|
300
|
+
sub.members.add(objectId);
|
|
301
|
+
patch = { upserted: [structuredClone(record.object)] };
|
|
302
|
+
}
|
|
303
|
+
else if (had) {
|
|
304
|
+
sub.members.delete(objectId);
|
|
305
|
+
patch = { removed: [objectId] };
|
|
306
|
+
}
|
|
307
|
+
if (patch) {
|
|
308
|
+
const p = patch;
|
|
309
|
+
queueMicrotask(() => {
|
|
310
|
+
if (this.subscriptions.has(sub))
|
|
311
|
+
sub.onPatch(p);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/** Adds past positions for an object (demos and tests). Points merge with recorded history by time. */
|
|
317
|
+
seedHistory(objectId, points, meta = {}) {
|
|
318
|
+
const h = this.history.get(objectId) ?? { ontologyType: meta.ontologyType ?? this.records.get(objectId)?.ontologyType, properties: meta.properties ?? this.records.get(objectId)?.object.properties, points: [] };
|
|
319
|
+
h.points = [...h.points, ...points.map((p) => ({ ...p }))].sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
|
|
320
|
+
this.history.set(objectId, h);
|
|
321
|
+
}
|
|
322
|
+
async getTracks(query, range, signal) {
|
|
323
|
+
await delay(this.latencyMs, signal);
|
|
324
|
+
const failure = this.failures.shift();
|
|
325
|
+
if (failure)
|
|
326
|
+
throw failure;
|
|
327
|
+
const from = Date.parse(range.start);
|
|
328
|
+
const to = Date.parse(range.end);
|
|
329
|
+
const out = [];
|
|
330
|
+
for (const [objectId, h] of this.history) {
|
|
331
|
+
if (query.ontologyType && h.ontologyType !== query.ontologyType)
|
|
332
|
+
continue;
|
|
333
|
+
if (query.objectIds && !query.objectIds.includes(objectId))
|
|
334
|
+
continue;
|
|
335
|
+
const props = this.records.get(objectId)?.object.properties ?? h.properties;
|
|
336
|
+
if (query.filter && !matchesQuery({ objectId, geometry: { type: 'Point', coordinates: { latitude: 0, longitude: 0 } }, ...(props ? { properties: props } : {}) }, { filter: query.filter }))
|
|
337
|
+
continue;
|
|
338
|
+
// In range, plus the last point before it so positions can be interpolated from the range start.
|
|
339
|
+
const before = [...h.points].reverse().find((p) => Date.parse(p.timestamp) < from);
|
|
340
|
+
const inRange = h.points.filter((p) => Date.parse(p.timestamp) >= from && Date.parse(p.timestamp) <= to);
|
|
341
|
+
const points = before ? [before, ...inRange] : inRange;
|
|
342
|
+
if (!points.length)
|
|
343
|
+
continue;
|
|
344
|
+
if (query.bounds && !points.some((p) => boundsContainPosition(query.bounds, p)))
|
|
345
|
+
continue;
|
|
346
|
+
out.push({ objectId, points: structuredClone(points), ...(props ? { properties: structuredClone(props) } : {}) });
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
/** Every point position with a timestamp becomes history. */
|
|
351
|
+
record(r) {
|
|
352
|
+
const g = r.object.geometry;
|
|
353
|
+
if (g.type !== 'Point')
|
|
354
|
+
return;
|
|
355
|
+
const ts = r.object.observedAt ?? new Date().toISOString();
|
|
356
|
+
const h = this.history.get(r.object.objectId) ?? { ontologyType: r.ontologyType, properties: r.object.properties, points: [] };
|
|
357
|
+
h.ontologyType = r.ontologyType ?? h.ontologyType;
|
|
358
|
+
h.properties = r.object.properties ?? h.properties;
|
|
359
|
+
const heading = typeof r.object.properties?.heading === 'number' ? { heading: r.object.properties.heading } : {};
|
|
360
|
+
const last = h.points[h.points.length - 1];
|
|
361
|
+
if (!last || Date.parse(ts) >= Date.parse(last.timestamp))
|
|
362
|
+
h.points.push({ timestamp: ts, latitude: g.coordinates.latitude, longitude: g.coordinates.longitude, ...heading });
|
|
363
|
+
this.history.set(r.object.objectId, h);
|
|
364
|
+
}
|
|
365
|
+
/** Synchronous view for tests. Not part of any provider contract. */
|
|
366
|
+
size() {
|
|
367
|
+
return this.records.size;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { BasemapConfig, MapConfig, MapLayer, MapViewport, RendererCapabilities, TerrainConfig, Position, SceneObject, ScenePatch, SceneState, Size, Unsubscribe } from '../core/types.js';
|
|
2
|
+
import type { MapClickEvent, PickEvent, Renderer, RendererOptions } from '../render/renderer.js';
|
|
3
|
+
type Listener<T> = (v: T) => void;
|
|
4
|
+
/**
|
|
5
|
+
* A headless Renderer that records what it is told and lets tests play user input back.
|
|
6
|
+
* Its `objects` map is what a real renderer would be drawing: render() plus every update() applied.
|
|
7
|
+
*/
|
|
8
|
+
export declare class TestRenderer implements Renderer {
|
|
9
|
+
initialized: boolean;
|
|
10
|
+
destroyed: boolean;
|
|
11
|
+
options: RendererOptions | undefined;
|
|
12
|
+
readonly renders: SceneState[];
|
|
13
|
+
readonly patches: ScenePatch[];
|
|
14
|
+
readonly viewportsSet: MapViewport[];
|
|
15
|
+
readonly objects: Map<string, SceneObject>;
|
|
16
|
+
layers: MapLayer[];
|
|
17
|
+
size: Size;
|
|
18
|
+
/** When true, setViewport echoes back through onViewportChange, like MapLibre's moveend. */
|
|
19
|
+
echoViewport: boolean;
|
|
20
|
+
failInitialize: Error | null;
|
|
21
|
+
private readonly listeners;
|
|
22
|
+
readonly capabilities: RendererCapabilities;
|
|
23
|
+
/** Projections and terrains set after initialize (null = terrain off). */
|
|
24
|
+
readonly projections: Array<NonNullable<MapConfig['projection']>>;
|
|
25
|
+
readonly terrains: Array<TerrainConfig | null>;
|
|
26
|
+
private readonly ground;
|
|
27
|
+
constructor(options?: {
|
|
28
|
+
size?: Size;
|
|
29
|
+
echoViewport?: boolean;
|
|
30
|
+
setBasemap?: boolean;
|
|
31
|
+
capabilities?: RendererCapabilities;
|
|
32
|
+
ground?: (p: Position) => number | null;
|
|
33
|
+
});
|
|
34
|
+
initializeCount: number;
|
|
35
|
+
/** Basemaps swapped in with setBasemap(). */
|
|
36
|
+
readonly basemaps: BasemapConfig[];
|
|
37
|
+
/** Present unless constructed with `setBasemap: false` (then the runtime has to remount to change basemap). */
|
|
38
|
+
setBasemap?: (basemap: BasemapConfig) => Promise<void>;
|
|
39
|
+
initialize(_container: HTMLElement, options: RendererOptions): Promise<void>;
|
|
40
|
+
setViewport(viewport: MapViewport): void;
|
|
41
|
+
render(scene: SceneState): void;
|
|
42
|
+
update(patch: ScenePatch): void;
|
|
43
|
+
resizeCount: number;
|
|
44
|
+
resize(): void;
|
|
45
|
+
getSize(): Size;
|
|
46
|
+
/** Test projection: the last viewport set, 1 degree = 1000 px from the centre (not a real projection). */
|
|
47
|
+
project(position: {
|
|
48
|
+
latitude: number;
|
|
49
|
+
longitude: number;
|
|
50
|
+
}): {
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
} | null;
|
|
54
|
+
setProjection(projection: NonNullable<MapConfig['projection']>): void;
|
|
55
|
+
setTerrain(terrain: TerrainConfig | null): void;
|
|
56
|
+
/** Heights from the `ground` function given to the constructor (a test terrain). */
|
|
57
|
+
sampleElevation(positions: Position[]): Promise<Array<number | null>>;
|
|
58
|
+
destroy(): void;
|
|
59
|
+
onViewportChange(l: Listener<MapViewport>): Unsubscribe;
|
|
60
|
+
onPick(l: Listener<PickEvent>): Unsubscribe;
|
|
61
|
+
onMapClick(l: Listener<MapClickEvent>): Unsubscribe;
|
|
62
|
+
onHover(l: Listener<PickEvent | null>): Unsubscribe;
|
|
63
|
+
onError(l: Listener<Error>): Unsubscribe;
|
|
64
|
+
simulateViewportChange(v: MapViewport): void;
|
|
65
|
+
simulatePick(objectId: string, layerId: string, position?: Position): void;
|
|
66
|
+
simulateMapClick(position: Position): void;
|
|
67
|
+
simulateHover(e: PickEvent | null): void;
|
|
68
|
+
simulateError(e: Error): void;
|
|
69
|
+
listenerCount(): number;
|
|
70
|
+
private add;
|
|
71
|
+
}
|
|
72
|
+
export {};
|