/*
 * Copyright (c) 2019, GPL-3.0+ Project, altrdev
 *
 *  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) 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: true,
        autohideMarker: true,
        collapsed: false,
        controlButton: {
            iconCssClass: "elevation-toggle-icon",
            title: "Elevation"
        },
        detached: true,
        distanceFactor: 1,
        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],
                    })
                },
            },
            polyline_options: {
                className: '',
                color: '#566B13',
                opacity: 0.75,
                weight: 5,
                lineCap: 'round'
            },
        },
        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",
        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) {
        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, 'elevation-polyline ' + this.options.theme);
            }
            layer
                .on("mousemove", this._mousemoveLayerHandler, this)
                .on("mouseout", this._mouseoutHandler, this);
        }

        this.track_info = this.track_info || {};
        this.track_info.distance = this._distance;
        this.track_info.elevation_max = this._maxElevation;
        this.track_info.elevation_min = this._minElevation;

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

        var evt = {
            data: d,
            layer: layer,
            track_info: this.track_info,
        };
        if (this.fire) this.fire("eledata_added", evt, true);
        if (this._map) this._map.fire("eledata_added", evt, true);
    },

    addTo: function(map) {
        if (this.options.detached) {
            this._addToChartDiv(map);
        } else {
            L.Control.prototype.addTo.call(this, map);
        }
        return this;
    },

    /*
     * Reset data and display
     */
    clear: function() {

        this._clearPath();
        this._clearChart();
        this._clearData();

        if (this.fire) this.fire("eledata_clear");
        if (this._map) this._map.fire("eledata_clear");
    },

    disableDragging: function() {
        this._draggingEnabled = false;
        this._resetDrag();
    },

    enableDragging: function() {
        this._draggingEnabled = true;
    },

    fitBounds: function(bounds) {
        bounds = bounds || this._fullExtent;
        if (this._map && bounds) this._map.fitBounds(bounds);
    },

    getZFollow: function() {
        return this._zFollow;
    },

    hide: function() {
        this._container.style.display = "none";
    },

    initialize: function(options) {
        this.options.autohide = typeof options.autohide !== "undefined" ? options.autohide : !L.Browser.mobile;

        // Aliases.
        if (typeof options.detachedView !== "undefined") this.options.detached = options.detachedView;
        if (typeof options.responsiveView !== "undefined") this.options.responsive = options.responsiveView;
        if (typeof options.showTrackInfo !== "undefined") this.options.summary = options.showTrackInfo;
        if (typeof options.summaryType !== "undefined") this.options.summary = options.summaryType;
        if (typeof options.autohidePositionMarker !== "undefined") this.options.autohideMarker = options.autohidePositionMarker;
        if (typeof options.followPositionMarker !== "undefined") this.options.followMarker = options.followPositionMarker;
        if (typeof options.useLeafletMarker !== "undefined") this.options.marker = options.useLeafletMarker ? 'position-marker' : 'elevation-line';
        if (typeof options.leafletMarkerIcon !== "undefined") this.options.markerIcon = options.leafletMarkerIcon;
        if (typeof options.download !== "undefined") this.options.downloadLink = options.download;

        // L.Util.setOptions(this, options);
        this.options = this._deepMerge({}, this.options, options);

        this._draggingEnabled = !L.Browser.mobile;
        this._chartEnabled = true;

        if (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._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;
    },

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

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

    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);
        }
    },

    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)
    },

    loadFile: function(url) {
        this._downloadURL = url; // TODO: handle multiple urls?
        try {
            var 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);
        }
    },

    loadGeoJSON: function(data) {
        if (typeof data === "string") {
            data = JSON.parse(data);
        }

        this.layer = this.geojson = L.geoJson(data, {
            style: function(feature) {
                return {
                    color: '#566B13',
                    className: 'elevation-polyline ' + this.options.theme,
                };
            }.bind(this),
            onEachFeature: function(feature, layer) {
                this.addData(feature, layer);

                this.track_info = this.track_info || {};
                this.track_info.type = "geojson";
                this.track_info.name = data.name;
                this.track_info.distance = this._distance;
                this.track_info.elevation_max = this._maxElevation;
                this.track_info.elevation_min = this._minElevation;

            }.bind(this),
        });
        if (this._map) {
            this._map.once('layeradd', function(e) {
                this.fitBounds(this.layer.getBounds());
                var evt = {
                    data: data,
                    layer: this.layer,
                    name: this.track_info.name,
                    track_info: this.track_info,
                };
                if (this.fire) this.fire("eledata_loaded", evt, true);
                if (this._map) this._map.fire("eledata_loaded", evt, true);
            }, this);

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

    loadGPX: function(data) {
        var callback = function(data) {
            this.options.gpxOptions.polyline_options.className += 'elevation-polyline ' + 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) {

                if(e.point_type === "start" || e.point_type === "end") {
                    e.point.setZIndexOffset(10000);
                }
                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();
                }
            });
            this.layer.once("addline", function(e) {
                this.addData(e.line /*, this.layer*/ );

                this.track_info = this.track_info || {};
                this.track_info.type = "gpx";
                this.track_info.name = this.layer.get_name();
                this.track_info.distance = this._distance;
                this.track_info.elevation_max = this._maxElevation;
                this.track_info.elevation_min = this._minElevation;

                var evt = {
                    data: data,
                    layer: this.layer,
                    name: this.track_info.name,
                    track_info: this.track_info,
                };

                if (this.fire) this.fire("eledata_loaded", evt, true);
                if (this._map) this._map.fire("eledata_loaded", evt, true);
            }, this);

            if (this._map) {
                this.layer.addTo(this._map);
            } else {
                console.warn("Undefined elevation map object");
            }
        }.bind(this, data);
        if (typeof L.GPX !== 'function' && this.options.lazyLoadJS) {
            L.Control.Elevation._gpxLazyLoader = this._lazyLoadJS('https://cdnjs.cloudflare.com/ajax/libs/leaflet-gpx/1.5.0/gpx.js', L.Control.Elevation._gpxLazyLoader);
            L.Control.Elevation._gpxLazyLoader.then(callback);
        } else {
            callback.call();
        }
    },

    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();
    },

    onAdd: function(map) {
        this._map = map;

        var 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);
        }

        var callback = 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_loaded', 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);
        if (typeof d3 !== 'object' && this.options.lazyLoadJS) {
            L.Control.Elevation._d3LazyLoader = this._lazyLoadJS('https://unpkg.com/d3@4.13.0/build/d3.min.js', L.Control.Elevation._d3LazyLoader);
            L.Control.Elevation._d3LazyLoader.then(callback);
        } else {
            callback.call();
        }
        return container;
    },

    onRemove: function(map) {
        this._container = null;
    },

    redraw: function() {
        this._resizeChart();
    },

    setZFollow: function(zoom) {
        this._zFollow = zoom;
    },

    show: function() {
        this._container.style.display = "block";
    },

    /*
     * Parsing data either from GPX or GeoJSON and update the diagram data
     */
    _addData: function(d) {
        var geom = d && d.geometry && d.geometry;
        var i;

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

                case 'MultiLineString':
                    for (i = 0; i < geom.coordinates.length; i++) {
                        this._addGeoJSONData(geom.coordinates[i]);
                    }
                    break;

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

        var feat = d && d.type === "FeatureCollection";
        if (feat) {
            for (i = 0; i < d.features.length; i++) {
                this._addData(d.features[i]);
            }
        }

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

    /*
     * Parsing of GeoJSON data lines and their elevation in z-coordinate
     */
    _addGeoJSONData: function(coords) {
        if (coords) {
            for (var i = 0; i < coords.length; i++) {
                this._addPoint(coords[i][1], coords[i][0], coords[i][2]);
            }
        }
    },

    /*
     * Parsing function for GPX data and their elevation in z-coordinate
     */
    _addGPXdata: function(coords) {
        if (coords) {
            for (var i = 0; i < coords.length; i++) {
                this._addPoint(coords[i].lat, coords[i].lng, coords[i].meta.ele);
            }
        }
    },

    _addPoint: function(x, y, z) {
        if (this.options.reverseCoords) {
            var tmp = x;
            x = y;
            y = tmp;
        }

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

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

        var 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) {
            var prevZ = data[data.length - 1].z;
            if (isNaN(prevZ)) {
                var lastZ = this._lastValidZ;
                var 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;
    },

    _addToChartDiv: function(map) {
        this._appendElevationDiv(map._container).appendChild(this.onAdd(map));
    },

    _appendChart: function(svg) {
        var 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);
    },

    _appendElevationDiv: function(container) {
        var 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;
    },

    _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);
    },

    _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("")
            );

    },

    _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", -5)
            .text(this._yLabel);
    },

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

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

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

    _appendFocusRect: function(g) {
        var 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);
    },

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

    _appendMouseFocusG: function(g) {
        var 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");
    },

    _appendLegend: function(g) {
        if (!this.options.legend) return;

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

        var 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('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);

    },

    _appendPositionMarker: function(pane) {
        var theme = this.options.theme;
        var 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");
    },

    _applyData: function() {
        if (!this._data) return;

        var xdomain = d3.extent(this._data, function(d) {
            return d.dist;
        });
        var ydomain = d3.extent(this._data, function(d) {
            return d.z;
        });
        var 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");
        }

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

        data.forEach(function(item) {
            ext.extend(item.latlng);
        });

        return ext;
    },

    _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);
        // }
    },

    _clearPath: function() {
        this._hidePositionMarker();
        for (var id in this._layers) {
            L.DomUtil.removeClass(this._layers[id]._path, "elevation-polyline");
            L.DomUtil.removeClass(this._layers[id]._path, this.options.theme);
        }
    },

    _collapse: function() {
        if (this._container) {
            L.DomUtil.removeClass(this._container, 'elevation-expanded');
            L.DomUtil.addClass(this._container, 'elevation-collapsed');
        }
    },

    _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);
    },

    _saveFile: function(fileUrl) {
        var 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);
    },

    _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();
            // autotoggle chart data on single click
            /*if (this._chartEnabled) {
                this._clearChart();
                this._clearPath();
                this._chartEnabled = false;
            } else {
                this._resizeChart();
                this._chartEnabled = true;
            }*/
            return;
        }

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

        if (item1 == item2) return;

        this._hidePositionMarker();

        this._fitSection(item1, item2);

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

        var evt = {
            data: {
                dragstart: this._data[item1],
                dragend: this._data[item2]
            }
        };
        if (this.fire) this.fire("elechart_dragged", evt, true);
        if (this._map) this._map.fire("elechart_dragged", evt, true);
    },

    _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;
        }

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

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

        if (!this._dragRectangle && !this._dragRectangleG) {
            var 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: 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) {
        var result = null,
            d = Infinity;
        this._data.forEach(function(item) {
            var 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) {
        var bisect = d3.bisector(function(d) {
            return d.dist;
        }).left;
        var xinvert = this._x.invert(x);
        return bisect(this._data, xinvert);
    },

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

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

    _height: function() {
        var 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");
        }
    },

    _initChart: function() {
        var 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) {
                var offWi = this.eleDiv.offsetWidth;
                var 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;
                var containerWidth = this._map._container.clientWidth;
                opts.width = opts._maxWidth > containerWidth ? containerWidth - 30 : opts.width;
            }
        }

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

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

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

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

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

        var 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) {
            var iconCssClass = "elevation-toggle " + this.options.controlButton.iconCssClass + (this.options.autohide ? "" : " close-button");
            var 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
            }
        } else {
            // TODO: handle autohide when detached=true
        }
    },

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

    _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;
        }
    },

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

    _isDomVisible: function(elem) {
        return !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
    },

    _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;
    },

    _lazyLoadJS: function(url, skip) {
        if (typeof skip == "undefined") {
            skip = false;
        }
        if (skip instanceof Promise) {
            return skip;
        }
        return new Promise(function(resolve, reject) {
            if (skip) return resolve();
            var tag = document.createElement("script");
            tag.addEventListener('load', resolve, { once: true });
            tag.src = url;
            document.head.appendChild(tag);
        });
    },

    _mouseenterHandler: function() {
        if (this.fire) {
            this.fire("elechart_enter", null, true);
        }
        if (this._map) {
            this._map.fire("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;
        }
        var coords = d3.mouse(this._focusRect.node());
        var xCoord = coords[0];
        var 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');
        }

        var evt = {
            data: item
        };
        if (this.fire) {
            this.fire("elechart_change", evt, true);
            this.fire("elechart_hover", evt, true);
        }
        if (this._map) {
            this._map.fire("elechart_change", evt, true);
            this._map.fire("elechart_hover", evt, true);
        }
    },

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

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

    _mouseoutHandler: function() {
        if (!this.options.detached) {
            this._hidePositionMarker();
        }

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

        if (this.fire) this.fire("elechart_leave", null, true);
        if (this._map) this._map.fire("elechart_leave", null, true);
    },

    _mousewheelHandler: function(e) {
        if (this._map.gestureHandling && this._map.gestureHandling._enabled) return;
        var ll = this._selectedItem ? this._selectedItem.latlng : this._map.getCenter();
        var 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();
        }
    },

    _resetView: function() {
        if (this._map && this._map._isFullscreen) return;
        this._resetDrag();
        this._hidePositionMarker();
        this.fitBounds(this._fullExtent);
    },

    _resizeChart: function() {
        if (this.options.responsive) {
            if (this.options.detached) {
                var 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);
            }
        }
    },

    _showDiagramIndicator: function(item, xCoordinate) {
        if (!this._chartEnabled) return;

        var 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);

        var 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);

        var focuslabeltext = this._focuslabeltext.node();
        if (this._isDomVisible(focuslabeltext)) {
            var bbox = focuslabeltext.getBBox();
            var 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);
            }
        }

    },

    _toggle: function() {
        if (L.DomUtil.hasClass(this._container, "elevation-expanded"))
            this._collapse();
        else
            this._expand();
    },

    _setMapView: function(item) {
        if (!this.options.followMarker || !this._map) return;
        var zoom = this._map.getZoom();
        zoom = zoom < this._zFollow ? this._zFollow : zoom;
        this._map.setView(item.latlng, zoom, { animate: true, duration: 0.25 });
    },

    _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);
        }
    },

    _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);
    },

    _updateHeightIndicator: function(item) {
        var opts = this.options;

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

        var normalizedAlt = this._height() / this._maxElevation * item.z,
            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");
    },

    _updateLeafletMarker: function(item) {
        var 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);
        }
    },

    _updatePointG: function(item) {
        this._pointG
            .attr("transform", "translate(" + item.x + "," + item.y + ")")
            .style("visibility", "visible");
    },

    _updatePositionMarker: function(item) {
        var point = this._map.latLngToLayerPoint(item.latlng);
        var 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
            var layerpane = d3.select(this._map.getContainer()).select(".leaflet-elevation-pane svg");
            this._appendPositionMarker(layerpane);
        }

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

    _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">Total Length: </span><span class="summaryvalue">' + this.track_info.distance.toFixed(2) + ' ' + this._xLabel + '</span></span><span class="maxele"><span class="summarylabel">Max Elevation: </span><span class="summaryvalue">' + this.track_info.elevation_max.toFixed(2) + ' ' + this._yLabel + '</span></span><span class="minele"><span class="summarylabel">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.
            var span = document.createElement('span');
            span.className = 'download';
            var save = document.createElement('a');
            save.innerHTML = "Download";
            save.href = "#";
            save.onclick = function(e) {
                e.preventDefault();
                var evt = { confirm: this._saveFile.bind(this, this._downloadURL) };
                var type = this.options.downloadLink;
                if (type == 'modal') {
                    if (typeof CustomEvent === "function") document.dispatchEvent(new CustomEvent("eletrack_download", { detail: evt }));
                    if (this.fire) this.fire('eletrack_download', evt);
                    if (this._map) this._map.fire('eletrack_download', evt);
                } else if (type == 'link' || type === true) {
                    evt.confirm();
                }
            }.bind(this);

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

    _width: function() {
        var opts = this.options;
        return opts.width - opts.margins.left - opts.margins.right;
    },

});

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