@raruto/leaflet-elevation 2.3.4 → 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,1332 +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._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.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
+ });