isard 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +2 -2
  3. package/dist/animation.d.ts +18 -0
  4. package/dist/animation.js +84 -0
  5. package/dist/animation.js.map +1 -0
  6. package/dist/basemap.worker.d.ts +16 -1
  7. package/dist/basemap.worker.js +2 -2
  8. package/dist/basemap.worker.js.map +8 -4
  9. package/dist/crs.d.ts +72 -0
  10. package/dist/crs.js +243 -0
  11. package/dist/crs.js.map +1 -0
  12. package/dist/decoder.worker.js +4 -4
  13. package/dist/decoder.worker.js.map +4 -4
  14. package/dist/engine-loader.d.ts +27 -0
  15. package/dist/engine-loader.js +112 -0
  16. package/dist/engine-loader.js.map +1 -0
  17. package/dist/features.d.ts +8 -0
  18. package/dist/features.js +17 -10
  19. package/dist/features.js.map +1 -1
  20. package/dist/geo.d.ts +16 -0
  21. package/dist/geo.js +32 -0
  22. package/dist/geo.js.map +1 -0
  23. package/dist/geoplateforme.d.ts +21 -5
  24. package/dist/geoplateforme.js +47 -12
  25. package/dist/geoplateforme.js.map +1 -1
  26. package/dist/glyphs.d.ts +10 -3
  27. package/dist/glyphs.js +14 -2
  28. package/dist/glyphs.js.map +1 -1
  29. package/dist/gpx.d.ts +11 -0
  30. package/dist/gpx.js +74 -0
  31. package/dist/gpx.js.map +1 -0
  32. package/dist/grid.d.ts +81 -0
  33. package/dist/grid.js +270 -0
  34. package/dist/grid.js.map +1 -0
  35. package/dist/index.d.ts +6 -0
  36. package/dist/index.js +5 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/interaction.d.ts +0 -4
  39. package/dist/interaction.js +8 -4
  40. package/dist/interaction.js.map +1 -1
  41. package/dist/isard_bg.wasm +0 -0
  42. package/dist/isard_core_bg.wasm +0 -0
  43. package/dist/kml.d.ts +12 -0
  44. package/dist/kml.js +159 -0
  45. package/dist/kml.js.map +1 -0
  46. package/dist/layers.d.ts +1 -1
  47. package/dist/layers.js +8 -13
  48. package/dist/layers.js.map +1 -1
  49. package/dist/lru.d.ts +22 -0
  50. package/dist/lru.js +52 -0
  51. package/dist/lru.js.map +1 -0
  52. package/dist/map.d.ts +16 -4
  53. package/dist/map.js +72 -302
  54. package/dist/map.js.map +1 -1
  55. package/dist/resample.d.ts +41 -0
  56. package/dist/resample.js +164 -0
  57. package/dist/resample.js.map +1 -0
  58. package/dist/settings.d.ts +39 -0
  59. package/dist/settings.js +104 -0
  60. package/dist/settings.js.map +1 -0
  61. package/dist/symbols.d.ts +12 -1
  62. package/dist/symbols.js +124 -26
  63. package/dist/symbols.js.map +1 -1
  64. package/dist/terrain.d.ts +19 -7
  65. package/dist/terrain.js +40 -28
  66. package/dist/terrain.js.map +1 -1
  67. package/dist/topojson.d.ts +29 -0
  68. package/dist/topojson.js +66 -0
  69. package/dist/topojson.js.map +1 -0
  70. package/dist/types.d.ts +12 -4
  71. package/dist/types.js.map +1 -1
  72. package/dist/vectortiles.d.ts +10 -3
  73. package/dist/vectortiles.js +64 -41
  74. package/dist/vectortiles.js.map +1 -1
  75. package/dist/wasm/isard.d.ts +9 -3
  76. package/dist/wasm/isard.js +8 -2
  77. package/dist/wasm/isard_core.d.ts +1 -1
  78. package/dist/wasm/isard_core.js +8 -2
  79. package/dist/wmts.d.ts +14 -0
  80. package/dist/wmts.js +76 -0
  81. package/dist/wmts.js.map +1 -0
  82. package/dist/xml.d.ts +26 -0
  83. package/dist/xml.js +109 -0
  84. package/dist/xml.js.map +1 -0
  85. package/package.json +1 -1
  86. package/dist/equirectangular.d.ts +0 -35
  87. package/dist/equirectangular.js +0 -143
  88. package/dist/equirectangular.js.map +0 -1
