isard 1.1.0 → 1.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.
Files changed (254) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +44 -39
  3. package/dist/basemap/basemaps.d.ts +1 -1
  4. package/dist/basemap/basemaps.js +2 -2
  5. package/dist/basemap/basemaps.js.map +1 -1
  6. package/dist/basemap/grid.d.ts +5 -5
  7. package/dist/basemap/grid.js +59 -65
  8. package/dist/basemap/grid.js.map +1 -1
  9. package/dist/basemap/mvt.d.ts +1 -1
  10. package/dist/basemap/mvt.js +6 -6
  11. package/dist/basemap/mvt.js.map +1 -1
  12. package/dist/basemap/pmtiles.d.ts +10 -10
  13. package/dist/basemap/pmtiles.js +39 -31
  14. package/dist/basemap/pmtiles.js.map +1 -1
  15. package/dist/basemap/pmtileslayer.d.ts +33 -0
  16. package/dist/basemap/pmtileslayer.js +97 -0
  17. package/dist/basemap/pmtileslayer.js.map +1 -0
  18. package/dist/basemap/resample.d.ts +2 -2
  19. package/dist/basemap/resample.js +16 -14
  20. package/dist/basemap/resample.js.map +1 -1
  21. package/dist/basemap/terrain.d.ts +3 -3
  22. package/dist/basemap/terrain.js +6 -6
  23. package/dist/basemap/terrain.js.map +1 -1
  24. package/dist/basemap/trees.d.ts +26 -0
  25. package/dist/basemap/trees.js +141 -0
  26. package/dist/basemap/trees.js.map +1 -0
  27. package/dist/basemap/vectorstyle.d.ts +22 -10
  28. package/dist/basemap/vectorstyle.js +163 -110
  29. package/dist/basemap/vectorstyle.js.map +1 -1
  30. package/dist/basemap/vectortiles.d.ts +70 -47
  31. package/dist/basemap/vectortiles.js +480 -358
  32. package/dist/basemap/vectortiles.js.map +1 -1
  33. package/dist/basemap/wmts.d.ts +3 -3
  34. package/dist/basemap/wmts.js +16 -11
  35. package/dist/basemap/wmts.js.map +1 -1
  36. package/dist/basemap/workerpool.d.ts +17 -0
  37. package/dist/basemap/workerpool.js +194 -0
  38. package/dist/basemap/workerpool.js.map +1 -0
  39. package/dist/basemap.worker.js +2 -2
  40. package/dist/basemap.worker.js.map +21 -13
  41. package/dist/bind/layers.d.ts +13 -4
  42. package/dist/bind/layers.js +64 -40
  43. package/dist/bind/layers.js.map +1 -1
  44. package/dist/data/crs.d.ts +10 -10
  45. package/dist/data/crs.js +38 -34
  46. package/dist/data/crs.js.map +1 -1
  47. package/dist/data/gpx.d.ts +1 -1
  48. package/dist/data/gpx.js +20 -20
  49. package/dist/data/gpx.js.map +1 -1
  50. package/dist/data/kml.d.ts +1 -1
  51. package/dist/data/kml.js +42 -39
  52. package/dist/data/kml.js.map +1 -1
  53. package/dist/data/topojson.d.ts +1 -1
  54. package/dist/data/topojson.js +15 -10
  55. package/dist/data/topojson.js.map +1 -1
  56. package/dist/data/xml.d.ts +5 -5
  57. package/dist/data/xml.js +19 -18
  58. package/dist/data/xml.js.map +1 -1
  59. package/dist/decoder.worker.js +4 -4
  60. package/dist/decoder.worker.js.map +6 -6
  61. package/dist/engine/decoders.d.ts +2 -2
  62. package/dist/engine/decoders.js +28 -25
  63. package/dist/engine/decoders.js.map +1 -1
  64. package/dist/engine/loader.js +13 -14
  65. package/dist/engine/loader.js.map +1 -1
  66. package/dist/engine/requests.d.ts +1 -1
  67. package/dist/engine/requests.js +19 -16
  68. package/dist/engine/requests.js.map +1 -1
  69. package/dist/engine-loader.d.ts +13 -9
  70. package/dist/engine-loader.js +38 -28
  71. package/dist/engine-loader.js.map +1 -1
  72. package/dist/geoplateforme.d.ts +26 -10
  73. package/dist/geoplateforme.js +45 -20
  74. package/dist/geoplateforme.js.map +1 -1
  75. package/dist/index.d.ts +12 -4
  76. package/dist/index.js +9 -2
  77. package/dist/index.js.map +1 -1
  78. package/dist/isard_bg.wasm +0 -0
  79. package/dist/isard_core_bg.wasm +0 -0
  80. package/dist/isard_points_bg.wasm +0 -0
  81. package/dist/isard_tess_bg.wasm +0 -0
  82. package/dist/layers/extrusions.d.ts +1 -1
  83. package/dist/layers/extrusions.js +10 -5
  84. package/dist/layers/extrusions.js.map +1 -1
  85. package/dist/layers/features.d.ts +10 -10
  86. package/dist/layers/features.js +41 -42
  87. package/dist/layers/features.js.map +1 -1
  88. package/dist/layers/glyphs.d.ts +3 -3
  89. package/dist/layers/glyphs.js +34 -40
  90. package/dist/layers/glyphs.js.map +1 -1
  91. package/dist/layers/icons.d.ts +8 -8
  92. package/dist/layers/icons.js +76 -68
  93. package/dist/layers/icons.js.map +1 -1
  94. package/dist/layers/layers.d.ts +1 -1
  95. package/dist/layers/layers.js +42 -29
  96. package/dist/layers/layers.js.map +1 -1
  97. package/dist/layers/panoramas.d.ts +2 -2
  98. package/dist/layers/panoramas.js +52 -41
  99. package/dist/layers/panoramas.js.map +1 -1
  100. package/dist/layers/pointclouds.d.ts +3 -3
  101. package/dist/layers/pointclouds.js +25 -33
  102. package/dist/layers/pointclouds.js.map +1 -1
  103. package/dist/layers/symbols.d.ts +1 -1
  104. package/dist/layers/symbols.js +68 -71
  105. package/dist/layers/symbols.js.map +1 -1
  106. package/dist/layers/vectors.d.ts +60 -0
  107. package/dist/layers/vectors.js +130 -0
  108. package/dist/layers/vectors.js.map +1 -0
  109. package/dist/map/basemap-layers.d.ts +26 -0
  110. package/dist/map/basemap-layers.js +61 -0
  111. package/dist/map/basemap-layers.js.map +1 -0
  112. package/dist/map/canvas.d.ts +17 -0
  113. package/dist/map/canvas.js +56 -0
  114. package/dist/map/canvas.js.map +1 -0
  115. package/dist/map/checks.d.ts +11 -0
  116. package/dist/map/checks.js +39 -0
  117. package/dist/map/checks.js.map +1 -0
  118. package/dist/map/credits.d.ts +17 -0
  119. package/dist/map/credits.js +34 -0
  120. package/dist/map/credits.js.map +1 -0
  121. package/dist/map/engine-settings.d.ts +15 -0
  122. package/dist/map/engine-settings.js +45 -0
  123. package/dist/map/engine-settings.js.map +1 -0
  124. package/dist/map/engines.d.ts +41 -0
  125. package/dist/map/engines.js +77 -0
  126. package/dist/map/engines.js.map +1 -0
  127. package/dist/map/events.d.ts +14 -0
  128. package/dist/map/events.js +46 -0
  129. package/dist/map/events.js.map +1 -0
  130. package/dist/map/frame-loop.d.ts +64 -0
  131. package/dist/map/frame-loop.js +145 -0
  132. package/dist/map/frame-loop.js.map +1 -0
  133. package/dist/map/hover-pick.d.ts +30 -0
  134. package/dist/map/hover-pick.js +56 -0
  135. package/dist/map/hover-pick.js.map +1 -0
  136. package/dist/map/layer-spec.d.ts +42 -0
  137. package/dist/map/layer-spec.js +34 -0
  138. package/dist/map/layer-spec.js.map +1 -0
  139. package/dist/map/layer-stack.d.ts +36 -0
  140. package/dist/map/layer-stack.js +101 -0
  141. package/dist/map/layer-stack.js.map +1 -0
  142. package/dist/map/overlay-host.d.ts +36 -0
  143. package/dist/map/overlay-host.js +101 -0
  144. package/dist/map/overlay-host.js.map +1 -0
  145. package/dist/map/panorama-host.d.ts +5 -0
  146. package/dist/map/panorama-host.js +32 -0
  147. package/dist/map/panorama-host.js.map +1 -0
  148. package/dist/map/raster-source.d.ts +11 -0
  149. package/dist/map/raster-source.js +26 -0
  150. package/dist/map/raster-source.js.map +1 -0
  151. package/dist/map/symbol-atlases.d.ts +26 -0
  152. package/dist/map/symbol-atlases.js +59 -0
  153. package/dist/map/symbol-atlases.js.map +1 -0
  154. package/dist/map/viewshed.d.ts +5 -0
  155. package/dist/map/viewshed.js +28 -0
  156. package/dist/map/viewshed.js.map +1 -0
  157. package/dist/map/workers.d.ts +46 -0
  158. package/dist/map/workers.js +73 -0
  159. package/dist/map/workers.js.map +1 -0
  160. package/dist/map.d.ts +54 -126
  161. package/dist/map.js +227 -698
  162. package/dist/map.js.map +1 -1
  163. package/dist/react/Control.d.ts +11 -0
  164. package/dist/react/Control.js +18 -0
  165. package/dist/react/Control.js.map +1 -0
  166. package/dist/react/CustomLayer.d.ts +11 -0
  167. package/dist/react/CustomLayer.js +30 -0
  168. package/dist/react/CustomLayer.js.map +1 -0
  169. package/dist/react/IsardMap.d.ts +43 -0
  170. package/dist/react/IsardMap.js +147 -0
  171. package/dist/react/IsardMap.js.map +1 -0
  172. package/dist/react/Layer.d.ts +42 -0
  173. package/dist/react/Layer.js +74 -0
  174. package/dist/react/Layer.js.map +1 -0
  175. package/dist/react/Marker.d.ts +24 -0
  176. package/dist/react/Marker.js +53 -0
  177. package/dist/react/Marker.js.map +1 -0
  178. package/dist/react/Panel.d.ts +11 -0
  179. package/dist/react/Panel.js +37 -0
  180. package/dist/react/Panel.js.map +1 -0
  181. package/dist/react/Popup.d.ts +18 -0
  182. package/dist/react/Popup.js +60 -0
  183. package/dist/react/Popup.js.map +1 -0
  184. package/dist/react/Swipe.d.ts +16 -0
  185. package/dist/react/Swipe.js +52 -0
  186. package/dist/react/Swipe.js.map +1 -0
  187. package/dist/react/VectorGeometry.d.ts +35 -0
  188. package/dist/react/VectorGeometry.js +82 -0
  189. package/dist/react/VectorGeometry.js.map +1 -0
  190. package/dist/react/Viewshed.d.ts +5 -0
  191. package/dist/react/Viewshed.js +28 -0
  192. package/dist/react/Viewshed.js.map +1 -0
  193. package/dist/react/context.d.ts +33 -0
  194. package/dist/react/context.js +20 -0
  195. package/dist/react/context.js.map +1 -0
  196. package/dist/react/index.d.ts +16 -108
  197. package/dist/react/index.js +11 -397
  198. package/dist/react/index.js.map +1 -1
  199. package/dist/settings.d.ts +5 -9
  200. package/dist/settings.js +17 -15
  201. package/dist/settings.js.map +1 -1
  202. package/dist/types.d.ts +27 -4
  203. package/dist/types.js.map +1 -1
  204. package/dist/ui/controls.js +6 -4
  205. package/dist/ui/controls.js.map +1 -1
  206. package/dist/ui/interaction.d.ts +2 -2
  207. package/dist/ui/interaction.js +8 -7
  208. package/dist/ui/interaction.js.map +1 -1
  209. package/dist/ui/minimap.d.ts +25 -0
  210. package/dist/ui/minimap.js +90 -0
  211. package/dist/ui/minimap.js.map +1 -0
  212. package/dist/ui/overlay.js +18 -20
  213. package/dist/ui/overlay.js.map +1 -1
  214. package/dist/ui/scale.d.ts +19 -0
  215. package/dist/ui/scale.js +44 -0
  216. package/dist/ui/scale.js.map +1 -0
  217. package/dist/ui/search.d.ts +25 -0
  218. package/dist/ui/search.js +119 -0
  219. package/dist/ui/search.js.map +1 -0
  220. package/dist/ui/style.d.ts +1 -1
  221. package/dist/ui/style.js +19 -2
  222. package/dist/ui/style.js.map +1 -1
  223. package/dist/util/animation.d.ts +3 -3
  224. package/dist/util/animation.js +6 -6
  225. package/dist/util/animation.js.map +1 -1
  226. package/dist/util/color.d.ts +1 -1
  227. package/dist/util/color.js +14 -16
  228. package/dist/util/color.js.map +1 -1
  229. package/dist/util/geo.d.ts +6 -6
  230. package/dist/util/geo.js +12 -12
  231. package/dist/util/geo.js.map +1 -1
  232. package/dist/util/guards.d.ts +7 -0
  233. package/dist/util/guards.js +8 -0
  234. package/dist/util/guards.js.map +1 -0
  235. package/dist/util/lru.js.map +1 -1
  236. package/dist/util/sun.d.ts +11 -0
  237. package/dist/util/sun.js +26 -0
  238. package/dist/util/sun.js.map +1 -0
  239. package/dist/wasm/isard.d.ts +250 -7
  240. package/dist/wasm/isard.js +375 -8
  241. package/dist/wasm/isard_core.d.ts +226 -7
  242. package/dist/wasm/isard_core.js +351 -8
  243. package/dist/wasm/isard_tess.d.ts +109 -0
  244. package/dist/wasm/isard_tess.js +398 -0
  245. package/dist/workers/basemap/geometry.d.ts +72 -0
  246. package/dist/workers/basemap/labels.d.ts +8 -0
  247. package/dist/workers/basemap/raster.d.ts +10 -0
  248. package/dist/workers/basemap/tiles.d.ts +65 -0
  249. package/dist/workers/basemap/trees.d.ts +19 -0
  250. package/dist/workers/basemap.worker.d.ts +126 -22
  251. package/dist/workers/basemap.worker.js +76 -544
  252. package/dist/workers/basemap.worker.js.map +1 -1
  253. package/package.json +5 -4
  254. package/vite.js +10 -2
