@maptiler/sdk 1.0.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 (203) hide show
  1. package/.eslintignore +1 -0
  2. package/.eslintrc.cjs +10 -0
  3. package/.github/workflows/npm-publish.yml +23 -0
  4. package/LICENSE +29 -0
  5. package/demos/maptiler-sdk.css +147 -0
  6. package/demos/maptiler-sdk.umd.js +3287 -0
  7. package/demos/simple.html +63 -0
  8. package/dist/maptiler-sdk.css +147 -0
  9. package/dist/maptiler-sdk.d.ts +531 -0
  10. package/dist/maptiler-sdk.min.mjs +1 -0
  11. package/dist/maptiler-sdk.mjs +1128 -0
  12. package/dist/maptiler-sdk.mjs.map +1 -0
  13. package/dist/maptiler-sdk.umd.js +3287 -0
  14. package/dist/maptiler-sdk.umd.js.map +1 -0
  15. package/dist/maptiler-sdk.umd.min.js +579 -0
  16. package/docs/.nojekyll +1 -0
  17. package/docs/assets/custom.css +118 -0
  18. package/docs/assets/highlight.css +134 -0
  19. package/docs/assets/main.js +54 -0
  20. package/docs/assets/search.js +1 -0
  21. package/docs/assets/style.css +1257 -0
  22. package/docs/classes/Map.html +273 -0
  23. package/docs/classes/Point.html +549 -0
  24. package/docs/classes/SdkConfig.html +188 -0
  25. package/docs/demos/maptiler-sdk.css +147 -0
  26. package/docs/demos/maptiler-sdk.umd.js +3287 -0
  27. package/docs/demos/simple.html +63 -0
  28. package/docs/functions/addProtocol.html +146 -0
  29. package/docs/functions/clearPrewarmedResources.html +92 -0
  30. package/docs/functions/clearStorage.html +124 -0
  31. package/docs/functions/getRTLTextPluginStatus.html +92 -0
  32. package/docs/functions/prewarm.html +92 -0
  33. package/docs/functions/removeProtocol.html +106 -0
  34. package/docs/functions/setRTLTextPlugin.html +112 -0
  35. package/docs/functions/supported.html +97 -0
  36. package/docs/images/JS-logo.svg +4 -0
  37. package/docs/images/TS-logo.svg +6 -0
  38. package/docs/images/maptiler-logo.svg +19 -0
  39. package/docs/images/maptiler-sdk-logo.afdesign +0 -0
  40. package/docs/images/maptiler-sdk-logo.svg +66 -0
  41. package/docs/images/screenshots/alps.gif +0 -0
  42. package/docs/images/screenshots/grandcanyon.gif +0 -0
  43. package/docs/images/screenshots/lang-arabic.png +0 -0
  44. package/docs/images/screenshots/lang-hebrew.png +0 -0
  45. package/docs/images/screenshots/multilang.gif +0 -0
  46. package/docs/images/screenshots/static-bounded-europe-1024.png +0 -0
  47. package/docs/images/screenshots/static-bounded-europe-2048.png +0 -0
  48. package/docs/images/screenshots/static-bounded-portugal-1024x2048.png +0 -0
  49. package/docs/images/screenshots/static-bounded-portugal-2048x2048.png +0 -0
  50. package/docs/images/screenshots/static-with-path.png +0 -0
  51. package/docs/images/screenshots/style-basic-v2.png +0 -0
  52. package/docs/images/screenshots/style-bright.png +0 -0
  53. package/docs/images/screenshots/style-dataviz-dark.png +0 -0
  54. package/docs/images/screenshots/style-hybrid.png +0 -0
  55. package/docs/images/screenshots/style-osm.png +0 -0
  56. package/docs/images/screenshots/style-outdoor.png +0 -0
  57. package/docs/images/screenshots/style-pastel.png +0 -0
  58. package/docs/images/screenshots/style-satellite.png +0 -0
  59. package/docs/images/screenshots/style-streets-v2-dark.png +0 -0
  60. package/docs/images/screenshots/style-streets-v2-light.png +0 -0
  61. package/docs/images/screenshots/style-streets-v2.png +0 -0
  62. package/docs/images/screenshots/style-toner.png +0 -0
  63. package/docs/images/screenshots/style-topo.png +0 -0
  64. package/docs/images/screenshots/style-topographique.png +0 -0
  65. package/docs/images/screenshots/style-voyager.png +0 -0
  66. package/docs/images/screenshots/style-winter.png +0 -0
  67. package/docs/index.html +601 -0
  68. package/docs/modules.html +142 -0
  69. package/docs/types/LanguageKey.html +90 -0
  70. package/docs/types/LanguageString.html +90 -0
  71. package/docs/types/MapOptions.html +90 -0
  72. package/docs/types/Matrix2.html +90 -0
  73. package/docs/types/Unit.html +88 -0
  74. package/docs/variables/AJAXError.html +88 -0
  75. package/docs/variables/AttributionControl.html +88 -0
  76. package/docs/variables/CanvasSource.html +88 -0
  77. package/docs/variables/Evented.html +88 -0
  78. package/docs/variables/FullscreenControl.html +88 -0
  79. package/docs/variables/GeoJSONSource.html +88 -0
  80. package/docs/variables/GeolocateControl.html +88 -0
  81. package/docs/variables/GeolocationType.html +95 -0
  82. package/docs/variables/ImageSource.html +88 -0
  83. package/docs/variables/Language.html +249 -0
  84. package/docs/variables/LngLat.html +88 -0
  85. package/docs/variables/LngLatBounds.html +88 -0
  86. package/docs/variables/LogoControl.html +88 -0
  87. package/docs/variables/Marker.html +88 -0
  88. package/docs/variables/MercatorCoordinate.html +88 -0
  89. package/docs/variables/NavigationControl.html +88 -0
  90. package/docs/variables/Popup.html +88 -0
  91. package/docs/variables/RasterDEMTileSource.html +88 -0
  92. package/docs/variables/RasterTileSource.html +88 -0
  93. package/docs/variables/ScaleControl.html +88 -0
  94. package/docs/variables/Style.html +88 -0
  95. package/docs/variables/TerrainControl.html +88 -0
  96. package/docs/variables/VectorTileSource.html +88 -0
  97. package/docs/variables/VideoSource.html +88 -0
  98. package/docs/variables/config.html +88 -0
  99. package/docs/variables/maxParallelImageRequests.html +88 -0
  100. package/docs/variables/version.html +88 -0
  101. package/docs/variables/workerCount.html +88 -0
  102. package/docs/variables/workerUrl.html +88 -0
  103. package/docsmd/.nojekyll +1 -0
  104. package/docsmd/README.md +710 -0
  105. package/docsmd/assets/custom.css +118 -0
  106. package/docsmd/classes/Map.md +292 -0
  107. package/docsmd/classes/Point.md +603 -0
  108. package/docsmd/classes/SdkConfig.md +186 -0
  109. package/docsmd/images/JS-logo.svg +4 -0
  110. package/docsmd/images/TS-logo.svg +6 -0
  111. package/docsmd/images/maptiler-logo.svg +19 -0
  112. package/docsmd/images/maptiler-sdk-logo.afdesign +0 -0
  113. package/docsmd/images/maptiler-sdk-logo.svg +66 -0
  114. package/docsmd/images/screenshots/alps.gif +0 -0
  115. package/docsmd/images/screenshots/grandcanyon.gif +0 -0
  116. package/docsmd/images/screenshots/lang-arabic.png +0 -0
  117. package/docsmd/images/screenshots/lang-hebrew.png +0 -0
  118. package/docsmd/images/screenshots/multilang.gif +0 -0
  119. package/docsmd/images/screenshots/static-bounded-europe-1024.png +0 -0
  120. package/docsmd/images/screenshots/static-bounded-europe-2048.png +0 -0
  121. package/docsmd/images/screenshots/static-bounded-portugal-1024x2048.png +0 -0
  122. package/docsmd/images/screenshots/static-bounded-portugal-2048x2048.png +0 -0
  123. package/docsmd/images/screenshots/static-with-path.png +0 -0
  124. package/docsmd/images/screenshots/style-basic-v2.png +0 -0
  125. package/docsmd/images/screenshots/style-bright.png +0 -0
  126. package/docsmd/images/screenshots/style-dataviz-dark.png +0 -0
  127. package/docsmd/images/screenshots/style-hybrid.png +0 -0
  128. package/docsmd/images/screenshots/style-osm.png +0 -0
  129. package/docsmd/images/screenshots/style-outdoor.png +0 -0
  130. package/docsmd/images/screenshots/style-pastel.png +0 -0
  131. package/docsmd/images/screenshots/style-satellite.png +0 -0
  132. package/docsmd/images/screenshots/style-streets-v2-dark.png +0 -0
  133. package/docsmd/images/screenshots/style-streets-v2-light.png +0 -0
  134. package/docsmd/images/screenshots/style-streets-v2.png +0 -0
  135. package/docsmd/images/screenshots/style-toner.png +0 -0
  136. package/docsmd/images/screenshots/style-topo.png +0 -0
  137. package/docsmd/images/screenshots/style-topographique.png +0 -0
  138. package/docsmd/images/screenshots/style-voyager.png +0 -0
  139. package/docsmd/images/screenshots/style-winter.png +0 -0
  140. package/images/JS-logo.svg +4 -0
  141. package/images/TS-logo.svg +6 -0
  142. package/images/maptiler-logo.svg +19 -0
  143. package/images/maptiler-sdk-logo.afdesign +0 -0
  144. package/images/maptiler-sdk-logo.svg +66 -0
  145. package/images/screenshots/alps.gif +0 -0
  146. package/images/screenshots/grandcanyon.gif +0 -0
  147. package/images/screenshots/lang-arabic.png +0 -0
  148. package/images/screenshots/lang-hebrew.png +0 -0
  149. package/images/screenshots/multilang.gif +0 -0
  150. package/images/screenshots/static-bounded-europe-1024.png +0 -0
  151. package/images/screenshots/static-bounded-europe-2048.png +0 -0
  152. package/images/screenshots/static-bounded-portugal-1024x2048.png +0 -0
  153. package/images/screenshots/static-bounded-portugal-2048x2048.png +0 -0
  154. package/images/screenshots/static-with-path.png +0 -0
  155. package/images/screenshots/style-basic-v2.png +0 -0
  156. package/images/screenshots/style-bright.png +0 -0
  157. package/images/screenshots/style-dataviz-dark.png +0 -0
  158. package/images/screenshots/style-hybrid.png +0 -0
  159. package/images/screenshots/style-osm.png +0 -0
  160. package/images/screenshots/style-outdoor.png +0 -0
  161. package/images/screenshots/style-pastel.png +0 -0
  162. package/images/screenshots/style-satellite.png +0 -0
  163. package/images/screenshots/style-streets-v2-dark.png +0 -0
  164. package/images/screenshots/style-streets-v2-light.png +0 -0
  165. package/images/screenshots/style-streets-v2.png +0 -0
  166. package/images/screenshots/style-toner.png +0 -0
  167. package/images/screenshots/style-topo.png +0 -0
  168. package/images/screenshots/style-topographique.png +0 -0
  169. package/images/screenshots/style-voyager.png +0 -0
  170. package/images/screenshots/style-winter.png +0 -0
  171. package/package.json +71 -0
  172. package/readme.md +609 -0
  173. package/rollup.config.js +161 -0
  174. package/scripts/replace-path-with-content.js +51 -0
  175. package/src/CustomGeolocateControl.ts +193 -0
  176. package/src/CustomLogoControl.ts +59 -0
  177. package/src/Map.ts +897 -0
  178. package/src/MaptilerNavigationControl.ts +66 -0
  179. package/src/Point.ts +336 -0
  180. package/src/TerrainControl.ts +87 -0
  181. package/src/config.ts +92 -0
  182. package/src/defaults.ts +20 -0
  183. package/src/index.ts +171 -0
  184. package/src/language.ts +139 -0
  185. package/src/mapstyle.ts +38 -0
  186. package/src/style/style_template.css +146 -0
  187. package/src/style/svg/v6-compass.svg +12 -0
  188. package/src/style/svg/v6-fullscreen-off.svg +7 -0
  189. package/src/style/svg/v6-fullscreen.svg +7 -0
  190. package/src/style/svg/v6-geolocate-active-error.svg +10 -0
  191. package/src/style/svg/v6-geolocate-active.svg +7 -0
  192. package/src/style/svg/v6-geolocate-background.svg +8 -0
  193. package/src/style/svg/v6-geolocate-disabled.svg +10 -0
  194. package/src/style/svg/v6-geolocate.svg +7 -0
  195. package/src/style/svg/v6-terrain-on.svg +7 -0
  196. package/src/style/svg/v6-terrain.svg +7 -0
  197. package/src/style/svg/v6-zoom-minus.svg +7 -0
  198. package/src/style/svg/v6-zoom-plus.svg +7 -0
  199. package/src/tools.ts +45 -0
  200. package/src/unit.ts +1 -0
  201. package/tsconfig.json +11 -0
  202. package/typedoc.css +118 -0
  203. package/typedoc.json +13 -0