package/dist/map.js CHANGED
@@ -1,36 +1,24 @@
1
1
  /**
2
- * The map: canvas, camera, layers, events and the frame loop. Frames are drawn on demand only: an idle map costs
3
- * nothing. Hover events are coalesced to one per frame, so the values computed for a mouse position are drawn in the
4
- * same frame.
2
+ * The map: canvas, camera, layers, events, overlays, credits and the frame loop. Frames are drawn on demand only: an
3
+ * idle map costs nothing. Hover events are coalesced to one per frame, so the values computed for a mouse position are
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
6
  */
6
- import { BASEMAPS, BODY_TERRAINS, basemapName, HILLSHADE, IGN_ATTRIBUTION, IMAGE_SERVER, imageServerTerrain, SEA, TERRAIN_TILES, TERRAIN_TILES_ATTRIBUTION, TERRAIN_URL, } from "./basemaps.js";
7
- import { parseCssColor } from "./color.js";
7
+ import { easeTo, flyTo } from "./animation.js";
8
+ import { BASEMAPS, basemapName, HILLSHADE, SEA } from "./basemaps.js";
8
9
  import { AttributionControl } from "./controls.js";
9
- import { DEFAULT_DECODERS } from "./decoders.js";
10
- import { equirectangularLoader } from "./equirectangular.js";
10
+ import { destroyedEngine, engineUrl, loadEngine, loadFullEngineLater, preloadFullEngine, startTileLoader, } from "./engine-loader.js";
11
11
  import { ExtrusionLayer, extrusionStyle } from "./extrusions.js";
12
12
  import { CircleLayer, circleStyle, FillLayer, fillStyle, LineLayer, lineStyle, } from "./features.js";
13
13
  import { Glyphs } from "./glyphs.js";
14
+ import { equirectangularGrid, gridLoader, gridZooms, resolveGrid } from "./grid.js";
14
15
  import { Interaction } from "./interaction.js";
15
16
  import { GroundLayer, NetworkLayer, RasterLayer, Tiles3dLayer } from "./layers.js";
16
- import { TileLoader } from "./loader.js";
17
+ import { atmosphereSettings, bodySettings, haloSettings, rgb, terrainSettings, } from "./settings.js";
17
18
  import { injectStyle } from "./style.js";
18
19
  import { SymbolLayer, symbolStyle } from "./symbols.js";
19
20
  import { encodedTerrainLoader } from "./terrain.js";
20
21
  import { BasemapWorkers, defaultWorkerCount, defaultWorkerUrl, VECTOR_BASEMAPS, VectorTiles, } from "./vectortiles.js";
21
- /** The engine of a destroyed map: every method a no-op returning undefined, but the camera (the last one) and the
22
- * bounds, which late callers read */
23
- function destroyed(camera) {
24
- return new Proxy({}, {
25
- get: (_, name) => {
26
- if (name === 'camera')
27
- return () => camera;
28
- if (name === 'bounds')
29
- return () => Float64Array.of(-180, -85, 180, 85);
30
- return () => undefined;
31
- },
32
- });
33
- }
34
22
  /** Reports an error without throwing it here: the console shows it, as an uncaught one */
35
23
  function report(error) {
36
24
  if (typeof reportError === 'function')
@@ -40,63 +28,6 @@ function report(error) {
40
28
  throw error;
41
29
  });
42
30
  }
