@raruto/leaflet-elevation 1.8.4 → 1.8.7

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.
package/src/control.js CHANGED
@@ -14,10 +14,10 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
14
14
  __mileFactor: 0.621371,
15
15
  __footFactor: 3.28084,
16
16
  __D3: 'https://unpkg.com/d3@6.5.0/dist/d3.min.js',
17
- __TOGEOJSON: 'https://unpkg.com/@tmcw/togeojson@4.3.0/dist/togeojson.umd.js',
17
+ __TOGEOJSON: 'https://unpkg.com/@tmcw/togeojson@4.5.0/dist/togeojson.umd.js',
18
18
  __LGEOMUTIL: 'https://unpkg.com/leaflet-geometryutil@0.9.3/src/leaflet.geometryutil.js',
19
19
  __LALMOSTOVER: 'https://unpkg.com/leaflet-almostover@1.0.1/src/leaflet.almostover.js',
20
- __LDISTANCEM: 'https://unpkg.com/@raruto/leaflet-elevation@1.8.4/libs/leaflet-distance-marker.min.js',
20
+ __LDISTANCEM: 'https://unpkg.com/@raruto/leaflet-elevation@1.8.7/libs/leaflet-distance-marker.min.js',
21
21
 
22
22
  /*
23
23
  * Add data to the diagram either from GPX or GeoJSON and update the axis domain and data
@@ -156,77 +156,62 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
156
156
  this._markedSegments = L.polyline([]);
157
157
  this._chartEnabled = true,
158
158
  this.track_info = {};
159
- this.options = _.deepMerge({}, this.options, options);
160
159
 
161
- if (this.options.followMarker) this._setMapView = L.Util.throttle(this._setMapView, 300, this);
162
- if (this.options.legend) this.options.margins.bottom += 30;
163
- if (this.options.theme) this.options.polylineSegments.className += ' ' + this.options.theme;
164
- if (this.options.wptIcons === true) this.options.wptIcons = Options.wptIcons;
160
+ L.setOptions(this, options);
161
+
162
+ if (this.options.followMarker) this._setMapView = L.Util.throttle(this._setMapView, 300, this);
163
+ if (this.options.legend) this.options.margins.bottom += 30;
164
+ if (this.options.theme) this.options.polylineSegments.className += ' ' + this.options.theme;
165
+ if (this.options.wptIcons === true) this.options.wptIcons = Options.wptIcons;
166
+ if (this.options.distanceMarkers === true) this.options.distanceMarkers = { lazy: true };
165
167
 
166
168
  this._markedSegments.setStyle(this.options.polylineSegments);
167
169
 
168
170
  // Leaflet canvas renderer colors
169
171
  L.extend(D3.Colors, this.options.colors || {});
172
+
173
+ // Various stuff
174
+ this._fixCanvasPaths();
175
+ this._fixTooltipSize();
170
176
  },
171
177
 
172
178
  /**
173
179
  * Javascript scripts downloader (lazy loader)
174
180
  */
