(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: '', 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: '', 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("" + name + "" + (desc.length > 0 ? '
' + 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('' + L._("Total Length: ") + '' + this.track_info.distance.toFixed(2) + ' ' + this._xLabel + '' + L._("Max Elevation: ") + '' + this.track_info.elevation_max.toFixed(2) + ' ' + this._yLabel + '' + L._("Min Elevation: ") + '' + this.track_info.elevation_min.toFixed(2) + ' ' + this._yLabel + ''); } 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); }; }))); //# sourceMappingURL=leaflet-elevation.js.map