43
- /** The two builds of the engine, each compiled once per page: `core` without 3D Tiles (a third smaller), loaded by
44
- * every map; `full` with them, loaded for the first 3D Tiles layer, its module shared with the decoding workers. The
45
- * core engine has the same API but the 3D Tiles methods: its class is typed as the full one. */
46
- const engines = {};
47
- /** The modules compiled (downloaded and compiled off the main thread, not instantiated): `tiles3d: 'preload'`
48
- * compiles the full engine ahead, its first 3D Tiles layer then only instantiates it */
49
- const modules = {};
50
- /** @internal Forgets the engines loaded (tests) */
51
- export function forgetEngines() {
52
- for (const kind of ['core', 'full']) {
53
- delete engines[kind];
54
- delete modules[kind];
55
- }
56
- }
57
- function compileEngine(kind, url) {
58
- const compiled = modules[kind];
59
- if (compiled)
60
- return compiled;
61
- const compiling = compile(url).catch((error) => {
62
- // A later call tries again (a network error, a wrong URL fixed)
63
- delete modules[kind];
64
- throw error;
65
- });
66
- modules[kind] = compiling;
67
- return compiling;
68
- }
69
- function loadEngine(kind, url) {
70
- const loaded = engines[kind];
71
- if (loaded)
72
- return loaded;
73
- const loading = (async () => {
74
- // The bindings by dynamic import: an application bundles the ones it loads only
75
- const [module, bindings] = await Promise.all([
76
- compileEngine(kind, url),
77
- kind === 'core' ? import('./wasm/isard_core.js') : import('./wasm/isard.js'),
78
- ]);
79
- await bindings.default({ module_or_path: module });
80
- return { module, Engine: bindings.Engine };
81
- })().catch((error) => {
82
- delete engines[kind];
83
- throw error;
84
- });
85
- engines[kind] = loading;
86
- return loading;
87
- }
88
- async function compile(url) {
89
- const response = await fetch(url);
90
- if (!response.ok)
91
- throw new Error(`isard: failed to load the engine from ${url}: ${response.status}`);
92
- try {
93
- return await WebAssembly.compileStreaming(response.clone());
94
- }
95
- catch {
96
- // Servers that do not send application/wasm
97
- return WebAssembly.compile(await response.arrayBuffer());
98
- }
99
- }
100
31
  /** Checks a network's arrays before the engine reads them: node index pairs, indices of existing nodes */
101
32
  function validateNetwork(nodes, edges) {
102
33
  if (nodes.length % 2 !== 0)
@@ -110,140 +41,6 @@ function validateNetwork(nodes, edges) {
110
41
  throw new Error(`isard: network edge ${i >> 1} refers to node ${node}, of ${count} nodes`);
111
42
  }
112
43
  }