175
- lazyLoad: function(src) {
176
- return _.lazyLoad(src, this);
181
+ lazyLoad: function(src, condition = false, loader = '') {
182
+ if (!this.options.lazyLoadJS) {
183
+ return Promise.resolve();
184
+ }
185
+ switch(src) {
186
+ case this.__D3:
187
+ loader = '_d3LazyLoader';
188
+ condition = typeof d3 !== 'object';
189
+ break;
190
+ case this.__TOGEOJSON:
191
+ loader = '_togeojsonLazyLoader';
192
+ condition = typeof toGeoJSON !== 'object';
193
+ break;
194
+ case this.__LGEOMUTIL:
195
+ loader = '_geomutilLazyLoader';
196
+ condition = typeof L.GeometryUtil !== 'object';
197
+ break;
198
+ case this.__LALMOSTOVER:
199
+ loader = '_almostoverLazyLoader';
200
+ condition = typeof L.Handler.AlmostOver !== 'function';
201
+ break;
202
+ case this.__LDISTANCEM:
203
+ loader = '_distanceMarkersLazyLoader';
204
+ condition = typeof L.DistanceMarkers !== 'function';
205
+ break;
206
+ }
207
+ return L.Control.Elevation[loader] = _.lazyLoader(src, condition, L.Control.Elevation[loader]);
177
208
  },
178
209
 
179
210
  /**
180
211
  * Load elevation data (GPX, GeoJSON or KML).
181
212
  */
182
213
  load: function(data) {
183
- if (_.isXMLDoc(data)) {
184
- this.loadXML(data);
185
- } else if (_.isJSONDoc(data)) {
186
- this.loadGeoJSON(data);
187
- } else {
188
- this.loadFile(data);
189
- }
190
- },
191
-
192
- /**
193
- * Load data from a remote url.
194
- */
195
- loadFile: function(url) {
196
- fetch(url)
197
- .then((response) => response.text())
198
- .then((data) => {
199
- this._downloadURL = url; // TODO: handle multiple urls?
200
- this.load(data);
201
- })
202
- .catch((err) => console.warn(err));
203
- },
204
-
205
- /**
206
- * Load raw GeoJSON data.
207
- */
208
- loadGeoJSON: function(data) {
209
- _.GeoJSONLoader(data, this);
210
- },
211
-
212
- /**
213
- * Load raw XML data.
214
- */
215
- loadXML: function(data) {
216
- this.lazyLoad(this.__TOGEOJSON)
217
- .then(() => {
218
- let xml = (new DOMParser()).parseFromString(data, "text/xml");
219
- let type = xml.documentElement.tagName.toLowerCase(); // "kml" or "gpx"
220
- if (!(type in toGeoJSON)) {
221
- type = xml.documentElement.tagName == "TrainingCenterDatabase" ? 'tcx' : 'gpx';
222
- }
223
- let geojson = toGeoJSON[type](xml);
224
- let name = xml.getElementsByTagName('name');
225
- if(name[0]) { geojson.name = name[0].innerHTML; }
226
- else if(this._downloadURL) { geojson.name = this._downloadURL.split('/').pop().split('#')[0].split('?')[0]; }
227
-
228
- return this.loadGeoJSON(geojson, this);
229
- });
214
+ this._parseFromString(data).then( geojson => geojson ? this._loadLayer(geojson) : this._loadFile(data));
230
215
  },
231
216
 
232
217
  /**
@@ -236,11 +221,8 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
236
221
  onAdd: function(map) {
237
222
  this._map = map;
238
223
 
239
- let container = this._container = _.create("div", "elevation-control elevation " + this.options.theme);
240
-
241
- if (!this.options.detached) {
242
- _.addClass(container, 'leaflet-control');
243
- }
224
+ let container = this._container = _.create("div", "elevation-control elevation " + this.options.theme + " " + (this.options.detached ? '' : 'leaflet-control'));
225
+
244
226
  this.lazyLoad(this.__D3)
245
227
  .then(() => {
246
228
  this._initButton(container);
@@ -256,8 +238,9 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
256
238
  .on('rotate', this._rotateMarker, this)
257
239
  .on('mousedown', this._resetDrag, this);
258
240
 
259
- _.on(map.getContainer(), 'mousewheel', this._resetDrag, this);
260
- _.on(map.getContainer(), 'touchstart', this._resetDrag, this);
241
+ _.on(map.getContainer(), 'mousewheel', this._resetDrag, this);
242
+ _.on(map.getContainer(), 'touchstart', this._resetDrag, this);
243
+ _.on(document, 'keydown', this._keydownHandler, this);
261
244
 
262
245
  this
263
246
  .on('eledata_added eledata_loaded', this._updateChart, this)
@@ -283,8 +266,9 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
283
266
  .off('resize', this._resizeChart, this)
284
267
  .off('mousedown', this._resetDrag, this);
285
268
 
286
- _.off(map.getContainer(), 'mousewheel', this._resetDrag, this);
287
- _.off(map.getContainer(), 'touchstart', this._resetDrag, this);
269
+ _.off(map.getContainer(), 'mousewheel', this._resetDrag, this);
270
+ _.off(map.getContainer(), 'touchstart', this._resetDrag, this);
271
+ _.off(document, 'keydown', this._keydownHandler, this);
288
272
 
289
273
  this
290
274
  .off('eledata_added eledata_loaded', this._updateChart, this)
@@ -320,6 +304,9 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
320
304
  return;
321
305
  }
322
306
 
307
+ /**
308
+ * Standard GeoJSON --> doesn't handle "time", "heart", …
309
+ */
323
310
  let geom = d.geometry;
324
311
  if (geom) {
325
312
  switch (geom.type) {
@@ -335,35 +322,47 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
335
322
  console.warn('Unsopperted GeoJSON feature geometry type:' + geom.type);
336
323
  }
337
324
  }
338
-
339
325
  if (d.type === "FeatureCollection") {
340
326
  _.each(d.features, feature => this._addData(feature));
341
327
  }
342
328
 
329
+ /**
330
+ * Extended GeoJSON --> rely on leaflet implementation
331
+ */
343
332
  if (d._latlngs) {
344
- this._addGPXData(d._latlngs);
333
+ this._addGeoJSONData(d._latlngs, d.feature && d.feature.properties);
345
334
  }
346
335
  },
