@maptiler/sdk 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.eslintrc.cjs +15 -5
  2. package/.github/pull_request_template.md +11 -0
  3. package/.github/workflows/format-lint.yml +24 -0
  4. package/CHANGELOG.md +94 -51
  5. package/colorramp.md +93 -0
  6. package/dist/maptiler-sdk.d.ts +1207 -123
  7. package/dist/maptiler-sdk.min.mjs +3 -1
  8. package/dist/maptiler-sdk.mjs +3561 -485
  9. package/dist/maptiler-sdk.mjs.map +1 -1
  10. package/dist/maptiler-sdk.umd.js +3825 -869
  11. package/dist/maptiler-sdk.umd.js.map +1 -1
  12. package/dist/maptiler-sdk.umd.min.js +51 -49
  13. package/package.json +27 -13
  14. package/readme.md +298 -0
  15. package/rollup.config.js +2 -16
  16. package/src/Map.ts +489 -357
  17. package/src/MaptilerGeolocateControl.ts +23 -20
  18. package/src/MaptilerLogoControl.ts +3 -3
  19. package/src/MaptilerNavigationControl.ts +9 -6
  20. package/src/MaptilerTerrainControl.ts +15 -14
  21. package/src/Minimap.ts +373 -0
  22. package/src/Point.ts +3 -5
  23. package/src/colorramp.ts +1216 -0
  24. package/src/config.ts +4 -3
  25. package/src/converters/index.ts +1 -0
  26. package/src/converters/xml.ts +681 -0
  27. package/src/defaults.ts +1 -1
  28. package/src/helpers/index.ts +27 -0
  29. package/src/helpers/stylehelper.ts +395 -0
  30. package/src/helpers/vectorlayerhelpers.ts +1511 -0
  31. package/src/index.ts +10 -0
  32. package/src/language.ts +116 -79
  33. package/src/mapstyle.ts +4 -2
  34. package/src/tools.ts +68 -16
  35. package/tsconfig.json +8 -5
  36. package/vite.config.ts +10 -0
  37. package/demos/maptiler-sdk.css +0 -147
  38. package/demos/maptiler-sdk.umd.js +0 -4041
  39. package/demos/mountain.html +0 -67
  40. package/demos/simple.html +0 -67
  41. package/demos/transform-request.html +0 -81
package/src/config.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import EventEmitter from "events";
2
- import { Language, LanguageString } from "./language";
2
+ import { LanguageString } from "./language";
3
3
  import { config as clientConfig, FetchFunction } from "@maptiler/client";
4
4
  import { v4 as uuidv4 } from "uuid";
5
5
  import { Unit } from "./unit";
6
+ import { defaults } from "./defaults";
6
7
 
7
8
  export const MAPTILER_SESSION_ID = uuidv4();
8
9
 
