@maptiler/sdk 1.1.2 → 1.2.1

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.
Files changed (41) hide show
  1. package/.eslintrc.cjs +15 -5
  2. package/.github/pull_request_template.md +11 -0
  3. package/.github/workflows/format-lint.yml +24 -0
  4. package/CHANGELOG.md +105 -51
  5. package/colorramp.md +93 -0
  6. package/dist/maptiler-sdk.d.ts +1226 -124
  7. package/dist/maptiler-sdk.min.mjs +3 -1
  8. package/dist/maptiler-sdk.mjs +3582 -483
  9. package/dist/maptiler-sdk.mjs.map +1 -1
  10. package/dist/maptiler-sdk.umd.js +4524 -863
  11. package/dist/maptiler-sdk.umd.js.map +1 -1
  12. package/dist/maptiler-sdk.umd.min.js +51 -49
  13. package/package.json +27 -13
  14. package/readme.md +493 -5
  15. package/rollup.config.js +2 -16
  16. package/src/Map.ts +515 -359
  17. package/src/MaptilerGeolocateControl.ts +23 -20
  18. package/src/MaptilerLogoControl.ts +3 -3
  19. package/src/MaptilerNavigationControl.ts +9 -6
  20. package/src/MaptilerTerrainControl.ts +15 -14
  21. package/src/Minimap.ts +373 -0
  22. package/src/Point.ts +3 -5
  23. package/src/colorramp.ts +1216 -0
  24. package/src/config.ts +4 -3
  25. package/src/converters/index.ts +1 -0
  26. package/src/converters/xml.ts +681 -0
  27. package/src/defaults.ts +1 -1
  28. package/src/helpers/index.ts +27 -0
  29. package/src/helpers/stylehelper.ts +395 -0
  30. package/src/helpers/vectorlayerhelpers.ts +1511 -0
  31. package/src/index.ts +90 -121
  32. package/src/language.ts +116 -79
  33. package/src/mapstyle.ts +4 -2
  34. package/src/tools.ts +68 -16
  35. package/tsconfig.json +8 -5
  36. package/vite.config.ts +10 -0
  37. package/demos/maptiler-sdk.css +0 -147
  38. package/demos/maptiler-sdk.umd.js +0 -4041
  39. package/demos/mountain.html +0 -67
  40. package/demos/simple.html +0 -67
  41. package/demos/transform-request.html +0 -81
package/src/Map.ts CHANGED
@@ -4,11 +4,19 @@ import type {
4
4
  StyleSpecification,
5
5
  MapOptions as MapOptionsML,
6
6
  ControlPosition,
7
+ StyleSwapOptions,
7
8
  StyleOptions,
8
9
  MapDataEvent,
9
10
  Tile,
10
11
  RasterDEMSourceSpecification,
11
12
  RequestTransformFunction,
13
+ Source,
14
+ LayerSpecification,
15
+ SourceSpecification,
16
+ CustomLayerInterface,
17
+ FilterSpecification,
18
+ StyleSetterOptions,
19
+ ExpressionSpecification,
12
20
  } from "maplibre-gl";
13
21
  import { ReferenceMapStyle, MapStyleVariant } from "@maptiler/client";
14
22
  import { config, MAPTILER_SESSION_ID, SdkConfig } from "./config";
@@ -30,9 +38,8 @@ import { AttributionControl } from "./AttributionControl";
30
38
  import { ScaleControl } from "./ScaleControl";
31
39
  import { FullscreenControl } from "./FullscreenControl";
32
40
 
33
- function sleepAsync(ms: number) {
34
- return new Promise((resolve) => setTimeout(resolve, ms));
35
- }
41
+ import Minimap from "./Minimap";
42
+ import type { MinimapOptionsInput } from "./Minimap";
36
43
 
37
44
  export type LoadWithTerrainEvent = {
38
45
  type: "loadWithTerrain";
@@ -43,17 +50,6 @@ export type LoadWithTerrainEvent = {
43
50
  };
44
51
  };
45
52
 