347
336
 
348
337
  /*
349
338
  * Parsing of GeoJSON data lines and their elevation in z-coordinate
350
339
  */
351
- _addGeoJSONData: function(coords) {
352
- _.each(coords, point => {
353
- this._addPoint(point[1], point[0], point[2]);
354
- this._fireEvt("elepoint_added", { point: point, index: this._data.length - 1 });
355
- });
356
- this._fireEvt("eletrack_added", { coords: coords, index: this._data.length - 1 });
357
- },
340
+ _addGeoJSONData: function(coords, properties) {
341
+ coords.forEach((point, i) => {
342
+
343
+ // Inspired by L.GPX layer properties
344
+ point.meta = point.meta ?? { time: null, ele: null, hr: null, cad: null, atemp: null };
345
+
346
+ // "alt" property is generated inside "leaflet"
347
+ if ("alt" in point) point.meta.ele = point.alt;
348
+
349
+ // "coordinateProperties" property is generated inside "@tmcw/toGeoJSON"
350
+ if (properties.coordinateProperties) {
351
+ let props = properties.coordinateProperties;
352
+ if("times" in props) point.meta.time = new Date(Date.parse(props.times[i]));
353
+ if("heart" in props) point.meta.hr = parseInt(props.heart[i]);
354
+ // TODO: ask to "@tmcw/toGeoJSON" for "cadence" and "temperature" implementation
355
+ }
356
+
357
+ this._addPoint(
358
+ point.lat ?? point[1],
359
+ point.lng ?? point[0],
360
+ point.alt ?? point.meta.ele ?? point[2]
361
+ );
358
362
 
359
- /*
360
- * Parsing function for GPX data and their elevation in z-coordinate
361
- */
362
- _addGPXData: function(coords) {
363
- _.each(coords, point => {
364
- this._addPoint(point.lat, point.lng, point.alt ?? point.meta.ele);
365
363
  this._fireEvt("elepoint_added", { point: point, index: this._data.length - 1 });
366
364
  });
365
+
367
366
  this._fireEvt("eletrack_added", { coords: coords, index: this._data.length - 1 });
368
367
  },
369
368
 
@@ -387,12 +386,59 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
387
386
 
388
387
  _addLayer: function(layer) {
389
388
  if (layer) this._layers.addLayer(layer)
389
+ // Postpone adding the distance markers (lazy: true)
390
+ if (layer && this.options.distanceMarkers && this.options.distanceMarkers.lazy) {
391
+ layer.on('add remove', (e) => L.DistanceMarkers && e.target instanceof L.Polyline && e.target[e.type + 'DistanceMarkers']());
392
+ }
390
393
  },
391
394
 
392
395
  _addMarker: function(marker) {
393
396
  if (marker) this._markers.addLayer(marker)
394
397
  },
395
398
 
399
+ _bindPopup: function(marker) {
400
+ let popup = marker._popup;
401
+ if (popup) {
402
+ popup.options.className = 'elevation-popup';
403
+ if (popup._content) {
404
+ popup._content = decodeURI(popup._content);
405
+ popup.bindTooltip(popup._content, { direction: 'auto', sticky: true, opacity: 1, className: 'elevation-tooltip' }).openTooltip();
406
+ }
407
+ }
408
+ },
409
+
410
+ /**
411
+ * Initialize "L.AlmostOver" integration
412
+ */
413
+ _initAlmostOverHandler: function(map, layer) {
414
+ if(!map || !this.options.almostOver) return;
415
+ this.lazyLoad(this.__LGEOMUTIL)
416
+ .then(() => this.lazyLoad(this.__LALMOSTOVER))
417
+ .then(() => {
418
+ map.addHandler('almostOver', L.Handler.AlmostOver)
419
+ if (L.GeometryUtil && map.almostOver && map.almostOver.enabled()) {
420
+ map.almostOver.addLayer(layer);
421
+ map
422
+ .on('almost:move', (e) => this._mousemoveLayerHandler(e))
423
+ .on('almost:out', (e) => this._mouseoutHandler(e));
424
+ }
425
+ });
426
+ },
427
+
428
+ /**
429
+ * Initialize "L.DistanceMarkers" integration
430
+ */
431
+ _initDistanceMarkers: function(map, layer) {
432
+ if (!map) return;
433
+ if (this.options.distanceMarkers) {
434
+ this.lazyLoad(this.__LGEOMUTIL)
435
+ .then(() => this.lazyLoad(this.__LDISTANCEM))
436
+ .then(() => this.options.polyline && layer.addTo(map));
437
+ } else if (this.options.polyline) {
438
+ layer.addTo(map);
439
+ }
440
+ },
441
+
396
442
  /**
397
443
  * Adds the control to the given "detached" div.
398
444
  */
@@ -405,8 +451,25 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
405
451
  if (this.options.detached) {
406
452
  _.replaceClass(eleDiv, 'leaflet-control', 'elevation-detached');
407
453
  }
408
- this.eleDiv = eleDiv;
409
- return this.eleDiv;
454
+ return this.eleDiv = eleDiv;
455
+ },
456
+
457
+ /**
458
+ * Initialize "L.AlmostOver" and "L.DistanceMarkers"
459
+ */
460
+ _initMapIntegrations: function(control, layer) {
461
+ let map = control._map;
462
+ if (control._map) {
463
+ map.once('layeradd', (e) => control.options.autofitBounds && control.fitBounds(layer.getBounds()));
464
+ if (!L.Browser.mobile) {
465
+ control._initAlmostOverHandler(map, layer);
466
+ control._initDistanceMarkers(map, layer);
467
+ } else if (control.options.polyline) {
468
+ layer.addTo(map);
469
+ }
470
+ } else {
471
+ console.warn("Undefined elevation map object");;
472
+ }
410
473
  },
411
474
 
412
475
  /*
@@ -423,6 +486,58 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
423
486
  _.replaceClass(this._container, 'elevation-collapsed', 'elevation-expanded');
424
487
  },
425
488
 
489
+ /**
490
+ * Add some basic colors to leaflet canvas renderer (preferCanvas: true).
491
+ */
492
+ _fixCanvasPaths: function() {
493
+ let oldProto = L.Canvas.prototype._fillStroke;
494
+ let control = this;
495
+ L.Canvas.include({
496
+ _fillStroke: function(ctx, layer) {
497
+ if (control._layers.hasLayer(layer)) {
498
+
499
+ let theme = control.options.theme.replace('-theme', '');
500
+ let color = D3.Colors[theme] || {};
501
+ let options = layer.options;
502
+
503
+ options.color = color.line || color.area || theme;
504
+ options.stroke = !!options.color;
505
+
506
+ oldProto.call(this, ctx, layer);
507
+
508
+ if (options.stroke && options.weight !== 0) {
509
+ let oldVal = ctx.globalCompositeOperation || 'source-over';
510
+ ctx.globalCompositeOperation = 'destination-over'
511
+ ctx.strokeStyle = color.outline || '#FFF';
512
+ ctx.lineWidth = options.weight * 1.75;
513
+ ctx.stroke();
514
+ ctx.globalCompositeOperation = oldVal;
515
+ }
516
+
517
+ } else {
518
+ oldProto.call(this, ctx, layer);
519
+ }
520
+ }
521
+ });
522
+ },
523
+
524
+ /**
525
+ * Partial fix for initial tooltip size
526
+ *
527
+ * @link https://github.com/Raruto/leaflet-elevation/issues/81#issuecomment-713477050
528
+ */
529
+ _fixTooltipSize: function() {
530
+ this.on('elechart_init', () =>
531
+ this.once('elechart_change elechart_hover', e => {
532
+ if (this._chartEnabled) {
533
+ this._chart._showDiagramIndicator(e.data, e.xCoord);
534
+ this._chart._showDiagramIndicator(e.data, e.xCoord);
535
+ }
536
+ this._updateMarker(e.data);
537
+ })
538
+ );
539
+ },
540
+
426
541
  /*
427
542
  * Finds a data entry for the given LatLng
428
543
  */
@@ -522,7 +637,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
522
637
  },
523
638
 
524
639
  _initMarker: function(map) {
525
- let pane = map.getPane('elevationPane');
640
+ let pane = map.getPane('elevationPane');
526
641
  if (!pane) {
527
642
  pane = this._pane = map.createPane('elevationPane', map.getPane('norotatePane') || map.getPane('mapPane'));
528
643
  pane.style.zIndex = 625; // This pane is above markers but below popups.
@@ -530,8 +645,8 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
530
645
  }
531
646
 
532
647
  if (this._renderer) this._renderer.remove()
533
- this._renderer = L.svg({ pane: "elevationPane" }).addTo(this._map); // default leaflet svg renderer
534
- this._marker = new Marker(this.options);
648
+ this._renderer = L.svg({ pane: "elevationPane" }).addTo(this._map); // default leaflet svg renderer
649
+ this._marker = new Marker(this.options);
535
650
 
536
651
  this._fireEvt("elechart_marker");
537
652
  },
@@ -540,21 +655,13 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
540
655
  * Inspired by L.Control.Layers
541
656
  */
542
657
  _initButton: function(container) {
543
- //Makes this work on IE10 Touch devices by stopping it from firing a mouseout event when the touch is released
544
- container.setAttribute('aria-haspopup', true);
545
658
 
546
- if (!this.options.detached) {
659
+ if (L.Browser.mobile || !this.options.detached) {
547
660
  L.DomEvent
548
661
  .disableClickPropagation(container)
549
662
  .disableScrollPropagation(container);
550
663
  }
551
664
 
552
- if (L.Browser.mobile) {
553
- _.on(container, 'click', L.DomEvent.stopPropagation);
554
- }
555
-
556
- _.on(container, 'mousewheel', this._mousewheelHandler, this);
557
-
558
665
  if (!this.options.detached) {
559
666
  let link = this._button = _.create('a', "elevation-toggle elevation-toggle-icon" + (this.options.autohide ? "" : " close-button"), { href: '#', title: L._('Elevation') }, container);
560
667
 
@@ -571,21 +678,100 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
571
678
  _.on(link, 'focus', this._toggle, this);
572
679
 
573
680
  this._map.on('click', this._collapse, this);
574
- // TODO: keyboard accessibility
575
681
  }
576
- } else {
577
- // TODO: handle autohide when detached=true
578
682
  }
579
683
  },
580
684
 
581
685
  _initSummary: function(container) {
582
- let summary = this._summary = new Summary({ summary: this.options.summary }, this);
686
+ this._summary = new Summary({ summary: this.options.summary }, this);
583
687
 
584
- d3
585
- .select(container)
586
- .call(summary.render());
688
+ d3.select(container).call(this._summary.render());
689
+ },
690
+
691
+ _keydownHandler: function(e) {
692
+ if(!this.options.detached && e.key === "Escape"){
693
+ this._collapse()
694
+ };
695
+ },
696
+
697
+ /**
698
+ * Retrieve data from a remote url (HTTP).
699
+ */
700
+ _loadFile: function(url) {
701
+ fetch(url)
702
+ .then((response) => response.text())
703
+ .then((data) => {
704
+ this._downloadURL = url; // TODO: handle multiple urls?
705
+ this._parseFromString(data)
706
+ .then( geojson => geojson && this._loadLayer(geojson));
707
+ }).catch((err) => console.warn(err));
708
+ },
709
+
710
+ /**
711
+ * Simple GeoJSON data loader (L.GeoJSON).
712
+ */
713
+ _loadLayer: function(geojson) {
714
+ let control = this;
715
+
716
+ let { wptIcons, wptLabels } = control.options;
717
+
718
+ let layer = L.geoJson(geojson, {
719
+ distanceMarkers: control.options.distanceMarkers,
720
+ style: (feature) => {
721
+ let style = L.extend({}, control.options.polyline);
722
+ if (control.options.theme) {
723
+ style.className += ' ' + control.options.theme;
724
+ }
725
+ return style;
726
+ },
727
+ pointToLayer: (feature, latlng) => {
728
+ if (!control.options.waypoints) return;
729
+
730
+ let prop = feature.properties;
731
+ let desc = prop.desc ?? '';
732
+ let name = prop.name ?? '';
733
+ let sym = (prop.sym ?? name).replace(' ', '-').replace('"', '').replace("'", '').toLowerCase();
734
+
735
+ // Handle chart waypoints (dots)
736
+ if ([true, 'dots'].includes(control.options.waypoints)) {
737
+ control._registerCheckPoint({
738
+ latlng: latlng,
739
+ label: ([true, 'dots'].includes(wptLabels) ? name : '')
740
+ });
741
+ }
742
+
743
+ // Handle map waypoints (markers)
744
+ if ([true, 'markers'].includes(control.options.waypoints) && wptIcons != false) {
745
+ // generate and cache appropriate icon symbol
746
+ if (!wptIcons.hasOwnProperty(sym)) {
747
+ wptIcons[sym] = L.divIcon(
748
+ L.extend({}, wptIcons[""].options, { html: '<i class="elevation-waypoint-icon ' + sym + '"></i>' } )
749
+ );
750
+ }
751
+ let marker = L.marker(latlng, { icon: wptIcons[sym] });
752
+ if ([true, 'markers'].includes(wptLabels) && (name || desc)) {
753
+ marker.bindPopup("<b>" + name + "</b>" + (desc.length > 0 ? '<br>' + desc : '')).openPopup();
754
+ }
755
+ control._addMarker(marker);
756
+ control._bindPopup(marker);
757
+ control.fire('waypoint_added', { point: marker, element: latlng, properties: prop });
758
+ return marker;
759
+ }
760
+ },
761
+ onEachFeature: (feature, layer) => {
762
+ if (feature.geometry && feature.geometry.type != 'Point') {
763
+ control.addData(layer);
764
+ control.track_info = L.extend({}, control.track_info, { name: geojson.name });
765
+ }
766
+ },
767
+ });
587
768
 
588
- this.summaryDiv = this._summary._container;
769
+ control.lazyLoad(control.__D3).then(() => {
770
+ control._initMapIntegrations(control, layer);
771
+ control._fireEvt("eledata_loaded", { data: geojson, layer: layer, name: control.track_info.name, track_info: control.track_info })
772
+ });
773
+
774
+ return layer;
589
775
  },
590
776
 
591
777
  _dragendHandler: function(e) {
@@ -657,19 +843,90 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
657
843
  this._fireEvt("elechart_leave");
658
844
  },
659
845
 
660
- /*
661
- * Handles the mouesewheel over the chart.
846
+ /**
847
+ * Simple GeoJSON Parser
662
848
  */
663
- _mousewheelHandler: function(e) {
664
- if (this._map.gestureHandling && this._map.gestureHandling._enabled) return;
665
- let ll = this._marker.getLatLng() || this._map.getCenter();
666
- let z = this._map.getZoom() + Math.sign(e.deltaY);
667
- this._resetDrag();
668
- this._map.flyTo(ll, z);
849
+ _parseFromGeoJSONString: function(data) {
850
+ try {
851
+ return JSON.parse(data);
852
+ } catch (e) { }
853
+ },
854
+
855
+ /**
856
+ * Attempt to parse raw response data (GeoJSON or XML > GeoJSON)
857
+ */
858
+ _parseFromString: function(data) {
859
+ return new Promise(resolve =>
860
+ this.lazyLoad(this.__TOGEOJSON).then(() => {
861
+ let geojson;
862
+ try {
863
+ geojson = this._parseFromXMLString(data.trim());
864
+ } catch (e) {
865
+ geojson = this._parseFromGeoJSONString(data.toString());
866
+ }
867
+ if (geojson) {
868
+ geojson = this._prepareMultiLineStrings(geojson);
869
+ geojson = this._prepareAdditionalProperties(geojson);
870
+ }
871
+ resolve(geojson);
872
+ })
873
+ );
874
+ },
875
+
876
+ /**
877
+ * Simple XML Parser (GPX, KML, TCX)
878
+ */
879
+ _parseFromXMLString: function(data) {
880
+ if (data.indexOf("<") != 0) {
881
+ throw 'Invalid XML';
882
+ }
883
+ let xml = (new DOMParser()).parseFromString(data, "text/xml");
884
+ let type = xml.documentElement.tagName.toLowerCase(); // "kml" or "gpx"
885
+ let name = xml.getElementsByTagName('name');
886
+ if (xml.getElementsByTagName('parsererror').length) {
887
+ throw 'Invalid XML';
888
+ }
889
+ if (!(type in toGeoJSON)) {
890
+ type = xml.documentElement.tagName == "TrainingCenterDatabase" ? 'tcx' : 'gpx';
891
+ }
892
+ let geojson = toGeoJSON[type](xml);
893
+ geojson.name = name.length > 0 ? (Array.from(name).find(tag => tag.parentElement.tagName == "trk") ?? name[0]).textContent : '';
894
+ return geojson;
895
+ },
896
+
897
+ /**
898
+ * Extend GeoJSON properties (name, time, ...)
899
+ */
900
+ _prepareAdditionalProperties: function(geojson) {
901
+ if (!geojson.name && this._downloadURL) {
902
+ geojson.name = this._downloadURL.split('/').pop().split('#')[0].split('?')[0];
903
+ }
904
+ return geojson;
905
+ },
906
+
907
+ /**
908
+ * Just a temporary fix for MultiLineString data (trk > trkseg + trkseg),
909
+ * just split them into seperate LineStrings (trk > trkseg, trk > trkseg)
910
+ *
911
+ * @link https://github.com/Raruto/leaflet-elevation/issues/56
912
+ */
913
+ _prepareMultiLineStrings: function(geojson) {
914
+ geojson.features.forEach(feauture => {
915
+ if (feauture.geometry.type == "MultiLineString") {
916
+ feauture.geometry.coordinates.forEach(coords => {
917
+ geojson.features.push({
918
+ geometry: { type: 'LineString', coordinates: coords },
919
+ properties: feauture.properties,
920
+ type: 'Feature'
921
+ });
922
+ });
923
+ }
924
+ });
925
+ return geojson.features.filter(feauture => feauture.geometry.type != "MultiLineString");
669
926
  },
670
927
 
671
928
  /**
672
- * Add a waypoint marker to the diagram
929
+ * Add a point of interest over the diagram
673
930
  */
674
931
  _registerCheckPoint: function(props) {
675
932
  this.on("elechart_updated", () => this._chart._registerCheckPoint(props));
@@ -699,7 +956,7 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
699
956
  /**
700
957
  * Add chart or marker tooltip info
701
958
  */
702
- _registerTooltip: function(props) {
959
+ _registerTooltip: function(props) {
703
960
  if (props.chart) {
704
961
  let label = L.extend({}, props, { value: props.chart });
705
962
  this.on("elechart_init", () => this._chart._registerTooltip(label));
@@ -887,9 +1144,14 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
887
1144
  this._fireEvt("elechart_summary");
888
1145
  }
889
1146
  if (this.options.downloadLink && this._downloadURL) { // TODO: generate dynamically file content instead of using static file urls.
890
- this.summaryDiv.innerHTML += '<span class="download"><a href="#">' + L._('Download') + '</a></span>'
891
- _.select('.download a', this.summaryDiv).onclick = (e) => {
1147
+ this._summary._container.innerHTML += '<span class="download"><a href="#">' + L._('Download') + '</a></span>'
1148
+ _.select('.download a', this._summary._container).onclick = (e) => {
892
1149
  e.preventDefault();
1150
+ if (e.downloadLink == 'modal' && typeof CustomEvent === "function") {
1151
+ document.dispatchEvent(new CustomEvent("eletrack_download", { detail: e }));
1152
+ } else if (e.downloadLink == 'link' || e.downloadLink === true) {
1153
+ e.confirm();
1154
+ }
893
1155
  this._fireEvt('eletrack_download', { downloadLink: this.options.downloadLink, confirm: _.saveFile.bind(this, this._downloadURL) });
894
1156
  };
895
1157
  };
@@ -921,125 +1183,4 @@ export const Elevation = L.Control.Elevation = L.Control.extend({
921
1183
  return this.__yTicks || Math.round(this._height() / 30);
922
1184
  }
923
1185
 
924
- });
925
-
926
- /**
927
- * Attach here some useful elevation hooks.
928
- */
929
- Elevation.addInitHook(function() {
930
-
931
- // Alias deprecated functions
932
- this.enableDragging = this.enableBrush;
933
- this.disableDragging = this.disableBrush;
934
- this.loadChart = this.addTo;
935
- this.loadData = this.load;
936
- this.loadGPX = this.loadXML;
937
-
938
- this.on('waypoint_added', function(e) {
939
- let p = e.point,
940
- pop = p._popup;
941
- if (pop) {
942
- pop.options.className = 'elevation-popup';
943
- }
944
- if (pop && pop._content) {
945
- pop._content = decodeURI(pop._content);
946
- p.bindTooltip(pop._content, { direction: 'auto', sticky: true, opacity: 1, className: 'elevation-tooltip' }).openTooltip();
947
- }
948
- });
949
-
950
- this.on("eletrack_download", function(e) {
951
- if (e.downloadLink == 'modal' && typeof CustomEvent === "function") {
952
- document.dispatchEvent(new CustomEvent("eletrack_download", { detail: e }));
953
- } else if (e.downloadLink == 'link' || e.downloadLink === true) {
954
- e.confirm();
955
- }
956
- });
957
-
958
- this.on('eledata_loaded', function(e) {
959
- let map = this._map;
960
- let layer = e.layer;
961
- if (!map) {
962
- console.warn("Undefined elevation map object");
963
- return;
964
- }
965
- map.once('layeradd', (e) => this.options.autofitBounds && this.fitBounds(layer.getBounds()));
966
-
967
- if (L.Browser.mobile) {
968
-
969
- if (this.options.polyline) layer.addTo(map);
970
-
971
- } else {
972
-
973
- // leaflet-geometryutil
974
- this.lazyLoad(this.__LGEOMUTIL)
975
- .then(() => {
976
-
977
- // leaflet-almostover
978
- if (this.options.almostOver) {
979
- this.lazyLoad(this.__LALMOSTOVER)
980
- .then(() => {
981
- map.addHandler('almostOver', L.Handler.AlmostOver)
982
- if (L.GeometryUtil && map.almostOver && map.almostOver.enabled()) {
983
- map.almostOver.addLayer(layer);
984
- map
985
- .on('almost:move', (e) => this._mousemoveLayerHandler(e))
986
- .on('almost:out', (e) => this._mouseoutHandler(e));
987
- }
988
- });
989
- }
990
-
991
- // leaflet-distance-markers
992
- if (this.options.distanceMarkers) {
993
- this.lazyLoad(this.__LDISTANCEM)
994
- .then(() => this.options.polyline && layer.addTo(map));
995
- } else {
996
- if (this.options.polyline) layer.addTo(map);
997
- }
998
- });
999
- }
1000
-
1001
- });
1002
-
1003
- // Basic canvas renderer support.
1004
- let oldProto = L.Canvas.prototype._fillStroke;
1005
- let control = this;
1006
- L.Canvas.include({
1007
- _fillStroke: function(ctx, layer) {
1008
- if (control._layers.hasLayer(layer)) {
1009
-
1010
- let theme = control.options.theme.replace('-theme', '');
1011
- let color = D3.Colors[theme] || {};
1012
- let options = layer.options;
1013
-
1014
- options.color = color.line || color.area || theme;
1015
- options.stroke = !!options.color;
1016
-
1017
- oldProto.call(this, ctx, layer);
1018
-
1019
- if (options.stroke && options.weight !== 0) {
1020
- let oldVal = ctx.globalCompositeOperation || 'source-over';
1021
- ctx.globalCompositeOperation = 'destination-over'
1022
- ctx.strokeStyle = color.outline || '#FFF';
1023
- ctx.lineWidth = options.weight * 1.75;
1024
- ctx.stroke();
1025
- ctx.globalCompositeOperation = oldVal;
1026
- }
1027
-
1028
- } else {
1029
- oldProto.call(this, ctx, layer);
1030
- }
1031
- }
1032
- });
1033
-
1034
- // Partially fix: https://github.com/Raruto/leaflet-elevation/issues/81#issuecomment-713477050
1035
- this.on('elechart_init', function() {
1036
- this.once('elechart_change elechart_hover', function(e) {
1037
- if (this._chartEnabled) {
1038
- this._chart._showDiagramIndicator(e.data, e.xCoord);
1039
- this._chart._showDiagramIndicator(e.data, e.xCoord);
1040
- }
1041
- this._updateMarker(e.data);
1042
- });
1043
- });
1044
-
1045
- });
1186
+ });