@maptiler/sdk 1.0.9 → 1.0.11
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 +14 -0
- package/demos/maptiler-sdk.umd.js +471 -32
- package/dist/maptiler-sdk.d.ts +89 -38
- 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 +471 -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 +291 -40
- package/src/index.ts +4 -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.11",
|
|
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
|
*/
|
|
@@ -74,7 +99,14 @@ export type MapOptions = Omit<MapOptionsML, "style" | "maplibreLogo"> & {
|
|
|
74
99
|
apiKey?: string;
|
|
75
100
|
|
|
76
101
|
/**
|
|
77
|
-
* Shows the MapTiler logo
|
|
102
|
+
* Shows or hides the MapTiler logo in the bottom left corner.
|
|
103
|
+
*
|
|
104
|
+
* For paid plans:
|
|
105
|
+
* - `true` shows MapTiler logo
|
|
106
|
+
* - `false` hodes MapTiler logo
|
|
107
|
+
* - default: `false` (hide)
|
|
108
|
+
*
|
|
109
|
+
* For free plans: MapTiler logo always shows, regardless of the value.
|
|
78
110
|
*/
|
|
79
111
|
maptilerLogo?: boolean;
|
|
80
112
|
|
|
@@ -132,7 +164,7 @@ export type MapOptions = Omit<MapOptionsML, "style" | "maplibreLogo"> & {
|
|
|
132
164
|
*
|
|
133
165
|
* Default: `false`
|
|
134
166
|
*/
|
|
135
|
-
geolocate?: typeof GeolocationType[keyof typeof GeolocationType] | boolean;
|
|
167
|
+
geolocate?: (typeof GeolocationType)[keyof typeof GeolocationType] | boolean;
|
|
136
168
|
};
|
|
137
169
|
|
|
138
170
|
/**
|
|
@@ -143,6 +175,8 @@ export class Map extends maplibregl.Map {
|
|
|
143
175
|
private terrainExaggeration = 1;
|
|
144
176
|
private primaryLanguage: LanguageString | null = null;
|
|
145
177
|
private secondaryLanguage: LanguageString | null = null;
|
|
178
|
+
private terrainGrowing = false;
|
|
179
|
+
private terrainFlattening = false;
|
|
146
180
|
|
|
147
181
|
constructor(options: MapOptions) {
|
|
148
182
|
if (options.apiKey) {
|
|
@@ -198,6 +232,8 @@ export class Map extends maplibregl.Map {
|
|
|
198
232
|
|
|
199
233
|
this.primaryLanguage = options.language ?? config.primaryLanguage;
|
|
200
234
|
this.secondaryLanguage = config.secondaryLanguage;
|
|
235
|
+
this.terrainExaggeration =
|
|
236
|
+
options.terrainExaggeration ?? this.terrainExaggeration;
|
|
201
237
|
|
|
202
238
|
// Map centering and geolocation
|
|
203
239
|
this.once("styledata", async () => {
|
|
@@ -239,6 +275,11 @@ export class Map extends maplibregl.Map {
|
|
|
239
275
|
console.warn(e.message);
|
|
240
276
|
}
|
|
241
277
|
|
|
278
|
+
// A more precise localization
|
|
279
|
+
|
|
280
|
+
// This more advanced localization is commented out because the easeTo animation
|
|
281
|
+
// triggers an error if the terrain grow is enabled (due to being nable to project the center while moving)
|
|
282
|
+
|
|
242
283
|
// Then, the get a more precise location, we rely on the browser location, but only if it was already granted
|
|
243
284
|
// before (we don't want to ask wih a popup at launch time)
|
|
244
285
|
const locationResult = await navigator.permissions.query({
|
|
@@ -254,11 +295,21 @@ export class Map extends maplibregl.Map {
|
|
|
254
295
|
return;
|
|
255
296
|
}
|
|
256
297
|
|
|
257
|
-
this.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
298
|
+
if (this.terrain) {
|
|
299
|
+
this.easeTo({
|
|
300
|
+
center: [data.coords.longitude, data.coords.latitude],
|
|
301
|
+
zoom: options.zoom || 12,
|
|
302
|
+
duration: 2000,
|
|
303
|
+
});
|
|
304
|
+
} else {
|
|
305
|
+
this.once("terrain", () => {
|
|
306
|
+
this.easeTo({
|
|
307
|
+
center: [data.coords.longitude, data.coords.latitude],
|
|
308
|
+
zoom: options.zoom || 12,
|
|
309
|
+
duration: 2000,
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}
|
|
262
313
|
},
|
|
263
314
|
|
|
264
315
|
// error callback
|
|
@@ -422,6 +473,40 @@ export class Map extends maplibregl.Map {
|
|
|
422
473
|
}
|
|
423
474
|
});
|
|
424
475
|
|
|
476
|
+
// Creating a custom event: "loadWithTerrain"
|
|
477
|
+
// that fires only once when both:
|
|
478
|
+
// - the map has full loaded (corresponds to the the "load" event)
|
|
479
|
+
// - the terrain has loaded (corresponds to the "terrain" event with terrain beion non-null)
|
|
480
|
+
// This custom event is necessary to wait for when the map is instanciated with `terrain: true`
|
|
481
|
+
// and some animation (flyTo, easeTo) are running from the begining.
|
|
482
|
+
let loadEventTriggered = false;
|
|
483
|
+
let terrainEventTriggered = false;
|
|
484
|
+
let terrainEventData: LoadWithTerrainEvent = null;
|
|
485
|
+
|
|
486
|
+
this.once("load", (_) => {
|
|
487
|
+
loadEventTriggered = true;
|
|
488
|
+
if (terrainEventTriggered) {
|
|
489
|
+
this.fire("loadWithTerrain", terrainEventData);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const terrainCallback = (evt) => {
|
|
494
|
+
if (!evt.terrain) return;
|
|
495
|
+
terrainEventTriggered = true;
|
|
496
|
+
terrainEventData = {
|
|
497
|
+
type: "loadWithTerrain",
|
|
498
|
+
target: this,
|
|
499
|
+
terrain: evt.terrain,
|
|
500
|
+
};
|
|
501
|
+
this.off("terrain", terrainCallback);
|
|
502
|
+
|
|
503
|
+
if (loadEventTriggered) {
|
|
504
|
+
this.fire("loadWithTerrain", terrainEventData as LoadWithTerrainEvent);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
this.on("terrain", terrainCallback);
|
|
509
|
+
|
|
425
510
|
// enable 3D terrain if provided in options
|
|
426
511
|
if (options.terrain) {
|
|
427
512
|
this.enableTerrain(
|
|
@@ -430,6 +515,43 @@ export class Map extends maplibregl.Map {
|
|
|
430
515
|
}
|
|
431
516
|
}
|
|
432
517
|
|
|
518
|
+
/**
|
|
519
|
+
* Awaits for _this_ Map instance to be "loaded" and returns a Promise to the Map.
|
|
520
|
+
* If _this_ Map instance is already loaded, the Promise is resolved directly,
|
|
521
|
+
* otherwise, it is resolved as a result of the "load" event.
|
|
522
|
+
* @returns
|
|
523
|
+
*/
|
|
524
|
+
async onLoadAsync() {
|
|
525
|
+
return new Promise<Map>((resolve, reject) => {
|
|
526
|
+
if (this.loaded()) {
|
|
527
|
+
return resolve(this);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
this.once("load", (_) => {
|
|
531
|
+
resolve(this);
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Awaits for _this_ Map instance to be "loaded" as well as with terrain being non-null for the first time
|
|
538
|
+
* and returns a Promise to the Map.
|
|
539
|
+
* If _this_ Map instance is already loaded with terrain, the Promise is resolved directly,
|
|
540
|
+
* otherwise, it is resolved as a result of the "loadWithTerrain" event.
|
|
541
|
+
* @returns
|
|
542
|
+
*/
|
|
543
|
+
async onLoadWithTerrainAsync() {
|
|
544
|
+
return new Promise<Map>((resolve, reject) => {
|
|
545
|
+
if (this.loaded() && this.terrain) {
|
|
546
|
+
return resolve(this);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
this.once("loadWithTerrain", (_) => {
|
|
550
|
+
resolve(this);
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
433
555
|
/**
|
|
434
556
|
* Update the style of the map.
|
|
435
557
|
* Can be:
|
|
@@ -789,6 +911,54 @@ export class Map extends maplibregl.Map {
|
|
|
789
911
|
return this.isTerrainEnabled;
|
|
790
912
|
}
|
|
791
913
|
|
|
914
|
+
private growTerrain(exaggeration, durationMs = 1000) {
|
|
915
|
+
// This method assumes the terrain is already built
|
|
916
|
+
if (!this.terrain) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const startTime = performance.now();
|
|
921
|
+
// This is supposedly 0, but it could be something else (e.g. already in the middle of growing, or user defined other)
|
|
922
|
+
const currentExaggeration = this.terrain.exaggeration;
|
|
923
|
+
const deltaExaggeration = exaggeration - currentExaggeration;
|
|
924
|
+
|
|
925
|
+
// This is again called in a requestAnimationFrame ~loop, until the terrain has grown enough
|
|
926
|
+
// that it has reached the target
|
|
927
|
+
const updateExaggeration = () => {
|
|
928
|
+
if (!this.terrain) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// If the flattening animation is triggered while the growing animation
|
|
933
|
+
// is running, then the flattening animation is stopped
|
|
934
|
+
if (this.terrainFlattening) {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// normalized value in interval [0, 1] of where we are currently in the animation loop
|
|
939
|
+
const positionInLoop = (performance.now() - startTime) / durationMs;
|
|
940
|
+
|
|
941
|
+
// The animation goes on until we reached 99% of the growing sequence duration
|
|
942
|
+
if (positionInLoop < 0.99) {
|
|
943
|
+
const exaggerationFactor = 1 - Math.pow(1 - positionInLoop, 4);
|
|
944
|
+
const newExaggeration =
|
|
945
|
+
currentExaggeration + exaggerationFactor * deltaExaggeration;
|
|
946
|
+
this.terrain.exaggeration = newExaggeration;
|
|
947
|
+
requestAnimationFrame(updateExaggeration);
|
|
948
|
+
} else {
|
|
949
|
+
this.terrainGrowing = false;
|
|
950
|
+
this.terrainFlattening = false;
|
|
951
|
+
this.terrain.exaggeration = exaggeration;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
this.triggerRepaint();
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
this.terrainGrowing = true;
|
|
958
|
+
this.terrainFlattening = false;
|
|
959
|
+
requestAnimationFrame(updateExaggeration);
|
|
960
|
+
}
|
|
961
|
+
|
|
792
962
|
/**
|
|
793
963
|
* Enables the 3D terrain visualization
|
|
794
964
|
* @param exaggeration
|
|
@@ -800,27 +970,72 @@ export class Map extends maplibregl.Map {
|
|
|
800
970
|
return;
|
|
801
971
|
}
|
|
802
972
|
|
|
803
|
-
|
|
973
|
+
// This function is mapped to a map "data" event. It checks that the terrain
|
|
974
|
+
// tiles are loaded and when so, it starts an animation to make the terrain grow
|
|
975
|
+
const dataEventTerrainGrow = async (evt: MapTerrainDataEvent) => {
|
|
976
|
+
if (!this.terrain) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (
|
|
981
|
+
evt.type !== "data" ||
|
|
982
|
+
evt.dataType !== "source" ||
|
|
983
|
+
!("source" in evt)
|
|
984
|
+
) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (evt.sourceId !== "maptiler-terrain") {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const source = evt.source;
|
|
993
|
+
|
|
994
|
+
if (source.type !== "raster-dem") {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (!evt.isSourceLoaded) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// We shut this event off because we want it to happen only once.
|
|
1003
|
+
// Yet, we cannot use the "once" method because only the last event of the series
|
|
1004
|
+
// has `isSourceLoaded` true
|
|
1005
|
+
this.off("data", dataEventTerrainGrow);
|
|
1006
|
+
|
|
1007
|
+
this.growTerrain(exaggeration);
|
|
1008
|
+
};
|
|
804
1009
|
|
|
1010
|
+
// This is put into a function so that it can be called regardless
|
|
1011
|
+
// of the loading state of _this_ the map instance
|
|
805
1012
|
const addTerrain = () => {
|
|
806
1013
|
// When style is changed,
|
|
807
1014
|
this.isTerrainEnabled = true;
|
|
808
1015
|
this.terrainExaggeration = exaggeration;
|
|
809
1016
|
|
|
1017
|
+
// Mapping it to the "data" event so that we can check that the terrain
|
|
1018
|
+
// growing starts only when terrain tiles are loaded (to reduce glitching)
|
|
1019
|
+
this.on("data", dataEventTerrainGrow);
|
|
1020
|
+
|
|
810
1021
|
this.addSource(defaults.terrainSourceId, {
|
|
811
1022
|
type: "raster-dem",
|
|
812
1023
|
url: defaults.terrainSourceURL,
|
|
813
1024
|
});
|
|
1025
|
+
|
|
1026
|
+
// Setting up the terrain with a 0 exaggeration factor
|
|
1027
|
+
// so it loads ~seamlessly and then can grow from there
|
|
814
1028
|
this.setTerrain({
|
|
815
1029
|
source: defaults.terrainSourceId,
|
|
816
|
-
exaggeration:
|
|
1030
|
+
exaggeration: 0,
|
|
817
1031
|
});
|
|
818
1032
|
};
|
|
819
1033
|
|
|
820
1034
|
// The terrain has already been loaded,
|
|
821
1035
|
// we just update the exaggeration.
|
|
822
|
-
if (
|
|
823
|
-
this.
|
|
1036
|
+
if (this.getTerrain()) {
|
|
1037
|
+
this.isTerrainEnabled = true;
|
|
1038
|
+
this.growTerrain(exaggeration);
|
|
824
1039
|
return;
|
|
825
1040
|
}
|
|
826
1041
|
|
|
@@ -840,44 +1055,80 @@ export class Map extends maplibregl.Map {
|
|
|
840
1055
|
* Disable the 3D terrain visualization
|
|
841
1056
|
*/
|
|
842
1057
|
disableTerrain() {
|
|
843
|
-
|
|
844
|
-
this.
|
|
845
|
-
|
|
846
|
-
this.removeSource(defaults.terrainSourceId);
|
|
1058
|
+
// It could be disabled already
|
|
1059
|
+
if (!this.terrain) {
|
|
1060
|
+
return;
|
|
847
1061
|
}
|
|
1062
|
+
|
|
1063
|
+
this.isTerrainEnabled = false;
|
|
1064
|
+
// this.stopFlattening = false;
|
|
1065
|
+
|
|
1066
|
+
// Duration of the animation in millisec
|
|
1067
|
+
const animationLoopDuration = 1 * 1000;
|
|
1068
|
+
const startTime = performance.now();
|
|
1069
|
+
// This is supposedly 0, but it could be something else (e.g. already in the middle of growing, or user defined other)
|
|
1070
|
+
const currentExaggeration = this.terrain.exaggeration;
|
|
1071
|
+
|
|
1072
|
+
// This is again called in a requestAnimationFrame ~loop, until the terrain has grown enough
|
|
1073
|
+
// that it has reached the target
|
|
1074
|
+
const updateExaggeration = () => {
|
|
1075
|
+
if (!this.terrain) {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// If the growing animation is triggered while flattening,
|
|
1080
|
+
// then we exist the flatening
|
|
1081
|
+
if (this.terrainGrowing) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// normalized value in interval [0, 1] of where we are currently in the animation loop
|
|
1086
|
+
const positionInLoop =
|
|
1087
|
+
(performance.now() - startTime) / animationLoopDuration;
|
|
1088
|
+
|
|
1089
|
+
// The animation goes on until we reached 99% of the growing sequence duration
|
|
1090
|
+
if (positionInLoop < 0.99) {
|
|
1091
|
+
const exaggerationFactor = Math.pow(1 - positionInLoop, 4);
|
|
1092
|
+
const newExaggeration = currentExaggeration * exaggerationFactor;
|
|
1093
|
+
this.terrain.exaggeration = newExaggeration;
|
|
1094
|
+
requestAnimationFrame(updateExaggeration);
|
|
1095
|
+
} else {
|
|
1096
|
+
this.terrain.exaggeration = 0;
|
|
1097
|
+
this.terrainGrowing = false;
|
|
1098
|
+
this.terrainFlattening = false;
|
|
1099
|
+
this.setTerrain(null);
|
|
1100
|
+
if (this.getSource(defaults.terrainSourceId)) {
|
|
1101
|
+
this.removeSource(defaults.terrainSourceId);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
this.triggerRepaint();
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
this.terrainGrowing = false;
|
|
1109
|
+
this.terrainFlattening = true;
|
|
1110
|
+
requestAnimationFrame(updateExaggeration);
|
|
848
1111
|
}
|
|
849
1112
|
|
|
850
1113
|
/**
|
|
851
1114
|
* Sets the 3D terrain exageration factor.
|
|
852
|
-
*
|
|
1115
|
+
* If the terrain was not enabled prior to the call of this method,
|
|
1116
|
+
* the method `.enableTerrain()` will be called.
|
|
1117
|
+
* If `animate` is `true`, the terrain transformation will be animated in the span of 1 second.
|
|
1118
|
+
* If `animate` is `false`, no animated transition to the newly defined exaggeration.
|
|
853
1119
|
* @param exaggeration
|
|
1120
|
+
* @param animate
|
|
854
1121
|
*/
|
|
855
|
-
setTerrainExaggeration(exaggeration: number) {
|
|
856
|
-
this.
|
|
1122
|
+
setTerrainExaggeration(exaggeration: number, animate = true) {
|
|
1123
|
+
if (!animate && this.terrain) {
|
|
1124
|
+
this.terrainExaggeration = exaggeration;
|
|
1125
|
+
this.terrain.exaggeration = exaggeration;
|
|
1126
|
+
this.triggerRepaint();
|
|
1127
|
+
} else {
|
|
1128
|
+
this.enableTerrain(exaggeration);
|
|
1129
|
+
}
|
|
857
1130
|
}
|
|
858
1131
|
|
|
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
1132
|
/**
|
|
882
1133
|
* Perform an action when the style is ready. It could be at the moment of calling this method
|
|
883
1134
|
* or later.
|