@raruto/leaflet-elevation 2.3.4 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,175 +3,216 @@
3
3
  factory();
4
4
  })((function () { 'use strict';
5
5
 
6
- /**
7
- * TODO: exget computed styles of theese values from actual "CSS vars"
8
- **/
9
- const Colors = {
10
- 'lightblue': { area: '#3366CC', alpha: 0.45, stroke: '#3366CC' },
11
- 'magenta' : { area: '#FF005E' },
12
- 'yellow' : { area: '#FF0' },
13
- 'purple' : { area: '#732C7B' },
14
- 'steelblue': { area: '#4682B4' },
15
- 'red' : { area: '#F00' },
16
- 'lime' : { area: '#9CC222', line: '#566B13' }
17
- };
18
-
19
- const SEC = 1000;
20
- const MIN = SEC * 60;
21
- const HOUR = MIN * 60;
22
- const DAY = HOUR * 24;
23
-
24
- function resolveURL(src, baseUrl) {
25
- return (new URL(src, (src.startsWith('../') || src.startsWith('./')) ? baseUrl : undefined)).toString()
26
- }
27
- /**
28
- * Convert a time (millis) to a human readable duration string (%Dd %H:%M'%S")
29
- */
30
- function formatTime(t) {
31
- let d = Math.floor(t / DAY);
32
- let h = Math.floor( (t - d * DAY) / HOUR);
33
- let m = Math.floor( (t - d * DAY - h * HOUR) / MIN);
34
- let s = Math.round( (t - d * DAY - h * HOUR - m * MIN) / SEC);
35
- if ( s === 60 ) { m++; s = 0; }
36
- if ( m === 60 ) { h++; m = 0; }
37
- if ( h === 24 ) { d++; h = 0; }
38
- return (d ? d + "d " : '') + h.toString().padStart(2, 0) + ':' + m.toString().padStart(2, 0) + "'" + s.toString().padStart(2, 0) + '"';
39
- }
40
-
41
- /**
42
- * Convert a time (millis) to human readable date string (dd-mm-yyyy hh:mm:ss)
43
- */
44
- function formatDate(format) {
45
- if (!format) {
46
- return (time) => (new Date(time)).toLocaleString().replaceAll('/', '-').replaceAll(',', ' ');
47
- } else if (format == 'time') {
48
- return (time) => (new Date(time)).toLocaleTimeString();
49
- } else if (format == 'date') {
50
- return (time) => (new Date(time)).toLocaleDateString();
51
- }
52
- return (time) => format(time);
53
- }
54
-
55
- /**
56
- * Generate download data event.
57
- */
58
- function saveFile(dataURI, fileName) {
59
- let a = create('a', '', { href: dataURI, target: '_new', download: fileName || "", style: "display:none;" });
60
- let b = document.body;
61
- b.appendChild(a);
62
- a.click();
63
- b.removeChild(a);
64
- }
65
-
66
-
67
- /**
68
- * Convert SVG Path into Path2D and then update canvas
69
- */
70
- function drawCanvas(ctx, path) {
71
- path.classed('canvas-path', true);
72
-
73
- ctx.beginPath();
74
- ctx.moveTo(0, 0);
75
- let p = new Path2D(path.attr('d'));
76
-
77
- ctx.strokeStyle = path.attr('stroke');
78
- ctx.fillStyle = path.attr('fill');
79
- ctx.lineWidth = 1.25;
80
- ctx.globalCompositeOperation = 'source-over';
81
-
82
- // stroke opacity
83
- ctx.globalAlpha = path.attr('stroke-opacity') || 0.3;
84
- ctx.stroke(p);
85
-
86
- // fill opacity
87
- ctx.globalAlpha = path.attr('fill-opacity') || 0.45;
88
- ctx.fill(p);
89
-
90
- ctx.globalAlpha = 1;
91
-
92
- ctx.closePath();
93
- }
94
-
95
- /**
96
- * Loop and extract GPX Extensions handled by "@tmcw/toGeoJSON" (eg. "coordinateProperties" > "times")
97
- */
98
- function coordPropsToMeta(coordProps, name, parser) {
99
- return coordProps && (({props, point, id, isMulti }) => {
100
- if (props) {
101
- for (const key of coordProps) {
102
- if (key in props) {
103
- point.meta[name] = (parser || parseNumeric).call(this, (isMulti ? props[key][isMulti] : props[key]), id);
104
- break;
105
- }
106
- }
107
- }
108
- });
109
- }
110
-
111
- /**
112
- * Extract numeric property (id) from GeoJSON object
113
- */
114
- const parseNumeric = (property, id) => parseInt((typeof property === 'object' ? property[id] : property));
115
-
116
- /**
117
- * Extract datetime property (id) from GeoJSON object
118
- */
119
- const parseDate = (property, id) => new Date(Date.parse((typeof property === 'object' ? property[id] : property)));
120
-
121
- /**
122
- * A little bit shorter than L.DomUtil
123
- */
124
- const addClass = (n, str) => n && str.split(" ").every(s => s && L.DomUtil.addClass(n, s));
125
- const removeClass = (n, str) => n && str.split(" ").every(s => s && L.DomUtil.removeClass(n, s));
126
- const toggleClass = (n, str, cond) => (cond ? addClass : removeClass)(n, str);
127
- const replaceClass = (n, rem, add) => (rem && removeClass(n, rem)) || (add && addClass(n, add));
128
- const style = (n, k, v) => (typeof v === "undefined" && L.DomUtil.getStyle(n, k)) || n.style.setProperty(k, v);
129
- const toggleStyle = (n, k, v, cond) => style(n, k, cond ? v : '');
130
- const setAttributes = (n, attrs) => { for (let k in attrs) { n.setAttribute(k, attrs[k]); } };
131
- const toggleEvent = (el, e, fn, cond) => el[cond ? 'on' : 'off'](e, fn);
132
- const create = (tag, str, attrs, n) => { let elem = L.DomUtil.create(tag, str || ""); if (attrs) setAttributes(elem, attrs); if (n) append(n, elem); return elem; };
133
- const append = (n, c) => n.appendChild(c);
134
- const insert = (n, c, pos) => n.insertAdjacentElement(pos, c);
135
- const select = (str, n) => (n || document).querySelector(str);
136
- const each = (obj, fn) => { for (let i in obj) fn(obj[i], i); };
137
- const randomId = () => Math.random().toString(36).substr(2, 9);
138
-
139
- /**
140
- * TODO: use generators instead? (ie. "yield")
141
- */
142
- const iMax = (iVal, max = -Infinity) => (iVal > max ? iVal : max);
143
- const iMin = (iVal, min = +Infinity) => (iVal < min ? iVal : min);
144
- const iAvg = (iVal, avg = 0, idx = 1) => (iVal + avg * (idx - 1)) / idx;
145
- const iSum = (iVal, sum = 0) => iVal + sum;
146
-
147
- /**
148
- * Alias for some leaflet core functions
149
- */
150
- const { on, off } = L.DomEvent;
151
- const { throttle, wrapNum } = L.Util;
152
- const { hasClass } = L.DomUtil;
153
-
154
- /**
155
- * Limit floating point precision
156
- */
157
- const round = L.Util.formatNum;
158
-
159
- /**
160
- * Limit a number between min / max values
161
- */
162
- const clamp = (val, range) => range ? (val < range[0] ? range[0] : val > range[1] ? range[1] : val) : val;
163
-
164
- /**
165
- * Limit a delta difference between two values
166
- */
6
+ /**
7
+ * TODO: exget computed styles of theese values from actual "CSS vars"
8
+ **/
9
+ const Colors = {
10
+ 'lightblue': { area: '#3366CC', alpha: 0.45, stroke: '#3366CC' },
11
+ 'magenta' : { area: '#FF005E' },
12
+ 'yellow' : { area: '#FF0' },
13
+ 'purple' : { area: '#732C7B' },
14
+ 'steelblue': { area: '#4682B4' },
15
+ 'red' : { area: '#F00' },
16
+ 'lime' : { area: '#9CC222', line: '#566B13' }
17
+ };
18
+
19
+ const SEC = 1000;
20
+ const MIN = SEC * 60;
21
+ const HOUR = MIN * 60;
22
+ const DAY = HOUR * 24;
23
+
24
+ function resolveURL(src, baseUrl) {
25
+ return (new URL(src, (src.startsWith('../') || src.startsWith('./')) ? baseUrl : undefined)).toString()
26
+ }
27
+ /**
28
+ * Convert a time (millis) to a human readable duration string (%Dd %H:%M'%S")
29
+ */
30
+ function formatTime(t) {
31
+ let d = Math.floor(t / DAY);
32
+ let h = Math.floor( (t - d * DAY) / HOUR);
33
+ let m = Math.floor( (t - d * DAY - h * HOUR) / MIN);
34
+ let s = Math.round( (t - d * DAY - h * HOUR - m * MIN) / SEC);
35
+ if ( s === 60 ) { m++; s = 0; }
36
+ if ( m === 60 ) { h++; m = 0; }
37
+ if ( h === 24 ) { d++; h = 0; }
38
+ return (d ? d + "d " : '') + h.toString().padStart(2, 0) + ':' + m.toString().padStart(2, 0) + "'" + s.toString().padStart(2, 0) + '"';
39
+ }
40
+
41
+ /**
42
+ * Convert a time (millis) to human readable date string (dd-mm-yyyy hh:mm:ss)
43
+ */
44
+ function formatDate(format) {
45
+ if (!format) {
46
+ return (time) => (new Date(time)).toLocaleString().replaceAll('/', '-').replaceAll(',', ' ');
47
+ } else if (format == 'time') {
48
+ return (time) => (new Date(time)).toLocaleTimeString();
49
+ } else if (format == 'date') {
50
+ return (time) => (new Date(time)).toLocaleDateString();
51
+ }
52
+ return (time) => format(time);
53
+ }
54
+
55
+ /**
56
+ * Generate download data event.
57
+ */
58
+ function saveFile(dataURI, fileName) {
59
+ let a = create('a', '', { href: dataURI, target: '_new', download: fileName || "", style: "display:none;" });
60
+ let b = document.body;
61
+ b.appendChild(a);
62
+ a.click();
63
+ b.removeChild(a);
64
+ }
65
+
66
+
67
+ /**
68
+ * Convert SVG Path into Path2D and then update canvas
69
+ */
70
+ function drawCanvas(ctx, path) {
71
+ path.classed('canvas-path', true);
72
+
73
+ ctx.beginPath();
74
+ ctx.moveTo(0, 0);
75
+ let p = new Path2D(path.attr('d'));
76
+
77
+ ctx.strokeStyle = path.__strokeStyle || path.attr('stroke');
78
+ ctx.fillStyle = path.__fillStyle || path.attr('fill');
79
+ ctx.lineWidth = 1.25;
80
+ ctx.globalCompositeOperation = 'source-over';
81
+
82
+ // stroke opacity
83
+ ctx.globalAlpha = path.attr('stroke-opacity') || 0.3;
84
+ ctx.stroke(p);
85
+
86
+ // fill opacity
87
+ ctx.globalAlpha = path.attr('fill-opacity') || 0.45;
88
+ ctx.fill(p);
89
+
90
+ ctx.globalAlpha = 1;
91
+
92
+ ctx.closePath();
93
+ }
94
+
95
+ /**
96
+ * Loop and extract GPX Extensions handled by "@tmcw/toGeoJSON" (eg. "coordinateProperties" > "times")
97
+ */
98
+ function coordPropsToMeta(coordProps, name, parser) {
99
+ return coordProps && (({props, point, id, isMulti }) => {
100
+ if (props) {
101
+ for (const key of coordProps) {
102
+ if (key in props) {
103
+ point.meta[name] = (parser || parseNumeric).call(this, (isMulti ? props[key][isMulti] : props[key]), id);
104
+ break;
105
+ }
106
+ }
107
+ }
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Extract numeric property (id) from GeoJSON object
113
+ */
114
+ const parseNumeric = (property, id) => parseInt((typeof property === 'object' ? property[id] : property));
115
+
116
+ /**
117
+ * Extract datetime property (id) from GeoJSON object
118
+ */
119
+ const parseDate = (property, id) => new Date(Date.parse((typeof property === 'object' ? property[id] : property)));
120
+
121
+ /**
122
+ * A little bit shorter than L.DomUtil
123
+ */
124
+ const addClass = (n, str) => n && str.split(" ").every(s => s && L.DomUtil.addClass(n, s));
125
+ const removeClass = (n, str) => n && str.split(" ").every(s => s && L.DomUtil.removeClass(n, s));
126
+ const toggleClass = (n, str, cond) => (cond ? addClass : removeClass)(n, str);
127
+ const replaceClass = (n, rem, add) => (rem && removeClass(n, rem)) || (add && addClass(n, add));
128
+ const style = (n, k, v) => (typeof v === "undefined" && L.DomUtil.getStyle(n, k)) || n.style.setProperty(k, v);
129
+ const toggleStyle = (n, k, v, cond) => style(n, k, cond ? v : '');
130
+ const setAttributes = (n, attrs) => { for (let k in attrs) { n.setAttribute(k, attrs[k]); } };
131
+ const toggleEvent = (el, e, fn, cond) => el[cond ? 'on' : 'off'](e, fn);
132
+ const create = (tag, str, attrs, n) => { let elem = L.DomUtil.create(tag, str || ""); if (attrs) setAttributes(elem, attrs); if (n) append(n, elem); return elem; };
133
+ const append = (n, c) => n.appendChild(c);
134
+ const insert = (n, c, pos) => n.insertAdjacentElement(pos, c);
135
+ const select = (str, n) => (n || document).querySelector(str);
136
+ const each = (obj, fn) => { for (let i in obj) fn(obj[i], i); };
137
+ const randomId = () => Math.random().toString(36).substr(2, 9);
138
+
139
+ /**
140
+ * TODO: use generators instead? (ie. "yield")
141
+ */
142
+ const iMax = (iVal, max = -Infinity) => (iVal > max ? iVal : max);
143
+ const iMin = (iVal, min = +Infinity) => (iVal < min ? iVal : min);
144
+ const iAvg = (iVal, avg = 0, idx = 1) => (iVal + avg * (idx - 1)) / idx;
145
+ const iSum = (iVal, sum = 0) => iVal + sum;
146
+
147
+ /**
148
+ * Alias for some leaflet core functions
149
+ */
150
+ const { on, off } = L.DomEvent;
151
+ const { throttle, wrapNum } = L.Util;
152
+ const { hasClass } = L.DomUtil;
153
+
154
+ /**
155
+ * Limit floating point precision
156
+ */
157
+ const round = L.Util.formatNum;
158
+
159
+ /**
160
+ * Limit a number between min / max values
161
+ */
162
+ const clamp = (val, range) => range ? (val < range[0] ? range[0] : val > range[1] ? range[1] : val) : val;
163
+
164
+ /**
165
+ * Limit a delta difference between two values
166
+ */
167
167
  const wrapDelta = (curr, prev, deltaMax) => Math.abs(curr - prev) > deltaMax ? prev + deltaMax * Math.sign(curr - prev) : curr;
168
168
 
169
- var _ = /*#__PURE__*/Object.freeze({
169
+ /**
170
+ * A deep copy implementation that takes care of correct prototype chain and cycles, references
171
+ *
172
+ * @see https://web.dev/structured-clone/#features-and-limitations
173
+ */
174
+ function cloneDeep(o, skipProps = [], cache = []) {
175
+ switch(!o || typeof o) {
176
+ case 'object':
177
+ const hit = cache.filter(c => o === c.original)[0];
178
+ if (hit) return hit.copy; // handle circular structures
179
+ const copy = Array.isArray(o) ? [] : Object.create(Object.getPrototypeOf(o));
180
+ cache.push({ original: o, copy });
181
+ Object
182
+ .getOwnPropertyNames(o)
183
+ .forEach(function (prop) {
184
+ const propdesc = Object.getOwnPropertyDescriptor(o, prop);
185
+ Object.defineProperty(
186
+ copy,
187
+ prop,
188
+ propdesc.get || propdesc.set
189
+ ? propdesc // just copy accessor properties
190
+ : { // deep copy data properties
191
+ writable: propdesc.writable,
192
+ configurable: propdesc.configurable,
193
+ enumerable: propdesc.enumerable,
194
+ value: skipProps.includes(prop) ? propdesc.value : cloneDeep(propdesc.value, skipProps, cache),
195
+ }
196
+ );
197
+ });
198
+ return copy;
199
+ case 'function':
200
+ case 'symbol':
201
+ console.warn('cloneDeep: ' + typeof o + 's not fully supported:', o);
202
+ case true:
203
+ // null, undefined or falsy primitive
204
+ default:
205
+ return o;
206
+ }
207
+ }
208
+
209
+ var _ = {
170
210
  __proto__: null,
171
211
  Colors: Colors,
172
212
  addClass: addClass,
173
213
  append: append,
174
214
  clamp: clamp,
215
+ cloneDeep: cloneDeep,
175
216
  coordPropsToMeta: coordPropsToMeta,
176
217
  create: create,
177
218
  drawCanvas: drawCanvas,
@@ -203,1422 +244,1425 @@
203
244
  toggleStyle: toggleStyle,
204
245
  wrapDelta: wrapDelta,
205
246
  wrapNum: wrapNum
206
- });
247
+ };
207
248
 
208
- var Options = {
209
- autofitBounds: true,
210
- autohide: false,
211
- autohideMarker: true,
212
- almostover: true,
213
- altitude: true,
214
- closeBtn: true,
215
- collapsed: false,
216
- detached: true,
217
- distance: true,
218
- distanceMarkers: { lazy: true, distance: true, direction: true },
219
- dragging: !L.Browser.mobile,
220
- downloadLink: 'link',
221
- elevationDiv: "#elevation-div",
222
- edgeScale: { bar: true, icon: false, coords: false },
223
- followMarker: true,
224
- imperial: false,
225
- legend: true,
226
- handlers: ["Distance", "Time", "Altitude", "Slope", "Speed", "Acceleration"],
227
- hotline: 'elevation',
228
- marker: 'elevation-line',
229
- markerIcon: L.divIcon({
230
- className: 'elevation-position-marker',
231
- html: '<i class="elevation-position-icon"></i>',
232
- iconSize: [32, 32],
233
- iconAnchor: [16, 16],
234
- }),
235
- position: "topright",
236
- polyline: {
237
- className: 'elevation-polyline',
238
- color: '#000',
239
- opacity: 0.75,
240
- weight: 5,
241
- lineCap: 'round'
242
- },
243
- polylineSegments: {
244
- className: 'elevation-polyline-segments',
245
- color: '#F00',
246
- interactive: false,
247
- },
248
- preferCanvas: false,
249
- reverseCoords: false,
250
- ruler: true,
251
- theme: "lightblue-theme",
252
- summary: 'inline',
253
- slope: false,
254
- speed: false,
255
- time: true,
256
- timeFactor: 3600,
257
- timestamps: false,
258
- trkStart: { className: 'start-marker', radius: 6, weight: 2, color: '#fff', fillColor: '#00d800', fillOpacity: 1, interactive: false },
259
- trkEnd: { className: 'end-marker', radius: 6, weight: 2, color: '#fff', fillColor: '#ff0606', fillOpacity: 1, interactive: false },
260
- waypoints: true,
261
- wptIcons: {
262
- '': L.divIcon({
263
- className: 'elevation-waypoint-marker',
264
- html: '<i class="elevation-waypoint-icon default"></i>',
265
- iconSize: [30, 30],
266
- iconAnchor: [8, 30],
267
- }),
268
- },
269
- wptLabels: true,
270
- xAttr: "dist",
271
- xLabel: "km",
272
- yAttr: "z",
273
- yLabel: "m",
274
- zFollow: false,
275
- zooming: !L.Browser.Mobile,
276
-
277
- // Quite uncommon and undocumented options
278
- margins: { top: 30, right: 30, bottom: 30, left: 40 },
279
- height: (screen.height * 0.3) || 200,
280
- width: (screen.width * 0.6) || 600,
281
- xTicks: undefined,
282
- yTicks: undefined,
283
-
284
- decimalsX: 2,
285
- decimalsY: 0,
286
- forceAxisBounds: false,
287
- interpolation: "curveLinear",
288
- yAxisMax: undefined,
289
- yAxisMin: undefined,
290
-
291
- // Prevent CORS issues for relative locations (dynamic import)
292
- srcFolder: ((document.currentScript && document.currentScript.src) || (({ url: (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (document.currentScript && document.currentScript.src || new URL('leaflet-elevation.js', document.baseURI).href)) }) && (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (document.currentScript && document.currentScript.src || new URL('leaflet-elevation.js', document.baseURI).href)))).split("/").slice(0,-1).join("/") + '/',
249
+ var Options = {
250
+ autofitBounds: true,
251
+ autohide: false,
252
+ autohideMarker: true,
253
+ almostover: true,
254
+ altitude: true,
255
+ closeBtn: true,
256
+ collapsed: false,
257
+ detached: true,
258
+ distance: true,
259
+ distanceMarkers: { lazy: true, distance: true, direction: true },
260
+ dragging: !L.Browser.mobile,
261
+ downloadLink: 'link',
262
+ elevationDiv: "#elevation-div",
263
+ edgeScale: { bar: true, icon: false, coords: false },
264
+ followMarker: true,
265
+ imperial: false,
266
+ legend: true,
267
+ handlers: ["Distance", "Time", "Altitude", "Slope", "Speed", "Acceleration"],
268
+ hotline: 'elevation',
269
+ marker: 'elevation-line',
270
+ markerIcon: L.divIcon({
271
+ className: 'elevation-position-marker',
272
+ html: '<i class="elevation-position-icon"></i>',
273
+ iconSize: [32, 32],
274
+ iconAnchor: [16, 16],
275
+ }),
276
+ position: "topright",
277
+ polyline: {
278
+ className: 'elevation-polyline',
279
+ color: '#000',
280
+ opacity: 0.75,
281
+ weight: 5,
282
+ lineCap: 'round'
283
+ },
284
+ polylineSegments: {
285
+ className: 'elevation-polyline-segments',
286
+ color: '#F00',
287
+ interactive: false,
288
+ },
289
+ preferCanvas: false,
290
+ reverseCoords: false,
291
+ ruler: true,
292
+ theme: "lightblue-theme",
293
+ summary: 'inline',
294
+ slope: false,
295
+ speed: false,
296
+ time: true,
297
+ timeFactor: 3600,
298
+ timestamps: false,
299
+ trkStart: { className: 'start-marker', radius: 6, weight: 2, color: '#fff', fillColor: '#00d800', fillOpacity: 1, interactive: false },
300
+ trkEnd: { className: 'end-marker', radius: 6, weight: 2, color: '#fff', fillColor: '#ff0606', fillOpacity: 1, interactive: false },
301
+ waypoints: true,
302
+ wptIcons: {
303
+ '': L.divIcon({
304
+ className: 'elevation-waypoint-marker',
305
+ html: '<i class="elevation-waypoint-icon default"></i>',
306
+ iconSize: [30, 30],
307
+ iconAnchor: [8, 30],
308
+ }),
309
+ },
310
+ wptLabels: true,
311
+ xAttr: "dist",
312
+ xLabel: "km",
313
+ yAttr: "z",
314
+ yLabel: "m",
315
+ zFollow: false,
316
+ zooming: !L.Browser.Mobile,
317
+
318
+ // Quite uncommon and undocumented options
319
+ margins: { top: 30, right: 30, bottom: 30, left: 40 },
320
+ height: (screen.height * 0.3) || 200,
321
+ width: (screen.width * 0.6) || 600,
322
+ xTicks: undefined,
323
+ yTicks: undefined,
324
+
325
+ decimalsX: 2,
326
+ decimalsY: 0,
327
+ forceAxisBounds: false,
328
+ interpolation: "curveLinear",
329
+ yAxisMax: undefined,
330
+ yAxisMin: undefined,
331
+
332
+ // Prevent CORS issues for relative locations (dynamic import)
333
+ srcFolder: ((document.currentScript && document.currentScript.src) || (({ url: (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (document.currentScript && document.currentScript.src || new URL('leaflet-elevation.js', document.baseURI).href)) }) && (typeof document === 'undefined' && typeof location === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : typeof document === 'undefined' ? location.href : (document.currentScript && document.currentScript.src || new URL('leaflet-elevation.js', document.baseURI).href)))).split("/").slice(0,-1).join("/") + '/',
293
334
  };
294
335
 
295
- // "leaflet-i18n" fallback
296
- if (!L._ || !L.i18n) {
297
- L._ = L.i18n = (string, data) => string;
298
- }
299
-
300
- const Elevation = L.Control.Elevation = L.Control.extend({
301
-
302
- includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
303
-
304
- options: Options,
305
- __mileFactor: 0.621371, // 1 km = (0.621371 mi)
306
- __footFactor: 3.28084, // 1 m = (3.28084 ft)
307
- __D3: 'https://unpkg.com/d3@7.8.4/dist/d3.min.js',
308
- __TOGEOJSON: 'https://unpkg.com/@tmcw/togeojson@5.6.0/dist/togeojson.umd.js',
309
- __LGEOMUTIL: 'https://unpkg.com/leaflet-geometryutil@0.10.1/src/leaflet.geometryutil.js',
310
- __LALMOSTOVER: 'https://unpkg.com/leaflet-almostover@1.0.1/src/leaflet.almostover.js',
311
- __LHOTLINE: '../libs/leaflet-hotline.min.js',
312
- __LDISTANCEM: '../libs/leaflet-distance-marker.min.js',
313
- __LEDGESCALE: '../libs/leaflet-edgescale.min.js',
314
- __LCHART: '../src/components/chart.js',
315
- __LMARKER: '../src/components/marker.js',
316
- __LSUMMARY: '../src/components/summary.js',
317
- __modulesFolder: '../src/handlers/',
318
- __btnIcon: '../images/elevation.svg',
319
-
320
- /*
321
- * Add data to the diagram either from GPX or GeoJSON and update the axis domain and data
322
- */
323
- addData(d, layer) {
324
- this.import(this.__D3)
325
- .then(() => {
326
- if (this._modulesLoaded) {
327
- layer = layer ?? (d.on && d);
328
- this._addData(d);
329
- this._addLayer(layer);
330
- this._fireEvt("eledata_added", { data: d, layer: layer, track_info: this.track_info });
331
- } else {
332
- this.once('modules_loaded', () => this.addData(d,layer));
333
- }
334
- });
335
- },
336
-
337
- /**
338
- * Adds the control to the given map.
339
- */
340
- addTo(map) {
341
- if (this.options.detached) {
342
- let parent = select(this.options.elevationDiv);
343
- let eleDiv = this.onAdd(map);
344
- parent ? append(parent, eleDiv) : insert(map.getContainer(), eleDiv, 'afterend');
345
- } else {
346
- L.Control.prototype.addTo.call(this, map);
347
- }
348
- return this;
349
- },
350
-
351
- /*
352
- * Reset data and display
353
- */
354
- clear() {
355
- if (this._marker) this._marker.remove();
356
- if (this._chart) this._clearChart();
357
- if (this._layers) this._clearLayers(this._layers);
358
- if (this._markers) this._clearLayers(this._markers);
359
- if (this._circleMarkers) this._circleMarkers.remove();
360
- if (this._hotline) this._hotline.eachLayer(l => l.options.renderer.remove()); // hotfix for: https://github.com/Raruto/leaflet-elevation/issues/233
361
- if (this._hotline) this._clearLayers(this._hotline);
362
-
363
- this._data = [];
364
- this.track_info = {};
365
-
366
- this._fireEvt("eledata_clear");
367
-
368
- this._updateChart();
369
- },
370
-
371
- _clearChart() {
372
- if (this._events && this._events.elechart_updated) {
373
- this._events.elechart_updated.forEach(({fn, ctx}) => this.off('elechart_updated', fn, ctx));
374
- }
375
- if (this._chart && this._chart._container) {
376
- this._chart._container.selectAll('g.point .point').remove();
377
- this._chart.clear();
378
- }
379
- },
380
-
381
- _clearLayers(l) {
382
- l = l || this._layers;
383
- if (l && l.eachLayer) {
384
- l.eachLayer(f => f.remove());
385
- l.clearLayers();
386
- }
387
- },
388
-
389
- /**
390
- * TODO: Create a base class to handle custom data attributes (heart rate, cadence, temperature, ...)
391
- *
392
- * @link https://leafletjs.com/examples/extending/extending-3-controls.html#handlers
393
- */
394
- // addHandler: function (name, HandlerClass) {
395
- // if (HandlerClass) {
396
- // let handler = this[name] = new HandlerClass(this);
397
- // this.handlers.push(handler);
398
- // if (this.options[name]) {
399
- // handler.enable();
400
- // }
401
- // }
402
- // return this;
403
- // },
404
-
405
- /**
406
- * Disable chart brushing.
407
- */
408
- disableBrush() {
409
- this._chart._brushEnabled = false;
410
- this._resetDrag();
411
- },
412
-
413
- /**
414
- * Enable chart brushing.
415
- */
416
- enableBrush() {
417
- this._chart._brushEnabled = true;
418
- },
419
-
420
- /**
421
- * Disable chart zooming.
422
- */
423
- disableZoom() {
424
- this._chart._zoomEnabled = false;
425
- this._chart._resetZoom();
426
- },
427
-
428
- /**
429
- * Enable chart zooming.
430
- */
431
- enableZoom() {
432
- this._chart._zoomEnabled = true;
433
- },
434
-
435
- /**
436
- * Sets a map view that contains the given geographical bounds.
437
- */
438
- fitBounds(bounds) {
439
- bounds = bounds || this.getBounds();
440
- if (this._map && bounds.isValid()) this._map.fitBounds(bounds);
441
- },
442
-
443
- getBounds(data) {
444
- return L.latLngBounds((data || this._data).map((d) => d.latlng));
445
- },
446
-
447
- /**
448
- * Get default zoom level (followMarker: true).
449
- */
450
- getZFollow() {
451
- return this.options.zFollow;
452
- },
453
-
454
- /**
455
- * Hide current elevation chart profile.
456
- */
457
- hide() {
458
- style(this._container, "display", "none");
459
- },
460
-
461
- /**
462
- * Initialize chart control "options" and "container".
463
- */
464
- initialize(opts) {
465
-
466
- // opts = L.setOptions(this, opts);
467
-
468
- // Fixes: https://github.com/Raruto/leaflet-elevation/pull/240
469
- opts = L.setOptions(this, L.extend({}, structuredClone(Options), opts)); // "deep copy" nested objects (multiple charts)
470
-
471
- this._data = [];
472
- this._layers = L.featureGroup();
473
- this._markers = L.featureGroup();
474
- this._hotline = L.featureGroup();
475
- this._circleMarkers = L.featureGroup();
476
- this._markedSegments = L.polyline([]);
477
- this._start = L.circleMarker([0,0], (opts.trkStart || Options.trkStart));
478
- this._end = L.circleMarker([0,0], (opts.trkEnd || Options.trkEnd));
479
- this._chartEnabled = true;
480
- this._yCoordMax = -Infinity;
481
- this.track_info = {};
482
- // this.handlers = [];
483
-
484
- if (opts.followMarker) this._setMapView = throttle(this._setMapView, 300, this);
485
- if (opts.legend) opts.margins.bottom += 30;
486
- if (opts.theme) opts.polylineSegments.className += ' ' + opts.theme;
487
- if (opts.wptIcons === true) opts.wptIcons = Options.wptIcons;
488
- if (opts.distanceMarkers === true) opts.distanceMarkers = Options.distanceMarkers;
489
- if (opts.trkStart) this._start.addTo(this._circleMarkers);
490
- if (opts.trkEnd) this._end.addTo(this._circleMarkers);
491
-
492
-
493
- this._markedSegments.setStyle(opts.polylineSegments);
494
-
495
- // Leaflet canvas renderer colors
496
- L.extend(Colors, opts.colors || {});
497
-
498
- // Various stuff
499
- this._fixCanvasPaths();
500
- this._fixTooltipSize();
501
-
502
- },
503
-
504
- /**
505
- * Javascript scripts downloader (lazy loader)
506
- */
507
- import(src, condition) {
508
- switch(src) {
509
- case this.__D3: condition = typeof d3 !== 'object'; break;
510
- case this.__TOGEOJSON: condition = typeof toGeoJSON !== 'object'; break;
511
- case this.__LGEOMUTIL: condition = typeof L.GeometryUtil !== 'object'; break;
512
- case this.__LALMOSTOVER: condition = typeof L.Handler.AlmostOver !== 'function'; break;
513
- case this.__LDISTANCEM: condition = typeof L.DistanceMarkers !== 'function'; break;
514
- case this.__LEDGESCALE: condition = typeof L.Control.EdgeScale !== 'function'; break;
515
- case this.__LHOTLINE: condition = typeof L.Hotline !== 'function'; break;
516
- }
517
- return condition !== false ? import(resolveURL(src, this.options.srcFolder)) : Promise.resolve();
518
- },
519
-
520
- /**
521
- * Load elevation data (GPX, GeoJSON, KML or TCX).
522
- */
523
- load(data) {
524
- this._parseFromString(data).then( geojson => geojson ? this._loadLayer(geojson) : this._loadFile(data));
525
- },
526
-
527
- /**
528
- * Create container DOM element and related event listeners.
529
- * Called on control.addTo(map).
530
- */
531
- onAdd(map) {
532
- this._map = map;
533
-
534
- let container = this._container = create("div", "elevation-control " + this.options.theme + " " + (this.options.detached ? 'elevation-detached' : 'leaflet-control'), this.options.detached ? { id: 'elevation-' + randomId() } : {});
535
-
536
- if (!this.eleDiv) this.eleDiv = container;
537
-
538
- this._loadModules(this.options.handlers).then(() => { // Inject here required modules (data handlers)
539
- this._initChart(container);
540
- this._initButton(container);
541
- this._initSummary(container);
542
- this._initMarker(map);
543
- this._initLayer(map);
544
- this._modulesLoaded = true;
545
- this.fire('modules_loaded');
546
- });
547
-
548
- this.fire('add');
549
-
550
- return container;
551
- },
552
-
553
- /**
554
- * Clean up control code and related event listeners.
555
- * Called on control.remove().
556
- */
557
- onRemove(map) {
558
- this._container = null;
559
-
560
- map
561
- .off('zoom viewreset zoomanim', this._hideMarker, this)
562
- .off('resize', this._resetView, this)
563
- .off('resize', this._resizeChart, this)
564
- .off('mousedown', this._resetDrag, this);
565
-
566
- off(map.getContainer(), 'mousewheel', this._resetDrag, this);
567
- off(map.getContainer(), 'touchstart', this._resetDrag, this);
568
- off(document, 'keydown', this._onKeyDown, this);
569
-
570
- this
571
- .off('eledata_added eledata_loaded', this._updateChart, this)
572
- .off('eledata_added eledata_loaded', this._updateSummary, this);
573
-
574
- this.fire('remove');
575
- },
576
-
577
- /**
578
- * Redraws the chart control. Sometimes useful after screen resize.
579
- */
580
- redraw() {
581
- this._resizeChart();
582
- },
583
-
584
- /**
585
- * Set default zoom level (followMarker: true).
586
- */
587
- setZFollow(zoom) {
588
- this.options.zFollow = zoom;
589
- },
590
-
591
- /**
592
- * Hide current elevation chart profile.
593
- */
594
- show() {
595
- style(this._container, "display", "block");
596
- },
597
-
598
- /*
599
- * Parsing data either from GPX or GeoJSON and update the diagram data
600
- */
601
- _addData(d) {
602
- if (!d) {
603
- return;
604
- }
605
-
606
- // Standard GeoJSON
607
- if (d.type === "FeatureCollection" ) {
608
- return each(d.features, feature => this._addData(feature));
609
- } else if (d.type === "Feature") {
610
- let geom = d.geometry;
611
- if (geom) {
612
- switch (geom.type) {
613
- case 'LineString': return this._addGeoJSONData(geom.coordinates, d.properties);
614
- case 'MultiLineString': return each(geom.coordinates, (coords, i) => this._addGeoJSONData(coords, d.properties, i));
615
- case 'Point':
616
- default: return console.warn('Unsopperted GeoJSON feature geometry type:' + geom.type);
617
- }
618
- }
619
- }
620
-
621
- // Fallback for leaflet layers (eg. L.Gpx)
622
- if (d._latlngs) {
623
- return this._addGeoJSONData(d._latlngs, d.feature && d.feature.properties);
624
- }
625
-
626
- },
627
-
628
- /*
629
- * Parsing of GeoJSON data lines and their elevation in z-coordinate
630
- */
631
- _addGeoJSONData(coords, properties, nestingLevel) {
632
-
633
- // "coordinateProperties" property is generated inside "@tmcw/toGeoJSON"
634
- let props = (properties && properties.coordinateProperties) || properties;
635
-
636
- coords.forEach((point, i) => {
637
-
638
- // GARMIN_EXTENSIONS = ["hr", "cad", "atemp", "wtemp", "depth", "course", "bearing"];
639
- point.meta = point.meta ?? { time: null, ele: null };
640
-
641
- point.prev = (attr) => (attr ? this._data[i > 0 ? i - 1 : 0][attr] : this._data[i > 0 ? i - 1 : 0]);
642
-
643
- this.fire("elepoint_init", { point: point, props: props, id: i, isMulti: nestingLevel });
644
-
645
- this._addPoint(
646
- point.lat ?? point[1],
647
- point.lng ?? point[0],
648
- point.alt ?? point.meta.ele ?? point[2]
649
- );
650
-
651
- this.fire("elepoint_added", { point: point, index: this._data.length - 1 });
652
-
653
- if (this._yCoordMax < this._data[this._data.length - 1][this.options.yAttr]) this._yCoordMax = this._data[this._data.length - 1][this.options.yAttr];
654
- });
655
-
656
- this.fire("eletrack_added", { coords: coords, index: this._data.length - 1 });
657
- },
658
-
659
- /*
660
- * Parse and push a single (x, y, z) point to current elevation profile.
661
- */
662
- _addPoint(x, y, z) {
663
- if (this.options.reverseCoords) {
664
- [x, y] = [y, x];
665
- }
666
-
667
- this._data.push({
668
- x: x,
669
- y: y,
670
- z: z,
671
- latlng: L.latLng(x, y, z)
672
- });
673
-
674
- this.fire("eledata_updated", { index: this._data.length - 1 });
675
- },
676
-
677
- _addLayer(layer) {
678
- if (layer) this._layers.addLayer(layer);
679
- // Postpone adding the distance markers (lazy: true)
680
- if (layer && this.options.distanceMarkers && this.options.distanceMarkers.lazy) {
681
- layer.on('add remove', ({target, type}) => L.DistanceMarkers && target instanceof L.Polyline && target[type + 'DistanceMarkers']());
682
- }
683
- return layer;
684
- },
685
-
686
- _addMarker(marker) {
687
- if (marker) this._markers.addLayer(marker);
688
- return marker;
689
- },
690
-
691
- /**
692
- * Initialize "L.AlmostOver" integration
693
- */
694
- _initAlmostOverHandler(map, layer) {
695
- return (map && this.options.almostOver && !L.Browser.mobile) ? Promise.all([
696
- this.import(this.__LGEOMUTIL),
697
- this.import(this.__LALMOSTOVER)
698
- ]).then(() => {
699
- map.addHandler('almostOver', L.Handler.AlmostOver);
700
- if (L.GeometryUtil && map.almostOver && map.almostOver.enabled()) {
701
- map.almostOver.addLayer(layer);
702
- map
703
- .on('almost:move', this._onMouseMoveLayer, this)
704
- .on('almost:out', this._onMouseOut, this);
705
- this.once('eledata_clear', () => {
706
- map.almostOver.removeLayer(layer);
707
- map
708
- .off('almost:move', this._onMouseMoveLayer, this)
709
- .off('almost:out', this._onMouseOut, this);
710
- });
711
- }
712
- }) : Promise.resolve();
713
- },
714
-
715
- /**
716
- * Initialize "L.DistanceMarkers" integration
717
- */
718
- _initDistanceMarkers() {
719
- return this.options.distanceMarkers ? Promise.all([this.import(this.__LGEOMUTIL), this.import(this.__LDISTANCEM)]) : Promise.resolve();
720
- },
721
-
722
- /**
723
- * Initialize "L.Control.EdgeScale" integration
724
- */
725
- _initEdgeScale(map) {
726
- return this.options.edgeScale ? Promise.all([this.import(this.__LEDGESCALE)])
727
- .then(() => {
728
- map.edgeScaleControl = map.edgeScaleControl || L.control.edgeScale('boolean' !== typeof this.options.edgeScale ? this.options.edgeScale : {}).addTo(map);
729
- }) : Promise.resolve();
730
- },
731
-
732
- _initHotLine(layer) {
733
- let prop = typeof this.options.hotline == 'string' ? this.options.hotline : 'elevation';
734
- return this.options.hotline ? this.import(this.__LHOTLINE)
735
- .then(() => {
736
- layer.eachLayer((trkseg) => {
737
- if(trkseg.feature.geometry.type != "Point") {
738
- let geo = L.geoJson(trkseg.toGeoJSON(), { coordsToLatLng: (coords) => L.latLng(coords[0], coords[1], coords[2] * (this.options.altitudeFactor || 1))});
739
- let line = L.hotline(geo.toGeoJSON().features[0].geometry.coordinates, {
740
- renderer: L.Hotline.renderer(),
741
- min: isFinite(this.track_info[prop + '_min']) ? this.track_info[prop + '_min'] : 0,
742
- max: isFinite(this.track_info[prop + '_max']) ? this.track_info[prop + '_max'] : 1,
743
- palette: {
744
- 0.0: '#008800',
745
- 0.5: '#ffff00',
746
- 1.0: '#ff0000'
747
- },
748
- weight: 5,
749
- outlineColor: '#000000',
750
- outlineWidth: 1
751
- }).addTo(this._hotline);
752
- let alpha = trkseg.options.style && trkseg.options.style.opacity || 1;
753
- trkseg.on('add remove', ({type}) => {
754
- trkseg.setStyle({opacity: (type == 'add' ? 0 : alpha)});
755
- line[(type == 'add' ? 'addTo' : 'removeFrom')](trkseg._map);
756
- if (line._renderer) line._renderer._container.parentElement.insertBefore(line._renderer._container, line._renderer._container.parentElement.firstChild);
757
- });
758
- }
759
- });
760
- }) : Promise.resolve();
761
- },
762
-
763
- /**
764
- * Initialize "L.AlmostOver" and "L.DistanceMarkers"
765
- */
766
- _initMapIntegrations(layer) {
767
- let map = this._map;
768
- if (map) {
769
- if (this._data.length) {
770
- this._start.setLatLng(this._data[0].latlng);
771
- this._end.setLatLng(this._data[this._data.length -1].latlng);
772
- }
773
- Promise.all([
774
- this._initHotLine(layer),
775
- this._initAlmostOverHandler(map, layer),
776
- this._initDistanceMarkers(),
777
- this._initEdgeScale(map),
778
- ]).then(() => {
779
- if (this.options.polyline) {
780
- this._layers.addLayer(layer.addTo(map)); // hotfix for: https://github.com/Raruto/leaflet-elevation/issues/233
781
- this._circleMarkers.addTo(map);
782
- }
783
- if (this.options.autofitBounds) {
784
- this.fitBounds(layer.getBounds());
785
- }
786
- map.invalidateSize();
787
- });
788
- } else {
789
- this.once('add', () => this._initMapIntegrations(layer));
790
- }
791
- },
792
-
793
- /*
794
- * Collapse current chart control.
795
- */
796
- _collapse() {
797
- replaceClass(this._container, 'elevation-expanded', 'elevation-collapsed');
798
- if (this._map) this._map.invalidateSize();
799
- },
800
-
801
- /*
802
- * Expand current chart control.
803
- */
804
- _expand() {
805
- replaceClass(this._container, 'elevation-collapsed', 'elevation-expanded');
806
- if (this._map) this._map.invalidateSize();
807
- },
808
-
809
- /**
810
- * Add some basic colors to leaflet canvas renderer (preferCanvas: true).
811
- */
812
- _fixCanvasPaths() {
813
- let oldProto = L.Canvas.prototype._fillStroke;
814
- let control = this;
815
-
816
- let theme = this.options.theme.split(' ')[0].replace('-theme', '');
817
- let color = Colors[theme] || {};
818
-
819
- L.Canvas.include({
820
- _fillStroke(ctx, layer) {
821
- if (control._layers.hasLayer(layer)) {
822
-
823
- let options = layer.options;
824
-
825
- options.color = color.line || color.area || theme;
826
- options.stroke = !!options.color;
827
-
828
- oldProto.call(this, ctx, layer);
829
-
830
- if (options.stroke && options.weight !== 0) {
831
- let oldVal = ctx.globalCompositeOperation || 'source-over';
832
- ctx.globalCompositeOperation = 'destination-over';
833
- ctx.strokeStyle = color.outline || '#FFF';
834
- ctx.lineWidth = options.weight * 1.75;
835
- ctx.stroke();
836
- ctx.globalCompositeOperation = oldVal;
837
- }
838
-
839
- } else {
840
- oldProto.call(this, ctx, layer);
841
- }
842
- }
843
- });
844
- },
845
-
846
- /**
847
- * Partial fix for initial tooltip size
848
- *
849
- * @link https://github.com/Raruto/leaflet-elevation/issues/81#issuecomment-713477050
850
- */
851
- _fixTooltipSize() {
852
- this.on('elechart_init', () =>
853
- this.once('elechart_change elechart_hover', ({data, xCoord}) => {
854
- if (this._chartEnabled) {
855
- this._chart._showDiagramIndicator(data, xCoord);
856
- this._chart._showDiagramIndicator(data, xCoord);
857
- }
858
- this._updateMarker(data);
859
- })
860
- );
861
- },
862
-
863
- /*
864
- * Finds a data entry for the given LatLng
865
- */
866
- _findItemForLatLng(latlng) {
867
- return this._data[this._chart._findIndexForLatLng(latlng)];
868
- },
869
-
870
- /*
871
- * Finds a data entry for the given xDiagCoord
872
- */
873
- _findItemForX(x) {
874
- return this._data[this._chart._findIndexForXCoord(x)];
875
- },
876
-
877
- /**
878
- * Fires an event of the specified type.
879
- */
880
- _fireEvt(type, data, propagate) {
881
- if (this.fire) this.fire(type, data, propagate);
882
- if (this._map) this._map.fire(type, data, propagate);
883
- },
884
-
885
- /*
886
- * Hides the position/height indicator marker drawn onto the map
887
- */
888
- _hideMarker() {
889
- if (this.options.autohideMarker) {
890
- this._marker.remove();
891
- }
892
- },
893
-
894
- /**
895
- * Generate "svg" chart (DOM element).
896
- */
897
- _initChart(container) {
898
- let opts = this.options;
899
- let map = this._map;
900
-
901
- if (opts.detached) {
902
- let { offsetWidth, offsetHeight} = this.eleDiv;
903
- if (offsetWidth > 0) opts.width = offsetWidth;
904
- if (offsetHeight > 20) opts.height = offsetHeight - 20; // 20 = horizontal scrollbar size.
905
- } else {
906
- let { clientWidth } = map.getContainer();
907
- opts._maxWidth = opts._maxWidth > opts.width ? opts._maxWidth : opts.width;
908
- this._container.style.maxWidth = opts._maxWidth + 'px';
909
- if (opts._maxWidth > clientWidth) opts.width = clientWidth - 30;
910
- }
911
-
912
- Promise.all([
913
- this.import(this.__D3),
914
- this.import(this.__LCHART)
915
- ]).then((m) => {
916
-
917
- let chart = this._chart = new (m[1] || Elevation).Chart(opts, this);
918
-
919
- this._x = this._chart._x;
920
- this._y = this._chart._y;
921
-
922
- d3
923
- .select(container)
924
- .call(chart.render());
925
-
926
- chart
927
- .on('reset_drag', this._hideMarker, this)
928
- .on('mouse_enter', this._onMouseEnter, this)
929
- .on('dragged', this._onDragEnd, this)
930
- .on('mouse_move', this._onMouseMove, this)
931
- .on('mouse_out', this._onMouseOut, this)
932
- .on('ruler_filter', this._onRulerFilter, this)
933
- .on('zoom', this._updateChart, this)
934
- .on('elepath_toggle', this._onToggleChart, this)
935
- .on('margins_updated', this._resizeChart, this);
936
-
937
-
938
- this.fire("elechart_init");
939
-
940
- map
941
- .on('zoom viewreset zoomanim', this._hideMarker, this)
942
- .on('resize', this._resetView, this)
943
- .on('resize', this._resizeChart, this)
944
- .on('rotate', this._rotateMarker, this)
945
- .on('mousedown', this._resetDrag, this);
946
-
947
- on(map.getContainer(), 'mousewheel', this._resetDrag, this);
948
- on(map.getContainer(), 'touchstart', this._resetDrag, this);
949
- on(document, 'keydown', this._onKeyDown, this);
950
-
951
- this
952
- .on('eledata_added eledata_loaded', this._updateChart, this)
953
- .on('eledata_added eledata_loaded', this._updateSummary, this);
954
-
955
- this._updateChart();
956
- this._updateSummary();
957
- });
958
-
959
-
960
- },
961
-
962
- _initLayer() {
963
- this._layers
964
- .on('layeradd layerremove', ({layer, type}) => {
965
- let node = layer.getElement && layer.getElement();
966
- toggleClass(node, this.options.polyline.className + ' ' + this.options.theme, type == 'layeradd');
967
- toggleEvent(layer, "mousemove", this._onMouseMoveLayer.bind(this), type == 'layeradd');
968
- toggleEvent(layer, "mouseout", this._onMouseOut.bind(this), type == 'layeradd');
969
- });
970
- },
971
-
972
- _initMarker(map) {
973
- let pane = map.getPane('elevationPane');
974
- if (!pane) {
975
- pane = this._pane = map.createPane('elevationPane', map.getPane('norotatePane') || map.getPane('mapPane'));
976
- pane.style.zIndex = 625; // This pane is above markers but below popups.
977
- pane.style.pointerEvents = 'none';
978
- }
979
-
980
- if (this._renderer) this._renderer.remove();
981
- this._renderer = L.svg({ pane: "elevationPane" }).addTo(this._map); // default leaflet svg renderer
982
-
983
- Promise.all([
984
- this.import(this.__D3),
985
- this.import(this.__LMARKER)
986
- ]).then((m) => {
987
- this._marker = new (m[1] || Elevation).Marker(this.options, this);
988
- this.fire("elechart_marker");
989
- });
990
- },
991
-
992
- /**
993
- * Inspired by L.Control.Layers
994
- */
995
- _initButton(container) {
996
- L.DomEvent
997
- .disableClickPropagation(container)
998
- .disableScrollPropagation(container);
999
-
1000
- this.options.collapsed ? this._collapse() : this._expand();
1001
-
1002
- if (this.options.autohide) {
1003
- on(container, 'mouseover', this._expand, this);
1004
- on(container, 'mouseout', this._collapse, this);
1005
- this._map.on('click', this._collapse, this);
1006
- }
1007
-
1008
- if (this.options.closeBtn) {
1009
- let link = this._button = create('a', "elevation-toggle-icon", { href: '#', title: L._('Elevation'), }, container);
1010
- on(link, 'click', L.DomEvent.stop);
1011
- on(link, 'click', this._toggle, this);
1012
- on(link, 'focus', this._toggle, this);
1013
- fetch(resolveURL(this.__btnIcon, this.options.srcFolder)).then(r => r.ok && r.text().then(img => link.innerHTML = img));
1014
- }
1015
- },
1016
-
1017
- _initSummary(container) {
1018
- this.import(this.__LSUMMARY).then((m)=>{
1019
- this._summary = new (m || Elevation).Summary({ summary: this.options.summary }, this);
1020
-
1021
- this.on('elechart_init', () => {
1022
- d3.select(container).call(this._summary.render());
1023
- });
1024
- });
1025
- },
1026
-
1027
- /**
1028
- * Retrieve data from a remote url (HTTP).
1029
- */
1030
- _loadFile(url) {
1031
- fetch(url)
1032
- .then((response) => response.text())
1033
- .then((data) => {
1034
- this._downloadURL = url; // TODO: handle multiple urls?
1035
- this._parseFromString(data)
1036
- .then( geojson => geojson && this._loadLayer(geojson));
1037
- }).catch((err) => console.warn(err));
1038
- },
1039
-
1040
- /**
1041
- * Dynamically import only required javascript modules (code splitting)
1042
- */
1043
- _loadModules(handlers) {
1044
- // First map known classnames (eg. "Altitude" --> L.Control.Elevation.Altitude)
1045
- handlers = handlers.map((h) => typeof h === 'string' && typeof Elevation[h] !== "undefined" ? Elevation[h] : h);
1046
- // Then load optional classes and custom imports (eg. "Cadence" --> import('../src/handlers/cadence.js'))
1047
- let modules = handlers.map(file => (typeof file === 'string' && this.import(this.__modulesFolder + file.toLowerCase() + '.js')) || (file instanceof Promise && file) || Promise.resolve());
1048
- return Promise.all(modules).then((m) => {
1049
- each(m, (exported, i) => {
1050
- let fn = exported && Object.keys(exported)[0];
1051
- if (fn) {
1052
- handlers[i] = Elevation[fn] = (Elevation[fn] ?? exported[fn]);
1053
- }
1054
- });
1055
- each(handlers, h => ["function", "object"].includes(typeof h) && this._registerHandler(h));
1056
- });
1057
- },
1058
-
1059
- /**
1060
- * Simple GeoJSON data loader (L.GeoJSON).
1061
- */
1062
- _loadLayer(geojson) {
1063
- let { polyline, theme, waypoints, wptIcons, wptLabels, distanceMarkers } = this.options;
1064
- let style = L.extend({}, polyline);
1065
-
1066
- if (theme) {
1067
- style.className += ' ' + theme;
1068
- }
1069
-
1070
- if (geojson.name) {
1071
- this.track_info.name = geojson.name;
1072
- }
1073
-
1074
- let layer = L.geoJson(geojson, {
1075
- distanceMarkers: distanceMarkers,
1076
- style: style,
1077
- pointToLayer: (feature, latlng) => {
1078
- if (waypoints) {
1079
- let { desc, name, sym } = feature.properties;
1080
- desc = desc || '';
1081
- name = name || '';
1082
- // Handle chart waypoints (dots)
1083
- if ([true, 'dots'].includes(waypoints)) {
1084
- this._registerCheckPoint({
1085
- latlng: latlng,
1086
- label : ([true, 'dots'].includes(wptLabels) ? name : '')
1087
- });
1088
- }
1089
- // Handle map waypoints (markers)
1090
- if ([true, 'markers'].includes(waypoints) && wptIcons != false) {
1091
- return this._registerMarker({
1092
- latlng : latlng,
1093
- sym : (sym ?? name).replace(' ', '-').replace('"', '').replace("'", '').toLowerCase(),
1094
- content: [true, 'markers'].includes(wptLabels) && (name || desc) && decodeURI("<b>" + name + "</b>" + (desc.length > 0 ? '<br>' + desc : ''))
1095
- });
1096
- }
1097
- }
1098
- },
1099
- onEachFeature: (feature, layer) => feature.geometry && feature.geometry.type != 'Point' && this.addData(feature, layer),
1100
- });
1101
-
1102
- this.import(this.__D3).then(() => {
1103
- this._initMapIntegrations(layer);
1104
- this._fireEvt("eledata_loaded", { data: geojson, layer: layer, name: this.track_info.name, track_info: this.track_info });
1105
- });
1106
-
1107
- return layer;
1108
- },
1109
-
1110
- _onDragEnd({ dragstart, dragend}) {
1111
- this._hideMarker();
1112
- this.fitBounds(L.latLngBounds([dragstart.latlng, dragend.latlng]));
1113
-
1114
- this.fire("elechart_dragged");
1115
- },
1116
-
1117
- _onKeyDown({key}) {
1118
- if(!this.options.detached && key === "Escape"){
1119
- this._collapse();
1120
- } },
1121
-
1122
- /**
1123
- * Trigger mouseenter event.
1124
- */
1125
- _onMouseEnter() {
1126
- this.fire('elechart_enter');
1127
- },
1128
-
1129
- /*
1130
- * Handles the moueseover the chart and displays distance and altitude level.
1131
- */
1132
- _onMouseMove({xCoord}) {
1133
- if (this._chartEnabled && this._data.length) {
1134
- let item = this._findItemForX(xCoord);
1135
- if (item) {
1136
- if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord);
1137
-
1138
- this._updateMarker(item);
1139
- this._setMapView(item);
1140
-
1141
- if (this._map) {
1142
- addClass(this._map.getContainer(), 'elechart-hover');
1143
- }
1144
-
1145
- this.fire("elechart_change", { data: item, xCoord: xCoord });
1146
- this.fire("elechart_hover", { data: item, xCoord: xCoord });
1147
- }
1148
- }
1149
- },
1150
-
1151
- /*
1152
- * Handles mouseover events of the data layers on the map.
1153
- */
1154
- _onMouseMoveLayer({latlng}) {
1155
- if (this._data.length) {
1156
- let item = this._findItemForLatLng(latlng);
1157
- if (item) {
1158
- let xCoord = item.xDiagCoord;
1159
-
1160
- if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord);
1161
-
1162
- this._updateMarker(item);
1163
-
1164
- this.fire("elechart_change", { data: item, xCoord: xCoord });
1165
- }
1166
- }
1167
- },
1168
-
1169
- /*
1170
- * Handles the moueseout over the chart.
1171
- */
1172
- _onMouseOut() {
1173
- if (!this.options.detached) {
1174
- this._hideMarker();
1175
- this._chart._hideDiagramIndicator();
1176
- }
1177
-
1178
- if (this._map) {
1179
- removeClass(this._map.getContainer(), 'elechart-hover');
1180
- }
1181
-
1182
- this.fire("elechart_leave");
1183
- },
1184
-
1185
- /**
1186
- * Handles the drag event over the ruler filter.
1187
- */
1188
- _onRulerFilter({coords}) {
1189
- this._updateMapSegments(coords);
1190
- },
1191
-
1192
- /**
1193
- * Toggle chart data on legend click
1194
- */
1195
- _onToggleChart({ name, enabled }) {
1196
-
1197
- this._chartEnabled = this._chart._hasActiveLayers();
1198
-
1199
- // toggle layer visibility on empty chart
1200
- this._layers.eachLayer(layer => toggleClass(layer.getElement && layer.getElement(), this.options.polyline.className + ' ' + this.options.theme, this._chartEnabled));
1201
-
1202
- // toggle option value (eg. altitude = { 'disabled' || 'enabled' })
1203
- this.options[name] = !enabled && this.options[name] == 'disabled' ? 'enabled' : 'disabled';
1204
-
1205
- // remove marker on empty chart
1206
- if (!this._chartEnabled) {
1207
- this._chart._hideDiagramIndicator();
1208
- this._marker.remove();
1209
- }
1210
- },
1211
-
1212
- /**
1213
- * Simple GeoJSON Parser
1214
- */
1215
- _parseFromGeoJSONString(data) {
1216
- try {
1217
- return JSON.parse(data);
1218
- } catch (e) { }
1219
- },
1220
-
1221
- /**
1222
- * Attempt to parse raw response data (GeoJSON or XML > GeoJSON)
1223
- */
1224
- _parseFromString(data) {
1225
- return new Promise(resolve =>
1226
- this.import(this.__TOGEOJSON).then(() => {
1227
- let geojson;
1228
- try {
1229
- geojson = this._parseFromXMLString(data.trim());
1230
- } catch (e) {
1231
- geojson = this._parseFromGeoJSONString(data.toString());
1232
- }
1233
- if (geojson) {
1234
- geojson.name = geojson.name || (this._downloadURL || '').split('/').pop().split('#')[0].split('?')[0];
1235
- }
1236
- resolve(geojson);
1237
- })
1238
- );
1239
- },
1240
-
1241
- /**
1242
- * Simple XML Parser (GPX, KML, TCX)
1243
- */
1244
- _parseFromXMLString(data) {
1245
- if (data.indexOf("<") != 0) {
1246
- throw 'Invalid XML';
1247
- }
1248
- let xml = (new DOMParser()).parseFromString(data, "text/xml");
1249
- let type = xml.documentElement.tagName.toLowerCase(); // "kml" or "gpx"
1250
- let name = xml.getElementsByTagName('name');
1251
- if (xml.getElementsByTagName('parsererror').length) {
1252
- throw 'Invalid XML';
1253
- }
1254
- if (!(type in toGeoJSON)) {
1255
- type = xml.documentElement.tagName == "TrainingCenterDatabase" ? 'tcx' : 'gpx';
1256
- }
1257
- let geojson = toGeoJSON[type](xml);
1258
- geojson.name = name.length > 0 ? (Array.from(name).find(tag => tag.parentElement.tagName == "trk") ?? name[0]).textContent : '';
1259
- return geojson;
1260
- },
1261
-
1262
- /**
1263
- * Add chart profile to diagram
1264
- */
1265
- _registerAreaPath(props) {
1266
- this.on("elechart_init", () => this._chart._registerAreaPath(props));
1267
- },
1268
-
1269
- /**
1270
- * Add chart grid to diagram
1271
- */
1272
- _registerAxisGrid(props) {
1273
- this.on("elechart_axis", () => this._chart._registerAxisGrid(props));
1274
- },
1275
-
1276
- /**
1277
- * Add chart axis to diagram
1278
- */
1279
- _registerAxisScale(props) {
1280
- this.on("elechart_axis", () => this._chart._registerAxisScale(props));
1281
- },
1282
-
1283
- /**
1284
- * Add a point of interest over the diagram
1285
- */
1286
- _registerCheckPoint(props) {
1287
- const cb = () => this._chart._registerCheckPoint(props);
1288
- this
1289
- .on("elechart_updated", cb)
1290
- .once("eledata_clear", () => this.off("elechart_updated", cb));
1291
- },
1292
-
1293
- /**
1294
- * Base handler for iterative track statistics (dist, time, z, slope, speed, acceleration, ...)
1295
- */
1296
- _registerDataAttribute(props) {
1297
-
1298
- // parse of "coordinateProperties" for later usage
1299
- if (props.coordPropsToMeta) {
1300
- this.on("elepoint_init", (e) => props.coordPropsToMeta.call(this, e));
1301
- }
1302
-
1303
- // prevent excessive variabile instanstations
1304
- let i, curr, prev, attr = props.attr || props.name;
1305
-
1306
- // save here a reference to last used point
1307
- let lastValid = {};
1308
-
1309
- // iteration
1310
- this.on("elepoint_added", ({index, point}) => {
1311
- i = index;
1312
-
1313
- prev = curr ?? this._data[i]; // same as: this._data[i > 0 ? i - 1 : i]
1314
- curr = this._data[i];
1315
-
1316
- // retrieve point value
1317
- curr[attr] = props.pointToAttr.call(this, point, i);
1318
-
1319
- // check and fix missing data on last added point
1320
- if (i > 0 && isNaN(prev[attr])) {
1321
- if (!isNaN(lastValid[attr]) && !isNaN(curr[attr])) {
1322
- prev[attr] = (lastValid[attr] + curr[attr]) / 2;
1323
- } else if (!isNaN(lastValid[attr])) {
1324
- prev[attr] = lastValid[attr];
1325
- } else if (!isNaN(curr[attr])) {
1326
- prev[attr] = curr[attr];
1327
- }
1328
- // update "yAttr" and "xAttr"
1329
- if (props.meta) {
1330
- prev[props.meta] = prev[attr];
1331
- }
1332
- }
1333
-
1334
- // skip to next iteration for invalid or missing data (eg. i == 0)
1335
- if (isNaN(curr[attr])) {
1336
- return;
1337
- }
1338
-
1339
- // update reference to last used point
1340
- lastValid[attr] = curr[attr];
1341
-
1342
- // Limit "crazy" delta values.
1343
- if (props.deltaMax) {
1344
- curr[attr] =wrapDelta(curr[attr], prev[attr], props.deltaMax);
1345
- }
1346
-
1347
- // Range of acceptable values.
1348
- if (props.clampRange) {
1349
- curr[attr] = clamp(curr[attr], props.clampRange);
1350
- }
1351
-
1352
- // Limit floating point precision.
1353
- if (!isNaN(props.decimals)) {
1354
- curr[attr] = round(curr[attr], props.decimals);
1355
- }
1356
-
1357
- // update "track_info" stats (min, max, avg, ...)
1358
- if (props.stats) {
1359
- for (const key in props.stats) {
1360
- let sname = (props.statsName || attr) + (key != '' ? '_' : '');
1361
- this.track_info[sname + key] = props.stats[key].call(this, curr[attr], this.track_info[sname + key], this._data.length);
1362
- }
1363
- }
1364
-
1365
- // update here some mixins (eg. complex "track_info" stuff)
1366
- if (props.onPointAdded) props.onPointAdded.call(this, curr[attr], i, point);
1367
- });
1368
- },
1369
-
1370
- /**
1371
- * Parse a module definition and attach related function listeners
1372
- */
1373
- _registerHandler(props) {
1374
-
1375
- // eg. L.Control.Altitude
1376
- if (typeof props === "function") {
1377
- return this._registerHandler(props.call(this));
1378
- }
1379
-
1380
- let {
1381
- name,
1382
- attr,
1383
- required,
1384
- deltaMax,
1385
- clampRange,
1386
- decimals,
1387
- meta,
1388
- unit,
1389
- coordinateProperties,
1390
- coordPropsToMeta: coordPropsToMeta$1,
1391
- pointToAttr,
1392
- onPointAdded,
1393
- stats,
1394
- statsName,
1395
- grid,
1396
- scale,
1397
- path,
1398
- tooltip,
1399
- summary
1400
- } = props;
1401
-
1402
- // eg. "altitude" == true
1403
- if (this.options[name] || required) {
1404
-
1405
- this._registerDataAttribute({
1406
- name,
1407
- attr,
1408
- meta,
1409
- deltaMax,
1410
- clampRange,
1411
- decimals,
1412
- coordPropsToMeta: coordPropsToMeta(coordinateProperties, meta || name, coordPropsToMeta$1),
1413
- pointToAttr,
1414
- onPointAdded,
1415
- stats,
1416
- statsName,
1417
- });
1418
-
1419
- if (grid) {
1420
- this._registerAxisGrid(L.extend({ name }, grid));
1421
- }
1422
-
1423
- if (this.options[name] !== "summary") {
1424
- if (scale) this._registerAxisScale(L.extend({ name, label: unit }, scale));
1425
- if (path) this._registerAreaPath(L.extend({ name }, path));
1426
- }
1427
-
1428
- if (tooltip || props.tooltips) {
1429
- each([tooltip, ...(props.tooltips || [])], t => t && this._registerTooltip(L.extend({ name }, t)));
1430
- }
1431
-
1432
- if (summary) {
1433
- each(summary, (s, k) => summary[k] = L.extend({ unit }, s));
1434
- this._registerSummary(summary);
1435
- }
1436
- }
1437
- },
1438
-
1439
- _registerMarker({latlng, sym, content}) {
1440
- let { wptIcons } = this.options;
1441
- // generate and cache appropriate icon symbol
1442
- if (!wptIcons.hasOwnProperty(sym)) {
1443
- wptIcons[sym] = L.divIcon(L.extend({}, wptIcons[""].options, { html: '<i class="elevation-waypoint-icon ' + sym + '"></i>' } ));
1444
- }
1445
- let marker = L.marker(latlng, { icon: wptIcons[sym] });
1446
- if (content) {
1447
- marker.bindPopup(content, { className: 'elevation-popup', keepInView: true }).openPopup();
1448
- marker.bindTooltip(content, { className: 'elevation-tooltip', direction: 'auto', sticky: true, opacity: 1 }).openTooltip();
1449
- }
1450
- return this._addMarker(marker)
1451
- },
1452
-
1453
- /**
1454
- * Add chart or marker tooltip info
1455
- */
1456
- _registerTooltip(props) {
1457
- props.chart && this.on("elechart_init", () => this._chart._registerTooltip(L.extend({}, props, { value: props.chart })));
1458
- props.marker && this.on("elechart_marker", () => this._marker._registerTooltip(L.extend({}, props, { value: props.marker })));
1459
- },
1460
-
1461
- /**
1462
- * Add summary info to diagram
1463
- */
1464
- _registerSummary(props) {
1465
- this.on('elechart_summary', () => this._summary._registerSummary(props));
1466
- },
1467
-
1468
- /*
1469
- * Removes the drag rectangle and zoms back to the total extent of the data.
1470
- */
1471
- _resetDrag() {
1472
- this._chart._resetDrag();
1473
- this._hideMarker();
1474
- },
1475
-
1476
- /**
1477
- * Resets drag, marker and bounds.
1478
- */
1479
- _resetView() {
1480
- if (this._map && this._map._isFullscreen) return;
1481
- this._resetDrag();
1482
- this._hideMarker();
1483
- if (this.options.autofitBounds) {
1484
- this.fitBounds();
1485
- }
1486
- },
1487
-
1488
- /**
1489
- * Hacky way for handling chart resize. Deletes it and redraw chart.
1490
- */
1491
- _resizeChart() {
1492
- if (this._container && style(this._container, "display") != "none") {
1493
- let opts = this.options;
1494
- let newWidth = opts.detached ? (this.eleDiv || this._container).offsetWidth : clamp(opts._maxWidth, [0, this._map.getContainer().clientWidth - 30]);
1495
- if (newWidth) {
1496
- opts.width = newWidth;
1497
- if (this._chart && this._chart._chart) {
1498
- this._chart._chart._resize(opts);
1499
- this._updateChart();
1500
- }
1501
- }
1502
- this._updateMapSegments();
1503
- }
1504
- },
1505
-
1506
- /**
1507
- * Collapse or Expand chart control.
1508
- */
1509
- _toggle() {
1510
- hasClass(this._container, "elevation-expanded") ? this._collapse() : this._expand();
1511
- },
1512
-
1513
- /**
1514
- * Update map center and zoom (followMarker: true)
1515
- */
1516
- _setMapView(item) {
1517
- if (this._map && this.options.followMarker) {
1518
- let zoom = this._map.getZoom();
1519
- let z = this.options.zFollow;
1520
- if (typeof z === "number") {
1521
- this._map.setView(item.latlng, (zoom < z ? z : zoom), { animate: true, duration: 0.25 });
1522
- } else if (!this._map.getBounds().contains(item.latlng)) {
1523
- this._map.setView(item.latlng, zoom, { animate: true, duration: 0.25 });
1524
- }
1525
- }
1526
- },
1527
-
1528
- /**
1529
- * Calculates [x, y] domain and then update chart.
1530
- */
1531
- _updateChart() {
1532
- if (this._chart && this._container) {
1533
- this.fire("elechart_axis");
1534
-
1535
- this._chart.update({ data: this._data, options: this.options });
1536
-
1537
- this._x = this._chart._x;
1538
- this._y = this._chart._y;
1539
-
1540
- this.fire('elechart_updated');
1541
- }
1542
- },
1543
-
1544
- /*
1545
- * Update the position/height indicator marker drawn onto the map
1546
- */
1547
- _updateMarker(item) {
1548
- if (this._marker) {
1549
- this._marker.update({
1550
- map : this._map,
1551
- item : item,
1552
- yCoordMax : this._yCoordMax || 0,
1553
- options : this.options
1554
- });
1555
- }
1556
- },
1557
-
1558
- /**
1559
- * Fix marker rotation on rotated maps
1560
- */
1561
- _rotateMarker() {
1562
- if (this._marker) {
1563
- this._marker.update();
1564
- }
1565
- },
1566
-
1567
- /**
1568
- * Highlight track segments on the map.
1569
- */
1570
- _updateMapSegments(coords) {
1571
- this._markedSegments.setLatLngs(coords || []);
1572
- if (coords && this._map && !this._map.hasLayer(this._markedSegments)) {
1573
- this._markedSegments.addTo(this._map);
1574
- }
1575
- },
1576
-
1577
- /**
1578
- * Update chart summary.
1579
- */
1580
- _updateSummary() {
1581
- if (this._summary) {
1582
- this._summary.reset();
1583
- if (this.options.summary) {
1584
- this.fire("elechart_summary");
1585
- this._summary.update();
1586
- }
1587
- if (this.options.downloadLink && this._downloadURL) { // TODO: generate dynamically file content instead of using static file urls.
1588
- this._summary._container.innerHTML += '<span class="download"><a href="#">' + L._('Download') + '</a></span>';
1589
- select('.download a', this._summary._container).onclick = (e) => {
1590
- e.preventDefault();
1591
- let event = { downloadLink: this.options.downloadLink, confirm: saveFile.bind(this, this._downloadURL) };
1592
- if (this.options.downloadLink == 'modal' && typeof CustomEvent === "function") {
1593
- document.dispatchEvent(new CustomEvent("eletrack_download", { detail: event }));
1594
- } else if (this.options.downloadLink == 'link' || this.options.downloadLink === true) {
1595
- event.confirm();
1596
- }
1597
- this.fire('eletrack_download', event);
1598
- };
1599
- }
1600
- }
1601
- },
1602
-
1603
-
1604
- /**
1605
- * Calculates chart width.
1606
- */
1607
- _width() {
1608
- if (this._chart) return this._chart._width();
1609
- const { width, margins } = this.options;
1610
- return width - margins.left - margins.right;
1611
- },
1612
-
1613
- /**
1614
- * Calculates chart height.
1615
- */
1616
- _height() {
1617
- if (this._chart) return this._chart._height();
1618
- const { height, margins } = this.options;
1619
- return height - margins.top - margins.bottom;
1620
- },
1621
-
336
+ // "leaflet-i18n" fallback
337
+ if (!L._ || !L.i18n) {
338
+ L._ = L.i18n = (string, data) => string;
339
+ }
340
+
341
+ const Elevation = L.Control.Elevation = L.Control.extend({
342
+
343
+ includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
344
+
345
+ options: Options,
346
+ __mileFactor: 0.621371, // 1 km = (0.621371 mi)
347
+ __footFactor: 3.28084, // 1 m = (3.28084 ft)
348
+ __D3: 'https://unpkg.com/d3@7.8.4/dist/d3.min.js',
349
+ __TOGEOJSON: 'https://unpkg.com/@tmcw/togeojson@5.6.2/dist/togeojson.umd.js',
350
+ __LGEOMUTIL: 'https://unpkg.com/leaflet-geometryutil@0.10.1/src/leaflet.geometryutil.js',
351
+ __LALMOSTOVER: 'https://unpkg.com/leaflet-almostover@1.0.1/src/leaflet.almostover.js',
352
+ __LHOTLINE: '../libs/leaflet-hotline.min.js',
353
+ __LDISTANCEM: '../libs/leaflet-distance-marker.min.js',
354
+ __LEDGESCALE: '../libs/leaflet-edgescale.min.js',
355
+ __LCHART: '../src/components/chart.js',
356
+ __LMARKER: '../src/components/marker.js',
357
+ __LSUMMARY: '../src/components/summary.js',
358
+ __modulesFolder: '../src/handlers/',
359
+ __btnIcon: '../images/elevation.svg',
360
+
361
+ /*
362
+ * Add data to the diagram either from GPX or GeoJSON and update the axis domain and data
363
+ */
364
+ addData(d, layer) {
365
+ this.import(this.__D3)
366
+ .then(() => {
367
+ if (this._modulesLoaded) {
368
+ layer = layer ?? (d.on && d);
369
+ this._addData(d);
370
+ this._addLayer(layer);
371
+ this._fireEvt("eledata_added", { data: d, layer: layer, track_info: this.track_info });
372
+ } else {
373
+ this.once('modules_loaded', () => this.addData(d,layer));
374
+ }
375
+ });
376
+ },
377
+
378
+ /**
379
+ * Adds the control to the given map.
380
+ */
381
+ addTo(map) {
382
+ if (this.options.detached) {
383
+ let parent = select(this.options.elevationDiv);
384
+ let eleDiv = this.onAdd(map);
385
+ parent ? append(parent, eleDiv) : insert(map.getContainer(), eleDiv, 'afterend');
386
+ } else {
387
+ L.Control.prototype.addTo.call(this, map);
388
+ }
389
+ return this;
390
+ },
391
+
392
+ /*
393
+ * Reset data and display
394
+ */
395
+ clear() {
396
+ if (this._marker) this._marker.remove();
397
+ if (this._chart) this._clearChart();
398
+ if (this._layers) this._clearLayers(this._layers);
399
+ if (this._markers) this._clearLayers(this._markers);
400
+ if (this._circleMarkers) this._circleMarkers.remove();
401
+ if (this._hotline) this._hotline.eachLayer(l => l.options.renderer.remove()); // hotfix for: https://github.com/Raruto/leaflet-elevation/issues/233
402
+ if (this._hotline) this._clearLayers(this._hotline);
403
+
404
+ this._data = [];
405
+ this.track_info = {};
406
+
407
+ this._fireEvt("eledata_clear");
408
+
409
+ this._updateChart();
410
+ },
411
+
412
+ _clearChart() {
413
+ if (this._events && this._events.elechart_updated) {
414
+ this._events.elechart_updated.forEach(({fn, ctx}) => this.off('elechart_updated', fn, ctx));
415
+ }
416
+ if (this._chart && this._chart._container) {
417
+ this._chart._container.selectAll('g.point .point').remove();
418
+ this._chart.clear();
419
+ }
420
+ },
421
+
422
+ _clearLayers(l) {
423
+ l = l || this._layers;
424
+ if (l && l.eachLayer) {
425
+ l.eachLayer(f => f.remove());
426
+ l.clearLayers();
427
+ }
428
+ },
429
+
430
+ /**
431
+ * TODO: Create a base class to handle custom data attributes (heart rate, cadence, temperature, ...)
432
+ *
433
+ * @link https://leafletjs.com/examples/extending/extending-3-controls.html#handlers
434
+ */
435
+ // addHandler: function (name, HandlerClass) {
436
+ // if (HandlerClass) {
437
+ // let handler = this[name] = new HandlerClass(this);
438
+ // this.handlers.push(handler);
439
+ // if (this.options[name]) {
440
+ // handler.enable();
441
+ // }
442
+ // }
443
+ // return this;
444
+ // },
445
+
446
+ /**
447
+ * Disable chart brushing.
448
+ */
449
+ disableBrush() {
450
+ this._chart._brushEnabled = false;
451
+ this._resetDrag();
452
+ },
453
+
454
+ /**
455
+ * Enable chart brushing.
456
+ */
457
+ enableBrush() {
458
+ this._chart._brushEnabled = true;
459
+ },
460
+
461
+ /**
462
+ * Disable chart zooming.
463
+ */
464
+ disableZoom() {
465
+ this._chart._zoomEnabled = false;
466
+ this._chart._resetZoom();
467
+ },
468
+
469
+ /**
470
+ * Enable chart zooming.
471
+ */
472
+ enableZoom() {
473
+ this._chart._zoomEnabled = true;
474
+ },
475
+
476
+ /**
477
+ * Sets a map view that contains the given geographical bounds.
478
+ */
479
+ fitBounds(bounds) {
480
+ bounds = bounds || this.getBounds();
481
+ if (this._map && bounds.isValid()) this._map.fitBounds(bounds);
482
+ },
483
+
484
+ getBounds(data) {
485
+ return L.latLngBounds((data || this._data).map((d) => d.latlng));
486
+ },
487
+
488
+ /**
489
+ * Get default zoom level (followMarker: true).
490
+ */
491
+ getZFollow() {
492
+ return this.options.zFollow;
493
+ },
494
+
495
+ /**
496
+ * Hide current elevation chart profile.
497
+ */
498
+ hide() {
499
+ style(this._container, "display", "none");
500
+ },
501
+
502
+ /**
503
+ * Initialize chart control "options" and "container".
504
+ */
505
+ initialize(opts) {
506
+
507
+ // opts = L.setOptions(this, opts);
508
+
509
+ // Fixes: https://github.com/Raruto/leaflet-elevation/pull/240
510
+ opts = L.setOptions(this, L.extend({}, cloneDeep(Options), opts)); // "deep copy" nested objects (multiple charts)
511
+
512
+ this._data = [];
513
+ this._layers = L.featureGroup();
514
+ this._markers = L.featureGroup();
515
+ this._hotline = L.featureGroup();
516
+ this._circleMarkers = L.featureGroup();
517
+ this._markedSegments = L.polyline([]);
518
+ this._start = L.circleMarker([0,0], (opts.trkStart || Options.trkStart));
519
+ this._end = L.circleMarker([0,0], (opts.trkEnd || Options.trkEnd));
520
+ this._chartEnabled = true;
521
+ this._yCoordMax = -Infinity;
522
+ this.track_info = {};
523
+ // this.handlers = [];
524
+
525
+ if (opts.followMarker) this._setMapView = throttle(this._setMapView, 300, this);
526
+ if (opts.legend) opts.margins.bottom += 30;
527
+ if (opts.theme) opts.polylineSegments.className += ' ' + opts.theme;
528
+ if (opts.wptIcons === true) opts.wptIcons = Options.wptIcons;
529
+ if (opts.distanceMarkers === true) opts.distanceMarkers = Options.distanceMarkers;
530
+ if (opts.trkStart) this._start.addTo(this._circleMarkers);
531
+ if (opts.trkEnd) this._end.addTo(this._circleMarkers);
532
+
533
+
534
+ this._markedSegments.setStyle(opts.polylineSegments);
535
+
536
+ // Leaflet canvas renderer colors
537
+ L.extend(Colors, opts.colors || {});
538
+
539
+ // Various stuff
540
+ this._fixCanvasPaths();
541
+ this._fixTooltipSize();
542
+
543
+ },
544
+
545
+ /**
546
+ * Javascript scripts downloader (lazy loader)
547
+ */
548
+ import(src, condition) {
549
+ if (Array.isArray(src)) {
550
+ return Promise.all(src.map(m => this.import(m)));
551
+ }
552
+ switch(src) {
553
+ case this.__D3: condition = typeof d3 !== 'object'; break;
554
+ case this.__TOGEOJSON: condition = typeof toGeoJSON !== 'object'; break;
555
+ case this.__LGEOMUTIL: condition = typeof L.GeometryUtil !== 'object'; break;
556
+ case this.__LALMOSTOVER: condition = typeof L.Handler.AlmostOver !== 'function'; break;
557
+ case this.__LDISTANCEM: condition = typeof L.DistanceMarkers !== 'function'; break;
558
+ case this.__LEDGESCALE: condition = typeof L.Control.EdgeScale !== 'function'; break;
559
+ case this.__LHOTLINE: condition = typeof L.Hotline !== 'function'; break;
560
+ }
561
+ return condition !== false ? import(resolveURL(src, this.options.srcFolder)) : Promise.resolve();
562
+ },
563
+
564
+ /**
565
+ * Load elevation data (GPX, GeoJSON, KML or TCX).
566
+ */
567
+ load(data) {
568
+ this._parseFromString(data).then( geojson => geojson ? this._loadLayer(geojson) : this._loadFile(data));
569
+ },
570
+
571
+ /**
572
+ * Create container DOM element and related event listeners.
573
+ * Called on control.addTo(map).
574
+ */
575
+ onAdd(map) {
576
+ this._map = map;
577
+
578
+ let container = this._container = create("div", "elevation-control " + this.options.theme + " " + (this.options.detached ? 'elevation-detached' : 'leaflet-control'), this.options.detached ? { id: 'elevation-' + randomId() } : {});
579
+
580
+ if (!this.eleDiv) this.eleDiv = container;
581
+
582
+ this._loadModules(this.options.handlers).then(() => { // Inject here required modules (data handlers)
583
+ this._initChart(container);
584
+ this._initButton(container);
585
+ this._initSummary(container);
586
+ this._initMarker(map);
587
+ this._initLayer(map);
588
+ this._modulesLoaded = true;
589
+ this.fire('modules_loaded');
590
+ });
591
+
592
+ this.fire('add');
593
+
594
+ return container;
595
+ },
596
+
597
+ /**
598
+ * Clean up control code and related event listeners.
599
+ * Called on control.remove().
600
+ */
601
+ onRemove(map) {
602
+ this._container = null;
603
+
604
+ map
605
+ .off('zoom viewreset zoomanim', this._hideMarker, this)
606
+ .off('resize', this._resetView, this)
607
+ .off('resize', this._resizeChart, this)
608
+ .off('mousedown', this._resetDrag, this);
609
+
610
+ off(map.getContainer(), 'mousewheel', this._resetDrag, this);
611
+ off(map.getContainer(), 'touchstart', this._resetDrag, this);
612
+ off(document, 'keydown', this._onKeyDown, this);
613
+
614
+ this
615
+ .off('eledata_added eledata_loaded', this._updateChart, this)
616
+ .off('eledata_added eledata_loaded', this._updateSummary, this);
617
+
618
+ this.fire('remove');
619
+ },
620
+
621
+ /**
622
+ * Redraws the chart control. Sometimes useful after screen resize.
623
+ */
624
+ redraw() {
625
+ this._resizeChart();
626
+ },
627
+
628
+ /**
629
+ * Set default zoom level (followMarker: true).
630
+ */
631
+ setZFollow(zoom) {
632
+ this.options.zFollow = zoom;
633
+ },
634
+
635
+ /**
636
+ * Hide current elevation chart profile.
637
+ */
638
+ show() {
639
+ style(this._container, "display", "block");
640
+ },
641
+
642
+ /*
643
+ * Parsing data either from GPX or GeoJSON and update the diagram data
644
+ */
645
+ _addData(d) {
646
+ if (!d) {
647
+ return;
648
+ }
649
+
650
+ // Standard GeoJSON
651
+ if (d.type === "FeatureCollection" ) {
652
+ return each(d.features, feature => this._addData(feature));
653
+ } else if (d.type === "Feature") {
654
+ let geom = d.geometry;
655
+ if (geom) {
656
+ switch (geom.type) {
657
+ case 'LineString': return this._addGeoJSONData(geom.coordinates, d.properties);
658
+ case 'MultiLineString': return each(geom.coordinates, (coords, i) => this._addGeoJSONData(coords, d.properties, i));
659
+ case 'Point':
660
+ default: return console.warn('Unsopperted GeoJSON feature geometry type:' + geom.type);
661
+ }
662
+ }
663
+ }
664
+
665
+ // Fallback for leaflet layers (eg. L.Gpx)
666
+ if (d._latlngs) {
667
+ return this._addGeoJSONData(d._latlngs, d.feature && d.feature.properties);
668
+ }
669
+
670
+ },
671
+
672
+ /*
673
+ * Parsing of GeoJSON data lines and their elevation in z-coordinate
674
+ */
675
+ _addGeoJSONData(coords, properties, nestingLevel) {
676
+
677
+ // "coordinateProperties" property is generated inside "@tmcw/toGeoJSON"
678
+ let props = (properties && properties.coordinateProperties) || properties;
679
+
680
+ coords.forEach((point, i) => {
681
+
682
+ // GARMIN_EXTENSIONS = ["hr", "cad", "atemp", "wtemp", "depth", "course", "bearing"];
683
+ point.meta = point.meta ?? { time: null, ele: null };
684
+
685
+ point.prev = (attr) => (attr ? this._data[i > 0 ? i - 1 : 0][attr] : this._data[i > 0 ? i - 1 : 0]);
686
+
687
+ this.fire("elepoint_init", { point: point, props: props, id: i, isMulti: nestingLevel });
688
+
689
+ this._addPoint(
690
+ point.lat ?? point[1],
691
+ point.lng ?? point[0],
692
+ point.alt ?? point.meta.ele ?? point[2]
693
+ );
694
+
695
+ this.fire("elepoint_added", { point: point, index: this._data.length - 1 });
696
+
697
+ if (this._yCoordMax < this._data[this._data.length - 1][this.options.yAttr]) this._yCoordMax = this._data[this._data.length - 1][this.options.yAttr];
698
+ });
699
+
700
+ this.fire("eletrack_added", { coords: coords, index: this._data.length - 1 });
701
+ },
702
+
703
+ /*
704
+ * Parse and push a single (x, y, z) point to current elevation profile.
705
+ */
706
+ _addPoint(x, y, z) {
707
+ if (this.options.reverseCoords) {
708
+ [x, y] = [y, x];
709
+ }
710
+
711
+ this._data.push({
712
+ x: x,
713
+ y: y,
714
+ z: z,
715
+ latlng: L.latLng(x, y, z)
716
+ });
717
+
718
+ this.fire("eledata_updated", { index: this._data.length - 1 });
719
+ },
720
+
721
+ _addLayer(layer) {
722
+ if (layer) this._layers.addLayer(layer);
723
+ // Postpone adding the distance markers (lazy: true)
724
+ if (layer && this.options.distanceMarkers && this.options.distanceMarkers.lazy) {
725
+ layer.on('add remove', ({target, type}) => L.DistanceMarkers && target instanceof L.Polyline && target[type + 'DistanceMarkers']());
726
+ }
727
+ return layer;
728
+ },
729
+
730
+ _addMarker(marker) {
731
+ if (marker) this._markers.addLayer(marker);
732
+ return marker;
733
+ },
734
+
735
+ /**
736
+ * Initialize "L.AlmostOver" integration
737
+ */
738
+ _initAlmostOverHandler(map, layer) {
739
+ return (map && this.options.almostOver && !L.Browser.mobile) ? this.import([this.__LGEOMUTIL, this.__LALMOSTOVER])
740
+ .then(() => {
741
+ map.addHandler('almostOver', L.Handler.AlmostOver);
742
+ if (L.GeometryUtil && map.almostOver && map.almostOver.enabled()) {
743
+ map.almostOver.addLayer(layer);
744
+ map
745
+ .on('almost:move', this._onMouseMoveLayer, this)
746
+ .on('almost:out', this._onMouseOut, this);
747
+ this.once('eledata_clear', () => {
748
+ map.almostOver.removeLayer(layer);
749
+ map
750
+ .off('almost:move', this._onMouseMoveLayer, this)
751
+ .off('almost:out', this._onMouseOut, this);
752
+ });
753
+ }
754
+ }) : Promise.resolve();
755
+ },
756
+
757
+ /**
758
+ * Initialize "L.DistanceMarkers" integration
759
+ */
760
+ _initDistanceMarkers() {
761
+ return this.options.distanceMarkers ? this.import([this.__LGEOMUTIL, this.__LDISTANCEM]) : Promise.resolve();
762
+ },
763
+
764
+ /**
765
+ * Initialize "L.Control.EdgeScale" integration
766
+ */
767
+ _initEdgeScale(map) {
768
+ return this.options.edgeScale ? this.import(this.__LEDGESCALE)
769
+ .then(() => {
770
+ map.edgeScaleControl = map.edgeScaleControl || L.control.edgeScale('boolean' !== typeof this.options.edgeScale ? this.options.edgeScale : {}).addTo(map);
771
+ }) : Promise.resolve();
772
+ },
773
+
774
+ _initHotLine(layer) {
775
+ let prop = typeof this.options.hotline == 'string' ? this.options.hotline : 'elevation';
776
+ return this.options.hotline ? this.import(this.__LHOTLINE)
777
+ .then(() => {
778
+ layer.eachLayer((trkseg) => {
779
+ if (trkseg.feature.geometry.type != "Point") {
780
+ let geo = L.geoJson(trkseg.toGeoJSON(), { coordsToLatLng: (coords) => L.latLng(coords[0], coords[1], coords[2] * (this.options.altitudeFactor || 1))});
781
+ let line = L.hotline(geo.toGeoJSON().features[0].geometry.coordinates, {
782
+ renderer: L.Hotline.renderer(),
783
+ min: isFinite(this.track_info[prop + '_min']) ? this.track_info[prop + '_min'] : 0,
784
+ max: isFinite(this.track_info[prop + '_max']) ? this.track_info[prop + '_max'] : 1,
785
+ palette: {
786
+ 0.0: '#008800',
787
+ 0.5: '#ffff00',
788
+ 1.0: '#ff0000'
789
+ },
790
+ weight: 5,
791
+ outlineColor: '#000000',
792
+ outlineWidth: 1
793
+ }).addTo(this._hotline);
794
+ let alpha = trkseg.options.style && trkseg.options.style.opacity || 1;
795
+ trkseg.on('add remove', ({type}) => {
796
+ trkseg.setStyle({opacity: (type == 'add' ? 0 : alpha)});
797
+ line[(type == 'add' ? 'addTo' : 'removeFrom')](trkseg._map);
798
+ if (line._renderer) line._renderer._container.parentElement.insertBefore(line._renderer._container, line._renderer._container.parentElement.firstChild);
799
+ });
800
+ }
801
+ });
802
+ }) : Promise.resolve();
803
+ },
804
+
805
+ /**
806
+ * Initialize "L.AlmostOver" and "L.DistanceMarkers"
807
+ */
808
+ _initMapIntegrations(layer) {
809
+ let map = this._map;
810
+ if (map) {
811
+ if (this._data.length) {
812
+ this._start.setLatLng(this._data[0].latlng);
813
+ this._end.setLatLng(this._data[this._data.length -1].latlng);
814
+ }
815
+ Promise.all([
816
+ this._initHotLine(layer),
817
+ this._initAlmostOverHandler(map, layer),
818
+ this._initDistanceMarkers(),
819
+ this._initEdgeScale(map),
820
+ ]).then(() => {
821
+ if (this.options.polyline) {
822
+ this._layers.addLayer(layer.addTo(map)); // hotfix for: https://github.com/Raruto/leaflet-elevation/issues/233
823
+ this._circleMarkers.addTo(map);
824
+ }
825
+ if (this.options.autofitBounds) {
826
+ this.fitBounds(layer.getBounds());
827
+ }
828
+ map.invalidateSize();
829
+ });
830
+ } else {
831
+ this.once('add', () => this._initMapIntegrations(layer));
832
+ }
833
+ },
834
+
835
+ /*
836
+ * Collapse current chart control.
837
+ */
838
+ _collapse() {
839
+ replaceClass(this._container, 'elevation-expanded', 'elevation-collapsed');
840
+ if (this._map) this._map.invalidateSize();
841
+ },
842
+
843
+ /*
844
+ * Expand current chart control.
845
+ */
846
+ _expand() {
847
+ replaceClass(this._container, 'elevation-collapsed', 'elevation-expanded');
848
+ if (this._map) this._map.invalidateSize();
849
+ },
850
+
851
+ /**
852
+ * Add some basic colors to leaflet canvas renderer (preferCanvas: true).
853
+ */
854
+ _fixCanvasPaths() {
855
+ let oldProto = L.Canvas.prototype._fillStroke;
856
+ let control = this;
857
+
858
+ let theme = this.options.theme.split(' ')[0].replace('-theme', '');
859
+ let color = Colors[theme] || {};
860
+
861
+ L.Canvas.include({
862
+ _fillStroke(ctx, layer) {
863
+ if (control._layers.hasLayer(layer)) {
864
+
865
+ let options = layer.options;
866
+
867
+ options.color = color.line || color.area || theme;
868
+ options.stroke = !!options.color;
869
+
870
+ oldProto.call(this, ctx, layer);
871
+
872
+ if (options.stroke && options.weight !== 0) {
873
+ let oldVal = ctx.globalCompositeOperation || 'source-over';
874
+ ctx.globalCompositeOperation = 'destination-over';
875
+ ctx.strokeStyle = color.outline || '#FFF';
876
+ ctx.lineWidth = options.weight * 1.75;
877
+ ctx.stroke();
878
+ ctx.globalCompositeOperation = oldVal;
879
+ }
880
+
881
+ } else {
882
+ oldProto.call(this, ctx, layer);
883
+ }
884
+ }
885
+ });
886
+ },
887
+
888
+ /**
889
+ * Partial fix for initial tooltip size
890
+ *
891
+ * @link https://github.com/Raruto/leaflet-elevation/issues/81#issuecomment-713477050
892
+ */
893
+ _fixTooltipSize() {
894
+ this.on('elechart_init', () =>
895
+ this.once('elechart_change elechart_hover', ({data, xCoord}) => {
896
+ if (this._chartEnabled) {
897
+ this._chart._showDiagramIndicator(data, xCoord);
898
+ this._chart._showDiagramIndicator(data, xCoord);
899
+ }
900
+ this._updateMarker(data);
901
+ })
902
+ );
903
+ },
904
+
905
+ /*
906
+ * Finds a data entry for the given LatLng
907
+ */
908
+ _findItemForLatLng(latlng) {
909
+ return this._data[this._chart._findIndexForLatLng(latlng)];
910
+ },
911
+
912
+ /*
913
+ * Finds a data entry for the given xDiagCoord
914
+ */
915
+ _findItemForX(x) {
916
+ return this._data[this._chart._findIndexForXCoord(x)];
917
+ },
918
+
919
+ /**
920
+ * Fires an event of the specified type.
921
+ */
922
+ _fireEvt(type, data, propagate) {
923
+ if (this.fire) this.fire(type, data, propagate);
924
+ if (this._map) this._map.fire(type, data, propagate);
925
+ },
926
+
927
+ /*
928
+ * Hides the position/height indicator marker drawn onto the map
929
+ */
930
+ _hideMarker() {
931
+ if (this.options.autohideMarker) {
932
+ this._marker.remove();
933
+ }
934
+ },
935
+
936
+ /**
937
+ * Generate "svg" chart (DOM element).
938
+ */
939
+ _initChart(container) {
940
+ let opts = this.options;
941
+ let map = this._map;
942
+
943
+ if (opts.detached) {
944
+ let { offsetWidth, offsetHeight} = this.eleDiv;
945
+ if (offsetWidth > 0) opts.width = offsetWidth;
946
+ if (offsetHeight > 20) opts.height = offsetHeight - 20; // 20 = horizontal scrollbar size.
947
+ } else {
948
+ let { clientWidth } = map.getContainer();
949
+ opts._maxWidth = opts._maxWidth > opts.width ? opts._maxWidth : opts.width;
950
+ this._container.style.maxWidth = opts._maxWidth + 'px';
951
+ if (opts._maxWidth > clientWidth) opts.width = clientWidth - 30;
952
+ }
953
+
954
+ this
955
+ .import([this.__D3, this.__LCHART])
956
+ .then((m) => {
957
+
958
+ let chart = this._chart = new (m[1] || Elevation).Chart(opts, this);
959
+
960
+ this._x = this._chart._x;
961
+ this._y = this._chart._y;
962
+
963
+ d3
964
+ .select(container)
965
+ .call(chart.render());
966
+
967
+ chart
968
+ .on('reset_drag', this._hideMarker, this)
969
+ .on('mouse_enter', this._onMouseEnter, this)
970
+ .on('dragged', this._onDragEnd, this)
971
+ .on('mouse_move', this._onMouseMove, this)
972
+ .on('mouse_out', this._onMouseOut, this)
973
+ .on('ruler_filter', this._onRulerFilter, this)
974
+ .on('zoom', this._updateChart, this)
975
+ .on('elepath_toggle', this._onToggleChart, this)
976
+ .on('margins_updated', this._resizeChart, this);
977
+
978
+
979
+ this.fire("elechart_init");
980
+
981
+ map
982
+ .on('zoom viewreset zoomanim', this._hideMarker, this)
983
+ .on('resize', this._resetView, this)
984
+ .on('resize', this._resizeChart, this)
985
+ .on('rotate', this._rotateMarker, this)
986
+ .on('mousedown', this._resetDrag, this);
987
+
988
+ on(map.getContainer(), 'mousewheel', this._resetDrag, this);
989
+ on(map.getContainer(), 'touchstart', this._resetDrag, this);
990
+ on(document, 'keydown', this._onKeyDown, this);
991
+
992
+ this
993
+ .on('eledata_added eledata_loaded', this._updateChart, this)
994
+ .on('eledata_added eledata_loaded', this._updateSummary, this);
995
+
996
+ this._updateChart();
997
+ this._updateSummary();
998
+ });
999
+
1000
+
1001
+ },
1002
+
1003
+ _initLayer() {
1004
+ this._layers
1005
+ .on('layeradd layerremove', ({layer, type}) => {
1006
+ let node = layer.getElement && layer.getElement();
1007
+ toggleClass(node, this.options.polyline.className + ' ' + this.options.theme, type == 'layeradd');
1008
+ toggleEvent(layer, "mousemove", this._onMouseMoveLayer.bind(this), type == 'layeradd');
1009
+ toggleEvent(layer, "mouseout", this._onMouseOut.bind(this), type == 'layeradd');
1010
+ });
1011
+ },
1012
+
1013
+ _initMarker(map) {
1014
+ let pane = map.getPane('elevationPane');
1015
+ if (!pane) {
1016
+ pane = this._pane = map.createPane('elevationPane', map.getPane('norotatePane') || map.getPane('mapPane'));
1017
+ pane.style.zIndex = 625; // This pane is above markers but below popups.
1018
+ pane.style.pointerEvents = 'none';
1019
+ }
1020
+
1021
+ if (this._renderer) this._renderer.remove();
1022
+ this._renderer = L.svg({ pane: "elevationPane" }).addTo(this._map); // default leaflet svg renderer
1023
+
1024
+ this.import([this.__D3, this.__LMARKER])
1025
+ .then((m) => {
1026
+ this._marker = new (m[1] || Elevation).Marker(this.options, this);
1027
+ this.fire("elechart_marker");
1028
+ });
1029
+ },
1030
+
1031
+ /**
1032
+ * Inspired by L.Control.Layers
1033
+ */
1034
+ _initButton(container) {
1035
+ L.DomEvent
1036
+ .disableClickPropagation(container)
1037
+ .disableScrollPropagation(container);
1038
+
1039
+ this.options.collapsed ? this._collapse() : this._expand();
1040
+
1041
+ if (this.options.autohide) {
1042
+ on(container, 'mouseover', this._expand, this);
1043
+ on(container, 'mouseout', this._collapse, this);
1044
+ this._map.on('click', this._collapse, this);
1045
+ }
1046
+
1047
+ if (this.options.closeBtn) {
1048
+ let link = this._button = create('a', "elevation-toggle-icon", { href: '#', title: L._('Elevation'), }, container);
1049
+ on(link, 'click', L.DomEvent.stop);
1050
+ on(link, 'click', this._toggle, this);
1051
+ on(link, 'focus', this._toggle, this);
1052
+ fetch(resolveURL(this.__btnIcon, this.options.srcFolder)).then(r => r.ok && r.text().then(img => link.innerHTML = img));
1053
+ }
1054
+ },
1055
+
1056
+ _initSummary(container) {
1057
+ this.import(this.__LSUMMARY).then((m)=>{
1058
+ this._summary = new (m || Elevation).Summary({ summary: this.options.summary }, this);
1059
+
1060
+ this.on('elechart_init', () => {
1061
+ d3.select(container).call(this._summary.render());
1062
+ });
1063
+ });
1064
+ },
1065
+
1066
+ /**
1067
+ * Retrieve data from a remote url (HTTP).
1068
+ */
1069
+ _loadFile(url) {
1070
+ fetch(url)
1071
+ .then((response) => response.text())
1072
+ .then((data) => {
1073
+ this._downloadURL = url; // TODO: handle multiple urls?
1074
+ this._parseFromString(data)
1075
+ .then( geojson => geojson && this._loadLayer(geojson));
1076
+ }).catch((err) => console.warn(err));
1077
+ },
1078
+
1079
+ /**
1080
+ * Dynamically import only required javascript modules (code splitting)
1081
+ */
1082
+ _loadModules(handlers) {
1083
+ // First map known classnames (eg. "Altitude" --> L.Control.Elevation.Altitude)
1084
+ handlers = handlers.map((h) => typeof h === 'string' && typeof Elevation[h] !== "undefined" ? Elevation[h] : h);
1085
+ // Then load optional classes and custom imports (eg. "Cadence" --> import('../src/handlers/cadence.js'))
1086
+ let modules = handlers.map(file => (typeof file === 'string' && this.import(this.__modulesFolder + file.toLowerCase() + '.js')) || (file instanceof Promise && file) || Promise.resolve());
1087
+ return Promise.all(modules).then((m) => {
1088
+ each(m, (exported, i) => {
1089
+ let fn = exported && Object.keys(exported)[0];
1090
+ if (fn) {
1091
+ handlers[i] = Elevation[fn] = (Elevation[fn] ?? exported[fn]);
1092
+ }
1093
+ });
1094
+ each(handlers, h => ["function", "object"].includes(typeof h) && this._registerHandler(h));
1095
+ });
1096
+ },
1097
+
1098
+ /**
1099
+ * Simple GeoJSON data loader (L.GeoJSON).
1100
+ */
1101
+ _loadLayer(geojson) {
1102
+ let { polyline, theme, waypoints, wptIcons, wptLabels, distanceMarkers } = this.options;
1103
+ let style = L.extend({}, polyline);
1104
+
1105
+ if (theme) {
1106
+ style.className += ' ' + theme;
1107
+ }
1108
+
1109
+ if (geojson.name) {
1110
+ this.track_info.name = geojson.name;
1111
+ }
1112
+
1113
+ let layer = L.geoJson(geojson, {
1114
+ distanceMarkers: distanceMarkers,
1115
+ style: style,
1116
+ pointToLayer: (feature, latlng) => {
1117
+ if (waypoints) {
1118
+ let { desc, name, sym } = feature.properties;
1119
+ desc = desc || '';
1120
+ name = name || '';
1121
+ // Handle chart waypoints (dots)
1122
+ if ([true, 'dots'].includes(waypoints)) {
1123
+ this._registerCheckPoint({
1124
+ latlng: latlng,
1125
+ label : ([true, 'dots'].includes(wptLabels) ? name : '')
1126
+ });
1127
+ }
1128
+ // Handle map waypoints (markers)
1129
+ if ([true, 'markers'].includes(waypoints) && wptIcons != false) {
1130
+ return this._registerMarker({
1131
+ latlng : latlng,
1132
+ sym : (sym ?? name).replace(' ', '-').replace('"', '').replace("'", '').toLowerCase(),
1133
+ content: [true, 'markers'].includes(wptLabels) && (name || desc) && decodeURI("<b>" + name + "</b>" + (desc.length > 0 ? '<br>' + desc : ''))
1134
+ });
1135
+ }
1136
+ }
1137
+ },
1138
+ onEachFeature: (feature, layer) => feature.geometry && feature.geometry.type != 'Point' && this.addData(feature, layer),
1139
+ });
1140
+
1141
+ this.import(this.__D3).then(() => {
1142
+ this._initMapIntegrations(layer);
1143
+ const event_data = { data: geojson, layer: layer, name: this.track_info.name, track_info: this.track_info };
1144
+ if (this._modulesLoaded) {
1145
+ this._fireEvt("eledata_loaded", event_data);
1146
+ } else {
1147
+ this.once('modules_loaded', () => this._fireEvt("eledata_loaded", event_data));
1148
+ }
1149
+ });
1150
+
1151
+ return layer;
1152
+ },
1153
+
1154
+ _onDragEnd({ dragstart, dragend}) {
1155
+ this._hideMarker();
1156
+ this.fitBounds(L.latLngBounds([dragstart.latlng, dragend.latlng]));
1157
+
1158
+ this.fire("elechart_dragged");
1159
+ },
1160
+
1161
+ _onKeyDown({key}) {
1162
+ if (!this.options.detached && key === "Escape"){
1163
+ this._collapse();
1164
+ } },
1165
+
1166
+ /**
1167
+ * Trigger mouseenter event.
1168
+ */
1169
+ _onMouseEnter() {
1170
+ this.fire('elechart_enter');
1171
+ },
1172
+
1173
+ /*
1174
+ * Handles the moueseover the chart and displays distance and altitude level.
1175
+ */
1176
+ _onMouseMove({xCoord}) {
1177
+ if (this._chartEnabled && this._data.length) {
1178
+ let item = this._findItemForX(xCoord);
1179
+ if (item) {
1180
+ if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord);
1181
+
1182
+ this._updateMarker(item);
1183
+ this._setMapView(item);
1184
+
1185
+ if (this._map) {
1186
+ addClass(this._map.getContainer(), 'elechart-hover');
1187
+ }
1188
+
1189
+ this.fire("elechart_change", { data: item, xCoord: xCoord });
1190
+ this.fire("elechart_hover", { data: item, xCoord: xCoord });
1191
+ }
1192
+ }
1193
+ },
1194
+
1195
+ /*
1196
+ * Handles mouseover events of the data layers on the map.
1197
+ */
1198
+ _onMouseMoveLayer({latlng}) {
1199
+ if (this._data.length) {
1200
+ let item = this._findItemForLatLng(latlng);
1201
+ if (item) {
1202
+ let xCoord = item.xDiagCoord;
1203
+
1204
+ if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord);
1205
+
1206
+ this._updateMarker(item);
1207
+
1208
+ this.fire("elechart_change", { data: item, xCoord: xCoord });
1209
+ }
1210
+ }
1211
+ },
1212
+
1213
+ /*
1214
+ * Handles the moueseout over the chart.
1215
+ */
1216
+ _onMouseOut() {
1217
+ if (!this.options.detached) {
1218
+ this._hideMarker();
1219
+ this._chart._hideDiagramIndicator();
1220
+ }
1221
+
1222
+ if (this._map) {
1223
+ removeClass(this._map.getContainer(), 'elechart-hover');
1224
+ }
1225
+
1226
+ this.fire("elechart_leave");
1227
+ },
1228
+
1229
+ /**
1230
+ * Handles the drag event over the ruler filter.
1231
+ */
1232
+ _onRulerFilter({coords}) {
1233
+ this._updateMapSegments(coords);
1234
+ },
1235
+
1236
+ /**
1237
+ * Toggle chart data on legend click
1238
+ */
1239
+ _onToggleChart({ name, enabled }) {
1240
+
1241
+ this._chartEnabled = this._chart._hasActiveLayers();
1242
+
1243
+ // toggle layer visibility on empty chart
1244
+ this._layers.eachLayer(layer => toggleClass(layer.getElement && layer.getElement(), this.options.polyline.className + ' ' + this.options.theme, this._chartEnabled));
1245
+
1246
+ // toggle option value (eg. altitude = { 'disabled' || 'enabled' })
1247
+ this.options[name] = !enabled && this.options[name] == 'disabled' ? 'enabled' : 'disabled';
1248
+
1249
+ // remove marker on empty chart
1250
+ if (!this._chartEnabled) {
1251
+ this._chart._hideDiagramIndicator();
1252
+ this._marker.remove();
1253
+ }
1254
+ },
1255
+
1256
+ /**
1257
+ * Simple GeoJSON Parser
1258
+ */
1259
+ _parseFromGeoJSONString(data) {
1260
+ try {
1261
+ return JSON.parse(data);
1262
+ } catch (e) { }
1263
+ },
1264
+
1265
+ /**
1266
+ * Attempt to parse raw response data (GeoJSON or XML > GeoJSON)
1267
+ */
1268
+ _parseFromString(data) {
1269
+ return new Promise(resolve =>
1270
+ this.import(this.__TOGEOJSON).then(() => {
1271
+ let geojson;
1272
+ try {
1273
+ geojson = this._parseFromXMLString(data.trim());
1274
+ } catch (e) {
1275
+ geojson = this._parseFromGeoJSONString(data.toString());
1276
+ }
1277
+ if (geojson) {
1278
+ geojson.name = geojson.name || (this._downloadURL || '').split('/').pop().split('#')[0].split('?')[0];
1279
+ }
1280
+ resolve(geojson);
1281
+ })
1282
+ );
1283
+ },
1284
+
1285
+ /**
1286
+ * Simple XML Parser (GPX, KML, TCX)
1287
+ */
1288
+ _parseFromXMLString(data) {
1289
+ if (data.indexOf("<") != 0) {
1290
+ throw 'Invalid XML';
1291
+ }
1292
+ let xml = (new DOMParser()).parseFromString(data, "text/xml");
1293
+ let type = xml.documentElement.tagName.toLowerCase(); // "kml" or "gpx"
1294
+ let name = xml.getElementsByTagName('name');
1295
+ if (xml.getElementsByTagName('parsererror').length) {
1296
+ throw 'Invalid XML';
1297
+ }
1298
+ if (!(type in toGeoJSON)) {
1299
+ type = xml.documentElement.tagName == "TrainingCenterDatabase" ? 'tcx' : 'gpx';
1300
+ }
1301
+ let geojson = toGeoJSON[type](xml);
1302
+ geojson.name = name.length > 0 ? (Array.from(name).find(tag => tag.parentElement.tagName == "trk") ?? name[0]).textContent : '';
1303
+ return geojson;
1304
+ },
1305
+
1306
+ /**
1307
+ * Add chart profile to diagram
1308
+ */
1309
+ _registerAreaPath(props) {
1310
+ this.on("elechart_init", () => this._chart._registerAreaPath(props));
1311
+ },
1312
+
1313
+ /**
1314
+ * Add chart grid to diagram
1315
+ */
1316
+ _registerAxisGrid(props) {
1317
+ this.on("elechart_axis", () => this._chart._registerAxisGrid(props));
1318
+ },
1319
+
1320
+ /**
1321
+ * Add chart axis to diagram
1322
+ */
1323
+ _registerAxisScale(props) {
1324
+ this.on("elechart_axis", () => this._chart._registerAxisScale(props));
1325
+ },
1326
+
1327
+ /**
1328
+ * Add a point of interest over the diagram
1329
+ */
1330
+ _registerCheckPoint(props) {
1331
+ const cb = () => this._chart._registerCheckPoint(props);
1332
+ this
1333
+ .on("elechart_updated", cb)
1334
+ .once("eledata_clear", () => this.off("elechart_updated", cb));
1335
+ },
1336
+
1337
+ /**
1338
+ * Base handler for iterative track statistics (dist, time, z, slope, speed, acceleration, ...)
1339
+ */
1340
+ _registerDataAttribute(props) {
1341
+
1342
+ // parse of "coordinateProperties" for later usage
1343
+ if (props.coordPropsToMeta) {
1344
+ this.on("elepoint_init", (e) => props.coordPropsToMeta.call(this, e));
1345
+ }
1346
+
1347
+ // prevent excessive variabile instanstations
1348
+ let i, curr, prev, attr = props.attr || props.name;
1349
+
1350
+ // save here a reference to last used point
1351
+ let lastValid = {};
1352
+
1353
+ // iteration
1354
+ this.on("elepoint_added", ({index, point}) => {
1355
+ i = index;
1356
+
1357
+ prev = curr ?? this._data[i]; // same as: this._data[i > 0 ? i - 1 : i]
1358
+ curr = this._data[i];
1359
+
1360
+ // retrieve point value
1361
+ curr[attr] = props.pointToAttr.call(this, point, i);
1362
+
1363
+ // check and fix missing data on last added point
1364
+ if (i > 0 && isNaN(prev[attr])) {
1365
+ if (!isNaN(lastValid[attr]) && !isNaN(curr[attr])) {
1366
+ prev[attr] = (lastValid[attr] + curr[attr]) / 2;
1367
+ } else if (!isNaN(lastValid[attr])) {
1368
+ prev[attr] = lastValid[attr];
1369
+ } else if (!isNaN(curr[attr])) {
1370
+ prev[attr] = curr[attr];
1371
+ }
1372
+ // update "yAttr" and "xAttr"
1373
+ if (props.meta) {
1374
+ prev[props.meta] = prev[attr];
1375
+ }
1376
+ }
1377
+
1378
+ // skip to next iteration for invalid or missing data (eg. i == 0)
1379
+ if (isNaN(curr[attr])) {
1380
+ return;
1381
+ }
1382
+
1383
+ // update reference to last used point
1384
+ lastValid[attr] = curr[attr];
1385
+
1386
+ // Limit "crazy" delta values.
1387
+ if (props.deltaMax) {
1388
+ curr[attr] =wrapDelta(curr[attr], prev[attr], props.deltaMax);
1389
+ }
1390
+
1391
+ // Range of acceptable values.
1392
+ if (props.clampRange) {
1393
+ curr[attr] = clamp(curr[attr], props.clampRange);
1394
+ }
1395
+
1396
+ // Limit floating point precision.
1397
+ if (!isNaN(props.decimals)) {
1398
+ curr[attr] = round(curr[attr], props.decimals);
1399
+ }
1400
+
1401
+ // update "track_info" stats (min, max, avg, ...)
1402
+ if (props.stats) {
1403
+ for (const key in props.stats) {
1404
+ let sname = (props.statsName || attr) + (key != '' ? '_' : '');
1405
+ this.track_info[sname + key] = props.stats[key].call(this, curr[attr], this.track_info[sname + key], this._data.length);
1406
+ }
1407
+ }
1408
+
1409
+ // update here some mixins (eg. complex "track_info" stuff)
1410
+ if (props.onPointAdded) props.onPointAdded.call(this, curr[attr], i, point);
1411
+ });
1412
+ },
1413
+
1414
+ /**
1415
+ * Parse a module definition and attach related function listeners
1416
+ */
1417
+ _registerHandler(props) {
1418
+
1419
+ // eg. L.Control.Altitude
1420
+ if (typeof props === "function") {
1421
+ return this._registerHandler(props.call(this));
1422
+ }
1423
+
1424
+ let {
1425
+ name,
1426
+ attr,
1427
+ required,
1428
+ deltaMax,
1429
+ clampRange,
1430
+ decimals,
1431
+ meta,
1432
+ unit,
1433
+ coordinateProperties,
1434
+ coordPropsToMeta: coordPropsToMeta$1,
1435
+ pointToAttr,
1436
+ onPointAdded,
1437
+ stats,
1438
+ statsName,
1439
+ grid,
1440
+ scale,
1441
+ path,
1442
+ tooltip,
1443
+ summary
1444
+ } = props;
1445
+
1446
+ // eg. "altitude" == true
1447
+ if (this.options[name] || required) {
1448
+
1449
+ this._registerDataAttribute({
1450
+ name,
1451
+ attr,
1452
+ meta,
1453
+ deltaMax,
1454
+ clampRange,
1455
+ decimals,
1456
+ coordPropsToMeta: coordPropsToMeta(coordinateProperties, meta || name, coordPropsToMeta$1),
1457
+ pointToAttr,
1458
+ onPointAdded,
1459
+ stats,
1460
+ statsName,
1461
+ });
1462
+
1463
+ if (grid) {
1464
+ this._registerAxisGrid(L.extend({ name }, grid));
1465
+ }
1466
+
1467
+ if (this.options[name] !== "summary") {
1468
+ if (scale) this._registerAxisScale(L.extend({ name, label: unit }, scale));
1469
+ if (path) this._registerAreaPath(L.extend({ name }, path));
1470
+ }
1471
+
1472
+ if (tooltip || props.tooltips) {
1473
+ each([tooltip, ...(props.tooltips || [])], t => t && this._registerTooltip(L.extend({ name }, t)));
1474
+ }
1475
+
1476
+ if (summary) {
1477
+ each(summary, (s, k) => summary[k] = L.extend({ unit }, s));
1478
+ this._registerSummary(summary);
1479
+ }
1480
+ }
1481
+ },
1482
+
1483
+ _registerMarker({latlng, sym, content}) {
1484
+ let { wptIcons } = this.options;
1485
+ // generate and cache appropriate icon symbol
1486
+ if (!wptIcons.hasOwnProperty(sym)) {
1487
+ wptIcons[sym] = L.divIcon(L.extend({}, wptIcons[""].options, { html: '<i class="elevation-waypoint-icon ' + sym + '"></i>' } ));
1488
+ }
1489
+ let marker = L.marker(latlng, { icon: wptIcons[sym] });
1490
+ if (content) {
1491
+ marker.bindPopup(content, { className: 'elevation-popup', keepInView: true }).openPopup();
1492
+ marker.bindTooltip(content, { className: 'elevation-tooltip', direction: 'auto', sticky: true, opacity: 1 }).openTooltip();
1493
+ }
1494
+ return this._addMarker(marker)
1495
+ },
1496
+
1497
+ /**
1498
+ * Add chart or marker tooltip info
1499
+ */
1500
+ _registerTooltip(props) {
1501
+ props.chart && this.on("elechart_init", () => this._chart._registerTooltip(L.extend({}, props, { value: props.chart })));
1502
+ props.marker && this.on("elechart_marker", () => this._marker._registerTooltip(L.extend({}, props, { value: props.marker })));
1503
+ },
1504
+
1505
+ /**
1506
+ * Add summary info to diagram
1507
+ */
1508
+ _registerSummary(props) {
1509
+ this.on('elechart_summary', () => this._summary._registerSummary(props));
1510
+ },
1511
+
1512
+ /*
1513
+ * Removes the drag rectangle and zoms back to the total extent of the data.
1514
+ */
1515
+ _resetDrag() {
1516
+ this._chart._resetDrag();
1517
+ this._hideMarker();
1518
+ },
1519
+
1520
+ /**
1521
+ * Resets drag, marker and bounds.
1522
+ */
1523
+ _resetView() {
1524
+ if (this._map && this._map._isFullscreen) return;
1525
+ this._resetDrag();
1526
+ this._hideMarker();
1527
+ if (this.options.autofitBounds) {
1528
+ this.fitBounds();
1529
+ }
1530
+ },
1531
+
1532
+ /**
1533
+ * Hacky way for handling chart resize. Deletes it and redraw chart.
1534
+ */
1535
+ _resizeChart() {
1536
+ if (this._container && style(this._container, "display") != "none") {
1537
+ let opts = this.options;
1538
+ let newWidth = opts.detached ? (this.eleDiv || this._container).offsetWidth : clamp(opts._maxWidth, [0, this._map.getContainer().clientWidth - 30]);
1539
+ if (newWidth) {
1540
+ opts.width = newWidth;
1541
+ if (this._chart && this._chart._chart) {
1542
+ this._chart._chart._resize(opts);
1543
+ this._updateChart();
1544
+ }
1545
+ }
1546
+ this._updateMapSegments();
1547
+ }
1548
+ },
1549
+
1550
+ /**
1551
+ * Collapse or Expand chart control.
1552
+ */
1553
+ _toggle() {
1554
+ hasClass(this._container, "elevation-expanded") ? this._collapse() : this._expand();
1555
+ },
1556
+
1557
+ /**
1558
+ * Update map center and zoom (followMarker: true)
1559
+ */
1560
+ _setMapView(item) {
1561
+ if (this._map && this.options.followMarker) {
1562
+ let zoom = this._map.getZoom();
1563
+ let z = this.options.zFollow;
1564
+ if (typeof z === "number") {
1565
+ this._map.setView(item.latlng, (zoom < z ? z : zoom), { animate: true, duration: 0.25 });
1566
+ } else if (!this._map.getBounds().contains(item.latlng)) {
1567
+ this._map.setView(item.latlng, zoom, { animate: true, duration: 0.25 });
1568
+ }
1569
+ }
1570
+ },
1571
+
1572
+ /**
1573
+ * Calculates [x, y] domain and then update chart.
1574
+ */
1575
+ _updateChart() {
1576
+ if (this._chart && this._container) {
1577
+ this.fire("elechart_axis");
1578
+
1579
+ this._chart.update({ data: this._data, options: this.options });
1580
+
1581
+ this._x = this._chart._x;
1582
+ this._y = this._chart._y;
1583
+
1584
+ this.fire('elechart_updated');
1585
+ }
1586
+ },
1587
+
1588
+ /*
1589
+ * Update the position/height indicator marker drawn onto the map
1590
+ */
1591
+ _updateMarker(item) {
1592
+ if (this._marker) {
1593
+ this._marker.update({
1594
+ map : this._map,
1595
+ item : item,
1596
+ yCoordMax : this._yCoordMax || 0,
1597
+ options : this.options
1598
+ });
1599
+ }
1600
+ },
1601
+
1602
+ /**
1603
+ * Fix marker rotation on rotated maps
1604
+ */
1605
+ _rotateMarker() {
1606
+ if (this._marker) {
1607
+ this._marker.update();
1608
+ }
1609
+ },
1610
+
1611
+ /**
1612
+ * Highlight track segments on the map.
1613
+ */
1614
+ _updateMapSegments(coords) {
1615
+ this._markedSegments.setLatLngs(coords || []);
1616
+ if (coords && this._map && !this._map.hasLayer(this._markedSegments)) {
1617
+ this._markedSegments.addTo(this._map);
1618
+ }
1619
+ },
1620
+
1621
+ /**
1622
+ * Update chart summary.
1623
+ */
1624
+ _updateSummary() {
1625
+ if (this._summary) {
1626
+ this._summary.reset();
1627
+ if (this.options.summary) {
1628
+ this.fire("elechart_summary");
1629
+ this._summary.update();
1630
+ }
1631
+ if (this.options.downloadLink && this._downloadURL) { // TODO: generate dynamically file content instead of using static file urls.
1632
+ this._summary._container.innerHTML += '<span class="download"><a href="#">' + L._('Download') + '</a></span>';
1633
+ select('.download a', this._summary._container).onclick = (e) => {
1634
+ e.preventDefault();
1635
+ let event = { downloadLink: this.options.downloadLink, confirm: saveFile.bind(this, this._downloadURL) };
1636
+ if (this.options.downloadLink == 'modal' && typeof CustomEvent === "function") {
1637
+ document.dispatchEvent(new CustomEvent("eletrack_download", { detail: event }));
1638
+ } else if (this.options.downloadLink == 'link' || this.options.downloadLink === true) {
1639
+ event.confirm();
1640
+ }
1641
+ this.fire('eletrack_download', event);
1642
+ };
1643
+ }
1644
+ }
1645
+ },
1646
+
1647
+
1648
+ /**
1649
+ * Calculates chart width.
1650
+ */
1651
+ _width() {
1652
+ if (this._chart) return this._chart._width();
1653
+ const { width, margins } = this.options;
1654
+ return width - margins.left - margins.right;
1655
+ },
1656
+
1657
+ /**
1658
+ * Calculates chart height.
1659
+ */
1660
+ _height() {
1661
+ if (this._chart) return this._chart._height();
1662
+ const { height, margins } = this.options;
1663
+ return height - margins.top - margins.bottom;
1664
+ },
1665
+
1622
1666
  });
1623
1667
 
1624
1668
  /*