@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,49 @@
|
|
|
1
|
+
import type { GeometryType, MapLayer, RendererCapabilities, SpatialObject, StyleRule, StyleTokens, VolumeConfig, SpatialQuery, TimeRange, ValidationResult, VisualizationType } from '../core/types.js';
|
|
2
|
+
/** Which geometry each generic visualization draws. Objects of other types are left out of that layer's scene. */
|
|
3
|
+
export declare const GEOMETRY_FOR: Record<VisualizationType, GeometryType>;
|
|
4
|
+
/** Can this layer draw this object? Families, not exact types: a marker layer draws Point and MultiPoint. */
|
|
5
|
+
export declare function drawable(layer: MapLayer, object: SpatialObject): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Which geometry a visualization's SOURCE must have. Trails are drawn as lines but built from point histories; a
|
|
8
|
+
* link layer's source names the objects whose links it draws (usually points).
|
|
9
|
+
*/
|
|
10
|
+
export declare const SOURCE_GEOMETRY_FOR: Record<VisualizationType, GeometryType>;
|
|
11
|
+
/** Layers whose objects do not come from object queries: trails (history) and links (the link provider). */
|
|
12
|
+
export declare const DERIVED_TYPES: readonly VisualizationType[];
|
|
13
|
+
export declare function validateLayer(value: unknown): ValidationResult;
|
|
14
|
+
/** Holds layer definitions. It knows nothing about data sources or rendering. */
|
|
15
|
+
export declare class LayerRegistry {
|
|
16
|
+
private readonly layers;
|
|
17
|
+
constructor(initial?: MapLayer[]);
|
|
18
|
+
add(layer: MapLayer): void;
|
|
19
|
+
update(id: string, patch: Partial<Omit<MapLayer, 'id'>>): MapLayer;
|
|
20
|
+
remove(id: string): void;
|
|
21
|
+
get(id: string): MapLayer | undefined;
|
|
22
|
+
has(id: string): boolean;
|
|
23
|
+
/** Layers sorted by zIndex, then id, so the order is deterministic. */
|
|
24
|
+
list(): MapLayer[];
|
|
25
|
+
visible(): MapLayer[];
|
|
26
|
+
private require;
|
|
27
|
+
}
|
|
28
|
+
/** What a layer needs from the renderer beyond flat 2D (ADR-026..028). */
|
|
29
|
+
export declare function layerNeeds(layer: MapLayer): Array<keyof RendererCapabilities>;
|
|
30
|
+
/** Provider hints the runtime adds to every layer query (doc §13): the zoom shown and, in replay, the time range. */
|
|
31
|
+
export interface QueryHints {
|
|
32
|
+
zoom?: number;
|
|
33
|
+
time?: TimeRange;
|
|
34
|
+
}
|
|
35
|
+
/** The query a layer needs inside the current viewport bounds (ADR-013: the viewport is a data boundary). */
|
|
36
|
+
export declare function layerQuery(layer: MapLayer, bounds?: SpatialQuery['bounds'], hints?: QueryHints): SpatialQuery;
|
|
37
|
+
/** First matching rule's value for a style token (StyleRule contract), evaluated in JS — for renderers that style objects one by one. */
|
|
38
|
+
export declare function tokenFor<K extends keyof StyleTokens>(rules: StyleRule[] | undefined, key: K, props: Record<string, unknown> | undefined, fallback: NonNullable<StyleTokens[K]>): NonNullable<StyleTokens[K]>;
|
|
39
|
+
/** One object's coverage volume (phase 3E), from the layer's fixed values or the object's properties. */
|
|
40
|
+
export interface ResolvedVolume {
|
|
41
|
+
shape: 'dome' | 'wedge';
|
|
42
|
+
range: number;
|
|
43
|
+
azimuth: number;
|
|
44
|
+
beamWidth: number;
|
|
45
|
+
minElevation: number;
|
|
46
|
+
maxElevation: number;
|
|
47
|
+
}
|
|
48
|
+
/** Resolves a layer's volume settings for one object; null when it has no usable range. */
|
|
49
|
+
export declare function volumeOf(v: VolumeConfig | undefined, props: Record<string, unknown> | undefined): ResolvedVolume | null;
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { MapEngineError } from '../core/errors.js';
|
|
2
|
+
import { geometryFamily } from '../core/geometry-family.js';
|
|
3
|
+
import { validateGeometry, validatePosition } from '../spatial/geometry.js';
|
|
4
|
+
import { matchesCondition, validateConditions } from '../spatial/query.js';
|
|
5
|
+
import { scaled, validatePresentation } from '../present/present.js';
|
|
6
|
+
const VISUALIZATIONS = ['marker', 'line', 'polygon', 'heatmap', 'track', 'extrusion', 'model', 'volume', 'link', 'image'];
|
|
7
|
+
/** Which geometry each generic visualization draws. Objects of other types are left out of that layer's scene. */
|
|
8
|
+
export const GEOMETRY_FOR = { marker: 'Point', line: 'LineString', polygon: 'Polygon', heatmap: 'Point', track: 'LineString', extrusion: 'Polygon', model: 'Point', volume: 'Point', link: 'LineString', image: 'Polygon' };
|
|
9
|
+
/** Can this layer draw this object? Families, not exact types: a marker layer draws Point and MultiPoint. */
|
|
10
|
+
export function drawable(layer, object) {
|
|
11
|
+
return geometryFamily(GEOMETRY_FOR[layer.visualization.type]) === geometryFamily(object.geometry.type);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Which geometry a visualization's SOURCE must have. Trails are drawn as lines but built from point histories; a
|
|
15
|
+
* link layer's source names the objects whose links it draws (usually points).
|
|
16
|
+
*/
|
|
17
|
+
export const SOURCE_GEOMETRY_FOR = { ...GEOMETRY_FOR, track: 'Point', link: 'Point' };
|
|
18
|
+
/** Layers whose objects do not come from object queries: trails (history) and links (the link provider). */
|
|
19
|
+
export const DERIVED_TYPES = ['track', 'link'];
|
|
20
|
+
export function validateLayer(value) {
|
|
21
|
+
if (typeof value !== 'object' || value === null)
|
|
22
|
+
return { valid: false, reason: 'layer is not an object' };
|
|
23
|
+
const l = value;
|
|
24
|
+
if (typeof l.id !== 'string' || l.id.trim() === '')
|
|
25
|
+
return { valid: false, reason: 'layer.id must be a non-empty string' };
|
|
26
|
+
if (l.id.includes(':'))
|
|
27
|
+
return { valid: false, reason: `layer.id "${l.id}" must not contain ":" (used in scene keys)` };
|
|
28
|
+
if (l.id.includes('~') || l.id.startsWith('__'))
|
|
29
|
+
return { valid: false, reason: `layer.id "${l.id}" must not contain "~" or start with "__" (used by engine layers)` };
|
|
30
|
+
if (typeof l.visible !== 'boolean')
|
|
31
|
+
return { valid: false, reason: `layer ${l.id}: visible must be a boolean` };
|
|
32
|
+
if (typeof l.zIndex !== 'number' || !Number.isFinite(l.zIndex))
|
|
33
|
+
return { valid: false, reason: `layer ${l.id}: zIndex must be a finite number` };
|
|
34
|
+
if (!l.visualization || !VISUALIZATIONS.includes(l.visualization.type)) {
|
|
35
|
+
return { valid: false, reason: `layer ${l.id}: visualization.type must be one of ${VISUALIZATIONS.join(', ')}` };
|
|
36
|
+
}
|
|
37
|
+
const s = l.source;
|
|
38
|
+
if (!s || typeof s !== 'object')
|
|
39
|
+
return { valid: false, reason: `layer ${l.id}: source is required` };
|
|
40
|
+
// An empty source would mean "load everything", which the performance model forbids.
|
|
41
|
+
if (s.ontologyType === undefined && s.objectIds === undefined && s.spatialQuery === undefined && s.filter === undefined && s.where === undefined) {
|
|
42
|
+
return { valid: false, reason: `layer ${l.id}: source needs at least one of ontologyType, objectIds, spatialQuery, filter, where` };
|
|
43
|
+
}
|
|
44
|
+
if (s.where !== undefined) {
|
|
45
|
+
const w = validateConditions(s.where, `layer ${l.id}: source.where`);
|
|
46
|
+
if (!w.valid)
|
|
47
|
+
return w;
|
|
48
|
+
}
|
|
49
|
+
if (s.spatialQuery) {
|
|
50
|
+
const q = s.spatialQuery;
|
|
51
|
+
if (q.relation === 'near') {
|
|
52
|
+
const p = validatePosition(q.position, `layer ${l.id} near.position`);
|
|
53
|
+
if (!p.valid)
|
|
54
|
+
return p;
|
|
55
|
+
if (!(q.distanceMeters >= 0))
|
|
56
|
+
return { valid: false, reason: `layer ${l.id}: near.distanceMeters must be >= 0` };
|
|
57
|
+
}
|
|
58
|
+
else if (q.relation === 'nearest') {
|
|
59
|
+
const p = validatePosition(q.position, `layer ${l.id} nearest.position`);
|
|
60
|
+
if (!p.valid)
|
|
61
|
+
return p;
|
|
62
|
+
if (!(Number.isInteger(q.count) && q.count >= 1))
|
|
63
|
+
return { valid: false, reason: `layer ${l.id}: nearest.count must be a whole number >= 1` };
|
|
64
|
+
}
|
|
65
|
+
else if (q.relation !== 'bbox') {
|
|
66
|
+
const g = validateGeometry(q.geometry);
|
|
67
|
+
if (!g.valid)
|
|
68
|
+
return { valid: false, reason: `layer ${l.id}: ${g.reason}` };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const [i, rule] of (l.visualization.style ?? []).entries()) {
|
|
72
|
+
const op = rule.op ?? 'eq';
|
|
73
|
+
if (!RULE_OPS.includes(op))
|
|
74
|
+
return { valid: false, reason: `layer ${l.id}: style[${i}].op "${String(op)}" is not one of ${RULE_OPS.join(', ')}` };
|
|
75
|
+
const v = rule.value ?? rule.equals;
|
|
76
|
+
if (rule.property !== undefined && op === 'between' && !(Array.isArray(v) && v.length === 2 && v.every((n) => typeof n === 'number'))) {
|
|
77
|
+
return { valid: false, reason: `layer ${l.id}: style[${i}] "between" needs value [min, max]` };
|
|
78
|
+
}
|
|
79
|
+
if (rule.property !== undefined && op === 'in' && !Array.isArray(v))
|
|
80
|
+
return { valid: false, reason: `layer ${l.id}: style[${i}] "in" needs an array value` };
|
|
81
|
+
const dash = rule.style?.dash;
|
|
82
|
+
if (dash !== undefined && !(Array.isArray(dash) && dash.length >= 2 && dash.every((n) => typeof n === 'number' && n >= 0) && dash.some((n) => n > 0))) {
|
|
83
|
+
return { valid: false, reason: `layer ${l.id}: style[${i}].dash must be at least two non-negative numbers, e.g. [2, 2]` };
|
|
84
|
+
}
|
|
85
|
+
if (rule.property !== undefined && ['gt', 'gte', 'lt', 'lte'].includes(op) && typeof v !== 'number') {
|
|
86
|
+
return { valid: false, reason: `layer ${l.id}: style[${i}] "${op}" needs a number value` };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const vz = l.visualization;
|
|
90
|
+
const SHAPES = ['circle', 'square', 'triangle', 'diamond', 'arrow'];
|
|
91
|
+
if ((vz.icon || vz.cluster) && vz.type !== 'marker')
|
|
92
|
+
return { valid: false, reason: `layer ${l.id}: icon and cluster apply to marker layers only` };
|
|
93
|
+
if (vz.heatmap && vz.type !== 'heatmap')
|
|
94
|
+
return { valid: false, reason: `layer ${l.id}: heatmap options need visualization.type "heatmap"` };
|
|
95
|
+
if (vz.track && vz.type !== 'track')
|
|
96
|
+
return { valid: false, reason: `layer ${l.id}: track options need visualization.type "track"` };
|
|
97
|
+
if (vz.track?.windowMinutes !== undefined && !(vz.track.windowMinutes > 0))
|
|
98
|
+
return { valid: false, reason: `layer ${l.id}: track.windowMinutes must be > 0` };
|
|
99
|
+
if (vz.icon) {
|
|
100
|
+
if (vz.icon.shape !== undefined && !SHAPES.includes(vz.icon.shape))
|
|
101
|
+
return { valid: false, reason: `layer ${l.id}: icon.shape must be one of ${SHAPES.join(', ')}` };
|
|
102
|
+
const ruleImages = (vz.style ?? []).some((r) => typeof r.style?.imageUrl === 'string');
|
|
103
|
+
if (vz.icon.shape === undefined && !vz.icon.imageUrl && !ruleImages)
|
|
104
|
+
return { valid: false, reason: `layer ${l.id}: icon needs a shape, an imageUrl, or images on its style rules` };
|
|
105
|
+
if (vz.icon.size !== undefined && !(vz.icon.size > 0))
|
|
106
|
+
return { valid: false, reason: `layer ${l.id}: icon.size must be > 0` };
|
|
107
|
+
}
|
|
108
|
+
if (vz.cluster) {
|
|
109
|
+
const { radius, maxZoom } = vz.cluster;
|
|
110
|
+
if ((radius !== undefined && !(radius > 0)) || (maxZoom !== undefined && !(maxZoom >= 0 && maxZoom <= 24)))
|
|
111
|
+
return { valid: false, reason: `layer ${l.id}: cluster.radius must be > 0 and maxZoom within [0, 24]` };
|
|
112
|
+
}
|
|
113
|
+
const v3 = validate3d(l);
|
|
114
|
+
if (!v3.valid)
|
|
115
|
+
return v3;
|
|
116
|
+
const vp = validatePresentation(l);
|
|
117
|
+
if (!vp.valid)
|
|
118
|
+
return vp;
|
|
119
|
+
const label = l.visualization.label;
|
|
120
|
+
if (label !== undefined && (typeof label.property !== 'string' || label.property === ''))
|
|
121
|
+
return { valid: false, reason: `layer ${l.id}: label.property is required` };
|
|
122
|
+
if (label?.placement !== undefined && !['center', 'top'].includes(label.placement))
|
|
123
|
+
return { valid: false, reason: `layer ${l.id}: label.placement must be center or top` };
|
|
124
|
+
return { valid: true };
|
|
125
|
+
}
|
|
126
|
+
const RULE_OPS = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'between', 'in'];
|
|
127
|
+
const REFERENCES = ['msl', 'agl', 'ellipsoid'];
|
|
128
|
+
const ELEVATED = ['marker', 'line', 'track', 'model', 'volume'];
|
|
129
|
+
const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
130
|
+
const isName = (v) => typeof v === 'string' && v !== '';
|
|
131
|
+
/** The 3D options of a layer (ADR-026..028): each on the visualizations it belongs to, with what it needs. */
|
|
132
|
+
function validate3d(l) {
|
|
133
|
+
const vz = l.visualization;
|
|
134
|
+
const bad = (reason) => ({ valid: false, reason: `layer ${l.id}: ${reason}` });
|
|
135
|
+
const e = vz.elevation;
|
|
136
|
+
if (e) {
|
|
137
|
+
if (!ELEVATED.includes(vz.type))
|
|
138
|
+
return bad(`elevation applies to ${ELEVATED.join(', ')} layers`);
|
|
139
|
+
if (e.from !== 'geometry' && e.from !== 'property')
|
|
140
|
+
return bad(`elevation.from must be geometry or property`);
|
|
141
|
+
if (e.from === 'property' && !isName(e.property))
|
|
142
|
+
return bad(`elevation.property is required with from: property`);
|
|
143
|
+
// ADR-028: the reference is never guessed.
|
|
144
|
+
if (!REFERENCES.includes(e.reference))
|
|
145
|
+
return bad(`elevation.reference must be one of ${REFERENCES.join(', ')} (it is never guessed)`);
|
|
146
|
+
if (e.exaggeration !== undefined && !(isNum(e.exaggeration) && e.exaggeration > 0))
|
|
147
|
+
return bad(`elevation.exaggeration must be > 0`);
|
|
148
|
+
}
|
|
149
|
+
if (vz.extrusion && vz.type !== 'extrusion')
|
|
150
|
+
return bad(`extrusion options need visualization.type "extrusion"`);
|
|
151
|
+
if (vz.type === 'extrusion') {
|
|
152
|
+
const x = vz.extrusion ?? {};
|
|
153
|
+
if (!isNum(x.height) && !isName(x.heightProperty))
|
|
154
|
+
return bad(`extrusion needs a height or a heightProperty`);
|
|
155
|
+
if (x.height !== undefined && !(isNum(x.height) && x.height >= 0))
|
|
156
|
+
return bad(`extrusion.height must be >= 0`);
|
|
157
|
+
if (x.base !== undefined && !(isNum(x.base) && x.base >= 0))
|
|
158
|
+
return bad(`extrusion.base must be >= 0`);
|
|
159
|
+
}
|
|
160
|
+
if (vz.model && vz.type !== 'model')
|
|
161
|
+
return bad(`model options need visualization.type "model"`);
|
|
162
|
+
if (vz.type === 'model') {
|
|
163
|
+
const ruleModels = (vz.style ?? []).some((r) => isName(r.style?.modelUrl));
|
|
164
|
+
if (!isName(vz.model?.url) && !ruleModels)
|
|
165
|
+
return bad(`a model layer needs model.url or models on its style rules (style.modelUrl)`);
|
|
166
|
+
if (vz.model?.sizeMeters !== undefined && !(isNum(vz.model.sizeMeters) && vz.model.sizeMeters > 0))
|
|
167
|
+
return bad(`model.sizeMeters must be > 0`);
|
|
168
|
+
}
|
|
169
|
+
if (vz.volume && vz.type !== 'volume')
|
|
170
|
+
return bad(`volume options need visualization.type "volume"`);
|
|
171
|
+
if (vz.type === 'volume') {
|
|
172
|
+
const v = vz.volume;
|
|
173
|
+
if (!v || (v.shape !== 'dome' && v.shape !== 'wedge'))
|
|
174
|
+
return bad(`volume.shape must be dome or wedge`);
|
|
175
|
+
if (!(isNum(v.range) && v.range > 0) && !isName(v.rangeProperty))
|
|
176
|
+
return bad(`volume needs a range (> 0) or a rangeProperty`);
|
|
177
|
+
if (v.shape === 'wedge') {
|
|
178
|
+
if (!isNum(v.azimuth) && !isName(v.azimuthProperty))
|
|
179
|
+
return bad(`a wedge needs an azimuth or azimuthProperty`);
|
|
180
|
+
if (!(isNum(v.beamWidth) && v.beamWidth > 0 && v.beamWidth <= 360) && !isName(v.beamWidthProperty))
|
|
181
|
+
return bad(`a wedge needs a beamWidth in (0, 360] or a beamWidthProperty`);
|
|
182
|
+
}
|
|
183
|
+
const lo = v.minElevation ?? 0;
|
|
184
|
+
const hi = v.maxElevation ?? 90;
|
|
185
|
+
if (!(isNum(lo) && isNum(hi) && lo >= -90 && hi <= 90 && lo < hi))
|
|
186
|
+
return bad(`volume elevations must satisfy -90 <= min < max <= 90`);
|
|
187
|
+
}
|
|
188
|
+
return { valid: true };
|
|
189
|
+
}
|
|
190
|
+
/** Holds layer definitions. It knows nothing about data sources or rendering. */
|
|
191
|
+
export class LayerRegistry {
|
|
192
|
+
layers = new Map();
|
|
193
|
+
constructor(initial = []) {
|
|
194
|
+
for (const l of initial)
|
|
195
|
+
this.add(l);
|
|
196
|
+
}
|
|
197
|
+
add(layer) {
|
|
198
|
+
const v = validateLayer(layer);
|
|
199
|
+
if (!v.valid)
|
|
200
|
+
throw new MapEngineError('INVALID_LAYER', v.reason, { layerId: layer?.id });
|
|
201
|
+
if (this.layers.has(layer.id))
|
|
202
|
+
throw new MapEngineError('DUPLICATE_LAYER', `layer ${layer.id} already exists`, { layerId: layer.id });
|
|
203
|
+
this.layers.set(layer.id, structuredClone(layer));
|
|
204
|
+
}
|
|
205
|
+
update(id, patch) {
|
|
206
|
+
const current = this.require(id);
|
|
207
|
+
const next = { ...current, ...patch, id };
|
|
208
|
+
const v = validateLayer(next);
|
|
209
|
+
if (!v.valid)
|
|
210
|
+
throw new MapEngineError('INVALID_LAYER', v.reason, { layerId: id });
|
|
211
|
+
this.layers.set(id, structuredClone(next));
|
|
212
|
+
return this.get(id);
|
|
213
|
+
}
|
|
214
|
+
remove(id) {
|
|
215
|
+
this.require(id);
|
|
216
|
+
this.layers.delete(id);
|
|
217
|
+
}
|
|
218
|
+
get(id) {
|
|
219
|
+
const l = this.layers.get(id);
|
|
220
|
+
return l ? structuredClone(l) : undefined;
|
|
221
|
+
}
|
|
222
|
+
has(id) {
|
|
223
|
+
return this.layers.has(id);
|
|
224
|
+
}
|
|
225
|
+
/** Layers sorted by zIndex, then id, so the order is deterministic. */
|
|
226
|
+
list() {
|
|
227
|
+
return [...this.layers.values()].sort((a, b) => a.zIndex - b.zIndex || a.id.localeCompare(b.id)).map((l) => structuredClone(l));
|
|
228
|
+
}
|
|
229
|
+
visible() {
|
|
230
|
+
return this.list().filter((l) => l.visible);
|
|
231
|
+
}
|
|
232
|
+
require(id) {
|
|
233
|
+
const l = this.layers.get(id);
|
|
234
|
+
if (!l)
|
|
235
|
+
throw new MapEngineError('UNKNOWN_LAYER', `layer ${id} does not exist`, { layerId: id });
|
|
236
|
+
return l;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** What a layer needs from the renderer beyond flat 2D (ADR-026..028). */
|
|
240
|
+
export function layerNeeds(layer) {
|
|
241
|
+
const needs = [];
|
|
242
|
+
const t = layer.visualization.type;
|
|
243
|
+
if (t === 'extrusion')
|
|
244
|
+
needs.push('extrusion');
|
|
245
|
+
if (t === 'model')
|
|
246
|
+
needs.push('models');
|
|
247
|
+
if (t === 'volume')
|
|
248
|
+
needs.push('volumes');
|
|
249
|
+
if (layer.visualization.elevation)
|
|
250
|
+
needs.push('altitude');
|
|
251
|
+
return needs;
|
|
252
|
+
}
|
|
253
|
+
/** The query a layer needs inside the current viewport bounds (ADR-013: the viewport is a data boundary). */
|
|
254
|
+
export function layerQuery(layer, bounds, hints) {
|
|
255
|
+
const q = { geometryType: GEOMETRY_FOR[layer.visualization.type] };
|
|
256
|
+
if (layer.source.ontologyType !== undefined)
|
|
257
|
+
q.ontologyType = layer.source.ontologyType;
|
|
258
|
+
if (layer.source.objectIds !== undefined)
|
|
259
|
+
q.objectIds = [...layer.source.objectIds];
|
|
260
|
+
if (layer.source.spatialQuery !== undefined)
|
|
261
|
+
q.spatial = structuredClone(layer.source.spatialQuery);
|
|
262
|
+
if (layer.source.filter !== undefined)
|
|
263
|
+
q.filter = structuredClone(layer.source.filter);
|
|
264
|
+
if (layer.source.where !== undefined)
|
|
265
|
+
q.where = structuredClone(layer.source.where);
|
|
266
|
+
if (bounds)
|
|
267
|
+
q.bounds = { ...bounds };
|
|
268
|
+
// Rounded, so a provider that caches by query is not defeated by tiny camera moves.
|
|
269
|
+
if (hints?.zoom !== undefined)
|
|
270
|
+
q.zoom = Math.round(hints.zoom * 100) / 100;
|
|
271
|
+
if (hints?.time)
|
|
272
|
+
q.time = { ...hints.time };
|
|
273
|
+
return q;
|
|
274
|
+
}
|
|
275
|
+
/** First matching rule's value for a style token (StyleRule contract), evaluated in JS — for renderers that style objects one by one. */
|
|
276
|
+
export function tokenFor(rules, key, props, fallback) {
|
|
277
|
+
// A continuous scale's value (set per object by the runtime) wins over every rule.
|
|
278
|
+
const own = props?.[scaled(key)];
|
|
279
|
+
if (own !== undefined && own !== null)
|
|
280
|
+
return own;
|
|
281
|
+
for (const r of rules ?? []) {
|
|
282
|
+
const v = r.style[key];
|
|
283
|
+
if (v === undefined)
|
|
284
|
+
continue;
|
|
285
|
+
if (r.property === undefined)
|
|
286
|
+
return v;
|
|
287
|
+
if (matchesCondition(props?.[r.property], { property: r.property, op: r.op ?? 'eq', value: r.value !== undefined ? r.value : r.equals }))
|
|
288
|
+
return v;
|
|
289
|
+
}
|
|
290
|
+
return fallback;
|
|
291
|
+
}
|
|
292
|
+
/** Resolves a layer's volume settings for one object; null when it has no usable range. */
|
|
293
|
+
export function volumeOf(v, props) {
|
|
294
|
+
if (!v)
|
|
295
|
+
return null;
|
|
296
|
+
const read = (fixed, prop) => {
|
|
297
|
+
if (prop !== undefined) {
|
|
298
|
+
const raw = props?.[prop];
|
|
299
|
+
const n = typeof raw === 'number' ? raw : typeof raw === 'string' && raw.trim() !== '' ? Number(raw) : Number.NaN;
|
|
300
|
+
if (Number.isFinite(n))
|
|
301
|
+
return n;
|
|
302
|
+
}
|
|
303
|
+
return fixed;
|
|
304
|
+
};
|
|
305
|
+
const range = read(v.range, v.rangeProperty);
|
|
306
|
+
if (!(range !== undefined && range > 0))
|
|
307
|
+
return null;
|
|
308
|
+
return {
|
|
309
|
+
shape: v.shape,
|
|
310
|
+
range,
|
|
311
|
+
azimuth: read(v.azimuth, v.azimuthProperty) ?? 0,
|
|
312
|
+
beamWidth: v.shape === 'wedge' ? Math.max(1, Math.min(360, read(v.beamWidth, v.beamWidthProperty) ?? 60)) : 360,
|
|
313
|
+
minElevation: v.minElevation ?? 0,
|
|
314
|
+
maxElevation: v.maxElevation ?? 90,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deck.gl inside the MapLibre map (ADR-027, phase 3C): the engine layers MapLibre cannot draw — points and lines at
|
|
3
|
+
* their real height (with stems and ground shadows) and 3D models — rendered by deck.gl in the same WebGL context,
|
|
4
|
+
* interleaved with MapLibre's layers and terrain. Loaded only when a map has such a layer.
|
|
5
|
+
*
|
|
6
|
+
* Heights follow ADR-028: the layer states the reference; heights scale with the terrain's exaggeration so an
|
|
7
|
+
* aircraft keeps its true relation to the (exaggerated) mountains; `agl` stands on the ground under the object.
|
|
8
|
+
* No deck.gl type crosses into the engine: the renderer talks to this host, the host to deck.gl.
|
|
9
|
+
*/
|
|
10
|
+
import type { MapLayer, SceneObject } from '../core/types.js';
|
|
11
|
+
import type { RendererTheme } from './style.js';
|
|
12
|
+
type LayerCtor = new (props: Record<string, unknown>) => unknown;
|
|
13
|
+
/** The slice of deck.gl the host uses (tests inject a fake; WebGL is not available in jsdom). */
|
|
14
|
+
export interface DeckModules {
|
|
15
|
+
MapboxOverlay: new (props: {
|
|
16
|
+
interleaved: boolean;
|
|
17
|
+
layers: unknown[];
|
|
18
|
+
}) => DeckOverlayLike;
|
|
19
|
+
ScatterplotLayer: LayerCtor;
|
|
20
|
+
IconLayer: LayerCtor;
|
|
21
|
+
PathLayer: LayerCtor;
|
|
22
|
+
LineLayer: LayerCtor;
|
|
23
|
+
TextLayer: LayerCtor;
|
|
24
|
+
ScenegraphLayer: LayerCtor;
|
|
25
|
+
SimpleMeshLayer: LayerCtor;
|
|
26
|
+
}
|
|
27
|
+
export interface DeckOverlayLike {
|
|
28
|
+
setProps(props: {
|
|
29
|
+
layers: unknown[];
|
|
30
|
+
}): void;
|
|
31
|
+
pickObject(params: {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
radius?: number;
|
|
35
|
+
}): {
|
|
36
|
+
object?: unknown;
|
|
37
|
+
} | null;
|
|
38
|
+
finalize(): void;
|
|
39
|
+
}
|
|
40
|
+
/** What the host needs from the map it lives in. */
|
|
41
|
+
export interface DeckHostEnv {
|
|
42
|
+
addControl(overlay: DeckOverlayLike): void;
|
|
43
|
+
removeControl(overlay: DeckOverlayLike): void;
|
|
44
|
+
theme: RendererTheme;
|
|
45
|
+
/** The label font's family (labels at height are drawn by deck.gl); none = no labels. */
|
|
46
|
+
font(): string | undefined;
|
|
47
|
+
/** The terrain's exaggeration (1 without terrain). */
|
|
48
|
+
terrainExaggeration(): number;
|
|
49
|
+
/** Ground height (already exaggerated, as the map draws it) under a point; 0 when unknown or flat. */
|
|
50
|
+
ground(lng: number, lat: number): number;
|
|
51
|
+
warn(error: Error): void;
|
|
52
|
+
}
|
|
53
|
+
/** Default loader: the real deck.gl, split into its own chunks so 2D maps never download it. */
|
|
54
|
+
export declare function loadDeck(): Promise<DeckModules>;
|
|
55
|
+
/**
|
|
56
|
+
* Version shim (MapLibre v6 × deck.gl 9.4): deck.gl's interleaved mode reads MapLibre's private `map.transform`
|
|
57
|
+
* (height, elevation, _nearZ, _farZ). MapLibre v6 moved the transform under `map._camera` and renamed the planes
|
|
58
|
+
* to nearZ/farZ. When `map.transform` is missing, define a read-only view onto the v6 transform. Delete this once
|
|
59
|
+
* deck.gl supports MapLibre v6 natively.
|
|
60
|
+
*/
|
|
61
|
+
export declare function shimTransformForDeck(map: object): void;
|
|
62
|
+
/** Layers this host draws instead of MapLibre: anything at a height, and models. */
|
|
63
|
+
export declare function hostedByDeck(layer: MapLayer): boolean;
|
|
64
|
+
export declare class DeckHost {
|
|
65
|
+
private readonly env;
|
|
66
|
+
private readonly load;
|
|
67
|
+
private modules;
|
|
68
|
+
private overlay;
|
|
69
|
+
private loading;
|
|
70
|
+
private destroyed;
|
|
71
|
+
private readonly hosted;
|
|
72
|
+
private warnedEllipsoid;
|
|
73
|
+
/** Longest side of each model file, in its own units, once loaded (sizes models in metres and pixels). */
|
|
74
|
+
private readonly extents;
|
|
75
|
+
/**
|
|
76
|
+
* Placed points per layer, keyed by the scene object: the renderer replaces only objects that changed, so a
|
|
77
|
+
* frame recomputes heights for those alone. Cleared when the layer definition or the ground changes.
|
|
78
|
+
*/
|
|
79
|
+
private placed;
|
|
80
|
+
/** Ground heights by position (terrain lookups are not free); cleared when terrain tiles arrive. */
|
|
81
|
+
private groundCache;
|
|
82
|
+
constructor(env: DeckHostEnv, load?: () => Promise<DeckModules>);
|
|
83
|
+
/** Sets (or replaces) what one engine layer shows. */
|
|
84
|
+
setLayer(layer: MapLayer, objects: SceneObject[]): void;
|
|
85
|
+
removeLayer(id: string): void;
|
|
86
|
+
has(id: string): boolean;
|
|
87
|
+
/** Heights depend on the ground: recompute after terrain loads or changes. */
|
|
88
|
+
refresh(): void;
|
|
89
|
+
/** The top object under a screen point among deck.gl-drawn layers. */
|
|
90
|
+
pick(x: number, y: number, radius: number): {
|
|
91
|
+
objectId: string;
|
|
92
|
+
layerId: string;
|
|
93
|
+
} | null;
|
|
94
|
+
destroy(): void;
|
|
95
|
+
private render;
|
|
96
|
+
private readonly meshes;
|
|
97
|
+
private meshFor;
|
|
98
|
+
/** Height of an object in the map's metres (ADR-028), and the ground under it. */
|
|
99
|
+
private place;
|
|
100
|
+
private altitudeOf;
|
|
101
|
+
private points;
|
|
102
|
+
private paths;
|
|
103
|
+
private deckLayers;
|
|
104
|
+
}
|
|
105
|
+
export {};
|