Deltacms/core/vendor/geolocation/leaflet/leaflet-elevation.js

1823 lines
54 KiB
JavaScript

(function (factory) {
typeof define === 'function' && define.amd ? define(factory) :
factory();
}((function () { 'use strict';
// Following https://github.com/Leaflet/Leaflet/blob/master/PLUGIN-GUIDE.md
(function (factory, window) {
// define an AMD module that relies on 'leaflet'
if (typeof define === 'function' && define.amd) {
define(['leaflet'], factory);
// define a Common JS module that relies on 'leaflet'
} else if (typeof exports === 'object') {
module.exports = factory(require('leaflet'));
}
// attach your plugin to the global 'L' variable
if (typeof window !== 'undefined' && window.L) {
factory(window.L);
}
}(function (L) {
L.locales = {};
L.locale = null;
L.registerLocale = function registerLocale(code, locale) {
L.locales[code] = L.Util.extend({}, L.locales[code], locale);
};
L.setLocale = function setLocale(code) {
L.locale = code;
};
return L.i18n = L._ = function translate(string, data) {
if (L.locale && L.locales[L.locale] && L.locales[L.locale][string]) {
string = L.locales[L.locale][string];
}
try {
// Do not fail if some data is missing
// a bad translation should not break the app
string = L.Util.template(string, data);
}
catch (err) {/*pass*/
}
return string;
};
}, window));
/*
* Copyright (c) 2019, GPL-3.0+ Project, Raruto
*
* This file is free software: you may copy, redistribute and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 2 of the License, or (at your
* option) any later version.
*
* This file is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright (c) 2013-2016, MIT License, Felix “MrMufflon” Bache
*
* Permission to use, copy, modify, and/or distribute this software
* for any purpose with or without fee is hereby granted, provided
* that the above copyright notice and this permission notice appear
* in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
* WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
* CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
* NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
L.Control.Elevation = L.Control.extend({
includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
options: {
autohide: !L.Browser.mobile,
autohideMarker: true,
collapsed: false,
controlButton: {
iconCssClass: "elevation-toggle-icon",
title: "Elevation"
},
detached: true,
distanceFactor: 1,
dragging: !L.Browser.mobile,
downloadLink: 'link',
elevationDiv: "#elevation-div",
followMarker: true,
forceAxisBounds: false,
gpxOptions: {
async: true,
marker_options: {
startIconUrl: null,
endIconUrl: null,
shadowUrl: null,
wptIcons: {
'': L.divIcon({
className: 'elevation-waypoint-marker',
html: '<i class="elevation-waypoint-icon"></i>',
iconSize: [30, 30],
iconAnchor: [8, 30],
})
},
},
},
height: 200,
heightFactor: 1,
hoverNumber: {
decimalsX: 2,
decimalsY: 0,
formatter: undefined
},
imperial: false,
interpolation: "curveLinear",
lazyLoadJS: true,
legend: true,
loadData: {
defer: false,
lazy: false,
},
marker: 'elevation-line',
markerIcon: L.divIcon({
className: 'elevation-position-marker',
html: '<i class="elevation-position-icon"></i>',
iconSize: [32, 32],
iconAnchor: [16, 16],
}),
placeholder: false,
position: "topright",
polyline: {
className: 'elevation-polyline',
color: '#000',
opacity: 0.75,
weight: 5,
lineCap: 'round'
},
reverseCoords: false,
skipNullZCoords: false,
theme: "lightblue-theme",
margins: {
top: 10,
right: 20,
bottom: 30,
left: 50
},
responsive: true,
summary: 'inline',
width: 600,
xLabel: "km",
xTicks: undefined,
yAxisMax: undefined,
yAxisMin: undefined,
yLabel: "m",
yTicks: undefined,
zFollow: 13,
},
__mileFactor: 0.621371,
__footFactor: 3.28084,
/*
* Add data to the diagram either from GPX or GeoJSON and update the axis domain and data
*/
addData: function(d, layer) {
L.Control.Elevation._d3LazyLoader = this._lazyLoadJS(
'https://unpkg.com/d3@5.15.0/dist/d3.min.js',
typeof d3 !== 'object',
L.Control.Elevation._d3LazyLoader
).then(
function(d, layer) {
this._addData(d);
if (this._container) {
this._applyData();
}
if ((typeof layer === "undefined" || layer === null) && d.on) {
layer = d;
}
if (layer) {
if (layer._path) {
L.DomUtil.addClass(layer._path, this.options.polyline.className + ' ' + this.options.theme);
}
layer
.on("mousemove", this._mousemoveLayerHandler, this)
.on("mouseout", this._mouseoutHandler, this);
}
this.track_info = L.extend({}, this.track_info, {
distance: this._distance,
elevation_max: this._maxElevation,
elevation_min: this._minElevation
});
this._layers = this._layers || {};
this._layers[L.Util.stamp(layer)] = layer;
this._fireEvt("eledata_added", { data: d, layer: layer, track_info: this.track_info }, true);
}.bind(this, d, layer));
},
/**
* Adds the control to the given map.
*/
addTo: function(map) {
if (this.options.detached) {
this._appendElevationDiv(map._container).appendChild(this.onAdd(map));
} else {
L.Control.prototype.addTo.call(this, map);
}
return this;
},
/*
* Reset data and display
*/
clear: function() {
this._clearPath();
this._clearChart();
this._clearData();
this._fireEvt("eledata_clear");
},
/**
* Disable dragging chart on touch events.
*/
disableDragging: function() {
this._draggingEnabled = false;
this._resetDrag();
},
/**
* Enable dragging chart on touch events.
*/
enableDragging: function() {
this._draggingEnabled = true;
},
/**
* Sets a map view that contains the given geographical bounds.
*/
fitBounds: function(bounds) {
bounds = bounds || this._fullExtent;
if (this._map && bounds) this._map.fitBounds(bounds);
},
/**
* Get default zoom level when "followMarker" is true.
*/
getZFollow: function() {
return this._zFollow;
},
/**
* Hide current elevation chart profile.
*/
hide: function() {
this._container.style.display = "none";
},
/**
* Initialize chart control "options" and "container".
*/
initialize: function(options) {
this.options = this._deepMerge({}, this.options, options);
if (this.options.imperial) {
this._distanceFactor = this.__mileFactor;
this._heightFactor = this.__footFactor;
this._xLabel = "mi";
this._yLabel = "ft";
} else {
this._distanceFactor = this.options.distanceFactor;
this._heightFactor = this.options.heightFactor;
this._xLabel = this.options.xLabel;
this._yLabel = this.options.yLabel;
}
this._chartEnabled = true;
this._draggingEnabled = this.options.dragging;
this._zFollow = this.options.zFollow;
if (this.options.followMarker) this._setMapView = L.Util.throttle(this._setMapView, 300, this);
if (this.options.placeholder) this.options.loadData.lazy = this.options.loadData.defer = true;
this.on('waypoint_added', function(e) {
if (e.point._popup) {
e.point._popup.options.className = 'elevation-popup';
e.point._popup._content = decodeURI(e.point._popup._content);
}
if (e.point._popup && e.point._popup._content) {
e.point.bindTooltip(e.point._popup._content, { direction: 'top', sticky: true, opacity: 1, className: 'elevation-tooltip' }).openTooltip();
}
});
},
/**
* Alias for loadData
*/
load: function(data, opts) {
this.loadData(data, opts);
},
/**
* Alias for addTo
*/
loadChart: function(map) {
this.addTo(map);
},
/**
* Load elevation data (GPX or GeoJSON).
*/
loadData: function(data, opts) {
opts = L.extend({}, this.options.loadData, opts);
if (opts.defer) {
this.loadDefer(data, opts);
} else if (opts.lazy) {
this.loadLazy(data, opts);
} else if (this._isXMLDoc(data)) {
this.loadGPX(data);
} else if (this._isJSONDoc(data)) {
this.loadGeoJSON(data);
} else {
this.loadFile(data);
}
},
/**
* Wait for document load before download data.
*/
loadDefer: function(data, opts) {
opts = L.extend({}, this.options.loadData, opts);
opts.defer = false;
if (document.readyState !== 'complete') window.addEventListener("load", L.bind(this.loadData, this, data, opts), { once: true });
else this.loadData(data, opts);
},
/**
* Load data from a remote url.
*/
loadFile: function(url) {
this._downloadURL = url; // TODO: handle multiple urls?
try {
let xhr = new XMLHttpRequest();
xhr.responseType = "text";
xhr.open('GET', url);
xhr.onload = function() {
if (xhr.status !== 200) {
throw "Error " + xhr.status + " while fetching remote file: " + url;
} else {
this.loadData(xhr.response, { lazy: false, defer: false });
}
}.bind(this);
xhr.send();
} catch (e) {
console.warn(e);
}
},
/**
* Load raw GeoJSON data.
*/
loadGeoJSON: function(data) {
if (typeof data === "string") {
data = JSON.parse(data);
}
this.layer = this.geojson = L.geoJson(data, {
style: function(feature) {
let style = L.extend({}, this.options.polyline);
if (this.options.theme) {
style.className += ' ' + this.options.theme;
}
return style;
}.bind(this),
pointToLayer: function(feature, latlng) {
let marker = L.marker(latlng, { icon: this.options.gpxOptions.marker_options.wptIcons[''] });
let desc = feature.properties.desc ? feature.properties.desc : '';
let name = feature.properties.name ? feature.properties.name : '';
if (name || desc) {
marker.bindPopup("<b>" + name + "</b>" + (desc.length > 0 ? '<br>' + desc : '')).openPopup();
}
this.fire('waypoint_added', { point: marker, point_type: 'waypoint', element: latlng });
return marker;
}.bind(this),
onEachFeature: function(feature, layer) {
if (feature.geometry.type == 'Point') return;
this.addData(feature, layer);
this.track_info = L.extend({}, this.track_info, { type: "geojson", name: data.name });
}.bind(this),
});
if (this._map) {
this._map.once('layeradd', function(e) {
this.fitBounds(this.layer.getBounds());
this._fireEvt("eledata_loaded", { data: data, layer: this.layer, name: this.track_info.name, track_info: this.track_info }, true);
}, this);
this.layer.addTo(this._map);
} else {
console.warn("Undefined elevation map object");
}
},
/**
* Load raw GPX data.
*/
loadGPX: function(data) {
L.Control.Elevation._gpxLazyLoader = this._lazyLoadJS(
'https://unpkg.com/leaflet-gpx@1.5.0/gpx.js',
typeof L.GPX !== 'function',
L.Control.Elevation._gpxLazyLoader
).then(
function(data) {
this.options.gpxOptions.polyline_options = L.extend({}, this.options.polyline, this.options.gpxOptions.polyline_options);
if (this.options.theme) {
this.options.gpxOptions.polyline_options.className += ' ' + this.options.theme;
}
this.layer = this.gpx = new L.GPX(data, this.options.gpxOptions);
this.layer.on('loaded', function(e) { this.fitBounds(e.target.getBounds()); }, this);
this.layer.on('addpoint', function(e) { this.fire("waypoint_added", e, true); }, this);
this.layer.once("addline", function(e) {
this.addData(e.line /*, this.layer*/ );
this.track_info = L.extend({}, this.track_info, { type: "gpx", name: this.layer.get_name() });
this._fireEvt("eledata_loaded", { data: data, layer: this.layer, name: this.track_info.name, track_info: this.track_info }, true);
}, this);
if (this._map) {
this.layer.addTo(this._map);
} else {
console.warn("Undefined elevation map object");
}
}.bind(this, data)
);
},
/**
* Wait for chart container visible before download data.
*/
loadLazy: function(data, opts) {
opts = L.extend({}, this.options.loadData, opts);
opts.lazy = false;
let ticking = false;
let scrollFn = L.bind(function(data) {
if (!ticking) {
L.Util.requestAnimFrame(function() {
if (this._isVisible(this.placeholder)) {
window.removeEventListener('scroll', scrollFn);
this.loadData(data, opts);
this.once('eledata_loaded', function() {
if (this.placeholder && this.placeholder.parentNode) {
this.placeholder.parentNode.removeChild(this.placeholder);
}
}, this);
}
ticking = false;
}, this);
ticking = true;
}
}, this, data);
window.addEventListener('scroll', scrollFn);
if (this.placeholder) this.placeholder.addEventListener('mouseenter', scrollFn, { once: true });
scrollFn();
},
/**
* Create container DOM element and related event listeners.
* Called on control.addTo(map).
*/
onAdd: function(map) {
this._map = map;
let container = this._container = L.DomUtil.create("div", "elevation-control elevation");
if (!this.options.detached) {
L.DomUtil.addClass(container, 'leaflet-control');
}
if (this.options.theme) {
L.DomUtil.addClass(container, this.options.theme); // append theme to control
}
if (this.options.placeholder && !this._data) {
this.placeholder = L.DomUtil.create('img', 'elevation-placeholder');
if (typeof this.options.placeholder === 'string') {
this.placeholder.src = this.options.placeholder;
this.placeholder.alt = '';
} else {
for (let i in this.options.placeholder) { this.placeholder.setAttribute(i, this.options.placeholder[i]); }
}
container.insertBefore(this.placeholder, container.firstChild);
}
L.Control.Elevation._d3LazyLoader = this._lazyLoadJS(
'https://unpkg.com/d3@5.15.0/dist/d3.min.js',
typeof d3 !== 'object',
L.Control.Elevation._d3LazyLoader
).then(
function(map, container) {
this._initToggle(container);
this._initChart(container);
this._applyData();
this._map.on('zoom viewreset zoomanim', this._hidePositionMarker, this);
this._map.on('resize', this._resetView, this);
this._map.on('resize', this._resizeChart, this);
this._map.on('mousedown', this._resetDrag, this);
this._map.on('eledata_added', this._updateSummary, this);
L.DomEvent.on(this._map._container, 'mousewheel', this._resetDrag, this);
L.DomEvent.on(this._map._container, 'touchstart', this._resetDrag, this);
}.bind(this, map, container)
);
return container;
},
/**
* Clean up control code and related event listeners.
* Called on control.remove().
*/
onRemove: function(map) {
this._container = null;
},
/**
* Redraws the chart control. Sometimes useful after screen resize.
*/
redraw: function() {
this._resizeChart();
},
/**
* Set default zoom level when "followMarker" is true.
*/
setZFollow: function(zoom) {
this._zFollow = zoom;
},
/**
* Hide current elevation chart profile.
*/
show: function() {
this._container.style.display = "block";
},
/*
* Parsing data either from GPX or GeoJSON and update the diagram data
*/
_addData: function(d) {
let geom = d && d.geometry;
let feat = d && d.type === "FeatureCollection";
let gpx = d && d._latlngs;
if (geom) {
switch (geom.type) {
case 'LineString':
this._addGeoJSONData(geom.coordinates);
break;
case 'MultiLineString':
geom.coordinates.forEach(coords => this._addGeoJSONData(coords));
break;
default:
console.warn('Unsopperted GeoJSON feature geometry type:' + geom.type);
}
}
if (feat) {
d.features.forEach(feature => this._addData(feature));
}
if (gpx) {
this._addGPXdata(d._latlngs);
}
},
/*
* Parsing of GeoJSON data lines and their elevation in z-coordinate
*/
_addGeoJSONData: function(coords) {
if (coords) {
coords.forEach(point => this._addPoint(point[1], point[0], point[2]));
}
},
/*
* Parsing function for GPX data and their elevation in z-coordinate
*/
_addGPXdata: function(coords) {
if (coords) {
coords.forEach(point => this._addPoint(point.lat, point.lng, point.meta.ele));
}
},
/*
* Parse and push a single (x, y, z) point to current elevation profile.
*/
_addPoint: function(x, y, z) {
if (this.options.reverseCoords) {
[x, y] = [y, x];
}
let data = this._data || [];
let eleMax = this._maxElevation || -Infinity;
let eleMin = this._minElevation || +Infinity;
let dist = this._distance || 0;
let curr = new L.LatLng(x, y);
let prev = data.length ? data[data.length - 1].latlng : curr;
let delta = curr.distanceTo(prev) * this._distanceFactor;
dist = dist + Math.round(delta / 1000 * 100000) / 100000;
// check and fix missing elevation data on last added point
if (!this.options.skipNullZCoords && data.length > 0) {
let prevZ = data[data.length - 1].z;
if (isNaN(prevZ)) {
let lastZ = this._lastValidZ;
let currZ = z * this._heightFactor;
if (!isNaN(lastZ) && !isNaN(currZ)) {
prevZ = (lastZ + currZ) / 2;
} else if (!isNaN(lastZ)) {
prevZ = lastZ;
} else if (!isNaN(currZ)) {
prevZ = currZ;
}
if (!isNaN(prevZ)) data[data.length - 1].z = prevZ;
else data.splice(data.length - 1, 1);
}
}
z = z * this._heightFactor;
// skip point if it has not elevation
if (!isNaN(z)) {
eleMax = eleMax < z ? z : eleMax;
eleMin = eleMin > z ? z : eleMin;
this._lastValidZ = z;
}
data.push({
dist: dist,
x: x,
y: y,
z: z,
latlng: curr
});
this._data = data;
this._distance = dist;
this._maxElevation = eleMax;
this._minElevation = eleMin;
},
/**
* Generate "svg" chart container.
*/
_appendChart: function(svg) {
let g = svg
.append("g")
.attr("transform", "translate(" + this.options.margins.left + "," + this.options.margins.top + ")");
this._appendGrid(g);
this._appendAreaPath(g);
this._appendAxis(g);
this._appendFocusRect(g);
this._appendMouseFocusG(g);
this._appendLegend(g);
},
/**
* Adds the control to the given "detached" div.
*/
_appendElevationDiv: function(container) {
let eleDiv = document.querySelector(this.options.elevationDiv);
if (!eleDiv) {
eleDiv = L.DomUtil.create('div', 'leaflet-control elevation elevation-div');
this.options.elevationDiv = '#elevation-div_' + Math.random().toString(36).substr(2, 9);
eleDiv.id = this.options.elevationDiv.substr(1);
container.parentNode.insertBefore(eleDiv, container.nextSibling); // insert after end of container.
}
if (this.options.detached) {
L.DomUtil.addClass(eleDiv, 'elevation-detached');
L.DomUtil.removeClass(eleDiv, 'leaflet-control');
}
this.eleDiv = eleDiv;
return this.eleDiv;
},
/**
* Generate "x-axis".
*/
_appendXaxis: function(axis) {
axis
.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + this._height() + ")")
.call(
d3
.axisBottom()
.scale(this._x)
.ticks(this.options.xTicks)
)
.append("text")
.attr("x", this._width() + 6)
.attr("y", 30)
.text(this._xLabel);
},
/**
* Generate "x-grid".
*/
_appendXGrid: function(grid) {
grid.append("g")
.attr("class", "x grid")
.attr("transform", "translate(0," + this._height() + ")")
.call(
d3
.axisBottom()
.scale(this._x)
.ticks(this.options.xTicks)
.tickSize(-this._height())
.tickFormat("")
);
},
/**
* Generate "y-axis".
*/
_appendYaxis: function(axis) {
axis
.append("g")
.attr("class", "y axis")
.call(
d3
.axisLeft()
.scale(this._y)
.ticks(this.options.yTicks)
)
.append("text")
.attr("x", -30)
.attr("y", 3)
.text(this._yLabel);
},
/**
* Generate "y-grid".
*/
_appendYGrid: function(grid) {
grid.append("g")
.attr("class", "y grid")
.call(
d3
.axisLeft()
.scale(this._y)
.ticks(this.options.yTicks)
.tickSize(-this._width())
.tickFormat("")
);
},
/**
* Generate "path".
*/
_appendAreaPath: function(g) {
this._areapath = g.append("path")
.attr("class", "area");
},
/**
* Generate "axis".
*/
_appendAxis: function(g) {
this._axis = g.append("g")
.attr("class", "axis");
this._appendXaxis(this._axis);
this._appendYaxis(this._axis);
},
/**
* Generate "mouse-focus" and "drag-rect".
*/
_appendFocusRect: function(g) {
let focusRect = this._focusRect = g.append("rect")
.attr("width", this._width())
.attr("height", this._height())
.style("fill", "none")
.style("stroke", "none")
.style("pointer-events", "all");
if (L.Browser.mobile) {
focusRect
.on("touchmove.drag", this._dragHandler.bind(this))
.on("touchstart.drag", this._dragStartHandler.bind(this))
.on("touchstart.focus", this._mousemoveHandler.bind(this))
.on("touchmove.focus", this._mousemoveHandler.bind(this));
L.DomEvent.on(this._container, 'touchend', this._dragEndHandler, this);
}
focusRect
.on("mousemove.drag", this._dragHandler.bind(this))
.on("mousedown.drag", this._dragStartHandler.bind(this))
.on("mouseenter.focus", this._mouseenterHandler.bind(this))
.on("mousemove.focus", this._mousemoveHandler.bind(this))
.on("mouseout.focus", this._mouseoutHandler.bind(this));
L.DomEvent.on(this._container, 'mouseup', this._dragEndHandler, this);
},
/**
* Generate "grid".
*/
_appendGrid: function(g) {
this._grid = g.append("g")
.attr("class", "grid");
this._appendXGrid(this._grid);
this._appendYGrid(this._grid);
},
/**
* Generate "mouse-focus".
*/
_appendMouseFocusG: function(g) {
let focusG = this._focusG = g.append("g")
.attr("class", "mouse-focus-group");
this._mousefocus = focusG.append('svg:line')
.attr('class', 'mouse-focus-line')
.attr('x2', '0')
.attr('y2', '0')
.attr('x1', '0')
.attr('y1', '0');
this._focuslabelrect = focusG.append("rect")
.attr('class', 'mouse-focus-label')
.attr("x", 0)
.attr("y", 0)
.attr("width", 0)
.attr("height", 0)
.attr("rx", 3)
.attr("ry", 3);
this._focuslabeltext = focusG.append("svg:text")
.attr("class", "mouse-focus-label-text");
this._focuslabelY = this._focuslabeltext.append("svg:tspan")
.attr("class", "mouse-focus-label-y")
.attr("dy", "-1em");
this._focuslabelX = this._focuslabeltext.append("svg:tspan")
.attr("class", "mouse-focus-label-x")
.attr("dy", "2em");
},
/**
* Generate "legend".
*/
_appendLegend: function(g) {
if (!this.options.legend) return;
let legend = this._legend = g.append('g')
.attr("class", "legend");
let altitude = this._altitudeLegend = this._legend.append('g')
.attr("class", "legend-altitude");
altitude.append("rect")
.attr("class", "area")
.attr("x", (this._width() / 2) - 50)
.attr("y", this._height() + this.options.margins.bottom - 17)
.attr("width", 50)
.attr("height", 5)
.attr("opacity", 0.75);
altitude.append('text')
.text(L._('Altitude'))
.attr("x", (this._width() / 2) + 5)
.attr("font-size", 10)
.style("text-decoration-thickness", "2px")
.style("font-weight", "700")
.attr('y', this._height() + this.options.margins.bottom - 11);
// autotoggle chart data on single click
legend.on('click', function() {
if (this._chartEnabled) {
this._clearChart();
this._clearPath();
this._chartEnabled = false;
} else {
this._resizeChart();
for (let id in this._layers) {
L.DomUtil.addClass(this._layers[id]._path, this.options.polyline.className + ' ' + this.options.theme);
}
this._chartEnabled = true;
}
}.bind(this));
},
/**
* Generate "svg:line".
*/
_appendPositionMarker: function(pane) {
let theme = this.options.theme;
let heightG = pane.select("g");
this._mouseHeightFocus = heightG.append('svg:line')
.attr("class", theme + " height-focus line")
.attr("x2", 0)
.attr("y2", 0)
.attr("x1", 0)
.attr("y1", 0);
this._pointG = heightG.append("g");
this._pointG.append("svg:circle")
.attr("class", theme + " height-focus circle-lower")
.attr("r", 6)
.attr("cx", 0)
.attr("cy", 0);
this._mouseHeightFocusLabel = heightG.append("svg:text")
.attr("class", theme + " height-focus-label")
.style("pointer-events", "none");
},
/**
* Calculates [x, y] domain and then update chart.
*/
_applyData: function() {
if (!this._data) return;
let xdomain = d3.extent(this._data, d => d.dist);
let ydomain = d3.extent(this._data, d => d.z);
let opts = this.options;
if (opts.yAxisMin !== undefined && (opts.yAxisMin < ydomain[0] || opts.forceAxisBounds)) {
ydomain[0] = opts.yAxisMin;
}
if (opts.yAxisMax !== undefined && (opts.yAxisMax > ydomain[1] || opts.forceAxisBounds)) {
ydomain[1] = opts.yAxisMax;
}
this._x.domain(xdomain);
this._y.domain(ydomain);
this._areapath.datum(this._data)
.attr("d", this._area);
this._updateAxis();
this._fullExtent = this._calculateFullExtent(this._data);
},
/*
* Calculates the full extent of the data array
*/
_calculateFullExtent: function(data) {
if (!data || data.length < 1) {
throw new Error("no data in parameters");
}
let ext = new L.latLngBounds(data[0].latlng, data[0].latlng);
data.forEach(item => ext.extend(item.latlng));
return ext;
},
/*
* Reset chart.
*/
_clearChart: function() {
this._resetDrag();
if (this._areapath) {
// workaround for 'Error: Problem parsing d=""' in Webkit when empty data
// https://groups.google.com/d/msg/d3-js/7rFxpXKXFhI/HzIO_NPeDuMJ
//this._areapath.datum(this._data).attr("d", this._area);
this._areapath.attr("d", "M0 0");
this._x.domain([0, 1]);
this._y.domain([0, 1]);
this._updateAxis();
}
if (this._altitudeLegend) {
this._altitudeLegend.select('text').style("text-decoration-line", "line-through");
}
},
/*
* Reset data.
*/
_clearData: function() {
this._data = null;
this._distance = null;
this._maxElevation = null;
this._minElevation = null;
this.track_info = null;
this._layers = null;
// if (this.layer) {
// this.layer.removeFrom(this._map);
// }
},
/*
* Reset path.
*/
_clearPath: function() {
this._hidePositionMarker();
for (let id in this._layers) {
L.DomUtil.removeClass(this._layers[id]._path, this.options.polyline.className);
L.DomUtil.removeClass(this._layers[id]._path, this.options.theme);
}
},
/*
* Collapse current chart control.
*/
_collapse: function() {
if (this._container) {
L.DomUtil.removeClass(this._container, 'elevation-expanded');
L.DomUtil.addClass(this._container, 'elevation-collapsed');
}
},
/**
* Recursive deep merge objects.
* Alternative to L.Util.setOptions(this, options).
*/
_deepMerge: function(target, ...sources) {
if (!sources.length) return target;
const source = sources.shift();
if (this._isObject(target) && this._isObject(source)) {
for (const key in source) {
if (this._isObject(source[key])) {
if (!target[key]) Object.assign(target, {
[key]: {}
});
this._deepMerge(target[key], source[key]);
} else {
Object.assign(target, {
[key]: source[key]
});
}
}
}
return this._deepMerge(target, ...sources);
},
/*
* Handle drag operations.
*/
_dragHandler: function() {
//we don't want map events to occur here
d3.event.preventDefault();
d3.event.stopPropagation();
this._gotDragged = true;
this._drawDragRectangle();
},
/*
* Handles end of drag operations. Zooms the map to the selected items extent.
*/
_dragEndHandler: function() {
if (!this._dragStartCoords || !this._dragCurrentCoords || !this._gotDragged) {
this._dragStartCoords = null;
this._gotDragged = false;
if (this._draggingEnabled) this._resetDrag();
return;
}
let item1 = this._findItemForX(this._dragStartCoords[0]);
let item2 = this._findItemForX(this._dragCurrentCoords[0]);
if (item1 == item2) return;
this._hidePositionMarker();
this._fitSection(item1, item2);
this._dragStartCoords = null;
this._gotDragged = false;
this._fireEvt("elechart_dragged", { data: { dragstart: this._data[item1], dragend: this._data[item2] } }, true);
},
/*
* Handles start of drag operations.
*/
_dragStartHandler: function() {
d3.event.preventDefault();
d3.event.stopPropagation();
this._gotDragged = false;
this._dragStartCoords = d3.mouse(this._focusRect.node());
},
/*
* Draws the currently dragged rectangle over the chart.
*/
_drawDragRectangle: function() {
if (!this._dragStartCoords || !this._draggingEnabled) {
return;
}
let dragEndCoords = this._dragCurrentCoords = d3.mouse(this._focusRect.node());
let x1 = Math.min(this._dragStartCoords[0], dragEndCoords[0]);
let x2 = Math.max(this._dragStartCoords[0], dragEndCoords[0]);
if (!this._dragRectangle && !this._dragRectangleG) {
let g = d3.select(this._container).select("svg").select("g");
this._dragRectangleG = g.insert("g", ".mouse-focus-group");
this._dragRectangle = this._dragRectangleG.append("rect")
.attr("width", x2 - x1)
.attr("height", this._height())
.attr("x", x1)
.attr('class', 'mouse-drag')
.style("pointer-events", "none");
} else {
this._dragRectangle.attr("width", x2 - x1)
.attr("x", x1);
}
},
/*
* Expand current chart control.
*/
_expand: function() {
if (this._container) {
L.DomUtil.removeClass(this._container, 'elevation-collapsed');
L.DomUtil.addClass(this._container, 'elevation-expanded');
}
},
/*
* Finds an item with the smallest delta in distance to the given latlng coords
*/
_findItemForLatLng: function(latlng) {
let result = null;
let d = Infinity;
this._data.forEach(item => {
let dist = latlng.distanceTo(item.latlng);
if (dist < d) {
d = dist;
result = item;
}
});
return result;
},
/*
* Finds a data entry for a given x-coordinate of the diagram
*/
_findItemForX: function(x) {
let data = this._data ? this._data : [0, 1];
let bisect = d3.bisector(d => d.dist).left;
let xinvert = this._x.invert(x);
return bisect(data, xinvert);
},
/**
* Fires an event of the specified type.
*/
_fireEvt: function(type, data, propagate) {
if (this.fire) {
this.fire(type, data, propagate);
}
if (this._map) {
this._map.fire(type, data, propagate);
}
},
/**
* Make the map fit the route section between given indexes.
*/
_fitSection: function(index1, index2) {
let start = Math.min(index1, index2);
let end = Math.max(index1, index2);
let ext = this._calculateFullExtent(this._data.slice(start, end));
this.fitBounds(ext);
},
/*
* Fromatting funciton using the given decimals and seperator
*/
_formatter: function(num, dec, sep) {
let res = L.Util.formatNum(num, dec).toString();
let numbers = res.split(".");
if (numbers[1]) {
for (let d = dec - numbers[1].length; d > 0; d--) {
numbers[1] += "0";
}
res = numbers.join(sep || ".");
}
return res;
},
/**
* Calculates chart height.
*/
_height: function() {
let opts = this.options;
return opts.height - opts.margins.top - opts.margins.bottom;
},
/*
* Hides the position/height indicator marker drawn onto the map
*/
_hidePositionMarker: function() {
if (!this.options.autohideMarker) {
return;
}
this._selectedItem = null;
if (this._marker) {
if (this._map) this._map.removeLayer(this._marker);
this._marker = null;
}
if (this._mouseHeightFocus) {
this._mouseHeightFocus.style("visibility", "hidden");
this._mouseHeightFocusLabel.style("visibility", "hidden");
}
if (this._pointG) {
this._pointG.style("visibility", "hidden");
}
if (this._focusG) {
this._focusG.style("visibility", "hidden");
}
},
/**
* Generate "svg" chart DOM element.
*/
_initChart: function() {
let opts = this.options;
opts.xTicks = opts.xTicks || Math.round(this._width() / 75);
opts.yTicks = opts.yTicks || Math.round(this._height() / 30);
opts.hoverNumber.formatter = opts.hoverNumber.formatter || this._formatter;
if (opts.responsive) {
if (opts.detached) {
let offWi = this.eleDiv.offsetWidth;
let offHe = this.eleDiv.offsetHeight;
opts.width = offWi > 0 ? offWi : opts.width;
opts.height = (offHe - 20) > 0 ? offHe - 20 : opts.height; // 20 = horizontal scrollbar size.
} else {
opts._maxWidth = opts._maxWidth > opts.width ? opts._maxWidth : opts.width;
let containerWidth = this._map._container.clientWidth;
opts.width = opts._maxWidth > containerWidth ? containerWidth - 30 : opts.width;
}
}
let x = this._x = d3.scaleLinear().range([0, this._width()]);
let y = this._y = d3.scaleLinear().range([this._height(), 0]);
let interpolation = typeof opts.interpolation === 'function' ? opts.interpolation : d3[opts.interpolation];
let area = this._area = d3.area().curve(interpolation)
.x(d => (d.xDiagCoord = x(d.dist)))
.y0(this._height())
.y1(d => y(d.z));
let line = this._line = d3.line()
.x(d => d3.mouse(svg.select("g"))[0])
.y(d => this._height());
let container = d3.select(this._container);
let svg = container.append("svg")
.attr("class", "background")
.attr("width", opts.width)
.attr("height", opts.height);
let summary = this.summaryDiv = container.append("div")
.attr("class", "elevation-summary " + this.options.summary + "-summary").node();
this._appendChart(svg);
this._updateSummary();
},
/**
* Inspired by L.Control.Layers
*/
_initToggle: function(container) {
//Makes this work on IE10 Touch devices by stopping it from firing a mouseout event when the touch is released
container.setAttribute('aria-haspopup', true);
if (!this.options.detached) {
L.DomEvent
.disableClickPropagation(container);
//.disableScrollPropagation(container);
}
if (L.Browser.mobile) {
L.DomEvent.on(container, 'click', L.DomEvent.stopPropagation);
}
L.DomEvent.on(container, 'mousewheel', this._mousewheelHandler, this);
if (!this.options.detached) {
let iconCssClass = "elevation-toggle " + this.options.controlButton.iconCssClass + (this.options.autohide ? "" : " close-button");
let link = this._button = L.DomUtil.create('a', iconCssClass, container);
link.href = '#';
link.title = this.options.controlButton.title;
if (this.options.collapsed) {
this._collapse();
if (this.options.autohide) {
L.DomEvent
.on(container, 'mouseover', this._expand, this)
.on(container, 'mouseout', this._collapse, this);
} else {
L.DomEvent
.on(link, 'click', L.DomEvent.stop)
.on(link, 'click', this._toggle, this);
}
L.DomEvent.on(link, 'focus', this._toggle, this);
this._map.on('click', this._collapse, this);
// TODO: keyboard accessibility
}
}
},
/**
* Check object type.
*/
_isObject: function(item) {
return (item && typeof item === 'object' && !Array.isArray(item));
},
/**
* Check JSON object type.
*/
_isJSONDoc: function(doc, lazy) {
lazy = typeof lazy === "undefined" ? true : lazy;
if (typeof doc === "string" && lazy) {
doc = doc.trim();
return doc.indexOf("{") == 0 || doc.indexOf("[") == 0;
} else {
try {
JSON.parse(doc.toString());
} catch (e) {
if (typeof doc === "object" && lazy) return true;
console.warn(e);
return false;
}
return true;
}
},
/**
* Check XML object type.
*/
_isXMLDoc: function(doc, lazy) {
lazy = typeof lazy === "undefined" ? true : lazy;
if (typeof doc === "string" && lazy) {
doc = doc.trim();
return doc.indexOf("<") == 0;
} else {
let documentElement = (doc ? doc.ownerDocument || doc : 0).documentElement;
return documentElement ? documentElement.nodeName !== "HTML" : false;
}
},
/**
* Check DOM element visibility.
*/
_isDomVisible: function(elem) {
return !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
},
/**
* Check DOM element viewport visibility.
*/
_isVisible: function(elem) {
if (!elem) return false;
let styles = window.getComputedStyle(elem);
function isVisibleByStyles(elem, styles) {
return styles.visibility !== 'hidden' && styles.display !== 'none';
}
function isAboveOtherElements(elem, styles) {
let boundingRect = elem.getBoundingClientRect();
let left = boundingRect.left + 1;
let right = boundingRect.right - 1;
let top = boundingRect.top + 1;
let bottom = boundingRect.bottom - 1;
let above = true;
let pointerEvents = elem.style.pointerEvents;
if (styles['pointer-events'] == 'none') elem.style.pointerEvents = 'auto';
if (document.elementFromPoint(left, top) !== elem) above = false;
if (document.elementFromPoint(right, top) !== elem) above = false;
// Only for completely visible elements
// if (document.elementFromPoint(left, bottom) !== elem) above = false;
// if (document.elementFromPoint(right, bottom) !== elem) above = false;
elem.style.pointerEvents = pointerEvents;
return above;
}
if (!isVisibleByStyles(elem, styles)) return false;
if (!isAboveOtherElements(elem, styles)) return false;
return true;
},
/**
* Async JS script download.
*/
_lazyLoadJS: function(url, skip, loader) {
if (skip === false || !this.options.lazyLoadJS) {
return Promise.resolve();
}
if (loader instanceof Promise) {
return loader;
}
return new Promise((resolve, reject) => {
let tag = document.createElement("script");
tag.addEventListener('load', resolve, { once: true });
tag.src = url;
document.head.appendChild(tag);
});
},
/*
* Handles the moueseenter over the chart.
*/
_mouseenterHandler: function() {
this._fireEvt("elechart_enter", null, true);
},
/*
* Handles the moueseover the chart and displays distance and altitude level.
*/
_mousemoveHandler: function(d, i, ctx) {
if (!this._data || this._data.length === 0 || !this._chartEnabled) {
return;
}
let coords = d3.mouse(this._focusRect.node());
let xCoord = coords[0];
let item = this._data[this._findItemForX(xCoord)];
this._hidePositionMarker();
this._showDiagramIndicator(item, xCoord);
this._showPositionMarker(item);
this._setMapView(item);
if (this._map && this._map._container) {
L.DomUtil.addClass(this._map._container, 'elechart-hover');
}
this._fireEvt("elechart_change", { data: item }, true);
this._fireEvt("elechart_hover", { data: item }, true);
},
/*
* Handles mouseover events of the data layers on the map.
*/
_mousemoveLayerHandler: function(e) {
if (!this._data || this._data.length === 0) {
return;
}
let latlng = e.latlng;
let item = this._findItemForLatLng(latlng);
if (item) {
let xCoord = item.xDiagCoord;
this._hidePositionMarker();
this._showDiagramIndicator(item, xCoord);
this._showPositionMarker(item);
}
},
/*
* Handles the moueseout over the chart.
*/
_mouseoutHandler: function() {
if (!this.options.detached) {
this._hidePositionMarker();
}
if (this._map && this._map._container) {
L.DomUtil.removeClass(this._map._container, 'elechart-hover');
}
this._fireEvt("elechart_leave", null, true);
},
/*
* Handles the mouesewheel over the chart.
*/
_mousewheelHandler: function(e) {
if (this._map.gestureHandling && this._map.gestureHandling._enabled) return;
let ll = this._selectedItem ? this._selectedItem.latlng : this._map.getCenter();
let z = e.deltaY > 0 ? this._map.getZoom() - 1 : this._map.getZoom() + 1;
this._resetDrag();
this._map.flyTo(ll, z);
},
/*
* Removes the drag rectangle and zoms back to the total extent of the data.
*/
_resetDrag: function() {
if (this._dragRectangleG) {
this._dragRectangleG.remove();
this._dragRectangleG = null;
this._dragRectangle = null;
this._hidePositionMarker();
}
},
/**
* Resets drag, marker and bounds.
*/
_resetView: function() {
if (this._map && this._map._isFullscreen) return;
this._resetDrag();
this._hidePositionMarker();
this.fitBounds(this._fullExtent);
},
/**
* Hacky way for handling chart resize. Deletes it and redraw chart.
*/
_resizeChart: function() {
if (this.options.responsive) {
if (this.options.detached) {
let newWidth = this.eleDiv.offsetWidth; // - 20;
if (newWidth <= 0) return;
this.options.width = newWidth;
this.eleDiv.innerHTML = "";
this.eleDiv.appendChild(this.onAdd(this._map));
} else {
this._map.removeControl(this._container);
this.addTo(this._map);
}
}
},
/**
* Generate GPX / GeoJSON download event.
*/
_saveFile: function(fileUrl) {
let d = document,
a = d.createElement('a'),
b = d.body;
a.href = fileUrl;
a.target = '_new';
a.download = ""; // fileName
a.style.display = 'none';
b.appendChild(a);
a.click();
b.removeChild(a);
},
/**
* Display distance and altitude level ("focus-rect").
*/
_showDiagramIndicator: function(item, xCoordinate) {
if (!this._chartEnabled) return;
let opts = this.options;
this._focusG.style("visibility", "visible");
this._mousefocus.attr('x1', xCoordinate)
.attr('y1', 0)
.attr('x2', xCoordinate)
.attr('y2', this._height())
.classed('hidden', false);
let alt = item.z,
dist = item.dist,
ll = item.latlng,
numY = opts.hoverNumber.formatter(alt, opts.hoverNumber.decimalsY),
numX = opts.hoverNumber.formatter(dist, opts.hoverNumber.decimalsX);
this._focuslabeltext
// .attr("x", xCoordinate)
.attr("y", this._y(item.z))
.style("font-weight", "700");
this._focuslabelX
.text(numX + " " + this._xLabel)
.attr("x", xCoordinate + 10);
this._focuslabelY
.text(numY + " " + this._yLabel)
.attr("x", xCoordinate + 10);
let focuslabeltext = this._focuslabeltext.node();
if (this._isDomVisible(focuslabeltext)) {
let bbox = focuslabeltext.getBBox();
let padding = 2;
this._focuslabelrect
.attr("x", bbox.x - padding)
.attr("y", bbox.y - padding)
.attr("width", bbox.width + (padding * 2))
.attr("height", bbox.height + (padding * 2));
// move focus label to left
if (xCoordinate >= this._width() / 2) {
this._focuslabelrect.attr("x", this._focuslabelrect.attr("x") - this._focuslabelrect.attr("width") - (padding * 2) - 10);
this._focuslabelX.attr("x", this._focuslabelX.attr("x") - this._focuslabelrect.attr("width") - (padding * 2) - 10);
this._focuslabelY.attr("x", this._focuslabelY.attr("x") - this._focuslabelrect.attr("width") - (padding * 2) - 10);
}
}
},
/**
* Collapse or Expand current chart control.
*/
_toggle: function() {
if (L.DomUtil.hasClass(this._container, "elevation-expanded"))
this._collapse();
else
this._expand();
},
/**
* Sets the view of the map (center and zoom). Useful when "followMarker" is true.
*/
_setMapView: function(item) {
if (!this.options.followMarker || !this._map) return;
let zoom = this._map.getZoom();
zoom = zoom < this._zFollow ? this._zFollow : zoom;
this._map.setView(item.latlng, zoom, { animate: true, duration: 0.25 });
},
/*
* Shows the position/height indicator marker drawn onto the map
*/
_showPositionMarker: function(item) {
this._selectedItem = item;
if (this._map && !this._map.getPane('elevationPane')) {
this._map.createPane('elevationPane');
this._map.getPane('elevationPane').style.zIndex = 625; // This pane is above markers but below popups.
this._map.getPane('elevationPane').style.pointerEvents = 'none';
}
if (this.options.marker == 'elevation-line') {
this._updatePositionMarker(item);
} else if (this.options.marker == 'position-marker') {
this._updateLeafletMarker(item);
}
},
/**
* Update chart axis.
*/
_updateAxis: function() {
this._grid.selectAll("g").remove();
this._axis.selectAll("g").remove();
this._appendXGrid(this._grid);
this._appendYGrid(this._grid);
this._appendXaxis(this._axis);
this._appendYaxis(this._axis);
},
/**
* Update distance and altitude level ("leaflet-marker").
*/
_updateHeightIndicator: function(item) {
let opts = this.options;
let numY = opts.hoverNumber.formatter(item.z, opts.hoverNumber.decimalsY);
let numX = opts.hoverNumber.formatter(item.dist, opts.hoverNumber.decimalsX);
let normalizedAlt = this._height() / this._maxElevation * item.z;
let normalizedY = item.y - normalizedAlt;
this._mouseHeightFocus
.attr("x1", item.x)
.attr("x2", item.x)
.attr("y1", item.y)
.attr("y2", normalizedY)
.style("visibility", "visible");
this._mouseHeightFocusLabel
.attr("x", item.x)
.attr("y", normalizedY)
.text(numY + " " + this._yLabel)
.style("visibility", "visible");
},
/**
* Update position marker ("leaflet-marker").
*/
_updateLeafletMarker: function(item) {
let ll = item.latlng;
if (!this._marker) {
this._marker = new L.Marker(ll, {
icon: this.options.markerIcon,
zIndexOffset: 1000000,
});
this._marker.addTo(this._map, {
pane: 'elevationPane',
});
} else {
this._marker.setLatLng(ll);
}
},
/**
* Update focus point ("leaflet-marker").
*/
_updatePointG: function(item) {
this._pointG
.attr("transform", "translate(" + item.x + "," + item.y + ")")
.style("visibility", "visible");
},
/**
* Update position marker ("leaflet-marker").
*/
_updatePositionMarker: function(item) {
let point = this._map.latLngToLayerPoint(item.latlng);
let layerpoint = {
dist: item.dist,
x: point.x,
y: point.y,
z: item.z,
};
if (!this._mouseHeightFocus) {
L.svg({ pane: "elevationPane" }).addTo(this._map); // default leaflet svg renderer
let layerpane = d3.select(this._map.getContainer()).select(".leaflet-elevation-pane svg");
this._appendPositionMarker(layerpane);
}
this._updatePointG(layerpoint);
this._updateHeightIndicator(layerpoint);
},
/**
* Update chart summary.
*/
_updateSummary: function() {
if (this.options.summary && this.summaryDiv) {
this.track_info = this.track_info || {};
this.track_info.distance = this._distance || 0;
this.track_info.elevation_max = this._maxElevation || 0;
this.track_info.elevation_min = this._minElevation || 0;
d3.select(this.summaryDiv).html('<span class="totlen"><span class="summarylabel">' + L._("Total Length: ") + '</span><span class="summaryvalue">' + this.track_info.distance.toFixed(2) + ' ' + this._xLabel + '</span></span><span class="maxele"><span class="summarylabel">' + L._("Max Elevation: ") + '</span><span class="summaryvalue">' + this.track_info.elevation_max.toFixed(2) + ' ' + this._yLabel + '</span></span><span class="minele"><span class="summarylabel">' + L._("Min Elevation: ") + '</span><span class="summaryvalue">' + this.track_info.elevation_min.toFixed(2) + ' ' + this._yLabel + '</span></span>');
}
if (this.options.downloadLink && this._downloadURL) { // TODO: generate dynamically file content instead of using static file urls.
let span = document.createElement('span');
span.className = 'download';
let save = document.createElement('a');
save.innerHTML = "Télécharger"; //modif
save.href = "#";
save.onclick = function(e) {
e.preventDefault();
let evt = { confirm: this._saveFile.bind(this, this._downloadURL) };
let type = this.options.downloadLink;
if (type == 'modal') {
if (typeof CustomEvent === "function") document.dispatchEvent(new CustomEvent("eletrack_download", { detail: evt }));
this._fireEvt('eletrack_download', evt);
} else if (type == 'link' || type === true) {
evt.confirm();
}
}.bind(this);
this.summaryDiv.appendChild(span).appendChild(save);
}
},
/**
* Calculates chart width.
*/
_width: function() {
let opts = this.options;
return opts.width - opts.margins.left - opts.margins.right;
},
});
L.control.elevation = function(options) {
return new L.Control.Elevation(options);
};
})));
//# sourceMappingURL=leaflet-elevation.js.map