46
- // StyleSwapOptions is not exported by Maplibre, but we can redefine it (used for setStyle)
47
- export type TransformStyleFunction = (
48
- previous: StyleSpecification,
49
- next: StyleSpecification
50
- ) => StyleSpecification;
51
-
52
- export type StyleSwapOptions = {
53
- diff?: boolean;
54
- transformStyle?: TransformStyleFunction;
55
- };
56
-
57
53
  export const GeolocationType: {
58
54
  POINT: "POINT";
59
55
  COUNTRY: "COUNTRY";
@@ -141,6 +137,17 @@ export type MapOptions = Omit<MapOptionsML, "style" | "maplibreLogo"> & {
141
137
  */
142
138
  fullscreenControl?: boolean | ControlPosition;
143
139
 
140
+ /**
141
+ * Display a minimap in a user defined corner of the map. (default: `bottom-left` corner)
142
+ * If set to true, the map will assume it is a minimap and forego the attribution control.
143
+ */
144
+ minimap?: boolean | ControlPosition | MinimapOptionsInput;
145
+
146
+ /**
147
+ * attributionControl
148
+ */
149
+ forceNoAttributionControl?: boolean;
150
+
144
151
  /**
145
152
  * Method to position the map at a given geolocation. Only if:
146
153
  * - `hash` is `false`
@@ -169,10 +176,13 @@ export type MapOptions = Omit<MapOptionsML, "style" | "maplibreLogo"> & {
169
176
  export class Map extends maplibregl.Map {
170
177
  private isTerrainEnabled = false;
171
178
  private terrainExaggeration = 1;
172
- private primaryLanguage: LanguageString | null = null;
173
- private secondaryLanguage: LanguageString | null = null;
179
+ private primaryLanguage: LanguageString;
174
180
  private terrainGrowing = false;
175
181
  private terrainFlattening = false;
182
+ private minimap?: Minimap;
183
+ private forceLanguageUpdate: boolean;
184
+ private languageAlwaysBeenStyle: boolean;
185
+ private isReady: boolean = false;
176
186
 
177
187
  constructor(options: MapOptions) {
178
188
  if (options.apiKey) {
@@ -184,7 +194,7 @@ export class Map extends maplibregl.Map {
184
194
 
185
195
  if (!config.apiKey) {
186
196
  console.warn(
187
- "MapTiler Cloud API key is not set. Visit https://maptiler.com and try Cloud for free!"
197
+ "MapTiler Cloud API key is not set. Visit https://maptiler.com and try Cloud for free!",
188
198
  );
189
199
  }
190
200
 
@@ -197,7 +207,12 @@ export class Map extends maplibregl.Map {
197
207
  });
198
208
 
199
209
  this.primaryLanguage = options.language ?? config.primaryLanguage;
200
- this.secondaryLanguage = config.secondaryLanguage;
210
+ this.forceLanguageUpdate =
211
+ this.primaryLanguage === Language.STYLE ||
212
+ this.primaryLanguage === Language.STYLE_LOCK
213
+ ? false
214
+ : true;
215
+ this.languageAlwaysBeenStyle = this.primaryLanguage === Language.STYLE;
201
216
  this.terrainExaggeration =
202
217
  options.terrainExaggeration ?? this.terrainExaggeration;
203
218
 
@@ -228,17 +243,17 @@ export class Map extends maplibregl.Map {
228
243
  }
229
244
  } catch (e) {
230
245
  // not raising
231
- console.warn(e.message);
246
+ console.warn((e as Error).message);
232
247
  }
233
248
 
234
249
  // As a fallback, we want to center the map on the visitor. First with IP geolocation...
235
- let ipLocatedCameraHash = null;
250
+ let ipLocatedCameraHash: string;
236
251
  try {
237
252
  await this.centerOnIpPoint(options.zoom);
238
253
  ipLocatedCameraHash = this.getCameraHash();
239
254
  } catch (e) {
240
255
  // not raising
241
- console.warn(e.message);
256
+ console.warn((e as Error).message);
242
257
  }
243
258
 
244
259
  // A more precise localization
@@ -286,7 +301,7 @@ export class Map extends maplibregl.Map {
286
301
  maximumAge: 24 * 3600 * 1000, // a day in millisec
287
302
  timeout: 5000, // milliseconds
288
303
  enableHighAccuracy: false,
289
- }
304
+ },
290
305
  );
291
306
  }
292
307
  });
@@ -294,7 +309,6 @@ export class Map extends maplibregl.Map {
294
309
  // If the config includes language changing, we must update the map language
295
310
  this.on("styledata", () => {
296
311
  this.setPrimaryLanguage(this.primaryLanguage);
297
- this.setSecondaryLanguage(this.secondaryLanguage);
298
312
  });
299
313
 
300
314
  // this even is in charge of reaplying the terrain elevation after the
@@ -321,12 +335,15 @@ export class Map extends maplibregl.Map {
321
335
  const possibleSources = Object.keys(this.style.sourceCaches)
322
336
  .map((sourceName) => this.getSource(sourceName))
323
337
  .filter(
324
- (s: any) =>
325
- typeof s.url === "string" && s.url.includes("tiles.json")
338
+ (s: Source | undefined) =>
339
+ s &&
340
+ "url" in s &&
341
+ typeof s.url === "string" &&
342
+ s?.url.includes("tiles.json"),
326
343
  );
327
344
 
328
345
  const styleUrl = new URL(
329
- (possibleSources[0] as maplibregl.VectorTileSource).url
346
+ (possibleSources[0] as maplibregl.VectorTileSource).url,
330
347
  );
331
348
 
332
349
  if (!styleUrl.searchParams.has("key")) {
@@ -340,24 +357,26 @@ export class Map extends maplibregl.Map {
340
357
  }
341
358
 
342
359
  // The attribution and logo must show when required
343
- if ("logo" in tileJsonContent && tileJsonContent.logo) {
344
- const logoURL: string = tileJsonContent.logo;
360
+ if (options.forceNoAttributionControl !== true) {
361
+ if ("logo" in tileJsonContent && tileJsonContent.logo) {
362
+ const logoURL: string = tileJsonContent.logo;
345
363
 
346
- this.addControl(
347
- new MaptilerLogoControl({ logoURL }),
348
- options.logoPosition
349
- );
350
-
351
- // if attribution in option is `false` but the the logo shows up in the tileJson, then the attribution must show anyways
352
- if (options.attributionControl === false) {
353
364
  this.addControl(
354
- new AttributionControl({
355
- customAttribution: options.customAttribution,
356
- })
365
+ new MaptilerLogoControl({ logoURL }),
366
+ options.logoPosition,
357
367
  );
368
+
369
+ // if attribution in option is `false` but the the logo shows up in the tileJson, then the attribution must show anyways
370
+ if (options.attributionControl === false) {
371
+ this.addControl(
372
+ new AttributionControl({
373
+ customAttribution: options.customAttribution,
374
+ }),
375
+ );
376
+ }
377
+ } else if (options.maptilerLogo) {
378
+ this.addControl(new MaptilerLogoControl(), options.logoPosition);
358
379
  }
359
- } else if (options.maptilerLogo) {
360
- this.addControl(new MaptilerLogoControl(), options.logoPosition);
361
380
  }
362
381
 
363
382
  // the other controls at init time but be after
@@ -414,7 +433,7 @@ export class Map extends maplibregl.Map {
414
433
  showAccuracyCircle: true,
415
434
  showUserLocation: true,
416
435
  }),
417
- position
436
+ position,
418
437
  );
419
438
  }
420
439
 
@@ -441,26 +460,89 @@ export class Map extends maplibregl.Map {
441
460
 
442
461
  this.addControl(new FullscreenControl({}), position);
443
462
  }
463
+
464
+ this.isReady = true;
465
+ this.fire("ready", { target: this });
444
466
  });
445
467
 
446
468
  // Creating a custom event: "loadWithTerrain"
447
469
  // that fires only once when both:
448
- // - the map has full loaded (corresponds to the the "load" event)
470
+ // - the map has full ready (corresponds to the the "ready" event)
449
471
  // - the terrain has loaded (corresponds to the "terrain" event with terrain beion non-null)
450
472
  // This custom event is necessary to wait for when the map is instanciated with `terrain: true`
451
473
  // and some animation (flyTo, easeTo) are running from the begining.
452
474
  let loadEventTriggered = false;
453
475
  let terrainEventTriggered = false;
454
- let terrainEventData: LoadWithTerrainEvent = null;
476
+ let terrainEventData: LoadWithTerrainEvent;
455
477
 
456
- this.once("load", (_) => {
478
+ this.once("ready", () => {
457
479
  loadEventTriggered = true;
458
480
  if (terrainEventTriggered) {
459
481
  this.fire("loadWithTerrain", terrainEventData);
460
482
  }
461
483
  });
462
484
 
463
- const terrainCallback = (evt) => {
485
+ this.once("style.load", () => {
486
+ const { minimap } = options;
487
+ if (typeof minimap === "object") {
488
+ const {
489
+ zoom,
490
+ center,
491
+ style,
492
+ language,
493
+ apiKey,
494
+ maptilerLogo,
495
+ antialias,
496
+ refreshExpiredTiles,
497
+ maxBounds,
498
+ scrollZoom,
499
+ minZoom,
500
+ maxZoom,
501
+ boxZoom,
502
+ locale,
503
+ fadeDuration,
504
+ crossSourceCollisions,
505
+ clickTolerance,
506
+ bounds,
507
+ fitBoundsOptions,
508
+ pixelRatio,
509
+ validateStyle,
510
+ } = options;
511
+ this.minimap = new Minimap(minimap, {
512
+ zoom,
513
+ center,
514
+ style,
515
+ language,
516
+ apiKey,
517
+ container: "null",
518
+ maptilerLogo,
519
+ antialias,
520
+ refreshExpiredTiles,
521
+ maxBounds,
522
+ scrollZoom,
523
+ minZoom,
524
+ maxZoom,
525
+ boxZoom,
526
+ locale,
527
+ fadeDuration,
528
+ crossSourceCollisions,
529
+ clickTolerance,
530
+ bounds,
531
+ fitBoundsOptions,
532
+ pixelRatio,
533
+ validateStyle,
534
+ });
535
+ this.addControl(this.minimap, minimap.position ?? "bottom-left");
536
+ } else if (minimap === true) {
537
+ this.minimap = new Minimap({}, options);
538
+ this.addControl(this.minimap, "bottom-left");
539
+ } else if (minimap !== undefined && minimap !== false) {
540
+ this.minimap = new Minimap({}, options);
541
+ this.addControl(this.minimap, minimap);
542
+ }
543
+ });
544
+
545
+ const terrainCallback = (evt: LoadWithTerrainEvent) => {
464
546
  if (!evt.terrain) return;
465
547
  terrainEventTriggered = true;
466
548
  terrainEventData = {
@@ -480,7 +562,7 @@ export class Map extends maplibregl.Map {
480
562
  // enable 3D terrain if provided in options
481
563
  if (options.terrain) {
482
564
  this.enableTerrain(
483
- options.terrainExaggeration ?? this.terrainExaggeration
565
+ options.terrainExaggeration ?? this.terrainExaggeration,
484
566
  );
485
567
  }
486
568
  }
@@ -492,12 +574,32 @@ export class Map extends maplibregl.Map {
492
574
  * @returns
493
575
  */
494
576
  async onLoadAsync() {
495
- return new Promise<Map>((resolve, reject) => {
577
+ return new Promise<Map>((resolve) => {
496
578
  if (this.loaded()) {
497
579
  return resolve(this);
498
580
  }
499
581
 
500
- this.once("load", (_) => {
582
+ this.once("load", () => {
583
+ resolve(this);
584
+ });
585
+ });
586
+ }
587
+
588
+ /**
589
+ * Awaits for _this_ Map instance to be "ready" and returns a Promise to the Map.
590
+ * If _this_ Map instance is already ready, the Promise is resolved directly,
591
+ * otherwise, it is resolved as a result of the "ready" event.
592
+ * A map instance is "ready" when all the controls that can be managed by the contructor are
593
+ * dealt with. This happens after the "load" event, due to the asynchronous nature
594
+ * of some built-in controls.
595
+ */
596
+ async onReadyAsync() {
597
+ return new Promise<Map>((resolve) => {
598
+ if (this.isReady) {
599
+ return resolve(this);
600
+ }
601
+
602
+ this.once("ready", () => {
501
603
  resolve(this);
502
604
  });
503
605
  });
@@ -511,12 +613,12 @@ export class Map extends maplibregl.Map {
511
613
  * @returns
512
614
  */
513
615
  async onLoadWithTerrainAsync() {
514
- return new Promise<Map>((resolve, reject) => {
515
- if (this.loaded() && this.terrain) {
616
+ return new Promise<Map>((resolve) => {
617
+ if (this.isReady && this.terrain) {
516
618
  return resolve(this);
517
619
  }
518
620
 
519
- this.once("loadWithTerrain", (_) => {
621
+ this.once("loadWithTerrain", () => {
520
622
  resolve(this);
521
623
  });
522
624
  });
@@ -528,340 +630,379 @@ export class Map extends maplibregl.Map {
528
630
  * - a full style URL (possibly with API key)
529
631
  * - a shorthand with only the MapTIler style name (eg. `"streets-v2"`)
530
632
  * - a longer form with the prefix `"maptiler://"` (eg. `"maptiler://streets-v2"`)
531
- * @param style
532
- * @param options
533
- * @returns
534
633
  */
535
- setStyle(
536
- style: ReferenceMapStyle | MapStyleVariant | StyleSpecification | string,
537
- options?: StyleSwapOptions & StyleOptions
538
- ) {
634
+ override setStyle(
635
+ style:
636
+ | null
637
+ | ReferenceMapStyle
638
+ | MapStyleVariant
639
+ | StyleSpecification
640
+ | string,
641
+ options?: StyleSwapOptions & StyleOptions,
642
+ ): this {
643
+ this.minimap?.setStyle(style);
644
+ this.forceLanguageUpdate = true;
645
+
646
+ this.once("idle", () => {
647
+ this.forceLanguageUpdate = false;
648
+ });
649
+
539
650
  return super.setStyle(styleToStyle(style), options);
540
651
  }
541
652
 
542
653
  /**
543
- * Define the primary language of the map. Note that not all the languages shorthands provided are available.
544
- * This function is a short for `.setPrimaryLanguage()`
545
- * @param language
654
+ * Adds a [MapLibre style layer](https://maplibre.org/maplibre-style-spec/layers)
655
+ * to the map's style.
656
+ *
657
+ * A layer defines how data from a specified source will be styled. Read more about layer types
658
+ * and available paint and layout properties in the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/layers).
659
+ *
660
+ * @param layer - The layer to add,
661
+ * conforming to either the MapLibre Style Specification's [layer definition](https://maplibre.org/maplibre-style-spec/layers) or,
662
+ * less commonly, the {@link CustomLayerInterface} specification.
663
+ * The MapLibre Style Specification's layer definition is appropriate for most layers.
664
+ *
665
+ * @param beforeId - The ID of an existing layer to insert the new layer before,
666
+ * resulting in the new layer appearing visually beneath the existing layer.
667
+ * If this argument is not specified, the layer will be appended to the end of the layers array
668
+ * and appear visually above all other layers.
669
+ *
670
+ * @returns `this`
546
671
  */
547
- setLanguage(language: LanguageString = defaults.primaryLanguage) {
548
- if (language === Language.AUTO) {
549
- return this.setLanguage(getBrowserLanguage());
550
- }
551
- this.setPrimaryLanguage(language);
672
+ addLayer(
673
+ layer:
674
+ | (LayerSpecification & {
675
+ source?: string | SourceSpecification;
676
+ })
677
+ | CustomLayerInterface,
678
+ beforeId?: string,
679
+ ): this {
680
+ this.minimap?.addLayer(layer, beforeId);
681
+ return super.addLayer(layer, beforeId);
552
682
  }
553
683
 
554
684
  /**
555
- * Define the primary language of the map. Note that not all the languages shorthands provided are available.
556
- * @param language
685
+ * Moves a layer to a different z-position.
686
+ *
687
+ * @param id - The ID of the layer to move.
688
+ * @param beforeId - The ID of an existing layer to insert the new layer before. When viewing the map, the `id` layer will appear beneath the `beforeId` layer. If `beforeId` is omitted, the layer will be appended to the end of the layers array and appear above all other layers on the map.
689
+ * @returns `this`
690
+ *
691
+ * @example
692
+ * Move a layer with ID 'polygon' before the layer with ID 'country-label'. The `polygon` layer will appear beneath the `country-label` layer on the map.
693
+ * ```ts
694
+ * map.moveLayer('polygon', 'country-label');
695
+ * ```
557
696
  */
558
- setPrimaryLanguage(language: LanguageString = defaults.primaryLanguage) {
559
- if (this.primaryLanguage === Language.STYLE_LOCK) {
560
- console.warn(
561
- "The language cannot be changed because this map has been instantiated with the STYLE_LOCK language flag."
562
- );
563
- return;
564
- }
565
-
566
- if (!isLanguageSupported(language as string)) {
567
- return;
568
- }
569
-
570
- this.primaryLanguage = language;
571
-
572
- this.onStyleReady(() => {
573
- if (language === Language.AUTO) {
574
- return this.setPrimaryLanguage(getBrowserLanguage());
575
- }
576
-
577
- const layers = this.getStyle().layers;
697
+ moveLayer(id: string, beforeId?: string): this {
698
+ this.minimap?.moveLayer(id, beforeId);
699
+ return super.moveLayer(id, beforeId);
700
+ }
578
701
 
579
- // detects pattern like "{name:somelanguage}" with loose spacing
580
- const strLanguageRegex = /^\s*{\s*name\s*(:\s*(\S*))?\s*}$/;
702
+ /**
703
+ * Removes the layer with the given ID from the map's style.
704
+ *
705
+ * An {@link ErrorEvent} will be fired if the image parameter is invald.
706
+ *
707
+ * @param id - The ID of the layer to remove
708
+ * @returns `this`
709
+ *
710
+ * @example
711
+ * If a layer with ID 'state-data' exists, remove it.
712
+ * ```ts
713
+ * if (map.getLayer('state-data')) map.removeLayer('state-data');
714
+ * ```
715
+ */
716
+ removeLayer(id: string): this {
717
+ this.minimap?.removeLayer(id);
718
+ return super.removeLayer(id);
719
+ }
581
720
 
582
- // detects pattern like "name:somelanguage" with loose spacing
583
- const strLanguageInArrayRegex = /^\s*name\s*(:\s*(\S*))?\s*$/;
721
+ /**
722
+ * Sets the zoom extent for the specified style layer. The zoom extent includes the
723
+ * [minimum zoom level](https://maplibre.org/maplibre-style-spec/layers/#minzoom)
724
+ * and [maximum zoom level](https://maplibre.org/maplibre-style-spec/layers/#maxzoom))
725
+ * at which the layer will be rendered.
726
+ *
727
+ * Note: For style layers using vector sources, style layers cannot be rendered at zoom levels lower than the
728
+ * minimum zoom level of the _source layer_ because the data does not exist at those zoom levels. If the minimum
729
+ * zoom level of the source layer is higher than the minimum zoom level defined in the style layer, the style
730
+ * layer will not be rendered at all zoom levels in the zoom range.
731
+ */
732
+ setLayerZoomRange(layerId: string, minzoom: number, maxzoom: number): this {
733
+ this.minimap?.setLayerZoomRange(layerId, minzoom, maxzoom);
734
+ return super.setLayerZoomRange(layerId, minzoom, maxzoom);
735
+ }
584
736
 
585
- // for string based bilingual lang such as "{name:latin} {name:nonlatin}" or "{name:latin} {name}"
586
- const strBilingualRegex =
587
- /^\s*{\s*name\s*(:\s*(\S*))?\s*}(\s*){\s*name\s*(:\s*(\S*))?\s*}$/;
737
+ /**
738
+ * Sets the filter for the specified style layer.
739
+ *
740
+ * Filters control which features a style layer renders from its source.
741
+ * Any feature for which the filter expression evaluates to `true` will be
742
+ * rendered on the map. Those that are false will be hidden.
743
+ *
744
+ * Use `setFilter` to show a subset of your source data.
745
+ *
746
+ * To clear the filter, pass `null` or `undefined` as the second parameter.
747
+ */
748
+ setFilter(
749
+ layerId: string,
750
+ filter?: FilterSpecification | null,
751
+ options?: StyleSetterOptions,
752
+ ): this {
753
+ this.minimap?.setFilter(layerId, filter, options);
754
+ return super.setFilter(layerId, filter, options);
755
+ }
588
756
 
589
- // Regex to capture when there are more info, such as mountains elevation with unit m/ft
590
- const strMoreInfoRegex = /^(.*)({\s*name\s*(:\s*(\S*))?\s*})(.*)$/;
757
+ /**
758
+ * Sets the value of a paint property in the specified style layer.
759
+ *
760
+ * @param layerId - The ID of the layer to set the paint property in.
761
+ * @param name - The name of the paint property to set.
762
+ * @param value - The value of the paint property to set.
763
+ * Must be of a type appropriate for the property, as defined in the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/).
764
+ * @param options - Options object.
765
+ * @returns `this`
766
+ * @example
767
+ * ```ts
768
+ * map.setPaintProperty('my-layer', 'fill-color', '#faafee');
769
+ * ```
770
+ */
771
+ setPaintProperty(
772
+ layerId: string,
773
+ name: string,
774
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
775
+ value: any,
776
+ options?: StyleSetterOptions,
777
+ ): this {
778
+ this.minimap?.setPaintProperty(layerId, name, value, options);
779
+ return super.setPaintProperty(layerId, name, value, options);
780
+ }
591
781
 
592
- const langStr = language ? `name:${language}` : "name"; // to handle local lang
593
- const replacer = [
594
- "case",
595
- ["has", langStr],
596
- ["get", langStr],
597
- ["get", "name"],
598
- ];
782
+ /**
783
+ * Sets the value of a layout property in the specified style layer.
784
+ * Layout properties define how the layer is styled.
785
+ * Layout properties for layers of the same type are documented together.
786
+ * Layers of different types have different layout properties.
787
+ * See the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/) for the complete list of layout properties.
788
+ * @param layerId - The ID of the layer to set the layout property in.
789
+ * @param name - The name of the layout property to set.
790
+ * @param value - The value of the layout property to set.
791
+ * Must be of a type appropriate for the property, as defined in the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/).
792
+ * @param options - Options object.
793
+ * @returns `this`
794
+ */
795
+ setLayoutProperty(
796
+ layerId: string,
797
+ name: string,
798
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
799
+ value: any,
800
+ options?: StyleSetterOptions,
801
+ ): this {
802
+ this.minimap?.setLayoutProperty(layerId, name, value, options);
803
+ return super.setLayoutProperty(layerId, name, value, options);
804
+ }
599
805
 
600
- for (let i = 0; i < layers.length; i += 1) {
601
- const layer = layers[i];
602
- const layout = layer.layout;
806
+ /**
807
+ * Sets the value of the style's glyphs property.
808
+ *
809
+ * @param glyphsUrl - Glyph URL to set. Must conform to the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/glyphs/).
810
+ * @param options - Options object.
811
+ * @returns `this`
812
+ * @example
813
+ * ```ts
814
+ * map.setGlyphs('https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf');
815
+ * ```
816
+ */
817
+ setGlyphs(glyphsUrl: string | null, options?: StyleSetterOptions): this {
818
+ this.minimap?.setGlyphs(glyphsUrl, options);
819
+ return super.setGlyphs(glyphsUrl, options);
820
+ }
603
821
 
604
- if (!layout) {
605
- continue;
606
- }
822
+ private getStyleLanguage(): string | null {
823
+ if (!this.style.stylesheet.metadata) return null;
824
+ if (typeof this.style.stylesheet.metadata !== "object") return null;
607
825
 
608
- if (!layout["text-field"]) {
609
- continue;
610
- }
826
+ if (
827
+ "maptiler:language" in this.style.stylesheet.metadata &&
828
+ typeof this.style.stylesheet.metadata["maptiler:language"] === "string"
829
+ ) {
830
+ return this.style.stylesheet.metadata["maptiler:language"];
831
+ } else {
832
+ return null;
833
+ }
834
+ }
611
835
 
612
- const textFieldLayoutProp = this.getLayoutProperty(
613
- layer.id,
614
- "text-field"
615
- );
836
+ /**
837
+ * Define the primary language of the map. Note that not all the languages shorthands provided are available.
838
+ */
839
+ setLanguage(language: LanguageString | string): void {
840
+ this.minimap?.map?.setLanguage(language);
841
+ this.onStyleReady(() => {
842
+ this.setPrimaryLanguage(language);
843
+ });
844
+ }
616
845
 
617
- // Note:
618
- // The value of the 'text-field' property can take multiple shape;
619
- // 1. can be an array with 'concat' on its first element (most likely means bilingual)
620
- // 2. can be an array with 'get' on its first element (monolingual)
621
- // 3. can be a string of shape '{name:latin}'
622
- // 4. can be a string referencing another prop such as '{housenumber}' or '{ref}'
623
- //
624
- // The case 1, 2 and 3 will be updated while maintaining their original type and shape.
625
- // The case 3 will not be updated
626
-
627
- let regexMatch;
628
-
629
- // This is case 1
630
- if (
631
- Array.isArray(textFieldLayoutProp) &&
632
- textFieldLayoutProp.length >= 2 &&
633
- textFieldLayoutProp[0].trim().toLowerCase() === "concat"
634
- ) {
635
- const newProp = textFieldLayoutProp.slice(); // newProp is Array
636
- // The style could possibly have defined more than 2 concatenated language strings but we only want to edit the first
637
- // The style could also define that there are more things being concatenated and not only languages
638
-
639
- for (let j = 0; j < textFieldLayoutProp.length; j += 1) {
640
- const elem = textFieldLayoutProp[j];
641
-
642
- // we are looking for an elem of shape '{name:somelangage}' (string) of `["get", "name:somelanguage"]` (array)
643
-
644
- // the entry of of shape '{name:somelangage}', possibly with loose spacing
645
- if (
646
- (typeof elem === "string" || elem instanceof String) &&
647
- strLanguageRegex.exec(elem.toString())
648
- ) {
649
- newProp[j] = replacer;
650
- break; // we just want to update the primary language
651
- }
652
- // the entry is of an array of shape `["get", "name:somelanguage"]`
653
- else if (
654
- Array.isArray(elem) &&
655
- elem.length >= 2 &&
656
- elem[0].trim().toLowerCase() === "get" &&
657
- strLanguageInArrayRegex.exec(elem[1].toString())
658
- ) {
659
- newProp[j] = replacer;
660
- break; // we just want to update the primary language
661
- } else if (
662
- Array.isArray(elem) &&
663
- elem.length === 4 &&
664
- elem[0].trim().toLowerCase() === "case"
665
- ) {
666
- newProp[j] = replacer;
667
- break; // we just want to update the primary language
668
- }
669
- }
846
+ /**
847
+ * Define the primary language of the map. Note that not all the languages shorthands provided are available.
848
+ */
670
849
 
671
- this.setLayoutProperty(layer.id, "text-field", newProp);
672
- }
850
+ private setPrimaryLanguage(language: LanguageString | string) {
851
+ const styleLanguage = this.getStyleLanguage();
852
+
853
+ // If the language is set to `STYLE` (which is the SDK default), but the language defined in
854
+ // the style is `auto`, we need to bypass some verification and modify the languages anyway
855
+ if (
856
+ !(
857
+ language === Language.STYLE &&
858
+ (styleLanguage === Language.AUTO || styleLanguage === Language.VISITOR)
859
+ )
860
+ ) {
861
+ if (language !== Language.STYLE) {
862
+ this.languageAlwaysBeenStyle = false;
863
+ }
673
864
 
674
- // This is case 2
675
- else if (
676
- Array.isArray(textFieldLayoutProp) &&
677
- textFieldLayoutProp.length >= 2 &&
678
- textFieldLayoutProp[0].trim().toLowerCase() === "get" &&
679
- strLanguageInArrayRegex.exec(textFieldLayoutProp[1].toString())
680
- ) {
681
- const newProp = replacer;
682
- this.setLayoutProperty(layer.id, "text-field", newProp);
683
- }
865
+ if (this.languageAlwaysBeenStyle) {
866
+ return;
867
+ }
684
868
 
685
- // This is case 3
686
- else if (
687
- (typeof textFieldLayoutProp === "string" ||
688
- textFieldLayoutProp instanceof String) &&
689
- strLanguageRegex.exec(textFieldLayoutProp.toString())
690
- ) {
691
- const newProp = replacer;
692
- this.setLayoutProperty(layer.id, "text-field", newProp);
693
- } else if (
694
- Array.isArray(textFieldLayoutProp) &&
695
- textFieldLayoutProp.length === 4 &&
696
- textFieldLayoutProp[0].trim().toLowerCase() === "case"
697
- ) {
698
- const newProp = replacer;
699
- this.setLayoutProperty(layer.id, "text-field", newProp);
700
- } else if (
701
- (typeof textFieldLayoutProp === "string" ||
702
- textFieldLayoutProp instanceof String) &&
703
- (regexMatch = strBilingualRegex.exec(
704
- textFieldLayoutProp.toString()
705
- )) !== null
706
- ) {
707
- const newProp = `{${langStr}}${regexMatch[3]}{name${
708
- regexMatch[4] || ""
709
- }}`;
710
- this.setLayoutProperty(layer.id, "text-field", newProp);
711
- } else if (
712
- (typeof textFieldLayoutProp === "string" ||
713
- textFieldLayoutProp instanceof String) &&
714
- (regexMatch = strMoreInfoRegex.exec(
715
- textFieldLayoutProp.toString()
716
- )) !== null
717
- ) {
718
- const newProp = `${regexMatch[1]}{${langStr}}${regexMatch[5]}`;
719
- this.setLayoutProperty(layer.id, "text-field", newProp);
720
- }
869
+ // No need to change the language
870
+ if (this.primaryLanguage === language && !this.forceLanguageUpdate) {
871
+ return;
721
872
  }
722
- });
723
- }
873
+ }
724
874
 
725
- /**
726
- * Define the secondary language of the map. Note that this is not supported by all the map styles
727
- * Note that most styles do not allow a secondary language and this function only works if the style allows (no force adding)
728
- * @param language
729
- */
730
- setSecondaryLanguage(language: LanguageString = defaults.secondaryLanguage) {
731
- // Using the lock flag as a primaty language also applies to the secondary
732
- if (this.primaryLanguage === Language.STYLE_LOCK) {
733
- console.warn(
734
- "The language cannot be changed because this map has been instantiated with the STYLE_LOCK language flag."
735
- );
875
+ if (!isLanguageSupported(language as string)) {
876
+ console.warn(`The language "${language}" is not supported.`);
736
877
  return;
737
878
  }
738
879
 
739
- if (!isLanguageSupported(language as string)) {
880
+ if (this.primaryLanguage === Language.STYLE_LOCK) {
881
+ console.warn(
882
+ "The language cannot be changed because this map has been instantiated with the STYLE_LOCK language flag.",
883
+ );
740
884
  return;
741
885
  }
742
886
 
743
- this.secondaryLanguage = language;
887
+ this.primaryLanguage = language as LanguageString;
888
+ let languageNonStyle: LanguageString = language as LanguageString;
744
889
 
745
- this.onStyleReady(() => {
746
- if (language === Language.AUTO) {
747
- return this.setSecondaryLanguage(getBrowserLanguage());
890
+ // STYLE needs to be translated into one of the other language,
891
+ // this is why it's addressed first
892
+ if (language === Language.STYLE) {
893
+ if (!styleLanguage) {
894
+ console.warn("The style has no default languages.");
895
+ return;
748
896
  }
749
897
 
750
- const layers = this.getStyle().layers;
898
+ if (!isLanguageSupported(styleLanguage)) {
899
+ console.warn("The language defined in the style is not valid.");
900
+ return;
901
+ }
751
902
 
752
- // detects pattern like "{name:somelanguage}" with loose spacing
753
- const strLanguageRegex = /^\s*{\s*name\s*(:\s*(\S*))?\s*}$/;
903
+ languageNonStyle = styleLanguage as LanguageString;
904
+ }
754
905
 
755
- // detects pattern like "name:somelanguage" with loose spacing
756
- const strLanguageInArrayRegex = /^\s*name\s*(:\s*(\S*))?\s*$/;
906
+ // may be overwritten below
907
+ let langStr: string | LanguageString = Language.LOCAL;
757
908
 
758
- // for string based bilingual lang such as "{name:latin} {name:nonlatin}" or "{name:latin} {name}"
759
- const strBilingualRegex =
760
- /^\s*{\s*name\s*(:\s*(\S*))?\s*}(\s*){\s*name\s*(:\s*(\S*))?\s*}$/;
909
+ // will be overwritten below
910
+ let replacer: ExpressionSpecification | string = `{${langStr}}`;
761
911
 
762
- let regexMatch;
912
+ if (languageNonStyle == Language.VISITOR) {
913
+ langStr = getBrowserLanguage();
914
+ replacer = [
915
+ "case",
916
+ ["all", ["has", langStr], ["has", Language.LOCAL]],
917
+ [
918
+ "case",
919
+ ["==", ["get", langStr], ["get", Language.LOCAL]],
920
+ ["get", Language.LOCAL],
921
+
922
+ [
923
+ "format",
924
+ ["get", langStr],
925
+ { "font-scale": 0.8 },
926
+ "\n",
927
+ ["get", Language.LOCAL],
928
+ { "font-scale": 1.1 },
929
+ ],
930
+ ],
931
+
932
+ ["get", Language.LOCAL],
933
+ ];
934
+ } else if (languageNonStyle == Language.VISITOR_ENGLISH) {
935
+ langStr = Language.ENGLISH;
936
+ replacer = [
937
+ "case",
938
+ ["all", ["has", langStr], ["has", Language.LOCAL]],
939
+ [
940
+ "case",
941
+ ["==", ["get", langStr], ["get", Language.LOCAL]],
942
+ ["get", Language.LOCAL],
943
+
944
+ [
945
+ "format",
946
+ ["get", langStr],
947
+ { "font-scale": 0.8 },
948
+ "\n",
949
+ ["get", Language.LOCAL],
950
+ { "font-scale": 1.1 },
951
+ ],
952
+ ],
953
+ ["get", Language.LOCAL],
954
+ ];
955
+ } else if (languageNonStyle === Language.AUTO) {
956
+ langStr = getBrowserLanguage();
957
+ replacer = [
958
+ "case",
959
+ ["has", langStr],
960
+ ["get", langStr],
961
+ ["get", Language.LOCAL],
962
+ ];
963
+ }
763
964
 
764
- for (let i = 0; i < layers.length; i += 1) {
765
- const layer = layers[i];
766
- const layout = layer.layout;
965
+ // This is for using the regular names as {name}
966
+ else if (languageNonStyle === Language.LOCAL) {
967
+ langStr = Language.LOCAL;
968
+ replacer = `{${langStr}}`;
969
+ }
767
970
 
768
- if (!layout) {
769
- continue;
770
- }
971
+ // This section is for the regular language ISO codes
972
+ else {
973
+ langStr = languageNonStyle;
974
+ replacer = [
975
+ "case",
976
+ ["has", langStr],
977
+ ["get", langStr],
978
+ ["get", Language.LOCAL],
979
+ ];
980
+ }
771
981
 
772
- if (!layout["text-field"]) {
773
- continue;
774
- }
982
+ const { layers } = this.getStyle();
775
983
 
776
- const textFieldLayoutProp = this.getLayoutProperty(
777
- layer.id,
778
- "text-field"
779
- );
984
+ for (const { id, layout } of layers) {
985
+ if (!layout) {
986
+ continue;
987
+ }
780
988
 
781
- let newProp;
782
-
783
- // Note:
784
- // The value of the 'text-field' property can take multiple shape;
785
- // 1. can be an array with 'concat' on its first element (most likely means bilingual)
786
- // 2. can be an array with 'get' on its first element (monolingual)
787
- // 3. can be a string of shape '{name:latin}'
788
- // 4. can be a string referencing another prop such as '{housenumber}' or '{ref}'
789
- //
790
- // Only the case 1 will be updated because we don't want to change the styling (read: add a secondary language where the original styling is only displaying 1)
791
-
792
- // This is case 1
793
- if (
794
- Array.isArray(textFieldLayoutProp) &&
795
- textFieldLayoutProp.length >= 2 &&
796
- textFieldLayoutProp[0].trim().toLowerCase() === "concat"
797
- ) {
798
- newProp = textFieldLayoutProp.slice(); // newProp is Array
799
- // The style could possibly have defined more than 2 concatenated language strings but we only want to edit the first
800
- // The style could also define that there are more things being concatenated and not only languages
801
-
802
- let languagesAlreadyFound = 0;
803
-
804
- for (let j = 0; j < textFieldLayoutProp.length; j += 1) {
805
- const elem = textFieldLayoutProp[j];
806
-
807
- // we are looking for an elem of shape '{name:somelangage}' (string) of `["get", "name:somelanguage"]` (array)
808
-
809
- // the entry of of shape '{name:somelangage}', possibly with loose spacing
810
- if (
811
- (typeof elem === "string" || elem instanceof String) &&
812
- strLanguageRegex.exec(elem.toString())
813
- ) {
814
- if (languagesAlreadyFound === 1) {
815
- newProp[j] = `{name:${language}}`;
816
- break; // we just want to update the secondary language
817
- }
818
-
819
- languagesAlreadyFound += 1;
820
- }
821
- // the entry is of an array of shape `["get", "name:somelanguage"]`
822
- else if (
823
- Array.isArray(elem) &&
824
- elem.length >= 2 &&
825
- elem[0].trim().toLowerCase() === "get" &&
826
- strLanguageInArrayRegex.exec(elem[1].toString())
827
- ) {
828
- if (languagesAlreadyFound === 1) {
829
- newProp[j][1] = `name:${language}`;
830
- break; // we just want to update the secondary language
831
- }
832
-
833
- languagesAlreadyFound += 1;
834
- } else if (
835
- Array.isArray(elem) &&
836
- elem.length === 4 &&
837
- elem[0].trim().toLowerCase() === "case"
838
- ) {
839
- if (languagesAlreadyFound === 1) {
840
- newProp[j] = ["get", `name:${language}`]; // the situation with 'case' is supposed to only happen with the primary lang
841
- break; // but in case a styling also does that for secondary...
842
- }
843
-
844
- languagesAlreadyFound += 1;
845
- }
846
- }
989
+ if (!("text-field" in layout)) {
990
+ continue;
991
+ }
847
992
 
848
- this.setLayoutProperty(layer.id, "text-field", newProp);
849
- }
993
+ const textFieldLayoutProp = this.getLayoutProperty(id, "text-field");
850
994
 
851
- // the language (both first and second) are defined into a single string model
852
- else if (
853
- (typeof textFieldLayoutProp === "string" ||
854
- textFieldLayoutProp instanceof String) &&
855
- (regexMatch = strBilingualRegex.exec(
856
- textFieldLayoutProp.toString()
857
- )) !== null
858
- ) {
859
- const langStr = language ? `name:${language}` : "name"; // to handle local lang
860
- newProp = `{name${regexMatch[1] || ""}}${regexMatch[3]}{${langStr}}`;
861
- this.setLayoutProperty(layer.id, "text-field", newProp);
862
- }
995
+ // If the label is not about a name, then we don't translate it
996
+ if (
997
+ typeof textFieldLayoutProp === "string" &&
998
+ (textFieldLayoutProp.toLowerCase().includes("ref") ||
999
+ textFieldLayoutProp.toLowerCase().includes("housenumber"))
1000
+ ) {
1001
+ continue;
863
1002
  }
864
- });
1003
+
1004
+ this.setLayoutProperty(id, "text-field", replacer);
1005
+ }
865
1006
  }
866
1007
 
867
1008
  /**
@@ -872,14 +1013,6 @@ export class Map extends maplibregl.Map {
872
1013
  return this.primaryLanguage;
873
1014
  }
874
1015
 
875
- /**
876
- * Get the secondary language
877
- * @returns
878
- */
879
- getSecondaryLanguage(): LanguageString {
880
- return this.secondaryLanguage;
881
- }
882
-
883
1016
  /**
884
1017
  * Get the exaggeration factor applied to the terrain
885
1018
  * @returns
@@ -896,7 +1029,7 @@ export class Map extends maplibregl.Map {
896
1029
  return this.isTerrainEnabled;
897
1030
  }
898
1031
 
899
- private growTerrain(exaggeration, durationMs = 1000) {
1032
+ private growTerrain(exaggeration: number, durationMs = 1000) {
900
1033
  // This method assumes the terrain is already built
901
1034
  if (!this.terrain) {
902
1035
  return;
@@ -946,8 +1079,6 @@ export class Map extends maplibregl.Map {
946
1079
 
947
1080
  /**
948
1081
  * Enables the 3D terrain visualization
949
- * @param exaggeration
950
- * @returns
951
1082
  */
952
1083
  enableTerrain(exaggeration = this.terrainExaggeration) {
953
1084
  if (exaggeration < 0) {
@@ -1081,7 +1212,8 @@ export class Map extends maplibregl.Map {
1081
1212
  this.terrain.exaggeration = 0;
1082
1213
  this.terrainGrowing = false;
1083
1214
  this.terrainFlattening = false;
1084
- this.setTerrain(null);
1215
+ // @ts-expect-error - https://github.com/maplibre/maplibre-gl-js/issues/2992
1216
+ this.setTerrain();
1085
1217
  if (this.getSource(defaults.terrainSourceId)) {
1086
1218
  this.removeSource(defaults.terrainSourceId);
1087
1219
  }
@@ -1101,8 +1233,6 @@ export class Map extends maplibregl.Map {
1101
1233
  * the method `.enableTerrain()` will be called.
1102
1234
  * If `animate` is `true`, the terrain transformation will be animated in the span of 1 second.
1103
1235
  * If `animate` is `false`, no animated transition to the newly defined exaggeration.
1104
- * @param exaggeration
1105
- * @param animate
1106
1236
  */
1107
1237
  setTerrainExaggeration(exaggeration: number, animate = true) {
1108
1238
  if (!animate && this.terrain) {
@@ -1117,9 +1247,8 @@ export class Map extends maplibregl.Map {
1117
1247
  /**
1118
1248
  * Perform an action when the style is ready. It could be at the moment of calling this method
1119
1249
  * or later.
1120
- * @param cb
1121
1250
  */
1122
- private onStyleReady(cb) {
1251
+ private onStyleReady(cb: () => void) {
1123
1252
  if (this.isStyleLoaded()) {
1124
1253
  cb();
1125
1254
  } else {
@@ -1136,14 +1265,17 @@ export class Map extends maplibregl.Map {
1136
1265
  {
1137
1266
  duration: 0,
1138
1267
  padding: 100,
1139
- }
1268
+ },
1140
1269
  );
1141
1270
  }
1142
1271
 
1143
1272
  async centerOnIpPoint(zoom: number | undefined) {
1144
1273
  const ipGeolocateResult = await geolocation.info();
1145
1274
  this.jumpTo({
1146
- center: [ipGeolocateResult.longitude, ipGeolocateResult.latitude],
1275
+ center: [
1276
+ ipGeolocateResult?.longitude ?? 0,
1277
+ ipGeolocateResult?.latitude ?? 0,
1278
+ ],
1147
1279
  zoom: zoom || 11,
1148
1280
  });
1149
1281
  }
@@ -1163,7 +1295,6 @@ export class Map extends maplibregl.Map {
1163
1295
  * Get the SDK config object.
1164
1296
  * This is convenient to dispatch the SDK configuration to externally built layers
1165
1297
  * that do not directly have access to the SDK configuration but do have access to a Map instance.
1166
- * @returns
1167
1298
  */
1168
1299
  getSdkConfig(): SdkConfig {
1169
1300
  return config;
@@ -1189,8 +1320,33 @@ export class Map extends maplibregl.Map {
1189
1320
  * @example
1190
1321
  * map.setTransformRequest((url: string, resourceType: string) => {});
1191
1322
  */
1192
- setTransformRequest(transformRequest: RequestTransformFunction) {
1323
+ override setTransformRequest(
1324
+ transformRequest: RequestTransformFunction,
1325
+ ): this {
1193
1326
  super.setTransformRequest(combineTransformRequest(transformRequest));
1194
1327
  return this;
1195
1328
  }
1329
+
1330
+ /**
1331
+ * Loads an image. This is an async equivalent of `Map.loadImage`
1332
+ */
1333
+ async loadImageAsync(
1334
+ url: string,
1335
+ ): Promise<HTMLImageElement | ImageBitmap | null | undefined> {
1336
+ return new Promise((resolve, reject) => {
1337
+ this.loadImage(
1338
+ url,
1339
+ (
1340
+ error: Error | null | undefined,
1341
+ image: HTMLImageElement | ImageBitmap | null | undefined,
1342
+ ) => {
1343
+ if (error) {
1344
+ reject(error);
1345
+ return;
1346
+ }
1347
+ resolve(image);
1348
+ },
1349
+ );
1350
+ });
1351
+ }
1196
1352
  }