@maptiler/sdk 1.0.9 → 1.0.10
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/.github/workflows/npm-publish.yml +0 -1
- package/CHANGELOG.md +10 -0
- package/demos/maptiler-sdk.umd.js +470 -32
- package/dist/maptiler-sdk.d.ts +66 -37
- package/dist/maptiler-sdk.min.mjs +1 -1
- package/dist/maptiler-sdk.mjs +456 -19
- package/dist/maptiler-sdk.mjs.map +1 -1
- package/dist/maptiler-sdk.umd.js +470 -32
- package/dist/maptiler-sdk.umd.js.map +1 -1
- package/dist/maptiler-sdk.umd.min.js +47 -47
- package/package.json +24 -26
- package/readme.md +108 -0
- package/src/Map.ts +283 -39
- package/src/index.ts +2 -26
- package/demos/embedded-config.html +0 -66
- package/demos/two-maps.html +0 -71
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maptiler/sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "The Javascript & TypeScript map SDK tailored for MapTiler Cloud",
|
|
5
5
|
"module": "dist/maptiler-sdk.mjs",
|
|
6
6
|
"types": "dist/maptiler-sdk.d.ts",
|
|
@@ -20,52 +20,50 @@
|
|
|
20
20
|
"sdk",
|
|
21
21
|
"webmap",
|
|
22
22
|
"cloud",
|
|
23
|
-
"webGL"
|
|
23
|
+
"webGL",
|
|
24
|
+
"maplibre"
|
|
24
25
|
],
|
|
25
|
-
"homepage": "https://
|
|
26
|
+
"homepage": "https://docs.maptiler.com/sdk-js/",
|
|
26
27
|
"license": "BSD-3-Clause",
|
|
27
28
|
"repository": {
|
|
28
29
|
"type": "git",
|
|
29
30
|
"url": "https://github.com/maptiler/maptiler-sdk-js.git"
|
|
30
31
|
},
|
|
31
32
|
"scripts": {
|
|
32
|
-
"build": "rm -rf dist/*
|
|
33
|
-
"dev": "rm -rf dist/*
|
|
33
|
+
"build": "rm -rf dist/* && NODE_ENV=production rollup -c",
|
|
34
|
+
"dev": "rm -rf dist/* && NODE_ENV=development rollup -c -w",
|
|
34
35
|
"format": "prettier --write \"src/**/*.{js,ts,tsx}\"",
|
|
35
36
|
"lint": "eslint --fix \"src/**/*.{js,ts}\"",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"doc": "npm run docmd; npm run dochtml",
|
|
39
|
-
"prepare": "npm run format; npm run lint; npm run build; npm run doc; cp -r demos docs/"
|
|
37
|
+
"doc": "rm -rf docs/* && typedoc --out docs && cp -r images docs/",
|
|
38
|
+
"prepare": "npm run format && npm run lint && npm run build && npm run doc && cp -r demos docs/"
|
|
40
39
|
},
|
|
41
40
|
"author": "MapTiler",
|
|
42
41
|
"devDependencies": {
|
|
43
|
-
"@rollup/plugin-commonjs": "^
|
|
44
|
-
"@rollup/plugin-json": "^
|
|
45
|
-
"@rollup/plugin-node-resolve": "^
|
|
46
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
47
|
-
"@typescript-eslint/parser": "^5.
|
|
48
|
-
"eslint": "^8.
|
|
49
|
-
"prettier": "^2.7
|
|
50
|
-
"rollup": "^
|
|
51
|
-
"rollup-plugin-copy-merge": "^0.
|
|
52
|
-
"rollup-plugin-dts": "^
|
|
53
|
-
"rollup-plugin-esbuild": "^
|
|
42
|
+
"@rollup/plugin-commonjs": "^24.1.0",
|
|
43
|
+
"@rollup/plugin-json": "^6.0.0",
|
|
44
|
+
"@rollup/plugin-node-resolve": "^15.0.2",
|
|
45
|
+
"@typescript-eslint/eslint-plugin": "^5.59.0",
|
|
46
|
+
"@typescript-eslint/parser": "^5.59.0",
|
|
47
|
+
"eslint": "^8.38.0",
|
|
48
|
+
"prettier": "^2.8.7",
|
|
49
|
+
"rollup": "^3.20.6",
|
|
50
|
+
"rollup-plugin-copy-merge": "^1.0.0",
|
|
51
|
+
"rollup-plugin-dts": "^5.3.0",
|
|
52
|
+
"rollup-plugin-esbuild": "^5.0.0",
|
|
54
53
|
"rollup-plugin-node-globals": "^1.4.0",
|
|
55
54
|
"rollup-plugin-shell": "^1.0.9",
|
|
56
55
|
"rollup-plugin-string": "^3.0.0",
|
|
57
56
|
"rollup-plugin-swc": "^0.2.1",
|
|
58
|
-
"serve": "^14.0
|
|
59
|
-
"terser": "^5.
|
|
60
|
-
"typedoc": "^0.
|
|
61
|
-
"
|
|
62
|
-
"typescript": "^4.8.4"
|
|
57
|
+
"serve": "^14.2.0",
|
|
58
|
+
"terser": "^5.17.1",
|
|
59
|
+
"typedoc": "^0.24.4",
|
|
60
|
+
"typescript": "^5.0.4"
|
|
63
61
|
},
|
|
64
62
|
"dependencies": {
|
|
65
63
|
"@maptiler/client": "^1.3.0",
|
|
66
64
|
"events": "^3.3.0",
|
|
67
65
|
"js-base64": "^3.7.4",
|
|
68
|
-
"maplibre-gl": "
|
|
66
|
+
"maplibre-gl": "3.0.0-pre.4",
|
|
69
67
|
"uuid": "^9.0.0"
|
|
70
68
|
}
|
|
71
69
|
}
|
package/readme.md
CHANGED
|
@@ -389,6 +389,114 @@ Languages that are written right-to-left such as arabic and hebrew are fully sup
|
|
|
389
389
|
<img src="images/screenshots/lang-hebrew.jpeg" width="48%"></img>
|
|
390
390
|
</p>
|
|
391
391
|
|
|
392
|
+
# Custom Events and Map Lifecycle
|
|
393
|
+
## Events
|
|
394
|
+
Since the SDK is fully compatible with MapLibre, [all these events](https://maplibre.org/maplibre-gl-js-docs/api/map/#map-events) are available, yet we have added one more: `loadWithTerrain`.
|
|
395
|
+
|
|
396
|
+
The `loadWithTerrain` event is triggered only *once* in a `Map` instance lifecycle, when both the `load` event and the `terrain` event **with non-null terrain** are fired.
|
|
397
|
+
|
|
398
|
+
**Why a new event?**
|
|
399
|
+
When a map is instanciated with the option `terrain: true`, then MapTiler terrain is directly added to it and some animation functions such as `.flyTo()` or `.easeTo()` if started straight after the map initialization will actually need to wait a few milliseconds that the terrain is properly initialized before running.
|
|
400
|
+
Relying on the `load` event to run an animation with a map with terrain may fail in some cases for this reason, and this is why waiting for `loadWithTerrain` is safer in this particular situation.
|
|
401
|
+
|
|
402
|
+
## Lifecycle Methods
|
|
403
|
+
The events `load` and `loadWithTerrain` are both called *at most once* and require a callback function to add more elements such as markers, layers, popups and data sources. Even though MapTiler SDK fully supports this logic, we have also included a *promise* logic to provide a more linear and less nested way to wait for a Map instance to be ready. Let's compare the two ways:
|
|
404
|
+
|
|
405
|
+
- Classic: with a callback on the `load` event:
|
|
406
|
+
```ts
|
|
407
|
+
function init() {
|
|
408
|
+
|
|
409
|
+
const map = new Map({
|
|
410
|
+
container,
|
|
411
|
+
center: [2.34804, 48.85439], // Paris, France
|
|
412
|
+
zoom: 14,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// We wait for the event.
|
|
416
|
+
// Once triggered, the callback is ranin it's own scope.
|
|
417
|
+
map.on("load", (evt) => {
|
|
418
|
+
// Adding a data source
|
|
419
|
+
map.addSource('my-gps-track-source', {
|
|
420
|
+
type: "geojson",
|
|
421
|
+
data: "https://example.com/some-gps-track.geojson",
|
|
422
|
+
});
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
- Modern: with a promise returned by the method `.onLoadAsync()`, used in an `async` function:
|
|
428
|
+
```ts
|
|
429
|
+
async function init() {
|
|
430
|
+
|
|
431
|
+
const map = new Map({
|
|
432
|
+
container,
|
|
433
|
+
center: [2.34804, 48.85439], // Paris, France
|
|
434
|
+
zoom: 14,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// We wait for the promise to resolve.
|
|
438
|
+
// Once triggered, the rest of the init function runs
|
|
439
|
+
await map.onLoadAsync();
|
|
440
|
+
|
|
441
|
+
// Adding a data source
|
|
442
|
+
map.addSource('my-gps-track-source', {
|
|
443
|
+
type: "geojson",
|
|
444
|
+
data: "https://example.com/some-gps-track.geojson",
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
We deployed exactely the same logic for the `loadWithTerrain` event. Let's see how they two ways compares.
|
|
450
|
+
- Classic: with a callback on the `loadWithTerrain` event:
|
|
451
|
+
```ts
|
|
452
|
+
function init() {
|
|
453
|
+
|
|
454
|
+
const map = new Map({
|
|
455
|
+
container,
|
|
456
|
+
center: [2.34804, 48.85439], // Paris, France
|
|
457
|
+
zoom: 14,
|
|
458
|
+
terrain: true,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// We wait for the event.
|
|
462
|
+
// Once triggered, the callback is ran in its own scope.
|
|
463
|
+
map.on("loadWithTerrain", (evt) => {
|
|
464
|
+
// make an animation
|
|
465
|
+
map.flyTo({
|
|
466
|
+
center: [-0.09956, 51.50509], // London, UK
|
|
467
|
+
zoom: 12.5,
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
- Modern: with a promise returned by the method `.onLoadWithTerrainAsync()`, used in an `async` function:
|
|
474
|
+
```ts
|
|
475
|
+
async function init() {
|
|
476
|
+
|
|
477
|
+
const map = new Map({
|
|
478
|
+
container,
|
|
479
|
+
center: [2.34804, 48.85439], // Paris, France
|
|
480
|
+
zoom: 14,
|
|
481
|
+
terrain: true,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// We wait for the promise to resolve.
|
|
485
|
+
// Once triggered, the rest of the init function runs
|
|
486
|
+
await map.onLoadWithTerrainAsync();
|
|
487
|
+
|
|
488
|
+
// make an animation
|
|
489
|
+
map.flyTo({
|
|
490
|
+
center: [-0.09956, 51.50509], // London, UK
|
|
491
|
+
zoom: 12.5,
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
We believe that the *promise* approach is better because it does not nest scopes and will allow for a linear non-nested stream of execution. It also corresponds to more modern development standards.
|
|
497
|
+
|
|
498
|
+
> 📣 *__Note:__* Generally speaking, *promises* are not a go to replacement for all event+callback and are suitable only for events that are called only once in the lifecycle of a Map instance. This is the reason why we have decided to provide a *promise* equivalent only for the `load` and `loadWithTerrain` events.
|
|
499
|
+
|
|
392
500
|
# Easy access to MapTiler Cloud API
|
|
393
501
|
Our map SDK is not only about maps! We also provide plenty of wrapper to our API calls!
|
|
394
502
|
|
package/src/Map.ts
CHANGED
|
@@ -5,6 +5,11 @@ import type {
|
|
|
5
5
|
MapOptions as MapOptionsML,
|
|
6
6
|
ControlPosition,
|
|
7
7
|
StyleOptions,
|
|
8
|
+
MapDataEvent,
|
|
9
|
+
Tile,
|
|
10
|
+
RasterDEMSourceSpecification,
|
|
11
|
+
TerrainSpecification,
|
|
12
|
+
MapTerrainEvent,
|
|
8
13
|
} from "maplibre-gl";
|
|
9
14
|
import { v4 as uuidv4 } from "uuid";
|
|
10
15
|
import { ReferenceMapStyle, MapStyleVariant } from "@maptiler/client";
|
|
@@ -27,6 +32,19 @@ import { AttributionControl } from "./AttributionControl";
|
|
|
27
32
|
import { ScaleControl } from "./ScaleControl";
|
|
28
33
|
import { FullscreenControl } from "./FullscreenControl";
|
|
29
34
|
|
|
35
|
+
function sleepAsync(ms: number) {
|
|
36
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type LoadWithTerrainEvent = {
|
|
40
|
+
type: "loadWithTerrain";
|
|
41
|
+
target: Map;
|
|
42
|
+
terrain: {
|
|
43
|
+
source: string;
|
|
44
|
+
exaggeration: number;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
30
48
|
// StyleSwapOptions is not exported by Maplibre, but we can redefine it (used for setStyle)
|
|
31
49
|
export type TransformStyleFunction = (
|
|
32
50
|
previous: StyleSpecification,
|
|
@@ -48,6 +66,13 @@ export const GeolocationType: {
|
|
|
48
66
|
COUNTRY: "COUNTRY",
|
|
49
67
|
} as const;
|
|
50
68
|
|
|
69
|
+
type MapTerrainDataEvent = MapDataEvent & {
|
|
70
|
+
isSourceLoaded: boolean;
|
|
71
|
+
tile: Tile;
|
|
72
|
+
sourceId: string;
|
|
73
|
+
source: RasterDEMSourceSpecification;
|
|
74
|
+
};
|
|
75
|
+
|
|
51
76
|
/**
|
|
52
77
|
* Options to provide to the `Map` constructor
|
|
53
78
|
*/
|
|
@@ -132,7 +157,7 @@ export type MapOptions = Omit<MapOptionsML, "style" | "maplibreLogo"> & {
|
|
|
132
157
|
*
|
|
133
158
|
* Default: `false`
|
|
134
159
|
*/
|
|
135
|
-
geolocate?: typeof GeolocationType[keyof typeof GeolocationType] | boolean;
|
|
160
|
+
geolocate?: (typeof GeolocationType)[keyof typeof GeolocationType] | boolean;
|
|
136
161
|
};
|
|
137
162
|
|
|
138
163
|
/**
|
|
@@ -143,6 +168,8 @@ export class Map extends maplibregl.Map {
|
|
|
143
168
|
private terrainExaggeration = 1;
|
|
144
169
|
private primaryLanguage: LanguageString | null = null;
|
|
145
170
|
private secondaryLanguage: LanguageString | null = null;
|
|
171
|
+
private terrainGrowing = false;
|
|
172
|
+
private terrainFlattening = false;
|
|
146
173
|
|
|
147
174
|
constructor(options: MapOptions) {
|
|
148
175
|
if (options.apiKey) {
|
|
@@ -198,6 +225,8 @@ export class Map extends maplibregl.Map {
|
|
|
198
225
|
|
|
199
226
|
this.primaryLanguage = options.language ?? config.primaryLanguage;
|
|
200
227
|
this.secondaryLanguage = config.secondaryLanguage;
|
|
228
|
+
this.terrainExaggeration =
|
|
229
|
+
options.terrainExaggeration ?? this.terrainExaggeration;
|
|
201
230
|
|
|
202
231
|
// Map centering and geolocation
|
|
203
232
|
this.once("styledata", async () => {
|
|
@@ -239,6 +268,11 @@ export class Map extends maplibregl.Map {
|
|
|
239
268
|
console.warn(e.message);
|
|
240
269
|
}
|
|
241
270
|
|
|
271
|
+
// A more precise localization
|
|
272
|
+
|
|
273
|
+
// This more advanced localization is commented out because the easeTo animation
|
|
274
|
+
// triggers an error if the terrain grow is enabled (due to being nable to project the center while moving)
|
|
275
|
+
|
|
242
276
|
// Then, the get a more precise location, we rely on the browser location, but only if it was already granted
|
|
243
277
|
// before (we don't want to ask wih a popup at launch time)
|
|
244
278
|
const locationResult = await navigator.permissions.query({
|
|
@@ -254,11 +288,21 @@ export class Map extends maplibregl.Map {
|
|
|
254
288
|
return;
|
|
255
289
|
}
|
|
256
290
|
|
|
257
|
-
this.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
291
|
+
if (this.terrain) {
|
|
292
|
+
this.easeTo({
|
|
293
|
+
center: [data.coords.longitude, data.coords.latitude],
|
|
294
|
+
zoom: options.zoom || 12,
|
|
295
|
+
duration: 2000,
|
|
296
|
+
});
|
|
297
|
+
} else {
|
|
298
|
+
this.once("terrain", () => {
|
|
299
|
+
this.easeTo({
|
|
300
|
+
center: [data.coords.longitude, data.coords.latitude],
|
|
301
|
+
zoom: options.zoom || 12,
|
|
302
|
+
duration: 2000,
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
}
|
|
262
306
|
},
|
|
263
307
|
|
|
264
308
|
// error callback
|
|
@@ -422,6 +466,40 @@ export class Map extends maplibregl.Map {
|
|
|
422
466
|
}
|
|
423
467
|
});
|
|
424
468
|
|
|
469
|
+
// Creating a custom event: "loadWithTerrain"
|
|
470
|
+
// that fires only once when both:
|
|
471
|
+
// - the map has full loaded (corresponds to the the "load" event)
|
|
472
|
+
// - the terrain has loaded (corresponds to the "terrain" event with terrain beion non-null)
|
|
473
|
+
// This custom event is necessary to wait for when the map is instanciated with `terrain: true`
|
|
474
|
+
// and some animation (flyTo, easeTo) are running from the begining.
|
|
475
|
+
let loadEventTriggered = false;
|
|
476
|
+
let terrainEventTriggered = false;
|
|
477
|
+
let terrainEventData: LoadWithTerrainEvent = null;
|
|
478
|
+
|
|
479
|
+
this.once("load", (_) => {
|
|
480
|
+
loadEventTriggered = true;
|
|
481
|
+
if (terrainEventTriggered) {
|
|
482
|
+
this.fire("loadWithTerrain", terrainEventData);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const terrainCallback = (evt) => {
|
|
487
|
+
if (!evt.terrain) return;
|
|
488
|
+
terrainEventTriggered = true;
|
|
489
|
+
terrainEventData = {
|
|
490
|
+
type: "loadWithTerrain",
|
|
491
|
+
target: this,
|
|
492
|
+
terrain: evt.terrain,
|
|
493
|
+
};
|
|
494
|
+
this.off("terrain", terrainCallback);
|
|
495
|
+
|
|
496
|
+
if (loadEventTriggered) {
|
|
497
|
+
this.fire("loadWithTerrain", terrainEventData as LoadWithTerrainEvent);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
this.on("terrain", terrainCallback);
|
|
502
|
+
|
|
425
503
|
// enable 3D terrain if provided in options
|
|
426
504
|
if (options.terrain) {
|
|
427
505
|
this.enableTerrain(
|
|
@@ -430,6 +508,43 @@ export class Map extends maplibregl.Map {
|
|
|
430
508
|
}
|
|
431
509
|
}
|
|
432
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Awaits for _this_ Map instance to be "loaded" and returns a Promise to the Map.
|
|
513
|
+
* If _this_ Map instance is already loaded, the Promise is resolved directly,
|
|
514
|
+
* otherwise, it is resolved as a result of the "load" event.
|
|
515
|
+
* @returns
|
|
516
|
+
*/
|
|
517
|
+
async onLoadAsync() {
|
|
518
|
+
return new Promise<Map>((resolve, reject) => {
|
|
519
|
+
if (this.loaded()) {
|
|
520
|
+
return resolve(this);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
this.once("load", (_) => {
|
|
524
|
+
resolve(this);
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Awaits for _this_ Map instance to be "loaded" as well as with terrain being non-null for the first time
|
|
531
|
+
* and returns a Promise to the Map.
|
|
532
|
+
* If _this_ Map instance is already loaded with terrain, the Promise is resolved directly,
|
|
533
|
+
* otherwise, it is resolved as a result of the "loadWithTerrain" event.
|
|
534
|
+
* @returns
|
|
535
|
+
*/
|
|
536
|
+
async onLoadWithTerrainAsync() {
|
|
537
|
+
return new Promise<Map>((resolve, reject) => {
|
|
538
|
+
if (this.loaded() && this.terrain) {
|
|
539
|
+
return resolve(this);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
this.once("loadWithTerrain", (_) => {
|
|
543
|
+
resolve(this);
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
433
548
|
/**
|
|
434
549
|
* Update the style of the map.
|
|
435
550
|
* Can be:
|
|
@@ -789,6 +904,54 @@ export class Map extends maplibregl.Map {
|
|
|
789
904
|
return this.isTerrainEnabled;
|
|
790
905
|
}
|
|
791
906
|
|
|
907
|
+
private growTerrain(exaggeration, durationMs = 1000) {
|
|
908
|
+
// This method assumes the terrain is already built
|
|
909
|
+
if (!this.terrain) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const startTime = performance.now();
|
|
914
|
+
// This is supposedly 0, but it could be something else (e.g. already in the middle of growing, or user defined other)
|
|
915
|
+
const currentExaggeration = this.terrain.exaggeration;
|
|
916
|
+
const deltaExaggeration = exaggeration - currentExaggeration;
|
|
917
|
+
|
|
918
|
+
// This is again called in a requestAnimationFrame ~loop, until the terrain has grown enough
|
|
919
|
+
// that it has reached the target
|
|
920
|
+
const updateExaggeration = () => {
|
|
921
|
+
if (!this.terrain) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// If the flattening animation is triggered while the growing animation
|
|
926
|
+
// is running, then the flattening animation is stopped
|
|
927
|
+
if (this.terrainFlattening) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// normalized value in interval [0, 1] of where we are currently in the animation loop
|
|
932
|
+
const positionInLoop = (performance.now() - startTime) / durationMs;
|
|
933
|
+
|
|
934
|
+
// The animation goes on until we reached 99% of the growing sequence duration
|
|
935
|
+
if (positionInLoop < 0.99) {
|
|
936
|
+
const exaggerationFactor = 1 - Math.pow(1 - positionInLoop, 4);
|
|
937
|
+
const newExaggeration =
|
|
938
|
+
currentExaggeration + exaggerationFactor * deltaExaggeration;
|
|
939
|
+
this.terrain.exaggeration = newExaggeration;
|
|
940
|
+
requestAnimationFrame(updateExaggeration);
|
|
941
|
+
} else {
|
|
942
|
+
this.terrainGrowing = false;
|
|
943
|
+
this.terrainFlattening = false;
|
|
944
|
+
this.terrain.exaggeration = exaggeration;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
this.triggerRepaint();
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
this.terrainGrowing = true;
|
|
951
|
+
this.terrainFlattening = false;
|
|
952
|
+
requestAnimationFrame(updateExaggeration);
|
|
953
|
+
}
|
|
954
|
+
|
|
792
955
|
/**
|
|
793
956
|
* Enables the 3D terrain visualization
|
|
794
957
|
* @param exaggeration
|
|
@@ -800,27 +963,72 @@ export class Map extends maplibregl.Map {
|
|
|
800
963
|
return;
|
|
801
964
|
}
|
|
802
965
|
|
|
803
|
-
|
|
966
|
+
// This function is mapped to a map "data" event. It checks that the terrain
|
|
967
|
+
// tiles are loaded and when so, it starts an animation to make the terrain grow
|
|
968
|
+
const dataEventTerrainGrow = async (evt: MapTerrainDataEvent) => {
|
|
969
|
+
if (!this.terrain) {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (
|
|
974
|
+
evt.type !== "data" ||
|
|
975
|
+
evt.dataType !== "source" ||
|
|
976
|
+
!("source" in evt)
|
|
977
|
+
) {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (evt.sourceId !== "maptiler-terrain") {
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const source = evt.source;
|
|
804
986
|
|
|
987
|
+
if (source.type !== "raster-dem") {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (!evt.isSourceLoaded) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// We shut this event off because we want it to happen only once.
|
|
996
|
+
// Yet, we cannot use the "once" method because only the last event of the series
|
|
997
|
+
// has `isSourceLoaded` true
|
|
998
|
+
this.off("data", dataEventTerrainGrow);
|
|
999
|
+
|
|
1000
|
+
this.growTerrain(exaggeration);
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
// This is put into a function so that it can be called regardless
|
|
1004
|
+
// of the loading state of _this_ the map instance
|
|
805
1005
|
const addTerrain = () => {
|
|
806
1006
|
// When style is changed,
|
|
807
1007
|
this.isTerrainEnabled = true;
|
|
808
1008
|
this.terrainExaggeration = exaggeration;
|
|
809
1009
|
|
|
1010
|
+
// Mapping it to the "data" event so that we can check that the terrain
|
|
1011
|
+
// growing starts only when terrain tiles are loaded (to reduce glitching)
|
|
1012
|
+
this.on("data", dataEventTerrainGrow);
|
|
1013
|
+
|
|
810
1014
|
this.addSource(defaults.terrainSourceId, {
|
|
811
1015
|
type: "raster-dem",
|
|
812
1016
|
url: defaults.terrainSourceURL,
|
|
813
1017
|
});
|
|
1018
|
+
|
|
1019
|
+
// Setting up the terrain with a 0 exaggeration factor
|
|
1020
|
+
// so it loads ~seamlessly and then can grow from there
|
|
814
1021
|
this.setTerrain({
|
|
815
1022
|
source: defaults.terrainSourceId,
|
|
816
|
-
exaggeration:
|
|
1023
|
+
exaggeration: 0,
|
|
817
1024
|
});
|
|
818
1025
|
};
|
|
819
1026
|
|
|
820
1027
|
// The terrain has already been loaded,
|
|
821
1028
|
// we just update the exaggeration.
|
|
822
|
-
if (
|
|
823
|
-
this.
|
|
1029
|
+
if (this.getTerrain()) {
|
|
1030
|
+
this.isTerrainEnabled = true;
|
|
1031
|
+
this.growTerrain(exaggeration);
|
|
824
1032
|
return;
|
|
825
1033
|
}
|
|
826
1034
|
|
|
@@ -840,44 +1048,80 @@ export class Map extends maplibregl.Map {
|
|
|
840
1048
|
* Disable the 3D terrain visualization
|
|
841
1049
|
*/
|
|
842
1050
|
disableTerrain() {
|
|
843
|
-
|
|
844
|
-
this.
|
|
845
|
-
|
|
846
|
-
this.removeSource(defaults.terrainSourceId);
|
|
1051
|
+
// It could be disabled already
|
|
1052
|
+
if (!this.terrain) {
|
|
1053
|
+
return;
|
|
847
1054
|
}
|
|
1055
|
+
|
|
1056
|
+
this.isTerrainEnabled = false;
|
|
1057
|
+
// this.stopFlattening = false;
|
|
1058
|
+
|
|
1059
|
+
// Duration of the animation in millisec
|
|
1060
|
+
const animationLoopDuration = 1 * 1000;
|
|
1061
|
+
const startTime = performance.now();
|
|
1062
|
+
// This is supposedly 0, but it could be something else (e.g. already in the middle of growing, or user defined other)
|
|
1063
|
+
const currentExaggeration = this.terrain.exaggeration;
|
|
1064
|
+
|
|
1065
|
+
// This is again called in a requestAnimationFrame ~loop, until the terrain has grown enough
|
|
1066
|
+
// that it has reached the target
|
|
1067
|
+
const updateExaggeration = () => {
|
|
1068
|
+
if (!this.terrain) {
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// If the growing animation is triggered while flattening,
|
|
1073
|
+
// then we exist the flatening
|
|
1074
|
+
if (this.terrainGrowing) {
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// normalized value in interval [0, 1] of where we are currently in the animation loop
|
|
1079
|
+
const positionInLoop =
|
|
1080
|
+
(performance.now() - startTime) / animationLoopDuration;
|
|
1081
|
+
|
|
1082
|
+
// The animation goes on until we reached 99% of the growing sequence duration
|
|
1083
|
+
if (positionInLoop < 0.99) {
|
|
1084
|
+
const exaggerationFactor = Math.pow(1 - positionInLoop, 4);
|
|
1085
|
+
const newExaggeration = currentExaggeration * exaggerationFactor;
|
|
1086
|
+
this.terrain.exaggeration = newExaggeration;
|
|
1087
|
+
requestAnimationFrame(updateExaggeration);
|
|
1088
|
+
} else {
|
|
1089
|
+
this.terrain.exaggeration = 0;
|
|
1090
|
+
this.terrainGrowing = false;
|
|
1091
|
+
this.terrainFlattening = false;
|
|
1092
|
+
this.setTerrain(null);
|
|
1093
|
+
if (this.getSource(defaults.terrainSourceId)) {
|
|
1094
|
+
this.removeSource(defaults.terrainSourceId);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
this.triggerRepaint();
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
this.terrainGrowing = false;
|
|
1102
|
+
this.terrainFlattening = true;
|
|
1103
|
+
requestAnimationFrame(updateExaggeration);
|
|
848
1104
|
}
|
|
849
1105
|
|
|
850
1106
|
/**
|
|
851
1107
|
* Sets the 3D terrain exageration factor.
|
|
852
|
-
*
|
|
1108
|
+
* If the terrain was not enabled prior to the call of this method,
|
|
1109
|
+
* the method `.enableTerrain()` will be called.
|
|
1110
|
+
* If `animate` is `true`, the terrain transformation will be animated in the span of 1 second.
|
|
1111
|
+
* If `animate` is `false`, no animated transition to the newly defined exaggeration.
|
|
853
1112
|
* @param exaggeration
|
|
1113
|
+
* @param animate
|
|
854
1114
|
*/
|
|
855
|
-
setTerrainExaggeration(exaggeration: number) {
|
|
856
|
-
this.
|
|
1115
|
+
setTerrainExaggeration(exaggeration: number, animate = true) {
|
|
1116
|
+
if (!animate && this.terrain) {
|
|
1117
|
+
this.terrainExaggeration = exaggeration;
|
|
1118
|
+
this.terrain.exaggeration = exaggeration;
|
|
1119
|
+
this.triggerRepaint();
|
|
1120
|
+
} else {
|
|
1121
|
+
this.enableTerrain(exaggeration);
|
|
1122
|
+
}
|
|
857
1123
|
}
|
|
858
1124
|
|
|
859
|
-
// getLanguages() {
|
|
860
|
-
// const layers = this.getStyle().layers;
|
|
861
|
-
|
|
862
|
-
// for (let i = 0; i < layers.length; i += 1) {
|
|
863
|
-
// const layer = layers[i];
|
|
864
|
-
// const layout = layer.layout;
|
|
865
|
-
|
|
866
|
-
// if (!layout) {
|
|
867
|
-
// continue;
|
|
868
|
-
// }
|
|
869
|
-
|
|
870
|
-
// if (!layout["text-field"]) {
|
|
871
|
-
// continue;
|
|
872
|
-
// }
|
|
873
|
-
|
|
874
|
-
// const textFieldLayoutProp = this.getLayoutProperty(
|
|
875
|
-
// layer.id,
|
|
876
|
-
// "text-field"
|
|
877
|
-
// );
|
|
878
|
-
// }
|
|
879
|
-
// }
|
|
880
|
-
|
|
881
1125
|
/**
|
|
882
1126
|
* Perform an action when the style is ready. It could be at the moment of calling this method
|
|
883
1127
|
* or later.
|