summaryrefslogtreecommitdiff
path: root/assets/static/vendor/leaflet-elevation
diff options
context:
space:
mode:
Diffstat (limited to 'assets/static/vendor/leaflet-elevation')
-rw-r--r--assets/static/vendor/leaflet-elevation/images/checkered.pngbin0 -> 8632 bytes
-rw-r--r--assets/static/vendor/leaflet-elevation/images/elevation-locate.pngbin0 -> 274 bytes
-rw-r--r--assets/static/vendor/leaflet-elevation/images/elevation-poi.pngbin0 -> 640 bytes
-rw-r--r--assets/static/vendor/leaflet-elevation/images/elevation-position.pngbin0 -> 542 bytes
-rw-r--r--assets/static/vendor/leaflet-elevation/images/elevation-position.svg1
-rw-r--r--assets/static/vendor/leaflet-elevation/images/elevation-pushpin.pngbin0 -> 3068 bytes
-rw-r--r--assets/static/vendor/leaflet-elevation/images/elevation-pushpin.svg1
-rw-r--r--assets/static/vendor/leaflet-elevation/images/elevation.svg1
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/fullpage.css13
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.css8
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.js314
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.min.css1
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.min.js1
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/leaflet-edgescale.js636
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/leaflet-edgescale.min.js1
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js347
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/leaflet-gpxgroup.min.js1
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/leaflet-hotline.js457
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/leaflet-hotline.min.js1
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.css42
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.js183
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.min.css1
-rw-r--r--assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.min.js1
-rw-r--r--assets/static/vendor/leaflet-elevation/src/components/chart.js783
-rw-r--r--assets/static/vendor/leaflet-elevation/src/components/d3.js649
-rw-r--r--assets/static/vendor/leaflet-elevation/src/components/marker.js96
-rw-r--r--assets/static/vendor/leaflet-elevation/src/components/summary.js43
-rw-r--r--assets/static/vendor/leaflet-elevation/src/control.js1335
-rw-r--r--assets/static/vendor/leaflet-elevation/src/handlers/acceleration.js64
-rw-r--r--assets/static/vendor/leaflet-elevation/src/handlers/altitude.js78
-rw-r--r--assets/static/vendor/leaflet-elevation/src/handlers/cadence.js54
-rw-r--r--assets/static/vendor/leaflet-elevation/src/handlers/distance.js46
-rw-r--r--assets/static/vendor/leaflet-elevation/src/handlers/heart.js53
-rw-r--r--assets/static/vendor/leaflet-elevation/src/handlers/labels.js45
-rw-r--r--assets/static/vendor/leaflet-elevation/src/handlers/lineargradient.js122
-rw-r--r--assets/static/vendor/leaflet-elevation/src/handlers/pace.js65
-rw-r--r--assets/static/vendor/leaflet-elevation/src/handlers/runner.js109
-rw-r--r--assets/static/vendor/leaflet-elevation/src/handlers/slope.js79
-rw-r--r--assets/static/vendor/leaflet-elevation/src/handlers/speed.js66
-rw-r--r--assets/static/vendor/leaflet-elevation/src/handlers/temperature.js62
-rw-r--r--assets/static/vendor/leaflet-elevation/src/handlers/time.js103
-rw-r--r--assets/static/vendor/leaflet-elevation/src/index.css329
-rw-r--r--assets/static/vendor/leaflet-elevation/src/index.js42
-rw-r--r--assets/static/vendor/leaflet-elevation/src/options.js88
-rw-r--r--assets/static/vendor/leaflet-elevation/src/utils.js203
-rw-r--r--assets/static/vendor/leaflet-elevation/src/utils.spec.js50
46 files changed, 6574 insertions, 0 deletions
diff --git a/assets/static/vendor/leaflet-elevation/images/checkered.png b/assets/static/vendor/leaflet-elevation/images/checkered.png
new file mode 100644
index 0000000..1ae989d
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/images/checkered.png
Binary files differ
diff --git a/assets/static/vendor/leaflet-elevation/images/elevation-locate.png b/assets/static/vendor/leaflet-elevation/images/elevation-locate.png
new file mode 100644
index 0000000..9505daa
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/images/elevation-locate.png
Binary files differ
diff --git a/assets/static/vendor/leaflet-elevation/images/elevation-poi.png b/assets/static/vendor/leaflet-elevation/images/elevation-poi.png
new file mode 100644
index 0000000..d29e63e
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/images/elevation-poi.png
Binary files differ
diff --git a/assets/static/vendor/leaflet-elevation/images/elevation-position.png b/assets/static/vendor/leaflet-elevation/images/elevation-position.png
new file mode 100644
index 0000000..509f063
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/images/elevation-position.png
Binary files differ
diff --git a/assets/static/vendor/leaflet-elevation/images/elevation-position.svg b/assets/static/vendor/leaflet-elevation/images/elevation-position.svg
new file mode 100644
index 0000000..9a3b180
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/images/elevation-position.svg
@@ -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>
diff --git a/assets/static/vendor/leaflet-elevation/images/elevation-pushpin.png b/assets/static/vendor/leaflet-elevation/images/elevation-pushpin.png
new file mode 100644
index 0000000..f4ec493
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/images/elevation-pushpin.png
Binary files differ
diff --git a/assets/static/vendor/leaflet-elevation/images/elevation-pushpin.svg b/assets/static/vendor/leaflet-elevation/images/elevation-pushpin.svg
new file mode 100644
index 0000000..fcc5a01
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/images/elevation-pushpin.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path fill="silver" d="M24.474 45.368c-.515.178-.44.974-.708 1.39-1.242 3.712-2.686 7.37-3.648 11.168-.336 1.367-.087 2.775.139 4.136 2.405-3.417 3.762-7.419 4.878-11.407.407-1.45.898-2.884 1.213-4.358-.353-.521-1.104-.623-1.652-.884-.072-.02-.146-.046-.222-.045z"/><path fill="#f08d8d" d="M17.193 27.237c-.95.368-1.645 1.228-2.399 1.893-.987 1.082-2.01 2.222-2.597 3.572.16.615.942.116 1.348.052 1.485-.443 2.408-1.797 3.013-3.141.299-.72.84-1.4.911-2.186a.286.286 0 00-.276-.19z"/><path fill="#fd6d6d" d="M28.156 9.549c-.495.55-.53 1.423-.83 2.096-1.487 4.598-2.669 9.299-4.36 13.825-.786.406-1.764.381-2.616.667-1.045.29-2.192.508-3.113 1.023-.986 1.725-1.497 4.038-3.446 4.975-.547.365-1.61.195-1.703 1.056-.333 2.283.656 4.608 2.23 6.229.584.012.966-.7 1.443-1.005 1.856-1.744 4.143-3.3 6.735-3.476 2.277-.23 4.602-.181 6.912-.262-2.39-1.27-5.13-2.41-6.595-4.809a10.078 10.078 0 0 1-.827-1.63l2.478 2.533c1.635-6.38 3.92-12.578 5.438-18.99.438-.742-.42-1.107-.823-1.602-.286-.202-.539-.703-.923-.63z"/><path fill="#fd6464" d="M28.454 1.686c-1.119.01-2.246.095-3.32.43-.22 1.754-.389 3.745.781 5.228.356.51.78.965 1.188 1.432l2.531-.955-3.087-3.15c5.929 2.954 12.037 5.904 18.664 6.842a63.84 63.84 0 0 0 6.017.304c.453-.288.3-.998.177-1.439-.947-1.112-2.273-1.856-3.486-2.651-4.678-2.746-9.907-4.441-15.185-5.6a17.269 17.269 0 0 0-4.28-.44z"/><path fill="#de5858" d="M39.98 31.987c-1.243.923-2.227 2.377-3.794 2.765-3.644.288-7.329-.682-11.008-.484-2.629.107-5.445.39-7.588 2.08-1.116.896-2.323 1.744-3.24 2.847-.028.554.75.78 1.05 1.172.934.712 1.8 1.54 2.807 2.145.631-.556.757-1.53 1.402-2.108 1.309-1.205 3.158-1.63 4.798-2.092 4.486-.97 9.147-.67 13.614-1.776l4.655-1.001c.228-.57-.531-1.012-.751-1.491-.567-.756-1.156-1.494-1.74-2.238l-.204.18z"/><path fill="#ba4a4a" d="M29.26 7.5c-.708.334-1.563.461-2.186.942-.088.523.64.855.93 1.258 1.125 1.108 2.793 1.326 4.185 1.95 3.774 1.357 7.6 2.604 11.521 3.459-1.481.707-3.154.576-4.69.111a27.13 27.13 0 01-9.328-4.253c-1.515 6.366-3.744 12.564-5.422 18.907-.925-.754-1.593-1.813-2.582-2.48-.63.004-.206.767-.084 1.083.924 2.59 3.198 4.371 5.605 5.516.88.504 1.783.926 2.82.9 2.14.174 4.337.67 6.472.23 1.558-.586 2.628-1.97 3.906-2.97-.622-.693-.914-1.662-.51-2.537 1.539-4.633 3.45-9.225 5.05-13.875 2.082-.496 4.585-.89 5.875-2.816.222-.448.876-.945.583-1.477-1.876-.164-3.784-.192-5.664-.177-1.12.095.824.692 1.252.859.607.229 1.395.497 1.865.74-5.345.176-10.525-1.763-15.445-3.74-1.386-.51-2.768-1.239-4.152-1.63zm13.334 27.608c-4.09.913-8.185 1.857-12.386 2.008-3.385.247-6.856.652-9.903 2.263-.695.262-1.269.742-1.54 1.449-.172.55-1.03 1.307-.633 1.698 3.447 2.002 7.05 4.007 11.082 4.457 3.591.551 7.35.802 10.888-.19 2.478-.72 3.861-3.344 3.927-5.792.132-2.046-.35-4.095-1.226-5.938l-.126.027-.083.018z"/><path fill="#9b3d3d" d="M38.85 15.362c-2.985-.756-5.734-1.923-7.866-3.34-.978-.65-2.28-1.868-1.815-1.698l1.997.756c3.254 1.239 7.48 2.592 10.3 3.299l2.804.702-.69.258c-1.679.628-2.329.631-4.73.023zM27.768 6.308l-.293-.333.333.293c.183.162.332.311.332.333 0 .095-.098.018-.372-.293z"/><path fill="#8b3838" d="M45.566 12.918c-3.393-.389-7.22-1.563-13.493-4.138L29.29 7.638l-1.236-1.172c-.68-.645-1.464-1.446-1.744-1.78l-.51-.608 2.471 1.237c6.623 3.316 10.796 4.868 15.754 5.86 1.76.351 3.485.878 4.807 1.467l.909.405-1.634-.012a31.11 31.11 0 0 1-2.54-.117z"/><path fill="#383838" d="M19.003 63.365c-.744-1.63-.854-3.21-.355-5.13.17-.656 1.096-3.554 2.057-6.44a677.285 677.285 0 0 0 1.748-5.284c0-.02-.695-.404-1.543-.853-5.16-2.729-8.047-5.216-9.541-8.22-.675-1.357-.866-2.392-.664-3.611.495-3 3.118-6.364 6.12-7.851.871-.431 2.212-.896 3.866-1.341l1.442-.388 1.069-3.51c2.17-7.125 3.13-10.477 3.044-10.621a14.91 14.91 0 0 0-.665-.843c-1.946-2.35-2.697-5.018-1.9-6.747.398-.864.8-1.33 1.623-1.875l.67-.445C26.621.046 28.556 0 28.556 0s1.819.038 2.67.304l3.085.794c5.906 1.521 10.247 3.17 14.022 5.327 2.07 1.358 3.377 2.396 4.686 3.895.207.242.368.543.35.729 0 0-.042.39-.144.549 0 0-1.71 2.613-2.17 3.077-.453.456-1.606 1.061-3.493 1.834-.917.376-1.891 1.178-2.092 1.723l-1.477 4.08a472.53 472.53 0 0 1-2.197 5.958l-.84 2.206.89 1.182c1.884 2.496 2.781 4.129 3.245 5.9.357 1.366.355 3.946-.005 5.325-.633 2.426-1.742 3.793-3.925 4.836-3.113 1.487-6.85 1.672-12.16.602-.825-.167-1.512-.287-1.527-.268s-.67 1.967-1.453 4.33c-1.745 5.258-2.49 7.318-2.994 8.279-.257.49-.819 1.172-1.678 2.037C20.25 63.807 20.003 64 19.676 64c-.344 0-.412-.064-.673-.635zm2.226-3.061c1.468-2.444 2.544-5.264 4.084-10.702l.947-3.245c.021-.021 1.012.124 2.2.323 3.1.52 4.94.666 7.352.583 3.419-.118 5.077-.61 6.331-1.882.72-.73 1.27-1.775 1.542-2.932.24-1.018.238-3.14-.005-4.32-.379-1.844-.808-2.621-2.958-5.35-.957-1.215-1.207-1.667-1.207-2.19 0-.456.52-2.005 1.845-5.494l2.21-5.99 1.247-3.45.647-.155c3.433-.826 4.365-1.336 5.442-2.98.404-.616.514-1.066.514-1.066s.182-.738-.045-1.014l-.817-.669C46.78 6.681 39.943 3.8 32.875 2.323c-1.923-.402-2.323-.447-4.07-.454-1.238-.006-2.208.052-2.694.16-.918.203-.927.223-.932 2.191-.003 1.273.025 1.453.333 2.11.379.809.89 1.488 1.925 2.555l.71.733-.193.538c-.105.296-.948 3.043-1.872 6.105l-2.33 7.495-.647 1.928-1.506.323c-2.972.637-4.066 1.023-5.152 1.816-1.395 1.02-2.882 2.715-3.859 4.398-.318.549-.343.684-.342 1.876 0 1.37.154 2.067.682 3.108a9.37 9.37 0 0 0 2.164 2.722c2.53 2.107 4.779 3.541 7.672 4.893l1.581.74-.884 2.473-2.035 5.934c-1.25 3.757-1.441 4.723-1.26 6.342a21.83 21.83 0 0 1 .102 1.059c.012.41.201.2.96-1.064z"/></svg>
diff --git a/assets/static/vendor/leaflet-elevation/images/elevation.svg b/assets/static/vendor/leaflet-elevation/images/elevation.svg
new file mode 100644
index 0000000..43c0e25
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/images/elevation.svg
@@ -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>
diff --git a/assets/static/vendor/leaflet-elevation/libs/fullpage.css b/assets/static/vendor/leaflet-elevation/libs/fullpage.css
new file mode 100644
index 0000000..c2b8539
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/fullpage.css
@@ -0,0 +1,13 @@
+html,
+body,
+.leaflet-map {
+ height: 100%;
+ width: 100%;
+ padding: 0px;
+ margin: 0px;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+}
diff --git a/assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.css b/assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.css
new file mode 100644
index 0000000..fcf9a40
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.css
@@ -0,0 +1,8 @@
+.dist-marker {
+ font-size: 9px;
+ border: 1px solid #777;
+ border-radius: 10px;
+ text-align: center;
+ color: #000;
+ background: #fff;
+}
diff --git a/assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.js b/assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.js
new file mode 100644
index 0000000..43beb77
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.js
@@ -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);
+ }
+
+});
diff --git a/assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.min.css b/assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.min.css
new file mode 100644
index 0000000..473e8f3
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.min.css
@@ -0,0 +1 @@
+.dist-marker{font-size:9px;border:1px solid #777;border-radius:10px;text-align:center;color:#000;background:#fff} \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.min.js b/assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.min.js
new file mode 100644
index 0000000..b8bc58b
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/leaflet-distance-marker.min.js
@@ -0,0 +1 @@
+L.DistanceMarker=L.CircleMarker.extend({_updatePath:function(){let ctx=this._renderer._ctx,p=this._point;if(this.options.rotation=this.options.rotation||0,this.options.radius&&this._renderer._updateCircle&&this._renderer._updateCircle(this),this.options.icon&&this.options.icon.url)if(this.options.icon.element){const icon=this.options.icon;let cx=p.x+icon.offset.x,cy=p.y+icon.offset.y;ctx.save(),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()}else{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}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",this.options.rotation&&(ctx.translate(p.x,p.y),ctx.rotate(this.options.rotation),cx=0,cy=0),this._map.getZoom()>17&&(ctx.fillStyle=this.options.strokeStyle||"black"),ctx.fillText(this.options.label,cx,cy),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:1e3,showAll:12,textFunction:(distance,i,offset)=>i,distance:!0,direction:!0},initialize:function(line,map,options){this._layers={},this._zoomLayers={},options=L.setOptions(this,options);let preferCanvas=map.options.preferCanvas,showAll=Math.min(map.getMaxZoom(),options.showAll);preferCanvas||map.options.rotate||console.warn('Missing dependency: "leaflet-rotate"');let coords="function"==typeof line.getLatLngs?line.getLatLngs():line;coords=L.LineUtil.isFlat(coords)?[coords]:coords,coords.forEach(latlngs=>{let accumulated=L.GeometryUtil.accumulatedLengths(latlngs),length=accumulated.length>0?accumulated[accumulated.length-1]:0;for(let i=1,count=Math.floor(length/options.offset),j=0;i<=count;++i){let distance=options.offset*i;for(;j<accumulated.length-1&&accumulated[j]<distance;)++j;let p1=latlngs[j-1],p2=latlngs[j],m_line=L.polyline([p1,p2]),ratio=(distance-accumulated[j-1])/(accumulated[j]-accumulated[j-1]),position=L.GeometryUtil.interpolateOnLine(map,m_line,ratio),delta=map.project(p2).subtract(map.project(p1)),angle=Math.atan2(delta.y,delta.x),text=options.textFunction.call(this,distance,i,options.offset),zoom=this._minimumZoomLevelForItem(i,showAll),markers=this._zoomLayers[zoom]=this._zoomLayers[zoom]||L.layerGroup();options.direction&&(options.distance&&i%2==1||!options.distance)&&(preferCanvas?markers.addLayer(new L.DistanceMarker(p1,{radius:0,icon:{url:options.arrowUrl,size:options.arrowSize,rotate:0,offset:{x:0,y:0}},rotation:angle,interactive:!1})):markers.addLayer(L.marker(position.latLng,{icon:L.icon({iconUrl:options.arrowUrl,iconSize:options.arrowSize}),rotation:angle,interactive:!1}))),options.distance&&i%2==0&&(preferCanvas?markers.addLayer(new L.DistanceMarker(position.latLng,{label:text,radius:7,fillColor:"#fff",fillOpacity:1,fillStyle:"black",color:"#777",weight:1,interactive:!1})):markers.addLayer(L.marker(position.latLng,{title:text,icon:L.divIcon({className:options.cssClass,html:text,iconSize:options.iconSize}),interactive:!1})))}});const updateMarkerVisibility=()=>{let oldZoom=this._lastZoomLevel||0,newZoom=map.getZoom();if(newZoom>oldZoom)for(let i=oldZoom+1;i<=newZoom;++i)void 0!==this._zoomLayers[i]&&this.addLayer(this._zoomLayers[i]);else if(newZoom<oldZoom)for(let i=oldZoom;i>newZoom;--i)void 0!==this._zoomLayers[i]&&this.removeLayer(this._zoomLayers[i]);this._lastZoomLevel=newZoom};map.on("zoomend",updateMarkerVisibility),updateMarkerVisibility()},_minimumZoomLevelForItem:function(i,zoom){for(;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(){this._map&&this._distanceMarkers&&this._map.addLayer(this._distanceMarkers)},removeDistanceMarkers:function(){this._map&&this._distanceMarkers&&this._map.removeLayer(this._distanceMarkers)},onAdd:function(map){this._originalOnAdd(map);let opts=this.options.distanceMarkers||{};this.options.distanceMarkers&&(this._distanceMarkers=this._distanceMarkers||new L.DistanceMarkers(this,map,opts)),void 0!==opts.lazy&&!1!==opts.lazy||this.addDistanceMarkers()},onRemove:function(map){this.removeDistanceMarkers(),this._originalOnRemove(map)}}); \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/libs/leaflet-edgescale.js b/assets/static/vendor/leaflet-elevation/libs/leaflet-edgescale.js
new file mode 100644
index 0000000..74c672d
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/leaflet-edgescale.js
@@ -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);
+ }
+}); \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/libs/leaflet-edgescale.min.js b/assets/static/vendor/leaflet-elevation/libs/leaflet-edgescale.min.js
new file mode 100644
index 0000000..81237d5
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/leaflet-edgescale.min.js
@@ -0,0 +1 @@
+L.Control.EdgeScale=L.Control.extend({options:{position:"bottomleft",icon:!0,coords:!0,bar:!0,onMove:!0,template:"{y} | {x}",projected:!1,formatProjected:"#.##0,000",latlngFormat:"DD",latlngDesignators:!0,latLngFormatter:void 0,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){return this.options.bar&&(this._scaleBar=new L.Control.EdgeScale.Layer(!0===this.options.bar?{}:this.options.bar).addTo(map)),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)),this._container=L.DomUtil.create("div","leaflet-control-mapcentercoord"),Object.assign(this._container.style,this.options.containerStyle),this.options.coords||(this._container.style.display="none"),L.DomEvent.disableClickPropagation(this._container),this._container.innerHTML=this._getMapCenterCoord(),map.on("move",this._onMapMove,this),map.on("moveend",this._onMapMove,this),this._container},onRemove:function(map){this.options.bar&&this._scaleBar.remove(),this.options.icon&&map.getContainer().removeChild(this._icon),map.off("move",this._onMapMove,this),map.off("moveend",this._onMapMove,this)},_onMapMove:function(e){(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:latLngFormatter,latlngFormat:latlngFormat,latlngDesignators:designators}=this.options;if(void 0!==latLngFormatter)return latLngFormatter(latLng.lat,latLng.lng);let lat,lng,deg,min,center={lat:latLng.lat,lng:latLng.lng,lng_neg:latLng.lng<0,lat_neg:latLng.lat<0};return center.lng<0&&(center.lng=Math.abs(center.lng)),center.lng>180&&(center.lng=360-center.lng,center.lng_neg=!center.lng_neg),center.lat<0&&(center.lat=Math.abs(center.lat)),"DM"===latlngFormat?(deg=parseInt(center.lng),lng=deg+"º "+this._format("00.000",60*(center.lng-deg))+"'",deg=parseInt(center.lat),lat=deg+"º "+this._format("00.000",60*(center.lat-deg))+"'"):"DMS"===latlngFormat?(deg=parseInt(center.lng),min=60*(center.lng-deg),lng=deg+"º "+this._format("00",parseInt(min))+"' "+this._format("00.0",60*(min-parseInt(min)))+"''",deg=parseInt(center.lat),min=60*(center.lat-deg),lat=deg+"º "+this._format("00",parseInt(min))+"' "+this._format("00.0",60*(min-parseInt(min)))+"''"):(lng=this._format("#0.00000",center.lng)+"º",lat=this._format("##0.00000",center.lat)+"º"),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":"")})},_format:function(m,v){if(!m||isNaN(+v))return v;let isNegative=(v="-"==m.charAt(0)?-v:+v)<0?v=-v:0,result=m.match(/[^\d\-\+#]/g),Decimal=result&&result[result.length-1]||".",Group=result&&result[1]&&result[0]||",";m=m.split(Decimal),v=+(v=v.toFixed(m[1]&&m[1].length))+"";let pos_trail_zero=m[1]&&m[1].lastIndexOf("0"),part=v.split(".");(!part[1]||part[1]&&part[1].length<=pos_trail_zero)&&(v=(+v).toFixed(pos_trail_zero+1));let szSep=m[0].split(Group);m[0]=szSep.join("");let pos_lead_zero=m[0]&&m[0].indexOf("0");if(pos_lead_zero>-1)for(;part[0].length<m[0].length-pos_lead_zero;)part[0]="0"+part[0];else 0==+part[0]&&(part[0]="");(v=v.split("."))[0]=part[0];var pos_separator=szSep[1]&&szSep[szSep.length-1].length;if(pos_separator){let integer=v[0],str="",offset=integer.length%pos_separator;for(let i=0,l=integer.length;i<l;i++)str+=integer.charAt(i),!((i-offset+1)%pos_separator)&&i<l-pos_separator&&(str+=Group);v[0]=str}return v[1]=m[1]&&v[1]?Decimal+v[1]:"",(isNegative?"-":"")+v[0]+v[1]}}),L.Control.EdgeScale.Layer=L.Layer.extend({includes:L.Evented?L.Evented.prototype:L.Mixin.Events,options:{opacity:1,weight:.8,gradient:{size:10,opacity:.5},color:"#000",font:"11px Arial",zoomInterval:[{start:0,end:2,interval:5e6},{start:3,end:3,interval:2e6},{start:4,end:4,interval:1e6},{start:5,end:5,interval:5e5},{start:6,end:7,interval:2e5},{start:8,end:8,interval:1e5},{start:9,end:9,interval:5e4},{start:10,end:10,interval:2e4},{start:11,end:11,interval:1e4},{start:12,end:12,interval:5e3},{start:13,end:13,interval:2e3},{start:14,end:14,interval:1e3},{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);const a=this._a=6378137,b=this._b=6356752.3142,n=this._n=(a-b)/(a+b),a2=a*a,b2=b*b,n2=n*n,n3=n2*n,n4=n3*n,n5=n4*n;this._A=a*(1-n)*(1-n2)*(1+9/4*n2+225/64*n4),this._e2=(a2-b2)/a2,this._ic1=1.5*n-29/12*n3+6.9125*n5,this._ic2=21/8*n2-1537/128*n4,this._ic3=151/24*n3-32373/640*n5,this._ic4=1097/64*n4,this._ic5=8011/150*n5,this._c1=-1.5*n+31/24*n3-669/640*n5,this._c2=15/18*n2-435/128*n4,this._c3=-35/12*n3+8.1375*n5,this._c4=315/64*n4,this._c5=-8.6625*n5,this._LIMIT_PHI=1.484419982},onAdd:function(map){this._map=map;let pane=map.getPane(this.options.pane);pane||(pane=this._pane=map.createPane("edgescalePane",map.getPane("norotatePane")||map.getPane("mapPane")),pane.style.zIndex=625,pane.style.pointerEvents="none"),this._pane=pane,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){return map.addLayer(this),this},setOpacity:function(opacity){return this.options.opacity=opacity,L.DomUtil.setOpacity(this._canvas,this.options.opacity),this},bringToFront:function(){return this._canvas&&this._pane.appendChild(this._canvas),this},bringToBack:function(){return this._canvas&&this._pane.insertBefore(this._canvas,pane.firstChild),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();if(this._setCanvasPosition(),canvas.width=size.x,canvas.height=size.y,canvas.style.width=size.x+"px",canvas.style.height=size.y+"px",this._map._bearing)return;const{gradient:gradient}=this.options;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),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:zoomInterval}=this.options,zoom=this._map.getZoom();if(void 0!==zoomInterval)for(const idx in zoomInterval){const dict=zoomInterval[idx];if(dict.start<=zoom&&dict.end&&dict.end>=zoom){this._interval=dict.interval;break}}else 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(),text=this._interval>=1e3?this._interval/1e3+" 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:weight}=this.options,size=this._map.getSize(),to_rad=Math.PI/180,center=this._merLength(this._map.containerPointToLatLng(L.point(0,size.y/2)).lat*to_rad),top=this._merLength(this._map.containerPointToLatLng(L.point(0,0)).lat*to_rad),bottom=this._merLength(this._map.containerPointToLatLng(L.point(0,size.y)).lat*to_rad);for(let i=center+this._interval/2;i<top;i+=this._interval){const phi=this._invmerLength(i);phi<this._LIMIT_PHI&&phi>-this._LIMIT_PHI&&this._draw_lat_tick(phi,10,1.5*weight)}for(let i=center-this._interval/2;i>bottom;i-=this._interval){const phi=this._invmerLength(i);phi>-this._LIMIT_PHI&&phi<this._LIMIT_PHI&&this._draw_lat_tick(phi,10,1.5*weight)}for(let i=center;i<top;i+=this._interval/10){const phi=this._invmerLength(i);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-=this._interval/10){const phi=this._invmerLength(i);phi>-this._LIMIT_PHI&&phi<this._LIMIT_PHI&&this._draw_lat_tick(phi,4,weight)}},_create_lon_ticks:function(){const{weight:weight}=this.options,size=this._map.getSize(),to_rad=Math.PI/180,to_deg=180/Math.PI,center=this._map.containerPointToLatLng(L.point(size.x/2,0)),left=this._map.containerPointToLatLng(L.point(0,0)),right=this._map.containerPointToLatLng(L.point(size.x,0)),sinPhi2=Math.pow(Math.sin(center.lat*to_rad),2),N=this._a/Math.sqrt(1-this._e2*sinPhi2),dl=this._interval/(N*Math.cos(center.lat*to_rad))*to_deg;for(let i=center.lng+dl/2;i<right.lng;i+=dl)this._draw_lon_tick(i,10,1.5*weight);for(let i=center.lng-dl/2;i>left.lng;i-=dl)this._draw_lon_tick(i,10,1.5*weight);for(let i=center.lng;i<right.lng;i+=dl/10)this._draw_lon_tick(i,4,weight);for(let i=center.lng-dl/10;i>left.lng;i-=dl/10)this._draw_lon_tick(i,4,weight)},_setCanvasPosition:function(){let lt=this._map.containerPointToLayerPoint([0,0]);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/Math.PI,size=this._map.getSize(),y=this._latLngToCanvasPoint(L.latLng(phi*to_deg,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,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*phi),sin2=Math.sin(2*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,cos2=Math.cos(2*psi),sin2=Math.sin(2*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:!1}),L.Map.addInitHook((function(){this.options.edgeScaleControl&&(this.edgeScaleControl=new L.Control.EdgeScale,this.addControl(this.edgeScaleControl))})); \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js b/assets/static/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js
new file mode 100644
index 0000000..c7a9dd6
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/leaflet-gpxgroup.js
@@ -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); \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/libs/leaflet-gpxgroup.min.js b/assets/static/vendor/leaflet-elevation/libs/leaflet-gpxgroup.min.js
new file mode 100644
index 0000000..5b30fb0
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/leaflet-gpxgroup.min.js
@@ -0,0 +1 @@
+L.Mixin.Selectable={includes:L.Mixin.Events,setSelected:function(s){var selected=!!s;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){this._selected===item?null!==item&&(item.setSelected(!item.isSelected()),item.isSelected()||(this._selected=null)):(this._selected&&this._selected.setSelected(!1),this._selected=item,this._selected&&this._selected.setSelected(!0)),this.fire("selection_changed")}},L.Control.LayersLegend=L.Control.Layers.extend({_onInputClick:function(){this._handlingClick=!0,this._layerControlInputs.reduceRight((_,input)=>{if(input.checked)return this._map.fireEvent("legend_selected",{layer:this._getLayer(input.layerId).layer,input:input},!0),input},0),this._handlingClick=!1,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:!0,legend:!1,legend_options:{position:"topright",collapsed:!1},elevation:!0,elevation_options:{theme:"yellow-theme",detached:!0,elevationDiv:"#elevation-div"},distanceMarkers:!0,distanceMarkers_options:{lazy:!0}},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){track instanceof Object?this._loadGeoJSON(track):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){geojson&&(geojson.name=geojson.name||geojson[0]&&geojson[0].properties.name||fallbackName,this._loadRoute(geojson))},_loadRoute:function(data){if(data){var line_style={color:this._uniqueColors(this._tracks.length)[this._count++],opacity:.75,weight:5,distanceMarkers:this.options.distanceMarkers_options},route=L.geoJson(data,{name:data.name||"",style:feature=>line_style,distanceMarkers:line_style.distanceMarkers,originalStyle:line_style,filter:feature=>"Point"!=feature.geometry.type});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:!0})},_onEachRouteLoaded:function(route){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}),++this._loadedCount===this._tracks.length&&(this.fire("loaded"),this.options.flyToBounds&&this._map.flyToBounds(this.getBounds(),{duration:.25,easeLinearity:.25,noMoveStart:!0}),this.options.legend&&this._legend.addTo(this._map))},highlight:function(route,polyline){polyline.setStyle(this.options.highlight),this.options.distanceMarkers&&polyline.addDistanceMarkers()},unhighlight:function(route,polyline){polyline.setStyle(route.options.originalStyle),this.options.distanceMarkers&&polyline.removeDistanceMarkers()},_onRouteMouseOver:function(route,polyline){route.isSelected()||(this.highlight(route,polyline),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){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){route.isSelected()||this.unhighlight(route,polyline)},_onSelectionChanged:function(e){var elevation=this._elevation,eleDiv=elevation.getContainer(),route=this.getSelection();elevation.clear(),route&&route.isSelected()?(eleDiv||elevation.addTo(this._map),route.getLayers().forEach((function(layer){layer instanceof L.Polyline&&(elevation.addData(layer,!1),layer.bringToFront())}))):eleDiv&&elevation.remove()},_onLegendSelected:function(e){var parent=e.input.closest(".leaflet-control-layers-list"),route=e.layer;if(!route.isSelected()){for(var i in this.setSelection(route),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 1===count?["#0000ff"]:new Array(count).fill(null).map((_,i)=>this._hsvToHex(i*(1/count),1,.7))},_hsvToHex:function(h,s,v){var i=Math.floor(6*h),f=6*h-i,p=v*(1-s),q=v*(1-f*s),t=v*(1-(1-f)*s),rgb;return{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].map(d=>255*d).reduce((hex,byte)=>hex+(byte>>4&15).toString(16)+(15&byte).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); \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/libs/leaflet-hotline.js b/assets/static/vendor/leaflet-elevation/libs/leaflet-hotline.js
new file mode 100644
index 0000000..1684560
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/leaflet-hotline.js
@@ -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 - &lt;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;
+})); \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/libs/leaflet-hotline.min.js b/assets/static/vendor/leaflet-elevation/libs/leaflet-hotline.min.js
new file mode 100644
index 0000000..a1466b8
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/leaflet-hotline.min.js
@@ -0,0 +1 @@
+!function(root,plugin){"function"==typeof define&&define.amd?define(["leaflet"],plugin):"object"==typeof exports?module.exports=plugin:plugin(root.L)}(window,(function(L){if(L.Hotline)return L;var Hotline=function(canvas){if(!(this instanceof Hotline))return new Hotline(canvas);var defaultPalette={0:"green",.5:"yellow",1:"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={width:function(width){return this._width=width,this},height:function(height){return this._height=height,this},weight:function(weight){return this._weight=weight,this},outlineWidth:function(outlineWidth){return this._outlineWidth=outlineWidth,this},outlineColor:function(outlineColor){return this._outlineColor=outlineColor,this},palette:function(palette){var canvas=document.createElement("canvas"),ctx=canvas.getContext("2d"),gradient=ctx.createLinearGradient(0,0,0,256);for(var i in canvas.width=1,canvas.height=256,palette)gradient.addColorStop(i,palette[i]);return ctx.fillStyle=gradient,ctx.fillRect(0,0,1,256),this._palette=ctx.getImageData(0,0,1,256).data,this},min:function(min){return this._min=min,this},max:function(max){return this._max=max,this},data:function(data){return this._data=data,this},add:function(path){return this._data.push(path),this},draw:function(){var ctx=this._ctx;return ctx.globalCompositeOperation="source-over",ctx.lineCap="round",this._drawOutline(ctx),this._drawHotline(ctx),this},getRGBForValue:function(value){var valueRelative=Math.min(Math.max((value-this._min)/(this._max-this._min),0),.999),paletteIndex=4*Math.floor(256*valueRelative);return[this._palette[paletteIndex],this._palette[paletteIndex+1],this._palette[paletteIndex+2]]},_drawOutline:function(ctx){var i,j,dataLength,path,pathLength,pointStart,pointEnd;if(this._outlineWidth)for(i=0,dataLength=this._data.length;i<dataLength;i++)for(path=this._data[i],ctx.lineWidth=this._weight+2*this._outlineWidth,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()},_drawHotline:function(ctx){var i,j,dataLength,path,pathLength,pointStart,pointEnd,gradient,gradientStartRGB,gradientEndRGB;for(ctx.lineWidth=this._weight,i=0,dataLength=this._data.length;i<dataLength;i++)for(j=1,pathLength=(path=this._data[i]).length;j<pathLength;j++)pointStart=path[j-1],pointEnd=path[j],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){var parts=layer._parts;parts.length&&(this._updateOptions(layer),this._hotline.data(parts).draw())}},_updateOptions:function(layer){null!=layer.options.min&&this._hotline.min(layer.options.min),null!=layer.options.max&&this._hotline.max(layer.options.max),null!=layer.options.weight&&this._hotline.weight(layer.options.weight),null!=layer.options.outlineWidth&&this._hotline.outlineWidth(layer.options.outlineWidth),null!=layer.options.outlineColor&&this._hotline.outlineColor(layer.options.outlineColor),layer.options.palette&&this._hotline.palette(layer.options.palette)}}),renderer=function(options){return L.Browser.canvas?new Renderer(options):null},Util={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;for(this._lastCode=codeB;;){if(!(codeA|codeB))return[a,b];if(codeA&codeB)return!1;codeOut=codeA||codeB,p=L.LineUtil._getEdgeIntersection(a,b,codeOut,bounds,round),newCode=L.LineUtil._getBitCode(p,bounds),codeOut===codeA?(p.z=a.z,a=p,codeA=newCode):(p.z=b.z,b=p,codeB=newCode)}}};return L.Hotline=L.Polyline.extend({statics:{Renderer:Renderer,renderer:renderer},options:{renderer:renderer(),min:0,max:1,palette:{0:"green",.5:"yellow",1:"red"},weight:5,outlineColor:"black",outlineWidth:1},getRGBForValue:function(value){return this._renderer._hotline.getRGBForValue(value)},_projectLatlngs:function(latlngs,result,projectedBounds){var flat=latlngs[0]instanceof L.LatLng,len=latlngs.length,i,ring;if(flat){for(ring=[],i=0;i<len;i++)ring[i]=this._map.latLngToLayerPoint(latlngs[i]),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)},_clipPoints:function(){if(this.options.noClip)this._parts=this._rings;else{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++)for(j=0,len2=(points=this._rings[i]).length;j<len2-1;j++)(segment=Util.clipSegment(points[j],points[j+1],bounds,j,!0))&&(parts[k]=parts[k]||[],parts[k].push(segment[0]),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)},L})); \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.css b/assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.css
new file mode 100644
index 0000000..e59d69b
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.css
@@ -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;
+}
+ \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.js b/assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.js
new file mode 100644
index 0000000..847f9b8
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.js
@@ -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: '&deg;',
+ 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>&nbsp;' + bearing.toFixed(this.options.angleUnit.decimal) + '&nbsp;' + this.options.angleUnit.display + '<br><b>' + this.options.lengthUnit.label + '</b>&nbsp;' + totalLength + '&nbsp;' + 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);
+}; \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.min.css b/assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.min.css
new file mode 100644
index 0000000..9532825
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.min.css
@@ -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} \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.min.js b/assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.min.js
new file mode 100644
index 0000000..e732020
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/libs/leaflet-ruler.min.js
@@ -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:"&deg;",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>&nbsp;"+bearing.toFixed(this.options.angleUnit.decimal)+"&nbsp;"+this.options.angleUnit.display+"<br><b>"+this.options.lengthUnit.label+"</b>&nbsp;"+totalLength+"&nbsp;"+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)}; \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/src/components/chart.js b/assets/static/vendor/leaflet-elevation/src/components/chart.js
new file mode 100644
index 0000000..b0b5458
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/components/chart.js
@@ -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 = _.clamp(coords[0], [0, this._width()]); // Clamps cursor position to chart's bounds (see issues: #270, #301)
+ 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);
+ }
+});
diff --git a/assets/static/vendor/leaflet-elevation/src/components/d3.js b/assets/static/vendor/leaflet-elevation/src/components/d3.js
new file mode 100644
index 0000000..2c35d57
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/components/d3.js
@@ -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;
+};
diff --git a/assets/static/vendor/leaflet-elevation/src/components/marker.js b/assets/static/vendor/leaflet-elevation/src/components/marker.js
new file mode 100644
index 0000000..6f1f6af
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/components/marker.js
@@ -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;
+ }
+
+});
diff --git a/assets/static/vendor/leaflet-elevation/src/components/summary.js b/assets/static/vendor/leaflet-elevation/src/components/summary.js
new file mode 100644
index 0000000..e8da57f
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/components/summary.js
@@ -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];
+ }
+ }
+
+});
diff --git a/assets/static/vendor/leaflet-elevation/src/control.js b/assets/static/vendor/leaflet-elevation/src/control.js
new file mode 100644
index 0000000..27ecf77
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/control.js
@@ -0,0 +1,1335 @@
+import * as _ from './utils';
+import { Options } from './options';
+
+// "leaflet-i18n" fallback
+if (!L._ || !L.i18n) {
+ L._ = L.i18n = (string, data) => string;
+}
+
+export const Elevation = L.Control.Elevation = L.Control.extend({
+
+ includes: L.Evented ? L.Evented.prototype : L.Mixin.Events,
+
+ options: Options,
+ __mileFactor: 0.621371, // 1 km = (0.621371 mi)
+ __footFactor: 3.28084, // 1 m = (3.28084 ft)
+ __D3: 'https://unpkg.com/d3@7.8.4/dist/d3.min.js',
+ __TOGEOJSON: 'https://unpkg.com/@tmcw/togeojson@5.6.2/dist/togeojson.umd.js',
+ __LGEOMUTIL: 'https://unpkg.com/leaflet-geometryutil@0.10.1/src/leaflet.geometryutil.js',
+ __LALMOSTOVER: 'https://unpkg.com/leaflet-almostover@1.0.1/src/leaflet.almostover.js',
+ __LHOTLINE: '../libs/leaflet-hotline.min.js',
+ __LDISTANCEM: '../libs/leaflet-distance-marker.min.js',
+ __LEDGESCALE: '../libs/leaflet-edgescale.min.js',
+ __LCHART: '../src/components/chart.js',
+ __LMARKER: '../src/components/marker.js',
+ __LSUMMARY: '../src/components/summary.js',
+ __modulesFolder: '../src/handlers/',
+ __btnIcon: '../images/elevation.svg',
+
+ /*
+ * Add data to the diagram either from GPX or GeoJSON and update the axis domain and data
+ */
+ addData(d, layer) {
+ this.import(this.__D3)
+ .then(() => {
+ if (this._modulesLoaded) {
+ layer = layer ?? (d.on && d);
+ this._addData(d);
+ this._addLayer(layer);
+ this._fireEvt("eledata_added", { data: d, layer: layer, track_info: this.track_info });
+ } else {
+ this.once('modules_loaded', () => this.addData(d,layer));
+ }
+ });
+ },
+
+ /**
+ * Adds the control to the given map.
+ */
+ addTo(map) {
+ if (this.options.detached) {
+ let parent = _.select(this.options.elevationDiv);
+ let eleDiv = this.onAdd(map);
+ parent ? _.append(parent, eleDiv) : _.insert(map.getContainer(), eleDiv, 'afterend');
+ } else {
+ L.Control.prototype.addTo.call(this, map);
+ }
+ return this;
+ },
+
+ /*
+ * Reset data and display
+ */
+ clear() {
+ if (this._marker) this._marker.remove();
+ if (this._chart) this._clearChart();
+ if (this._layers) this._clearLayers(this._layers);
+ if (this._markers) this._clearLayers(this._markers);
+ if (this._circleMarkers) this._circleMarkers.remove();
+ if (this._hotline) this._hotline.eachLayer(l => l.options.renderer.remove()); // hotfix for: https://github.com/Raruto/leaflet-elevation/issues/233
+ if (this._hotline) this._clearLayers(this._hotline);
+
+ this._data = [];
+ this.track_info = {};
+
+ this._fireEvt("eledata_clear");
+
+ this._updateChart();
+ },
+
+ _clearChart() {
+ if (this._events && this._events.elechart_updated) {
+ this._events.elechart_updated.forEach(({fn, ctx}) => this.off('elechart_updated', fn, ctx));
+ }
+ if (this._chart && this._chart._container) {
+ this._chart._container.selectAll('g.point .point').remove();
+ this._chart.clear();
+ }
+ },
+
+ _clearLayers(l) {
+ l = l || this._layers;
+ if (l && l.eachLayer) {
+ l.eachLayer(f => f.remove())
+ l.clearLayers();
+ }
+ },
+
+ /**
+ * TODO: Create a base class to handle custom data attributes (heart rate, cadence, temperature, ...)
+ *
+ * @link https://leafletjs.com/examples/extending/extending-3-controls.html#handlers
+ */
+ // addHandler: function (name, HandlerClass) {
+ // if (HandlerClass) {
+ // let handler = this[name] = new HandlerClass(this);
+ // this.handlers.push(handler);
+ // if (this.options[name]) {
+ // handler.enable();
+ // }
+ // }
+ // return this;
+ // },
+
+ /**
+ * Disable chart brushing.
+ */
+ disableBrush() {
+ this._chart._brushEnabled = false;
+ this._resetDrag();
+ },
+
+ /**
+ * Enable chart brushing.
+ */
+ enableBrush() {
+ this._chart._brushEnabled = true;
+ },
+
+ /**
+ * Disable chart zooming.
+ */
+ disableZoom() {
+ this._chart._zoomEnabled = false;
+ this._chart._resetZoom();
+ },
+
+ /**
+ * Enable chart zooming.
+ */
+ enableZoom() {
+ this._chart._zoomEnabled = true;
+ },
+
+ /**
+ * Sets a map view that contains the given geographical bounds.
+ */
+ fitBounds(bounds) {
+ bounds = bounds || this.getBounds();
+ if (this._map && bounds.isValid()) this._map.fitBounds(bounds);
+ },
+
+ getBounds(data) {
+ return L.latLngBounds((data || this._data).map((d) => d.latlng));
+ },
+
+ /**
+ * Get default zoom level (followMarker: true).
+ */
+ getZFollow() {
+ return this.options.zFollow;
+ },
+
+ /**
+ * Hide current elevation chart profile.
+ */
+ hide() {
+ _.style(this._container, "display", "none");
+ },
+
+ /**
+ * Initialize chart control "options" and "container".
+ */
+ initialize(opts) {
+
+ // opts = L.setOptions(this, opts);
+
+ // Fixes: https://github.com/Raruto/leaflet-elevation/pull/240
+ opts = L.setOptions(this, L.extend({}, _.cloneDeep(Options), opts)); // "deep copy" nested objects (multiple charts)
+
+ this._data = [];
+ this._layers = L.featureGroup();
+ this._markers = L.featureGroup();
+ this._hotline = L.featureGroup();
+ this._circleMarkers = L.featureGroup();
+ this._markedSegments = L.polyline([]);
+ this._start = L.circleMarker([0,0], (opts.trkStart || Options.trkStart));
+ this._end = L.circleMarker([0,0], (opts.trkEnd || Options.trkEnd));
+ this._chartEnabled = true;
+ this._yCoordMax = -Infinity;
+ this.track_info = {};
+ // this.handlers = [];
+
+ if (opts.followMarker) this._setMapView = _.throttle(this._setMapView, 300, this);
+ if (opts.legend) opts.margins.bottom += 30;
+ if (opts.theme) opts.polylineSegments.className += ' ' + opts.theme;
+ if (opts.wptIcons === true) opts.wptIcons = Options.wptIcons;
+ if (opts.distanceMarkers === true) opts.distanceMarkers = Options.distanceMarkers;
+ if (opts.trkStart) this._start.addTo(this._circleMarkers);
+ if (opts.trkEnd) this._end.addTo(this._circleMarkers);
+
+
+ this._markedSegments.setStyle(opts.polylineSegments);
+
+ // Leaflet canvas renderer colors
+ L.extend(_.Colors, opts.colors || {});
+
+ // Various stuff
+ this._fixCanvasPaths();
+ this._fixTooltipSize();
+
+ },
+
+ /**
+ * Javascript scripts downloader (lazy loader)
+ */
+ import(src, condition) {
+ if (Array.isArray(src)) {
+ return Promise.all(src.map(m => this.import(m)));
+ }
+ switch(src) {
+ case this.__D3: condition = typeof d3 !== 'object'; break;
+ case this.__TOGEOJSON: condition = typeof toGeoJSON !== 'object'; break;
+ case this.__LGEOMUTIL: condition = typeof L.GeometryUtil !== 'object'; break;
+ case this.__LALMOSTOVER: condition = typeof L.Handler.AlmostOver !== 'function'; break;
+ case this.__LDISTANCEM: condition = typeof L.DistanceMarkers !== 'function'; break;
+ case this.__LEDGESCALE: condition = typeof L.Control.EdgeScale !== 'function'; break;
+ case this.__LHOTLINE: condition = typeof L.Hotline !== 'function'; break;
+ }
+ return condition !== false ? import(_.resolveURL(src, this.options.srcFolder)) : Promise.resolve();
+ },
+
+ /**
+ * Load elevation data (GPX, GeoJSON, KML or TCX).
+ */
+ load(data) {
+ this._parseFromString(data).then( geojson => geojson ? this._loadLayer(geojson) : this._loadFile(data));
+ },
+
+ /**
+ * Create container DOM element and related event listeners.
+ * Called on control.addTo(map).
+ */
+ onAdd(map) {
+ this._map = map;
+
+ let container = this._container = _.create("div", "elevation-control " + this.options.theme + " " + (this.options.detached ? 'elevation-detached' : 'leaflet-control'), this.options.detached ? { id: 'elevation-' + _.randomId() } : {});
+
+ if (!this.eleDiv) this.eleDiv = container;
+
+ this._loadModules(this.options.handlers).then(() => { // Inject here required modules (data handlers)
+ this._initChart(container);
+ this._initButton(container);
+ this._initSummary(container);
+ this._initMarker(map);
+ this._initLayer(map);
+ this._modulesLoaded = true;
+ this.fire('modules_loaded');
+ });
+
+ this.fire('add');
+
+ return container;
+ },
+
+ /**
+ * Clean up control code and related event listeners.
+ * Called on control.remove().
+ */
+ onRemove(map) {
+ this._container = null;
+
+ map
+ .off('zoom viewreset zoomanim', this._hideMarker, this)
+ .off('resize', this._resetView, this)
+ .off('resize', this._resizeChart, this)
+ .off('mousedown', this._resetDrag, this);
+
+ _.off(map.getContainer(), 'mousewheel', this._resetDrag, this);
+ _.off(map.getContainer(), 'touchstart', this._resetDrag, this);
+ _.off(document, 'keydown', this._onKeyDown, this);
+
+ this
+ .off('eledata_added eledata_loaded', this._updateChart, this)
+ .off('eledata_added eledata_loaded', this._updateSummary, this);
+
+ this.fire('remove');
+ },
+
+ /**
+ * Redraws the chart control. Sometimes useful after screen resize.
+ */
+ redraw() {
+ this._resizeChart();
+ },
+
+ /**
+ * Set default zoom level (followMarker: true).
+ */
+ setZFollow(zoom) {
+ this.options.zFollow = zoom;
+ },
+
+ /**
+ * Hide current elevation chart profile.
+ */
+ show() {
+ _.style(this._container, "display", "block");
+ },
+
+ /*
+ * Parsing data either from GPX or GeoJSON and update the diagram data
+ */
+ _addData(d) {
+ if (!d) {
+ return;
+ }
+
+ // Standard GeoJSON
+ if (d.type === "FeatureCollection" ) {
+ return _.each(d.features, feature => this._addData(feature));
+ } else if (d.type === "Feature") {
+ let geom = d.geometry;
+ if (geom) {
+ switch (geom.type) {
+ case 'LineString': return this._addGeoJSONData(geom.coordinates, d.properties);
+ case 'MultiLineString': return _.each(geom.coordinates, (coords, i) => this._addGeoJSONData(coords, d.properties, i));
+ case 'Point':
+ default: return console.warn('Unsopperted GeoJSON feature geometry type:' + geom.type);
+ }
+ }
+ }
+
+ // Fallback for leaflet layers (eg. L.Gpx)
+ if (d._latlngs) {
+ return this._addGeoJSONData(d._latlngs, d.feature && d.feature.properties);
+ }
+
+ },
+
+ /*
+ * Parsing of GeoJSON data lines and their elevation in z-coordinate
+ */
+ _addGeoJSONData(coords, properties, nestingLevel) {
+
+ // "coordinateProperties" property is generated inside "@tmcw/toGeoJSON"
+ let props = (properties && properties.coordinateProperties) || properties;
+
+ coords.forEach((point, i) => {
+
+ // GARMIN_EXTENSIONS = ["hr", "cad", "atemp", "wtemp", "depth", "course", "bearing"];
+ point.meta = point.meta ?? { time: null, ele: null };
+
+ point.prev = (attr) => (attr ? this._data[i > 0 ? i - 1 : 0][attr] : this._data[i > 0 ? i - 1 : 0]);
+
+ this.fire("elepoint_init", { point: point, props: props, id: i, isMulti: nestingLevel });
+
+ this._addPoint(
+ point.lat ?? point[1],
+ point.lng ?? point[0],
+ point.alt ?? point.meta.ele ?? point[2]
+ );
+
+ this.fire("elepoint_added", { point: point, index: this._data.length - 1 });
+
+ if (this._yCoordMax < this._data[this._data.length - 1][this.options.yAttr]) this._yCoordMax = this._data[this._data.length - 1][this.options.yAttr];
+ });
+
+ this.fire("eletrack_added", { coords: coords, index: this._data.length - 1 });
+ },
+
+ /*
+ * Parse and push a single (x, y, z) point to current elevation profile.
+ */
+ _addPoint(x, y, z) {
+ if (this.options.reverseCoords) {
+ [x, y] = [y, x];
+ }
+
+ this._data.push({
+ x: x,
+ y: y,
+ z: z,
+ latlng: L.latLng(x, y, z)
+ });
+
+ this.fire("eledata_updated", { index: this._data.length - 1 });
+ },
+
+ _addLayer(layer) {
+ if (layer) this._layers.addLayer(layer)
+ // Postpone adding the distance markers (lazy: true)
+ if (layer && this.options.distanceMarkers && this.options.distanceMarkers.lazy) {
+ layer.on('add remove', ({target, type}) => L.DistanceMarkers && target instanceof L.Polyline && target[type + 'DistanceMarkers']());
+ }
+ return layer;
+ },
+
+ _addMarker(marker) {
+ if (marker) this._markers.addLayer(marker)
+ return marker;
+ },
+
+ /**
+ * Initialize "L.AlmostOver" integration
+ */
+ _initAlmostOverHandler(map, layer) {
+ return (map && this.options.almostOver && !L.Browser.mobile) ? this.import([this.__LGEOMUTIL, this.__LALMOSTOVER])
+ .then(() => {
+ map.addHandler('almostOver', L.Handler.AlmostOver)
+ if (L.GeometryUtil && map.almostOver && map.almostOver.enabled()) {
+ map.almostOver.addLayer(layer);
+ map
+ .on('almost:move', this._onMouseMoveLayer, this)
+ .on('almost:out', this._onMouseOut, this);
+ this.once('eledata_clear', () => {
+ map.almostOver.removeLayer(layer);
+ map
+ .off('almost:move', this._onMouseMoveLayer, this)
+ .off('almost:out', this._onMouseOut, this);
+ })
+ }
+ }) : Promise.resolve();
+ },
+
+ /**
+ * Initialize "L.DistanceMarkers" integration
+ */
+ _initDistanceMarkers() {
+ return this.options.distanceMarkers ? this.import([this.__LGEOMUTIL, this.__LDISTANCEM]) : Promise.resolve();
+ },
+
+ /**
+ * Initialize "L.Control.EdgeScale" integration
+ */
+ _initEdgeScale(map) {
+ return this.options.edgeScale ? this.import(this.__LEDGESCALE)
+ .then(() => {
+ map.edgeScaleControl = map.edgeScaleControl || L.control.edgeScale('boolean' !== typeof this.options.edgeScale ? this.options.edgeScale : {}).addTo(map);
+ }) : Promise.resolve();
+ },
+
+ _initHotLine(layer) {
+ let prop = typeof this.options.hotline == 'string' ? this.options.hotline : 'elevation';
+ return this.options.hotline ? this.import(this.__LHOTLINE)
+ .then(() => {
+ layer.eachLayer((trkseg) => {
+ if (trkseg.feature.geometry.type != "Point") {
+ let geo = L.geoJson(trkseg.toGeoJSON(), { coordsToLatLng: (coords) => L.latLng(coords[0], coords[1], coords[2] * (this.options.altitudeFactor || 1))});
+ let line = L.hotline(geo.toGeoJSON().features[0].geometry.coordinates, {
+ renderer: L.Hotline.renderer(),
+ min: isFinite(this.track_info[prop + '_min']) ? this.track_info[prop + '_min'] : 0,
+ max: isFinite(this.track_info[prop + '_max']) ? this.track_info[prop + '_max'] : 1,
+ palette: {
+ 0.0: '#008800',
+ 0.5: '#ffff00',
+ 1.0: '#ff0000'
+ },
+ weight: 5,
+ outlineColor: '#000000',
+ outlineWidth: 1
+ }).addTo(this._hotline);
+ let alpha = trkseg.options.style && trkseg.options.style.opacity || 1;
+ trkseg.on('add remove', ({type}) => {
+ trkseg.setStyle({opacity: (type == 'add' ? 0 : alpha)});
+ line[(type == 'add' ? 'addTo' : 'removeFrom')](trkseg._map);
+ if (line._renderer) line._renderer._container.parentElement.insertBefore(line._renderer._container, line._renderer._container.parentElement.firstChild);
+ });
+ }
+ });
+ }) : Promise.resolve();
+ },
+
+ /**
+ * Initialize "L.AlmostOver" and "L.DistanceMarkers"
+ */
+ _initMapIntegrations(layer) {
+ let map = this._map;
+ if (map) {
+ if (this._data.length) {
+ this._start.setLatLng(this._data[0].latlng);
+ this._end.setLatLng(this._data[this._data.length -1].latlng);
+ }
+ Promise.all([
+ this._initHotLine(layer),
+ this._initAlmostOverHandler(map, layer),
+ this._initDistanceMarkers(),
+ this._initEdgeScale(map),
+ ]).then(() => {
+ if (this.options.polyline) {
+ this._layers.addLayer(layer.addTo(map)); // hotfix for: https://github.com/Raruto/leaflet-elevation/issues/233
+ this._circleMarkers.addTo(map);
+ }
+ if (this.options.autofitBounds) {
+ this.fitBounds(layer.getBounds());
+ }
+ map.invalidateSize();
+ });
+ } else {
+ this.once('add', () => this._initMapIntegrations(layer));
+ }
+ },
+
+ /*
+ * Collapse current chart control.
+ */
+ _collapse() {
+ _.replaceClass(this._container, 'elevation-expanded', 'elevation-collapsed');
+ if (this._map) this._map.invalidateSize();
+ },
+
+ /*
+ * Expand current chart control.
+ */
+ _expand() {
+ _.replaceClass(this._container, 'elevation-collapsed', 'elevation-expanded');
+ if (this._map) this._map.invalidateSize();
+ },
+
+ /**
+ * Add some basic colors to leaflet canvas renderer (preferCanvas: true).
+ */
+ _fixCanvasPaths() {
+ let oldProto = L.Canvas.prototype._fillStroke;
+ let control = this;
+
+ let theme = this.options.theme.split(' ')[0].replace('-theme', '');
+ let color = _.Colors[theme] || {};
+
+ L.Canvas.include({
+ _fillStroke(ctx, layer) {
+ if (control._layers.hasLayer(layer)) {
+
+ let options = layer.options;
+
+ options.color = color.line || color.area || theme;
+ options.stroke = !!options.color;
+
+ oldProto.call(this, ctx, layer);
+
+ if (options.stroke && options.weight !== 0) {
+ let oldVal = ctx.globalCompositeOperation || 'source-over';
+ ctx.globalCompositeOperation = 'destination-over'
+ ctx.strokeStyle = color.outline || '#FFF';
+ ctx.lineWidth = options.weight * 1.75;
+ ctx.stroke();
+ ctx.globalCompositeOperation = oldVal;
+ }
+
+ } else {
+ oldProto.call(this, ctx, layer);
+ }
+ }
+ });
+ },
+
+ /**
+ * Partial fix for initial tooltip size
+ *
+ * @link https://github.com/Raruto/leaflet-elevation/issues/81#issuecomment-713477050
+ */
+ _fixTooltipSize() {
+ this.on('elechart_init', () =>
+ this.once('elechart_change elechart_hover', ({data, xCoord}) => {
+ if (this._chartEnabled) {
+ this._chart._showDiagramIndicator(data, xCoord);
+ this._chart._showDiagramIndicator(data, xCoord);
+ }
+ this._updateMarker(data);
+ })
+ );
+ },
+
+ /*
+ * Finds a data entry for the given LatLng
+ */
+ _findItemForLatLng(latlng) {
+ return this._data[this._chart._findIndexForLatLng(latlng)];
+ },
+
+ /*
+ * Finds a data entry for the given xDiagCoord
+ */
+ _findItemForX(x) {
+ return this._data[this._chart._findIndexForXCoord(x)];
+ },
+
+ /**
+ * Fires an event of the specified type.
+ */
+ _fireEvt(type, data, propagate) {
+ if (this.fire) this.fire(type, data, propagate);
+ if (this._map) this._map.fire(type, data, propagate);
+ },
+
+ /*
+ * Hides the position/height indicator marker drawn onto the map
+ */
+ _hideMarker() {
+ if (this.options.autohideMarker) {
+ this._marker.remove();
+ }
+ },
+
+ /**
+ * Generate "svg" chart (DOM element).
+ */
+ _initChart(container) {
+ let opts = this.options;
+ let map = this._map;
+
+ if (opts.detached) {
+ let { offsetWidth, offsetHeight} = this.eleDiv;
+ if (offsetWidth > 0) opts.width = offsetWidth;
+ if (offsetHeight > 20) opts.height = offsetHeight - 20; // 20 = horizontal scrollbar size.
+ } else {
+ let { clientWidth } = map.getContainer();
+ opts._maxWidth = opts._maxWidth > opts.width ? opts._maxWidth : opts.width;
+ this._container.style.maxWidth = opts._maxWidth + 'px';
+ if (opts._maxWidth > clientWidth) opts.width = clientWidth - 30;
+ }
+
+ this
+ .import([this.__D3, this.__LCHART])
+ .then((m) => {
+
+ let chart = this._chart = new (m[1] || Elevation).Chart(opts, this);
+
+ this._x = this._chart._x;
+ this._y = this._chart._y;
+
+ d3
+ .select(container)
+ .call(chart.render())
+
+ chart
+ .on('reset_drag', this._hideMarker, this)
+ .on('mouse_enter', this._onMouseEnter, this)
+ .on('dragged', this._onDragEnd, this)
+ .on('mouse_move', this._onMouseMove, this)
+ .on('mouse_out', this._onMouseOut, this)
+ .on('ruler_filter', this._onRulerFilter, this)
+ .on('zoom', this._updateChart, this)
+ .on('elepath_toggle', this._onToggleChart, this)
+ .on('margins_updated', this._resizeChart, this);
+
+
+ this.fire("elechart_init");
+
+ map
+ .on('zoom viewreset zoomanim', this._hideMarker, this)
+ .on('resize', this._resetView, this)
+ .on('resize', this._resizeChart, this)
+ .on('rotate', this._rotateMarker, this)
+ .on('mousedown', this._resetDrag, this);
+
+ _.on(map.getContainer(), 'mousewheel', this._resetDrag, this);
+ _.on(map.getContainer(), 'touchstart', this._resetDrag, this);
+ _.on(document, 'keydown', this._onKeyDown, this);
+
+ this
+ .on('eledata_added eledata_loaded', this._updateChart, this)
+ .on('eledata_added eledata_loaded', this._updateSummary, this);
+
+ this._updateChart();
+ this._updateSummary();
+ });
+
+
+ },
+
+ _initLayer() {
+ this._layers
+ .on('layeradd layerremove', ({layer, type}) => {
+ let node = layer.getElement && layer.getElement();
+ _.toggleClass(node, this.options.polyline.className + ' ' + this.options.theme, type == 'layeradd');
+ _.toggleEvent(layer, "mousemove", this._onMouseMoveLayer.bind(this), type == 'layeradd')
+ _.toggleEvent(layer, "mouseout", this._onMouseOut.bind(this), type == 'layeradd');
+ });
+ },
+
+ _initMarker(map) {
+ let pane = map.getPane('elevationPane');
+ if (!pane) {
+ pane = this._pane = map.createPane('elevationPane', map.getPane('norotatePane') || map.getPane('mapPane'));
+ pane.style.zIndex = 625; // This pane is above markers but below popups.
+ pane.style.pointerEvents = 'none';
+ }
+
+ if (this._renderer) this._renderer.remove()
+ this._renderer = L.svg({ pane: "elevationPane" }).addTo(this._map); // default leaflet svg renderer
+
+ this.import([this.__D3, this.__LMARKER])
+ .then((m) => {
+ this._marker = new (m[1] || Elevation).Marker(this.options, this);
+ this.fire("elechart_marker");
+ });
+ },
+
+ /**
+ * Inspired by L.Control.Layers
+ */
+ _initButton(container) {
+ L.DomEvent
+ .disableClickPropagation(container)
+ .disableScrollPropagation(container);
+
+ this.options.collapsed ? this._collapse() : this._expand();
+
+ if (this.options.autohide) {
+ _.on(container, 'mouseover', this._expand, this);
+ _.on(container, 'mouseout', this._collapse, this);
+ this._map.on('click', this._collapse, this);
+ }
+
+ if (this.options.closeBtn) {
+ let link = this._button = _.create('a', "elevation-toggle-icon", { href: '#', title: L._('Elevation'), }, container);
+ _.on(link, 'click', L.DomEvent.stop);
+ _.on(link, 'click', this._toggle, this);
+ _.on(link, 'focus', this._toggle, this);
+ fetch(_.resolveURL(this.__btnIcon, this.options.srcFolder)).then(r => r.ok && r.text().then(img => link.innerHTML = img));
+ }
+ },
+
+ _initSummary(container) {
+ this.import(this.__LSUMMARY).then((m)=>{
+ this._summary = new (m || Elevation).Summary({ summary: this.options.summary }, this);
+
+ this.on('elechart_init', () => {
+ d3.select(container).call(this._summary.render());
+ });
+ });
+ },
+
+ /**
+ * Retrieve data from a remote url (HTTP).
+ */
+ _loadFile(url) {
+ fetch(url)
+ .then((response) => response.text())
+ .then((data) => {
+ this._downloadURL = url; // TODO: handle multiple urls?
+ this._parseFromString(data)
+ .then( geojson => geojson && this._loadLayer(geojson));
+ }).catch((err) => console.warn(err));
+ },
+
+ /**
+ * Dynamically import only required javascript modules (code splitting)
+ */
+ _loadModules(handlers) {
+ // First map known classnames (eg. "Altitude" --> L.Control.Elevation.Altitude)
+ handlers = handlers.map((h) => typeof h === 'string' && typeof Elevation[h] !== "undefined" ? Elevation[h] : h);
+ // Then load optional classes and custom imports (eg. "Cadence" --> import('../src/handlers/cadence.js'))
+ let modules = handlers.map(file => (typeof file === 'string' && this.import(this.__modulesFolder + file.toLowerCase() + '.js')) || (file instanceof Promise && file) || Promise.resolve());
+ return Promise.all(modules).then((m) => {
+ _.each(m, (exported, i) => {
+ let fn = exported && Object.keys(exported)[0];
+ if (fn) {
+ handlers[i] = Elevation[fn] = (Elevation[fn] ?? exported[fn]);
+ }
+ });
+ _.each(handlers, h => ["function", "object"].includes(typeof h) && this._registerHandler(h));
+ });
+ },
+
+ /**
+ * Simple GeoJSON data loader (L.GeoJSON).
+ */
+ _loadLayer(geojson) {
+ let { polyline, theme, waypoints, wptIcons, wptLabels, distanceMarkers } = this.options;
+ let style = L.extend({}, polyline);
+
+ if (theme) {
+ style.className += ' ' + theme;
+ }
+
+ if (geojson.name) {
+ this.track_info.name = geojson.name;
+ }
+
+ let layer = L.geoJson(geojson, {
+ distanceMarkers: distanceMarkers,
+ style: style,
+ pointToLayer: (feature, latlng) => {
+ if (waypoints) {
+ let { desc, name, sym } = feature.properties;
+ desc = desc || '';
+ name = name || '';
+ // Handle chart waypoints (dots)
+ if ([true, 'dots'].includes(waypoints)) {
+ this._registerCheckPoint({
+ latlng: latlng,
+ label : ([true, 'dots'].includes(wptLabels) ? name : '')
+ });
+ }
+ // Handle map waypoints (markers)
+ if ([true, 'markers'].includes(waypoints) && wptIcons != false) {
+ return this._registerMarker({
+ latlng : latlng,
+ sym : (sym ?? name).replace(' ', '-').replace('"', '').replace("'", '').toLowerCase(),
+ content: [true, 'markers'].includes(wptLabels) && (name || desc) && decodeURI("<b>" + name + "</b>" + (desc.length > 0 ? '<br>' + desc : ''))
+ });
+ }
+ }
+ },
+ onEachFeature: (feature, layer) => feature.geometry && feature.geometry.type != 'Point' && this.addData(feature, layer),
+ });
+
+ this.import(this.__D3).then(() => {
+ this._initMapIntegrations(layer);
+ const event_data = { data: geojson, layer: layer, name: this.track_info.name, track_info: this.track_info };
+ if (this._modulesLoaded) {
+ this._fireEvt("eledata_loaded", event_data);
+ } else {
+ this.once('modules_loaded', () => this._fireEvt("eledata_loaded", event_data));
+ }
+ });
+
+ return layer;
+ },
+
+ _onDragEnd({ dragstart, dragend}) {
+ this._hideMarker();
+ this.fitBounds(L.latLngBounds([dragstart.latlng, dragend.latlng]));
+
+ this.fire("elechart_dragged");
+ },
+
+ _onKeyDown({key}) {
+ if (!this.options.detached && key === "Escape"){
+ this._collapse()
+ };
+ },
+
+ /**
+ * Trigger mouseenter event.
+ */
+ _onMouseEnter() {
+ this.fire('elechart_enter');
+ },
+
+ /*
+ * Handles the moueseover the chart and displays distance and altitude level.
+ */
+ _onMouseMove({xCoord}) {
+ if (this._chartEnabled && this._data.length) {
+ let item = this._findItemForX(xCoord);
+ if (item) {
+ if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord);
+
+ this._updateMarker(item);
+ this._setMapView(item);
+
+ if (this._map) {
+ _.addClass(this._map.getContainer(), 'elechart-hover');
+ }
+
+ this.fire("elechart_change", { data: item, xCoord: xCoord });
+ this.fire("elechart_hover", { data: item, xCoord: xCoord });
+ }
+ }
+ },
+
+ /*
+ * Handles mouseover events of the data layers on the map.
+ */
+ _onMouseMoveLayer({latlng}) {
+ if (this._data.length) {
+ let item = this._findItemForLatLng(latlng);
+ if (item) {
+ let xCoord = item.xDiagCoord;
+
+ if (this._chartEnabled) this._chart._showDiagramIndicator(item, xCoord);
+
+ this._updateMarker(item);
+
+ this.fire("elechart_change", { data: item, xCoord: xCoord });
+ }
+ }
+ },
+
+ /*
+ * Handles the moueseout over the chart.
+ */
+ _onMouseOut() {
+ if (!this.options.detached) {
+ this._hideMarker();
+ this._chart._hideDiagramIndicator();
+ }
+
+ if (this._map) {
+ _.removeClass(this._map.getContainer(), 'elechart-hover');
+ }
+
+ this.fire("elechart_leave");
+ },
+
+ /**
+ * Handles the drag event over the ruler filter.
+ */
+ _onRulerFilter({coords}) {
+ this._updateMapSegments(coords);
+ },
+
+ /**
+ * Toggle chart data on legend click
+ */
+ _onToggleChart({ name, enabled }) {
+
+ this._chartEnabled = this._chart._hasActiveLayers();
+
+ // toggle layer visibility on empty chart
+ this._layers.eachLayer(layer => _.toggleClass(layer.getElement && layer.getElement(), this.options.polyline.className + ' ' + this.options.theme, this._chartEnabled));
+
+ // toggle option value (eg. altitude = { 'disabled' || 'enabled' })
+ this.options[name] = !enabled && this.options[name] == 'disabled' ? 'enabled' : 'disabled';
+
+ // remove marker on empty chart
+ if (!this._chartEnabled) {
+ this._chart._hideDiagramIndicator();
+ this._marker.remove();
+ }
+ },
+
+ /**
+ * Simple GeoJSON Parser
+ */
+ _parseFromGeoJSONString(data) {
+ try {
+ return JSON.parse(data);
+ } catch (e) { }
+ },
+
+ /**
+ * Attempt to parse raw response data (GeoJSON or XML > GeoJSON)
+ */
+ _parseFromString(data) {
+ return new Promise(resolve =>
+ this.import(this.__TOGEOJSON).then(() => {
+ let geojson;
+ try {
+ geojson = this._parseFromXMLString(data.trim());
+ } catch (e) {
+ geojson = this._parseFromGeoJSONString(data.toString());
+ }
+ if (geojson) {
+ geojson.name = geojson.name || (this._downloadURL || '').split('/').pop().split('#')[0].split('?')[0];
+ }
+ resolve(geojson);
+ })
+ );
+ },
+
+ /**
+ * Simple XML Parser (GPX, KML, TCX)
+ */
+ _parseFromXMLString(data) {
+ if (data.indexOf("<") != 0) {
+ throw 'Invalid XML';
+ }
+ let xml = (new DOMParser()).parseFromString(data, "text/xml");
+ let type = xml.documentElement.tagName.toLowerCase(); // "kml" or "gpx"
+ let name = xml.getElementsByTagName('name');
+ if (xml.getElementsByTagName('parsererror').length) {
+ throw 'Invalid XML';
+ }
+ if (!(type in toGeoJSON)) {
+ type = xml.documentElement.tagName == "TrainingCenterDatabase" ? 'tcx' : 'gpx';
+ }
+ let geojson = toGeoJSON[type](xml);
+ geojson.name = name.length > 0 ? (Array.from(name).find(tag => tag.parentElement.tagName == "trk") ?? name[0]).textContent : '';
+ return geojson;
+ },
+
+ /**
+ * Add chart profile to diagram
+ */
+ _registerAreaPath(props) {
+ this.on("elechart_init", () => this._chart._registerAreaPath(props));
+ },
+
+ /**
+ * Add chart grid to diagram
+ */
+ _registerAxisGrid(props) {
+ this.on("elechart_axis", () => this._chart._registerAxisGrid(props));
+ },
+
+ /**
+ * Add chart axis to diagram
+ */
+ _registerAxisScale(props) {
+ this.on("elechart_axis", () => this._chart._registerAxisScale(props));
+ },
+
+ /**
+ * Add a point of interest over the diagram
+ */
+ _registerCheckPoint(props) {
+ const cb = () => this._chart._registerCheckPoint(props);
+ this
+ .on("elechart_updated", cb)
+ .once("eledata_clear", () => this.off("elechart_updated", cb));
+ },
+
+ /**
+ * Base handler for iterative track statistics (dist, time, z, slope, speed, acceleration, ...)
+ */
+ _registerDataAttribute(props) {
+
+ // parse of "coordinateProperties" for later usage
+ if (props.coordPropsToMeta) {
+ this.on("elepoint_init", (e) => props.coordPropsToMeta.call(this, e));
+ }
+
+ // prevent excessive variabile instanstations
+ let i, curr, prev, attr = props.attr || props.name;
+
+ // save here a reference to last used point
+ let lastValid = {};
+
+ // iteration
+ this.on("elepoint_added", ({index, point}) => {
+ i = index;
+
+ prev = curr ?? this._data[i]; // same as: this._data[i > 0 ? i - 1 : i]
+ curr = this._data[i];
+
+ // retrieve point value
+ curr[attr] = props.pointToAttr.call(this, point, i);
+
+ // check and fix missing data on last added point
+ if (i > 0 && isNaN(prev[attr])) {
+ if (!isNaN(lastValid[attr]) && !isNaN(curr[attr])) {
+ prev[attr] = (lastValid[attr] + curr[attr]) / 2;
+ } else if (!isNaN(lastValid[attr])) {
+ prev[attr] = lastValid[attr];
+ } else if (!isNaN(curr[attr])) {
+ prev[attr] = curr[attr];
+ }
+ // update "yAttr" and "xAttr"
+ if (props.meta) {
+ prev[props.meta] = prev[attr];
+ }
+ }
+
+ // skip to next iteration for invalid or missing data (eg. i == 0)
+ if (isNaN(curr[attr])) {
+ return;
+ }
+
+ // update reference to last used point
+ lastValid[attr] = curr[attr];
+
+ // Limit "crazy" delta values.
+ if (props.deltaMax) {
+ curr[attr] =_.wrapDelta(curr[attr], prev[attr], props.deltaMax);
+ }
+
+ // Range of acceptable values.
+ if (props.clampRange) {
+ curr[attr] = _.clamp(curr[attr], props.clampRange);
+ }
+
+ // Limit floating point precision.
+ if (!isNaN(props.decimals)) {
+ curr[attr] = _.round(curr[attr], props.decimals);
+ }
+
+ // update "track_info" stats (min, max, avg, ...)
+ if (props.stats) {
+ for (const key in props.stats) {
+ let sname = (props.statsName || attr) + (key != '' ? '_' : '');
+ this.track_info[sname + key] = props.stats[key].call(this, curr[attr], this.track_info[sname + key], this._data.length);
+ }
+ }
+
+ // update here some mixins (eg. complex "track_info" stuff)
+ if (props.onPointAdded) props.onPointAdded.call(this, curr[attr], i, point);
+ });
+ },
+
+ /**
+ * Parse a module definition and attach related function listeners
+ */
+ _registerHandler(props) {
+
+ // eg. L.Control.Altitude
+ if (typeof props === "function") {
+ return this._registerHandler(props.call(this));
+ }
+
+ let {
+ name,
+ attr,
+ required,
+ deltaMax,
+ clampRange,
+ decimals,
+ meta,
+ unit,
+ coordinateProperties,
+ coordPropsToMeta,
+ pointToAttr,
+ onPointAdded,
+ stats,
+ statsName,
+ grid,
+ scale,
+ path,
+ tooltip,
+ summary
+ } = props;
+
+ // eg. "altitude" == true
+ if (this.options[name] || required) {
+
+ this._registerDataAttribute({
+ name,
+ attr,
+ meta,
+ deltaMax,
+ clampRange,
+ decimals,
+ coordPropsToMeta: _.coordPropsToMeta(coordinateProperties, meta || name, coordPropsToMeta),
+ pointToAttr,
+ onPointAdded,
+ stats,
+ statsName,
+ });
+
+ if (grid) {
+ this._registerAxisGrid(L.extend({ name }, grid));
+ }
+
+ if (this.options[name] !== "summary") {
+ if (scale) this._registerAxisScale(L.extend({ name, label: unit }, scale));
+ if (path) this._registerAreaPath(L.extend({ name }, path));
+ }
+
+ if (tooltip || props.tooltips) {
+ _.each([tooltip, ...(props.tooltips || [])], t => t && this._registerTooltip(L.extend({ name }, t)));
+ }
+
+ if (summary) {
+ _.each(summary, (s, k) => summary[k] = L.extend({ unit }, s));
+ this._registerSummary(summary);
+ }
+ }
+ },
+
+ _registerMarker({latlng, sym, content}) {
+ let { wptIcons } = this.options;
+ // generate and cache appropriate icon symbol
+ if (!wptIcons.hasOwnProperty(sym)) {
+ wptIcons[sym] = L.divIcon(L.extend({}, wptIcons[""].options, { html: '<i class="elevation-waypoint-icon ' + sym + '"></i>' } ));
+ }
+ let marker = L.marker(latlng, { icon: wptIcons[sym] });
+ if (content) {
+ marker.bindPopup(content, { className: 'elevation-popup', keepInView: true }).openPopup();
+ marker.bindTooltip(content, { className: 'elevation-tooltip', direction: 'auto', sticky: true, opacity: 1 }).openTooltip();
+ }
+ return this._addMarker(marker)
+ },
+
+ /**
+ * Add chart or marker tooltip info
+ */
+ _registerTooltip(props) {
+ props.chart && this.on("elechart_init", () => this._chart._registerTooltip(L.extend({}, props, { value: props.chart })));
+ props.marker && this.on("elechart_marker", () => this._marker._registerTooltip(L.extend({}, props, { value: props.marker })));
+ },
+
+ /**
+ * Add summary info to diagram
+ */
+ _registerSummary(props) {
+ this.on('elechart_summary', () => this._summary._registerSummary(props));
+ },
+
+ /*
+ * Removes the drag rectangle and zoms back to the total extent of the data.
+ */
+ _resetDrag() {
+ this._chart._resetDrag();
+ this._hideMarker();
+ },
+
+ /**
+ * Resets drag, marker and bounds.
+ */
+ _resetView() {
+ if (this._map && this._map._isFullscreen) return;
+ this._resetDrag();
+ this._hideMarker();
+ if (this.options.autofitBounds) {
+ this.fitBounds();
+ }
+ },
+
+ /**
+ * Hacky way for handling chart resize. Deletes it and redraw chart.
+ */
+ _resizeChart() {
+ if (this._container && _.style(this._container, "display") != "none") {
+ let opts = this.options;
+ let newWidth = opts.detached ? (this.eleDiv || this._container).offsetWidth : _.clamp(opts._maxWidth, [0, this._map.getContainer().clientWidth - 30]);
+ if (newWidth) {
+ opts.width = newWidth;
+ if (this._chart && this._chart._chart) {
+ this._chart._chart._resize(opts);
+ this._updateChart();
+ }
+ }
+ this._updateMapSegments();
+ }
+ },
+
+ /**
+ * Collapse or Expand chart control.
+ */
+ _toggle() {
+ _.hasClass(this._container, "elevation-expanded") ? this._collapse() : this._expand();
+ },
+
+ /**
+ * Update map center and zoom (followMarker: true)
+ */
+ _setMapView(item) {
+ if (this._map && this.options.followMarker) {
+ let zoom = this._map.getZoom();
+ let z = this.options.zFollow;
+ if (typeof z === "number") {
+ this._map.setView(item.latlng, (zoom < z ? z : zoom), { animate: true, duration: 0.25 });
+ } else if (!this._map.getBounds().contains(item.latlng)) {
+ this._map.setView(item.latlng, zoom, { animate: true, duration: 0.25 });
+ }
+ }
+ },
+
+ /**
+ * Calculates [x, y] domain and then update chart.
+ */
+ _updateChart() {
+ if (this._chart && this._container) {
+ this.fire("elechart_axis");
+
+ this._chart.update({ data: this._data, options: this.options });
+
+ this._x = this._chart._x;
+ this._y = this._chart._y;
+
+ this.fire('elechart_updated');
+ }
+ },
+
+ /*
+ * Update the position/height indicator marker drawn onto the map
+ */
+ _updateMarker(item) {
+ if (this._marker) {
+ this._marker.update({
+ map : this._map,
+ item : item,
+ yCoordMax : this._yCoordMax || 0,
+ options : this.options
+ });
+ }
+ },
+
+ /**
+ * Fix marker rotation on rotated maps
+ */
+ _rotateMarker() {
+ if (this._marker) {
+ this._marker.update();
+ }
+ },
+
+ /**
+ * Highlight track segments on the map.
+ */
+ _updateMapSegments(coords) {
+ this._markedSegments.setLatLngs(coords || []);
+ if (coords && this._map && !this._map.hasLayer(this._markedSegments)) {
+ this._markedSegments.addTo(this._map);
+ }
+ },
+
+ /**
+ * Update chart summary.
+ */
+ _updateSummary() {
+ if (this._summary) {
+ this._summary.reset();
+ if (this.options.summary) {
+ this.fire("elechart_summary");
+ this._summary.update();
+ }
+ if (this.options.downloadLink && this._downloadURL) { // TODO: generate dynamically file content instead of using static file urls.
+ this._summary._container.innerHTML += '<span class="download"><a href="#">' + L._('Download') + '</a></span>'
+ _.select('.download a', this._summary._container).onclick = (e) => {
+ e.preventDefault();
+ let event = { downloadLink: this.options.downloadLink, confirm: _.saveFile.bind(this, this._downloadURL) };
+ if (this.options.downloadLink == 'modal' && typeof CustomEvent === "function") {
+ document.dispatchEvent(new CustomEvent("eletrack_download", { detail: event }));
+ } else if (this.options.downloadLink == 'link' || this.options.downloadLink === true) {
+ event.confirm();
+ }
+ this.fire('eletrack_download', event);
+ };
+ }
+ }
+ },
+
+
+ /**
+ * Calculates chart width.
+ */
+ _width() {
+ if (this._chart) return 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;
+ },
+
+});
diff --git a/assets/static/vendor/leaflet-elevation/src/handlers/acceleration.js b/assets/static/vendor/leaflet-elevation/src/handlers/acceleration.js
new file mode 100644
index 0000000..9b31b5c
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/handlers/acceleration.js
@@ -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) + '&nbsp;' + unit,
+ order: 60
+ },
+ "maxacceleration" : {
+ label: "Max Acceleration: ",
+ value: (track, unit) => Math.round(track.acceleration_max || 0) + '&nbsp;' + unit,
+ order: 61
+ },
+ "avgacceleration": {
+ label: "Avg Acceleration: ",
+ value: (track, unit) => Math.round(track.acceleration_avg || 0) + '&nbsp;' + unit,
+ order: 62
+ },
+ }
+ };
+}
diff --git a/assets/static/vendor/leaflet-elevation/src/handlers/altitude.js b/assets/static/vendor/leaflet-elevation/src/handlers/altitude.js
new file mode 100644
index 0000000..b7fe6e3
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/handlers/altitude.js
@@ -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) + '&nbsp;' + unit,
+ order: 30,
+ },
+ "maxele" : {
+ label: "Max Elevation: ",
+ value: (track, unit) => (track.elevation_max || 0).toFixed(2) + '&nbsp;' + unit,
+ order: 31,
+ },
+ "avgele" : {
+ label: "Avg Elevation: ",
+ value: (track, unit) => (track.elevation_avg || 0).toFixed(2) + '&nbsp;' + unit,
+ order: 32,
+ },
+ }
+ };
+}
diff --git a/assets/static/vendor/leaflet-elevation/src/handlers/cadence.js b/assets/static/vendor/leaflet-elevation/src/handlers/cadence.js
new file mode 100644
index 0000000..0ad7704
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/handlers/cadence.js
@@ -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) + '&nbsp;' + unit,
+ // order: 30
+ },
+ "maxrpm": {
+ label: "Max RPM: ",
+ value: (track, unit) => Math.round(track.cadence_max || 0) + '&nbsp;' + unit,
+ // order: 30
+ },
+ "avgrpm": {
+ label: "Avg RPM: ",
+ value: (track, unit) => Math.round(track.cadence_avg || 0) + '&nbsp;' + unit,
+ // order: 20
+ },
+ }
+ };
+} \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/src/handlers/distance.js b/assets/static/vendor/leaflet-elevation/src/handlers/distance.js
new file mode 100644
index 0000000..fa6fa1d
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/handlers/distance.js
@@ -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) + '&nbsp;' + distance.label,
+ order: 10
+ }
+ }
+ };
+}
diff --git a/assets/static/vendor/leaflet-elevation/src/handlers/heart.js b/assets/static/vendor/leaflet-elevation/src/handlers/heart.js
new file mode 100644
index 0000000..1930f60
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/handlers/heart.js
@@ -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) + '&nbsp;' + unit,
+ // order: 30
+ },
+ "maxbpm": {
+ label: "Max BPM: ",
+ value: (track, unit) => Math.round(track.heart_max || 0) + '&nbsp;' + unit,
+ // order: 30
+ },
+ "avgbpm": {
+ label: "Avg BPM: ",
+ value: (track, unit) => Math.round(track.heart_avg || 0) + '&nbsp;' + unit,
+ // order: 20
+ },
+ }
+ };
+}; \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/src/handlers/labels.js b/assets/static/vendor/leaflet-elevation/src/handlers/labels.js
new file mode 100644
index 0000000..31b3cd6
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/handlers/labels.js
@@ -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 { };
+}
diff --git a/assets/static/vendor/leaflet-elevation/src/handlers/lineargradient.js b/assets/static/vendor/leaflet-elevation/src/handlers/lineargradient.js
new file mode 100644
index 0000000..90e33ce
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/handlers/lineargradient.js
@@ -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 { };
+} \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/src/handlers/pace.js b/assets/static/vendor/leaflet-elevation/src/handlers/pace.js
new file mode 100644
index 0000000..9017f39
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/handlers/pace.js
@@ -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) + '&nbsp;' + unit,
+ order: 51
+ },
+ "maxpace" : {
+ label: "Max Pace: ",
+ value: (track, unit) => Math.round(track.pace_max || 0) + '&nbsp;' + unit,
+ order: 51
+ },
+ "avgpace": {
+ label: "Avg Pace: ",
+ value: (track, unit) => Math.round(track.pace_avg || 0) + '&nbsp;' + unit,
+ order: 52
+ },
+ }
+ };
+}
diff --git a/assets/static/vendor/leaflet-elevation/src/handlers/runner.js b/assets/static/vendor/leaflet-elevation/src/handlers/runner.js
new file mode 100644
index 0000000..30dd80a
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/handlers/runner.js
@@ -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 {};
+}
diff --git a/assets/static/vendor/leaflet-elevation/src/handlers/slope.js b/assets/static/vendor/leaflet-elevation/src/handlers/slope.js
new file mode 100644
index 0000000..37291b3
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/handlers/slope.js
@@ -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) + '&nbsp;' + unit,
+ order: 40
+ },
+ "maxslope": {
+ label: "Max Slope: ",
+ value: (track, unit) => Math.round(track.slope_max || 0) + '&nbsp;' + unit,
+ order: 41
+ },
+ "avgslope": {
+ label: "Avg Slope: ",
+ value: (track, unit) => Math.round(track.slope_avg || 0) + '&nbsp;' + unit,
+ order: 42
+ },
+ "ascent" : {
+ label: "Total Ascent: ",
+ value: (track, unit) => Math.round(track.ascent || 0) + '&nbsp;' + (this.options.imperial ? 'ft' : 'm'),
+ order: 43
+ },
+ "descent" : {
+ label: "Total Descent: ",
+ value: (track, unit) => Math.round(track.descent || 0) + '&nbsp;' + (this.options.imperial ? 'ft' : 'm'),
+ order: 45
+ },
+ }
+ };
+}
diff --git a/assets/static/vendor/leaflet-elevation/src/handlers/speed.js b/assets/static/vendor/leaflet-elevation/src/handlers/speed.js
new file mode 100644
index 0000000..534b340
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/handlers/speed.js
@@ -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) + '&nbsp;' + unit,
+ order: 51
+ },
+ "maxspeed" : {
+ label: "Max Speed: ",
+ value: (track, unit) => Math.round(track.speed_max || 0) + '&nbsp;' + unit,
+ order: 51
+ },
+ "avgspeed": {
+ label: "Avg Speed: ",
+ value: (track, unit) => Math.round(track.speed_avg || 0) + '&nbsp;' + unit,
+ order: 52
+ },
+ }
+ };
+}
diff --git a/assets/static/vendor/leaflet-elevation/src/handlers/temperature.js b/assets/static/vendor/leaflet-elevation/src/handlers/temperature.js
new file mode 100644
index 0000000..cf05308
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/handlers/temperature.js
@@ -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) + '&nbsp;' + unit,
+ },
+ "maxtemp": {
+ label: "Max Temp: ",
+ value: (track, unit) => Math.round(track.temperature_max || 0) + '&nbsp;' + unit,
+ },
+ "avgtemp": {
+ label: "Avg Temp: ",
+ value: (track, unit) => Math.round(track.temperature_avg || 0) + '&nbsp;' + unit,
+ },
+ }
+ };
+}
diff --git a/assets/static/vendor/leaflet-elevation/src/handlers/time.js b/assets/static/vendor/leaflet-elevation/src/handlers/time.js
new file mode 100644
index 0000000..d94b136
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/handlers/time.js
@@ -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
+ }
+ }
+ };
+} \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/src/index.css b/assets/static/vendor/leaflet-elevation/src/index.css
new file mode 100644
index 0000000..9b29174
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/index.css
@@ -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;
+}
diff --git a/assets/static/vendor/leaflet-elevation/src/index.js b/assets/static/vendor/leaflet-elevation/src/index.js
new file mode 100644
index 0000000..4e094ff
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/index.js
@@ -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);
diff --git a/assets/static/vendor/leaflet-elevation/src/options.js b/assets/static/vendor/leaflet-elevation/src/options.js
new file mode 100644
index 0000000..5923163
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/options.js
@@ -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("/") + '/',
+};
diff --git a/assets/static/vendor/leaflet-elevation/src/utils.js b/assets/static/vendor/leaflet-elevation/src/utils.js
new file mode 100644
index 0000000..90035f7
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/utils.js
@@ -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;
+ }
+} \ No newline at end of file
diff --git a/assets/static/vendor/leaflet-elevation/src/utils.spec.js b/assets/static/vendor/leaflet-elevation/src/utils.spec.js
new file mode 100644
index 0000000..7371a96
--- /dev/null
+++ b/assets/static/vendor/leaflet-elevation/src/utils.spec.js
@@ -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(); \ No newline at end of file