@@ -13,13 +14,13 @@ class SdkConfig extends EventEmitter {
13
14
  /**
14
15
  * The primary language. By default, the language of the web browser is used.
15
16
  */
16
- primaryLanguage: LanguageString | null = Language.AUTO;
17
+ primaryLanguage: LanguageString = defaults.primaryLanguage;
17
18
 
18
19
  /**
19
20
  * The secondary language, to overwrite the default language defined in the map style.
20
21
  * This settings is highly dependant on the style compatibility and may not work in most cases.
21
22
  */
22
- secondaryLanguage: LanguageString | null = null;
23
+ secondaryLanguage?: LanguageString;
23
24
 
24
25
  /**
25
26
  * Setting on whether of not the SDK runs with a session logic.
@@ -0,0 +1 @@
1
+ export * from "./xml";
@@ -0,0 +1,681 @@
1
+ // Typescript port of https://github.com/mapbox/togeojson/
2
+ // This includes KML and GPX parsing to GeoJSON
3
+
4
+ export interface Link {
5
+ href: string | null;
6
+ }
7
+
8
+ export interface XMLProperties {
9
+ links?: Link[];
10
+ }
11
+
12
+ export interface PlacemarkProperties {
13
+ name?: string;
14
+ address?: string;
15
+ styleUrl?: string;
16
+ description?: string;
17
+ styleHash?: string;
18
+ styleMapHash?: Record<string, string | null>;
19
+ timespan?: {
20
+ begin: string;
21
+ end: string;
22
+ };
23
+ timestamp?: string;
24
+ stroke?: string;
25
+ "stroke-opacity"?: number;
26
+ "stroke-width"?: number;
27
+ fill?: string;
28
+ "fill-opacity"?: number;
29
+ visibility?: string;
30
+ icon?: string;
31
+ coordTimes?: (string | null)[] | (string | null)[][];
32
+ }
33
+
34
+ /**
35
+ * create a function that converts a string to XML
36
+ * https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
37
+ */
38
+ export function str2xml(str: string): Document {
39
+ if (typeof DOMParser !== "undefined") {
40
+ const doc = new DOMParser().parseFromString(str, "application/xml");
41
+
42
+ // If the input string was not valid XML
43
+ if (doc.querySelector("parsererror")) {
44
+ throw new Error("The provided string is not valid XML");
45
+ }
46
+
47
+ return doc;
48
+ } else {
49
+ throw new Error("No XML parser found");
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Check one of the top level child node is of a given type ("gpx", "kml").
55
+ * The check is not case sensitive.
56
+ * @param doc
57
+ * @param nodeName
58
+ * @returns
59
+ */
60
+ export function hasChildNodeWithName(doc: Document, nodeName: string): boolean {
61
+ if (!doc.hasChildNodes()) {
62
+ return false;
63
+ }
64
+
65
+ for (const childNode of Array.from(doc.childNodes)) {
66
+ const currentNodeName = childNode.nodeName;
67
+ if (
68
+ typeof currentNodeName === "string" &&
69
+ currentNodeName.trim().toLowerCase() === nodeName.toLowerCase()
70
+ ) {
71
+ return true;
72
+ }
73
+ }
74
+
75
+ return false;
76
+ }
77
+
78
+ /**
79
+ * create a function that converts a XML to a string
80
+ * https://developer.mozilla.org/en-US/docs/Web/API/XMLSerializer
81
+ */
82
+ export function xml2str(node: Node): string {
83
+ if (typeof XMLSerializer !== "undefined") {
84
+ return new XMLSerializer().serializeToString(node);
85
+ }
86
+ throw new Error("No XML serializer found");
87
+ }
88
+
89
+ /**
90
+ * Given a XML document using the GPX spec, return GeoJSON
91
+ */
92
+ export function gpx(doc: string | Document): GeoJSON.FeatureCollection {
93
+ if (typeof doc === "string") doc = str2xml(doc);
94
+ // doc.firstChild
95
+ // The document is valid XML but not valid GPX (at leas the first node is not)
96
+ if (!hasChildNodeWithName(doc, "gpx")) {
97
+ throw new Error("The XML document is not valid GPX");
98
+ }
99
+
100
+ const tracks = get(doc, "trk");
101
+ const routes = get(doc, "rte");
102
+ const waypoints = get(doc, "wpt");
103
+ // a feature collection
104
+ const gj: GeoJSON.FeatureCollection = {
105
+ type: "FeatureCollection",
106
+ features: [],
107
+ };
108
+ for (const track of Array.from(tracks)) {
109
+ const feature = getTrack(track);
110
+ if (feature) gj.features.push(feature);
111
+ }
112
+ for (const route of Array.from(routes)) {
113
+ const feature = getRoute(route);
114
+ if (feature) gj.features.push(feature);
115
+ }
116
+ for (const waypoint of Array.from(waypoints)) {
117
+ gj.features.push(getPoint(waypoint));
118
+ }
119
+ return gj;
120
+ }
121
+
122
+ /**
123
+ * Given a XML document using the KML spec, return GeoJSON
124
+ */
125
+ export function kml(
126
+ doc: string | Document,
127
+ xml2string?: (node: Node) => string,
128
+ ): GeoJSON.FeatureCollection {
129
+ if (typeof doc === "string") doc = str2xml(doc);
130
+
131
+ // The document is valid XML but not valid KML (at leas the first node is not)
132
+ if (!hasChildNodeWithName(doc, "kml")) {
133
+ throw new Error("The XML document is not valid KML");
134
+ }
135
+
136
+ const gj: GeoJSON.FeatureCollection = {
137
+ type: "FeatureCollection",
138
+ features: [],
139
+ };
140
+ // styleindex keeps track of hashed styles in order to match features
141
+ const styleIndex: Record<string, string> = {};
142
+ const styleByHash: Record<string, Element> = {};
143
+ // stylemapindex keeps track of style maps to expose in properties
144
+ const styleMapIndex: Record<string, Record<string, string | null>> = {};
145
+ // all root placemarks in the file
146
+ const placemarks = get(doc, "Placemark");
147
+ const styles = get(doc, "Style");
148
+ const styleMaps = get(doc, "StyleMap");
149
+
150
+ for (const style of Array.from(styles)) {
151
+ const hash = okhash(
152
+ xml2string !== undefined ? xml2string(style) : xml2str(style),
153
+ ).toString(16);
154
+ styleIndex["#" + attr(style, "id")] = hash;
155
+ styleByHash[hash] = style;
156
+ }
157
+ for (const styleMap of Array.from(styleMaps)) {
158
+ styleIndex["#" + attr(styleMap, "id")] = okhash(
159
+ xml2string !== undefined ? xml2string(styleMap) : xml2str(styleMap),
160
+ ).toString(16);
161
+ const pairs = get(styleMap, "Pair");
162
+ const pairsMap: Record<string, string | null> = {};
163
+ for (const pair of Array.from(pairs)) {
164
+ pairsMap[nodeVal(get1(pair, "key")) ?? ""] = nodeVal(
165
+ get1(pair, "styleUrl"),
166
+ );
167
+ }
168
+ styleMapIndex["#" + attr(styleMap, "id")] = pairsMap;
169
+ }
170
+ for (const placemark of Array.from(placemarks)) {
171
+ gj.features = gj.features.concat(
172
+ getPlacemark(placemark, styleIndex, styleByHash, styleMapIndex),
173
+ );
174
+ }
175
+ return gj;
176
+ }
177
+
178
+ // parse color string to hex string with opacity. black with 100% opacity will be returned if no data found
179
+ function kmlColor(v: string | null): [string, number] {
180
+ if (v === null) return ["#000000", 1];
181
+ let color = "";
182
+ let opacity = 1;
183
+ if (v.substring(0, 1) === "#") v = v.substring(1);
184
+ if (v.length === 6 || v.length === 3) color = v;
185
+ if (v.length === 8) {
186
+ opacity = parseInt(v.substring(0, 2), 16) / 255;
187
+ color = "#" + v.substring(6, 8) + v.substring(4, 6) + v.substring(2, 4);
188
+ }
189
+ return [color ?? "#000000", opacity ?? 1];
190
+ }
191
+
192
+ function gxCoord(v: string): number[] {
193
+ return numarray(v.split(" "));
194
+ }
195
+
196
+ // grab coordinates and timestamps (when available) from the gx:Track extension
197
+ function gxCoords(root: Document | Element): {
198
+ coords: number[][];
199
+ times: (string | null)[];
200
+ } {
201
+ let elems = get(root, "coord");
202
+ const coords: number[][] = [];
203
+ const times: (string | null)[] = [];
204
+ if (elems.length === 0) elems = get(root, "gx:coord");
205
+ for (const elem of Array.from(elems)) {
206
+ coords.push(gxCoord(nodeVal(elem) ?? ""));
207
+ }
208
+ const timeElems = get(root, "when");
209
+ for (const timeElem of Array.from(timeElems)) times.push(nodeVal(timeElem));
210
+ return {
211
+ coords: coords,
212
+ times,
213
+ };
214
+ }
215
+
216
+ // get the geometry data and coordinate timestamps if available
217
+ function getGeometry(root: Element): {
218
+ geoms: GeoJSON.Geometry[];
219
+ coordTimes: (string | null)[][];
220
+ } {
221
+ // atomic geospatial types supported by KML - MultiGeometry is
222
+ // handled separately
223
+ const geotypes = ["Polygon", "LineString", "Point", "Track", "gx:Track"];
224
+ // setup variables
225
+ let geomNode, geomNodes, i, j, k;
226
+ const geoms: GeoJSON.Geometry[] = [];
227
+ const coordTimes: (string | null)[][] = [];
228
+ // simple cases
229
+ if (get1(root, "MultiGeometry") !== null) {
230
+ return getGeometry(get1(root, "MultiGeometry") as Element);
231
+ }
232
+ if (get1(root, "MultiTrack") !== null) {
233
+ return getGeometry(get1(root, "MultiTrack") as Element);
234
+ }
235
+ if (get1(root, "gx:MultiTrack") !== null) {
236
+ return getGeometry(get1(root, "gx:MultiTrack") as Element);
237
+ }
238
+ for (i = 0; i < geotypes.length; i++) {
239
+ geomNodes = get(root, geotypes[i]);
240
+ if (geomNodes) {
241
+ for (j = 0; j < geomNodes.length; j++) {
242
+ geomNode = geomNodes[j];
243
+ if (geotypes[i] === "Point") {
244
+ geoms.push({
245
+ type: "Point",
246
+ coordinates: coord1(nodeVal(get1(geomNode, "coordinates")) ?? ""),
247
+ });
248
+ } else if (geotypes[i] === "LineString") {
249
+ geoms.push({
250
+ type: "LineString",
251
+ coordinates: coord(nodeVal(get1(geomNode, "coordinates")) ?? ""),
252
+ });
253
+ } else if (geotypes[i] === "Polygon") {
254
+ const rings = get(geomNode, "LinearRing");
255
+ const coords = [];
256
+ for (k = 0; k < rings.length; k++) {
257
+ coords.push(coord(nodeVal(get1(rings[k], "coordinates")) ?? ""));
258
+ }
259
+ geoms.push({
260
+ type: "Polygon",
261
+ coordinates: coords,
262
+ });
263
+ } else if (geotypes[i] === "Track" || geotypes[i] === "gx:Track") {
264
+ const track = gxCoords(geomNode);
265
+ geoms.push({
266
+ type: "LineString",
267
+ coordinates: track.coords,
268
+ });
269
+ if (track.times.length) coordTimes.push(track.times);
270
+ }
271
+ }
272
+ }
273
+ }
274
+ return { geoms, coordTimes };
275
+ }
276
+
277
+ // build geojson feature sets with all their attributes and property data
278
+ function getPlacemark(
279
+ root: Element,
280
+ styleIndex: Record<string, string>,
281
+ styleByHash: Record<string, Element>,
282
+ styleMapIndex: Record<string, Record<string, string | null>>,
283
+ ) {
284
+ const geomsAndTimes = getGeometry(root);
285
+ const properties: PlacemarkProperties & Record<string, string> = {};
286
+ const name = nodeVal(get1(root, "name"));
287
+ const address = nodeVal(get1(root, "address"));
288
+ const description = nodeVal(get1(root, "description"));
289
+ const timeSpan = get1(root, "TimeSpan");
290
+ const timeStamp = get1(root, "TimeStamp");
291
+ const extendedData = get1(root, "ExtendedData");
292
+ const visibility = get1(root, "visibility");
293
+
294
+ let i: number;
295
+ let styleUrl = nodeVal(get1(root, "styleUrl"));
296
+ let lineStyle = get1(root, "LineStyle");
297
+ let polyStyle = get1(root, "PolyStyle");
298
+
299
+ if (!geomsAndTimes.geoms.length) return [];
300
+ if (name) properties.name = name;
301
+ if (address) properties.address = address;
302
+ if (styleUrl) {
303
+ if (styleUrl[0] !== "#") styleUrl = "#" + styleUrl;
304
+
305
+ properties.styleUrl = styleUrl;
306
+ if (styleIndex[styleUrl]) {
307
+ properties.styleHash = styleIndex[styleUrl];
308
+ }
309
+ if (styleMapIndex[styleUrl]) {
310
+ properties.styleMapHash = styleMapIndex[styleUrl];
311
+ properties.styleHash = styleIndex[styleMapIndex[styleUrl].normal ?? ""];
312
+ }
313
+ // Try to populate the lineStyle or polyStyle since we got the style hash
314
+ const style = styleByHash[properties.styleHash ?? ""];
315
+ if (style) {
316
+ if (!lineStyle) lineStyle = get1(style, "LineStyle");
317
+ if (!polyStyle) polyStyle = get1(style, "PolyStyle");
318
+ const iconStyle = get1(style, "IconStyle");
319
+ if (iconStyle) {
320
+ const icon = get1(iconStyle, "Icon");
321
+ if (icon) {
322
+ const href = nodeVal(get1(icon, "href"));
323
+ if (href) properties.icon = href;
324
+ }
325
+ }
326
+ }
327
+ }
328
+ if (description) properties.description = description;
329
+ if (timeSpan) {
330
+ const begin = nodeVal(get1(timeSpan, "begin"));
331
+ const end = nodeVal(get1(timeSpan, "end"));
332
+ if (begin && end) properties.timespan = { begin, end };
333
+ }
334
+ if (timeStamp !== null) {
335
+ properties.timestamp =
336
+ nodeVal(get1(timeStamp, "when")) ?? new Date().toISOString();
337
+ }
338
+ if (lineStyle !== null) {
339
+ const linestyles = kmlColor(nodeVal(get1(lineStyle, "color")));
340
+ const color = linestyles[0];
341
+ const opacity = linestyles[1];
342
+ const width = parseFloat(nodeVal(get1(lineStyle, "width")) ?? "");
343
+ if (color) properties.stroke = color;
344
+ if (!isNaN(opacity)) properties["stroke-opacity"] = opacity;
345
+ if (!isNaN(width)) properties["stroke-width"] = width;
346
+ }
347
+ if (polyStyle) {
348
+ const polystyles = kmlColor(nodeVal(get1(polyStyle, "color")));
349
+ const pcolor = polystyles[0];
350
+ const popacity = polystyles[1];
351
+ const fill = nodeVal(get1(polyStyle, "fill"));
352
+ const outline = nodeVal(get1(polyStyle, "outline"));
353
+ if (pcolor) properties.fill = pcolor;
354
+ if (!isNaN(popacity)) properties["fill-opacity"] = popacity;
355
+ if (fill)
356
+ properties["fill-opacity"] =
357
+ fill === "1" ? properties["fill-opacity"] || 1 : 0;
358
+ if (outline)
359
+ properties["stroke-opacity"] =
360
+ outline === "1" ? properties["stroke-opacity"] || 1 : 0;
361
+ }
362
+ if (extendedData) {
363
+ const datas = get(extendedData, "Data"),
364
+ simpleDatas = get(extendedData, "SimpleData");
365
+
366
+ for (i = 0; i < datas.length; i++) {
367
+ properties[datas[i].getAttribute("name") ?? ""] =
368
+ nodeVal(get1(datas[i], "value")) ?? "";
369
+ }
370
+ for (i = 0; i < simpleDatas.length; i++) {
371
+ properties[simpleDatas[i].getAttribute("name") ?? ""] =
372
+ nodeVal(simpleDatas[i]) ?? "";
373
+ }
374
+ }
375
+ if (visibility !== null) {
376
+ properties.visibility = nodeVal(visibility) ?? "";
377
+ }
378
+ if (geomsAndTimes.coordTimes.length !== 0) {
379
+ properties.coordTimes =
380
+ geomsAndTimes.coordTimes.length === 1
381
+ ? geomsAndTimes.coordTimes[0]
382
+ : geomsAndTimes.coordTimes;
383
+ }
384
+ const feature: GeoJSON.Feature = {
385
+ type: "Feature",
386
+ geometry:
387
+ geomsAndTimes.geoms.length === 1
388
+ ? geomsAndTimes.geoms[0]
389
+ : {
390
+ type: "GeometryCollection",
391
+ geometries: geomsAndTimes.geoms,
392
+ },
393
+ properties: properties,
394
+ };
395
+ if (attr(root, "id")) feature.id = attr(root, "id") ?? undefined;
396
+ return [feature];
397
+ }
398
+
399
+ function getPoints(
400
+ node: Element,
401
+ pointname: string,
402
+ ):
403
+ | undefined
404
+ | {
405
+ line: number[][];
406
+ times: string[];
407
+ heartRates: (number | null)[];
408
+ } {
409
+ const pts = get(node, pointname);
410
+ const line: number[][] = [];
411
+ const times: string[] = [];
412
+ let heartRates: (number | null)[] = [];
413
+ const ptsLength = pts.length;
414
+ if (ptsLength < 2) return; // Invalid line in GeoJSON
415
+ for (let i = 0; i < ptsLength; i++) {
416
+ const cPair = coordPair(pts[i]);
417
+ line.push(cPair.coordinates);
418
+ if (cPair.time) times.push(cPair.time);
419
+ if (cPair.heartRate || heartRates.length) {
420
+ if (heartRates.length === 0) heartRates = new Array(i).fill(null);
421
+ heartRates.push(cPair.heartRate);
422
+ }
423
+ }
424
+ return {
425
+ line: line,
426
+ times: times,
427
+ heartRates,
428
+ };
429
+ }
430
+
431
+ function getTrack(node: Element): undefined | GeoJSON.Feature {
432
+ const segments = get(node, "trkseg");
433
+ const track = [];
434
+ const times = [];
435
+ const heartRates: (number | null)[][] = [];
436
+ let line;
437
+ for (let i = 0; i < segments.length; i++) {
438
+ line = getPoints(segments[i], "trkpt");
439
+ if (line !== undefined) {
440
+ if (line.line) track.push(line.line);
441
+ if (line.times && line.times.length) times.push(line.times);
442
+ if (heartRates.length || (line.heartRates && line.heartRates.length)) {
443
+ if (!heartRates.length) {
444
+ for (let s = 0; s < i; s++) {
445
+ heartRates.push(new Array(track[s].length).fill(null));
446
+ }
447
+ }
448
+ if (line.heartRates && line.heartRates.length) {
449
+ heartRates.push(line.heartRates);
450
+ } else {
451
+ heartRates.push(new Array(line.line.length).fill(null));
452
+ }
453
+ }
454
+ }
455
+ }
456
+ if (track.length === 0) return;
457
+ const properties: {
458
+ coordTimes?: string[] | string[][];
459
+ heartRates?: (number | null)[] | (number | null)[][];
460
+ } & XMLProperties &
461
+ Record<string, string | number> = {
462
+ ...getProperties(node),
463
+ ...getLineStyle(get1(node, "extensions")),
464
+ };
465
+ if (times.length !== 0)
466
+ properties.coordTimes = track.length === 1 ? times[0] : times;
467
+ if (heartRates.length !== 0) {
468
+ properties.heartRates = track.length === 1 ? heartRates[0] : heartRates;
469
+ }
470
+ if (track.length === 1) {
471
+ return {
472
+ type: "Feature",
473
+ properties,
474
+ geometry: {
475
+ type: "LineString",
476
+ coordinates: track[0],
477
+ },
478
+ };
479
+ } else {
480
+ return {
481
+ type: "Feature",
482
+ properties,
483
+ geometry: {
484
+ type: "MultiLineString",
485
+ coordinates: track,
486
+ },
487
+ };
488
+ }
489
+ }
490
+
491
+ function getRoute(node: Element): GeoJSON.Feature | undefined {
492
+ const line = getPoints(node, "rtept");
493
+ if (line === undefined) return;
494
+ const prop = {
495
+ ...getProperties(node),
496
+ ...getLineStyle(get1(node, "extensions")),
497
+ };
498
+ return {
499
+ type: "Feature",
500
+ properties: prop,
501
+ geometry: {
502
+ type: "LineString",
503
+ coordinates: line.line,
504
+ },
505
+ };
506
+ }
507
+
508
+ function getPoint(node: Element): GeoJSON.Feature {
509
+ const prop = { ...getProperties(node), ...getMulti(node, ["sym"]) };
510
+ return {
511
+ type: "Feature",
512
+ properties: prop,
513
+ geometry: {
514
+ type: "Point",
515
+ coordinates: coordPair(node).coordinates,
516
+ },
517
+ };
518
+ }
519
+
520
+ function getLineStyle(
521
+ extensions: Element | null,
522
+ ): Record<string, string | number> {
523
+ const style: Record<string, string | number> = {};
524
+ if (extensions) {
525
+ const lineStyle = get1(extensions, "line");
526
+ if (lineStyle) {
527
+ const color = nodeVal(get1(lineStyle, "color"));
528
+ const opacity = parseFloat(nodeVal(get1(lineStyle, "opacity")) ?? "0");
529
+ const width = parseFloat(nodeVal(get1(lineStyle, "width")) ?? "0");
530
+ if (color) style.stroke = color;
531
+ if (!isNaN(opacity)) style["stroke-opacity"] = opacity;
532
+ // GPX width is in mm, convert to px with 96 px per inch
533
+ if (!isNaN(width)) style["stroke-width"] = (width * 96) / 25.4;
534
+ }
535
+ }
536
+ return style;
537
+ }
538
+
539
+ function getProperties(node: Element): XMLProperties & Record<string, string> {
540
+ const prop: XMLProperties & Record<string, string> = getMulti(node, [
541
+ "name",
542
+ "cmt",
543
+ "desc",
544
+ "type",
545
+ "time",
546
+ "keywords",
547
+ ]);
548
+ const links = get(node, "link");
549
+ if (links.length !== 0) {
550
+ prop.links = [];
551
+ for (const l of Array.from(links)) {
552
+ const link = {
553
+ href: attr(l, "href"),
554
+ ...getMulti(l, ["text", "type"]),
555
+ };
556
+ prop.links.push(link);
557
+ }
558
+ }
559
+ return prop;
560
+ }
561
+
562
+ function okhash(x: string): number {
563
+ let h = 0;
564
+ if (!x || !x.length) return h;
565
+ for (let i = 0; i < x.length; i++) {
566
+ h = ((h << 5) - h + x.charCodeAt(i)) | 0;
567
+ }
568
+ return h;
569
+ }
570
+
571
+ function get(x: Document | Element, y: string): HTMLCollectionOf<Element> {
572
+ return x.getElementsByTagName(y);
573
+ }
574
+
575
+ function attr(x: Element, y: string): string | null {
576
+ return x.getAttribute(y);
577
+ }
578
+
579
+ function attrf(x: Element, y: string): number {
580
+ return parseFloat(attr(x, y) ?? "0");
581
+ }
582
+
583
+ function get1(x: Element, y: string): Element | null {
584
+ const n = get(x, y);
585
+ return n.length ? n[0] : null;
586
+ }
587
+
588
+ // https://developer.mozilla.org/en-US/docs/Web/API/Node.normalize
589
+ function norm(el: Element): Element {
590
+ if (el.normalize) el.normalize();
591
+ return el;
592
+ }
593
+
594
+ // cast array x into numbers
595
+ function numarray(x: string[]): number[] {
596
+ return x.map(parseFloat).map((n) => (isNaN(n) ? null : n)) as number[];
597
+ }
598
+
599
+ // get the content of a text node, if any
600
+ function nodeVal(x: Element | null): string | null {
601
+ if (x) norm(x);
602
+ return x && x.textContent;
603
+ }
604
+
605
+ // get the contents of multiple text nodes, if present
606
+ function getMulti(x: Element, ys: string[]): Record<string, string> {
607
+ const o: Record<string, string> = {};
608
+ let n;
609
+ let k;
610
+ for (k = 0; k < ys.length; k++) {
611
+ n = get1(x, ys[k]);
612
+ if (n) o[ys[k]] = nodeVal(n) ?? "";
613
+ }
614
+ return o;
615
+ }
616
+
617
+ // get one coordinate from a coordinate array, if any
618
+ function coord1(v: string): number[] {
619
+ return numarray(v.replace(/\s*/g, "").split(","));
620
+ }
621
+
622
+ // get all coordinates from a coordinate array as [[],[]]
623
+ function coord(v: string): number[][] {
624
+ const coords = v.replace(/^\s*|\s*$/g, "").split(/\s+/);
625
+ const out = [];
626
+ for (const coord of coords) out.push(coord1(coord));
627
+ return out;
628
+ }
629
+
630
+ // build a set of coordinates, timestamps, and heartrate
631
+ function coordPair(x: Element): {
632
+ coordinates: number[];
633
+ time: string | null;
634
+ heartRate: number | null;
635
+ } {
636
+ const ll = [attrf(x, "lon"), attrf(x, "lat")];
637
+ const ele = get1(x, "ele");
638
+ // handle namespaced attribute in browser
639
+ const heartRate = get1(x, "gpxtpx:hr") || get1(x, "hr");
640
+ const time = get1(x, "time");
641
+ let e: number;
642
+ if (ele) {
643
+ e = parseFloat(nodeVal(ele) ?? "0");
644
+ if (!isNaN(e)) ll.push(e);
645
+ }
646
+ return {
647
+ coordinates: ll,
648
+ time: time ? nodeVal(time) : null,
649
+ heartRate:
650
+ heartRate !== null ? parseFloat(nodeVal(heartRate) ?? "0") : null,
651
+ };
652
+ }
653
+
654
+ export function gpxOrKml(
655
+ doc: string | Document,
656
+ ): GeoJSON.FeatureCollection | null {
657
+ try {
658
+ // Converting only once rather than in each converter
659
+ if (typeof doc === "string") doc = str2xml(doc);
660
+ } catch (e) {
661
+ // The doc is a string but not valid XML
662
+ return null;
663
+ }
664
+
665
+ try {
666
+ const result = gpx(doc);
667
+ return result;
668
+ } catch (e) {
669
+ // The doc is valid XML but not valid GPX
670
+ }
671
+
672
+ try {
673
+ const result = kml(doc);
674
+ return result;
675
+ } catch (e) {
676
+ // The doc is valid XML but not valid KML
677
+ }
678
+
679
+ // At this point, the doc is not of a compatible vector format
680
+ return null;
681
+ }