isard 0.2.1 → 0.4.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 +93 -0
- package/README.md +8 -4
- 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 -4
- package/dist/basemap.worker.js +2 -2
- package/dist/basemap.worker.js.map +9 -5
- 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 +28 -0
- package/dist/engine-loader.js +112 -0
- package/dist/engine-loader.js.map +1 -0
- 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 +13 -3
- package/dist/glyphs.js +26 -4
- 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/icons.d.ts +146 -0
- package/dist/icons.js +333 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +0 -4
- package/dist/interaction.js +8 -4
- 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 +89 -2
- package/dist/layers.js +171 -15
- 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 +45 -7
- package/dist/map.js +138 -310
- 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 +54 -9
- package/dist/symbols.js +278 -55
- package/dist/symbols.js.map +1 -1
- package/dist/terrain.d.ts +19 -7
- package/dist/terrain.js +40 -28
- 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 +14 -6
- package/dist/types.js.map +1 -1
- package/dist/vectorstyle.d.ts +32 -3
- package/dist/vectorstyle.js.map +1 -1
- package/dist/vectortiles.d.ts +19 -7
- package/dist/vectortiles.js +117 -69
- package/dist/vectortiles.js.map +1 -1
- package/dist/wasm/isard.d.ts +55 -7
- package/dist/wasm/isard.js +117 -6
- package/dist/wasm/isard_core.d.ts +29 -4
- package/dist/wasm/isard_core.js +84 -5
- 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 -143
- package/dist/equirectangular.js.map +0 -1
package/dist/map.js
CHANGED
|
@@ -1,36 +1,25 @@
|
|
|
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";
|
|
15
|
+
import { Icons, readPixels } from "./icons.js";
|
|
14
16
|
import { Interaction } from "./interaction.js";
|
|
15
|
-
import { GroundLayer, NetworkLayer, RasterLayer, Tiles3dLayer } from "./layers.js";
|
|
16
|
-
import {
|
|
17
|
+
import { CustomLayer, GroundLayer, ModelLayer, NetworkLayer, RasterLayer, Tiles3dLayer, } from "./layers.js";
|
|
18
|
+
import { atmosphereSettings, bodySettings, haloSettings, rgb, terrainSettings, } from "./settings.js";
|
|
17
19
|
import { injectStyle } from "./style.js";
|
|
18
20
|
import { SymbolLayer, symbolStyle } from "./symbols.js";
|
|
19
21
|
import { encodedTerrainLoader } from "./terrain.js";
|
|
20
22
|
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
23
|
/** Reports an error without throwing it here: the console shows it, as an uncaught one */
|
|
35
24
|
function report(error) {
|
|
36
25
|
if (typeof reportError === 'function')
|
|
@@ -40,63 +29,6 @@ function report(error) {
|
|
|
40
29
|
throw error;
|
|
41
30
|
});
|
|
42
31
|
}
|
|
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
32
|
/** Checks a network's arrays before the engine reads them: node index pairs, indices of existing nodes */
|
|
101
33
|
function validateNetwork(nodes, edges) {
|
|
102
34
|
if (nodes.length % 2 !== 0)
|
|
@@ -110,140 +42,6 @@ function validateNetwork(nodes, edges) {
|
|
|
110
42
|
throw new Error(`isard: network edge ${i >> 1} refers to node ${node}, of ${count} nodes`);
|
|
111
43
|
}
|
|
112
44
|
}
|
|
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
45
|
const CORNERS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
|
248
46
|
/** Frames the hover picking waits for its answer at most (usually one) */
|
|
249
47
|
const MAX_POLLS = 30;
|
|
@@ -259,12 +57,15 @@ function checkBasemap(basemap) {
|
|
|
259
57
|
export class GeoMap {
|
|
260
58
|
canvas;
|
|
261
59
|
#glyphs;
|
|
60
|
+
#icons;
|
|
262
61
|
/** Layers new layers go under */
|
|
263
62
|
onTop = new Set();
|
|
264
63
|
/** The vector basemap, if any: its tiles' workers and its labels */
|
|
265
64
|
vector;
|
|
266
65
|
glyphsUrl;
|
|
267
66
|
basemapWorkerUrl;
|
|
67
|
+
/** Texture size of the vector basemap's tiles */
|
|
68
|
+
basemapTileSize;
|
|
268
69
|
container;
|
|
269
70
|
/** Markers and popups, over the canvas */
|
|
270
71
|
overlay;
|
|
@@ -328,6 +129,8 @@ export class GeoMap {
|
|
|
328
129
|
/** The last frame found everything loaded ('idle' sent) */
|
|
329
130
|
settled = false;
|
|
330
131
|
inFrame = false;
|
|
132
|
+
/** The camera of the frame being drawn: what `getCamera` gives the custom layers while the engine is busy */
|
|
133
|
+
frameCamera;
|
|
331
134
|
/** Vector tiles made by `vectorTiles()`, destroyed with the map */
|
|
332
135
|
vectorSets = new Set();
|
|
333
136
|
/** The workers drawing vector tiles, shared by every `VectorTiles` of the map; created on first use */
|
|
@@ -336,6 +139,9 @@ export class GeoMap {
|
|
|
336
139
|
unwatchRatio;
|
|
337
140
|
/** Stops listening to the canvas's context loss and restoration */
|
|
338
141
|
unwatchContext;
|
|
142
|
+
/** The loaders of the relief by encoding, URL and floor: the same loader for the same tiles (the engine keeps its
|
|
143
|
+
* tiles when only the exaggeration changes) */
|
|
144
|
+
terrainLoaders = new Map();
|
|
339
145
|
/** Loads the engine (once per page) and creates a map filling `container` */
|
|
340
146
|
static async create(container, options = {}) {
|
|
341
147
|
// With 3D Tiles from the start when asked, or when only that engine's URL is given (as before the core engine)
|
|
@@ -343,9 +149,8 @@ export class GeoMap {
|
|
|
343
149
|
// Before anything is created
|
|
344
150
|
if (options.basemap !== undefined)
|
|
345
151
|
checkBasemap(options.basemap);
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
: await loadEngine('core', options.coreWasmUrl ?? new URL('./isard_core_bg.wasm', import.meta.url));
|
|
152
|
+
const kind = full ? 'full' : 'core';
|
|
153
|
+
const bindings = await loadEngine(kind, engineUrl(kind, options));
|
|
349
154
|
const map = new GeoMap(container, options, bindings.Engine, full ? bindings.module : undefined);
|
|
350
155
|
if (options.tiles3d === 'preload' && !full)
|
|
351
156
|
map.preloadTiles3d();
|
|
@@ -357,6 +162,7 @@ export class GeoMap {
|
|
|
357
162
|
this.tiles3dOptions = { ...options };
|
|
358
163
|
this.glyphsUrl = options.glyphs;
|
|
359
164
|
this.basemapWorkerUrl = options.basemapWorkerUrl;
|
|
165
|
+
this.basemapTileSize = options.basemapTileSize;
|
|
360
166
|
this.container = container;
|
|
361
167
|
this.canvas = document.createElement('canvas');
|
|
362
168
|
Object.assign(this.canvas.style, { display: 'block', width: '100%', height: '100%', touchAction: 'none' });
|
|
@@ -389,7 +195,7 @@ export class GeoMap {
|
|
|
389
195
|
globe: (options.projection ?? 'globe') === 'globe',
|
|
390
196
|
sky: options.sky ?? true,
|
|
391
197
|
atmosphere: atmosphereSettings(options.atmosphere),
|
|
392
|
-
terrain: terrainSettings(options.terrain, options.body, body.radius),
|
|
198
|
+
terrain: terrainSettings(options.terrain, options.body, body.radius, this.terrainLoader),
|
|
393
199
|
loading: options.loading ?? 'immediate',
|
|
394
200
|
background: rgb(options.background ?? (this.earth ? '#f2f2f0' : '#000000')),
|
|
395
201
|
globeColor: this.earth ? SEA['osm-positron'] : rgb(body.color),
|
|
@@ -466,20 +272,12 @@ export class GeoMap {
|
|
|
466
272
|
}
|
|
467
273
|
/** The decoding workers of 3D Tiles, with the engine that has them (they run its module) and unless turned off */
|
|
468
274
|
startLoader(engine) {
|
|
469
|
-
|
|
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());
|
|
275
|
+
return this.tiles3dModule && startTileLoader(this.tiles3dModule, this.tiles3dOptions, engine);
|
|
474
276
|
}
|
|
475
|
-
/** A 3D Tiles layer on the core engine: loads the engine with 3D Tiles, then rebuilds the map on it
|
|
476
|
-
* settings and layers kept, as after a context loss); the layer is created then */
|
|
277
|
+
/** A 3D Tiles layer or a model on the core engine: loads the engine with 3D Tiles, then rebuilds the map on it
|
|
278
|
+
* (camera, settings and layers kept, as after a context loss); the layer is created then */
|
|
477
279
|
upgrade() {
|
|
478
|
-
|
|
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 }) => {
|
|
280
|
+
this.upgrading ??= loadFullEngineLater(engineUrl('full', this.tiles3dOptions)).then(({ module, Engine }) => {
|
|
483
281
|
this.upgrading = undefined;
|
|
484
282
|
if (this.gone)
|
|
485
283
|
return;
|
|
@@ -500,17 +298,8 @@ export class GeoMap {
|
|
|
500
298
|
/** Downloads and compiles the engine with 3D Tiles once the map is idle (`tiles3d: 'preload'`), without running
|
|
501
299
|
* it: the first 3D Tiles layer then only instantiates it */
|
|
502
300
|
preloadTiles3d() {
|
|
503
|
-
const url =
|
|
504
|
-
const
|
|
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
|
-
};
|
|
301
|
+
const url = engineUrl('full', this.tiles3dOptions);
|
|
302
|
+
const later = () => preloadFullEngine(url, () => !this.gone && !this.tiles3dModule);
|
|
514
303
|
if (this.settled)
|
|
515
304
|
later();
|
|
516
305
|
else {
|
|
@@ -531,13 +320,14 @@ export class GeoMap {
|
|
|
531
320
|
this.lost = false;
|
|
532
321
|
this.setCamera(camera);
|
|
533
322
|
this.#glyphs?.attach(this.#engine);
|
|
323
|
+
this.#icons?.attach(this.#engine);
|
|
534
324
|
const engine = this.#engine;
|
|
535
325
|
// No layer refers to an id of the old engine while the others are created
|
|
536
326
|
for (const layer of this.layers)
|
|
537
327
|
layer.forgetEngine();
|
|
538
328
|
for (const layer of this.layers) {
|
|
539
329
|
// Still waiting for the engine with 3D Tiles (loading, or failed): created with it
|
|
540
|
-
if (layer
|
|
330
|
+
if (layer.needsFullEngine && !this.tiles3dModule)
|
|
541
331
|
continue;
|
|
542
332
|
// A layer that fails is reported and left out; the others are created
|
|
543
333
|
try {
|
|
@@ -578,8 +368,9 @@ export class GeoMap {
|
|
|
578
368
|
this.layers.splice(index, 1);
|
|
579
369
|
}
|
|
580
370
|
add(layer) {
|
|
581
|
-
// On the core engine, a 3D Tiles layer waits for the engine with 3D Tiles: it is created with the
|
|
582
|
-
|
|
371
|
+
// On the core engine, a 3D Tiles layer or a model waits for the engine with 3D Tiles: it is created with the
|
|
372
|
+
// others on it
|
|
373
|
+
if (layer.needsFullEngine && !this.tiles3dModule)
|
|
583
374
|
this.upgrade();
|
|
584
375
|
else
|
|
585
376
|
layer.attach(this.engine);
|
|
@@ -648,7 +439,7 @@ export class GeoMap {
|
|
|
648
439
|
}
|
|
649
440
|
// ── Camera ──
|
|
650
441
|
getCamera() {
|
|
651
|
-
return this.engine.camera();
|
|
442
|
+
return this.frameCamera ?? this.engine.camera();
|
|
652
443
|
}
|
|
653
444
|
setCamera(camera) {
|
|
654
445
|
const c = { ...this.getCamera(), ...camera };
|
|
@@ -657,21 +448,9 @@ export class GeoMap {
|
|
|
657
448
|
}
|
|
658
449
|
/** Animated move to a camera: over `duration` milliseconds (or the options), ease-out by default */
|
|
659
450
|
easeTo(camera, options = 600) {
|
|
660
|
-
const { duration = 600, easing = (t) => 1 - (1 - t) ** 3 } = typeof options === 'number' ? { duration: options } : options;
|
|
661
451
|
const from = this.getCamera();
|
|
662
452
|
const to = { ...from, ...camera };
|
|
663
|
-
|
|
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
|
-
});
|
|
453
|
+
this.animate(easeTo(() => this.engine, from, to, options));
|
|
675
454
|
// The destination's tiles load during the flight (as Cesium does)
|
|
676
455
|
this.engine.preload(to.center[0], to.center[1], to.zoom, to.pitch, to.bearing);
|
|
677
456
|
}
|
|
@@ -682,43 +461,8 @@ export class GeoMap {
|
|
|
682
461
|
flyTo(camera, options = {}) {
|
|
683
462
|
const from = this.getCamera();
|
|
684
463
|
const to = { ...from, ...camera };
|
|
685
|
-
const
|
|
686
|
-
|
|
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
|
-
});
|
|
464
|
+
const viewport = Math.max(this.canvas.clientWidth, this.canvas.clientHeight);
|
|
465
|
+
this.animate(flyTo(() => this.engine, from, to, viewport, options));
|
|
722
466
|
this.engine.preload(to.center[0], to.center[1], to.zoom, to.pitch, to.bearing);
|
|
723
467
|
}
|
|
724
468
|
/** Longitudes and latitudes the view covers on the ground: `[west, south, east, north]` */
|
|
@@ -770,7 +514,7 @@ export class GeoMap {
|
|
|
770
514
|
* on the Moon and Mars), one of the Earth's models by name (`'ign'`), its options, or false; on another body, its
|
|
771
515
|
* options with a `url` */
|
|
772
516
|
setTerrain(terrain) {
|
|
773
|
-
this.settings.terrain = terrainSettings(terrain, this.body, this.settings.body.radius);
|
|
517
|
+
this.settings.terrain = terrainSettings(terrain, this.body, this.settings.body.radius, this.terrainLoader);
|
|
774
518
|
const t = this.settings.terrain;
|
|
775
519
|
this.engine.setTerrain(t?.url, t?.exaggeration ?? 1, t?.maxZoom, t?.geoid, t?.loader);
|
|
776
520
|
this.attributionChanged();
|
|
@@ -931,7 +675,7 @@ export class GeoMap {
|
|
|
931
675
|
style: vector,
|
|
932
676
|
...(this.basemapWorkerUrl ? { workerUrl: this.basemapWorkerUrl } : {}),
|
|
933
677
|
});
|
|
934
|
-
this.basemap = tiles.raster();
|
|
678
|
+
this.basemap = tiles.raster(this.basemapTileSize ? { size: this.basemapTileSize } : {});
|
|
935
679
|
this.vector = { tiles, labels: tiles.labels() };
|
|
936
680
|
}
|
|
937
681
|
else {
|
|
@@ -948,20 +692,28 @@ export class GeoMap {
|
|
|
948
692
|
this.toBottom(this.basemap);
|
|
949
693
|
this.attributionChanged();
|
|
950
694
|
}
|
|
951
|
-
/** Raster tiles ({z}, {x}, {y} URL; Web Mercator, or reprojected from
|
|
952
|
-
* already added */
|
|
695
|
+
/** Raster tiles ({z}, {x}, {y} URL; Web Mercator, or reprojected from another `grid`: longitudes and latitudes,
|
|
696
|
+
* Lambert-93, UTM…), over the layers already added */
|
|
953
697
|
addRaster(options) {
|
|
954
|
-
const { url, grid = 'mercator',
|
|
955
|
-
let
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
698
|
+
const { url, grid = 'mercator', opacity = 1, saturation = 1, size, before } = options;
|
|
699
|
+
let { minZoom = 0, maxZoom = 19 } = options;
|
|
700
|
+
let loader = options.loader;
|
|
701
|
+
if (grid !== 'mercator' && !loader) {
|
|
702
|
+
if (grid === 'equirectangular') {
|
|
703
|
+
// `minZoom` and `maxZoom` are levels of the source: its level n has the pixel size of Mercator level n + 1 at
|
|
704
|
+
// the equator
|
|
705
|
+
loader = gridLoader(url, resolveGrid(equirectangularGrid(minZoom, maxZoom)), size ?? 256, this.remoteReprojection);
|
|
706
|
+
[minZoom, maxZoom] = [minZoom + 1, maxZoom + 1];
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
const resolved = resolveGrid(grid);
|
|
710
|
+
const zooms = gridZooms(resolved);
|
|
711
|
+
minZoom = options.minZoom ?? zooms.minZoom;
|
|
712
|
+
maxZoom = options.maxZoom ?? zooms.maxZoom;
|
|
713
|
+
loader = gridLoader(url, resolved, size ?? 256, this.remoteReprojection);
|
|
714
|
+
}
|
|
964
715
|
}
|
|
716
|
+
const layer = new RasterLayer(this, { url, minZoom, maxZoom, opacity, saturation, size, loader });
|
|
965
717
|
this.place(layer, before);
|
|
966
718
|
if (options.attribution) {
|
|
967
719
|
this.rasterCredits.set(layer, options.attribution);
|
|
@@ -1012,6 +764,34 @@ export class GeoMap {
|
|
|
1012
764
|
}
|
|
1013
765
|
return this.#glyphs;
|
|
1014
766
|
}
|
|
767
|
+
/** @internal The icons of the symbol layers: the application's images and the vector basemaps' sprites */
|
|
768
|
+
get icons() {
|
|
769
|
+
if (!this.#icons) {
|
|
770
|
+
const gl = this.canvas.getContext('webgl2');
|
|
771
|
+
const max = gl?.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
772
|
+
this.#icons = new Icons(typeof max === 'number' ? max : undefined);
|
|
773
|
+
this.#icons.attach(this.engine);
|
|
774
|
+
}
|
|
775
|
+
return this.#icons;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* An image for the symbol layers' icons (`icon: name`), as MapLibre's `addImage`: its pixels (an image, a canvas,
|
|
779
|
+
* `ImageData`, or RGBA bytes with their size), `pixelRatio` 2 for an image drawn at twice its CSS size; `sdf` for a
|
|
780
|
+
* signed distance field drawn in the `iconColor`; `stretchX`, `stretchY` and `content` for an icon stretched around its
|
|
781
|
+
* text. An image of the same name is replaced
|
|
782
|
+
*/
|
|
783
|
+
addImage(name, image, options = {}) {
|
|
784
|
+
const pixels = 'data' in image ? image : readPixels(image);
|
|
785
|
+
if (!pixels)
|
|
786
|
+
throw new Error(`isard: the image ${name} has no pixels`);
|
|
787
|
+
this.icons.add(name, pixels, options);
|
|
788
|
+
}
|
|
789
|
+
hasImage(name) {
|
|
790
|
+
return this.#icons?.has(name) ?? false;
|
|
791
|
+
}
|
|
792
|
+
removeImage(name) {
|
|
793
|
+
this.#icons?.remove(name);
|
|
794
|
+
}
|
|
1015
795
|
/**
|
|
1016
796
|
* Vector tiles with a MapLibre style (OpenFreeMap's Positron by default): `tiles.raster(…)` draws its fills and lines
|
|
1017
797
|
* (or a subset) as a raster layer, `tiles.labels(…)` its labels. `setBasemap('osm-positron')` does both for the
|
|
@@ -1045,6 +825,30 @@ export class GeoMap {
|
|
|
1045
825
|
}
|
|
1046
826
|
return { pool: new BasemapWorkers(workers, workerUrl), own: true };
|
|
1047
827
|
}
|
|
828
|
+
/**
|
|
829
|
+
* The workers decoding the relief and reprojecting rasters: the map's pool, unless the map has no workers
|
|
830
|
+
* (`workers: false` or 0, or none in this browser): that is done in the page then
|
|
831
|
+
*/
|
|
832
|
+
decodingPool() {
|
|
833
|
+
const { workers } = this.tiles3dOptions;
|
|
834
|
+
if (this.gone || workers === false || (workers !== undefined && workers <= 0))
|
|
835
|
+
return undefined;
|
|
836
|
+
if (typeof Worker === 'undefined' || typeof OffscreenCanvas === 'undefined')
|
|
837
|
+
return undefined;
|
|
838
|
+
return this.basemapWorkers().pool;
|
|
839
|
+
}
|
|
840
|
+
remoteHeights = (url, encoding, floor, tile, signal) => this.decodingPool()?.heights(url, encoding, floor, tile, signal, this);
|
|
841
|
+
remoteReprojection = (job, tile, signal) => this.decodingPool()?.reproject(job, tile, signal, this);
|
|
842
|
+
/** The loader of the relief tiles of `url` in a PNG encoding (one per URL, encoding and floor) */
|
|
843
|
+
terrainLoader = (url, encoding, floor) => {
|
|
844
|
+
const key = `${encoding} ${floor ?? ''} ${url}`;
|
|
845
|
+
let loader = this.terrainLoaders.get(key);
|
|
846
|
+
if (!loader) {
|
|
847
|
+
loader = encodedTerrainLoader(url, encoding, floor, this.remoteHeights);
|
|
848
|
+
this.terrainLoaders.set(key, loader);
|
|
849
|
+
}
|
|
850
|
+
return loader;
|
|
851
|
+
};
|
|
1048
852
|
/** Adds a layer, under `before` if given */
|
|
1049
853
|
place(layer, before) {
|
|
1050
854
|
this.add(layer);
|
|
@@ -1057,6 +861,18 @@ export class GeoMap {
|
|
|
1057
861
|
const { url, maxScreenSpaceError = 16, opacity = 1 } = options;
|
|
1058
862
|
return this.add(new Tiles3dLayer(this, url, { maxScreenSpaceError, opacity }));
|
|
1059
863
|
}
|
|
864
|
+
/** A glTF model drawn at positions (see `ModelOptions`), with depth; on the engine with 3D Tiles, as they are */
|
|
865
|
+
addModels(options) {
|
|
866
|
+
return this.place(new ModelLayer(this, options), options.before);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* The page's own WebGL drawing at its place among the layers, in every frame (see `CustomLayerInterface`): raw
|
|
870
|
+
* WebGL, three.js… with the matrices of the frame (`frame.matrixAt`); under `before` if given
|
|
871
|
+
*/
|
|
872
|
+
addCustomLayer(layer, options = {}) {
|
|
873
|
+
const custom = new CustomLayer(this, layer, this.canvas, this, (lon, lat, altitude) => this.engineClass.customMatrix(lon, lat, altitude), () => this.getCamera());
|
|
874
|
+
return this.place(custom, options.before);
|
|
875
|
+
}
|
|
1060
876
|
// ── Frames ──
|
|
1061
877
|
/** Asks for a frame; several calls before the next frame draw it once */
|
|
1062
878
|
requestRender() {
|
|
@@ -1129,7 +945,15 @@ export class GeoMap {
|
|
|
1129
945
|
const t0 = performance.now();
|
|
1130
946
|
// The engine asks for the next frame while it draws (uploads past the frame budget, fades): not ignored
|
|
1131
947
|
this.inFrame = false;
|
|
1132
|
-
|
|
948
|
+
// As the engine has it now (a listener may have moved it), for the custom layers drawn in the frame
|
|
949
|
+
this.frameCamera = this.layers.some((layer) => layer instanceof CustomLayer) ? this.engine.camera() : undefined;
|
|
950
|
+
let loading;
|
|
951
|
+
try {
|
|
952
|
+
loading = this.engine.render();
|
|
953
|
+
}
|
|
954
|
+
finally {
|
|
955
|
+
this.frameCamera = undefined;
|
|
956
|
+
}
|
|
1133
957
|
this.inFrame = true;
|
|
1134
958
|
// The picking pass sees the frame just drawn; its answer comes on a later frame
|
|
1135
959
|
if (hovered && this.listeners.hoverfeature.size > 0)
|
|
@@ -1200,12 +1024,16 @@ export class GeoMap {
|
|
|
1200
1024
|
this.resizeObserver.disconnect();
|
|
1201
1025
|
this.interaction?.destroy();
|
|
1202
1026
|
this.loader?.destroy();
|
|
1027
|
+
// Custom layers free their WebGL objects, models their data, while the context is there
|
|
1028
|
+
for (const layer of this.layers)
|
|
1029
|
+
layer.dispose();
|
|
1203
1030
|
this.engine.free();
|
|
1204
1031
|
// Work still under way (labels being shaped, tiles arriving, timers) may call the engine after this: it finds one
|
|
1205
1032
|
// that does nothing, and nothing is drawn again
|
|
1206
|
-
this.#engine =
|
|
1033
|
+
this.#engine = destroyedEngine(camera);
|
|
1207
1034
|
// Glyph ranges still arriving upload to it, not to the freed engine
|
|
1208
1035
|
this.#glyphs?.attach(this.#engine);
|
|
1036
|
+
this.#icons?.attach(this.#engine);
|
|
1209
1037
|
this.lost = true;
|
|
1210
1038
|
for (const control of [...this.controls.keys()])
|
|
1211
1039
|
this.removeControl(control);
|