@maptiler/sdk 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +94 -51
  5. package/colorramp.md +93 -0
  6. package/dist/maptiler-sdk.d.ts +1207 -123
  7. package/dist/maptiler-sdk.min.mjs +3 -1
  8. package/dist/maptiler-sdk.mjs +3561 -485
  9. package/dist/maptiler-sdk.mjs.map +1 -1
  10. package/dist/maptiler-sdk.umd.js +3825 -869
  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 +298 -0
  15. package/rollup.config.js +2 -16
  16. package/src/Map.ts +489 -357
  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 +10 -0
  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,12 @@ 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;
176
185
 
177
186
  constructor(options: MapOptions) {
178
187
  if (options.apiKey) {
@@ -184,7 +193,7 @@ export class Map extends maplibregl.Map {
184
193
 
185
194
  if (!config.apiKey) {
186
195
  console.warn(
187
- "MapTiler Cloud API key is not set. Visit https://maptiler.com and try Cloud for free!"
196
+ "MapTiler Cloud API key is not set. Visit https://maptiler.com and try Cloud for free!",
188
197
  );
189
198
  }
190
199
 
@@ -197,7 +206,12 @@ export class Map extends maplibregl.Map {
197
206
  });
198
207
 
199
208
  this.primaryLanguage = options.language ?? config.primaryLanguage;
200
- this.secondaryLanguage = config.secondaryLanguage;
209
+ this.forceLanguageUpdate =
210
+ this.primaryLanguage === Language.STYLE ||
211
+ this.primaryLanguage === Language.STYLE_LOCK
212
+ ? false
213
+ : true;
214
+ this.languageAlwaysBeenStyle = this.primaryLanguage === Language.STYLE;
201
215
  this.terrainExaggeration =
202
216
  options.terrainExaggeration ?? this.terrainExaggeration;
203
217
 
@@ -228,17 +242,17 @@ export class Map extends maplibregl.Map {
228
242
  }
229
243
  } catch (e) {
230
244
  // not raising
231
- console.warn(e.message);
245
+ console.warn((e as Error).message);
232
246
  }
233
247
 
234
248
  // As a fallback, we want to center the map on the visitor. First with IP geolocation...
235
- let ipLocatedCameraHash = null;
249
+ let ipLocatedCameraHash: string;
236
250
  try {
237
251
  await this.centerOnIpPoint(options.zoom);
238
252
  ipLocatedCameraHash = this.getCameraHash();
239
253
  } catch (e) {
240
254
  // not raising
241
- console.warn(e.message);
255
+ console.warn((e as Error).message);
242
256
  }
243
257
 
244
258
  // A more precise localization
@@ -286,7 +300,7 @@ export class Map extends maplibregl.Map {
286
300
  maximumAge: 24 * 3600 * 1000, // a day in millisec
287
301
  timeout: 5000, // milliseconds
288
302
  enableHighAccuracy: false,
289
- }
303
+ },
290
304
  );
291
305
  }
292
306
  });
