@raruto/leaflet-elevation 2.2.3 → 2.2.6

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