@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,270 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* React integration (doc 19, §48). React owns the container; the engine owns everything spatial.
|
|
4
|
+
* No GIS logic here (Decision: React is the integration layer only).
|
|
5
|
+
*
|
|
6
|
+
* Two ways to use it:
|
|
7
|
+
* - `<MapView config provider createRenderer …>`: MapView creates the runtime (once per provider, realtime,
|
|
8
|
+
* ontology and history) and destroys it on unmount. `config.layers` changes are applied through the runtime API,
|
|
9
|
+
* `config.basemap` changes through setBasemap() — neither remounts.
|
|
10
|
+
* - `<MapView map={runtime}>`: a runtime made outside React (shared, headless-first, or kept across views). MapView
|
|
11
|
+
* only mounts it into its container and unmounts it again; the owner destroys it.
|
|
12
|
+
*
|
|
13
|
+
* `config.viewport` is the initial camera; move the camera with `runtime.setViewport` / `fitBounds` / `flyTo`.
|
|
14
|
+
* `selection` makes selection controlled: the application owns it and pushes it in.
|
|
15
|
+
*/
|
|
16
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
17
|
+
import { deepEqual } from '../core/equal.js';
|
|
18
|
+
import { MapEngineError } from '../core/errors.js';
|
|
19
|
+
import { createMapRuntime } from '../runtime/runtime.js';
|
|
20
|
+
import { MapContext } from './context.js';
|
|
21
|
+
import { MapStore } from './store.js';
|
|
22
|
+
const ownedOf = (p) => (p.map === undefined ? p : null);
|
|
23
|
+
/**
|
|
24
|
+
* The map element needs a positioned box. It is set by a zero-specificity rule, not inline, so an application class
|
|
25
|
+
* such as `absolute inset-0` or `size-full` wins (an inline `position: relative` silently gave zero height).
|
|
26
|
+
*/
|
|
27
|
+
const BASE_STYLE_ID = 'map-engine-base';
|
|
28
|
+
function ensureBaseStyle() {
|
|
29
|
+
if (typeof document === 'undefined' || document.getElementById(BASE_STYLE_ID))
|
|
30
|
+
return;
|
|
31
|
+
const el = document.createElement('style');
|
|
32
|
+
el.id = BASE_STYLE_ID;
|
|
33
|
+
el.textContent = ':where([data-map-engine]){position:relative;overflow:hidden}';
|
|
34
|
+
document.head.appendChild(el);
|
|
35
|
+
}
|
|
36
|
+
function syncLayers(runtime, prev, next) {
|
|
37
|
+
const before = new Map(prev.map((l) => [l.id, l]));
|
|
38
|
+
const after = new Map(next.map((l) => [l.id, l]));
|
|
39
|
+
for (const id of before.keys())
|
|
40
|
+
if (!after.has(id))
|
|
41
|
+
runtime.removeLayer(id);
|
|
42
|
+
for (const [id, layer] of after) {
|
|
43
|
+
const old = before.get(id);
|
|
44
|
+
if (!old)
|
|
45
|
+
runtime.addLayer(layer);
|
|
46
|
+
else if (!deepEqual(old, layer)) {
|
|
47
|
+
const { id: _id, ...patch } = layer;
|
|
48
|
+
runtime.updateLayer(id, patch);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const toEngineError = (e) => e instanceof MapEngineError ? e : new MapEngineError('RENDERER_ERROR', e instanceof Error ? e.message : String(e), undefined, { cause: e });
|
|
53
|
+
export function MapView(props) {
|
|
54
|
+
ensureBaseStyle();
|
|
55
|
+
const containerRef = useRef(null);
|
|
56
|
+
const [runtime, setRuntime] = useState(null);
|
|
57
|
+
const store = useMemo(() => new MapStore(), []);
|
|
58
|
+
const latest = useRef(props);
|
|
59
|
+
useLayoutEffect(() => {
|
|
60
|
+
latest.current = props;
|
|
61
|
+
});
|
|
62
|
+
const own = ownedOf(props);
|
|
63
|
+
const appliedLayers = useRef(own?.config.layers ?? []);
|
|
64
|
+
const appliedBasemap = useRef(own?.config.basemap);
|
|
65
|
+
// Create (or adopt) the runtime and mount it. StrictMode's double effect mounts, releases, then mounts again —
|
|
66
|
+
// safe, because destroy()/unmount() cancel a mount still in flight.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const el = containerRef.current;
|
|
69
|
+
if (!el)
|
|
70
|
+
return;
|
|
71
|
+
const p = latest.current;
|
|
72
|
+
const o = ownedOf(p);
|
|
73
|
+
const rt = o
|
|
74
|
+
? createMapRuntime(o.config, {
|
|
75
|
+
renderer: o.createRenderer(),
|
|
76
|
+
provider: o.provider,
|
|
77
|
+
...(o.realtime ? { realtime: o.realtime } : {}),
|
|
78
|
+
...(o.ontology ? { ontology: o.ontology } : {}),
|
|
79
|
+
...(o.history ? { history: o.history } : {}),
|
|
80
|
+
...(o.links ? { links: o.links } : {}),
|
|
81
|
+
...(o.journal ? { journal: o.journal } : {}),
|
|
82
|
+
...(o.annotations ? { annotations: o.annotations } : {}),
|
|
83
|
+
...(o.auth ? { auth: o.auth } : {}),
|
|
84
|
+
}, o.runtimeOptions)
|
|
85
|
+
: p.map;
|
|
86
|
+
if (o) {
|
|
87
|
+
appliedLayers.current = o.config.layers;
|
|
88
|
+
appliedBasemap.current = o.config.basemap;
|
|
89
|
+
}
|
|
90
|
+
let live = true;
|
|
91
|
+
store.set({ state: rt.state, selection: rt.getSelection(), viewport: rt.getViewport(), time: rt.getTime() });
|
|
92
|
+
const offs = [
|
|
93
|
+
rt.on('state', (state) => store.set({ state })),
|
|
94
|
+
rt.on('selection', (selection) => {
|
|
95
|
+
store.set({ selection });
|
|
96
|
+
latest.current.onSelect?.(selection);
|
|
97
|
+
}),
|
|
98
|
+
rt.on('viewport', (viewport) => {
|
|
99
|
+
store.set({ viewport });
|
|
100
|
+
latest.current.onViewportChange?.(viewport);
|
|
101
|
+
}),
|
|
102
|
+
rt.on('mapClick', (e) => latest.current.onMapClick?.(e.position)),
|
|
103
|
+
rt.on('click', (e) => latest.current.onClick?.(e)),
|
|
104
|
+
rt.on('hover', (h) => latest.current.onHover?.(h)),
|
|
105
|
+
rt.on('error', (e) => latest.current.onError?.(e)),
|
|
106
|
+
rt.on('warning', (w) => latest.current.onWarning?.(w)),
|
|
107
|
+
rt.on('time', (time) => store.set({ time })),
|
|
108
|
+
rt.on('geofence', (e) => latest.current.onGeofence?.(e)),
|
|
109
|
+
];
|
|
110
|
+
try {
|
|
111
|
+
if (p.geofences)
|
|
112
|
+
rt.setGeofences(p.geofences);
|
|
113
|
+
if (p.time && p.time.mode === 'replay')
|
|
114
|
+
rt.setTime(p.time);
|
|
115
|
+
const controlled = p.selection;
|
|
116
|
+
if (controlled)
|
|
117
|
+
rt.setSelection({ objectIds: controlled.objectIds, source: controlled.source ?? 'app', ...(controlled.primaryObjectId ? { primaryObjectId: controlled.primaryObjectId } : {}) });
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
p.onError?.(toEngineError(e));
|
|
121
|
+
}
|
|
122
|
+
setRuntime(rt);
|
|
123
|
+
rt.mount(el).then(() => live && rt.isMounted() && latest.current.onReady?.(rt), (e) => live && latest.current.onError?.(toEngineError(e)));
|
|
124
|
+
let observer;
|
|
125
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
126
|
+
let first = true;
|
|
127
|
+
observer = new ResizeObserver(() => {
|
|
128
|
+
if (first)
|
|
129
|
+
return void (first = false); // the initial observation is not a resize
|
|
130
|
+
if (rt.isMounted())
|
|
131
|
+
rt.resize();
|
|
132
|
+
});
|
|
133
|
+
observer.observe(el);
|
|
134
|
+
}
|
|
135
|
+
return () => {
|
|
136
|
+
live = false;
|
|
137
|
+
observer?.disconnect();
|
|
138
|
+
for (const off of offs)
|
|
139
|
+
off();
|
|
140
|
+
if (o)
|
|
141
|
+
rt.destroy();
|
|
142
|
+
else
|
|
143
|
+
rt.unmount(); // not ours: release the container, keep the runtime
|
|
144
|
+
store.set({ state: o ? 'destroyed' : 'idle', selection: undefined, viewport: undefined, time: { mode: 'live' } });
|
|
145
|
+
setRuntime((cur) => (cur === rt ? null : cur));
|
|
146
|
+
};
|
|
147
|
+
// `createRenderer`, callbacks and `config` are read from `latest` on creation on purpose.
|
|
148
|
+
}, [props.map, own?.provider, own?.realtime, own?.ontology, own?.history, own?.links, own?.journal, own?.annotations, store]);
|
|
149
|
+
// Owned: a new identity is handed to the providers in place (the map reloads what this user may see).
|
|
150
|
+
const authKey = own ? JSON.stringify(own.auth ?? null) : '';
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
const o = ownedOf(latest.current);
|
|
153
|
+
if (!runtime || runtime.state === 'destroyed' || !o || o.auth === undefined)
|
|
154
|
+
return;
|
|
155
|
+
runtime.setAuth(o.auth);
|
|
156
|
+
// authKey stands for the identity
|
|
157
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
158
|
+
}, [runtime, authKey]);
|
|
159
|
+
// Owned: basemap changes swap the basemap in place (camera, layers, selection and time stay).
|
|
160
|
+
const basemapKey = own ? JSON.stringify(own.config.basemap) : '';
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const o = ownedOf(latest.current);
|
|
163
|
+
if (!runtime || runtime.state === 'destroyed' || !o || deepEqual(appliedBasemap.current, o.config.basemap))
|
|
164
|
+
return;
|
|
165
|
+
appliedBasemap.current = o.config.basemap;
|
|
166
|
+
runtime.setBasemap(o.config.basemap).catch((e) => latest.current.onError?.(toEngineError(e)));
|
|
167
|
+
// basemapKey stands for config.basemap
|
|
168
|
+
}, [runtime, basemapKey]);
|
|
169
|
+
// Owned: projection and terrain changes apply in place too (ADR-026).
|
|
170
|
+
const projection = own?.config.projection ?? 'mercator';
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!runtime || runtime.state === 'destroyed' || !ownedOf(latest.current))
|
|
173
|
+
return;
|
|
174
|
+
try {
|
|
175
|
+
runtime.setProjection(projection);
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
latest.current.onError?.(toEngineError(e));
|
|
179
|
+
}
|
|
180
|
+
}, [runtime, projection]);
|
|
181
|
+
const terrainKey = own ? JSON.stringify(own.config.terrain ?? null) : '';
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
const o = ownedOf(latest.current);
|
|
184
|
+
if (!runtime || runtime.state === 'destroyed' || !o)
|
|
185
|
+
return;
|
|
186
|
+
try {
|
|
187
|
+
runtime.setTerrain(o.config.terrain ?? null);
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
latest.current.onError?.(toEngineError(e));
|
|
191
|
+
}
|
|
192
|
+
// terrainKey stands for config.terrain
|
|
193
|
+
}, [runtime, terrainKey]);
|
|
194
|
+
const tilesetsKey = own ? JSON.stringify(own.config.tilesets ?? []) : '';
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
const o = ownedOf(latest.current);
|
|
197
|
+
if (!runtime || runtime.state === 'destroyed' || !o)
|
|
198
|
+
return;
|
|
199
|
+
try {
|
|
200
|
+
runtime.setTilesets(o.config.tilesets ?? []);
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
latest.current.onError?.(toEngineError(e));
|
|
204
|
+
}
|
|
205
|
+
// tilesetsKey stands for config.tilesets
|
|
206
|
+
}, [runtime, tilesetsKey]);
|
|
207
|
+
// Owned: apply layer changes without recreating the map.
|
|
208
|
+
const layersKey = own ? JSON.stringify(own.config.layers) : '';
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
const o = ownedOf(latest.current);
|
|
211
|
+
if (!runtime || runtime.state === 'destroyed' || !o)
|
|
212
|
+
return;
|
|
213
|
+
try {
|
|
214
|
+
syncLayers(runtime, appliedLayers.current, o.config.layers);
|
|
215
|
+
appliedLayers.current = o.config.layers;
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
latest.current.onError?.(toEngineError(e));
|
|
219
|
+
}
|
|
220
|
+
// layersKey stands for config.layers
|
|
221
|
+
}, [runtime, layersKey]);
|
|
222
|
+
// Controlled selection.
|
|
223
|
+
const sel = props.selection;
|
|
224
|
+
const selectionKey = sel ? JSON.stringify([sel.objectIds, sel.primaryObjectId ?? null]) : null;
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (!runtime || runtime.state === 'destroyed' || !sel)
|
|
227
|
+
return;
|
|
228
|
+
runtime.setSelection({ objectIds: sel.objectIds, source: sel.source ?? 'app', ...(sel.primaryObjectId ? { primaryObjectId: sel.primaryObjectId } : {}) });
|
|
229
|
+
// selectionKey stands for props.selection
|
|
230
|
+
}, [runtime, selectionKey]);
|
|
231
|
+
// Application-owned time: pushed into the runtime whenever it changes. A runtime we do not own keeps its own
|
|
232
|
+
// time unless the prop is given.
|
|
233
|
+
const timeKey = JSON.stringify(props.time ?? null);
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (!runtime || runtime.state === 'destroyed')
|
|
236
|
+
return;
|
|
237
|
+
if (props.time === undefined && !own)
|
|
238
|
+
return;
|
|
239
|
+
try {
|
|
240
|
+
runtime.setTime(props.time ?? { mode: 'live' });
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
latest.current.onError?.(toEngineError(e));
|
|
244
|
+
}
|
|
245
|
+
// timeKey stands for props.time
|
|
246
|
+
}, [runtime, timeKey]);
|
|
247
|
+
const geofencesKey = JSON.stringify(props.geofences ?? null);
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (!runtime || runtime.state === 'destroyed')
|
|
250
|
+
return;
|
|
251
|
+
if (props.geofences === undefined && !own)
|
|
252
|
+
return;
|
|
253
|
+
try {
|
|
254
|
+
runtime.setGeofences(props.geofences ?? []);
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
latest.current.onError?.(toEngineError(e));
|
|
258
|
+
}
|
|
259
|
+
// geofencesKey stands for props.geofences
|
|
260
|
+
}, [runtime, geofencesKey]);
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (!runtime || runtime.state === 'destroyed')
|
|
263
|
+
return;
|
|
264
|
+
if (props.selectable === undefined && !own)
|
|
265
|
+
return;
|
|
266
|
+
runtime.setSelectable(props.selectable ?? true);
|
|
267
|
+
}, [runtime, props.selectable]);
|
|
268
|
+
const ctx = useMemo(() => ({ runtime, store }), [runtime, store]);
|
|
269
|
+
return (_jsx(MapContext.Provider, { value: ctx, children: _jsxs("div", { className: props.className, style: props.style, "data-map-engine": "", children: [_jsx("div", { ref: containerRef, style: { position: 'absolute', inset: 0 } }), props.children] }) }));
|
|
270
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { GeofenceEvent, MapViewport, Position, SelectionContext, TimeState } from '../core/types.js';
|
|
2
|
+
import type { MapRuntime, RuntimeState } from '../runtime/runtime.js';
|
|
3
|
+
import type { MapStore } from './store.js';
|
|
4
|
+
export interface MapContextValue {
|
|
5
|
+
runtime: MapRuntime | null;
|
|
6
|
+
store: MapStore;
|
|
7
|
+
}
|
|
8
|
+
export declare const MapContext: import("react").Context<MapContextValue | null>;
|
|
9
|
+
/** The runtime behind the nearest <MapView>, or null before it is created. Use it to call engine APIs. */
|
|
10
|
+
export declare function useMapRuntime(): MapRuntime | null;
|
|
11
|
+
export declare function useSelection(): SelectionContext | undefined;
|
|
12
|
+
export declare function useViewport(): MapViewport | undefined;
|
|
13
|
+
export declare function useMapState(): RuntimeState;
|
|
14
|
+
/** What time the map shows: live, or a replay time and range. */
|
|
15
|
+
export declare function useMapTime(): TimeState;
|
|
16
|
+
export interface GeofenceView {
|
|
17
|
+
/** Newest first. Live: what happened this session. Replay: what happened in the range up to the replay time. */
|
|
18
|
+
events: GeofenceEvent[];
|
|
19
|
+
/** Object ids inside each geofence now (live) or at the replay time. */
|
|
20
|
+
occupancy: Record<string, string[]>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Geofence events and occupancy, kept current as objects move, geofences change, or time changes. Inside <MapView>
|
|
24
|
+
* it uses that map; panels outside the map pass the runtime (from `onReady`) instead.
|
|
25
|
+
*/
|
|
26
|
+
export declare function useGeofences(options?: {
|
|
27
|
+
limit?: number;
|
|
28
|
+
runtime?: MapRuntime | null;
|
|
29
|
+
}): GeofenceView;
|
|
30
|
+
/**
|
|
31
|
+
* Where a position is on screen (pixels from the map's top-left), kept current while the camera moves — for
|
|
32
|
+
* popups and other overlays pinned to an object. Null when there is no position or no map yet.
|
|
33
|
+
*/
|
|
34
|
+
export declare function useProjected(position: Position | null | undefined, options?: {
|
|
35
|
+
runtime?: MapRuntime | null;
|
|
36
|
+
}): {
|
|
37
|
+
x: number;
|
|
38
|
+
y: number;
|
|
39
|
+
} | null;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useState, useSyncExternalStore } from 'react';
|
|
2
|
+
export const MapContext = createContext(null);
|
|
3
|
+
function useMapContext(hook) {
|
|
4
|
+
const ctx = useContext(MapContext);
|
|
5
|
+
if (!ctx)
|
|
6
|
+
throw new Error(`${hook} must be used inside <MapView>`);
|
|
7
|
+
return ctx;
|
|
8
|
+
}
|
|
9
|
+
/** The runtime behind the nearest <MapView>, or null before it is created. Use it to call engine APIs. */
|
|
10
|
+
export function useMapRuntime() {
|
|
11
|
+
return useMapContext('useMapRuntime').runtime;
|
|
12
|
+
}
|
|
13
|
+
export function useSelection() {
|
|
14
|
+
const { store } = useMapContext('useSelection');
|
|
15
|
+
return useSyncExternalStore(store.subscribe, () => store.get().selection, () => undefined);
|
|
16
|
+
}
|
|
17
|
+
export function useViewport() {
|
|
18
|
+
const { store } = useMapContext('useViewport');
|
|
19
|
+
return useSyncExternalStore(store.subscribe, () => store.get().viewport, () => undefined);
|
|
20
|
+
}
|
|
21
|
+
export function useMapState() {
|
|
22
|
+
const { store } = useMapContext('useMapState');
|
|
23
|
+
return useSyncExternalStore(store.subscribe, () => store.get().state, () => 'idle');
|
|
24
|
+
}
|
|
25
|
+
const LIVE = { mode: 'live' };
|
|
26
|
+
/** What time the map shows: live, or a replay time and range. */
|
|
27
|
+
export function useMapTime() {
|
|
28
|
+
const { store } = useMapContext('useMapTime');
|
|
29
|
+
return useSyncExternalStore(store.subscribe, () => store.get().time, () => LIVE);
|
|
30
|
+
}
|
|
31
|
+
const EMPTY = { events: [], occupancy: {} };
|
|
32
|
+
/**
|
|
33
|
+
* Geofence events and occupancy, kept current as objects move, geofences change, or time changes. Inside <MapView>
|
|
34
|
+
* it uses that map; panels outside the map pass the runtime (from `onReady`) instead.
|
|
35
|
+
*/
|
|
36
|
+
export function useGeofences(options = {}) {
|
|
37
|
+
const ctx = useContext(MapContext);
|
|
38
|
+
const runtime = options.runtime !== undefined ? options.runtime : ctx ? ctx.runtime : undefined;
|
|
39
|
+
if (runtime === undefined)
|
|
40
|
+
throw new Error('useGeofences must be used inside <MapView> or be given a runtime');
|
|
41
|
+
const limit = options.limit ?? 100;
|
|
42
|
+
const [view, setView] = useState(EMPTY);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!runtime)
|
|
45
|
+
return setView(EMPTY);
|
|
46
|
+
let key = '';
|
|
47
|
+
const read = () => {
|
|
48
|
+
if (runtime.state === 'destroyed')
|
|
49
|
+
return;
|
|
50
|
+
const next = { events: runtime.getGeofenceEvents(limit), occupancy: runtime.getGeofenceOccupancy() };
|
|
51
|
+
const k = JSON.stringify(next);
|
|
52
|
+
if (k !== key) {
|
|
53
|
+
key = k;
|
|
54
|
+
setView(next);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
read();
|
|
58
|
+
const offs = [runtime.on('geofence', read), runtime.on('geofences', read), runtime.on('time', read), runtime.on('scene', read), runtime.on('state', read)];
|
|
59
|
+
return () => offs.forEach((off) => off());
|
|
60
|
+
}, [runtime, limit]);
|
|
61
|
+
return view;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Where a position is on screen (pixels from the map's top-left), kept current while the camera moves — for
|
|
65
|
+
* popups and other overlays pinned to an object. Null when there is no position or no map yet.
|
|
66
|
+
*/
|
|
67
|
+
export function useProjected(position, options = {}) {
|
|
68
|
+
const ctx = useContext(MapContext);
|
|
69
|
+
const runtime = options.runtime !== undefined ? options.runtime : (ctx?.runtime ?? null);
|
|
70
|
+
const lat = position?.latitude;
|
|
71
|
+
const lon = position?.longitude;
|
|
72
|
+
const [point, setPoint] = useState(null);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!runtime || lat === undefined || lon === undefined)
|
|
75
|
+
return setPoint(null);
|
|
76
|
+
let frame = 0;
|
|
77
|
+
const read = () => {
|
|
78
|
+
cancelAnimationFrame(frame);
|
|
79
|
+
frame = requestAnimationFrame(() => {
|
|
80
|
+
if (runtime.state === 'destroyed')
|
|
81
|
+
return;
|
|
82
|
+
const p = runtime.project({ latitude: lat, longitude: lon });
|
|
83
|
+
setPoint((cur) => (cur && p && Math.abs(cur.x - p.x) < 0.5 && Math.abs(cur.y - p.y) < 0.5 ? cur : p));
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
read();
|
|
87
|
+
const offs = [runtime.on('move', read), runtime.on('viewport', read), runtime.on('state', read)];
|
|
88
|
+
return () => {
|
|
89
|
+
cancelAnimationFrame(frame);
|
|
90
|
+
offs.forEach((off) => off());
|
|
91
|
+
};
|
|
92
|
+
}, [runtime, lat, lon]);
|
|
93
|
+
return point;
|
|
94
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { MapView, type MapViewCommonProps, type MapViewOwnedProps, type MapViewProps, type MapViewRuntimeProps } from './MapView.js';
|
|
2
|
+
export { useGeofences, useProjected, useMapRuntime, useMapState, useMapTime, useSelection, useViewport, type GeofenceView } from './context.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { MapViewport, SelectionContext, TimeState } from '../core/types.js';
|
|
2
|
+
import type { RuntimeState } from '../runtime/runtime.js';
|
|
3
|
+
export interface MapSnapshot {
|
|
4
|
+
state: RuntimeState;
|
|
5
|
+
selection: SelectionContext | undefined;
|
|
6
|
+
viewport: MapViewport | undefined;
|
|
7
|
+
time: TimeState;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Per-MapView external store. Slices are replaced (never mutated) only when they change, so
|
|
11
|
+
* `useSyncExternalStore` selectors stay stable and components re-render only for what they read.
|
|
12
|
+
*/
|
|
13
|
+
export declare class MapStore {
|
|
14
|
+
private snapshot;
|
|
15
|
+
private readonly listeners;
|
|
16
|
+
readonly subscribe: (listener: () => void) => (() => void);
|
|
17
|
+
readonly get: () => MapSnapshot;
|
|
18
|
+
set(patch: Partial<MapSnapshot>): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-MapView external store. Slices are replaced (never mutated) only when they change, so
|
|
3
|
+
* `useSyncExternalStore` selectors stay stable and components re-render only for what they read.
|
|
4
|
+
*/
|
|
5
|
+
export class MapStore {
|
|
6
|
+
snapshot = { state: 'idle', selection: undefined, viewport: undefined, time: { mode: 'live' } };
|
|
7
|
+
listeners = new Set();
|
|
8
|
+
subscribe = (listener) => {
|
|
9
|
+
this.listeners.add(listener);
|
|
10
|
+
return () => this.listeners.delete(listener);
|
|
11
|
+
};
|
|
12
|
+
get = () => this.snapshot;
|
|
13
|
+
set(patch) {
|
|
14
|
+
this.snapshot = { ...this.snapshot, ...patch };
|
|
15
|
+
for (const l of [...this.listeners])
|
|
16
|
+
l();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realtime (doc 14, §39–40, §62): how one frame of incremental changes is applied. Pure functions over the data
|
|
3
|
+
* the runtime hands in — no scene, no renderer, no events of its own — so the rules (coalescing, stale and
|
|
4
|
+
* out-of-order updates, clock skew, leaving the view, incremental index updates, trail growth) are testable alone.
|
|
5
|
+
*/
|
|
6
|
+
import { MapEngineError } from '../core/errors.js';
|
|
7
|
+
import type { BoundingBox, MapLayer, SpatialObject, Track } from '../core/types.js';
|
|
8
|
+
import type { SpatialPatch } from '../data/contracts.js';
|
|
9
|
+
import type { SpatialIndex } from '../spatial/spatial-index.js';
|
|
10
|
+
/** One stream event waiting for the next frame. `trail` events grow track layers instead of moving objects. */
|
|
11
|
+
export interface PendingPatch {
|
|
12
|
+
layerIds: string[];
|
|
13
|
+
patch: SpatialPatch;
|
|
14
|
+
trail?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** What one frame changed in one layer, so the scene and the index touch only those objects. */
|
|
17
|
+
export interface LayerChanges {
|
|
18
|
+
upserted: Map<string, SpatialObject>;
|
|
19
|
+
removed: Set<string>;
|
|
20
|
+
}
|
|
21
|
+
export interface ApplyContext {
|
|
22
|
+
layer(layerId: string): MapLayer | undefined;
|
|
23
|
+
/** The objects the layer holds now. */
|
|
24
|
+
current(layerId: string): SpatialObject[];
|
|
25
|
+
/** The viewport data boundary: an object that moves out of it leaves the layer. */
|
|
26
|
+
bounds: BoundingBox;
|
|
27
|
+
/** Held times after this are clock skew and cannot order updates (doc 14). */
|
|
28
|
+
trustUntil: number;
|
|
29
|
+
counters: {
|
|
30
|
+
applied: number;
|
|
31
|
+
stale: number;
|
|
32
|
+
outOfView: number;
|
|
33
|
+
};
|
|
34
|
+
warn(warning: MapEngineError): void;
|
|
35
|
+
/** Every valid incoming object, before ordering (for clock-skew reporting). */
|
|
36
|
+
saw(object: SpatialObject, layerId: string): void;
|
|
37
|
+
/** Grows a track layer from a patch; true when it changed. */
|
|
38
|
+
trail(layerId: string, patch: SpatialPatch): boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface AppliedFrame {
|
|
41
|
+
/** Each touched layer's objects after the frame. */
|
|
42
|
+
touched: Map<string, Map<string, SpatialObject>>;
|
|
43
|
+
changes: Map<string, LayerChanges>;
|
|
44
|
+
/** Fresh point readings (including ones that leave the view), for geofencing. */
|
|
45
|
+
fresh: Array<{
|
|
46
|
+
layerId: string;
|
|
47
|
+
object: SpatialObject;
|
|
48
|
+
}>;
|
|
49
|
+
/** Track layers that grew. */
|
|
50
|
+
trails: Set<string>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Is `next` older than what is held? A held time beyond `trustUntil` (clock skew) cannot order anything: a real
|
|
54
|
+
* update must not be refused because a source once stamped the future.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isOlder(next: SpatialObject, prev: SpatialObject, trustUntil: number): boolean;
|
|
57
|
+
/** Applies one frame of stream events. Many events for one object collapse into its newest state. */
|
|
58
|
+
export declare function applyFrame(batch: PendingPatch[], ctx: ApplyContext): AppliedFrame;
|
|
59
|
+
/**
|
|
60
|
+
* Applies one frame's changes to the client index instead of rebuilding it. The index holds each object once
|
|
61
|
+
* however many layers show it, so an object leaves only when no other layer still shows it.
|
|
62
|
+
*
|
|
63
|
+
* `displayed` is what every layer shows; `touched` holds the frame's new state for the layers it changed;
|
|
64
|
+
* `skip` names layers kept out of the index (trails).
|
|
65
|
+
*/
|
|
66
|
+
export declare function patchIndex(index: SpatialIndex, changes: Map<string, LayerChanges>, touched: Map<string, Map<string, SpatialObject>>, displayed: Map<string, SpatialObject[]>, skip: (layerId: string) => boolean): void;
|
|
67
|
+
/**
|
|
68
|
+
* Grows trails from live point updates and drops points older than `keepFrom`. Returns how many points were added
|
|
69
|
+
* and whether anything changed (a removal changes a trail without adding points).
|
|
70
|
+
*/
|
|
71
|
+
export declare function growTrails(tracks: Map<string, Track>, patch: SpatialPatch, keepFrom: number, now: number): {
|
|
72
|
+
changed: boolean;
|
|
73
|
+
added: number;
|
|
74
|
+
};
|