@@ -294,7 +308,6 @@ export class Map extends maplibregl.Map {
294
308
  // If the config includes language changing, we must update the map language
295
309
  this.on("styledata", () => {
296
310
  this.setPrimaryLanguage(this.primaryLanguage);
297
- this.setSecondaryLanguage(this.secondaryLanguage);
298
311
  });
299
312
 
300
313
  // this even is in charge of reaplying the terrain elevation after the
@@ -321,12 +334,15 @@ export class Map extends maplibregl.Map {
321
334
  const possibleSources = Object.keys(this.style.sourceCaches)
322
335
  .map((sourceName) => this.getSource(sourceName))
323
336
  .filter(
324
- (s: any) =>
325
- typeof s.url === "string" && s.url.includes("tiles.json")
337
+ (s: Source | undefined) =>
338
+ s &&
339
+ "url" in s &&
340
+ typeof s.url === "string" &&
341
+ s?.url.includes("tiles.json"),
326
342
  );
327
343
 
328
344
  const styleUrl = new URL(
329
- (possibleSources[0] as maplibregl.VectorTileSource).url
345
+ (possibleSources[0] as maplibregl.VectorTileSource).url,
330
346
  );
331
347
 
332
348
  if (!styleUrl.searchParams.has("key")) {
@@ -340,24 +356,26 @@ export class Map extends maplibregl.Map {
340
356
  }
341
357
 
342
358
  // The attribution and logo must show when required
343
- if ("logo" in tileJsonContent && tileJsonContent.logo) {
344
- const logoURL: string = tileJsonContent.logo;
359
+ if (options.forceNoAttributionControl !== true) {
360
+ if ("logo" in tileJsonContent && tileJsonContent.logo) {
361
+ const logoURL: string = tileJsonContent.logo;
345
362
 
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
363
  this.addControl(
354
- new AttributionControl({
355
- customAttribution: options.customAttribution,
356
- })
364
+ new MaptilerLogoControl({ logoURL }),
365
+ options.logoPosition,
357
366
  );
367
+
368
+ // if attribution in option is `false` but the the logo shows up in the tileJson, then the attribution must show anyways
369
+ if (options.attributionControl === false) {
370
+ this.addControl(
371
+ new AttributionControl({
372
+ customAttribution: options.customAttribution,
373
+ }),
374
+ );
375
+ }
376
+ } else if (options.maptilerLogo) {
377
+ this.addControl(new MaptilerLogoControl(), options.logoPosition);
358
378
  }
359
- } else if (options.maptilerLogo) {
360
- this.addControl(new MaptilerLogoControl(), options.logoPosition);
361
379
  }
362
380
 
363
381
  // the other controls at init time but be after
@@ -414,7 +432,7 @@ export class Map extends maplibregl.Map {
414
432
  showAccuracyCircle: true,
415
433
  showUserLocation: true,
416
434
  }),
417
- position
435
+ position,
418
436
  );
419
437
  }
420
438
 
@@ -451,16 +469,76 @@ export class Map extends maplibregl.Map {
451
469
  // and some animation (flyTo, easeTo) are running from the begining.
452
470
  let loadEventTriggered = false;
453
471
  let terrainEventTriggered = false;
454
- let terrainEventData: LoadWithTerrainEvent = null;
472
+ let terrainEventData: LoadWithTerrainEvent;
455
473
 
456
- this.once("load", (_) => {
474
+ this.once("load", () => {
457
475
  loadEventTriggered = true;
458
476
  if (terrainEventTriggered) {
459
477
  this.fire("loadWithTerrain", terrainEventData);
460
478
  }
461
479
  });
462
480
 
463
- const terrainCallback = (evt) => {
481
+ this.once("style.load", () => {
482
+ const { minimap } = options;
483
+ if (typeof minimap === "object") {
484
+ const {
485
+ zoom,
486
+ center,
487
+ style,
488
+ language,
489
+ apiKey,
490
+ maptilerLogo,
491
+ antialias,
492
+ refreshExpiredTiles,
493
+ maxBounds,
494
+ scrollZoom,
495
+ minZoom,
496
+ maxZoom,
497
+ boxZoom,
498
+ locale,
499
+ fadeDuration,
500
+ crossSourceCollisions,
501
+ clickTolerance,
502
+ bounds,
503
+ fitBoundsOptions,
504
+ pixelRatio,
505
+ validateStyle,
506
+ } = options;
507
+ this.minimap = new Minimap(minimap, {
508
+ zoom,
509
+ center,
510
+ style,
511
+ language,
512
+ apiKey,
513
+ container: "null",
514
+ maptilerLogo,
515
+ antialias,
516
+ refreshExpiredTiles,
517
+ maxBounds,
518
+ scrollZoom,
519
+ minZoom,
520
+ maxZoom,
521
+ boxZoom,
522
+ locale,
523
+ fadeDuration,
524
+ crossSourceCollisions,
525
+ clickTolerance,
526
+ bounds,
527
+ fitBoundsOptions,
528
+ pixelRatio,
529
+ validateStyle,
530
+ });
531
+ this.addControl(this.minimap, minimap.position ?? "bottom-left");
532
+ } else if (minimap === true) {
533
+ this.minimap = new Minimap({}, options);
534
+ this.addControl(this.minimap, "bottom-left");
535
+ } else if (minimap !== undefined && minimap !== false) {
536
+ this.minimap = new Minimap({}, options);
537
+ this.addControl(this.minimap, minimap);
538
+ }
539
+ });
540
+
541
+ const terrainCallback = (evt: LoadWithTerrainEvent) => {
464
542
  if (!evt.terrain) return;
465
543
  terrainEventTriggered = true;
466
544
  terrainEventData = {
@@ -480,7 +558,7 @@ export class Map extends maplibregl.Map {
480
558
  // enable 3D terrain if provided in options
481
559
  if (options.terrain) {
482
560
  this.enableTerrain(
483
- options.terrainExaggeration ?? this.terrainExaggeration
561
+ options.terrainExaggeration ?? this.terrainExaggeration,
484
562
  );
485
563
  }
486
564
  }
@@ -492,12 +570,12 @@ export class Map extends maplibregl.Map {
492
570
  * @returns
493
571
  */
494
572
  async onLoadAsync() {
495
- return new Promise<Map>((resolve, reject) => {
573
+ return new Promise<Map>((resolve) => {
496
574
  if (this.loaded()) {
497
575
  return resolve(this);
498
576
  }
499
577
 
500
- this.once("load", (_) => {
578
+ this.once("load", () => {
501
579
  resolve(this);
502
580
  });
503
581
  });
@@ -511,12 +589,12 @@ export class Map extends maplibregl.Map {
511
589
  * @returns
512
590
  */
513
591
  async onLoadWithTerrainAsync() {
514
- return new Promise<Map>((resolve, reject) => {
592
+ return new Promise<Map>((resolve) => {
515
593
  if (this.loaded() && this.terrain) {
516
594
  return resolve(this);
517
595
  }
518
596
 
519
- this.once("loadWithTerrain", (_) => {
597
+ this.once("loadWithTerrain", () => {
520
598
  resolve(this);
521
599
  });
522
600
  });
@@ -528,340 +606,379 @@ export class Map extends maplibregl.Map {
528
606
  * - a full style URL (possibly with API key)
529
607
  * - a shorthand with only the MapTIler style name (eg. `"streets-v2"`)
530
608
  * - a longer form with the prefix `"maptiler://"` (eg. `"maptiler://streets-v2"`)
531
- * @param style
532
- * @param options
533
- * @returns
534
609
  */
535
- setStyle(
536
- style: ReferenceMapStyle | MapStyleVariant | StyleSpecification | string,
537
- options?: StyleSwapOptions & StyleOptions
538
- ) {
610
+ override setStyle(
611
+ style:
612
+ | null
613
+ | ReferenceMapStyle
614
+ | MapStyleVariant
615
+ | StyleSpecification
616
+ | string,
617
+ options?: StyleSwapOptions & StyleOptions,
618
+ ): this {
619
+ this.minimap?.setStyle(style);
620
+ this.forceLanguageUpdate = true;
621
+
622
+ this.once("idle", () => {
623
+ this.forceLanguageUpdate = false;
624
+ });
625
+
539
626
  return super.setStyle(styleToStyle(style), options);
540
627
  }
541
628
 
542
629
  /**
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
630
+ * Adds a [MapLibre style layer](https://maplibre.org/maplibre-style-spec/layers)
631
+ * to the map's style.
632
+ *
633
+ * A layer defines how data from a specified source will be styled. Read more about layer types
634
+ * and available paint and layout properties in the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/layers).
635
+ *
636
+ * @param layer - The layer to add,
637
+ * conforming to either the MapLibre Style Specification's [layer definition](https://maplibre.org/maplibre-style-spec/layers) or,
638
+ * less commonly, the {@link CustomLayerInterface} specification.
639
+ * The MapLibre Style Specification's layer definition is appropriate for most layers.
640
+ *
641
+ * @param beforeId - The ID of an existing layer to insert the new layer before,
642
+ * resulting in the new layer appearing visually beneath the existing layer.
643
+ * If this argument is not specified, the layer will be appended to the end of the layers array
644
+ * and appear visually above all other layers.
645
+ *
646
+ * @returns `this`
546
647
  */
547
- setLanguage(language: LanguageString = defaults.primaryLanguage) {
548
- if (language === Language.AUTO) {
549
- return this.setLanguage(getBrowserLanguage());
550
- }
551
- this.setPrimaryLanguage(language);
648
+ addLayer(
649
+ layer:
650
+ | (LayerSpecification & {
651
+ source?: string | SourceSpecification;
652
+ })
653
+ | CustomLayerInterface,
654
+ beforeId?: string,
655
+ ): this {
656
+ this.minimap?.addLayer(layer, beforeId);
657
+ return super.addLayer(layer, beforeId);
552
658
  }
553
659
 
554
660
  /**
555
- * Define the primary language of the map. Note that not all the languages shorthands provided are available.
556
- * @param language
661
+ * Moves a layer to a different z-position.
662
+ *
663
+ * @param id - The ID of the layer to move.
664
+ * @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.
665
+ * @returns `this`
666
+ *
667
+ * @example
668
+ * 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.
669
+ * ```ts
670
+ * map.moveLayer('polygon', 'country-label');
671
+ * ```
557
672
  */
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;
673
+ moveLayer(id: string, beforeId?: string): this {
674
+ this.minimap?.moveLayer(id, beforeId);
675
+ return super.moveLayer(id, beforeId);
676
+ }
578
677
 
579
- // detects pattern like "{name:somelanguage}" with loose spacing
580
- const strLanguageRegex = /^\s*{\s*name\s*(:\s*(\S*))?\s*}$/;
678
+ /**
679
+ * Removes the layer with the given ID from the map's style.
680
+ *
681
+ * An {@link ErrorEvent} will be fired if the image parameter is invald.
682
+ *
683
+ * @param id - The ID of the layer to remove
684
+ * @returns `this`
685
+ *
686
+ * @example
687
+ * If a layer with ID 'state-data' exists, remove it.
688
+ * ```ts
689
+ * if (map.getLayer('state-data')) map.removeLayer('state-data');
690
+ * ```
691
+ */
692
+ removeLayer(id: string): this {
693
+ this.minimap?.removeLayer(id);
694
+ return super.removeLayer(id);
695
+ }
581
696
 
582
- // detects pattern like "name:somelanguage" with loose spacing
583
- const strLanguageInArrayRegex = /^\s*name\s*(:\s*(\S*))?\s*$/;
697
+ /**
698
+ * Sets the zoom extent for the specified style layer. The zoom extent includes the
699
+ * [minimum zoom level](https://maplibre.org/maplibre-style-spec/layers/#minzoom)
700
+ * and [maximum zoom level](https://maplibre.org/maplibre-style-spec/layers/#maxzoom))
701
+ * at which the layer will be rendered.
702
+ *
703
+ * Note: For style layers using vector sources, style layers cannot be rendered at zoom levels lower than the
704
+ * minimum zoom level of the _source layer_ because the data does not exist at those zoom levels. If the minimum
705
+ * zoom level of the source layer is higher than the minimum zoom level defined in the style layer, the style
706
+ * layer will not be rendered at all zoom levels in the zoom range.
707
+ */
708
+ setLayerZoomRange(layerId: string, minzoom: number, maxzoom: number): this {
709
+ this.minimap?.setLayerZoomRange(layerId, minzoom, maxzoom);
710
+ return super.setLayerZoomRange(layerId, minzoom, maxzoom);
711
+ }
584
712
 
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*}$/;
713
+ /**
714
+ * Sets the filter for the specified style layer.
715
+ *
716
+ * Filters control which features a style layer renders from its source.
717
+ * Any feature for which the filter expression evaluates to `true` will be
718
+ * rendered on the map. Those that are false will be hidden.
719
+ *
720
+ * Use `setFilter` to show a subset of your source data.
721
+ *
722
+ * To clear the filter, pass `null` or `undefined` as the second parameter.
723
+ */
724
+ setFilter(
725
+ layerId: string,
726
+ filter?: FilterSpecification | null,
727
+ options?: StyleSetterOptions,
728
+ ): this {
729
+ this.minimap?.setFilter(layerId, filter, options);
730
+ return super.setFilter(layerId, filter, options);
731
+ }
588
732
 
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*})(.*)$/;
733
+ /**
734
+ * Sets the value of a paint property in the specified style layer.
735
+ *
736
+ * @param layerId - The ID of the layer to set the paint property in.
737
+ * @param name - The name of the paint property to set.
738
+ * @param value - The value of the paint property to set.
739
+ * Must be of a type appropriate for the property, as defined in the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/).
740
+ * @param options - Options object.
741
+ * @returns `this`
742
+ * @example
743
+ * ```ts
744
+ * map.setPaintProperty('my-layer', 'fill-color', '#faafee');
745
+ * ```
746
+ */
747
+ setPaintProperty(
748
+ layerId: string,
749
+ name: string,
750
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
751
+ value: any,
752
+ options?: StyleSetterOptions,
753
+ ): this {
754
+ this.minimap?.setPaintProperty(layerId, name, value, options);
755
+ return super.setPaintProperty(layerId, name, value, options);
756
+ }
591
757
 
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
- ];
758
+ /**
759
+ * Sets the value of a layout property in the specified style layer.
760
+ * Layout properties define how the layer is styled.
761
+ * Layout properties for layers of the same type are documented together.
762
+ * Layers of different types have different layout properties.
763
+ * See the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/) for the complete list of layout properties.
764
+ * @param layerId - The ID of the layer to set the layout property in.
765
+ * @param name - The name of the layout property to set.
766
+ * @param value - The value of the layout property to set.
767
+ * Must be of a type appropriate for the property, as defined in the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/).
768
+ * @param options - Options object.
769
+ * @returns `this`
770
+ */
771
+ setLayoutProperty(
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?.setLayoutProperty(layerId, name, value, options);
779
+ return super.setLayoutProperty(layerId, name, value, options);
780
+ }
599
781
 
600
- for (let i = 0; i < layers.length; i += 1) {
601
- const layer = layers[i];
602
- const layout = layer.layout;
782
+ /**
783
+ * Sets the value of the style's glyphs property.
784
+ *
785
+ * @param glyphsUrl - Glyph URL to set. Must conform to the [MapLibre Style Specification](https://maplibre.org/maplibre-style-spec/glyphs/).
786
+ * @param options - Options object.
787
+ * @returns `this`
788
+ * @example
789
+ * ```ts
790
+ * map.setGlyphs('https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf');
791
+ * ```
792
+ */
793
+ setGlyphs(glyphsUrl: string | null, options?: StyleSetterOptions): this {
794
+ this.minimap?.setGlyphs(glyphsUrl, options);
795
+ return super.setGlyphs(glyphsUrl, options);
796
+ }
603
797
 
604
- if (!layout) {
605
- continue;
606
- }
798
+ private getStyleLanguage(): string | null {
799
+ if (!this.style.stylesheet.metadata) return null;
800
+ if (typeof this.style.stylesheet.metadata !== "object") return null;
607
801
 
608
- if (!layout["text-field"]) {
609
- continue;
610
- }
802
+ if (
803
+ "maptiler:language" in this.style.stylesheet.metadata &&
804
+ typeof this.style.stylesheet.metadata["maptiler:language"] === "string"
805
+ ) {
806
+ return this.style.stylesheet.metadata["maptiler:language"];
807
+ } else {
808
+ return null;
809
+ }
810
+ }
611
811
 
612
- const textFieldLayoutProp = this.getLayoutProperty(
613
- layer.id,
614
- "text-field"
615
- );
812
+ /**
813
+ * Define the primary language of the map. Note that not all the languages shorthands provided are available.
814
+ */
815
+ setLanguage(language: LanguageString | string): void {
816
+ this.minimap?.map?.setLanguage(language);
817
+ this.onStyleReady(() => {
818
+ this.setPrimaryLanguage(language);
819
+ });
820
+ }
616
821
 
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
- }
822
+ /**
823
+ * Define the primary language of the map. Note that not all the languages shorthands provided are available.
824
+ */
670
825
 
671
- this.setLayoutProperty(layer.id, "text-field", newProp);
672
- }
826
+ private setPrimaryLanguage(language: LanguageString | string) {
827
+ const styleLanguage = this.getStyleLanguage();
828
+
829
+ // If the language is set to `STYLE` (which is the SDK default), but the language defined in
830
+ // the style is `auto`, we need to bypass some verification and modify the languages anyway
831
+ if (
832
+ !(
833
+ language === Language.STYLE &&
834
+ (styleLanguage === Language.AUTO || styleLanguage === Language.VISITOR)
835
+ )
836
+ ) {
837
+ if (language !== Language.STYLE) {
838
+ this.languageAlwaysBeenStyle = false;
839
+ }
673
840
 
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
- }
841
+ if (this.languageAlwaysBeenStyle) {
842
+ return;
843
+ }
684
844
 
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
- }
845
+ // No need to change the language
846
+ if (this.primaryLanguage === language && !this.forceLanguageUpdate) {
847
+ return;
721
848
  }
722
- });
723
- }
849
+ }
724
850
 
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
- );
851
+ if (!isLanguageSupported(language as string)) {
852
+ console.warn(`The language "${language}" is not supported.`);
736
853
  return;
737
854
  }
738
855
 
739
- if (!isLanguageSupported(language as string)) {
856
+ if (this.primaryLanguage === Language.STYLE_LOCK) {
857
+ console.warn(
858
+ "The language cannot be changed because this map has been instantiated with the STYLE_LOCK language flag.",
859
+ );
740
860
  return;
741
861
  }
742
862
 
743
- this.secondaryLanguage = language;
863
+ this.primaryLanguage = language as LanguageString;
864
+ let languageNonStyle: LanguageString = language as LanguageString;
744
865
 
745
- this.onStyleReady(() => {
746
- if (language === Language.AUTO) {
747
- return this.setSecondaryLanguage(getBrowserLanguage());
866
+ // STYLE needs to be translated into one of the other language,
867
+ // this is why it's addressed first
868
+ if (language === Language.STYLE) {
869
+ if (!styleLanguage) {
870
+ console.warn("The style has no default languages.");
871
+ return;
748
872
  }
749
873
 
750
- const layers = this.getStyle().layers;
874
+ if (!isLanguageSupported(styleLanguage)) {
875
+ console.warn("The language defined in the style is not valid.");
876
+ return;
877
+ }
751
878
 
752
- // detects pattern like "{name:somelanguage}" with loose spacing
753
- const strLanguageRegex = /^\s*{\s*name\s*(:\s*(\S*))?\s*}$/;
879
+ languageNonStyle = styleLanguage as LanguageString;
880
+ }
754
881
 
755
- // detects pattern like "name:somelanguage" with loose spacing
756
- const strLanguageInArrayRegex = /^\s*name\s*(:\s*(\S*))?\s*$/;
882
+ // may be overwritten below
883
+ let langStr: string | LanguageString = Language.LOCAL;
757
884
 
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*}$/;
885
+ // will be overwritten below
886
+ let replacer: ExpressionSpecification | string = `{${langStr}}`;
761
887
 
762
- let regexMatch;
888
+ if (languageNonStyle == Language.VISITOR) {
889
+ langStr = getBrowserLanguage();
890
+ replacer = [
891
+ "case",
892
+ ["all", ["has", langStr], ["has", Language.LOCAL]],
893
+ [
894
+ "case",
895
+ ["==", ["get", langStr], ["get", Language.LOCAL]],
896
+ ["get", Language.LOCAL],
897
+
898
+ [
899
+ "format",
900
+ ["get", langStr],
901
+ { "font-scale": 0.8 },
902
+ "\n",
903
+ ["get", Language.LOCAL],
904
+ { "font-scale": 1.1 },
905
+ ],
906
+ ],
907
+
908
+ ["get", Language.LOCAL],
909
+ ];
910
+ } else if (languageNonStyle == Language.VISITOR_ENGLISH) {
911
+ langStr = Language.ENGLISH;
912
+ replacer = [
913
+ "case",
914
+ ["all", ["has", langStr], ["has", Language.LOCAL]],
915
+ [
916
+ "case",
917
+ ["==", ["get", langStr], ["get", Language.LOCAL]],
918
+ ["get", Language.LOCAL],
919
+
920
+ [
921
+ "format",
922
+ ["get", langStr],
923
+ { "font-scale": 0.8 },
924
+ "\n",
925
+ ["get", Language.LOCAL],
926
+ { "font-scale": 1.1 },
927
+ ],
928
+ ],
929
+ ["get", Language.LOCAL],
930
+ ];
931
+ } else if (languageNonStyle === Language.AUTO) {
932
+ langStr = getBrowserLanguage();
933
+ replacer = [
934
+ "case",
935
+ ["has", langStr],
936
+ ["get", langStr],
937
+ ["get", Language.LOCAL],
938
+ ];
939
+ }
763
940
 
764
- for (let i = 0; i < layers.length; i += 1) {
765
- const layer = layers[i];
766
- const layout = layer.layout;
941
+ // This is for using the regular names as {name}
942
+ else if (languageNonStyle === Language.LOCAL) {
943
+ langStr = Language.LOCAL;
944
+ replacer = `{${langStr}}`;
945
+ }
767
946
 
768
- if (!layout) {
769
- continue;
770
- }
947
+ // This section is for the regular language ISO codes
948
+ else {
949
+ langStr = languageNonStyle;
950
+ replacer = [
951
+ "case",
952
+ ["has", langStr],
953
+ ["get", langStr],
954
+ ["get", Language.LOCAL],
955
+ ];
956
+ }
771
957
 
772
- if (!layout["text-field"]) {
773
- continue;
774
- }
958
+ const { layers } = this.getStyle();
775
959
 
776
- const textFieldLayoutProp = this.getLayoutProperty(
777
- layer.id,
778
- "text-field"
779
- );
960
+ for (const { id, layout } of layers) {
961
+ if (!layout) {
962
+ continue;
963
+ }
780
964
 
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
- }
965
+ if (!("text-field" in layout)) {
966
+ continue;
967
+ }
847
968
 
848
- this.setLayoutProperty(layer.id, "text-field", newProp);
849
- }
969
+ const textFieldLayoutProp = this.getLayoutProperty(id, "text-field");
850
970
 
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
- }
971
+ // If the label is not about a name, then we don't translate it
972
+ if (
973
+ typeof textFieldLayoutProp === "string" &&
974
+ (textFieldLayoutProp.toLowerCase().includes("ref") ||
975
+ textFieldLayoutProp.toLowerCase().includes("housenumber"))
976
+ ) {
977
+ continue;
863
978
  }
864
- });
979
+
980
+ this.setLayoutProperty(id, "text-field", replacer);
981
+ }
865
982
  }
866
983
 
867
984
  /**
@@ -872,14 +989,6 @@ export class Map extends maplibregl.Map {
872
989
  return this.primaryLanguage;
873
990
  }
874
991
 
875
- /**
876
- * Get the secondary language
877
- * @returns
878
- */
879
- getSecondaryLanguage(): LanguageString {
880
- return this.secondaryLanguage;
881
- }
882
-
883
992
  /**
884
993
  * Get the exaggeration factor applied to the terrain
885
994
  * @returns
@@ -896,7 +1005,7 @@ export class Map extends maplibregl.Map {
896
1005
  return this.isTerrainEnabled;
897
1006
  }
898
1007
 
899
- private growTerrain(exaggeration, durationMs = 1000) {
1008
+ private growTerrain(exaggeration: number, durationMs = 1000) {
900
1009
  // This method assumes the terrain is already built
901
1010
  if (!this.terrain) {
902
1011
  return;
@@ -946,8 +1055,6 @@ export class Map extends maplibregl.Map {
946
1055
 
947
1056
  /**
948
1057
  * Enables the 3D terrain visualization
949
- * @param exaggeration
950
- * @returns
951
1058
  */
952
1059
  enableTerrain(exaggeration = this.terrainExaggeration) {
953
1060
  if (exaggeration < 0) {
@@ -1081,7 +1188,8 @@ export class Map extends maplibregl.Map {
1081
1188
  this.terrain.exaggeration = 0;
1082
1189
  this.terrainGrowing = false;
1083
1190
  this.terrainFlattening = false;
1084
- this.setTerrain(null);
1191
+ // @ts-expect-error - https://github.com/maplibre/maplibre-gl-js/issues/2992
1192
+ this.setTerrain();
1085
1193
  if (this.getSource(defaults.terrainSourceId)) {
1086
1194
  this.removeSource(defaults.terrainSourceId);
1087
1195
  }
@@ -1101,8 +1209,6 @@ export class Map extends maplibregl.Map {
1101
1209
  * the method `.enableTerrain()` will be called.
1102
1210
  * If `animate` is `true`, the terrain transformation will be animated in the span of 1 second.
1103
1211
  * If `animate` is `false`, no animated transition to the newly defined exaggeration.
1104
- * @param exaggeration
1105
- * @param animate
1106
1212
  */
1107
1213
  setTerrainExaggeration(exaggeration: number, animate = true) {
1108
1214
  if (!animate && this.terrain) {
@@ -1117,9 +1223,8 @@ export class Map extends maplibregl.Map {
1117
1223
  /**
1118
1224
  * Perform an action when the style is ready. It could be at the moment of calling this method
1119
1225
  * or later.
1120
- * @param cb
1121
1226
  */
1122
- private onStyleReady(cb) {
1227
+ private onStyleReady(cb: () => void) {
1123
1228
  if (this.isStyleLoaded()) {
1124
1229
  cb();
1125
1230
  } else {
@@ -1136,14 +1241,17 @@ export class Map extends maplibregl.Map {
1136
1241
  {
1137
1242
  duration: 0,
1138
1243
  padding: 100,
1139
- }
1244
+ },
1140
1245
  );
1141
1246
  }
1142
1247
 
1143
1248
  async centerOnIpPoint(zoom: number | undefined) {
1144
1249
  const ipGeolocateResult = await geolocation.info();
1145
1250
  this.jumpTo({
1146
- center: [ipGeolocateResult.longitude, ipGeolocateResult.latitude],
1251
+ center: [
1252
+ ipGeolocateResult?.longitude ?? 0,
1253
+ ipGeolocateResult?.latitude ?? 0,
1254
+ ],
1147
1255
  zoom: zoom || 11,
1148
1256
  });
1149
1257
  }
@@ -1163,7 +1271,6 @@ export class Map extends maplibregl.Map {
1163
1271
  * Get the SDK config object.
1164
1272
  * This is convenient to dispatch the SDK configuration to externally built layers
1165
1273
  * that do not directly have access to the SDK configuration but do have access to a Map instance.
1166
- * @returns
1167
1274
  */
1168
1275
  getSdkConfig(): SdkConfig {
1169
1276
  return config;
@@ -1189,8 +1296,33 @@ export class Map extends maplibregl.Map {
1189
1296
  * @example
1190
1297
  * map.setTransformRequest((url: string, resourceType: string) => {});
1191
1298
  */
1192
- setTransformRequest(transformRequest: RequestTransformFunction) {
1299
+ override setTransformRequest(
1300
+ transformRequest: RequestTransformFunction,
1301
+ ): this {
1193
1302
  super.setTransformRequest(combineTransformRequest(transformRequest));
1194
1303
  return this;
1195
1304
  }
1305
+
1306
+ /**
1307
+ * Loads an image. This is an async equivalent of `Map.loadImage`
1308
+ */
1309
+ async loadImageAsync(
1310
+ url: string,
1311
+ ): Promise<HTMLImageElement | ImageBitmap | null | undefined> {
1312
+ return new Promise((resolve, reject) => {
1313
+ this.loadImage(
1314
+ url,
1315
+ (
1316
+ error: Error | null | undefined,
1317
+ image: HTMLImageElement | ImageBitmap | null | undefined,
1318
+ ) => {
1319
+ if (error) {
1320
+ reject(error);
1321
+ return;
1322
+ }
1323
+ resolve(image);
1324
+ },
1325
+ );
1326
+ });
1327
+ }
1196
1328
  }