113
- /** The named bodies; a body's `color` is that of its globe under the tiles (the Earth's follows its basemap), its
114
- * `relief` the range of its ground (the Earth's dry land, from the Dead Sea to Everest). Mars: the sphere of its
115
- * equatorial radius (its data take planetocentric latitudes), a faint butterscotch halo of dust */
116
- const BODIES = {
117
- earth: { radius: 6378137, flattening: 1 / 298.257223563, atmosphere: true, color: '#cacaca', relief: [-450, 8900] },
118
- moon: { radius: 1737400, flattening: 0, atmosphere: false, color: '#77756f', relief: [-9200, 10800] },
119
- mars: {
120
- radius: 3396190,
121
- flattening: 0,
122
- atmosphere: 'rgba(232, 168, 120, 0.6)',
123
- color: '#b5714a',
124
- relief: [-8300, 21300],
125
- },
126
- };
127
- /** The Earth's halo, RGB 0-1 and its strength: what `atmosphere: true` draws */
128
- const EARTH_HALO = Float32Array.of(0.42, 0.66, 1, 1);
129
- /** A body from the option, its defaults filled in */
130
- function bodySettings(body) {
131
- if (body === undefined || typeof body === 'string')
132
- return BODIES[body ?? 'earth'];
133
- const relief = [-0.01 * body.radius, 0.01 * body.radius];
134
- return { flattening: 0, atmosphere: false, color: '#808080', relief, ...body };
135
- }
136
- /** The halo of an atmosphere for the engine (RGB 0-1 and its strength, the color's alpha): undefined for none */
137
- function haloSettings(atmosphere) {
138
- if (atmosphere === true)
139
- return EARTH_HALO;
140
- const color = atmosphere === false ? undefined : parseCssColor(atmosphere);
141
- return color && Float32Array.from(color);
142
- }
143
- /** The loaders of the relief by encoding, URL and floor: the same loader for the same tiles (the engine keeps its
144
- * tiles when only the exaggeration changes) */
145
- const terrainLoaders = new Map();
146
- function terrainLoader(url, encoding, floor) {
147
- const key = `${encoding} ${floor ?? ''} ${url}`;
148
- let loader = terrainLoaders.get(key);
149
- if (!loader) {
150
- loader = encodedTerrainLoader(url, encoding, floor);
151
- terrainLoaders.set(key, loader);
152
- }
153
- return loader;
154
- }
155
- /** Relief settings from the option: undefined when off. On the Earth the Terrain Tiles by default (worldwide,
156
- * Terrarium PNGs decoded in the page) or the IGN terrain model (France, raised by the geoid); NASA's on the Moon and
157
- * Mars; an ArcGIS image service asked for in the body's Mercator */
158
- function terrainSettings(terrain, body, radius) {
159
- if (!terrain)
160
- return undefined;
161
- let options = {};
162
- if (typeof terrain === 'string')
163
- options = { model: terrain };
164
- else if (terrain !== true)
165
- options = terrain;
166
- const earth = (body ?? 'earth') === 'earth';
167
- const exaggeration = options.exaggeration ?? 1;
168
- if (options.url === undefined && earth) {
169
- if (options.model === 'ign') {
170
- const geoid = options.geoid ?? true;
171
- return { url: TERRAIN_URL, exaggeration, maxZoom: options.maxZoom ?? 15, geoid, attribution: IGN_ATTRIBUTION };
172
- }
173
- return {
174
- url: TERRAIN_TILES.url,
175
- exaggeration,
176
- maxZoom: options.maxZoom ?? TERRAIN_TILES.maxZoom,
177
- geoid: options.geoid ?? false,
178
- // The sea floor of the model at sea level: the oceans stay flat under the basemap's water
179
- loader: terrainLoader(TERRAIN_TILES.url, 'terrarium', 0),
180
- attribution: TERRAIN_TILES_ATTRIBUTION,
181
- };
182
- }
183
- const preset = body === 'moon' || body === 'mars' ? BODY_TERRAINS[body] : undefined;
184
- const url = options.url ?? preset?.url;
185
- if (url === undefined)
186
- throw new Error('isard: the relief of this body needs a url');
187
- const encoding = options.encoding ?? 'float32';
188
- // PNG encodings are fetched tile by tile: a {bbox} or an image service would be fetched as it is
189
- if (encoding !== 'float32' &&
190
- (IMAGE_SERVER.test(url) || !/\{z\}/.test(url) || !/\{x\}/.test(url) || !/\{y\}/.test(url))) {
191
- throw new Error(`isard: the '${encoding}' terrain encoding takes a tile URL with {z}, {x} and {y} (${url})`);
192
- }
193
- return {
194
- url: IMAGE_SERVER.test(url) ? imageServerTerrain(url, earth, radius) : url,
195
- exaggeration,
196
- maxZoom: options.maxZoom ?? (options.url === undefined && preset ? preset.maxZoom : 15),
197
- geoid: options.geoid ?? false,
198
- ...(encoding === 'float32' ? {} : { loader: terrainLoader(url, encoding) }),
199
- // NASA's credits for its models
200
- ...(options.url === undefined && preset ? { attribution: preset.attribution } : {}),
201
- };
202
- }
203
- /** Sky settings for the engine: colors (RGB 0-1) and blends, MapLibre's defaults */
204
- function atmosphereSettings(options) {
205
- if (!options)
206
- return undefined;
207
- return Float32Array.from([
208
- ...rgb(options.skyColor ?? '#9cc7ec'),
209
- ...rgb(options.horizonColor ?? '#e8f1f8'),
210
- ...rgb(options.fogColor ?? '#f2f5f8'),
211
- options.skyHorizonBlend ?? 0.6,
212
- options.horizonFogBlend ?? 0.7,
213
- options.fogGroundBlend ?? 0.4,
214
- ]);
215
- }
216
- /** MapLibre's default easing: cubic Bézier (0.25, 0.1, 0.25, 1) */
217
- export function defaultEasing(t) {
218
- if (t <= 0 || t >= 1)
219
- return Math.max(0, Math.min(1, t));
220
- // Solve x(u) = t for the curve parameter, Newton then bisection
221
- const [x1, y1, x2, y2] = [0.25, 0.1, 0.25, 1];
222
- const bezier = (u, a, b) => 3 * a * u * (1 - u) ** 2 + 3 * b * u * u * (1 - u) + u ** 3;
223
- let u = t;
224
- for (let i = 0; i < 8; i += 1) {
225
- const x = bezier(u, x1, x2) - t;
226
- const dx = 3 * x1 * (1 - u) ** 2 + 6 * (x2 - x1) * u * (1 - u) + 3 * (1 - x2) * u * u;
227
- if (Math.abs(x) < 1e-6 || dx === 0)
228
- break;
229
- u = Math.max(0, Math.min(1, u - x / dx));
230
- }
231
- return bezier(u, y1, y2);
232
- }
233
- const MERCATOR_LAT = 85.051128779806589;
234
- const toMercator = ([lon, lat]) => {
235
- const phi = (Math.max(-MERCATOR_LAT, Math.min(MERCATOR_LAT, lat)) * Math.PI) / 180;
236
- return [(lon + 180) / 360, 0.5 - Math.log(Math.tan(Math.PI / 4 + phi / 2)) / (2 * Math.PI)];
237
- };
238
- const fromMercator = ([x, y]) => [
239
- x * 360 - 180,
240
- (Math.atan(Math.sinh(Math.PI * (1 - 2 * y))) * 180) / Math.PI,
241
- ];
242
- /** A CSS color to RGB 0-1 (its alpha dropped); black if unknown */
243
- function rgb(color) {
244
- const [r, g, b] = parseCssColor(color) ?? [0, 0, 0, 1];
245
- return [r, g, b];
246
- }
247
44
  const CORNERS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
