@raruto/leaflet-elevation 2.2.2 → 2.2.5

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.
@@ -285,1306 +285,1313 @@
285
285
  yAxisMin: undefined,
286
286
  };
287
287
 
288
- // "leaflet-i18n" fallback
289
- if (!L._ || !L.i18n) {
290
- L._ = L.i18n = (string, data) => string;
291
- }
292
-
293
- const Elevation = L.Control.Elevation = L.Control.extend({
294
-
295
- includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
296
-
297
- options: Options,
298
- __mileFactor: 0.621371, // 1 km = (0.621371 mi)
299
- __footFactor: 3.28084, // 1 m = (3.28084 ft)
300
- __D3: 'https://unpkg.com/d3@6.5.0/dist/d3.min.js',
301
- __TOGEOJSON: 'https://unpkg.com/@tmcw/togeojson@4.6.0/dist/togeojson.umd.js',
302
- __LGEOMUTIL: 'https://unpkg.com/leaflet-geometryutil@0.9.3/src/leaflet.geometryutil.js',
303
- __LALMOSTOVER: 'https://unpkg.com/leaflet-almostover@1.0.1/src/leaflet.almostover.js',
304
- __LHOTLINE: '../libs/leaflet-hotline.min.js',
305
- __LDISTANCEM: '../libs/leaflet-distance-marker.min.js',
306
- __LCHART: '../src/components/chart.js',
307
- __LMARKER: '../src/components/marker.js',
308
- __LSUMMARY: '../src/components/summary.js',
309
- __modulesFolder: '../src/handlers/',
310
-
311
- /*
312
- * Add data to the diagram either from GPX or GeoJSON and update the axis domain and data
313
- */
314
- addData: function(d, layer) {
315
- this.import(this.__D3)
316
- .then(() => {
317
- if (this._modulesLoaded) {
318
- layer = layer ?? (d.on && d);
319
- this._addData(d);
320
- this._addLayer(layer);
321
- this._fireEvt("eledata_added", { data: d, layer: layer, track_info: this.track_info });
322
- } else {
323
- this.once('modules_loaded', () => this.addData(d,layer));
324
- }
325
- });
326
- },
327
-
328
- /**
329
- * Adds the control to the given map.
330
- */
331
- addTo: function(map) {
332
- if (this.options.detached) {
333
- let parent = select(this.options.elevationDiv);
334
- let eleDiv = this.onAdd(map);
335
- parent ? append(parent, eleDiv) : insert(map.getContainer(), eleDiv, 'afterend');
336
- } else {
337
- L.Control.prototype.addTo.call(this, map);
338
- }
339
- return this;
340
- },
341
-
342
- /*
343
- * Reset data and display
344
- */
345
- clear: function() {
346
- if (this._marker) this._marker.remove();
347
- if (this._chart) this._clearChart();
348
- if (this._layers) this._clearLayers(this._layers);
349
- if (this._markers) this._clearLayers(this._markers);
350
- if (this._circleMarkers) this._circleMarkers.remove();
351
- if (this._hotline) this._clearLayers(this._hotline);
352
-
353
- this._data = [];
354
- this.track_info = {};
355
-
356
- this._fireEvt("eledata_clear");
357
-
358
- this._updateChart();
359
- },
360
-
361
- _clearChart: function() {
362
- if (this._events && this._events.elechart_updated) {
363
- this._events.elechart_updated.forEach(({fn, ctx}) => this.off('elechart_updated', fn, ctx));
364
- }
365
- if (this._chart && this._chart._container) {
366
- this._chart._container.selectAll('g.point .point').remove();
367
- this._chart.clear();
368
- }
369
- },
370
-
371
- _clearLayers: function(l) {
372
- l = l || this._layers;
373
- if (l && l.eachLayer) {
374
- l.eachLayer(f => f.remove());
375
- l.clearLayers();
376
- }
377
- },
378
-
379
- /**
380
- * TODO: Create a base class to handle custom data attributes (heart rate, cadence, temperature, ...)
381
- *
382
- * @link https://leafletjs.com/examples/extending/extending-3-controls.html#handlers
383
- */
384
- // addHandler: function (name, HandlerClass) {
385
- // if (HandlerClass) {
386
- // let handler = this[name] = new HandlerClass(this);
387
- // this.handlers.push(handler);
388
- // if (this.options[name]) {
389
- // handler.enable();
390
- // }
391
- // }
392
- // return this;
393
- // },
394
-
395
- /**
396
- * Disable chart brushing.
397
- */
398
- disableBrush: function() {
399
- this._chart._brushEnabled = false;
400
- this._resetDrag();
401
- },
402
-
403
- /**
404
- * Enable chart brushing.
405
- */
406
- enableBrush: function() {
407
- this._chart._brushEnabled = true;
408
- },
409
-
410
- /**
411
- * Disable chart zooming.
412
- */
413
- disableZoom: function() {
414
- this._chart._zoomEnabled = false;
415
- this._chart._resetZoom();
416
- },
417
-
418
- /**
419
- * Enable chart zooming.
420
- */
421
- enableZoom: function() {
422
- this._chart._zoomEnabled = true;
423
- },
424
-
425
- /**
426
- * Sets a map view that contains the given geographical bounds.
427
- */
428
- fitBounds: function(bounds) {
429
- bounds = bounds || this.getBounds();
430
- if (this._map && bounds.isValid()) this._map.fitBounds(bounds);
431
- },
432
-
433
- getBounds: function(data) {
434
- return L.latLngBounds((data || this._data).map((d) => d.latlng));
435
- },
436
-
437
- /**
438
- * Get default zoom level (followMarker: true).
439
- */
440
- getZFollow: function() {
441
- return this.options.zFollow;
442
- },
443
-
444
- /**
445
- * Hide current elevation chart profile.
446
- */
447
- hide: function() {
448
- style(this._container, "display", "none");
449
- },
450
-
451
- /**
452
- * Initialize chart control "options" and "container".
453
- */
454
- initialize: function(opts) {
455
-
456
- opts = L.setOptions(this, opts);
457
-
458
- this._data = [];
459
- this._layers = L.featureGroup();
460
- this._markers = L.featureGroup();
461
- this._hotline = L.featureGroup();
462
- this._circleMarkers = L.featureGroup();
463
- this._markedSegments = L.polyline([]);
464
- this._start = (opts.trkStart || Options.trkStart);
465
- this._end = (opts.trkEnd || Options.trkEnd);
466
- this._chartEnabled = true;
467
- this._yCoordMax = -Infinity;
468
- this.track_info = {};
469
- // this.handlers = [];
470
-
471
- if (opts.followMarker) this._setMapView = throttle(this._setMapView, 300, this);
472
- if (opts.legend) opts.margins.bottom += 30;
473
- if (opts.theme) opts.polylineSegments.className += ' ' + opts.theme;
474
- if (opts.wptIcons === true) opts.wptIcons = Options.wptIcons;
475
- if (opts.distanceMarkers === true) opts.distanceMarkers = Options.distanceMarkers;
476
- if (opts.trkStart) this._start.addTo(this._circleMarkers);
477
- if (opts.trkEnd) this._end.addTo(this._circleMarkers);
478
-
479
-
480
- this._markedSegments.setStyle(opts.polylineSegments);
481
-
482
- // Leaflet canvas renderer colors
483
- L.extend(Colors, opts.colors || {});
484
-
485
- // Various stuff
486
- this._fixCanvasPaths();
487
- this._fixTooltipSize();
488
-
489
- },
490
-
491
- /**
492
- * Javascript scripts downloader (lazy loader)
493
- */
494
- import: function(src, condition) {
495
- switch(src) {
496
- case this.__D3: condition = typeof d3 !== 'object'; break;
497
- case this.__TOGEOJSON: condition = typeof toGeoJSON !== 'object'; break;
498
- case this.__LGEOMUTIL: condition = typeof L.GeometryUtil !== 'object'; break;
499
- case this.__LALMOSTOVER: condition = typeof L.Handler.AlmostOver !== 'function'; break;
500
- case this.__LDISTANCEM: condition = typeof L.DistanceMarkers !== 'function'; break;
501
- }
502
- return condition !== false ? import(src) : Promise.resolve();
503
- },
504
-
505
- /**
506
- * Load elevation data (GPX, GeoJSON, KML or TCX).
507
- */
508
- load: function(data) {
509
- this._parseFromString(data).then( geojson => geojson ? this._loadLayer(geojson) : this._loadFile(data));
510
- },
511
-
512
- /**
513
- * Create container DOM element and related event listeners.
514
- * Called on control.addTo(map).
515
- */
516
- onAdd: function(map) {
517
- this._map = map;
518
-
519
- let container = this._container = create("div", "elevation-control " + this.options.theme + " " + (this.options.detached ? 'elevation-detached' : 'leaflet-control'), this.options.detached ? { id: 'elevation-' + randomId() } : {});
520
-
521
- if (!this.eleDiv) this.eleDiv = container;
522
-
523
- this._loadModules(this.options.handlers).then(() => { // Inject here required modules (data handlers)
524
- this._initChart(container);
525
- this._initButton(container);
526
- this._initSummary(container);
527
- this._initMarker(map);
528
- this._initLayer(map);
529
- this._modulesLoaded = true;
530
- this.fire('modules_loaded');
531
- });
532
-
533
- this.fire('add');
534
-
535
- return container;
536
- },
537
-
538
- /**
539
- * Clean up control code and related event listeners.
540
- * Called on control.remove().
541
- */
542
- onRemove: function(map) {
543
- this._container = null;
544
-
545
- map
546
- .off('zoom viewreset zoomanim', this._hideMarker, this)
547
- .off('resize', this._resetView, this)
548
- .off('resize', this._resizeChart, this)
549
- .off('mousedown', this._resetDrag, this);
550
-
551
- off(map.getContainer(), 'mousewheel', this._resetDrag, this);
552
- off(map.getContainer(), 'touchstart', this._resetDrag, this);
553
- off(document, 'keydown', this._onKeyDown, this);
554
-
555
- this
556
- .off('eledata_added eledata_loaded', this._updateChart, this)
557
- .off('eledata_added eledata_loaded', this._updateSummary, this);
558
-
559
- this.fire('remove');
560
- },
561
-
562
- /**
563
- * Redraws the chart control. Sometimes useful after screen resize.
564
- */
565
- redraw: function() {
566
- this._resizeChart();
567
- },
568
-
569
- /**
570
- * Set default zoom level (followMarker: true).
571
- */
572
- setZFollow: function(zoom) {
573
- this.options.zFollow = zoom;
574
- },
575
-
576
- /**
577
- * Hide current elevation chart profile.
578
- */
579
- show: function() {
580
- style(this._container, "display", "block");
581
- },
582
-
583
- /*
584
- * Parsing data either from GPX or GeoJSON and update the diagram data
585
- */
586
- _addData: function(d) {
587
- if (!d) {
588
- return;
589
- }
590
-
591
- // Standard GeoJSON
592
- if (d.type === "FeatureCollection" ) {
593
- return each(d.features, feature => this._addData(feature));
594
- } else if (d.type === "Feature") {
595
- let geom = d.geometry;
596
- if (geom) {
597
- switch (geom.type) {
598
- case 'LineString': return this._addGeoJSONData(geom.coordinates, d.properties);
599
- case 'MultiLineString': return each(geom.coordinates, (coords, i) => this._addGeoJSONData(coords, d.properties, i));
600
- case 'Point':
601
- default: return console.warn('Unsopperted GeoJSON feature geometry type:' + geom.type);
602
- }
603
- }
604
- }
605
-
606
- // Fallback for leaflet layers (eg. L.Gpx)
607
- if (d._latlngs) {
608
- return this._addGeoJSONData(d._latlngs, d.feature && d.feature.properties);
609
- }
610
-
611
- },
612
-
613
- /*
614
- * Parsing of GeoJSON data lines and their elevation in z-coordinate
615
- */
616
- _addGeoJSONData: function(coords, properties, nestingLevel) {
617
-
618
- // "coordinateProperties" property is generated inside "@tmcw/toGeoJSON"
619
- let props = (properties && properties.coordinateProperties) || properties;
620
-
621
- coords.forEach((point, i) => {
622
-
623
- // GARMIN_EXTENSIONS = ["hr", "cad", "atemp", "wtemp", "depth", "course", "bearing"];
624
- point.meta = point.meta ?? { time: null, ele: null };
625
-
626
- point.prev = (attr) => (attr ? this._data[i > 0 ? i - 1 : 0][attr] : this._data[i > 0 ? i - 1 : 0]);
627
-
628
- this.fire("elepoint_init", { point: point, props: props, id: i, isMulti: nestingLevel });
629
-
630
- this._addPoint(
631
- point.lat ?? point[1],
632
- point.lng ?? point[0],
633
- point.alt ?? point.meta.ele ?? point[2]
634
- );
635
-
636
- this.fire("elepoint_added", { point: point, index: this._data.length - 1 });
637
-
638
- if (this._yCoordMax < this._data[this._data.length - 1][this.options.yAttr]) this._yCoordMax = this._data[this._data.length - 1][this.options.yAttr];
639
- });
640
-
641
- this.fire("eletrack_added", { coords: coords, index: this._data.length - 1 });
642
- },
643
-
644
- /*
645
- * Parse and push a single (x, y, z) point to current elevation profile.
646
- */
647
- _addPoint: function(x, y, z) {
648
- if (this.options.reverseCoords) {
649
- [x, y] = [y, x];
650
- }
651
-
652
- this._data.push({
653
- x: x,
654
- y: y,
655
- z: z,
656
- latlng: L.latLng(x, y, z)
657
- });
658
-
659
- this.fire("eledata_updated", { index: this._data.length - 1 });
660
- },
661
-
662
- _addLayer: function(layer) {
663
- if (layer) this._layers.addLayer(layer);
664
- // Postpone adding the distance markers (lazy: true)
665
- if (layer && this.options.distanceMarkers && this.options.distanceMarkers.lazy) {
666
- layer.on('add remove', ({target, type}) => L.DistanceMarkers && target instanceof L.Polyline && target[type + 'DistanceMarkers']());
667
- }
668
- return layer;
669
- },
670
-
671
- _addMarker: function(marker) {
672
- if (marker) this._markers.addLayer(marker);
673
- return marker;
674
- },
675
-
676
- /**
677
- * Initialize "L.AlmostOver" integration
678
- */
679
- _initAlmostOverHandler: function(map, layer) {
680
- return (map && this.options.almostOver && !L.Browser.mobile) ? Promise.all([
681
- this.import(this.__LGEOMUTIL),
682
- this.import(this.__LALMOSTOVER)
683
- ]).then(() => {
684
- map.addHandler('almostOver', L.Handler.AlmostOver);
685
- if (L.GeometryUtil && map.almostOver && map.almostOver.enabled()) {
686
- map.almostOver.addLayer(layer);
687
- map
688
- .on('almost:move', (e) => this._onMouseMoveLayer(e))
689
- .on('almost:out', (e) => this._onMouseOut(e));
690
- }
691
- }) : Promise.resolve();
692
- },
693
-
694
- /**
695
- * Initialize "L.DistanceMarkers" integration
696
- */
697
- _initDistanceMarkers: function() {
698
- return this.options.distanceMarkers ? Promise.all([this.import(this.__LGEOMUTIL), this.import(this.__LDISTANCEM)]) : Promise.resolve();
699
- },
700
-
701
- _initHotLine: function(layer) {
702
- let prop = typeof this.options.hotline == 'string' ? this.options.hotline : 'elevation';
703
- return this.options.hotline ? this.import(this.__LHOTLINE)
704
- .then(() => {
705
- layer.eachLayer((trkseg) => {
706
- if(trkseg.feature.geometry.type != "Point") {
707
- let geo = L.geoJson(trkseg.toGeoJSON(), { coordsToLatLng: (coords) => L.latLng(coords[0], coords[1], coords[2])});
708
- let line = L.hotline(geo.toGeoJSON().features[0].geometry.coordinates, {
709
- min: isFinite(this.track_info[prop + '_min']) ? this.track_info[prop + '_min'] : 0,
710
- max: isFinite(this.track_info[prop + '_max']) ? this.track_info[prop + '_max'] : 1,
711
- palette: {
712
- 0.0: '#008800',
713
- 0.5: '#ffff00',
714
- 1.0: '#ff0000'
715
- },
716
- weight: 5,
717
- outlineColor: '#000000',
718
- outlineWidth: 1
719
- }).addTo(this._hotline);
720
- let alpha = trkseg.options.style && trkseg.options.style.opacity || 1;
721
- trkseg.on('add remove', ({type}) => {
722
- trkseg.setStyle({opacity: (type == 'add' ? 0 : alpha)});
723
- line[(type == 'add' ? 'addTo' : 'removeFrom')](trkseg._map);
724
- if (line._renderer) line._renderer._container.parentElement.insertBefore(line._renderer._container, line._renderer._container.parentElement.firstChild);
725
- });
726
- }
727
- });
728
- }) : Promise.resolve();
729
- },
730
-
731
- /**
732
- * Initialize "L.AlmostOver" and "L.DistanceMarkers"
733
- */
734
- _initMapIntegrations: function(layer) {
735
- let map = this._map;
736
- if (map) {
737
- if (this._data.length) {
738
- this._start.setLatLng(this._data[0].latlng);
739
- this._end.setLatLng(this._data[this._data.length -1].latlng);
740
- }
741
- Promise.all([
742
- this._initHotLine(layer),
743
- this._initAlmostOverHandler(map, layer),
744
- this._initDistanceMarkers(),
745
- ]).then(() => {
746
- if (this.options.polyline) {
747
- layer.addTo(map);
748
- this._circleMarkers.addTo(map);
749
- }
750
- if (this.options.autofitBounds) {
751
- this.fitBounds(layer.getBounds());
752
- }
753
- map.invalidateSize();
754
- });
755
- } else {
756
- this.once('add', () => this._initMapIntegrations(layer));
757
- }
758
- },
759
-
760
- /*
761
- * Collapse current chart control.
762
- */
763
- _collapse: function() {
764
- replaceClass(this._container, 'elevation-expanded', 'elevation-collapsed');
765
- if (this._map) this._map.invalidateSize();
766
- },
767
-
768
- /*
769
- * Expand current chart control.
770
- */
771
- _expand: function() {
772
- replaceClass(this._container, 'elevation-collapsed', 'elevation-expanded');
773
- if (this._map) this._map.invalidateSize();
774
- },
775
-
776
- /**
777
- * Add some basic colors to leaflet canvas renderer (preferCanvas: true).
778
- */
779
- _fixCanvasPaths: function() {
780
- let oldProto = L.Canvas.prototype._fillStroke;
781
- let control = this;
782
-
783
- let theme = this.options.theme.split(' ')[0].replace('-theme', '');
784
- let color = Colors[theme] || {};
785
-
786
- L.Canvas.include({
787
- _fillStroke: function(ctx, layer) {
788
- if (control._layers.hasLayer(layer)) {
789
-
790
- let options = layer.options;
791
-
792
- options.color = color.line || color.area || theme;
793
- options.stroke = !!options.color;
794
-
795
- oldProto.call(this, ctx, layer);
796
-
797
- if (options.stroke && options.weight !== 0) {
798
- let oldVal = ctx.globalCompositeOperation || 'source-over';
799
- ctx.globalCompositeOperation = 'destination-over';
800
- ctx.strokeStyle = color.outline || '#FFF';
801
- ctx.lineWidth = options.weight * 1.75;
802
- ctx.stroke();
803
- ctx.globalCompositeOperation = oldVal;
804
- }
805
-
806
- } else {
807
- oldProto.call(this, ctx, layer);
808
- }
809
- }
810
- });
811
- },
812
-
813
- /**
814
- * Partial fix for initial tooltip size
815
- *
816
- * @link https://github.com/Raruto/leaflet-elevation/issues/81#issuecomment-713477050
817
- */
818
- _fixTooltipSize: function() {
819
- this.on('elechart_init', () =>
820
- this.once('elechart_change elechart_hover', ({data, xCoord}) => {
821
- if (this._chartEnabled) {
822
- this._chart._showDiagramIndicator(data, xCoord);
823
- this._chart._showDiagramIndicator(data, xCoord);
824
- }
825
- this._updateMarker(data);
826
- })
827
- );
828
- },
829
-
830
- /*
831
- * Finds a data entry for the given LatLng
832
- */
833
- _findItemForLatLng: function(latlng) {
834
- return this._data[this._chart._findIndexForLatLng(latlng)];
835
- },
836
-
837
- /*
838
- * Finds a data entry for the given xDiagCoord
839
- */
840
- _findItemForX: function(x) {
841
- return this._data[this._chart._findIndexForXCoord(x)];
842
- },
843
-
844
- /**
845
- * Fires an event of the specified type.
846
- */
847
- _fireEvt: function(type, data, propagate) {
848
- if (this.fire) this.fire(type, data, propagate);
849
- if (this._map) this._map.fire(type, data, propagate);
850
- },
851
-
852
- /*
853
- * Hides the position/height indicator marker drawn onto the map
854
- */
855
- _hideMarker: function() {
856
- if (this.options.autohideMarker) {
857
- this._marker.remove();
858
- }
859
- },
860
-
861
- /**
862
- * Generate "svg" chart (DOM element).
863
- */
864
- _initChart: function(container) {
865
- let opts = this.options;
866
- let map = this._map;
867
-
868
- if (opts.detached) {
869
- let { offsetWidth, offsetHeight} = this.eleDiv;
870
- if (offsetWidth > 0) opts.width = offsetWidth;
871
- if (offsetHeight > 20) opts.height = offsetHeight - 20; // 20 = horizontal scrollbar size.
872
- } else {
873
- let { clientWidth } = map.getContainer();
874
- opts._maxWidth = opts._maxWidth > opts.width ? opts._maxWidth : opts.width;
875
- this._container.style.maxWidth = opts._maxWidth + 'px';
876
- if (opts._maxWidth > clientWidth) opts.width = clientWidth - 30;
877
- }
878
-
879
- Promise.all([
880
- this.import(this.__D3),
881
- this.import(this.__LCHART)
882
- ]).then((m) => {
883
-
884
- let chart = this._chart = new (m[1] || Elevation).Chart(opts, this);
885
-
886
- this._x = this._chart._x;
887
- this._y = this._chart._y;
888
-
889
- d3
890
- .select(container)
891
- .call(chart.render());
892
-
893
- chart
894
- .on('reset_drag', this._hideMarker, this)
895
- .on('mouse_enter', this._onMouseEnter, this)
896
- .on('dragged', this._onDragEnd, this)
897
- .on('mouse_move', this._onMouseMove, this)
898
- .on('mouse_out', this._onMouseOut, this)
899
- .on('ruler_filter', this._onRulerFilter, this)
900
- .on('zoom', this._updateChart, this)
901
- .on('elepath_toggle', this._onToggleChart, this)
902
- .on('margins_updated', this._resizeChart, this);
903
-
904
-
905
- this.fire("elechart_init");
906
-
907
- map
908
- .on('zoom viewreset zoomanim', this._hideMarker, this)
909
- .on('resize', this._resetView, this)
910
- .on('resize', this._resizeChart, this)
911
- .on('rotate', this._rotateMarker, this)
912
- .on('mousedown', this._resetDrag, this);
913
-
914
- on(map.getContainer(), 'mousewheel', this._resetDrag, this);
915
- on(map.getContainer(), 'touchstart', this._resetDrag, this);
916
- on(document, 'keydown', this._onKeyDown, this);
917
-
918
- this
919
- .on('eledata_added eledata_loaded', this._updateChart, this)
920
- .on('eledata_added eledata_loaded', this._updateSummary, this);
921
-
922
- this._updateChart();
923
- this._updateSummary();
924
- });
925
-
926
-
927
- },
928
-
929
- _initLayer: function() {
930
- this._layers
931
- .on('layeradd layerremove', ({layer, type}) => {
932
- let node = layer.getElement && layer.getElement();
933
- toggleClass(node, this.options.polyline.className + ' ' + this.options.theme, type == 'layeradd');
934
- toggleEvent(layer, "mousemove", this._onMouseMoveLayer.bind(this), type == 'layeradd');
935
- toggleEvent(layer, "mouseout", this._onMouseOut.bind(this), type == 'layeradd');
936
- });
937
- },
938
-
939
- _initMarker: function(map) {
940
- let pane = map.getPane('elevationPane');
941
- if (!pane) {
942
- pane = this._pane = map.createPane('elevationPane', map.getPane('norotatePane') || map.getPane('mapPane'));
943
- pane.style.zIndex = 625; // This pane is above markers but below popups.
944
- pane.style.pointerEvents = 'none';
945
- }
946
-
947
- if (this._renderer) this._renderer.remove();
948
- this._renderer = L.svg({ pane: "elevationPane" }).addTo(this._map); // default leaflet svg renderer
949
-
950
- Promise.all([
951
- this.import(this.__D3),
952
- this.import(this.__LMARKER)
953
- ]).then((m) => {
954
- this._marker = new (m[1] || Elevation).Marker(this.options, this);
955
- this.fire("elechart_marker");
956
- });
957
- },
958
-
959
- /**
960
- * Inspired by L.Control.Layers
961
- */
962
- _initButton: function(container) {
963
- L.DomEvent
964
- .disableClickPropagation(container)
965
- .disableScrollPropagation(container);
966
-
967
- this.options.collapsed ? this._collapse() : this._expand();
968
-
969
- if (this.options.autohide) {
970
- on(container, 'mouseover', this._expand, this);
971
- on(container, 'mouseout', this._collapse, this);
972
- this._map.on('click', this._collapse, this);
973
- }
974
-
975
- if (this.options.closeBtn) {
976
- let link = this._button = create('a', "elevation-toggle-icon", { href: '#', title: L._('Elevation'), }, container);
977
- on(link, 'click', L.DomEvent.stop);
978
- on(link, 'click', this._toggle, this);
979
- on(link, 'focus', this._toggle, this);
980
- }
981
- },
982
-
983
- _initSummary: function(container) {
984
- this.import(this.__LSUMMARY).then((m)=>{
985
- this._summary = new (m || Elevation).Summary({ summary: this.options.summary }, this);
986
-
987
- this.on('elechart_init', () => {
988
- d3.select(container).call(this._summary.render());
989
- });
990
- });
991
- },
992
-
993
- /**
994
- * Retrieve data from a remote url (HTTP).
995
- */
996
- _loadFile: function(url) {
997
- fetch(url)
998
- .then((response) => response.text())
999
- .then((data) => {
1000
- this._downloadURL = url; // TODO: handle multiple urls?
1001
- this._parseFromString(data)
1002
- .then( geojson => geojson && this._loadLayer(geojson));
1003
- }).catch((err) => console.warn(err));
1004
- },
1005
-
1006
- /**
1007
- * Dynamically import only required javascript modules (code splitting)
1008
- */
1009
- _loadModules: function(handlers) {
1010
- // First map known classnames (eg. "Altitude" --> L.Control.Elevation.Altitude)
1011
- handlers = handlers.map((h) => typeof h === 'string' && typeof Elevation[h] !== "undefined" ? Elevation[h] : h);
1012
- // Then load optional classes and custom imports (eg. "Cadence" --> import('../src/handlers/cadence.js'))
1013
- let modules = handlers.map(file => (typeof file === 'string' && this.import(this.__modulesFolder + file.toLowerCase() + '.js')) || (file instanceof Promise && file) || Promise.resolve());
1014
- return Promise.all(modules).then((m) => {
1015
- each(m, (exported, i) => {
1016
- let fn = exported && Object.keys(exported)[0];
1017
- if (fn) {
1018
- handlers[i] = Elevation[fn] = (Elevation[fn] ?? exported[fn]);
1019
- }
1020
- });
1021
- each(handlers, h => ["function", "object"].includes(typeof h) && this._registerHandler(h));
1022
- });
1023
- },
1024
-
1025
- /**
1026
- * Simple GeoJSON data loader (L.GeoJSON).
1027
- */
1028
- _loadLayer: function(geojson) {
1029
- let { polyline, theme, waypoints, wptIcons, wptLabels, distanceMarkers } = this.options;
1030
- let style = L.extend({}, polyline);
1031
-
1032
- if (theme) {
1033
- style.className += ' ' + theme;
1034
- }
1035
-
1036
- if (geojson.name) {
1037
- this.track_info.name = geojson.name;
1038
- }
1039
-
1040
- let layer = L.geoJson(geojson, {
1041
- distanceMarkers: distanceMarkers,
1042
- style: style,
1043
- pointToLayer: (feature, latlng) => {
1044
- if (waypoints) {
1045
- let { desc, name, sym } = feature.properties;
1046
- desc = desc || '';
1047
- name = name || '';
1048
- // Handle chart waypoints (dots)
1049
- if ([true, 'dots'].includes(waypoints)) {
1050
- this._registerCheckPoint({
1051
- latlng: latlng,
1052
- label : ([true, 'dots'].includes(wptLabels) ? name : '')
1053
- });
1054
- }
1055
- // Handle map waypoints (markers)
1056
- if ([true, 'markers'].includes(waypoints) && wptIcons != false) {
1057
- return this._registerMarker({
1058
- latlng : latlng,
1059
- sym : (sym ?? name).replace(' ', '-').replace('"', '').replace("'", '').toLowerCase(),
1060
- content: [true, 'markers'].includes(wptLabels) && (name || desc) && decodeURI("<b>" + name + "</b>" + (desc.length > 0 ? '<br>' + desc : ''))
1061
- });
1062
- }
1063
- }
1064
- },
1065
- onEachFeature: (feature, layer) => feature.geometry && feature.geometry.type != 'Point' && this.addData(feature, layer),
1066
- });
1067
-
1068
- this.import(this.__D3).then(() => {
1069
- this._initMapIntegrations(layer);
1070
- this._fireEvt("eledata_loaded", { data: geojson, layer: layer, name: this.track_info.name, track_info: this.track_info });
1071
- });
1072
-
1073
- return layer;
1074
- },
1075
-
1076
- _onDragEnd: function({ dragstart, dragend}) {
1077
- this._hideMarker();
1078
- this.fitBounds(L.latLngBounds([dragstart.latlng, dragend.latlng]));
1079
-
1080
- this.fire("elechart_dragged");
1081
- },
1082
-
1083
- _onKeyDown: function({key}) {
1084
- if(!this.options.detached && key === "Escape"){
1085
- this._collapse();
1086
- } },
1087
-
1088
- /**
1089
- * Trigger mouseenter event.
1090
- */
1091
- _onMouseEnter: function() {
1092
- this.fire('elechart_enter');
1093
- },
1094
-
1095
- /*
1096
- * Handles the moueseover the chart and displays distance and altitude level.
1097
- */
1098
- _onMouseMove: function({xCoord}) {
1099
- if (this._chartEnabled && this._data.length) {
1100
- let item = this._findItemForX(xCoord);
1101
- if (item) {
1102
- if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord);
1103
-
1104
- this._updateMarker(item);
1105
- this._setMapView(item);
1106
-
1107
- if (this._map) {
1108
- addClass(this._map.getContainer(), 'elechart-hover');
1109
- }
1110
-
1111
- this.fire("elechart_change", { data: item, xCoord: xCoord });
1112
- this.fire("elechart_hover", { data: item, xCoord: xCoord });
1113
- }
1114
- }
1115
- },
1116
-
1117
- /*
1118
- * Handles mouseover events of the data layers on the map.
1119
- */
1120
- _onMouseMoveLayer: function({latlng}) {
1121
- if (this._data.length) {
1122
- let item = this._findItemForLatLng(latlng);
1123
- if (item) {
1124
- let xCoord = item.xDiagCoord;
1125
-
1126
- if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord);
1127
-
1128
- this._updateMarker(item);
1129
-
1130
- this.fire("elechart_change", { data: item, xCoord: xCoord });
1131
- }
1132
- }
1133
- },
1134
-
1135
- /*
1136
- * Handles the moueseout over the chart.
1137
- */
1138
- _onMouseOut: function() {
1139
- if (!this.options.detached) {
1140
- this._hideMarker();
1141
- this._chart._hideDiagramIndicator();
1142
- }
1143
-
1144
- if (this._map) {
1145
- removeClass(this._map.getContainer(), 'elechart-hover');
1146
- }
1147
-
1148
- this.fire("elechart_leave");
1149
- },
1150
-
1151
- /**
1152
- * Handles the drag event over the ruler filter.
1153
- */
1154
- _onRulerFilter: function({coords}) {
1155
- this._updateMapSegments(coords);
1156
- },
1157
-
1158
- /**
1159
- * Toggle chart data on legend click
1160
- */
1161
- _onToggleChart: function({ name, enabled }) {
1162
-
1163
- this._chartEnabled = this._chart._hasActiveLayers();
1164
-
1165
- // toggle layer visibility on empty chart
1166
- this._layers.eachLayer(layer => toggleClass(layer.getElement && layer.getElement(), this.options.polyline.className + ' ' + this.options.theme, this._chartEnabled));
1167
-
1168
- // toggle option value (eg. altitude = { 'disabled' || 'enabled' })
1169
- this.options[name] = !enabled && this.options[name] == 'disabled' ? 'enabled' : 'disabled';
1170
-
1171
- // remove marker on empty chart
1172
- if (!this._chartEnabled) {
1173
- this._chart._hideDiagramIndicator();
1174
- this._marker.remove();
1175
- }
1176
- },
1177
-
1178
- /**
1179
- * Simple GeoJSON Parser
1180
- */
1181
- _parseFromGeoJSONString: function(data) {
1182
- try {
1183
- return JSON.parse(data);
1184
- } catch (e) { }
1185
- },
1186
-
1187
- /**
1188
- * Attempt to parse raw response data (GeoJSON or XML > GeoJSON)
1189
- */
1190
- _parseFromString: function(data) {
1191
- return new Promise(resolve =>
1192
- this.import(this.__TOGEOJSON).then(() => {
1193
- let geojson;
1194
- try {
1195
- geojson = this._parseFromXMLString(data.trim());
1196
- } catch (e) {
1197
- geojson = this._parseFromGeoJSONString(data.toString());
1198
- }
1199
- if (geojson) {
1200
- geojson.name = geojson.name || (this._downloadURL || '').split('/').pop().split('#')[0].split('?')[0];
1201
- }
1202
- resolve(geojson);
1203
- })
1204
- );
1205
- },
1206
-
1207
- /**
1208
- * Simple XML Parser (GPX, KML, TCX)
1209
- */
1210
- _parseFromXMLString: function(data) {
1211
- if (data.indexOf("<") != 0) {
1212
- throw 'Invalid XML';
1213
- }
1214
- let xml = (new DOMParser()).parseFromString(data, "text/xml");
1215
- let type = xml.documentElement.tagName.toLowerCase(); // "kml" or "gpx"
1216
- let name = xml.getElementsByTagName('name');
1217
- if (xml.getElementsByTagName('parsererror').length) {
1218
- throw 'Invalid XML';
1219
- }
1220
- if (!(type in toGeoJSON)) {
1221
- type = xml.documentElement.tagName == "TrainingCenterDatabase" ? 'tcx' : 'gpx';
1222
- }
1223
- let geojson = toGeoJSON[type](xml);
1224
- geojson.name = name.length > 0 ? (Array.from(name).find(tag => tag.parentElement.tagName == "trk") ?? name[0]).textContent : '';
1225
- return geojson;
1226
- },
1227
-
1228
- /**
1229
- * Add chart profile to diagram
1230
- */
1231
- _registerAreaPath: function(props) {
1232
- this.on("elechart_init", () => this._chart._registerAreaPath(props));
1233
- },
1234
-
1235
- /**
1236
- * Add chart grid to diagram
1237
- */
1238
- _registerAxisGrid: function(props) {
1239
- this.on("elechart_axis", () => this._chart._registerAxisGrid(props));
1240
- },
1241
-
1242
- /**
1243
- * Add chart axis to diagram
1244
- */
1245
- _registerAxisScale: function(props) {
1246
- this.on("elechart_axis", () => this._chart._registerAxisScale(props));
1247
- },
1248
-
1249
- /**
1250
- * Add a point of interest over the diagram
1251
- */
1252
- _registerCheckPoint: function(props) {
1253
- this.on("elechart_updated", () => this._chart._registerCheckPoint(props));
1254
- },
1255
-
1256
-
1257
- /**
1258
- * Base handler for iterative track statistics (dist, time, z, slope, speed, acceleration, ...)
1259
- */
1260
- _registerDataAttribute: function(props) {
1261
-
1262
- // parse of "coordinateProperties" for later usage
1263
- if (props.coordPropsToMeta) {
1264
- this.on("elepoint_init", (e) => props.coordPropsToMeta.call(this, e));
1265
- }
1266
-
1267
- // prevent excessive variabile instanstations
1268
- let i, curr, prev, attr;
1269
-
1270
- // save here a reference to last used point
1271
- let lastValid = null;
1272
-
1273
- // iteration
1274
- this.on("elepoint_added", ({index, point}) => {
1275
- i = index;
1276
-
1277
- prev = curr ?? this._data[i]; // same as: this._data[i > 0 ? i - 1 : i]
1278
- curr = this._data[i];
1279
- attr = props.attr || props.name;
1280
-
1281
- // check and fix missing data on last added point
1282
- if (props.skipNull === false) {
1283
- let curr = this._data[i][attr];
1284
- if(i > 0) {
1285
- let prev = this._data[i - 1][attr];
1286
- if (isNaN(prev)) {
1287
- if (!isNaN(lastValid) && !isNaN(curr)) {
1288
- prev = (lastValid + curr) / 2;
1289
- } else if (!isNaN(lastValid)) {
1290
- prev = lastValid;
1291
- } else if (!isNaN(curr)) {
1292
- prev = curr;
1293
- }
1294
- if (!isNaN(prev)) return this._data.splice(i - 1, 1);
1295
- this._data[i - 1][attr] = prev;
1296
- }
1297
- }
1298
- // update reference to last used point (ie. if it has data)
1299
- if (!isNaN(curr)) {
1300
- lastValid = curr;
1301
- }
1302
- }
1303
-
1304
- // retrieve point value
1305
- curr[attr] = props.pointToAttr.call(this, point, i);
1306
-
1307
- // update "track_info" stats (min, max, avg, ...)
1308
- if (props.stats) {
1309
- for (const key in props.stats) {
1310
- let sname = (props.statsName || attr) + (key != '' ? '_' : '');
1311
- this.track_info[sname + key] = props.stats[key].call(this, curr[attr], this.track_info[sname + key], this._data.length);
1312
- }
1313
- }
1314
-
1315
- // Limit "crazy" delta values.
1316
- if (props.deltaMax) {
1317
- curr[attr] =wrapDelta(curr[attr], prev[attr], props.deltaMax);
1318
- }
1319
-
1320
- // Range of acceptable values.
1321
- if (props.clampRange) {
1322
- curr[attr] = clamp(curr[attr], props.clampRange);
1323
- }
1324
-
1325
- // Limit floating point precision.
1326
- if (!isNaN(props.decimals)) {
1327
- curr[attr] = round(curr[attr], props.decimals);
1328
- }
1329
-
1330
- // update here some mixins (eg. complex "track_info" stuff)
1331
- if(props.onPointAdded) props.onPointAdded.call(this, curr[attr], i, point);
1332
- });
1333
- },
1334
-
1335
- /**
1336
- * Parse a module definition and attach related function listeners
1337
- */
1338
- _registerHandler: function(props) {
1339
-
1340
- // eg. L.Control.Altitude
1341
- if (typeof props === "function") {
1342
- return this._registerHandler(props.call(this));
1343
- }
1344
-
1345
- let {
1346
- name,
1347
- attr,
1348
- required,
1349
- skipNull,
1350
- deltaMax,
1351
- clampRange,
1352
- decimals,
1353
- meta,
1354
- unit,
1355
- coordinateProperties,
1356
- coordPropsToMeta: coordPropsToMeta$1,
1357
- pointToAttr,
1358
- onPointAdded,
1359
- stats,
1360
- statsName,
1361
- grid,
1362
- scale,
1363
- path,
1364
- tooltip,
1365
- summary
1366
- } = props;
1367
-
1368
- // eg. "altitude" == true
1369
- if (this.options[name] || required) {
1370
-
1371
- this._registerDataAttribute({
1372
- name,
1373
- attr,
1374
- skipNull,
1375
- deltaMax,
1376
- clampRange,
1377
- decimals,
1378
- coordPropsToMeta: coordPropsToMeta(coordinateProperties, meta || name, coordPropsToMeta$1),
1379
- pointToAttr,
1380
- onPointAdded,
1381
- stats,
1382
- statsName,
1383
- });
1384
-
1385
- if (grid) {
1386
- this._registerAxisGrid(L.extend({ name }, grid));
1387
- }
1388
-
1389
- if (this.options[name] !== "summary") {
1390
- if (scale) this._registerAxisScale(L.extend({ name, label: unit }, scale));
1391
- if (path) this._registerAreaPath(L.extend({ name }, path));
1392
- }
1393
-
1394
- if (tooltip || props.tooltips) {
1395
- each([tooltip, ...(props.tooltips || [])], t => t && this._registerTooltip(L.extend({ name }, t)));
1396
- }
1397
-
1398
- if (summary) {
1399
- each(summary, (s, k) => summary[k] = L.extend({ unit }, s));
1400
- this._registerSummary(summary);
1401
- }
1402
- }
1403
- },
1404
-
1405
- _registerMarker({latlng, sym, content}) {
1406
- let { wptIcons } = this.options;
1407
- // generate and cache appropriate icon symbol
1408
- if (!wptIcons.hasOwnProperty(sym)) {
1409
- wptIcons[sym] = L.divIcon(L.extend({}, wptIcons[""].options, { html: '<i class="elevation-waypoint-icon ' + sym + '"></i>' } ));
1410
- }
1411
- let marker = L.marker(latlng, { icon: wptIcons[sym] });
1412
- if (content) {
1413
- marker.bindPopup(content, { className: 'elevation-popup', keepInView: true }).openPopup();
1414
- marker.bindTooltip(content, { className: 'elevation-tooltip', direction: 'auto', sticky: true, opacity: 1 }).openTooltip();
1415
- }
1416
- return this._addMarker(marker)
1417
- },
1418
-
1419
- /**
1420
- * Add chart or marker tooltip info
1421
- */
1422
- _registerTooltip: function(props) {
1423
- props.chart && this.on("elechart_init", () => this._chart._registerTooltip(L.extend({}, props, { value: props.chart })));
1424
- props.marker && this.on("elechart_marker", () => this._marker._registerTooltip(L.extend({}, props, { value: props.marker })));
1425
- },
1426
-
1427
- /**
1428
- * Add summary info to diagram
1429
- */
1430
- _registerSummary: function(props) {
1431
- this.on('elechart_summary', () => this._summary._registerSummary(props));
1432
- },
1433
-
1434
- /*
1435
- * Removes the drag rectangle and zoms back to the total extent of the data.
1436
- */
1437
- _resetDrag: function() {
1438
- this._chart._resetDrag();
1439
- this._hideMarker();
1440
- },
1441
-
1442
- /**
1443
- * Resets drag, marker and bounds.
1444
- */
1445
- _resetView: function() {
1446
- if (this._map && this._map._isFullscreen) return;
1447
- this._resetDrag();
1448
- this._hideMarker();
1449
- if (this.options.autofitBounds) {
1450
- this.fitBounds();
1451
- }
1452
- },
1453
-
1454
- /**
1455
- * Hacky way for handling chart resize. Deletes it and redraw chart.
1456
- */
1457
- _resizeChart: function() {
1458
- if (this._container && style(this._container, "display") != "none") {
1459
- let opts = this.options;
1460
- let newWidth = opts.detached ? (this.eleDiv || this._container).offsetWidth : clamp(opts._maxWidth, [0, this._map.getContainer().clientWidth - 30]);
1461
- if (newWidth) {
1462
- opts.width = newWidth;
1463
- if (this._chart && this._chart._chart) {
1464
- this._chart._chart._resize(opts);
1465
- this._updateChart();
1466
- }
1467
- }
1468
- this._updateMapSegments();
1469
- }
1470
- },
1471
-
1472
- /**
1473
- * Collapse or Expand chart control.
1474
- */
1475
- _toggle: function() {
1476
- hasClass(this._container, "elevation-expanded") ? this._collapse() : this._expand();
1477
- },
1478
-
1479
- /**
1480
- * Update map center and zoom (followMarker: true)
1481
- */
1482
- _setMapView: function(item) {
1483
- if (this._map && this.options.followMarker) {
1484
- let zoom = this._map.getZoom();
1485
- let z = this.options.zFollow;
1486
- if (typeof z === "number") {
1487
- this._map.setView(item.latlng, (zoom < z ? z : zoom), { animate: true, duration: 0.25 });
1488
- } else if (!this._map.getBounds().contains(item.latlng)) {
1489
- this._map.setView(item.latlng, zoom, { animate: true, duration: 0.25 });
1490
- }
1491
- }
1492
- },
1493
-
1494
- /**
1495
- * Calculates [x, y] domain and then update chart.
1496
- */
1497
- _updateChart: function() {
1498
- if (this._chart && this._container) {
1499
- this.fire("elechart_axis");
1500
-
1501
- this._chart.update({ data: this._data, options: this.options });
1502
-
1503
- this._x = this._chart._x;
1504
- this._y = this._chart._y;
1505
-
1506
- this.fire('elechart_updated');
1507
- }
1508
- },
1509
-
1510
- /*
1511
- * Update the position/height indicator marker drawn onto the map
1512
- */
1513
- _updateMarker: function(item) {
1514
- if (this._marker) {
1515
- this._marker.update({
1516
- map : this._map,
1517
- item : item,
1518
- yCoordMax : this._yCoordMax || 0,
1519
- options : this.options
1520
- });
1521
- }
1522
- },
1523
-
1524
- /**
1525
- * Fix marker rotation on rotated maps
1526
- */
1527
- _rotateMarker: function() {
1528
- if (this._marker) {
1529
- this._marker.update();
1530
- }
1531
- },
1532
-
1533
- /**
1534
- * Highlight track segments on the map.
1535
- */
1536
- _updateMapSegments: function(coords) {
1537
- this._markedSegments.setLatLngs(coords || []);
1538
- if (coords && this._map && !this._map.hasLayer(this._markedSegments)) {
1539
- this._markedSegments.addTo(this._map);
1540
- }
1541
- },
1542
-
1543
- /**
1544
- * Update chart summary.
1545
- */
1546
- _updateSummary: function() {
1547
- if (this._summary) {
1548
- this._summary.reset();
1549
- if (this.options.summary) {
1550
- this.fire("elechart_summary");
1551
- this._summary.update();
1552
- }
1553
- if (this.options.downloadLink && this._downloadURL) { // TODO: generate dynamically file content instead of using static file urls.
1554
- this._summary._container.innerHTML += '<span class="download"><a href="#">' + L._('Download') + '</a></span>';
1555
- select('.download a', this._summary._container).onclick = (e) => {
1556
- e.preventDefault();
1557
- let event = { downloadLink: this.options.downloadLink, confirm: saveFile.bind(this, this._downloadURL) };
1558
- if (this.options.downloadLink == 'modal' && typeof CustomEvent === "function") {
1559
- document.dispatchEvent(new CustomEvent("eletrack_download", { detail: event }));
1560
- } else if (this.options.downloadLink == 'link' || this.options.downloadLink === true) {
1561
- event.confirm();
1562
- }
1563
- this.fire('eletrack_download', event);
1564
- };
1565
- }
1566
- }
1567
- },
1568
-
1569
-
1570
- /**
1571
- * Calculates chart width.
1572
- */
1573
- _width: function() {
1574
- if (this._chart) return this._chart._width();
1575
- const { width, margins } = this.options;
1576
- return width - margins.left - margins.right;
1577
- },
1578
-
1579
- /**
1580
- * Calculates chart height.
1581
- */
1582
- _height: function() {
1583
- if (this._chart) return this._chart._height();
1584
- const { height, margins } = this.options;
1585
- return height - margins.top - margins.bottom;
1586
- },
1587
-
288
+ // "leaflet-i18n" fallback
289
+ if (!L._ || !L.i18n) {
290
+ L._ = L.i18n = (string, data) => string;
291
+ }
292
+
293
+ // Prevent CORS issues for relative locations (dynamic import)
294
+ const baseURL = ((document.currentScript && document.currentScript.src) || (({ url: (typeof document === 'undefined' && typeof location === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : typeof document === 'undefined' ? location.href : (document.currentScript && document.currentScript.src || new URL('leaflet-elevation.js', document.baseURI).href)) }) && (typeof document === 'undefined' && typeof location === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : typeof document === 'undefined' ? location.href : (document.currentScript && document.currentScript.src || new URL('leaflet-elevation.js', document.baseURI).href)))).split("/").slice(0,-1).join("/") + '/';
295
+
296
+ const Elevation = L.Control.Elevation = L.Control.extend({
297
+
298
+ includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
299
+
300
+ options: Options,
301
+ __mileFactor: 0.621371, // 1 km = (0.621371 mi)
302
+ __footFactor: 3.28084, // 1 m = (3.28084 ft)
303
+ __D3: 'https://unpkg.com/d3@6.5.0/dist/d3.min.js',
304
+ __TOGEOJSON: 'https://unpkg.com/@tmcw/togeojson@4.6.0/dist/togeojson.umd.js',
305
+ __LGEOMUTIL: 'https://unpkg.com/leaflet-geometryutil@0.9.3/src/leaflet.geometryutil.js',
306
+ __LALMOSTOVER: 'https://unpkg.com/leaflet-almostover@1.0.1/src/leaflet.almostover.js',
307
+ __LHOTLINE: '../libs/leaflet-hotline.min.js',
308
+ __LDISTANCEM: '../libs/leaflet-distance-marker.min.js',
309
+ __LCHART: '../src/components/chart.js',
310
+ __LMARKER: '../src/components/marker.js',
311
+ __LSUMMARY: '../src/components/summary.js',
312
+ __modulesFolder: '../src/handlers/',
313
+ __btnIcon: '../images/elevation.svg',
314
+
315
+ /*
316
+ * Add data to the diagram either from GPX or GeoJSON and update the axis domain and data
317
+ */
318
+ addData: function(d, layer) {
319
+ this.import(this.__D3)
320
+ .then(() => {
321
+ if (this._modulesLoaded) {
322
+ layer = layer ?? (d.on && d);
323
+ this._addData(d);
324
+ this._addLayer(layer);
325
+ this._fireEvt("eledata_added", { data: d, layer: layer, track_info: this.track_info });
326
+ } else {
327
+ this.once('modules_loaded', () => this.addData(d,layer));
328
+ }
329
+ });
330
+ },
331
+
332
+ /**
333
+ * Adds the control to the given map.
334
+ */
335
+ addTo: function(map) {
336
+ if (this.options.detached) {
337
+ let parent = select(this.options.elevationDiv);
338
+ let eleDiv = this.onAdd(map);
339
+ parent ? append(parent, eleDiv) : insert(map.getContainer(), eleDiv, 'afterend');
340
+ } else {
341
+ L.Control.prototype.addTo.call(this, map);
342
+ }
343
+ return this;
344
+ },
345
+
346
+ /*
347
+ * Reset data and display
348
+ */
349
+ clear: function() {
350
+ if (this._marker) this._marker.remove();
351
+ if (this._chart) this._clearChart();
352
+ if (this._layers) this._clearLayers(this._layers);
353
+ if (this._markers) this._clearLayers(this._markers);
354
+ if (this._circleMarkers) this._circleMarkers.remove();
355
+ if (this._hotline) this._clearLayers(this._hotline);
356
+
357
+ this._data = [];
358
+ this.track_info = {};
359
+
360
+ this._fireEvt("eledata_clear");
361
+
362
+ this._updateChart();
363
+ },
364
+
365
+ _clearChart: function() {
366
+ if (this._events && this._events.elechart_updated) {
367
+ this._events.elechart_updated.forEach(({fn, ctx}) => this.off('elechart_updated', fn, ctx));
368
+ }
369
+ if (this._chart && this._chart._container) {
370
+ this._chart._container.selectAll('g.point .point').remove();
371
+ this._chart.clear();
372
+ }
373
+ },
374
+
375
+ _clearLayers: function(l) {
376
+ l = l || this._layers;
377
+ if (l && l.eachLayer) {
378
+ l.eachLayer(f => f.remove());
379
+ l.clearLayers();
380
+ }
381
+ },
382
+
383
+ /**
384
+ * TODO: Create a base class to handle custom data attributes (heart rate, cadence, temperature, ...)
385
+ *
386
+ * @link https://leafletjs.com/examples/extending/extending-3-controls.html#handlers
387
+ */
388
+ // addHandler: function (name, HandlerClass) {
389
+ // if (HandlerClass) {
390
+ // let handler = this[name] = new HandlerClass(this);
391
+ // this.handlers.push(handler);
392
+ // if (this.options[name]) {
393
+ // handler.enable();
394
+ // }
395
+ // }
396
+ // return this;
397
+ // },
398
+
399
+ /**
400
+ * Disable chart brushing.
401
+ */
402
+ disableBrush: function() {
403
+ this._chart._brushEnabled = false;
404
+ this._resetDrag();
405
+ },
406
+
407
+ /**
408
+ * Enable chart brushing.
409
+ */
410
+ enableBrush: function() {
411
+ this._chart._brushEnabled = true;
412
+ },
413
+
414
+ /**
415
+ * Disable chart zooming.
416
+ */
417
+ disableZoom: function() {
418
+ this._chart._zoomEnabled = false;
419
+ this._chart._resetZoom();
420
+ },
421
+
422
+ /**
423
+ * Enable chart zooming.
424
+ */
425
+ enableZoom: function() {
426
+ this._chart._zoomEnabled = true;
427
+ },
428
+
429
+ /**
430
+ * Sets a map view that contains the given geographical bounds.
431
+ */
432
+ fitBounds: function(bounds) {
433
+ bounds = bounds || this.getBounds();
434
+ if (this._map && bounds.isValid()) this._map.fitBounds(bounds);
435
+ },
436
+
437
+ getBounds: function(data) {
438
+ return L.latLngBounds((data || this._data).map((d) => d.latlng));
439
+ },
440
+
441
+ /**
442
+ * Get default zoom level (followMarker: true).
443
+ */
444
+ getZFollow: function() {
445
+ return this.options.zFollow;
446
+ },
447
+
448
+ /**
449
+ * Hide current elevation chart profile.
450
+ */
451
+ hide: function() {
452
+ style(this._container, "display", "none");
453
+ },
454
+
455
+ /**
456
+ * Initialize chart control "options" and "container".
457
+ */
458
+ initialize: function(opts) {
459
+
460
+ opts = L.setOptions(this, opts);
461
+
462
+ this._data = [];
463
+ this._layers = L.featureGroup();
464
+ this._markers = L.featureGroup();
465
+ this._hotline = L.featureGroup();
466
+ this._circleMarkers = L.featureGroup();
467
+ this._markedSegments = L.polyline([]);
468
+ this._start = (opts.trkStart || Options.trkStart);
469
+ this._end = (opts.trkEnd || Options.trkEnd);
470
+ this._chartEnabled = true;
471
+ this._yCoordMax = -Infinity;
472
+ this.track_info = {};
473
+ // this.handlers = [];
474
+
475
+ if (opts.followMarker) this._setMapView = throttle(this._setMapView, 300, this);
476
+ if (opts.legend) opts.margins.bottom += 30;
477
+ if (opts.theme) opts.polylineSegments.className += ' ' + opts.theme;
478
+ if (opts.wptIcons === true) opts.wptIcons = Options.wptIcons;
479
+ if (opts.distanceMarkers === true) opts.distanceMarkers = Options.distanceMarkers;
480
+ if (opts.trkStart) this._start.addTo(this._circleMarkers);
481
+ if (opts.trkEnd) this._end.addTo(this._circleMarkers);
482
+
483
+
484
+ this._markedSegments.setStyle(opts.polylineSegments);
485
+
486
+ // Leaflet canvas renderer colors
487
+ L.extend(Colors, opts.colors || {});
488
+
489
+ // Various stuff
490
+ this._fixCanvasPaths();
491
+ this._fixTooltipSize();
492
+
493
+ },
494
+
495
+ /**
496
+ * Javascript scripts downloader (lazy loader)
497
+ */
498
+ import: function(src, condition) {
499
+ switch(src) {
500
+ case this.__D3: condition = typeof d3 !== 'object'; break;
501
+ case this.__TOGEOJSON: condition = typeof toGeoJSON !== 'object'; break;
502
+ case this.__LGEOMUTIL: condition = typeof L.GeometryUtil !== 'object'; break;
503
+ case this.__LALMOSTOVER: condition = typeof L.Handler.AlmostOver !== 'function'; break;
504
+ case this.__LDISTANCEM: condition = typeof L.DistanceMarkers !== 'function'; break;
505
+ case this.__LHOTLINE: condition = typeof L.Hotline !== 'function'; break;
506
+ }
507
+ src = (src.startsWith('../') || src.startsWith('./') ? baseURL : '') + src;
508
+ return condition !== false ? import(src) : Promise.resolve();
509
+ },
510
+
511
+ /**
512
+ * Load elevation data (GPX, GeoJSON, KML or TCX).
513
+ */
514
+ load: function(data) {
515
+ this._parseFromString(data).then( geojson => geojson ? this._loadLayer(geojson) : this._loadFile(data));
516
+ },
517
+
518
+ /**
519
+ * Create container DOM element and related event listeners.
520
+ * Called on control.addTo(map).
521
+ */
522
+ onAdd: function(map) {
523
+ this._map = map;
524
+
525
+ let container = this._container = create("div", "elevation-control " + this.options.theme + " " + (this.options.detached ? 'elevation-detached' : 'leaflet-control'), this.options.detached ? { id: 'elevation-' + randomId() } : {});
526
+
527
+ if (!this.eleDiv) this.eleDiv = container;
528
+
529
+ this._loadModules(this.options.handlers).then(() => { // Inject here required modules (data handlers)
530
+ this._initChart(container);
531
+ this._initButton(container);
532
+ this._initSummary(container);
533
+ this._initMarker(map);
534
+ this._initLayer(map);
535
+ this._modulesLoaded = true;
536
+ this.fire('modules_loaded');
537
+ });
538
+
539
+ this.fire('add');
540
+
541
+ return container;
542
+ },
543
+
544
+ /**
545
+ * Clean up control code and related event listeners.
546
+ * Called on control.remove().
547
+ */
548
+ onRemove: function(map) {
549
+ this._container = null;
550
+
551
+ map
552
+ .off('zoom viewreset zoomanim', this._hideMarker, this)
553
+ .off('resize', this._resetView, this)
554
+ .off('resize', this._resizeChart, this)
555
+ .off('mousedown', this._resetDrag, this);
556
+
557
+ off(map.getContainer(), 'mousewheel', this._resetDrag, this);
558
+ off(map.getContainer(), 'touchstart', this._resetDrag, this);
559
+ off(document, 'keydown', this._onKeyDown, this);
560
+
561
+ this
562
+ .off('eledata_added eledata_loaded', this._updateChart, this)
563
+ .off('eledata_added eledata_loaded', this._updateSummary, this);
564
+
565
+ this.fire('remove');
566
+ },
567
+
568
+ /**
569
+ * Redraws the chart control. Sometimes useful after screen resize.
570
+ */
571
+ redraw: function() {
572
+ this._resizeChart();
573
+ },
574
+
575
+ /**
576
+ * Set default zoom level (followMarker: true).
577
+ */
578
+ setZFollow: function(zoom) {
579
+ this.options.zFollow = zoom;
580
+ },
581
+
582
+ /**
583
+ * Hide current elevation chart profile.
584
+ */
585
+ show: function() {
586
+ style(this._container, "display", "block");
587
+ },
588
+
589
+ /*
590
+ * Parsing data either from GPX or GeoJSON and update the diagram data
591
+ */
592
+ _addData: function(d) {
593
+ if (!d) {
594
+ return;
595
+ }
596
+
597
+ // Standard GeoJSON
598
+ if (d.type === "FeatureCollection" ) {
599
+ return each(d.features, feature => this._addData(feature));
600
+ } else if (d.type === "Feature") {
601
+ let geom = d.geometry;
602
+ if (geom) {
603
+ switch (geom.type) {
604
+ case 'LineString': return this._addGeoJSONData(geom.coordinates, d.properties);
605
+ case 'MultiLineString': return each(geom.coordinates, (coords, i) => this._addGeoJSONData(coords, d.properties, i));
606
+ case 'Point':
607
+ default: return console.warn('Unsopperted GeoJSON feature geometry type:' + geom.type);
608
+ }
609
+ }
610
+ }
611
+
612
+ // Fallback for leaflet layers (eg. L.Gpx)
613
+ if (d._latlngs) {
614
+ return this._addGeoJSONData(d._latlngs, d.feature && d.feature.properties);
615
+ }
616
+
617
+ },
618
+
619
+ /*
620
+ * Parsing of GeoJSON data lines and their elevation in z-coordinate
621
+ */
622
+ _addGeoJSONData: function(coords, properties, nestingLevel) {
623
+
624
+ // "coordinateProperties" property is generated inside "@tmcw/toGeoJSON"
625
+ let props = (properties && properties.coordinateProperties) || properties;
626
+
627
+ coords.forEach((point, i) => {
628
+
629
+ // GARMIN_EXTENSIONS = ["hr", "cad", "atemp", "wtemp", "depth", "course", "bearing"];
630
+ point.meta = point.meta ?? { time: null, ele: null };
631
+
632
+ point.prev = (attr) => (attr ? this._data[i > 0 ? i - 1 : 0][attr] : this._data[i > 0 ? i - 1 : 0]);
633
+
634
+ this.fire("elepoint_init", { point: point, props: props, id: i, isMulti: nestingLevel });
635
+
636
+ this._addPoint(
637
+ point.lat ?? point[1],
638
+ point.lng ?? point[0],
639
+ point.alt ?? point.meta.ele ?? point[2]
640
+ );
641
+
642
+ this.fire("elepoint_added", { point: point, index: this._data.length - 1 });
643
+
644
+ if (this._yCoordMax < this._data[this._data.length - 1][this.options.yAttr]) this._yCoordMax = this._data[this._data.length - 1][this.options.yAttr];
645
+ });
646
+
647
+ this.fire("eletrack_added", { coords: coords, index: this._data.length - 1 });
648
+ },
649
+
650
+ /*
651
+ * Parse and push a single (x, y, z) point to current elevation profile.
652
+ */
653
+ _addPoint: function(x, y, z) {
654
+ if (this.options.reverseCoords) {
655
+ [x, y] = [y, x];
656
+ }
657
+
658
+ this._data.push({
659
+ x: x,
660
+ y: y,
661
+ z: z,
662
+ latlng: L.latLng(x, y, z)
663
+ });
664
+
665
+ this.fire("eledata_updated", { index: this._data.length - 1 });
666
+ },
667
+
668
+ _addLayer: function(layer) {
669
+ if (layer) this._layers.addLayer(layer);
670
+ // Postpone adding the distance markers (lazy: true)
671
+ if (layer && this.options.distanceMarkers && this.options.distanceMarkers.lazy) {
672
+ layer.on('add remove', ({target, type}) => L.DistanceMarkers && target instanceof L.Polyline && target[type + 'DistanceMarkers']());
673
+ }
674
+ return layer;
675
+ },
676
+
677
+ _addMarker: function(marker) {
678
+ if (marker) this._markers.addLayer(marker);
679
+ return marker;
680
+ },
681
+
682
+ /**
683
+ * Initialize "L.AlmostOver" integration
684
+ */
685
+ _initAlmostOverHandler: function(map, layer) {
686
+ return (map && this.options.almostOver && !L.Browser.mobile) ? Promise.all([
687
+ this.import(this.__LGEOMUTIL),
688
+ this.import(this.__LALMOSTOVER)
689
+ ]).then(() => {
690
+ map.addHandler('almostOver', L.Handler.AlmostOver);
691
+ if (L.GeometryUtil && map.almostOver && map.almostOver.enabled()) {
692
+ map.almostOver.addLayer(layer);
693
+ map
694
+ .on('almost:move', (e) => this._onMouseMoveLayer(e))
695
+ .on('almost:out', (e) => this._onMouseOut(e));
696
+ }
697
+ }) : Promise.resolve();
698
+ },
699
+
700
+ /**
701
+ * Initialize "L.DistanceMarkers" integration
702
+ */
703
+ _initDistanceMarkers: function() {
704
+ return this.options.distanceMarkers ? Promise.all([this.import(this.__LGEOMUTIL), this.import(this.__LDISTANCEM)]) : Promise.resolve();
705
+ },
706
+
707
+ _initHotLine: function(layer) {
708
+ let prop = typeof this.options.hotline == 'string' ? this.options.hotline : 'elevation';
709
+ return this.options.hotline ? this.import(this.__LHOTLINE)
710
+ .then(() => {
711
+ layer.eachLayer((trkseg) => {
712
+ if(trkseg.feature.geometry.type != "Point") {
713
+ let geo = L.geoJson(trkseg.toGeoJSON(), { coordsToLatLng: (coords) => L.latLng(coords[0], coords[1], coords[2] * (this.options.altitudeFactor || 1))});
714
+ let line = L.hotline(geo.toGeoJSON().features[0].geometry.coordinates, {
715
+ min: isFinite(this.track_info[prop + '_min']) ? this.track_info[prop + '_min'] : 0,
716
+ max: isFinite(this.track_info[prop + '_max']) ? this.track_info[prop + '_max'] : 1,
717
+ palette: {
718
+ 0.0: '#008800',
719
+ 0.5: '#ffff00',
720
+ 1.0: '#ff0000'
721
+ },
722
+ weight: 5,
723
+ outlineColor: '#000000',
724
+ outlineWidth: 1
725
+ }).addTo(this._hotline);
726
+ let alpha = trkseg.options.style && trkseg.options.style.opacity || 1;
727
+ trkseg.on('add remove', ({type}) => {
728
+ trkseg.setStyle({opacity: (type == 'add' ? 0 : alpha)});
729
+ line[(type == 'add' ? 'addTo' : 'removeFrom')](trkseg._map);
730
+ if (line._renderer) line._renderer._container.parentElement.insertBefore(line._renderer._container, line._renderer._container.parentElement.firstChild);
731
+ });
732
+ }
733
+ });
734
+ }) : Promise.resolve();
735
+ },
736
+
737
+ /**
738
+ * Initialize "L.AlmostOver" and "L.DistanceMarkers"
739
+ */
740
+ _initMapIntegrations: function(layer) {
741
+ let map = this._map;
742
+ if (map) {
743
+ if (this._data.length) {
744
+ this._start.setLatLng(this._data[0].latlng);
745
+ this._end.setLatLng(this._data[this._data.length -1].latlng);
746
+ }
747
+ Promise.all([
748
+ this._initHotLine(layer),
749
+ this._initAlmostOverHandler(map, layer),
750
+ this._initDistanceMarkers(),
751
+ ]).then(() => {
752
+ if (this.options.polyline) {
753
+ layer.addTo(map);
754
+ this._circleMarkers.addTo(map);
755
+ }
756
+ if (this.options.autofitBounds) {
757
+ this.fitBounds(layer.getBounds());
758
+ }
759
+ map.invalidateSize();
760
+ });
761
+ } else {
762
+ this.once('add', () => this._initMapIntegrations(layer));
763
+ }
764
+ },
765
+
766
+ /*
767
+ * Collapse current chart control.
768
+ */
769
+ _collapse: function() {
770
+ replaceClass(this._container, 'elevation-expanded', 'elevation-collapsed');
771
+ if (this._map) this._map.invalidateSize();
772
+ },
773
+
774
+ /*
775
+ * Expand current chart control.
776
+ */
777
+ _expand: function() {
778
+ replaceClass(this._container, 'elevation-collapsed', 'elevation-expanded');
779
+ if (this._map) this._map.invalidateSize();
780
+ },
781
+
782
+ /**
783
+ * Add some basic colors to leaflet canvas renderer (preferCanvas: true).
784
+ */
785
+ _fixCanvasPaths: function() {
786
+ let oldProto = L.Canvas.prototype._fillStroke;
787
+ let control = this;
788
+
789
+ let theme = this.options.theme.split(' ')[0].replace('-theme', '');
790
+ let color = Colors[theme] || {};
791
+
792
+ L.Canvas.include({
793
+ _fillStroke: function(ctx, layer) {
794
+ if (control._layers.hasLayer(layer)) {
795
+
796
+ let options = layer.options;
797
+
798
+ options.color = color.line || color.area || theme;
799
+ options.stroke = !!options.color;
800
+
801
+ oldProto.call(this, ctx, layer);
802
+
803
+ if (options.stroke && options.weight !== 0) {
804
+ let oldVal = ctx.globalCompositeOperation || 'source-over';
805
+ ctx.globalCompositeOperation = 'destination-over';
806
+ ctx.strokeStyle = color.outline || '#FFF';
807
+ ctx.lineWidth = options.weight * 1.75;
808
+ ctx.stroke();
809
+ ctx.globalCompositeOperation = oldVal;
810
+ }
811
+
812
+ } else {
813
+ oldProto.call(this, ctx, layer);
814
+ }
815
+ }
816
+ });
817
+ },
818
+
819
+ /**
820
+ * Partial fix for initial tooltip size
821
+ *
822
+ * @link https://github.com/Raruto/leaflet-elevation/issues/81#issuecomment-713477050
823
+ */
824
+ _fixTooltipSize: function() {
825
+ this.on('elechart_init', () =>
826
+ this.once('elechart_change elechart_hover', ({data, xCoord}) => {
827
+ if (this._chartEnabled) {
828
+ this._chart._showDiagramIndicator(data, xCoord);
829
+ this._chart._showDiagramIndicator(data, xCoord);
830
+ }
831
+ this._updateMarker(data);
832
+ })
833
+ );
834
+ },
835
+
836
+ /*
837
+ * Finds a data entry for the given LatLng
838
+ */
839
+ _findItemForLatLng: function(latlng) {
840
+ return this._data[this._chart._findIndexForLatLng(latlng)];
841
+ },
842
+
843
+ /*
844
+ * Finds a data entry for the given xDiagCoord
845
+ */
846
+ _findItemForX: function(x) {
847
+ return this._data[this._chart._findIndexForXCoord(x)];
848
+ },
849
+
850
+ /**
851
+ * Fires an event of the specified type.
852
+ */
853
+ _fireEvt: function(type, data, propagate) {
854
+ if (this.fire) this.fire(type, data, propagate);
855
+ if (this._map) this._map.fire(type, data, propagate);
856
+ },
857
+
858
+ /*
859
+ * Hides the position/height indicator marker drawn onto the map
860
+ */
861
+ _hideMarker: function() {
862
+ if (this.options.autohideMarker) {
863
+ this._marker.remove();
864
+ }
865
+ },
866
+
867
+ /**
868
+ * Generate "svg" chart (DOM element).
869
+ */
870
+ _initChart: function(container) {
871
+ let opts = this.options;
872
+ let map = this._map;
873
+
874
+ if (opts.detached) {
875
+ let { offsetWidth, offsetHeight} = this.eleDiv;
876
+ if (offsetWidth > 0) opts.width = offsetWidth;
877
+ if (offsetHeight > 20) opts.height = offsetHeight - 20; // 20 = horizontal scrollbar size.
878
+ } else {
879
+ let { clientWidth } = map.getContainer();
880
+ opts._maxWidth = opts._maxWidth > opts.width ? opts._maxWidth : opts.width;
881
+ this._container.style.maxWidth = opts._maxWidth + 'px';
882
+ if (opts._maxWidth > clientWidth) opts.width = clientWidth - 30;
883
+ }
884
+
885
+ Promise.all([
886
+ this.import(this.__D3),
887
+ this.import(this.__LCHART)
888
+ ]).then((m) => {
889
+
890
+ let chart = this._chart = new (m[1] || Elevation).Chart(opts, this);
891
+
892
+ this._x = this._chart._x;
893
+ this._y = this._chart._y;
894
+
895
+ d3
896
+ .select(container)
897
+ .call(chart.render());
898
+
899
+ chart
900
+ .on('reset_drag', this._hideMarker, this)
901
+ .on('mouse_enter', this._onMouseEnter, this)
902
+ .on('dragged', this._onDragEnd, this)
903
+ .on('mouse_move', this._onMouseMove, this)
904
+ .on('mouse_out', this._onMouseOut, this)
905
+ .on('ruler_filter', this._onRulerFilter, this)
906
+ .on('zoom', this._updateChart, this)
907
+ .on('elepath_toggle', this._onToggleChart, this)
908
+ .on('margins_updated', this._resizeChart, this);
909
+
910
+
911
+ this.fire("elechart_init");
912
+
913
+ map
914
+ .on('zoom viewreset zoomanim', this._hideMarker, this)
915
+ .on('resize', this._resetView, this)
916
+ .on('resize', this._resizeChart, this)
917
+ .on('rotate', this._rotateMarker, this)
918
+ .on('mousedown', this._resetDrag, this);
919
+
920
+ on(map.getContainer(), 'mousewheel', this._resetDrag, this);
921
+ on(map.getContainer(), 'touchstart', this._resetDrag, this);
922
+ on(document, 'keydown', this._onKeyDown, this);
923
+
924
+ this
925
+ .on('eledata_added eledata_loaded', this._updateChart, this)
926
+ .on('eledata_added eledata_loaded', this._updateSummary, this);
927
+
928
+ this._updateChart();
929
+ this._updateSummary();
930
+ });
931
+
932
+
933
+ },
934
+
935
+ _initLayer: function() {
936
+ this._layers
937
+ .on('layeradd layerremove', ({layer, type}) => {
938
+ let node = layer.getElement && layer.getElement();
939
+ toggleClass(node, this.options.polyline.className + ' ' + this.options.theme, type == 'layeradd');
940
+ toggleEvent(layer, "mousemove", this._onMouseMoveLayer.bind(this), type == 'layeradd');
941
+ toggleEvent(layer, "mouseout", this._onMouseOut.bind(this), type == 'layeradd');
942
+ });
943
+ },
944
+
945
+ _initMarker: function(map) {
946
+ let pane = map.getPane('elevationPane');
947
+ if (!pane) {
948
+ pane = this._pane = map.createPane('elevationPane', map.getPane('norotatePane') || map.getPane('mapPane'));
949
+ pane.style.zIndex = 625; // This pane is above markers but below popups.
950
+ pane.style.pointerEvents = 'none';
951
+ }
952
+
953
+ if (this._renderer) this._renderer.remove();
954
+ this._renderer = L.svg({ pane: "elevationPane" }).addTo(this._map); // default leaflet svg renderer
955
+
956
+ Promise.all([
957
+ this.import(this.__D3),
958
+ this.import(this.__LMARKER)
959
+ ]).then((m) => {
960
+ this._marker = new (m[1] || Elevation).Marker(this.options, this);
961
+ this.fire("elechart_marker");
962
+ });
963
+ },
964
+
965
+ /**
966
+ * Inspired by L.Control.Layers
967
+ */
968
+ _initButton: function(container) {
969
+ L.DomEvent
970
+ .disableClickPropagation(container)
971
+ .disableScrollPropagation(container);
972
+
973
+ this.options.collapsed ? this._collapse() : this._expand();
974
+
975
+ if (this.options.autohide) {
976
+ on(container, 'mouseover', this._expand, this);
977
+ on(container, 'mouseout', this._collapse, this);
978
+ this._map.on('click', this._collapse, this);
979
+ }
980
+
981
+ if (this.options.closeBtn) {
982
+ let link = this._button = create('a', "elevation-toggle-icon", { href: '#', title: L._('Elevation'), }, container);
983
+ on(link, 'click', L.DomEvent.stop);
984
+ on(link, 'click', this._toggle, this);
985
+ on(link, 'focus', this._toggle, this);
986
+ fetch(this.__btnIcon).then(r => r.ok && r.text().then(img => link.innerHTML = img));
987
+ }
988
+ },
989
+
990
+ _initSummary: function(container) {
991
+ this.import(this.__LSUMMARY).then((m)=>{
992
+ this._summary = new (m || Elevation).Summary({ summary: this.options.summary }, this);
993
+
994
+ this.on('elechart_init', () => {
995
+ d3.select(container).call(this._summary.render());
996
+ });
997
+ });
998
+ },
999
+
1000
+ /**
1001
+ * Retrieve data from a remote url (HTTP).
1002
+ */
1003
+ _loadFile: function(url) {
1004
+ fetch(url)
1005
+ .then((response) => response.text())
1006
+ .then((data) => {
1007
+ this._downloadURL = url; // TODO: handle multiple urls?
1008
+ this._parseFromString(data)
1009
+ .then( geojson => geojson && this._loadLayer(geojson));
1010
+ }).catch((err) => console.warn(err));
1011
+ },
1012
+
1013
+ /**
1014
+ * Dynamically import only required javascript modules (code splitting)
1015
+ */
1016
+ _loadModules: function(handlers) {
1017
+ // First map known classnames (eg. "Altitude" --> L.Control.Elevation.Altitude)
1018
+ handlers = handlers.map((h) => typeof h === 'string' && typeof Elevation[h] !== "undefined" ? Elevation[h] : h);
1019
+ // Then load optional classes and custom imports (eg. "Cadence" --> import('../src/handlers/cadence.js'))
1020
+ let modules = handlers.map(file => (typeof file === 'string' && this.import(this.__modulesFolder + file.toLowerCase() + '.js')) || (file instanceof Promise && file) || Promise.resolve());
1021
+ return Promise.all(modules).then((m) => {
1022
+ each(m, (exported, i) => {
1023
+ let fn = exported && Object.keys(exported)[0];
1024
+ if (fn) {
1025
+ handlers[i] = Elevation[fn] = (Elevation[fn] ?? exported[fn]);
1026
+ }
1027
+ });
1028
+ each(handlers, h => ["function", "object"].includes(typeof h) && this._registerHandler(h));
1029
+ });
1030
+ },
1031
+
1032
+ /**
1033
+ * Simple GeoJSON data loader (L.GeoJSON).
1034
+ */
1035
+ _loadLayer: function(geojson) {
1036
+ let { polyline, theme, waypoints, wptIcons, wptLabels, distanceMarkers } = this.options;
1037
+ let style = L.extend({}, polyline);
1038
+
1039
+ if (theme) {
1040
+ style.className += ' ' + theme;
1041
+ }
1042
+
1043
+ if (geojson.name) {
1044
+ this.track_info.name = geojson.name;
1045
+ }
1046
+
1047
+ let layer = L.geoJson(geojson, {
1048
+ distanceMarkers: distanceMarkers,
1049
+ style: style,
1050
+ pointToLayer: (feature, latlng) => {
1051
+ if (waypoints) {
1052
+ let { desc, name, sym } = feature.properties;
1053
+ desc = desc || '';
1054
+ name = name || '';
1055
+ // Handle chart waypoints (dots)
1056
+ if ([true, 'dots'].includes(waypoints)) {
1057
+ this._registerCheckPoint({
1058
+ latlng: latlng,
1059
+ label : ([true, 'dots'].includes(wptLabels) ? name : '')
1060
+ });
1061
+ }
1062
+ // Handle map waypoints (markers)
1063
+ if ([true, 'markers'].includes(waypoints) && wptIcons != false) {
1064
+ return this._registerMarker({
1065
+ latlng : latlng,
1066
+ sym : (sym ?? name).replace(' ', '-').replace('"', '').replace("'", '').toLowerCase(),
1067
+ content: [true, 'markers'].includes(wptLabels) && (name || desc) && decodeURI("<b>" + name + "</b>" + (desc.length > 0 ? '<br>' + desc : ''))
1068
+ });
1069
+ }
1070
+ }
1071
+ },
1072
+ onEachFeature: (feature, layer) => feature.geometry && feature.geometry.type != 'Point' && this.addData(feature, layer),
1073
+ });
1074
+
1075
+ this.import(this.__D3).then(() => {
1076
+ this._initMapIntegrations(layer);
1077
+ this._fireEvt("eledata_loaded", { data: geojson, layer: layer, name: this.track_info.name, track_info: this.track_info });
1078
+ });
1079
+
1080
+ return layer;
1081
+ },
1082
+
1083
+ _onDragEnd: function({ dragstart, dragend}) {
1084
+ this._hideMarker();
1085
+ this.fitBounds(L.latLngBounds([dragstart.latlng, dragend.latlng]));
1086
+
1087
+ this.fire("elechart_dragged");
1088
+ },
1089
+
1090
+ _onKeyDown: function({key}) {
1091
+ if(!this.options.detached && key === "Escape"){
1092
+ this._collapse();
1093
+ } },
1094
+
1095
+ /**
1096
+ * Trigger mouseenter event.
1097
+ */
1098
+ _onMouseEnter: function() {
1099
+ this.fire('elechart_enter');
1100
+ },
1101
+
1102
+ /*
1103
+ * Handles the moueseover the chart and displays distance and altitude level.
1104
+ */
1105
+ _onMouseMove: function({xCoord}) {
1106
+ if (this._chartEnabled && this._data.length) {
1107
+ let item = this._findItemForX(xCoord);
1108
+ if (item) {
1109
+ if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord);
1110
+
1111
+ this._updateMarker(item);
1112
+ this._setMapView(item);
1113
+
1114
+ if (this._map) {
1115
+ addClass(this._map.getContainer(), 'elechart-hover');
1116
+ }
1117
+
1118
+ this.fire("elechart_change", { data: item, xCoord: xCoord });
1119
+ this.fire("elechart_hover", { data: item, xCoord: xCoord });
1120
+ }
1121
+ }
1122
+ },
1123
+
1124
+ /*
1125
+ * Handles mouseover events of the data layers on the map.
1126
+ */
1127
+ _onMouseMoveLayer: function({latlng}) {
1128
+ if (this._data.length) {
1129
+ let item = this._findItemForLatLng(latlng);
1130
+ if (item) {
1131
+ let xCoord = item.xDiagCoord;
1132
+
1133
+ if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord);
1134
+
1135
+ this._updateMarker(item);
1136
+
1137
+ this.fire("elechart_change", { data: item, xCoord: xCoord });
1138
+ }
1139
+ }
1140
+ },
1141
+
1142
+ /*
1143
+ * Handles the moueseout over the chart.
1144
+ */
1145
+ _onMouseOut: function() {
1146
+ if (!this.options.detached) {
1147
+ this._hideMarker();
1148
+ this._chart._hideDiagramIndicator();
1149
+ }
1150
+
1151
+ if (this._map) {
1152
+ removeClass(this._map.getContainer(), 'elechart-hover');
1153
+ }
1154
+
1155
+ this.fire("elechart_leave");
1156
+ },
1157
+
1158
+ /**
1159
+ * Handles the drag event over the ruler filter.
1160
+ */
1161
+ _onRulerFilter: function({coords}) {
1162
+ this._updateMapSegments(coords);
1163
+ },
1164
+
1165
+ /**
1166
+ * Toggle chart data on legend click
1167
+ */
1168
+ _onToggleChart: function({ name, enabled }) {
1169
+
1170
+ this._chartEnabled = this._chart._hasActiveLayers();
1171
+
1172
+ // toggle layer visibility on empty chart
1173
+ this._layers.eachLayer(layer => toggleClass(layer.getElement && layer.getElement(), this.options.polyline.className + ' ' + this.options.theme, this._chartEnabled));
1174
+
1175
+ // toggle option value (eg. altitude = { 'disabled' || 'enabled' })
1176
+ this.options[name] = !enabled && this.options[name] == 'disabled' ? 'enabled' : 'disabled';
1177
+
1178
+ // remove marker on empty chart
1179
+ if (!this._chartEnabled) {
1180
+ this._chart._hideDiagramIndicator();
1181
+ this._marker.remove();
1182
+ }
1183
+ },
1184
+
1185
+ /**
1186
+ * Simple GeoJSON Parser
1187
+ */
1188
+ _parseFromGeoJSONString: function(data) {
1189
+ try {
1190
+ return JSON.parse(data);
1191
+ } catch (e) { }
1192
+ },
1193
+
1194
+ /**
1195
+ * Attempt to parse raw response data (GeoJSON or XML > GeoJSON)
1196
+ */
1197
+ _parseFromString: function(data) {
1198
+ return new Promise(resolve =>
1199
+ this.import(this.__TOGEOJSON).then(() => {
1200
+ let geojson;
1201
+ try {
1202
+ geojson = this._parseFromXMLString(data.trim());
1203
+ } catch (e) {
1204
+ geojson = this._parseFromGeoJSONString(data.toString());
1205
+ }
1206
+ if (geojson) {
1207
+ geojson.name = geojson.name || (this._downloadURL || '').split('/').pop().split('#')[0].split('?')[0];
1208
+ }
1209
+ resolve(geojson);
1210
+ })
1211
+ );
1212
+ },
1213
+
1214
+ /**
1215
+ * Simple XML Parser (GPX, KML, TCX)
1216
+ */
1217
+ _parseFromXMLString: function(data) {
1218
+ if (data.indexOf("<") != 0) {
1219
+ throw 'Invalid XML';
1220
+ }
1221
+ let xml = (new DOMParser()).parseFromString(data, "text/xml");
1222
+ let type = xml.documentElement.tagName.toLowerCase(); // "kml" or "gpx"
1223
+ let name = xml.getElementsByTagName('name');
1224
+ if (xml.getElementsByTagName('parsererror').length) {
1225
+ throw 'Invalid XML';
1226
+ }
1227
+ if (!(type in toGeoJSON)) {
1228
+ type = xml.documentElement.tagName == "TrainingCenterDatabase" ? 'tcx' : 'gpx';
1229
+ }
1230
+ let geojson = toGeoJSON[type](xml);
1231
+ geojson.name = name.length > 0 ? (Array.from(name).find(tag => tag.parentElement.tagName == "trk") ?? name[0]).textContent : '';
1232
+ return geojson;
1233
+ },
1234
+
1235
+ /**
1236
+ * Add chart profile to diagram
1237
+ */
1238
+ _registerAreaPath: function(props) {
1239
+ this.on("elechart_init", () => this._chart._registerAreaPath(props));
1240
+ },
1241
+
1242
+ /**
1243
+ * Add chart grid to diagram
1244
+ */
1245
+ _registerAxisGrid: function(props) {
1246
+ this.on("elechart_axis", () => this._chart._registerAxisGrid(props));
1247
+ },
1248
+
1249
+ /**
1250
+ * Add chart axis to diagram
1251
+ */
1252
+ _registerAxisScale: function(props) {
1253
+ this.on("elechart_axis", () => this._chart._registerAxisScale(props));
1254
+ },
1255
+
1256
+ /**
1257
+ * Add a point of interest over the diagram
1258
+ */
1259
+ _registerCheckPoint: function(props) {
1260
+ this.on("elechart_updated", () => this._chart._registerCheckPoint(props));
1261
+ },
1262
+
1263
+
1264
+ /**
1265
+ * Base handler for iterative track statistics (dist, time, z, slope, speed, acceleration, ...)
1266
+ */
1267
+ _registerDataAttribute: function(props) {
1268
+
1269
+ // parse of "coordinateProperties" for later usage
1270
+ if (props.coordPropsToMeta) {
1271
+ this.on("elepoint_init", (e) => props.coordPropsToMeta.call(this, e));
1272
+ }
1273
+
1274
+ // prevent excessive variabile instanstations
1275
+ let i, curr, prev, attr;
1276
+
1277
+ // save here a reference to last used point
1278
+ let lastValid = null;
1279
+
1280
+ // iteration
1281
+ this.on("elepoint_added", ({index, point}) => {
1282
+ i = index;
1283
+
1284
+ prev = curr ?? this._data[i]; // same as: this._data[i > 0 ? i - 1 : i]
1285
+ curr = this._data[i];
1286
+ attr = props.attr || props.name;
1287
+
1288
+ // check and fix missing data on last added point
1289
+ if (props.skipNull === false) {
1290
+ let curr = this._data[i][attr];
1291
+ if(i > 0) {
1292
+ let prev = this._data[i - 1][attr];
1293
+ if (isNaN(prev)) {
1294
+ if (!isNaN(lastValid) && !isNaN(curr)) {
1295
+ prev = (lastValid + curr) / 2;
1296
+ } else if (!isNaN(lastValid)) {
1297
+ prev = lastValid;
1298
+ } else if (!isNaN(curr)) {
1299
+ prev = curr;
1300
+ }
1301
+ if (!isNaN(prev)) return this._data.splice(i - 1, 1);
1302
+ this._data[i - 1][attr] = prev;
1303
+ }
1304
+ }
1305
+ // update reference to last used point (ie. if it has data)
1306
+ if (!isNaN(curr)) {
1307
+ lastValid = curr;
1308
+ }
1309
+ }
1310
+
1311
+ // retrieve point value
1312
+ curr[attr] = props.pointToAttr.call(this, point, i);
1313
+
1314
+ // update "track_info" stats (min, max, avg, ...)
1315
+ if (props.stats) {
1316
+ for (const key in props.stats) {
1317
+ let sname = (props.statsName || attr) + (key != '' ? '_' : '');
1318
+ this.track_info[sname + key] = props.stats[key].call(this, curr[attr], this.track_info[sname + key], this._data.length);
1319
+ }
1320
+ }
1321
+
1322
+ // Limit "crazy" delta values.
1323
+ if (props.deltaMax) {
1324
+ curr[attr] =wrapDelta(curr[attr], prev[attr], props.deltaMax);
1325
+ }
1326
+
1327
+ // Range of acceptable values.
1328
+ if (props.clampRange) {
1329
+ curr[attr] = clamp(curr[attr], props.clampRange);
1330
+ }
1331
+
1332
+ // Limit floating point precision.
1333
+ if (!isNaN(props.decimals)) {
1334
+ curr[attr] = round(curr[attr], props.decimals);
1335
+ }
1336
+
1337
+ // update here some mixins (eg. complex "track_info" stuff)
1338
+ if(props.onPointAdded) props.onPointAdded.call(this, curr[attr], i, point);
1339
+ });
1340
+ },
1341
+
1342
+ /**
1343
+ * Parse a module definition and attach related function listeners
1344
+ */
1345
+ _registerHandler: function(props) {
1346
+
1347
+ // eg. L.Control.Altitude
1348
+ if (typeof props === "function") {
1349
+ return this._registerHandler(props.call(this));
1350
+ }
1351
+
1352
+ let {
1353
+ name,
1354
+ attr,
1355
+ required,
1356
+ skipNull,
1357
+ deltaMax,
1358
+ clampRange,
1359
+ decimals,
1360
+ meta,
1361
+ unit,
1362
+ coordinateProperties,
1363
+ coordPropsToMeta: coordPropsToMeta$1,
1364
+ pointToAttr,
1365
+ onPointAdded,
1366
+ stats,
1367
+ statsName,
1368
+ grid,
1369
+ scale,
1370
+ path,
1371
+ tooltip,
1372
+ summary
1373
+ } = props;
1374
+
1375
+ // eg. "altitude" == true
1376
+ if (this.options[name] || required) {
1377
+
1378
+ this._registerDataAttribute({
1379
+ name,
1380
+ attr,
1381
+ skipNull,
1382
+ deltaMax,
1383
+ clampRange,
1384
+ decimals,
1385
+ coordPropsToMeta: coordPropsToMeta(coordinateProperties, meta || name, coordPropsToMeta$1),
1386
+ pointToAttr,
1387
+ onPointAdded,
1388
+ stats,
1389
+ statsName,
1390
+ });
1391
+
1392
+ if (grid) {
1393
+ this._registerAxisGrid(L.extend({ name }, grid));
1394
+ }
1395
+
1396
+ if (this.options[name] !== "summary") {
1397
+ if (scale) this._registerAxisScale(L.extend({ name, label: unit }, scale));
1398
+ if (path) this._registerAreaPath(L.extend({ name }, path));
1399
+ }
1400
+
1401
+ if (tooltip || props.tooltips) {
1402
+ each([tooltip, ...(props.tooltips || [])], t => t && this._registerTooltip(L.extend({ name }, t)));
1403
+ }
1404
+
1405
+ if (summary) {
1406
+ each(summary, (s, k) => summary[k] = L.extend({ unit }, s));
1407
+ this._registerSummary(summary);
1408
+ }
1409
+ }
1410
+ },
1411
+
1412
+ _registerMarker({latlng, sym, content}) {
1413
+ let { wptIcons } = this.options;
1414
+ // generate and cache appropriate icon symbol
1415
+ if (!wptIcons.hasOwnProperty(sym)) {
1416
+ wptIcons[sym] = L.divIcon(L.extend({}, wptIcons[""].options, { html: '<i class="elevation-waypoint-icon ' + sym + '"></i>' } ));
1417
+ }
1418
+ let marker = L.marker(latlng, { icon: wptIcons[sym] });
1419
+ if (content) {
1420
+ marker.bindPopup(content, { className: 'elevation-popup', keepInView: true }).openPopup();
1421
+ marker.bindTooltip(content, { className: 'elevation-tooltip', direction: 'auto', sticky: true, opacity: 1 }).openTooltip();
1422
+ }
1423
+ return this._addMarker(marker)
1424
+ },
1425
+
1426
+ /**
1427
+ * Add chart or marker tooltip info
1428
+ */
1429
+ _registerTooltip: function(props) {
1430
+ props.chart && this.on("elechart_init", () => this._chart._registerTooltip(L.extend({}, props, { value: props.chart })));
1431
+ props.marker && this.on("elechart_marker", () => this._marker._registerTooltip(L.extend({}, props, { value: props.marker })));
1432
+ },
1433
+
1434
+ /**
1435
+ * Add summary info to diagram
1436
+ */
1437
+ _registerSummary: function(props) {
1438
+ this.on('elechart_summary', () => this._summary._registerSummary(props));
1439
+ },
1440
+
1441
+ /*
1442
+ * Removes the drag rectangle and zoms back to the total extent of the data.
1443
+ */
1444
+ _resetDrag: function() {
1445
+ this._chart._resetDrag();
1446
+ this._hideMarker();
1447
+ },
1448
+
1449
+ /**
1450
+ * Resets drag, marker and bounds.
1451
+ */
1452
+ _resetView: function() {
1453
+ if (this._map && this._map._isFullscreen) return;
1454
+ this._resetDrag();
1455
+ this._hideMarker();
1456
+ if (this.options.autofitBounds) {
1457
+ this.fitBounds();
1458
+ }
1459
+ },
1460
+
1461
+ /**
1462
+ * Hacky way for handling chart resize. Deletes it and redraw chart.
1463
+ */
1464
+ _resizeChart: function() {
1465
+ if (this._container && style(this._container, "display") != "none") {
1466
+ let opts = this.options;
1467
+ let newWidth = opts.detached ? (this.eleDiv || this._container).offsetWidth : clamp(opts._maxWidth, [0, this._map.getContainer().clientWidth - 30]);
1468
+ if (newWidth) {
1469
+ opts.width = newWidth;
1470
+ if (this._chart && this._chart._chart) {
1471
+ this._chart._chart._resize(opts);
1472
+ this._updateChart();
1473
+ }
1474
+ }
1475
+ this._updateMapSegments();
1476
+ }
1477
+ },
1478
+
1479
+ /**
1480
+ * Collapse or Expand chart control.
1481
+ */
1482
+ _toggle: function() {
1483
+ hasClass(this._container, "elevation-expanded") ? this._collapse() : this._expand();
1484
+ },
1485
+
1486
+ /**
1487
+ * Update map center and zoom (followMarker: true)
1488
+ */
1489
+ _setMapView: function(item) {
1490
+ if (this._map && this.options.followMarker) {
1491
+ let zoom = this._map.getZoom();
1492
+ let z = this.options.zFollow;
1493
+ if (typeof z === "number") {
1494
+ this._map.setView(item.latlng, (zoom < z ? z : zoom), { animate: true, duration: 0.25 });
1495
+ } else if (!this._map.getBounds().contains(item.latlng)) {
1496
+ this._map.setView(item.latlng, zoom, { animate: true, duration: 0.25 });
1497
+ }
1498
+ }
1499
+ },
1500
+
1501
+ /**
1502
+ * Calculates [x, y] domain and then update chart.
1503
+ */
1504
+ _updateChart: function() {
1505
+ if (this._chart && this._container) {
1506
+ this.fire("elechart_axis");
1507
+
1508
+ this._chart.update({ data: this._data, options: this.options });
1509
+
1510
+ this._x = this._chart._x;
1511
+ this._y = this._chart._y;
1512
+
1513
+ this.fire('elechart_updated');
1514
+ }
1515
+ },
1516
+
1517
+ /*
1518
+ * Update the position/height indicator marker drawn onto the map
1519
+ */
1520
+ _updateMarker: function(item) {
1521
+ if (this._marker) {
1522
+ this._marker.update({
1523
+ map : this._map,
1524
+ item : item,
1525
+ yCoordMax : this._yCoordMax || 0,
1526
+ options : this.options
1527
+ });
1528
+ }
1529
+ },
1530
+
1531
+ /**
1532
+ * Fix marker rotation on rotated maps
1533
+ */
1534
+ _rotateMarker: function() {
1535
+ if (this._marker) {
1536
+ this._marker.update();
1537
+ }
1538
+ },
1539
+
1540
+ /**
1541
+ * Highlight track segments on the map.
1542
+ */
1543
+ _updateMapSegments: function(coords) {
1544
+ this._markedSegments.setLatLngs(coords || []);
1545
+ if (coords && this._map && !this._map.hasLayer(this._markedSegments)) {
1546
+ this._markedSegments.addTo(this._map);
1547
+ }
1548
+ },
1549
+
1550
+ /**
1551
+ * Update chart summary.
1552
+ */
1553
+ _updateSummary: function() {
1554
+ if (this._summary) {
1555
+ this._summary.reset();
1556
+ if (this.options.summary) {
1557
+ this.fire("elechart_summary");
1558
+ this._summary.update();
1559
+ }
1560
+ if (this.options.downloadLink && this._downloadURL) { // TODO: generate dynamically file content instead of using static file urls.
1561
+ this._summary._container.innerHTML += '<span class="download"><a href="#">' + L._('Download') + '</a></span>';
1562
+ select('.download a', this._summary._container).onclick = (e) => {
1563
+ e.preventDefault();
1564
+ let event = { downloadLink: this.options.downloadLink, confirm: saveFile.bind(this, this._downloadURL) };
1565
+ if (this.options.downloadLink == 'modal' && typeof CustomEvent === "function") {
1566
+ document.dispatchEvent(new CustomEvent("eletrack_download", { detail: event }));
1567
+ } else if (this.options.downloadLink == 'link' || this.options.downloadLink === true) {
1568
+ event.confirm();
1569
+ }
1570
+ this.fire('eletrack_download', event);
1571
+ };
1572
+ }
1573
+ }
1574
+ },
1575
+
1576
+
1577
+ /**
1578
+ * Calculates chart width.
1579
+ */
1580
+ _width: function() {
1581
+ if (this._chart) return this._chart._width();
1582
+ const { width, margins } = this.options;
1583
+ return width - margins.left - margins.right;
1584
+ },
1585
+
1586
+ /**
1587
+ * Calculates chart height.
1588
+ */
1589
+ _height: function() {
1590
+ if (this._chart) return this._chart._height();
1591
+ const { height, margins } = this.options;
1592
+ return height - margins.top - margins.bottom;
1593
+ },
1594
+
1588
1595
  });
1589
1596
 
1590
1597
  /*