isard 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +134 -14
- package/README.md +2 -2
- package/dist/animation.d.ts +18 -0
- package/dist/animation.js +84 -0
- package/dist/animation.js.map +1 -0
- package/dist/basemap.worker.d.ts +26 -1
- package/dist/basemap.worker.js +2 -2
- package/dist/basemap.worker.js.map +8 -4
- package/dist/basemaps.d.ts +5 -3
- package/dist/basemaps.js +5 -3
- package/dist/basemaps.js.map +1 -1
- package/dist/crs.d.ts +72 -0
- package/dist/crs.js +243 -0
- package/dist/crs.js.map +1 -0
- package/dist/decoder.worker.js +4 -4
- package/dist/decoder.worker.js.map +4 -4
- package/dist/engine-loader.d.ts +27 -0
- package/dist/engine-loader.js +112 -0
- package/dist/engine-loader.js.map +1 -0
- package/dist/extrusions.js +11 -2
- package/dist/extrusions.js.map +1 -1
- package/dist/features.d.ts +8 -0
- package/dist/features.js +17 -10
- package/dist/features.js.map +1 -1
- package/dist/geo.d.ts +16 -0
- package/dist/geo.js +32 -0
- package/dist/geo.js.map +1 -0
- package/dist/geoplateforme.d.ts +21 -5
- package/dist/geoplateforme.js +47 -12
- package/dist/geoplateforme.js.map +1 -1
- package/dist/glyphs.d.ts +10 -3
- package/dist/glyphs.js +14 -2
- package/dist/glyphs.js.map +1 -1
- package/dist/gpx.d.ts +11 -0
- package/dist/gpx.js +74 -0
- package/dist/gpx.js.map +1 -0
- package/dist/grid.d.ts +81 -0
- package/dist/grid.js +270 -0
- package/dist/grid.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +3 -4
- package/dist/interaction.js +27 -7
- package/dist/interaction.js.map +1 -1
- package/dist/isard_bg.wasm +0 -0
- package/dist/isard_core_bg.wasm +0 -0
- package/dist/kml.d.ts +12 -0
- package/dist/kml.js +159 -0
- package/dist/kml.js.map +1 -0
- package/dist/layers.d.ts +5 -2
- package/dist/layers.js +22 -18
- package/dist/layers.js.map +1 -1
- package/dist/lru.d.ts +22 -0
- package/dist/lru.js +52 -0
- package/dist/lru.js.map +1 -0
- package/dist/map.d.ts +26 -6
- package/dist/map.js +196 -287
- package/dist/map.js.map +1 -1
- package/dist/resample.d.ts +41 -0
- package/dist/resample.js +164 -0
- package/dist/resample.js.map +1 -0
- package/dist/settings.d.ts +39 -0
- package/dist/settings.js +104 -0
- package/dist/settings.js.map +1 -0
- package/dist/symbols.d.ts +12 -1
- package/dist/symbols.js +124 -26
- package/dist/symbols.js.map +1 -1
- package/dist/terrain.d.ts +19 -7
- package/dist/terrain.js +50 -26
- package/dist/terrain.js.map +1 -1
- package/dist/topojson.d.ts +29 -0
- package/dist/topojson.js +66 -0
- package/dist/topojson.js.map +1 -0
- package/dist/types.d.ts +23 -7
- package/dist/types.js.map +1 -1
- package/dist/vectortiles.d.ts +13 -3
- package/dist/vectortiles.js +85 -46
- package/dist/vectortiles.js.map +1 -1
- package/dist/wasm/isard.d.ts +14 -7
- package/dist/wasm/isard.js +33 -10
- package/dist/wasm/isard_core.d.ts +6 -5
- package/dist/wasm/isard_core.js +41 -10
- package/dist/wmts.d.ts +14 -0
- package/dist/wmts.js +76 -0
- package/dist/wmts.js.map +1 -0
- package/dist/xml.d.ts +26 -0
- package/dist/xml.js +109 -0
- package/dist/xml.js.map +1 -0
- package/package.json +1 -1
- package/dist/equirectangular.d.ts +0 -35
- package/dist/equirectangular.js +0 -113
- 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
|
|
3
|
-
* nothing. Hover events are coalesced to one per frame, so the values computed for a mouse position are
|
|
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 {
|
|
7
|
-
import {
|
|
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 {
|
|
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 {
|
|
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,42 +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
|
-
function loadEngine(kind, url) {
|
|
48
|
-
const loaded = engines[kind];
|
|
49
|
-
if (loaded)
|
|
50
|
-
return loaded;
|
|
51
|
-
const loading = (async () => {
|
|
52
|
-
// The bindings by dynamic import: an application bundles the ones it loads only
|
|
53
|
-
const [module, bindings] = await Promise.all([
|
|
54
|
-
compile(url),
|
|
55
|
-
kind === 'core' ? import('./wasm/isard_core.js') : import('./wasm/isard.js'),
|
|
56
|
-
]);
|
|
57
|
-
await bindings.default({ module_or_path: module });
|
|
58
|
-
return { module, Engine: bindings.Engine };
|
|
59
|
-
})().catch((error) => {
|
|
60
|
-
// A later call tries again (a network error, a wrong URL fixed)
|
|
61
|
-
delete engines[kind];
|
|
62
|
-
throw error;
|
|
63
|
-
});
|
|
64
|
-
engines[kind] = loading;
|
|
65
|
-
return loading;
|
|
66
|
-
}
|
|
67
|
-
async function compile(url) {
|
|
68
|
-
const response = await fetch(url);
|
|
69
|
-
if (!response.ok)
|
|
70
|
-
throw new Error(`isard: failed to load the engine from ${url}: ${response.status}`);
|
|
71
|
-
try {
|
|
72
|
-
return await WebAssembly.compileStreaming(response.clone());
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
// Servers that do not send application/wasm
|
|
76
|
-
return WebAssembly.compile(await response.arrayBuffer());
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
31
|
/** Checks a network's arrays before the engine reads them: node index pairs, indices of existing nodes */
|
|
80
32
|
function validateNetwork(nodes, edges) {
|
|
81
33
|
if (nodes.length % 2 !== 0)
|
|
@@ -89,134 +41,17 @@ function validateNetwork(nodes, edges) {
|
|
|
89
41
|
throw new Error(`isard: network edge ${i >> 1} refers to node ${node}, of ${count} nodes`);
|
|
90
42
|
}
|
|
91
43
|
}
|
|
92
|
-
/** The named bodies; a body's `color` is that of its globe under the tiles (the Earth's follows its basemap), its
|
|
93
|
-
* `relief` the range of its ground (the Earth's dry land, from the Dead Sea to Everest). Mars: the sphere of its
|
|
94
|
-
* equatorial radius (its data take planetocentric latitudes), a faint butterscotch halo of dust */
|
|
95
|
-
const BODIES = {
|
|
96
|
-
earth: { radius: 6378137, flattening: 1 / 298.257223563, atmosphere: true, color: '#cacaca', relief: [-450, 8900] },
|
|
97
|
-
moon: { radius: 1737400, flattening: 0, atmosphere: false, color: '#77756f', relief: [-9200, 10800] },
|
|
98
|
-
mars: {
|
|
99
|
-
radius: 3396190,
|
|
100
|
-
flattening: 0,
|
|
101
|
-
atmosphere: 'rgba(232, 168, 120, 0.6)',
|
|
102
|
-
color: '#b5714a',
|
|
103
|
-
relief: [-8300, 21300],
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
/** The Earth's halo, RGB 0-1 and its strength: what `atmosphere: true` draws */
|
|
107
|
-
const EARTH_HALO = Float32Array.of(0.42, 0.66, 1, 1);
|
|
108
|
-
/** A body from the option, its defaults filled in */
|
|
109
|
-
function bodySettings(body) {
|
|
110
|
-
if (body === undefined || typeof body === 'string')
|
|
111
|
-
return BODIES[body ?? 'earth'];
|
|
112
|
-
const relief = [-0.01 * body.radius, 0.01 * body.radius];
|
|
113
|
-
return { flattening: 0, atmosphere: false, color: '#808080', relief, ...body };
|
|
114
|
-
}
|
|
115
|
-
/** The halo of an atmosphere for the engine (RGB 0-1 and its strength, the color's alpha): undefined for none */
|
|
116
|
-
function haloSettings(atmosphere) {
|
|
117
|
-
if (atmosphere === true)
|
|
118
|
-
return EARTH_HALO;
|
|
119
|
-
const color = atmosphere === false ? undefined : parseCssColor(atmosphere);
|
|
120
|
-
return color && Float32Array.from(color);
|
|
121
|
-
}
|
|
122
|
-
/** The loaders of the relief by encoding, URL and floor: the same loader for the same tiles (the engine keeps its
|
|
123
|
-
* tiles when only the exaggeration changes) */
|
|
124
|
-
const terrainLoaders = new Map();
|
|
125
|
-
function terrainLoader(url, encoding, floor) {
|
|
126
|
-
const key = `${encoding} ${floor ?? ''} ${url}`;
|
|
127
|
-
let loader = terrainLoaders.get(key);
|
|
128
|
-
if (!loader) {
|
|
129
|
-
loader = encodedTerrainLoader(url, encoding, floor);
|
|
130
|
-
terrainLoaders.set(key, loader);
|
|
131
|
-
}
|
|
132
|
-
return loader;
|
|
133
|
-
}
|
|
134
|
-
/** Relief settings from the option: undefined when off. On the Earth the Terrain Tiles by default (worldwide,
|
|
135
|
-
* Terrarium PNGs decoded in the page) or the IGN terrain model (France, raised by the geoid); NASA's on the Moon and
|
|
136
|
-
* Mars; an ArcGIS image service asked for in the body's Mercator */
|
|
137
|
-
function terrainSettings(terrain, body, radius) {
|
|
138
|
-
if (!terrain)
|
|
139
|
-
return undefined;
|
|
140
|
-
let options = {};
|
|
141
|
-
if (typeof terrain === 'string')
|
|
142
|
-
options = { model: terrain };
|
|
143
|
-
else if (terrain !== true)
|
|
144
|
-
options = terrain;
|
|
145
|
-
const earth = (body ?? 'earth') === 'earth';
|
|
146
|
-
const exaggeration = options.exaggeration ?? 1;
|
|
147
|
-
if (options.url === undefined && earth) {
|
|
148
|
-
if (options.model === 'ign') {
|
|
149
|
-
const geoid = options.geoid ?? true;
|
|
150
|
-
return { url: TERRAIN_URL, exaggeration, maxZoom: options.maxZoom ?? 15, geoid, attribution: IGN_ATTRIBUTION };
|
|
151
|
-
}
|
|
152
|
-
return {
|
|
153
|
-
url: TERRAIN_TILES.url,
|
|
154
|
-
exaggeration,
|
|
155
|
-
maxZoom: options.maxZoom ?? TERRAIN_TILES.maxZoom,
|
|
156
|
-
geoid: options.geoid ?? false,
|
|
157
|
-
// The sea floor of the model at sea level: the oceans stay flat under the basemap's water
|
|
158
|
-
loader: terrainLoader(TERRAIN_TILES.url, 'terrarium', 0),
|
|
159
|
-
attribution: TERRAIN_TILES_ATTRIBUTION,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
const preset = body === 'moon' || body === 'mars' ? BODY_TERRAINS[body] : undefined;
|
|
163
|
-
const url = options.url ?? preset?.url;
|
|
164
|
-
if (url === undefined)
|
|
165
|
-
throw new Error('isard: the relief of this body needs a url');
|
|
166
|
-
const encoding = options.encoding ?? 'float32';
|
|
167
|
-
return {
|
|
168
|
-
url: IMAGE_SERVER.test(url) ? imageServerTerrain(url, earth, radius) : url,
|
|
169
|
-
exaggeration,
|
|
170
|
-
maxZoom: options.maxZoom ?? (options.url === undefined && preset ? preset.maxZoom : 15),
|
|
171
|
-
geoid: options.geoid ?? false,
|
|
172
|
-
...(encoding === 'float32' ? {} : { loader: terrainLoader(url, encoding) }),
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
/** Sky settings for the engine: colors (RGB 0-1) and blends, MapLibre's defaults */
|
|
176
|
-
function atmosphereSettings(options) {
|
|
177
|
-
if (!options)
|
|
178
|
-
return undefined;
|
|
179
|
-
return Float32Array.from([
|
|
180
|
-
...rgb(options.skyColor ?? '#9cc7ec'),
|
|
181
|
-
...rgb(options.horizonColor ?? '#e8f1f8'),
|
|
182
|
-
...rgb(options.fogColor ?? '#f2f5f8'),
|
|
183
|
-
options.skyHorizonBlend ?? 0.6,
|
|
184
|
-
options.horizonFogBlend ?? 0.7,
|
|
185
|
-
options.fogGroundBlend ?? 0.4,
|
|
186
|
-
]);
|
|
187
|
-
}
|
|
188
|
-
/** MapLibre's default easing: cubic Bézier (0.25, 0.1, 0.25, 1) */
|
|
189
|
-
export function defaultEasing(t) {
|
|
190
|
-
if (t <= 0 || t >= 1)
|
|
191
|
-
return Math.max(0, Math.min(1, t));
|
|
192
|
-
// Solve x(u) = t for the curve parameter, Newton then bisection
|
|
193
|
-
const [x1, y1, x2, y2] = [0.25, 0.1, 0.25, 1];
|
|
194
|
-
const bezier = (u, a, b) => 3 * a * u * (1 - u) ** 2 + 3 * b * u * u * (1 - u) + u ** 3;
|
|
195
|
-
let u = t;
|
|
196
|
-
for (let i = 0; i < 8; i += 1) {
|
|
197
|
-
const x = bezier(u, x1, x2) - t;
|
|
198
|
-
const dx = 3 * x1 * (1 - u) ** 2 + 6 * (x2 - x1) * u * (1 - u) + 3 * (1 - x2) * u * u;
|
|
199
|
-
if (Math.abs(x) < 1e-6 || dx === 0)
|
|
200
|
-
break;
|
|
201
|
-
u = Math.max(0, Math.min(1, u - x / dx));
|
|
202
|
-
}
|
|
203
|
-
return bezier(u, y1, y2);
|
|
204
|
-
}
|
|
205
|
-
const MERCATOR_LAT = 85.051128779806589;
|
|
206
|
-
const toMercator = ([lon, lat]) => {
|
|
207
|
-
const phi = (Math.max(-MERCATOR_LAT, Math.min(MERCATOR_LAT, lat)) * Math.PI) / 180;
|
|
208
|
-
return [(lon + 180) / 360, 0.5 - Math.log(Math.tan(Math.PI / 4 + phi / 2)) / (2 * Math.PI)];
|
|
209
|
-
};
|
|
210
|
-
const fromMercator = ([x, y]) => [
|
|
211
|
-
x * 360 - 180,
|
|
212
|
-
(Math.atan(Math.sinh(Math.PI * (1 - 2 * y))) * 180) / Math.PI,
|
|
213
|
-
];
|
|
214
|
-
/** A CSS color to RGB 0-1 (its alpha dropped); black if unknown */
|
|
215
|
-
function rgb(color) {
|
|
216
|
-
const [r, g, b] = parseCssColor(color) ?? [0, 0, 0, 1];
|
|
217
|
-
return [r, g, b];
|
|
218
|
-
}
|
|
219
44
|
const CORNERS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
|
45
|
+
/** Frames the hover picking waits for its answer at most (usually one) */
|
|
46
|
+
const MAX_POLLS = 30;
|
|
47
|
+
/** The name of a basemap, checked: an unknown one throws */
|
|
48
|
+
function checkBasemap(basemap) {
|
|
49
|
+
const name = basemapName(basemap);
|
|
50
|
+
if (name !== 'none' && !(name in VECTOR_BASEMAPS) && !(name in BASEMAPS)) {
|
|
51
|
+
throw new Error(`isard: unknown basemap '${basemap}'`);
|
|
52
|
+
}
|
|
53
|
+
return name;
|
|
54
|
+
}
|
|
220
55
|
/** A map (see `GeoMap.create`); layers take it as their host */
|
|
221
56
|
export class GeoMap {
|
|
222
57
|
canvas;
|
|
@@ -227,6 +62,8 @@ export class GeoMap {
|
|
|
227
62
|
vector;
|
|
228
63
|
glyphsUrl;
|
|
229
64
|
basemapWorkerUrl;
|
|
65
|
+
/** Texture size of the vector basemap's tiles */
|
|
66
|
+
basemapTileSize;
|
|
230
67
|
container;
|
|
231
68
|
/** Markers and popups, over the canvas */
|
|
232
69
|
overlay;
|
|
@@ -279,6 +116,8 @@ export class GeoMap {
|
|
|
279
116
|
hoverPoint;
|
|
280
117
|
/** Frame polling the engine for the answer of the hover picking, 0 if none */
|
|
281
118
|
hoverPoll = 0;
|
|
119
|
+
/** Polls of the hover picking under way: given up after `MAX_POLLS` (an answer that never comes) */
|
|
120
|
+
hoverPolls = 0;
|
|
282
121
|
/** What 'hoverfeature' looks for: within `radius` CSS pixels, in `layers` only if given */
|
|
283
122
|
hoverPicking = {};
|
|
284
123
|
frameRequest = 0;
|
|
@@ -294,14 +133,24 @@ export class GeoMap {
|
|
|
294
133
|
basemapPool;
|
|
295
134
|
/** Stops watching the device pixel ratio */
|
|
296
135
|
unwatchRatio;
|
|
136
|
+
/** Stops listening to the canvas's context loss and restoration */
|
|
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();
|
|
297
141
|
/** Loads the engine (once per page) and creates a map filling `container` */
|
|
298
142
|
static async create(container, options = {}) {
|
|
299
143
|
// With 3D Tiles from the start when asked, or when only that engine's URL is given (as before the core engine)
|
|
300
144
|
const full = options.tiles3d === true || (options.wasmUrl !== undefined && options.coreWasmUrl === undefined);
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
145
|
+
// Before anything is created
|
|
146
|
+
if (options.basemap !== undefined)
|
|
147
|
+
checkBasemap(options.basemap);
|
|
148
|
+
const kind = full ? 'full' : 'core';
|
|
149
|
+
const bindings = await loadEngine(kind, engineUrl(kind, options));
|
|
150
|
+
const map = new GeoMap(container, options, bindings.Engine, full ? bindings.module : undefined);
|
|
151
|
+
if (options.tiles3d === 'preload' && !full)
|
|
152
|
+
map.preloadTiles3d();
|
|
153
|
+
return map;
|
|
305
154
|
}
|
|
306
155
|
constructor(container, options, engineClass, tiles3dModule) {
|
|
307
156
|
this.engineClass = engineClass;
|
|
@@ -309,6 +158,7 @@ export class GeoMap {
|
|
|
309
158
|
this.tiles3dOptions = { ...options };
|
|
310
159
|
this.glyphsUrl = options.glyphs;
|
|
311
160
|
this.basemapWorkerUrl = options.basemapWorkerUrl;
|
|
161
|
+
this.basemapTileSize = options.basemapTileSize;
|
|
312
162
|
this.container = container;
|
|
313
163
|
this.canvas = document.createElement('canvas');
|
|
314
164
|
Object.assign(this.canvas.style, { display: 'block', width: '100%', height: '100%', touchAction: 'none' });
|
|
@@ -341,20 +191,26 @@ export class GeoMap {
|
|
|
341
191
|
globe: (options.projection ?? 'globe') === 'globe',
|
|
342
192
|
sky: options.sky ?? true,
|
|
343
193
|
atmosphere: atmosphereSettings(options.atmosphere),
|
|
344
|
-
terrain: terrainSettings(options.terrain, options.body, body.radius),
|
|
194
|
+
terrain: terrainSettings(options.terrain, options.body, body.radius, this.terrainLoader),
|
|
345
195
|
loading: options.loading ?? 'immediate',
|
|
346
196
|
background: rgb(options.background ?? (this.earth ? '#f2f2f0' : '#000000')),
|
|
347
197
|
globeColor: this.earth ? SEA['osm-positron'] : rgb(body.color),
|
|
348
198
|
};
|
|
349
199
|
this.#engine = this.createEngine();
|
|
350
200
|
// The browser may take the GPU away (driver reset, background tab on mobile): the map waits, then rebuilds
|
|
351
|
-
|
|
201
|
+
const lost = (event) => {
|
|
352
202
|
event.preventDefault();
|
|
353
203
|
this.lost = true;
|
|
354
204
|
cancelAnimationFrame(this.frameRequest);
|
|
355
205
|
this.frameRequest = 0;
|
|
356
|
-
}
|
|
357
|
-
|
|
206
|
+
};
|
|
207
|
+
const restored = () => this.restore();
|
|
208
|
+
this.canvas.addEventListener('webglcontextlost', lost);
|
|
209
|
+
this.canvas.addEventListener('webglcontextrestored', restored);
|
|
210
|
+
this.unwatchContext = () => {
|
|
211
|
+
this.canvas.removeEventListener('webglcontextlost', lost);
|
|
212
|
+
this.canvas.removeEventListener('webglcontextrestored', restored);
|
|
213
|
+
};
|
|
358
214
|
const start = this.earth
|
|
359
215
|
? { center: [2.4246, 48.8457], zoom: 13, pitch: 0, bearing: 0 }
|
|
360
216
|
: { center: [0, 0], zoom: 2, pitch: 0, bearing: 0 };
|
|
@@ -412,17 +268,12 @@ export class GeoMap {
|
|
|
412
268
|
}
|
|
413
269
|
/** The decoding workers of 3D Tiles, with the engine that has them (they run its module) and unless turned off */
|
|
414
270
|
startLoader(engine) {
|
|
415
|
-
|
|
416
|
-
const workers = count ?? Math.min(4, Math.max(1, (navigator.hardwareConcurrency || 2) - 1));
|
|
417
|
-
if (!this.tiles3dModule || workers === false || workers <= 0 || typeof Worker === 'undefined')
|
|
418
|
-
return undefined;
|
|
419
|
-
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);
|
|
420
272
|
}
|
|
421
273
|
/** A 3D Tiles layer on the core engine: loads the engine with 3D Tiles, then rebuilds the map on it (camera,
|
|
422
274
|
* settings and layers kept, as after a context loss); the layer is created then */
|
|
423
275
|
upgrade() {
|
|
424
|
-
|
|
425
|
-
this.upgrading ??= loadEngine('full', url).then(({ module, Engine }) => {
|
|
276
|
+
this.upgrading ??= loadFullEngineLater(engineUrl('full', this.tiles3dOptions)).then(({ module, Engine }) => {
|
|
426
277
|
this.upgrading = undefined;
|
|
427
278
|
if (this.gone)
|
|
428
279
|
return;
|
|
@@ -434,12 +285,30 @@ export class GeoMap {
|
|
|
434
285
|
}, (error) => {
|
|
435
286
|
// The 3D Tiles layers stay empty; the next one added tries again
|
|
436
287
|
this.upgrading = undefined;
|
|
288
|
+
if (this.gone)
|
|
289
|
+
return;
|
|
437
290
|
report(error);
|
|
438
291
|
this.requestRender();
|
|
439
292
|
});
|
|
440
293
|
}
|
|
294
|
+
/** Downloads and compiles the engine with 3D Tiles once the map is idle (`tiles3d: 'preload'`), without running
|
|
295
|
+
* it: the first 3D Tiles layer then only instantiates it */
|
|
296
|
+
preloadTiles3d() {
|
|
297
|
+
const url = engineUrl('full', this.tiles3dOptions);
|
|
298
|
+
const later = () => preloadFullEngine(url, () => !this.gone && !this.tiles3dModule);
|
|
299
|
+
if (this.settled)
|
|
300
|
+
later();
|
|
301
|
+
else {
|
|
302
|
+
const off = this.on('idle', () => {
|
|
303
|
+
off();
|
|
304
|
+
later();
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
441
308
|
/** After a context loss: a new engine with the same camera, settings and layers (same handles, same order) */
|
|
442
309
|
restore() {
|
|
310
|
+
if (this.gone)
|
|
311
|
+
return;
|
|
443
312
|
this.cancelHoverPick();
|
|
444
313
|
const camera = this.getCamera();
|
|
445
314
|
this.#engine.free();
|
|
@@ -447,16 +316,46 @@ export class GeoMap {
|
|
|
447
316
|
this.lost = false;
|
|
448
317
|
this.setCamera(camera);
|
|
449
318
|
this.#glyphs?.attach(this.#engine);
|
|
319
|
+
const engine = this.#engine;
|
|
320
|
+
// No layer refers to an id of the old engine while the others are created
|
|
450
321
|
for (const layer of this.layers)
|
|
451
|
-
layer.
|
|
322
|
+
layer.forgetEngine();
|
|
323
|
+
for (const layer of this.layers) {
|
|
324
|
+
// Still waiting for the engine with 3D Tiles (loading, or failed): created with it
|
|
325
|
+
if (layer instanceof Tiles3dLayer && !this.tiles3dModule)
|
|
326
|
+
continue;
|
|
327
|
+
// A layer that fails is reported and left out; the others are created
|
|
328
|
+
try {
|
|
329
|
+
layer.attach(engine);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
report(error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Links between layers, once every layer has its new id (buildings colored by a network above them)
|
|
336
|
+
for (const layer of this.layers) {
|
|
337
|
+
if (layer.id < 0)
|
|
338
|
+
continue;
|
|
339
|
+
try {
|
|
340
|
+
layer.relink(engine);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
report(error);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
452
346
|
this.resize();
|
|
453
347
|
}
|
|
454
348
|
/** @internal Forgets a removed layer */
|
|
455
349
|
detach(layer) {
|
|
456
350
|
this.unlist(layer);
|
|
351
|
+
this.onTop.delete(layer);
|
|
457
352
|
if (this.rasterCredits.delete(layer))
|
|
458
353
|
this.attributionChanged();
|
|
459
354
|
}
|
|
355
|
+
/** @internal The layer is on the map */
|
|
356
|
+
hasLayer(layer) {
|
|
357
|
+
return !this.gone && this.layers.includes(layer);
|
|
358
|
+
}
|
|
460
359
|
/** Takes a layer out of the drawing order (to put it back elsewhere, or for good) */
|
|
461
360
|
unlist(layer) {
|
|
462
361
|
const index = this.layers.indexOf(layer);
|
|
@@ -543,21 +442,9 @@ export class GeoMap {
|
|
|
543
442
|
}
|
|
544
443
|
/** Animated move to a camera: over `duration` milliseconds (or the options), ease-out by default */
|
|
545
444
|
easeTo(camera, options = 600) {
|
|
546
|
-
const { duration = 600, easing = (t) => 1 - (1 - t) ** 3 } = typeof options === 'number' ? { duration: options } : options;
|
|
547
445
|
const from = this.getCamera();
|
|
548
446
|
const to = { ...from, ...camera };
|
|
549
|
-
|
|
550
|
-
const turn = ((to.bearing - from.bearing + 540) % 360) - 180;
|
|
551
|
-
const start = performance.now();
|
|
552
|
-
this.animate((now) => {
|
|
553
|
-
if (now - start >= duration)
|
|
554
|
-
this.engine.clearPreload();
|
|
555
|
-
const t = duration > 0 ? Math.min(1, (now - start) / duration) : 1;
|
|
556
|
-
const k = easing(t);
|
|
557
|
-
const mix = (a, b) => a + (b - a) * k;
|
|
558
|
-
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);
|
|
559
|
-
return t < 1;
|
|
560
|
-
});
|
|
447
|
+
this.animate(easeTo(() => this.engine, from, to, options));
|
|
561
448
|
// The destination's tiles load during the flight (as Cesium does)
|
|
562
449
|
this.engine.preload(to.center[0], to.center[1], to.zoom, to.pitch, to.bearing);
|
|
563
450
|
}
|
|
@@ -568,43 +455,8 @@ export class GeoMap {
|
|
|
568
455
|
flyTo(camera, options = {}) {
|
|
569
456
|
const from = this.getCamera();
|
|
570
457
|
const to = { ...from, ...camera };
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
const [x1, y1] = toMercator(to.center);
|
|
574
|
-
// Across the antimeridian, the short way
|
|
575
|
-
const dx = x1 - x0 - Math.round(x1 - x0);
|
|
576
|
-
const dy = y1 - y0;
|
|
577
|
-
// In pixels at the start zoom (512 px world at zoom 0)
|
|
578
|
-
const world = 512 * 2 ** from.zoom;
|
|
579
|
-
const u1 = Math.hypot(dx, dy) * world;
|
|
580
|
-
const w0 = Math.max(this.canvas.clientWidth, this.canvas.clientHeight, 1);
|
|
581
|
-
const w1 = w0 / 2 ** (to.zoom - from.zoom);
|
|
582
|
-
const rho2 = rho * rho;
|
|
583
|
-
const r = (i) => {
|
|
584
|
-
const b = (w1 * w1 - w0 * w0 + (i ? -1 : 1) * rho2 * rho2 * u1 * u1) / (2 * (i ? w1 : w0) * rho2 * u1);
|
|
585
|
-
return Math.log(Math.sqrt(b * b + 1) - b);
|
|
586
|
-
};
|
|
587
|
-
// No move on the ground (or nearly): a plain zoom
|
|
588
|
-
const still = u1 < 1e-6 || !Number.isFinite(r(0)) || !Number.isFinite(r(1));
|
|
589
|
-
const r0 = still ? 0 : r(0);
|
|
590
|
-
const length = still ? Math.abs(Math.log(w1 / w0)) / rho : (r(1) - r0) / rho;
|
|
591
|
-
const width = (s) => still ? Math.exp((w1 < w0 ? -1 : 1) * rho * s) : Math.cosh(r0) / Math.cosh(r0 + rho * s);
|
|
592
|
-
const along = (s) => still ? 0 : (w0 * ((Math.cosh(r0) * Math.tanh(r0 + rho * s) - Math.sinh(r0)) / rho2)) / u1;
|
|
593
|
-
const duration = options.duration ?? (1000 * length) / speed;
|
|
594
|
-
const turn = ((to.bearing - from.bearing + 540) % 360) - 180;
|
|
595
|
-
const start = performance.now();
|
|
596
|
-
this.animate((now) => {
|
|
597
|
-
if (now - start >= duration)
|
|
598
|
-
this.engine.clearPreload();
|
|
599
|
-
const t = duration > 0 ? Math.min(1, (now - start) / duration) : 1;
|
|
600
|
-
const k = easing(t);
|
|
601
|
-
const s = k * length;
|
|
602
|
-
const u = t >= 1 ? 1 : along(s);
|
|
603
|
-
const zoom = t >= 1 ? to.zoom : from.zoom + Math.log2(1 / width(s));
|
|
604
|
-
const [lon, lat] = fromMercator([x0 + dx * u, y0 + dy * u]);
|
|
605
|
-
this.engine.setCamera(lon, lat, zoom, from.pitch + (to.pitch - from.pitch) * k, from.bearing + turn * k);
|
|
606
|
-
return t < 1;
|
|
607
|
-
});
|
|
458
|
+
const viewport = Math.max(this.canvas.clientWidth, this.canvas.clientHeight);
|
|
459
|
+
this.animate(flyTo(() => this.engine, from, to, viewport, options));
|
|
608
460
|
this.engine.preload(to.center[0], to.center[1], to.zoom, to.pitch, to.bearing);
|
|
609
461
|
}
|
|
610
462
|
/** Longitudes and latitudes the view covers on the ground: `[west, south, east, north]` */
|
|
@@ -656,7 +508,7 @@ export class GeoMap {
|
|
|
656
508
|
* on the Moon and Mars), one of the Earth's models by name (`'ign'`), its options, or false; on another body, its
|
|
657
509
|
* options with a `url` */
|
|
658
510
|
setTerrain(terrain) {
|
|
659
|
-
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);
|
|
660
512
|
const t = this.settings.terrain;
|
|
661
513
|
this.engine.setTerrain(t?.url, t?.exaggeration ?? 1, t?.maxZoom, t?.geoid, t?.loader);
|
|
662
514
|
this.attributionChanged();
|
|
@@ -688,8 +540,9 @@ export class GeoMap {
|
|
|
688
540
|
return this.engine.stats();
|
|
689
541
|
}
|
|
690
542
|
/**
|
|
691
|
-
*
|
|
692
|
-
*
|
|
543
|
+
* The feature under a screen point (CSS pixels), undefined if none: its layer id and index. Reads one pixel of an
|
|
544
|
+
* offscreen picking pass, whatever the number of features, but waits for the GPU to finish the frame (a stall): for
|
|
545
|
+
* clicks and one-off queries. On hover, listen to 'hoverfeature' instead, which picks without waiting.
|
|
693
546
|
*/
|
|
694
547
|
pick(point, radius = 0, layers) {
|
|
695
548
|
const only = layers && Uint32Array.from(layers.map((l) => l.id));
|
|
@@ -718,10 +571,15 @@ export class GeoMap {
|
|
|
718
571
|
requestHoverPick(point) {
|
|
719
572
|
const { radius = 0, layers } = this.hoverPicking;
|
|
720
573
|
const only = layers && Uint32Array.from(layers.map((l) => l.id));
|
|
721
|
-
//
|
|
722
|
-
if (!this.engine.requestPick(point[0], point[1], radius, only)
|
|
574
|
+
// The engine cannot pick (no request in flight): the poll under way would wait for nothing
|
|
575
|
+
if (!this.engine.requestPick(point[0], point[1], radius, only)) {
|
|
576
|
+
this.cancelHoverPick();
|
|
723
577
|
return;
|
|
724
|
-
|
|
578
|
+
}
|
|
579
|
+
// A new request replaces the one in flight (in the engine too): one poll loop at most
|
|
580
|
+
this.hoverPolls = 0;
|
|
581
|
+
if (!this.hoverPoll)
|
|
582
|
+
this.hoverPoll = requestAnimationFrame(this.pollHoverPick);
|
|
725
583
|
}
|
|
726
584
|
pollHoverPick = () => {
|
|
727
585
|
this.hoverPoll = 0;
|
|
@@ -729,8 +587,10 @@ export class GeoMap {
|
|
|
729
587
|
return;
|
|
730
588
|
const result = this.engine.pollPick();
|
|
731
589
|
if (!result) {
|
|
732
|
-
// The GPU is not done yet (usually a frame): asked again next frame, without drawing
|
|
733
|
-
this.
|
|
590
|
+
// The GPU is not done yet (usually a frame): asked again next frame, without drawing; given up at last
|
|
591
|
+
this.hoverPolls += 1;
|
|
592
|
+
if (this.hoverPolls < MAX_POLLS)
|
|
593
|
+
this.hoverPoll = requestAnimationFrame(this.pollHoverPick);
|
|
734
594
|
return;
|
|
735
595
|
}
|
|
736
596
|
this.emit('hoverfeature', {
|
|
@@ -795,22 +655,21 @@ export class GeoMap {
|
|
|
795
655
|
/** Replaces the basemap and the IGN hillshade, under the other layers (hillshade by default: 0, none). The former
|
|
796
656
|
* names of the Géoplateforme basemaps (`LegacyBasemap`) are taken under their new ones */
|
|
797
657
|
setBasemap(basemap, hillshade = 0) {
|
|
798
|
-
const name =
|
|
799
|
-
this.basemap
|
|
800
|
-
|
|
801
|
-
if (this.
|
|
802
|
-
this.
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}
|
|
658
|
+
const name = checkBasemap(basemap);
|
|
659
|
+
if (this.basemap && this.hasLayer(this.basemap))
|
|
660
|
+
this.basemap.remove();
|
|
661
|
+
if (this.hillshade && this.hasLayer(this.hillshade))
|
|
662
|
+
this.hillshade.remove();
|
|
663
|
+
// Its raster and labels go with it
|
|
664
|
+
this.vector?.tiles.destroy();
|
|
665
|
+
this.vector = undefined;
|
|
807
666
|
const vector = VECTOR_BASEMAPS[name];
|
|
808
667
|
if (vector) {
|
|
809
668
|
const tiles = new VectorTiles(this, {
|
|
810
669
|
style: vector,
|
|
811
670
|
...(this.basemapWorkerUrl ? { workerUrl: this.basemapWorkerUrl } : {}),
|
|
812
671
|
});
|
|
813
|
-
this.basemap = tiles.raster();
|
|
672
|
+
this.basemap = tiles.raster(this.basemapTileSize ? { size: this.basemapTileSize } : {});
|
|
814
673
|
this.vector = { tiles, labels: tiles.labels() };
|
|
815
674
|
}
|
|
816
675
|
else {
|
|
@@ -827,19 +686,28 @@ export class GeoMap {
|
|
|
827
686
|
this.toBottom(this.basemap);
|
|
828
687
|
this.attributionChanged();
|
|
829
688
|
}
|
|
830
|
-
/** Raster tiles ({z}, {x}, {y} URL; Web Mercator, or reprojected from
|
|
831
|
-
* 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 */
|
|
832
691
|
addRaster(options) {
|
|
833
|
-
const { url, grid = 'mercator',
|
|
834
|
-
let
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
+
}
|
|
842
709
|
}
|
|
710
|
+
const layer = new RasterLayer(this, { url, minZoom, maxZoom, opacity, saturation, size, loader });
|
|
843
711
|
this.place(layer, before);
|
|
844
712
|
if (options.attribution) {
|
|
845
713
|
this.rasterCredits.set(layer, options.attribution);
|
|
@@ -904,6 +772,10 @@ export class GeoMap {
|
|
|
904
772
|
this.vectorSets.add(tiles);
|
|
905
773
|
return tiles;
|
|
906
774
|
}
|
|
775
|
+
/** @internal Forgets a destroyed `VectorTiles` */
|
|
776
|
+
dropVectorTiles(tiles) {
|
|
777
|
+
this.vectorSets.delete(tiles);
|
|
778
|
+
}
|
|
907
779
|
/**
|
|
908
780
|
* @internal The workers for a `VectorTiles`: the map's shared pool, or a pool of its own (`own`, to stop with it)
|
|
909
781
|
* when it asks for another count or URL
|
|
@@ -919,6 +791,30 @@ export class GeoMap {
|
|
|
919
791
|
}
|
|
920
792
|
return { pool: new BasemapWorkers(workers, workerUrl), own: true };
|
|
921
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
|
+
};
|
|
922
818
|
/** Adds a layer, under `before` if given */
|
|
923
819
|
place(layer, before) {
|
|
924
820
|
this.add(layer);
|
|
@@ -947,13 +843,18 @@ export class GeoMap {
|
|
|
947
843
|
this.animation = step;
|
|
948
844
|
this.requestRender();
|
|
949
845
|
}
|
|
950
|
-
/** @internal
|
|
846
|
+
/** @internal The pointer moved over the map (undefined: it left). Without 'hover' and 'hoverfeature' listeners,
|
|
847
|
+
* nothing is drawn: a still map stays idle under a moving mouse */
|
|
951
848
|
hover(point) {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
this.requestRender();
|
|
955
|
-
else
|
|
849
|
+
if (!point) {
|
|
850
|
+
this.hoverPoint = undefined;
|
|
956
851
|
this.cancelHoverPick();
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (this.listeners.hover.size === 0 && this.listeners.hoverfeature.size === 0)
|
|
855
|
+
return;
|
|
856
|
+
this.hoverPoint = point;
|
|
857
|
+
this.requestRender();
|
|
957
858
|
}
|
|
958
859
|
frame(now) {
|
|
959
860
|
this.frameRequest = 0;
|
|
@@ -991,7 +892,9 @@ export class GeoMap {
|
|
|
991
892
|
const point = this.hoverPoint;
|
|
992
893
|
hovered = point;
|
|
993
894
|
this.hoverPoint = undefined;
|
|
994
|
-
|
|
895
|
+
// The position under the pointer for its listeners only (a 'hoverfeature' alone does not need it)
|
|
896
|
+
if (this.listeners.hover.size > 0)
|
|
897
|
+
this.emit('hover', { point, lonLat: this.unproject(point) });
|
|
995
898
|
}
|
|
996
899
|
const t0 = performance.now();
|
|
997
900
|
// The engine asks for the next frame while it draws (uploads past the frame budget, fades): not ignored
|
|
@@ -1048,12 +951,16 @@ export class GeoMap {
|
|
|
1048
951
|
this.frame(performance.now());
|
|
1049
952
|
}
|
|
1050
953
|
destroy() {
|
|
954
|
+
if (this.gone)
|
|
955
|
+
return;
|
|
1051
956
|
this.gone = true;
|
|
957
|
+
// A context restored later rebuilds nothing
|
|
958
|
+
this.unwatchContext();
|
|
1052
959
|
const camera = this.getCamera();
|
|
1053
960
|
cancelAnimationFrame(this.frameRequest);
|
|
1054
961
|
this.cancelHoverPick();
|
|
1055
962
|
this.vector?.tiles.destroy();
|
|
1056
|
-
for (const tiles of this.vectorSets)
|
|
963
|
+
for (const tiles of [...this.vectorSets])
|
|
1057
964
|
tiles.destroy();
|
|
1058
965
|
this.vectorSets.clear();
|
|
1059
966
|
this.basemapPool?.terminate();
|
|
@@ -1066,7 +973,9 @@ export class GeoMap {
|
|
|
1066
973
|
this.engine.free();
|
|
1067
974
|
// Work still under way (labels being shaped, tiles arriving, timers) may call the engine after this: it finds one
|
|
1068
975
|
// that does nothing, and nothing is drawn again
|
|
1069
|
-
this.#engine =
|
|
976
|
+
this.#engine = destroyedEngine(camera);
|
|
977
|
+
// Glyph ranges still arriving upload to it, not to the freed engine
|
|
978
|
+
this.#glyphs?.attach(this.#engine);
|
|
1070
979
|
this.lost = true;
|
|
1071
980
|
for (const control of [...this.controls.keys()])
|
|
1072
981
|
this.removeControl(control);
|