package/dist/map.js CHANGED
@@ -2,154 +2,97 @@
2
2
  * The map: canvas, camera, layers, events, overlays, credits and the frame loop. Frames are drawn on demand only: an
3
3
  * idle map costs nothing. Hover events are coalesced to one per frame, so the values computed for a mouse position are
4
4
  * drawn in the same frame. The engine's loading is in engine-loader.ts, its settings from the options in settings.ts,
5
- * the camera animations in animation.ts.
5
+ * the camera animations in animation.ts; the parts of the map with a state of their own (the drawing order of the
6
+ * layers, the overlays and controls, the frame loop, the hover picking, the workers…) in map/.
6
7
  */
7
- import { BASEMAPS, HILLSHADE, RENAMED_BASEMAPS, SEA } from "./basemap/basemaps.js";
8
- import { equirectangularGrid, gridLoader, gridZooms, resolveGrid } from "./basemap/grid.js";
9
- import { encodedTerrainLoader } from "./basemap/terrain.js";
10
- import { BasemapWorkers, defaultWorkerCount, defaultWorkerUrl, pmtilesLayer, pmtilesRasterLoader, VECTOR_BASEMAPS, VectorTiles, } from "./basemap/vectortiles.js";
11
- import { headerRules, onHeaderRules } from "./engine/requests.js";
12
- import { destroyedEngine, engineUrl, loadEngine, loadFullEngineLater, preloadFullEngine, startTileLoader, } from "./engine-loader.js";
8
+ import { SEA } from "./basemap/basemaps.js";
9
+ import { pmtilesLayer } from "./basemap/pmtileslayer.js";
10
+ import { VectorTiles } from "./basemap/vectortiles.js";
11
+ import { defaultWorkerUrl } from "./basemap/workerpool.js";
12
+ import { onHeaderRules } from "./engine/requests.js";
13
+ import { destroyedEngine, engineUrl, loadEngine, tessUrl } from "./engine-loader.js";
13
14
  import { IGN_PRESETS } from "./geoplateforme.js";
14
15
  import { ExtrusionLayer, extrusionStyle } from "./layers/extrusions.js";
15
16
  import { CircleLayer, circleStyle, FillLayer, fillStyle, LineLayer, lineStyle, } from "./layers/features.js";
16
- import { Glyphs } from "./layers/glyphs.js";
17
- import { Icons, readPixels } from "./layers/icons.js";
18
17
  import { CustomLayer, GroundLayer, ModelLayer, NetworkLayer, RasterLayer, Tiles3dLayer, } from "./layers/layers.js";
19
18
  import { loadPanoramas } from "./layers/panoramas.js";
20
19
  import { PointCloudLayer } from "./layers/pointclouds.js";
21
20
  import { SymbolLayer, symbolStyle } from "./layers/symbols.js";
22
- import { atmosphereSettings, bodySettings, haloSettings, rgb, terrainSettings, } from "./settings.js";
21
+ import { VectorLayer } from "./layers/vectors.js";
22
+ import { BasemapLayers } from "./map/basemap-layers.js";
23
+ import { createCanvas, labelCanvas, watchContext, watchPixelRatio } from "./map/canvas.js";
24
+ import { checkBasemap, report, validateNetwork } from "./map/checks.js";
25
+ import { Credits } from "./map/credits.js";
26
+ import { mapSettings, sendHeaders } from "./map/engine-settings.js";
27
+ import { MapEngines } from "./map/engines.js";
28
+ import { MapListeners } from "./map/events.js";
29
+ import { FrameLoop } from "./map/frame-loop.js";
30
+ import { HoverPick } from "./map/hover-pick.js";
31
+ import { addSpec } from "./map/layer-spec.js";
32
+ import { LayerStack } from "./map/layer-stack.js";
33
+ import { OverlayHost } from "./map/overlay-host.js";
34
+ import { panoramaHost } from "./map/panorama-host.js";
35
+ import { rasterSource } from "./map/raster-source.js";
36
+ import { SymbolAtlases } from "./map/symbol-atlases.js";
37
+ import { applyViewshed } from "./map/viewshed.js";
38
+ import { MapWorkers } from "./map/workers.js";
39
+ import { atmosphereSettings, rgb, terrainSettings } from "./settings.js";
23
40
  import { AttributionControl } from "./ui/controls.js";
24
41
  import { Interaction } from "./ui/interaction.js";
25
- import { injectStyle } from "./ui/style.js";
26
42
  import { easeTo, flyTo } from "./util/animation.js";
