@raruto/leaflet-elevation 2.3.3 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/control.js CHANGED
@@ -1,1325 +1,1337 @@
1
- import * as _ from './utils';
2
- import { Options } from './options';
3
-
4
- // "leaflet-i18n" fallback
5
- if (!L._ || !L.i18n) {
6
- L._ = L.i18n = (string, data) => string;
7
- }
8
-
9
- export const Elevation = L.Control.Elevation = L.Control.extend({
10
-
11
- includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
12
-
13
- options: Options,
14
- __mileFactor: 0.621371, // 1 km = (0.621371 mi)
15
- __footFactor: 3.28084, // 1 m = (3.28084 ft)
16
- __D3: 'https://unpkg.com/d3@7.8.4/dist/d3.min.js',
17
- __TOGEOJSON: 'https://unpkg.com/@tmcw/togeojson@5.6.0/dist/togeojson.umd.js',
18
- __LGEOMUTIL: 'https://unpkg.com/leaflet-geometryutil@0.10.1/src/leaflet.geometryutil.js',
19
- __LALMOSTOVER: 'https://unpkg.com/leaflet-almostover@1.0.1/src/leaflet.almostover.js',
20
- __LHOTLINE: '../libs/leaflet-hotline.min.js',
21
- __LDISTANCEM: '../libs/leaflet-distance-marker.min.js',
22
- __LEDGESCALE: '../libs/leaflet-edgescale.min.js',
23
- __LCHART: '../src/components/chart.js',
24
- __LMARKER: '../src/components/marker.js',
25
- __LSUMMARY: '../src/components/summary.js',
26
- __modulesFolder: '../src/handlers/',
27
- __btnIcon: '../images/elevation.svg',
28
-
29
- /*
30
- * Add data to the diagram either from GPX or GeoJSON and update the axis domain and data
31
- */
32
- addData(d, layer) {
33
- this.import(this.__D3)
34
- .then(() => {
35
- if (this._modulesLoaded) {
36
- layer = layer ?? (d.on && d);
37
- this._addData(d);
38
- this._addLayer(layer);
39
- this._fireEvt("eledata_added", { data: d, layer: layer, track_info: this.track_info });
40
- } else {
41
- this.once('modules_loaded', () => this.addData(d,layer));
42
- }
43
- });
44
- },
45
-
46
- /**
47
- * Adds the control to the given map.
48
- */
49
- addTo(map) {
50
- if (this.options.detached) {
51
- let parent = _.select(this.options.elevationDiv);
52
- let eleDiv = this.onAdd(map);
53
- parent ? _.append(parent, eleDiv) : _.insert(map.getContainer(), eleDiv, 'afterend');
54
- } else {
55
- L.Control.prototype.addTo.call(this, map);
56
- }
57
- return this;
58
- },
59
-
60
- /*
61
- * Reset data and display
62
- */
63
- clear() {
64
- if (this._marker) this._marker.remove();
65
- if (this._chart) this._clearChart();
66
- if (this._layers) this._clearLayers(this._layers);
67
- if (this._markers) this._clearLayers(this._markers);
68
- if (this._circleMarkers) this._circleMarkers.remove();
69
- if (this._hotline) this._clearLayers(this._hotline);
70
-
71
- this._data = [];
72
- this.track_info = {};
73
-
74
- this._fireEvt("eledata_clear");
75
-
76
- this._updateChart();
77
- },
78
-
79
- _clearChart() {
80
- if (this._events && this._events.elechart_updated) {
81
- this._events.elechart_updated.forEach(({fn, ctx}) => this.off('elechart_updated', fn, ctx));
82
- }
83
- if (this._chart && this._chart._container) {
84
- this._chart._container.selectAll('g.point .point').remove();
85
- this._chart.clear();
86
- }
87
- },
88
-
89
- _clearLayers(l) {
90
- l = l || this._layers;
91
- if (l && l.eachLayer) {
92
- l.eachLayer(f => f.remove())
93
- l.clearLayers();
94
- }
95
- },
96
-
97
- /**
98
- * TODO: Create a base class to handle custom data attributes (heart rate, cadence, temperature, ...)
99
- *
100
- * @link https://leafletjs.com/examples/extending/extending-3-controls.html#handlers
101
- */
102
- // addHandler: function (name, HandlerClass) {
103
- // if (HandlerClass) {
104
- // let handler = this[name] = new HandlerClass(this);
105
- // this.handlers.push(handler);
106
- // if (this.options[name]) {
107
- // handler.enable();
108
- // }
109
- // }
110
- // return this;
111
- // },
112
-
113
- /**
114
- * Disable chart brushing.
115
- */
116
- disableBrush() {
117
- this._chart._brushEnabled = false;
118
- this._resetDrag();
119
- },
120
-
121
- /**
122
- * Enable chart brushing.
123
- */
124
- enableBrush() {
125
- this._chart._brushEnabled = true;
126
- },
127
-
128
- /**
129
- * Disable chart zooming.
130
- */
131
- disableZoom() {
132
- this._chart._zoomEnabled = false;
133
- this._chart._resetZoom();
134
- },
135
-
136
- /**
137
- * Enable chart zooming.
138
- */
139
- enableZoom() {
140
- this._chart._zoomEnabled = true;
141
- },
142
-
143
- /**
144
- * Sets a map view that contains the given geographical bounds.
145
- */
146
- fitBounds(bounds) {
147
- bounds = bounds || this.getBounds();
148
- if (this._map && bounds.isValid()) this._map.fitBounds(bounds);
149
- },
150
-
151
- getBounds(data) {
152
- return L.latLngBounds((data || this._data).map((d) => d.latlng));
153
- },
154
-
155
- /**
156
- * Get default zoom level (followMarker: true).
157
- */
158
- getZFollow() {
159
- return this.options.zFollow;
160
- },
161
-
162
- /**
163
- * Hide current elevation chart profile.
164
- */
165
- hide() {
166
- _.style(this._container, "display", "none");
167
- },
168
-
169
- /**
170
- * Initialize chart control "options" and "container".
171
- */
172
- initialize(opts) {
173
-
174
- // opts = L.setOptions(this, opts);
175
-
176
- // Fixes: https://github.com/Raruto/leaflet-elevation/pull/240
177
- opts = L.setOptions(this, L.extend({}, structuredClone(Options), opts)); // "deep copy" nested objects (multiple charts)
178
-
179
- this._data = [];
180
- this._layers = L.featureGroup();
181
- this._markers = L.featureGroup();
182
- this._hotline = L.featureGroup();
183
- this._circleMarkers = L.featureGroup();
184
- this._markedSegments = L.polyline([]);
185
- this._start = L.circleMarker([0,0], (opts.trkStart || Options.trkStart));
186
- this._end = L.circleMarker([0,0], (opts.trkEnd || Options.trkEnd));
187
- this._chartEnabled = true;
188
- this._yCoordMax = -Infinity;
189
- this.track_info = {};
190
- // this.handlers = [];
191
-
192
- if (opts.followMarker) this._setMapView = _.throttle(this._setMapView, 300, this);
193
- if (opts.legend) opts.margins.bottom += 30;
194
- if (opts.theme) opts.polylineSegments.className += ' ' + opts.theme;
195
- if (opts.wptIcons === true) opts.wptIcons = Options.wptIcons;
196
- if (opts.distanceMarkers === true) opts.distanceMarkers = Options.distanceMarkers;
197
- if (opts.trkStart) this._start.addTo(this._circleMarkers);
198
- if (opts.trkEnd) this._end.addTo(this._circleMarkers);
199
-
200
-
201
- this._markedSegments.setStyle(opts.polylineSegments);
202
-
203
- // Leaflet canvas renderer colors
204
- L.extend(_.Colors, opts.colors || {});
205
-
206
- // Various stuff
207
- this._fixCanvasPaths();
208
- this._fixTooltipSize();
209
-
210
- },
211
-
212
- /**
213
- * Javascript scripts downloader (lazy loader)
214
- */
215
- import(src, condition) {
216
- switch(src) {
217
- case this.__D3: condition = typeof d3 !== 'object'; break;
218
- case this.__TOGEOJSON: condition = typeof toGeoJSON !== 'object'; break;
219
- case this.__LGEOMUTIL: condition = typeof L.GeometryUtil !== 'object'; break;
220
- case this.__LALMOSTOVER: condition = typeof L.Handler.AlmostOver !== 'function'; break;
221
- case this.__LDISTANCEM: condition = typeof L.DistanceMarkers !== 'function'; break;
222
- case this.__LEDGESCALE: condition = typeof L.Control.EdgeScale !== 'function'; break;
223
- case this.__LHOTLINE: condition = typeof L.Hotline !== 'function'; break;
224
- }
225
- return condition !== false ? import(_.resolveURL(src, this.options.srcFolder)) : Promise.resolve();
226
- },
227
-
228
- /**
229
- * Load elevation data (GPX, GeoJSON, KML or TCX).
230
- */
231
- load(data) {
232
- this._parseFromString(data).then( geojson => geojson ? this._loadLayer(geojson) : this._loadFile(data));
233
- },
234
-
235
- /**
236
- * Create container DOM element and related event listeners.
237
- * Called on control.addTo(map).
238
- */
239
- onAdd(map) {
240
- this._map = map;
241
-
242
- let container = this._container = _.create("div", "elevation-control " + this.options.theme + " " + (this.options.detached ? 'elevation-detached' : 'leaflet-control'), this.options.detached ? { id: 'elevation-' + _.randomId() } : {});
243
-
244
- if (!this.eleDiv) this.eleDiv = container;
245
-
246
- this._loadModules(this.options.handlers).then(() => { // Inject here required modules (data handlers)
247
- this._initChart(container);
248
- this._initButton(container);
249
- this._initSummary(container);
250
- this._initMarker(map);
251
- this._initLayer(map);
252
- this._modulesLoaded = true;
253
- this.fire('modules_loaded');
254
- });
255
-
256
- this.fire('add');
257
-
258
- return container;
259
- },
260
-
261
- /**
262
- * Clean up control code and related event listeners.
263
- * Called on control.remove().
264
- */
265
- onRemove(map) {
266
- this._container = null;
267
-
268
- map
269
- .off('zoom viewreset zoomanim', this._hideMarker, this)
270
- .off('resize', this._resetView, this)
271
- .off('resize', this._resizeChart, this)
272
- .off('mousedown', this._resetDrag, this);
273
-
274
- _.off(map.getContainer(), 'mousewheel', this._resetDrag, this);
275
- _.off(map.getContainer(), 'touchstart', this._resetDrag, this);
276
- _.off(document, 'keydown', this._onKeyDown, this);
277
-
278
- this
279
- .off('eledata_added eledata_loaded', this._updateChart, this)
280
- .off('eledata_added eledata_loaded', this._updateSummary, this);
281
-
282
- this.fire('remove');
283
- },
284
-
285
- /**
286
- * Redraws the chart control. Sometimes useful after screen resize.
287
- */
288
- redraw() {
289
- this._resizeChart();
290
- },
291
-
292
- /**
293
- * Set default zoom level (followMarker: true).
294
- */
295
- setZFollow(zoom) {
296
- this.options.zFollow = zoom;
297
- },
298
-
299
- /**
300
- * Hide current elevation chart profile.
301
- */
302
- show() {
303
- _.style(this._container, "display", "block");
304
- },
305
-
306
- /*
307
- * Parsing data either from GPX or GeoJSON and update the diagram data
308
- */
309
- _addData(d) {
310
- if (!d) {
311
- return;
312
- }
313
-
314
- // Standard GeoJSON
315
- if (d.type === "FeatureCollection" ) {
316
- return _.each(d.features, feature => this._addData(feature));
317
- } else if (d.type === "Feature") {
318
- let geom = d.geometry;
319
- if (geom) {
320
- switch (geom.type) {
321
- case 'LineString': return this._addGeoJSONData(geom.coordinates, d.properties);
322
- case 'MultiLineString': return _.each(geom.coordinates, (coords, i) => this._addGeoJSONData(coords, d.properties, i));
323
- case 'Point':
324
- default: return console.warn('Unsopperted GeoJSON feature geometry type:' + geom.type);
325
- }
326
- }
327
- }
328
-
329
- // Fallback for leaflet layers (eg. L.Gpx)
330
- if (d._latlngs) {
331
- return this._addGeoJSONData(d._latlngs, d.feature && d.feature.properties);
332
- }
333
-
334
- },
335
-
336
- /*
337
- * Parsing of GeoJSON data lines and their elevation in z-coordinate
338
- */
339
- _addGeoJSONData(coords, properties, nestingLevel) {
340
-
341
- // "coordinateProperties" property is generated inside "@tmcw/toGeoJSON"
342
- let props = (properties && properties.coordinateProperties) || properties;
343
-
344
- coords.forEach((point, i) => {
345
-
346
- // GARMIN_EXTENSIONS = ["hr", "cad", "atemp", "wtemp", "depth", "course", "bearing"];
347
- point.meta = point.meta ?? { time: null, ele: null };
348
-
349
- point.prev = (attr) => (attr ? this._data[i > 0 ? i - 1 : 0][attr] : this._data[i > 0 ? i - 1 : 0]);
350
-
351
- this.fire("elepoint_init", { point: point, props: props, id: i, isMulti: nestingLevel });
352
-
353
- this._addPoint(
354
- point.lat ?? point[1],
355
- point.lng ?? point[0],
356
- point.alt ?? point.meta.ele ?? point[2]
357
- );
358
-
359
- this.fire("elepoint_added", { point: point, index: this._data.length - 1 });
360
-
361
- if (this._yCoordMax < this._data[this._data.length - 1][this.options.yAttr]) this._yCoordMax = this._data[this._data.length - 1][this.options.yAttr];
362
- });
363
-
364
- this.fire("eletrack_added", { coords: coords, index: this._data.length - 1 });
365
- },
366
-
367
- /*
368
- * Parse and push a single (x, y, z) point to current elevation profile.
369
- */
370
- _addPoint(x, y, z) {
371
- if (this.options.reverseCoords) {
372
- [x, y] = [y, x];
373
- }
374
-
375
- this._data.push({
376
- x: x,
377
- y: y,
378
- z: z,
379
- latlng: L.latLng(x, y, z)
380
- });
381
-
382
- this.fire("eledata_updated", { index: this._data.length - 1 });
383
- },
384
-
385
- _addLayer(layer) {
386
- if (layer) this._layers.addLayer(layer)
387
- // Postpone adding the distance markers (lazy: true)
388
- if (layer && this.options.distanceMarkers && this.options.distanceMarkers.lazy) {
389
- layer.on('add remove', ({target, type}) => L.DistanceMarkers && target instanceof L.Polyline && target[type + 'DistanceMarkers']());
390
- }
391
- return layer;
392
- },
393
-
394
- _addMarker(marker) {
395
- if (marker) this._markers.addLayer(marker)
396
- return marker;
397
- },
398
-
399
- /**
400
- * Initialize "L.AlmostOver" integration
401
- */
402
- _initAlmostOverHandler(map, layer) {
403
- return (map && this.options.almostOver && !L.Browser.mobile) ? Promise.all([
404
- this.import(this.__LGEOMUTIL),
405
- this.import(this.__LALMOSTOVER)
406
- ]).then(() => {
407
- map.addHandler('almostOver', L.Handler.AlmostOver)
408
- if (L.GeometryUtil && map.almostOver && map.almostOver.enabled()) {
409
- map.almostOver.addLayer(layer);
410
- map
411
- .on('almost:move', (e) => this._onMouseMoveLayer(e))
412
- .on('almost:out', (e) => this._onMouseOut(e));
413
- }
414
- }) : Promise.resolve();
415
- },
416
-
417
- /**
418
- * Initialize "L.DistanceMarkers" integration
419
- */
420
- _initDistanceMarkers() {
421
- return this.options.distanceMarkers ? Promise.all([this.import(this.__LGEOMUTIL), this.import(this.__LDISTANCEM)]) : Promise.resolve();
422
- },
423
-
424
- /**
425
- * Initialize "L.Control.EdgeScale" integration
426
- */
427
- _initEdgeScale(map) {
428
- return this.options.edgeScale ? Promise.all([this.import(this.__LEDGESCALE)])
429
- .then(() => {
430
- map.edgeScaleControl = map.edgeScaleControl || L.control.edgeScale('boolean' !== typeof this.options.edgeScale ? this.options.edgeScale : {}).addTo(map);
431
- }) : Promise.resolve();
432
- },
433
-
434
- _initHotLine(layer) {
435
- let prop = typeof this.options.hotline == 'string' ? this.options.hotline : 'elevation';
436
- return this.options.hotline ? this.import(this.__LHOTLINE)
437
- .then(() => {
438
- layer.eachLayer((trkseg) => {
439
- if(trkseg.feature.geometry.type != "Point") {
440
- let geo = L.geoJson(trkseg.toGeoJSON(), { coordsToLatLng: (coords) => L.latLng(coords[0], coords[1], coords[2] * (this.options.altitudeFactor || 1))});
441
- let line = L.hotline(geo.toGeoJSON().features[0].geometry.coordinates, {
442
- renderer: L.Hotline.renderer(),
443
- min: isFinite(this.track_info[prop + '_min']) ? this.track_info[prop + '_min'] : 0,
444
- max: isFinite(this.track_info[prop + '_max']) ? this.track_info[prop + '_max'] : 1,
445
- palette: {
446
- 0.0: '#008800',
447
- 0.5: '#ffff00',
448
- 1.0: '#ff0000'
449
- },
450
- weight: 5,
451
- outlineColor: '#000000',
452
- outlineWidth: 1
453
- }).addTo(this._hotline);
454
- let alpha = trkseg.options.style && trkseg.options.style.opacity || 1;
455
- trkseg.on('add remove', ({type}) => {
456
- trkseg.setStyle({opacity: (type == 'add' ? 0 : alpha)});
457
- line[(type == 'add' ? 'addTo' : 'removeFrom')](trkseg._map);
458
- if (line._renderer) line._renderer._container.parentElement.insertBefore(line._renderer._container, line._renderer._container.parentElement.firstChild);
459
- });
460
- }
461
- });
462
- }) : Promise.resolve();
463
- },
464
-
465
- /**
466
- * Initialize "L.AlmostOver" and "L.DistanceMarkers"
467
- */
468
- _initMapIntegrations(layer) {
469
- let map = this._map;
470
- if (map) {
471
- if (this._data.length) {
472
- this._start.setLatLng(this._data[0].latlng);
473
- this._end.setLatLng(this._data[this._data.length -1].latlng);
474
- }
475
- Promise.all([
476
- this._initHotLine(layer),
477
- this._initAlmostOverHandler(map, layer),
478
- this._initDistanceMarkers(),
479
- this._initEdgeScale(map),
480
- ]).then(() => {
481
- if (this.options.polyline) {
482
- layer.addTo(map);
483
- this._circleMarkers.addTo(map);
484
- }
485
- if (this.options.autofitBounds) {
486
- this.fitBounds(layer.getBounds());
487
- }
488
- map.invalidateSize();
489
- });
490
- } else {
491
- this.once('add', () => this._initMapIntegrations(layer));
492
- }
493
- },
494
-
495
- /*
496
- * Collapse current chart control.
497
- */
498
- _collapse() {
499
- _.replaceClass(this._container, 'elevation-expanded', 'elevation-collapsed');
500
- if (this._map) this._map.invalidateSize();
501
- },
502
-
503
- /*
504
- * Expand current chart control.
505
- */
506
- _expand() {
507
- _.replaceClass(this._container, 'elevation-collapsed', 'elevation-expanded');
508
- if (this._map) this._map.invalidateSize();
509
- },
510
-
511
- /**
512
- * Add some basic colors to leaflet canvas renderer (preferCanvas: true).
513
- */
514
- _fixCanvasPaths() {
515
- let oldProto = L.Canvas.prototype._fillStroke;
516
- let control = this;
517
-
518
- let theme = this.options.theme.split(' ')[0].replace('-theme', '');
519
- let color = _.Colors[theme] || {};
520
-
521
- L.Canvas.include({
522
- _fillStroke(ctx, layer) {
523
- if (control._layers.hasLayer(layer)) {
524
-
525
- let options = layer.options;
526
-
527
- options.color = color.line || color.area || theme;
528
- options.stroke = !!options.color;
529
-
530
- oldProto.call(this, ctx, layer);
531
-
532
- if (options.stroke && options.weight !== 0) {
533
- let oldVal = ctx.globalCompositeOperation || 'source-over';
534
- ctx.globalCompositeOperation = 'destination-over'
535
- ctx.strokeStyle = color.outline || '#FFF';
536
- ctx.lineWidth = options.weight * 1.75;
537
- ctx.stroke();
538
- ctx.globalCompositeOperation = oldVal;
539
- }
540
-
541
- } else {
542
- oldProto.call(this, ctx, layer);
543
- }
544
- }
545
- });
546
- },
547
-
548
- /**
549
- * Partial fix for initial tooltip size
550
- *
551
- * @link https://github.com/Raruto/leaflet-elevation/issues/81#issuecomment-713477050
552
- */
553
- _fixTooltipSize() {
554
- this.on('elechart_init', () =>
555
- this.once('elechart_change elechart_hover', ({data, xCoord}) => {
556
- if (this._chartEnabled) {
557
- this._chart._showDiagramIndicator(data, xCoord);
558
- this._chart._showDiagramIndicator(data, xCoord);
559
- }
560
- this._updateMarker(data);
561
- })
562
- );
563
- },
564
-
565
- /*
566
- * Finds a data entry for the given LatLng
567
- */
568
- _findItemForLatLng(latlng) {
569
- return this._data[this._chart._findIndexForLatLng(latlng)];
570
- },
571
-
572
- /*
573
- * Finds a data entry for the given xDiagCoord
574
- */
575
- _findItemForX(x) {
576
- return this._data[this._chart._findIndexForXCoord(x)];
577
- },
578
-
579
- /**
580
- * Fires an event of the specified type.
581
- */
582
- _fireEvt(type, data, propagate) {
583
- if (this.fire) this.fire(type, data, propagate);
584
- if (this._map) this._map.fire(type, data, propagate);
585
- },
586
-
587
- /*
588
- * Hides the position/height indicator marker drawn onto the map
589
- */
590
- _hideMarker() {
591
- if (this.options.autohideMarker) {
592
- this._marker.remove();
593
- }
594
- },
595
-
596
- /**
597
- * Generate "svg" chart (DOM element).
598
- */
599
- _initChart(container) {
600
- let opts = this.options;
601
- let map = this._map;
602
-
603
- if (opts.detached) {
604
- let { offsetWidth, offsetHeight} = this.eleDiv;
605
- if (offsetWidth > 0) opts.width = offsetWidth;
606
- if (offsetHeight > 20) opts.height = offsetHeight - 20; // 20 = horizontal scrollbar size.
607
- } else {
608
- let { clientWidth } = map.getContainer();
609
- opts._maxWidth = opts._maxWidth > opts.width ? opts._maxWidth : opts.width;
610
- this._container.style.maxWidth = opts._maxWidth + 'px';
611
- if (opts._maxWidth > clientWidth) opts.width = clientWidth - 30;
612
- }
613
-
614
- Promise.all([
615
- this.import(this.__D3),
616
- this.import(this.__LCHART)
617
- ]).then((m) => {
618
-
619
- let chart = this._chart = new (m[1] || Elevation).Chart(opts, this);
620
-
621
- this._x = this._chart._x;
622
- this._y = this._chart._y;
623
-
624
- d3
625
- .select(container)
626
- .call(chart.render())
627
-
628
- chart
629
- .on('reset_drag', this._hideMarker, this)
630
- .on('mouse_enter', this._onMouseEnter, this)
631
- .on('dragged', this._onDragEnd, this)
632
- .on('mouse_move', this._onMouseMove, this)
633
- .on('mouse_out', this._onMouseOut, this)
634
- .on('ruler_filter', this._onRulerFilter, this)
635
- .on('zoom', this._updateChart, this)
636
- .on('elepath_toggle', this._onToggleChart, this)
637
- .on('margins_updated', this._resizeChart, this);
638
-
639
-
640
- this.fire("elechart_init");
641
-
642
- map
643
- .on('zoom viewreset zoomanim', this._hideMarker, this)
644
- .on('resize', this._resetView, this)
645
- .on('resize', this._resizeChart, this)
646
- .on('rotate', this._rotateMarker, this)
647
- .on('mousedown', this._resetDrag, this);
648
-
649
- _.on(map.getContainer(), 'mousewheel', this._resetDrag, this);
650
- _.on(map.getContainer(), 'touchstart', this._resetDrag, this);
651
- _.on(document, 'keydown', this._onKeyDown, this);
652
-
653
- this
654
- .on('eledata_added eledata_loaded', this._updateChart, this)
655
- .on('eledata_added eledata_loaded', this._updateSummary, this);
656
-
657
- this._updateChart();
658
- this._updateSummary();
659
- });
660
-
661
-
662
- },
663
-
664
- _initLayer() {
665
- this._layers
666
- .on('layeradd layerremove', ({layer, type}) => {
667
- let node = layer.getElement && layer.getElement();
668
- _.toggleClass(node, this.options.polyline.className + ' ' + this.options.theme, type == 'layeradd');
669
- _.toggleEvent(layer, "mousemove", this._onMouseMoveLayer.bind(this), type == 'layeradd')
670
- _.toggleEvent(layer, "mouseout", this._onMouseOut.bind(this), type == 'layeradd');
671
- });
672
- },
673
-
674
- _initMarker(map) {
675
- let pane = map.getPane('elevationPane');
676
- if (!pane) {
677
- pane = this._pane = map.createPane('elevationPane', map.getPane('norotatePane') || map.getPane('mapPane'));
678
- pane.style.zIndex = 625; // This pane is above markers but below popups.
679
- pane.style.pointerEvents = 'none';
680
- }
681
-
682
- if (this._renderer) this._renderer.remove()
683
- this._renderer = L.svg({ pane: "elevationPane" }).addTo(this._map); // default leaflet svg renderer
684
-
685
- Promise.all([
686
- this.import(this.__D3),
687
- this.import(this.__LMARKER)
688
- ]).then((m) => {
689
- this._marker = new (m[1] || Elevation).Marker(this.options, this);
690
- this.fire("elechart_marker");
691
- });
692
- },
693
-
694
- /**
695
- * Inspired by L.Control.Layers
696
- */
697
- _initButton(container) {
698
- L.DomEvent
699
- .disableClickPropagation(container)
700
- .disableScrollPropagation(container);
701
-
702
- this.options.collapsed ? this._collapse() : this._expand();
703
-
704
- if (this.options.autohide) {
705
- _.on(container, 'mouseover', this._expand, this);
706
- _.on(container, 'mouseout', this._collapse, this);
707
- this._map.on('click', this._collapse, this);
708
- }
709
-
710
- if (this.options.closeBtn) {
711
- let link = this._button = _.create('a', "elevation-toggle-icon", { href: '#', title: L._('Elevation'), }, container);
712
- _.on(link, 'click', L.DomEvent.stop);
713
- _.on(link, 'click', this._toggle, this);
714
- _.on(link, 'focus', this._toggle, this);
715
- fetch(_.resolveURL(this.__btnIcon, this.options.srcFolder)).then(r => r.ok && r.text().then(img => link.innerHTML = img));
716
- }
717
- },
718
-
719
- _initSummary(container) {
720
- this.import(this.__LSUMMARY).then((m)=>{
721
- this._summary = new (m || Elevation).Summary({ summary: this.options.summary }, this);
722
-
723
- this.on('elechart_init', () => {
724
- d3.select(container).call(this._summary.render());
725
- });
726
- });
727
- },
728
-
729
- /**
730
- * Retrieve data from a remote url (HTTP).
731
- */
732
- _loadFile(url) {
733
- fetch(url)
734
- .then((response) => response.text())
735
- .then((data) => {
736
- this._downloadURL = url; // TODO: handle multiple urls?
737
- this._parseFromString(data)
738
- .then( geojson => geojson && this._loadLayer(geojson));
739
- }).catch((err) => console.warn(err));
740
- },
741
-
742
- /**
743
- * Dynamically import only required javascript modules (code splitting)
744
- */
745
- _loadModules(handlers) {
746
- // First map known classnames (eg. "Altitude" --> L.Control.Elevation.Altitude)
747
- handlers = handlers.map((h) => typeof h === 'string' && typeof Elevation[h] !== "undefined" ? Elevation[h] : h);
748
- // Then load optional classes and custom imports (eg. "Cadence" --> import('../src/handlers/cadence.js'))
749
- let modules = handlers.map(file => (typeof file === 'string' && this.import(this.__modulesFolder + file.toLowerCase() + '.js')) || (file instanceof Promise && file) || Promise.resolve());
750
- return Promise.all(modules).then((m) => {
751
- _.each(m, (exported, i) => {
752
- let fn = exported && Object.keys(exported)[0];
753
- if (fn) {
754
- handlers[i] = Elevation[fn] = (Elevation[fn] ?? exported[fn]);
755
- }
756
- });
757
- _.each(handlers, h => ["function", "object"].includes(typeof h) && this._registerHandler(h));
758
- });
759
- },
760
-
761
- /**
762
- * Simple GeoJSON data loader (L.GeoJSON).
763
- */
764
- _loadLayer(geojson) {
765
- let { polyline, theme, waypoints, wptIcons, wptLabels, distanceMarkers } = this.options;
766
- let style = L.extend({}, polyline);
767
-
768
- if (theme) {
769
- style.className += ' ' + theme;
770
- }
771
-
772
- if (geojson.name) {
773
- this.track_info.name = geojson.name;
774
- }
775
-
776
- let layer = L.geoJson(geojson, {
777
- distanceMarkers: distanceMarkers,
778
- style: style,
779
- pointToLayer: (feature, latlng) => {
780
- if (waypoints) {
781
- let { desc, name, sym } = feature.properties;
782
- desc = desc || '';
783
- name = name || '';
784
- // Handle chart waypoints (dots)
785
- if ([true, 'dots'].includes(waypoints)) {
786
- this._registerCheckPoint({
787
- latlng: latlng,
788
- label : ([true, 'dots'].includes(wptLabels) ? name : '')
789
- });
790
- }
791
- // Handle map waypoints (markers)
792
- if ([true, 'markers'].includes(waypoints) && wptIcons != false) {
793
- return this._registerMarker({
794
- latlng : latlng,
795
- sym : (sym ?? name).replace(' ', '-').replace('"', '').replace("'", '').toLowerCase(),
796
- content: [true, 'markers'].includes(wptLabels) && (name || desc) && decodeURI("<b>" + name + "</b>" + (desc.length > 0 ? '<br>' + desc : ''))
797
- });
798
- }
799
- }
800
- },
801
- onEachFeature: (feature, layer) => feature.geometry && feature.geometry.type != 'Point' && this.addData(feature, layer),
802
- });
803
-
804
- this.import(this.__D3).then(() => {
805
- this._initMapIntegrations(layer);
806
- this._fireEvt("eledata_loaded", { data: geojson, layer: layer, name: this.track_info.name, track_info: this.track_info });
807
- });
808
-
809
- return layer;
810
- },
811
-
812
- _onDragEnd({ dragstart, dragend}) {
813
- this._hideMarker();
814
- this.fitBounds(L.latLngBounds([dragstart.latlng, dragend.latlng]));
815
-
816
- this.fire("elechart_dragged");
817
- },
818
-
819
- _onKeyDown({key}) {
820
- if(!this.options.detached && key === "Escape"){
821
- this._collapse()
822
- };
823
- },
824
-
825
- /**
826
- * Trigger mouseenter event.
827
- */
828
- _onMouseEnter() {
829
- this.fire('elechart_enter');
830
- },
831
-
832
- /*
833
- * Handles the moueseover the chart and displays distance and altitude level.
834
- */
835
- _onMouseMove({xCoord}) {
836
- if (this._chartEnabled && this._data.length) {
837
- let item = this._findItemForX(xCoord);
838
- if (item) {
839
- if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord);
840
-
841
- this._updateMarker(item);
842
- this._setMapView(item);
843
-
844
- if (this._map) {
845
- _.addClass(this._map.getContainer(), 'elechart-hover');
846
- }
847
-
848
- this.fire("elechart_change", { data: item, xCoord: xCoord });
849
- this.fire("elechart_hover", { data: item, xCoord: xCoord });
850
- }
851
- }
852
- },
853
-
854
- /*
855
- * Handles mouseover events of the data layers on the map.
856
- */
857
- _onMouseMoveLayer({latlng}) {
858
- if (this._data.length) {
859
- let item = this._findItemForLatLng(latlng);
860
- if (item) {
861
- let xCoord = item.xDiagCoord;
862
-
863
- if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord);
864
-
865
- this._updateMarker(item);
866
-
867
- this.fire("elechart_change", { data: item, xCoord: xCoord });
868
- }
869
- }
870
- },
871
-
872
- /*
873
- * Handles the moueseout over the chart.
874
- */
875
- _onMouseOut() {
876
- if (!this.options.detached) {
877
- this._hideMarker();
878
- this._chart._hideDiagramIndicator();
879
- }
880
-
881
- if (this._map) {
882
- _.removeClass(this._map.getContainer(), 'elechart-hover');
883
- }
884
-
885
- this.fire("elechart_leave");
886
- },
887
-
888
- /**
889
- * Handles the drag event over the ruler filter.
890
- */
891
- _onRulerFilter({coords}) {
892
- this._updateMapSegments(coords);
893
- },
894
-
895
- /**
896
- * Toggle chart data on legend click
897
- */
898
- _onToggleChart({ name, enabled }) {
899
-
900
- this._chartEnabled = this._chart._hasActiveLayers();
901
-
902
- // toggle layer visibility on empty chart
903
- this._layers.eachLayer(layer => _.toggleClass(layer.getElement && layer.getElement(), this.options.polyline.className + ' ' + this.options.theme, this._chartEnabled));
904
-
905
- // toggle option value (eg. altitude = { 'disabled' || 'enabled' })
906
- this.options[name] = !enabled && this.options[name] == 'disabled' ? 'enabled' : 'disabled';
907
-
908
- // remove marker on empty chart
909
- if (!this._chartEnabled) {
910
- this._chart._hideDiagramIndicator();
911
- this._marker.remove();
912
- }
913
- },
914
-
915
- /**
916
- * Simple GeoJSON Parser
917
- */
918
- _parseFromGeoJSONString(data) {
919
- try {
920
- return JSON.parse(data);
921
- } catch (e) { }
922
- },
923
-
924
- /**
925
- * Attempt to parse raw response data (GeoJSON or XML > GeoJSON)
926
- */
927
- _parseFromString(data) {
928
- return new Promise(resolve =>
929
- this.import(this.__TOGEOJSON).then(() => {
930
- let geojson;
931
- try {
932
- geojson = this._parseFromXMLString(data.trim());
933
- } catch (e) {
934
- geojson = this._parseFromGeoJSONString(data.toString());
935
- }
936
- if (geojson) {
937
- geojson.name = geojson.name || (this._downloadURL || '').split('/').pop().split('#')[0].split('?')[0];
938
- }
939
- resolve(geojson);
940
- })
941
- );
942
- },
943
-
944
- /**
945
- * Simple XML Parser (GPX, KML, TCX)
946
- */
947
- _parseFromXMLString(data) {
948
- if (data.indexOf("<") != 0) {
949
- throw 'Invalid XML';
950
- }
951
- let xml = (new DOMParser()).parseFromString(data, "text/xml");
952
- let type = xml.documentElement.tagName.toLowerCase(); // "kml" or "gpx"
953
- let name = xml.getElementsByTagName('name');
954
- if (xml.getElementsByTagName('parsererror').length) {
955
- throw 'Invalid XML';
956
- }
957
- if (!(type in toGeoJSON)) {
958
- type = xml.documentElement.tagName == "TrainingCenterDatabase" ? 'tcx' : 'gpx';
959
- }
960
- let geojson = toGeoJSON[type](xml);
961
- geojson.name = name.length > 0 ? (Array.from(name).find(tag => tag.parentElement.tagName == "trk") ?? name[0]).textContent : '';
962
- return geojson;
963
- },
964
-
965
- /**
966
- * Add chart profile to diagram
967
- */
968
- _registerAreaPath(props) {
969
- this.on("elechart_init", () => this._chart._registerAreaPath(props));
970
- },
971
-
972
- /**
973
- * Add chart grid to diagram
974
- */
975
- _registerAxisGrid(props) {
976
- this.on("elechart_axis", () => this._chart._registerAxisGrid(props));
977
- },
978
-
979
- /**
980
- * Add chart axis to diagram
981
- */
982
- _registerAxisScale(props) {
983
- this.on("elechart_axis", () => this._chart._registerAxisScale(props));
984
- },
985
-
986
- /**
987
- * Add a point of interest over the diagram
988
- */
989
- _registerCheckPoint(props) {
990
- const cb = () => this._chart._registerCheckPoint(props);
991
- this
992
- .on("elechart_updated", cb)
993
- .once("eledata_clear", () => this.off("elechart_updated", cb));
994
- },
995
-
996
- /**
997
- * Base handler for iterative track statistics (dist, time, z, slope, speed, acceleration, ...)
998
- */
999
- _registerDataAttribute(props) {
1000
-
1001
- // parse of "coordinateProperties" for later usage
1002
- if (props.coordPropsToMeta) {
1003
- this.on("elepoint_init", (e) => props.coordPropsToMeta.call(this, e));
1004
- }
1005
-
1006
- // prevent excessive variabile instanstations
1007
- let i, curr, prev, attr = props.attr || props.name;
1008
-
1009
- // save here a reference to last used point
1010
- let lastValid = {};
1011
-
1012
- // iteration
1013
- this.on("elepoint_added", ({index, point}) => {
1014
- i = index;
1015
-
1016
- prev = curr ?? this._data[i]; // same as: this._data[i > 0 ? i - 1 : i]
1017
- curr = this._data[i];
1018
-
1019
- // retrieve point value
1020
- curr[attr] = props.pointToAttr.call(this, point, i);
1021
-
1022
- // check and fix missing data on last added point
1023
- if (i > 0 && isNaN(prev[attr])) {
1024
- if (!isNaN(lastValid[attr]) && !isNaN(curr[attr])) {
1025
- prev[attr] = (lastValid[attr] + curr[attr]) / 2;
1026
- } else if (!isNaN(lastValid[attr])) {
1027
- prev[attr] = lastValid[attr];
1028
- } else if (!isNaN(curr[attr])) {
1029
- prev[attr] = curr[attr];
1030
- }
1031
- // update "yAttr" and "xAttr"
1032
- if (props.meta) {
1033
- prev[props.meta] = prev[attr];
1034
- }
1035
- }
1036
-
1037
- // skip to next iteration for invalid or missing data (eg. i == 0)
1038
- if (isNaN(curr[attr])) {
1039
- return;
1040
- }
1041
-
1042
- // update reference to last used point
1043
- lastValid[attr] = curr[attr];
1044
-
1045
- // Limit "crazy" delta values.
1046
- if (props.deltaMax) {
1047
- curr[attr] =_.wrapDelta(curr[attr], prev[attr], props.deltaMax);
1048
- }
1049
-
1050
- // Range of acceptable values.
1051
- if (props.clampRange) {
1052
- curr[attr] = _.clamp(curr[attr], props.clampRange);
1053
- }
1054
-
1055
- // Limit floating point precision.
1056
- if (!isNaN(props.decimals)) {
1057
- curr[attr] = _.round(curr[attr], props.decimals);
1058
- }
1059
-
1060
- // update "track_info" stats (min, max, avg, ...)
1061
- if (props.stats) {
1062
- for (const key in props.stats) {
1063
- let sname = (props.statsName || attr) + (key != '' ? '_' : '');
1064
- this.track_info[sname + key] = props.stats[key].call(this, curr[attr], this.track_info[sname + key], this._data.length);
1065
- }
1066
- }
1067
-
1068
- // update here some mixins (eg. complex "track_info" stuff)
1069
- if (props.onPointAdded) props.onPointAdded.call(this, curr[attr], i, point);
1070
- });
1071
- },
1072
-
1073
- /**
1074
- * Parse a module definition and attach related function listeners
1075
- */
1076
- _registerHandler(props) {
1077
-
1078
- // eg. L.Control.Altitude
1079
- if (typeof props === "function") {
1080
- return this._registerHandler(props.call(this));
1081
- }
1082
-
1083
- let {
1084
- name,
1085
- attr,
1086
- required,
1087
- deltaMax,
1088
- clampRange,
1089
- decimals,
1090
- meta,
1091
- unit,
1092
- coordinateProperties,
1093
- coordPropsToMeta,
1094
- pointToAttr,
1095
- onPointAdded,
1096
- stats,
1097
- statsName,
1098
- grid,
1099
- scale,
1100
- path,
1101
- tooltip,
1102
- summary
1103
- } = props;
1104
-
1105
- // eg. "altitude" == true
1106
- if (this.options[name] || required) {
1107
-
1108
- this._registerDataAttribute({
1109
- name,
1110
- attr,
1111
- meta,
1112
- deltaMax,
1113
- clampRange,
1114
- decimals,
1115
- coordPropsToMeta: _.coordPropsToMeta(coordinateProperties, meta || name, coordPropsToMeta),
1116
- pointToAttr,
1117
- onPointAdded,
1118
- stats,
1119
- statsName,
1120
- });
1121
-
1122
- if (grid) {
1123
- this._registerAxisGrid(L.extend({ name }, grid));
1124
- }
1125
-
1126
- if (this.options[name] !== "summary") {
1127
- if (scale) this._registerAxisScale(L.extend({ name, label: unit }, scale));
1128
- if (path) this._registerAreaPath(L.extend({ name }, path));
1129
- }
1130
-
1131
- if (tooltip || props.tooltips) {
1132
- _.each([tooltip, ...(props.tooltips || [])], t => t && this._registerTooltip(L.extend({ name }, t)));
1133
- }
1134
-
1135
- if (summary) {
1136
- _.each(summary, (s, k) => summary[k] = L.extend({ unit }, s));
1137
- this._registerSummary(summary);
1138
- }
1139
- }
1140
- },
1141
-
1142
- _registerMarker({latlng, sym, content}) {
1143
- let { wptIcons } = this.options;
1144
- // generate and cache appropriate icon symbol
1145
- if (!wptIcons.hasOwnProperty(sym)) {
1146
- wptIcons[sym] = L.divIcon(L.extend({}, wptIcons[""].options, { html: '<i class="elevation-waypoint-icon ' + sym + '"></i>' } ));
1147
- }
1148
- let marker = L.marker(latlng, { icon: wptIcons[sym] });
1149
- if (content) {
1150
- marker.bindPopup(content, { className: 'elevation-popup', keepInView: true }).openPopup();
1151
- marker.bindTooltip(content, { className: 'elevation-tooltip', direction: 'auto', sticky: true, opacity: 1 }).openTooltip();
1152
- }
1153
- return this._addMarker(marker)
1154
- },
1155
-
1156
- /**
1157
- * Add chart or marker tooltip info
1158
- */
1159
- _registerTooltip(props) {
1160
- props.chart && this.on("elechart_init", () => this._chart._registerTooltip(L.extend({}, props, { value: props.chart })));
1161
- props.marker && this.on("elechart_marker", () => this._marker._registerTooltip(L.extend({}, props, { value: props.marker })));
1162
- },
1163
-
1164
- /**
1165
- * Add summary info to diagram
1166
- */
1167
- _registerSummary(props) {
1168
- this.on('elechart_summary', () => this._summary._registerSummary(props));
1169
- },
1170
-
1171
- /*
1172
- * Removes the drag rectangle and zoms back to the total extent of the data.
1173
- */
1174
- _resetDrag() {
1175
- this._chart._resetDrag();
1176
- this._hideMarker();
1177
- },
1178
-
1179
- /**
1180
- * Resets drag, marker and bounds.
1181
- */
1182
- _resetView() {
1183
- if (this._map && this._map._isFullscreen) return;
1184
- this._resetDrag();
1185
- this._hideMarker();
1186
- if (this.options.autofitBounds) {
1187
- this.fitBounds();
1188
- }
1189
- },
1190
-
1191
- /**
1192
- * Hacky way for handling chart resize. Deletes it and redraw chart.
1193
- */
1194
- _resizeChart() {
1195
- if (this._container && _.style(this._container, "display") != "none") {
1196
- let opts = this.options;
1197
- let newWidth = opts.detached ? (this.eleDiv || this._container).offsetWidth : _.clamp(opts._maxWidth, [0, this._map.getContainer().clientWidth - 30]);
1198
- if (newWidth) {
1199
- opts.width = newWidth;
1200
- if (this._chart && this._chart._chart) {
1201
- this._chart._chart._resize(opts);
1202
- this._updateChart();
1203
- }
1204
- }
1205
- this._updateMapSegments();
1206
- }
1207
- },
1208
-
1209
- /**
1210
- * Collapse or Expand chart control.
1211
- */
1212
- _toggle() {
1213
- _.hasClass(this._container, "elevation-expanded") ? this._collapse() : this._expand();
1214
- },
1215
-
1216
- /**
1217
- * Update map center and zoom (followMarker: true)
1218
- */
1219
- _setMapView(item) {
1220
- if (this._map && this.options.followMarker) {
1221
- let zoom = this._map.getZoom();
1222
- let z = this.options.zFollow;
1223
- if (typeof z === "number") {
1224
- this._map.setView(item.latlng, (zoom < z ? z : zoom), { animate: true, duration: 0.25 });
1225
- } else if (!this._map.getBounds().contains(item.latlng)) {
1226
- this._map.setView(item.latlng, zoom, { animate: true, duration: 0.25 });
1227
- }
1228
- }
1229
- },
1230
-
1231
- /**
1232
- * Calculates [x, y] domain and then update chart.
1233
- */
1234
- _updateChart() {
1235
- if (this._chart && this._container) {
1236
- this.fire("elechart_axis");
1237
-
1238
- this._chart.update({ data: this._data, options: this.options });
1239
-
1240
- this._x = this._chart._x;
1241
- this._y = this._chart._y;
1242
-
1243
- this.fire('elechart_updated');
1244
- }
1245
- },
1246
-
1247
- /*
1248
- * Update the position/height indicator marker drawn onto the map
1249
- */
1250
- _updateMarker(item) {
1251
- if (this._marker) {
1252
- this._marker.update({
1253
- map : this._map,
1254
- item : item,
1255
- yCoordMax : this._yCoordMax || 0,
1256
- options : this.options
1257
- });
1258
- }
1259
- },
1260
-
1261
- /**
1262
- * Fix marker rotation on rotated maps
1263
- */
1264
- _rotateMarker() {
1265
- if (this._marker) {
1266
- this._marker.update();
1267
- }
1268
- },
1269
-
1270
- /**
1271
- * Highlight track segments on the map.
1272
- */
1273
- _updateMapSegments(coords) {
1274
- this._markedSegments.setLatLngs(coords || []);
1275
- if (coords && this._map && !this._map.hasLayer(this._markedSegments)) {
1276
- this._markedSegments.addTo(this._map);
1277
- }
1278
- },
1279
-
1280
- /**
1281
- * Update chart summary.
1282
- */
1283
- _updateSummary() {
1284
- if (this._summary) {
1285
- this._summary.reset();
1286
- if (this.options.summary) {
1287
- this.fire("elechart_summary");
1288
- this._summary.update();
1289
- }
1290
- if (this.options.downloadLink && this._downloadURL) { // TODO: generate dynamically file content instead of using static file urls.
1291
- this._summary._container.innerHTML += '<span class="download"><a href="#">' + L._('Download') + '</a></span>'
1292
- _.select('.download a', this._summary._container).onclick = (e) => {
1293
- e.preventDefault();
1294
- let event = { downloadLink: this.options.downloadLink, confirm: _.saveFile.bind(this, this._downloadURL) };
1295
- if (this.options.downloadLink == 'modal' && typeof CustomEvent === "function") {
1296
- document.dispatchEvent(new CustomEvent("eletrack_download", { detail: event }));
1297
- } else if (this.options.downloadLink == 'link' || this.options.downloadLink === true) {
1298
- event.confirm();
1299
- }
1300
- this.fire('eletrack_download', event);
1301
- };
1302
- }
1303
- }
1304
- },
1305
-
1306
-
1307
- /**
1308
- * Calculates chart width.
1309
- */
1310
- _width() {
1311
- if (this._chart) return this._chart._width();
1312
- const { width, margins } = this.options;
1313
- return width - margins.left - margins.right;
1314
- },
1315
-
1316
- /**
1317
- * Calculates chart height.
1318
- */
1319
- _height() {
1320
- if (this._chart) return this._chart._height();
1321
- const { height, margins } = this.options;
1322
- return height - margins.top - margins.bottom;
1323
- },
1324
-
1325
- });
1
+ import * as _ from './utils';
2
+ import { Options } from './options';
3
+
4
+ // "leaflet-i18n" fallback
5
+ if (!L._ || !L.i18n) {
6
+ L._ = L.i18n = (string, data) => string;
7
+ }
8
+
9
+ export const Elevation = L.Control.Elevation = L.Control.extend({
10
+
11
+ includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
12
+
13
+ options: Options,
14
+ __mileFactor: 0.621371, // 1 km = (0.621371 mi)
15
+ __footFactor: 3.28084, // 1 m = (3.28084 ft)
16
+ __D3: 'https://unpkg.com/d3@7.8.4/dist/d3.min.js',
17
+ __TOGEOJSON: 'https://unpkg.com/@tmcw/togeojson@5.6.0/dist/togeojson.umd.js',
18
+ __LGEOMUTIL: 'https://unpkg.com/leaflet-geometryutil@0.10.1/src/leaflet.geometryutil.js',
19
+ __LALMOSTOVER: 'https://unpkg.com/leaflet-almostover@1.0.1/src/leaflet.almostover.js',
20
+ __LHOTLINE: '../libs/leaflet-hotline.min.js',
21
+ __LDISTANCEM: '../libs/leaflet-distance-marker.min.js',
22
+ __LEDGESCALE: '../libs/leaflet-edgescale.min.js',
23
+ __LCHART: '../src/components/chart.js',
24
+ __LMARKER: '../src/components/marker.js',
25
+ __LSUMMARY: '../src/components/summary.js',
26
+ __modulesFolder: '../src/handlers/',
27
+ __btnIcon: '../images/elevation.svg',
28
+
29
+ /*
30
+ * Add data to the diagram either from GPX or GeoJSON and update the axis domain and data
31
+ */
32
+ addData(d, layer) {
33
+ this.import(this.__D3)
34
+ .then(() => {
35
+ if (this._modulesLoaded) {
36
+ layer = layer ?? (d.on && d);
37
+ this._addData(d);
38
+ this._addLayer(layer);
39
+ this._fireEvt("eledata_added", { data: d, layer: layer, track_info: this.track_info });
40
+ } else {
41
+ this.once('modules_loaded', () => this.addData(d,layer));
42
+ }
43
+ });
44
+ },
45
+
46
+ /**
47
+ * Adds the control to the given map.
48
+ */
49
+ addTo(map) {
50
+ if (this.options.detached) {
51
+ let parent = _.select(this.options.elevationDiv);
52
+ let eleDiv = this.onAdd(map);
53
+ parent ? _.append(parent, eleDiv) : _.insert(map.getContainer(), eleDiv, 'afterend');
54
+ } else {
55
+ L.Control.prototype.addTo.call(this, map);
56
+ }
57
+ return this;
58
+ },
59
+
60
+ /*
61
+ * Reset data and display
62
+ */
63
+ clear() {
64
+ if (this._marker) this._marker.remove();
65
+ if (this._chart) this._clearChart();
66
+ if (this._layers) this._clearLayers(this._layers);
67
+ if (this._markers) this._clearLayers(this._markers);
68
+ if (this._circleMarkers) this._circleMarkers.remove();
69
+ if (this._hotline) this._hotline.eachLayer(l => l.options.renderer.remove()); // hotfix for: https://github.com/Raruto/leaflet-elevation/issues/233
70
+ if (this._hotline) this._clearLayers(this._hotline);
71
+
72
+ this._data = [];
73
+ this.track_info = {};
74
+
75
+ this._fireEvt("eledata_clear");
76
+
77
+ this._updateChart();
78
+ },
79
+
80
+ _clearChart() {
81
+ if (this._events && this._events.elechart_updated) {
82
+ this._events.elechart_updated.forEach(({fn, ctx}) => this.off('elechart_updated', fn, ctx));
83
+ }
84
+ if (this._chart && this._chart._container) {
85
+ this._chart._container.selectAll('g.point .point').remove();
86
+ this._chart.clear();
87
+ }
88
+ },
89
+
90
+ _clearLayers(l) {
91
+ l = l || this._layers;
92
+ if (l && l.eachLayer) {
93
+ l.eachLayer(f => f.remove())
94
+ l.clearLayers();
95
+ }
96
+ },
97
+
98
+ /**
99
+ * TODO: Create a base class to handle custom data attributes (heart rate, cadence, temperature, ...)
100
+ *
101
+ * @link https://leafletjs.com/examples/extending/extending-3-controls.html#handlers
102
+ */
103
+ // addHandler: function (name, HandlerClass) {
104
+ // if (HandlerClass) {
105
+ // let handler = this[name] = new HandlerClass(this);
106
+ // this.handlers.push(handler);
107
+ // if (this.options[name]) {
108
+ // handler.enable();
109
+ // }
110
+ // }
111
+ // return this;
112
+ // },
113
+
114
+ /**
115
+ * Disable chart brushing.
116
+ */
117
+ disableBrush() {
118
+ this._chart._brushEnabled = false;
119
+ this._resetDrag();
120
+ },
121
+
122
+ /**
123
+ * Enable chart brushing.
124
+ */
125
+ enableBrush() {
126
+ this._chart._brushEnabled = true;
127
+ },
128
+
129
+ /**
130
+ * Disable chart zooming.
131
+ */
132
+ disableZoom() {
133
+ this._chart._zoomEnabled = false;
134
+ this._chart._resetZoom();
135
+ },
136
+
137
+ /**
138
+ * Enable chart zooming.
139
+ */
140
+ enableZoom() {
141
+ this._chart._zoomEnabled = true;
142
+ },
143
+
144
+ /**
145
+ * Sets a map view that contains the given geographical bounds.
146
+ */
147
+ fitBounds(bounds) {
148
+ bounds = bounds || this.getBounds();
149
+ if (this._map && bounds.isValid()) this._map.fitBounds(bounds);
150
+ },
151
+
152
+ getBounds(data) {
153
+ return L.latLngBounds((data || this._data).map((d) => d.latlng));
154
+ },
155
+
156
+ /**
157
+ * Get default zoom level (followMarker: true).
158
+ */
159
+ getZFollow() {
160
+ return this.options.zFollow;
161
+ },
162
+
163
+ /**
164
+ * Hide current elevation chart profile.
165
+ */
166
+ hide() {
167
+ _.style(this._container, "display", "none");
168
+ },
169
+
170
+ /**
171
+ * Initialize chart control "options" and "container".
172
+ */
173
+ initialize(opts) {
174
+
175
+ // opts = L.setOptions(this, opts);
176
+
177
+ // Fixes: https://github.com/Raruto/leaflet-elevation/pull/240
178
+ opts = L.setOptions(this, L.extend({}, _.cloneDeep(Options), opts)); // "deep copy" nested objects (multiple charts)
179
+
180
+ this._data = [];
181
+ this._layers = L.featureGroup();
182
+ this._markers = L.featureGroup();
183
+ this._hotline = L.featureGroup();
184
+ this._circleMarkers = L.featureGroup();
185
+ this._markedSegments = L.polyline([]);
186
+ this._start = L.circleMarker([0,0], (opts.trkStart || Options.trkStart));
187
+ this._end = L.circleMarker([0,0], (opts.trkEnd || Options.trkEnd));
188
+ this._chartEnabled = true;
189
+ this._yCoordMax = -Infinity;
190
+ this.track_info = {};
191
+ // this.handlers = [];
192
+
193
+ if (opts.followMarker) this._setMapView = _.throttle(this._setMapView, 300, this);
194
+ if (opts.legend) opts.margins.bottom += 30;
195
+ if (opts.theme) opts.polylineSegments.className += ' ' + opts.theme;
196
+ if (opts.wptIcons === true) opts.wptIcons = Options.wptIcons;
197
+ if (opts.distanceMarkers === true) opts.distanceMarkers = Options.distanceMarkers;
198
+ if (opts.trkStart) this._start.addTo(this._circleMarkers);
199
+ if (opts.trkEnd) this._end.addTo(this._circleMarkers);
200
+
201
+
202
+ this._markedSegments.setStyle(opts.polylineSegments);
203
+
204
+ // Leaflet canvas renderer colors
205
+ L.extend(_.Colors, opts.colors || {});
206
+
207
+ // Various stuff
208
+ this._fixCanvasPaths();
209
+ this._fixTooltipSize();
210
+
211
+ },
212
+
213
+ /**
214
+ * Javascript scripts downloader (lazy loader)
215
+ */
216
+ import(src, condition) {
217
+ switch(src) {
218
+ case this.__D3: condition = typeof d3 !== 'object'; break;
219
+ case this.__TOGEOJSON: condition = typeof toGeoJSON !== 'object'; break;
220
+ case this.__LGEOMUTIL: condition = typeof L.GeometryUtil !== 'object'; break;
221
+ case this.__LALMOSTOVER: condition = typeof L.Handler.AlmostOver !== 'function'; break;
222
+ case this.__LDISTANCEM: condition = typeof L.DistanceMarkers !== 'function'; break;
223
+ case this.__LEDGESCALE: condition = typeof L.Control.EdgeScale !== 'function'; break;
224
+ case this.__LHOTLINE: condition = typeof L.Hotline !== 'function'; break;
225
+ }
226
+ return condition !== false ? import(_.resolveURL(src, this.options.srcFolder)) : Promise.resolve();
227
+ },
228
+
229
+ /**
230
+ * Load elevation data (GPX, GeoJSON, KML or TCX).
231
+ */
232
+ load(data) {
233
+ this._parseFromString(data).then( geojson => geojson ? this._loadLayer(geojson) : this._loadFile(data));
234
+ },
235
+
236
+ /**
237
+ * Create container DOM element and related event listeners.
238
+ * Called on control.addTo(map).
239
+ */
240
+ onAdd(map) {
241
+ this._map = map;
242
+
243
+ let container = this._container = _.create("div", "elevation-control " + this.options.theme + " " + (this.options.detached ? 'elevation-detached' : 'leaflet-control'), this.options.detached ? { id: 'elevation-' + _.randomId() } : {});
244
+
245
+ if (!this.eleDiv) this.eleDiv = container;
246
+
247
+ this._loadModules(this.options.handlers).then(() => { // Inject here required modules (data handlers)
248
+ this._initChart(container);
249
+ this._initButton(container);
250
+ this._initSummary(container);
251
+ this._initMarker(map);
252
+ this._initLayer(map);
253
+ this._modulesLoaded = true;
254
+ this.fire('modules_loaded');
255
+ });
256
+
257
+ this.fire('add');
258
+
259
+ return container;
260
+ },
261
+
262
+ /**
263
+ * Clean up control code and related event listeners.
264
+ * Called on control.remove().
265
+ */
266
+ onRemove(map) {
267
+ this._container = null;
268
+
269
+ map
270
+ .off('zoom viewreset zoomanim', this._hideMarker, this)
271
+ .off('resize', this._resetView, this)
272
+ .off('resize', this._resizeChart, this)
273
+ .off('mousedown', this._resetDrag, this);
274
+
275
+ _.off(map.getContainer(), 'mousewheel', this._resetDrag, this);
276
+ _.off(map.getContainer(), 'touchstart', this._resetDrag, this);
277
+ _.off(document, 'keydown', this._onKeyDown, this);
278
+
279
+ this
280
+ .off('eledata_added eledata_loaded', this._updateChart, this)
281
+ .off('eledata_added eledata_loaded', this._updateSummary, this);
282
+
283
+ this.fire('remove');
284
+ },
285
+
286
+ /**
287
+ * Redraws the chart control. Sometimes useful after screen resize.
288
+ */
289
+ redraw() {
290
+ this._resizeChart();
291
+ },
292
+
293
+ /**
294
+ * Set default zoom level (followMarker: true).
295
+ */
296
+ setZFollow(zoom) {
297
+ this.options.zFollow = zoom;
298
+ },
299
+
300
+ /**
301
+ * Hide current elevation chart profile.
302
+ */
303
+ show() {
304
+ _.style(this._container, "display", "block");
305
+ },
306
+
307
+ /*
308
+ * Parsing data either from GPX or GeoJSON and update the diagram data
309
+ */
310
+ _addData(d) {
311
+ if (!d) {
312
+ return;
313
+ }
314
+
315
+ // Standard GeoJSON
316
+ if (d.type === "FeatureCollection" ) {
317
+ return _.each(d.features, feature => this._addData(feature));
318
+ } else if (d.type === "Feature") {
319
+ let geom = d.geometry;
320
+ if (geom) {
321
+ switch (geom.type) {
322
+ case 'LineString': return this._addGeoJSONData(geom.coordinates, d.properties);
323
+ case 'MultiLineString': return _.each(geom.coordinates, (coords, i) => this._addGeoJSONData(coords, d.properties, i));
324
+ case 'Point':
325
+ default: return console.warn('Unsopperted GeoJSON feature geometry type:' + geom.type);
326
+ }
327
+ }
328
+ }
329
+
330
+ // Fallback for leaflet layers (eg. L.Gpx)
331
+ if (d._latlngs) {
332
+ return this._addGeoJSONData(d._latlngs, d.feature && d.feature.properties);
333
+ }
334
+
335
+ },
336
+
337
+ /*
338
+ * Parsing of GeoJSON data lines and their elevation in z-coordinate
339
+ */
340
+ _addGeoJSONData(coords, properties, nestingLevel) {
341
+
342
+ // "coordinateProperties" property is generated inside "@tmcw/toGeoJSON"
343
+ let props = (properties && properties.coordinateProperties) || properties;
344
+
345
+ coords.forEach((point, i) => {
346
+
347
+ // GARMIN_EXTENSIONS = ["hr", "cad", "atemp", "wtemp", "depth", "course", "bearing"];
348
+ point.meta = point.meta ?? { time: null, ele: null };
349
+
350
+ point.prev = (attr) => (attr ? this._data[i > 0 ? i - 1 : 0][attr] : this._data[i > 0 ? i - 1 : 0]);
351
+
352
+ this.fire("elepoint_init", { point: point, props: props, id: i, isMulti: nestingLevel });
353
+
354
+ this._addPoint(
355
+ point.lat ?? point[1],
356
+ point.lng ?? point[0],
357
+ point.alt ?? point.meta.ele ?? point[2]
358
+ );
359
+
360
+ this.fire("elepoint_added", { point: point, index: this._data.length - 1 });
361
+
362
+ if (this._yCoordMax < this._data[this._data.length - 1][this.options.yAttr]) this._yCoordMax = this._data[this._data.length - 1][this.options.yAttr];
363
+ });
364
+
365
+ this.fire("eletrack_added", { coords: coords, index: this._data.length - 1 });
366
+ },
367
+
368
+ /*
369
+ * Parse and push a single (x, y, z) point to current elevation profile.
370
+ */
371
+ _addPoint(x, y, z) {
372
+ if (this.options.reverseCoords) {
373
+ [x, y] = [y, x];
374
+ }
375
+
376
+ this._data.push({
377
+ x: x,
378
+ y: y,
379
+ z: z,
380
+ latlng: L.latLng(x, y, z)
381
+ });
382
+
383
+ this.fire("eledata_updated", { index: this._data.length - 1 });
384
+ },
385
+
386
+ _addLayer(layer) {
387
+ if (layer) this._layers.addLayer(layer)
388
+ // Postpone adding the distance markers (lazy: true)
389
+ if (layer && this.options.distanceMarkers && this.options.distanceMarkers.lazy) {
390
+ layer.on('add remove', ({target, type}) => L.DistanceMarkers && target instanceof L.Polyline && target[type + 'DistanceMarkers']());
391
+ }
392
+ return layer;
393
+ },
394
+
395
+ _addMarker(marker) {
396
+ if (marker) this._markers.addLayer(marker)
397
+ return marker;
398
+ },
399
+
400
+ /**
401
+ * Initialize "L.AlmostOver" integration
402
+ */
403
+ _initAlmostOverHandler(map, layer) {
404
+ return (map && this.options.almostOver && !L.Browser.mobile) ? Promise.all([
405
+ this.import(this.__LGEOMUTIL),
406
+ this.import(this.__LALMOSTOVER)
407
+ ]).then(() => {
408
+ map.addHandler('almostOver', L.Handler.AlmostOver)
409
+ if (L.GeometryUtil && map.almostOver && map.almostOver.enabled()) {
410
+ map.almostOver.addLayer(layer);
411
+ map
412
+ .on('almost:move', this._onMouseMoveLayer, this)
413
+ .on('almost:out', this._onMouseOut, this);
414
+ this.once('eledata_clear', () => {
415
+ map.almostOver.removeLayer(layer);
416
+ map
417
+ .off('almost:move', this._onMouseMoveLayer, this)
418
+ .off('almost:out', this._onMouseOut, this);
419
+ })
420
+ }
421
+ }) : Promise.resolve();
422
+ },
423
+
424
+ /**
425
+ * Initialize "L.DistanceMarkers" integration
426
+ */
427
+ _initDistanceMarkers() {
428
+ return this.options.distanceMarkers ? Promise.all([this.import(this.__LGEOMUTIL), this.import(this.__LDISTANCEM)]) : Promise.resolve();
429
+ },
430
+
431
+ /**
432
+ * Initialize "L.Control.EdgeScale" integration
433
+ */
434
+ _initEdgeScale(map) {
435
+ return this.options.edgeScale ? Promise.all([this.import(this.__LEDGESCALE)])
436
+ .then(() => {
437
+ map.edgeScaleControl = map.edgeScaleControl || L.control.edgeScale('boolean' !== typeof this.options.edgeScale ? this.options.edgeScale : {}).addTo(map);
438
+ }) : Promise.resolve();
439
+ },
440
+
441
+ _initHotLine(layer) {
442
+ let prop = typeof this.options.hotline == 'string' ? this.options.hotline : 'elevation';
443
+ return this.options.hotline ? this.import(this.__LHOTLINE)
444
+ .then(() => {
445
+ layer.eachLayer((trkseg) => {
446
+ if(trkseg.feature.geometry.type != "Point") {
447
+ let geo = L.geoJson(trkseg.toGeoJSON(), { coordsToLatLng: (coords) => L.latLng(coords[0], coords[1], coords[2] * (this.options.altitudeFactor || 1))});
448
+ let line = L.hotline(geo.toGeoJSON().features[0].geometry.coordinates, {
449
+ renderer: L.Hotline.renderer(),
450
+ min: isFinite(this.track_info[prop + '_min']) ? this.track_info[prop + '_min'] : 0,
451
+ max: isFinite(this.track_info[prop + '_max']) ? this.track_info[prop + '_max'] : 1,
452
+ palette: {
453
+ 0.0: '#008800',
454
+ 0.5: '#ffff00',
455
+ 1.0: '#ff0000'
456
+ },
457
+ weight: 5,
458
+ outlineColor: '#000000',
459
+ outlineWidth: 1
460
+ }).addTo(this._hotline);
461
+ let alpha = trkseg.options.style && trkseg.options.style.opacity || 1;
462
+ trkseg.on('add remove', ({type}) => {
463
+ trkseg.setStyle({opacity: (type == 'add' ? 0 : alpha)});
464
+ line[(type == 'add' ? 'addTo' : 'removeFrom')](trkseg._map);
465
+ if (line._renderer) line._renderer._container.parentElement.insertBefore(line._renderer._container, line._renderer._container.parentElement.firstChild);
466
+ });
467
+ }
468
+ });
469
+ }) : Promise.resolve();
470
+ },
471
+
472
+ /**
473
+ * Initialize "L.AlmostOver" and "L.DistanceMarkers"
474
+ */
475
+ _initMapIntegrations(layer) {
476
+ let map = this._map;
477
+ if (map) {
478
+ if (this._data.length) {
479
+ this._start.setLatLng(this._data[0].latlng);
480
+ this._end.setLatLng(this._data[this._data.length -1].latlng);
481
+ }
482
+ Promise.all([
483
+ this._initHotLine(layer),
484
+ this._initAlmostOverHandler(map, layer),
485
+ this._initDistanceMarkers(),
486
+ this._initEdgeScale(map),
487
+ ]).then(() => {
488
+ if (this.options.polyline) {
489
+ this._layers.addLayer(layer.addTo(map)); // hotfix for: https://github.com/Raruto/leaflet-elevation/issues/233
490
+ this._circleMarkers.addTo(map);
491
+ }
492
+ if (this.options.autofitBounds) {
493
+ this.fitBounds(layer.getBounds());
494
+ }
495
+ map.invalidateSize();
496
+ });
497
+ } else {
498
+ this.once('add', () => this._initMapIntegrations(layer));
499
+ }
500
+ },
501
+
502
+ /*
503
+ * Collapse current chart control.
504
+ */
505
+ _collapse() {
506
+ _.replaceClass(this._container, 'elevation-expanded', 'elevation-collapsed');
507
+ if (this._map) this._map.invalidateSize();
508
+ },
509
+
510
+ /*
511
+ * Expand current chart control.
512
+ */
513
+ _expand() {
514
+ _.replaceClass(this._container, 'elevation-collapsed', 'elevation-expanded');
515
+ if (this._map) this._map.invalidateSize();
516
+ },
517
+
518
+ /**
519
+ * Add some basic colors to leaflet canvas renderer (preferCanvas: true).
520
+ */
521
+ _fixCanvasPaths() {
522
+ let oldProto = L.Canvas.prototype._fillStroke;
523
+ let control = this;
524
+
525
+ let theme = this.options.theme.split(' ')[0].replace('-theme', '');
526
+ let color = _.Colors[theme] || {};
527
+
528
+ L.Canvas.include({
529
+ _fillStroke(ctx, layer) {
530
+ if (control._layers.hasLayer(layer)) {
531
+
532
+ let options = layer.options;
533
+
534
+ options.color = color.line || color.area || theme;
535
+ options.stroke = !!options.color;
536
+
537
+ oldProto.call(this, ctx, layer);
538
+
539
+ if (options.stroke && options.weight !== 0) {
540
+ let oldVal = ctx.globalCompositeOperation || 'source-over';
541
+ ctx.globalCompositeOperation = 'destination-over'
542
+ ctx.strokeStyle = color.outline || '#FFF';
543
+ ctx.lineWidth = options.weight * 1.75;
544
+ ctx.stroke();
545
+ ctx.globalCompositeOperation = oldVal;
546
+ }
547
+
548
+ } else {
549
+ oldProto.call(this, ctx, layer);
550
+ }
551
+ }
552
+ });
553
+ },
554
+
555
+ /**
556
+ * Partial fix for initial tooltip size
557
+ *
558
+ * @link https://github.com/Raruto/leaflet-elevation/issues/81#issuecomment-713477050
559
+ */
560
+ _fixTooltipSize() {
561
+ this.on('elechart_init', () =>
562
+ this.once('elechart_change elechart_hover', ({data, xCoord}) => {
563
+ if (this._chartEnabled) {
564
+ this._chart._showDiagramIndicator(data, xCoord);
565
+ this._chart._showDiagramIndicator(data, xCoord);
566
+ }
567
+ this._updateMarker(data);
568
+ })
569
+ );
570
+ },
571
+
572
+ /*
573
+ * Finds a data entry for the given LatLng
574
+ */
575
+ _findItemForLatLng(latlng) {
576
+ return this._data[this._chart._findIndexForLatLng(latlng)];
577
+ },
578
+
579
+ /*
580
+ * Finds a data entry for the given xDiagCoord
581
+ */
582
+ _findItemForX(x) {
583
+ return this._data[this._chart._findIndexForXCoord(x)];
584
+ },
585
+
586
+ /**
587
+ * Fires an event of the specified type.
588
+ */
589
+ _fireEvt(type, data, propagate) {
590
+ if (this.fire) this.fire(type, data, propagate);
591
+ if (this._map) this._map.fire(type, data, propagate);
592
+ },
593
+
594
+ /*
595
+ * Hides the position/height indicator marker drawn onto the map
596
+ */
597
+ _hideMarker() {
598
+ if (this.options.autohideMarker) {
599
+ this._marker.remove();
600
+ }
601
+ },
602
+
603
+ /**
604
+ * Generate "svg" chart (DOM element).
605
+ */
606
+ _initChart(container) {
607
+ let opts = this.options;
608
+ let map = this._map;
609
+
610
+ if (opts.detached) {
611
+ let { offsetWidth, offsetHeight} = this.eleDiv;
612
+ if (offsetWidth > 0) opts.width = offsetWidth;
613
+ if (offsetHeight > 20) opts.height = offsetHeight - 20; // 20 = horizontal scrollbar size.
614
+ } else {
615
+ let { clientWidth } = map.getContainer();
616
+ opts._maxWidth = opts._maxWidth > opts.width ? opts._maxWidth : opts.width;
617
+ this._container.style.maxWidth = opts._maxWidth + 'px';
618
+ if (opts._maxWidth > clientWidth) opts.width = clientWidth - 30;
619
+ }
620
+
621
+ Promise.all([
622
+ this.import(this.__D3),
623
+ this.import(this.__LCHART)
624
+ ]).then((m) => {
625
+
626
+ let chart = this._chart = new (m[1] || Elevation).Chart(opts, this);
627
+
628
+ this._x = this._chart._x;
629
+ this._y = this._chart._y;
630
+
631
+ d3
632
+ .select(container)
633
+ .call(chart.render())
634
+
635
+ chart
636
+ .on('reset_drag', this._hideMarker, this)
637
+ .on('mouse_enter', this._onMouseEnter, this)
638
+ .on('dragged', this._onDragEnd, this)
639
+ .on('mouse_move', this._onMouseMove, this)
640
+ .on('mouse_out', this._onMouseOut, this)
641
+ .on('ruler_filter', this._onRulerFilter, this)
642
+ .on('zoom', this._updateChart, this)
643
+ .on('elepath_toggle', this._onToggleChart, this)
644
+ .on('margins_updated', this._resizeChart, this);
645
+
646
+
647
+ this.fire("elechart_init");
648
+
649
+ map
650
+ .on('zoom viewreset zoomanim', this._hideMarker, this)
651
+ .on('resize', this._resetView, this)
652
+ .on('resize', this._resizeChart, this)
653
+ .on('rotate', this._rotateMarker, this)
654
+ .on('mousedown', this._resetDrag, this);
655
+
656
+ _.on(map.getContainer(), 'mousewheel', this._resetDrag, this);
657
+ _.on(map.getContainer(), 'touchstart', this._resetDrag, this);
658
+ _.on(document, 'keydown', this._onKeyDown, this);
659
+
660
+ this
661
+ .on('eledata_added eledata_loaded', this._updateChart, this)
662
+ .on('eledata_added eledata_loaded', this._updateSummary, this);
663
+
664
+ this._updateChart();
665
+ this._updateSummary();
666
+ });
667
+
668
+
669
+ },
670
+
671
+ _initLayer() {
672
+ this._layers
673
+ .on('layeradd layerremove', ({layer, type}) => {
674
+ let node = layer.getElement && layer.getElement();
675
+ _.toggleClass(node, this.options.polyline.className + ' ' + this.options.theme, type == 'layeradd');
676
+ _.toggleEvent(layer, "mousemove", this._onMouseMoveLayer.bind(this), type == 'layeradd')
677
+ _.toggleEvent(layer, "mouseout", this._onMouseOut.bind(this), type == 'layeradd');
678
+ });
679
+ },
680
+
681
+ _initMarker(map) {
682
+ let pane = map.getPane('elevationPane');
683
+ if (!pane) {
684
+ pane = this._pane = map.createPane('elevationPane', map.getPane('norotatePane') || map.getPane('mapPane'));
685
+ pane.style.zIndex = 625; // This pane is above markers but below popups.
686
+ pane.style.pointerEvents = 'none';
687
+ }
688
+
689
+ if (this._renderer) this._renderer.remove()
690
+ this._renderer = L.svg({ pane: "elevationPane" }).addTo(this._map); // default leaflet svg renderer
691
+
692
+ Promise.all([
693
+ this.import(this.__D3),
694
+ this.import(this.__LMARKER)
695
+ ]).then((m) => {
696
+ this._marker = new (m[1] || Elevation).Marker(this.options, this);
697
+ this.fire("elechart_marker");
698
+ });
699
+ },
700
+
701
+ /**
702
+ * Inspired by L.Control.Layers
703
+ */
704
+ _initButton(container) {
705
+ L.DomEvent
706
+ .disableClickPropagation(container)
707
+ .disableScrollPropagation(container);
708
+
709
+ this.options.collapsed ? this._collapse() : this._expand();
710
+
711
+ if (this.options.autohide) {
712
+ _.on(container, 'mouseover', this._expand, this);
713
+ _.on(container, 'mouseout', this._collapse, this);
714
+ this._map.on('click', this._collapse, this);
715
+ }
716
+
717
+ if (this.options.closeBtn) {
718
+ let link = this._button = _.create('a', "elevation-toggle-icon", { href: '#', title: L._('Elevation'), }, container);
719
+ _.on(link, 'click', L.DomEvent.stop);
720
+ _.on(link, 'click', this._toggle, this);
721
+ _.on(link, 'focus', this._toggle, this);
722
+ fetch(_.resolveURL(this.__btnIcon, this.options.srcFolder)).then(r => r.ok && r.text().then(img => link.innerHTML = img));
723
+ }
724
+ },
725
+
726
+ _initSummary(container) {
727
+ this.import(this.__LSUMMARY).then((m)=>{
728
+ this._summary = new (m || Elevation).Summary({ summary: this.options.summary }, this);
729
+
730
+ this.on('elechart_init', () => {
731
+ d3.select(container).call(this._summary.render());
732
+ });
733
+ });
734
+ },
735
+
736
+ /**
737
+ * Retrieve data from a remote url (HTTP).
738
+ */
739
+ _loadFile(url) {
740
+ fetch(url)
741
+ .then((response) => response.text())
742
+ .then((data) => {
743
+ this._downloadURL = url; // TODO: handle multiple urls?
744
+ this._parseFromString(data)
745
+ .then( geojson => geojson && this._loadLayer(geojson));
746
+ }).catch((err) => console.warn(err));
747
+ },
748
+
749
+ /**
750
+ * Dynamically import only required javascript modules (code splitting)
751
+ */
752
+ _loadModules(handlers) {
753
+ // First map known classnames (eg. "Altitude" --> L.Control.Elevation.Altitude)
754
+ handlers = handlers.map((h) => typeof h === 'string' && typeof Elevation[h] !== "undefined" ? Elevation[h] : h);
755
+ // Then load optional classes and custom imports (eg. "Cadence" --> import('../src/handlers/cadence.js'))
756
+ let modules = handlers.map(file => (typeof file === 'string' && this.import(this.__modulesFolder + file.toLowerCase() + '.js')) || (file instanceof Promise && file) || Promise.resolve());
757
+ return Promise.all(modules).then((m) => {
758
+ _.each(m, (exported, i) => {
759
+ let fn = exported && Object.keys(exported)[0];
760
+ if (fn) {
761
+ handlers[i] = Elevation[fn] = (Elevation[fn] ?? exported[fn]);
762
+ }
763
+ });
764
+ _.each(handlers, h => ["function", "object"].includes(typeof h) && this._registerHandler(h));
765
+ });
766
+ },
767
+
768
+ /**
769
+ * Simple GeoJSON data loader (L.GeoJSON).
770
+ */
771
+ _loadLayer(geojson) {
772
+ let { polyline, theme, waypoints, wptIcons, wptLabels, distanceMarkers } = this.options;
773
+ let style = L.extend({}, polyline);
774
+
775
+ if (theme) {
776
+ style.className += ' ' + theme;
777
+ }
778
+
779
+ if (geojson.name) {
780
+ this.track_info.name = geojson.name;
781
+ }
782
+
783
+ let layer = L.geoJson(geojson, {
784
+ distanceMarkers: distanceMarkers,
785
+ style: style,
786
+ pointToLayer: (feature, latlng) => {
787
+ if (waypoints) {
788
+ let { desc, name, sym } = feature.properties;
789
+ desc = desc || '';
790
+ name = name || '';
791
+ // Handle chart waypoints (dots)
792
+ if ([true, 'dots'].includes(waypoints)) {
793
+ this._registerCheckPoint({
794
+ latlng: latlng,
795
+ label : ([true, 'dots'].includes(wptLabels) ? name : '')
796
+ });
797
+ }
798
+ // Handle map waypoints (markers)
799
+ if ([true, 'markers'].includes(waypoints) && wptIcons != false) {
800
+ return this._registerMarker({
801
+ latlng : latlng,
802
+ sym : (sym ?? name).replace(' ', '-').replace('"', '').replace("'", '').toLowerCase(),
803
+ content: [true, 'markers'].includes(wptLabels) && (name || desc) && decodeURI("<b>" + name + "</b>" + (desc.length > 0 ? '<br>' + desc : ''))
804
+ });
805
+ }
806
+ }
807
+ },
808
+ onEachFeature: (feature, layer) => feature.geometry && feature.geometry.type != 'Point' && this.addData(feature, layer),
809
+ });
810
+
811
+ this.import(this.__D3).then(() => {
812
+ this._initMapIntegrations(layer);
813
+ const event_data = { data: geojson, layer: layer, name: this.track_info.name, track_info: this.track_info };
814
+ if (this._modulesLoaded) {
815
+ this._fireEvt("eledata_loaded", event_data);
816
+ } else {
817
+ this.once('modules_loaded', () => this._fireEvt("eledata_loaded", event_data));
818
+ }
819
+ });
820
+
821
+ return layer;
822
+ },
823
+
824
+ _onDragEnd({ dragstart, dragend}) {
825
+ this._hideMarker();
826
+ this.fitBounds(L.latLngBounds([dragstart.latlng, dragend.latlng]));
827
+
828
+ this.fire("elechart_dragged");
829
+ },
830
+
831
+ _onKeyDown({key}) {
832
+ if(!this.options.detached && key === "Escape"){
833
+ this._collapse()
834
+ };
835
+ },
836
+
837
+ /**
838
+ * Trigger mouseenter event.
839
+ */
840
+ _onMouseEnter() {
841
+ this.fire('elechart_enter');
842
+ },
843
+
844
+ /*
845
+ * Handles the moueseover the chart and displays distance and altitude level.
846
+ */
847
+ _onMouseMove({xCoord}) {
848
+ if (this._chartEnabled && this._data.length) {
849
+ let item = this._findItemForX(xCoord);
850
+ if (item) {
851
+ if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord);
852
+
853
+ this._updateMarker(item);
854
+ this._setMapView(item);
855
+
856
+ if (this._map) {
857
+ _.addClass(this._map.getContainer(), 'elechart-hover');
858
+ }
859
+
860
+ this.fire("elechart_change", { data: item, xCoord: xCoord });
861
+ this.fire("elechart_hover", { data: item, xCoord: xCoord });
862
+ }
863
+ }
864
+ },
865
+
866
+ /*
867
+ * Handles mouseover events of the data layers on the map.
868
+ */
869
+ _onMouseMoveLayer({latlng}) {
870
+ if (this._data.length) {
871
+ let item = this._findItemForLatLng(latlng);
872
+ if (item) {
873
+ let xCoord = item.xDiagCoord;
874
+
875
+ if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord);
876
+
877
+ this._updateMarker(item);
878
+
879
+ this.fire("elechart_change", { data: item, xCoord: xCoord });
880
+ }
881
+ }
882
+ },
883
+
884
+ /*
885
+ * Handles the moueseout over the chart.
886
+ */
887
+ _onMouseOut() {
888
+ if (!this.options.detached) {
889
+ this._hideMarker();
890
+ this._chart._hideDiagramIndicator();
891
+ }
892
+
893
+ if (this._map) {
894
+ _.removeClass(this._map.getContainer(), 'elechart-hover');
895
+ }
896
+
897
+ this.fire("elechart_leave");
898
+ },
899
+
900
+ /**
901
+ * Handles the drag event over the ruler filter.
902
+ */
903
+ _onRulerFilter({coords}) {
904
+ this._updateMapSegments(coords);
905
+ },
906
+
907
+ /**
908
+ * Toggle chart data on legend click
909
+ */
910
+ _onToggleChart({ name, enabled }) {
911
+
912
+ this._chartEnabled = this._chart._hasActiveLayers();
913
+
914
+ // toggle layer visibility on empty chart
915
+ this._layers.eachLayer(layer => _.toggleClass(layer.getElement && layer.getElement(), this.options.polyline.className + ' ' + this.options.theme, this._chartEnabled));
916
+
917
+ // toggle option value (eg. altitude = { 'disabled' || 'enabled' })
918
+ this.options[name] = !enabled && this.options[name] == 'disabled' ? 'enabled' : 'disabled';
919
+
920
+ // remove marker on empty chart
921
+ if (!this._chartEnabled) {
922
+ this._chart._hideDiagramIndicator();
923
+ this._marker.remove();
924
+ }
925
+ },
926
+
927
+ /**
928
+ * Simple GeoJSON Parser
929
+ */
930
+ _parseFromGeoJSONString(data) {
931
+ try {
932
+ return JSON.parse(data);
933
+ } catch (e) { }
934
+ },
935
+
936
+ /**
937
+ * Attempt to parse raw response data (GeoJSON or XML > GeoJSON)
938
+ */
939
+ _parseFromString(data) {
940
+ return new Promise(resolve =>
941
+ this.import(this.__TOGEOJSON).then(() => {
942
+ let geojson;
943
+ try {
944
+ geojson = this._parseFromXMLString(data.trim());
945
+ } catch (e) {
946
+ geojson = this._parseFromGeoJSONString(data.toString());
947
+ }
948
+ if (geojson) {
949
+ geojson.name = geojson.name || (this._downloadURL || '').split('/').pop().split('#')[0].split('?')[0];
950
+ }
951
+ resolve(geojson);
952
+ })
953
+ );
954
+ },
955
+
956
+ /**
957
+ * Simple XML Parser (GPX, KML, TCX)
958
+ */
959
+ _parseFromXMLString(data) {
960
+ if (data.indexOf("<") != 0) {
961
+ throw 'Invalid XML';
962
+ }
963
+ let xml = (new DOMParser()).parseFromString(data, "text/xml");
964
+ let type = xml.documentElement.tagName.toLowerCase(); // "kml" or "gpx"
965
+ let name = xml.getElementsByTagName('name');
966
+ if (xml.getElementsByTagName('parsererror').length) {
967
+ throw 'Invalid XML';
968
+ }
969
+ if (!(type in toGeoJSON)) {
970
+ type = xml.documentElement.tagName == "TrainingCenterDatabase" ? 'tcx' : 'gpx';
971
+ }
972
+ let geojson = toGeoJSON[type](xml);
973
+ geojson.name = name.length > 0 ? (Array.from(name).find(tag => tag.parentElement.tagName == "trk") ?? name[0]).textContent : '';
974
+ return geojson;
975
+ },
976
+
977
+ /**
978
+ * Add chart profile to diagram
979
+ */
980
+ _registerAreaPath(props) {
981
+ this.on("elechart_init", () => this._chart._registerAreaPath(props));
982
+ },
983
+
984
+ /**
985
+ * Add chart grid to diagram
986
+ */
987
+ _registerAxisGrid(props) {
988
+ this.on("elechart_axis", () => this._chart._registerAxisGrid(props));
989
+ },
990
+
991
+ /**
992
+ * Add chart axis to diagram
993
+ */
994
+ _registerAxisScale(props) {
995
+ this.on("elechart_axis", () => this._chart._registerAxisScale(props));
996
+ },
997
+
998
+ /**
999
+ * Add a point of interest over the diagram
1000
+ */
1001
+ _registerCheckPoint(props) {
1002
+ const cb = () => this._chart._registerCheckPoint(props);
1003
+ this
1004
+ .on("elechart_updated", cb)
1005
+ .once("eledata_clear", () => this.off("elechart_updated", cb));
1006
+ },
1007
+
1008
+ /**
1009
+ * Base handler for iterative track statistics (dist, time, z, slope, speed, acceleration, ...)
1010
+ */
1011
+ _registerDataAttribute(props) {
1012
+
1013
+ // parse of "coordinateProperties" for later usage
1014
+ if (props.coordPropsToMeta) {
1015
+ this.on("elepoint_init", (e) => props.coordPropsToMeta.call(this, e));
1016
+ }
1017
+
1018
+ // prevent excessive variabile instanstations
1019
+ let i, curr, prev, attr = props.attr || props.name;
1020
+
1021
+ // save here a reference to last used point
1022
+ let lastValid = {};
1023
+
1024
+ // iteration
1025
+ this.on("elepoint_added", ({index, point}) => {
1026
+ i = index;
1027
+
1028
+ prev = curr ?? this._data[i]; // same as: this._data[i > 0 ? i - 1 : i]
1029
+ curr = this._data[i];
1030
+
1031
+ // retrieve point value
1032
+ curr[attr] = props.pointToAttr.call(this, point, i);
1033
+
1034
+ // check and fix missing data on last added point
1035
+ if (i > 0 && isNaN(prev[attr])) {
1036
+ if (!isNaN(lastValid[attr]) && !isNaN(curr[attr])) {
1037
+ prev[attr] = (lastValid[attr] + curr[attr]) / 2;
1038
+ } else if (!isNaN(lastValid[attr])) {
1039
+ prev[attr] = lastValid[attr];
1040
+ } else if (!isNaN(curr[attr])) {
1041
+ prev[attr] = curr[attr];
1042
+ }
1043
+ // update "yAttr" and "xAttr"
1044
+ if (props.meta) {
1045
+ prev[props.meta] = prev[attr];
1046
+ }
1047
+ }
1048
+
1049
+ // skip to next iteration for invalid or missing data (eg. i == 0)
1050
+ if (isNaN(curr[attr])) {
1051
+ return;
1052
+ }
1053
+
1054
+ // update reference to last used point
1055
+ lastValid[attr] = curr[attr];
1056
+
1057
+ // Limit "crazy" delta values.
1058
+ if (props.deltaMax) {
1059
+ curr[attr] =_.wrapDelta(curr[attr], prev[attr], props.deltaMax);
1060
+ }
1061
+
1062
+ // Range of acceptable values.
1063
+ if (props.clampRange) {
1064
+ curr[attr] = _.clamp(curr[attr], props.clampRange);
1065
+ }
1066
+
1067
+ // Limit floating point precision.
1068
+ if (!isNaN(props.decimals)) {
1069
+ curr[attr] = _.round(curr[attr], props.decimals);
1070
+ }
1071
+
1072
+ // update "track_info" stats (min, max, avg, ...)
1073
+ if (props.stats) {
1074
+ for (const key in props.stats) {
1075
+ let sname = (props.statsName || attr) + (key != '' ? '_' : '');
1076
+ this.track_info[sname + key] = props.stats[key].call(this, curr[attr], this.track_info[sname + key], this._data.length);
1077
+ }
1078
+ }
1079
+
1080
+ // update here some mixins (eg. complex "track_info" stuff)
1081
+ if (props.onPointAdded) props.onPointAdded.call(this, curr[attr], i, point);
1082
+ });
1083
+ },
1084
+
1085
+ /**
1086
+ * Parse a module definition and attach related function listeners
1087
+ */
1088
+ _registerHandler(props) {
1089
+
1090
+ // eg. L.Control.Altitude
1091
+ if (typeof props === "function") {
1092
+ return this._registerHandler(props.call(this));
1093
+ }
1094
+
1095
+ let {
1096
+ name,
1097
+ attr,
1098
+ required,
1099
+ deltaMax,
1100
+ clampRange,
1101
+ decimals,
1102
+ meta,
1103
+ unit,
1104
+ coordinateProperties,
1105
+ coordPropsToMeta,
1106
+ pointToAttr,
1107
+ onPointAdded,
1108
+ stats,
1109
+ statsName,
1110
+ grid,
1111
+ scale,
1112
+ path,
1113
+ tooltip,
1114
+ summary
1115
+ } = props;
1116
+
1117
+ // eg. "altitude" == true
1118
+ if (this.options[name] || required) {
1119
+
1120
+ this._registerDataAttribute({
1121
+ name,
1122
+ attr,
1123
+ meta,
1124
+ deltaMax,
1125
+ clampRange,
1126
+ decimals,
1127
+ coordPropsToMeta: _.coordPropsToMeta(coordinateProperties, meta || name, coordPropsToMeta),
1128
+ pointToAttr,
1129
+ onPointAdded,
1130
+ stats,
1131
+ statsName,
1132
+ });
1133
+
1134
+ if (grid) {
1135
+ this._registerAxisGrid(L.extend({ name }, grid));
1136
+ }
1137
+
1138
+ if (this.options[name] !== "summary") {
1139
+ if (scale) this._registerAxisScale(L.extend({ name, label: unit }, scale));
1140
+ if (path) this._registerAreaPath(L.extend({ name }, path));
1141
+ }
1142
+
1143
+ if (tooltip || props.tooltips) {
1144
+ _.each([tooltip, ...(props.tooltips || [])], t => t && this._registerTooltip(L.extend({ name }, t)));
1145
+ }
1146
+
1147
+ if (summary) {
1148
+ _.each(summary, (s, k) => summary[k] = L.extend({ unit }, s));
1149
+ this._registerSummary(summary);
1150
+ }
1151
+ }
1152
+ },
1153
+
1154
+ _registerMarker({latlng, sym, content}) {
1155
+ let { wptIcons } = this.options;
1156
+ // generate and cache appropriate icon symbol
1157
+ if (!wptIcons.hasOwnProperty(sym)) {
1158
+ wptIcons[sym] = L.divIcon(L.extend({}, wptIcons[""].options, { html: '<i class="elevation-waypoint-icon ' + sym + '"></i>' } ));
1159
+ }
1160
+ let marker = L.marker(latlng, { icon: wptIcons[sym] });
1161
+ if (content) {
1162
+ marker.bindPopup(content, { className: 'elevation-popup', keepInView: true }).openPopup();
1163
+ marker.bindTooltip(content, { className: 'elevation-tooltip', direction: 'auto', sticky: true, opacity: 1 }).openTooltip();
1164
+ }
1165
+ return this._addMarker(marker)
1166
+ },
1167
+
1168
+ /**
1169
+ * Add chart or marker tooltip info
1170
+ */
1171
+ _registerTooltip(props) {
1172
+ props.chart && this.on("elechart_init", () => this._chart._registerTooltip(L.extend({}, props, { value: props.chart })));
1173
+ props.marker && this.on("elechart_marker", () => this._marker._registerTooltip(L.extend({}, props, { value: props.marker })));
1174
+ },
1175
+
1176
+ /**
1177
+ * Add summary info to diagram
1178
+ */
1179
+ _registerSummary(props) {
1180
+ this.on('elechart_summary', () => this._summary._registerSummary(props));
1181
+ },
1182
+
1183
+ /*
1184
+ * Removes the drag rectangle and zoms back to the total extent of the data.
1185
+ */
1186
+ _resetDrag() {
1187
+ this._chart._resetDrag();
1188
+ this._hideMarker();
1189
+ },
1190
+
1191
+ /**
1192
+ * Resets drag, marker and bounds.
1193
+ */
1194
+ _resetView() {
1195
+ if (this._map && this._map._isFullscreen) return;
1196
+ this._resetDrag();
1197
+ this._hideMarker();
1198
+ if (this.options.autofitBounds) {
1199
+ this.fitBounds();
1200
+ }
1201
+ },
1202
+
1203
+ /**
1204
+ * Hacky way for handling chart resize. Deletes it and redraw chart.
1205
+ */
1206
+ _resizeChart() {
1207
+ if (this._container && _.style(this._container, "display") != "none") {
1208
+ let opts = this.options;
1209
+ let newWidth = opts.detached ? (this.eleDiv || this._container).offsetWidth : _.clamp(opts._maxWidth, [0, this._map.getContainer().clientWidth - 30]);
1210
+ if (newWidth) {
1211
+ opts.width = newWidth;
1212
+ if (this._chart && this._chart._chart) {
1213
+ this._chart._chart._resize(opts);
1214
+ this._updateChart();
1215
+ }
1216
+ }
1217
+ this._updateMapSegments();
1218
+ }
1219
+ },
1220
+
1221
+ /**
1222
+ * Collapse or Expand chart control.
1223
+ */
1224
+ _toggle() {
1225
+ _.hasClass(this._container, "elevation-expanded") ? this._collapse() : this._expand();
1226
+ },
1227
+
1228
+ /**
1229
+ * Update map center and zoom (followMarker: true)
1230
+ */
1231
+ _setMapView(item) {
1232
+ if (this._map && this.options.followMarker) {
1233
+ let zoom = this._map.getZoom();
1234
+ let z = this.options.zFollow;
1235
+ if (typeof z === "number") {
1236
+ this._map.setView(item.latlng, (zoom < z ? z : zoom), { animate: true, duration: 0.25 });
1237
+ } else if (!this._map.getBounds().contains(item.latlng)) {
1238
+ this._map.setView(item.latlng, zoom, { animate: true, duration: 0.25 });
1239
+ }
1240
+ }
1241
+ },
1242
+
1243
+ /**
1244
+ * Calculates [x, y] domain and then update chart.
1245
+ */
1246
+ _updateChart() {
1247
+ if (this._chart && this._container) {
1248
+ this.fire("elechart_axis");
1249
+
1250
+ this._chart.update({ data: this._data, options: this.options });
1251
+
1252
+ this._x = this._chart._x;
1253
+ this._y = this._chart._y;
1254
+
1255
+ this.fire('elechart_updated');
1256
+ }
1257
+ },
1258
+
1259
+ /*
1260
+ * Update the position/height indicator marker drawn onto the map
1261
+ */
1262
+ _updateMarker(item) {
1263
+ if (this._marker) {
1264
+ this._marker.update({
1265
+ map : this._map,
1266
+ item : item,
1267
+ yCoordMax : this._yCoordMax || 0,
1268
+ options : this.options
1269
+ });
1270
+ }
1271
+ },
1272
+
1273
+ /**
1274
+ * Fix marker rotation on rotated maps
1275
+ */
1276
+ _rotateMarker() {
1277
+ if (this._marker) {
1278
+ this._marker.update();
1279
+ }
1280
+ },
1281
+
1282
+ /**
1283
+ * Highlight track segments on the map.
1284
+ */
1285
+ _updateMapSegments(coords) {
1286
+ this._markedSegments.setLatLngs(coords || []);
1287
+ if (coords && this._map && !this._map.hasLayer(this._markedSegments)) {
1288
+ this._markedSegments.addTo(this._map);
1289
+ }
1290
+ },
1291
+
1292
+ /**
1293
+ * Update chart summary.
1294
+ */
1295
+ _updateSummary() {
1296
+ if (this._summary) {
1297
+ this._summary.reset();
1298
+ if (this.options.summary) {
1299
+ this.fire("elechart_summary");
1300
+ this._summary.update();
1301
+ }
1302
+ if (this.options.downloadLink && this._downloadURL) { // TODO: generate dynamically file content instead of using static file urls.
1303
+ this._summary._container.innerHTML += '<span class="download"><a href="#">' + L._('Download') + '</a></span>'
1304
+ _.select('.download a', this._summary._container).onclick = (e) => {
1305
+ e.preventDefault();
1306
+ let event = { downloadLink: this.options.downloadLink, confirm: _.saveFile.bind(this, this._downloadURL) };
1307
+ if (this.options.downloadLink == 'modal' && typeof CustomEvent === "function") {
1308
+ document.dispatchEvent(new CustomEvent("eletrack_download", { detail: event }));
1309
+ } else if (this.options.downloadLink == 'link' || this.options.downloadLink === true) {
1310
+ event.confirm();
1311
+ }
1312
+ this.fire('eletrack_download', event);
1313
+ };
1314
+ }
1315
+ }
1316
+ },
1317
+
1318
+
1319
+ /**
1320
+ * Calculates chart width.
1321
+ */
1322
+ _width() {
1323
+ if (this._chart) return this._chart._width();
1324
+ const { width, margins } = this.options;
1325
+ return width - margins.left - margins.right;
1326
+ },
1327
+
1328
+ /**
1329
+ * Calculates chart height.
1330
+ */
1331
+ _height() {
1332
+ if (this._chart) return this._chart._height();
1333
+ const { height, margins } = this.options;
1334
+ return height - margins.top - margins.bottom;
1335
+ },
1336
+
1337
+ });