@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,401 @@
|
|
|
1
|
+
import { tokenFor, volumeOf } from '../layers/layers.js';
|
|
2
|
+
import { volumeMesh } from '../spatial/volume-mesh.js';
|
|
3
|
+
/** Default loader: the real deck.gl, split into its own chunks so 2D maps never download it. */
|
|
4
|
+
export async function loadDeck() {
|
|
5
|
+
const [mapbox, layers, mesh] = await Promise.all([import('@deck.gl/mapbox'), import('@deck.gl/layers'), import('@deck.gl/mesh-layers')]);
|
|
6
|
+
return {
|
|
7
|
+
MapboxOverlay: mapbox.MapboxOverlay,
|
|
8
|
+
ScatterplotLayer: layers.ScatterplotLayer,
|
|
9
|
+
IconLayer: layers.IconLayer,
|
|
10
|
+
PathLayer: layers.PathLayer,
|
|
11
|
+
LineLayer: layers.LineLayer,
|
|
12
|
+
TextLayer: layers.TextLayer,
|
|
13
|
+
ScenegraphLayer: mesh.ScenegraphLayer,
|
|
14
|
+
SimpleMeshLayer: mesh.SimpleMeshLayer,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Version shim (MapLibre v6 × deck.gl 9.4): deck.gl's interleaved mode reads MapLibre's private `map.transform`
|
|
19
|
+
* (height, elevation, _nearZ, _farZ). MapLibre v6 moved the transform under `map._camera` and renamed the planes
|
|
20
|
+
* to nearZ/farZ. When `map.transform` is missing, define a read-only view onto the v6 transform. Delete this once
|
|
21
|
+
* deck.gl supports MapLibre v6 natively.
|
|
22
|
+
*/
|
|
23
|
+
export function shimTransformForDeck(map) {
|
|
24
|
+
const m = map;
|
|
25
|
+
if (m.transform !== undefined)
|
|
26
|
+
return;
|
|
27
|
+
const t = () => m._camera?.transform ?? m.painter?.transform;
|
|
28
|
+
if (!t())
|
|
29
|
+
return;
|
|
30
|
+
const view = {
|
|
31
|
+
get height() {
|
|
32
|
+
return t()?.height;
|
|
33
|
+
},
|
|
34
|
+
get elevation() {
|
|
35
|
+
return t()?.elevation;
|
|
36
|
+
},
|
|
37
|
+
get _nearZ() {
|
|
38
|
+
return t()?.nearZ ?? t()?._nearZ;
|
|
39
|
+
},
|
|
40
|
+
get _farZ() {
|
|
41
|
+
return t()?.farZ ?? t()?._farZ;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(map, 'transform', { get: () => view, configurable: true });
|
|
45
|
+
}
|
|
46
|
+
/** Layers this host draws instead of MapLibre: anything at a height, and models. */
|
|
47
|
+
export function hostedByDeck(layer) {
|
|
48
|
+
const t = layer.visualization.type;
|
|
49
|
+
return t === 'model' || t === 'volume' || ((t === 'marker' || t === 'line' || t === 'track') && layer.visualization.elevation !== undefined);
|
|
50
|
+
}
|
|
51
|
+
/** Smallest on-screen length of a model, so platforms stay visible when zoomed out. */
|
|
52
|
+
const MIN_MODEL_PIXELS = 36;
|
|
53
|
+
function rgba(color, alpha = 1) {
|
|
54
|
+
const hex = (color ?? '#4CC3FF').replace('#', '');
|
|
55
|
+
const full = hex.length === 3 ? hex.split('').map((c) => c + c).join('') : hex.slice(0, 6);
|
|
56
|
+
const n = Number.parseInt(full, 16);
|
|
57
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255, Math.round(255 * Math.max(0, Math.min(1, alpha)))];
|
|
58
|
+
}
|
|
59
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v)) ? Number(v) : undefined);
|
|
60
|
+
export class DeckHost {
|
|
61
|
+
env;
|
|
62
|
+
load;
|
|
63
|
+
modules = null;
|
|
64
|
+
overlay = null;
|
|
65
|
+
loading = null;
|
|
66
|
+
destroyed = false;
|
|
67
|
+
hosted = new Map();
|
|
68
|
+
warnedEllipsoid = false;
|
|
69
|
+
/** Longest side of each model file, in its own units, once loaded (sizes models in metres and pixels). */
|
|
70
|
+
extents = new Map();
|
|
71
|
+
/**
|
|
72
|
+
* Placed points per layer, keyed by the scene object: the renderer replaces only objects that changed, so a
|
|
73
|
+
* frame recomputes heights for those alone. Cleared when the layer definition or the ground changes.
|
|
74
|
+
*/
|
|
75
|
+
placed = new Map();
|
|
76
|
+
/** Ground heights by position (terrain lookups are not free); cleared when terrain tiles arrive. */
|
|
77
|
+
groundCache = new Map();
|
|
78
|
+
constructor(env, load = loadDeck) {
|
|
79
|
+
this.env = env;
|
|
80
|
+
this.load = load;
|
|
81
|
+
}
|
|
82
|
+
/** Sets (or replaces) what one engine layer shows. */
|
|
83
|
+
setLayer(layer, objects) {
|
|
84
|
+
this.hosted.set(layer.id, { layer, objects });
|
|
85
|
+
this.render();
|
|
86
|
+
}
|
|
87
|
+
removeLayer(id) {
|
|
88
|
+
if (this.hosted.delete(id))
|
|
89
|
+
this.render();
|
|
90
|
+
}
|
|
91
|
+
has(id) {
|
|
92
|
+
return this.hosted.has(id);
|
|
93
|
+
}
|
|
94
|
+
/** Heights depend on the ground: recompute after terrain loads or changes. */
|
|
95
|
+
refresh() {
|
|
96
|
+
this.groundCache.clear();
|
|
97
|
+
this.placed.clear();
|
|
98
|
+
if (this.hosted.size)
|
|
99
|
+
this.render();
|
|
100
|
+
}
|
|
101
|
+
/** The top object under a screen point among deck.gl-drawn layers. */
|
|
102
|
+
pick(x, y, radius) {
|
|
103
|
+
const hit = this.overlay?.pickObject({ x, y, radius });
|
|
104
|
+
const so = hit?.object?.so;
|
|
105
|
+
return so ? { objectId: so.object.objectId, layerId: so.layerId } : null;
|
|
106
|
+
}
|
|
107
|
+
destroy() {
|
|
108
|
+
this.destroyed = true;
|
|
109
|
+
if (this.overlay) {
|
|
110
|
+
try {
|
|
111
|
+
this.env.removeControl(this.overlay);
|
|
112
|
+
this.overlay.finalize();
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Releasing must always complete.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.overlay = null;
|
|
119
|
+
this.hosted.clear();
|
|
120
|
+
}
|
|
121
|
+
render() {
|
|
122
|
+
if (this.destroyed)
|
|
123
|
+
return;
|
|
124
|
+
if (!this.modules) {
|
|
125
|
+
if (!this.hosted.size)
|
|
126
|
+
return;
|
|
127
|
+
this.loading ??= this.load().then((m) => {
|
|
128
|
+
if (this.destroyed)
|
|
129
|
+
return;
|
|
130
|
+
this.modules = m;
|
|
131
|
+
this.overlay = new m.MapboxOverlay({ interleaved: true, layers: [] });
|
|
132
|
+
this.env.addControl(this.overlay);
|
|
133
|
+
this.render();
|
|
134
|
+
}, (e) => this.env.warn(new Error(`3D layers could not load (deck.gl): ${e instanceof Error ? e.message : String(e)}`)));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const layers = [...this.hosted.values()].sort((a, b) => a.layer.zIndex - b.layer.zIndex).flatMap(({ layer, objects }) => (layer.visible ? this.deckLayers(layer, objects) : []));
|
|
138
|
+
this.overlay.setProps({ layers });
|
|
139
|
+
}
|
|
140
|
+
meshes = new Map();
|
|
141
|
+
meshFor(key, v) {
|
|
142
|
+
let mesh = this.meshes.get(key);
|
|
143
|
+
if (!mesh)
|
|
144
|
+
this.meshes.set(key, (mesh = volumeMesh({ shape: v.shape, beamWidth: v.beamWidth, minElevation: v.minElevation, maxElevation: v.maxElevation })));
|
|
145
|
+
return mesh;
|
|
146
|
+
}
|
|
147
|
+
/** Height of an object in the map's metres (ADR-028), and the ground under it. */
|
|
148
|
+
place(layer, lng, lat, altitude) {
|
|
149
|
+
const key = `${lng.toFixed(6)},${lat.toFixed(6)}`;
|
|
150
|
+
let ground = this.groundCache.get(key);
|
|
151
|
+
if (ground === undefined) {
|
|
152
|
+
ground = this.env.ground(lng, lat);
|
|
153
|
+
if (this.groundCache.size > 200_000)
|
|
154
|
+
this.groundCache.clear();
|
|
155
|
+
this.groundCache.set(key, ground);
|
|
156
|
+
}
|
|
157
|
+
const e = layer.visualization.elevation;
|
|
158
|
+
if (!e || altitude === undefined)
|
|
159
|
+
return { z: ground, ground };
|
|
160
|
+
const k = this.env.terrainExaggeration();
|
|
161
|
+
const lift = e.exaggeration ?? 1;
|
|
162
|
+
if (e.reference === 'agl')
|
|
163
|
+
return { z: ground + altitude * k * lift, ground };
|
|
164
|
+
if (e.reference === 'ellipsoid' && !this.warnedEllipsoid) {
|
|
165
|
+
this.warnedEllipsoid = true;
|
|
166
|
+
this.env.warn(new Error(`layer ${layer.id}: ellipsoid heights are drawn as sea-level heights (no geoid model in this renderer; up to ~100 m off)`));
|
|
167
|
+
}
|
|
168
|
+
const above = altitude * k - ground; // height above the drawn ground
|
|
169
|
+
return { z: ground + above * lift, ground };
|
|
170
|
+
}
|
|
171
|
+
altitudeOf(layer, so, alt) {
|
|
172
|
+
const e = layer.visualization.elevation;
|
|
173
|
+
if (e?.from === 'property')
|
|
174
|
+
return num(so.object.properties?.[e.property]);
|
|
175
|
+
return alt;
|
|
176
|
+
}
|
|
177
|
+
points(layer, objects) {
|
|
178
|
+
let cache = this.placed.get(layer.id);
|
|
179
|
+
if (!cache || cache.layer !== layer)
|
|
180
|
+
this.placed.set(layer.id, (cache = { layer, items: new WeakMap() }));
|
|
181
|
+
const out = [];
|
|
182
|
+
for (const so of objects) {
|
|
183
|
+
const known = cache.items.get(so);
|
|
184
|
+
if (known) {
|
|
185
|
+
out.push(known);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const g = so.object.geometry;
|
|
189
|
+
if (g.type !== 'Point')
|
|
190
|
+
continue;
|
|
191
|
+
const { longitude: lng, latitude: lat, altitude } = g.coordinates;
|
|
192
|
+
const { z, ground } = this.place(layer, lng, lat, this.altitudeOf(layer, so, altitude));
|
|
193
|
+
const item = { so, at: [lng, lat, z], ground };
|
|
194
|
+
cache.items.set(so, item);
|
|
195
|
+
out.push(item);
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
paths(layer, objects) {
|
|
200
|
+
const out = [];
|
|
201
|
+
for (const so of objects) {
|
|
202
|
+
const g = so.object.geometry;
|
|
203
|
+
const lines = g.type === 'LineString' ? [g.coordinates] : g.type === 'MultiLineString' ? g.coordinates : [];
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
out.push({ so, path: line.map((p) => [p.longitude, p.latitude, this.place(layer, p.longitude, p.latitude, this.altitudeOf(layer, so, p.altitude)).z]) });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
deckLayers(layer, objects) {
|
|
211
|
+
const m = this.modules;
|
|
212
|
+
const t = this.env.theme;
|
|
213
|
+
const vz = layer.visualization;
|
|
214
|
+
const rules = vz.style;
|
|
215
|
+
const id = (part) => `me3d:${layer.id}:${part}`;
|
|
216
|
+
const color = (so, fallback) => {
|
|
217
|
+
if (so.selected)
|
|
218
|
+
return rgba(t.selectionColor);
|
|
219
|
+
return rgba(tokenFor(rules, 'color', so.object.properties, fallback), tokenFor(rules, 'opacity', so.object.properties, 1));
|
|
220
|
+
};
|
|
221
|
+
const triggers = objects; // a new list after every scene change: deck re-reads accessors
|
|
222
|
+
const out = [];
|
|
223
|
+
if (vz.type === 'line' || vz.type === 'track') {
|
|
224
|
+
const data = this.paths(layer, objects);
|
|
225
|
+
out.push(new m.PathLayer({
|
|
226
|
+
id: id('path'),
|
|
227
|
+
data,
|
|
228
|
+
getPath: (d) => d.path,
|
|
229
|
+
getColor: (d) => color(d.so, vz.type === 'track' ? t.trackColor : t.lineColor),
|
|
230
|
+
getWidth: (d) => tokenFor(rules, 'size', d.so.object.properties, 3) + (d.so.selected ? 2 : 0),
|
|
231
|
+
widthUnits: 'pixels',
|
|
232
|
+
capRounded: true,
|
|
233
|
+
jointRounded: true,
|
|
234
|
+
pickable: true,
|
|
235
|
+
updateTriggers: { getColor: triggers, getWidth: triggers },
|
|
236
|
+
}));
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
const data = this.points(layer, objects);
|
|
240
|
+
const e = vz.elevation;
|
|
241
|
+
if (e?.shadow) {
|
|
242
|
+
out.push(new m.ScatterplotLayer({
|
|
243
|
+
id: id('shadow'),
|
|
244
|
+
data,
|
|
245
|
+
getPosition: (d) => [d.at[0], d.at[1], d.ground],
|
|
246
|
+
getRadius: 5,
|
|
247
|
+
radiusUnits: 'pixels',
|
|
248
|
+
getFillColor: [0, 0, 0, 70],
|
|
249
|
+
updateTriggers: { getPosition: triggers },
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
if (e?.stems) {
|
|
253
|
+
out.push(new m.LineLayer({
|
|
254
|
+
id: id('stems'),
|
|
255
|
+
data,
|
|
256
|
+
getSourcePosition: (d) => [d.at[0], d.at[1], d.ground],
|
|
257
|
+
getTargetPosition: (d) => d.at,
|
|
258
|
+
getColor: (d) => [...color(d.so, t.markerColor).slice(0, 3), 150],
|
|
259
|
+
getWidth: 1.5,
|
|
260
|
+
widthUnits: 'pixels',
|
|
261
|
+
updateTriggers: { getSourcePosition: triggers, getTargetPosition: triggers, getColor: triggers },
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
if (vz.type === 'volume') {
|
|
265
|
+
// One mesh per distinct shape (dome, or wedge beam and elevation limits); scaled by range, turned by azimuth.
|
|
266
|
+
const groups = new Map();
|
|
267
|
+
for (const d of data) {
|
|
268
|
+
const v = volumeOf(vz.volume, d.so.object.properties);
|
|
269
|
+
if (!v)
|
|
270
|
+
continue;
|
|
271
|
+
const key = `${v.shape}|${v.beamWidth}|${v.minElevation}|${v.maxElevation}`;
|
|
272
|
+
let g = groups.get(key);
|
|
273
|
+
if (!g)
|
|
274
|
+
groups.set(key, (g = { mesh: this.meshFor(key, v), items: [] }));
|
|
275
|
+
g.items.push({ d, v });
|
|
276
|
+
}
|
|
277
|
+
const k = this.env.terrainExaggeration();
|
|
278
|
+
let i = 0;
|
|
279
|
+
for (const { mesh, items } of groups.values()) {
|
|
280
|
+
out.push(new m.SimpleMeshLayer({
|
|
281
|
+
id: id(`volume-${i++}`),
|
|
282
|
+
data: items,
|
|
283
|
+
mesh: { positions: { value: mesh.positions, size: 3 }, normals: { value: mesh.normals, size: 3 } },
|
|
284
|
+
getPosition: (x) => x.d.at,
|
|
285
|
+
// Heights follow the terrain's exaggeration so a volume keeps its relation to the ground.
|
|
286
|
+
getScale: (x) => [x.v.range, x.v.range, x.v.range * k],
|
|
287
|
+
getOrientation: (x) => [0, -x.v.azimuth, 0],
|
|
288
|
+
getColor: (x) => [...color(x.d.so, t.fillColor).slice(0, 3), x.d.so.selected ? 110 : 60],
|
|
289
|
+
parameters: { depthWriteEnabled: false, cullMode: 'none' },
|
|
290
|
+
pickable: true,
|
|
291
|
+
updateTriggers: { getPosition: triggers, getScale: triggers, getOrientation: triggers, getColor: triggers },
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
if (vz.type === 'model') {
|
|
297
|
+
// One scenegraph layer per model file (per-rule models choose by style.modelUrl).
|
|
298
|
+
const byUrl = new Map();
|
|
299
|
+
for (const d of data) {
|
|
300
|
+
const url = tokenFor(rules, 'modelUrl', d.so.object.properties, vz.model?.url ?? '');
|
|
301
|
+
if (url)
|
|
302
|
+
byUrl.set(url, [...(byUrl.get(url) ?? []), d]);
|
|
303
|
+
}
|
|
304
|
+
const model = vz.model ?? {};
|
|
305
|
+
const axis = model.axis ?? {};
|
|
306
|
+
const angle = (d, prop) => (prop ? (num(d.so.object.properties?.[prop]) ?? 0) : 0);
|
|
307
|
+
let i = 0;
|
|
308
|
+
for (const [url, list] of byUrl) {
|
|
309
|
+
const extent = this.extents.get(url);
|
|
310
|
+
out.push(new m.ScenegraphLayer({
|
|
311
|
+
id: id(`model-${i++}`),
|
|
312
|
+
data: list,
|
|
313
|
+
scenegraph: url,
|
|
314
|
+
// Learn the model's size when it loads (deck.gl's default scene choice, plus measuring it once).
|
|
315
|
+
getScene: (gltf) => {
|
|
316
|
+
const scene = gltf && gltf.scenes ? (typeof gltf.scene === 'object' ? gltf.scene : gltf.scenes[Number(gltf.scene ?? 0)]) : gltf;
|
|
317
|
+
const bounds = scene?.getBounds?.();
|
|
318
|
+
if (bounds && !this.extents.has(url)) {
|
|
319
|
+
const longest = Math.max(...bounds[1].map((v, k) => v - bounds[0][k]));
|
|
320
|
+
if (longest > 0) {
|
|
321
|
+
this.extents.set(url, longest);
|
|
322
|
+
queueMicrotask(() => this.render());
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return scene;
|
|
326
|
+
},
|
|
327
|
+
getPosition: (d) => d.at,
|
|
328
|
+
// deck.gl: [pitch, yaw, roll] in degrees; yaw turns counter-clockwise, heading clockwise from north.
|
|
329
|
+
// deck.gl takes a model's z as up: roll 90 stands a glTF file (+Y up) upright, yaw 180 turns its front
|
|
330
|
+
// (+Z) to heading 0; deck's yaw runs counter-clockwise, headings clockwise.
|
|
331
|
+
getOrientation: (d) => [(axis.pitch ?? 0) + angle(d, model.pitchProperty), 180 + (axis.yaw ?? 0) - angle(d, model.headingProperty), 90 + (axis.roll ?? 0) + angle(d, model.rollProperty)],
|
|
332
|
+
getColor: (d) => (d.so.selected ? rgba(t.selectionColor) : [255, 255, 255, 255]),
|
|
333
|
+
// sizeMeters is the model's real length; deck.gl scales model units, so divide by the model's length.
|
|
334
|
+
sizeScale: extent && model.sizeMeters ? model.sizeMeters / extent : 1,
|
|
335
|
+
// deck.gl's minimum is per model unit: keep the whole model at least ~36 px long when zoomed out.
|
|
336
|
+
sizeMinPixels: extent ? MIN_MODEL_PIXELS / extent : 0,
|
|
337
|
+
_lighting: 'pbr',
|
|
338
|
+
pickable: true,
|
|
339
|
+
updateTriggers: { getPosition: triggers, getOrientation: triggers, getColor: triggers },
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
const size = vz.icon?.size ?? 18;
|
|
345
|
+
const withImages = data.filter((d) => tokenFor(rules, 'imageUrl', d.so.object.properties, vz.icon?.imageUrl ?? ''));
|
|
346
|
+
const dots = data.filter((d) => !withImages.includes(d));
|
|
347
|
+
if (withImages.length) {
|
|
348
|
+
out.push(new m.IconLayer({
|
|
349
|
+
id: id('icons'),
|
|
350
|
+
data: withImages,
|
|
351
|
+
getPosition: (d) => d.at,
|
|
352
|
+
getIcon: (d) => ({ url: tokenFor(rules, 'imageUrl', d.so.object.properties, vz.icon?.imageUrl ?? ''), width: 64, height: 64, id: tokenFor(rules, 'imageUrl', d.so.object.properties, vz.icon?.imageUrl ?? '') }),
|
|
353
|
+
getSize: (d) => size + (d.so.selected ? 6 : 0),
|
|
354
|
+
sizeUnits: 'pixels',
|
|
355
|
+
billboard: true,
|
|
356
|
+
pickable: true,
|
|
357
|
+
updateTriggers: { getPosition: triggers, getSize: triggers },
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
if (dots.length) {
|
|
361
|
+
out.push(new m.ScatterplotLayer({
|
|
362
|
+
id: id('dots'),
|
|
363
|
+
data: dots,
|
|
364
|
+
getPosition: (d) => d.at,
|
|
365
|
+
getRadius: (d) => tokenFor(rules, 'size', d.so.object.properties, size / 2) + (d.so.selected ? 3 : 0),
|
|
366
|
+
radiusUnits: 'pixels',
|
|
367
|
+
billboard: true,
|
|
368
|
+
stroked: true,
|
|
369
|
+
getFillColor: (d) => color(d.so, t.markerColor),
|
|
370
|
+
getLineColor: (d) => (d.so.selected ? rgba(t.selectionColor) : rgba(tokenFor(rules, 'strokeColor', d.so.object.properties, '#0B100E'))),
|
|
371
|
+
lineWidthUnits: 'pixels',
|
|
372
|
+
getLineWidth: 1.5,
|
|
373
|
+
pickable: true,
|
|
374
|
+
updateTriggers: { getPosition: triggers, getRadius: triggers, getFillColor: triggers, getLineColor: triggers },
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const font = this.env.font();
|
|
379
|
+
if (vz.label && font) {
|
|
380
|
+
const label = vz.label;
|
|
381
|
+
out.push(new m.TextLayer({
|
|
382
|
+
id: id('labels'),
|
|
383
|
+
data,
|
|
384
|
+
getPosition: (d) => d.at,
|
|
385
|
+
getText: (d) => String(d.so.object.properties?.[label.property] ?? ''),
|
|
386
|
+
getSize: label.size ?? 12,
|
|
387
|
+
getColor: rgba(label.color ?? t.labelColor),
|
|
388
|
+
fontFamily: font,
|
|
389
|
+
fontSettings: { sdf: true },
|
|
390
|
+
outlineWidth: 3,
|
|
391
|
+
outlineColor: rgba(label.haloColor ?? t.labelHalo),
|
|
392
|
+
getPixelOffset: [0, 16],
|
|
393
|
+
getTextAnchor: 'middle',
|
|
394
|
+
getAlignmentBaseline: 'top',
|
|
395
|
+
billboard: true,
|
|
396
|
+
updateTriggers: { getPosition: triggers, getText: triggers },
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in marker shapes as signed-distance-field images, generated in code: no image files, no DOM.
|
|
3
|
+
* SDF images can be recoloured by style rules (`icon-color`) and stay sharp when scaled.
|
|
4
|
+
*/
|
|
5
|
+
import type { IconShape } from '../core/types.js';
|
|
6
|
+
export declare const ICON_PIXELS = 32;
|
|
7
|
+
/**
|
|
8
|
+
* RGBA image whose alpha encodes the SDF the way MapLibre expects (edge at 0.75, `buffer` px of falloff).
|
|
9
|
+
*/
|
|
10
|
+
export declare function shapeImage(shape: IconShape, size?: number, buffer?: number): {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
data: Uint8Array;
|
|
14
|
+
};
|
|
15
|
+
export declare const shapeImageId: (shape: IconShape) => string;
|
|
16
|
+
/** Stable id for an image URL (FNV-1a), so the same image is added once per map. */
|
|
17
|
+
export declare function urlImageId(url: string): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Unit-square polygons ([-1, 1], y down = screen down). The arrow points north at rotation 0. */
|
|
2
|
+
const POLYGONS = {
|
|
3
|
+
square: [[-0.78, -0.78], [0.78, -0.78], [0.78, 0.78], [-0.78, 0.78]],
|
|
4
|
+
triangle: [[0, -0.92], [0.9, 0.78], [-0.9, 0.78]],
|
|
5
|
+
diamond: [[0, -0.98], [0.98, 0], [0, 0.98], [-0.98, 0]],
|
|
6
|
+
arrow: [[0, -1], [0.78, 0.9], [0, 0.42], [-0.78, 0.9]],
|
|
7
|
+
};
|
|
8
|
+
export const ICON_PIXELS = 32;
|
|
9
|
+
function segmentDistance(px, py, ax, ay, bx, by) {
|
|
10
|
+
const dx = bx - ax;
|
|
11
|
+
const dy = by - ay;
|
|
12
|
+
const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy)));
|
|
13
|
+
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
|
|
14
|
+
}
|
|
15
|
+
function inside(px, py, poly) {
|
|
16
|
+
let c = false;
|
|
17
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
18
|
+
const [xi, yi] = poly[i];
|
|
19
|
+
const [xj, yj] = poly[j];
|
|
20
|
+
if (yi > py !== yj > py && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi)
|
|
21
|
+
c = !c;
|
|
22
|
+
}
|
|
23
|
+
return c;
|
|
24
|
+
}
|
|
25
|
+
/** Signed distance in unit space: negative inside the shape. */
|
|
26
|
+
function signedDistance(shape, x, y) {
|
|
27
|
+
if (shape === 'circle')
|
|
28
|
+
return Math.hypot(x, y) - 0.82;
|
|
29
|
+
const poly = POLYGONS[shape];
|
|
30
|
+
let d = Infinity;
|
|
31
|
+
for (let i = 0; i < poly.length; i++) {
|
|
32
|
+
const [ax, ay] = poly[i];
|
|
33
|
+
const [bx, by] = poly[(i + 1) % poly.length];
|
|
34
|
+
d = Math.min(d, segmentDistance(x, y, ax, ay, bx, by));
|
|
35
|
+
}
|
|
36
|
+
return inside(x, y, poly) ? -d : d;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* RGBA image whose alpha encodes the SDF the way MapLibre expects (edge at 0.75, `buffer` px of falloff).
|
|
40
|
+
*/
|
|
41
|
+
export function shapeImage(shape, size = ICON_PIXELS, buffer = 6) {
|
|
42
|
+
const full = size + buffer * 2;
|
|
43
|
+
const data = new Uint8Array(full * full * 4);
|
|
44
|
+
const half = size / 2;
|
|
45
|
+
const radius = buffer + 2;
|
|
46
|
+
for (let py = 0; py < full; py++) {
|
|
47
|
+
for (let px = 0; px < full; px++) {
|
|
48
|
+
const ux = (px + 0.5 - full / 2) / half;
|
|
49
|
+
const uy = (py + 0.5 - full / 2) / half;
|
|
50
|
+
const dPixels = signedDistance(shape, ux, uy) * half;
|
|
51
|
+
const a = Math.max(0, Math.min(255, Math.round(255 - 255 * (dPixels / radius + 0.25))));
|
|
52
|
+
const i = (py * full + px) * 4;
|
|
53
|
+
data[i] = data[i + 1] = data[i + 2] = data[i + 3] = a;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { width: full, height: full, data };
|
|
57
|
+
}
|
|
58
|
+
export const shapeImageId = (shape) => `me-shape-${shape}`;
|
|
59
|
+
/** Stable id for an image URL (FNV-1a), so the same image is added once per map. */
|
|
60
|
+
export function urlImageId(url) {
|
|
61
|
+
let h = 0x811c9dc5;
|
|
62
|
+
for (let i = 0; i < url.length; i++)
|
|
63
|
+
h = Math.imul(h ^ url.charCodeAt(i), 0x01000193);
|
|
64
|
+
return `me-img-${(h >>> 0).toString(36)}`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of MapLibre GL JS the adapter uses, derived from MapLibre's own types (type-only import: no
|
|
3
|
+
* runtime cost, and the slice cannot drift from the real API). Tests drive a fake that fits this slice,
|
|
4
|
+
* because jsdom has no WebGL.
|
|
5
|
+
*/
|
|
6
|
+
import type * as GeoJSON from 'geojson';
|
|
7
|
+
import type { Map as MapLibreMap, MapOptions } from 'maplibre-gl';
|
|
8
|
+
export type MapLike = Pick<MapLibreMap, 'on' | 'off' | 'addSource' | 'getSource' | 'removeSource' | 'addLayer' | 'getLayer' | 'removeLayer' | 'moveLayer' | 'setLayoutProperty' | 'setFeatureState' | 'removeFeatureState' | 'queryRenderedFeatures' | 'jumpTo' | 'easeTo' | 'getCenter' | 'getZoom' | 'getBearing' | 'getPitch' | 'getBounds' | 'getCanvas' | 'getContainer' | 'resize' | 'remove' | 'addImage' | 'hasImage' | 'loadImage' | 'project' | 'setStyle' | 'isSourceLoaded' | 'setProjection' | 'setTerrain' | 'setSky' | 'queryTerrainElevation' | 'addControl' | 'removeControl'>;
|
|
9
|
+
/** What the adapter needs from the `maplibre-gl` module: just the Map constructor. */
|
|
10
|
+
export interface MapLibreModuleLike {
|
|
11
|
+
Map: new (options: MapOptions) => MapLike;
|
|
12
|
+
/** Where MapLibre loads its web worker from (v6). Optional so older modules and fakes still fit. */
|
|
13
|
+
setWorkerUrl?: (url: string) => void;
|
|
14
|
+
/** Custom URL schemes (pmtiles://, contour tiles). Optional so fakes without it still fit. */
|
|
15
|
+
addProtocol?: (id: string, handler: never) => void;
|
|
16
|
+
}
|
|
17
|
+
/** An image source (MapLibre places a picture by four corners). */
|
|
18
|
+
export interface ImageSourceLike {
|
|
19
|
+
updateImage(options: {
|
|
20
|
+
url: string;
|
|
21
|
+
coordinates?: Array<[number, number]>;
|
|
22
|
+
}): unknown;
|
|
23
|
+
setCoordinates(coordinates: Array<[number, number]>): unknown;
|
|
24
|
+
}
|
|
25
|
+
/** The GeoJSON source methods the adapter calls (MapLibre's `getSource` returns a generic Source). */
|
|
26
|
+
export interface GeoJSONSourceLike {
|
|
27
|
+
setData(data: GeoJSON.FeatureCollection): Promise<void> | unknown;
|
|
28
|
+
updateData?(diff: {
|
|
29
|
+
removeAll?: boolean;
|
|
30
|
+
remove?: Array<string | number>;
|
|
31
|
+
add?: GeoJSON.Feature[];
|
|
32
|
+
}): Promise<void> | unknown;
|
|
33
|
+
getClusterExpansionZoom?(clusterId: number): Promise<number>;
|
|
34
|
+
}
|
|
35
|
+
/** Fields the adapter reads from MapLibre mouse events and rendered features. */
|
|
36
|
+
export interface MapMouseEventLike {
|
|
37
|
+
point: {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
};
|
|
41
|
+
lngLat: {
|
|
42
|
+
lng: number;
|
|
43
|
+
lat: number;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export interface RenderedFeatureLike {
|
|
47
|
+
id?: string | number;
|
|
48
|
+
properties: Record<string, unknown>;
|
|
49
|
+
layer: {
|
|
50
|
+
id: string;
|
|
51
|
+
};
|
|
52
|
+
geometry?: {
|
|
53
|
+
type: string;
|
|
54
|
+
coordinates: unknown;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|