27
- /** Reports an error without throwing it here: the console shows it, as an uncaught one */
28
- function report(error) {
29
- if (typeof reportError === 'function')
30
- reportError(error);
31
- else
32
- queueMicrotask(() => {
33
- throw error;
34
- });
35
- }
36
- /** Checks a network's arrays before the engine reads them: node index pairs, indices of existing nodes */
37
- function validateNetwork(nodes, edges) {
38
- if (nodes.length % 2 !== 0)
39
- throw new Error(`isard: network nodes must be lon, lat pairs (length ${nodes.length})`);
40
- if (edges.length % 2 !== 0)
41
- throw new Error(`isard: network edges must be node index pairs (length ${edges.length})`);
42
- const count = nodes.length / 2;
43
- for (let i = 0; i < edges.length; i += 1) {
44
- const node = edges[i];
45
- if (node >= count)
46
- throw new Error(`isard: network edge ${i >> 1} refers to node ${node}, of ${count} nodes`);
47
- }
48
- }
49
- const CORNERS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
50
- /** Frames the hover picking waits for its answer at most (usually one) */
51
- const MAX_POLLS = 30;
52
- /** The name of a basemap, checked: an unknown one throws */
53
- function checkBasemap(basemap) {
54
- const renamed = RENAMED_BASEMAPS[basemap];
55
- if (renamed)
56
- throw new Error(`isard: the basemap '${basemap}' is now named '${renamed}'`);
57
- if (basemap !== 'none' && !(basemap in VECTOR_BASEMAPS) && !(basemap in BASEMAPS)) {
58
- throw new Error(`isard: unknown basemap '${basemap}'`);
59
- }
60
- return basemap;
61
- }
43
+ import { sunPosition } from "./util/sun.js";
44
+ /** A layer whose picked features come back with their properties (lines, circles, fills, extrusions, 3D Tiles…) */
45
+ const hasFeatures = (layer) => 'featureAt' in layer && typeof layer.featureAt === 'function';
62
46
  /** A map (see `GeoMap.create`); layers take it as their host */