package/src/Map.ts ADDED
@@ -0,0 +1,897 @@
1
+ import maplibregl from "maplibre-gl";
2
+ import { Base64 } from "js-base64";
3
+ import type {
4
+ StyleSpecification,
5
+ MapOptions as MapOptionsML,
6
+ ControlPosition,
7
+ StyleOptions,
8
+ } from "maplibre-gl";
9
+ import { v4 as uuidv4 } from "uuid";
10
+ import { ReferenceMapStyle, MapStyleVariant } from "@maptiler/client";
11
+ import { config } from "./config";
12
+ import { defaults } from "./defaults";
13
+ import { CustomLogoControl } from "./CustomLogoControl";
14
+ import { enableRTL } from "./tools";
15
+ import {
16
+ getBrowserLanguage,
17
+ isLanguageSupported,
18
+ Language,
19
+ LanguageString,
20
+ } from "./language";
21
+ import { styleToStyle } from "./mapstyle";
22
+ import { TerrainControl } from "./TerrainControl";
23
+ import { MaptilerNavigationControl } from "./MaptilerNavigationControl";
24
+ import { geolocation } from "@maptiler/client";
25
+ import { CustomGeolocateControl } from "./CustomGeolocateControl";
26
+
27
+ // StyleSwapOptions is not exported by Maplibre, but we can redefine it (used for setStyle)
28
+ export type TransformStyleFunction = (
29
+ previous: StyleSpecification,
30
+ next: StyleSpecification
31
+ ) => StyleSpecification;
32
+
33
+ export type StyleSwapOptions = {
34
+ diff?: boolean;
35
+ transformStyle?: TransformStyleFunction;
36
+ };
37
+
38
+ const MAPTILER_SESSION_ID = uuidv4();
39
+
40
+ export const GeolocationType: {
41
+ POINT: "POINT";
42
+ COUNTRY: "COUNTRY";
43
+ } = {
44
+ POINT: "POINT",
45
+ COUNTRY: "COUNTRY",
46
+ } as const;
47
+
48
+ /**
49
+ * Options to provide to the `Map` constructor
50
+ */
51
+ export type MapOptions = Omit<MapOptionsML, "style" | "maplibreLogo"> & {
52
+ /**
53
+ * Style of the map. Can be:
54
+ * - a full style URL (possibly with API key)
55
+ * - a shorthand with only the MapTIler style name (eg. `"streets-v2"`)
56
+ * - a longer form with the prefix `"maptiler://"` (eg. `"maptiler://streets-v2"`)
57
+ */
58
+ style?: ReferenceMapStyle | MapStyleVariant | StyleSpecification | string;
59
+
60
+ /**
61
+ * Shows the MapTiler logo if `true`. Note that the logo is always displayed on free plan.
62
+ */
63
+ maptilerLogo?: boolean;
64
+
65
+ /**
66
+ * Enables 3D terrain if `true`. (default: `false`)
67
+ */
68
+ terrain?: boolean;
69
+
70
+ /**
71
+ * Exaggeration factor of the terrain. (default: `1`, no exaggeration)
72
+ */
73
+ terrainExaggeration?: number;
74
+
75
+ /**
76
+ * Show the navigation control. (default: `true`, will hide if `false`)
77
+ */
78
+ navigationControl?: boolean | ControlPosition;
79
+
80
+ /**
81
+ * Show the terrain control. (default: `false`, will show if `true`)
82
+ */
83
+ terrainControl?: boolean | ControlPosition;
84
+
85
+ /**
86
+ * Show the geolocate control. (default: `true`, will hide if `false`)
87
+ */
88
+ geolocateControl?: boolean | ControlPosition;
89
+
90
+ /**
91
+ * Show the scale control. (default: `false`, will show if `true`)
92
+ */
93
+ scaleControl?: boolean | ControlPosition;
94
+
95
+ /**
96
+ * Show the full screen control. (default: `false`, will show if `true`)
97
+ */
98
+ fullscreenControl?: boolean | ControlPosition;
99
+
100
+ /**
101
+ * Method to position the map at a given geolocation. Only if:
102
+ * - `hash` is `false`
103
+ * - `center` is not provided
104
+ *
105
+ * If the value is `true` of `"POINT"` (given by `GeolocationType.POINT`) then the positionning uses the MapTiler Cloud
106
+ * Geolocation to find the non-GPS location point.
107
+ * The zoom level can be provided in the `Map` constructor with the `zoom` option or will be `13` if not provided.
108
+ *
109
+ * If the value is `"COUNTRY"` (given by `GeolocationType.COUNTRY`) then the map is centered around the bounding box of the country.
110
+ * In this case, the `zoom` option will be ignored.
111
+ *
112
+ * If the value is `false`, no geolocation is performed and the map centering and zooming depends on other options or on
113
+ * the built-in defaults.
114
+ *
115
+ * If this option is non-false and the options `center` is also provided, then `center` prevails.
116
+ *
117
+ * Default: `false`
118
+ */
119
+ geolocate?: typeof GeolocationType[keyof typeof GeolocationType] | boolean;
120
+ };
121
+
122
+ /**
123
+ * The Map class can be instanciated to display a map in a `<div>`
124
+ */
125
+ export class Map extends maplibregl.Map {
126
+ private languageShouldUpdate = false;
127
+ private isStyleInitialized = false;
128
+ private isTerrainEnabled = false;
129
+ private terrainExaggeration = 1;
130
+
131
+ constructor(options: MapOptions) {
132
+ const style = styleToStyle(options.style);
133
+ console.log(style);
134
+
135
+ const hashPreConstructor = location.hash;
136
+
137
+ if (!config.apiKey) {
138
+ console.warn(
139
+ "MapTiler Cloud API key is not set. Visit https://maptiler.com and try Cloud for free!"
140
+ );
141
+ }
142
+
143
+ // calling the map constructor with full length style
144
+ super({
145
+ ...options,
146
+ style,
147
+ maplibreLogo: false,
148
+
149
+ transformRequest: (url: string) => {
150
+ const reqUrl = new URL(url);
151
+
152
+ if (reqUrl.host === defaults.maptilerApiHost) {
153
+ if (!reqUrl.searchParams.has("key")) {
154
+ reqUrl.searchParams.append("key", config.apiKey);
155
+ }
156
+
157
+ if (config.session) {
158
+ reqUrl.searchParams.append("mtsid", MAPTILER_SESSION_ID);
159
+ }
160
+ }
161
+
162
+ return {
163
+ url: reqUrl.href,
164
+ headers: {},
165
+ };
166
+ },
167
+ });
168
+
169
+ // Map centering and geolocation
170
+ this.once("styledata", async () => {
171
+ // Not using geolocation centering if...
172
+
173
+ if (options.geolocate === false) {
174
+ return;
175
+ }
176
+
177
+ // ... a center is provided in options
178
+ if (options.center) {
179
+ return;
180
+ }
181
+
182
+ // ... the hash option is enabled and a hash is present in the URL
183
+ if (options.hash && !!hashPreConstructor) {
184
+ return;
185
+ }
186
+
187
+ // If the geolocation is set to COUNTRY:
188
+ try {
189
+ if (options.geolocate === GeolocationType.COUNTRY) {
190
+ await this.fitToIpBounds();
191
+ return;
192
+ }
193
+ } catch (e) {
194
+ // not raising
195
+ console.warn(e.message);
196
+ }
197
+
198
+ // As a fallback, we want to center the map on the visitor. First with IP geolocation...
199
+ let ipLocatedCameraHash = null;
200
+ try {
201
+ await this.centerOnIpPoint(options.zoom);
202
+ ipLocatedCameraHash = this.getCameraHash();
203
+ } catch (e) {
204
+ // not raising
205
+ console.warn(e.message);
206
+ }
207
+
208
+ // Then, the get a more precise location, we rely on the browser location, but only if it was already granted
209
+ // before (we don't want to ask wih a popup at launch time)
210
+ const locationResult = await navigator.permissions.query({
211
+ name: "geolocation",
212
+ });
213
+
214
+ if (locationResult.state === "granted") {
215
+ navigator.geolocation.getCurrentPosition(
216
+ // success callback
217
+ (data) => {
218
+ // If the user has already moved since the ip location, then we no longer want to move the center
219
+ if (ipLocatedCameraHash !== this.getCameraHash()) {
220
+ return;
221
+ }
222
+
223
+ this.easeTo({
224
+ center: [data.coords.longitude, data.coords.latitude],
225
+ zoom: options.zoom || 12,
226
+ duration: 2000,
227
+ });
228
+ },
229
+
230
+ // error callback
231
+ null,
232
+
233
+ // options
234
+ {
235
+ maximumAge: 24 * 3600 * 1000, // a day in millisec
236
+ timeout: 5000, // milliseconds
237
+ enableHighAccuracy: false,
238
+ }
239
+ );
240
+ }
241
+ });
242
+
243
+ // Check if language has been modified and. If so, it will be updated during the next lifecycle step
244
+ this.on("styledataloading", () => {
245
+ this.languageShouldUpdate =
246
+ !!config.primaryLanguage || !!config.secondaryLanguage;
247
+ });
248
+
249
+ // If the config includes language changing, we must update the map language
250
+ this.on("styledata", () => {
251
+ if (
252
+ config.primaryLanguage &&
253
+ (this.languageShouldUpdate || !this.isStyleInitialized)
254
+ ) {
255
+ this.setPrimaryLanguage(config.primaryLanguage);
256
+ }
257
+
258
+ if (
259
+ config.secondaryLanguage &&
260
+ (this.languageShouldUpdate || !this.isStyleInitialized)
261
+ ) {
262
+ this.setSecondaryLanguage(config.secondaryLanguage);
263
+ }
264
+
265
+ this.languageShouldUpdate = false;
266
+ this.isStyleInitialized = true;
267
+ });
268
+
269
+ // this even is in charge of reaplying the terrain elevation after the
270
+ // style has changed because depending on the src/tgt style,
271
+ // the style logic is not always able to resolve the application of terrain
272
+ this.on("styledata", () => {
273
+ // the styling resolver did no manage to reaply the terrain,
274
+ // so let's reload it
275
+ if (this.getTerrain() === null && this.isTerrainEnabled) {
276
+ this.enableTerrain(this.terrainExaggeration);
277
+ }
278
+ });
279
+
280
+ // load the Right-to-Left text plugin (will happen only once)
281
+ this.once("load", async () => {
282
+ enableRTL();
283
+ });
284
+
285
+ // Update logo and attibution
286
+ this.once("load", async () => {
287
+ let tileJsonContent = { logo: null };
288
+
289
+ try {
290
+ const possibleSources = Object.keys(this.style.sourceCaches)
291
+ .map((sourceName) => this.getSource(sourceName))
292
+ .filter(
293
+ (s: any) =>
294
+ typeof s.url === "string" && s.url.includes("tiles.json")
295
+ );
296
+
297
+ const styleUrl = new URL(
298
+ (possibleSources[0] as maplibregl.VectorTileSource).url
299
+ );
300
+
301
+ if (!styleUrl.searchParams.has("key")) {
302
+ styleUrl.searchParams.append("key", config.apiKey);
303
+ }
304
+
305
+ const tileJsonRes = await fetch(styleUrl.href);
306
+ tileJsonContent = await tileJsonRes.json();
307
+ } catch (e) {
308
+ // No tiles.json found (should not happen on maintained styles)
309
+ }
310
+
311
+ // The attribution and logo must show when required
312
+ if ("logo" in tileJsonContent && tileJsonContent.logo) {
313
+ const logoURL: string = tileJsonContent.logo;
314
+
315
+ this.addControl(
316
+ new CustomLogoControl({ logoURL }),
317
+ options.logoPosition
318
+ );
319
+
320
+ // if attribution in option is `false` but the the logo shows up in the tileJson, then the attribution must show anyways
321
+ if (options.attributionControl === false) {
322
+ this.addControl(new maplibregl.AttributionControl(options));
323
+ }
324
+ } else if (options.maptilerLogo) {
325
+ this.addControl(new CustomLogoControl(), options.logoPosition);
326
+ }
327
+
328
+ // the other controls at init time but be after
329
+ // (due to the async nature of logo control)
330
+
331
+ // By default, no scale control
332
+ if (options.scaleControl) {
333
+ // default position, if not provided, is top left corner
334
+ const position = (
335
+ options.scaleControl === true || options.scaleControl === undefined
336
+ ? "bottom-right"
337
+ : options.scaleControl
338
+ ) as ControlPosition;
339
+
340
+ const scaleControl = new maplibregl.ScaleControl({ unit: config.unit });
341
+ this.addControl(scaleControl, position);
342
+ config.on("unit", (unit) => {
343
+ scaleControl.setUnit(unit);
344
+ });
345
+ }
346
+
347
+ if (options.navigationControl !== false) {
348
+ // default position, if not provided, is top left corner
349
+ const position = (
350
+ options.navigationControl === true ||
351
+ options.navigationControl === undefined
352
+ ? "top-right"
353
+ : options.navigationControl
354
+ ) as ControlPosition;
355
+ this.addControl(new MaptilerNavigationControl(), position);
356
+ }
357
+
358
+ if (options.geolocateControl !== false) {
359
+ // default position, if not provided, is top left corner
360
+ const position = (
361
+ options.geolocateControl === true ||
362
+ options.geolocateControl === undefined
363
+ ? "top-right"
364
+ : options.geolocateControl
365
+ ) as ControlPosition;
366
+
367
+ this.addControl(
368
+ // new maplibregl.GeolocateControl({
369
+ new CustomGeolocateControl({
370
+ positionOptions: {
371
+ enableHighAccuracy: true,
372
+ maximumAge: 0,
373
+ timeout: 6000 /* 6 sec */,
374
+ },
375
+ fitBoundsOptions: {
376
+ maxZoom: 15,
377
+ },
378
+ trackUserLocation: true,
379
+ showAccuracyCircle: true,
380
+ showUserLocation: true,
381
+ }),
382
+ position
383
+ );
384
+ }
385
+
386
+ if (options.terrainControl) {
387
+ // default position, if not provided, is top left corner
388
+ const position = (
389
+ options.terrainControl === true ||
390
+ options.terrainControl === undefined
391
+ ? "top-right"
392
+ : options.terrainControl
393
+ ) as ControlPosition;
394
+ this.addControl(new TerrainControl(), position);
395
+ }
396
+
397
+ // By default, no fullscreen control
398
+ if (options.fullscreenControl) {
399
+ // default position, if not provided, is top left corner
400
+ const position = (
401
+ options.fullscreenControl === true ||
402
+ options.fullscreenControl === undefined
403
+ ? "top-right"
404
+ : options.fullscreenControl
405
+ ) as ControlPosition;
406
+
407
+ this.addControl(new maplibregl.FullscreenControl({}), position);
408
+ }
409
+ });
410
+
411
+ // enable 3D terrain if provided in options
412
+ if (options.terrain) {
413
+ this.enableTerrain(
414
+ options.terrainExaggeration ?? this.terrainExaggeration
415
+ );
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Update the style of the map.
421
+ * Can be:
422
+ * - a full style URL (possibly with API key)
423
+ * - a shorthand with only the MapTIler style name (eg. `"streets-v2"`)
424
+ * - a longer form with the prefix `"maptiler://"` (eg. `"maptiler://streets-v2"`)
425
+ * @param style
426
+ * @param options
427
+ * @returns
428
+ */
429
+ setStyle(
430
+ style: ReferenceMapStyle | MapStyleVariant | StyleSpecification | string,
431
+ options?: StyleSwapOptions & StyleOptions
432
+ ) {
433
+ return super.setStyle(styleToStyle(style), options);
434
+ }
435
+
436
+ /**
437
+ * Define the primary language of the map. Note that not all the languages shorthands provided are available.
438
+ * This function is a short for `.setPrimaryLanguage()`
439
+ * @param language
440
+ */
441
+ setLanguage(language: LanguageString = defaults.primaryLanguage) {
442
+ if (language === Language.AUTO) {
443
+ return this.setLanguage(getBrowserLanguage());
444
+ }
445
+ this.setPrimaryLanguage(language);
446
+ }
447
+
448
+ /**
449
+ * Define the primary language of the map. Note that not all the languages shorthands provided are available.
450
+ * @param language
451
+ */
452
+ setPrimaryLanguage(language: LanguageString = defaults.primaryLanguage) {
453
+ if (!isLanguageSupported(language as string)) {
454
+ return;
455
+ }
456
+
457
+ this.onStyleReady(() => {
458
+ if (language === Language.AUTO) {
459
+ return this.setPrimaryLanguage(getBrowserLanguage());
460
+ }
461
+
462
+ // We want to keep track of it to apply the language again when changing the style
463
+ config.primaryLanguage = language;
464
+
465
+ const layers = this.getStyle().layers;
466
+
467
+ // detects pattern like "{name:somelanguage}" with loose spacing
468
+ const strLanguageRegex = /^\s*{\s*name\s*(:\s*(\S*))?\s*}$/;
469
+
470
+ // detects pattern like "name:somelanguage" with loose spacing
471
+ const strLanguageInArrayRegex = /^\s*name\s*(:\s*(\S*))?\s*$/;
472
+
473
+ // for string based bilingual lang such as "{name:latin} {name:nonlatin}" or "{name:latin} {name}"
474
+ const strBilingualRegex =
475
+ /^\s*{\s*name\s*(:\s*(\S*))?\s*}(\s*){\s*name\s*(:\s*(\S*))?\s*}$/;
476
+
477
+ // Regex to capture when there are more info, such as mountains elevation with unit m/ft
478
+ const strMoreInfoRegex = /^(.*)({\s*name\s*(:\s*(\S*))?\s*})(.*)$/;
479
+
480
+ const langStr = language ? `name:${language}` : "name"; // to handle local lang
481
+ const replacer = [
482
+ "case",
483
+ ["has", langStr],
484
+ ["get", langStr],
485
+ ["get", "name:latin"],
486
+ ];
487
+
488
+ for (let i = 0; i < layers.length; i += 1) {
489
+ const layer = layers[i];
490
+ const layout = layer.layout;
491
+
492
+ if (!layout) {
493
+ continue;
494
+ }
495
+
496
+ if (!layout["text-field"]) {
497
+ continue;
498
+ }
499
+
500
+ const textFieldLayoutProp = this.getLayoutProperty(
501
+ layer.id,
502
+ "text-field"
503
+ );
504
+
505
+ // Note:
506
+ // The value of the 'text-field' property can take multiple shape;
507
+ // 1. can be an array with 'concat' on its first element (most likely means bilingual)
508
+ // 2. can be an array with 'get' on its first element (monolingual)
509
+ // 3. can be a string of shape '{name:latin}'
510
+ // 4. can be a string referencing another prop such as '{housenumber}' or '{ref}'
511
+ //
512
+ // The case 1, 2 and 3 will be updated while maintaining their original type and shape.
513
+ // The case 3 will not be updated
514
+
515
+ let regexMatch;
516
+
517
+ // This is case 1
518
+ if (
519
+ Array.isArray(textFieldLayoutProp) &&
520
+ textFieldLayoutProp.length >= 2 &&
521
+ textFieldLayoutProp[0].trim().toLowerCase() === "concat"
522
+ ) {
523
+ const newProp = textFieldLayoutProp.slice(); // newProp is Array
524
+ // The style could possibly have defined more than 2 concatenated language strings but we only want to edit the first
525
+ // The style could also define that there are more things being concatenated and not only languages
526
+
527
+ for (let j = 0; j < textFieldLayoutProp.length; j += 1) {
528
+ const elem = textFieldLayoutProp[j];
529
+
530
+ // we are looking for an elem of shape '{name:somelangage}' (string) of `["get", "name:somelanguage"]` (array)
531
+
532
+ // the entry of of shape '{name:somelangage}', possibly with loose spacing
533
+ if (
534
+ (typeof elem === "string" || elem instanceof String) &&
535
+ strLanguageRegex.exec(elem.toString())
536
+ ) {
537
+ newProp[j] = replacer;
538
+ break; // we just want to update the primary language
539
+ }
540
+ // the entry is of an array of shape `["get", "name:somelanguage"]`
541
+ else if (
542
+ Array.isArray(elem) &&
543
+ elem.length >= 2 &&
544
+ elem[0].trim().toLowerCase() === "get" &&
545
+ strLanguageInArrayRegex.exec(elem[1].toString())
546
+ ) {
547
+ newProp[j] = replacer;
548
+ break; // we just want to update the primary language
549
+ } else if (
550
+ Array.isArray(elem) &&
551
+ elem.length === 4 &&
552
+ elem[0].trim().toLowerCase() === "case"
553
+ ) {
554
+ newProp[j] = replacer;
555
+ break; // we just want to update the primary language
556
+ }
557
+ }
558
+
559
+ this.setLayoutProperty(layer.id, "text-field", newProp);
560
+ }
561
+
562
+ // This is case 2
563
+ else if (
564
+ Array.isArray(textFieldLayoutProp) &&
565
+ textFieldLayoutProp.length >= 2 &&
566
+ textFieldLayoutProp[0].trim().toLowerCase() === "get" &&
567
+ strLanguageInArrayRegex.exec(textFieldLayoutProp[1].toString())
568
+ ) {
569
+ const newProp = replacer;
570
+ this.setLayoutProperty(layer.id, "text-field", newProp);
571
+ }
572
+
573
+ // This is case 3
574
+ else if (
575
+ (typeof textFieldLayoutProp === "string" ||
576
+ textFieldLayoutProp instanceof String) &&
577
+ strLanguageRegex.exec(textFieldLayoutProp.toString())
578
+ ) {
579
+ const newProp = replacer;
580
+ this.setLayoutProperty(layer.id, "text-field", newProp);
581
+ } else if (
582
+ Array.isArray(textFieldLayoutProp) &&
583
+ textFieldLayoutProp.length === 4 &&
584
+ textFieldLayoutProp[0].trim().toLowerCase() === "case"
585
+ ) {
586
+ const newProp = replacer;
587
+ this.setLayoutProperty(layer.id, "text-field", newProp);
588
+ } else if (
589
+ (typeof textFieldLayoutProp === "string" ||
590
+ textFieldLayoutProp instanceof String) &&
591
+ (regexMatch = strBilingualRegex.exec(
592
+ textFieldLayoutProp.toString()
593
+ )) !== null
594
+ ) {
595
+ const newProp = `{${langStr}}${regexMatch[3]}{name${
596
+ regexMatch[4] || ""
597
+ }}`;
598
+ this.setLayoutProperty(layer.id, "text-field", newProp);
599
+ } else if (
600
+ (typeof textFieldLayoutProp === "string" ||
601
+ textFieldLayoutProp instanceof String) &&
602
+ (regexMatch = strMoreInfoRegex.exec(
603
+ textFieldLayoutProp.toString()
604
+ )) !== null
605
+ ) {
606
+ const newProp = `${regexMatch[1]}{${langStr}}${regexMatch[5]}`;
607
+ this.setLayoutProperty(layer.id, "text-field", newProp);
608
+ }
609
+ }
610
+ });
611
+ }
612
+
613
+ /**
614
+ * Define the secondary language of the map.
615
+ * Note that most styles do not allow a secondary language and this function only works if the style allows (no force adding)
616
+ * @param language
617
+ */
618
+ setSecondaryLanguage(language: LanguageString = defaults.secondaryLanguage) {
619
+ if (!isLanguageSupported(language as string)) {
620
+ return;
621
+ }
622
+
623
+ this.onStyleReady(() => {
624
+ if (language === Language.AUTO) {
625
+ return this.setSecondaryLanguage(getBrowserLanguage());
626
+ }
627
+
628
+ // We want to keep track of it to apply the language again when changing the style
629
+ config.secondaryLanguage = language;
630
+
631
+ const layers = this.getStyle().layers;
632
+
633
+ // detects pattern like "{name:somelanguage}" with loose spacing
634
+ const strLanguageRegex = /^\s*{\s*name\s*(:\s*(\S*))?\s*}$/;
635
+
636
+ // detects pattern like "name:somelanguage" with loose spacing
637
+ const strLanguageInArrayRegex = /^\s*name\s*(:\s*(\S*))?\s*$/;
638
+
639
+ // for string based bilingual lang such as "{name:latin} {name:nonlatin}" or "{name:latin} {name}"
640
+ const strBilingualRegex =
641
+ /^\s*{\s*name\s*(:\s*(\S*))?\s*}(\s*){\s*name\s*(:\s*(\S*))?\s*}$/;
642
+
643
+ let regexMatch;
644
+
645
+ for (let i = 0; i < layers.length; i += 1) {
646
+ const layer = layers[i];
647
+ const layout = layer.layout;
648
+
649
+ if (!layout) {
650
+ continue;
651
+ }
652
+
653
+ if (!layout["text-field"]) {
654
+ continue;
655
+ }
656
+
657
+ const textFieldLayoutProp = this.getLayoutProperty(
658
+ layer.id,
659
+ "text-field"
660
+ );
661
+
662
+ let newProp;
663
+
664
+ // Note:
665
+ // The value of the 'text-field' property can take multiple shape;
666
+ // 1. can be an array with 'concat' on its first element (most likely means bilingual)
667
+ // 2. can be an array with 'get' on its first element (monolingual)
668
+ // 3. can be a string of shape '{name:latin}'
669
+ // 4. can be a string referencing another prop such as '{housenumber}' or '{ref}'
670
+ //
671
+ // 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)
672
+
673
+ // This is case 1
674
+ if (
675
+ Array.isArray(textFieldLayoutProp) &&
676
+ textFieldLayoutProp.length >= 2 &&
677
+ textFieldLayoutProp[0].trim().toLowerCase() === "concat"
678
+ ) {
679
+ newProp = textFieldLayoutProp.slice(); // newProp is Array
680
+ // The style could possibly have defined more than 2 concatenated language strings but we only want to edit the first
681
+ // The style could also define that there are more things being concatenated and not only languages
682
+
683
+ let languagesAlreadyFound = 0;
684
+
685
+ for (let j = 0; j < textFieldLayoutProp.length; j += 1) {
686
+ const elem = textFieldLayoutProp[j];
687
+
688
+ // we are looking for an elem of shape '{name:somelangage}' (string) of `["get", "name:somelanguage"]` (array)
689
+
690
+ // the entry of of shape '{name:somelangage}', possibly with loose spacing
691
+ if (
692
+ (typeof elem === "string" || elem instanceof String) &&
693
+ strLanguageRegex.exec(elem.toString())
694
+ ) {
695
+ if (languagesAlreadyFound === 1) {
696
+ newProp[j] = `{name:${language}}`;
697
+ break; // we just want to update the secondary language
698
+ }
699
+
700
+ languagesAlreadyFound += 1;
701
+ }
702
+ // the entry is of an array of shape `["get", "name:somelanguage"]`
703
+ else if (
704
+ Array.isArray(elem) &&
705
+ elem.length >= 2 &&
706
+ elem[0].trim().toLowerCase() === "get" &&
707
+ strLanguageInArrayRegex.exec(elem[1].toString())
708
+ ) {
709
+ if (languagesAlreadyFound === 1) {
710
+ newProp[j][1] = `name:${language}`;
711
+ break; // we just want to update the secondary language
712
+ }
713
+
714
+ languagesAlreadyFound += 1;
715
+ } else if (
716
+ Array.isArray(elem) &&
717
+ elem.length === 4 &&
718
+ elem[0].trim().toLowerCase() === "case"
719
+ ) {
720
+ if (languagesAlreadyFound === 1) {
721
+ newProp[j] = ["get", `name:${language}`]; // the situation with 'case' is supposed to only happen with the primary lang
722
+ break; // but in case a styling also does that for secondary...
723
+ }
724
+
725
+ languagesAlreadyFound += 1;
726
+ }
727
+ }
728
+
729
+ this.setLayoutProperty(layer.id, "text-field", newProp);
730
+ }
731
+
732
+ // the language (both first and second) are defined into a single string model
733
+ else if (
734
+ (typeof textFieldLayoutProp === "string" ||
735
+ textFieldLayoutProp instanceof String) &&
736
+ (regexMatch = strBilingualRegex.exec(
737
+ textFieldLayoutProp.toString()
738
+ )) !== null
739
+ ) {
740
+ const langStr = language ? `name:${language}` : "name"; // to handle local lang
741
+ newProp = `{name${regexMatch[1] || ""}}${regexMatch[3]}{${langStr}}`;
742
+ this.setLayoutProperty(layer.id, "text-field", newProp);
743
+ }
744
+ }
745
+ });
746
+ }
747
+
748
+ /**
749
+ * Get the exaggeration factor applied to the terrain
750
+ * @returns
751
+ */
752
+ getTerrainExaggeration(): number {
753
+ return this.terrainExaggeration;
754
+ }
755
+
756
+ /**
757
+ * Know if terrian is enabled or not
758
+ * @returns
759
+ */
760
+ hasTerrain(): boolean {
761
+ return this.isTerrainEnabled;
762
+ }
763
+
764
+ /**
765
+ * Enables the 3D terrain visualization
766
+ * @param exaggeration
767
+ * @returns
768
+ */
769
+ enableTerrain(exaggeration = this.terrainExaggeration) {
770
+ if (exaggeration < 0) {
771
+ console.warn("Terrain exaggeration cannot be negative.");
772
+ return;
773
+ }
774
+
775
+ const terrainInfo = this.getTerrain();
776
+
777
+ const addTerrain = () => {
778
+ // When style is changed,
779
+ this.isTerrainEnabled = true;
780
+ this.terrainExaggeration = exaggeration;
781
+
782
+ this.addSource(defaults.terrainSourceId, {
783
+ type: "raster-dem",
784
+ url: defaults.terrainSourceURL,
785
+ });
786
+ this.setTerrain({
787
+ source: defaults.terrainSourceId,
788
+ exaggeration: exaggeration,
789
+ });
790
+ };
791
+
792
+ // The terrain has already been loaded,
793
+ // we just update the exaggeration.
794
+ if (terrainInfo) {
795
+ this.setTerrain({ ...terrainInfo, exaggeration });
796
+ return;
797
+ }
798
+
799
+ if (this.loaded() || this.isTerrainEnabled) {
800
+ addTerrain();
801
+ } else {
802
+ this.once("load", () => {
803
+ if (this.getTerrain() && this.getSource(defaults.terrainSourceId)) {
804
+ return;
805
+ }
806
+ addTerrain();
807
+ });
808
+ }
809
+ }
810
+
811
+ /**
812
+ * Disable the 3D terrain visualization
813
+ */
814
+ disableTerrain() {
815
+ this.isTerrainEnabled = false;
816
+ this.setTerrain(null);
817
+ if (this.getSource(defaults.terrainSourceId)) {
818
+ this.removeSource(defaults.terrainSourceId);
819
+ }
820
+ }
821
+
822
+ /**
823
+ * Sets the 3D terrain exageration factor.
824
+ * Note: this is only a shortcut to `.enableTerrain()`
825
+ * @param exaggeration
826
+ */
827
+ setTerrainExaggeration(exaggeration: number) {
828
+ this.enableTerrain(exaggeration);
829
+ }
830
+
831
+ // getLanguages() {
832
+ // const layers = this.getStyle().layers;
833
+
834
+ // for (let i = 0; i < layers.length; i += 1) {
835
+ // const layer = layers[i];
836
+ // const layout = layer.layout;
837
+
838
+ // if (!layout) {
839
+ // continue;
840
+ // }
841
+
842
+ // if (!layout["text-field"]) {
843
+ // continue;
844
+ // }
845
+
846
+ // const textFieldLayoutProp = this.getLayoutProperty(
847
+ // layer.id,
848
+ // "text-field"
849
+ // );
850
+ // }
851
+ // }
852
+
853
+ /**
854
+ * Perform an action when the style is ready. It could be at the moment of calling this method
855
+ * or later.
856
+ * @param cb
857
+ */
858
+ private onStyleReady(cb) {
859
+ if (this.isStyleLoaded()) {
860
+ cb();
861
+ } else {
862
+ this.once("styledata", () => {
863
+ cb();
864
+ });
865
+ }
866
+ }
867
+
868
+ async fitToIpBounds() {
869
+ const ipGeolocateResult = await geolocation.info();
870
+ this.fitBounds(
871
+ ipGeolocateResult.country_bounds as [number, number, number, number],
872
+ {
873
+ duration: 0,
874
+ padding: 100,
875
+ }
876
+ );
877
+ }
878
+
879
+ async centerOnIpPoint(zoom: number | undefined) {
880
+ const ipGeolocateResult = await geolocation.info();
881
+ this.jumpTo({
882
+ center: [ipGeolocateResult.longitude, ipGeolocateResult.latitude],
883
+ zoom: zoom || 11,
884
+ });
885
+ }
886
+
887
+ getCameraHash() {
888
+ const hashBin = new Float32Array(5);
889
+ const center = this.getCenter();
890
+ hashBin[0] = center.lng;
891
+ hashBin[1] = center.lat;
892
+ hashBin[2] = this.getZoom();
893
+ hashBin[3] = this.getPitch();
894
+ hashBin[4] = this.getBearing();
895
+ return Base64.fromUint8Array(new Uint8Array(hashBin.buffer));
896
+ }
897
+ }