@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.
Files changed (178) hide show
  1. package/README.md +34 -1
  2. package/dist/SignIn.js +2 -2
  3. package/dist/config.d.ts +5 -0
  4. package/dist/config.js +1 -0
  5. package/dist/map/globe.d.ts +5 -0
  6. package/dist/map/globe.js +5 -0
  7. package/dist/map/index.d.ts +21 -0
  8. package/dist/map/index.js +21 -0
  9. package/dist/map/source.d.ts +46 -0
  10. package/dist/map/source.js +54 -0
  11. package/dist/map/testing.d.ts +5 -0
  12. package/dist/map/testing.js +5 -0
  13. package/dist/session.d.ts +1 -1
  14. package/dist/session.js +1 -1
  15. package/package.json +55 -5
  16. package/vendor/SOURCE.txt +1 -0
  17. package/vendor/map-engine/cesium/camera.d.ts +10 -0
  18. package/vendor/map-engine/cesium/camera.js +25 -0
  19. package/vendor/map-engine/cesium/index.d.ts +2 -0
  20. package/vendor/map-engine/cesium/index.js +2 -0
  21. package/vendor/map-engine/cesium/renderer.d.ts +149 -0
  22. package/vendor/map-engine/cesium/renderer.js +837 -0
  23. package/vendor/map-engine/commands/index.d.ts +2 -0
  24. package/vendor/map-engine/commands/index.js +2 -0
  25. package/vendor/map-engine/commands/post-message.d.ts +32 -0
  26. package/vendor/map-engine/commands/post-message.js +77 -0
  27. package/vendor/map-engine/commands/webmcp.d.ts +17 -0
  28. package/vendor/map-engine/commands/webmcp.js +59 -0
  29. package/vendor/map-engine/core/commands.d.ts +82 -0
  30. package/vendor/map-engine/core/commands.js +46 -0
  31. package/vendor/map-engine/core/emitter.d.ts +9 -0
  32. package/vendor/map-engine/core/emitter.js +38 -0
  33. package/vendor/map-engine/core/equal.d.ts +2 -0
  34. package/vendor/map-engine/core/equal.js +28 -0
  35. package/vendor/map-engine/core/errors.d.ts +20 -0
  36. package/vendor/map-engine/core/errors.js +34 -0
  37. package/vendor/map-engine/core/geometry-family.d.ts +3 -0
  38. package/vendor/map-engine/core/geometry-family.js +13 -0
  39. package/vendor/map-engine/core/index.d.ts +4 -0
  40. package/vendor/map-engine/core/index.js +3 -0
  41. package/vendor/map-engine/core/types.d.ts +709 -0
  42. package/vendor/map-engine/core/types.js +7 -0
  43. package/vendor/map-engine/data/annotations.d.ts +3 -0
  44. package/vendor/map-engine/data/annotations.js +63 -0
  45. package/vendor/map-engine/data/contracts.d.ts +172 -0
  46. package/vendor/map-engine/data/contracts.js +12 -0
  47. package/vendor/map-engine/data/index.d.ts +4 -0
  48. package/vendor/map-engine/data/index.js +3 -0
  49. package/vendor/map-engine/data/schema.d.ts +7 -0
  50. package/vendor/map-engine/data/schema.js +40 -0
  51. package/vendor/map-engine/geofence/evaluator.d.ts +35 -0
  52. package/vendor/map-engine/geofence/evaluator.js +172 -0
  53. package/vendor/map-engine/geofence/history.d.ts +9 -0
  54. package/vendor/map-engine/geofence/history.js +19 -0
  55. package/vendor/map-engine/geofence/index.d.ts +2 -0
  56. package/vendor/map-engine/geofence/index.js +2 -0
  57. package/vendor/map-engine/index.d.ts +27 -0
  58. package/vendor/map-engine/index.js +14 -0
  59. package/vendor/map-engine/layers/index.d.ts +1 -0
  60. package/vendor/map-engine/layers/index.js +1 -0
  61. package/vendor/map-engine/layers/layers.d.ts +49 -0
  62. package/vendor/map-engine/layers/layers.js +316 -0
  63. package/vendor/map-engine/maplibre/deck-host.d.ts +105 -0
  64. package/vendor/map-engine/maplibre/deck-host.js +401 -0
  65. package/vendor/map-engine/maplibre/icons.d.ts +17 -0
  66. package/vendor/map-engine/maplibre/icons.js +65 -0
  67. package/vendor/map-engine/maplibre/index.d.ts +3 -0
  68. package/vendor/map-engine/maplibre/index.js +2 -0
  69. package/vendor/map-engine/maplibre/map-like.d.ts +56 -0
  70. package/vendor/map-engine/maplibre/map-like.js +1 -0
  71. package/vendor/map-engine/maplibre/renderer.d.ts +173 -0
  72. package/vendor/map-engine/maplibre/renderer.js +889 -0
  73. package/vendor/map-engine/maplibre/style.d.ts +84 -0
  74. package/vendor/map-engine/maplibre/style.js +493 -0
  75. package/vendor/map-engine/present/decorations.d.ts +25 -0
  76. package/vendor/map-engine/present/decorations.js +115 -0
  77. package/vendor/map-engine/present/index.d.ts +2 -0
  78. package/vendor/map-engine/present/index.js +2 -0
  79. package/vendor/map-engine/present/present.d.ts +47 -0
  80. package/vendor/map-engine/present/present.js +149 -0
  81. package/vendor/map-engine/query/index.d.ts +1 -0
  82. package/vendor/map-engine/query/index.js +1 -0
  83. package/vendor/map-engine/query/planner.d.ts +26 -0
  84. package/vendor/map-engine/query/planner.js +47 -0
  85. package/vendor/map-engine/react/MapView.d.ts +87 -0
  86. package/vendor/map-engine/react/MapView.js +270 -0
  87. package/vendor/map-engine/react/context.d.ts +39 -0
  88. package/vendor/map-engine/react/context.js +94 -0
  89. package/vendor/map-engine/react/index.d.ts +2 -0
  90. package/vendor/map-engine/react/index.js +2 -0
  91. package/vendor/map-engine/react/store.d.ts +19 -0
  92. package/vendor/map-engine/react/store.js +18 -0
  93. package/vendor/map-engine/realtime/apply.d.ts +74 -0
  94. package/vendor/map-engine/realtime/apply.js +155 -0
  95. package/vendor/map-engine/realtime/index.d.ts +1 -0
  96. package/vendor/map-engine/realtime/index.js +1 -0
  97. package/vendor/map-engine/render/index.d.ts +1 -0
  98. package/vendor/map-engine/render/index.js +1 -0
  99. package/vendor/map-engine/render/offline.d.ts +8 -0
  100. package/vendor/map-engine/render/offline.js +47 -0
  101. package/vendor/map-engine/render/renderer.d.ts +74 -0
  102. package/vendor/map-engine/render/renderer.js +1 -0
  103. package/vendor/map-engine/render/terrarium.d.ts +48 -0
  104. package/vendor/map-engine/render/terrarium.js +110 -0
  105. package/vendor/map-engine/runtime/drawing.d.ts +28 -0
  106. package/vendor/map-engine/runtime/drawing.js +58 -0
  107. package/vendor/map-engine/runtime/index.d.ts +5 -0
  108. package/vendor/map-engine/runtime/index.js +2 -0
  109. package/vendor/map-engine/runtime/offline.d.ts +24 -0
  110. package/vendor/map-engine/runtime/offline.js +37 -0
  111. package/vendor/map-engine/runtime/overlays.d.ts +25 -0
  112. package/vendor/map-engine/runtime/overlays.js +65 -0
  113. package/vendor/map-engine/runtime/runtime.d.ts +4 -0
  114. package/vendor/map-engine/runtime/runtime.js +2105 -0
  115. package/vendor/map-engine/runtime/types.d.ts +407 -0
  116. package/vendor/map-engine/runtime/types.js +1 -0
  117. package/vendor/map-engine/scene/index.d.ts +1 -0
  118. package/vendor/map-engine/scene/index.js +1 -0
  119. package/vendor/map-engine/scene/scene.d.ts +31 -0
  120. package/vendor/map-engine/scene/scene.js +216 -0
  121. package/vendor/map-engine/selection/index.d.ts +1 -0
  122. package/vendor/map-engine/selection/index.js +1 -0
  123. package/vendor/map-engine/selection/selection.d.ts +12 -0
  124. package/vendor/map-engine/selection/selection.js +28 -0
  125. package/vendor/map-engine/spatial/geojson.d.ts +15 -0
  126. package/vendor/map-engine/spatial/geojson.js +83 -0
  127. package/vendor/map-engine/spatial/geometry.d.ts +13 -0
  128. package/vendor/map-engine/spatial/geometry.js +206 -0
  129. package/vendor/map-engine/spatial/import.d.ts +27 -0
  130. package/vendor/map-engine/spatial/import.js +96 -0
  131. package/vendor/map-engine/spatial/index.d.ts +38 -0
  132. package/vendor/map-engine/spatial/index.js +37 -0
  133. package/vendor/map-engine/spatial/line-of-sight.d.ts +35 -0
  134. package/vendor/map-engine/spatial/line-of-sight.js +43 -0
  135. package/vendor/map-engine/spatial/measure.d.ts +28 -0
  136. package/vendor/map-engine/spatial/measure.js +90 -0
  137. package/vendor/map-engine/spatial/operations.d.ts +21 -0
  138. package/vendor/map-engine/spatial/operations.js +171 -0
  139. package/vendor/map-engine/spatial/query.d.ts +17 -0
  140. package/vendor/map-engine/spatial/query.js +88 -0
  141. package/vendor/map-engine/spatial/spatial-index.d.ts +24 -0
  142. package/vendor/map-engine/spatial/spatial-index.js +36 -0
  143. package/vendor/map-engine/spatial/volume-mesh.d.ts +19 -0
  144. package/vendor/map-engine/spatial/volume-mesh.js +56 -0
  145. package/vendor/map-engine/temporal/index.d.ts +3 -0
  146. package/vendor/map-engine/temporal/index.js +3 -0
  147. package/vendor/map-engine/temporal/journal.d.ts +20 -0
  148. package/vendor/map-engine/temporal/journal.js +34 -0
  149. package/vendor/map-engine/temporal/playback.d.ts +41 -0
  150. package/vendor/map-engine/temporal/playback.js +80 -0
  151. package/vendor/map-engine/temporal/tracks.d.ts +33 -0
  152. package/vendor/map-engine/temporal/tracks.js +119 -0
  153. package/vendor/map-engine/testing/annotation-store.d.ts +24 -0
  154. package/vendor/map-engine/testing/annotation-store.js +43 -0
  155. package/vendor/map-engine/testing/contracts.d.ts +41 -0
  156. package/vendor/map-engine/testing/contracts.js +328 -0
  157. package/vendor/map-engine/testing/fixtures-monitoring.d.ts +22 -0
  158. package/vendor/map-engine/testing/fixtures-monitoring.js +103 -0
  159. package/vendor/map-engine/testing/fixtures.d.ts +18 -0
  160. package/vendor/map-engine/testing/fixtures.js +28 -0
  161. package/vendor/map-engine/testing/index.d.ts +7 -0
  162. package/vendor/map-engine/testing/index.js +7 -0
  163. package/vendor/map-engine/testing/mock-ontology.d.ts +13 -0
  164. package/vendor/map-engine/testing/mock-ontology.js +24 -0
  165. package/vendor/map-engine/testing/mock-provider.d.ts +97 -0
  166. package/vendor/map-engine/testing/mock-provider.js +369 -0
  167. package/vendor/map-engine/testing/test-renderer.d.ts +72 -0
  168. package/vendor/map-engine/testing/test-renderer.js +153 -0
  169. package/vendor/map-engine/viewport/index.d.ts +1 -0
  170. package/vendor/map-engine/viewport/index.js +1 -0
  171. package/vendor/map-engine/viewport/viewport.d.ts +19 -0
  172. package/vendor/map-engine/viewport/viewport.js +102 -0
  173. package/vendor/map-provider-uns/client.d.ts +75 -0
  174. package/vendor/map-provider-uns/client.js +111 -0
  175. package/vendor/map-provider-uns/index.d.ts +2 -0
  176. package/vendor/map-provider-uns/index.js +2 -0
  177. package/vendor/map-provider-uns/provider.d.ts +131 -0
  178. package/vendor/map-provider-uns/provider.js +561 -0
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Pure translation from engine layers and basemap config to MapLibre style objects.
3
+ * Generic visualization only — nothing here knows what an object represents.
4
+ */
5
+ import type { BasemapConfig, MapLayer, StyleRule, StyleTokens, TerrainConfig } from '../core/types.js';
6
+ /** Font name used in `text-font` when the basemap supplies a label font. */
7
+ export declare const LABEL_FONT = "me-label";
8
+ export interface RendererTheme {
9
+ markerColor: string;
10
+ lineColor: string;
11
+ fillColor: string;
12
+ selectionColor: string;
13
+ hoverColor: string;
14
+ background: string;
15
+ labelColor: string;
16
+ labelHalo: string;
17
+ clusterColor: string;
18
+ clusterRing: string;
19
+ clusterText: string;
20
+ heatLow: string;
21
+ heatMid: string;
22
+ heatHigh: string;
23
+ heatMax: string;
24
+ trackColor: string;
25
+ /** Sky and horizon when the camera tilts or the map is a globe (phase 3B). */
26
+ skyColor: string;
27
+ horizonColor: string;
28
+ hillshadeShadow: string;
29
+ hillshadeHighlight: string;
30
+ contourColor: string;
31
+ }
32
+ export declare const DEFAULT_THEME: RendererTheme;
33
+ /** Source options for an engine layer: clustering is a GeoJSON source option. */
34
+ export declare function sourceSpecFor(layer: MapLayer): Record<string, unknown>;
35
+ /** Source and layer ids of the terrain the renderer adds under the engine's layers (phase 3B). */
36
+ export declare const TERRAIN_SOURCE = "me:terrain-dem";
37
+ export declare const HILLSHADE_SOURCE = "me:hillshade-dem";
38
+ export declare const HILLSHADE_LAYER = "me:hillshade";
39
+ /** A raster-dem source for a terrain config (one for the 3D surface, one for hillshade: MapLibre advises two). */
40
+ /** A `pmtiles://` archive is one URL (MapLibre reads its tiles through the pmtiles protocol), not a tile template. */
41
+ export declare const isPmtiles: (url: string | undefined) => boolean;
42
+ export declare function demSourceFor(terrain: TerrainConfig): Record<string, unknown>;
43
+ /** Relief shading from the terrain, drawn under every engine layer. */
44
+ export declare function hillshadeLayer(theme: RendererTheme): Record<string, unknown>;
45
+ export declare const CONTOUR_SOURCE = "me:contours";
46
+ export declare const CONTOUR_LINES = "me:contour-lines";
47
+ export declare const CONTOUR_LABELS = "me:contour-labels";
48
+ /** Contour lines (doc 28 G11) from a vector source of `contours` features with `ele` and `level` (1 = index line). */
49
+ export declare function contourLayers(theme: RendererTheme, color: string | undefined, hasFont: boolean): Array<Record<string, unknown>>;
50
+ /** Images a layer needs registered before its symbol layers render. */
51
+ export declare function imagesFor(layer: MapLayer): Array<{
52
+ id: string;
53
+ shape?: NonNullable<MapLayer['visualization']['icon']>['shape'];
54
+ url?: string;
55
+ }>;
56
+ /** Prefix for every source and style layer the engine owns, so basemap layers are never touched. */
57
+ export declare const PREFIX = "me";
58
+ export declare const sourceId: (layerId: string) => string;
59
+ export declare const OBJECT_ID_PROP = "__oid";
60
+ export declare const LAYER_ID_PROP = "__lid";
61
+ /** Marks the one label point an area gets in its own source (so a large area is labelled once, not once per tile). */
62
+ export declare const LABEL_POINT_PROP = "__lp";
63
+ /** Area layers with a label carry a label point per object next to the shape. */
64
+ export declare function needsLabelPoints(layer: MapLayer): boolean;
65
+ /** Data-driven dashes (StyleTokens.dash), first matching rule wins; null when no rule asks for dashes. */
66
+ export declare function dashExpression(rules: StyleRule[] | undefined): unknown;
67
+ export type StyleLayerSpec = Record<string, unknown> & {
68
+ id: string;
69
+ type: string;
70
+ source?: string;
71
+ };
72
+ /** MapLibre style for the configured basemap. 'none' needs no network at all. */
73
+ export declare function basemapStyle(basemap: BasemapConfig, theme: RendererTheme): string | Record<string, unknown>;
74
+ /**
75
+ * A fetched street-map style with the engine's label font added (MapLibre v6 font-faces) and its relative URLs
76
+ * made absolute, so it can be passed as an object.
77
+ */
78
+ export declare function withLabelFont(style: Record<string, unknown>, styleUrl: string, fontUrl: string): Record<string, unknown>;
79
+ /** The MapLibre condition for one rule, or null when the rule cannot be expressed (it is then skipped). */
80
+ export declare function ruleCondition(rule: StyleRule): unknown | null;
81
+ /** A token's expression: a continuous scale's per-object value (`$s:<token>`) first, then the rules. */
82
+ export declare function tokenExpression<K extends keyof StyleTokens>(rules: StyleRule[] | undefined, key: K, fallback: NonNullable<StyleTokens[K]>): unknown;
83
+ /** The MapLibre style layers for one engine layer, bottom to top. Labels need a label font (`hasFont`). */
84
+ export declare function styleLayersFor(layer: MapLayer, theme: RendererTheme, hasFont?: boolean): StyleLayerSpec[];
@@ -0,0 +1,493 @@
1
+ import { ICON_PIXELS, shapeImageId, urlImageId } from './icons.js';
2
+ import { hostedByDeck } from './deck-host.js';
3
+ import { scaled } from '../present/present.js';
4
+ /** Font name used in `text-font` when the basemap supplies a label font. */
5
+ export const LABEL_FONT = 'me-label';
6
+ export const DEFAULT_THEME = {
7
+ skyColor: '#7FB2E5',
8
+ horizonColor: '#DCE8F2',
9
+ hillshadeShadow: '#2B2B2B',
10
+ hillshadeHighlight: '#FFFFFF',
11
+ contourColor: '#8A6F4E',
12
+ markerColor: '#4CC3FF',
13
+ lineColor: '#C9A66B',
14
+ fillColor: '#4CC3FF',
15
+ selectionColor: '#FFD43B',
16
+ hoverColor: '#FFFFFF',
17
+ background: '#0E1512',
18
+ labelColor: '#DCE5DF',
19
+ labelHalo: '#0B100E',
20
+ clusterColor: '#1E6F95',
21
+ clusterRing: '#4CC3FF',
22
+ clusterText: '#FFFFFF',
23
+ heatLow: '#1E6F95',
24
+ heatMid: '#4CC3FF',
25
+ heatHigh: '#FFD43B',
26
+ heatMax: '#FF5A4E',
27
+ trackColor: '#4CC3FF',
28
+ };
29
+ /** Source options for an engine layer: clustering is a GeoJSON source option. */
30
+ export function sourceSpecFor(layer) {
31
+ const c = layer.visualization.type === 'marker' ? layer.visualization.cluster : undefined;
32
+ return {
33
+ type: 'geojson',
34
+ data: { type: 'FeatureCollection', features: [] },
35
+ ...(c ? { cluster: true, clusterRadius: c.radius ?? 50, clusterMaxZoom: c.maxZoom ?? 14 } : {}),
36
+ };
37
+ }
38
+ /** Source and layer ids of the terrain the renderer adds under the engine's layers (phase 3B). */
39
+ export const TERRAIN_SOURCE = `${'me'}:terrain-dem`;
40
+ export const HILLSHADE_SOURCE = `${'me'}:hillshade-dem`;
41
+ export const HILLSHADE_LAYER = `${'me'}:hillshade`;
42
+ /** A raster-dem source for a terrain config (one for the 3D surface, one for hillshade: MapLibre advises two). */
43
+ /** A `pmtiles://` archive is one URL (MapLibre reads its tiles through the pmtiles protocol), not a tile template. */
44
+ export const isPmtiles = (url) => !!url && /^pmtiles:\/\//i.test(url);
45
+ const tilesOrUrl = (url) => (isPmtiles(url) ? { url } : { tiles: [url] });
46
+ export function demSourceFor(terrain) {
47
+ return {
48
+ type: 'raster-dem',
49
+ ...tilesOrUrl(terrain.tileUrl),
50
+ tileSize: terrain.tileSize ?? 256,
51
+ maxzoom: terrain.maxZoom ?? 15,
52
+ encoding: terrain.encoding ?? 'terrarium',
53
+ ...(terrain.attribution ? { attribution: terrain.attribution } : {}),
54
+ };
55
+ }
56
+ /** Relief shading from the terrain, drawn under every engine layer. */
57
+ export function hillshadeLayer(theme) {
58
+ return {
59
+ id: HILLSHADE_LAYER,
60
+ type: 'hillshade',
61
+ source: HILLSHADE_SOURCE,
62
+ paint: { 'hillshade-exaggeration': 0.35, 'hillshade-shadow-color': theme.hillshadeShadow, 'hillshade-highlight-color': theme.hillshadeHighlight },
63
+ };
64
+ }
65
+ export const CONTOUR_SOURCE = `${'me'}:contours`;
66
+ export const CONTOUR_LINES = `${'me'}:contour-lines`;
67
+ export const CONTOUR_LABELS = `${'me'}:contour-labels`;
68
+ /** Contour lines (doc 28 G11) from a vector source of `contours` features with `ele` and `level` (1 = index line). */
69
+ export function contourLayers(theme, color, hasFont) {
70
+ const c = color ?? theme.contourColor;
71
+ return [
72
+ { id: CONTOUR_LINES, type: 'line', source: CONTOUR_SOURCE, 'source-layer': 'contours', paint: { 'line-color': c, 'line-opacity': 0.55, 'line-width': ['match', ['get', 'level'], 1, 1.1, 0.5] } },
73
+ ...(hasFont
74
+ ? [{ id: CONTOUR_LABELS, type: 'symbol', source: CONTOUR_SOURCE, 'source-layer': 'contours', filter: ['>', ['get', 'level'], 0], layout: { 'symbol-placement': 'line', 'text-field': ['concat', ['number-format', ['get', 'ele'], {}], ' m'], 'text-font': [LABEL_FONT], 'text-size': 10 }, paint: { 'text-color': c, 'text-halo-color': 'rgba(255,255,255,0.85)', 'text-halo-width': 1.4 } }]
75
+ : []),
76
+ ];
77
+ }
78
+ /** Images a layer needs registered before its symbol layers render. */
79
+ export function imagesFor(layer) {
80
+ const icon = layer.visualization.type === 'marker' ? layer.visualization.icon : undefined;
81
+ if (!icon)
82
+ return [];
83
+ const base = icon.shape ? [{ id: shapeImageId(icon.shape), shape: icon.shape }] : icon.imageUrl ? [{ id: urlImageId(icon.imageUrl), url: icon.imageUrl }] : [];
84
+ // Per-rule images (an icon per status, threat, …).
85
+ const perRule = (layer.visualization.style ?? []).flatMap((r) => (r.style.imageUrl ? [{ id: urlImageId(r.style.imageUrl), url: r.style.imageUrl }] : []));
86
+ return [...base, ...perRule.filter((img, i) => perRule.findIndex((x) => x.id === img.id) === i && !base.some((b) => b.id === img.id))];
87
+ }
88
+ /** Prefix for every source and style layer the engine owns, so basemap layers are never touched. */
89
+ export const PREFIX = 'me';
90
+ export const sourceId = (layerId) => `${PREFIX}:${layerId}`;
91
+ export const OBJECT_ID_PROP = '__oid';
92
+ export const LAYER_ID_PROP = '__lid';
93
+ /** Marks the one label point an area gets in its own source (so a large area is labelled once, not once per tile). */
94
+ export const LABEL_POINT_PROP = '__lp';
95
+ const notLabelPoint = ['!=', ['get', LABEL_POINT_PROP], true];
96
+ /** Area layers with a label carry a label point per object next to the shape. */
97
+ export function needsLabelPoints(layer) {
98
+ return layer.visualization.type === 'polygon' && layer.visualization.label !== undefined;
99
+ }
100
+ /** Data-driven dashes (StyleTokens.dash), first matching rule wins; null when no rule asks for dashes. */
101
+ export function dashExpression(rules) {
102
+ if (!(rules ?? []).some((r) => r.style.dash))
103
+ return null;
104
+ const solid = ['literal', [1, 0]];
105
+ const branches = [];
106
+ for (const rule of rules ?? []) {
107
+ if (rule.style.dash === undefined)
108
+ continue;
109
+ const value = ['literal', rule.style.dash];
110
+ if (rule.property === undefined)
111
+ return branches.length ? ['case', ...branches, value] : value;
112
+ const condition = ruleCondition(rule);
113
+ if (condition !== null)
114
+ branches.push(condition, value);
115
+ }
116
+ return ['case', ...branches, solid];
117
+ }
118
+ const BACKGROUND_ID = `${PREFIX}:background`;
119
+ /** MapLibre style for the configured basemap. 'none' needs no network at all. */
120
+ export function basemapStyle(basemap, theme) {
121
+ const background = { id: BACKGROUND_ID, type: 'background', paint: { 'background-color': basemap.background ?? theme.background } };
122
+ // Labels are rasterized in the browser from the app-supplied font file: no glyph server (MapLibre v6 font-faces).
123
+ const fonts = basemap.font ? { 'font-faces': { [LABEL_FONT]: basemap.font.url } } : {};
124
+ if (basemap.kind === 'style' && basemap.styleUrl)
125
+ return basemap.styleUrl;
126
+ if (basemap.kind === 'tiles' && basemap.tileUrl) {
127
+ return {
128
+ version: 8,
129
+ ...fonts,
130
+ sources: {
131
+ [`${PREFIX}:basemap`]: { type: 'raster', ...tilesOrUrl(basemap.tileUrl), tileSize: 256, ...(basemap.maxZoom !== undefined ? { maxzoom: basemap.maxZoom } : {}), ...(basemap.attribution ? { attribution: basemap.attribution } : {}) },
132
+ },
133
+ layers: [background, { id: `${PREFIX}:basemap`, type: 'raster', source: `${PREFIX}:basemap` }],
134
+ };
135
+ }
136
+ return { version: 8, ...fonts, sources: {}, layers: [background] };
137
+ }
138
+ /**
139
+ * A fetched street-map style with the engine's label font added (MapLibre v6 font-faces) and its relative URLs
140
+ * made absolute, so it can be passed as an object.
141
+ */
142
+ export function withLabelFont(style, styleUrl, fontUrl) {
143
+ const base = styleUrl.slice(0, styleUrl.lastIndexOf('/') + 1);
144
+ // Not new URL(): it would percent-encode the {fontstack}/{range} placeholders.
145
+ const abs = (u) => (typeof u === 'string' && !/^[a-z][a-z0-9+.-]*:/i.test(u) ? (u.startsWith('/') ? new URL(styleUrl).origin + u : base + u) : u);
146
+ const sources = Object.fromEntries(Object.entries((style.sources ?? {})).map(([k, src]) => [
147
+ k,
148
+ { ...src, ...(src.url ? { url: abs(src.url) } : {}), ...(Array.isArray(src.tiles) ? { tiles: src.tiles.map(abs) } : {}), ...(typeof src.data === 'string' ? { data: abs(src.data) } : {}) },
149
+ ]));
150
+ const sprite = Array.isArray(style.sprite) ? style.sprite.map((s) => ({ ...s, url: abs(s.url) })) : abs(style.sprite);
151
+ return {
152
+ ...style,
153
+ ...(style.glyphs ? { glyphs: abs(style.glyphs) } : {}),
154
+ ...(style.sprite ? { sprite } : {}),
155
+ sources,
156
+ 'font-faces': { ...(style['font-faces'] ?? {}), [LABEL_FONT]: fontUrl },
157
+ };
158
+ }
159
+ const isPrimitive = (v) => ['string', 'number', 'boolean'].includes(typeof v);
160
+ /** Very large finite sentinels: a missing or non-numeric value never satisfies a numeric comparison. */
161
+ const LOW = -1e300;
162
+ const HIGH = 1e300;
163
+ /** The MapLibre condition for one rule, or null when the rule cannot be expressed (it is then skipped). */
164
+ export function ruleCondition(rule) {
165
+ if (rule.property === undefined)
166
+ return true;
167
+ const get = ['get', rule.property];
168
+ // A missing (or hidden, doc 28 G13) property must never match a comparison: MapLibre's to-number turns null into
169
+ // 0, so the fallback is put in first. Non-numbers fail the conversion and get the fallback too.
170
+ const num = (fallback) => ['to-number', ['coalesce', get, fallback], fallback];
171
+ const op = rule.op ?? 'eq';
172
+ const v = rule.value !== undefined ? rule.value : rule.equals;
173
+ switch (op) {
174
+ case 'eq':
175
+ return isPrimitive(v) ? ['==', get, v] : null;
176
+ case 'neq':
177
+ return isPrimitive(v) ? ['!=', get, v] : null;
178
+ case 'gt':
179
+ return typeof v === 'number' ? ['>', num(LOW), v] : null;
180
+ case 'gte':
181
+ return typeof v === 'number' ? ['>=', num(LOW), v] : null;
182
+ case 'lt':
183
+ return typeof v === 'number' ? ['<', num(HIGH), v] : null;
184
+ case 'lte':
185
+ return typeof v === 'number' ? ['<=', num(HIGH), v] : null;
186
+ case 'between':
187
+ return Array.isArray(v) && typeof v[0] === 'number' && typeof v[1] === 'number' ? ['all', ['>=', num(LOW), v[0]], ['<=', num(HIGH), v[1]]] : null;
188
+ case 'in':
189
+ return Array.isArray(v) && v.every(isPrimitive) ? ['in', ['to-string', get], ['literal', v.map(String)]] : null;
190
+ }
191
+ }
192
+ /**
193
+ * A data-driven expression for one style token: first matching rule wins (as the StyleRule contract says).
194
+ * Rules without `property` match everything and end the chain.
195
+ */
196
+ const SCALABLE = new Set(['opacity', 'size', 'strokeWidth', 'color', 'strokeColor']);
197
+ /** A token's expression: a continuous scale's per-object value (`$s:<token>`) first, then the rules. */
198
+ export function tokenExpression(rules, key, fallback) {
199
+ const base = ruleExpression(rules, key, fallback);
200
+ return SCALABLE.has(key) ? ['coalesce', ['get', scaled(key)], base] : base;
201
+ }
202
+ function ruleExpression(rules, key, fallback) {
203
+ const branches = [];
204
+ for (const rule of rules ?? []) {
205
+ const value = rule.style[key];
206
+ if (value === undefined)
207
+ continue;
208
+ if (rule.property === undefined) {
209
+ return branches.length ? ['case', ...branches, value] : value;
210
+ }
211
+ const condition = ruleCondition(rule);
212
+ if (condition === null)
213
+ continue;
214
+ branches.push(condition, value);
215
+ }
216
+ return branches.length ? ['case', ...branches, fallback] : fallback;
217
+ }
218
+ /** Point markers stand up and keep their size when the camera tilts (3D views), instead of lying flat on the ground. */
219
+ const UPRIGHT = { 'circle-pitch-alignment': 'viewport', 'circle-pitch-scale': 'viewport' };
220
+ const state = (name) => ['boolean', ['feature-state', name], false];
221
+ /** Wraps a base expression: selected wins, then hovered, then the data-driven value. */
222
+ const interactive = (selected, hovered, base) => ['case', state('selected'), selected, state('hovered'), hovered, base];
223
+ const withDash = (rules) => {
224
+ const dash = dashExpression(rules);
225
+ return dash === null ? {} : { 'line-dasharray': dash };
226
+ };
227
+ /** The text layer for a labelled engine layer, or none when there is no label or no label font. */
228
+ function labelLayer(layer, theme, hasFont) {
229
+ const label = layer.visualization.label;
230
+ if (!label || !hasFont)
231
+ return [];
232
+ const line = layer.visualization.type === 'line';
233
+ const area = needsLabelPoints(layer);
234
+ return [
235
+ {
236
+ id: `${PREFIX}:${layer.id}:label`,
237
+ type: 'symbol',
238
+ source: sourceId(layer.id),
239
+ ...(area ? { filter: ['==', ['get', LABEL_POINT_PROP], true] } : {}),
240
+ ...(label.minZoom !== undefined ? { minzoom: label.minZoom } : {}),
241
+ layout: {
242
+ visibility: layer.visible ? 'visible' : 'none',
243
+ 'text-field': ['to-string', ['get', label.property]],
244
+ 'text-font': [LABEL_FONT],
245
+ 'text-size': label.size ?? 12,
246
+ ...(line
247
+ ? { 'symbol-placement': 'line' }
248
+ : area
249
+ ? label.placement === 'top'
250
+ ? // Hangs below its point, just inside the top edge.
251
+ { 'text-anchor': 'top', 'text-offset': [0, 0.2] }
252
+ : // An area's one label moves to free space around its point rather than vanishing behind markers.
253
+ { 'text-variable-anchor': ['center', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'], 'text-radial-offset': 0.8, 'text-justify': 'auto' }
254
+ : { 'text-anchor': 'top', 'text-offset': [0, 0.9] }),
255
+ 'text-optional': true,
256
+ },
257
+ paint: { 'text-color': label.color ?? theme.labelColor, 'text-halo-color': label.haloColor ?? theme.labelHalo, 'text-halo-width': 1.4 },
258
+ },
259
+ ];
260
+ }
261
+ /** The MapLibre style layers for one engine layer, bottom to top. Labels need a label font (`hasFont`). */
262
+ export function styleLayersFor(layer, theme, hasFont = false) {
263
+ const shapes = shapeLayersFor(layer, theme, hasFont);
264
+ // No label for a layer this style cannot draw (a label without its shape would mislead).
265
+ return [...shapes, ...(layer.visualization.type === 'heatmap' || !shapes.length ? [] : labelLayer(layer, theme, hasFont))];
266
+ }
267
+ function shapeLayersFor(layer, theme, hasFont) {
268
+ // Heights and models are drawn by the deck.gl host (deck-host.ts), not by MapLibre style layers.
269
+ if (hostedByDeck(layer))
270
+ return [];
271
+ const src = sourceId(layer.id);
272
+ const rules = layer.visualization.style;
273
+ const visibility = layer.visible ? 'visible' : 'none';
274
+ const id = (part) => `${PREFIX}:${layer.id}:${part}`;
275
+ switch (layer.visualization.type) {
276
+ case 'marker': {
277
+ const size = tokenExpression(rules, 'size', 6);
278
+ const vz = layer.visualization;
279
+ // Clustered sources mix bubbles and points: point layers draw only unclustered features.
280
+ const unclustered = vz.cluster ? { filter: ['!', ['has', 'point_count']] } : {};
281
+ const clusters = vz.cluster
282
+ ? [
283
+ {
284
+ id: id('clusters'),
285
+ type: 'circle',
286
+ source: src,
287
+ filter: ['has', 'point_count'],
288
+ layout: { visibility },
289
+ paint: {
290
+ ...UPRIGHT,
291
+ 'circle-color': theme.clusterColor,
292
+ 'circle-opacity': 0.85,
293
+ 'circle-radius': ['step', ['get', 'point_count'], 13, 10, 17, 50, 22, 200, 28],
294
+ 'circle-stroke-color': theme.clusterRing,
295
+ 'circle-stroke-width': 2,
296
+ },
297
+ },
298
+ ...(hasFont
299
+ ? [
300
+ {
301
+ id: id('cluster-count'),
302
+ type: 'symbol',
303
+ source: src,
304
+ filter: ['has', 'point_count'],
305
+ layout: { visibility, 'text-field': ['get', 'point_count_abbreviated'], 'text-font': [LABEL_FONT], 'text-size': 12, 'text-allow-overlap': true },
306
+ paint: { 'text-color': theme.clusterText },
307
+ },
308
+ ]
309
+ : []),
310
+ ]
311
+ : [];
312
+ if (vz.icon) {
313
+ const icon = vz.icon;
314
+ const px = icon.size ?? 18;
315
+ const fallback = icon.shape ? shapeImageId(icon.shape) : icon.imageUrl ? urlImageId(icon.imageUrl) : '';
316
+ // First matching rule's image wins, like every other style token.
317
+ const imageRules = (rules ?? []).map((r) => (r.style.imageUrl ? { ...r, style: { ...r.style, imageUrl: urlImageId(r.style.imageUrl) } } : r));
318
+ const image = tokenExpression(imageRules, 'imageUrl', fallback);
319
+ return [
320
+ ...clusters,
321
+ {
322
+ // Selection / hover halo under the icon (icons themselves cannot carry a stroke).
323
+ id: id('halo'),
324
+ type: 'circle',
325
+ source: src,
326
+ ...unclustered,
327
+ layout: { visibility },
328
+ paint: {
329
+ ...UPRIGHT,
330
+ 'circle-radius': px / 2 + 6,
331
+ 'circle-color': interactive(theme.selectionColor, theme.hoverColor, theme.selectionColor),
332
+ 'circle-opacity': interactive(0.55, 0.3, 0),
333
+ },
334
+ },
335
+ {
336
+ id: id('icon'),
337
+ type: 'symbol',
338
+ source: src,
339
+ ...unclustered,
340
+ layout: {
341
+ visibility,
342
+ 'icon-image': image,
343
+ 'icon-size': px / ICON_PIXELS,
344
+ 'icon-allow-overlap': true,
345
+ 'icon-ignore-placement': true,
346
+ 'icon-rotation-alignment': 'map',
347
+ // Upright in a tilted 3D view (facing the camera), still turned by heading relative to north.
348
+ 'icon-pitch-alignment': 'viewport',
349
+ ...(icon.rotateBy ? { 'icon-rotate': ['to-number', ['get', icon.rotateBy], 0] } : {}),
350
+ },
351
+ paint: {
352
+ ...(icon.shape ? { 'icon-color': tokenExpression(rules, 'color', theme.markerColor), 'icon-halo-color': tokenExpression(rules, 'strokeColor', '#0B100E'), 'icon-halo-width': 1 } : {}),
353
+ 'icon-opacity': tokenExpression(rules, 'opacity', 1),
354
+ },
355
+ },
356
+ ];
357
+ }
358
+ return [
359
+ ...clusters,
360
+ {
361
+ ...unclustered,
362
+ id: id('circle'),
363
+ type: 'circle',
364
+ source: src,
365
+ layout: { visibility },
366
+ paint: {
367
+ ...UPRIGHT,
368
+ 'circle-color': tokenExpression(rules, 'color', theme.markerColor),
369
+ 'circle-opacity': tokenExpression(rules, 'opacity', 1),
370
+ 'circle-radius': interactive(['+', size, 3], ['+', size, 2], size),
371
+ 'circle-stroke-color': interactive(theme.selectionColor, theme.hoverColor, tokenExpression(rules, 'strokeColor', '#0B100E')),
372
+ 'circle-stroke-width': interactive(3, 2, tokenExpression(rules, 'strokeWidth', 1.5)),
373
+ },
374
+ },
375
+ ];
376
+ }
377
+ case 'track': {
378
+ const width = tokenExpression(rules, 'size', 2.5);
379
+ return [
380
+ {
381
+ id: id('track'),
382
+ type: 'line',
383
+ source: src,
384
+ layout: { visibility, 'line-join': 'round', 'line-cap': 'round' },
385
+ paint: {
386
+ 'line-color': interactive(theme.selectionColor, theme.hoverColor, tokenExpression(rules, 'color', theme.trackColor)),
387
+ 'line-opacity': tokenExpression(rules, 'opacity', 0.7),
388
+ 'line-width': interactive(['+', width, 1.5], ['+', width, 1], width),
389
+ },
390
+ },
391
+ ];
392
+ }
393
+ case 'line': {
394
+ const width = tokenExpression(rules, 'size', 2.5);
395
+ return [
396
+ {
397
+ id: id('line'),
398
+ type: 'line',
399
+ source: src,
400
+ layout: { visibility, 'line-join': 'round', 'line-cap': 'round' },
401
+ paint: {
402
+ 'line-color': interactive(theme.selectionColor, theme.hoverColor, tokenExpression(rules, 'color', theme.lineColor)),
403
+ 'line-opacity': tokenExpression(rules, 'opacity', 0.9),
404
+ 'line-width': interactive(['+', width, 2.5], ['+', width, 1.5], width),
405
+ ...withDash(rules),
406
+ },
407
+ },
408
+ ];
409
+ }
410
+ case 'heatmap': {
411
+ const h = layer.visualization.heatmap ?? {};
412
+ const weight = h.weightProperty ? ['*', ['to-number', ['get', h.weightProperty], 0], h.weightScale ?? 1] : h.weightScale ?? 1;
413
+ return [
414
+ {
415
+ id: id('heatmap'),
416
+ type: 'heatmap',
417
+ source: src,
418
+ layout: { visibility },
419
+ paint: {
420
+ 'heatmap-weight': weight,
421
+ 'heatmap-radius': h.radius ?? 24,
422
+ 'heatmap-intensity': h.intensity ?? 1,
423
+ 'heatmap-opacity': 0.85,
424
+ 'heatmap-color': ['interpolate', ['linear'], ['heatmap-density'], 0, 'rgba(0,0,0,0)', 0.2, theme.heatLow, 0.5, theme.heatMid, 0.8, theme.heatHigh, 1, theme.heatMax],
425
+ },
426
+ },
427
+ ];
428
+ }
429
+ // Images are drawn by the renderer itself (one image source per object); link layers arrive as lines.
430
+ // The pictures themselves are image sources the renderer adds per object (under these layers). Here: a
431
+ // transparent fill so a picture can be clicked, and its outline when hovered or selected.
432
+ case 'image':
433
+ return [
434
+ { id: id('fill'), type: 'fill', source: src, layout: { visibility }, paint: { 'fill-color': '#000000', 'fill-opacity': 0 } },
435
+ { id: id('outline'), type: 'line', source: src, layout: { visibility }, paint: { 'line-color': interactive(theme.selectionColor, theme.hoverColor, '#000000'), 'line-width': interactive(3, 2, 0), 'line-opacity': interactive(1, 0.8, 0) } },
436
+ ];
437
+ // Link layers reach renderers as lines (the runtime's effective layer).
438
+ case 'link':
439
+ return [];
440
+ case 'extrusion': {
441
+ // MapLibre's own extrusions: they stand on terrain natively.
442
+ const x = layer.visualization.extrusion ?? {};
443
+ const height = x.heightProperty ? ['to-number', ['get', x.heightProperty], x.height ?? 0] : (x.height ?? 0);
444
+ const base = x.baseProperty ? ['to-number', ['get', x.baseProperty], x.base ?? 0] : (x.base ?? 0);
445
+ return [
446
+ {
447
+ id: id('extrusion'),
448
+ type: 'fill-extrusion',
449
+ source: src,
450
+ filter: notLabelPoint,
451
+ layout: { visibility },
452
+ paint: {
453
+ 'fill-extrusion-color': interactive(theme.selectionColor, theme.hoverColor, tokenExpression(rules, 'color', theme.fillColor)),
454
+ 'fill-extrusion-height': height,
455
+ 'fill-extrusion-base': base,
456
+ // Not data-driven in MapLibre: one opacity per layer (the catch-all rule's, else 0.85).
457
+ 'fill-extrusion-opacity': (rules ?? []).find((r) => r.property === undefined && r.style.opacity !== undefined)?.style.opacity ?? 0.85,
458
+ },
459
+ },
460
+ ];
461
+ }
462
+ case 'model':
463
+ case 'volume':
464
+ // Models are drawn by the deck.gl host; volumes come in phase 3E (the renderer reports the gap until then).
465
+ return [];
466
+ case 'polygon':
467
+ return [
468
+ {
469
+ id: id('fill'),
470
+ type: 'fill',
471
+ source: src,
472
+ filter: notLabelPoint,
473
+ layout: { visibility },
474
+ paint: {
475
+ 'fill-color': tokenExpression(rules, 'color', theme.fillColor),
476
+ 'fill-opacity': interactive(0.32, 0.26, tokenExpression(rules, 'opacity', 0.18)),
477
+ },
478
+ },
479
+ {
480
+ id: id('outline'),
481
+ type: 'line',
482
+ source: src,
483
+ filter: notLabelPoint,
484
+ layout: { visibility },
485
+ paint: {
486
+ 'line-color': interactive(theme.selectionColor, theme.hoverColor, tokenExpression(rules, 'strokeColor', tokenExpression(rules, 'color', theme.fillColor))),
487
+ 'line-width': interactive(3, 2, tokenExpression(rules, 'strokeWidth', 1.5)),
488
+ ...withDash(rules),
489
+ },
490
+ },
491
+ ];
492
+ }
493
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Decorations: shapes the engine derives from a layer's objects and draws with it, in engine-owned layers next to
3
+ * it (doc 28 G2, G9, G12): position error areas, unresolved alternatives, range rings, camera footprints. They are
4
+ * plain polygons, lines and points, so every renderer draws them with what it already has. They follow their
5
+ * objects (rebuilt with each change of the layer), and a click on one selects the object it belongs to.
6
+ */
7
+ import type { FootprintConfig, Geometry, MapLayer, Position, SpatialObject } from '../core/types.js';
8
+ /** Engine layer ids are `__deco~<layerId>~<part>`; the part says what it draws. */
9
+ export declare const DECO_PREFIX = "__deco~";
10
+ declare const PARTS: readonly ["footprint", "uncertainty", "alt-lines", "alt-points", "rings"];
11
+ type Part = (typeof PARTS)[number];
12
+ export declare const decoLayerId: (layerId: string, part: Part) => string;
13
+ /** The application layer and object a decoration belongs to (for picking), or null for any other layer. */
14
+ export declare function decorationOwner(layerId: string, objectId: string): {
15
+ layerId: string;
16
+ objectId: string;
17
+ } | null;
18
+ /** The engine layers a layer's decorations need (none when it has no decoration options). */
19
+ export declare function decorationLayers(layer: MapLayer): MapLayer[];
20
+ export declare const formatDistance: (m: number) => string;
21
+ /** The ground area a camera sees (FootprintConfig); null without a heading. */
22
+ export declare function footprintOf(f: FootprintConfig, at: Position, props: Record<string, unknown> | undefined): Geometry | null;
23
+ /** Decoration objects per engine layer, for a layer's (presented) objects. */
24
+ export declare function decorationObjects(layer: MapLayer, objects: SpatialObject[]): Record<string, SpatialObject[]>;
25
+ export {};