@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
@@ -0,0 +1,1511 @@
1
+ import type { Geometry, FeatureCollection, GeoJsonProperties } from "geojson";
2
+ import type {
3
+ DataDrivenPropertyValueSpecification,
4
+ PropertyValueSpecification,
5
+ } from "maplibre-gl";
6
+ import type { Map } from "../Map";
7
+ import { config } from "../config";
8
+ import { isUUID, jsonParseNoThrow } from "../tools";
9
+ import {
10
+ computeRampedOutlineWidth,
11
+ generateRandomLayerName,
12
+ generateRandomSourceName,
13
+ getRandomColor,
14
+ paintColorOptionsToPaintSpec,
15
+ rampedOptionsToLayerPaintSpec,
16
+ dashArrayMaker,
17
+ colorDrivenByProperty,
18
+ radiusDrivenByProperty,
19
+ opacityDrivenByProperty,
20
+ heatmapIntensityFromColorRamp,
21
+ rampedPropertyValueWeight,
22
+ radiusDrivenByPropertyHeatmap,
23
+ } from "./stylehelper";
24
+
25
+ import { gpx, gpxOrKml, kml } from "../converters";
26
+ import { ColorRampCollection, ColorRamp } from "../colorramp";
27
+
28
+ /**
29
+ * Array of string values that depend on zoom level
30
+ */
31
+ export type ZoomStringValues = Array<{
32
+ /**
33
+ * Zoom level
34
+ */
35
+ zoom: number;
36
+
37
+ /**
38
+ * Value for the given zoom level
39
+ */
40
+ value: string;
41
+ }>;
42
+
43
+ /**
44
+ *
45
+ * Array of number values that depend on zoom level
46
+ */
47
+ export type ZoomNumberValues = Array<{
48
+ /**
49
+ * Zoom level
50
+ */
51
+ zoom: number;
52
+
53
+ /**
54
+ * Value for the given zoom level
55
+ */
56
+ value: number;
57
+ }>;
58
+
59
+ export type PropertyValues = Array<{
60
+ /**
61
+ * Value of the property (input)
62
+ */
63
+ propertyValue: number;
64
+
65
+ /**
66
+ * Value to associate it with (output)
67
+ */
68
+ value: number;
69
+ }>;
70
+
71
+ /**
72
+ * Describes how to render a cluster of points
73
+ */
74
+ export type DataDrivenStyle = Array<{
75
+ /**
76
+ * Numerical value to observe and apply the style upon.
77
+ * In case of clusters, the value to observe is automatically the number of elements in a cluster.
78
+ * In other cases, it can be a provided value.
79
+ */
80
+ value: number;
81
+
82
+ /**
83
+ * Radius of the cluster circle
84
+ */
85
+ pointRadius: number;
86
+
87
+ /**
88
+ * Color of the cluster
89
+ */
90
+ color: string;
91
+ }>;
92
+
93
+ export type CommonShapeLayerOptions = {
94
+ /**
95
+ * ID to give to the layer.
96
+ * If not provided, an auto-generated ID of the for "maptiler-layer-xxxxxx" will be auto-generated,
97
+ * with "xxxxxx" being a random string.
98
+ */
99
+ layerId?: string;
100
+
101
+ /**
102
+ * ID to give to the geojson source.
103
+ * If not provided, an auto-generated ID of the for "maptiler-source-xxxxxx" will be auto-generated,
104
+ * with "xxxxxx" being a random string.
105
+ */
106
+ sourceId?: string;
107
+
108
+ /**
109
+ * A geojson Feature collection or a URL to a geojson or the UUID of a MapTiler Cloud dataset.
110
+ */
111
+ data: FeatureCollection | string;
112
+
113
+ /**
114
+ * The ID of an existing layer to insert the new layer before, resulting in the new layer appearing
115
+ * visually beneath the existing layer. If this argument is not specified, the layer will be appended
116
+ * to the end of the layers array and appear visually above all other layers.
117
+ */
118
+ beforeId?: string;
119
+
120
+ /**
121
+ * Zoom level at which it starts to show.
122
+ * Default: `0`
123
+ */
124
+ minzoom?: number;
125
+
126
+ /**
127
+ * Zoom level after which it no longer show.
128
+ * Default: `22`
129
+ */
130
+ maxzoom?: number;
131
+
132
+ /**
133
+ * Whether or not to add an outline.
134
+ * Default: `false`
135
+ */
136
+ outline?: boolean;
137
+
138
+ /**
139
+ * Color of the outline. This is can be a constant color string or a definition based on zoom levels.
140
+ * Applies only if `.outline` is `true`.
141
+ * Default: `white`
142
+ */
143
+ outlineColor?: string | ZoomStringValues;
144
+
145
+ /**
146
+ * Width of the outline (relative to screen-space). This is can be a constant width or a definition based on zoom levels.
147
+ * Applies only if `.outline` is `true`.
148
+ * Default: `1`
149
+ */
150
+ outlineWidth?: number | ZoomNumberValues;
151
+
152
+ /**
153
+ * Opacity of the outline. This is can be a constant opacity in [0, 1] or a definition based on zoom levels
154
+ * Applies only if `.outline` is `true`.
155
+ * Default: `1`
156
+ */
157
+ outlineOpacity?: number | ZoomNumberValues;
158
+ };
159
+
160
+ export type PolylineLayerOptions = CommonShapeLayerOptions & {
161
+ /**
162
+ * Color of the line (or polyline). This is can be a constant color string or a definition based on zoom levels.
163
+ * Default: a color randomly pick from a list
164
+ */
165
+ lineColor?: string | ZoomStringValues;
166
+
167
+ /**
168
+ * Width of the line (relative to screen-space). This is can be a constant width or a definition based on zoom levels
169
+ * Default: `3`
170
+ */
171
+ lineWidth?: number | ZoomNumberValues;
172
+
173
+ /**
174
+ * Opacity of the line. This is can be a constant opacity in [0, 1] or a definition based on zoom levels.
175
+ * Default: `1`
176
+ */
177
+ lineOpacity?: number | ZoomNumberValues;
178
+
179
+ /**
180
+ * How blury the line is, with `0` being no blur and `10` and beyond being quite blurry.
181
+ * Default: `0`
182
+ */
183
+ lineBlur?: number | ZoomNumberValues;
184
+
185
+ /**
186
+ * Draws a line casing outside of a line's actual path. Value indicates the width of the inner gap.
187
+ * Default: `0`
188
+ */
189
+ lineGapWidth?: number | ZoomNumberValues;
190
+
191
+ /**
192
+ * Sequence of line and void to create a dash pattern. The unit is the line width so that
193
+ * a dash array value of `[3, 1]` will create a segment worth 3 times the width of the line,
194
+ * followed by a spacing worth 1 time the line width, and then repeat.
195
+ *
196
+ * Alternatively, this property can be a string made of underscore and whitespace characters
197
+ * such as `"___ _ "` and internaly this will be translated into [3, 1, 1, 1]. Note that
198
+ * this way of describing dash arrays with a string only works for integer values.
199
+ *
200
+ * Dash arrays can contain more than 2 element to create more complex patters. For instance
201
+ * a dash array value of [3, 2, 1, 2] will create the following sequence:
202
+ * - a segment worth 3 times the width
203
+ * - a spacing worth 2 times the width
204
+ * - a segment worth 1 times the width
205
+ * - a spacing worth 2 times the width
206
+ * - repeat
207
+ *
208
+ * Default: no dash pattern
209
+ */
210
+ lineDashArray?: Array<number> | string;
211
+
212
+ /**
213
+ * The display of line endings for both the line and the outline (if `.outline` is `true`)
214
+ * - "butt": A cap with a squared-off end which is drawn to the exact endpoint of the line.
215
+ * - "round": A cap with a rounded end which is drawn beyond the endpoint of the line at a radius of one-half of the line's width and centered on the endpoint of the line.
216
+ * - "square": A cap with a squared-off end which is drawn beyond the endpoint of the line at a distance of one-half of the line's width.
217
+ * Default: "round"
218
+ */
219
+ lineCap?: "butt" | "round" | "square";
220
+
221
+ /**
222
+ * The display of lines when joining for both the line and the outline (if `.outline` is `true`)
223
+ * - "bevel": A join with a squared-off end which is drawn beyond the endpoint of the line at a distance of one-half of the line's width.
224
+ * - "round": A join with a rounded end which is drawn beyond the endpoint of the line at a radius of one-half of the line's width and centered on the endpoint of the line.
225
+ * - "miter": A join with a sharp, angled corner which is drawn with the outer sides beyond the endpoint of the path until they meet.
226
+ * Default: "round"
227
+ */
228
+ lineJoin?: "bevel" | "round" | "miter";
229
+
230
+ /**
231
+ * How blury the outline is, with `0` being no blur and `10` and beyond being quite blurry.
232
+ * Applies only if `.outline` is `true`.
233
+ * Default: `0`
234
+ */
235
+ outlineBlur?: number | ZoomNumberValues;
236
+ };
237
+
238
+ export type PolygonLayerOptions = CommonShapeLayerOptions & {
239
+ /**
240
+ * Color of the polygon. This is can be a constant color string or a definition based on zoom levels.
241
+ * Default: a color randomly pick from a list
242
+ */
243
+ fillColor?: string | ZoomStringValues;
244
+
245
+ /**
246
+ * Opacity of the polygon. This is can be a constant opacity in [0, 1] or a definition based on zoom levels
247
+ * Default: `1`
248
+ */
249
+ fillOpacity?: ZoomNumberValues;
250
+
251
+ /**
252
+ * Position of the outline with regard to the polygon edge (when `.outline` is `true`)
253
+ * Default: `"center"`
254
+ */
255
+ outlinePosition: "center" | "inside" | "outside";
256
+
257
+ /**
258
+ * Sequence of line and void to create a dash pattern. The unit is the line width so that
259
+ * a dash array value of `[3, 1]` will create a segment worth 3 times the width of the line,
260
+ * followed by a spacing worth 1 time the line width, and then repeat.
261
+ *
262
+ * Alternatively, this property can be a string made of underscore and whitespace characters
263
+ * such as `"___ _ "` and internaly this will be translated into [3, 1, 1, 1]. Note that
264
+ * this way of describing dash arrays with a string only works for integer values.
265
+ *
266
+ * Dash arrays can contain more than 2 element to create more complex patters. For instance
267
+ * a dash array value of [3, 2, 1, 2] will create the following sequence:
268
+ * - a segment worth 3 times the width
269
+ * - a spacing worth 2 times the width
270
+ * - a segment worth 1 times the width
271
+ * - a spacing worth 2 times the width
272
+ * - repeat
273
+ *
274
+ * Default: no dash pattern
275
+ */
276
+ outlineDashArray?: Array<number> | string;
277
+
278
+ /**
279
+ * The display of line endings for both the line and the outline (if `.outline` is `true`)
280
+ * - "butt": A cap with a squared-off end which is drawn to the exact endpoint of the line.
281
+ * - "round": A cap with a rounded end which is drawn beyond the endpoint of the line at a radius of one-half of the line's width and centered on the endpoint of the line.
282
+ * - "square": A cap with a squared-off end which is drawn beyond the endpoint of the line at a distance of one-half of the line's width.
283
+ * Default: "round"
284
+ */
285
+ outlineCap?: "butt" | "round" | "square";
286
+
287
+ /**
288
+ * The display of lines when joining for both the line and the outline (if `.outline` is `true`)
289
+ * - "bevel": A join with a squared-off end which is drawn beyond the endpoint of the line at a distance of one-half of the line's width.
290
+ * - "round": A join with a rounded end which is drawn beyond the endpoint of the line at a radius of one-half of the line's width and centered on the endpoint of the line.
291
+ * - "miter": A join with a sharp, angled corner which is drawn with the outer sides beyond the endpoint of the path until they meet.
292
+ * Default: "round"
293
+ */
294
+ outlineJoin?: "bevel" | "round" | "miter";
295
+
296
+ /**
297
+ * The pattern is an image URL to be put as a repeated background pattern of the polygon.
298
+ * Default: `null` (no pattern, `fillColor` will be used)
299
+ */
300
+ pattern?: string | null;
301
+
302
+ /**
303
+ * How blury the outline is, with `0` being no blur and `10` and beyond being quite blurry.
304
+ * Applies only if `.outline` is `true`.
305
+ * Default: `0`
306
+ */
307
+ outlineBlur?: number | ZoomNumberValues;
308
+ };
309
+
310
+ export type PointLayerOptions = CommonShapeLayerOptions & {
311
+ /**
312
+ * Can be a unique point color as a string (CSS color such as "#FF0000" or "red").
313
+ * Alternatively, the color can be a ColorRamp with a range.
314
+ * In case of `.cluster` being `true`, the range of the ColorRamp will be addressed with the number of elements in
315
+ * the cluster. If `.cluster` is `false`, the color will be addressed using the value of the `.property`.
316
+ * If no `.property` is given but `.pointColor` is a ColorRamp, the chosen color is the one at the lower bound of the ColorRamp.
317
+ * Default: a color randomly pick from a list
318
+ */
319
+ pointColor?: string | ColorRamp;
320
+
321
+ /**
322
+ * Radius of the points. Can be a fixed size or a value dependant on the zoom.
323
+ * If `.pointRadius` is not provided, the radius will depend on the size of each cluster (if `.cluster` is `true`)
324
+ * or on the value of each point (if `.property` is provided and `.pointColor` is a ColorRamp).
325
+ * The radius will be between `.minPointRadius` and `.maxPointRadius`
326
+ */
327
+ pointRadius?: number | ZoomNumberValues;
328
+
329
+ /**
330
+ * The minimum point radius posible.
331
+ * Default: `10`
332
+ */
333
+ minPointRadius?: number;
334
+
335
+ /**
336
+ * The maximum point radius posible.
337
+ * Default: `40`
338
+ */
339
+ maxPointRadius?: number;
340
+
341
+ /**
342
+ * The point property to observe and apply the radius and color upon.
343
+ * This is ignored if `.cluster` is `true` as the observed value will be fiorced to being the number
344
+ * of elements in each cluster.
345
+ *
346
+ * Default: none
347
+ */
348
+ property?: string;
349
+
350
+ /**
351
+ * Opacity of the point or icon. This is can be a constant opacity in [0, 1] or a definition based on zoom levels.
352
+ * Alternatively, if not provided but the `.pointColor` is a ColorRamp, the opacity will be extracted from tha alpha
353
+ * component if present.
354
+ * Default: `1`
355
+ */
356
+ pointOpacity?: number | ZoomNumberValues;
357
+
358
+ /**
359
+ * If `true`, the points will keep their circular shape align with the wiewport.
360
+ * If `false`, the points will be like flatten on the map. This difference shows
361
+ * when the map is tilted.
362
+ * Default: `true`
363
+ */
364
+ alignOnViewport?: boolean;
365
+
366
+ /**
367
+ * Whether the points should cluster
368
+ */
369
+ cluster?: boolean;
370
+
371
+ /**
372
+ * Shows a label with the numerical value id `true`.
373
+ * If `.cluster` is `true`, the value will be the numebr of elements in the cluster.
374
+ *
375
+ *
376
+ * Default: `true` if `cluster` or `dataDrivenStyleProperty` are used, `false` otherwise.
377
+ */
378
+ showLabel?: boolean;
379
+
380
+ /**
381
+ * text color used for the number elements in each cluster.
382
+ * Applicable only when `cluster` is `true`.
383
+ * Default: `#000000` (black)
384
+ */
385
+ labelColor?: string;
386
+
387
+ /**
388
+ * text size used for the number elements in each cluster.
389
+ * Applicable only when `cluster` is `true`.
390
+ * Default: `12`
391
+ */
392
+ labelSize?: number;
393
+
394
+ /**
395
+ * Only if `.cluster` is `false`.
396
+ * If the radius is driven by a property, then it will also scale by zoomming if `.zoomCompensation` is `true`.
397
+ * If `false`, the radius will not adapt according to the zoom level.
398
+ * Default: `true`
399
+ */
400
+ zoomCompensation?: boolean;
401
+ };
402
+
403
+ export type HeatmapLayerOptions = {
404
+ /**
405
+ * ID to give to the layer.
406
+ * If not provided, an auto-generated ID of the for "maptiler-layer-xxxxxx" will be auto-generated,
407
+ * with "xxxxxx" being a random string.
408
+ */
409
+ layerId?: string;
410
+
411
+ /**
412
+ * ID to give to the geojson source.
413
+ * If not provided, an auto-generated ID of the for "maptiler-source-xxxxxx" will be auto-generated,
414
+ * with "xxxxxx" being a random string.
415
+ */
416
+ sourceId?: string;
417
+
418
+ /**
419
+ * A geojson Feature collection or a URL to a geojson or the UUID of a MapTiler Cloud dataset.
420
+ */
421
+ data: FeatureCollection | string;
422
+
423
+ /**
424
+ * The ID of an existing layer to insert the new layer before, resulting in the new layer appearing
425
+ * visually beneath the existing layer. If this argument is not specified, the layer will be appended
426
+ * to the end of the layers array and appear visually above all other layers.
427
+ */
428
+ beforeId?: string;
429
+
430
+ /**
431
+ * Zoom level at which it starts to show.
432
+ * Default: `0`
433
+ */
434
+ minzoom?: number;
435
+
436
+ /**
437
+ * Zoom level after which it no longer show.
438
+ * Default: `22`
439
+ */
440
+ maxzoom?: number;
441
+
442
+ /**
443
+ * The ColorRamp instance to use for visualization. The color ramp is expected to be defined in the
444
+ * range `[0, 1]` or else will be forced to this range.
445
+ * Default: `ColorRampCollection.TURBO`
446
+ */
447
+ colorRamp?: ColorRamp;
448
+
449
+ /**
450
+ * Use a property to apply a weight to each data point. Using a property requires also using
451
+ * the options `.propertyValueWeight` or otherwise will be ignored.
452
+ * Default: none, the points will all have a weight of `1`.
453
+ */
454
+ property?: string;
455
+
456
+ /**
457
+ * The weight to give to each data point. If of type `PropertyValueWeights`, then the options `.property`
458
+ * must also be provided. If used a number, all data points will be weighted by the same number (which is of little interest)
459
+ */
460
+ weight?: PropertyValues | number;
461
+
462
+ /**
463
+ * The radius (in screenspace) can be:
464
+ * - a fixed number that will be constant across zoom level
465
+ * - of type `ZoomNumberValues` to be ramped accoding to zoom level (`.zoomCompensation` will then be ignored)
466
+ * - of type `PropertyValues` to be driven by the value of a property.
467
+ * If so, the option `.property` must be provided and will still be resized according to zoom level,
468
+ * unless the option `.zoomCompensation` is set to `false`.
469
+ *
470
+ * Default:
471
+ */
472
+ radius?: number | ZoomNumberValues | PropertyValues;
473
+
474
+ /**
475
+ * The opacity can be a fixed value or zoom-driven.
476
+ * Default: fades-in 0.25z after minzoom and fade-out 0.25z before maxzoom
477
+ */
478
+ opacity?: number | ZoomNumberValues;
479
+
480
+ /**
481
+ * The intensity is zoom-dependent. By default, the intensity is going to be scaled by zoom to preserve
482
+ * a natural aspect or the data distribution.
483
+ */
484
+ intensity?: number | ZoomNumberValues;
485
+
486
+ /**
487
+ * If the radius is driven by a property, then it will also scale by zoomming if `.zoomCompensation` is `true`.
488
+ * If `false`, the radius will not adapt according to the zoom level.
489
+ * Default: `true`
490
+ */
491
+ zoomCompensation?: boolean;
492
+ };
493
+
494
+ /**
495
+ * Add a polyline to the map from various sources and with builtin styling.
496
+ * Compatible sources:
497
+ * - gpx content as string
498
+ * - gpx file from URL
499
+ * - kml content from string
500
+ * - kml from url
501
+ * - geojson from url
502
+ * - geojson content as string
503
+ * - geojson content as JS object
504
+ * - uuid of a MapTiler Cloud dataset
505
+ *
506
+ * The method also gives the possibility to add an outline layer (if `options.outline` is `true`)
507
+ * and if so , the returned property `polylineOutlineLayerId` will be a string. As a result, two layers
508
+ * would be added.
509
+ *
510
+ * The default styling creates a line layer of constant width of 3px, the color will be randomly picked
511
+ * from a curated list of colors and the opacity will be 1.
512
+ * If the outline is enabled, the outline width is of 1px at all zoom levels, the color is white and
513
+ * the opacity is 1.
514
+ *
515
+ * Those style properties can be changed and ramped according to zoom level using an easier syntax.
516
+ *
517
+ */
518
+ export async function addPolyline(
519
+ /**
520
+ * Map instance to add a polyline layer to
521
+ */
522
+ map: Map,
523
+ /**
524
+ * Options related to adding a polyline layer
525
+ */
526
+ options: PolylineLayerOptions,
527
+ /**
528
+ * When the polyline data is loaded from a distant source, these options are propagated to the call of `fetch`
529
+ */
530
+ fetchOptions: RequestInit = {},
531
+ ): Promise<{
532
+ polylineLayerId: string;
533
+ polylineOutlineLayerId: string;
534
+ polylineSourceId: string;
535
+ }> {
536
+ // We need to have the sourceId of the sourceData
537
+ if (!options.sourceId && !options.data) {
538
+ throw new Error(
539
+ "Creating a polyline layer requires an existing .sourceId or a valid .data property",
540
+ );
541
+ }
542
+
543
+ // We are going to evaluate the content of .data, if provided
544
+ let data = options.data;
545
+
546
+ if (typeof data === "string") {
547
+ // if options.data exists and is a uuid string, we consider that it points to a MapTiler Dataset
548
+ if (isUUID(data)) {
549
+ data = `https://api.maptiler.com/data/${options.data}/features.json?key=${config.apiKey}`;
550
+ }
551
+
552
+ // options.data could be a url to a .gpx file
553
+ else if (data.split(".").pop()?.toLowerCase().trim() === "gpx") {
554
+ // fetch the file
555
+ const res = await fetch(data, fetchOptions);
556
+ const gpxStr = await res.text();
557
+ // Convert it to geojson. Will throw is invalid GPX content
558
+ data = gpx(gpxStr);
559
+ }
560
+
561
+ // options.data could be a url to a .kml file
562
+ else if (data.split(".").pop()?.toLowerCase().trim() === "kml") {
563
+ // fetch the file
564
+ const res = await fetch(data, fetchOptions);
565
+ const kmlStr = await res.text();
566
+ // Convert it to geojson. Will throw is invalid GPX content
567
+ data = kml(kmlStr);
568
+ } else {
569
+ // From this point, we consider that the string content provided could
570
+ // be the string content of one of the compatible format (GeoJSON, KML, GPX)
571
+ const tmpData =
572
+ jsonParseNoThrow<FeatureCollection<Geometry, GeoJsonProperties>>(
573
+ data,
574
+ ) ?? gpxOrKml(data);
575
+ if (tmpData) data = tmpData;
576
+ }
577
+
578
+ if (!data) {
579
+ throw new Error(
580
+ "Polyline data was provided as string but is incompatible with valid formats.",
581
+ );
582
+ }
583
+ }
584
+
585
+ return addGeoJSONPolyline(map, {
586
+ ...options,
587
+ data,
588
+ });
589
+ }
590
+
591
+ /**
592
+ * Add a polyline from a GeoJSON object
593
+ */
594
+ function addGeoJSONPolyline(
595
+ map: Map,
596
+ // The data or data source is expected to contain LineStrings or MultiLineStrings
597
+ options: PolylineLayerOptions,
598
+ ): {
599
+ /**
600
+ * ID of the main line layer
601
+ */
602
+ polylineLayerId: string;
603
+
604
+ /**
605
+ * ID of the outline layer (will be `""` if no outline)
606
+ */
607
+ polylineOutlineLayerId: string;
608
+
609
+ /**
610
+ * ID of the data source
611
+ */
612
+ polylineSourceId: string;
613
+ } {
614
+ if (options.layerId && map.getLayer(options.layerId)) {
615
+ throw new Error(
616
+ `A layer already exists with the layer id: ${options.layerId}`,
617
+ );
618
+ }
619
+
620
+ const sourceId = options.sourceId ?? generateRandomSourceName();
621
+ const layerId = options.layerId ?? generateRandomLayerName();
622
+
623
+ const returnedInfo = {
624
+ polylineLayerId: layerId,
625
+ polylineOutlineLayerId: "",
626
+ polylineSourceId: sourceId,
627
+ };
628
+
629
+ // A new source is added if the map does not have this sourceId and the data is provided
630
+ if (options.data && !map.getSource(sourceId)) {
631
+ // Adding the source
632
+ map.addSource(sourceId, {
633
+ type: "geojson",
634
+ data: options.data,
635
+ });
636
+ }
637
+
638
+ const lineWidth = options.lineWidth ?? 3;
639
+ const lineColor = options.lineColor ?? getRandomColor();
640
+ const lineOpacity = options.lineOpacity ?? 1;
641
+ const lineBlur = options.lineBlur ?? 0;
642
+ const lineGapWidth = options.lineGapWidth ?? 0;
643
+ let lineDashArray = options.lineDashArray ?? null;
644
+ const outlineWidth = options.outlineWidth ?? 1;
645
+ const outlineColor = options.outlineColor ?? "#FFFFFF";
646
+ const outlineOpacity = options.outlineOpacity ?? 1;
647
+ const outlineBlur = options.outlineBlur ?? 0;
648
+
649
+ if (typeof lineDashArray === "string") {
650
+ lineDashArray = dashArrayMaker(lineDashArray);
651
+ }
652
+
653
+ // We want to create an outline for this line layer
654
+ if (options.outline === true) {
655
+ const outlineLayerId = `${layerId}_outline`;
656
+ returnedInfo.polylineOutlineLayerId = outlineLayerId;
657
+
658
+ map.addLayer(
659
+ {
660
+ id: outlineLayerId,
661
+ type: "line",
662
+ source: sourceId,
663
+ layout: {
664
+ "line-join": options.lineJoin ?? "round",
665
+ "line-cap": options.lineCap ?? "round",
666
+ },
667
+ minzoom: options.minzoom ?? 0,
668
+ maxzoom: options.maxzoom ?? 23,
669
+ paint: {
670
+ "line-opacity":
671
+ typeof outlineOpacity === "number"
672
+ ? outlineOpacity
673
+ : rampedOptionsToLayerPaintSpec(outlineOpacity),
674
+ "line-color":
675
+ typeof outlineColor === "string"
676
+ ? outlineColor
677
+ : paintColorOptionsToPaintSpec(outlineColor),
678
+ "line-width": computeRampedOutlineWidth(lineWidth, outlineWidth),
679
+ "line-blur":
680
+ typeof outlineBlur === "number"
681
+ ? outlineBlur
682
+ : rampedOptionsToLayerPaintSpec(outlineBlur),
683
+ },
684
+ },
685
+ options.beforeId,
686
+ );
687
+ }
688
+
689
+ map.addLayer(
690
+ {
691
+ id: layerId,
692
+ type: "line",
693
+ source: sourceId,
694
+ layout: {
695
+ "line-join": options.lineJoin ?? "round",
696
+ "line-cap": options.lineCap ?? "round",
697
+ },
698
+ minzoom: options.minzoom ?? 0,
699
+ maxzoom: options.maxzoom ?? 23,
700
+ paint: {
701
+ "line-opacity":
702
+ typeof lineOpacity === "number"
703
+ ? lineOpacity
704
+ : rampedOptionsToLayerPaintSpec(lineOpacity),
705
+ "line-color":
706
+ typeof lineColor === "string"
707
+ ? lineColor
708
+ : paintColorOptionsToPaintSpec(lineColor),
709
+ "line-width":
710
+ typeof lineWidth === "number"
711
+ ? lineWidth
712
+ : rampedOptionsToLayerPaintSpec(lineWidth),
713
+
714
+ "line-blur":
715
+ typeof lineBlur === "number"
716
+ ? lineBlur
717
+ : rampedOptionsToLayerPaintSpec(lineBlur),
718
+
719
+ "line-gap-width":
720
+ typeof lineGapWidth === "number"
721
+ ? lineGapWidth
722
+ : rampedOptionsToLayerPaintSpec(lineGapWidth),
723
+
724
+ // For some reasons passing "line-dasharray" with the value "undefined"
725
+ // results in no showing the line while it should have the same behavior
726
+ // of not adding the property "line-dasharray" as all.
727
+ // As a workaround, we are inlining the addition of the prop with a conditional
728
+ // which is less readable.
729
+ ...(lineDashArray && { "line-dasharray": lineDashArray }),
730
+ },
731
+ },
732
+ options.beforeId,
733
+ );
734
+
735
+ return returnedInfo;
736
+ }
737
+
738
+ /**
739
+ * Add a polygon with styling options.
740
+ */
741
+ export function addPolygon(
742
+ map: Map,
743
+ // this Feature collection is expected to contain on LineStrings and MultiLinestrings
744
+ options: PolygonLayerOptions,
745
+ ): {
746
+ /**
747
+ * ID of the fill layer
748
+ */
749
+ polygonLayerId: string;
750
+
751
+ /**
752
+ * ID of the outline layer (will be `""` if no outline)
753
+ */
754
+ polygonOutlineLayerId: string;
755
+
756
+ /**
757
+ * ID of the source that contains the data
758
+ */
759
+ polygonSourceId: string;
760
+ } {
761
+ if (options.layerId && map.getLayer(options.layerId)) {
762
+ throw new Error(
763
+ `A layer already exists with the layer id: ${options.layerId}`,
764
+ );
765
+ }
766
+
767
+ const sourceId = options.sourceId ?? generateRandomSourceName();
768
+ const layerId = options.layerId ?? generateRandomLayerName();
769
+
770
+ const returnedInfo = {
771
+ polygonLayerId: layerId,
772
+ polygonOutlineLayerId: options.outline ? `${layerId}_outline` : "",
773
+ polygonSourceId: sourceId,
774
+ };
775
+
776
+ // A new source is added if the map does not have this sourceId and the data is provided
777
+ if (options.data && !map.getSource(sourceId)) {
778
+ let data: string | FeatureCollection = options.data;
779
+
780
+ // If is a UUID, we extend it to be the URL to a MapTiler Cloud hosted dataset
781
+ if (typeof data === "string" && isUUID(data)) {
782
+ data = `https://api.maptiler.com/data/${data}/features.json?key=${config.apiKey}`;
783
+ }
784
+
785
+ // Adding the source
786
+ map.addSource(sourceId, {
787
+ type: "geojson",
788
+ data: data,
789
+ });
790
+ }
791
+
792
+ let outlineDashArray = options.outlineDashArray ?? null;
793
+ const outlineWidth = options.outlineWidth ?? 1;
794
+ const outlineColor = options.outlineColor ?? "#FFFFFF";
795
+ const outlineOpacity = options.outlineOpacity ?? 1;
796
+ const outlineBlur = options.outlineBlur ?? 0;
797
+ const fillColor = options.fillColor ?? getRandomColor();
798
+ const fillOpacity = options.fillOpacity ?? 1;
799
+ const outlinePosition = options.outlinePosition ?? "center";
800
+ const pattern = options.pattern ?? null;
801
+
802
+ if (typeof outlineDashArray === "string") {
803
+ outlineDashArray = dashArrayMaker(outlineDashArray);
804
+ }
805
+
806
+ const addLayers = (patternImageId: string | null = null) => {
807
+ map.addLayer(
808
+ {
809
+ id: layerId,
810
+ type: "fill",
811
+ source: sourceId,
812
+ minzoom: options.minzoom ?? 0,
813
+ maxzoom: options.maxzoom ?? 23,
814
+ paint: {
815
+ "fill-color":
816
+ typeof fillColor === "string"
817
+ ? fillColor
818
+ : paintColorOptionsToPaintSpec(fillColor),
819
+
820
+ "fill-opacity":
821
+ typeof fillOpacity === "number"
822
+ ? fillOpacity
823
+ : rampedOptionsToLayerPaintSpec(fillOpacity),
824
+
825
+ // Adding a pattern if provided
826
+ ...(patternImageId && { "fill-pattern": patternImageId }),
827
+ },
828
+ },
829
+ options.beforeId,
830
+ );
831
+
832
+ // We want to create an outline for this line layer
833
+ if (options.outline === true) {
834
+ let computedOutlineOffset:
835
+ | DataDrivenPropertyValueSpecification<number>
836
+ | number;
837
+
838
+ if (outlinePosition === "inside") {
839
+ if (typeof outlineWidth === "number") {
840
+ computedOutlineOffset = 0.5 * outlineWidth;
841
+ } else {
842
+ computedOutlineOffset = rampedOptionsToLayerPaintSpec(
843
+ outlineWidth.map(({ zoom, value }) => ({
844
+ zoom,
845
+ value: 0.5 * value,
846
+ })),
847
+ );
848
+ }
849
+ } else if (outlinePosition === "outside") {
850
+ if (typeof outlineWidth === "number") {
851
+ computedOutlineOffset = -0.5 * outlineWidth;
852
+ } else {
853
+ computedOutlineOffset = rampedOptionsToLayerPaintSpec(
854
+ outlineWidth.map((el) => ({
855
+ zoom: el.zoom,
856
+ value: -0.5 * el.value,
857
+ })),
858
+ );
859
+ }
860
+ } else {
861
+ computedOutlineOffset = 0;
862
+ }
863
+
864
+ map.addLayer(
865
+ {
866
+ id: returnedInfo.polygonOutlineLayerId,
867
+ type: "line",
868
+ source: sourceId,
869
+ layout: {
870
+ "line-join": options.outlineJoin ?? "round",
871
+ "line-cap": options.outlineCap ?? "butt",
872
+ },
873
+ minzoom: options.minzoom ?? 0,
874
+ maxzoom: options.maxzoom ?? 23,
875
+ paint: {
876
+ "line-opacity":
877
+ typeof outlineOpacity === "number"
878
+ ? outlineOpacity
879
+ : rampedOptionsToLayerPaintSpec(outlineOpacity),
880
+ "line-color":
881
+ typeof outlineColor === "string"
882
+ ? outlineColor
883
+ : paintColorOptionsToPaintSpec(outlineColor),
884
+ "line-width":
885
+ typeof outlineWidth === "number"
886
+ ? outlineWidth
887
+ : rampedOptionsToLayerPaintSpec(outlineWidth),
888
+ "line-blur":
889
+ typeof outlineBlur === "number"
890
+ ? outlineBlur
891
+ : rampedOptionsToLayerPaintSpec(outlineBlur),
892
+
893
+ "line-offset": computedOutlineOffset,
894
+
895
+ // For some reasons passing "line-dasharray" with the value "undefined"
896
+ // results in no showing the line while it should have the same behavior
897
+ // of not adding the property "line-dasharray" as all.
898
+ // As a workaround, we are inlining the addition of the prop with a conditional
899
+ // which is less readable.
900
+ ...(outlineDashArray && {
901
+ "line-dasharray": outlineDashArray as PropertyValueSpecification<
902
+ number[]
903
+ >,
904
+ }),
905
+ },
906
+ },
907
+ options.beforeId,
908
+ );
909
+ }
910
+ };
911
+
912
+ if (pattern) {
913
+ if (map.hasImage(pattern)) {
914
+ addLayers(pattern);
915
+ } else {
916
+ map.loadImage(
917
+ pattern,
918
+
919
+ // (error?: Error | null, image?: HTMLImageElement | ImageBitmap | null, expiry?: ExpiryData | null)
920
+ (
921
+ error: Error | null | undefined,
922
+ image: HTMLImageElement | ImageBitmap | null | undefined,
923
+ ) => {
924
+ // Throw an error if something goes wrong.
925
+ if (error) {
926
+ console.error("Could not load the pattern image.", error.message);
927
+ return addLayers();
928
+ }
929
+
930
+ if (!image) {
931
+ console.error(
932
+ `An image cannot be created from the pattern URL ${pattern}.`,
933
+ );
934
+ return addLayers();
935
+ }
936
+
937
+ // Add the image to the map style, using the image URL as an ID
938
+ map.addImage(pattern, image);
939
+
940
+ addLayers(pattern);
941
+ },
942
+ );
943
+ }
944
+ } else {
945
+ addLayers();
946
+ }
947
+
948
+ return returnedInfo;
949
+ }
950
+
951
+ /**
952
+ * Add a point layer from a GeoJSON source (or an existing sourceId) with many styling options
953
+ */
954
+ export function addPoint(
955
+ /**
956
+ * The Map instance to add a point layer to
957
+ */
958
+ map: Map,
959
+ // The data or data source is expected to contain LineStrings or MultiLineStrings
960
+ options: PointLayerOptions,
961
+ ): {
962
+ /**
963
+ * ID of the unclustered point layer
964
+ */
965
+ pointLayerId: string;
966
+
967
+ /**
968
+ * ID of the clustered point layer (empty if `cluster` options id `false`)
969
+ */
970
+ clusterLayerId: string;
971
+
972
+ /**
973
+ * ID of the layer that shows the count of elements in each cluster (empty if `cluster` options id `false`)
974
+ */
975
+ labelLayerId: string;
976
+
977
+ /**
978
+ * ID of the data source
979
+ */
980
+ pointSourceId: string;
981
+ } {
982
+ if (options.layerId && map.getLayer(options.layerId)) {
983
+ throw new Error(
984
+ `A layer already exists with the layer id: ${options.layerId}`,
985
+ );
986
+ }
987
+
988
+ const minPointRadius = options.minPointRadius ?? 10;
989
+ const maxPointRadius = options.maxPointRadius ?? 50;
990
+ const cluster = options.cluster ?? false;
991
+ const nbDefaultDataDrivenStyleSteps = 20;
992
+ const colorramp = Array.isArray(options.pointColor)
993
+ ? options.pointColor
994
+ : ColorRampCollection.TURBO.scale(
995
+ 10,
996
+ options.cluster ? 10000 : 1000,
997
+ ).resample("ease-out-square");
998
+ const colorRampBounds = colorramp.getBounds();
999
+ const sourceId = options.sourceId ?? generateRandomSourceName();
1000
+ const layerId = options.layerId ?? generateRandomLayerName();
1001
+ const showLabel = options.showLabel ?? cluster;
1002
+ const alignOnViewport = options.alignOnViewport ?? true;
1003
+ const outline = options.outline ?? false;
1004
+ const outlineOpacity = options.outlineOpacity ?? 1;
1005
+ const outlineWidth = options.outlineWidth ?? 1;
1006
+ const outlineColor = options.outlineColor ?? "#FFFFFF";
1007
+ let pointOpacity;
1008
+ const zoomCompensation = options.zoomCompensation ?? true;
1009
+ const minzoom = options.minzoom ?? 0;
1010
+ const maxzoom = options.maxzoom ?? 23;
1011
+
1012
+ if (typeof options.pointOpacity === "number") {
1013
+ pointOpacity = options.pointOpacity;
1014
+ } else if (Array.isArray(options.pointOpacity)) {
1015
+ pointOpacity = rampedOptionsToLayerPaintSpec(options.pointOpacity);
1016
+ } else if (options.cluster) {
1017
+ pointOpacity = opacityDrivenByProperty(colorramp, "point_count");
1018
+ } else if (options.property) {
1019
+ pointOpacity = opacityDrivenByProperty(colorramp, options.property);
1020
+ } else {
1021
+ pointOpacity = rampedOptionsToLayerPaintSpec([
1022
+ { zoom: minzoom, value: 0 },
1023
+ { zoom: minzoom + 0.25, value: 1 },
1024
+ { zoom: maxzoom - 0.25, value: 1 },
1025
+ { zoom: maxzoom, value: 0 },
1026
+ ]);
1027
+ }
1028
+
1029
+ const returnedInfo = {
1030
+ pointLayerId: layerId,
1031
+ clusterLayerId: "",
1032
+ labelLayerId: "",
1033
+ pointSourceId: sourceId,
1034
+ };
1035
+
1036
+ // A new source is added if the map does not have this sourceId and the data is provided
1037
+ if (options.data && !map.getSource(sourceId)) {
1038
+ let data: string | FeatureCollection = options.data;
1039
+
1040
+ // If is a UUID, we extend it to be the URL to a MapTiler Cloud hosted dataset
1041
+ if (typeof data === "string" && isUUID(data)) {
1042
+ data = `https://api.maptiler.com/data/${data}/features.json?key=${config.apiKey}`;
1043
+ }
1044
+
1045
+ // Adding the source
1046
+ map.addSource(sourceId, {
1047
+ type: "geojson",
1048
+ data: data,
1049
+ cluster,
1050
+ });
1051
+ }
1052
+
1053
+ if (cluster) {
1054
+ // If using clusters, the size and color of the circles (clusters) are driven by the
1055
+ // numbner of elements they contain and cannot be driven by the zoom level or a property
1056
+
1057
+ returnedInfo.clusterLayerId = `${layerId}_cluster`;
1058
+
1059
+ const clusterStyle: DataDrivenStyle = Array.from(
1060
+ { length: nbDefaultDataDrivenStyleSteps },
1061
+ (_, i) => {
1062
+ const value =
1063
+ colorRampBounds.min +
1064
+ (i * (colorRampBounds.max - colorRampBounds.min)) /
1065
+ (nbDefaultDataDrivenStyleSteps - 1);
1066
+ return {
1067
+ value,
1068
+ pointRadius:
1069
+ minPointRadius +
1070
+ (maxPointRadius - minPointRadius) *
1071
+ Math.pow(i / (nbDefaultDataDrivenStyleSteps - 1), 0.5),
1072
+ color: colorramp.getColorHex(value),
1073
+ };
1074
+ },
1075
+ );
1076
+
1077
+ map.addLayer(
1078
+ {
1079
+ id: returnedInfo.clusterLayerId,
1080
+ type: "circle",
1081
+ source: sourceId,
1082
+ filter: ["has", "point_count"],
1083
+ paint: {
1084
+ // 'circle-color': options.pointColor ?? colorDrivenByProperty(clusterStyle, "point_count"),
1085
+ "circle-color":
1086
+ typeof options.pointColor === "string"
1087
+ ? options.pointColor
1088
+ : colorDrivenByProperty(clusterStyle, "point_count"),
1089
+
1090
+ "circle-radius":
1091
+ typeof options.pointRadius === "number"
1092
+ ? options.pointRadius
1093
+ : Array.isArray(options.pointRadius)
1094
+ ? rampedOptionsToLayerPaintSpec(options.pointRadius)
1095
+ : radiusDrivenByProperty(clusterStyle, "point_count", false),
1096
+
1097
+ "circle-pitch-alignment": alignOnViewport ? "viewport" : "map",
1098
+ "circle-pitch-scale": "map", // scale with camera distance regardless of viewport/biewport alignement
1099
+ "circle-opacity": pointOpacity,
1100
+ ...(outline && {
1101
+ "circle-stroke-opacity":
1102
+ typeof outlineOpacity === "number"
1103
+ ? outlineOpacity
1104
+ : rampedOptionsToLayerPaintSpec(outlineOpacity),
1105
+
1106
+ "circle-stroke-width":
1107
+ typeof outlineWidth === "number"
1108
+ ? outlineWidth
1109
+ : rampedOptionsToLayerPaintSpec(outlineWidth),
1110
+
1111
+ "circle-stroke-color":
1112
+ typeof outlineColor === "string"
1113
+ ? outlineColor
1114
+ : paintColorOptionsToPaintSpec(outlineColor),
1115
+ }),
1116
+ },
1117
+ minzoom,
1118
+ maxzoom,
1119
+ },
1120
+ options.beforeId,
1121
+ );
1122
+
1123
+ // Adding the layer of unclustered point (visible only when ungrouped)
1124
+ map.addLayer(
1125
+ {
1126
+ id: returnedInfo.pointLayerId,
1127
+ type: "circle",
1128
+ source: sourceId,
1129
+ filter: ["!", ["has", "point_count"]],
1130
+ paint: {
1131
+ "circle-pitch-alignment": alignOnViewport ? "viewport" : "map",
1132
+ "circle-pitch-scale": "map", // scale with camera distance regardless of viewport/biewport alignement
1133
+ // 'circle-color': options.pointColor ?? clusterStyle[0].color,
1134
+ "circle-color":
1135
+ typeof options.pointColor === "string"
1136
+ ? options.pointColor
1137
+ : colorramp.getColorHex(colorramp.getBounds().min),
1138
+ "circle-radius":
1139
+ typeof options.pointRadius === "number"
1140
+ ? options.pointRadius
1141
+ : Array.isArray(options.pointRadius)
1142
+ ? rampedOptionsToLayerPaintSpec(options.pointRadius)
1143
+ : clusterStyle[0].pointRadius * 0.75,
1144
+ "circle-opacity": pointOpacity,
1145
+ ...(outline && {
1146
+ "circle-stroke-opacity":
1147
+ typeof outlineOpacity === "number"
1148
+ ? outlineOpacity
1149
+ : rampedOptionsToLayerPaintSpec(outlineOpacity),
1150
+
1151
+ "circle-stroke-width":
1152
+ typeof outlineWidth === "number"
1153
+ ? outlineWidth
1154
+ : rampedOptionsToLayerPaintSpec(outlineWidth),
1155
+
1156
+ "circle-stroke-color":
1157
+ typeof outlineColor === "string"
1158
+ ? outlineColor
1159
+ : paintColorOptionsToPaintSpec(outlineColor),
1160
+ }),
1161
+ },
1162
+ minzoom,
1163
+ maxzoom,
1164
+ },
1165
+ options.beforeId,
1166
+ );
1167
+ }
1168
+
1169
+ // Not displaying clusters
1170
+ else {
1171
+ let pointColor: DataDrivenPropertyValueSpecification<string> =
1172
+ typeof options.pointColor === "string"
1173
+ ? options.pointColor
1174
+ : Array.isArray(options.pointColor)
1175
+ ? options.pointColor.getColorHex(options.pointColor.getBounds().min) // if color ramp is given, we choose the first color of it, even if the property may not be provided
1176
+ : getRandomColor();
1177
+
1178
+ let pointRadius: DataDrivenPropertyValueSpecification<number> =
1179
+ typeof options.pointRadius === "number"
1180
+ ? zoomCompensation
1181
+ ? rampedOptionsToLayerPaintSpec([
1182
+ { zoom: 0, value: options.pointRadius * 0.025 },
1183
+ { zoom: 2, value: options.pointRadius * 0.05 },
1184
+ { zoom: 4, value: options.pointRadius * 0.1 },
1185
+ { zoom: 8, value: options.pointRadius * 0.25 },
1186
+ { zoom: 16, value: options.pointRadius * 1 },
1187
+ ])
1188
+ : options.pointRadius
1189
+ : Array.isArray(options.pointRadius)
1190
+ ? rampedOptionsToLayerPaintSpec(options.pointRadius)
1191
+ : zoomCompensation
1192
+ ? rampedOptionsToLayerPaintSpec([
1193
+ { zoom: 0, value: minPointRadius * 0.05 },
1194
+ { zoom: 2, value: minPointRadius * 0.1 },
1195
+ { zoom: 4, value: minPointRadius * 0.2 },
1196
+ { zoom: 8, value: minPointRadius * 0.5 },
1197
+ { zoom: 16, value: minPointRadius * 1 },
1198
+ ])
1199
+ : minPointRadius;
1200
+
1201
+ // If the styling depends on a property, then we build a custom style
1202
+ if (options.property && Array.isArray(options.pointColor)) {
1203
+ const dataDrivenStyle: DataDrivenStyle = Array.from(
1204
+ { length: nbDefaultDataDrivenStyleSteps },
1205
+ (_, i) => {
1206
+ const value =
1207
+ colorRampBounds.min +
1208
+ (i * (colorRampBounds.max - colorRampBounds.min)) /
1209
+ (nbDefaultDataDrivenStyleSteps - 1);
1210
+ return {
1211
+ value,
1212
+ pointRadius:
1213
+ typeof options.pointRadius === "number"
1214
+ ? options.pointRadius
1215
+ : minPointRadius +
1216
+ (maxPointRadius - minPointRadius) *
1217
+ Math.pow(i / (nbDefaultDataDrivenStyleSteps - 1), 0.5),
1218
+ color:
1219
+ typeof options.pointColor === "string"
1220
+ ? options.pointColor
1221
+ : colorramp.getColorHex(value),
1222
+ };
1223
+ },
1224
+ );
1225
+ pointColor = colorDrivenByProperty(dataDrivenStyle, options.property);
1226
+ pointRadius = radiusDrivenByProperty(
1227
+ dataDrivenStyle,
1228
+ options.property,
1229
+ zoomCompensation,
1230
+ );
1231
+ }
1232
+
1233
+ // Adding the layer of unclustered point
1234
+ map.addLayer(
1235
+ {
1236
+ id: returnedInfo.pointLayerId,
1237
+ type: "circle",
1238
+ source: sourceId,
1239
+ layout: {
1240
+ // Contrary to labels, we want to see the small one in front. Weirdly "circle-sort-key" works in the opposite direction as "symbol-sort-key".
1241
+ "circle-sort-key": options.property
1242
+ ? ["/", 1, ["get", options.property]]
1243
+ : 0,
1244
+ },
1245
+ paint: {
1246
+ "circle-pitch-alignment": alignOnViewport ? "viewport" : "map",
1247
+ "circle-pitch-scale": "map", // scale with camera distance regardless of viewport/biewport alignement
1248
+ "circle-color": pointColor,
1249
+ "circle-opacity": pointOpacity,
1250
+ "circle-radius": pointRadius,
1251
+
1252
+ ...(outline && {
1253
+ "circle-stroke-opacity":
1254
+ typeof outlineOpacity === "number"
1255
+ ? outlineOpacity
1256
+ : rampedOptionsToLayerPaintSpec(outlineOpacity),
1257
+
1258
+ "circle-stroke-width":
1259
+ typeof outlineWidth === "number"
1260
+ ? outlineWidth
1261
+ : rampedOptionsToLayerPaintSpec(outlineWidth),
1262
+
1263
+ "circle-stroke-color":
1264
+ typeof outlineColor === "string"
1265
+ ? outlineColor
1266
+ : paintColorOptionsToPaintSpec(outlineColor),
1267
+ }),
1268
+ },
1269
+ minzoom,
1270
+ maxzoom,
1271
+ },
1272
+ options.beforeId,
1273
+ );
1274
+ }
1275
+
1276
+ if (showLabel !== false && (options.cluster || options.property)) {
1277
+ returnedInfo.labelLayerId = `${layerId}_label`;
1278
+ const labelColor = options.labelColor ?? "#fff";
1279
+ const labelSize = options.labelSize ?? 12;
1280
+
1281
+ // With clusters, a layer with clouster count is also added
1282
+ map.addLayer(
1283
+ {
1284
+ id: returnedInfo.labelLayerId,
1285
+ type: "symbol",
1286
+ source: sourceId,
1287
+ filter: [
1288
+ "has",
1289
+ options.cluster ? "point_count" : (options.property as string),
1290
+ ],
1291
+ layout: {
1292
+ "text-field": options.cluster
1293
+ ? "{point_count_abbreviated}"
1294
+ : `{${options.property as string}}`,
1295
+ "text-font": ["Noto Sans Regular"],
1296
+ "text-size": labelSize,
1297
+ "text-pitch-alignment": alignOnViewport ? "viewport" : "map",
1298
+ "symbol-sort-key": [
1299
+ "/",
1300
+ 1,
1301
+ [
1302
+ "get",
1303
+ options.cluster ? "point_count" : (options.property as string),
1304
+ ],
1305
+ ], // so that the largest value goes on top
1306
+ },
1307
+ paint: {
1308
+ "text-color": labelColor,
1309
+ "text-opacity": pointOpacity,
1310
+ },
1311
+ minzoom,
1312
+ maxzoom,
1313
+ },
1314
+ options.beforeId,
1315
+ );
1316
+ }
1317
+ return returnedInfo;
1318
+ }
1319
+
1320
+ /**
1321
+ * Add a polyline witgh optional outline from a GeoJSON object
1322
+ */
1323
+ export function addHeatmap(
1324
+ /**
1325
+ * Map instance to add a heatmap layer to
1326
+ */
1327
+ map: Map,
1328
+ // The data or data source is expected to contain LineStrings or MultiLineStrings
1329
+ options: HeatmapLayerOptions,
1330
+ ): {
1331
+ /**
1332
+ * ID of the heatmap layer
1333
+ */
1334
+ heatmapLayerId: string;
1335
+
1336
+ /**
1337
+ * ID of the data source
1338
+ */
1339
+ heatmapSourceId: string;
1340
+ } {
1341
+ if (options.layerId && map.getLayer(options.layerId)) {
1342
+ throw new Error(
1343
+ `A layer already exists with the layer id: ${options.layerId}`,
1344
+ );
1345
+ }
1346
+
1347
+ const sourceId = options.sourceId ?? generateRandomSourceName();
1348
+ const layerId = options.layerId ?? generateRandomLayerName();
1349
+ const minzoom = options.minzoom ?? 0;
1350
+ const maxzoom = options.maxzoom ?? 23;
1351
+ const zoomCompensation = options.zoomCompensation ?? true;
1352
+
1353
+ const opacity = options.opacity ?? [
1354
+ { zoom: minzoom, value: 0 },
1355
+ { zoom: minzoom + 0.25, value: 1 },
1356
+ { zoom: maxzoom - 0.25, value: 1 },
1357
+ { zoom: maxzoom, value: 0 },
1358
+ ];
1359
+
1360
+ // const colorRamp = "colorRamp" in options
1361
+ let colorRamp = Array.isArray(options.colorRamp)
1362
+ ? options.colorRamp
1363
+ : ColorRampCollection.TURBO.transparentStart();
1364
+
1365
+ // making sure the color ramp has [0, 1] bounds
1366
+ const crBounds = colorRamp.getBounds();
1367
+ if (crBounds.min !== 0 || crBounds.max !== 1) {
1368
+ colorRamp = colorRamp.scale(0, 1);
1369
+ }
1370
+
1371
+ // making sure the color ramp has is transparent in 0
1372
+ if (!colorRamp.hasTransparentStart()) {
1373
+ colorRamp = colorRamp.transparentStart();
1374
+ }
1375
+
1376
+ const intensity = options.intensity ?? [
1377
+ { zoom: 0, value: 0.01 },
1378
+ { zoom: 4, value: 0.2 },
1379
+ { zoom: 16, value: 1 },
1380
+ ];
1381
+
1382
+ const property = options.property ?? null;
1383
+ const propertyValueWeight = options.weight ?? 1;
1384
+
1385
+ let heatmapWeight: DataDrivenPropertyValueSpecification<number> = 1; // = typeof propertyValueWeights === "number" ? propertyValueWeights : 1;
1386
+
1387
+ if (property) {
1388
+ if (typeof propertyValueWeight === "number") {
1389
+ heatmapWeight = propertyValueWeight;
1390
+
1391
+ // In case this numerical weight was provided by the user and not be the default value:
1392
+ if (typeof options.weight === "number") {
1393
+ console.warn(
1394
+ "The option `.property` is ignored when `.propertyValueWeights` is not of type `PropertyValueWeights`",
1395
+ );
1396
+ }
1397
+ } else if (Array.isArray(propertyValueWeight)) {
1398
+ heatmapWeight = rampedPropertyValueWeight(propertyValueWeight, property);
1399
+ } else {
1400
+ console.warn(
1401
+ "The option `.property` is ignored when `.propertyValueWeights` is not of type `PropertyValueWeights`",
1402
+ );
1403
+ }
1404
+ } else {
1405
+ if (typeof propertyValueWeight === "number") {
1406
+ heatmapWeight = propertyValueWeight;
1407
+ } else if (Array.isArray(propertyValueWeight)) {
1408
+ console.warn(
1409
+ "The options `.propertyValueWeights` can only be used when `.property` is provided.",
1410
+ );
1411
+ }
1412
+ }
1413
+
1414
+ const defaultRadiusZoomRamping = [
1415
+ { zoom: 0, value: 50 * 0.025 },
1416
+ { zoom: 2, value: 50 * 0.05 },
1417
+ { zoom: 4, value: 50 * 0.1 },
1418
+ { zoom: 8, value: 50 * 0.25 },
1419
+ { zoom: 16, value: 50 },
1420
+ ];
1421
+
1422
+ const radius =
1423
+ options.radius ?? (zoomCompensation ? defaultRadiusZoomRamping : 10);
1424
+
1425
+ let radiusHeatmap: DataDrivenPropertyValueSpecification<number> = 1;
1426
+
1427
+ if (typeof radius === "number") {
1428
+ radiusHeatmap = radius;
1429
+ }
1430
+
1431
+ // Radius is provided as a zoom-ramping array
1432
+ else if (Array.isArray(radius) && "zoom" in radius[0]) {
1433
+ radiusHeatmap = rampedOptionsToLayerPaintSpec(radius as ZoomNumberValues);
1434
+ }
1435
+
1436
+ // Radius is provided as data driven
1437
+ else if (property && Array.isArray(radius) && "propertyValue" in radius[0]) {
1438
+ radiusHeatmap = radiusDrivenByPropertyHeatmap(
1439
+ radius as unknown as PropertyValues,
1440
+ property,
1441
+ zoomCompensation,
1442
+ );
1443
+ } else if (
1444
+ !property &&
1445
+ Array.isArray(radius) &&
1446
+ "propertyValue" in radius[0]
1447
+ ) {
1448
+ radiusHeatmap = rampedOptionsToLayerPaintSpec(
1449
+ defaultRadiusZoomRamping as ZoomNumberValues,
1450
+ );
1451
+ console.warn(
1452
+ "The option `.radius` can only be property-driven if the option `.property` is provided.",
1453
+ );
1454
+ } else {
1455
+ radiusHeatmap = rampedOptionsToLayerPaintSpec(
1456
+ defaultRadiusZoomRamping as ZoomNumberValues,
1457
+ );
1458
+ }
1459
+
1460
+ const returnedInfo = {
1461
+ heatmapLayerId: layerId,
1462
+ heatmapSourceId: sourceId,
1463
+ };
1464
+
1465
+ // A new source is added if the map does not have this sourceId and the data is provided
1466
+ if (options.data && !map.getSource(sourceId)) {
1467
+ let data: string | FeatureCollection = options.data;
1468
+
1469
+ // If is a UUID, we extend it to be the URL to a MapTiler Cloud hosted dataset
1470
+ if (typeof data === "string" && isUUID(data)) {
1471
+ data = `https://api.maptiler.com/data/${data}/features.json?key=${config.apiKey}`;
1472
+ }
1473
+
1474
+ // Adding the source
1475
+ map.addSource(sourceId, {
1476
+ type: "geojson",
1477
+ data: data,
1478
+ });
1479
+ }
1480
+
1481
+ map.addLayer({
1482
+ id: layerId,
1483
+ type: "heatmap",
1484
+ source: sourceId,
1485
+ minzoom,
1486
+ maxzoom,
1487
+ paint: {
1488
+ "heatmap-weight": heatmapWeight,
1489
+
1490
+ "heatmap-intensity":
1491
+ typeof intensity === "number"
1492
+ ? intensity
1493
+ : (rampedOptionsToLayerPaintSpec(
1494
+ intensity,
1495
+ ) as PropertyValueSpecification<number>),
1496
+
1497
+ "heatmap-color": heatmapIntensityFromColorRamp(colorRamp),
1498
+
1499
+ "heatmap-radius": radiusHeatmap,
1500
+
1501
+ "heatmap-opacity":
1502
+ typeof opacity === "number"
1503
+ ? opacity
1504
+ : (rampedOptionsToLayerPaintSpec(
1505
+ opacity,
1506
+ ) as PropertyValueSpecification<number>),
1507
+ },
1508
+ });
1509
+
1510
+ return returnedInfo;
1511
+ }