adds elevation profile
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
export let tabs: string[];
|
export let tabs: string[];
|
||||||
export let activeTab: number;
|
export let activeTab: number;
|
||||||
|
export let extraClasses: string = "";
|
||||||
|
|
||||||
const indicatorPosition = tweened(0, {
|
const indicatorPosition = tweened(0, {
|
||||||
duration: 300,
|
duration: 300,
|
||||||
@@ -33,7 +34,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id="tabs" class="flex gap-2 overflow-x-auto relative">
|
<div id="tabs" class="flex gap-2 overflow-x-auto relative {extraClasses}">
|
||||||
<div
|
<div
|
||||||
class="absolute h-full bg-menu-item-background-hover rounded-t-lg top-0 z-0"
|
class="absolute h-full bg-menu-item-background-hover rounded-t-lg top-0 z-0"
|
||||||
style="width: {$indicatorWidth}px; left: {$indicatorPosition}px;"
|
style="width: {$indicatorWidth}px; left: {$indicatorPosition}px;"
|
||||||
|
|||||||
BIN
web/src/lib/vendor/leaflet-elevation/images/checkered.png
vendored
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
web/src/lib/vendor/leaflet-elevation/images/elevation-locate.png
vendored
Normal file
|
After Width: | Height: | Size: 274 B |
BIN
web/src/lib/vendor/leaflet-elevation/images/elevation-poi.png
vendored
Normal file
|
After Width: | Height: | Size: 640 B |
BIN
web/src/lib/vendor/leaflet-elevation/images/elevation-position.png
vendored
Normal file
|
After Width: | Height: | Size: 542 B |
1
web/src/lib/vendor/leaflet-elevation/images/elevation-position.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 8.5 8.5"><defs><filter id="shadow"><feDropShadow dx="0" dy="0" stdDeviation="0.3" flood-color="#000000"/></filter></defs><circle cx="4.2" cy="4.2" r="2.2" fill="#ffffff" fill-opacity="1" filter="url(#shadow)"/><circle cx="4.2" cy="4.2" r="1.85" fill="none" stroke="#333333" stroke-linejoin="round" stroke-width="0.45"/><circle cx="4.2" cy="4.2" r="1.25" fill="none" stroke="#333333" stroke-linejoin="round" stroke-width="0.25"/><circle cx="4.2" cy="4.2" r="0.55" fill="#333333"/></svg>
|
||||||
|
After Width: | Height: | Size: 563 B |
BIN
web/src/lib/vendor/leaflet-elevation/images/elevation-pushpin.png
vendored
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
1
web/src/lib/vendor/leaflet-elevation/images/elevation-pushpin.svg
vendored
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
1
web/src/lib/vendor/leaflet-elevation/images/elevation.svg
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg viewBox="0 0 36 36" style="display: var(--ele-toggle-bg, inline-block);"><path fill="var(--ele-area, #addb55)" fill-opacity=".8" opacity=".9" d="m 28.3,23.9 -4.2,-8 -3.9,-0.2 -2.7,1.8 -2.4,-5.5 -5.2,6.2 0,5.7" /><path fill="none" stroke="var(--ele-line, var(--ele-stroke, #000))" stroke-width="1.5" d="m 9.9,17.9 1.1,0 4.3,-6.2 2.5,5.6 2.5,-2.3 3.7,0.2 4,8.3 0,0.4" /><path fill="none" stroke="#737373" stroke-linecap="square" d="m 8.6,8.1 0,19.8 m 21.3,-2.4 -23.8,0" /></svg>
|
||||||
|
After Width: | Height: | Size: 483 B |
13
web/src/lib/vendor/leaflet-elevation/libs/fullpage.css
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
html,
|
||||||
|
body,
|
||||||
|
.leaflet-map {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
8
web/src/lib/vendor/leaflet-elevation/libs/leaflet-distance-marker.css
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.dist-marker {
|
||||||
|
font-size: 9px;
|
||||||
|
border: 1px solid #777;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: #000;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
314
web/src/lib/vendor/leaflet-elevation/libs/leaflet-distance-marker.js
vendored
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022, 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) 2014- Doroszlai Attila, 2016- Phil Whitehurst
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: the "L.DistanceMarker" (canvas marker) class could be alternatively provided by "leaflet-rotate"?
|
||||||
|
|
||||||
|
L.DistanceMarker = L.CircleMarker.extend({
|
||||||
|
_updatePath: function () {
|
||||||
|
let ctx = this._renderer._ctx;
|
||||||
|
let p = this._point;
|
||||||
|
|
||||||
|
// Calculate image direction (rotation)
|
||||||
|
this.options.rotation = this.options.rotation || 0;
|
||||||
|
|
||||||
|
// Draw circle marker (canvas point)
|
||||||
|
if (this.options.radius && this._renderer._updateCircle) {
|
||||||
|
this._renderer._updateCircle(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw image over circle (distance marker)
|
||||||
|
if (this.options.icon && this.options.icon.url) {
|
||||||
|
if (!this.options.icon.element) {
|
||||||
|
const icon = document.createElement('img');
|
||||||
|
this.options.icon = L.extend({ rotate: 0, size: [40, 40], offset: { x: 0, y: 0 } }, this.options.icon);
|
||||||
|
this.options.icon.rotate += this.options.rotation;
|
||||||
|
this.options.icon.element = icon;
|
||||||
|
icon.src = this.options.icon.url;
|
||||||
|
icon.onload = () => this.redraw();
|
||||||
|
icon.onerror = () => this.options.icon = null;
|
||||||
|
} else {
|
||||||
|
const icon = this.options.icon;
|
||||||
|
let cx = p.x + icon.offset.x;
|
||||||
|
let cy = p.y + icon.offset.y;
|
||||||
|
ctx.save();
|
||||||
|
if (icon.rotate) {
|
||||||
|
ctx.translate(p.x, p.y);
|
||||||
|
ctx.rotate(icon.rotate);
|
||||||
|
cx = 0;
|
||||||
|
cy = 0;
|
||||||
|
}
|
||||||
|
ctx.drawImage(icon.element, cx - icon.size[0] / 2, cy - icon.size[1] / 2, icon.size[0], icon.size[1]);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a label inside the circle (distance marker)
|
||||||
|
if (this.options.label) {
|
||||||
|
let cx = p.x, cy = p.y;
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
ctx.font = this.options.font || 'normal 7pt "Helvetica Neue", Arial, Helvetica, sans-serif';
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillStyle = this.options.fillStyle || 'black';
|
||||||
|
|
||||||
|
// TODO rescale circle to fit text
|
||||||
|
// let fontSize = Number(/[0-9\.]+/.exec(ctx.font)[0]);
|
||||||
|
// let fontWidth = ctx.measureText(this.options.html).width;
|
||||||
|
|
||||||
|
if (this.options.rotation) {
|
||||||
|
ctx.translate(p.x, p.y);
|
||||||
|
ctx.rotate(this.options.rotation);
|
||||||
|
cx = 0;
|
||||||
|
cy = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary fix to prevent stroke blurs at higher zoom levels
|
||||||
|
if (this._map.getZoom() > 17) {
|
||||||
|
ctx.fillStyle = this.options.strokeStyle || 'black';
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillText(this.options.label, cx, cy);
|
||||||
|
|
||||||
|
if (this.options.strokeStyle && this._map.getZoom() <= 17) {
|
||||||
|
ctx.strokeStyle = this.options.strokeStyle;
|
||||||
|
ctx.strokeText(this.options.label, cx, cy);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
L.DistanceMarkers = L.LayerGroup.extend({
|
||||||
|
options: {
|
||||||
|
cssClass: 'dist-marker',
|
||||||
|
iconSize: [12, 12],
|
||||||
|
arrowSize: [10, 10],
|
||||||
|
arrowUrl: "data:image/svg+xml,%3Csvg transform='rotate(90)' xmlns='http://www.w3.org/2000/svg' width='560px' height='560px' viewBox='0 0 560 560'%3E%3Cpath stroke-width='35' fill='%23000' stroke='%23FFF' d='M280,40L522,525L280,420L38,525z'/%3E%3C/svg%3E",
|
||||||
|
offset: 1000,
|
||||||
|
showAll: 12,
|
||||||
|
textFunction: (distance, i, offset) => i,
|
||||||
|
distance: true,
|
||||||
|
direction: true,
|
||||||
|
},
|
||||||
|
initialize: function (line, map, options) {
|
||||||
|
|
||||||
|
this._layers = {};
|
||||||
|
this._zoomLayers = {};
|
||||||
|
|
||||||
|
options = L.setOptions(this, options);
|
||||||
|
|
||||||
|
let preferCanvas = map.options.preferCanvas;
|
||||||
|
let showAll = Math.min(map.getMaxZoom(), options.showAll);
|
||||||
|
|
||||||
|
// You should use "leaflet-rotate" to show rotated arrow markers (preferCanvas: false)
|
||||||
|
if (!preferCanvas && !map.options.rotate) {
|
||||||
|
console.warn('Missing dependency: "leaflet-rotate"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get line coords as an array
|
||||||
|
let coords = typeof line.getLatLngs == 'function' ? line.getLatLngs() : line;
|
||||||
|
|
||||||
|
// Handle "MultiLineString" features
|
||||||
|
coords = L.LineUtil.isFlat(coords) ? [coords] : coords;
|
||||||
|
|
||||||
|
coords.forEach(latlngs => {
|
||||||
|
// Get accumulated line lengths as well as overall length
|
||||||
|
let accumulated = L.GeometryUtil.accumulatedLengths(latlngs);
|
||||||
|
let length = accumulated.length > 0 ? accumulated[accumulated.length - 1] : 0;
|
||||||
|
|
||||||
|
// count = Number of distance markers to be added
|
||||||
|
// j = Position in accumulated line length array
|
||||||
|
for (let i = 1, count = Math.floor(length / options.offset), j = 0; i <= count; ++i) {
|
||||||
|
|
||||||
|
let distance = options.offset * i;
|
||||||
|
|
||||||
|
// Find the first accumulated distance that is greater than the distance of this marker
|
||||||
|
while (j < accumulated.length - 1 && accumulated[j] < distance) ++j;
|
||||||
|
|
||||||
|
// Grab two nearest points either side marker position
|
||||||
|
let p1 = latlngs[j - 1];
|
||||||
|
let p2 = latlngs[j];
|
||||||
|
let m_line = L.polyline([p1, p2]);
|
||||||
|
|
||||||
|
// and create a simple line to interpolate on
|
||||||
|
let ratio = (distance - accumulated[j - 1]) / (accumulated[j] - accumulated[j - 1]);
|
||||||
|
let position = L.GeometryUtil.interpolateOnLine(map, m_line, ratio);
|
||||||
|
let delta = map.project(p2).subtract(map.project(p1));
|
||||||
|
let angle = Math.atan2(delta.y, delta.x);
|
||||||
|
|
||||||
|
// Generate distance marker label
|
||||||
|
let text = options.textFunction.call(this, distance, i, options.offset);
|
||||||
|
|
||||||
|
// Grouping layer of visible layers at zoom level (arrow + distance)
|
||||||
|
let zoom = this._minimumZoomLevelForItem(i, showAll);
|
||||||
|
let markers = this._zoomLayers[zoom] = this._zoomLayers[zoom] || L.layerGroup()
|
||||||
|
|
||||||
|
// create arrow markers
|
||||||
|
if (options.direction && ((options.distance && i % 2 == 1) || !options.distance)) {
|
||||||
|
if (preferCanvas) {
|
||||||
|
markers.addLayer(
|
||||||
|
new L.DistanceMarker(p1, {
|
||||||
|
radius: 0,
|
||||||
|
icon: {
|
||||||
|
url: options.arrowUrl, //image link
|
||||||
|
size: options.arrowSize, //image size ( default [40, 40] )
|
||||||
|
rotate: 0, //image base rotate ( default 0 )
|
||||||
|
offset: { x: 0, y: 0 }, //image offset ( default { x: 0, y: 0 } )
|
||||||
|
},
|
||||||
|
rotation: angle,
|
||||||
|
interactive: false,
|
||||||
|
// label: '⮞', //'➜',
|
||||||
|
// font: 'normal 20pt "Helvetica Neue", Arial, Helvetica, sans-serif',
|
||||||
|
// fillStyle: 'white',//'#3366CC',
|
||||||
|
// strokeStyle: 'black',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
markers.addLayer(
|
||||||
|
L.marker(position.latLng, {
|
||||||
|
icon: L.icon({
|
||||||
|
iconUrl: options.arrowUrl,
|
||||||
|
iconSize: options.arrowSize,
|
||||||
|
}),
|
||||||
|
// NB the following option is added by "leaflet-rotate"
|
||||||
|
rotation: angle,
|
||||||
|
interactive: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create distance markers
|
||||||
|
if (options.distance && i % 2 == 0) {
|
||||||
|
if (preferCanvas) {
|
||||||
|
markers.addLayer(
|
||||||
|
new L.DistanceMarker(position.latLng, {
|
||||||
|
label: text, // TODO: handle text rotation (leaflet-rotate)
|
||||||
|
radius: 7,
|
||||||
|
fillColor: '#fff',
|
||||||
|
fillOpacity: 1,
|
||||||
|
fillStyle: 'black',
|
||||||
|
color: '#777',
|
||||||
|
weight: 1,
|
||||||
|
interactive: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
markers.addLayer(
|
||||||
|
L.marker(position.latLng, {
|
||||||
|
title: text,
|
||||||
|
icon: L.divIcon({
|
||||||
|
className: options.cssClass,
|
||||||
|
html: text,
|
||||||
|
iconSize: options.iconSize
|
||||||
|
}),
|
||||||
|
interactive: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMarkerVisibility = () => {
|
||||||
|
let oldZoom = this._lastZoomLevel || 0;
|
||||||
|
let newZoom = map.getZoom();
|
||||||
|
if (newZoom > oldZoom) {
|
||||||
|
for (let i = oldZoom + 1; i <= newZoom; ++i) {
|
||||||
|
if (this._zoomLayers[i] !== undefined) {
|
||||||
|
this.addLayer(this._zoomLayers[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (newZoom < oldZoom) {
|
||||||
|
for (let i = oldZoom; i > newZoom; --i) {
|
||||||
|
if (this._zoomLayers[i] !== undefined) {
|
||||||
|
this.removeLayer(this._zoomLayers[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._lastZoomLevel = newZoom;
|
||||||
|
};
|
||||||
|
map.on('zoomend', updateMarkerVisibility);
|
||||||
|
updateMarkerVisibility();
|
||||||
|
},
|
||||||
|
|
||||||
|
_minimumZoomLevelForItem: function (i, zoom) {
|
||||||
|
while (i > 0 && i % 2 === 0) {
|
||||||
|
--zoom;
|
||||||
|
i = Math.floor(i / 2);
|
||||||
|
}
|
||||||
|
return zoom;
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
L.Polyline.include({
|
||||||
|
|
||||||
|
_originalOnAdd: L.Polyline.prototype.onAdd,
|
||||||
|
_originalOnRemove: L.Polyline.prototype.onRemove,
|
||||||
|
|
||||||
|
addDistanceMarkers: function () {
|
||||||
|
if (this._map && this._distanceMarkers) {
|
||||||
|
this._map.addLayer(this._distanceMarkers);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeDistanceMarkers: function () {
|
||||||
|
if (this._map && this._distanceMarkers) {
|
||||||
|
this._map.removeLayer(this._distanceMarkers);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function (map) {
|
||||||
|
this._originalOnAdd(map);
|
||||||
|
|
||||||
|
let opts = this.options.distanceMarkers || {};
|
||||||
|
if (this.options.distanceMarkers) {
|
||||||
|
this._distanceMarkers = this._distanceMarkers || new L.DistanceMarkers(this, map, opts);
|
||||||
|
}
|
||||||
|
if (opts.lazy === undefined || opts.lazy === false) {
|
||||||
|
this.addDistanceMarkers();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function (map) {
|
||||||
|
this.removeDistanceMarkers();
|
||||||
|
this._originalOnRemove(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
1
web/src/lib/vendor/leaflet-elevation/libs/leaflet-distance-marker.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.dist-marker{font-size:9px;border:1px solid #777;border-radius:10px;text-align:center;color:#000;background:#fff}
|
||||||
1
web/src/lib/vendor/leaflet-elevation/libs/leaflet-distance-marker.min.js
vendored
Normal file
636
web/src/lib/vendor/leaflet-elevation/libs/leaflet-edgescale.js
vendored
Normal file
@@ -0,0 +1,636 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023, 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) GPL-3.0+ Project - 2018- Dražen Tutić - https://github.com/dtutic/Leaflet.EdgeScaleBar
|
||||||
|
* Copyright (c) MIT License (MIT) - 2015- Xisco Guaita - https://github.com/xguaita/Leaflet.MapCenterCoord
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Original source: https://github.com/xguaita/Leaflet.MapCenterCoord
|
||||||
|
*/
|
||||||
|
L.Control.EdgeScale = L.Control.extend({
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
options: {
|
||||||
|
position: 'bottomleft',
|
||||||
|
icon: true,
|
||||||
|
coords: true,
|
||||||
|
bar: true,
|
||||||
|
onMove: true,
|
||||||
|
template: '{y} | {x}', // https://en.wikipedia.org/wiki/ISO_6709
|
||||||
|
projected: false,
|
||||||
|
formatProjected: '#.##0,000',
|
||||||
|
latlngFormat: 'DD', // DD, DM, DMS
|
||||||
|
latlngDesignators: true,
|
||||||
|
latLngFormatter: undefined,
|
||||||
|
iconStyle: {
|
||||||
|
background: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xml:space='preserve' viewBox='0 0 100 100'%3E%3Cg stroke='%23fff'%3E%3Ccircle cx='50' cy='50.2' r='3.9' stroke-width='2' /%3E%3Cpath stroke-width='3' d='M5 54h32a4 4 0 1 0 0-8H5a4 4 0 1 0 0 8z M54 5a4 4 0 1 0-8 0v32a4 4 0 1 0 8 0V5z M99 50c0-2-2-4-4-4H63a4 4 0 1 0 0 8h32c2 0 4-1 4-4zM46 95a4 4 0 1 0 8 0V64a4 4 0 1 0-8 0v31z'/%3E%3C/g%3E%3C/svg%3E%0A")`,
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
left: 'calc(50% - 12px)',
|
||||||
|
top: 'calc(50% - 12px)',
|
||||||
|
content: '',
|
||||||
|
display: 'block',
|
||||||
|
position: 'absolute',
|
||||||
|
zIndex: 999,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
containerStyle: {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
boxShadow: '0 0 5px #bbb',
|
||||||
|
borderRadius: '3px',
|
||||||
|
padding: '3px 2px',
|
||||||
|
color: '#333',
|
||||||
|
font: '11px/1.5 Consolas, monaco, monospace',
|
||||||
|
writingMode: 'vertical-lr',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function(options) {
|
||||||
|
L.setOptions(this, options);
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function (map) {
|
||||||
|
|
||||||
|
if (this.options.bar) {
|
||||||
|
this._scaleBar = (new L.Control.EdgeScale.Layer(true === this.options.bar ? {} : this.options.bar)).addTo(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a DOM element and put it into overlayPane
|
||||||
|
if (this.options.icon) {
|
||||||
|
this._icon = L.DomUtil.create('div', 'leaflet-crosshair');
|
||||||
|
Object.assign(this._icon.style, this.options.iconStyle);
|
||||||
|
map.getContainer().insertBefore(this._icon, map.getContainer().firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control container
|
||||||
|
this._container = L.DomUtil.create('div', 'leaflet-control-mapcentercoord');
|
||||||
|
|
||||||
|
Object.assign(this._container.style, this.options.containerStyle);
|
||||||
|
|
||||||
|
if (!this.options.coords) {
|
||||||
|
this._container.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
L.DomEvent.disableClickPropagation(this._container);
|
||||||
|
|
||||||
|
this._container.innerHTML = this._getMapCenterCoord();
|
||||||
|
|
||||||
|
// Add events listeners for updating coordinates & icon's position
|
||||||
|
map.on('move', this._onMapMove, this);
|
||||||
|
map.on('moveend', this._onMapMove, this);
|
||||||
|
|
||||||
|
return this._container;
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function (map) {
|
||||||
|
if (this.options.bar) {
|
||||||
|
this._scaleBar.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove icon's DOM elements and listeners
|
||||||
|
if (this.options.icon) {
|
||||||
|
map.getContainer().removeChild(this._icon);
|
||||||
|
}
|
||||||
|
map.off('move', this._onMapMove, this);
|
||||||
|
map.off('moveend', this._onMapMove, this);
|
||||||
|
},
|
||||||
|
|
||||||
|
// update coordinates
|
||||||
|
_onMapMove: function (e) {
|
||||||
|
if (this.options.onMove || 'moveend' === e.type) {
|
||||||
|
this._container.innerHTML = this._getMapCenterCoord();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_getMapCenterCoord: function () {
|
||||||
|
const center = this._map.getCenter();
|
||||||
|
return this.options.projected
|
||||||
|
? this._getProjectedCoord(this._map.options.crs.project(center))
|
||||||
|
: this._getLatLngCoord(center);
|
||||||
|
},
|
||||||
|
|
||||||
|
_getProjectedCoord: function (center) {
|
||||||
|
return L.Util.template(
|
||||||
|
this.options.template,
|
||||||
|
{
|
||||||
|
x: this._format(this.options.formatProjected, center.x),
|
||||||
|
y: this._format(this.options.formatProjected, center.y)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_getLatLngCoord: function (latLng) {
|
||||||
|
|
||||||
|
const { latLngFormatter, latlngFormat, latlngDesignators: designators } = this.options;
|
||||||
|
|
||||||
|
if (undefined !== latLngFormatter ) {
|
||||||
|
return latLngFormatter(latLng.lat, latLng.lng);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lat, lng, deg, min;
|
||||||
|
|
||||||
|
// make a copy of center so we aren't affecting leaflet's internal state
|
||||||
|
let center = {
|
||||||
|
lat: latLng.lat,
|
||||||
|
lng: latLng.lng,
|
||||||
|
lng_neg: latLng.lng < 0,
|
||||||
|
lat_neg: latLng.lat < 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 180 degrees & negative
|
||||||
|
if (center.lng < 0) {
|
||||||
|
center.lng = Math.abs(center.lng);
|
||||||
|
}
|
||||||
|
if (center.lng > 180) {
|
||||||
|
center.lng = 360 - center.lng;
|
||||||
|
center.lng_neg = !center.lng_neg;
|
||||||
|
}
|
||||||
|
if (center.lat < 0) {
|
||||||
|
center.lat = Math.abs(center.lat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// format
|
||||||
|
if ('DM' === latlngFormat) {
|
||||||
|
deg = parseInt(center.lng);
|
||||||
|
lng = deg + 'º ' + this._format('00.000', (center.lng - deg) * 60) + "'";
|
||||||
|
deg = parseInt(center.lat);
|
||||||
|
lat = deg + 'º ' + this._format('00.000', (center.lat - deg) * 60) + "'";
|
||||||
|
} else if ('DMS' === latlngFormat) {
|
||||||
|
deg = parseInt(center.lng);
|
||||||
|
min = (center.lng - deg) * 60;
|
||||||
|
lng = deg + 'º ' + this._format('00', parseInt(min)) + "' " + this._format('00.0', (min - parseInt(min)) * 60) + "''";
|
||||||
|
deg = parseInt(center.lat);
|
||||||
|
min = (center.lat - deg) * 60;
|
||||||
|
lat = deg + 'º ' + this._format('00', parseInt(min)) + "' " + this._format('00.0', (min - parseInt(min)) * 60) + "''";
|
||||||
|
} else { // 'DD'
|
||||||
|
lng = this._format('#0.00000', center.lng) + 'º';
|
||||||
|
lat = this._format('##0.00000', center.lat) + 'º';
|
||||||
|
}
|
||||||
|
|
||||||
|
return L.Util.template(this.options.template, {
|
||||||
|
x: (!designators && center.lng_neg ? '-' : '') + lng + (designators ? (center.lng_neg ? ' W' : ' E') : ''),
|
||||||
|
y: (!designators && center.lat_neg ? '-' : '') + lat + (designators ? (center.lat_neg ? ' S' : ' N') : '')
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IntegraXor Web SCADA - JavaScript Number Formatter
|
||||||
|
*
|
||||||
|
* @see https://code.google.com/p/javascript-number-formatter
|
||||||
|
* @authors KPL, KHL
|
||||||
|
*/
|
||||||
|
_format: function (m, v) {
|
||||||
|
if (!m || isNaN(+v)) {
|
||||||
|
return v; // return as it is.
|
||||||
|
}
|
||||||
|
|
||||||
|
v = m.charAt(0) == '-' ? -v : +v; // convert any string to number according to formation sign.
|
||||||
|
let isNegative = v < 0 ? v = -v : 0; // process only abs(), and turn on flag.
|
||||||
|
|
||||||
|
let result = m.match(/[^\d\-\+#]/g); // search for separator for grp & decimal, anything not digit, not +/- sign, not #.
|
||||||
|
let Decimal = (result && result[result.length - 1]) || '.'; // treat the right most symbol as decimal
|
||||||
|
let Group = (result && result[1] && result[0]) || ','; // treat the left most symbol as group separator
|
||||||
|
|
||||||
|
m = m.split(Decimal); // split the decimal for the format string if any.
|
||||||
|
v = v.toFixed(m[1] && m[1].length); // Fix the decimal first, toFixed will auto fill trailing zero.
|
||||||
|
v = +(v) + ''; // convert number to string to trim off *all* trailing decimal zero(es)
|
||||||
|
|
||||||
|
let pos_trail_zero = m[1] && m[1].lastIndexOf('0'); // fill back any trailing zero according to format (look for last zero in format)
|
||||||
|
let part = v.split('.');
|
||||||
|
|
||||||
|
if (!part[1] || part[1] && part[1].length <= pos_trail_zero) { // integer will get !part[1]
|
||||||
|
v = (+v).toFixed(pos_trail_zero + 1);
|
||||||
|
}
|
||||||
|
let szSep = m[0].split(Group); // look for separator
|
||||||
|
m[0] = szSep.join(''); // join back without separator for counting the pos of any leading 0.
|
||||||
|
|
||||||
|
let pos_lead_zero = m[0] && m[0].indexOf('0');
|
||||||
|
if (pos_lead_zero > -1) {
|
||||||
|
while (part[0].length < (m[0].length - pos_lead_zero)) {
|
||||||
|
part[0] = '0' + part[0];
|
||||||
|
}
|
||||||
|
} else if (+part[0] == 0) {
|
||||||
|
part[0] = '';
|
||||||
|
}
|
||||||
|
v = v.split('.');
|
||||||
|
v[0] = part[0];
|
||||||
|
|
||||||
|
var pos_separator = (szSep[1] && szSep[szSep.length - 1].length); // process the first group separator from decimal (.) only, the rest ignore. Get the length of the last slice of split result.
|
||||||
|
if (pos_separator) {
|
||||||
|
let integer = v[0];
|
||||||
|
let str = '';
|
||||||
|
let offset = integer.length % pos_separator;
|
||||||
|
for (let i = 0, l = integer.length; i < l; i++) {
|
||||||
|
str += integer.charAt(i); // ie6 only support charAt for sz.
|
||||||
|
if (
|
||||||
|
!((i - offset + 1) % pos_separator) &&
|
||||||
|
i < l - pos_separator // -pos_separator so that won't trail separator on full length
|
||||||
|
) {
|
||||||
|
str += Group;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v[0] = str;
|
||||||
|
}
|
||||||
|
|
||||||
|
v[1] = (m[1] && v[1]) ? Decimal + v[1] : "";
|
||||||
|
return (isNegative ? '-' : '') + v[0] + v[1]; // put back any negation and combine integer and fraction.
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Original Source: https://github.com/dtutic/Leaflet.EdgeScaleBar
|
||||||
|
*
|
||||||
|
* Draws the metric scale bars in Web Mercator map along top and right edges.
|
||||||
|
* Authors: Dražen Tutić (dtutic@geof.hr), Ana Kuveždić Divjak (akuvezdic@geof.hr)
|
||||||
|
* University of Zagreb, Faculty of Geodesy, GEOF-OSGL Lab
|
||||||
|
* Inspired by LatLonGraticule Leaflet plugin by: lanwei@cloudybay.com.tw
|
||||||
|
*/
|
||||||
|
L.Control.EdgeScale.Layer = L.Layer.extend({
|
||||||
|
|
||||||
|
includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
|
||||||
|
|
||||||
|
options: {
|
||||||
|
opacity: 1,
|
||||||
|
weight: 0.8,
|
||||||
|
gradient: {
|
||||||
|
size: 10,
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
color: '#000',
|
||||||
|
font: '11px Arial',
|
||||||
|
zoomInterval: [
|
||||||
|
{start: 0, end: 2, interval: 5000000},
|
||||||
|
{start: 3, end: 3, interval: 2000000},
|
||||||
|
{start: 4, end: 4, interval: 1000000},
|
||||||
|
{start: 5, end: 5, interval: 500000},
|
||||||
|
{start: 6, end: 7, interval: 200000},
|
||||||
|
{start: 8, end: 8, interval: 100000},
|
||||||
|
{start: 9, end: 9, interval: 50000},
|
||||||
|
{start: 10, end: 10, interval: 20000},
|
||||||
|
{start: 11, end: 11, interval: 10000},
|
||||||
|
{start: 12, end: 12, interval: 5000},
|
||||||
|
{start: 13, end: 13, interval: 2000},
|
||||||
|
{start: 14, end: 14, interval: 1000},
|
||||||
|
{start: 15, end: 15, interval: 500},
|
||||||
|
{start: 16, end: 16, interval: 200},
|
||||||
|
{start: 17, end: 17, interval: 100},
|
||||||
|
{start: 18, end: 18, interval: 50},
|
||||||
|
{start: 19, end: 19, interval: 20},
|
||||||
|
{start: 20, end: 20, interval: 10}
|
||||||
|
],
|
||||||
|
pane: 'edgescalePane'
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function (options) {
|
||||||
|
|
||||||
|
L.setOptions(this, options);
|
||||||
|
|
||||||
|
// Constants of the WGS84 ellipsoid needed to calculate meridian length or latitute
|
||||||
|
const a = this._a = 6378137.0;
|
||||||
|
const b = this._b = 6356752.3142;
|
||||||
|
const n = this._n = (a - b)/(a + b);
|
||||||
|
const a2 = a * a;
|
||||||
|
const b2 = b * b;
|
||||||
|
const n2 = n * n;
|
||||||
|
const n3 = n2 * n;
|
||||||
|
const n4 = n3 * n;
|
||||||
|
const n5 = n4 * n;
|
||||||
|
this._A = a * (1.0 - n) * (1.0 - n2) * (1.0 + 9.0/4.0 * n2 + 225.0/64.0 * n4);
|
||||||
|
this._e2 = (a2 - b2) / a2;
|
||||||
|
this._ic1 = 1.5 * n - 29.0/12.0 * n3 + 553.0/80.0 * n5;
|
||||||
|
this._ic2 = 21.0/8.0 * n2 - 1537.0/128.0 * n4;
|
||||||
|
this._ic3 = 151.0/24.0 * n3 - 32373.0/640.0 * n5;
|
||||||
|
this._ic4 = 1097.0/64.0 * n4;
|
||||||
|
this._ic5 = 8011.0/150.0 * n5;
|
||||||
|
this._c1 = -1.5 * n + 31.0/24.0 * n3 - 669.0/640.0 * n5;
|
||||||
|
this._c2 = 15.0/18.0 * n2 - 435.0/128.0 * n4;
|
||||||
|
this._c3 = -35.0/12.0 * n3 + 651.0/80.0 * n5;
|
||||||
|
this._c4 = 315.0/64.0 * n4;
|
||||||
|
this._c5 = -693.0/80.0 * n5;
|
||||||
|
|
||||||
|
// Latitude limit of the Web Mercator projection
|
||||||
|
this._LIMIT_PHI = 1.484419982;
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function (map) {
|
||||||
|
this._map = map;
|
||||||
|
|
||||||
|
let pane = map.getPane(this.options.pane);
|
||||||
|
if (!pane) {
|
||||||
|
pane = this._pane = map.createPane('edgescalePane', map.getPane('norotatePane') || map.getPane('mapPane'));
|
||||||
|
pane.style.zIndex = 625; // This pane is above markers but below popups.
|
||||||
|
pane.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
this._pane = pane;
|
||||||
|
|
||||||
|
// if (this._renderer) this._renderer.remove()
|
||||||
|
// this._renderer = L.canvas({ pane: "edgescalePane" }).addTo(this._map); // default leaflet svg renderer
|
||||||
|
|
||||||
|
if (!this._canvas) {
|
||||||
|
this._initCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._pane.appendChild(this._canvas);
|
||||||
|
|
||||||
|
map.on('viewreset', this._reset, this);
|
||||||
|
map.on('move', this._reset, this);
|
||||||
|
map.on('moveend', this._reset, this);
|
||||||
|
map.on('rotate', this._reset, this);
|
||||||
|
|
||||||
|
this._reset();
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function (map) {
|
||||||
|
this._pane.removeChild(this._canvas);
|
||||||
|
|
||||||
|
map.off('viewreset', this._reset, this);
|
||||||
|
map.off('move', this._reset, this);
|
||||||
|
map.off('moveend', this._reset, this);
|
||||||
|
},
|
||||||
|
|
||||||
|
addTo: function (map) {
|
||||||
|
map.addLayer(this);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
setOpacity: function (opacity) {
|
||||||
|
this.options.opacity = opacity;
|
||||||
|
L.DomUtil.setOpacity(this._canvas, this.options.opacity);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
bringToFront: function () {
|
||||||
|
if (this._canvas) {
|
||||||
|
this._pane.appendChild(this._canvas);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
bringToBack: function () {
|
||||||
|
if (this._canvas) {
|
||||||
|
this._pane.insertBefore(this._canvas, pane.firstChild);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
_initCanvas: function () {
|
||||||
|
this._canvas = L.DomUtil.create('canvas', '');
|
||||||
|
this._ctx = this._canvas.getContext('2d');
|
||||||
|
|
||||||
|
this.setOpacity();
|
||||||
|
|
||||||
|
L.extend(this._canvas, {
|
||||||
|
onselectstart: L.Util.falseFn,
|
||||||
|
onmousemove: L.Util.falseFn,
|
||||||
|
onload: L.bind(this._onCanvasLoad, this)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_reset: function () {
|
||||||
|
var canvas = this._canvas,
|
||||||
|
size = this._map.getSize();
|
||||||
|
|
||||||
|
this._setCanvasPosition();
|
||||||
|
|
||||||
|
canvas.width = size.x;
|
||||||
|
canvas.height = size.y;
|
||||||
|
canvas.style.width = size.x + 'px';
|
||||||
|
canvas.style.height = size.y + 'px';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @TODO add support for "leaflet-rotate"
|
||||||
|
*/
|
||||||
|
if (this._map._bearing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { gradient } = this.options;
|
||||||
|
|
||||||
|
// horizontal gradient
|
||||||
|
if (!this._hor_gradient) {
|
||||||
|
this._hor_gradient = this._ctx.createLinearGradient(0, 0, 0, gradient.size);
|
||||||
|
this._hor_gradient.addColorStop(0,"rgba(255, 255, 255, " + gradient.opacity + ")");
|
||||||
|
this._hor_gradient.addColorStop(1,"rgba(255, 255, 255, 0)");
|
||||||
|
}
|
||||||
|
this._ctx.fillStyle = this._hor_gradient;
|
||||||
|
this._ctx.fillRect(0, 0, size.x, gradient.size);
|
||||||
|
|
||||||
|
// vertical gradient
|
||||||
|
if (!this._vert_gradient) {
|
||||||
|
this._vert_gradient = this._ctx.createLinearGradient(0, 0, gradient.size, 0);
|
||||||
|
this._vert_gradient.addColorStop(0,"rgba(255, 255, 255, " + gradient.opacity + ")");
|
||||||
|
this._vert_gradient.addColorStop(1,"rgba(255, 255, 255, 0)");
|
||||||
|
}
|
||||||
|
this._ctx.fillStyle = this._vert_gradient;
|
||||||
|
this._ctx.fillRect(0, 0, gradient.size, size.y);
|
||||||
|
|
||||||
|
this._ctx.beginPath();
|
||||||
|
this._ctx.moveTo(0,0);
|
||||||
|
this._ctx.lineTo(size.x,0);
|
||||||
|
this._ctx.lineTo(size.x,size.y);
|
||||||
|
this._ctx.stroke();
|
||||||
|
|
||||||
|
this._calcInterval();
|
||||||
|
this._draw();
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCanvasLoad: function () {
|
||||||
|
this.fire('load');
|
||||||
|
},
|
||||||
|
|
||||||
|
_calcInterval: function() {
|
||||||
|
const { zoomInterval } = this.options;
|
||||||
|
const zoom = this._map.getZoom();
|
||||||
|
|
||||||
|
if (undefined !== zoomInterval) {
|
||||||
|
// Manually set scale using a custom this.options.zoomInterval object
|
||||||
|
for (const idx in zoomInterval) {
|
||||||
|
const dict = zoomInterval[idx];
|
||||||
|
if (dict.start <= zoom && dict.end && dict.end >= zoom) {
|
||||||
|
this._interval = dict.interval;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Autamatically get current scale using L.Control.Scale
|
||||||
|
// Source: https://gis.stackexchange.com/a/198444
|
||||||
|
this._interval = L.Control.Scale.prototype._getRoundNum(
|
||||||
|
this._map
|
||||||
|
.containerPointToLatLng([0, this._map.getSize().y / 2 ])
|
||||||
|
.distanceTo(
|
||||||
|
this._map.containerPointToLatLng([L.Control.Scale.prototype.options.maxWidth, this._map.getSize().y / 2 ]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._currZoom = zoom;
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
_draw: function() {
|
||||||
|
this._ctx.strokeStyle = this.options.color;
|
||||||
|
this._create_lat_ticks();
|
||||||
|
this._create_lon_ticks();
|
||||||
|
|
||||||
|
this._ctx.fillStyle = this.options.color;
|
||||||
|
this._ctx.font = this.options.font;
|
||||||
|
|
||||||
|
const size = this._map.getSize();
|
||||||
|
const text = this._interval >= 1000 ? (this._interval / 1000 + ' km') : this._interval + ' m';
|
||||||
|
|
||||||
|
this._ctx.textAlign = 'left';
|
||||||
|
this._ctx.textBaseline = 'middle';
|
||||||
|
this._ctx.fillText(text, +12, size.y / 2);
|
||||||
|
|
||||||
|
this._ctx.textAlign = 'center';
|
||||||
|
this._ctx.textBaseline = 'top';
|
||||||
|
this._ctx.fillText(text, size.x / 2, 12);
|
||||||
|
},
|
||||||
|
|
||||||
|
_create_lat_ticks: function() {
|
||||||
|
const { weight } = this.options;
|
||||||
|
const size = this._map.getSize();
|
||||||
|
const to_rad = Math.PI/180.0;
|
||||||
|
const center = this._merLength(this._map.containerPointToLatLng(L.point(0, size.y / 2)).lat * to_rad);
|
||||||
|
const top = this._merLength(this._map.containerPointToLatLng(L.point(0,0)).lat * to_rad);
|
||||||
|
const bottom = this._merLength(this._map.containerPointToLatLng(L.point(0, size.y)).lat * to_rad);
|
||||||
|
|
||||||
|
// draw major ticks
|
||||||
|
for (let i = center + this._interval / 2; i < top; i = i + this._interval) {
|
||||||
|
const phi = this._invmerLength(i);
|
||||||
|
if ((phi < this._LIMIT_PHI) && (phi > -this._LIMIT_PHI)) {
|
||||||
|
this._draw_lat_tick(phi, 10, weight * 1.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = center - this._interval / 2; i > bottom; i = i - this._interval) {
|
||||||
|
const phi = this._invmerLength(i);
|
||||||
|
if ((phi > -this._LIMIT_PHI) && (phi < this._LIMIT_PHI)) {
|
||||||
|
this._draw_lat_tick(phi, 10, weight * 1.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw minor ticks
|
||||||
|
for (let i = center; i < top; i = i + this._interval / 10.0) {
|
||||||
|
const phi = this._invmerLength(i);
|
||||||
|
if ((phi < this._LIMIT_PHI) && (phi > -this._LIMIT_PHI)) {
|
||||||
|
this._draw_lat_tick(phi, 4, weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = center - this._interval / 10; i > bottom; i = i - this._interval / 10.0) {
|
||||||
|
const phi = this._invmerLength(i);
|
||||||
|
if ((phi > -this._LIMIT_PHI) && (phi < this._LIMIT_PHI)) {
|
||||||
|
this._draw_lat_tick(phi, 4, weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_create_lon_ticks: function() {
|
||||||
|
const { weight } = this.options;
|
||||||
|
const size = this._map.getSize();
|
||||||
|
const to_rad = Math.PI/180.0;
|
||||||
|
const to_deg = 180.0/Math.PI;
|
||||||
|
const center = this._map.containerPointToLatLng(L.point(size.x / 2, 0));
|
||||||
|
const left = this._map.containerPointToLatLng(L.point(0, 0));
|
||||||
|
const right = this._map.containerPointToLatLng(L.point(size.x, 0));
|
||||||
|
const sinPhi2 = Math.pow(Math.sin(center.lat * to_rad), 2);
|
||||||
|
const N = this._a / Math.sqrt(1.0 - this._e2 * sinPhi2);
|
||||||
|
const dl = this._interval / (N * Math.cos(center.lat * to_rad)) * to_deg;
|
||||||
|
|
||||||
|
// draw major ticks
|
||||||
|
for (let i = center.lng + dl / 2; i < right.lng; i = i + dl) this._draw_lon_tick(i, 10, weight * 1.5);
|
||||||
|
for (let i = center.lng - dl / 2; i > left.lng; i = i - dl) this._draw_lon_tick(i, 10, weight * 1.5);
|
||||||
|
|
||||||
|
// draw minor ticks
|
||||||
|
for (let i = center.lng; i < right.lng; i = i + dl / 10) this._draw_lon_tick(i, 4, weight);
|
||||||
|
for (let i = center.lng - dl / 10; i > left.lng; i = i - dl / 10) this._draw_lon_tick(i, 4, weight);
|
||||||
|
},
|
||||||
|
|
||||||
|
_setCanvasPosition: function() {
|
||||||
|
let lt = this._map.containerPointToLayerPoint([0, 0]);
|
||||||
|
/**
|
||||||
|
* @TODO add support for "leaflet-rotate"
|
||||||
|
*/
|
||||||
|
if (this._map._bearing) {
|
||||||
|
lt = this._map.rotatedPointToMapPanePoint(
|
||||||
|
this._map.containerPointToLayerPoint(L.point(this._map._container.getBoundingClientRect()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
L.DomUtil.setPosition(this._canvas, lt);
|
||||||
|
},
|
||||||
|
|
||||||
|
_latLngToCanvasPoint: function (latlng) {
|
||||||
|
return L.point(
|
||||||
|
this._map
|
||||||
|
.project(L.latLng(latlng))
|
||||||
|
._subtract(this._map.getPixelOrigin())
|
||||||
|
).add(this._map._getMapPanePos());
|
||||||
|
},
|
||||||
|
|
||||||
|
_draw_lat_tick: function (phi, lenght, weight) {
|
||||||
|
const to_deg = 180.0/Math.PI;
|
||||||
|
const size = this._map.getSize();
|
||||||
|
const y = this._latLngToCanvasPoint(L.latLng(phi * to_deg, 0.0)).y;
|
||||||
|
this._ctx.lineWidth = weight;
|
||||||
|
this._ctx.beginPath();
|
||||||
|
this._ctx.moveTo(0, y);
|
||||||
|
this._ctx.lineTo(+ lenght, y);
|
||||||
|
this._ctx.stroke();
|
||||||
|
},
|
||||||
|
|
||||||
|
_draw_lon_tick: function(lam, lenght, weight) {
|
||||||
|
const x = this._latLngToCanvasPoint(L.latLng(0.0, lam)).x;
|
||||||
|
this._ctx.lineWidth = weight;
|
||||||
|
this._ctx.beginPath();
|
||||||
|
this._ctx.moveTo(x, 0);
|
||||||
|
this._ctx.lineTo(x, lenght);
|
||||||
|
this._ctx.stroke();
|
||||||
|
},
|
||||||
|
|
||||||
|
_merLength: function(phi) {
|
||||||
|
const cos2 = Math.cos(2.0 * phi);
|
||||||
|
const sin2 = Math.sin(2.0 * phi);
|
||||||
|
return this._A * (phi + sin2 * (this._c1 + (this._c2 + (this._c3 + (this._c4 + this._c5 * cos2) * cos2) *cos2) * cos2));
|
||||||
|
},
|
||||||
|
|
||||||
|
_invmerLength: function(s) {
|
||||||
|
const psi = s/this._A;
|
||||||
|
const cos2 = Math.cos(2.0 * psi);
|
||||||
|
const sin2 = Math.sin(2.0 * psi);
|
||||||
|
return psi + sin2 * (this._ic1 + (this._ic2 + (this._ic3 + (this._ic4 + this._ic5 * cos2) * cos2) * cos2) * cos2);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
L.control.edgeScale = function (options) {
|
||||||
|
return new L.Control.EdgeScale(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
L.Map.mergeOptions({
|
||||||
|
edgeScaleControl: false
|
||||||
|
});
|
||||||
|
|
||||||
|
L.Map.addInitHook(function () {
|
||||||
|
if (this.options.edgeScaleControl) {
|
||||||
|
this.edgeScaleControl = new L.Control.EdgeScale();
|
||||||
|
this.addControl(this.edgeScaleControl);
|
||||||
|
}
|
||||||
|
});
|
||||||
1
web/src/lib/vendor/leaflet-elevation/libs/leaflet-edgescale.min.js
vendored
Normal file
347
web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js
vendored
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
/*
|
||||||
|
* https://github.com/adoroszlai/joebed/tree/gh-pages
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2014- Doroszlai Attila, 2019- Raruto
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
L.Mixin.Selectable = {
|
||||||
|
includes: L.Mixin.Events,
|
||||||
|
|
||||||
|
setSelected: function(s) {
|
||||||
|
var selected = !!s;
|
||||||
|
if (this._selected !== selected) {
|
||||||
|
this._selected = selected;
|
||||||
|
this.fire('selected');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isSelected: function() {
|
||||||
|
return !!this._selected;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
L.Mixin.Selection = {
|
||||||
|
includes: L.Mixin.Events,
|
||||||
|
|
||||||
|
getSelection: function() {
|
||||||
|
return this._selected;
|
||||||
|
},
|
||||||
|
|
||||||
|
setSelection: function(item) {
|
||||||
|
if (this._selected === item) {
|
||||||
|
if (item !== null) {
|
||||||
|
item.setSelected(!item.isSelected());
|
||||||
|
if (!item.isSelected()) {
|
||||||
|
this._selected = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this._selected) {
|
||||||
|
this._selected.setSelected(false);
|
||||||
|
}
|
||||||
|
this._selected = item;
|
||||||
|
if (this._selected) {
|
||||||
|
this._selected.setSelected(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.fire('selection_changed');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
L.Control.LayersLegend = L.Control.Layers.extend({
|
||||||
|
_onInputClick: function() {
|
||||||
|
this._handlingClick = true;
|
||||||
|
|
||||||
|
this._layerControlInputs.reduceRight((_,input) => {
|
||||||
|
if (input.checked) {
|
||||||
|
this._map.fireEvent("legend_selected", {
|
||||||
|
layer: this._getLayer(input.layerId).layer,
|
||||||
|
input: input,
|
||||||
|
}, true);
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
this._handlingClick = false;
|
||||||
|
|
||||||
|
this._refocusOnMap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
L.control.layersLegend = (baseLayers, overlays, options) => new L.Control.LayersLegend(baseLayers, overlays, options);
|
||||||
|
|
||||||
|
L.GeoJSON.include(L.Mixin.Selectable);
|
||||||
|
|
||||||
|
L.GpxGroup = L.Class.extend({
|
||||||
|
options: {
|
||||||
|
highlight: {
|
||||||
|
color: '#ff0',
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
points: [],
|
||||||
|
points_options: {
|
||||||
|
icon: {
|
||||||
|
iconUrl: '../images/elevation-poi.png',
|
||||||
|
iconSize: [12, 12],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
flyToBounds: true,
|
||||||
|
legend: false,
|
||||||
|
legend_options: {
|
||||||
|
position: "topright",
|
||||||
|
collapsed: false,
|
||||||
|
},
|
||||||
|
elevation: true,
|
||||||
|
elevation_options: {
|
||||||
|
theme: 'yellow-theme',
|
||||||
|
detached: true,
|
||||||
|
elevationDiv: '#elevation-div',
|
||||||
|
},
|
||||||
|
distanceMarkers: true,
|
||||||
|
distanceMarkers_options: {
|
||||||
|
lazy: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function(tracks, options) {
|
||||||
|
|
||||||
|
L.Util.setOptions(this, options);
|
||||||
|
|
||||||
|
this._count = 0;
|
||||||
|
this._loadedCount = 0;
|
||||||
|
this._tracks = tracks;
|
||||||
|
this._layers = L.featureGroup();
|
||||||
|
this._markers = L.featureGroup();
|
||||||
|
this._elevation = L.control.elevation(this.options.elevation_options);
|
||||||
|
this._legend = L.control.layersLegend(null, null, this.options.legend_options);
|
||||||
|
|
||||||
|
this.options.points.forEach((poi) =>
|
||||||
|
L
|
||||||
|
.marker(poi.latlng, { icon: L.icon(this.options.points_options.icon) })
|
||||||
|
.bindTooltip(poi.name, { direction: 'auto' }).addTo(this._markers)
|
||||||
|
);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getBounds: function() {
|
||||||
|
return this._layers.getBounds();
|
||||||
|
},
|
||||||
|
|
||||||
|
addTo: function(map) {
|
||||||
|
this._layers.addTo(map);
|
||||||
|
this._markers.addTo(map);
|
||||||
|
|
||||||
|
this._map = map;
|
||||||
|
|
||||||
|
this.on('selection_changed', this._onSelectionChanged, this);
|
||||||
|
this._map.on('legend_selected', this._onLegendSelected, this);
|
||||||
|
this._tracks.forEach(this.addTrack, this);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
addTrack: function(track) {
|
||||||
|
if (track instanceof Object) {
|
||||||
|
this._loadGeoJSON(track);
|
||||||
|
} else {
|
||||||
|
fetch(track)
|
||||||
|
.then(response => response.ok && response.text())
|
||||||
|
.then(text => this._elevation._parseFromString(text))
|
||||||
|
.then(geojson => this._loadGeoJSON(geojson, track.split('/').pop().split('#')[0].split('?')[0]));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_loadGeoJSON: function(geojson, fallbackName) {
|
||||||
|
if (geojson) {
|
||||||
|
geojson.name = geojson.name || (geojson[0] && geojson[0].properties.name) || fallbackName;
|
||||||
|
this._loadRoute(geojson);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_loadRoute: function(data) {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
var line_style = {
|
||||||
|
color: this._uniqueColors(this._tracks.length)[this._count++],
|
||||||
|
opacity: 0.75,
|
||||||
|
weight: 5,
|
||||||
|
distanceMarkers: this.options.distanceMarkers_options,
|
||||||
|
};
|
||||||
|
|
||||||
|
var route = L.geoJson(data, {
|
||||||
|
name: data.name || '',
|
||||||
|
style: (feature) => line_style,
|
||||||
|
distanceMarkers: line_style.distanceMarkers,
|
||||||
|
originalStyle: line_style,
|
||||||
|
filter: feature => feature.geometry.type != "Point",
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
this._elevation.import(this._elevation.__LGEOMUTIL).then(() => {
|
||||||
|
route.addTo(this._layers);
|
||||||
|
|
||||||
|
route.eachLayer((layer) => this._onEachRouteLayer(route, layer));
|
||||||
|
this._onEachRouteLoaded(route);
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
_onEachRouteLayer: function(route, layer) {
|
||||||
|
var polyline = layer;
|
||||||
|
|
||||||
|
route.on('selected', L.bind(this._onRouteSelected, this, route, polyline));
|
||||||
|
|
||||||
|
polyline.on('mouseover', L.bind(this._onRouteMouseOver, this, route, polyline));
|
||||||
|
polyline.on('mouseout', L.bind(this._onRouteMouseOut, this, route, polyline));
|
||||||
|
polyline.on('click', L.bind(this._onRouteClick, this, route, polyline));
|
||||||
|
|
||||||
|
polyline.bindTooltip(route.options.name, { direction: 'auto', sticky: true, });
|
||||||
|
},
|
||||||
|
|
||||||
|
_onEachRouteLoaded: function(route) {
|
||||||
|
if (this.options.legend) {
|
||||||
|
this._legend.addBaseLayer(route, '<svg id="legend_' + route._leaflet_id + '" width="25" height="10" version="1.1" xmlns="http://www.w3.org/2000/svg">' + '<line x1="0" x2="50" y1="5" y2="5" stroke="' + route.options.originalStyle.color + '" fill="transparent" stroke-width="5" /></svg>' + ' ' + route.options.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fire('route_loaded', { route: route });
|
||||||
|
|
||||||
|
if (++this._loadedCount === this._tracks.length) {
|
||||||
|
this.fire('loaded');
|
||||||
|
if (this.options.flyToBounds) {
|
||||||
|
this._map.flyToBounds(this.getBounds(), { duration: 0.25, easeLinearity: 0.25, noMoveStart: true });
|
||||||
|
}
|
||||||
|
if (this.options.legend) {
|
||||||
|
this._legend.addTo(this._map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
highlight: function(route, polyline) {
|
||||||
|
polyline.setStyle(this.options.highlight);
|
||||||
|
if (this.options.distanceMarkers) {
|
||||||
|
polyline.addDistanceMarkers();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unhighlight: function(route, polyline) {
|
||||||
|
polyline.setStyle(route.options.originalStyle);
|
||||||
|
if (this.options.distanceMarkers) {
|
||||||
|
polyline.removeDistanceMarkers();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRouteMouseOver: function(route, polyline) {
|
||||||
|
if (!route.isSelected()) {
|
||||||
|
this.highlight(route, polyline);
|
||||||
|
if (this.options.legend) {
|
||||||
|
this.setSelection(route);
|
||||||
|
L.DomUtil.get('legend_' + route._leaflet_id).parentNode.previousSibling.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.fire('route_mouseover', { route: route, polyline: polyline });
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRouteMouseOut: function(route, polyline) {
|
||||||
|
if (!route.isSelected()) {
|
||||||
|
this.unhighlight(route, polyline);
|
||||||
|
}
|
||||||
|
this.fire('route_mouseout', { route: route, polyline: polyline });
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRouteClick: function(route, polyline) {
|
||||||
|
this.setSelection(route);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRouteSelected: function(route, polyline) {
|
||||||
|
if (!route.isSelected()) {
|
||||||
|
this.unhighlight(route, polyline);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onSelectionChanged: function(e) {
|
||||||
|
var elevation = this._elevation;
|
||||||
|
var eleDiv = elevation.getContainer();
|
||||||
|
var route = this.getSelection();
|
||||||
|
|
||||||
|
elevation.clear();
|
||||||
|
|
||||||
|
if (route && route.isSelected()) {
|
||||||
|
if (!eleDiv) {
|
||||||
|
elevation.addTo(this._map);
|
||||||
|
}
|
||||||
|
route.getLayers().forEach(function(layer) {
|
||||||
|
if (layer instanceof L.Polyline) {
|
||||||
|
elevation.addData(layer, false);
|
||||||
|
layer.bringToFront();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (eleDiv) {
|
||||||
|
elevation.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onLegendSelected: function(e) {
|
||||||
|
var parent = e.input.closest('.leaflet-control-layers-list');
|
||||||
|
var route = e.layer;
|
||||||
|
|
||||||
|
if (!route.isSelected()) {
|
||||||
|
this.setSelection(route);
|
||||||
|
for (var i in route._layers) {
|
||||||
|
this.highlight(route, route._layers[i]);
|
||||||
|
}
|
||||||
|
this._map.flyToBounds(e.layer.getBounds());
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.scroll({ top: (e.input.offsetTop - parent.offsetTop) || 0, behavior: 'smooth' });
|
||||||
|
|
||||||
|
this._layers.eachLayer(layer => {
|
||||||
|
var legend = L.DomUtil.get('legend_' + layer._leaflet_id);
|
||||||
|
legend.querySelector("line").style.stroke = layer.isSelected() ? this.options.highlight.color : "";
|
||||||
|
legend.parentNode.style.fontWeight = layer.isSelected() ? "700" : "";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_uniqueColors: function(count) {
|
||||||
|
return count === 1 ? ['#0000ff'] : new Array(count).fill(null).map((_,i) => this._hsvToHex(i * (1 / count), 1, 0.7));
|
||||||
|
},
|
||||||
|
|
||||||
|
_hsvToHex: function(h, s, v) {
|
||||||
|
var i = Math.floor(h * 6);
|
||||||
|
var f = h * 6 - i;
|
||||||
|
var p = v * (1 - s);
|
||||||
|
var q = v * (1 - f * s);
|
||||||
|
var t = v * (1 - (1 - f) * s);
|
||||||
|
var rgb = { 0: [v, t, p], 1: [q, v, p], 2: [p, v, t], 3: [p, q, v], 4: [t, p, v], 5: [v, p, q] }[i % 6];
|
||||||
|
return rgb.map(d => d * 255).reduce((hex, byte) => hex + ((byte >> 4) & 0x0F).toString(16) + (byte & 0x0F).toString(16), "#");
|
||||||
|
},
|
||||||
|
|
||||||
|
removeFrom: function(map) {
|
||||||
|
this._layers.removeFrom(map);
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
L.GpxGroup.include(L.Mixin.Events);
|
||||||
|
L.GpxGroup.include(L.Mixin.Selection);
|
||||||
|
|
||||||
|
L.gpxGroup = (tracks, options) => new L.GpxGroup(tracks, options);
|
||||||
1
web/src/lib/vendor/leaflet-elevation/libs/leaflet-gpxgroup.min.js
vendored
Normal file
457
web/src/lib/vendor/leaflet-elevation/libs/leaflet-hotline.js
vendored
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
/*
|
||||||
|
(c) 2017, iosphere GmbH
|
||||||
|
Leaflet.hotline, a Leaflet plugin for drawing gradients along polylines.
|
||||||
|
https://github.com/iosphere/Leaflet.hotline/
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function (root, plugin) {
|
||||||
|
/**
|
||||||
|
* UMD wrapper.
|
||||||
|
* When used directly in the Browser it expects Leaflet to be globally
|
||||||
|
* available as `L`. The plugin then adds itself to Leaflet.
|
||||||
|
* When used as a CommonJS module (e.g. with browserify) only the plugin
|
||||||
|
* factory gets exported, so one hast to call the factory manually and pass
|
||||||
|
* Leaflet as the only parameter.
|
||||||
|
* @see {@link https://github.com/umdjs/umd}
|
||||||
|
*/
|
||||||
|
if (typeof define === 'function' && define.amd) {
|
||||||
|
define(['leaflet'], plugin);
|
||||||
|
} else if (typeof exports === 'object') {
|
||||||
|
module.exports = plugin;
|
||||||
|
} else {
|
||||||
|
plugin(root.L);
|
||||||
|
}
|
||||||
|
}(window, function (L) {
|
||||||
|
// Plugin is already added to Leaflet
|
||||||
|
if (L.Hotline) {
|
||||||
|
return L;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core renderer.
|
||||||
|
* @constructor
|
||||||
|
* @param {HTMLElement | string} canvas - <canvas> element or its id
|
||||||
|
* to initialize the instance on.
|
||||||
|
*/
|
||||||
|
var Hotline = function (canvas) {
|
||||||
|
if (!(this instanceof Hotline)) { return new Hotline(canvas); }
|
||||||
|
|
||||||
|
var defaultPalette = {
|
||||||
|
0.0: 'green',
|
||||||
|
0.5: 'yellow',
|
||||||
|
1.0: 'red'
|
||||||
|
};
|
||||||
|
|
||||||
|
this._canvas = canvas = ('string' === typeof canvas)
|
||||||
|
? document.getElementById(canvas)
|
||||||
|
: canvas;
|
||||||
|
|
||||||
|
this._ctx = canvas.getContext('2d');
|
||||||
|
this._width = canvas.width;
|
||||||
|
this._height = canvas.height;
|
||||||
|
|
||||||
|
this._weight = 5;
|
||||||
|
this._outlineWidth = 1;
|
||||||
|
this._outlineColor = 'black';
|
||||||
|
|
||||||
|
this._min = 0;
|
||||||
|
this._max = 1;
|
||||||
|
|
||||||
|
this._data = [];
|
||||||
|
|
||||||
|
this.palette(defaultPalette);
|
||||||
|
};
|
||||||
|
|
||||||
|
Hotline.prototype = {
|
||||||
|
/**
|
||||||
|
* Sets the width of the canvas. Used when clearing the canvas.
|
||||||
|
* @param {number} width - Width of the canvas.
|
||||||
|
*/
|
||||||
|
width: function (width) {
|
||||||
|
this._width = width;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the height of the canvas. Used when clearing the canvas.
|
||||||
|
* @param {number} height - Height of the canvas.
|
||||||
|
*/
|
||||||
|
height: function (height) {
|
||||||
|
this._height = height;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the weight of the path.
|
||||||
|
* @param {number} weight - Weight of the path in px.
|
||||||
|
*/
|
||||||
|
weight: function (weight) {
|
||||||
|
this._weight = weight;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the width of the outline around the path.
|
||||||
|
* @param {number} outlineWidth - Width of the outline in px.
|
||||||
|
*/
|
||||||
|
outlineWidth: function (outlineWidth) {
|
||||||
|
this._outlineWidth = outlineWidth;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the color of the outline around the path.
|
||||||
|
* @param {string} outlineColor - A CSS color value.
|
||||||
|
*/
|
||||||
|
outlineColor: function (outlineColor) {
|
||||||
|
this._outlineColor = outlineColor;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the palette gradient.
|
||||||
|
* @param {Object.<number, string>} palette - Gradient definition.
|
||||||
|
* e.g. { 0.0: 'white', 1.0: 'black' }
|
||||||
|
*/
|
||||||
|
palette: function (palette) {
|
||||||
|
var canvas = document.createElement('canvas'),
|
||||||
|
ctx = canvas.getContext('2d'),
|
||||||
|
gradient = ctx.createLinearGradient(0, 0, 0, 256);
|
||||||
|
|
||||||
|
canvas.width = 1;
|
||||||
|
canvas.height = 256;
|
||||||
|
|
||||||
|
for (var i in palette) {
|
||||||
|
gradient.addColorStop(i, palette[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.fillRect(0, 0, 1, 256);
|
||||||
|
|
||||||
|
this._palette = ctx.getImageData(0, 0, 1, 256).data;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value used at the start of the palette gradient.
|
||||||
|
* @param {number} min
|
||||||
|
*/
|
||||||
|
min: function (min) {
|
||||||
|
this._min = min;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value used at the end of the palette gradient.
|
||||||
|
* @param {number} max
|
||||||
|
*/
|
||||||
|
max: function (max) {
|
||||||
|
this._max = max;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A path to rander as a hotline.
|
||||||
|
* @typedef Array.<{x:number, y:number, z:number}> Path - Array of x, y and z coordinates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the data that gets drawn on the canvas.
|
||||||
|
* @param {(Path|Path[])} data - A single path or an array of paths.
|
||||||
|
*/
|
||||||
|
data: function (data) {
|
||||||
|
this._data = data;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a path to the list of paths.
|
||||||
|
* @param {Path} path
|
||||||
|
*/
|
||||||
|
add: function (path) {
|
||||||
|
this._data.push(path);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the currently set paths.
|
||||||
|
*/
|
||||||
|
draw: function () {
|
||||||
|
var ctx = this._ctx;
|
||||||
|
|
||||||
|
ctx.globalCompositeOperation = 'source-over';
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
|
||||||
|
this._drawOutline(ctx);
|
||||||
|
this._drawHotline(ctx);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the RGB values of a given z value of the current palette.
|
||||||
|
* @param {number} value - Value to get the color for, should be between min and max.
|
||||||
|
* @returns {Array.<number>} The RGB values as an array [r, g, b]
|
||||||
|
*/
|
||||||
|
getRGBForValue: function (value) {
|
||||||
|
var valueRelative = Math.min(Math.max((value - this._min) / (this._max - this._min), 0), 0.999);
|
||||||
|
var paletteIndex = Math.floor(valueRelative * 256) * 4;
|
||||||
|
|
||||||
|
return [
|
||||||
|
this._palette[paletteIndex],
|
||||||
|
this._palette[paletteIndex + 1],
|
||||||
|
this._palette[paletteIndex + 2]
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the outline of the graphs.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_drawOutline: function (ctx) {
|
||||||
|
var i, j, dataLength, path, pathLength, pointStart, pointEnd;
|
||||||
|
|
||||||
|
if (this._outlineWidth) {
|
||||||
|
for (i = 0, dataLength = this._data.length; i < dataLength; i++) {
|
||||||
|
path = this._data[i];
|
||||||
|
ctx.lineWidth = this._weight + 2 * this._outlineWidth;
|
||||||
|
|
||||||
|
for (j = 1, pathLength = path.length; j < pathLength; j++) {
|
||||||
|
pointStart = path[j - 1];
|
||||||
|
pointEnd = path[j];
|
||||||
|
|
||||||
|
ctx.strokeStyle = this._outlineColor;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(pointStart.x, pointStart.y);
|
||||||
|
ctx.lineTo(pointEnd.x, pointEnd.y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the color encoded hotline of the graphs.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_drawHotline: function (ctx) {
|
||||||
|
var i, j, dataLength, path, pathLength, pointStart, pointEnd,
|
||||||
|
gradient, gradientStartRGB, gradientEndRGB;
|
||||||
|
|
||||||
|
ctx.lineWidth = this._weight;
|
||||||
|
|
||||||
|
for (i = 0, dataLength = this._data.length; i < dataLength; i++) {
|
||||||
|
path = this._data[i];
|
||||||
|
|
||||||
|
for (j = 1, pathLength = path.length; j < pathLength; j++) {
|
||||||
|
pointStart = path[j - 1];
|
||||||
|
pointEnd = path[j];
|
||||||
|
|
||||||
|
// Create a gradient for each segment, pick start end end colors from palette gradient
|
||||||
|
gradient = ctx.createLinearGradient(pointStart.x, pointStart.y, pointEnd.x, pointEnd.y);
|
||||||
|
gradientStartRGB = this.getRGBForValue(pointStart.z);
|
||||||
|
gradientEndRGB = this.getRGBForValue(pointEnd.z);
|
||||||
|
gradient.addColorStop(0, 'rgb(' + gradientStartRGB.join(',') + ')');
|
||||||
|
gradient.addColorStop(1, 'rgb(' + gradientEndRGB.join(',') + ')');
|
||||||
|
|
||||||
|
ctx.strokeStyle = gradient;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(pointStart.x, pointStart.y);
|
||||||
|
ctx.lineTo(pointEnd.x, pointEnd.y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var Renderer = L.Canvas.extend({
|
||||||
|
_initContainer: function () {
|
||||||
|
L.Canvas.prototype._initContainer.call(this);
|
||||||
|
this._hotline = new Hotline(this._container);
|
||||||
|
},
|
||||||
|
|
||||||
|
_update: function () {
|
||||||
|
L.Canvas.prototype._update.call(this);
|
||||||
|
this._hotline.width(this._container.width);
|
||||||
|
this._hotline.height(this._container.height);
|
||||||
|
},
|
||||||
|
|
||||||
|
_updatePoly: function (layer) {
|
||||||
|
if (!this._drawing) { return; }
|
||||||
|
|
||||||
|
var parts = layer._parts;
|
||||||
|
|
||||||
|
if (!parts.length) { return; }
|
||||||
|
|
||||||
|
this._updateOptions(layer);
|
||||||
|
|
||||||
|
this._hotline
|
||||||
|
.data(parts)
|
||||||
|
.draw();
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateOptions: function (layer) {
|
||||||
|
if (layer.options.min != null) {
|
||||||
|
this._hotline.min(layer.options.min);
|
||||||
|
}
|
||||||
|
if (layer.options.max != null) {
|
||||||
|
this._hotline.max(layer.options.max);
|
||||||
|
}
|
||||||
|
if (layer.options.weight != null) {
|
||||||
|
this._hotline.weight(layer.options.weight);
|
||||||
|
}
|
||||||
|
if (layer.options.outlineWidth != null) {
|
||||||
|
this._hotline.outlineWidth(layer.options.outlineWidth);
|
||||||
|
}
|
||||||
|
if (layer.options.outlineColor != null) {
|
||||||
|
this._hotline.outlineColor(layer.options.outlineColor);
|
||||||
|
}
|
||||||
|
if (layer.options.palette) {
|
||||||
|
this._hotline.palette(layer.options.palette);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var renderer = function (options) {
|
||||||
|
return L.Browser.canvas ? new Renderer(options) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var Util = {
|
||||||
|
/**
|
||||||
|
* This is just a copy of the original Leaflet version that support a third z coordinate.
|
||||||
|
* @see {@link http://leafletjs.com/reference.html#lineutil-clipsegment|Leaflet}
|
||||||
|
*/
|
||||||
|
clipSegment: function (a, b, bounds, useLastCode, round) {
|
||||||
|
var codeA = useLastCode ? this._lastCode : L.LineUtil._getBitCode(a, bounds),
|
||||||
|
codeB = L.LineUtil._getBitCode(b, bounds),
|
||||||
|
codeOut, p, newCode;
|
||||||
|
|
||||||
|
// save 2nd code to avoid calculating it on the next segment
|
||||||
|
this._lastCode = codeB;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// if a,b is inside the clip window (trivial accept)
|
||||||
|
if (!(codeA | codeB)) {
|
||||||
|
return [a, b];
|
||||||
|
// if a,b is outside the clip window (trivial reject)
|
||||||
|
} else if (codeA & codeB) {
|
||||||
|
return false;
|
||||||
|
// other cases
|
||||||
|
} else {
|
||||||
|
codeOut = codeA || codeB;
|
||||||
|
p = L.LineUtil._getEdgeIntersection(a, b, codeOut, bounds, round);
|
||||||
|
newCode = L.LineUtil._getBitCode(p, bounds);
|
||||||
|
|
||||||
|
if (codeOut === codeA) {
|
||||||
|
p.z = a.z;
|
||||||
|
a = p;
|
||||||
|
codeA = newCode;
|
||||||
|
} else {
|
||||||
|
p.z = b.z;
|
||||||
|
b = p;
|
||||||
|
codeB = newCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
L.Hotline = L.Polyline.extend({
|
||||||
|
statics: {
|
||||||
|
Renderer: Renderer,
|
||||||
|
renderer: renderer
|
||||||
|
},
|
||||||
|
|
||||||
|
options: {
|
||||||
|
renderer: renderer(),
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
palette: {
|
||||||
|
0.0: 'green',
|
||||||
|
0.5: 'yellow',
|
||||||
|
1.0: 'red'
|
||||||
|
},
|
||||||
|
weight: 5,
|
||||||
|
outlineColor: 'black',
|
||||||
|
outlineWidth: 1
|
||||||
|
},
|
||||||
|
|
||||||
|
getRGBForValue: function (value) {
|
||||||
|
return this._renderer._hotline.getRGBForValue(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just like the Leaflet version, but with support for a z coordinate.
|
||||||
|
*/
|
||||||
|
_projectLatlngs: function (latlngs, result, projectedBounds) {
|
||||||
|
var flat = latlngs[0] instanceof L.LatLng,
|
||||||
|
len = latlngs.length,
|
||||||
|
i, ring;
|
||||||
|
|
||||||
|
if (flat) {
|
||||||
|
ring = [];
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
ring[i] = this._map.latLngToLayerPoint(latlngs[i]);
|
||||||
|
// Add the altitude of the latLng as the z coordinate to the point
|
||||||
|
ring[i].z = latlngs[i].alt;
|
||||||
|
projectedBounds.extend(ring[i]);
|
||||||
|
}
|
||||||
|
result.push(ring);
|
||||||
|
} else {
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
this._projectLatlngs(latlngs[i], result, projectedBounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just like the Leaflet version, but uses `Util.clipSegment()`.
|
||||||
|
*/
|
||||||
|
_clipPoints: function () {
|
||||||
|
if (this.options.noClip) {
|
||||||
|
this._parts = this._rings;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._parts = [];
|
||||||
|
|
||||||
|
var parts = this._parts,
|
||||||
|
bounds = this._renderer._bounds,
|
||||||
|
i, j, k, len, len2, segment, points;
|
||||||
|
|
||||||
|
for (i = 0, k = 0, len = this._rings.length; i < len; i++) {
|
||||||
|
points = this._rings[i];
|
||||||
|
|
||||||
|
for (j = 0, len2 = points.length; j < len2 - 1; j++) {
|
||||||
|
segment = Util.clipSegment(points[j], points[j + 1], bounds, j, true);
|
||||||
|
|
||||||
|
if (!segment) { continue; }
|
||||||
|
|
||||||
|
parts[k] = parts[k] || [];
|
||||||
|
parts[k].push(segment[0]);
|
||||||
|
|
||||||
|
// if segment goes out of screen, or it's the last one, it's the end of the line part
|
||||||
|
if ((segment[1] !== points[j + 1]) || (j === len2 - 2)) {
|
||||||
|
parts[k].push(segment[1]);
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_clickTolerance: function () {
|
||||||
|
return this.options.weight / 2 + this.options.outlineWidth + (L.Browser.touch ? 10 : 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
L.hotline = function (latlngs, options) {
|
||||||
|
return new L.Hotline(latlngs, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return L;
|
||||||
|
}));
|
||||||
1
web/src/lib/vendor/leaflet-elevation/libs/leaflet-hotline.min.js
vendored
Normal file
42
web/src/lib/vendor/leaflet-elevation/libs/leaflet-ruler.css
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
.leaflet-ruler {
|
||||||
|
height: 35px;
|
||||||
|
width: 35px;
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAACx0lEQVRIS52VWahOURTHXRSJzBkiJcpYxgeS4oUi90GR4VK6DzcUD3gQIQ8UCXkwlCJRkjJEpqT7oGTMeHONmacyRRl/v9ve2n2d7/vOtevXPmefff5r7bXXXruiSf42gKkToTu0hY/wEk7CnWIyFWX0B/N9OkyF1tANdsEz6AIL4Sl8h8NwEK6nmsUMNGPSalgePFxLfwl2hrG39O1hM8yFobAKpsAGWAE/NJRloBfj++ErnIYesDj1qsjzRsYN2VhwdTPgUaGBkUF0e/D0TxDrRD8ehsFw6AcP4FrgCL17EpsrWAKDUgNtwuQP9FehJsyeSb8FbsOV8O0efVeYHOZV0h8N8w3dWXBOVWpgDwO/oBqc1BxciV7Pg3OJhz66ivPB+PoM8TnqRQPTeNkKfeALtAQ39QYsgE85xNsFz+voFe8AldFALS8DYU3waAd9b5gAv3OKn2HefagK4q64VgPm9k0we0wtN9pN6w+mY9qywqLnWeKXGa/WwHwYFSwrZtw/w9Ic4p5oxesLPG8Qd/UacMf13IE38ArGwN3EQJbnintOHsJsMOaG5Z+4/2vAY282eALd4FvQqoj4Aca3gQfPw1hSPBr4xoNH3bz1EO0D428r9LwpY3vB+nSolOfRQVeg1yOCgXH01pu+GeL+Y1hOwWOYBZlhieJxBU94eA2Lwo9WRw+X8fUEx0PUaPFowKW6Cg+UBc6M6AzrMsR1xtJR1vO4CkNk1TMLJoXBlcHQpvAePW+0eFyBRe45jAbL7fskhqm4MbdGmYruUU94l8zNfIyl4hhfLQteHqaiLYq7J4YlipvnXkgWRotgyRYNWB4st6bpizLiHsiO4H1gMrhnRVtarpcxy0vd+9eLPMvzhuMf1E7QmyC78xrQmDk+BC6Am98CLoKXi/dvWll1xjvjeAkD9YVXpkJ6ae7/BOPus4b+p9X9BdB/zv8zawwXAAAAAElFTkSuQmCC"); /* <div>Icons made by <a href="http://www.freepik.com" title="Freepik">Freepik</a> from <a href="http://www.flaticon.com" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a></div> */
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
.leaflet-ruler-clicked,
|
||||||
|
.leaflet-ruler:hover {
|
||||||
|
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAE/ElEQVRIS6WVeWwUVRzHfzOzs7Pdi+7Rdq/ubrH0sNaKBFMCSEvBQLivFkhAE8GDYAyHATWgETUoMZ7BGCMBNKYiQbTKpYQKBqKlWmBbadmW7bbbFjv0mu2eM298M5SlpS3U+LL/zL6Zz/f7vr/3e4+AsQ6j80EgxTkKhGwkQRqQiLp5UtEOSDwOXf660TDEPfkGR75RTT8VF1EpJRJ6LsbrVuaZg2YtrWND8WD5FVajV9IcTyCOoYhv2QgcgE5fzWDmaAKU0eHa3RMStszNMqIdM+zUY3YdPFvRCG+VOCFFQ0N3mIfNJ3ywb3Em/BHgYNeZFnTc20OOU1Efd7eZtwBUxyWh4QLJGS6jFv04MU2bNWeCQRnoi8L7czPuG+RLJ31g0Smhsqk3dqGVa7wZZOZBb/31oQKm8ZMZRbxyU6FV9XaJmyQGZsNxBD/Ud8FXNWy/rzci9ER4Spqa5tYrit3jmNI8M2DnCRNvnm1Bu35tjcR4lH1HIDtbp+2NNuSmqCyTrBr4dMEDuH4A754LCLvOtQo44zouKp7hQagGIK+CIFgoipqPEHru6OocWJhtlAWk6GZ8cSXs64sd49qbSxMCaenu7+ZkGuftW5JJSy/FBARlh+vDF1tDfWEerYWullNDcjK5c9Q0Or/jcad++3SbbP8ueBn+S7glkOIsNTDUQf+mRxmtkoIIj6Bgb024hYtVhGPK9dDl7RsJvhPDtw3AcWzYuSd8vTcqOS8DrcUIKsUiWcDgyKghRLFgZ1E6vFhohfXfN8YP1bFVfW3N0/E0Ggu8CMNxfY9z7b5SCU4o6dMkhc4RYM61aulwU8fWSSqaIqEqEIRZBzzBSCg+HoIdncPglHgBG9ENdj4inEAXhZuBdQSYXBuWZOnfOLI61yTB1ld4Y1/+xe6Ndvo33Q+ON5Rc0GHOB+DS6gmF2XF6dkZy0eeLM8lU3EDGd6qiQRAnQnvz3wkBXFANdr5jkHMJXrS/NtTIRk5yN3wrErEMgsuNpkp13ViRZ0zdOtUOWiUJ2R/WxPibfmYk+Mp8E/XCT01y4y0/1HBfuCygMDljVzYW0DnmJKhuC8Lsg7WB7kCzQxa4y7nUF08euQblHlZMoqij93J+2yBBpzgjlzcUMJLAmeu9sOjrq+1cR7PtNvzVYrv+5WkOUvpAiqUYx+JlI6cwfPlosQyuHcGYnYECq9r2AV62O5mB9PcuItxjj2ho6uz/hcsRESbH4TX5qQv3LsigNbjJ7Huq+tkIT75enM4Mdx76mbvhXzYW54mIwOBeNd2l+eTs03nyYYIPqrhaSVGbC213xfLf4fIKAB9yqp4IW/VMvtKqVYJJrUhEeCdzGS5lbki22Wu5btYg0IwV2hrYIb0ywoN8VCTbXadDUTRz/9IJsCrfLL82CP7LQCyGcVab56HColQkCKL3ctWxTs/5+WMSAHwPGBih0rNxotqGL42R4DqLpebhKTNta7fvgWBfN7yybIoQjYdzoLPVey+RxHFtdrhfy05J2laxKjep5EAd3oqhhHNtqrm6YOoT6RKcIOXSwEdb1ojXLv35fDRQ99mYBKR6GOyu3zBhcowXTvR3+JdCWhqjVhk9FEW7Z5Wtw1vuFlwatb9XQmuD51J/MFQ+qgAheIdemSl5FoDgOuhs3o0/4vWOKcZkh/4bEJFKxAMA/6QhgsjzAvrH3+SXn0YZAinU/wuTosgK53+p+wAAAABJRU5ErkJggg=="); /* <div>Icons made by <a href="http://www.freepik.com" title="Freepik">Freepik</a> from <a href="http://www.flaticon.com" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a></div> */
|
||||||
|
}
|
||||||
|
.leaflet-ruler-clicked {
|
||||||
|
height: 35px;
|
||||||
|
width: 35px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
border-color: chartreuse !important;
|
||||||
|
}
|
||||||
|
.leaflet-bar.leaflet-ruler {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
.leaflet-control.leaflet-ruler {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.result-tooltip {
|
||||||
|
background-color: white;
|
||||||
|
border-width: medium;
|
||||||
|
border-color: #de0000;
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
.moving-tooltip {
|
||||||
|
background-color: rgba(255, 255, 255, .7);
|
||||||
|
background-clip: padding-box;
|
||||||
|
opacity: 0.5;
|
||||||
|
border: dotted;
|
||||||
|
border-color: red;
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
.plus-length {
|
||||||
|
padding-left: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
183
web/src/lib/vendor/leaflet-elevation/libs/leaflet-ruler.js
vendored
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022, 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) 2017 Goker Tanrisever
|
||||||
|
*
|
||||||
|
* 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.Ruler = L.Control.extend({
|
||||||
|
options: {
|
||||||
|
position: 'topright',
|
||||||
|
circleMarker: {
|
||||||
|
color: 'red',
|
||||||
|
radius: 2,
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
color: 'red',
|
||||||
|
dashArray: '1,6'
|
||||||
|
},
|
||||||
|
lengthUnit: {
|
||||||
|
display: 'km',
|
||||||
|
decimal: 2,
|
||||||
|
factor: 0.001, // meters -> kilometers
|
||||||
|
label: 'Distance:'
|
||||||
|
},
|
||||||
|
angleUnit: {
|
||||||
|
display: '°',
|
||||||
|
decimal: 2,
|
||||||
|
factor: 360,
|
||||||
|
label: 'Bearing:'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initialize: function (options) {
|
||||||
|
L.setOptions(this, options);
|
||||||
|
this._layers = L.layerGroup();
|
||||||
|
this._enabled = false;
|
||||||
|
},
|
||||||
|
onAdd: function (map) {
|
||||||
|
this._defaultCursor = map._container.style.cursor;
|
||||||
|
this._map = map;
|
||||||
|
let container = L.DomUtil.create('div', 'leaflet-bar');
|
||||||
|
container.classList.add('leaflet-ruler');
|
||||||
|
L.DomEvent.disableClickPropagation(container);
|
||||||
|
L.DomEvent.on(container, 'click', this._toggleMeasure, this);
|
||||||
|
return this._container = container;
|
||||||
|
},
|
||||||
|
onRemove: function () {
|
||||||
|
L.DomEvent.off(this._container, 'click', this._toggleMeasure, this);
|
||||||
|
},
|
||||||
|
_attachMouseEvents: function () {
|
||||||
|
let map = this._map;
|
||||||
|
map.doubleClickZoom.disable();
|
||||||
|
L.DomEvent.on(map._container, 'keydown', this._escape, this);
|
||||||
|
L.DomEvent.on(map._container, 'dblclick', this._closePath, this);
|
||||||
|
map._container.style.cursor = 'crosshair';
|
||||||
|
map.on('click', this._addPoint, this);
|
||||||
|
map.on('mousemove', this._moving, this);
|
||||||
|
},
|
||||||
|
_removeMouseEvents: function () {
|
||||||
|
let map = this._map;
|
||||||
|
map.doubleClickZoom.enable();
|
||||||
|
L.DomEvent.off(map._container, 'keydown', this._escape, this);
|
||||||
|
L.DomEvent.off(map._container, 'dblclick', this._closePath, this);
|
||||||
|
map._container.style.cursor = this._defaultCursor;
|
||||||
|
map.off('click', this._addPoint, this);
|
||||||
|
map.off('mousemove', this._moving, this);
|
||||||
|
},
|
||||||
|
_disable: function () {
|
||||||
|
this._enabled = false;
|
||||||
|
this._container.classList.remove("leaflet-ruler-clicked");
|
||||||
|
this._layers.remove().clearLayers();
|
||||||
|
this._latlngs = [];
|
||||||
|
this._totalLength = 0;
|
||||||
|
this._removeMouseEvents();
|
||||||
|
},
|
||||||
|
_enable: function () {
|
||||||
|
this._enabled = true;
|
||||||
|
this._container.classList.add("leaflet-ruler-clicked");
|
||||||
|
this._circles = L.featureGroup().addTo(this._layers);
|
||||||
|
this._polyline = L.polyline([], this.options.lineStyle).addTo(this._layers);
|
||||||
|
this._layers.addTo(this._map);
|
||||||
|
this._latlngs = [];
|
||||||
|
this._totalLength = 0;
|
||||||
|
this._attachMouseEvents();
|
||||||
|
},
|
||||||
|
_toggleMeasure: function () {
|
||||||
|
this._enabled ? this._disable() : this._enable();
|
||||||
|
},
|
||||||
|
_drawTooltip: function (latlng, layer, incremental) {
|
||||||
|
let lastClick = this._latlngs[this._latlngs.length - 1] ?? latlng;
|
||||||
|
let bearing = this._calculateBearing(lastClick, latlng);
|
||||||
|
let distance = lastClick.distanceTo(latlng) * this.options.lengthUnit.factor;
|
||||||
|
let accumulated = this._totalLength + distance;
|
||||||
|
let totalLength = accumulated.toFixed(this.options.lengthUnit.decimal);
|
||||||
|
let plusLength = incremental ? '<br><div class="plus-length">(+' + distance.toFixed(this.options.lengthUnit.decimal) + ')</div>' : '';
|
||||||
|
this._totalLength = incremental ? this._totalLength : accumulated;
|
||||||
|
if (!layer.getTooltip()) layer.bindTooltip('', incremental ? { direction: "auto", sticky: true, offset: L.point(0, -40), className: 'moving-tooltip' } : { permanent: true, className: 'result-tooltip' }).openTooltip();
|
||||||
|
layer.setLatLng(latlng).setTooltipContent('<b>' + this.options.angleUnit.label + '</b> ' + bearing.toFixed(this.options.angleUnit.decimal) + ' ' + this.options.angleUnit.display + '<br><b>' + this.options.lengthUnit.label + '</b> ' + totalLength + ' ' + this.options.lengthUnit.display + plusLength);
|
||||||
|
},
|
||||||
|
_addPoint: function (e) {
|
||||||
|
let latlng = e.latlng || e;
|
||||||
|
let point = L.circleMarker(latlng, this.options.circleMarker).addTo(this._circles);
|
||||||
|
this._polyline.addLatLng(latlng);
|
||||||
|
if(this._latlngs.length && !latlng.equals(this._latlngs[this._latlngs.length - 1])){
|
||||||
|
this._drawTooltip(latlng, point, false);
|
||||||
|
}
|
||||||
|
this._latlngs.push(latlng);
|
||||||
|
},
|
||||||
|
_moving: function (e) {
|
||||||
|
if (this._latlngs.length) {
|
||||||
|
let lastCLick = this._latlngs[this._latlngs.length - 1];
|
||||||
|
if (!this._tempLine) this._tempLine = L.polyline([], this.options.lineStyle).addTo(this._map);
|
||||||
|
if (!this._tempPoint) this._tempPoint = L.circleMarker(e.latlng, this.options.circleMarker).addTo(this._map);
|
||||||
|
this._tempLine.setLatLngs([lastCLick, e.latlng]);
|
||||||
|
this._drawTooltip(e.latlng, this._tempPoint, true);
|
||||||
|
L.DomEvent.off(this._container, 'click', this._toggleMeasure, this);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_escape: function (e) {
|
||||||
|
if (e.keyCode === 27) {
|
||||||
|
if (this._latlngs.length) {
|
||||||
|
this._closePath();
|
||||||
|
} else {
|
||||||
|
this._enabled = true;
|
||||||
|
this._toggleMeasure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_calculateBearing: function (start, end) {
|
||||||
|
const toRad = L.DomUtil.DEG_TO_RAD;
|
||||||
|
const toDeg = (this.options.angleUnit.factor / 2) / Math.PI;
|
||||||
|
let y = Math.sin((end.lng - start.lng) * toRad) * Math.cos(end.lat * toRad);
|
||||||
|
let x = Math.cos(start.lat * toRad) * Math.sin(end.lat * toRad) - Math.sin(start.lat * toRad) * Math.cos(end.lat * toRad) * Math.cos((end.lng - start.lng) * toRad);
|
||||||
|
return (Math.atan2(y, x) * toDeg + this.options.angleUnit.factor) % this.options.angleUnit.factor;
|
||||||
|
},
|
||||||
|
_closePath: function () {
|
||||||
|
if (this._tempLine) {
|
||||||
|
this._tempLine.remove();
|
||||||
|
this._tempLine = null;
|
||||||
|
}
|
||||||
|
if (this._tempPoint) {
|
||||||
|
this._tempPoint.remove();
|
||||||
|
this._tempLine = null;
|
||||||
|
}
|
||||||
|
if (this._latlngs.length <= 1) {
|
||||||
|
this._circles.remove();
|
||||||
|
}
|
||||||
|
this._enabled = false;
|
||||||
|
L.DomEvent.on(this._container, 'click', this._toggleMeasure, this);
|
||||||
|
this._toggleMeasure();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
L.control.ruler = function (options) {
|
||||||
|
return new L.Control.Ruler(options);
|
||||||
|
};
|
||||||
1
web/src/lib/vendor/leaflet-elevation/libs/leaflet-ruler.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.leaflet-ruler{height:35px;width:35px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAACx0lEQVRIS52VWahOURTHXRSJzBkiJcpYxgeS4oUi90GR4VK6DzcUD3gQIQ8UCXkwlCJRkjJEpqT7oGTMeHONmacyRRl/v9ve2n2d7/vOtevXPmefff5r7bXXXruiSf42gKkToTu0hY/wEk7CnWIyFWX0B/N9OkyF1tANdsEz6AIL4Sl8h8NwEK6nmsUMNGPSalgePFxLfwl2hrG39O1hM8yFobAKpsAGWAE/NJRloBfj++ErnIYesDj1qsjzRsYN2VhwdTPgUaGBkUF0e/D0TxDrRD8ehsFw6AcP4FrgCL17EpsrWAKDUgNtwuQP9FehJsyeSb8FbsOV8O0efVeYHOZV0h8N8w3dWXBOVWpgDwO/oBqc1BxciV7Pg3OJhz66ivPB+PoM8TnqRQPTeNkKfeALtAQ39QYsgE85xNsFz+voFe8AldFALS8DYU3waAd9b5gAv3OKn2HefagK4q64VgPm9k0we0wtN9pN6w+mY9qywqLnWeKXGa/WwHwYFSwrZtw/w9Ic4p5oxesLPG8Qd/UacMf13IE38ArGwN3EQJbnintOHsJsMOaG5Z+4/2vAY282eALd4FvQqoj4Aca3gQfPw1hSPBr4xoNH3bz1EO0D428r9LwpY3vB+nSolOfRQVeg1yOCgXH01pu+GeL+Y1hOwWOYBZlhieJxBU94eA2Lwo9WRw+X8fUEx0PUaPFowKW6Cg+UBc6M6AzrMsR1xtJR1vO4CkNk1TMLJoXBlcHQpvAePW+0eFyBRe45jAbL7fskhqm4MbdGmYruUU94l8zNfIyl4hhfLQteHqaiLYq7J4YlipvnXkgWRotgyRYNWB4st6bpizLiHsiO4H1gMrhnRVtarpcxy0vd+9eLPMvzhuMf1E7QmyC78xrQmDk+BC6Am98CLoKXi/dvWll1xjvjeAkD9YVXpkJ6ae7/BOPus4b+p9X9BdB/zv8zawwXAAAAAElFTkSuQmCC);background-repeat:no-repeat;background-position:center}.leaflet-ruler-clicked,.leaflet-ruler:hover{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAE/ElEQVRIS6WVeWwUVRzHfzOzs7Pdi+7Rdq/ubrH0sNaKBFMCSEvBQLivFkhAE8GDYAyHATWgETUoMZ7BGCMBNKYiQbTKpYQKBqKlWmBbadmW7bbbFjv0mu2eM298M5SlpS3U+LL/zL6Zz/f7vr/3e4+AsQ6j80EgxTkKhGwkQRqQiLp5UtEOSDwOXf660TDEPfkGR75RTT8VF1EpJRJ6LsbrVuaZg2YtrWND8WD5FVajV9IcTyCOoYhv2QgcgE5fzWDmaAKU0eHa3RMStszNMqIdM+zUY3YdPFvRCG+VOCFFQ0N3mIfNJ3ywb3Em/BHgYNeZFnTc20OOU1Efd7eZtwBUxyWh4QLJGS6jFv04MU2bNWeCQRnoi8L7czPuG+RLJ31g0Smhsqk3dqGVa7wZZOZBb/31oQKm8ZMZRbxyU6FV9XaJmyQGZsNxBD/Ud8FXNWy/rzci9ER4Spqa5tYrit3jmNI8M2DnCRNvnm1Bu35tjcR4lH1HIDtbp+2NNuSmqCyTrBr4dMEDuH4A754LCLvOtQo44zouKp7hQagGIK+CIFgoipqPEHru6OocWJhtlAWk6GZ8cSXs64sd49qbSxMCaenu7+ZkGuftW5JJSy/FBARlh+vDF1tDfWEerYWullNDcjK5c9Q0Or/jcad++3SbbP8ueBn+S7glkOIsNTDUQf+mRxmtkoIIj6Bgb024hYtVhGPK9dDl7RsJvhPDtw3AcWzYuSd8vTcqOS8DrcUIKsUiWcDgyKghRLFgZ1E6vFhohfXfN8YP1bFVfW3N0/E0Ggu8CMNxfY9z7b5SCU4o6dMkhc4RYM61aulwU8fWSSqaIqEqEIRZBzzBSCg+HoIdncPglHgBG9ENdj4inEAXhZuBdQSYXBuWZOnfOLI61yTB1ld4Y1/+xe6Ndvo33Q+ON5Rc0GHOB+DS6gmF2XF6dkZy0eeLM8lU3EDGd6qiQRAnQnvz3wkBXFANdr5jkHMJXrS/NtTIRk5yN3wrErEMgsuNpkp13ViRZ0zdOtUOWiUJ2R/WxPibfmYk+Mp8E/XCT01y4y0/1HBfuCygMDljVzYW0DnmJKhuC8Lsg7WB7kCzQxa4y7nUF08euQblHlZMoqij93J+2yBBpzgjlzcUMJLAmeu9sOjrq+1cR7PtNvzVYrv+5WkOUvpAiqUYx+JlI6cwfPlosQyuHcGYnYECq9r2AV62O5mB9PcuItxjj2ho6uz/hcsRESbH4TX5qQv3LsigNbjJ7Huq+tkIT75enM4Mdx76mbvhXzYW54mIwOBeNd2l+eTs03nyYYIPqrhaSVGbC213xfLf4fIKAB9yqp4IW/VMvtKqVYJJrUhEeCdzGS5lbki22Wu5btYg0IwV2hrYIb0ywoN8VCTbXadDUTRz/9IJsCrfLL82CP7LQCyGcVab56HColQkCKL3ctWxTs/5+WMSAHwPGBih0rNxotqGL42R4DqLpebhKTNta7fvgWBfN7yybIoQjYdzoLPVey+RxHFtdrhfy05J2laxKjep5EAd3oqhhHNtqrm6YOoT6RKcIOXSwEdb1ojXLv35fDRQ99mYBKR6GOyu3zBhcowXTvR3+JdCWhqjVhk9FEW7Z5Wtw1vuFlwatb9XQmuD51J/MFQ+qgAheIdemSl5FoDgOuhs3o0/4vWOKcZkh/4bEJFKxAMA/6QhgsjzAvrH3+SXn0YZAinU/wuTosgK53+p+wAAAABJRU5ErkJggg==)}.leaflet-ruler-clicked{height:35px;width:35px;background-repeat:no-repeat;background-position:center;border-color:#7fff00!important}.leaflet-bar.leaflet-ruler{background-color:#fff}.leaflet-control.leaflet-ruler{cursor:pointer}.result-tooltip{background-color:#fff;border-width:medium;border-color:#de0000;font-size:smaller}.moving-tooltip{background-color:rgba(255,255,255,.7);background-clip:padding-box;opacity:.5;border:dotted;border-color:red;font-size:smaller}.plus-length{padding-left:45px}
|
||||||
1
web/src/lib/vendor/leaflet-elevation/libs/leaflet-ruler.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
L.Control.Ruler=L.Control.extend({options:{position:"topright",circleMarker:{color:"red",radius:2},lineStyle:{color:"red",dashArray:"1,6"},lengthUnit:{display:"km",decimal:2,factor:.001,label:"Distance:"},angleUnit:{display:"°",decimal:2,factor:360,label:"Bearing:"}},initialize:function(options){L.setOptions(this,options),this._layers=L.layerGroup(),this._enabled=!1},onAdd:function(map){this._defaultCursor=map._container.style.cursor,this._map=map;let container=L.DomUtil.create("div","leaflet-bar");return container.classList.add("leaflet-ruler"),L.DomEvent.disableClickPropagation(container),L.DomEvent.on(container,"click",this._toggleMeasure,this),this._container=container},onRemove:function(){L.DomEvent.off(this._container,"click",this._toggleMeasure,this)},_attachMouseEvents:function(){let map=this._map;map.doubleClickZoom.disable(),L.DomEvent.on(map._container,"keydown",this._escape,this),L.DomEvent.on(map._container,"dblclick",this._closePath,this),map._container.style.cursor="crosshair",map.on("click",this._addPoint,this),map.on("mousemove",this._moving,this)},_removeMouseEvents:function(){let map=this._map;map.doubleClickZoom.enable(),L.DomEvent.off(map._container,"keydown",this._escape,this),L.DomEvent.off(map._container,"dblclick",this._closePath,this),map._container.style.cursor=this._defaultCursor,map.off("click",this._addPoint,this),map.off("mousemove",this._moving,this)},_disable:function(){this._enabled=!1,this._container.classList.remove("leaflet-ruler-clicked"),this._map.removeLayer(this._layers),this._layers=L.layerGroup(),this._latlngs=[],this._totalLength=0,this._removeMouseEvents()},_enable:function(){this._enabled=!0,this._container.classList.add("leaflet-ruler-clicked"),this._circles=L.featureGroup().addTo(this._layers),this._polyline=L.polyline([],this.options.lineStyle).addTo(this._layers),this._layers.addTo(this._map),this._latlngs=[],this._totalLength=0,this._attachMouseEvents()},_toggleMeasure:function(){this._enabled?this._disable():this._enable()},_drawTooltip:function(latlng,layer,incremental){let clickCount=this._latlngs.length,lastClick=clickCount?this._latlngs[clickCount-1]:latlng,bearing=this._calculateBearing(lastClick,latlng),distance=lastClick.distanceTo(latlng)*this.options.lengthUnit.factor,totalLength,plusLength;incremental?(totalLength=(clickCount?distance+this._totalLength||0:distance).toFixed(this.options.lengthUnit.decimal),plusLength=clickCount?'<br><div class="plus-length">(+'+distance.toFixed(this.options.lengthUnit.decimal)+")</div>":""):(this._totalLength+=distance,totalLength=(clickCount?this._totalLength:distance).toFixed(this.options.lengthUnit.decimal),plusLength="");var text="<b>"+this.options.angleUnit.label+"</b> "+bearing.toFixed(this.options.angleUnit.decimal)+" "+this.options.angleUnit.display+"<br><b>"+this.options.lengthUnit.label+"</b> "+totalLength+" "+this.options.lengthUnit.display+plusLength;layer.setLatLng(latlng);let tooltip=layer.getTooltip();return tooltip?tooltip.setTooltipContent(text):layer.bindTooltip(text,incremental?{direction:"auto",sticky:!0,offset:L.point(0,-40),className:"moving-tooltip"}:{permanent:!0,className:"result-tooltip"}).openTooltip(),layer},_addPoint:function(e){let latlng=e.latlng||e,point=L.circleMarker(latlng,this.options.circleMarker).addTo(this._circles);this._polyline.addLatLng(latlng),this._latlngs.length&&!latlng.equals(this._latlngs[this._latlngs.length-1])&&this._drawTooltip(latlng,point,!1),this._latlngs.push(latlng)},_moving:function(e){if(this._latlngs.length){let lastCLick=this._latlngs[this._latlngs.length-1];this._tempLine||(this._tempLine=L.polyline([],this.options.lineStyle).addTo(this._map)),this._tempPoint||(this._tempPoint=L.circleMarker(e.latlng,this.options.circleMarker).addTo(this._map)),this._tempLine.setLatLngs([lastCLick,e.latlng]),this._drawTooltip(e.latlng,this._tempPoint,!0),L.DomEvent.off(this._container,"click",this._toggleMeasure,this)}},_escape:function(e){27===e.keyCode&&(this._latlngs.length?this._closePath():(this._enabled=!0,this._toggleMeasure()))},_calculateBearing:function(start,end){const toRad=L.DomUtil.DEG_TO_RAD,toDeg=this.options.angleUnit.factor/2/Math.PI;let y=Math.sin((end.lng-start.lng)*toRad)*Math.cos(end.lat*toRad),x=Math.cos(start.lat*toRad)*Math.sin(end.lat*toRad)-Math.sin(start.lat*toRad)*Math.cos(end.lat*toRad)*Math.cos((end.lng-start.lng)*toRad);return(Math.atan2(y,x)*toDeg+this.options.angleUnit.factor)%this.options.angleUnit.factor},_closePath:function(){this._tempLine&&(this._map.removeLayer(this._tempLine),this._tempLine=null),this._tempPoint&&(this._map.removeLayer(this._tempPoint),this._tempLine=null),this._latlngs.length<=1&&this._map.removeLayer(this._circles),this._enabled=!1,L.DomEvent.on(this._container,"click",this._toggleMeasure,this),this._toggleMeasure()}}),L.control.ruler=function(options){return new L.Control.Ruler(options)};
|
||||||
783
web/src/lib/vendor/leaflet-elevation/src/components/chart.js
vendored
Normal file
@@ -0,0 +1,783 @@
|
|||||||
|
import * as D3 from './d3.js';
|
||||||
|
|
||||||
|
const _ = L.Control.Elevation.Utils;
|
||||||
|
|
||||||
|
export var Chart = L.Control.Elevation.Chart = L.Class.extend({
|
||||||
|
|
||||||
|
includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
|
||||||
|
|
||||||
|
initialize(opts, control) {
|
||||||
|
this.options = opts;
|
||||||
|
this.control = control;
|
||||||
|
|
||||||
|
this._data = control._data || [];
|
||||||
|
|
||||||
|
// cache registered components
|
||||||
|
this._props = {
|
||||||
|
scales : {},
|
||||||
|
paths : {},
|
||||||
|
areas : {},
|
||||||
|
grids : {},
|
||||||
|
axes : {},
|
||||||
|
legendItems : {},
|
||||||
|
tooltipItems: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
this._scales = {};
|
||||||
|
this._domains = {};
|
||||||
|
this._ranges = {};
|
||||||
|
this._paths = {};
|
||||||
|
|
||||||
|
this._brushEnabled = opts.dragging;
|
||||||
|
this._zoomEnabled = opts.zooming;
|
||||||
|
|
||||||
|
opts.xTicks = this._xTicks();
|
||||||
|
opts.yTicks = this._yTicks();
|
||||||
|
|
||||||
|
let chart = this._chart = D3.Chart(opts);
|
||||||
|
|
||||||
|
// SVG Container
|
||||||
|
this._container = chart.svg;
|
||||||
|
|
||||||
|
// Panes
|
||||||
|
this._grid = chart.pane('grid');
|
||||||
|
this._area = chart.pane('area');
|
||||||
|
this._point = chart.pane('point');
|
||||||
|
this._axis = chart.pane('axis');
|
||||||
|
this._legend = chart.pane('legend');
|
||||||
|
this._tooltip = chart.pane('tooltip');
|
||||||
|
this._ruler = chart.pane('ruler');
|
||||||
|
|
||||||
|
// Scales
|
||||||
|
this._initScale();
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
this._mask = chart.get('mask');
|
||||||
|
this._context = chart.get('context');
|
||||||
|
this._brush = chart.get('brush');
|
||||||
|
|
||||||
|
this._zoom = d3.zoom();
|
||||||
|
this._drag = d3.drag();
|
||||||
|
|
||||||
|
// Interactions
|
||||||
|
this._initInteractions();
|
||||||
|
|
||||||
|
// svg.on('resize', (e)=>console.log(e.detail));
|
||||||
|
|
||||||
|
// Handle multi-track segments
|
||||||
|
this._maskGaps = [];
|
||||||
|
control.on('eletrack_added', ({index}) => {
|
||||||
|
this._maskGaps.push(index);
|
||||||
|
control.once('elepoint_added', ({index}) => this._maskGaps.push(index));
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
update(props) {
|
||||||
|
if (props) {
|
||||||
|
if (props.data) this._data = props.data;
|
||||||
|
if (props.options) this.options = props.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options.xTicks = this._xTicks();
|
||||||
|
this.options.yTicks = this._yTicks();
|
||||||
|
|
||||||
|
this._updateScale();
|
||||||
|
this._updateAxis();
|
||||||
|
this._updateMargins();
|
||||||
|
this._updateLegend();
|
||||||
|
this._updateClipper();
|
||||||
|
this._updateArea();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return container => container.append(() => this._container.node());
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._resetDrag();
|
||||||
|
this._hideDiagramIndicator()
|
||||||
|
this._area.selectAll('path').attr("d", "M0 0");
|
||||||
|
this._context.clearRect(0, 0, this._width(), this._height());
|
||||||
|
this._mask.selectAll(".gap").remove();
|
||||||
|
this._maskGaps = [];
|
||||||
|
|
||||||
|
// if (this._path) {
|
||||||
|
// this._x.domain([0, 1]);
|
||||||
|
// this._y.domain([0, 1]);
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
|
||||||
|
_drawPath(name) {
|
||||||
|
let path = this._paths[name];
|
||||||
|
let area = this._props.areas[name];
|
||||||
|
|
||||||
|
path.datum(this._data).attr("d",
|
||||||
|
D3.Area(
|
||||||
|
L.extend({}, area, {
|
||||||
|
width : this._width(),
|
||||||
|
height : this._height(),
|
||||||
|
scaleX : this._scales[area.scaleX],
|
||||||
|
scaleY : this._scales[area.scaleY]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!path.classed('leaflet-hidden')) {
|
||||||
|
this.options.preferCanvas
|
||||||
|
? _.drawCanvas(this._context, path)
|
||||||
|
: _.append(this._area.node(), path.node());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_hasActiveLayers() {
|
||||||
|
const paths = this._paths;
|
||||||
|
for (var i in paths) {
|
||||||
|
if (!paths[i].classed('leaflet-hidden')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize "d3-brush".
|
||||||
|
*/
|
||||||
|
_initBrush(e) {
|
||||||
|
const brush = ({selection}) => {
|
||||||
|
if (selection && this._data.length) {
|
||||||
|
let start = this._findIndexForXCoord(selection[0]);
|
||||||
|
let end = this._findIndexForXCoord(selection[1]);
|
||||||
|
this.fire('dragged', { dragstart: this._data[start], dragend: this._data[end] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const focus = (e) => {
|
||||||
|
if (this._data.length && (e.type != 'brush' /*|| e.sourceEvent*/)) {
|
||||||
|
let rect = this._chart.panes.brush.select('.overlay').node();
|
||||||
|
let coords = d3.pointers(e, rect)[0];
|
||||||
|
let xCoord = coords[0];
|
||||||
|
let item = this._data[this._findIndexForXCoord(xCoord)];
|
||||||
|
|
||||||
|
this.fire("mouse_move", { item: item, xCoord: xCoord });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this._brush
|
||||||
|
.filter(({shiftKey, button}) => !shiftKey && !button && this._brushEnabled)
|
||||||
|
.on("end.update", brush)
|
||||||
|
.on("brush.update", focus);
|
||||||
|
|
||||||
|
this._chart.panes.brush
|
||||||
|
.on("mouseenter.focus touchstart.focus", this.fire.bind(this, "mouse_enter"))
|
||||||
|
.on("mouseout.focus touchend.focus", this.fire.bind(this, "mouse_out") )
|
||||||
|
.on("mousemove.focus touchmove.focus", focus );
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize "d3-zoom"
|
||||||
|
*/
|
||||||
|
_initClipper() {
|
||||||
|
let svg = this._container;
|
||||||
|
let margin = this.options.margins;
|
||||||
|
|
||||||
|
const zoom = this._zoom;
|
||||||
|
|
||||||
|
const onStart = ({transform, sourceEvent}) => {
|
||||||
|
if (sourceEvent && sourceEvent.type == "mousedown") svg.style('cursor', 'grabbing');
|
||||||
|
if (transform.k == 1 && transform.x == 0) {
|
||||||
|
this._container.classed('zoomed', true);
|
||||||
|
// Apply d3-zoom and bind <mask>
|
||||||
|
if (this._mask) {
|
||||||
|
this._point.attr('mask', 'url(#' + this._mask.attr('id') + ')');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.zooming = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEnd = ({transform}) => {
|
||||||
|
if (transform.k ==1 && transform.x == 0) {
|
||||||
|
this._container.classed('zoomed', false);
|
||||||
|
// Reset d3-zoom and remove <mask>
|
||||||
|
if (this._mask) {
|
||||||
|
this._point.attr('mask', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.zooming = false;
|
||||||
|
svg.style('cursor', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onZoom = ({transform, sourceEvent}) => {
|
||||||
|
// TODO: find a faster way to redraw the chart.
|
||||||
|
this.zooming = false;
|
||||||
|
this._updateScale(); // hacky way for restoring x scale when zooming out
|
||||||
|
this.zooming = true;
|
||||||
|
this._scales.distance = this._x = transform.rescaleX(this._x); // calculate x scale at zoom level
|
||||||
|
if (this._scales.time) this._scales.time = transform.rescaleX(this._scales.time); // calculate x scale at zoom level
|
||||||
|
this._resetDrag();
|
||||||
|
if (sourceEvent && sourceEvent.type == "mousemove") {
|
||||||
|
this._hideDiagramIndicator();
|
||||||
|
}
|
||||||
|
this.fire('zoom');
|
||||||
|
};
|
||||||
|
|
||||||
|
zoom
|
||||||
|
.scaleExtent([1, 10])
|
||||||
|
.extent([
|
||||||
|
[margin.left, 0],
|
||||||
|
[this._width() - margin.right, this._height()]
|
||||||
|
])
|
||||||
|
.translateExtent([
|
||||||
|
[margin.left, -Infinity],
|
||||||
|
[this._width() - margin.right, Infinity]
|
||||||
|
])
|
||||||
|
.filter(({shiftKey, buttons}) => (shiftKey || buttons == 4) && this._zoomEnabled)
|
||||||
|
.on("start", onStart)
|
||||||
|
.on("end", onEnd)
|
||||||
|
.on("zoom", onZoom);
|
||||||
|
|
||||||
|
svg.call(zoom); // add zoom functionality to "svg" group
|
||||||
|
|
||||||
|
// d3.select("body").on("keydown.grabzoom keyup.grabzoom", (e) => svg.style('cursor', e.shiftKey ? 'move' : ''));
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
_initInteractions() {
|
||||||
|
this._initBrush();
|
||||||
|
this._initRuler();
|
||||||
|
this._initClipper();
|
||||||
|
this._initLegend();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle chart data on legend click
|
||||||
|
*/
|
||||||
|
_initLegend() {
|
||||||
|
this._container.on('legend_clicked', ({detail}) => {
|
||||||
|
let { path, legend, name, enabled } = detail;
|
||||||
|
if (path) {
|
||||||
|
let label = _.select('text', legend);
|
||||||
|
let rect = _.select('rect', legend);
|
||||||
|
_.toggleStyle(label, 'text-decoration-line', 'line-through', enabled);
|
||||||
|
_.toggleStyle(rect, 'fill-opacity', '0', enabled);
|
||||||
|
_.toggleClass(path, 'leaflet-hidden', enabled);
|
||||||
|
this._updateArea();
|
||||||
|
this.fire("elepath_toggle", { path, name, legend, enabled })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize "ruler".
|
||||||
|
*/
|
||||||
|
_initRuler() {
|
||||||
|
if (!this.options.ruler) return;
|
||||||
|
|
||||||
|
// const yMax = this._height();
|
||||||
|
const formatNum = d3.format(".0f");
|
||||||
|
const drag = this._drag;
|
||||||
|
|
||||||
|
const label = (e, d) => {
|
||||||
|
let yMax = this._height();
|
||||||
|
let y = this._ruler.data()[0].y;
|
||||||
|
if (y >= yMax || y <= 0) this._ruler.select(".horizontal-drag-label").text('');
|
||||||
|
this._hideDiagramIndicator();
|
||||||
|
};
|
||||||
|
|
||||||
|
const position = (e, d) => {
|
||||||
|
let yCoord = d3.pointers(e, this._area.node())[0][1];
|
||||||
|
let yMax = this._height();
|
||||||
|
let y = _.clamp(yCoord, [0, yMax]);
|
||||||
|
|
||||||
|
this._ruler
|
||||||
|
.data([L.extend(this._ruler.data()[0], { y: y })])
|
||||||
|
.attr("transform", d => "translate(" + d.x + "," + d.y + ")")
|
||||||
|
.classed('active', y < yMax);
|
||||||
|
|
||||||
|
this._container
|
||||||
|
.select(".horizontal-drag-label")
|
||||||
|
.text(formatNum(this._y.invert(y)) + " " + (this.options.imperial ? 'ft' : 'm'));
|
||||||
|
|
||||||
|
this.fire('ruler_filter', { coords: yCoord < yMax && yCoord > 0 ? this._findCoordsForY(yCoord) : [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
drag
|
||||||
|
.on("start end", label)
|
||||||
|
.on("drag", position);
|
||||||
|
|
||||||
|
this._ruler.call(drag);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize x and y scales
|
||||||
|
*/
|
||||||
|
_initScale() {
|
||||||
|
let opts = this.options;
|
||||||
|
|
||||||
|
this._registerAxisScale({
|
||||||
|
axis : 'x',
|
||||||
|
position: 'bottom',
|
||||||
|
attr : opts.xAttr,
|
||||||
|
min : opts.xAxisMin,
|
||||||
|
max : opts.xAxisMax,
|
||||||
|
name : 'distance'
|
||||||
|
});
|
||||||
|
|
||||||
|
this._registerAxisScale({
|
||||||
|
axis : 'y',
|
||||||
|
position : 'left',
|
||||||
|
attr : opts.yAttr,
|
||||||
|
min : opts.yAxisMin,
|
||||||
|
max : opts.yAxisMax,
|
||||||
|
name : 'altitude'
|
||||||
|
});
|
||||||
|
|
||||||
|
this._x = this._scales.distance;
|
||||||
|
this._y = this._scales.altitude;
|
||||||
|
},
|
||||||
|
|
||||||
|
_registerAreaPath(props) {
|
||||||
|
if (props.scale == 'y') props.scale = this._y;
|
||||||
|
else if (props.scale == 'x') props.scale = this._x;
|
||||||
|
|
||||||
|
let opts = this.options;
|
||||||
|
|
||||||
|
if (!props.xAttr) props.xAttr = opts.xAttr;
|
||||||
|
if (!props.yAttr) props.yAttr = opts.yAttr;
|
||||||
|
if (typeof props.preferCanvas === "undefined") props.preferCanvas = opts.preferCanvas;
|
||||||
|
|
||||||
|
// Save paths in memory for latter usage
|
||||||
|
this._paths[props.name] = D3.Path(props);
|
||||||
|
this._props.areas[props.name] = props;
|
||||||
|
|
||||||
|
if (opts.legend) {
|
||||||
|
this._props.legendItems[props.name] = {
|
||||||
|
name : props.name,
|
||||||
|
label : props.label,
|
||||||
|
color : props.color,
|
||||||
|
className: props.className,
|
||||||
|
path : this._paths[props.name]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
_registerAxisGrid(props) {
|
||||||
|
if (props.scale == 'y') props.scale = this._y;
|
||||||
|
else if (props.scale == 'x') props.scale = this._x;
|
||||||
|
|
||||||
|
this._props.grids[props.name || props.axis] = props;
|
||||||
|
},
|
||||||
|
|
||||||
|
_registerAxisScale(props) {
|
||||||
|
if (props.scale == 'y') props.scale = this._y;
|
||||||
|
else if (props.scale == 'x') props.scale = this._x;
|
||||||
|
|
||||||
|
let opts = this.options;
|
||||||
|
let scale = props.scale;
|
||||||
|
|
||||||
|
if (typeof this._scales[props.name] === 'function') {
|
||||||
|
props.scale = this._scales[props.name]; // retrieve cached scale
|
||||||
|
} else if (typeof scale !== 'function') {
|
||||||
|
scale = L.extend({
|
||||||
|
data : this._data,
|
||||||
|
forceBounds: opts.forceAxisBounds
|
||||||
|
}, scale);
|
||||||
|
|
||||||
|
scale.attr = scale.attr || props.name;
|
||||||
|
|
||||||
|
let domain = this._domains[props.name] = D3.Domain(props);
|
||||||
|
let range = this._ranges[props.name] = D3.Range(props);
|
||||||
|
scale.range = scale.range || range(this._width(),this._height());
|
||||||
|
scale.domain = scale.domain || domain(this._data);
|
||||||
|
|
||||||
|
this._props.scales[props.name] = scale;
|
||||||
|
|
||||||
|
props.scale = this._scales[props.name] = D3.Scale(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.ticks) {
|
||||||
|
if (props.axis == 'x') props.ticks = this._xTicks.bind(this);
|
||||||
|
else if (props.axis == 'y') props.ticks = this._yTicks.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._props.axes[props.name] = props;
|
||||||
|
|
||||||
|
if (props.name == this.options.yScale) this._y = scale;
|
||||||
|
if (props.name == this.options.xScale) this._x = scale;
|
||||||
|
|
||||||
|
return scale;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a point of interest over the chart
|
||||||
|
*/
|
||||||
|
_registerCheckPoint(point) {
|
||||||
|
if (!this._data.length) return;
|
||||||
|
|
||||||
|
const {xAttr, yAttr} = this.options;
|
||||||
|
let item, x, y;
|
||||||
|
|
||||||
|
if (point.latlng) {
|
||||||
|
item = this._data[this._findIndexForLatLng(point.latlng)];
|
||||||
|
x = this._x(item[xAttr]);
|
||||||
|
y = this._y(item[yAttr]);
|
||||||
|
} else if (!isNaN(point[xAttr])) {
|
||||||
|
x = this._x(point[xAttr]);
|
||||||
|
item = this._data[this._findIndexForXCoord(x)]
|
||||||
|
y = this._y(item[yAttr]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._point.call(D3.CheckPoint({
|
||||||
|
point: point,
|
||||||
|
width: this._width(),
|
||||||
|
height: this._height(),
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
_registerTooltip(props) {
|
||||||
|
props.order = props.order ?? 1000;
|
||||||
|
this._props.tooltipItems[props.name] = props;
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateArea() {
|
||||||
|
// Reset and update chart profiles
|
||||||
|
this._context.clearRect(0, 0, this._width(), this._height());
|
||||||
|
_.each(this._paths, (path, i) => !path.classed('leaflet-hidden') && this._drawPath(i));
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateAxis() {
|
||||||
|
|
||||||
|
let opts = this.options;
|
||||||
|
|
||||||
|
const gridOpts = {
|
||||||
|
width : this._width(),
|
||||||
|
height : this._height(),
|
||||||
|
tickFormat: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const axesOpts = {
|
||||||
|
width : this._width(),
|
||||||
|
height : this._height(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset grids
|
||||||
|
this._grid.selectAll('g').remove();
|
||||||
|
_.each(this._props.grids, (grid, i) => {
|
||||||
|
if (opts[i] !== false && opts[i] !== 'summary') {
|
||||||
|
this._grid.call(D3.Grid(L.extend({}, gridOpts, grid)))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rest axis
|
||||||
|
this._axis.selectAll('g').remove();
|
||||||
|
_.each(this._props.axes, (axis, i) => {
|
||||||
|
if (opts[i] !== false && opts[i] !== 'summary') {
|
||||||
|
this._axis.call(D3.Axis(L.extend({}, axesOpts, axis)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Adjust axis scale positions
|
||||||
|
this._axis
|
||||||
|
.selectAll('.y.axis.right')
|
||||||
|
.each((d, i, n) => {
|
||||||
|
|
||||||
|
let axis = d3.select(n[i]);
|
||||||
|
let transform = axis.attr('transform');
|
||||||
|
let translate = transform.substring(transform.indexOf("(") + 1, transform.indexOf(")")).split(",");
|
||||||
|
|
||||||
|
axis.attr('transform', 'translate(' + (+translate[0] + (i * 40)) + ',' + translate[1] + ')')
|
||||||
|
|
||||||
|
if (i > 0) {
|
||||||
|
axis.select(':scope > path') .attr('opacity', 0.25);
|
||||||
|
axis.selectAll(':scope > .tick line').attr('opacity', 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateClipper() {
|
||||||
|
const { xAttr, margins } = this.options;
|
||||||
|
const data = this._data;
|
||||||
|
|
||||||
|
this._zoom
|
||||||
|
.scaleExtent([1, 10])
|
||||||
|
.extent([
|
||||||
|
[margins.left, 0],
|
||||||
|
[this._width() - margins.right, this._height()]
|
||||||
|
])
|
||||||
|
.translateExtent([
|
||||||
|
[margins.left, -Infinity],
|
||||||
|
[this._width() - margins.right, Infinity]
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Apply svg mask on multi-track segments
|
||||||
|
this._mask.selectAll(".gap").remove()
|
||||||
|
this._maskGaps.forEach((d, i) => {
|
||||||
|
if (i >= this._maskGaps.length - 2) return;
|
||||||
|
let x1 = this._x(data[this._findIndexForLatLng(data[this._maskGaps[i]].latlng)][xAttr]);
|
||||||
|
let x2 = this._x(data[this._findIndexForLatLng(data[this._maskGaps[i + 1]].latlng)][xAttr]);
|
||||||
|
this._mask
|
||||||
|
.append("rect")
|
||||||
|
.attr("x", x1)
|
||||||
|
.attr("y", 0)
|
||||||
|
.attr("width", x2 - x1 )
|
||||||
|
.attr("height", this._height())
|
||||||
|
.attr('class', 'gap')
|
||||||
|
.attr('fill-opacity', '0.8')
|
||||||
|
.attr("fill", 'black'); // hide = black (mask)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateLegend: function () {
|
||||||
|
let xAxesB = this._axis.selectAll('.x.axis.bottom').nodes().length;
|
||||||
|
|
||||||
|
// Get legend items
|
||||||
|
let items = Object.keys(this._paths);
|
||||||
|
|
||||||
|
// Calculate legend item positions
|
||||||
|
let n = items.length;
|
||||||
|
let v = Array(Math.floor(n / 2)).fill(null).map((d, i) => (i + 1) * 2 - (1 - Math.sign(n % 2)));
|
||||||
|
let rev = v.slice().reverse().map((d) => -(d));
|
||||||
|
|
||||||
|
// push a fake element to handle center alignment (odd numbers)
|
||||||
|
if (n % 2 !== 0) {
|
||||||
|
rev.push(0);
|
||||||
|
}
|
||||||
|
v = rev.concat(v);
|
||||||
|
|
||||||
|
// Reset legend items
|
||||||
|
this._legend.selectAll('g').remove();
|
||||||
|
|
||||||
|
// Render legend items
|
||||||
|
_.each(this._props.legendItems, (legend) => {
|
||||||
|
this._legend.append("g").call(
|
||||||
|
D3.LegendItem(L.extend({
|
||||||
|
width : this._width(),
|
||||||
|
height : this._height(),
|
||||||
|
margins: this.options.margins,
|
||||||
|
}, legend))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render legend item switcher
|
||||||
|
if (n > 1) {
|
||||||
|
this._legend.append("g").call(
|
||||||
|
D3.LegendSmall({
|
||||||
|
width : this._width(),
|
||||||
|
height : this._height(),
|
||||||
|
items : items,
|
||||||
|
onClick: (selected) => {
|
||||||
|
_.each(items, name => this._togglePath(name, selected == name, true));
|
||||||
|
this._updateArea();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_.each(items, (name, i) => {
|
||||||
|
// Adjust legend item positions
|
||||||
|
this._legend.select('[data-name=' + name + ']').attr("transform", "translate(" + (v[i] * 55) + ", " + (xAxesB * 2) + ")");
|
||||||
|
// Set initial state (disabled controls)
|
||||||
|
this._togglePath(name, !(name in this.options && this.options[name] == 'disabled'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateMargins() {
|
||||||
|
// Get chart margins
|
||||||
|
let xAxesB = this._axis.selectAll('.x.axis.bottom').nodes().length;
|
||||||
|
let xAxesL = this._axis.selectAll('.y.axis.left').nodes().length;
|
||||||
|
let xAxesR = this._axis.selectAll('.y.axis.right').nodes().length;
|
||||||
|
let marginB = 60 + (xAxesB * 2);
|
||||||
|
let marginL = 10 + (xAxesL * 30);
|
||||||
|
let marginR = 40 + (xAxesR * 30);
|
||||||
|
|
||||||
|
let marginsUpdated = false
|
||||||
|
|
||||||
|
// Adjust right margin
|
||||||
|
if (xAxesR && this.options.margins.right < marginR) {
|
||||||
|
this.options.margins.right = marginR;
|
||||||
|
marginsUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust left margin
|
||||||
|
if (xAxesL && this.options.margins.left < marginL) {
|
||||||
|
this.options.margins.left = marginL;
|
||||||
|
marginsUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust bottom margin
|
||||||
|
if (xAxesB && this.options.margins.bottom < marginB) {
|
||||||
|
this.options.margins.bottom = marginB;
|
||||||
|
marginsUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (marginsUpdated) {
|
||||||
|
this.fire('margins_updated');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateScale() {
|
||||||
|
if (this.zooming) return { x: this._x, y: this._y };
|
||||||
|
|
||||||
|
for (let i in this._scales) {
|
||||||
|
this._scales[i]
|
||||||
|
.domain(this._domains[i](this._data))
|
||||||
|
.range(this._ranges[i](this._width(), this._height()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return { x: this._x, y: this._y };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates chart width.
|
||||||
|
*/
|
||||||
|
_width() {
|
||||||
|
if (this._chart) this._chart._width;
|
||||||
|
const { width, margins } = this.options;
|
||||||
|
return width - margins.left - margins.right;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates chart height.
|
||||||
|
*/
|
||||||
|
_height() {
|
||||||
|
if (this._chart) return this._chart._height;
|
||||||
|
const { height, margins } = this.options;
|
||||||
|
return height - margins.top - margins.bottom;
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Finds data entries above a given y-elevation value and returns geo-coordinates
|
||||||
|
*/
|
||||||
|
_findCoordsForY(y) {
|
||||||
|
let data = this._data;
|
||||||
|
let z = this._y.invert(y);
|
||||||
|
|
||||||
|
// save indexes of elevation values above the horizontal line
|
||||||
|
const list = data.reduce((array, item, index) => {
|
||||||
|
if (item[this.options.yAttr] >= z) array.push(index);
|
||||||
|
return array;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
let start = 0;
|
||||||
|
let next;
|
||||||
|
|
||||||
|
// split index list into blocks of coordinates
|
||||||
|
const coords = list.reduce((array, _, curr) => {
|
||||||
|
next = curr + 1;
|
||||||
|
if (list[next] !== list[curr] + 1 || next === list.length) {
|
||||||
|
array.push(
|
||||||
|
list
|
||||||
|
.slice(start, next)
|
||||||
|
.map(i => data[i].latlng)
|
||||||
|
);
|
||||||
|
start = next;
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return coords;
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Finds a data entry for a given x-coordinate of the diagram
|
||||||
|
*/
|
||||||
|
_findIndexForXCoord(x) {
|
||||||
|
return d3
|
||||||
|
.bisector(d => d[this.options.xAttr])
|
||||||
|
.left(this._data || [0, 1], this._x.invert(x));
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Finds a data entry for a given latlng of the map
|
||||||
|
*/
|
||||||
|
_findIndexForLatLng(latlng) {
|
||||||
|
let result = null;
|
||||||
|
let d = Infinity;
|
||||||
|
this._data.forEach((item, index) => {
|
||||||
|
let dist = latlng.distanceTo(item.latlng);
|
||||||
|
if (dist < d) {
|
||||||
|
d = dist;
|
||||||
|
result = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Removes the drag rectangle and zoms back to the total extent of the data.
|
||||||
|
*/
|
||||||
|
_resetDrag() {
|
||||||
|
if (this._chart.panes.brush.select(".selection").attr('width')) {
|
||||||
|
this._chart.panes.brush.call(this._brush.clear);
|
||||||
|
this._hideDiagramIndicator();
|
||||||
|
this.fire('reset_drag');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_resetZoom() {
|
||||||
|
if (this._zoom) {
|
||||||
|
this._zoom.transform(this._chart.svg, d3.zoomIdentity);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display distance and altitude level ("focus-rect").
|
||||||
|
*/
|
||||||
|
_showDiagramIndicator(item, xCoordinate) {
|
||||||
|
this._tooltip
|
||||||
|
.attr("display", null)
|
||||||
|
.call(D3.Tooltip({
|
||||||
|
xCoord: xCoordinate,
|
||||||
|
yCoord: this._y(item[this.options.yAttr]),
|
||||||
|
height: this._height(),
|
||||||
|
width : this._width(),
|
||||||
|
labels: this._props.tooltipItems,
|
||||||
|
item: item
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
_togglePath(name, enabled = true, lazy = false) {
|
||||||
|
let path = this._paths[name];
|
||||||
|
let legend = this._container.select('.legend [data-name=' + name + ']');
|
||||||
|
path.classed('leaflet-hidden', !enabled);
|
||||||
|
legend.select('text').style('text-decoration-line', enabled ? '' : 'line-through');
|
||||||
|
legend.select('rect').style('fill-opacity', enabled ? '': '0');
|
||||||
|
// Apply d3-zoom (bind <clipPath> mask)
|
||||||
|
if (this._mask) {
|
||||||
|
path.attr('mask', 'url(#' + this._mask.attr('id') + ')');
|
||||||
|
}
|
||||||
|
if (!lazy) {
|
||||||
|
this._updateArea();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_hideDiagramIndicator() {
|
||||||
|
this._tooltip.attr("display", 'none');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate chart xTicks
|
||||||
|
*/
|
||||||
|
_xTicks() {
|
||||||
|
if (this.__xTicks) this.__xTicks = this.options.xTicks;
|
||||||
|
return this.__xTicks || Math.round(this._width() / 75);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate chart yTicks
|
||||||
|
*/
|
||||||
|
_yTicks() {
|
||||||
|
if (this.__yTicks) this.__yTicks = this.options.yTicks;
|
||||||
|
return this.__yTicks || Math.round(this._height() / 30);
|
||||||
|
}
|
||||||
|
});
|
||||||
649
web/src/lib/vendor/leaflet-elevation/src/components/d3.js
vendored
Normal file
@@ -0,0 +1,649 @@
|
|||||||
|
export const Area = ({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
xAttr,
|
||||||
|
yAttr,
|
||||||
|
scaleX,
|
||||||
|
scaleY,
|
||||||
|
interpolation = "curveLinear"
|
||||||
|
}) => {
|
||||||
|
return d3.area()
|
||||||
|
.curve(typeof interpolation === 'string' ? d3[interpolation] : interpolation)
|
||||||
|
.x(d => (d.xDiagCoord = scaleX(d[xAttr])))
|
||||||
|
.y0(height)
|
||||||
|
.y1(d => scaleY(d[yAttr]));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Path = ({
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
strokeColor,
|
||||||
|
strokeOpacity,
|
||||||
|
fillOpacity,
|
||||||
|
className = ''
|
||||||
|
}) => {
|
||||||
|
let path = d3.create('svg:path')
|
||||||
|
|
||||||
|
if (name) path.classed(name + ' ' + className, true);
|
||||||
|
|
||||||
|
path.style("pointer-events", "none");
|
||||||
|
|
||||||
|
path
|
||||||
|
.attr("fill", color || '#3366CC')
|
||||||
|
.attr("stroke", strokeColor || '#000')
|
||||||
|
.attr("stroke-opacity", strokeOpacity || '1')
|
||||||
|
.attr("fill-opacity", fillOpacity || '0.8');
|
||||||
|
|
||||||
|
return path;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Axis = ({
|
||||||
|
type = "axis",
|
||||||
|
tickSize = 6,
|
||||||
|
tickPadding = 3,
|
||||||
|
position,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
axis,
|
||||||
|
scale,
|
||||||
|
ticks,
|
||||||
|
tickFormat,
|
||||||
|
label,
|
||||||
|
labelX,
|
||||||
|
labelY,
|
||||||
|
name = "",
|
||||||
|
onAxisMount,
|
||||||
|
}) => {
|
||||||
|
return g => {
|
||||||
|
let [w, h] = [0, 0];
|
||||||
|
if (position == "bottom") h = height;
|
||||||
|
if (position == "right") w = width;
|
||||||
|
|
||||||
|
if (axis == "x" && type == "grid") {
|
||||||
|
tickSize = -height;
|
||||||
|
} else if (axis == "y" && type == "grid") {
|
||||||
|
tickSize = -width;
|
||||||
|
}
|
||||||
|
|
||||||
|
let axisScale = d3["axis" + position.replace(/\b\w/g, l => l.toUpperCase())]()
|
||||||
|
.scale(scale)
|
||||||
|
.ticks(typeof ticks === 'function' ? ticks() : ticks)
|
||||||
|
.tickPadding(tickPadding)
|
||||||
|
.tickSize(tickSize)
|
||||||
|
.tickFormat(tickFormat);
|
||||||
|
|
||||||
|
let axisGroup = g.append("g")
|
||||||
|
.attr("class", [axis, type, position, name].join(" "))
|
||||||
|
.attr("transform", "translate(" + w + "," + h + ")")
|
||||||
|
.call(axisScale);
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
axisGroup.append("svg:text")
|
||||||
|
.attr("x", labelX)
|
||||||
|
.attr("y", labelY)
|
||||||
|
.text(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onAxisMount) {
|
||||||
|
axisGroup.call(onAxisMount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return axisGroup;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Grid = (props) => {
|
||||||
|
props.type = "grid";
|
||||||
|
return Axis(props);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PositionMarker = ({
|
||||||
|
theme,
|
||||||
|
xCoord = 0,
|
||||||
|
yCoord = 0,
|
||||||
|
labels = {},
|
||||||
|
item = {},
|
||||||
|
length = 0,
|
||||||
|
}) => {
|
||||||
|
return g => {
|
||||||
|
|
||||||
|
g.attr("class", "height-focus-group");
|
||||||
|
|
||||||
|
let line = g.select('.height-focus.line');
|
||||||
|
let circle = g.select('.height-focus.circle-lower');
|
||||||
|
let text = g.select('.height-focus-label');
|
||||||
|
|
||||||
|
if (!line.node()) line = g.append('svg:line');
|
||||||
|
if (!circle.node()) circle = g.append('svg:circle');
|
||||||
|
if (!text.node()) text = g.append('svg:text');
|
||||||
|
|
||||||
|
if (isNaN(xCoord) || isNaN(yCoord)) return g;
|
||||||
|
|
||||||
|
circle
|
||||||
|
.attr("class", theme + " height-focus circle-lower")
|
||||||
|
.attr("transform", "translate(" + xCoord + "," + yCoord + ")")
|
||||||
|
.attr("r", 6)
|
||||||
|
.attr("cx", 0)
|
||||||
|
.attr("cy", 0);
|
||||||
|
|
||||||
|
line
|
||||||
|
.attr("class", theme + " height-focus line")
|
||||||
|
.attr("x1", xCoord)
|
||||||
|
.attr("x2", xCoord)
|
||||||
|
.attr("y1", yCoord)
|
||||||
|
.attr("y2", length);
|
||||||
|
|
||||||
|
text
|
||||||
|
.attr("class", theme + " height-focus-label")
|
||||||
|
.style("pointer-events", "none")
|
||||||
|
.attr("x", xCoord + 5)
|
||||||
|
.attr("y", length);
|
||||||
|
|
||||||
|
let label;
|
||||||
|
|
||||||
|
Object
|
||||||
|
.keys(labels)
|
||||||
|
.sort((a, b) => labels[a].order - labels[b].order) // TODO: any performance issues?
|
||||||
|
.forEach((i)=> {
|
||||||
|
label = text.select(".height-focus-" + labels[i].name);
|
||||||
|
|
||||||
|
if (!label.size()) {
|
||||||
|
label = text.append("svg:tspan")
|
||||||
|
.attr("class", "height-focus-" + labels[i].name /*+ " " + "order-" + labels[i].order*/)
|
||||||
|
.attr("dy", "1.5em");
|
||||||
|
}
|
||||||
|
|
||||||
|
label.text(typeof labels[i].value !== "function" ? labels[i].value : labels[i].value(item));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
text.select('tspan').attr("dy", text.selectAll('tspan').size() > 1 ? "-1.5em" : "0em" );
|
||||||
|
text.selectAll('tspan').attr("x", xCoord + 5);
|
||||||
|
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LegendItem = ({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
margins = {},
|
||||||
|
color,
|
||||||
|
path,
|
||||||
|
className = ''
|
||||||
|
}) => {
|
||||||
|
return g => {
|
||||||
|
g
|
||||||
|
.attr("class", "legend-item legend-" + name.toLowerCase())
|
||||||
|
.attr("data-name", name);
|
||||||
|
|
||||||
|
g.on('click.legend', () => d3.select(g.node().ownerSVGElement || g)
|
||||||
|
.dispatch("legend_clicked", {
|
||||||
|
detail: {
|
||||||
|
path: path.node(),
|
||||||
|
name: name,
|
||||||
|
legend: g.node(),
|
||||||
|
enabled: !path.classed('leaflet-hidden'),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
g.append("svg:rect")
|
||||||
|
.attr("x", (width / 2) - 50)
|
||||||
|
.attr("y", height + margins.bottom / 2)
|
||||||
|
.attr("width", 50)
|
||||||
|
.attr("height", 10)
|
||||||
|
.attr("fill", color)
|
||||||
|
.attr("stroke", "#000")
|
||||||
|
.attr("stroke-opacity", "0.5")
|
||||||
|
.attr("fill-opacity", "0.25")
|
||||||
|
.attr("class", className);
|
||||||
|
|
||||||
|
g.append('svg:text')
|
||||||
|
.text(L._(label || name))
|
||||||
|
.attr("x", (width / 2) + 5)
|
||||||
|
.attr("font-size", 10)
|
||||||
|
.style("font-weight", "700")
|
||||||
|
.attr('y', height + margins.bottom / 2)
|
||||||
|
.attr('dy', "0.75em");
|
||||||
|
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LegendSmall = ({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
items,
|
||||||
|
onClick
|
||||||
|
}) => {
|
||||||
|
return g => {
|
||||||
|
let idx = 0;
|
||||||
|
|
||||||
|
g.data([{
|
||||||
|
x: width,
|
||||||
|
y: height + 40
|
||||||
|
}]).attr("transform", d => "translate(" + d.x + "," + d.y + ")").classed('legend-switcher');
|
||||||
|
|
||||||
|
// let label = g.selectAll(".legend-switcher-label") .data([{ idx: 0 }]);
|
||||||
|
let label = g.append('svg:text')
|
||||||
|
.attr("class", "legend-switcher-label")
|
||||||
|
.attr("text-anchor", "end")
|
||||||
|
.attr("x", -25)
|
||||||
|
.attr("y", 5)
|
||||||
|
.on("mousedown", (e, d) => setIdx(L.Util.wrapNum((idx + 1), [0, items.length])));
|
||||||
|
|
||||||
|
let symbol = g.selectAll(".legend-switcher-symbol").data([
|
||||||
|
{ type: d3.symbolTriangle, x: 0, y: 3, angle: 0, size: 50, id: "down" },
|
||||||
|
{ type: d3.symbolTriangle, x: -13, y: 0, angle: 180, size: 50, id: "up" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
symbol.exit().remove();
|
||||||
|
label.exit().remove();
|
||||||
|
|
||||||
|
symbol.enter()
|
||||||
|
.append("svg:path")
|
||||||
|
.attr("class", "legend-switcher-symbol")
|
||||||
|
.attr("cursor", 'pointer')
|
||||||
|
.attr("fill", "#000")
|
||||||
|
.merge(symbol)
|
||||||
|
.attr("d",
|
||||||
|
d3.symbol()
|
||||||
|
.type(d => d.type)
|
||||||
|
.size(d => d.size)
|
||||||
|
)
|
||||||
|
.attr("transform", d => "translate(" + d.x + "," + d.y + ") rotate(" + d.angle + ")")
|
||||||
|
.on("mousedown", (e, d) => setIdx(L.Util.wrapNum((d.id === "up" ? idx + 1 : idx - 1), [0, items.length])));
|
||||||
|
|
||||||
|
const setIdx = (id) => {
|
||||||
|
idx = id;
|
||||||
|
// label.enter()
|
||||||
|
// .append('text')
|
||||||
|
// .attr("class", "legend-switcher-label")
|
||||||
|
// .attr("text-anchor", "end")
|
||||||
|
// .attr("x", -25)
|
||||||
|
// .attr("y", 5)
|
||||||
|
// .merge(label.data([{ idx: id }]))
|
||||||
|
// .text(d => items.length ? (items[idx][0].toUpperCase() + items[idx].slice(1)) : '')
|
||||||
|
// .on("mousedown", (e, d) => setIdx(L.Util.wrapNum((idx + 1), [0, items.length])));
|
||||||
|
label.text(items.length ? L._((items[idx][0].toUpperCase() + items[idx].slice(1))) : '');
|
||||||
|
onClick(items[idx]);
|
||||||
|
};
|
||||||
|
setIdx(0);
|
||||||
|
|
||||||
|
return g;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Tooltip = ({
|
||||||
|
xCoord,
|
||||||
|
yCoord,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
labels = {},
|
||||||
|
item = {},
|
||||||
|
}) => {
|
||||||
|
return g => {
|
||||||
|
|
||||||
|
let line = g.select('.mouse-focus-line');
|
||||||
|
let box = g.select('.mouse-focus-label');
|
||||||
|
|
||||||
|
if (!line.node()) line = g.append('svg:line');
|
||||||
|
if (!box.node()) box = g.append("g");
|
||||||
|
|
||||||
|
let rect = box.select(".mouse-focus-label-rect");
|
||||||
|
let text = box.select(".mouse-focus-label-text");
|
||||||
|
|
||||||
|
if (!rect.node()) rect = box.append("svg:rect");
|
||||||
|
if (!text.node()) text = box.append("svg:text");
|
||||||
|
|
||||||
|
// Sets focus-label-text position to the left / right of the mouse-focus-line
|
||||||
|
let xAlign = 0;
|
||||||
|
let yAlign = 0;
|
||||||
|
let bbox = { width: 0, height: 0 };
|
||||||
|
try { bbox = text.node().getBBox(); } catch (e) { return g; }
|
||||||
|
|
||||||
|
if (xCoord) xAlign = xCoord + (xCoord < width / 2 ? 10 : -bbox.width - 10);
|
||||||
|
if (yCoord) yAlign = Math.max(yCoord - bbox.height, L.Browser.webkit ? 0 : -Infinity);
|
||||||
|
|
||||||
|
line
|
||||||
|
.attr('class', 'mouse-focus-line')
|
||||||
|
.attr('x2', xCoord)
|
||||||
|
.attr('y2', 0)
|
||||||
|
.attr('x1', xCoord)
|
||||||
|
.attr('y1', height);
|
||||||
|
|
||||||
|
box
|
||||||
|
.attr('class', 'mouse-focus-label');
|
||||||
|
|
||||||
|
rect
|
||||||
|
.attr("class", "mouse-focus-label-rect")
|
||||||
|
.attr("x", xAlign - 5)
|
||||||
|
.attr("y", yAlign - 5)
|
||||||
|
.attr("width", bbox.width + 10)
|
||||||
|
.attr("height", bbox.height + 10)
|
||||||
|
.attr("rx", 3)
|
||||||
|
.attr("ry", 3);
|
||||||
|
|
||||||
|
text
|
||||||
|
.attr("class", "mouse-focus-label-text")
|
||||||
|
.style("font-weight", "700")
|
||||||
|
.attr("y", yAlign);
|
||||||
|
|
||||||
|
let label;
|
||||||
|
|
||||||
|
Object
|
||||||
|
.keys(labels)
|
||||||
|
.sort((a, b) => labels[a].order - labels[b].order) // TODO: any performance issues?
|
||||||
|
.forEach((i)=> {
|
||||||
|
label = text.select(".mouse-focus-label-" + labels[i].name);
|
||||||
|
|
||||||
|
if (!label.size()) {
|
||||||
|
label = text.append("svg:tspan", ".mouse-focus-label-x")
|
||||||
|
.attr("class", "mouse-focus-label-" + labels[i].name /*+ " " + "order-" + labels[i].order*/)
|
||||||
|
.attr("dy", "1.5em");
|
||||||
|
}
|
||||||
|
|
||||||
|
label.text(typeof labels[i].value !== "function" ? labels[i].value : labels[i].value(item));
|
||||||
|
});
|
||||||
|
|
||||||
|
text.select('tspan').attr("dy", "1em");
|
||||||
|
text.selectAll('tspan').attr("x", xAlign);
|
||||||
|
|
||||||
|
return g;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Ruler = ({
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
}) => {
|
||||||
|
return g => {
|
||||||
|
|
||||||
|
g.data([{
|
||||||
|
x: 0,
|
||||||
|
y: height,
|
||||||
|
}])
|
||||||
|
.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
|
||||||
|
|
||||||
|
let rect = g.selectAll('.horizontal-drag-rect').data([{ w: width }]);
|
||||||
|
let line = g.selectAll('.horizontal-drag-line').data([{ w: width }]);
|
||||||
|
let label = g.selectAll('.horizontal-drag-label').data([{ w: width - 8 }]);
|
||||||
|
let symbol = g.selectAll('.horizontal-drag-symbol')
|
||||||
|
.data([{
|
||||||
|
type: d3.symbolTriangle,
|
||||||
|
x: width + 7,
|
||||||
|
y: 0,
|
||||||
|
angle: -90,
|
||||||
|
size: 50
|
||||||
|
}]);
|
||||||
|
|
||||||
|
rect.exit().remove();
|
||||||
|
line.exit().remove();
|
||||||
|
label.exit().remove();
|
||||||
|
symbol.exit().remove();
|
||||||
|
|
||||||
|
rect.enter()
|
||||||
|
.append("svg:rect")
|
||||||
|
.attr("class", "horizontal-drag-rect")
|
||||||
|
.attr("x", 0)
|
||||||
|
.attr("y", -8)
|
||||||
|
.attr("height", 8)
|
||||||
|
.attr('fill', 'none')
|
||||||
|
.attr('pointer-events', 'all')
|
||||||
|
.merge(rect)
|
||||||
|
.attr("width", d => d.w);
|
||||||
|
|
||||||
|
line.enter()
|
||||||
|
.append("svg:line")
|
||||||
|
.attr("class", "horizontal-drag-line")
|
||||||
|
.attr("x1", 0)
|
||||||
|
.merge(line)
|
||||||
|
.attr("x2", d => d.w);
|
||||||
|
|
||||||
|
label.enter()
|
||||||
|
.append("svg:text")
|
||||||
|
.attr("class", "horizontal-drag-label")
|
||||||
|
.attr("text-anchor", "end")
|
||||||
|
.attr("y", -8)
|
||||||
|
.merge(label)
|
||||||
|
.attr("x", d => d.w)
|
||||||
|
|
||||||
|
symbol
|
||||||
|
.enter()
|
||||||
|
.append("svg:path")
|
||||||
|
.attr("class", "horizontal-drag-symbol")
|
||||||
|
.merge(symbol)
|
||||||
|
.attr("d",
|
||||||
|
d3.symbol()
|
||||||
|
.type(d => d.type)
|
||||||
|
.size(d => d.size)
|
||||||
|
)
|
||||||
|
.attr("transform", d => "translate(" + d.x + "," + d.y + ") rotate(" + d.angle + ")");
|
||||||
|
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CheckPoint = ({
|
||||||
|
point,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
x,
|
||||||
|
y
|
||||||
|
}) => {
|
||||||
|
return g => {
|
||||||
|
|
||||||
|
if (isNaN(x) || isNaN(y)) return g;
|
||||||
|
|
||||||
|
if (!point.item || !point.item.property('isConnected')) {
|
||||||
|
point.position = point.position || "bottom";
|
||||||
|
|
||||||
|
point.item = g.append('g');
|
||||||
|
|
||||||
|
point.item.append("svg:line")
|
||||||
|
.attr("y1", 0)
|
||||||
|
.attr("x1", 0)
|
||||||
|
.attr("style","stroke: rgb(51, 51, 51); stroke-width: 0.5; stroke-dasharray: 2, 2;");
|
||||||
|
|
||||||
|
point.item
|
||||||
|
.append("svg:circle")
|
||||||
|
.attr("class", " height-focus circle-lower")
|
||||||
|
.attr("r", 3);
|
||||||
|
|
||||||
|
if (point.label) {
|
||||||
|
point.item.append("svg:text")
|
||||||
|
.attr("dx", "4px")
|
||||||
|
.attr("dy", "-4px");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
point.item
|
||||||
|
.datum({
|
||||||
|
pos: point.position,
|
||||||
|
x: x,
|
||||||
|
y: y
|
||||||
|
})
|
||||||
|
.attr("class", d => "point " + d.pos)
|
||||||
|
.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
|
||||||
|
|
||||||
|
point.item.select('line')
|
||||||
|
.datum({
|
||||||
|
y2: ({'top': -y, 'bottom': height - y})[point.position],
|
||||||
|
x2: ({'left': -x, 'right': width - x})[point.position] || 0
|
||||||
|
})
|
||||||
|
.attr("y2", d => d.y2)
|
||||||
|
.attr("x2", d => d.x2)
|
||||||
|
|
||||||
|
if (point.label) {
|
||||||
|
point.item.select('text')
|
||||||
|
.text(point.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Domain = ({
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
attr,
|
||||||
|
name,
|
||||||
|
forceBounds,
|
||||||
|
scale
|
||||||
|
}) => function(data) {
|
||||||
|
attr = (scale && scale.attr) || attr || name;
|
||||||
|
let domain = data && data.length ? d3.extent(data, d => d[attr]) : [0, 1];
|
||||||
|
if (typeof min !== "undefined" && (min < domain[0] || forceBounds)) {
|
||||||
|
domain[0] = min;
|
||||||
|
}
|
||||||
|
if (typeof max !== "undefined" && (max > domain[1] || forceBounds)) {
|
||||||
|
domain[1] = max;
|
||||||
|
}
|
||||||
|
return domain;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Range = ({
|
||||||
|
axis
|
||||||
|
}) => function(width, height) {
|
||||||
|
if (axis == 'x') return [0, width];
|
||||||
|
else if (axis == 'y') return [height, 0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Scale = ({
|
||||||
|
data,
|
||||||
|
attr,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
forceBounds,
|
||||||
|
range,
|
||||||
|
}) => {
|
||||||
|
return d3.scaleLinear()
|
||||||
|
.range(range)
|
||||||
|
.domain(Domain({min, max, attr, forceBounds})(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Bisect = ({
|
||||||
|
data = [0, 1],
|
||||||
|
scale,
|
||||||
|
x,
|
||||||
|
attr
|
||||||
|
}) => {
|
||||||
|
return d3
|
||||||
|
.bisector(d => d[attr])
|
||||||
|
.left(data, scale.invert(x));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Chart = ({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
margins = {},
|
||||||
|
ruler,
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const w = width - margins.left - margins.right;
|
||||||
|
const h = height - margins.top - margins.bottom;
|
||||||
|
|
||||||
|
// SVG Container
|
||||||
|
const svg = d3.create("svg:svg").attr("class", "background");
|
||||||
|
|
||||||
|
const defs = svg.append("svg:defs");
|
||||||
|
|
||||||
|
// SVG Groups
|
||||||
|
const g = svg.append("g");
|
||||||
|
const panes = {
|
||||||
|
grid : g.append("g").attr("class", "grid"),
|
||||||
|
area : g.append('g').attr("class", "area"),
|
||||||
|
axis : g.append('g').attr("class", "axis"),
|
||||||
|
point : g.append('g').attr("class", "point"),
|
||||||
|
brush : g.append("g").attr("class", "brush"),
|
||||||
|
tooltip: g.append("g").attr("class", "tooltip").attr('display', 'none'),
|
||||||
|
ruler : g.append('g').attr('class', 'ruler'),
|
||||||
|
legend : g.append('g').attr("class", "legend"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// SVG Paths
|
||||||
|
const mask = panes.area .append("svg:mask") .attr("id", 'elevation-clipper-' + Math.random().toString(36).substr(2, 9)).attr('fill-opacity', 1);
|
||||||
|
const maskRect = mask .append("svg:rect") .attr('class', 'zoom') .attr('fill', 'white'); // white = transparent
|
||||||
|
|
||||||
|
// Canvas Paths
|
||||||
|
const foreignObject = panes.area .append('svg:foreignObject').attr('mask', 'url(#' + mask.attr('id') + ')');
|
||||||
|
const canvas = foreignObject.append('xhtml:canvas') .attr('class', 'canvas-plot');
|
||||||
|
const context = canvas.node().getContext('2d');
|
||||||
|
|
||||||
|
// Add tooltip
|
||||||
|
panes.tooltip.call(Tooltip({ xCoord: 0, yCoord: 0, height: h, width : w, labels: {} }));
|
||||||
|
|
||||||
|
// Add the brushing
|
||||||
|
let brush = d3.brushX().on('start.cursor end.cursor brush.cursor', () => panes.brush.select(".overlay").attr('cursor', null));
|
||||||
|
|
||||||
|
// Scales
|
||||||
|
const scale = (opts) => ({ x: Scale(opts.x), y: Scale(opts.y)});
|
||||||
|
|
||||||
|
let utils = {
|
||||||
|
defs,
|
||||||
|
mask,
|
||||||
|
canvas,
|
||||||
|
context,
|
||||||
|
brush,
|
||||||
|
};
|
||||||
|
|
||||||
|
let chart = {
|
||||||
|
svg,
|
||||||
|
g,
|
||||||
|
panes,
|
||||||
|
utils,
|
||||||
|
scale,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resize
|
||||||
|
chart._resize = ({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
margins = {},
|
||||||
|
ruler,
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const w = width - margins.left - margins.right;
|
||||||
|
const h = height - margins.top - margins.bottom;
|
||||||
|
|
||||||
|
svg .attr("width", width).attr("height", height).attr("viewBox", `0 0 ${width} ${height}`);
|
||||||
|
g .attr("transform", "translate(" + margins.left + "," + margins.top + ")");
|
||||||
|
|
||||||
|
// Fix: https://github.com/Raruto/leaflet-elevation/issues/123
|
||||||
|
// Fix: https://github.com/Raruto/leaflet-elevation/issues/232
|
||||||
|
if (
|
||||||
|
/Mac|iPod|iPhone|iPad/.test(navigator.platform) &&
|
||||||
|
/AppleWebkit/i.test(navigator.userAgent) &&
|
||||||
|
!/Chrome/.test(navigator.userAgent)
|
||||||
|
) {
|
||||||
|
canvas .style("transform", "translate(" + margins.left + "px," + margins.top + "px)");
|
||||||
|
}
|
||||||
|
|
||||||
|
maskRect .attr("width", w).attr("height", h).attr("x", 0).attr("y", 0);
|
||||||
|
foreignObject.attr('width', w).attr('height', h);
|
||||||
|
canvas .attr('width', w).attr('height', h);
|
||||||
|
|
||||||
|
if (ruler) {
|
||||||
|
panes.ruler.call(Ruler({ height: h, width: w }));
|
||||||
|
}
|
||||||
|
|
||||||
|
panes.brush.call(brush.extent( [ [0,0], [w, h] ] ));
|
||||||
|
panes.brush.select(".overlay").attr('cursor', null);
|
||||||
|
|
||||||
|
chart._width = w;
|
||||||
|
chart._height = h;
|
||||||
|
|
||||||
|
chart.svg.dispatch('resize', { detail: { width: w, height: h } } );
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
chart.pane = (name) => (panes[name] || (panes[name] = g.append('g').attr("class", name)));
|
||||||
|
chart.get = (name) => utils[name];
|
||||||
|
|
||||||
|
chart._resize({ width, height, margins});
|
||||||
|
|
||||||
|
return chart;
|
||||||
|
};
|
||||||
96
web/src/lib/vendor/leaflet-elevation/src/components/marker.js
vendored
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import * as D3 from './d3.js';
|
||||||
|
|
||||||
|
const _ = L.Control.Elevation.Utils;
|
||||||
|
|
||||||
|
export var Marker = L.Class.extend({
|
||||||
|
|
||||||
|
initialize(options, control) {
|
||||||
|
this.options = options;
|
||||||
|
this.control = control
|
||||||
|
|
||||||
|
switch(this.options.marker) {
|
||||||
|
case 'elevation-line':
|
||||||
|
// this._container = d3.create("g").attr("class", "height-focus-group");
|
||||||
|
break;
|
||||||
|
case 'position-marker':
|
||||||
|
// this._marker = L.circleMarker([0, 0], { pane: 'overlayPane', radius: 6, fillColor: '#fff', fillOpacity:1, color: '#000', weight:1, interactive: false });
|
||||||
|
this._marker = L.marker([0, 0], { icon: this.options.markerIcon, zIndexOffset: 1000000, interactive: false });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._labels = {};
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
addTo(map) {
|
||||||
|
this._map = map;
|
||||||
|
switch(this.options.marker) {
|
||||||
|
case 'elevation-line': this._container = d3.select(map.getPane('elevationPane')).select("svg > g").call(D3.PositionMarker({})); break;
|
||||||
|
case 'position-marker': this._marker.addTo(map, { pane: 'overlayPane' }); break;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update position marker ("leaflet-marker").
|
||||||
|
*/
|
||||||
|
update(props) {
|
||||||
|
if (props) this._props = props;
|
||||||
|
else props = this._props;
|
||||||
|
|
||||||
|
if (!props) return;
|
||||||
|
|
||||||
|
if (props.options) this.options = props.options;
|
||||||
|
if (!this._map) this.addTo(props.map);
|
||||||
|
|
||||||
|
this._latlng = props.item.latlng;
|
||||||
|
|
||||||
|
switch(this.options.marker) {
|
||||||
|
case 'elevation-line':
|
||||||
|
if (this._container) {
|
||||||
|
let point = this._map.latLngToLayerPoint(this._latlng);
|
||||||
|
point = L.extend({}, props.item, this._map._rotate ? this._map.rotatedPointToMapPanePoint(point) : point);
|
||||||
|
|
||||||
|
let yMax = (this.control._height() / props.yCoordMax * point[this.options.yAttr]);
|
||||||
|
|
||||||
|
if (!isFinite(yMax) || isNaN(yMax)) yMax = 0;
|
||||||
|
|
||||||
|
this._container.classed("leaflet-hidden", false);
|
||||||
|
this._container.call(D3.PositionMarker({
|
||||||
|
theme : this.options.theme,
|
||||||
|
xCoord: point.x,
|
||||||
|
yCoord: point.y,
|
||||||
|
length: point.y - yMax, // normalized Y
|
||||||
|
labels: this._labels,
|
||||||
|
item: point,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'position-marker':
|
||||||
|
_.removeClass(this._marker.getElement(), 'leaflet-hidden');
|
||||||
|
this._marker.setLatLng(this._latlng);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Hides the position/height indicator marker drawn onto the map
|
||||||
|
*/
|
||||||
|
remove() {
|
||||||
|
this._props = null;
|
||||||
|
switch(this.options.marker) {
|
||||||
|
case 'elevation-line': this._container && this._container.classed("leaflet-hidden", true); break;
|
||||||
|
case 'position-marker': _.addClass(this._marker.getElement(), 'leaflet-hidden'); break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getLatLng() {
|
||||||
|
return this._latlng;
|
||||||
|
},
|
||||||
|
|
||||||
|
_registerTooltip(props) {
|
||||||
|
this._labels[props.name] = props;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
43
web/src/lib/vendor/leaflet-elevation/src/components/summary.js
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const _ = L.Control.Elevation.Utils;
|
||||||
|
|
||||||
|
export var Summary = L.Class.extend({
|
||||||
|
initialize(opts, control) {
|
||||||
|
this.options = opts;
|
||||||
|
this.control = control;
|
||||||
|
this.labels = {};
|
||||||
|
|
||||||
|
let summary = this._container = _.create("div", "elevation-summary " + (opts.summary ? opts.summary + "-summary" : ''));
|
||||||
|
_.style(summary, 'max-width', opts.width ? opts.width + 'px' : '');
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return container => container.append(() => this._container);
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this._container.innerHTML = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
append(className, label, value) {
|
||||||
|
this._container.innerHTML += `<span class="${className}"><span class="summarylabel">${label}</span><span class="summaryvalue">${value}</span></span>`;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
Object
|
||||||
|
.keys(this.labels)
|
||||||
|
.sort((a, b) => this.labels[a].order - this.labels[b].order) // TODO: any performance issues?
|
||||||
|
.forEach((i)=> {
|
||||||
|
let value = typeof this.labels[i].value !== "function" ? this.labels[i].value : this.labels[i].value(this.control.track_info, this.labels[i].unit || '');
|
||||||
|
this.append(i /*+ " order-" + this.labels[i].order*/, L._(this.labels[i].label), value, this.labels[i].order);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_registerSummary(data) {
|
||||||
|
for (let i in data) {
|
||||||
|
data[i].order = data[i].order ?? 1000;
|
||||||
|
this.labels[i] = data[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
1335
web/src/lib/vendor/leaflet-elevation/src/control.js
vendored
Normal file
64
web/src/lib/vendor/leaflet-elevation/src/handlers/acceleration.js
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
export function Acceleration() {
|
||||||
|
|
||||||
|
const _ = L.Control.Elevation.Utils;
|
||||||
|
|
||||||
|
let opts = this.options;
|
||||||
|
let acceleration = {};
|
||||||
|
|
||||||
|
acceleration.label = opts.accelerationLabel || L._(opts.imperial ? 'ft/s²' : 'm/s²');
|
||||||
|
opts.accelerationFactor = opts.accelerationFactor || 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'acceleration',
|
||||||
|
unit: acceleration.label,
|
||||||
|
deltaMax: this.options.accelerationDeltaMax,
|
||||||
|
clampRange: this.options.accelerationRange,
|
||||||
|
decimals: 2,
|
||||||
|
pointToAttr: (_, i) => {
|
||||||
|
let dv = (this._data[i].speed - this._data[i > 0 ? i - 1 : i].speed) * (1000 / opts.timeFactor);
|
||||||
|
let dt = (this._data[i].time - this._data[i > 0 ? i - 1 : i].time) / 1000;
|
||||||
|
return dt > 0 ? Math.abs((dv / dt)) * opts.accelerationFactor : NaN;
|
||||||
|
},
|
||||||
|
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
|
||||||
|
scale: {
|
||||||
|
axis : "y",
|
||||||
|
position : "right",
|
||||||
|
scale : { min: 0, max: +1 },
|
||||||
|
tickPadding: 16,
|
||||||
|
labelX : 25,
|
||||||
|
labelY : -8,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
label : 'Acceleration',
|
||||||
|
yAttr : 'acceleration',
|
||||||
|
scaleX : 'distance',
|
||||||
|
scaleY : 'acceleration',
|
||||||
|
color : '#050402',
|
||||||
|
strokeColor : '#000',
|
||||||
|
strokeOpacity: "0.5",
|
||||||
|
fillOpacity : "0.25",
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
chart: (item) => L._("a: ") + item.acceleration + " " + acceleration.label,
|
||||||
|
marker: (item) => Math.round(item.acceleration) + " " + acceleration.label,
|
||||||
|
order: 60,
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
"minacceleration" : {
|
||||||
|
label: "Min Acceleration: ",
|
||||||
|
value: (track, unit) => Math.round(track.acceleration_min || 0) + ' ' + unit,
|
||||||
|
order: 60
|
||||||
|
},
|
||||||
|
"maxacceleration" : {
|
||||||
|
label: "Max Acceleration: ",
|
||||||
|
value: (track, unit) => Math.round(track.acceleration_max || 0) + ' ' + unit,
|
||||||
|
order: 61
|
||||||
|
},
|
||||||
|
"avgacceleration": {
|
||||||
|
label: "Avg Acceleration: ",
|
||||||
|
value: (track, unit) => Math.round(track.acceleration_avg || 0) + ' ' + unit,
|
||||||
|
order: 62
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
78
web/src/lib/vendor/leaflet-elevation/src/handlers/altitude.js
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
export function Altitude() {
|
||||||
|
|
||||||
|
const _ = L.Control.Elevation.Utils;
|
||||||
|
|
||||||
|
let opts = this.options;
|
||||||
|
let altitude = {};
|
||||||
|
|
||||||
|
let theme = opts.theme.split(' ')[0].replace('-theme', '');
|
||||||
|
let color = _.Colors[theme] || {};
|
||||||
|
|
||||||
|
opts.altitudeFactor = opts.imperial ? this.__footFactor : (opts.altitudeFactor || 1); // 1 m = (1 m)
|
||||||
|
altitude.label = opts.imperial ? "ft" : opts.yLabel;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'altitude',
|
||||||
|
required: this.options.slope,
|
||||||
|
meta: 'z',
|
||||||
|
unit: altitude.label,
|
||||||
|
statsName: 'elevation',
|
||||||
|
deltaMax: this.options.altitudeDeltaMax,
|
||||||
|
clampRange: this.options.altitudeRange,
|
||||||
|
// init: ({point}) => {
|
||||||
|
// // "alt" property is generated inside "leaflet"
|
||||||
|
// if ("alt" in point) point.meta.ele = point.alt;
|
||||||
|
// },
|
||||||
|
pointToAttr: (point, i) => {
|
||||||
|
if ("alt" in point) point.meta.ele = point.alt; // "alt" property is generated inside "leaflet"
|
||||||
|
return this._data[i].z *= opts.altitudeFactor;
|
||||||
|
},
|
||||||
|
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
|
||||||
|
grid: {
|
||||||
|
axis : "y",
|
||||||
|
position : "left",
|
||||||
|
scale : "y" // this._chart._y,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
axis : "y",
|
||||||
|
position: "left",
|
||||||
|
scale : "y", // this._chart._y,
|
||||||
|
labelX : -3,
|
||||||
|
labelY : -8,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
label : 'Altitude',
|
||||||
|
scaleX : 'distance',
|
||||||
|
scaleY : 'altitude',
|
||||||
|
className : 'area',
|
||||||
|
color : color.area || theme,
|
||||||
|
strokeColor : opts.detached ? color.stroke : '#000',
|
||||||
|
strokeOpacity: "1",
|
||||||
|
fillOpacity : opts.detached ? (color.alpha || '0.8') : 1,
|
||||||
|
preferCanvas : opts.preferCanvas,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
name: 'y',
|
||||||
|
chart: (item) => L._("y: ") + _.round(item[opts.yAttr], opts.decimalsY) + " " + altitude.label,
|
||||||
|
marker: (item) => _.round(item[opts.yAttr], opts.decimalsY) + " " + altitude.label,
|
||||||
|
order: 10,
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
"minele" : {
|
||||||
|
label: "Min Elevation: ",
|
||||||
|
value: (track, unit) => (track.elevation_min || 0).toFixed(2) + ' ' + unit,
|
||||||
|
order: 30,
|
||||||
|
},
|
||||||
|
"maxele" : {
|
||||||
|
label: "Max Elevation: ",
|
||||||
|
value: (track, unit) => (track.elevation_max || 0).toFixed(2) + ' ' + unit,
|
||||||
|
order: 31,
|
||||||
|
},
|
||||||
|
"avgele" : {
|
||||||
|
label: "Avg Elevation: ",
|
||||||
|
value: (track, unit) => (track.elevation_avg || 0).toFixed(2) + ' ' + unit,
|
||||||
|
order: 32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
54
web/src/lib/vendor/leaflet-elevation/src/handlers/cadence.js
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
export function Cadence() {
|
||||||
|
|
||||||
|
const _ = L.Control.Elevation.Utils;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'cadence', // <-- Your custom option name (eg. "cadence: true")
|
||||||
|
unit: 'rpm',
|
||||||
|
meta: 'cad', // <-- point.meta.cad
|
||||||
|
coordinateProperties: ["cads", "cadences", "cad", "cadence"], // List of GPX Extensions ("coordinateProperties") to be handled by "@tmcw/toGeoJSON"
|
||||||
|
pointToAttr: (point, i) => (point.cad ?? point.meta.cad ?? point.prev('cadence')) || 0,
|
||||||
|
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
|
||||||
|
scale: {
|
||||||
|
axis : "y",
|
||||||
|
position : "right",
|
||||||
|
scale : { min: -1, max: +1 },
|
||||||
|
tickPadding: 16,
|
||||||
|
labelX : 25,
|
||||||
|
labelY : -8,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
label : 'RPM',
|
||||||
|
yAttr : 'cadence',
|
||||||
|
scaleX : 'distance',
|
||||||
|
scaleY : 'cadence',
|
||||||
|
color : '#FFF',
|
||||||
|
strokeColor : 'blue',
|
||||||
|
strokeOpacity: "0.85",
|
||||||
|
fillOpacity : "0.1",
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
name: 'cadence',
|
||||||
|
chart: (item) => L._("cad: ") + item.cadence + " " + 'rpm',
|
||||||
|
marker: (item) => Math.round(item.cadence) + " " + 'rpm',
|
||||||
|
order: 1
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
"minrpm": {
|
||||||
|
label: "Min RPM: ",
|
||||||
|
value: (track, unit) => Math.round(track.cadence_min || 0) + ' ' + unit,
|
||||||
|
// order: 30
|
||||||
|
},
|
||||||
|
"maxrpm": {
|
||||||
|
label: "Max RPM: ",
|
||||||
|
value: (track, unit) => Math.round(track.cadence_max || 0) + ' ' + unit,
|
||||||
|
// order: 30
|
||||||
|
},
|
||||||
|
"avgrpm": {
|
||||||
|
label: "Avg RPM: ",
|
||||||
|
value: (track, unit) => Math.round(track.cadence_avg || 0) + ' ' + unit,
|
||||||
|
// order: 20
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
46
web/src/lib/vendor/leaflet-elevation/src/handlers/distance.js
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
export function Distance() {
|
||||||
|
|
||||||
|
const _ = L.Control.Elevation.Utils;
|
||||||
|
|
||||||
|
let opts = this.options;
|
||||||
|
let distance = {};
|
||||||
|
|
||||||
|
opts.distanceFactor = opts.imperial ? this.__mileFactor : (opts.distanceFactor || 1); // 1 km = (1000 m)
|
||||||
|
distance.label = opts.imperial ? "mi" : opts.xLabel;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'distance',
|
||||||
|
required: true,
|
||||||
|
attr: 'dist',
|
||||||
|
unit: distance.label,
|
||||||
|
decimals: 5,
|
||||||
|
pointToAttr: (_, i) => (i > 0 ? this._data[i - 1].dist : 0) + (this._data[i].latlng.distanceTo(this._data[i > 0 ? i - 1 : i].latlng) * opts.distanceFactor) / 1000, // convert back km to meters
|
||||||
|
// stats: { total: _.iSum },
|
||||||
|
onPointAdded: (distance, i) => this.track_info.distance = distance,
|
||||||
|
scale: opts.distance && {
|
||||||
|
axis : "x",
|
||||||
|
position: "bottom",
|
||||||
|
scale : "x", // this._chart._x,
|
||||||
|
labelY : 25,
|
||||||
|
labelX : () => this._width() + 6,
|
||||||
|
ticks : () => _.clamp(this._chart._xTicks() / 2, [4, +Infinity]),
|
||||||
|
},
|
||||||
|
grid: opts.distance && {
|
||||||
|
axis : "x",
|
||||||
|
position : "bottom",
|
||||||
|
scale : "x" // this._chart._x,
|
||||||
|
},
|
||||||
|
tooltip: opts.distance && {
|
||||||
|
name: 'x',
|
||||||
|
chart: (item) => L._("x: ") + _.round(item[opts.xAttr], opts.decimalsX) + " " + distance.label,
|
||||||
|
order: 20
|
||||||
|
},
|
||||||
|
summary: opts.distance && {
|
||||||
|
"totlen" : {
|
||||||
|
label: "Total Length: ",
|
||||||
|
value: (track) => (track.distance || 0).toFixed(2) + ' ' + distance.label,
|
||||||
|
order: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
53
web/src/lib/vendor/leaflet-elevation/src/handlers/heart.js
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
export function Heart() {
|
||||||
|
|
||||||
|
const _ = L.Control.Elevation.Utils;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'heart', // <-- Your custom option name (eg. "heart: true")
|
||||||
|
unit: 'bpm',
|
||||||
|
meta: 'hr', // <-- point.meta.hr
|
||||||
|
coordinateProperties: ["heart", "heartRates", "heartRate"], // List of GPX Extensions ("coordinateProperties") to be handled by "@tmcw/toGeoJSON"
|
||||||
|
pointToAttr: (point, i) => (point.hr ?? point.meta.hr ?? point.prev('heart')) || 0,
|
||||||
|
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
|
||||||
|
scale: {
|
||||||
|
axis : "y",
|
||||||
|
position : "left",
|
||||||
|
scale : { min: -1, max: +1 },
|
||||||
|
tickPadding: 25,
|
||||||
|
labelX : -30,
|
||||||
|
labelY : -8,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
label : 'ECG',
|
||||||
|
yAttr : 'heart',
|
||||||
|
scaleX : 'distance',
|
||||||
|
scaleY : 'heart',
|
||||||
|
color : 'white',
|
||||||
|
strokeColor : 'red',
|
||||||
|
strokeOpacity: "0.85",
|
||||||
|
fillOpacity : "0.1",
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
chart: (item) => L._("hr: ") + item.heart + " " + 'bpm',
|
||||||
|
marker: (item) => Math.round(item.heart) + " " + 'bpm',
|
||||||
|
order: 1
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
"minbpm": {
|
||||||
|
label: "Min BPM: ",
|
||||||
|
value: (track, unit) => Math.round(track.heart_min || 0) + ' ' + unit,
|
||||||
|
// order: 30
|
||||||
|
},
|
||||||
|
"maxbpm": {
|
||||||
|
label: "Max BPM: ",
|
||||||
|
value: (track, unit) => Math.round(track.heart_max || 0) + ' ' + unit,
|
||||||
|
// order: 30
|
||||||
|
},
|
||||||
|
"avgbpm": {
|
||||||
|
label: "Avg BPM: ",
|
||||||
|
value: (track, unit) => Math.round(track.heart_avg || 0) + ' ' + unit,
|
||||||
|
// order: 20
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
45
web/src/lib/vendor/leaflet-elevation/src/handlers/labels.js
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* @see https://github.com/Raruto/leaflet-elevation/issues/211
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* L.control.Elevation({ handlers: [ 'Labels' ], labelsRotation: 25, labelsAlign: 'start' })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function Labels() {
|
||||||
|
|
||||||
|
this.on('elechart_updated', function(e) {
|
||||||
|
|
||||||
|
const pointG = this._chart._chart.pane('point');
|
||||||
|
|
||||||
|
const textRotation = this.options.labelsRotation ?? -90;
|
||||||
|
const textAnchor = this.options.labelsAlign;
|
||||||
|
|
||||||
|
if (90 == Math.abs(textRotation)) {
|
||||||
|
|
||||||
|
pointG.selectAll('text')
|
||||||
|
.attr('dy', '4px')
|
||||||
|
.attr("dx", (d, i, el) => Math.sign(textRotation) * (this._height() - d3.select(el[i].parentElement).datum().y - 8) + 'px')
|
||||||
|
.attr('text-anchor', textRotation > 0 ? 'end' : 'start')
|
||||||
|
.attr('transform', 'rotate(' + textRotation + ')')
|
||||||
|
|
||||||
|
pointG.selectAll('circle')
|
||||||
|
.attr('r', '2.5')
|
||||||
|
.attr('fill', '#fff')
|
||||||
|
.attr("stroke", '#000')
|
||||||
|
.attr("stroke-width", '1.1');
|
||||||
|
|
||||||
|
} else if (!isNaN(textRotation)) {
|
||||||
|
|
||||||
|
pointG.selectAll('text')
|
||||||
|
.attr("dx", "4px")
|
||||||
|
.attr("dy", "-9px")
|
||||||
|
.attr('text-anchor', textAnchor ?? (0 == textRotation ? 'start' : 'middle'))
|
||||||
|
.attr('transform', 'rotate('+ -textRotation +')')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return { };
|
||||||
|
}
|
||||||
122
web/src/lib/vendor/leaflet-elevation/src/handlers/lineargradient.js
vendored
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* @see https://github.com/Raruto/leaflet-elevation/issues/251
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* L.control.Elevation({
|
||||||
|
* altitude: true,
|
||||||
|
* distance: true,
|
||||||
|
* handlers: [ 'Altitude', 'Distance', 'LinearGradient', ],
|
||||||
|
* linearGradient: {
|
||||||
|
* attr: 'z',
|
||||||
|
* path: 'altitude',
|
||||||
|
* range: { 0.0: '#008800', 0.5: '#ffff00', 1.0: '#ff0000' },
|
||||||
|
* min: 'elevation_min',
|
||||||
|
* max: 'elevation_max',
|
||||||
|
* },
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function LinearGradient() {
|
||||||
|
|
||||||
|
if (!this.options.linearGradient) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const _ = L.Control.Elevation.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize gradient color palette.
|
||||||
|
*/
|
||||||
|
const get_palette = function ({range, min, max, depth = 256}) {
|
||||||
|
const canvas = document.createElement('canvas'),
|
||||||
|
ctx = canvas.getContext('2d'),
|
||||||
|
gradient = ctx.createLinearGradient(0, 0, 0, depth);
|
||||||
|
|
||||||
|
canvas.width = 1;
|
||||||
|
canvas.height = depth;
|
||||||
|
|
||||||
|
for (let i in range) {
|
||||||
|
gradient.addColorStop(i, range[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.fillRect(0, 0, 1, depth);
|
||||||
|
|
||||||
|
const { data } = ctx.getImageData(0, 0, 1, depth);
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Gets the RGB values of a given z value of the current palette.
|
||||||
|
*
|
||||||
|
* @param {number} value - Value to get the color for, should be between min and max.
|
||||||
|
* @returns {string} The RGB values as `rgb(r, g, b)` string
|
||||||
|
*/
|
||||||
|
getRGBColor(value) {
|
||||||
|
const idx = Math.floor(Math.min(Math.max((value - min) / (max - min), 0), 0.999) * depth) * 4;
|
||||||
|
return 'rgb(' + [data[idx], data[idx + 1], data[idx + 2]].join(',') + ')';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const { preferCanvas } = this.options;
|
||||||
|
const { attr, path: path_name, range, min, max } = L.extend({
|
||||||
|
attr: 'z',
|
||||||
|
path: 'altitude',
|
||||||
|
range: { 0.0: '#008800', 0.5: '#ffff00', 1.0: '#ff0000' },
|
||||||
|
min: 'elevation_min',
|
||||||
|
max: 'elevation_max',
|
||||||
|
}, (true === this.options.linearGradient) ? {} : this.options.linearGradient);
|
||||||
|
|
||||||
|
const gradient_id = path_name + '-gradient-' + _.randomId();
|
||||||
|
const legend_id = 'legend-' + gradient_id;
|
||||||
|
|
||||||
|
// Charte profile gradient
|
||||||
|
this.on('elechart_axis', () => {
|
||||||
|
if (!this._data.length) return;
|
||||||
|
|
||||||
|
const chart = this._chart;
|
||||||
|
const path = chart._paths[path_name];
|
||||||
|
const { defs } = chart._chart.utils;
|
||||||
|
|
||||||
|
const palette = get_palette({
|
||||||
|
min: isFinite(this.track_info[min]) ? this.track_info[min] : 0,
|
||||||
|
max: isFinite(this.track_info[max]) ? this.track_info[max] : 1,
|
||||||
|
range,
|
||||||
|
});
|
||||||
|
|
||||||
|
let gradient;
|
||||||
|
|
||||||
|
if (preferCanvas) {
|
||||||
|
/** ref: `path.__fillStyle` within L.Control.Elevation.Utils::drawCanvas(ctx, path) */
|
||||||
|
path.__fillStyle = gradient = chart._context.createLinearGradient(0, 0, chart._width(), 0);
|
||||||
|
} else {
|
||||||
|
defs.select('#' + gradient_id).remove();
|
||||||
|
gradient = defs.append('svg:linearGradient').attr('id', gradient_id);
|
||||||
|
gradient.addColorStop = function(offset, color) { gradient.append('svg:stop').attr('offset', offset).attr('stop-color', color) };
|
||||||
|
path.attr('fill', 'url(#' + gradient_id + ')').classed('area', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate gradient for each segment picking colors from palette
|
||||||
|
for (let i = 0, data = this._data; i < data.length; i++) {
|
||||||
|
gradient.addColorStop((i) / data.length, palette.getRGBColor(data[i][attr]));
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Legend item gradient
|
||||||
|
this.on('elechart_updated', () => {
|
||||||
|
const chart = this._chart;
|
||||||
|
const { defs } = chart._chart.utils;
|
||||||
|
defs.select('#' + legend_id).remove();
|
||||||
|
const legendGradient = defs.append('svg:linearGradient').attr('id', legend_id);
|
||||||
|
Object.keys(range).sort().forEach(i => legendGradient.append('svg:stop').attr('offset', i).attr('stop-color', range[i]));
|
||||||
|
|
||||||
|
chart._container
|
||||||
|
.select('.legend-' + path_name + ' > rect')
|
||||||
|
.attr('fill', 'url(#' + legend_id + ')')
|
||||||
|
.classed('area', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { };
|
||||||
|
}
|
||||||
65
web/src/lib/vendor/leaflet-elevation/src/handlers/pace.js
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
export function Pace() {
|
||||||
|
|
||||||
|
const _ = L.Control.Elevation.Utils;
|
||||||
|
|
||||||
|
let opts = this.options;
|
||||||
|
let pace = {};
|
||||||
|
|
||||||
|
pace.label = opts.paceLabel || L._(opts.imperial ? 'min/mi' : 'min/km');
|
||||||
|
opts.paceFactor = opts.paceFactor || 60; // 1 min = 60 sec
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'pace',
|
||||||
|
unit: pace.label,
|
||||||
|
deltaMax: this.options.paceDeltaMax,
|
||||||
|
clampRange: this.options.paceRange,
|
||||||
|
decimals: 2,
|
||||||
|
pointToAttr: (_, i) => {
|
||||||
|
let dx = (this._data[i].dist - this._data[i > 0 ? i - 1 : i].dist) * 1000;
|
||||||
|
let dt = this._data[i].time - this._data[ i > 0 ? i - 1 : i].time;
|
||||||
|
return dx > 0 ? Math.abs((dt / dx) / opts.paceFactor) : NaN;
|
||||||
|
},
|
||||||
|
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
|
||||||
|
scale : (this.options.pace && this.options.pace != "summary") && {
|
||||||
|
axis : "y",
|
||||||
|
position : "right",
|
||||||
|
scale : { min : 0, max : +1 },
|
||||||
|
tickPadding: 16,
|
||||||
|
labelX : 25,
|
||||||
|
labelY : -8,
|
||||||
|
},
|
||||||
|
path: (this.options.pace && this.options.pace != "summary") && {
|
||||||
|
// name : 'pace',
|
||||||
|
label : 'Pace',
|
||||||
|
yAttr : "pace",
|
||||||
|
scaleX : 'distance',
|
||||||
|
scaleY : 'pace',
|
||||||
|
color : '#03ffff',
|
||||||
|
strokeColor : '#000',
|
||||||
|
strokeOpacity: "0.5",
|
||||||
|
fillOpacity : "0.25",
|
||||||
|
},
|
||||||
|
tooltip: (this.options.pace) && {
|
||||||
|
chart: (item) => L._('pace: ') + item.pace + " " + pace.label,
|
||||||
|
marker: (item) => Math.round(item.pace) + " " + pace.label,
|
||||||
|
order: 50,
|
||||||
|
},
|
||||||
|
summary: (this.options.pace) && {
|
||||||
|
"minpace" : {
|
||||||
|
label: "Min Pace: ",
|
||||||
|
value: (track, unit) => Math.round(track.pace_min || 0) + ' ' + unit,
|
||||||
|
order: 51
|
||||||
|
},
|
||||||
|
"maxpace" : {
|
||||||
|
label: "Max Pace: ",
|
||||||
|
value: (track, unit) => Math.round(track.pace_max || 0) + ' ' + unit,
|
||||||
|
order: 51
|
||||||
|
},
|
||||||
|
"avgpace": {
|
||||||
|
label: "Avg Pace: ",
|
||||||
|
value: (track, unit) => Math.round(track.pace_avg || 0) + ' ' + unit,
|
||||||
|
order: 52
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
109
web/src/lib/vendor/leaflet-elevation/src/handlers/runner.js
vendored
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* @see https://github.com/Igor-Vladyka/leaflet.motion
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* L.control.Elevation({ handlers: [ 'Runner' ], runnerOptions: { polyline: {..}, motion: {..}, marker {..} } })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function Runner() {
|
||||||
|
|
||||||
|
await this.import(this.__LMOTION || 'https://unpkg.com/leaflet.motion@0.3.2/dist/leaflet.motion.min.js', typeof L.Motion !== 'object')
|
||||||
|
|
||||||
|
let { runnerOptions } = this.options;
|
||||||
|
|
||||||
|
runnerOptions = L.extend(
|
||||||
|
{ polyline: {}, motion: {}, marker: undefined },
|
||||||
|
'object' === typeof runnerOptions ? runnerOptions : {}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Custom tooltips
|
||||||
|
this._registerTooltip({
|
||||||
|
name: 'here',
|
||||||
|
marker: (item) => L._("You are here: "),
|
||||||
|
order: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._registerTooltip({
|
||||||
|
name: 'distance',
|
||||||
|
marker: (item) => Math.round(item.dist) + " " + this.options.xLabel,
|
||||||
|
order: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCheckpoint = function (checkpoint) {
|
||||||
|
return this._registerCheckPoint({ // <-- NB these are private functions use them at your own risk!
|
||||||
|
latlng: this._findItemForX(this._x(checkpoint.dist)).latlng,
|
||||||
|
label: checkpoint.label || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addRunner = function (runner) {
|
||||||
|
let x = this._x(runner.dist);
|
||||||
|
let y = this._y(this._findItemForX(x).z)
|
||||||
|
let g = d3.select(this._container)
|
||||||
|
.select('svg > g')
|
||||||
|
.append("svg:circle")
|
||||||
|
.attr("class", "runner " + this.options.theme + " height-focus circle-lower")
|
||||||
|
.attr("r", 6)
|
||||||
|
.attr("cx", x)
|
||||||
|
.attr("cy", y);
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setPositionFromLatLng = function (latlng) {
|
||||||
|
this._onMouseMoveLayer({ latlng: latlng }); // Update map and chart "markers" from latlng
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tick = function (runner, dist = 0, inc = 0.1) {
|
||||||
|
dist = (dist <= this.track_info.distance - inc) ? dist + inc : 0;
|
||||||
|
this.updateRunnerPos(runner, dist);
|
||||||
|
setTimeout(() => this.tick(runner, dist), 150);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateRunnerPos = function (runner, pos) {
|
||||||
|
let curr, x;
|
||||||
|
|
||||||
|
if (pos instanceof L.LatLng) {
|
||||||
|
curr = this._findItemForLatLng(pos);
|
||||||
|
x = this._x(curr.dist);
|
||||||
|
} else {
|
||||||
|
x = this._x(pos);
|
||||||
|
curr = this._findItemForX(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
runner
|
||||||
|
.attr("cx", x)
|
||||||
|
.attr("cy", this._y(curr.z));
|
||||||
|
this.setPositionFromLatLng(curr.latlng);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.animate = function (layer, speed = 1500) {
|
||||||
|
|
||||||
|
if (this._runner) {
|
||||||
|
this._runner.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.setStyle({ opacity: 0.5 });
|
||||||
|
|
||||||
|
const geo = L.geoJson(layer.toGeoJSON(), { coordsToLatLng: (coords) => L.latLng(coords[0], coords[1], coords[2] * (this.options.altitudeFactor || 1)) });
|
||||||
|
this._runner = L.motion.polyline(
|
||||||
|
geo.toGeoJSON().features[0].geometry.coordinates,
|
||||||
|
L.extend({}, { color: 'red', pane: 'elevationPane', attribution: '' }, runnerOptions.polyline),
|
||||||
|
L.extend({}, { auto: true, speed: speed, }, runnerOptions.motion),
|
||||||
|
runnerOptions.marker || undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
// Override default function behavior: `L.Motion.Polyline::_drawMarker()`
|
||||||
|
this._runner._drawMarker = new Proxy(this._runner._drawMarker, {
|
||||||
|
apply: (target, thisArg, argArray) => {
|
||||||
|
thisArg._runner = thisArg._runner || this.addRunner({ dist: 0 });
|
||||||
|
this.updateRunnerPos(thisArg._runner, argArray[0]);
|
||||||
|
return target.apply(thisArg, argArray);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._runner.addTo(this._map);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
79
web/src/lib/vendor/leaflet-elevation/src/handlers/slope.js
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
export function Slope() {
|
||||||
|
|
||||||
|
const _ = L.Control.Elevation.Utils;
|
||||||
|
|
||||||
|
let opts = this.options;
|
||||||
|
let slope = {};
|
||||||
|
|
||||||
|
slope.label = opts.slopeLabel || '%';
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'slope',
|
||||||
|
meta: 'slope',
|
||||||
|
unit: slope.label,
|
||||||
|
deltaMax: this.options.slopedDeltaMax,
|
||||||
|
clampRange: this.options.slopeRange,
|
||||||
|
decimals: 2,
|
||||||
|
pointToAttr: (_, i) => { // slope in % = ( dy / dx ) * 100;
|
||||||
|
let dx = (this._data[i].dist - this._data[i > 0 ? i - 1 : i].dist) * 1000;
|
||||||
|
let dy = this._data[i][this.options.yAttr] - this._data[i > 0 ? i - 1 : i][this.options.yAttr];
|
||||||
|
return dx !== 0 ? (dy / dx) * 100 : NaN;
|
||||||
|
},
|
||||||
|
onPointAdded: (_, i) => {
|
||||||
|
let dz = this._data[i][this.options.yAttr] - this._data[i > 0 ? i - 1 : i][this.options.yAttr];
|
||||||
|
if (dz > 0) this.track_info.ascent = (this.track_info.ascent || 0) + dz; // Total Ascent
|
||||||
|
else if (dz < 0) this.track_info.descent = (this.track_info.descent || 0) - dz; // Total Descent
|
||||||
|
},
|
||||||
|
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg, },
|
||||||
|
scale: {
|
||||||
|
axis : "y",
|
||||||
|
position : "right",
|
||||||
|
scale : { min: -1, max: +1 },
|
||||||
|
tickPadding: 16,
|
||||||
|
labelX : 25,
|
||||||
|
labelY : -8,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
label : 'Slope',
|
||||||
|
yAttr : 'slope',
|
||||||
|
scaleX : 'distance',
|
||||||
|
scaleY : 'slope',
|
||||||
|
color : '#F00',
|
||||||
|
strokeColor : '#000',
|
||||||
|
strokeOpacity: "0.5",
|
||||||
|
fillOpacity : "0.25",
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
chart: (item) => L._("m: ") + item.slope + slope.label,
|
||||||
|
marker: (item) => Math.round(item.slope) + slope.label,
|
||||||
|
order: 40,
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
"minslope": {
|
||||||
|
label: "Min Slope: ",
|
||||||
|
value: (track, unit) => Math.round(track.slope_min || 0) + ' ' + unit,
|
||||||
|
order: 40
|
||||||
|
},
|
||||||
|
"maxslope": {
|
||||||
|
label: "Max Slope: ",
|
||||||
|
value: (track, unit) => Math.round(track.slope_max || 0) + ' ' + unit,
|
||||||
|
order: 41
|
||||||
|
},
|
||||||
|
"avgslope": {
|
||||||
|
label: "Avg Slope: ",
|
||||||
|
value: (track, unit) => Math.round(track.slope_avg || 0) + ' ' + unit,
|
||||||
|
order: 42
|
||||||
|
},
|
||||||
|
"ascent" : {
|
||||||
|
label: "Total Ascent: ",
|
||||||
|
value: (track, unit) => Math.round(track.ascent || 0) + ' ' + (this.options.imperial ? 'ft' : 'm'),
|
||||||
|
order: 43
|
||||||
|
},
|
||||||
|
"descent" : {
|
||||||
|
label: "Total Descent: ",
|
||||||
|
value: (track, unit) => Math.round(track.descent || 0) + ' ' + (this.options.imperial ? 'ft' : 'm'),
|
||||||
|
order: 45
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
66
web/src/lib/vendor/leaflet-elevation/src/handlers/speed.js
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
export function Speed() {
|
||||||
|
|
||||||
|
const _ = L.Control.Elevation.Utils;
|
||||||
|
|
||||||
|
let opts = this.options;
|
||||||
|
let speed = {};
|
||||||
|
|
||||||
|
speed.label = opts.speedLabel || L._(opts.imperial ? 'mph' : 'km/h');
|
||||||
|
opts.speedFactor = opts.speedFactor || 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'speed',
|
||||||
|
required: (this.options.acceleration),
|
||||||
|
unit: speed.label,
|
||||||
|
deltaMax: this.options.speedDeltaMax,
|
||||||
|
clampRange: this.options.speedRange,
|
||||||
|
decimals: 2,
|
||||||
|
pointToAttr: (_, i) => {
|
||||||
|
let dx = (this._data[i].dist - this._data[i > 0 ? i - 1 : i].dist) * 1000;
|
||||||
|
let dt = this._data[i].time - this._data[ i > 0 ? i - 1 : i].time;
|
||||||
|
return dt > 0 ? Math.abs((dx / dt) * opts.timeFactor) * opts.speedFactor : NaN;
|
||||||
|
},
|
||||||
|
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
|
||||||
|
scale : (this.options.speed && this.options.speed != "summary") && {
|
||||||
|
axis : "y",
|
||||||
|
position : "right",
|
||||||
|
scale : { min : 0, max : +1 },
|
||||||
|
tickPadding: 16,
|
||||||
|
labelX : 25,
|
||||||
|
labelY : -8,
|
||||||
|
},
|
||||||
|
path: (this.options.speed && this.options.speed != "summary") && {
|
||||||
|
// name : 'speed',
|
||||||
|
label : 'Speed',
|
||||||
|
yAttr : "speed",
|
||||||
|
scaleX : 'distance',
|
||||||
|
scaleY : 'speed',
|
||||||
|
color : '#03ffff',
|
||||||
|
strokeColor : '#000',
|
||||||
|
strokeOpacity: "0.5",
|
||||||
|
fillOpacity : "0.25",
|
||||||
|
},
|
||||||
|
tooltip: (this.options.speed) && {
|
||||||
|
chart: (item) => L._('v: ') + item.speed + " " + speed.label,
|
||||||
|
marker: (item) => Math.round(item.speed) + " " + speed.label,
|
||||||
|
order: 50,
|
||||||
|
},
|
||||||
|
summary: (this.options.speed) && {
|
||||||
|
"minspeed" : {
|
||||||
|
label: "Min Speed: ",
|
||||||
|
value: (track, unit) => Math.round(track.speed_min || 0) + ' ' + unit,
|
||||||
|
order: 51
|
||||||
|
},
|
||||||
|
"maxspeed" : {
|
||||||
|
label: "Max Speed: ",
|
||||||
|
value: (track, unit) => Math.round(track.speed_max || 0) + ' ' + unit,
|
||||||
|
order: 51
|
||||||
|
},
|
||||||
|
"avgspeed": {
|
||||||
|
label: "Avg Speed: ",
|
||||||
|
value: (track, unit) => Math.round(track.speed_avg || 0) + ' ' + unit,
|
||||||
|
order: 52
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
62
web/src/lib/vendor/leaflet-elevation/src/handlers/temperature.js
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
export function Temperature() {
|
||||||
|
|
||||||
|
const _ = L.Control.Elevation.Utils;
|
||||||
|
|
||||||
|
let temperature = {};
|
||||||
|
let opts = this.options;
|
||||||
|
temperature.label = opts.label || L._(opts.imperial ? '°F' : '°C');
|
||||||
|
|
||||||
|
// Fahrenheit = (Celsius * 9/5) + 32
|
||||||
|
opts.temperatureFactor1 = opts.temperatureFactor1 ?? (opts.imperial ? 1.8 : 1);
|
||||||
|
opts.temperatureFactor2 = opts.temperatureFactor2 ?? (opts.imperial ? 32 : 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'temperature', // <-- Your custom option name (eg. "temperature: true")
|
||||||
|
unit: temperature.label,
|
||||||
|
meta: 'atemps', // <-- point.meta.atemps
|
||||||
|
coordinateProperties: ["atemps"], // List of GPX Extensions ("coordinateProperties") to be handled by "@tmcw/toGeoJSON"
|
||||||
|
deltaMax: this.options.temperatureDeltaMax,
|
||||||
|
clampRange: this.options.temperatureRange,
|
||||||
|
decimals: 2,
|
||||||
|
pointToAttr: (point, i) => (point.meta.atemps ?? point.meta.atemps ?? point.prev('temperature')) * opts.temperatureFactor1 + opts.temperatureFactor2,
|
||||||
|
stats: { max: _.iMax, min: _.iMin, avg: _.iAvg },
|
||||||
|
scale: {
|
||||||
|
axis : "y",
|
||||||
|
position : "right",
|
||||||
|
scale : { min: -1, max: +1 },
|
||||||
|
tickPadding: 16,
|
||||||
|
labelX : +18,
|
||||||
|
labelY : -8,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
label : temperature.label,
|
||||||
|
yAttr : 'temperature',
|
||||||
|
scaleX : 'distance',
|
||||||
|
scaleY : 'temperature',
|
||||||
|
color : 'transparent',
|
||||||
|
strokeColor : '#000',
|
||||||
|
strokeOpacity: "0.85",
|
||||||
|
// fillOpacity : "0.1",
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
name: 'temperature',
|
||||||
|
chart: (item) => L._("Temp: ") + Math.round(item.temperature).toLocaleString() + " " + temperature.label,
|
||||||
|
marker: (item) => Math.round(item.temperature).toLocaleString() + " " + temperature.label,
|
||||||
|
order: 1
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
"mintemp": {
|
||||||
|
label: "Min Temp: ",
|
||||||
|
value: (track, unit) => Math.round(track.temperature_min || 0) + ' ' + unit,
|
||||||
|
},
|
||||||
|
"maxtemp": {
|
||||||
|
label: "Max Temp: ",
|
||||||
|
value: (track, unit) => Math.round(track.temperature_max || 0) + ' ' + unit,
|
||||||
|
},
|
||||||
|
"avgtemp": {
|
||||||
|
label: "Avg Temp: ",
|
||||||
|
value: (track, unit) => Math.round(track.temperature_avg || 0) + ' ' + unit,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
103
web/src/lib/vendor/leaflet-elevation/src/handlers/time.js
vendored
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
export function Time() {
|
||||||
|
|
||||||
|
const _ = L.Control.Elevation.Utils;
|
||||||
|
|
||||||
|
let opts = this.options;
|
||||||
|
let time = {};
|
||||||
|
|
||||||
|
time.label = opts.timeLabel || 't';
|
||||||
|
opts.timeFactor = opts.timeFactor || 3600;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common AVG speeds:
|
||||||
|
* ----------------------
|
||||||
|
* slow walk = 1.8 km/h
|
||||||
|
* walking = 3.6 km/h <-- default: 3.6
|
||||||
|
* running = 10.8 km/h
|
||||||
|
* cycling = 18 km/h
|
||||||
|
* driving = 72 km/h
|
||||||
|
* ----------------------
|
||||||
|
*/
|
||||||
|
this._timeAVGSpeed = (opts.timeAVGSpeed || 3.6) * (opts.speedFactor || 1);
|
||||||
|
|
||||||
|
if (!opts.timeFormat) {
|
||||||
|
opts.timeFormat = (time) => (new Date(time)).toLocaleString().replaceAll('/', '-').replaceAll(',', ' ');
|
||||||
|
} else if (opts.timeFormat == 'time') {
|
||||||
|
opts.timeFormat = (time) => (new Date(time)).toLocaleTimeString();
|
||||||
|
} else if (opts.timeFormat == 'date') {
|
||||||
|
opts.timeFormat = (time) => (new Date(time)).toLocaleDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.xTimeFormat = opts.xTimeFormat || ((t) => _.formatTime(t).split("'")[0]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'time',
|
||||||
|
required: (this.options.speed || this.options.acceleration || this.options.timestamps),
|
||||||
|
coordinateProperties: ["coordTimes", "times", "time"],
|
||||||
|
coordPropsToMeta: _.parseDate,
|
||||||
|
pointToAttr: function(point, i) {
|
||||||
|
// Add missing timestamps (see: options.timeAVGSpeed)
|
||||||
|
if (!point.meta || !point.meta.time) {
|
||||||
|
point.meta = point.meta || {};
|
||||||
|
if (i > 0) {
|
||||||
|
let dx = (this._data[i].dist - this._data[i - 1].dist);
|
||||||
|
let t0 = this._data[i - 1].time.getTime();
|
||||||
|
point.meta.time = new Date(t0 + ( dx / this._timeAVGSpeed) * this.options.timeFactor * 1000);
|
||||||
|
} else {
|
||||||
|
point.meta.time = new Date(Date.now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle timezone offset
|
||||||
|
let time = (point.meta.time.getTime() - point.meta.time.getTimezoneOffset() * 60 * 1000 !== 0) ? point.meta.time : 0;
|
||||||
|
// Update duration
|
||||||
|
this._data[i].duration = i > 0 ? (this._data[i - 1].duration || 0) + Math.abs(time - this._data[i - 1].time) : 0;
|
||||||
|
return time;
|
||||||
|
},
|
||||||
|
onPointAdded: (_, i) => this.track_info.time = this._data[i].duration,
|
||||||
|
scale: (opts.time && opts.time != "summary" && !L.Browser.mobile) && {
|
||||||
|
axis : "x",
|
||||||
|
position : "top",
|
||||||
|
scale : {
|
||||||
|
attr : "duration",
|
||||||
|
min : 0,
|
||||||
|
},
|
||||||
|
label : time.label,
|
||||||
|
labelY : -10,
|
||||||
|
labelX : () => this._width(),
|
||||||
|
name : "time",
|
||||||
|
ticks : () => _.clamp(this._chart._xTicks() / 2, [4, +Infinity]),
|
||||||
|
tickFormat : (d) => (d == 0 ? '' : opts.xTimeFormat(d)),
|
||||||
|
onAxisMount: axis => {
|
||||||
|
axis.select(".domain")
|
||||||
|
.remove();
|
||||||
|
axis.selectAll("text")
|
||||||
|
.attr('opacity', 0.65)
|
||||||
|
.style('font-family', 'Monospace')
|
||||||
|
.style('font-size', '110%');
|
||||||
|
axis.selectAll(".tick line")
|
||||||
|
.attr('y2', this._height())
|
||||||
|
.attr('stroke-dasharray', 2)
|
||||||
|
.attr('opacity', 0.75);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltips: [
|
||||||
|
(this.options.time) && {
|
||||||
|
name: 'time',
|
||||||
|
chart: (item) => L._("T: ") + _.formatTime(item.duration || 0),
|
||||||
|
order: 20
|
||||||
|
},
|
||||||
|
(this.options.timestamps) && {
|
||||||
|
name: 'date',
|
||||||
|
chart: (item) => L._("t: ") + this.options.timeFormat(item.time),
|
||||||
|
order: 21,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
summary: (this.options.time) && {
|
||||||
|
"tottime" : {
|
||||||
|
label: "Total Time: ",
|
||||||
|
value: (track) => _.formatTime(track.time || 0),
|
||||||
|
order: 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
329
web/src/lib/vendor/leaflet-elevation/src/index.css
vendored
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
.leaflet-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
z-index: 0;
|
||||||
|
/* prevent overlapping the .elevation-detached chart */
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .background {
|
||||||
|
background-color: var(--ele-bg, rgba(70, 130, 180, 0.2));
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: visible;
|
||||||
|
display: block;
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .grid,
|
||||||
|
.elevation-control .area > foreignObject,
|
||||||
|
.elevation-control .axis,
|
||||||
|
.elevation-control .tooltip,
|
||||||
|
.height-focus.line {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .axis line,
|
||||||
|
.elevation-control .axis path {
|
||||||
|
stroke: var(--ele-axis, #2D1130);
|
||||||
|
stroke-width: 1;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .grid .tick line {
|
||||||
|
stroke: var(--ele-grid, #EEE);
|
||||||
|
stroke-width: 1px;
|
||||||
|
shape-rendering: crispEdges;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .grid path {
|
||||||
|
stroke-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .axis text,
|
||||||
|
.elevation-control .legend text,
|
||||||
|
.elevation-control .point text {
|
||||||
|
fill: #000;
|
||||||
|
font-weight: 700;
|
||||||
|
paint-order: stroke fill;
|
||||||
|
stroke: #fff;
|
||||||
|
stroke-width: 2px
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .y.axis text {
|
||||||
|
text-anchor: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .area {
|
||||||
|
fill: var(--ele-area, #4682B4);
|
||||||
|
stroke: var(--ele-stroke, #000);
|
||||||
|
stroke-width: 1.2;
|
||||||
|
paint-order: stroke fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .horizontal-drag-line {
|
||||||
|
cursor: row-resize;
|
||||||
|
stroke: transparent;
|
||||||
|
stroke-dasharray: 5;
|
||||||
|
stroke-width: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .active .horizontal-drag-line {
|
||||||
|
stroke: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .horizontal-drag-label {
|
||||||
|
fill: #000;
|
||||||
|
font-weight: 700;
|
||||||
|
paint-order: stroke;
|
||||||
|
stroke: #FFF;
|
||||||
|
stroke-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .ruler {
|
||||||
|
color: #000;
|
||||||
|
cursor: row-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .mouse-focus-line {
|
||||||
|
stroke: #000;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .mouse-focus-label-rect {
|
||||||
|
fill: #000;
|
||||||
|
fill-opacity: 0.75;
|
||||||
|
stroke-width: 1;
|
||||||
|
stroke: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .mouse-focus-label-text {
|
||||||
|
fill: #FFF;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .brush .overlay {
|
||||||
|
cursor: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control .brush .selection {
|
||||||
|
fill: var(--ele-brush, rgba(23, 74, 117, 0.4));
|
||||||
|
stroke: none;
|
||||||
|
fill-opacity: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-summary {
|
||||||
|
font-family: "Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: var(--ele-sum-margin, 0 auto);
|
||||||
|
text-shadow: var(--ele-sum-shadow, 1px 0 0 #FFF, -1px 0 0 #FFF, 0 1px 0 #FFF, 0 -1px 0 #FFF, 1px 1px #FFF, -1px -1px 0 #FFF, 1px -1px 0 #FFF, -1px 1px 0 #FFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-summary>span:not(:last-child):after {
|
||||||
|
content: var(--ele-sum-sep, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiline-summary>span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiline-summary .download {
|
||||||
|
float: right;
|
||||||
|
margin-top: -3em;
|
||||||
|
margin-right: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-summary .summaryvalue {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-toggle-icon {
|
||||||
|
background-color: #fff;
|
||||||
|
right: 5px;
|
||||||
|
top: 5px;
|
||||||
|
height: var(--ele-toggle-size, 36px);
|
||||||
|
width: var(--ele-toggle-size, 36px);
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 7px rgba(0, 0, 0, 0.4);
|
||||||
|
border-radius: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
position: var(--ele-toggle-pos, relative);
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-toggle-icon:before {
|
||||||
|
content: '\2716';
|
||||||
|
display: var(--ele-close-btn, none);
|
||||||
|
color: #000;
|
||||||
|
width: 100%;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-elevation-pane .height-focus,
|
||||||
|
.leaflet-overlay-pane .height-focus {
|
||||||
|
stroke: #000;
|
||||||
|
fill: var(--ele-circle, var(--ele-area, #FFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-elevation-pane .height-focus.line,
|
||||||
|
.leaflet-overlay-pane .height-focus.line {
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-elevation-pane .height-focus-label,
|
||||||
|
.leaflet-overlay-pane .height-focus-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
fill: #000;
|
||||||
|
paint-order: stroke;
|
||||||
|
stroke: #FFF;
|
||||||
|
stroke-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-waypoint-icon:before,
|
||||||
|
.elevation-position-icon:before {
|
||||||
|
content: "";
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--ele-marker) no-repeat center center / contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-polyline {
|
||||||
|
stroke: var(--ele-poly, var(--ele-area, #000));
|
||||||
|
filter: drop-shadow(1px 1px 0 #FFF) drop-shadow(-1px -1px 0 #FFF) drop-shadow(1px -1px 0 #FFF) drop-shadow(-1px 1px 0 #FFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CHART STATES /////////////////////////////////////////////////// */
|
||||||
|
|
||||||
|
.elevation-detached {
|
||||||
|
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
|
height: auto;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-detached .area {
|
||||||
|
fill-opacity: var(--ele-alpha, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-detached.elevation-collapsed .elevation-summary {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-detached.elevation-collapsed .elevation-toggle-icon {
|
||||||
|
top: 5px;
|
||||||
|
right: 9px;
|
||||||
|
bottom: 5px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control.elevation-collapsed > * {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-control.elevation-collapsed > .elevation-toggle-icon {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-detached {
|
||||||
|
--ele-sum-margin: 12px 35px;
|
||||||
|
--ele-sum-shadow: none;
|
||||||
|
--ele-toggle-pos: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-expanded {
|
||||||
|
--ele-close-btn: inline-block;
|
||||||
|
--ele-toggle-bg: none;
|
||||||
|
--ele-toggle-pos: absolute;
|
||||||
|
--ele-toggle-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-summary {
|
||||||
|
--ele-sum-sep: "\0020\2014\0020";
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-waypoint-icon {
|
||||||
|
--ele-marker: url(../images/elevation-pushpin.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-position-icon {
|
||||||
|
--ele-marker: url(../images/elevation-position.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LIME THEME ///////////////////////////////////////////////////// */
|
||||||
|
.lime-theme {
|
||||||
|
--ele-bg: rgba(156, 194, 34, 0.2);
|
||||||
|
--ele-axis: #566B13;
|
||||||
|
--ele-area: #9CC222;
|
||||||
|
--ele-grid: #CCC;
|
||||||
|
--ele-brush: rgba(99, 126, 11, 0.4);
|
||||||
|
--ele-poly: #566B13;
|
||||||
|
--ele-line: #70ab00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* STEELBLUE THEME //////////////////////////////////////////////// */
|
||||||
|
.steelblue-theme {
|
||||||
|
--ele-axis: #0D1821;
|
||||||
|
--ele-area: #4682B4;
|
||||||
|
--ele-brush: rgba(23, 74, 117, 0.4);
|
||||||
|
--ele-line: #174A75;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PURPLE THEME /////////////////////////////////////////////////// */
|
||||||
|
.purple-theme {
|
||||||
|
--ele-bg: rgba(115, 44, 123, 0.2);
|
||||||
|
--ele-area: #732C7B;
|
||||||
|
--ele-brush: rgba(74, 14, 80, 0.4);
|
||||||
|
--ele-line: #732c7b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* YELLOW THEME /////////////////////////////////////////////////// */
|
||||||
|
.yellow-theme {
|
||||||
|
--ele-area: #FF0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RED THEME ////////////////////////////////////////////////////// */
|
||||||
|
.red-theme {
|
||||||
|
--ele-area: #F00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MAGENTA THEME ////////////////////////////////////////////////// */
|
||||||
|
.magenta-theme {
|
||||||
|
--ele-bg: rgba(255, 255, 255, 0.47);
|
||||||
|
--ele-area: #FF005E;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LIGHTBLUE THEME //////////////////////////////////////////////// */
|
||||||
|
.lightblue-theme {
|
||||||
|
--ele-area: #3366CC;
|
||||||
|
--ele-alpha: 0.45;
|
||||||
|
--ele-stroke: #4682B4;
|
||||||
|
--ele-circle: #fff;
|
||||||
|
--ele-line: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation-detached.lightblue-theme .area {
|
||||||
|
stroke: #3366CC;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* leaflet-distance-markers */
|
||||||
|
.dist-marker {
|
||||||
|
font-size: 0.5rem;
|
||||||
|
border: 1px solid #777;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: #000;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
42
web/src/lib/vendor/leaflet-elevation/src/index.js
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as _ from './utils';
|
||||||
|
import { Elevation } from './control';
|
||||||
|
|
||||||
|
Elevation.Utils = _;
|
||||||
|
|
||||||
|
L.control.elevation = (options) => new Elevation(options);
|
||||||
88
web/src/lib/vendor/leaflet-elevation/src/options.js
vendored
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import * as _ from './utils';
|
||||||
|
|
||||||
|
export var Options = {
|
||||||
|
autofitBounds: true,
|
||||||
|
autohide: false,
|
||||||
|
autohideMarker: true,
|
||||||
|
almostover: true,
|
||||||
|
altitude: true,
|
||||||
|
closeBtn: true,
|
||||||
|
collapsed: false,
|
||||||
|
detached: true,
|
||||||
|
distance: true,
|
||||||
|
distanceMarkers: { lazy: true, distance: true, direction: true },
|
||||||
|
dragging: !L.Browser.mobile,
|
||||||
|
downloadLink: 'link',
|
||||||
|
elevationDiv: "#elevation-div",
|
||||||
|
edgeScale: { bar: true, icon: false, coords: false },
|
||||||
|
followMarker: true,
|
||||||
|
imperial: false,
|
||||||
|
legend: true,
|
||||||
|
handlers: ["Distance", "Time", "Altitude", "Slope", "Speed", "Acceleration"],
|
||||||
|
hotline: 'elevation',
|
||||||
|
marker: 'elevation-line',
|
||||||
|
markerIcon: L.divIcon({
|
||||||
|
className: 'elevation-position-marker',
|
||||||
|
html: '<i class="elevation-position-icon"></i>',
|
||||||
|
iconSize: [32, 32],
|
||||||
|
iconAnchor: [16, 16],
|
||||||
|
}),
|
||||||
|
position: "topright",
|
||||||
|
polyline: {
|
||||||
|
className: 'elevation-polyline',
|
||||||
|
color: '#000',
|
||||||
|
opacity: 0.75,
|
||||||
|
weight: 5,
|
||||||
|
lineCap: 'round'
|
||||||
|
},
|
||||||
|
polylineSegments: {
|
||||||
|
className: 'elevation-polyline-segments',
|
||||||
|
color: '#F00',
|
||||||
|
interactive: false,
|
||||||
|
},
|
||||||
|
preferCanvas: false,
|
||||||
|
reverseCoords: false,
|
||||||
|
ruler: true,
|
||||||
|
theme: "lightblue-theme",
|
||||||
|
summary: 'inline',
|
||||||
|
slope: false,
|
||||||
|
speed: false,
|
||||||
|
time: true,
|
||||||
|
timeFactor: 3600,
|
||||||
|
timestamps: false,
|
||||||
|
trkStart: { className: 'start-marker', radius: 6, weight: 2, color: '#fff', fillColor: '#00d800', fillOpacity: 1, interactive: false },
|
||||||
|
trkEnd: { className: 'end-marker', radius: 6, weight: 2, color: '#fff', fillColor: '#ff0606', fillOpacity: 1, interactive: false },
|
||||||
|
waypoints: true,
|
||||||
|
wptIcons: {
|
||||||
|
'': L.divIcon({
|
||||||
|
className: 'elevation-waypoint-marker',
|
||||||
|
html: '<i class="elevation-waypoint-icon default"></i>',
|
||||||
|
iconSize: [30, 30],
|
||||||
|
iconAnchor: [8, 30],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
wptLabels: true,
|
||||||
|
xAttr: "dist",
|
||||||
|
xLabel: "km",
|
||||||
|
yAttr: "z",
|
||||||
|
yLabel: "m",
|
||||||
|
zFollow: false,
|
||||||
|
zooming: !L.Browser.Mobile,
|
||||||
|
|
||||||
|
// Quite uncommon and undocumented options
|
||||||
|
margins: { top: 30, right: 30, bottom: 30, left: 40 },
|
||||||
|
height: (screen.height * 0.3) || 200,
|
||||||
|
width: (screen.width * 0.6) || 600,
|
||||||
|
xTicks: undefined,
|
||||||
|
yTicks: undefined,
|
||||||
|
|
||||||
|
decimalsX: 2,
|
||||||
|
decimalsY: 0,
|
||||||
|
forceAxisBounds: false,
|
||||||
|
interpolation: "curveLinear",
|
||||||
|
yAxisMax: undefined,
|
||||||
|
yAxisMin: undefined,
|
||||||
|
|
||||||
|
// Prevent CORS issues for relative locations (dynamic import)
|
||||||
|
srcFolder: ((document.currentScript && document.currentScript.src) || (import.meta && import.meta.url)).split("/").slice(0,-1).join("/") + '/',
|
||||||
|
};
|
||||||
203
web/src/lib/vendor/leaflet-elevation/src/utils.js
vendored
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* TODO: exget computed styles of theese values from actual "CSS vars"
|
||||||
|
**/
|
||||||
|
export const Colors = {
|
||||||
|
'lightblue': { area: '#3366CC', alpha: 0.45, stroke: '#3366CC' },
|
||||||
|
'magenta' : { area: '#FF005E' },
|
||||||
|
'yellow' : { area: '#FF0' },
|
||||||
|
'purple' : { area: '#732C7B' },
|
||||||
|
'steelblue': { area: '#4682B4' },
|
||||||
|
'red' : { area: '#F00' },
|
||||||
|
'lime' : { area: '#9CC222', line: '#566B13' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEC = 1000;
|
||||||
|
const MIN = SEC * 60;
|
||||||
|
const HOUR = MIN * 60;
|
||||||
|
const DAY = HOUR * 24;
|
||||||
|
|
||||||
|
export function resolveURL(src, baseUrl) {
|
||||||
|
return (new URL(src, (src.startsWith('../') || src.startsWith('./')) ? baseUrl : undefined)).toString()
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a time (millis) to a human readable duration string (%Dd %H:%M'%S")
|
||||||
|
*/
|
||||||
|
export function formatTime(t) {
|
||||||
|
let d = Math.floor(t / DAY);
|
||||||
|
let h = Math.floor( (t - d * DAY) / HOUR);
|
||||||
|
let m = Math.floor( (t - d * DAY - h * HOUR) / MIN);
|
||||||
|
let s = Math.round( (t - d * DAY - h * HOUR - m * MIN) / SEC);
|
||||||
|
if ( s === 60 ) { m++; s = 0; }
|
||||||
|
if ( m === 60 ) { h++; m = 0; }
|
||||||
|
if ( h === 24 ) { d++; h = 0; }
|
||||||
|
return (d ? d + "d " : '') + h.toString().padStart(2, 0) + ':' + m.toString().padStart(2, 0) + "'" + s.toString().padStart(2, 0) + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a time (millis) to human readable date string (dd-mm-yyyy hh:mm:ss)
|
||||||
|
*/
|
||||||
|
export function formatDate(format) {
|
||||||
|
if (!format) {
|
||||||
|
return (time) => (new Date(time)).toLocaleString().replaceAll('/', '-').replaceAll(',', ' ');
|
||||||
|
} else if (format == 'time') {
|
||||||
|
return (time) => (new Date(time)).toLocaleTimeString();
|
||||||
|
} else if (format == 'date') {
|
||||||
|
return (time) => (new Date(time)).toLocaleDateString();
|
||||||
|
}
|
||||||
|
return (time) => format(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate download data event.
|
||||||
|
*/
|
||||||
|
export function saveFile(dataURI, fileName) {
|
||||||
|
let a = create('a', '', { href: dataURI, target: '_new', download: fileName || "", style: "display:none;" });
|
||||||
|
let b = document.body;
|
||||||
|
b.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
b.removeChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert SVG Path into Path2D and then update canvas
|
||||||
|
*/
|
||||||
|
export function drawCanvas(ctx, path) {
|
||||||
|
path.classed('canvas-path', true);
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, 0);
|
||||||
|
let p = new Path2D(path.attr('d'));
|
||||||
|
|
||||||
|
ctx.strokeStyle = path.__strokeStyle || path.attr('stroke');
|
||||||
|
ctx.fillStyle = path.__fillStyle || path.attr('fill');
|
||||||
|
ctx.lineWidth = 1.25;
|
||||||
|
ctx.globalCompositeOperation = 'source-over';
|
||||||
|
|
||||||
|
// stroke opacity
|
||||||
|
ctx.globalAlpha = path.attr('stroke-opacity') || 0.3;
|
||||||
|
ctx.stroke(p);
|
||||||
|
|
||||||
|
// fill opacity
|
||||||
|
ctx.globalAlpha = path.attr('fill-opacity') || 0.45;
|
||||||
|
ctx.fill(p);
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
|
||||||
|
ctx.closePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loop and extract GPX Extensions handled by "@tmcw/toGeoJSON" (eg. "coordinateProperties" > "times")
|
||||||
|
*/
|
||||||
|
export function coordPropsToMeta(coordProps, name, parser) {
|
||||||
|
return coordProps && (({props, point, id, isMulti }) => {
|
||||||
|
if (props) {
|
||||||
|
for (const key of coordProps) {
|
||||||
|
if (key in props) {
|
||||||
|
point.meta[name] = (parser || parseNumeric).call(this, (isMulti ? props[key][isMulti] : props[key]), id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract numeric property (id) from GeoJSON object
|
||||||
|
*/
|
||||||
|
export const parseNumeric = (property, id) => parseInt((typeof property === 'object' ? property[id] : property));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract datetime property (id) from GeoJSON object
|
||||||
|
*/
|
||||||
|
export const parseDate = (property, id) => new Date(Date.parse((typeof property === 'object' ? property[id] : property)));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A little bit shorter than L.DomUtil
|
||||||
|
*/
|
||||||
|
export const addClass = (n, str) => n && str.split(" ").every(s => s && L.DomUtil.addClass(n, s));
|
||||||
|
export const removeClass = (n, str) => n && str.split(" ").every(s => s && L.DomUtil.removeClass(n, s));
|
||||||
|
export const toggleClass = (n, str, cond) => (cond ? addClass : removeClass)(n, str);
|
||||||
|
export const replaceClass = (n, rem, add) => (rem && removeClass(n, rem)) || (add && addClass(n, add));
|
||||||
|
export const style = (n, k, v) => (typeof v === "undefined" && L.DomUtil.getStyle(n, k)) || n.style.setProperty(k, v);
|
||||||
|
export const toggleStyle = (n, k, v, cond) => style(n, k, cond ? v : '');
|
||||||
|
export const setAttributes = (n, attrs) => { for (let k in attrs) { n.setAttribute(k, attrs[k]); } };
|
||||||
|
export const toggleEvent = (el, e, fn, cond) => el[cond ? 'on' : 'off'](e, fn);
|
||||||
|
export const create = (tag, str, attrs, n) => { let elem = L.DomUtil.create(tag, str || ""); if (attrs) setAttributes(elem, attrs); if (n) append(n, elem); return elem; };
|
||||||
|
export const append = (n, c) => n.appendChild(c);
|
||||||
|
export const insert = (n, c, pos) => n.insertAdjacentElement(pos, c);
|
||||||
|
export const select = (str, n) => (n || document).querySelector(str);
|
||||||
|
export const each = (obj, fn) => { for (let i in obj) fn(obj[i], i); };
|
||||||
|
export const randomId = () => Math.random().toString(36).substr(2, 9);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: use generators instead? (ie. "yield")
|
||||||
|
*/
|
||||||
|
export const iMax = (iVal, max = -Infinity) => (iVal > max ? iVal : max);
|
||||||
|
export const iMin = (iVal, min = +Infinity) => (iVal < min ? iVal : min);
|
||||||
|
export const iAvg = (iVal, avg = 0, idx = 1) => (iVal + avg * (idx - 1)) / idx;
|
||||||
|
export const iSum = (iVal, sum = 0) => iVal + sum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for some leaflet core functions
|
||||||
|
*/
|
||||||
|
export const { on, off } = L.DomEvent;
|
||||||
|
export const { throttle, wrapNum } = L.Util;
|
||||||
|
export const { hasClass } = L.DomUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit floating point precision
|
||||||
|
*/
|
||||||
|
export const round = L.Util.formatNum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit a number between min / max values
|
||||||
|
*/
|
||||||
|
export const clamp = (val, range) => range ? (val < range[0] ? range[0] : val > range[1] ? range[1] : val) : val;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit a delta difference between two values
|
||||||
|
*/
|
||||||
|
export const wrapDelta = (curr, prev, deltaMax) => Math.abs(curr - prev) > deltaMax ? prev + deltaMax * Math.sign(curr - prev) : curr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A deep copy implementation that takes care of correct prototype chain and cycles, references
|
||||||
|
*
|
||||||
|
* @see https://web.dev/structured-clone/#features-and-limitations
|
||||||
|
*/
|
||||||
|
export function cloneDeep(o, skipProps = [], cache = []) {
|
||||||
|
switch(!o || typeof o) {
|
||||||
|
case 'object':
|
||||||
|
const hit = cache.filter(c => o === c.original)[0];
|
||||||
|
if (hit) return hit.copy; // handle circular structures
|
||||||
|
const copy = Array.isArray(o) ? [] : Object.create(Object.getPrototypeOf(o));
|
||||||
|
cache.push({ original: o, copy });
|
||||||
|
Object
|
||||||
|
.getOwnPropertyNames(o)
|
||||||
|
.forEach(function (prop) {
|
||||||
|
const propdesc = Object.getOwnPropertyDescriptor(o, prop);
|
||||||
|
Object.defineProperty(
|
||||||
|
copy,
|
||||||
|
prop,
|
||||||
|
propdesc.get || propdesc.set
|
||||||
|
? propdesc // just copy accessor properties
|
||||||
|
: { // deep copy data properties
|
||||||
|
writable: propdesc.writable,
|
||||||
|
configurable: propdesc.configurable,
|
||||||
|
enumerable: propdesc.enumerable,
|
||||||
|
value: skipProps.includes(prop) ? propdesc.value : cloneDeep(propdesc.value, skipProps, cache),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return copy;
|
||||||
|
case 'function':
|
||||||
|
case 'symbol':
|
||||||
|
console.warn('cloneDeep: ' + typeof o + 's not fully supported:', o);
|
||||||
|
case true:
|
||||||
|
// null, undefined or falsy primitive
|
||||||
|
default:
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
web/src/lib/vendor/leaflet-elevation/src/utils.spec.js
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* src/utils.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { suite } from 'uvu';
|
||||||
|
import * as assert from 'uvu/assert';
|
||||||
|
import '../test/setup/jsdom.js'
|
||||||
|
import { iAvg, iMin, iMax, iSum } from "../src/utils.js";
|
||||||
|
|
||||||
|
const toFixed = (n) => +n.toFixed(2);
|
||||||
|
|
||||||
|
const test = suite('src/utils.js');
|
||||||
|
|
||||||
|
test('iAvg()', () => {
|
||||||
|
let avg;
|
||||||
|
avg = iAvg(100, undefined, 1); assert.is(toFixed(avg), 100); // average for [100] is 100
|
||||||
|
avg = iAvg(100, avg, 2); assert.is(toFixed(avg), 100); // average for [100, 100] is 100
|
||||||
|
avg = iAvg(200, avg, 3); assert.is(toFixed(avg), 133.33); // average for [100, 100, 200] is 133.33
|
||||||
|
avg = iAvg(200, avg, 4); assert.is(toFixed(avg), 150); // average for [100, 100, 200, 200] is 150
|
||||||
|
avg = iAvg(NaN, avg, 5); assert.ok(isNaN(avg)); // average for [100, 100, 200, 200, NaN] is NaN
|
||||||
|
});
|
||||||
|
|
||||||
|
test('iMin()', () => {
|
||||||
|
let min;
|
||||||
|
min = iMin(100, undefined); assert.is(toFixed(min), 100); // min for [100] is 100
|
||||||
|
min = iMin(NaN, min); assert.is(toFixed(min), 100); // min for [100, NaN] is 100
|
||||||
|
min = iMin(0, min); assert.is(toFixed(min), 0); // min for [100, NaN, 0] is 100
|
||||||
|
min = iMin(-200, min); assert.is(toFixed(min), -200); // min for [100, NaN, 0, -200] is -200
|
||||||
|
min = iMin(200, min); assert.is(toFixed(min), -200); // min for [100, NaN, -100, -200, 200] is -200
|
||||||
|
});
|
||||||
|
|
||||||
|
test('iMax()', () => {
|
||||||
|
let max;
|
||||||
|
max = iMax(100, undefined); assert.is(toFixed(max), 100); // max for [100] is 100
|
||||||
|
max = iMax(NaN, max); assert.is(toFixed(max), 100); // max for [100, NaN] is 100
|
||||||
|
max = iMax(0, max); assert.is(toFixed(max), 100); // max for [100, NaN, 0] is 100
|
||||||
|
max = iMax(-200, max); assert.is(toFixed(max), 100); // max for [100, NaN, 0, -200] is 100
|
||||||
|
max = iMax(200, max); assert.is(toFixed(max), 200); // max for [100, NaN, -100, -200, 200] is 200
|
||||||
|
});
|
||||||
|
|
||||||
|
test('iSum()', () => {
|
||||||
|
let sum;
|
||||||
|
sum = iSum(10.25, undefined); assert.is(toFixed(sum), 10.25); // sum for [10.25] is 10.25
|
||||||
|
sum = iSum(0, sum); assert.is(toFixed(sum), 10.25); // sum for [10.25, 0] is 10.25
|
||||||
|
sum = iSum(-0.25, sum); assert.is(toFixed(sum), 10); // sum for [10.25, 0, -0.25] is 10
|
||||||
|
sum = iSum(-10, sum); assert.is(toFixed(sum), 0); // sum for [10.25, 0, -0.25, -10] is 0
|
||||||
|
sum = iSum(NaN, sum); assert.ok(isNaN(sum)); // sum for [10.25, 0, -0.25, -10, NaN] is NaN
|
||||||
|
});
|
||||||
|
|
||||||
|
test.run();
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { browser } from "$app/environment";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
import Search, {
|
import Search, {
|
||||||
type SearchItem,
|
type SearchItem,
|
||||||
} from "$lib/components/base/search.svelte";
|
} from "$lib/components/base/search.svelte";
|
||||||
import TrailCard from "$lib/components/trail/trail_card.svelte";
|
|
||||||
import EmptyStateSearch from "$lib/components/empty_states/empty_state_search.svelte";
|
import EmptyStateSearch from "$lib/components/empty_states/empty_state_search.svelte";
|
||||||
|
import TrailCard from "$lib/components/trail/trail_card.svelte";
|
||||||
|
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
|
||||||
import { ms } from "$lib/meilisearch";
|
import { ms } from "$lib/meilisearch";
|
||||||
import type { Trail, TrailFilter } from "$lib/models/trail";
|
import type { Trail, TrailFilter } from "$lib/models/trail";
|
||||||
|
import { categories } from "$lib/stores/category_store";
|
||||||
import {
|
import {
|
||||||
trails,
|
trails,
|
||||||
trails_search_bounding_box,
|
trails_search_bounding_box,
|
||||||
} from "$lib/stores/trail_store";
|
} from "$lib/stores/trail_store";
|
||||||
import { country_codes } from "$lib/util/country_code_util";
|
import { country_codes } from "$lib/util/country_code_util";
|
||||||
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||||
|
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||||
import type {
|
import type {
|
||||||
GPX,
|
GPX,
|
||||||
Icon,
|
Icon,
|
||||||
@@ -26,11 +31,7 @@
|
|||||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import TrailFilterPanel from "$lib/components/trail/trail_filter_panel.svelte";
|
|
||||||
import { categories } from "$lib/stores/category_store";
|
|
||||||
import { slide } from "svelte/transition";
|
import { slide } from "svelte/transition";
|
||||||
import { browser } from "$app/environment";
|
|
||||||
import { getFileURL } from "$lib/util/file_util";
|
|
||||||
|
|
||||||
let L: any;
|
let L: any;
|
||||||
let map: Map;
|
let map: Map;
|
||||||
@@ -166,6 +167,10 @@
|
|||||||
}
|
}
|
||||||
const gpxLayer = new L.GPX(trail.expand.gpx_data!, {
|
const gpxLayer = new L.GPX(trail.expand.gpx_data!, {
|
||||||
async: true,
|
async: true,
|
||||||
|
polyline_options: {
|
||||||
|
className: "lightblue-theme elevation-polyline",
|
||||||
|
weight: 5,
|
||||||
|
},
|
||||||
gpx_options: {
|
gpx_options: {
|
||||||
parseElements: ["track"],
|
parseElements: ["track"],
|
||||||
},
|
},
|
||||||
@@ -186,11 +191,12 @@
|
|||||||
const marker: Marker = e.point as Marker;
|
const marker: Marker = e.point as Marker;
|
||||||
startMarkers[trail.id!] = marker;
|
startMarkers[trail.id!] = marker;
|
||||||
marker.bindPopup(
|
marker.bindPopup(
|
||||||
`<a href="/trail/view/${trail.id}">
|
`<a href="map/trail/${trail.id}">
|
||||||
<li class="flex items-center gap-4 cursor-pointer text-black">
|
<li class="flex items-center gap-4 cursor-pointer text-black">
|
||||||
<div class="shrink-0"><img class="h-14 w-14 object-cover rounded-xl" src="${
|
<div class="shrink-0"><img class="h-14 w-14 object-cover rounded-xl" src="${getFileURL(
|
||||||
getFileURL(trail, trail.thumbnail)
|
trail,
|
||||||
}" alt="">
|
trail.thumbnail,
|
||||||
|
)}" alt="">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="font-semibold text-lg">${trail.name}</h4>
|
<h4 class="font-semibold text-lg">${trail.name}</h4>
|
||||||
@@ -255,7 +261,11 @@
|
|||||||
<button
|
<button
|
||||||
class="btn-icon md:hidden"
|
class="btn-icon md:hidden"
|
||||||
on:click={() => (showMap = !showMap)}
|
on:click={() => (showMap = !showMap)}
|
||||||
><i class="fa-regular fa-{showMap ? 'rectangle-list' : 'map'}"></i></button
|
><i
|
||||||
|
class="fa-regular fa-{showMap
|
||||||
|
? 'rectangle-list'
|
||||||
|
: 'map'}"
|
||||||
|
></i></button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{#if showFilter}
|
{#if showFilter}
|
||||||
@@ -276,7 +286,7 @@
|
|||||||
<EmptyStateSearch></EmptyStateSearch>
|
<EmptyStateSearch></EmptyStateSearch>
|
||||||
{/if}
|
{/if}
|
||||||
{#each $trails as trail}
|
{#each $trails as trail}
|
||||||
<a href="/trail/view/{trail.id}">
|
<a href="map/trail/{trail.id}">
|
||||||
<TrailCard
|
<TrailCard
|
||||||
{trail}
|
{trail}
|
||||||
on:mouseenter={() => handleTrailCardMouseEnter(trail)}
|
on:mouseenter={() => handleTrailCardMouseEnter(trail)}
|
||||||
@@ -286,7 +296,11 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div id="map" class="rounded-xl z-0" class:hidden={!showMap && browser && window.innerWidth < 768}></div>
|
<div
|
||||||
|
id="map"
|
||||||
|
class="rounded-xl z-0"
|
||||||
|
class:hidden={!showMap && browser && window.innerWidth < 768}
|
||||||
|
></div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
201
web/src/routes/map/trail/[id]/+page.svelte
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
|
||||||
|
import Tabs from "$lib/components/tabs.svelte";
|
||||||
|
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
||||||
|
import { trail } from "$lib/stores/trail_store";
|
||||||
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
|
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||||
|
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||||
|
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||||
|
import type { Map, Marker } from "leaflet";
|
||||||
|
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
let L: any;
|
||||||
|
let map: Map;
|
||||||
|
let markers: Marker[] = [];
|
||||||
|
|
||||||
|
const tabs = ["Description", "Waypoints", "Photos", "Summit book"];
|
||||||
|
let activeTab = 0;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
L = (await import("leaflet")).default;
|
||||||
|
await import("leaflet-gpx");
|
||||||
|
await import("leaflet.awesome-markers");
|
||||||
|
//@ts-ignore
|
||||||
|
await import("$lib/vendor/leaflet-elevation/src/index.js");
|
||||||
|
|
||||||
|
map = L.map("map").setView([$trail.lat, $trail.lon], 14);
|
||||||
|
|
||||||
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
|
attribution: "© OpenStreetMap contributors",
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
const elevation_options = {
|
||||||
|
height: 200,
|
||||||
|
|
||||||
|
theme: "lightblue-theme",
|
||||||
|
detached: true,
|
||||||
|
elevationDiv: "#elevation",
|
||||||
|
closeBtn: false,
|
||||||
|
followMarker: true,
|
||||||
|
autofitBounds: true,
|
||||||
|
imperial: false,
|
||||||
|
reverseCoords: false,
|
||||||
|
acceleration: false,
|
||||||
|
slope: true,
|
||||||
|
speed: false,
|
||||||
|
altitude: true,
|
||||||
|
time: true,
|
||||||
|
distance: true,
|
||||||
|
// Summary track info style: "inline" || "multiline" || false
|
||||||
|
summary: false,
|
||||||
|
downloadLink: false,
|
||||||
|
ruler: false,
|
||||||
|
legend: true,
|
||||||
|
// Toggle "leaflet-almostover" integration
|
||||||
|
almostOver: true,
|
||||||
|
// Toggle "leaflet-distance-markers" integration
|
||||||
|
distanceMarkers: false,
|
||||||
|
// Toggle "leaflet-edgescale" integration
|
||||||
|
edgeScale: false,
|
||||||
|
// Toggle "leaflet-hotline" integration
|
||||||
|
hotline: true,
|
||||||
|
// Display track datetimes: true || false
|
||||||
|
timestamps: false,
|
||||||
|
waypoints: false,
|
||||||
|
wptIcons: false,
|
||||||
|
wptLabels: false,
|
||||||
|
preferCanvas: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const controlElevation = L.control
|
||||||
|
.elevation(elevation_options)
|
||||||
|
.addTo(map);
|
||||||
|
controlElevation.load(getFileURL($trail, $trail.gpx));
|
||||||
|
|
||||||
|
// addGPXLayer($trail);
|
||||||
|
|
||||||
|
for (const waypoint of $trail.expand.waypoints) {
|
||||||
|
const marker = createMarkerFromWaypoint(L, waypoint);
|
||||||
|
marker.addTo(map);
|
||||||
|
markers.push(marker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function openMarkerPopup(i: number) {
|
||||||
|
markers[i].openPopup();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main class="grid grid-cols-1 md:grid-cols-[452px_1fr] gap-x-1">
|
||||||
|
<div
|
||||||
|
id="trail-details"
|
||||||
|
class="md:overflow-y-auto md:overflow-x-hidden flex flex-col items-stretch gap-4 rounded-xl"
|
||||||
|
>
|
||||||
|
<section class="relative h-80">
|
||||||
|
<img
|
||||||
|
class="w-full h-80 object-cover"
|
||||||
|
src={getFileURL($trail, $trail.thumbnail)}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="flex absolute flex-wrap justify-between items-end w-full bottom-8 left-0 px-8 gap-y-4"
|
||||||
|
>
|
||||||
|
<div class="text-white">
|
||||||
|
<h4 class="text-4xl font-bold">
|
||||||
|
{$trail.name}
|
||||||
|
</h4>
|
||||||
|
<h3 class="text-xl mt-4">
|
||||||
|
<i class="fa fa-location-dot mr-3"></i>
|
||||||
|
{$trail.location}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="grid grid-cols-2 sm:grid-cols-4 gap-y-4 justify-around">
|
||||||
|
<div class="flex flex-col items-center text-sm">
|
||||||
|
<span class="text-gray-500">Distance</span>
|
||||||
|
<span class="font-semibold"
|
||||||
|
>{formatMeters($trail.distance)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center text-sm">
|
||||||
|
<span class="text-gray-500">Elevation gain</span>
|
||||||
|
<span class="font-semibold"
|
||||||
|
>{formatMeters($trail.elevation_gain)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center text-sm">
|
||||||
|
<span class="text-gray-500">Est. duration</span>
|
||||||
|
<span class="font-semibold"
|
||||||
|
>{formatTimeHHMM($trail.duration)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{#if $trail.expand.category}
|
||||||
|
<div class="flex flex-col items-center text-sm">
|
||||||
|
<span class="text-gray-500">Category</span>
|
||||||
|
<span class="font-semibold"
|
||||||
|
>{$trail.expand.category.name}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
<hr class=" border-input-border" />
|
||||||
|
<section class="mx-4">
|
||||||
|
<Tabs extraClasses="text-sm mb-4" {tabs} bind:activeTab></Tabs>
|
||||||
|
{#if activeTab == 0}
|
||||||
|
<article class="text-justify whitespace-pre-line text-sm">
|
||||||
|
{$trail.description}
|
||||||
|
</article>
|
||||||
|
{/if}
|
||||||
|
{#if activeTab == 1}
|
||||||
|
<ul>
|
||||||
|
{#each $trail.expand.waypoints ?? [] as waypoint, i}
|
||||||
|
<li on:mouseenter={() => openMarkerPopup(i)}>
|
||||||
|
<WaypointCard {waypoint}></WaypointCard>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
{#if activeTab == 2}
|
||||||
|
<div id="photo-gallery" class="">
|
||||||
|
{#each $trail.photos ?? [] as photo, i}
|
||||||
|
<img
|
||||||
|
class="rounded-xl cursor-pointer hover:scale-105 transition-transform"
|
||||||
|
src={photo}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if activeTab == 3}
|
||||||
|
<ul>
|
||||||
|
{#each $trail.expand.summit_logs ?? [] as log}
|
||||||
|
<li><SummitLogCard {log}></SummitLogCard></li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div id="map-container" class="flex flex-col">
|
||||||
|
<div id="map" class="rounded-xl z-0 basis-full"></div>
|
||||||
|
<div id="elevation"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#map-container {
|
||||||
|
height: calc(100vh - 180px);
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 768px) {
|
||||||
|
#map-container,
|
||||||
|
#trail-details {
|
||||||
|
height: calc(100vh - 124px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
16
web/src/routes/map/trail/[id]/+page.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { trails, trails_show } from "$lib/stores/trail_store";
|
||||||
|
import { error, type ServerLoad } from "@sveltejs/kit";
|
||||||
|
import { ClientResponseError } from "pocketbase";
|
||||||
|
|
||||||
|
export const load: ServerLoad = async ({ params, locals }) => {
|
||||||
|
try {
|
||||||
|
await trails_show(params.id!, true)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ClientResponseError && e.status == 404) {
|
||||||
|
error(404, {
|
||||||
|
message: 'Not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
import { waypoint } from "$lib/stores/waypoint_store";
|
import { waypoint } from "$lib/stores/waypoint_store";
|
||||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||||
|
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||||
import cryptoRandomString from "crypto-random-string";
|
import cryptoRandomString from "crypto-random-string";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
@@ -114,6 +115,11 @@
|
|||||||
gpxLayer?.remove();
|
gpxLayer?.remove();
|
||||||
gpxLayer = new L.GPX(gpx, {
|
gpxLayer = new L.GPX(gpx, {
|
||||||
async: true,
|
async: true,
|
||||||
|
polyline_options: {
|
||||||
|
className: "lightblue-theme elevation-polyline",
|
||||||
|
opacity: 0.75,
|
||||||
|
weight: 5,
|
||||||
|
},
|
||||||
gpx_options: {
|
gpx_options: {
|
||||||
parseElements: [
|
parseElements: [
|
||||||
"track",
|
"track",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import Dropdown from "$lib/components/base/dropdown.svelte";
|
|
||||||
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
||||||
|
import Dropdown from "$lib/components/base/dropdown.svelte";
|
||||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||||
import ListSelectModal from "$lib/components/list/list_select_modal.svelte";
|
import ListSelectModal from "$lib/components/list/list_select_modal.svelte";
|
||||||
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
|
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
import { getFileURL } from "$lib/util/file_util";
|
import { getFileURL } from "$lib/util/file_util";
|
||||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||||
|
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||||
import type { Icon, Map, Marker } from "leaflet";
|
import type { Icon, Map, Marker } from "leaflet";
|
||||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
@@ -41,12 +42,12 @@
|
|||||||
let openConfirmModal: () => void;
|
let openConfirmModal: () => void;
|
||||||
let openListSelectModal: () => void;
|
let openListSelectModal: () => void;
|
||||||
|
|
||||||
let mapFullScreen: boolean = false;
|
|
||||||
|
|
||||||
const dropdownItems: DropdownItem[] = [
|
const dropdownItems: DropdownItem[] = [
|
||||||
{ text: "Show on map", value: "map", icon: "map" },
|
{ text: "Show on map", value: "map", icon: "map" },
|
||||||
{ text: "Directions", value: "direction", icon: "car" },
|
{ text: "Directions", value: "direction", icon: "car" },
|
||||||
...($trail.gpx ? [{ text: "Download GPX", value: "download", icon: "download" }] : []),
|
...($trail.gpx
|
||||||
|
? [{ text: "Download GPX", value: "download", icon: "download" }]
|
||||||
|
: []),
|
||||||
{ text: "Add to list", value: "list", icon: "bookmark" },
|
{ text: "Add to list", value: "list", icon: "bookmark" },
|
||||||
{ text: "Edit", value: "edit", icon: "pen" },
|
{ text: "Edit", value: "edit", icon: "pen" },
|
||||||
{ text: "Delete", value: "delete", icon: "trash" },
|
{ text: "Delete", value: "delete", icon: "trash" },
|
||||||
@@ -65,24 +66,15 @@
|
|||||||
|
|
||||||
const gpxLayer = new L.GPX($trail.expand.gpx_data!, {
|
const gpxLayer = new L.GPX($trail.expand.gpx_data!, {
|
||||||
async: true,
|
async: true,
|
||||||
|
polyline_options: {
|
||||||
|
className: "lightblue-theme elevation-polyline",
|
||||||
|
opacity: 0.75,
|
||||||
|
weight: 5,
|
||||||
|
},
|
||||||
gpx_options: {
|
gpx_options: {
|
||||||
parseElements: ["track"] as any,
|
parseElements: ["track"] as any,
|
||||||
},
|
},
|
||||||
marker_options: {
|
marker_options: {
|
||||||
wptIcons: {
|
|
||||||
"": L.AwesomeMarkers.icon({
|
|
||||||
icon: "circle",
|
|
||||||
prefix: "fa",
|
|
||||||
markerColor: "cadetblue",
|
|
||||||
iconColor: "white",
|
|
||||||
}) as Icon,
|
|
||||||
Summit: L.AwesomeMarkers.icon({
|
|
||||||
icon: "mountain",
|
|
||||||
prefix: "fa",
|
|
||||||
markerColor: "cadetblue",
|
|
||||||
iconColor: "white",
|
|
||||||
}) as Icon,
|
|
||||||
},
|
|
||||||
startIcon: L.AwesomeMarkers.icon({
|
startIcon: L.AwesomeMarkers.icon({
|
||||||
icon: "circle-half-stroke",
|
icon: "circle-half-stroke",
|
||||||
prefix: "fa",
|
prefix: "fa",
|
||||||
@@ -142,7 +134,7 @@
|
|||||||
|
|
||||||
async function handleDropdownClick(item: { text: string; value: any }) {
|
async function handleDropdownClick(item: { text: string; value: any }) {
|
||||||
if (item.value == "map") {
|
if (item.value == "map") {
|
||||||
goto(`/map/?lat=${$trail.lat}&lon=${$trail.lon}`);
|
goto(`/map/trail/${$trail.id!}`);
|
||||||
} else if (item.value == "list") {
|
} else if (item.value == "list") {
|
||||||
openListSelectModal();
|
openListSelectModal();
|
||||||
} else if (item.value == "direction") {
|
} else if (item.value == "direction") {
|
||||||
@@ -154,7 +146,6 @@
|
|||||||
?.focus();
|
?.focus();
|
||||||
} else if (item.value == "download") {
|
} else if (item.value == "download") {
|
||||||
downloadURI(getFileURL($trail, $trail.gpx), $trail.gpx!);
|
downloadURI(getFileURL($trail, $trail.gpx), $trail.gpx!);
|
||||||
|
|
||||||
} else if (item.value == "edit") {
|
} else if (item.value == "edit") {
|
||||||
goto(`/trail/edit/${$trail.id}`);
|
goto(`/trail/edit/${$trail.id}`);
|
||||||
} else if (item.value == "delete") {
|
} else if (item.value == "delete") {
|
||||||
@@ -176,9 +167,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function toggleMapFullScreen() {
|
async function toggleMapFullScreen() {
|
||||||
mapFullScreen = !mapFullScreen;
|
goto(`/map/trail/${$trail.id!}`);
|
||||||
await tick();
|
|
||||||
map.invalidateSize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleListSelection(list: List) {
|
async function handleListSelection(list: List) {
|
||||||
@@ -291,10 +280,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<section class="p-8">
|
<section class="p-8">
|
||||||
<Tabs {tabs} bind:activeTab></Tabs>
|
<Tabs {tabs} bind:activeTab></Tabs>
|
||||||
<div
|
<div class="grid grid-cols-1 md:grid-cols-[1fr_18rem] mt-6 gap-8">
|
||||||
class="grid grid-cols-1 mt-6 gap-8"
|
|
||||||
class:md:grid-cols-[1fr_18rem]={!mapFullScreen}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
{#if activeTab == 0}
|
{#if activeTab == 0}
|
||||||
<article class="text-justify whitespace-pre-line text-sm">
|
<article class="text-justify whitespace-pre-line text-sm">
|
||||||
@@ -335,13 +321,11 @@
|
|||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="relative" class:-order-1={mapFullScreen}>
|
<div class="relative">
|
||||||
<div class="rounded-xl h-72" id="map">
|
<div class="rounded-xl h-72" id="map">
|
||||||
<div class="leaflet-top leaflet-right">
|
<div class="leaflet-top leaflet-right">
|
||||||
<button
|
<button
|
||||||
class="leaflet-control fa fa-{mapFullScreen
|
class="leaflet-control fa fa-maximize rounded-full text-lg bg-white text-black px-[14px] py-2 hover:bg-gray-100"
|
||||||
? 'minimize'
|
|
||||||
: 'maximize'} rounded-full text-lg bg-white text-black px-[14px] py-2 hover:bg-gray-100"
|
|
||||||
style="cursor: pointer !important"
|
style="cursor: pointer !important"
|
||||||
on:click={() => toggleMapFullScreen()}
|
on:click={() => toggleMapFullScreen()}
|
||||||
></button>
|
></button>
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import { lists_index } from "$lib/stores/list_store";
|
import { lists_index } from "$lib/stores/list_store";
|
||||||
import { trails_show } from "$lib/stores/trail_store";
|
import { trails_show } from "$lib/stores/trail_store";
|
||||||
import type { Load } from "@sveltejs/kit";
|
import { error, type Load } from "@sveltejs/kit";
|
||||||
|
import { ClientResponseError } from "pocketbase";
|
||||||
|
|
||||||
export const load: Load = async ({ params }) => {
|
export const load: Load = async ({ params }) => {
|
||||||
await trails_show(params.id!, true)
|
try {
|
||||||
await lists_index();
|
await trails_show(params.id!, true)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ClientResponseError && e.status == 404) {
|
||||||
|
error(404, {
|
||||||
|
message: 'Not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} await lists_index();
|
||||||
};
|
};
|
||||||