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