63
47
  export class GeoMap {
64
48
  canvas;
65
- #glyphs;
66
- #icons;
67
- /** Layers new layers go under */
68
- onTop = new Set();
69
- /** The vector basemap, if any: its tiles' workers and its labels */
70
- vector;
71
- glyphsUrl;
72
- basemapWorkerUrl;
73
- /** Texture size of the vector basemap's tiles */
74
- basemapTileSize;
75
- container;
76
- /** Markers and popups, over the canvas */
77
- overlay;
78
- corners;
79
- controls = new Map();
80
- positioned = new Set();
81
- /** Canvas size in CSS pixels, kept on resize so placing overlays never reads the layout */
82
- size = [0, 0];
83
49
  #engine;
84
- /** Layers in drawing order (bottom first), to recreate them after a context loss */
85
- layers = [];
50
+ /** The engine class (core, or with 3D Tiles) and the engine with 3D Tiles loaded when needed */
51
+ engines;
52
+ /** The layers in drawing order and those kept on top */
53
+ stack = new LayerStack(() => this.#engine);
54
+ /** The basemap and the hillshade */
55
+ basemaps;
56
+ /** The glyphs and icons of the symbol layers */
57
+ atlases;
58
+ /** The workers of the vector tiles, the relief and the reprojected rasters */
59
+ workers;
60
+ container;
61
+ /** The canvas in the container, the markers and popups over it, the controls in its corners */
62
+ overlays;
86
63
  settings;
64
+ /** @internal Where the basemap workers load the tessellation module from (`isard_tess_bg.wasm`) */
65
+ tessUrl;
87
66
  /** The WebGL context is lost: nothing is drawn until it is restored */
88
67
  lost = false;
89
- listeners = {
90
- movestart: new Set(),
91
- move: new Set(),
92
- moveend: new Set(),
93
- resize: new Set(),
94
- hover: new Set(),
95
- hoverfeature: new Set(),
96
- click: new Set(),
97
- render: new Set(),
98
- idle: new Set(),
99
- leave: new Set(),
100
- };
68
+ gone = false;
69
+ listeners = new MapListeners();
70
+ /** Frames drawn on demand, with their events */
71
+ frames;
101
72
  resizeObserver;
102
73
  interaction;
103
- loader;
104
- /** The class of the engine: without 3D Tiles until a 3D Tiles layer is added, unless asked for from the start */
105
- engineClass;
106
- /** The module of the engine with 3D Tiles once it runs this map (the decoding workers run it too) */
107
- tiles3dModule;
108
- /** The engine with 3D Tiles is loading, for the 3D Tiles layers added on the core engine */
109
- upgrading;
110
- /** What loading the engine with 3D Tiles and its workers needs */
111
- tiles3dOptions;
112
- gone = false;
113
74
  /** The map is of the Earth: a basemap by default */
114
75
  earth;
115
76
  /** The body, as the option named it */
116
77
  body;
117
- basemap;
118
- hillshade;
119
- /** Called when the credits of the layers or relief change (the attribution controls) */
120
- attributionListeners = new Set();
121
- /** Credits of the raster layers that have some (basemaps, hillshade, the Géoplateforme's rasters…) */
122
- rasterCredits = new Map();
123
- animation;
124
- hoverPoint;
125
- /** Frame polling the engine for the answer of the hover picking, 0 if none */
126
- hoverPoll = 0;
127
- /** Polls of the hover picking under way: given up after `MAX_POLLS` (an answer that never comes) */
128
- hoverPolls = 0;
78
+ /** Credits of the raster layers that have some, and the attribution controls told when they change */
79
+ credits = new Credits();
80
+ /** The picking for 'hoverfeature', its answer emitted on a later frame */
81
+ hoverPick = new HoverPick(() => this.engine, () => this.lost, (result) => this.emit('hoverfeature', {
82
+ point: [result.x, result.y],
83
+ lonLat: result.lonLat,
84
+ hit: result.hit && this.featureHit(result.hit),
85
+ }));
129
86
  /** What 'hoverfeature' looks for: within `radius` CSS pixels, in `layers` only if given */
130
87
  hoverPicking = {};
131
- frameRequest = 0;
132
- lastCamera = '';
133
- /** The camera moved since the last 'moveend' */
134
- moving = false;
135
- /** The last frame found everything loaded ('idle' sent) */
136
- settled = false;
137
- inFrame = false;
138
- /** The camera of the frame being drawn: what `getCamera` gives the custom layers while the engine is busy */
139
- frameCamera;
140
88
  /** Vector tiles made by `vectorTiles()`, destroyed with the map */
141
89
  vectorSets = new Set();
142
- /** The workers drawing vector tiles, shared by every `VectorTiles` of the map; created on first use */
143
- basemapPool;
144
90
  /** Stops watching the device pixel ratio */
145
91
  unwatchRatio;
146
92
  /** Stops following the page's request headers (`setRequestHeaders`) */
147
93
  unwatchHeaders = () => undefined;
148
94
  /** Stops listening to the canvas's context loss and restoration */
149
95
  unwatchContext;
150
- /** The loaders of the relief by encoding, URL and floor: the same loader for the same tiles (the engine keeps its
151
- * tiles when only the exaggeration changes) */
152
- terrainLoaders = new Map();
153
96
  /** Loads the engine (once per page) and creates a map filling `container` */
154
97
  static async create(container, given = {}) {
155
98
  // A preset under the options given
@@ -167,85 +110,56 @@ export class GeoMap {
167
110
  return map;
168
111
  }
169
112
  constructor(container, options, engineClass, tiles3dModule) {
170
- this.engineClass = engineClass;
171
- this.tiles3dModule = tiles3dModule;
172
- this.tiles3dOptions = { ...options };
173
- this.glyphsUrl = options.glyphs;
174
- this.basemapWorkerUrl = options.basemapWorkerUrl ?? defaultWorkerUrl(options.assetsUrl);
175
- this.basemapTileSize = options.basemapTileSize;
113
+ this.engines = new MapEngines(engineClass, tiles3dModule, options, () => this.gone);
114
+ const workerUrl = options.basemapWorkerUrl ?? defaultWorkerUrl(options.assetsUrl);
115
+ this.tessUrl = tessUrl(options);
116
+ this.workers = new MapWorkers(workerUrl, options.workers, () => this.gone, this);
117
+ this.basemaps = new BasemapLayers(this, this.stack, workerUrl, options.basemapTileSize);
176
118
  this.container = container;
177
- this.canvas = document.createElement('canvas');
178
- Object.assign(this.canvas.style, { display: 'block', width: '100%', height: '100%', touchAction: 'none' });
179
- // Overlays and controls are positioned against the container
180
- injectStyle(container.ownerDocument);
181
- if (getComputedStyle(container).position === 'static')
182
- container.style.position = 'relative';
183
- container.classList.add('isard-map');
184
- this.overlay = document.createElement('div');
185
- this.overlay.className = 'isard-overlay';
186
- const corner = (position) => {
187
- const element = document.createElement('div');
188
- element.className = `isard-ctrl-${position}`;
189
- return element;
190
- };
191
- this.corners = {
192
- 'top-left': corner('top-left'),
193
- 'top-right': corner('top-right'),
194
- 'bottom-left': corner('bottom-left'),
195
- 'bottom-right': corner('bottom-right'),
196
- };
197
- container.append(this.canvas, this.overlay, ...CORNERS.map((position) => this.corners[position]));
119
+ this.canvas = createCanvas();
120
+ this.atlases = new SymbolAtlases(this.canvas, options.glyphs, () => this.#engine);
121
+ this.overlays = new OverlayHost(container, this.canvas, this);
198
122
  this.earth = (options.body ?? 'earth') === 'earth';
199
123
  this.body = options.body;
200
- const body = bodySettings(options.body);
201
- this.settings = {
202
- body,
203
- zoomRange: [options.minZoom ?? 0, options.maxZoom ?? 22],
204
- maxPitch: options.maxPitch ?? 70,
205
- globe: (options.projection ?? 'globe') === 'globe',
206
- sky: options.sky ?? true,
207
- atmosphere: atmosphereSettings(options.atmosphere),
208
- terrain: terrainSettings(options.terrain, options.body, body.radius, this.terrainLoader),
209
- loading: options.loading ?? 'immediate',
210
- background: rgb(options.background ?? (this.earth ? '#f2f2f0' : '#000000')),
211
- globeColor: this.earth ? SEA['osm-positron'] : rgb(body.color),
212
- };
124
+ this.settings = mapSettings(options, this.earth, this.workers.terrainLoader);
125
+ this.frames = new FrameLoop({
126
+ engine: () => this.engine,
127
+ camera: () => this.getCamera(),
128
+ lost: () => this.lost,
129
+ interacting: () => this.interaction?.active === true,
130
+ customLayers: () => this.layers.some((layer) => layer instanceof CustomLayer),
131
+ upgrading: () => this.engines.upgrading !== undefined,
132
+ drawn: (hovered) => {
133
+ // The picking pass sees the frame just drawn; its answer comes on a later frame
134
+ if (hovered && this.listeners.has('hoverfeature'))
135
+ this.hoverPick.request(hovered, this.hoverPicking);
136
+ // Overlays follow the frame just drawn
137
+ this.overlays.positionAll();
138
+ },
139
+ }, this.listeners);
213
140
  this.#engine = this.createEngine();
214
141
  // Once it has an engine: a map whose creation failed before is not left following them
215
142
  this.unwatchHeaders = onHeaderRules((rules) => sendHeaders(this.#engine, rules));
216
143
  // The browser may take the GPU away (driver reset, background tab on mobile): the map waits, then rebuilds
217
- const lost = (event) => {
218
- event.preventDefault();
144
+ this.unwatchContext = watchContext(this.canvas, () => {
219
145
  this.lost = true;
220
- cancelAnimationFrame(this.frameRequest);
221
- this.frameRequest = 0;
222
- };
223
- const restored = () => this.restore();
224
- this.canvas.addEventListener('webglcontextlost', lost);
225
- this.canvas.addEventListener('webglcontextrestored', restored);
226
- this.unwatchContext = () => {
227
- this.canvas.removeEventListener('webglcontextlost', lost);
228
- this.canvas.removeEventListener('webglcontextrestored', restored);
229
- };
146
+ this.frames.cancel();
147
+ }, () => this.restore());
230
148
  const start = this.earth
231
149
  ? { center: [2.4246, 48.8457], zoom: 13, pitch: 0, bearing: 0 }
232
150
  : { center: [0, 0], zoom: 2, pitch: 0, bearing: 0 };
233
151
  this.setCamera({ ...start, ...options });
234
152
  this.setBasemap(options.basemap ?? (this.earth ? 'osm-positron' : 'none'), options.hillshade ?? 0);
153
+ if (options.sun)
154
+ this.setSun(options.sun);
235
155
  this.resizeObserver = new ResizeObserver(() => this.resize());
236
156
  this.resizeObserver.observe(container);
237
- this.watchRatio();
157
+ this.unwatchRatio = watchPixelRatio(() => this.resize());
238
158
  this.resize();
239
159
  const interactive = options.interactive ?? true;
240
160
  const keyboard = interactive && (options.keyboard ?? true);
241
161
  this.interaction = interactive ? new Interaction(this, { keyboard }) : undefined;
242
- // Reached with Tab when the keys move it; named for screen readers either way
243
- this.canvas.classList.add('isard-canvas');
244
- this.canvas.setAttribute('role', keyboard ? 'application' : 'region');
245
- this.canvas.setAttribute('aria-roledescription', 'map');
246
- this.canvas.setAttribute('aria-label', options.ariaLabel ?? 'Map');
247
- if (keyboard)
248
- this.canvas.tabIndex = 0;
162
+ labelCanvas(this.canvas, keyboard, options.ariaLabel ?? 'Map');
249
163
  const attribution = options.attributionControl ?? true;
250
164
  if (attribution !== false) {
251
165
  this.addControl(new AttributionControl(attribution === true ? {} : attribution), 'bottom-right');
@@ -262,48 +176,26 @@ export class GeoMap {
262
176
  get engine() {
263
177
  return this.#engine;
264
178
  }
179
+ /** Layers in drawing order (bottom first) */
180
+ get layers() {
181
+ return this.stack.layers;
182
+ }
183
+ /** Layers new layers go under */
184
+ get onTop() {
185
+ return this.stack.onTop;
186
+ }
265
187
  createEngine() {
266
- const engine = new this.engineClass(this.canvas, () => this.requestRender());
267
- // The body first: the layers' shaders may take its size
268
- const { radius, flattening, atmosphere: air, relief } = this.settings.body;
269
- engine.setBody(radius, flattening, haloSettings(air), ...relief);
270
- sendHeaders(engine, headerRules());
271
- const { zoomRange, maxPitch, globe, sky, atmosphere, terrain, loading, background, globeColor } = this.settings;
272
- engine.setZoomRange(...zoomRange);
273
- engine.setMaxPitch(maxPitch);
274
- engine.setGlobe(globe);
275
- engine.setSky(sky);
276
- engine.setAtmosphere(atmosphere);
277
- engine.setTerrain(terrain?.url, terrain?.exaggeration ?? 1, terrain?.maxZoom, terrain?.geoid, terrain?.loader);
278
- engine.setImmediateLoading(loading === 'immediate');
279
- engine.setBackground(...background);
280
- engine.setGlobeColor(...globeColor);
281
- this.loader ??= this.startLoader(engine);
282
- if (this.loader)
283
- engine.setTileLoader(this.loader);
284
- return engine;
285
- }
286
- /** The decoding workers of 3D Tiles, with the engine that has them (they run its module) and unless turned off */
287
- startLoader(engine) {
288
- return this.tiles3dModule && startTileLoader(this.tiles3dModule, this.tiles3dOptions, engine);
188
+ return this.engines.create(this.canvas, () => this.requestRender(), this.settings);
289
189
  }
290
190
  /** A 3D Tiles layer or a model on the core engine: loads the engine with 3D Tiles, then rebuilds the map on it
291
191
  * (camera, settings and layers kept, as after a context loss); the layer is created then */
292
192
  upgrade() {
293
- this.upgrading ??= loadFullEngineLater(engineUrl('full', this.tiles3dOptions)).then(({ module, Engine }) => {
294
- this.upgrading = undefined;
295
- if (this.gone)
296
- return;
297
- this.engineClass = Engine;
298
- this.tiles3dModule = module;
193
+ this.engines.upgrade(() => {
299
194
  // A lost context: the engine is rebuilt on it once restored
300
195
  if (!this.lost)
301
196
  this.restore();
302
197
  }, (error) => {
303
198
  // The 3D Tiles layers stay empty; the next one added tries again
304
- this.upgrading = undefined;
305
- if (this.gone)
306
- return;
307
199
  report(error);
308
200
  this.requestRender();
309
201
  });
@@ -311,14 +203,12 @@ export class GeoMap {
311
203
  /** Downloads and compiles the engine with 3D Tiles once the map is idle (`tiles3d: 'preload'`), without running
312
204
  * it: the first 3D Tiles layer then only instantiates it */
313
205
  preloadTiles3d() {
314
- const url = engineUrl('full', this.tiles3dOptions);
315
- const later = () => preloadFullEngine(url, () => !this.gone && !this.tiles3dModule);
316
- if (this.settled)
317
- later();
206
+ if (this.frames.settled)
207
+ this.engines.preload();
318
208
  else {
319
209
  const off = this.on('idle', () => {
320
210
  off();
321
- later();
211
+ this.engines.preload();
322
212
  });
323
213
  }
324
214
  }
@@ -326,51 +216,28 @@ export class GeoMap {
326
216
  restore() {
327
217
  if (this.gone)
328
218
  return;
329
- this.cancelHoverPick();
219
+ this.hoverPick.cancel();
330
220
  const camera = this.getCamera();
331
221
  this.#engine.free();
332
222
  this.#engine = this.createEngine();
333
223
  this.lost = false;
334
224
  this.setCamera(camera);
335
- this.#glyphs?.attach(this.#engine);
336
- this.#icons?.attach(this.#engine);
337
- const engine = this.#engine;
338
- // No layer refers to an id of the old engine while the others are created
339
- for (const layer of this.layers)
340
- layer.forgetEngine();
341
- for (const layer of this.layers) {
342
- // Still waiting for the engine with 3D Tiles (loading, or failed): created with it
343
- if (layer.needsFullEngine && !this.tiles3dModule)
344
- continue;
345
- // A layer that fails is reported and left out; the others are created
346
- try {
347
- layer.attach(engine);
348
- }
349
- catch (error) {
350
- report(error);
351
- }
352
- }
353
- // Links between layers, once every layer has its new id (buildings colored by a network above them)
354
- for (const layer of this.layers) {
355
- if (layer.id < 0)
356
- continue;
357
- try {
358
- layer.relink(engine);
359
- }
360
- catch (error) {
361
- report(error);
362
- }
363
- }
225
+ this.atlases.attach(this.#engine);
226
+ // Those still waiting for the engine with 3D Tiles (loading, or failed) are created with it
227
+ this.stack.reattach(this.#engine, (layer) => layer.needsFullEngine && !this.engines.tiles3d);
364
228
  this.applySwipe();
365
229
  // A street-level eye stays one in the new engine
366
230
  if (this.#firstPerson)
367
231
  this.setFirstPerson(this.#firstPerson);
368
- // What the old engine drew over the scene: the viewshed, the panoramas shown
232
+ // What the old engine drew over the scene: the viewshed, the sun's shadows, the panoramas shown
369
233
  if (this.viewshed)
370
234
  this.setViewshed(this.viewshed);
371
- for (const set of this.panoramaSets)
235
+ if (this.sun)
236
+ this.setSun(this.sun);
237
+ this.panoramaSets.forEach((set) => {
372
238
  if (set.current)
373
239
  void set.show(set.current.index).catch(report);
240
+ });
374
241
  this.resize();
375
242
  }
376
243
  /** The street-level eye, while there is one (`setFirstPerson`) */
@@ -392,13 +259,8 @@ export class GeoMap {
392
259
  this.setCamera({ center: was.lonLat, zoom: 18, pitch: 60, bearing: was.bearing });
393
260
  return;
394
261
  }
395
- const eye = {
396
- height: 1.7,
397
- pitch: 90,
398
- fov: 75,
399
- ...view,
400
- lonLat: [view.lonLat[0], view.lonLat[1]],
401
- };
262
+ const lonLat = [view.lonLat[0], view.lonLat[1]];
263
+ const eye = { height: 1.7, pitch: 90, fov: 75, ...view, lonLat };
402
264
  eye.pitch = Math.max(1, Math.min(179, eye.pitch));
403
265
  eye.fov = Math.max(10, Math.min(140, eye.fov));
404
266
  eye.bearing = ((eye.bearing % 360) + 360) % 360;
@@ -411,47 +273,41 @@ export class GeoMap {
411
273
  /**
412
274
  * Compares two states of a place: `layers` shown on one side of a vertical line only, at `position` CSS pixels
413
275
  * from the left of the map (left of it by default, `side: 'right'` for the other), the layers beneath them on the
414
- * other side. Set again as the line moves (a slider, a drag); undefined stops
276
+ * other side; `other`: layers shown on the other side only (two meshes of a place, each on its side). Set again as
277
+ * the line moves (a slider, a drag); undefined stops
415
278
  */
416
279
  setSwipe(options) {
417
- this.swipe = options && options.layers.length > 0 ? { ...options, layers: [...options.layers] } : undefined;
280
+ const used = options && (options.layers.length > 0 || (options.other?.length ?? 0) > 0);
281
+ this.swipe = used ? { ...options, layers: [...options.layers], other: [...(options.other ?? [])] } : undefined;
418
282
  this.applySwipe();
419
283
  this.requestRender();
420
284
  }
421
285
  applySwipe() {
422
286
  const s = this.swipe;
423
- const ids = Uint32Array.from((s?.layers ?? []).filter((l) => l.id >= 0), (l) => l.id);
424
- this.engine.setSwipe(s && ids.length > 0 ? s.position : Number.NaN, s?.side === 'right', ids);
287
+ const idsOf = (layers) => Uint32Array.from((layers ?? []).filter((l) => l.id >= 0), (l) => l.id);
288
+ const ids = idsOf(s?.layers);
289
+ const others = idsOf(s?.other);
290
+ const shown = ids.length > 0 || others.length > 0;
291
+ this.engine.setSwipe(s && shown ? s.position : Number.NaN, s?.side === 'right', ids, others);
425
292
  }
426
293
  /** @internal Forgets a removed layer */
427
294
  detach(layer) {
428
- this.unlist(layer);
429
- this.onTop.delete(layer);
430
- if (this.rasterCredits.delete(layer))
431
- this.attributionChanged();
295
+ this.stack.forget(layer);
296
+ this.credits.forget(layer);
432
297
  }
433
298
  /** @internal The layer is on the map */
434
299
  hasLayer(layer) {
435
- return !this.gone && this.layers.includes(layer);
436
- }
437
- /** Takes a layer out of the drawing order (to put it back elsewhere, or for good) */
438
- unlist(layer) {
439
- const index = this.layers.indexOf(layer);
440
- if (index >= 0)
441
- this.layers.splice(index, 1);
300
+ return !this.gone && this.stack.has(layer);
442
301
  }
443
302
  addLayer(layer) {
444
303
  // On the core engine, a 3D Tiles layer or a model waits for the engine with 3D Tiles: it is created with the
445
304
  // others on it
446
- if (layer.needsFullEngine && !this.tiles3dModule)
305
+ if (layer.needsFullEngine && !this.engines.tiles3d)
447
306
  this.upgrade();
448
307
  else
449
308
  layer.attach(this.engine);
450
- this.layers.push(layer);
451
309
  // Under the layers kept on top (the basemap's labels)
452
- const top = this.layers.find((l) => this.onTop.has(l) && l !== layer);
453
- if (top)
454
- this.moveLayer(layer, top);
310
+ this.stack.add(layer);
455
311
  this.requestRender();
456
312
  return layer;
457
313
  }
@@ -461,54 +317,21 @@ export class GeoMap {
461
317
  }
462
318
  /** @internal Moves a layer under `before`, or on top of all */
463
319
  moveLayer(layer, before) {
464
- const handle = layer;
465
- this.unlist(handle);
466
- const index = before ? this.layers.indexOf(before) : -1;
467
- const at = index < 0 ? this.layers.length : index;
468
- this.layers.splice(at, 0, handle);
469
- if (handle.id >= 0)
470
- this.engine.moveLayer(handle.id, this.engineIndex(at));
320
+ this.stack.move(layer, before);
471
321
  this.requestRender();
472
322
  }
473
323
  /** @internal See `LayerHost.replaceLayer` */
474
324
  replaceLayer(layer, old) {
475
- this.engine.removeLayer(old);
476
- const index = this.layers.indexOf(layer);
477
- if (index >= 0)
478
- this.engine.moveLayer(layer.id, this.engineIndex(index));
479
- }
480
- /** Index in the engine of the layer at `index`: the layers waiting for the engine with 3D Tiles are not in it yet */
481
- engineIndex(index) {
482
- let count = 0;
483
- for (let i = 0; i < index; i += 1)
484
- if ((this.layers[i]?.id ?? -1) >= 0)
485
- count += 1;
486
- return count;
487
- }
488
- /** Moves a layer under all the others */
489
- toBottom(layer) {
490
- this.engine.moveLayer(layer.id, 0);
491
- this.unlist(layer);
492
- this.layers.unshift(layer);
325
+ this.stack.replace(layer, old);
493
326
  }
494
327
  // ── Events ──
495
328
  /** Listens to an event; returns the function that stops listening */
496
329
  on(type, listener) {
497
- const listeners = this.listeners[type];
498
- listeners.add(listener);
499
- return () => listeners.delete(listener);
330
+ return this.listeners.on(type, listener);
500
331
  }
501
332
  /** @internal Calls the listeners; one that throws is reported and does not stop the others nor the frame */
502
333
  emit(type, event) {
503
- const listeners = this.listeners[type];
504
- for (const listener of listeners) {
505
- try {
506
- listener(event);
507
- }
508
- catch (error) {
509
- report(error);
510
- }
511
- }
334
+ this.listeners.emit(type, event);
512
335
  }
513
336
  // ── Camera ──
514
337
  /**
@@ -537,13 +360,14 @@ export class GeoMap {
537
360
  elevationAt: (lonLat) => this.elevationAt(lonLat),
538
361
  metersPerPixel: (point) => this.metersPerPixel(point),
539
362
  };
540
- /** What is drawn over the scene, together: `viewshed` (`setViewshed`), `swipe` (`setSwipe`) */
363
+ /** What is drawn over the scene, together: `viewshed` (`setViewshed`), `sun` (`setSun`), `swipe` (`setSwipe`) */
541
364
  effects = {
542
365
  viewshed: (options) => this.setViewshed(options),
366
+ sun: (options) => this.setSun(options),
543
367
  swipe: (options) => this.setSwipe(options),
544
368
  };
545
369
  getCamera() {
546
- return this.frameCamera ?? this.engine.camera();
370
+ return this.frames.camera ?? this.engine.camera();
547
371
  }
548
372
  setCamera(camera) {
549
373
  const c = { ...this.getCamera(), ...camera };
@@ -614,57 +438,67 @@ export class GeoMap {
614
438
  this.engine.setSky(enabled);
615
439
  this.requestRender();
616
440
  }
441
+ /** The viewshed set, set again in a new engine */
442
+ viewshed;
443
+ /** The sun set, set again in a new engine */
444
+ sun;
445
+ /**
446
+ * The sun casting shadows: the buildings, trees, models, 3D Tiles and the relief shade the ground and one another,
447
+ * the walls turned away from it are in their own shade; the labels stay over them. The sun stands where it is at
448
+ * `date` over the map's center (now by default), or at `azimuth` and `altitude`. As it sets the night falls (`night`
449
+ * to set it by hand): the scene tinted in its blue, the windows of the buildings and the streets of `vectors()` lit,
450
+ * the labels light on dark.
451
+ * The shadows cover the ground around the view's center, fading out away from it, and cost two more depth passes
452
+ * of the solid scene each frame. Undefined removes them
453
+ */
454
+ setSun(options) {
455
+ this.sun = options;
456
+ if (!options) {
457
+ this.engine.setSun(Number.NaN, Number.NaN, 0, 0);
458
+ this.requestRender();
459
+ return;
460
+ }
461
+ const { azimuth, altitude } = options.azimuth !== undefined && options.altitude !== undefined
462
+ ? { azimuth: options.azimuth, altitude: options.altitude }
463
+ : sunPosition(options.date ?? new Date(), this.getCamera().center);
464
+ // The twilight: from 6° above the horizon to 6° under it, smoothly
465
+ const dusk = Math.min(1, Math.max(0, (6 - altitude) / 12));
466
+ const night = Math.min(1, Math.max(0, options.night ?? dusk * dusk * (3 - 2 * dusk)));
467
+ // The shadows fade with the sun's light
468
+ const opacity = Math.min(1, Math.max(0, options.opacity ?? 0.35)) * (1 - night);
469
+ this.engine.setSun(azimuth, altitude, opacity, night);
470
+ this.requestRender();
471
+ }
617
472
  /** Tints what can be seen from a point and what is hidden from it, within a range, all around or within a cone
618
473
  * (`direction`): the relief, extrusions, 3D Tiles and models hide what lies behind them (only what the map has
619
474
  * loaded for its view). Several observers (16 at most): what any of them sees is seen, what is hidden from all those
620
475
  * that reach it is hidden, in the colors and opacity of the first. Undefined or none removes it */
621
- /** The viewshed set, set again in a new engine */
622
- viewshed;
623
476
  setViewshed(options) {
624
477
  this.viewshed = options;
625
- let observers = [];
626
- if (options)
627
- observers = 'position' in options ? [options] : options;
628
- const [first] = observers;
629
- if (!first) {
630
- this.engine.clearViewshed();
631
- return;
632
- }
633
- this.engine.setViewshed(new Float64Array(observers.flatMap((o) => [
634
- o.position[0],
635
- o.position[1],
636
- o.height ?? 1.7,
637
- o.range ?? 1000,
638
- ...(o.direction === undefined
639
- ? [Number.NaN, 0, 0, 0]
640
- : [o.direction, o.tilt ?? 0, o.fov ?? 60, o.verticalFov ?? 40]),
641
- ])), new Float32Array(rgb(first.visibleColor ?? '#2f9e44')), new Float32Array(rgb(first.hiddenColor ?? '#e03131')), first.opacity ?? 0.7, new Float32Array(first.coneColor ? rgb(first.coneColor) : []));
642
- this.requestRender();
478
+ if (applyViewshed(this.engine, options))
479
+ this.requestRender();
643
480
  }
644
481
  /** Relief under the basemap: true for the body's terrain model (on the Earth the worldwide Terrain Tiles, NASA's
645
482
  * on the Moon and Mars), one of the Earth's models by name (`'ign'`), its options, or false; on another body, its
646
483
  * options with a `url` */
647
484
  setTerrain(terrain) {
648
- this.settings.terrain = terrainSettings(terrain, this.body, this.settings.body.radius, this.terrainLoader);
485
+ this.settings.terrain = terrainSettings(terrain, this.body, this.settings.body.radius, this.workers.terrainLoader);
649
486
  const t = this.settings.terrain;
650
487
  this.engine.setTerrain(t?.url, t?.exaggeration ?? 1, t?.maxZoom, t?.geoid, t?.loader);
651
- this.attributionChanged();
488
+ this.credits.changed();
652
489
  this.requestRender();
653
490
  }
491
+ /** @internal Whether a relief is under the basemap: the fills of vector tiles drawn as geometry need their prisms */
492
+ get hasRelief() {
493
+ return this.settings.terrain !== undefined;
494
+ }
654
495
  /** @internal The credits of the raster layers (the basemap first) and of the relief in use, each once */
655
496
  attributions() {
656
- const layers = this.layers.map((layer) => this.rasterCredits.get(layer));
657
- const credits = [...layers, this.settings.terrain?.attribution];
658
- return [...new Set(credits.filter((c) => c !== undefined))];
497
+ return this.credits.of(this.layers, this.settings.terrain?.attribution);
659
498
  }
660
499
  /** @internal Calls `listener` when the credits change; returns what stops it */
661
500
  onAttributionChange(listener) {
662
- this.attributionListeners.add(listener);
663
- return () => this.attributionListeners.delete(listener);
664
- }
665
- attributionChanged() {
666
- for (const listener of this.attributionListeners)
667
- listener();
501
+ return this.credits.onChange(listener);
668
502
  }
669
503
  /** Color behind the map, a CSS color (its alpha ignored) */
670
504
  setBackground(color) {
@@ -699,47 +533,9 @@ export class GeoMap {
699
533
  const layer = this.layers.find((l) => l.id === hit.layer);
700
534
  if (!layer)
701
535
  return undefined;
702
- const feature = 'featureAt' in layer && typeof layer.featureAt === 'function'
703
- ? layer.featureAt(hit.index)
704
- : undefined;
536
+ const feature = hasFeatures(layer) ? layer.featureAt(hit.index) : undefined;
705
537
  return { layer, index: hit.index, feature };
706
538
  }
707
- /** Asks the engine what is under the pointer; its answer, read without waiting for the GPU, comes on a later frame */
708
- requestHoverPick(point) {
709
- const { radius = 0, layers } = this.hoverPicking;
710
- const only = layers && Uint32Array.from(layers.map((l) => l.id));
711
- // The engine cannot pick (no request in flight): the poll under way would wait for nothing
712
- if (!this.engine.requestPick(point[0], point[1], radius, only)) {
713
- this.cancelHoverPick();
714
- return;
715
- }
716
- // A new request replaces the one in flight (in the engine too): one poll loop at most
717
- this.hoverPolls = 0;
718
- if (!this.hoverPoll)
719
- this.hoverPoll = requestAnimationFrame(this.pollHoverPick);
720
- }
721
- pollHoverPick = () => {
722
- this.hoverPoll = 0;
723
- if (this.lost)
724
- return;
725
- const result = this.engine.pollPick();
726
- if (!result) {
727
- // The GPU is not done yet (usually a frame): asked again next frame, without drawing; given up at last
728
- this.hoverPolls += 1;
729
- if (this.hoverPolls < MAX_POLLS)
730
- this.hoverPoll = requestAnimationFrame(this.pollHoverPick);
731
- return;
732
- }
733
- this.emit('hoverfeature', {
734
- point: [result.x, result.y],
735
- lonLat: result.lonLat,
736
- hit: result.hit && this.featureHit(result.hit),
737
- });
738
- };
739
- cancelHoverPick() {
740
- cancelAnimationFrame(this.hoverPoll);
741
- this.hoverPoll = 0;
742
- }
743
539
  /** Screen point (CSS pixels) of a position on the ground (the relief), or `height` metres above it, undefined
744
540
  * behind the camera */
745
541
  project(lonLat, height = 0) {
@@ -755,41 +551,13 @@ export class GeoMap {
755
551
  */
756
552
  async addPanoramas(options) {
757
553
  // Drawn by the engine with 3D Tiles: loaded first if needed
758
- if (!this.tiles3dModule) {
554
+ if (!this.engines.tiles3d) {
759
555
  this.upgrade();
760
- await this.upgrading;
761
- if (!this.tiles3dModule)
556
+ await this.engines.upgrading;
557
+ if (!this.engines.tiles3d)
762
558
  throw new Error('isard: panoramas need the engine with 3D Tiles, which failed to load');
763
559
  }
764
- const host = {
765
- setPanorama: (...args) => {
766
- this.engine.setPanorama(...args);
767
- this.requestRender();
768
- },
769
- setPanoramaImage: (index, image) => this.engine.setPanoramaImage(index, image),
770
- clearPanorama: () => this.engine.clearPanorama(),
771
- geoidHeight: (lon, lat) => this.engineClass.geoidHeight(lon, lat),
772
- firstPerson: () => this.firstPerson,
773
- setFirstPerson: (view) => this.setFirstPerson(view),
774
- elevationAt: (lonLat) => this.elevationAt(lonLat),
775
- listen: (click, key, wheel) => {
776
- const off = this.on('click', ({ lonLat }) => click(lonLat));
777
- const container = this.getContainer();
778
- // Before the map's own wheel (its zoom, the eye's field of view)
779
- const onWheel = (e) => {
780
- e.preventDefault();
781
- e.stopImmediatePropagation();
782
- wheel(e);
783
- };
784
- container.addEventListener('keydown', key);
785
- container.addEventListener('wheel', onWheel, { capture: true, passive: false });
786
- return () => {
787
- off();
788
- container.removeEventListener('keydown', key);
789
- container.removeEventListener('wheel', onWheel, { capture: true });
790
- };
791
- },
792
- };
560
+ const host = panoramaHost(this, (lon, lat) => this.engines.geoidHeight(lon, lat));
793
561
  const panoramas = await loadPanoramas(host, options);
794
562
  this.panoramaSets.add(panoramas);
795
563
  return panoramas;
@@ -836,106 +604,47 @@ export class GeoMap {
836
604
  // ── Overlays and controls ──
837
605
  /** Adds a control in a corner of the map; controls stack from the corner inward */
838
606
  addControl(control, position = 'top-right') {
839
- if (this.controls.has(control))
840
- return this;
841
- const element = control.onAdd(this);
842
- element.classList.add('isard-ctrl');
843
- // In the bottom corners the first control added stays lowest, as in MapLibre
844
- if (position.startsWith('bottom'))
845
- this.corners[position].prepend(element);
846
- else
847
- this.corners[position].append(element);
848
- this.controls.set(control, element);
607
+ this.overlays.addControl(control, position);
849
608
  return this;
850
609
  }
851
610
  removeControl(control) {
852
- const element = this.controls.get(control);
853
- if (!element)
854
- return this;
855
- this.controls.delete(control);
856
- control.onRemove(this);
857
- element.remove();
611
+ this.overlays.removeControl(control);
858
612
  return this;
859
613
  }
860
614
  /** @internal Shows a marker or popup, placed after every frame from now on */
861
615
  attachOverlay(item, element) {
862
- this.overlay.append(element);
863
- this.positioned.add(item);
864
- item.position(this.size);
616
+ this.overlays.attach(item, element);
865
617
  }
866
618
  /** @internal */
867
619
  detachOverlay(item, element) {
868
- this.positioned.delete(item);
869
- element.remove();
620
+ this.overlays.detach(item, element);
870
621
  }
871
622
  /** @internal Places one overlay now (it moved), without drawing a frame */
872
623
  positionOverlay(item) {
873
- if (this.positioned.has(item))
874
- item.position(this.size);
624
+ this.overlays.position(item);
875
625
  }
876
626
  // ── Layers ──
877
627
  /** Replaces the basemap and the IGN hillshade, under the other layers (hillshade by default: 0, none). The former
878
628
  * names of the Géoplateforme basemaps throw, naming the new ones */
879
629
  setBasemap(basemap, hillshade = 0) {
880
630
  const name = checkBasemap(basemap);
881
- if (this.basemap && this.hasLayer(this.basemap))
882
- this.basemap.remove();
883
- if (this.hillshade && this.hasLayer(this.hillshade))
884
- this.hillshade.remove();
885
- // Its raster and labels go with it
886
- this.vector?.tiles.destroy();
887
- this.vector = undefined;
888
- const vector = VECTOR_BASEMAPS[name];
889
- if (vector) {
890
- const tiles = new VectorTiles(this, {
891
- style: vector,
892
- ...(this.basemapWorkerUrl ? { workerUrl: this.basemapWorkerUrl } : {}),
893
- });
894
- this.basemap = tiles.raster(this.basemapTileSize ? { size: this.basemapTileSize } : {});
895
- this.vector = { tiles, labels: tiles.labels() };
896
- }
897
- else {
898
- this.basemap = name === 'none' ? undefined : this.addRaster(BASEMAPS[name]);
899
- }
631
+ this.basemaps.replace(name);
900
632
  // Off the Earth, the body's own color
901
633
  if (this.earth)
902
634
  this.settings.globeColor = SEA[name];
903
635
  this.engine.setGlobeColor(...this.settings.globeColor);
904
- this.hillshade = hillshade > 0 ? this.addRaster({ ...HILLSHADE, opacity: hillshade }) : undefined;
905
- if (this.hillshade)
906
- this.toBottom(this.hillshade);
907
- if (this.basemap)
908
- this.toBottom(this.basemap);
909
- this.attributionChanged();
636
+ this.basemaps.addHillshade(hillshade);
637
+ this.credits.changed();
910
638
  }
911
639
  /** Raster tiles ({z}, {x}, {y} URL; Web Mercator, or reprojected from another `grid`: longitudes and latitudes,
912
640
  * Lambert-93, UTM…), over the layers already added */
913
641
  addRaster(options) {
914
- const { url, grid = 'mercator', opacity = 1, saturation = 1, size, before } = options;
915
- let { minZoom = 0, maxZoom = 19 } = options;
916
- // A PMTiles archive (`pmtiles://…`): its tiles read by range requests
917
- let loader = options.loader ?? pmtilesRasterLoader(url, size ?? 256);
918
- if (grid !== 'mercator' && !loader) {
919
- if (grid === 'equirectangular') {
920
- // `minZoom` and `maxZoom` are levels of the source: its level n has the pixel size of Mercator level n + 1 at
921
- // the equator
922
- loader = gridLoader(url, resolveGrid(equirectangularGrid(minZoom, maxZoom)), size ?? 256, this.remoteReprojection);
923
- [minZoom, maxZoom] = [minZoom + 1, maxZoom + 1];
924
- }
925
- else {
926
- const resolved = resolveGrid(grid);
927
- const zooms = gridZooms(resolved);
928
- minZoom = options.minZoom ?? zooms.minZoom;
929
- maxZoom = options.maxZoom ?? zooms.maxZoom;
930
- loader = gridLoader(url, resolved, size ?? 256, this.remoteReprojection);
931
- }
932
- }
642
+ const { url, opacity = 1, saturation = 1, size, before } = options;
643
+ const { loader, minZoom, maxZoom } = rasterSource(options, this.workers.remoteReprojection);
933
644
  const layer = new RasterLayer(this, { url, minZoom, maxZoom, opacity, saturation, size, loader });
934
645
  this.place(layer, before);
935
- if (options.attribution) {
936
- this.rasterCredits.set(layer, options.attribution);
937
- this.attributionChanged();
938
- }
646
+ if (options.attribution)
647
+ this.credits.set(layer, options.attribution);
939
648
  return layer;
940
649
  }
941
650
  /**
@@ -961,32 +670,7 @@ export class GeoMap {
961
670
  * another layer. The layer's `ready` resolves once the map has drawn it
962
671
  */
963
672
  add(spec) {
964
- switch (spec.type) {
965
- case 'lines':
966
- return this.addLines(spec);
967
- case 'circles':
968
- return this.addCircles(spec);
969
- case 'fills':
970
- return this.addFills(spec);
971
- case 'symbols':
972
- return this.addSymbols(spec);
973
- case 'extrusions':
974
- return this.addExtrusions(spec);
975
- case 'raster':
976
- return this.addRaster(spec);
977
- case 'ground':
978
- return this.addGround(spec);
979
- case 'network':
980
- return this.addNetwork(spec);
981
- case 'tiles3d':
982
- return this.addTiles3d(spec);
983
- case 'pointcloud':
984
- return this.addPointCloud(spec);
985
- case 'models':
986
- return this.addModels(spec);
987
- default:
988
- throw new Error(`isard: no layer of type ${String(spec.type)}`);
989
- }
673
+ return addSpec(this, spec);
990
674
  }
991
675
  /** @internal A layer made anew from its description, in the place of `old` in the drawing order (old removed) */
992
676
  rebuildLayer(old, spec) {
@@ -1007,7 +691,7 @@ export class GeoMap {
1007
691
  off();
1008
692
  resolve();
1009
693
  });
1010
- this.requestRender();
694
+ this.frames.requestIdle();
1011
695
  });
1012
696
  }
1013
697
  /** Lines from GeoJSON (line strings, and polygon outlines), styled per feature */
@@ -1036,24 +720,11 @@ export class GeoMap {
1036
720
  }
1037
721
  /** @internal The glyphs of the symbol layers, loaded on first use */
1038
722
  get glyphs() {
1039
- if (!this.#glyphs) {
1040
- // The engine's context: the atlas stays within its texture size limit
1041
- const gl = this.canvas.getContext('webgl2');
1042
- const max = gl?.getParameter(gl.MAX_TEXTURE_SIZE);
1043
- this.#glyphs = new Glyphs(this.glyphsUrl, typeof max === 'number' ? max : undefined);
1044
- this.#glyphs.attach(this.engine);
1045
- }
1046
- return this.#glyphs;
723
+ return this.atlases.glyphs;
1047
724
  }
1048
725
  /** @internal The icons of the symbol layers: the application's images and the vector basemaps' sprites */
1049
726
  get icons() {
1050
- if (!this.#icons) {
1051
- const gl = this.canvas.getContext('webgl2');
1052
- const max = gl?.getParameter(gl.MAX_TEXTURE_SIZE);
1053
- this.#icons = new Icons(typeof max === 'number' ? max : undefined);
1054
- this.#icons.attach(this.engine);
1055
- }
1056
- return this.#icons;
727
+ return this.atlases.icons;
1057
728
  }
1058
729
  /**
1059
730
  * An image for the symbol layers' icons (`icon: name`), as MapLibre's `addImage`: its pixels (an image, a canvas,
@@ -1062,31 +733,32 @@ export class GeoMap {
1062
733
  * text. An image of the same name is replaced
1063
734
  */
1064
735
  addImage(name, image, options = {}) {
1065
- const pixels = 'data' in image ? image : readPixels(image);
1066
- if (!pixels)
1067
- throw new Error(`isard: the image ${name} has no pixels`);
1068
- this.icons.add(name, pixels, options);
736
+ this.atlases.addImage(name, image, options);
1069
737
  }
1070
738
  hasImage(name) {
1071
- return this.#icons?.has(name) ?? false;
739
+ return this.atlases.hasImage(name);
1072
740
  }
1073
741
  removeImage(name) {
1074
- this.#icons?.remove(name);
742
+ this.atlases.removeImage(name);
1075
743
  }
1076
744
  /**
1077
745
  * Vector tiles with a MapLibre style (OpenFreeMap's Positron by default): `tiles.raster(…)` draws its fills and lines
1078
- * (or a subset) as a raster layer, `tiles.labels(…)` its labels. `setBasemap('osm-positron')` does both for the
1079
- * basemap.
746
+ * (or a subset) as a raster layer, `tiles.vectors(…)` as geometry, `tiles.labels(…)` its labels.
747
+ * `setBasemap('osm-positron')` draws the basemap with a raster layer and the labels.
1080
748
  */
1081
749
  vectorTiles(options = {}) {
1082
750
  const tiles = new VectorTiles(this, {
1083
- ...(this.basemapWorkerUrl ? { workerUrl: this.basemapWorkerUrl } : {}),
751
+ ...(this.workers.url ? { workerUrl: this.workers.url } : {}),
1084
752
  ...options,
1085
753
  });
1086
754
  // Its workers stop with the map
1087
755
  this.vectorSets.add(tiles);
1088
756
  return tiles;
1089
757
  }
758
+ /** @internal The geometry of a `VectorTiles` (`vectors`), under `before` if given */
759
+ addVectorLayer(before) {
760
+ return this.place(new VectorLayer(this), before);
761
+ }
1090
762
  /** @internal Forgets a destroyed `VectorTiles` */
1091
763
  dropVectorTiles(tiles) {
1092
764
  this.vectorSets.delete(tiles);
@@ -1096,40 +768,8 @@ export class GeoMap {
1096
768
  * when it asks for another count or URL
1097
769
  */
1098
770
  basemapWorkers(count, url) {
1099
- const sharedUrl = this.basemapWorkerUrl;
1100
- const shared = defaultWorkerCount();
1101
- const workers = Math.max(1, count ?? shared);
1102
- const workerUrl = url ?? sharedUrl;
1103
- if (workers === shared && String(workerUrl) === String(sharedUrl)) {
1104
- this.basemapPool ??= new BasemapWorkers(shared, sharedUrl);
1105
- return { pool: this.basemapPool, own: false };
1106
- }
1107
- return { pool: new BasemapWorkers(workers, workerUrl), own: true };
771
+ return this.workers.pool(count, url);
1108
772
  }
1109
- /**
1110
- * The workers decoding the relief and reprojecting rasters: the map's pool, unless the map has no workers
1111
- * (`workers: false` or 0, or none in this browser): that is done in the page then
1112
- */
1113
- decodingPool() {
1114
- const { workers } = this.tiles3dOptions;
1115
- if (this.gone || workers === false || (workers !== undefined && workers <= 0))
1116
- return undefined;
1117
- if (typeof Worker === 'undefined' || typeof OffscreenCanvas === 'undefined')
1118
- return undefined;
1119
- return this.basemapWorkers().pool;
1120
- }
1121
- remoteHeights = (url, encoding, floor, tile, signal) => this.decodingPool()?.heights(url, encoding, floor, tile, signal, this);
1122
- remoteReprojection = (job, tile, signal) => this.decodingPool()?.reproject(job, tile, signal, this);
1123
- /** The loader of the relief tiles of `url` in a PNG encoding (one per URL, encoding and floor) */
1124
- terrainLoader = (url, encoding, floor) => {
1125
- const key = `${encoding} ${floor ?? ''} ${url}`;
1126
- let loader = this.terrainLoaders.get(key);
1127
- if (!loader) {
1128
- loader = encodedTerrainLoader(url, encoding, floor, this.remoteHeights);
1129
- this.terrainLoaders.set(key, loader);
1130
- }
1131
- return loader;
1132
- };
1133
773
  /** Adds a layer, under `before` if given */
1134
774
  place(layer, before) {
1135
775
  this.addLayer(layer);
@@ -1163,126 +803,31 @@ export class GeoMap {
1163
803
  * WebGL, three.js… with the matrices of the frame (`frame.matrixAt`); under `before` if given
1164
804
  */
1165
805
  addCustomLayer(layer, options = {}) {
1166
- const custom = new CustomLayer(this, layer, this.canvas, this, (lon, lat, altitude) => this.engineClass.customMatrix(lon, lat, altitude), () => this.getCamera());
806
+ const custom = new CustomLayer(this, layer, this.canvas, this, (lon, lat, altitude) => this.engines.customMatrix(lon, lat, altitude), () => this.getCamera());
1167
807
  return this.place(custom, options.before);
1168
808
  }
1169
809
  // ── Frames ──
1170
810
  /** Asks for a frame; several calls before the next frame draw it once */
1171
811
  requestRender() {
1172
- // Changes made by listeners during a frame are drawn by that frame; nothing is drawn without a context
1173
- if (this.inFrame || this.lost)
1174
- return;
1175
- this.frameRequest ||= requestAnimationFrame((now) => this.frame(now));
812
+ this.frames.request();
1176
813
  }
1177
814
  /** @internal Runs `step` every frame until it returns false; replaces the running animation */
1178
815
  animate(step) {
1179
- // A flight cut short no longer needs its destination
1180
- if (this.animation)
1181
- this.engine.clearPreload();
1182
- this.animation = step;
1183
- this.requestRender();
816
+ this.frames.animate(step);
1184
817
  }
1185
818
  /** @internal The pointer moved over the map (undefined: it left). Without 'hover' and 'hoverfeature' listeners,
1186
819
  * nothing is drawn: a still map stays idle under a moving mouse */
1187
820
  hover(point) {
1188
821
  if (!point) {
1189
- this.hoverPoint = undefined;
1190
- this.cancelHoverPick();
822
+ this.frames.hover(undefined);
823
+ this.hoverPick.cancel();
1191
824
  return;
1192
825
  }
1193
- if (this.listeners.hover.size === 0 && this.listeners.hoverfeature.size === 0)
826
+ if (!this.listeners.has('hover') && !this.listeners.has('hoverfeature'))
1194
827
  return;
1195
- this.hoverPoint = point;
828
+ this.frames.hover(point);
1196
829
  this.requestRender();
1197
830
  }
1198
- frame(now) {
1199
- this.frameRequest = 0;
1200
- try {
1201
- this.inFrame = true;
1202
- this.drawFrame(now);
1203
- }
1204
- finally {
1205
- // Whatever throws in a frame, the next ones are still drawn
1206
- this.inFrame = false;
1207
- }
1208
- }
1209
- drawFrame(now) {
1210
- if (this.animation && !this.animation(now))
1211
- this.animation = undefined;
1212
- const camera = this.getCamera();
1213
- const key = `${camera.center}|${camera.zoom}|${camera.pitch}|${camera.bearing}`;
1214
- const moved = key !== this.lastCamera;
1215
- if (moved) {
1216
- // The first frame is no move
1217
- if (!this.moving && this.lastCamera !== '') {
1218
- this.moving = true;
1219
- this.emit('movestart', camera);
1220
- }
1221
- this.lastCamera = key;
1222
- this.emit('move', camera);
1223
- }
1224
- else if (this.moving && !this.animation && !this.interaction?.active) {
1225
- this.moving = false;
1226
- this.emit('moveend', camera);
1227
- }
1228
- // Values for the pointer position are computed before drawing, in the same frame
1229
- let hovered;
1230
- if (this.hoverPoint) {
1231
- const point = this.hoverPoint;
1232
- hovered = point;
1233
- this.hoverPoint = undefined;
1234
- // The position under the pointer for its listeners only (a 'hoverfeature' alone does not need it)
1235
- if (this.listeners.hover.size > 0)
1236
- this.emit('hover', { point, lonLat: this.unproject(point) });
1237
- }
1238
- const t0 = performance.now();
1239
- // The engine asks for the next frame while it draws (uploads past the frame budget, fades): not ignored
1240
- this.inFrame = false;
1241
- // As the engine has it now (a listener may have moved it), for the custom layers drawn in the frame
1242
- this.frameCamera = this.layers.some((layer) => layer instanceof CustomLayer) ? this.engine.camera() : undefined;
1243
- let loading;
1244
- try {
1245
- loading = this.engine.render();
1246
- }
1247
- finally {
1248
- this.frameCamera = undefined;
1249
- }
1250
- this.inFrame = true;
1251
- // The picking pass sees the frame just drawn; its answer comes on a later frame
1252
- if (hovered && this.listeners.hoverfeature.size > 0)
1253
- this.requestHoverPick(hovered);
1254
- // Overlays follow the frame just drawn (camera, relief altitudes): all in one pass, transforms only
1255
- for (const item of this.positioned)
1256
- item.position(this.size);
1257
- this.emit('render', { ms: performance.now() - t0 });
1258
- this.inFrame = false;
1259
- // A move goes on until a frame finds the camera still: that frame sends 'moveend'
1260
- if (this.animation || (moved && this.moving))
1261
- this.requestRender();
1262
- // Idle: nothing loading (the engine with 3D Tiles neither), no animation, no frame asked for (uploads waiting,
1263
- // fades, listeners)
1264
- if (loading || this.upgrading || this.animation || this.frameRequest !== 0) {
1265
- this.settled = false;
1266
- }
1267
- else if (!this.settled) {
1268
- this.settled = true;
1269
- this.emit('idle', {});
1270
- }
1271
- }
1272
- /** Resizes when the device pixel ratio changes (browser zoom, a window moved to another screen) */
1273
- watchRatio() {
1274
- if (typeof matchMedia !== 'function')
1275
- return;
1276
- const query = matchMedia(`(resolution: ${window.devicePixelRatio || 1}dppx)`);
1277
- const changed = () => {
1278
- // A query for the new ratio
1279
- this.unwatchRatio?.();
1280
- this.watchRatio();
1281
- this.resize();
1282
- };
1283
- query.addEventListener('change', changed, { once: true });
1284
- this.unwatchRatio = () => query.removeEventListener('change', changed);
1285
- }
1286
831
  resize() {
1287
832
  if (this.lost)
1288
833
  return;
@@ -1291,11 +836,10 @@ export class GeoMap {
1291
836
  this.canvas.width = Math.round(width * ratio);
1292
837
  this.canvas.height = Math.round(height * ratio);
1293
838
  this.engine.resize(width, height, ratio);
1294
- this.size = [width, height];
839
+ this.overlays.size = [width, height];
1295
840
  this.emit('resize', { width, height });
1296
841
  // Draw at once: a resized canvas is cleared, waiting one frame would flash
1297
- cancelAnimationFrame(this.frameRequest);
1298
- this.frame(performance.now());
842
+ this.frames.drawNow();
1299
843
  }
1300
844
  destroy() {
1301
845
  if (this.gone)
@@ -1304,45 +848,30 @@ export class GeoMap {
1304
848
  // A context restored later rebuilds nothing
1305
849
  this.unwatchContext();
1306
850
  const camera = this.getCamera();
1307
- cancelAnimationFrame(this.frameRequest);
1308
- this.cancelHoverPick();
1309
- this.vector?.tiles.destroy();
1310
- for (const tiles of [...this.vectorSets])
851
+ this.frames.stop();
852
+ this.hoverPick.cancel();
853
+ this.basemaps.destroy();
854
+ [...this.vectorSets].forEach((tiles) => {
1311
855
  tiles.destroy();
856
+ });
1312
857
  this.vectorSets.clear();
1313
- this.basemapPool?.terminate();
1314
- this.basemapPool = undefined;
1315
- this.unwatchRatio?.();
1316
- this.unwatchRatio = undefined;
858
+ this.workers.terminate();
859
+ this.unwatchRatio();
1317
860
  this.unwatchHeaders();
1318
861
  this.resizeObserver.disconnect();
1319
862
  this.interaction?.destroy();
1320
- this.loader?.destroy();
863
+ this.engines.destroy();
1321
864
  // Custom layers free their WebGL objects, models their data, while the context is there
1322
- for (const layer of this.layers)
1323
- layer.dispose();
865
+ this.stack.dispose();
1324
866
  this.engine.free();
1325
867
  // Work still under way (labels being shaped, tiles arriving, timers) may call the engine after this: it finds one
1326
868
  // that does nothing, and nothing is drawn again
1327
869
  this.#engine = destroyedEngine(camera);
1328
870
  // Glyph ranges still arriving upload to it, not to the freed engine
1329
- this.#glyphs?.attach(this.#engine);
1330
- this.#icons?.attach(this.#engine);
871
+ this.atlases.attach(this.#engine);
1331
872
  this.lost = true;
1332
- for (const control of [...this.controls.keys()])
1333
- this.removeControl(control);
1334
- this.positioned.clear();
1335
- this.canvas.remove();
1336
- this.overlay.remove();
1337
- for (const position of CORNERS)
1338
- this.corners[position].remove();
1339
- this.container.classList.remove('isard-map');
1340
- for (const listeners of Object.values(this.listeners))
1341
- listeners.clear();
873
+ this.overlays.destroy();
874
+ this.listeners.clear();
1342
875
  }
1343
876
  }
1344
- /** The page's request headers to an engine (its own requests: raster tiles, tilesets, point clouds) */
1345
- function sendHeaders(engine, rules) {
1346
- engine.setRequestHeaders(rules.map(([prefix]) => prefix), Uint32Array.from(rules, ([, headers]) => Object.keys(headers).length), rules.flatMap(([, headers]) => Object.entries(headers).flat()));
1347
- }
1348
877
  //# sourceMappingURL=map.js.map