(function (factory) {
    typeof define === 'function' && define.amd ? define(factory) :
    factory();
}((function () { 'use strict';

    // Following https://github.com/Leaflet/Leaflet/blob/master/PLUGIN-GUIDE.md
    (function (factory, window) {

        // define an AMD module that relies on 'leaflet'
        if (typeof define === 'function' && define.amd) {
            define(['leaflet'], factory);

            // define a Common JS module that relies on 'leaflet'
        } else if (typeof exports === 'object') {
            module.exports = factory(require('leaflet'));
        }

        // attach your plugin to the global 'L' variable
        if (typeof window !== 'undefined' && window.L) {
            factory(window.L);

        }
    }(function (L) {
        L.locales = {};
        L.locale = null;
        L.registerLocale = function registerLocale(code, locale) {
            L.locales[code] = L.Util.extend({}, L.locales[code], locale);
        };
        L.setLocale = function setLocale(code) {
            L.locale = code;
        };
        return L.i18n = L._ = function translate(string, data) {
            if (L.locale && L.locales[L.locale] && L.locales[L.locale][string]) {
                string = L.locales[L.locale][string];
            }
            try {
                // Do not fail if some data is missing
                // a bad translation should not break the app
                string = L.Util.template(string, data);
            }
            catch (err) {/*pass*/
            }

            return string;
        };
    }, window));

    /*
     * Copyright (c) 2019, GPL-3.0+ Project, Raruto
     *
     *  This file is free software: you may copy, redistribute and/or modify it
     *  under the terms of the GNU General Public License as published by the
     *  Free Software Foundation, either version 2 of the License, or (at your
     *  option) any later version.
     *
     *  This file is distributed in the hope that it will be useful, but
     *  WITHOUT ANY WARRANTY; without even the implied warranty of
     *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     *  General Public License for more details.
     *
     *  You should have received a copy of the GNU General Public License
     *  along with this program.  If not, see .
     *
     * This file incorporates work covered by the following copyright and
     * permission notice:
     *
     *     Copyright (c) 2013-2016, MIT License, Felix “MrMufflon” Bache
     *
     *     Permission to use, copy, modify, and/or distribute this software
     *     for any purpose with or without fee is hereby granted, provided
     *     that the above copyright notice and this permission notice appear
     *     in all copies.
     *
     *     THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
     *     WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
     *     WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
     *     AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
     *     CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
     *     OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
     *     NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
     *     CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     */

    L.Control.Elevation = L.Control.extend({

    	includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,

    	options: {
    		autohide: !L.Browser.mobile,
    		autohideMarker: true,
    		collapsed: false,
    		controlButton: {
    			iconCssClass: "elevation-toggle-icon",
    			title: "Elevation"
    		},
    		detached: true,
    		distanceFactor: 1,
    		dragging: !L.Browser.mobile,
    		downloadLink: 'link',
    		elevationDiv: "#elevation-div",
    		followMarker: true,
    		forceAxisBounds: false,
    		gpxOptions: {
    			async: true,
    			marker_options: {
    				startIconUrl: null,
    				endIconUrl: null,
    				shadowUrl: null,
    				wptIcons: {
    					'': L.divIcon({
    						className: 'elevation-waypoint-marker',
    						html: '<i class="elevation-waypoint-icon"></i>',
    						iconSize: [30, 30],
    						iconAnchor: [8, 30],
    					})
    				},
    			},
    		},
    		height: 200,
    		heightFactor: 1,
    		hoverNumber: {
    			decimalsX: 2,
    			decimalsY: 0,
    			formatter: undefined
    		},
    		imperial: false,
    		interpolation: "curveLinear",
    		lazyLoadJS: true,
    		legend: true,
    		loadData: {
    			defer: false,
    			lazy: false,
    		},
    		marker: 'elevation-line',
    		markerIcon: L.divIcon({
    			className: 'elevation-position-marker',
    			html: '<i class="elevation-position-icon"></i>',
    			iconSize: [32, 32],
    			iconAnchor: [16, 16],
    		}),
    		placeholder: false,
    		position: "topright",
    		polyline: {
    			className: 'elevation-polyline',
    			color: '#000',
    			opacity: 0.75,
    			weight: 5,
    			lineCap: 'round'
    		},
    		reverseCoords: false,
    		skipNullZCoords: false,
    		theme: "lightblue-theme",
    		margins: {
    			top: 10,
    			right: 20,
    			bottom: 30,
    			left: 50
    		},
    		responsive: true,
    		summary: 'inline',
    		width: 600,
    		xLabel: "km",
    		xTicks: undefined,
    		yAxisMax: undefined,
    		yAxisMin: undefined,
    		yLabel: "m",
    		yTicks: undefined,
    		zFollow: 13,
    	},
    	__mileFactor: 0.621371,
    	__footFactor: 3.28084,

    	/*
    	 * Add data to the diagram either from GPX or GeoJSON and update the axis domain and data
    	 */
    	addData: function(d, layer) {
    		L.Control.Elevation._d3LazyLoader = this._lazyLoadJS(
    			'https://unpkg.com/d3@5.15.0/dist/d3.min.js',
    			typeof d3 !== 'object',
    			L.Control.Elevation._d3LazyLoader
    		).then(
    			function(d, layer) {
    				this._addData(d);

    				if (this._container) {
    					this._applyData();
    				}
    				if ((typeof layer === "undefined" || layer === null) && d.on) {
    					layer = d;
    				}
    				if (layer) {
    					if (layer._path) {
    						L.DomUtil.addClass(layer._path, this.options.polyline.className + ' ' + this.options.theme);
    					}
    					layer
    						.on("mousemove", this._mousemoveLayerHandler, this)
    						.on("mouseout", this._mouseoutHandler, this);
    				}

    				this.track_info = L.extend({}, this.track_info, {
    					distance: this._distance,
    					elevation_max: this._maxElevation,
    					elevation_min: this._minElevation
    				});

    				this._layers = this._layers || {};
    				this._layers[L.Util.stamp(layer)] = layer;

    				this._fireEvt("eledata_added", { data: d, layer: layer, track_info: this.track_info }, true);
    			}.bind(this, d, layer));
    	},

    	/**
    	 * Adds the control to the given map.
    	 */
    	addTo: function(map) {
    		if (this.options.detached) {
    			this._appendElevationDiv(map._container).appendChild(this.onAdd(map));
    		} else {
    			L.Control.prototype.addTo.call(this, map);
    		}
    		return this;
    	},

    	/*
    	 * Reset data and display
    	 */
    	clear: function() {
    		this._clearPath();
    		this._clearChart();
    		this._clearData();

    		this._fireEvt("eledata_clear");
    	},

    	/**
    	 * Disable dragging chart on touch events.
    	 */
    	disableDragging: function() {
    		this._draggingEnabled = false;
    		this._resetDrag();
    	},

    	/**
    	 * Enable dragging chart on touch events.
    	 */
    	enableDragging: function() {
    		this._draggingEnabled = true;
    	},

    	/**
    	 * Sets a map view that contains the given geographical bounds.
    	 */
    	fitBounds: function(bounds) {
    		bounds = bounds || this._fullExtent;
    		if (this._map && bounds) this._map.fitBounds(bounds);
    	},

    	/**
    	 * Get default zoom level when "followMarker" is true.
    	 */
    	getZFollow: function() {
    		return this._zFollow;
    	},

    	/**
    	 * Hide current elevation chart profile.
    	 */
    	hide: function() {
    		this._container.style.display = "none";
    	},

    	/**
    	 * Initialize chart control "options" and "container".
    	 */
    	initialize: function(options) {
    		this.options = this._deepMerge({}, this.options, options);

    		if (this.options.imperial) {
    			this._distanceFactor = this.__mileFactor;
    			this._heightFactor = this.__footFactor;
    			this._xLabel = "mi";
    			this._yLabel = "ft";
    		} else {
    			this._distanceFactor = this.options.distanceFactor;
    			this._heightFactor = this.options.heightFactor;
    			this._xLabel = this.options.xLabel;
    			this._yLabel = this.options.yLabel;
    		}

    		this._chartEnabled = true;
    		this._draggingEnabled = this.options.dragging;
    		this._zFollow = this.options.zFollow;

    		if (this.options.followMarker) this._setMapView = L.Util.throttle(this._setMapView, 300, this);
    		if (this.options.placeholder) this.options.loadData.lazy = this.options.loadData.defer = true;

    		this.on('waypoint_added', function(e) {
    			if (e.point._popup) {
    				e.point._popup.options.className = 'elevation-popup';
    				e.point._popup._content = decodeURI(e.point._popup._content);
    			}
    			if (e.point._popup && e.point._popup._content) {
    				e.point.bindTooltip(e.point._popup._content, { direction: 'top', sticky: true, opacity: 1, className: 'elevation-tooltip' }).openTooltip();
    			}
    		});
    	},

    	/**
    	 * Alias for loadData
    	 */
    	load: function(data, opts) {
    		this.loadData(data, opts);
    	},

    	/**
    	 * Alias for addTo
    	 */
    	loadChart: function(map) {
    		this.addTo(map);
    	},

    	/**
    	 * Load elevation data (GPX or GeoJSON).
    	 */
    	loadData: function(data, opts) {
    		opts = L.extend({}, this.options.loadData, opts);
    		if (opts.defer) {
    			this.loadDefer(data, opts);
    		} else if (opts.lazy) {
    			this.loadLazy(data, opts);
    		} else if (this._isXMLDoc(data)) {
    			this.loadGPX(data);
    		} else if (this._isJSONDoc(data)) {
    			this.loadGeoJSON(data);
    		} else {
    			this.loadFile(data);
    		}
    	},

    	/**
    	 * Wait for document load before download data.
    	 */
    	loadDefer: function(data, opts) {
    		opts = L.extend({}, this.options.loadData, opts);
    		opts.defer = false;
    		if (document.readyState !== 'complete') window.addEventListener("load", L.bind(this.loadData, this, data, opts), { once: true });
    		else this.loadData(data, opts);
    	},

    	/**
    	 * Load data from a remote url.
    	 */
    	loadFile: function(url) {
    		this._downloadURL = url; // TODO: handle multiple urls?
    		try {
    			let xhr = new XMLHttpRequest();
    			xhr.responseType = "text";
    			xhr.open('GET', url);
    			xhr.onload = function() {
    				if (xhr.status !== 200) {
    					throw "Error " + xhr.status + " while fetching remote file: " + url;
    				} else {
    					this.loadData(xhr.response, { lazy: false, defer: false });
    				}
    			}.bind(this);
    			xhr.send();
    		} catch (e) {
    			console.warn(e);
    		}
    	},

    	/**
    	 * Load raw GeoJSON data.
    	 */
    	loadGeoJSON: function(data) {
    		if (typeof data === "string") {
    			data = JSON.parse(data);
    		}

    		this.layer = this.geojson = L.geoJson(data, {
    			style: function(feature) {
    				let style = L.extend({}, this.options.polyline);
    				if (this.options.theme) {
    					style.className += ' ' + this.options.theme;
    				}
    				return style;
    			}.bind(this),
    			pointToLayer: function(feature, latlng) {
    				let marker = L.marker(latlng, { icon: this.options.gpxOptions.marker_options.wptIcons[''] });
    				let desc = feature.properties.desc ? feature.properties.desc : '';
    				let name = feature.properties.name ? feature.properties.name : '';
    				if (name || desc) {
    					marker.bindPopup("<b>" + name + "</b>" + (desc.length > 0 ? '<br>' + desc : '')).openPopup();
    				}
    				this.fire('waypoint_added', { point: marker, point_type: 'waypoint', element: latlng });
    				return marker;
    			}.bind(this),
    			onEachFeature: function(feature, layer) {
    				if (feature.geometry.type == 'Point') return;

    				this.addData(feature, layer);

    				this.track_info = L.extend({}, this.track_info, { type: "geojson", name: data.name });
    			}.bind(this),
    		});
    		if (this._map) {
    			this._map.once('layeradd', function(e) {
    				this.fitBounds(this.layer.getBounds());
    				this._fireEvt("eledata_loaded", { data: data, layer: this.layer, name: this.track_info.name, track_info: this.track_info }, true);
    			}, this);

    			this.layer.addTo(this._map);
    		} else {
    			console.warn("Undefined elevation map object");
    		}
    	},

    	/**
    	 * Load raw GPX data.
    	 */
    	loadGPX: function(data) {
    		L.Control.Elevation._gpxLazyLoader = this._lazyLoadJS(
    			'https://unpkg.com/leaflet-gpx@1.5.0/gpx.js',
    			typeof L.GPX !== 'function',
    			L.Control.Elevation._gpxLazyLoader
    		).then(
    			function(data) {
    				this.options.gpxOptions.polyline_options = L.extend({}, this.options.polyline, this.options.gpxOptions.polyline_options);

    				if (this.options.theme) {
    					this.options.gpxOptions.polyline_options.className += ' ' + this.options.theme;
    				}

    				this.layer = this.gpx = new L.GPX(data, this.options.gpxOptions);

    				this.layer.on('loaded', function(e) { this.fitBounds(e.target.getBounds()); }, this);
    				this.layer.on('addpoint', function(e) { this.fire("waypoint_added", e, true); }, this);
    				this.layer.once("addline", function(e) {
    					this.addData(e.line /*, this.layer*/ );

    					this.track_info = L.extend({}, this.track_info, { type: "gpx", name: this.layer.get_name() });

    					this._fireEvt("eledata_loaded", { data: data, layer: this.layer, name: this.track_info.name, track_info: this.track_info }, true);
    				}, this);

    				if (this._map) {
    					this.layer.addTo(this._map);
    				} else {
    					console.warn("Undefined elevation map object");
    				}
    			}.bind(this, data)
    		);
    	},

    	/**
    	 * Wait for chart container visible before download data.
    	 */
    	loadLazy: function(data, opts) {
    		opts = L.extend({}, this.options.loadData, opts);
    		opts.lazy = false;
    		let ticking = false;
    		let scrollFn = L.bind(function(data) {
    			if (!ticking) {
    				L.Util.requestAnimFrame(function() {
    					if (this._isVisible(this.placeholder)) {
    						window.removeEventListener('scroll', scrollFn);
    						this.loadData(data, opts);
    						this.once('eledata_loaded', function() {
    							if (this.placeholder && this.placeholder.parentNode) {
    								this.placeholder.parentNode.removeChild(this.placeholder);
    							}
    						}, this);
    					}
    					ticking = false;
    				}, this);
    				ticking = true;
    			}
    		}, this, data);
    		window.addEventListener('scroll', scrollFn);
    		if (this.placeholder) this.placeholder.addEventListener('mouseenter', scrollFn, { once: true });
    		scrollFn();
    	},

    	/**
    	 * Create container DOM element and related event listeners.
    	 * Called on control.addTo(map).
    	 */
    	onAdd: function(map) {
    		this._map = map;

    		let container = this._container = L.DomUtil.create("div", "elevation-control elevation");

    		if (!this.options.detached) {
    			L.DomUtil.addClass(container, 'leaflet-control');
    		}

    		if (this.options.theme) {
    			L.DomUtil.addClass(container, this.options.theme); // append theme to control
    		}

    		if (this.options.placeholder && !this._data) {
    			this.placeholder = L.DomUtil.create('img', 'elevation-placeholder');
    			if (typeof this.options.placeholder === 'string') {
    				this.placeholder.src = this.options.placeholder;
    				this.placeholder.alt = '';
    			} else {
    				for (let i in this.options.placeholder) { this.placeholder.setAttribute(i, this.options.placeholder[i]); }
    			}
    			container.insertBefore(this.placeholder, container.firstChild);
    		}

    		L.Control.Elevation._d3LazyLoader = this._lazyLoadJS(
    			'https://unpkg.com/d3@5.15.0/dist/d3.min.js',
    			typeof d3 !== 'object',
    			L.Control.Elevation._d3LazyLoader
    		).then(
    			function(map, container) {
    				this._initToggle(container);
    				this._initChart(container);

    				this._applyData();

    				this._map.on('zoom viewreset zoomanim', this._hidePositionMarker, this);
    				this._map.on('resize', this._resetView, this);
    				this._map.on('resize', this._resizeChart, this);
    				this._map.on('mousedown', this._resetDrag, this);

    				this._map.on('eledata_added', this._updateSummary, this);

    				L.DomEvent.on(this._map._container, 'mousewheel', this._resetDrag, this);
    				L.DomEvent.on(this._map._container, 'touchstart', this._resetDrag, this);

    			}.bind(this, map, container)
    		);

    		return container;
    	},

    	/**
    	 * Clean up control code and related event listeners.
    	 * Called on control.remove().
    	 */
    	onRemove: function(map) {
    		this._container = null;
    	},

    	/**
    	 * Redraws the chart control. Sometimes useful after screen resize.
    	 */
    	redraw: function() {
    		this._resizeChart();
    	},

    	/**
    	 * Set default zoom level when "followMarker" is true.
    	 */
    	setZFollow: function(zoom) {
    		this._zFollow = zoom;
    	},

    	/**
    	 * Hide current elevation chart profile.
    	 */
    	show: function() {
    		this._container.style.display = "block";
    	},

    	/*
    	 * Parsing data either from GPX or GeoJSON and update the diagram data
    	 */
    	_addData: function(d) {
    		let geom = d && d.geometry;
    		let feat = d && d.type === "FeatureCollection";
    		let gpx = d && d._latlngs;

    		if (geom) {
    			switch (geom.type) {
    				case 'LineString':
    					this._addGeoJSONData(geom.coordinates);
    					break;

    				case 'MultiLineString':
    					geom.coordinates.forEach(coords => this._addGeoJSONData(coords));
    					break;

    				default:
    					console.warn('Unsopperted GeoJSON feature geometry type:' + geom.type);
    			}
    		}

    		if (feat) {
    			d.features.forEach(feature => this._addData(feature));
    		}

    		if (gpx) {
    			this._addGPXdata(d._latlngs);
    		}
    	},

    	/*
    	 * Parsing of GeoJSON data lines and their elevation in z-coordinate
    	 */
    	_addGeoJSONData: function(coords) {
    		if (coords) {
    			coords.forEach(point => this._addPoint(point[1], point[0], point[2]));
    		}
    	},

    	/*
    	 * Parsing function for GPX data and their elevation in z-coordinate
    	 */
    	_addGPXdata: function(coords) {
    		if (coords) {
    			coords.forEach(point => this._addPoint(point.lat, point.lng, point.meta.ele));
    		}
    	},

    	/*
    	 * Parse and push a single (x, y, z) point to current elevation profile.
    	 */
    	_addPoint: function(x, y, z) {
    		if (this.options.reverseCoords) {
    			[x, y] = [y, x];
    		}

    		let data = this._data || [];
    		let eleMax = this._maxElevation || -Infinity;
    		let eleMin = this._minElevation || +Infinity;
    		let dist = this._distance || 0;

    		let curr = new L.LatLng(x, y);
    		let prev = data.length ? data[data.length - 1].latlng : curr;

    		let delta = curr.distanceTo(prev) * this._distanceFactor;

    		dist = dist + Math.round(delta / 1000 * 100000) / 100000;

    		// check and fix missing elevation data on last added point
    		if (!this.options.skipNullZCoords && data.length > 0) {
    			let prevZ = data[data.length - 1].z;
    			if (isNaN(prevZ)) {
    				let lastZ = this._lastValidZ;
    				let currZ = z * this._heightFactor;
    				if (!isNaN(lastZ) && !isNaN(currZ)) {
    					prevZ = (lastZ + currZ) / 2;
    				} else if (!isNaN(lastZ)) {
    					prevZ = lastZ;
    				} else if (!isNaN(currZ)) {
    					prevZ = currZ;
    				}
    				if (!isNaN(prevZ)) data[data.length - 1].z = prevZ;
    				else data.splice(data.length - 1, 1);
    			}
    		}

    		z = z * this._heightFactor;

    		// skip point if it has not elevation
    		if (!isNaN(z)) {
    			eleMax = eleMax < z ? z : eleMax;
    			eleMin = eleMin > z ? z : eleMin;
    			this._lastValidZ = z;
    		}

    		data.push({
    			dist: dist,
    			x: x,
    			y: y,
    			z: z,
    			latlng: curr
    		});

    		this._data = data;
    		this._distance = dist;
    		this._maxElevation = eleMax;
    		this._minElevation = eleMin;
    	},

    	/**
    	 * Generate "svg" chart container.
    	 */
    	_appendChart: function(svg) {
    		let g = svg
    			.append("g")
    			.attr("transform", "translate(" + this.options.margins.left + "," + this.options.margins.top + ")");

    		this._appendGrid(g);
    		this._appendAreaPath(g);
    		this._appendAxis(g);
    		this._appendFocusRect(g);
    		this._appendMouseFocusG(g);
    		this._appendLegend(g);
    	},

    	/**
    	 * Adds the control to the given "detached" div.
    	 */
    	_appendElevationDiv: function(container) {
    		let eleDiv = document.querySelector(this.options.elevationDiv);
    		if (!eleDiv) {
    			eleDiv = L.DomUtil.create('div', 'leaflet-control elevation elevation-div');
    			this.options.elevationDiv = '#elevation-div_' + Math.random().toString(36).substr(2, 9);
    			eleDiv.id = this.options.elevationDiv.substr(1);
    			container.parentNode.insertBefore(eleDiv, container.nextSibling); // insert after end of container.
    		}
    		if (this.options.detached) {
    			L.DomUtil.addClass(eleDiv, 'elevation-detached');
    			L.DomUtil.removeClass(eleDiv, 'leaflet-control');
    		}
    		this.eleDiv = eleDiv;
    		return this.eleDiv;
    	},

    	/**
    	 * Generate "x-axis".
    	 */
    	_appendXaxis: function(axis) {
    		axis
    			.append("g")
    			.attr("class", "x axis")
    			.attr("transform", "translate(0," + this._height() + ")")
    			.call(
    				d3
    				.axisBottom()
    				.scale(this._x)
    				.ticks(this.options.xTicks)
    			)
    			.append("text")
    			.attr("x", this._width() + 6)
    			.attr("y", 30)
    			.text(this._xLabel);
    	},

    	/**
    	 * Generate "x-grid".
    	 */
    	_appendXGrid: function(grid) {
    		grid.append("g")
    			.attr("class", "x grid")
    			.attr("transform", "translate(0," + this._height() + ")")
    			.call(
    				d3
    				.axisBottom()
    				.scale(this._x)
    				.ticks(this.options.xTicks)
    				.tickSize(-this._height())
    				.tickFormat("")
    			);

    	},

    	/**
    	 * Generate "y-axis".
    	 */
    	_appendYaxis: function(axis) {
    		axis
    			.append("g")
    			.attr("class", "y axis")
    			.call(
    				d3
    				.axisLeft()
    				.scale(this._y)
    				.ticks(this.options.yTicks)
    			)
    			.append("text")
    			.attr("x", -30)
    			.attr("y", 3)
    			.text(this._yLabel);
    	},

    	/**
    	 * Generate "y-grid".
    	 */
    	_appendYGrid: function(grid) {
    		grid.append("g")
    			.attr("class", "y grid")
    			.call(
    				d3
    				.axisLeft()
    				.scale(this._y)
    				.ticks(this.options.yTicks)
    				.tickSize(-this._width())
    				.tickFormat("")
    			);
    	},

    	/**
    	 * Generate "path".
    	 */
    	_appendAreaPath: function(g) {
    		this._areapath = g.append("path")
    			.attr("class", "area");
    	},

    	/**
    	 * Generate "axis".
    	 */
    	_appendAxis: function(g) {
    		this._axis = g.append("g")
    			.attr("class", "axis");
    		this._appendXaxis(this._axis);
    		this._appendYaxis(this._axis);
    	},

    	/**
    	 * Generate "mouse-focus" and "drag-rect".
    	 */
    	_appendFocusRect: function(g) {
    		let focusRect = this._focusRect = g.append("rect")
    			.attr("width", this._width())
    			.attr("height", this._height())
    			.style("fill", "none")
    			.style("stroke", "none")
    			.style("pointer-events", "all");

    		if (L.Browser.mobile) {
    			focusRect
    				.on("touchmove.drag", this._dragHandler.bind(this))
    				.on("touchstart.drag", this._dragStartHandler.bind(this))
    				.on("touchstart.focus", this._mousemoveHandler.bind(this))
    				.on("touchmove.focus", this._mousemoveHandler.bind(this));
    			L.DomEvent.on(this._container, 'touchend', this._dragEndHandler, this);
    		}

    		focusRect
    			.on("mousemove.drag", this._dragHandler.bind(this))
    			.on("mousedown.drag", this._dragStartHandler.bind(this))
    			.on("mouseenter.focus", this._mouseenterHandler.bind(this))
    			.on("mousemove.focus", this._mousemoveHandler.bind(this))
    			.on("mouseout.focus", this._mouseoutHandler.bind(this));
    		L.DomEvent.on(this._container, 'mouseup', this._dragEndHandler, this);
    	},

    	/**
    	 * Generate "grid".
    	 */
    	_appendGrid: function(g) {
    		this._grid = g.append("g")
    			.attr("class", "grid");
    		this._appendXGrid(this._grid);
    		this._appendYGrid(this._grid);
    	},

    	/**
    	 * Generate "mouse-focus".
    	 */
    	_appendMouseFocusG: function(g) {
    		let focusG = this._focusG = g.append("g")
    			.attr("class", "mouse-focus-group");

    		this._mousefocus = focusG.append('svg:line')
    			.attr('class', 'mouse-focus-line')
    			.attr('x2', '0')
    			.attr('y2', '0')
    			.attr('x1', '0')
    			.attr('y1', '0');

    		this._focuslabelrect = focusG.append("rect")
    			.attr('class', 'mouse-focus-label')
    			.attr("x", 0)
    			.attr("y", 0)
    			.attr("width", 0)
    			.attr("height", 0)
    			.attr("rx", 3)
    			.attr("ry", 3);

    		this._focuslabeltext = focusG.append("svg:text")
    			.attr("class", "mouse-focus-label-text");
    		this._focuslabelY = this._focuslabeltext.append("svg:tspan")
    			.attr("class", "mouse-focus-label-y")
    			.attr("dy", "-1em");
    		this._focuslabelX = this._focuslabeltext.append("svg:tspan")
    			.attr("class", "mouse-focus-label-x")
    			.attr("dy", "2em");
    	},

    	/**
    	 * Generate "legend".
    	 */
    	_appendLegend: function(g) {
    		if (!this.options.legend) return;

    		let legend = this._legend = g.append('g')
    			.attr("class", "legend");

    		let altitude = this._altitudeLegend = this._legend.append('g')
    			.attr("class", "legend-altitude");

    		altitude.append("rect")
    			.attr("class", "area")
    			.attr("x", (this._width() / 2) - 50)
    			.attr("y", this._height() + this.options.margins.bottom - 17)
    			.attr("width", 50)
    			.attr("height", 5)
    			.attr("opacity", 0.75);

    		altitude.append('text')
    			.text(L._('Altitude'))
    			.attr("x", (this._width() / 2) + 5)
    			.attr("font-size", 10)
    			.style("text-decoration-thickness", "2px")
    			.style("font-weight", "700")
    			.attr('y', this._height() + this.options.margins.bottom - 11);

    		// autotoggle chart data on single click
    		legend.on('click', function() {
    			if (this._chartEnabled) {
    				this._clearChart();
    				this._clearPath();
    				this._chartEnabled = false;
    			} else {
    				this._resizeChart();
    				for (let id in this._layers) {
    					L.DomUtil.addClass(this._layers[id]._path, this.options.polyline.className + ' ' + this.options.theme);
    				}
    				this._chartEnabled = true;
    			}
    		}.bind(this));

    	},

    	/**
    	 * Generate "svg:line".
    	 */
    	_appendPositionMarker: function(pane) {
    		let theme = this.options.theme;
    		let heightG = pane.select("g");

    		this._mouseHeightFocus = heightG.append('svg:line')
    			.attr("class", theme + " height-focus line")
    			.attr("x2", 0)
    			.attr("y2", 0)
    			.attr("x1", 0)
    			.attr("y1", 0);

    		this._pointG = heightG.append("g");
    		this._pointG.append("svg:circle")
    			.attr("class", theme + " height-focus circle-lower")
    			.attr("r", 6)
    			.attr("cx", 0)
    			.attr("cy", 0);

    		this._mouseHeightFocusLabel = heightG.append("svg:text")
    			.attr("class", theme + " height-focus-label")
    			.style("pointer-events", "none");
    	},

    	/**
    	 * Calculates [x, y] domain and then update chart.
    	 */
    	_applyData: function() {
    		if (!this._data) return;

    		let xdomain = d3.extent(this._data, d => d.dist);
    		let ydomain = d3.extent(this._data, d => d.z);
    		let opts = this.options;

    		if (opts.yAxisMin !== undefined && (opts.yAxisMin < ydomain[0] || opts.forceAxisBounds)) {
    			ydomain[0] = opts.yAxisMin;
    		}
    		if (opts.yAxisMax !== undefined && (opts.yAxisMax > ydomain[1] || opts.forceAxisBounds)) {
    			ydomain[1] = opts.yAxisMax;
    		}

    		this._x.domain(xdomain);
    		this._y.domain(ydomain);
    		this._areapath.datum(this._data)
    			.attr("d", this._area);
    		this._updateAxis();

    		this._fullExtent = this._calculateFullExtent(this._data);
    	},

    	/*
    	 * Calculates the full extent of the data array
    	 */
    	_calculateFullExtent: function(data) {
    		if (!data || data.length < 1) {
    			throw new Error("no data in parameters");
    		}

    		let ext = new L.latLngBounds(data[0].latlng, data[0].latlng);

    		data.forEach(item => ext.extend(item.latlng));

    		return ext;
    	},

    	/*
    	 * Reset chart.
    	 */
    	_clearChart: function() {
    		this._resetDrag();
    		if (this._areapath) {
    			// workaround for 'Error: Problem parsing d=""' in Webkit when empty data
    			// https://groups.google.com/d/msg/d3-js/7rFxpXKXFhI/HzIO_NPeDuMJ
    			//this._areapath.datum(this._data).attr("d", this._area);
    			this._areapath.attr("d", "M0 0");

    			this._x.domain([0, 1]);
    			this._y.domain([0, 1]);
    			this._updateAxis();
    		}
    		if (this._altitudeLegend) {
    			this._altitudeLegend.select('text').style("text-decoration-line", "line-through");
    		}
    	},

    	/*
    	 * Reset data.
    	 */
    	_clearData: function() {
    		this._data = null;
    		this._distance = null;
    		this._maxElevation = null;
    		this._minElevation = null;
    		this.track_info = null;
    		this._layers = null;
    		// if (this.layer) {
    		// 	this.layer.removeFrom(this._map);
    		// }
    	},

    	/*
    	 * Reset path.
    	 */
    	_clearPath: function() {
    		this._hidePositionMarker();
    		for (let id in this._layers) {
    			L.DomUtil.removeClass(this._layers[id]._path, this.options.polyline.className);
    			L.DomUtil.removeClass(this._layers[id]._path, this.options.theme);
    		}
    	},

    	/*
    	 * Collapse current chart control.
    	 */
    	_collapse: function() {
    		if (this._container) {
    			L.DomUtil.removeClass(this._container, 'elevation-expanded');
    			L.DomUtil.addClass(this._container, 'elevation-collapsed');
    		}
    	},

    	/**
    	 * Recursive deep merge objects.
    	 * Alternative to L.Util.setOptions(this, options).
    	 */
    	_deepMerge: function(target, ...sources) {
    		if (!sources.length) return target;
    		const source = sources.shift();
    		if (this._isObject(target) && this._isObject(source)) {
    			for (const key in source) {
    				if (this._isObject(source[key])) {
    					if (!target[key]) Object.assign(target, {
    						[key]: {}
    					});
    					this._deepMerge(target[key], source[key]);
    				} else {
    					Object.assign(target, {
    						[key]: source[key]
    					});
    				}
    			}
    		}
    		return this._deepMerge(target, ...sources);
    	},

    	/*
    	 * Handle drag operations.
    	 */
    	_dragHandler: function() {
    		//we don't want map events to occur here
    		d3.event.preventDefault();
    		d3.event.stopPropagation();

    		this._gotDragged = true;
    		this._drawDragRectangle();
    	},

    	/*
    	 * Handles end of drag operations. Zooms the map to the selected items extent.
    	 */
    	_dragEndHandler: function() {
    		if (!this._dragStartCoords || !this._dragCurrentCoords || !this._gotDragged) {
    			this._dragStartCoords = null;
    			this._gotDragged = false;
    			if (this._draggingEnabled) this._resetDrag();
    			return;
    		}

    		let item1 = this._findItemForX(this._dragStartCoords[0]);
    		let item2 = this._findItemForX(this._dragCurrentCoords[0]);

    		if (item1 == item2) return;

    		this._hidePositionMarker();

    		this._fitSection(item1, item2);

    		this._dragStartCoords = null;
    		this._gotDragged = false;

    		this._fireEvt("elechart_dragged", { data: { dragstart: this._data[item1], dragend: this._data[item2] } }, true);
    	},

    	/*
    	 * Handles start of drag operations.
    	 */
    	_dragStartHandler: function() {
    		d3.event.preventDefault();
    		d3.event.stopPropagation();

    		this._gotDragged = false;
    		this._dragStartCoords = d3.mouse(this._focusRect.node());
    	},

    	/*
    	 * Draws the currently dragged rectangle over the chart.
    	 */
    	_drawDragRectangle: function() {
    		if (!this._dragStartCoords || !this._draggingEnabled) {
    			return;
    		}

    		let dragEndCoords = this._dragCurrentCoords = d3.mouse(this._focusRect.node());

    		let x1 = Math.min(this._dragStartCoords[0], dragEndCoords[0]);
    		let x2 = Math.max(this._dragStartCoords[0], dragEndCoords[0]);

    		if (!this._dragRectangle && !this._dragRectangleG) {
    			let g = d3.select(this._container).select("svg").select("g");

    			this._dragRectangleG = g.insert("g", ".mouse-focus-group");

    			this._dragRectangle = this._dragRectangleG.append("rect")
    				.attr("width", x2 - x1)
    				.attr("height", this._height())
    				.attr("x", x1)
    				.attr('class', 'mouse-drag')
    				.style("pointer-events", "none");
    		} else {
    			this._dragRectangle.attr("width", x2 - x1)
    				.attr("x", x1);
    		}
    	},

    	/*
    	 * Expand current chart control.
    	 */
    	_expand: function() {
    		if (this._container) {
    			L.DomUtil.removeClass(this._container, 'elevation-collapsed');
    			L.DomUtil.addClass(this._container, 'elevation-expanded');
    		}
    	},

    	/*
    	 * Finds an item with the smallest delta in distance to the given latlng coords
    	 */
    	_findItemForLatLng: function(latlng) {
    		let result = null;
    		let d = Infinity;
    		this._data.forEach(item => {
    			let dist = latlng.distanceTo(item.latlng);
    			if (dist < d) {
    				d = dist;
    				result = item;
    			}
    		});
    		return result;
    	},

    	/*
    	 * Finds a data entry for a given x-coordinate of the diagram
    	 */
    	_findItemForX: function(x) {
    		let data = this._data ? this._data : [0, 1];
    		let bisect = d3.bisector(d => d.dist).left;
    		let xinvert = this._x.invert(x);
    		return bisect(data, xinvert);
    	},

    	/**
    	 * Fires an event of the specified type.
    	 */
    	_fireEvt: function(type, data, propagate) {
    		if (this.fire) {
    			this.fire(type, data, propagate);
    		}
    		if (this._map) {
    			this._map.fire(type, data, propagate);
    		}
    	},

    	/**
    	 * Make the map fit the route section between given indexes.
    	 */
    	_fitSection: function(index1, index2) {
    		let start = Math.min(index1, index2);
    		let end = Math.max(index1, index2);
    		let ext = this._calculateFullExtent(this._data.slice(start, end));
    		this.fitBounds(ext);
    	},

    	/*
    	 * Fromatting funciton using the given decimals and seperator
    	 */
    	_formatter: function(num, dec, sep) {
    		let res = L.Util.formatNum(num, dec).toString();
    		let numbers = res.split(".");
    		if (numbers[1]) {
    			for (let d = dec - numbers[1].length; d > 0; d--) {
    				numbers[1] += "0";
    			}
    			res = numbers.join(sep || ".");
    		}
    		return res;
    	},

    	/**
    	 * Calculates chart height.
    	 */
    	_height: function() {
    		let opts = this.options;
    		return opts.height - opts.margins.top - opts.margins.bottom;
    	},

    	/*
    	 * Hides the position/height indicator marker drawn onto the map
    	 */
    	_hidePositionMarker: function() {
    		if (!this.options.autohideMarker) {
    			return;
    		}

    		this._selectedItem = null;

    		if (this._marker) {
    			if (this._map) this._map.removeLayer(this._marker);
    			this._marker = null;
    		}
    		if (this._mouseHeightFocus) {
    			this._mouseHeightFocus.style("visibility", "hidden");
    			this._mouseHeightFocusLabel.style("visibility", "hidden");
    		}
    		if (this._pointG) {
    			this._pointG.style("visibility", "hidden");
    		}
    		if (this._focusG) {
    			this._focusG.style("visibility", "hidden");
    		}
    	},

    	/**
    	 * Generate "svg" chart DOM element.
    	 */
    	_initChart: function() {
    		let opts = this.options;
    		opts.xTicks = opts.xTicks || Math.round(this._width() / 75);
    		opts.yTicks = opts.yTicks || Math.round(this._height() / 30);
    		opts.hoverNumber.formatter = opts.hoverNumber.formatter || this._formatter;

    		if (opts.responsive) {
    			if (opts.detached) {
    				let offWi = this.eleDiv.offsetWidth;
    				let offHe = this.eleDiv.offsetHeight;
    				opts.width = offWi > 0 ? offWi : opts.width;
    				opts.height = (offHe - 20) > 0 ? offHe - 20 : opts.height; // 20 = horizontal scrollbar size.
    			} else {
    				opts._maxWidth = opts._maxWidth > opts.width ? opts._maxWidth : opts.width;
    				let containerWidth = this._map._container.clientWidth;
    				opts.width = opts._maxWidth > containerWidth ? containerWidth - 30 : opts.width;
    			}
    		}

    		let x = this._x = d3.scaleLinear().range([0, this._width()]);
    		let y = this._y = d3.scaleLinear().range([this._height(), 0]);

    		let interpolation = typeof opts.interpolation === 'function' ? opts.interpolation : d3[opts.interpolation];

    		let area = this._area = d3.area().curve(interpolation)
    			.x(d => (d.xDiagCoord = x(d.dist)))
    			.y0(this._height())
    			.y1(d => y(d.z));
    		let line = this._line = d3.line()
    			.x(d => d3.mouse(svg.select("g"))[0])
    			.y(d => this._height());

    		let container = d3.select(this._container);

    		let svg = container.append("svg")
    			.attr("class", "background")
    			.attr("width", opts.width)
    			.attr("height", opts.height);

    		let summary = this.summaryDiv = container.append("div")
    			.attr("class", "elevation-summary " + this.options.summary + "-summary").node();

    		this._appendChart(svg);
    		this._updateSummary();

    	},

    	/**
    	 * Inspired by L.Control.Layers
    	 */
    	_initToggle: function(container) {
    		//Makes this work on IE10 Touch devices by stopping it from firing a mouseout event when the touch is released
    		container.setAttribute('aria-haspopup', true);

    		if (!this.options.detached) {
    			L.DomEvent
    				.disableClickPropagation(container);
    			//.disableScrollPropagation(container);
    		}

    		if (L.Browser.mobile) {
    			L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation);
    		}

    		L.DomEvent.on(container, 'mousewheel', this._mousewheelHandler, this);

    		if (!this.options.detached) {
    			let iconCssClass = "elevation-toggle " + this.options.controlButton.iconCssClass + (this.options.autohide ? "" : " close-button");
    			let link = this._button = L.DomUtil.create('a', iconCssClass, container);
    			link.href = '#';
    			link.title = this.options.controlButton.title;

    			if (this.options.collapsed) {
    				this._collapse();
    				if (this.options.autohide) {
    					L.DomEvent
    						.on(container, 'mouseover', this._expand, this)
    						.on(container, 'mouseout', this._collapse, this);
    				} else {
    					L.DomEvent
    						.on(link, 'click', L.DomEvent.stop)
    						.on(link, 'click', this._toggle, this);
    				}

    				L.DomEvent.on(link, 'focus', this._toggle, this);

    				this._map.on('click', this._collapse, this);
    				// TODO: keyboard accessibility
    			}
    		}
    	},

    	/**
    	 * Check object type.
    	 */
    	_isObject: function(item) {
    		return (item && typeof item === 'object' && !Array.isArray(item));
    	},

    	/**
    	 * Check JSON object type.
    	 */
    	_isJSONDoc: function(doc, lazy) {
    		lazy = typeof lazy === "undefined" ? true : lazy;
    		if (typeof doc === "string" && lazy) {
    			doc = doc.trim();
    			return doc.indexOf("{") == 0 || doc.indexOf("[") == 0;
    		} else {
    			try {
    				JSON.parse(doc.toString());
    			} catch (e) {
    				if (typeof doc === "object" && lazy) return true;
    				console.warn(e);
    				return false;
    			}
    			return true;
    		}
    	},

    	/**
    	 * Check XML object type.
    	 */
    	_isXMLDoc: function(doc, lazy) {
    		lazy = typeof lazy === "undefined" ? true : lazy;
    		if (typeof doc === "string" && lazy) {
    			doc = doc.trim();
    			return doc.indexOf("<") == 0;
    		} else {
    			let documentElement = (doc ? doc.ownerDocument || doc : 0).documentElement;
    			return documentElement ? documentElement.nodeName !== "HTML" : false;
    		}
    	},

    	/**
    	 * Check DOM element visibility.
    	 */
    	_isDomVisible: function(elem) {
    		return !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
    	},

    	/**
    	 * Check DOM element viewport visibility.
    	 */
    	_isVisible: function(elem) {
    		if (!elem) return false;

    		let styles = window.getComputedStyle(elem);

    		function isVisibleByStyles(elem, styles) {
    			return styles.visibility !== 'hidden' && styles.display !== 'none';
    		}

    		function isAboveOtherElements(elem, styles) {
    			let boundingRect = elem.getBoundingClientRect();
    			let left = boundingRect.left + 1;
    			let right = boundingRect.right - 1;
    			let top = boundingRect.top + 1;
    			let bottom = boundingRect.bottom - 1;
    			let above = true;

    			let pointerEvents = elem.style.pointerEvents;

    			if (styles['pointer-events'] == 'none') elem.style.pointerEvents = 'auto';

    			if (document.elementFromPoint(left, top) !== elem) above = false;
    			if (document.elementFromPoint(right, top) !== elem) above = false;

    			// Only for completely visible elements
    			// if (document.elementFromPoint(left, bottom) !== elem) above = false;
    			// if (document.elementFromPoint(right, bottom) !== elem) above = false;

    			elem.style.pointerEvents = pointerEvents;

    			return above;
    		}

    		if (!isVisibleByStyles(elem, styles)) return false;
    		if (!isAboveOtherElements(elem, styles)) return false;
    		return true;
    	},

    	/**
    	 * Async JS script download.
    	 */
    	_lazyLoadJS: function(url, skip, loader) {
    		if (skip === false || !this.options.lazyLoadJS) {
    			return Promise.resolve();
    		}
    		if (loader instanceof Promise) {
    			return loader;
    		}
    		return new Promise((resolve, reject) => {
    			let tag = document.createElement("script");
    			tag.addEventListener('load', resolve, { once: true });
    			tag.src = url;
    			document.head.appendChild(tag);
    		});
    	},

    	/*
    	 * Handles the moueseenter over the chart.
    	 */
    	_mouseenterHandler: function() {
    		this._fireEvt("elechart_enter", null, true);
    	},

    	/*
    	 * Handles the moueseover the chart and displays distance and altitude level.
    	 */
    	_mousemoveHandler: function(d, i, ctx) {
    		if (!this._data || this._data.length === 0 || !this._chartEnabled) {
    			return;
    		}
    		let coords = d3.mouse(this._focusRect.node());
    		let xCoord = coords[0];
    		let item = this._data[this._findItemForX(xCoord)];

    		this._hidePositionMarker();
    		this._showDiagramIndicator(item, xCoord);
    		this._showPositionMarker(item);
    		this._setMapView(item);

    		if (this._map && this._map._container) {
    			L.DomUtil.addClass(this._map._container, 'elechart-hover');
    		}

    		this._fireEvt("elechart_change", { data: item }, true);
    		this._fireEvt("elechart_hover", { data: item }, true);
    	},

    	/*
    	 * Handles mouseover events of the data layers on the map.
    	 */
    	_mousemoveLayerHandler: function(e) {
    		if (!this._data || this._data.length === 0) {
    			return;
    		}
    		let latlng = e.latlng;
    		let item = this._findItemForLatLng(latlng);
    		if (item) {
    			let xCoord = item.xDiagCoord;

    			this._hidePositionMarker();
    			this._showDiagramIndicator(item, xCoord);
    			this._showPositionMarker(item);
    		}
    	},

    	/*
    	 * Handles the moueseout over the chart.
    	 */
    	_mouseoutHandler: function() {
    		if (!this.options.detached) {
    			this._hidePositionMarker();
    		}

    		if (this._map && this._map._container) {
    			L.DomUtil.removeClass(this._map._container, 'elechart-hover');
    		}

    		this._fireEvt("elechart_leave", null, true);
    	},

    	/*
    	 * Handles the mouesewheel over the chart.
    	 */
    	_mousewheelHandler: function(e) {
    		if (this._map.gestureHandling && this._map.gestureHandling._enabled) return;
    		let ll = this._selectedItem ? this._selectedItem.latlng : this._map.getCenter();
    		let z = e.deltaY > 0 ? this._map.getZoom() - 1 : this._map.getZoom() + 1;
    		this._resetDrag();
    		this._map.flyTo(ll, z);

    	},

    	/*
    	 * Removes the drag rectangle and zoms back to the total extent of the data.
    	 */
    	_resetDrag: function() {
    		if (this._dragRectangleG) {
    			this._dragRectangleG.remove();
    			this._dragRectangleG = null;
    			this._dragRectangle = null;
    			this._hidePositionMarker();
    		}
    	},

    	/**
    	 * Resets drag, marker and bounds.
    	 */
    	_resetView: function() {
    		if (this._map && this._map._isFullscreen) return;
    		this._resetDrag();
    		this._hidePositionMarker();
    		this.fitBounds(this._fullExtent);
    	},

    	/**
    	 * Hacky way for handling chart resize. Deletes it and redraw chart.
    	 */
    	_resizeChart: function() {
    		if (this.options.responsive) {
    			if (this.options.detached) {
    				let newWidth = this.eleDiv.offsetWidth; // - 20;

    				if (newWidth <= 0) return;

    				this.options.width = newWidth;
    				this.eleDiv.innerHTML = "";
    				this.eleDiv.appendChild(this.onAdd(this._map));
    			} else {
    				this._map.removeControl(this._container);
    				this.addTo(this._map);
    			}
    		}
    	},

    	/**
    	 * Generate GPX / GeoJSON download event.
    	 */
    	_saveFile: function(fileUrl) {
    		let d = document,
    			a = d.createElement('a'),
    			b = d.body;
    		a.href = fileUrl;
    		a.target = '_new';
    		a.download = ""; // fileName
    		a.style.display = 'none';
    		b.appendChild(a);
    		a.click();
    		b.removeChild(a);
    	},

    	/**
    	 * Display distance and altitude level ("focus-rect").
    	 */
    	_showDiagramIndicator: function(item, xCoordinate) {
    		if (!this._chartEnabled) return;

    		let opts = this.options;
    		this._focusG.style("visibility", "visible");

    		this._mousefocus.attr('x1', xCoordinate)
    			.attr('y1', 0)
    			.attr('x2', xCoordinate)
    			.attr('y2', this._height())
    			.classed('hidden', false);

    		let alt = item.z,
    			dist = item.dist,
    			ll = item.latlng,
    			numY = opts.hoverNumber.formatter(alt, opts.hoverNumber.decimalsY),
    			numX = opts.hoverNumber.formatter(dist, opts.hoverNumber.decimalsX);

    		this._focuslabeltext
    			// .attr("x", xCoordinate)
    			.attr("y", this._y(item.z))
    			.style("font-weight", "700");

    		this._focuslabelX
    			.text(numX + " " + this._xLabel)
    			.attr("x", xCoordinate + 10);

    		this._focuslabelY
    			.text(numY + " " + this._yLabel)
    			.attr("x", xCoordinate + 10);

    		let focuslabeltext = this._focuslabeltext.node();
    		if (this._isDomVisible(focuslabeltext)) {
    			let bbox = focuslabeltext.getBBox();
    			let padding = 2;

    			this._focuslabelrect
    				.attr("x", bbox.x - padding)
    				.attr("y", bbox.y - padding)
    				.attr("width", bbox.width + (padding * 2))
    				.attr("height", bbox.height + (padding * 2));

    			// move focus label to left
    			if (xCoordinate >= this._width() / 2) {
    				this._focuslabelrect.attr("x", this._focuslabelrect.attr("x") - this._focuslabelrect.attr("width") - (padding * 2) - 10);
    				this._focuslabelX.attr("x", this._focuslabelX.attr("x") - this._focuslabelrect.attr("width") - (padding * 2) - 10);
    				this._focuslabelY.attr("x", this._focuslabelY.attr("x") - this._focuslabelrect.attr("width") - (padding * 2) - 10);
    			}
    		}

    	},

    	/**
    	 * Collapse or Expand current chart control.
    	 */
    	_toggle: function() {
    		if (L.DomUtil.hasClass(this._container, "elevation-expanded"))
    			this._collapse();
    		else
    			this._expand();
    	},

    	/**
    	 * Sets the view of the map (center and zoom). Useful when "followMarker" is true.
    	 */
    	_setMapView: function(item) {
    		if (!this.options.followMarker || !this._map) return;
    		let zoom = this._map.getZoom();
    		zoom = zoom < this._zFollow ? this._zFollow : zoom;
    		this._map.setView(item.latlng, zoom, { animate: true, duration: 0.25 });
    	},

    	/*
    	 * Shows the position/height indicator marker drawn onto the map
    	 */
    	_showPositionMarker: function(item) {
    		this._selectedItem = item;

    		if (this._map && !this._map.getPane('elevationPane')) {
    			this._map.createPane('elevationPane');
    			this._map.getPane('elevationPane').style.zIndex = 625; // This pane is above markers but below popups.
    			this._map.getPane('elevationPane').style.pointerEvents = 'none';
    		}

    		if (this.options.marker == 'elevation-line') {
    			this._updatePositionMarker(item);
    		} else if (this.options.marker == 'position-marker') {
    			this._updateLeafletMarker(item);
    		}
    	},

    	/**
    	 * Update chart axis.
    	 */
    	_updateAxis: function() {
    		this._grid.selectAll("g").remove();
    		this._axis.selectAll("g").remove();
    		this._appendXGrid(this._grid);
    		this._appendYGrid(this._grid);
    		this._appendXaxis(this._axis);
    		this._appendYaxis(this._axis);
    	},

    	/**
    	 * Update distance and altitude level ("leaflet-marker").
    	 */
    	_updateHeightIndicator: function(item) {
    		let opts = this.options;

    		let numY = opts.hoverNumber.formatter(item.z, opts.hoverNumber.decimalsY);
    		let numX = opts.hoverNumber.formatter(item.dist, opts.hoverNumber.decimalsX);

    		let normalizedAlt = this._height() / this._maxElevation * item.z;
    		let normalizedY = item.y - normalizedAlt;

    		this._mouseHeightFocus
    			.attr("x1", item.x)
    			.attr("x2", item.x)
    			.attr("y1", item.y)
    			.attr("y2", normalizedY)
    			.style("visibility", "visible");

    		this._mouseHeightFocusLabel
    			.attr("x", item.x)
    			.attr("y", normalizedY)
    			.text(numY + " " + this._yLabel)
    			.style("visibility", "visible");
    	},

    	/**
    	 * Update position marker ("leaflet-marker").
    	 */
    	_updateLeafletMarker: function(item) {
    		let ll = item.latlng;

    		if (!this._marker) {
    			this._marker = new L.Marker(ll, {
    				icon: this.options.markerIcon,
    				zIndexOffset: 1000000,
    			});
    			this._marker.addTo(this._map, {
    				pane: 'elevationPane',
    			});
    		} else {
    			this._marker.setLatLng(ll);
    		}
    	},

    	/**
    	 * Update focus point ("leaflet-marker").
    	 */
    	_updatePointG: function(item) {
    		this._pointG
    			.attr("transform", "translate(" + item.x + "," + item.y + ")")
    			.style("visibility", "visible");
    	},

    	/**
    	 * Update position marker ("leaflet-marker").
    	 */
    	_updatePositionMarker: function(item) {
    		let point = this._map.latLngToLayerPoint(item.latlng);
    		let layerpoint = {
    			dist: item.dist,
    			x: point.x,
    			y: point.y,
    			z: item.z,
    		};

    		if (!this._mouseHeightFocus) {
    			L.svg({ pane: "elevationPane" }).addTo(this._map); // default leaflet svg renderer
    			let layerpane = d3.select(this._map.getContainer()).select(".leaflet-elevation-pane svg");
    			this._appendPositionMarker(layerpane);
    		}

    		this._updatePointG(layerpoint);
    		this._updateHeightIndicator(layerpoint);
    	},

    	/**
    	 * Update chart summary.
    	 */
    	_updateSummary: function() {
    		if (this.options.summary && this.summaryDiv) {
    			this.track_info = this.track_info || {};
    			this.track_info.distance = this._distance || 0;
    			this.track_info.elevation_max = this._maxElevation || 0;
    			this.track_info.elevation_min = this._minElevation || 0;
    			d3.select(this.summaryDiv).html('<span class="totlen"><span class="summarylabel">' + L._("Total Length: ") + '</span><span class="summaryvalue">' + this.track_info.distance.toFixed(2) + ' ' + this._xLabel + '</span></span><span class="maxele"><span class="summarylabel">' + L._("Max Elevation: ") + '</span><span class="summaryvalue">' + this.track_info.elevation_max.toFixed(2) + ' ' + this._yLabel + '</span></span><span class="minele"><span class="summarylabel">' + L._("Min Elevation: ") + '</span><span class="summaryvalue">' + this.track_info.elevation_min.toFixed(2) + ' ' + this._yLabel + '</span></span>');
    		}
    		if (this.options.downloadLink && this._downloadURL) { // TODO: generate dynamically file content instead of using static file urls.
    			let span = document.createElement('span');
    			span.className = 'download';
    			let save = document.createElement('a');
    			save.innerHTML = "Télécharger"; //modif
    			save.href = "#";
    			save.onclick = function(e) {
    				e.preventDefault();
    				let evt = { confirm: this._saveFile.bind(this, this._downloadURL) };
    				let type = this.options.downloadLink;
    				if (type == 'modal') {
    					if (typeof CustomEvent === "function") document.dispatchEvent(new CustomEvent("eletrack_download", { detail: evt }));
    					this._fireEvt('eletrack_download', evt);
    				} else if (type == 'link' || type === true) {
    					evt.confirm();
    				}
    			}.bind(this);

    			this.summaryDiv.appendChild(span).appendChild(save);
    		}
    	},

    	/**
    	 * Calculates chart width.
    	 */
    	_width: function() {
    		let opts = this.options;
    		return opts.width - opts.margins.left - opts.margins.right;
    	},

    });

    L.control.elevation = function(options) {
    	return new L.Control.Elevation(options);
    };

})));