248
45
  /** Frames the hover picking waits for its answer at most (usually one) */
249
46
  const MAX_POLLS = 30;
@@ -265,6 +62,8 @@ export class GeoMap {
265
62
  vector;
266
63
  glyphsUrl;
267
64
  basemapWorkerUrl;
65
+ /** Texture size of the vector basemap's tiles */
66
+ basemapTileSize;
268
67
  container;
269
68
  /** Markers and popups, over the canvas */
270
69
  overlay;
@@ -336,6 +135,9 @@ export class GeoMap {
336
135
  unwatchRatio;
337
136
  /** Stops listening to the canvas's context loss and restoration */
338
137
  unwatchContext;
138
+ /** The loaders of the relief by encoding, URL and floor: the same loader for the same tiles (the engine keeps its
139
+ * tiles when only the exaggeration changes) */
140
+ terrainLoaders = new Map();
339
141
  /** Loads the engine (once per page) and creates a map filling `container` */
340
142
  static async create(container, options = {}) {
341
143
  // With 3D Tiles from the start when asked, or when only that engine's URL is given (as before the core engine)
@@ -343,9 +145,8 @@ export class GeoMap {
343
145
  // Before anything is created
344
146
  if (options.basemap !== undefined)
345
147
  checkBasemap(options.basemap);
346
- const bindings = full
347
- ? await loadEngine('full', options.wasmUrl ?? new URL('./isard_bg.wasm', import.meta.url))
348
- : await loadEngine('core', options.coreWasmUrl ?? new URL('./isard_core_bg.wasm', import.meta.url));
148
+ const kind = full ? 'full' : 'core';
149
+ const bindings = await loadEngine(kind, engineUrl(kind, options));
349
150
  const map = new GeoMap(container, options, bindings.Engine, full ? bindings.module : undefined);
350
151
  if (options.tiles3d === 'preload' && !full)
351
152
  map.preloadTiles3d();
@@ -357,6 +158,7 @@ export class GeoMap {
357
158
  this.tiles3dOptions = { ...options };
358
159
  this.glyphsUrl = options.glyphs;
359
160
  this.basemapWorkerUrl = options.basemapWorkerUrl;
161
+ this.basemapTileSize = options.basemapTileSize;
360
162
  this.container = container;
361
163
  this.canvas = document.createElement('canvas');
362
164
  Object.assign(this.canvas.style, { display: 'block', width: '100%', height: '100%', touchAction: 'none' });
@@ -389,7 +191,7 @@ export class GeoMap {
389
191
  globe: (options.projection ?? 'globe') === 'globe',
390
192
  sky: options.sky ?? true,
391
193
  atmosphere: atmosphereSettings(options.atmosphere),
392
- terrain: terrainSettings(options.terrain, options.body, body.radius),
194
+ terrain: terrainSettings(options.terrain, options.body, body.radius, this.terrainLoader),
393
195
  loading: options.loading ?? 'immediate',
394
196
  background: rgb(options.background ?? (this.earth ? '#f2f2f0' : '#000000')),
395
197
  globeColor: this.earth ? SEA['osm-positron'] : rgb(body.color),
@@ -466,20 +268,12 @@ export class GeoMap {
466
268
  }
467
269
  /** The decoding workers of 3D Tiles, with the engine that has them (they run its module) and unless turned off */
468
270
  startLoader(engine) {
469
- const { workers: count, workerUrl, decoders } = this.tiles3dOptions;
470
- const workers = count ?? Math.min(4, Math.max(1, (navigator.hardwareConcurrency || 2) - 1));
471
- if (!this.tiles3dModule || workers === false || workers <= 0 || typeof Worker === 'undefined')
472
- return undefined;
473
- return new TileLoader(this.tiles3dModule, workerUrl ?? new URL('./decoder.worker.js', import.meta.url), workers, { ...DEFAULT_DECODERS, ...decoders }, engine.textureFormats());
271
+ return this.tiles3dModule && startTileLoader(this.tiles3dModule, this.tiles3dOptions, engine);
474
272
  }
475
273
  /** A 3D Tiles layer on the core engine: loads the engine with 3D Tiles, then rebuilds the map on it (camera,
476
274
  * settings and layers kept, as after a context loss); the layer is created then */
477
275
  upgrade() {
478
- const url = this.tiles3dOptions.wasmUrl ?? new URL('./isard_bg.wasm', import.meta.url);
479
- this.upgrading ??= loadEngine('full', url)
480
- // The map is rebuilt in a task of its own, not in the one that instantiated the engine: two shorter tasks
481
- .then((bindings) => new Promise((resolve) => setTimeout(() => resolve(bindings), 0)))
482
- .then(({ module, Engine }) => {
276
+ this.upgrading ??= loadFullEngineLater(engineUrl('full', this.tiles3dOptions)).then(({ module, Engine }) => {
483
277
  this.upgrading = undefined;
484
278
  if (this.gone)
485
279
  return;
@@ -500,17 +294,8 @@ export class GeoMap {
500
294
  /** Downloads and compiles the engine with 3D Tiles once the map is idle (`tiles3d: 'preload'`), without running
501
295
  * it: the first 3D Tiles layer then only instantiates it */
502
296
  preloadTiles3d() {
503
- const url = this.tiles3dOptions.wasmUrl ?? new URL('./isard_bg.wasm', import.meta.url);
504
- const start = () => {
505
- if (!this.gone && !this.tiles3dModule)
506
- compileEngine('full', url).catch(() => undefined);
507
- };
508
- const later = () => {
509
- if (typeof requestIdleCallback === 'function')
510
- requestIdleCallback(start, { timeout: 2000 });
511
- else
512
- setTimeout(start, 0);
513
- };
297
+ const url = engineUrl('full', this.tiles3dOptions);
298
+ const later = () => preloadFullEngine(url, () => !this.gone && !this.tiles3dModule);
514
299
  if (this.settled)
515
300
  later();
516
301
  else {
@@ -657,21 +442,9 @@ export class GeoMap {
657
442
  }
658
443
  /** Animated move to a camera: over `duration` milliseconds (or the options), ease-out by default */
659
444
  easeTo(camera, options = 600) {
660
- const { duration = 600, easing = (t) => 1 - (1 - t) ** 3 } = typeof options === 'number' ? { duration: options } : options;
661
445
  const from = this.getCamera();
662
446
  const to = { ...from, ...camera };
663
- // Shortest way round
664
- const turn = ((to.bearing - from.bearing + 540) % 360) - 180;
665
- const start = performance.now();
666
- this.animate((now) => {
667
- if (now - start >= duration)
668
- this.engine.clearPreload();
669
- const t = duration > 0 ? Math.min(1, (now - start) / duration) : 1;
670
- const k = easing(t);
671
- const mix = (a, b) => a + (b - a) * k;
672
- this.engine.setCamera(mix(from.center[0], to.center[0]), mix(from.center[1], to.center[1]), mix(from.zoom, to.zoom), mix(from.pitch, to.pitch), from.bearing + turn * k);
673
- return t < 1;
674
- });
447
+ this.animate(easeTo(() => this.engine, from, to, options));
675
448
  // The destination's tiles load during the flight (as Cesium does)
676
449
  this.engine.preload(to.center[0], to.center[1], to.zoom, to.pitch, to.bearing);
677
450
  }
@@ -682,43 +455,8 @@ export class GeoMap {
682
455
  flyTo(camera, options = {}) {
683
456
  const from = this.getCamera();
684
457
  const to = { ...from, ...camera };
685
- const { curve: rho = 1.42, speed = 1.2, easing = defaultEasing } = options;
686
- const [x0, y0] = toMercator(from.center);
687
- const [x1, y1] = toMercator(to.center);
688
- // Across the antimeridian, the short way
689
- const dx = x1 - x0 - Math.round(x1 - x0);
690
- const dy = y1 - y0;
691
- // In pixels at the start zoom (512 px world at zoom 0)
692
- const world = 512 * 2 ** from.zoom;
693
- const u1 = Math.hypot(dx, dy) * world;
694
- const w0 = Math.max(this.canvas.clientWidth, this.canvas.clientHeight, 1);
695
- const w1 = w0 / 2 ** (to.zoom - from.zoom);
696
- const rho2 = rho * rho;
697
- const r = (i) => {
698
- const b = (w1 * w1 - w0 * w0 + (i ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (i ? w1 : w0) * rho2 * u1);
699
- return Math.log(Math.sqrt(b * b + 1) - b);
700
- };
701
- // No move on the ground (or nearly): a plain zoom
702
- const still = u1 < 1e-6 || !Number.isFinite(r(0)) || !Number.isFinite(r(1));
703
- const r0 = still ? 0 : r(0);
704
- const length = still ? Math.abs(Math.log(w1 / w0)) / rho : (r(1) - r0) / rho;
705
- const width = (s) => still ? Math.exp((w1 < w0 ? -1 : 1) * rho * s) : Math.cosh(r0) / Math.cosh(r0 + rho * s);
706
- const along = (s) => still ? 0 : (w0 * ((Math.cosh(r0) * Math.tanh(r0 + rho * s) - Math.sinh(r0)) / rho2)) / u1;
707
- const duration = options.duration ?? (1000 * length) / speed;
708
- const turn = ((to.bearing - from.bearing + 540) % 360) - 180;
709
- const start = performance.now();
710
- this.animate((now) => {
711
- if (now - start >= duration)
712
- this.engine.clearPreload();
713
- const t = duration > 0 ? Math.min(1, (now - start) / duration) : 1;
714
- const k = easing(t);
715
- const s = k * length;
716
- const u = t >= 1 ? 1 : along(s);
717
- const zoom = t >= 1 ? to.zoom : from.zoom + Math.log2(1 / width(s));
718
- const [lon, lat] = fromMercator([x0 + dx * u, y0 + dy * u]);
719
- this.engine.setCamera(lon, lat, zoom, from.pitch + (to.pitch - from.pitch) * k, from.bearing + turn * k);
720
- return t < 1;
721
- });
458
+ const viewport = Math.max(this.canvas.clientWidth, this.canvas.clientHeight);
459
+ this.animate(flyTo(() => this.engine, from, to, viewport, options));
722
460
  this.engine.preload(to.center[0], to.center[1], to.zoom, to.pitch, to.bearing);
723
461
  }
724
462
  /** Longitudes and latitudes the view covers on the ground: `[west, south, east, north]` */
@@ -770,7 +508,7 @@ export class GeoMap {
770
508
  * on the Moon and Mars), one of the Earth's models by name (`'ign'`), its options, or false; on another body, its
771
509
  * options with a `url` */
772
510
  setTerrain(terrain) {
773
- this.settings.terrain = terrainSettings(terrain, this.body, this.settings.body.radius);
511
+ this.settings.terrain = terrainSettings(terrain, this.body, this.settings.body.radius, this.terrainLoader);
774
512
  const t = this.settings.terrain;
775
513
  this.engine.setTerrain(t?.url, t?.exaggeration ?? 1, t?.maxZoom, t?.geoid, t?.loader);
776
514
  this.attributionChanged();
@@ -931,7 +669,7 @@ export class GeoMap {
931
669
  style: vector,
932
670
  ...(this.basemapWorkerUrl ? { workerUrl: this.basemapWorkerUrl } : {}),
933
671
  });
934
- this.basemap = tiles.raster();
672
+ this.basemap = tiles.raster(this.basemapTileSize ? { size: this.basemapTileSize } : {});
935
673
  this.vector = { tiles, labels: tiles.labels() };
936
674
  }
937
675
  else {
@@ -948,20 +686,28 @@ export class GeoMap {
948
686
  this.toBottom(this.basemap);
949
687
  this.attributionChanged();
950
688
  }
951
- /** Raster tiles ({z}, {x}, {y} URL; Web Mercator, or reprojected from an equirectangular `grid`), over the layers
952
- * already added */
689
+ /** Raster tiles ({z}, {x}, {y} URL; Web Mercator, or reprojected from another `grid`: longitudes and latitudes,
690
+ * Lambert-93, UTM…), over the layers already added */
953
691
  addRaster(options) {
954
- const { url, grid = 'mercator', minZoom = 0, maxZoom = 19, opacity = 1, saturation = 1, size, before } = options;
955
- let layer;
956
- if (grid === 'equirectangular' && !options.loader) {
957
- // The source's level n has the pixel size of Mercator level n + 1 at the equator
958
- const loader = equirectangularLoader(url, maxZoom, size ?? 256, minZoom);
959
- const levels = { minZoom: minZoom + 1, maxZoom: maxZoom + 1 };
960
- layer = new RasterLayer(this, { url, ...levels, opacity, saturation, size, loader });
961
- }
962
- else {
963
- layer = new RasterLayer(this, { url, minZoom, maxZoom, opacity, saturation, size, loader: options.loader });
692
+ const { url, grid = 'mercator', opacity = 1, saturation = 1, size, before } = options;
693
+ let { minZoom = 0, maxZoom = 19 } = options;
694
+ let loader = options.loader;
695
+ if (grid !== 'mercator' && !loader) {
696
+ if (grid === 'equirectangular') {
697
+ // `minZoom` and `maxZoom` are levels of the source: its level n has the pixel size of Mercator level n + 1 at
698
+ // the equator
699
+ loader = gridLoader(url, resolveGrid(equirectangularGrid(minZoom, maxZoom)), size ?? 256, this.remoteReprojection);
700
+ [minZoom, maxZoom] = [minZoom + 1, maxZoom + 1];
701
+ }
702
+ else {
703
+ const resolved = resolveGrid(grid);
704
+ const zooms = gridZooms(resolved);
705
+ minZoom = options.minZoom ?? zooms.minZoom;
706
+ maxZoom = options.maxZoom ?? zooms.maxZoom;
707
+ loader = gridLoader(url, resolved, size ?? 256, this.remoteReprojection);
708
+ }
964
709
  }
710
+ const layer = new RasterLayer(this, { url, minZoom, maxZoom, opacity, saturation, size, loader });
965
711
  this.place(layer, before);
966
712
  if (options.attribution) {
967
713
  this.rasterCredits.set(layer, options.attribution);
@@ -1045,6 +791,30 @@ export class GeoMap {
1045
791
  }
1046
792
  return { pool: new BasemapWorkers(workers, workerUrl), own: true };
1047
793
  }
794
+ /**
795
+ * The workers decoding the relief and reprojecting rasters: the map's pool, unless the map has no workers
796
+ * (`workers: false` or 0, or none in this browser): that is done in the page then
797
+ */
798
+ decodingPool() {
799
+ const { workers } = this.tiles3dOptions;
800
+ if (this.gone || workers === false || (workers !== undefined && workers <= 0))
801
+ return undefined;
802
+ if (typeof Worker === 'undefined' || typeof OffscreenCanvas === 'undefined')
803
+ return undefined;
804
+ return this.basemapWorkers().pool;
805
+ }
806
+ remoteHeights = (url, encoding, floor, tile, signal) => this.decodingPool()?.heights(url, encoding, floor, tile, signal, this);
807
+ remoteReprojection = (job, tile, signal) => this.decodingPool()?.reproject(job, tile, signal, this);
808
+ /** The loader of the relief tiles of `url` in a PNG encoding (one per URL, encoding and floor) */
809
+ terrainLoader = (url, encoding, floor) => {
810
+ const key = `${encoding} ${floor ?? ''} ${url}`;
811
+ let loader = this.terrainLoaders.get(key);
812
+ if (!loader) {
813
+ loader = encodedTerrainLoader(url, encoding, floor, this.remoteHeights);
814
+ this.terrainLoaders.set(key, loader);
815
+ }
816
+ return loader;
817
+ };
1048
818
  /** Adds a layer, under `before` if given */
1049
819
  place(layer, before) {
1050
820
  this.add(layer);
@@ -1203,7 +973,7 @@ export class GeoMap {
1203
973
  this.engine.free();
1204
974
  // Work still under way (labels being shaped, tiles arriving, timers) may call the engine after this: it finds one
1205
975
  // that does nothing, and nothing is drawn again
1206
- this.#engine = destroyed(camera);
976
+ this.#engine = destroyedEngine(camera);
1207
977
  // Glyph ranges still arriving upload to it, not to the freed engine
1208
978
  this.#glyphs?.attach(this.#engine);
1209
979
  this.lost = true;