@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maptiler/sdk",
3
- "version": "1.0.9",
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://www.maptiler.com/cloud/",
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/* ; NODE_ENV=production rollup -c",
33
- "dev": "rm -rf dist/* ; NODE_ENV=development rollup -c -w",
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
- "docmd": "rm -rf docsmd/*; typedoc --readme none --plugin typedoc-plugin-markdown --out docsmd src/index.ts; cp -r images docsmd/",
37
- "dochtml": "rm -rf docs/*; typedoc --plugin none --out docs; cp -r images docs/",
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": "^22.0.2",
44
- "@rollup/plugin-json": "^5.0.1",
45
- "@rollup/plugin-node-resolve": "^14.1.0",
46
- "@typescript-eslint/eslint-plugin": "^5.41.0",
47
- "@typescript-eslint/parser": "^5.41.0",
48
- "eslint": "^8.26.0",
49
- "prettier": "^2.7.1",
50
- "rollup": "^2.79.0",
51
- "rollup-plugin-copy-merge": "^0.3.5",
52
- "rollup-plugin-dts": "^4.2.2",
53
- "rollup-plugin-esbuild": "^4.10.1",
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.1",
59
- "terser": "^5.15.0",
60
- "typedoc": "^0.23.21",
61
- "typedoc-plugin-markdown": "^3.13.6",
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": "^3.0.0-pre.3",
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 if `true`. Note that the logo is always displayed on free plan.
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.easeTo({
258
- center: [data.coords.longitude, data.coords.latitude],
259
- zoom: options.zoom || 12,
260
- duration: 2000,
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
- const terrainInfo = this.getTerrain();
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: 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 (terrainInfo) {
823
- this.setTerrain({ ...terrainInfo, exaggeration });
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
- this.isTerrainEnabled = false;
844
- this.setTerrain(null);
845
- if (this.getSource(defaults.terrainSourceId)) {
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
- * Note: this is only a shortcut to `.enableTerrain()`
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.enableTerrain(exaggeration);
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.