diff --git a/web/package-lock.json b/web/package-lock.json
index c4457dbe..34c71143 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "wanderer",
- "version": "0.10.1",
+ "version": "0.11.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wanderer",
- "version": "0.10.1",
+ "version": "0.11.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.1",
"@sveltejs/adapter-node": "^4.0.1",
@@ -18,6 +18,8 @@
"@types/xmldom": "^0.1.34",
"canvg": "^4.0.1",
"chart.js": "^4.4.6",
+ "chartjs-plugin-crosshair": "^2.0.0",
+ "chartjs-plugin-zoom": "^2.1.0",
"crypto-random-string": "^5.0.0",
"heic2any": "^0.0.4",
"instead": "^1.0.3",
@@ -27,6 +29,7 @@
"leaflet": "^1.9.4",
"leaflet-gpx": "^1.7.0",
"leaflet.awesome-markers": "^2.0.5",
+ "maplibre-gl": "^4.7.1",
"meilisearch": "^0.37.0",
"nouislider": "^15.7.1",
"pdfkit": "^0.15.0",
@@ -568,6 +571,92 @@
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
},
+ "node_modules/@mapbox/geojson-rewind": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
+ "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
+ "dependencies": {
+ "get-stream": "^6.0.1",
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "geojson-rewind": "geojson-rewind"
+ }
+ },
+ "node_modules/@mapbox/geojson-rewind/node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@mapbox/jsonlint-lines-primitives": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
+ "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@mapbox/point-geometry": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
+ "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ=="
+ },
+ "node_modules/@mapbox/tiny-sdf": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz",
+ "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA=="
+ },
+ "node_modules/@mapbox/unitbezier": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
+ "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw=="
+ },
+ "node_modules/@mapbox/vector-tile": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
+ "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
+ "dependencies": {
+ "@mapbox/point-geometry": "~0.1.0"
+ }
+ },
+ "node_modules/@mapbox/whoots-js": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
+ "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@maplibre/maplibre-gl-style-spec": {
+ "version": "20.4.0",
+ "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz",
+ "integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==",
+ "dependencies": {
+ "@mapbox/jsonlint-lines-primitives": "~2.0.2",
+ "@mapbox/unitbezier": "^0.0.1",
+ "json-stringify-pretty-compact": "^4.0.0",
+ "minimist": "^1.2.8",
+ "quickselect": "^2.0.0",
+ "rw": "^1.3.3",
+ "tinyqueue": "^3.0.0"
+ },
+ "bin": {
+ "gl-style-format": "dist/gl-style-format.mjs",
+ "gl-style-migrate": "dist/gl-style-migrate.mjs",
+ "gl-style-validate": "dist/gl-style-validate.mjs"
+ }
+ },
+ "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/quickselect": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
+ "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw=="
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1089,9 +1178,22 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
},
"node_modules/@types/geojson": {
- "version": "7946.0.13",
- "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.13.tgz",
- "integrity": "sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ=="
+ "version": "7946.0.14",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz",
+ "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg=="
+ },
+ "node_modules/@types/geojson-vt": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
+ "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/hammerjs": {
+ "version": "2.0.46",
+ "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz",
+ "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw=="
},
"node_modules/@types/leaflet": {
"version": "1.9.8",
@@ -1118,6 +1220,21 @@
"@types/leaflet": "*"
}
},
+ "node_modules/@types/mapbox__point-geometry": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
+ "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA=="
+ },
+ "node_modules/@types/mapbox__vector-tile": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
+ "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
+ "dependencies": {
+ "@types/geojson": "*",
+ "@types/mapbox__point-geometry": "*",
+ "@types/pbf": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "20.11.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz",
@@ -1131,6 +1248,11 @@
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="
},
+ "node_modules/@types/pbf": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
+ "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA=="
+ },
"node_modules/@types/pug": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz",
@@ -1152,6 +1274,14 @@
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz",
"integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ=="
},
+ "node_modules/@types/supercluster": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
+ "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
"node_modules/@types/three": {
"version": "0.161.2",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.161.2.tgz",
@@ -1704,6 +1834,26 @@
"pnpm": ">=8"
}
},
+ "node_modules/chartjs-plugin-crosshair": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chartjs-plugin-crosshair/-/chartjs-plugin-crosshair-2.0.0.tgz",
+ "integrity": "sha512-Vs6strRvYpzL92v40r/rAWhh+7au8VnfyC/m2ZHXcXeLEWeFjDgMvXjWdX+eARhbgHQo+uGIKyq9fmkS79GIyQ==",
+ "peerDependencies": {
+ "chart.js": "^4.0.1"
+ }
+ },
+ "node_modules/chartjs-plugin-zoom": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.1.0.tgz",
+ "integrity": "sha512-7lMimfQCUaIJLhPJaWSAA4gw+1m8lyR3Wn+M3MxjHbM/XxRUnOxN7cM5RR9jUmxmyW0h7L2hZ8KhvUsqrFxy/Q==",
+ "dependencies": {
+ "@types/hammerjs": "^2.0.45",
+ "hammerjs": "^2.0.8"
+ },
+ "peerDependencies": {
+ "chart.js": ">=3.2.0"
+ }
+ },
"node_modules/check-error": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
@@ -2263,6 +2413,11 @@
"integrity": "sha512-5RXhAXSCrKTqt9pSbobT9PVRX+oPpENplTZqCiK1l0ya+ZOzwo9kqsGLbYRsAhzIiLCwKEy99XKSSrqnRTLVcw==",
"optional": true
},
+ "node_modules/earcut": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.0.tgz",
+ "integrity": "sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg=="
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -2669,6 +2824,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/geojson-vt": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz",
+ "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A=="
+ },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -2716,6 +2876,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/gl-matrix": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
+ "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
+ },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -2748,6 +2913,41 @@
"node": ">= 6"
}
},
+ "node_modules/global-prefix": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz",
+ "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==",
+ "dependencies": {
+ "ini": "^4.1.3",
+ "kind-of": "^6.0.3",
+ "which": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/global-prefix/node_modules/isexe": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+ "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/global-prefix/node_modules/which": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+ "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
+ "dependencies": {
+ "isexe": "^3.1.1"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^16.13.0 || >=18.0.0"
+ }
+ },
"node_modules/globalyzer": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz",
@@ -2775,6 +2975,14 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true
},
+ "node_modules/hammerjs": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
+ "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/has-bigints": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@@ -2926,6 +3134,25 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
@@ -2970,6 +3197,14 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
+ "node_modules/ini": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
+ "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
"node_modules/instead": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/instead/-/instead-1.0.3.tgz",
@@ -3457,6 +3692,11 @@
"node": ">=18"
}
},
+ "node_modules/json-stringify-pretty-compact": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
+ "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q=="
+ },
"node_modules/jsonc-parser": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
@@ -3526,6 +3766,19 @@
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
+ "node_modules/kdbush": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
+ "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -3658,6 +3911,46 @@
"node": ">=12"
}
},
+ "node_modules/maplibre-gl": {
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz",
+ "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==",
+ "dependencies": {
+ "@mapbox/geojson-rewind": "^0.5.2",
+ "@mapbox/jsonlint-lines-primitives": "^2.0.2",
+ "@mapbox/point-geometry": "^0.1.0",
+ "@mapbox/tiny-sdf": "^2.0.6",
+ "@mapbox/unitbezier": "^0.0.1",
+ "@mapbox/vector-tile": "^1.3.1",
+ "@mapbox/whoots-js": "^3.1.0",
+ "@maplibre/maplibre-gl-style-spec": "^20.3.1",
+ "@types/geojson": "^7946.0.14",
+ "@types/geojson-vt": "3.2.5",
+ "@types/mapbox__point-geometry": "^0.1.4",
+ "@types/mapbox__vector-tile": "^1.3.4",
+ "@types/pbf": "^3.0.5",
+ "@types/supercluster": "^7.1.3",
+ "earcut": "^3.0.0",
+ "geojson-vt": "^4.0.2",
+ "gl-matrix": "^3.4.3",
+ "global-prefix": "^4.0.0",
+ "kdbush": "^4.0.2",
+ "murmurhash-js": "^1.0.0",
+ "pbf": "^3.3.0",
+ "potpack": "^2.0.0",
+ "quickselect": "^3.0.0",
+ "supercluster": "^8.0.1",
+ "tinyqueue": "^3.0.0",
+ "vt-pbf": "^3.1.3"
+ },
+ "engines": {
+ "node": ">=16.14.0",
+ "npm": ">=8.1.0"
+ },
+ "funding": {
+ "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
+ }
+ },
"node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
@@ -3781,7 +4074,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -3853,6 +4145,11 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
+ "node_modules/murmurhash-js": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
+ "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw=="
+ },
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -4199,6 +4496,18 @@
"node": "*"
}
},
+ "node_modules/pbf": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
+ "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
+ "dependencies": {
+ "ieee754": "^1.1.12",
+ "resolve-protobuf-schema": "^2.1.0"
+ },
+ "bin": {
+ "pbf": "bin/pbf"
+ }
+ },
"node_modules/pdfkit": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.15.0.tgz",
@@ -4480,6 +4789,11 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
+ "node_modules/potpack": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz",
+ "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw=="
+ },
"node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
@@ -4504,6 +4818,11 @@
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
},
+ "node_modules/protocol-buffers-schema": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
+ "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
+ },
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -4568,6 +4887,11 @@
}
]
},
+ "node_modules/quickselect": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
+ "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="
+ },
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
@@ -4698,6 +5022,14 @@
"node": ">=4"
}
},
+ "node_modules/resolve-protobuf-schema": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
+ "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
+ "dependencies": {
+ "protocol-buffers-schema": "^3.3.1"
+ }
+ },
"node_modules/restructure": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz",
@@ -4795,6 +5127,11 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
+ },
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
@@ -5226,6 +5563,14 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/supercluster": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
+ "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
+ "dependencies": {
+ "kdbush": "^4.0.2"
+ }
+ },
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -5545,6 +5890,11 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tinyqueue": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
+ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="
+ },
"node_modules/tinyspy": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz",
@@ -5941,6 +6291,16 @@
}
}
},
+ "node_modules/vt-pbf": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
+ "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
+ "dependencies": {
+ "@mapbox/point-geometry": "0.1.0",
+ "@mapbox/vector-tile": "^1.3.1",
+ "pbf": "^3.2.1"
+ }
+ },
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
diff --git a/web/package.json b/web/package.json
index b1188024..9fd3113b 100644
--- a/web/package.json
+++ b/web/package.json
@@ -42,6 +42,8 @@
"@types/xmldom": "^0.1.34",
"canvg": "^4.0.1",
"chart.js": "^4.4.6",
+ "chartjs-plugin-crosshair": "^2.0.0",
+ "chartjs-plugin-zoom": "^2.1.0",
"crypto-random-string": "^5.0.0",
"heic2any": "^0.0.4",
"instead": "^1.0.3",
@@ -51,6 +53,7 @@
"leaflet": "^1.9.4",
"leaflet-gpx": "^1.7.0",
"leaflet.awesome-markers": "^2.0.5",
+ "maplibre-gl": "^4.7.1",
"meilisearch": "^0.37.0",
"nouislider": "^15.7.1",
"pdfkit": "^0.15.0",
diff --git a/web/src/css/theme.css b/web/src/css/theme.css
index 01515bf6..175c1747 100644
--- a/web/src/css/theme.css
+++ b/web/src/css/theme.css
@@ -4,7 +4,10 @@
--secondary-hover: 243, 244, 246;
--background: 255, 255, 255;
+ --background-inverse: 7, 10, 36;
+
--content: 0, 0, 0;
+ --content-inverse: 255, 255, 255;
--input-background: 249, 250, 251;
--input-background-error: 254, 242, 242;
@@ -30,7 +33,10 @@
--secondary-hover: 36, 39, 52;
--background: 7, 10, 36;
+ --background-inverse: 255, 255, 255;
+
--content: 255, 255, 255;
+ --content-inverse: 0, 0, 0;
--input-background: 36, 39, 52;
--input-background-error: 69, 10, 10;
diff --git a/web/src/lib/components/summit_log/summit_log_table.svelte b/web/src/lib/components/summit_log/summit_log_table.svelte
index 64fe25d6..5bb20b25 100644
--- a/web/src/lib/components/summit_log/summit_log_table.svelte
+++ b/web/src/lib/components/summit_log/summit_log_table.svelte
@@ -7,9 +7,9 @@
import type { Trail } from "$lib/models/trail";
import { gpx2trail } from "$lib/util/gpx_util";
- import type { Map } from "leaflet";
+ import * as M from "maplibre-gl";
import { _ } from "svelte-i18n";
- import MapWithElevation from "../trail/map_with_elevation.svelte";
+ import MapWithElevationMaplibre from "../trail/map_with_elevation_maplibre.svelte";
export let summitLogs: SummitLog[] = [];
export let showCategory: boolean = false;
@@ -22,7 +22,7 @@
let openTextModal: () => void;
let closeTextModal: () => void;
- let map: Map;
+ let map: M.Map;
let trail: Trail | null = null;
@@ -40,8 +40,6 @@
openMapModal();
await tick();
-
- map.invalidateSize();
return;
}
@@ -103,7 +101,10 @@
bind:closeModal={closeMapModal}
>
-
+ {#if trail}
+
+ {/if}
+ import { page } from "$app/stores";
+ import type { Trail } from "$lib/models/trail";
+ import { theme } from "$lib/stores/theme_store";
+ import { findStartAndEndPoints } from "$lib/util/geojson_util";
+ import { toGeoJson } from "$lib/util/gpx_util";
+ import {
+ createMarkerFromWaypoint,
+ FontawesomeMarker,
+ } from "$lib/util/maplibre_util";
+ import type { ElevationProfileControl } from "$lib/vendor/maplibre-elevation-profile/elevationprofile-control";
+ import type { GeoJsonObject } from "geojson";
+ import * as M from "maplibre-gl";
+ import "maplibre-gl/dist/maplibre-gl.css";
+ import { onDestroy, onMount } from "svelte";
+
+ export let trail: Trail;
+ export let markers: M.Marker[] = [];
+ export let map: M.Map | null = null;
+ export let crosshairCursor: boolean = false;
+
+ let mapContainer: HTMLDivElement;
+
+ let epc: ElevationProfileControl;
+
+ $: data = toGeoJson(trail.expand.gpx_data!) as GeoJsonObject;
+
+ $: if ($theme == "dark") {
+ epc?.toggleTheme({
+ profileBackgroundColor: "#191b24",
+ elevationGridColor: "#ddd2",
+ labelColor: "#ddd8",
+ crosshairColor: "#fff5",
+ });
+ } else {
+ epc?.toggleTheme({
+ profileBackgroundColor: "#242734",
+ elevationGridColor: "#0002",
+ labelColor: "#0009",
+ crosshairColor: "#0005",
+ });
+ }
+
+ onMount(async () => {
+ const initialState = {
+ lng: 0,
+ lat: 0,
+ zoom: 14,
+ };
+ const ElevationProfileControl = (
+ await import(
+ "$lib/vendor/maplibre-elevation-profile/elevationprofile-control"
+ )
+ ).ElevationProfileControl;
+
+ map = new M.Map({
+ container: mapContainer,
+ style: `https://basemaps.cartocdn.com/gl/positron-gl-style/style.json`,
+ center: [initialState.lng, initialState.lat],
+ zoom: initialState.zoom,
+ });
+
+ for (const waypoint of trail?.expand.waypoints ?? []) {
+ const marker = createMarkerFromWaypoint(waypoint);
+ marker.addTo(map);
+ markers.push(marker);
+ }
+
+ const startEndPoint = findStartAndEndPoints(data);
+
+ const startMarker = new FontawesomeMarker(
+ { icon: "fa fa-bullseye" },
+ {},
+ )
+ .setLngLat(startEndPoint[0] as M.LngLatLike)
+ .addTo(map);
+
+ const endMarker = new FontawesomeMarker(
+ { icon: "fa fa-flag-checkered" },
+ {},
+ )
+ .setLngLat(startEndPoint[1] as M.LngLatLike)
+ .addTo(map);
+
+ const elevationMarker = new FontawesomeMarker(
+ {
+ icon: "fa-regular fa-circle",
+ fontSize: "xs",
+ width: 4,
+ backgroundColor: "primary",
+ fontColor: "white",
+ },
+ {},
+ );
+ elevationMarker.setLngLat([0, 0]).addTo(map);
+ elevationMarker.setOpacity("0");
+
+ epc = new ElevationProfileControl({
+ visible: true,
+ profileBackgroundColor: $theme == "light" ? "#242734" : "#191b24",
+ backgroundColor: "bg-menu-background/90",
+ unit: $page.data.settings?.unit ?? "metric",
+ profileLineWidth: 3,
+ displayDistanceGrid: true,
+ tooltipDisplayDPlus: false,
+ onEnter: () => {
+ elevationMarker.setOpacity("1");
+ },
+ onLeave: () => {
+ elevationMarker.setOpacity("0");
+ },
+ onMove: (data) => {
+ elevationMarker.setLngLat(data.position as M.LngLatLike);
+ },
+ });
+ map.addControl(new M.NavigationControl());
+ map.addControl(
+ new M.ScaleControl({
+ maxWidth: 120,
+ unit: $page.data.settings?.unit ?? "metric",
+ }),
+ "top-left",
+ );
+ map.addControl(epc);
+ epc.setData(data, trail.expand.waypoints);
+
+ map.on("load", () => {
+ map!.addSource("trail-source", {
+ type: "geojson",
+ data: data as any,
+ });
+
+ map!.addLayer({
+ id: "uploaded-polygons",
+ type: "line",
+ source: "trail-source",
+ paint: {
+ "line-color": "#648ad5",
+ "line-width": 5,
+ },
+ });
+ map!.fitBounds(data.bbox as any, {
+ animate: false,
+ padding: {
+ top: 16,
+ left: 16,
+ right: 16,
+ bottom: map!.getContainer().clientHeight * 0.3 + 16,
+ },
+ });
+ });
+ });
+
+ onDestroy(() => {
+ map?.remove();
+ });
+
+
+
+
+
diff --git a/web/src/lib/components/trail/trail_info_panel.svelte b/web/src/lib/components/trail/trail_info_panel.svelte
index 2dea8a60..8aba00e2 100644
--- a/web/src/lib/components/trail/trail_info_panel.svelte
+++ b/web/src/lib/components/trail/trail_info_panel.svelte
@@ -39,10 +39,11 @@
import ShareInfo from "../share_info.svelte";
import SummitLogTable from "../summit_log/summit_log_table.svelte";
import { pb } from "$lib/pocketbase";
+ import * as M from "maplibre-gl";
export let trail: Trail;
export let mode: "overview" | "map" | "list" = "map";
- export let markers: Marker[] = [];
+ export let markers: (Marker | M.Marker)[] = [];
const tabs = [
$_("description"),
@@ -118,11 +119,19 @@
});
function openMarkerPopup(i: number) {
- markers[i].openPopup();
+ if (typeof (markers[i] as Marker).openPopup === "function") {
+ (markers[i] as Marker).openPopup();
+ } else {
+ (markers[i] as M.Marker).togglePopup();
+ }
}
function closeMarkerPopup(i: number) {
- markers[i].closePopup();
+ if (typeof (markers[i] as Marker).closePopup === "function") {
+ (markers[i] as Marker).closePopup();
+ } else {
+ (markers[i] as M.Marker).togglePopup();
+ }
}
async function toggleMapFullScreen() {
@@ -239,7 +248,11 @@
`https://api.dicebear.com/7.x/initials/svg?seed=${trail.expand.author.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
- {trail.expand.author.username}
+ {trail.expand.author.username}
{/if}
@@ -358,7 +371,9 @@
{/if}
{#if activeTab == 3}
-
{/if}
diff --git a/web/src/lib/models/gpx/gpx.ts b/web/src/lib/models/gpx/gpx.ts
index 13a2a2c6..01dfa786 100644
--- a/web/src/lib/models/gpx/gpx.ts
+++ b/web/src/lib/models/gpx/gpx.ts
@@ -2,7 +2,7 @@ import * as xml2js from 'isomorphic-xml2js';
import Metadata from './metadata';
import Route from './route';
import Track from './track';
-import { allDatesToISOString, calculateDistance, removeEmpty } from './utils';
+import { allDatesToISOString, haversineDistance, removeEmpty } from './utils';
import Waypoint from './waypoint';
const defaultAttributes = {
@@ -104,7 +104,7 @@ export default class GPX {
totalElevationLoss += Math.abs(elevationDiff)
}
- const distance = calculateDistance(
+ const distance = haversineDistance(
prevPoint.$.lat ?? 0,
prevPoint.$.lon ?? 0,
point.$.lat ?? 0,
diff --git a/web/src/lib/models/gpx/utils.ts b/web/src/lib/models/gpx/utils.ts
index 55057b23..8ad8cadb 100644
--- a/web/src/lib/models/gpx/utils.ts
+++ b/web/src/lib/models/gpx/utils.ts
@@ -20,7 +20,7 @@ function allDatesToISOString(obj: Record
) {
});
}
-function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
+function haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371; // Radius of the Earth in km
const dLat = (lat2 - lat1) * (Math.PI / 180); // Convert degrees to radians
const dLon = (lon2 - lon1) * (Math.PI / 180);
@@ -33,4 +33,4 @@ function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: numbe
return distance;
}
-export { removeEmpty, allDatesToISOString, calculateDistance };
+export { removeEmpty, allDatesToISOString, haversineDistance };
diff --git a/web/src/lib/util/format_util.ts b/web/src/lib/util/format_util.ts
index ddd0134c..77bbc3ec 100644
--- a/web/src/lib/util/format_util.ts
+++ b/web/src/lib/util/format_util.ts
@@ -1,5 +1,4 @@
import { page } from "$app/stores";
-import { currentUser } from "$lib/stores/user_store";
import { get } from "svelte/store";
export function formatTimeHHMM(minutes?: number) {
diff --git a/web/src/lib/util/geojson_util.ts b/web/src/lib/util/geojson_util.ts
new file mode 100644
index 00000000..b7510d01
--- /dev/null
+++ b/web/src/lib/util/geojson_util.ts
@@ -0,0 +1,216 @@
+import type { BBox, Feature, FeatureCollection, GeoJSON, GeoJsonObject, GeometryCollection, Position } from "geojson";
+
+export function bbox(
+ geojson: GeoJSON,
+): BBox {
+ if (geojson.bbox != null) {
+ return geojson.bbox;
+ }
+ const result: BBox = [Infinity, Infinity, -Infinity, -Infinity];
+ let startPoint: Position, endPoint: Position;
+ coordEach(geojson, (coord) => {
+ startPoint ??= [coord[0], coord[1]];
+ endPoint = coord;
+ if (result[0] > coord[0]) {
+ result[0] = coord[0];
+ }
+ if (result[1] > coord[1]) {
+ result[1] = coord[1];
+ }
+ if (result[2] < coord[0]) {
+ result[2] = coord[0];
+ }
+ if (result[3] < coord[1]) {
+ result[3] = coord[1];
+ }
+ });
+
+ return result;
+}
+
+export function findStartAndEndPoints(geojson: GeoJsonObject): Position[] {
+ const startEndPoints: Position[] = [];
+
+ (geojson as FeatureCollection).features.forEach((feature) => {
+ const geometry = feature.geometry;
+
+ if (geometry.type === "LineString") {
+ const coords = geometry.coordinates as number[][];
+ const start: [number, number] = [coords[0][0], coords[0][1]]; // First point
+ const end: [number, number] = [
+ coords[coords.length - 1][0],
+ coords[coords.length - 1][1],
+ ]; // Last point
+ startEndPoints.push(start);
+ startEndPoints.push(end)
+ } else if (geometry.type === "MultiLineString") {
+ const coords = geometry.coordinates as number[][][];
+ const start: [number, number] = [
+ coords[0][0][0],
+ coords[0][0][1],
+ ]; // First point of the first line
+ const lastLine = coords[coords.length - 1];
+ const end: [number, number] = [
+ lastLine[lastLine.length - 1][0],
+ lastLine[lastLine.length - 1][1],
+ ]; // Last point of the last line
+ startEndPoints.push(start);
+ startEndPoints.push(end)
+ } else {
+ console.warn(
+ `Geometry type ${geometry.type} is not supported for start/end point extraction.`
+ );
+ }
+ });
+
+ return startEndPoints;
+}
+
+function coordEach(geojson: GeoJSON, callback: (
+ currentCoord: number[],
+ coordIndex: number,
+ featureIndex: number,
+ multiFeatureIndex: number,
+ geometryIndex: number
+) => void | false,
+ excludeWrapCoord?: boolean,) {
+ // Handles null Geometry -- Skips this GeoJSON
+ if (geojson === null) return;
+ var j,
+ k,
+ l,
+ geometry,
+ stopG,
+ coords,
+ geometryMaybeCollection,
+ wrapShrink = 0,
+ coordIndex = 0,
+ isGeometryCollection,
+ type = geojson.type,
+ isFeatureCollection = type === "FeatureCollection",
+ isFeature = type === "Feature",
+ stop = isFeatureCollection ? (geojson as FeatureCollection).features.length : 1;
+
+ for (var featureIndex = 0; featureIndex < stop; featureIndex++) {
+ geometryMaybeCollection = isFeatureCollection
+ ? (geojson as FeatureCollection).features[featureIndex].geometry
+ : isFeature
+ ? (geojson as Feature).geometry
+ : geojson;
+ isGeometryCollection = geometryMaybeCollection
+ ? geometryMaybeCollection.type === "GeometryCollection"
+ : false;
+ stopG = isGeometryCollection
+ ? (geometryMaybeCollection as GeometryCollection).geometries.length
+ : 1;
+
+ for (var geomIndex = 0; geomIndex < stopG; geomIndex++) {
+ var multiFeatureIndex = 0;
+ var geometryIndex = 0;
+ geometry = isGeometryCollection
+ ? (geometryMaybeCollection as GeometryCollection).geometries[geomIndex]
+ : geometryMaybeCollection;
+
+ // Handles null Geometry -- Skips this geometry
+ if (geometry === null) continue;
+ coords = (geometry as any).coordinates;
+ var geomType = geometry.type;
+
+ wrapShrink =
+ excludeWrapCoord &&
+ (geomType === "Polygon" || geomType === "MultiPolygon")
+ ? 1
+ : 0;
+
+ switch (geomType) {
+ case null:
+ break;
+ case "Point":
+ if (
+ callback(
+ coords,
+ coordIndex,
+ featureIndex,
+ multiFeatureIndex,
+ geometryIndex
+ ) === false
+ )
+ return false;
+ coordIndex++;
+ multiFeatureIndex++;
+ break;
+ case "LineString":
+ case "MultiPoint":
+ for (j = 0; j < coords.length; j++) {
+ if (
+ callback(
+ coords[j],
+ coordIndex,
+ featureIndex,
+ multiFeatureIndex,
+ geometryIndex
+ ) === false
+ )
+ return false;
+ coordIndex++;
+ if (geomType === "MultiPoint") multiFeatureIndex++;
+ }
+ if (geomType === "LineString") multiFeatureIndex++;
+ break;
+ case "Polygon":
+ case "MultiLineString":
+ for (j = 0; j < coords.length; j++) {
+ for (k = 0; k < coords[j].length - wrapShrink; k++) {
+ if (
+ callback(
+ coords[j][k],
+ coordIndex,
+ featureIndex,
+ multiFeatureIndex,
+ geometryIndex
+ ) === false
+ )
+ return false;
+ coordIndex++;
+ }
+ if (geomType === "MultiLineString") multiFeatureIndex++;
+ if (geomType === "Polygon") geometryIndex++;
+ }
+ if (geomType === "Polygon") multiFeatureIndex++;
+ break;
+ case "MultiPolygon":
+ for (j = 0; j < coords.length; j++) {
+ geometryIndex = 0;
+ for (k = 0; k < coords[j].length; k++) {
+ for (l = 0; l < coords[j][k].length - wrapShrink; l++) {
+ if (
+ callback(
+ coords[j][k][l],
+ coordIndex,
+ featureIndex,
+ multiFeatureIndex,
+ geometryIndex
+ ) === false
+ )
+ return false;
+ coordIndex++;
+ }
+ geometryIndex++;
+ }
+ multiFeatureIndex++;
+ }
+ break;
+ case "GeometryCollection":
+ for (j = 0; j < (geometry as GeometryCollection).geometries.length; j++)
+ if (
+ coordEach((geometry as GeometryCollection).geometries[j], callback, excludeWrapCoord) ===
+ false
+ )
+ return false;
+ break;
+ default:
+ throw new Error("Unknown Geometry Type");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/src/lib/util/gpx_util.ts b/web/src/lib/util/gpx_util.ts
index 24c34570..7b5192d1 100644
--- a/web/src/lib/util/gpx_util.ts
+++ b/web/src/lib/util/gpx_util.ts
@@ -2,21 +2,22 @@ import GPX from "$lib/models/gpx/gpx";
import { Trail } from "$lib/models/trail";
import { Waypoint } from "$lib/models/waypoint";
import { currentUser } from "$lib/stores/user_store";
-import { kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON";
+import { gpx, kml, tcx } from "$lib/vendor/toGeoJSON/toGeoJSON";
import cryptoRandomString from "crypto-random-string";
import { get } from "svelte/store";
//@ts-ignore
-import EasyFit from "$lib/vendor/easy-fit/easy-fit"
+import { browser } from "$app/environment";
import Track from "$lib/models/gpx/track";
import TrackSegment from "$lib/models/gpx/track-segment";
import GPXWaypoint from "$lib/models/gpx/waypoint";
-import { browser } from "$app/environment";
-import * as xmldom from 'xmldom'
-import type { Feature, FeatureCollection, GeoJsonProperties, Position } from 'geojson';
+import EasyFit from "$lib/vendor/easy-fit/easy-fit";
+import type { GeoJSON, Feature, FeatureCollection, GeoJsonProperties, Position } from 'geojson';
+import * as xmldom from 'xmldom';
+import { bbox } from "./geojson_util";
export async function gpx2trail(gpxString: string, fallbackName?: string) {
-
+
const gpx = await GPX.parse(gpxString);
if (gpx instanceof Error) {
@@ -281,4 +282,13 @@ export function isFITFile(buffer: ArrayBuffer) {
}
return true;
+}
+
+export function toGeoJson(gpxData: string) {
+ const parser = browser ? new DOMParser() : new xmldom.DOMParser();
+ const geojson = gpx(
+ parser.parseFromString(gpxData, "text/xml"),
+ ) as GeoJSON;
+ geojson.bbox = bbox(geojson)
+ return geojson
}
\ No newline at end of file
diff --git a/web/src/lib/util/maplibre_util.ts b/web/src/lib/util/maplibre_util.ts
new file mode 100644
index 00000000..b86fc15a
--- /dev/null
+++ b/web/src/lib/util/maplibre_util.ts
@@ -0,0 +1,44 @@
+import type { Waypoint } from "$lib/models/waypoint";
+import M from "maplibre-gl";
+
+export class FontawesomeMarker extends M.Marker {
+ constructor(options: { icon: string, fontSize?: string, width?: number, backgroundColor?: string, fontColor?: string }, markerOptions?: M.MarkerOptions) {
+ const element = document.createElement('div')
+ element.className = `cursor-pointer flex items-center justify-center w-${options.width ?? 7} aspect-square bg-${options.backgroundColor ?? "gray-500"} rounded-full text-${options.fontSize ?? "normal"}`
+ super({ element: element, ...markerOptions });
+
+ const {
+ icon,
+ } = options
+
+ const iconElementString = ``
+ this._element.insertAdjacentHTML('beforeend', iconElementString)
+ }
+}
+
+export function createMarkerFromWaypoint(waypoint: Waypoint, onDragEnd?: (marker: M.Marker) => void): FontawesomeMarker {
+ const marker = new FontawesomeMarker({
+ icon: `fa fa-${waypoint.icon}`,
+ }, {
+ draggable: onDragEnd != null,
+ color: "#6b7280"
+
+ })
+ const popup = new M.Popup({ offset: 25, closeButton: false }).setHTML(
+ "" +
+ waypoint.name +
+ "" +
+ (waypoint.description && waypoint.description.length > 0
+ ? "
" + waypoint.description
+ : ""),
+ );
+ marker
+ .setLngLat([waypoint.lon, waypoint.lat])
+ .setPopup(popup)
+
+ if (onDragEnd) {
+ marker.on("dragend", () => onDragEnd(marker));
+ }
+
+ return marker;
+}
\ No newline at end of file
diff --git a/web/src/lib/vendor/maplibre-elevation-profile/elevationprofile-control.ts b/web/src/lib/vendor/maplibre-elevation-profile/elevationprofile-control.ts
new file mode 100644
index 00000000..dae71368
--- /dev/null
+++ b/web/src/lib/vendor/maplibre-elevation-profile/elevationprofile-control.ts
@@ -0,0 +1,218 @@
+import { type ControlPosition, type IControl } from "maplibre-gl";
+import * as M from "maplibre-gl";
+import { ElevationProfile, type ElevationProfileOptions } from "./elevationprofile";
+// @ts-ignore
+import type { GeoJsonObject } from "geojson";
+import type { Waypoint } from "$lib/models/waypoint";
+
+/**
+ * Elevation profile control options
+ */
+export type ElevationProfileControlOptions = ElevationProfileOptions & {
+ /**
+ * If `true`, the elevation profile control will be visible as soon as it's ready.
+ * If `false`, a click on the control button (or a programmatic call to `.showProfile()`)
+ * will be neccesary to show the profile.
+ *
+ * Default: `false`
+ */
+ visible?: boolean;
+ /**
+ * Size of the profile as a CSS rule.
+ * This `size` will be the `width` if the `.position` is "left" or "right",
+ * and will be the `height` if the `.position` is "top" or "bottom".
+ *
+ * Default: `"30%"`
+ */
+ size?: string;
+ /**
+ * Position of the elevation profile chart when shown.
+ *
+ * Default: `"botton"`
+ */
+ position?: "top" | "left" | "right" | "bottom";
+ /**
+ * Show the control button. If can be handy to hide it, especially if the profile is displayed
+ * in a custom container and that its visiblity is managed by logic external to this control.
+ *
+ * Default: `true`
+ */
+ showButton?: boolean;
+ /**
+ * A CSS class to add to the container. This is especially relevant when the options `.container` is not provided.
+ * Important: if provided, no styling is added by this control and even placement will have to be managed by external CSS.
+ *
+ * Default: `""`
+ */
+ containerClass?: string;
+
+ /**
+ * DIV element to contain the control.
+ * Important: if provided, no styling is added by this control.
+ * Default: automatically created inside the map container
+ */
+ container?: string | HTMLDivElement;
+};
+
+export class ElevationProfileControl implements IControl {
+ private map?: M.Map;
+ private buttonContainer?: HTMLDivElement;
+ private toggleButton?: HTMLButtonElement;
+ private isProfileShown = false;
+ private iconSpan?: HTMLSpanElement;
+
+ private profileContainer?: HTMLDivElement;
+ private settings: ElevationProfileControlOptions;
+ private data: GeoJsonObject | null = null;
+ private elevationProfileChart?: ElevationProfile;
+
+ constructor(options: ElevationProfileControlOptions = {}) {
+ if (typeof window === "undefined")
+ throw new Error("This pluggin must be mounted client-side");
+ this.settings = { ...options };
+ }
+
+ toggleTheme(options: ElevationProfileControlOptions) {
+ this.settings = { ...this.settings, ...options };
+ this.elevationProfileChart?.toggleTheme(this.settings)
+ }
+
+ getContainer(): HTMLDivElement | undefined {
+ return this.profileContainer;
+ }
+
+ onAdd(map: M.Map): HTMLElement {
+ this.map = map;
+
+ this.buttonContainer = document.createElement("div");
+
+ if (this.settings.showButton === false) {
+ this.buttonContainer.style.setProperty("display", "none");
+ }
+
+ this.buttonContainer.classList.add(
+ "maplibregl-ctrl",
+ "maplibregl-ctrl-group"
+ );
+ this.toggleButton = document.createElement("button");
+ this.buttonContainer.appendChild(this.toggleButton);
+ this.iconSpan = document.createElement("i");
+ this.iconSpan.classList.add("fa", "fa-chart-line", "text-black");
+ this.toggleButton.appendChild(this.iconSpan);
+ this.toggleButton.addEventListener("click", this.toggleProfile.bind(this));
+ const mapContainer = map.getContainer();
+ const size = this.settings.size ?? "30%";
+
+ if (this.settings.container) {
+ const tmpContainer =
+ typeof this.settings.container === "string"
+ ? document.getElementById(this.settings.container)
+ : this.settings.container;
+ if (!tmpContainer) throw new Error("The provided container is invalid");
+ this.profileContainer = tmpContainer as HTMLDivElement;
+ } else {
+ this.profileContainer = document.createElement("div");
+ this.profileContainer.style.setProperty("display", "none");
+
+ if (!this.settings.containerClass) {
+ this.profileContainer.classList.add(this.settings.backgroundColor ?? "white")
+
+ this.profileContainer.style.setProperty("position", "absolute");
+
+ if (this.settings.position === "bottom" || !this.settings.position) {
+ // To prevent clashing with MapTiler logo and attribution control
+ this.settings.paddingBottom = this.settings.paddingBottom ?? 35;
+ this.profileContainer.style.setProperty("width", "100%");
+ this.profileContainer.style.setProperty("height", size);
+ this.profileContainer.style.setProperty("bottom", "0");
+ } else if (this.settings.position === "top") {
+ this.profileContainer.style.setProperty("width", "100%");
+ this.profileContainer.style.setProperty("height", size);
+ this.profileContainer.style.setProperty("top", "0");
+ } else if (this.settings.position === "left") {
+ // To prevent clashing with MapTiler logo and attribution control
+ this.settings.paddingBottom = this.settings.paddingBottom ?? 35;
+ this.profileContainer.style.setProperty("width", size);
+ this.profileContainer.style.setProperty("height", "100%");
+ this.profileContainer.style.setProperty("left", "0");
+ } else if (this.settings.position === "right") {
+ // To prevent clashing with MapTiler logo and attribution control
+ this.settings.paddingBottom = this.settings.paddingBottom ?? 35;
+ this.profileContainer.style.setProperty("width", size);
+ this.profileContainer.style.setProperty("height", "100%");
+ this.profileContainer.style.setProperty("right", "0");
+ }
+ }
+
+ const waypointContainer = document.createElement("div")
+ waypointContainer.className = "absolute w-full"
+ waypointContainer.id = "waypoint-container"
+
+ this.profileContainer.append(waypointContainer);
+
+ mapContainer.appendChild(this.profileContainer);
+ }
+
+ if (this.settings.containerClass) {
+ this.profileContainer.classList.add(this.settings.containerClass);
+ }
+
+ this.elevationProfileChart = new ElevationProfile(
+ this.profileContainer,
+ this.settings
+ );
+
+ if (this.settings.visible) {
+ this.showProfile();
+ }
+
+ return this.buttonContainer;
+ }
+
+ private toggleProfile() {
+ if (!this.profileContainer) return;
+
+ if (this.isProfileShown) {
+ this.hideProfile();
+ } else {
+ this.showProfile();
+ }
+ }
+
+ showProfile() {
+ this.profileContainer?.style.setProperty("display", "inherit");
+ this.isProfileShown = true;
+ }
+
+ hideProfile() {
+ this.profileContainer?.style.setProperty("display", "none");
+ this.isProfileShown = false;
+ }
+
+ onRemove(): void {
+ // remove button
+ if (this.buttonContainer?.parentNode) {
+ this.buttonContainer.parentNode.removeChild(this.buttonContainer);
+ }
+ this.map = undefined;
+ this.buttonContainer = undefined;
+ this.toggleButton = undefined;
+ this.isProfileShown = false;
+ }
+
+ getDefaultPosition?: (() => ControlPosition) | undefined;
+
+ async setData(data: GeoJsonObject, waypoints?: Waypoint[]) {
+ if (!this.map || !this.elevationProfileChart) {
+ throw new Error(
+ "The Elevation Profile Control needs to be mounted on a map instance before setting any data."
+ );
+ }
+
+ this.data = data;
+
+ if (!this.data) return;
+
+ this.elevationProfileChart.setData(this.data, waypoints);
+ }
+}
\ No newline at end of file
diff --git a/web/src/lib/vendor/maplibre-elevation-profile/elevationprofile.ts b/web/src/lib/vendor/maplibre-elevation-profile/elevationprofile.ts
new file mode 100644
index 00000000..04310213
--- /dev/null
+++ b/web/src/lib/vendor/maplibre-elevation-profile/elevationprofile.ts
@@ -0,0 +1,1006 @@
+import type {
+ Feature,
+ FeatureCollection,
+ GeoJsonObject,
+ GeometryObject,
+ LineString,
+ MultiLineString,
+ Position,
+} from "geojson";
+
+import { Chart, registerables } from "chart.js";
+import zoomPlugin from "chartjs-plugin-zoom";
+// @ts-ignore
+import { CrosshairPlugin } from "chartjs-plugin-crosshair";
+
+import type { Waypoint } from "$lib/models/waypoint";
+import { haversineCumulatedDistanceWgs84, smoothElevations } from "./tools";
+import { haversineDistance } from "$lib/models/gpx/utils";
+import { formatTimeHHMM } from "$lib/util/format_util";
+
+const FEET_PER_METER = 3.28084;
+const MILES_PER_METER = 0.000621371;
+const KILOMETERS_HOUR_PER_METER_SECOND = 3.6
+const MILES_HOUR_PER_METER_SECOND = 2.23694
+
+function extractLineStrings(
+ geoJson: GeoJsonObject
+): { lineStrings: Array, times: Date[] } {
+ const lineStrings: Array = [];
+ const times: Date[] = [];
+
+ function extractFromGeometry(geometry: GeometryObject) {
+ if (geometry.type === "LineString" || geometry.type === "MultiLineString") {
+ lineStrings.push(geometry as LineString | MultiLineString);
+ }
+ }
+
+ function extractFromFeature(feature: Feature) {
+ if (feature.geometry) {
+ extractFromGeometry(feature.geometry);
+ }
+ if (feature.properties?.coordinateProperties?.times) {
+ const coordinateTimes = feature.properties?.coordinateProperties?.times.map((t: string) => new Date(t))
+ times.push(...coordinateTimes)
+ }
+ }
+
+ function extractFromFeatureCollection(collection: FeatureCollection) {
+ for (const feature of collection.features) {
+ if (feature.type === "Feature") {
+ extractFromFeature(feature);
+ } else if (feature.type === "FeatureCollection") {
+ extractFromFeatureCollection(feature as unknown as FeatureCollection); // had to add unknown
+ }
+ }
+ }
+
+ if (geoJson.type === "Feature") {
+ extractFromFeature(geoJson as Feature);
+ } else if (geoJson.type === "FeatureCollection") {
+ extractFromFeatureCollection(geoJson as FeatureCollection);
+ } else {
+ // It's a single geometry
+ extractFromGeometry(geoJson as GeometryObject);
+ }
+
+ return { lineStrings, times };
+}
+
+function geoJsonObjectToPositionsAndTimes(geoJson: GeoJsonObject): { positions: Position[], times: Date[] } {
+ const { lineStrings, times } = extractLineStrings(geoJson);
+ const positionsGroups: Position[][] = [];
+
+ for (let i = 0; i < lineStrings.length; i += 1) {
+ const feature = lineStrings[i];
+ if (feature.type === "LineString") {
+ positionsGroups.push(feature.coordinates);
+ } else if (feature.type === "MultiLineString") {
+ positionsGroups.push(feature.coordinates.flat());
+ }
+ }
+ return { positions: positionsGroups.flat(), times };
+}
+
+/**
+ * Event data to `onMove` and `onClick` callback
+ */
+export type CallbackData = {
+ /**
+ * The position as `[lon, lat, elevation]`.
+ * Elevation will be in meters if the component has been set with the unit "metric" (default)
+ * of in feet if the unit is "imperial".
+ */
+ position: Position;
+ /**
+ * The distance from the start of the route. In km if the component has been set with the unit "metric" (default)
+ * of in miles if the unit is "imperial".
+ */
+ distance: number;
+ /**
+ * Cumulated positive elevation from the begining of the route up to this location.
+ * In meters if the component has been set with the unit "metric" (default)
+ * of in feet if the unit is "imperial".
+ */
+ dPlus: number;
+ /**
+ * Slope grade in percentage (1% being a increase of 1m on a 100m distance)
+ */
+ gradePercent: number;
+};
+
+export type ElevationProfileOptions = {
+ /**
+ * Color of the background of the chart
+ */
+ backgroundColor?: string | null;
+ /**
+ * Unit system to use.
+ * If "metric", elevation and D+ will be in meters, distances will be in km.
+ * If "imperial", elevation and D+ will be in feet, distances will be in miles.
+ *
+ * Default: "metric"
+ */
+ unit?: "metric" | "imperial";
+ /**
+ * Font size applied to axes labels and tooltip.
+ *
+ * Default: `12`
+ */
+ fontSize?: number;
+ /**
+ * If `true`, will force the computation of the elevation of the GeoJSON data provided to the `.setData()` method,
+ * even if they already contain elevation (possibly from GPS while recording). If `false`, the elevation will only
+ * be computed if missing from the positions.
+ *
+ * Default: `false`
+ */
+ forceComputeElevation?: boolean;
+ /**
+ * Display the elevation label along the vertical axis.
+ *
+ * Default: `true`
+ */
+ displayElevationLabels?: boolean;
+ /**
+ * Display the distance labels alon the horizontal axis.
+ *
+ * Default: `true`
+ */
+ displayDistanceLabels?: boolean;
+ /**
+ * Display the distance and elevation units alongside the labels.
+ *
+ * Default: `true`
+ */
+ displayUnits?: boolean;
+ /**
+ * Color of the elevation and distance labels.
+ *
+ * Default: `"#0009"` (partially transparent black)
+ */
+ labelColor?: string;
+ /**
+ * Color of the elevation profile line.
+ * Can be `null` to not display the line and rely on the background color only.
+ *
+ * Default: `"#66ccff"`
+ */
+ profileLineColor?: string | null;
+ /**
+ * Width of the elevation profile line.
+ *
+ * Default: `1.5`
+ */
+ profileLineWidth?: number;
+ /**
+ * Color of the elevation profile background (below the profile line)
+ * Can be `null` to not display any backgound color.
+ *
+ * Default: `"#66ccff22"`
+ */
+ profileBackgroundColor?: string | null;
+ /**
+ * Display the tooltip folowing the pointer.
+ *
+ * Default: `true`
+ */
+ displayTooltip?: boolean;
+ /**
+ * Color of the text inside the tooltip.
+ *
+ * Default: `"#fff"`
+ */
+ tooltipTextColor?: string;
+ /**
+ * Color of the tooltip background.
+ *
+ * Default: `"#000A"` (partially transparent black)
+ */
+ tooltipBackgroundColor?: string;
+ /**
+ * Display the distance information inside the tooltip if `true`.
+ *
+ * Default: `true`
+ */
+ tooltipDisplayDistance?: boolean;
+ /**
+ * Display the elevation information inside the tooltip if `true`.
+ *
+ * Default: `true`
+ */
+ tooltipDisplayElevation?: boolean;
+ /**
+ * Display the D+ (cumulated positive ascent) inside the tooltip if `true`.
+ *
+ * Default: `true`
+ */
+ tooltipDisplayDPlus?: boolean;
+ /**
+ * Display the slope grade in percentage inside the tooltip if `true`.
+ *
+ * Default: `true`
+ */
+ tooltipDisplayGrade?: boolean;
+ /**
+ * Display the slope grade in percentage inside the tooltip if `true`.
+ *
+ * Default: `true`
+ */
+ tooltipDisplaySpeed?: boolean;
+ /**
+ * Display the distance grid lines (vertical lines matching the distance labels) if `true`.
+ *
+ * Default: `false`
+ */
+ displayDistanceGrid?: boolean;
+ /**
+ * Display the elevation grid lines (horizontal lines matching the elevation labels) if `true`.
+ *
+ * Default: `true`
+ */
+ displayElevationGrid?: boolean;
+ /**
+ * Color of the distance grid lines.
+ *
+ * Default: `"#0001"` (partially transparent black)
+ */
+ distanceGridColor?: string;
+ /**
+ * Color of the elevation drig lines.
+ *
+ * Default: `"#0001"` (partially transparent black)
+ */
+ elevationGridColor?: string;
+ /**
+ * Padding at the top of the chart, in number of pixels.
+ *
+ * Default: `30`
+ */
+ paddingTop?: number;
+ /**
+ * Padding at the bottom of the chart, in number of pixels.
+ *
+ * Default: `10`
+ */
+ paddingBottom?: number;
+ /**
+ * Padding at the left of the chart, in number of pixels.
+ *
+ * Default: `10`
+ */
+ paddingLeft?: number;
+ /**
+ * Padding at the right of the chart, in number of pixels.
+ *
+ * Default: `10`
+ */
+ paddingRight?: number;
+ /**
+ * Display the crosshair, a vertical line that follows the pointer, if `true`.
+ *
+ * Default: `true`
+ */
+ displayCrosshair?: boolean;
+ /**
+ * Color of the crosshair.
+ *
+ * Default: `"#0005"` (partially transparent black)
+ */
+ crosshairColor?: string;
+ /**
+ * Callback function to call when the chart is zoomed or panned.
+ * The argument `windowedLineString` is the GeoJSON LineString corresponding
+ * to the portion of the route visible in the elevation chart.
+ *
+ * Default: `null`
+ */
+ onChangeView?: ((windowedLineString: LineString) => void) | null;
+ /**
+ * Callback function to call when the the elevation chart is clicked.
+ *
+ * Default: `null`
+ */
+ onClick?: ((data: CallbackData) => void) | null;
+ /**
+ * Callback function to call when the pointer is moving on the elevation chart.
+ *
+ * Default: `null`
+ */
+ onMove?: ((data: CallbackData) => void) | null;
+
+ onEnter?: (() => void) | null;
+
+ onLeave?: (() => void) | null;
+
+};
+
+const elevationProfileDefaultOptions: ElevationProfileOptions = {
+ backgroundColor: null,
+ unit: "metric",
+ fontSize: 12,
+ forceComputeElevation: false,
+ displayElevationLabels: true,
+ displayDistanceLabels: true,
+ displayUnits: true,
+ labelColor: "#0009",
+ profileLineColor: "#66ccff",
+ profileLineWidth: 1.5,
+ profileBackgroundColor: "#66ccff22",
+ displayTooltip: true,
+ tooltipTextColor: "#fff",
+ tooltipBackgroundColor: "#000A",
+ tooltipDisplayDistance: true,
+ tooltipDisplayElevation: true,
+ tooltipDisplayDPlus: true,
+ tooltipDisplayGrade: true,
+ tooltipDisplaySpeed: true,
+ displayDistanceGrid: false,
+ displayElevationGrid: true,
+ distanceGridColor: "#0001",
+ elevationGridColor: "#0001",
+ displayCrosshair: true,
+ crosshairColor: "#0005",
+ onChangeView: null,
+ paddingTop: 30,
+ paddingBottom: 10,
+ paddingLeft: 10,
+ paddingRight: 10,
+ onClick: null,
+ onMove: null,
+};
+
+/**
+ * Elevation profile chart
+ */
+export class ElevationProfile {
+ private canvas: HTMLCanvasElement;
+ private settings: ElevationProfileOptions;
+ private chart: Chart<"line", Array, number>;
+ private elevatedPositions: Position[] = [];
+ private elevatedPositionsAdjustedUnit: Position[] = [];
+ private cumulatedDistance: number[] = [];
+ private cumulatedDistanceAdjustedUnit: number[] = [];
+ private cumulatedDPlus: number[] = [];
+ private grade: number[] = [];
+ private waypointPositions: number[] = []
+ private waypoints: Waypoint[] = [];
+ private times: Date[] = [];
+ private cumulatedTime: number[] = []
+ private speed: number[] = [];
+
+ constructor(
+ /**
+ * DIV element to place the chart into
+ */
+ container: HTMLDivElement | string,
+ /**
+ * Options
+ */
+ options: ElevationProfileOptions = {}
+ ) {
+ const appContainer =
+ typeof container === "string"
+ ? document.getElementById(container)
+ : container;
+ if (!appContainer) {
+ throw new Error("The container does not exist.");
+ }
+
+ this.canvas = document.createElement("canvas");
+ appContainer.appendChild(this.canvas);
+
+ Chart.register(...registerables);
+ Chart.register(zoomPlugin);
+ Chart.register(CrosshairPlugin);
+
+ this.settings = {
+ ...elevationProfileDefaultOptions,
+ ...options,
+ };
+
+ const gradeColor = [
+ "#0d0887", // 0% and less
+ "#3e049c", // 1
+ "#6300a7", // 2
+ "#8606a6", // 3
+ "#a62098", // 4
+ "#c03a83", // 5
+ "#d5546e", // 6
+ "#e76f5a", // 7
+ "#f68d45", // 8
+ "#fdae32", // 9
+ "#fcd225", // 10
+ "#f0f921", // more than 10%
+ ]
+
+ const distanceUnit = this.settings.unit === "imperial" ? "mi" : "km";
+ const elevationUnit = this.settings.unit === "imperial" ? "ft" : "m";
+ Chart.defaults.font.size = this.settings.fontSize;
+
+ let width: number, height: number, gradient: CanvasGradient;
+ this.chart = new Chart<"line", Array, number>(this.canvas, {
+ type: "line",
+
+ data: {
+ labels: [],
+ datasets: [
+ {
+ label: "Elevation",
+ yAxisID: "y",
+ data: [],
+ pointRadius: 0,
+ fill: !!this.settings.profileBackgroundColor,
+ borderColor: (context) => {
+ const { ctx, chartArea } = context.chart;
+
+ if (!chartArea) {
+ return;
+ }
+
+ const chartWidth = chartArea.right - chartArea.left;
+ const chartHeight = chartArea.bottom - chartArea.top;
+ if (!gradient || width !== chartWidth || height !== chartHeight) {
+ width = chartWidth;
+ height = chartHeight;
+
+ gradient = ctx.createLinearGradient(chartArea.left, 0, chartArea.right, 0);
+
+ gradient.addColorStop(0, gradeColor[0])
+
+ let prevColor = gradeColor[0];
+ for (let i = 0; i < this.grade.length; i++) {
+ const grade = ~~(Math.abs(this.grade[i]) / 2.5);
+
+ let color;
+ if (grade < 1) {
+ color = gradeColor[0]
+ }
+ else if (grade > 10) {
+ color = gradeColor[11];
+ } else {
+ color = gradeColor[grade];
+ }
+
+
+ if (color !== prevColor) {
+ const percentDone = this.cumulatedDistance[i] / this.cumulatedDistance[this.cumulatedDistance.length - 1]
+ gradient.addColorStop(percentDone, color);
+ prevColor = color;
+ }
+
+ }
+ }
+
+ return gradient;
+ },
+ // borderColor: this.settings.profileLineColor ?? "#0000",
+ backgroundColor: this.settings.profileBackgroundColor ?? "#0000",
+ tension: 0.1,
+ spanGaps: true,
+
+ // If line color is null, the line width is set to 0
+ borderWidth: this.settings.profileLineColor
+ ? this.settings.profileLineWidth
+ : 0,
+ }
+ ],
+ },
+
+ options: {
+ layout: {
+ padding: {
+ left: this.settings.paddingLeft,
+ right: this.settings.paddingRight,
+ bottom: this.settings.paddingBottom,
+ top: this.settings.paddingTop,
+ },
+ },
+ onClick: (_e, item) => {
+ if (typeof this.settings.onClick !== "function") return;
+
+ try {
+ const i = item[0].index;
+
+ this.settings.onClick.apply(this, [
+ {
+ position: this.elevatedPositionsAdjustedUnit[i],
+ distance: this.cumulatedDistanceAdjustedUnit[i],
+ dPlus: this.cumulatedDPlus[i],
+ gradePercent: this.grade[i],
+ },
+ ]);
+ } catch (e) {
+ // Nothing to do
+ }
+ },
+ onHover: (_e, item) => {
+ if (typeof this.settings.onMove !== "function") return;
+
+ try {
+ const i = item[0].index;
+
+ this.settings.onMove.apply(this, [
+ {
+ position: this.elevatedPositionsAdjustedUnit[i],
+ distance: this.cumulatedDistanceAdjustedUnit[i],
+ dPlus: this.cumulatedDPlus[i],
+ gradePercent: this.grade[i],
+ },
+ ]);
+ } catch (e) {
+ // Nothing to do
+ }
+ },
+ animation: false,
+ maintainAspectRatio: false,
+ scales: {
+ x: {
+ min: 0,
+ max: 0,
+ type: "linear",
+ grid: {
+ display: this.settings.displayDistanceGrid,
+ drawOnChartArea: false,
+ color: this.settings.distanceGridColor,
+ drawTicks: true,
+ tickLength: 5,
+ tickColor: "#0002"
+ },
+ ticks: {
+ stepSize: 0.1,
+ align: "inner",
+ display: this.settings.displayDistanceLabels,
+ color: this.settings.labelColor,
+ maxRotation: 0,
+ callback: (value, index) => {
+ if (index % 10 !== 0) {
+ return "";
+ }
+ const roundedValue = ~~((value as number) * 100) / 100;
+ return this.settings.displayUnits
+ ? `${roundedValue} ${distanceUnit}`
+ : roundedValue;
+ },
+ },
+ },
+ y: {
+ min: 0,
+ max: 0,
+ type: "linear",
+ afterTickToLabelConversion: (scaleInstance) => {
+ scaleInstance.ticks.pop();
+ scaleInstance.ticks.shift();
+ },
+ ticks: {
+ mirror: true,
+ maxTicksLimit: 10,
+ align: "end",
+ display: this.settings.displayElevationLabels,
+ color: this.settings.labelColor,
+
+ callback: (value) => {
+ const roundedValue = ~~((value as number) * 100) / 100;
+ return this.settings.displayUnits
+ ? `${roundedValue} ${elevationUnit}`
+ : roundedValue;
+ },
+ },
+ border: {
+ dash: [5, 5],
+ display: true,
+ color: this.settings.elevationGridColor,
+ },
+ grid: {
+ display: this.settings.displayElevationGrid,
+ color: this.settings.elevationGridColor,
+ drawTicks: false,
+ },
+ },
+ },
+
+ interaction: {
+ intersect: false,
+ mode: "index",
+ },
+
+ plugins: {
+ zoom: {
+ zoom: {
+ wheel: {
+ enabled: true,
+ },
+ pinch: {
+ enabled: true,
+ },
+ mode: "x",
+ },
+ pan: {
+ enabled: true,
+ mode: "x",
+ },
+ limits: {
+ x: {
+ min: "original",
+ max: "original",
+ },
+ },
+ },
+ title: {
+ display: false,
+ },
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ enabled: this.settings.displayTooltip,
+ yAlign: "center",
+ cornerRadius: 3,
+ displayColors: false,
+ backgroundColor: this.settings.tooltipBackgroundColor,
+ bodyColor: this.settings.tooltipTextColor,
+ callbacks: {
+ title: () => {
+ return "";
+ },
+
+ label: (tooltipItem) => {
+ if (tooltipItem.datasetIndex != 0) {
+ return "";
+ }
+ const tooltipInfo = [];
+ if (this.settings.tooltipDisplayDistance) {
+ tooltipInfo.push(
+ `After: ${this.cumulatedDistanceAdjustedUnit[
+ tooltipItem.dataIndex
+ ].toFixed(4)} ${distanceUnit} ${this.cumulatedTime.length ? '(' + formatTimeHHMM(this.cumulatedTime[tooltipItem.dataIndex]) + ')' : ''}`
+ );
+ }
+
+ if (this.settings.tooltipDisplayElevation) {
+ tooltipInfo.push(
+ `Elevation: ${this.elevatedPositionsAdjustedUnit[
+ tooltipItem.dataIndex
+ ][2].toFixed(2)} ${elevationUnit}`
+ );
+ }
+
+ if (this.settings.tooltipDisplayDPlus) {
+ tooltipInfo.push(
+ `D+: ${this.cumulatedDPlus[tooltipItem.dataIndex].toFixed(
+ 0
+ )} ${elevationUnit}`
+ );
+ }
+
+ if (this.settings.tooltipDisplayGrade) {
+ tooltipInfo.push(
+ `Grade: ${this.grade[tooltipItem.dataIndex].toFixed(1)}%`
+ );
+ }
+
+ if (this.settings.tooltipDisplaySpeed && this.speed.length) {
+ tooltipInfo.push(`Speed: ${this.speed[
+ tooltipItem.dataIndex
+ ].toFixed(2)} ${distanceUnit}/h`
+ );
+ }
+
+ return tooltipInfo;
+ },
+ },
+ },
+
+ // The crosshair plugin does not have types
+ // @ts-ignore
+ crosshair: {
+ zoom: {
+ enabled: false,
+ },
+ line: {
+ color: this.settings.displayCrosshair
+ ? this.settings.crosshairColor
+ : "#0000",
+ width: 1,
+ },
+ },
+ },
+ },
+
+ plugins: [
+ {
+ id: "waypointPlugin",
+ afterDraw: (chart, args, options) => {
+ const waypointContainer = document.getElementById("waypoint-container") as HTMLDivElement;
+ waypointContainer.innerHTML = ""; // Clear previous ticks
+
+ const xScale = chart.scales.x; // Get X-axis scale
+ const chartRect = chart.canvas.getBoundingClientRect(); // Canvas position
+
+ this.waypointPositions.forEach((position: number, index: number) => {
+
+ const xPos = xScale.getPixelForValue(position); // X-axis pixel for tick
+ console.log(xPos);
+
+ // Create custom HTML tick
+ const wpDiv = document.createElement("div");
+ wpDiv.className = "wp-marker absolute -translate-x-1/2 w-6 aspect-square bg-background-inverse rounded-full flex justify-center items-center text-content-inverse cursor-pointer hover:scale-110";
+ wpDiv.style.left = `${xPos}px`; // Position horizontally
+ wpDiv.style.top = `8px`; // Position horizontally
+
+ // Add custom HTML content (e.g., icon + label)
+ wpDiv.innerHTML = ``;
+
+ waypointContainer.appendChild(wpDiv); // Add to container
+ });
+ }
+ },
+ {
+ id: "customZoomEvent",
+ afterDataLimits: () => {
+ if (typeof this.settings.onChangeView !== "function") return;
+ try {
+ this.settings.onChangeView.apply(this, [
+ this.createWindowExtractLineString(),
+ ]);
+ } catch (e) {
+ // nothing to do
+ }
+ },
+ },
+ ],
+ });
+ if (typeof this.settings.onLeave === "function") {
+ this.chart.canvas.addEventListener("mouseout", this.settings.onLeave)
+ }
+
+ if (typeof this.settings.onEnter === "function") {
+ this.chart.canvas.addEventListener("mouseenter", this.settings.onEnter)
+ }
+
+ // If the tooltip is shown, then we hide it when panning the chart
+ if (this.settings.displayTooltip) {
+ let mouseDown = false;
+ this.chart.canvas.addEventListener("mousedown", () => {
+ mouseDown = true;
+ });
+
+ this.chart.canvas.addEventListener("mousemove", () => {
+ if (
+ mouseDown &&
+ this.chart.options.plugins &&
+ this.chart.options.plugins.tooltip
+ ) {
+ this.chart.options.plugins.tooltip.enabled = false;
+ this.chart.update();
+ }
+ });
+
+ window.addEventListener("mouseup", () => {
+ if (this.chart.options.plugins && this.chart.options.plugins.tooltip) {
+ this.chart.options.plugins.tooltip.enabled = true;
+ this.chart.update();
+ mouseDown = false;
+ }
+ });
+ }
+ }
+
+ createWindowExtractLineString(): LineString {
+ const scaleMin = this.chart.scales.x.min;
+ const scaleMax = this.chart.scales.x.max;
+
+ const cda = this.cumulatedDistanceAdjustedUnit;
+ const nbElem = cda.length;
+
+ let indexStart = 0;
+ let indexEnd = nbElem - 1;
+
+ // find the start index
+ for (let i = 0; i < nbElem; i += 1) {
+ if (cda[i] >= scaleMin) {
+ indexStart = i;
+ break;
+ }
+ }
+
+ // find the end index
+ for (let i = nbElem - 1; i >= indexStart; i -= 1) {
+ if (cda[i] <= scaleMax) {
+ indexEnd = i;
+ break;
+ }
+ }
+
+ return this.createExtractLineString(indexStart, indexEnd);
+ }
+
+ createExtractLineString(fromIndex: number, toIndex: number): LineString {
+ const elevatedPositionsWindow: Position[] = this.elevatedPositions.slice(
+ fromIndex,
+ toIndex
+ );
+
+ return {
+ type: "LineString",
+ coordinates: elevatedPositionsWindow,
+ };
+ }
+
+ toggleTheme(options: ElevationProfileOptions) {
+ this.settings = {
+ ...this.settings,
+ ...options,
+ };
+ this.chart.data.datasets[0].backgroundColor = this.settings.profileBackgroundColor ?? "#0000";
+ this.chart.options.scales!.x!.ticks!.color = this.settings.labelColor;
+
+ this.chart.options.scales!.y!.grid!.color = this.settings.elevationGridColor;
+ this.chart.options.scales!.y!.border!.color = this.settings.elevationGridColor;
+ this.chart.options.scales!.y!.ticks!.color = this.settings.labelColor;
+
+ (this.chart.options.plugins as any).crosshair!.line.color = this.settings.crosshairColor;
+
+ this.chart.update();
+ }
+
+ async setData(data: GeoJsonObject, waypoints?: Waypoint[]) {
+ // Concatenates the positions that may come from multiple LineStrings or MultiLineString
+ const { positions, times } = geoJsonObjectToPositionsAndTimes(data);
+
+ this.times = times;
+
+ this.elevatedPositions = smoothElevations(positions, positions.length / 100);
+
+ this.cumulatedDistance = haversineCumulatedDistanceWgs84(
+ this.elevatedPositions
+ );
+
+ // Conversion of distance to miles and elevation to feet
+ if (this.settings.unit === "imperial") {
+ this.cumulatedDistanceAdjustedUnit = this.cumulatedDistance.map(
+ (dist) => dist * MILES_PER_METER
+ );
+ this.elevatedPositionsAdjustedUnit = this.elevatedPositions.map((pos) => [
+ pos[0],
+ pos[1],
+ pos[2] * FEET_PER_METER,
+ ]);
+ this.cumulatedDPlus = this.cumulatedDPlus.map(
+ (ele) => ele * FEET_PER_METER
+ );
+ } else {
+ this.cumulatedDistanceAdjustedUnit = this.cumulatedDistance.map(
+ (dist) => dist / 1000
+ ); // we still need to convert distance to km
+ this.elevatedPositionsAdjustedUnit = this.elevatedPositions;
+ }
+
+ this.cumulatedDPlus = [];
+ this.grade = [];
+ this.waypoints = waypoints ?? [];
+ // this.waypointPositions = [];
+
+ let cumulatedDPlus = 0;
+ let cumulatedTime = 0;
+
+ const minSegmentDistance = (this.cumulatedDistance.at(-1) ?? 1000) / 100;
+ let segmentStartIndex = 0;
+
+ // Initialize an array to store the minimum distance for each waypoint
+ const minDistances = new Array(waypoints?.length).fill(Infinity);
+
+ for (let i = 0; i < this.elevatedPositions.length; i++) {
+
+ // Check if a waypoint is closest to this point
+ this.waypoints.forEach((waypoint, waypointIndex) => {
+ const distance = haversineDistance(this.elevatedPositions[i][1], this.elevatedPositions[i][0], waypoint.lat, waypoint.lon);
+
+ // Update if the current route coordinate is closer to the waypoint
+ if (distance < minDistances[waypointIndex]) {
+ minDistances[waypointIndex] = distance;
+ this.waypointPositions[waypointIndex] = this.cumulatedDistanceAdjustedUnit[i];
+ }
+ });
+
+ const elevation = this.elevatedPositions[i][2];
+ const time = this.times[i]
+ if (i > 1) {
+ const elevationPrevious = this.elevatedPositions[i - 1][2];
+ const elevationDelta = elevation - elevationPrevious;
+ const segmentDistance =
+ this.cumulatedDistance[i] - this.cumulatedDistance[segmentStartIndex];
+ cumulatedDPlus += Math.max(0, elevationDelta);
+ this.cumulatedDPlus.push(cumulatedDPlus);
+
+ if (time) {
+ const timePrevious = this.times[i - 1];
+ const timeDelta = (time.getTime() - timePrevious.getTime()) / (1000 * 60);
+ cumulatedTime += timeDelta;
+ this.cumulatedTime.push(cumulatedTime)
+ }
+
+
+ // Check if the segment distance is greater than or equal to the minimum threshold
+ if (segmentDistance >= minSegmentDistance || i === this.elevatedPositions.length - 1) {
+ // Calculate the grade for the consolidated segment
+ const elevationStart = this.elevatedPositions[segmentStartIndex][2];
+ const elevationEnd = this.elevatedPositions[i][2];
+ const elevationDelta = elevationEnd - elevationStart;
+ const gradePercent = (elevationDelta / segmentDistance) * 100; // Grade as a percentage
+
+ let speed;
+ if (this.times.length) {
+ const distanceStart = this.cumulatedDistance[segmentStartIndex]
+ const distanceEnd = this.cumulatedDistance[i]
+ const distanceDelta = distanceEnd - distanceStart
+
+ const timeStart = this.times[segmentStartIndex]
+ const timeEnd = this.times[i]
+ const timeDelta = (timeEnd.getTime() - timeStart.getTime()) / 1000
+
+ speed = (distanceDelta / timeDelta)
+ if (this.settings.unit === "imperial") {
+ speed = speed * MILES_HOUR_PER_METER_SECOND
+ } else {
+ speed = speed * KILOMETERS_HOUR_PER_METER_SECOND;
+ }
+ }
+
+
+ // Apply the same grade to all positions within this segment
+ for (let j = segmentStartIndex; j <= i; j++) {
+ this.grade.push(gradePercent);
+ if (speed) {
+ this.speed.push(speed)
+ }
+ }
+
+ // Move to the next segment
+ segmentStartIndex = i + 1;
+ }
+ }
+ }
+
+ this.grade.push(this.grade.at(-1) ?? 0);
+ this.cumulatedDPlus.push(cumulatedDPlus);
+
+
+ let minElevation = +Infinity;
+ let maxElevation = -Infinity;
+
+ for (let i = 0; i < this.elevatedPositionsAdjustedUnit.length; i += 1) {
+ if (this.elevatedPositionsAdjustedUnit[i][2] < minElevation) {
+ minElevation = this.elevatedPositionsAdjustedUnit[i][2];
+ }
+
+ if (this.elevatedPositionsAdjustedUnit[i][2] > maxElevation) {
+ maxElevation = this.elevatedPositionsAdjustedUnit[i][2];
+ }
+ }
+
+ const elevationPadding = (maxElevation - minElevation) * 0.1;
+ this.chart.data.labels = this.cumulatedDistanceAdjustedUnit;
+ this.chart.data.datasets[0].data = this.elevatedPositionsAdjustedUnit.map(
+ (pos) => pos[2]
+ );
+
+ if (
+ this.chart.options.scales &&
+ this.chart.options.scales.x &&
+ this.chart.options.scales.y
+ ) {
+ this.chart.options.scales.x.min = this.cumulatedDistanceAdjustedUnit[0];
+ this.chart.options.scales.x.max =
+ this.cumulatedDistanceAdjustedUnit[
+ this.cumulatedDistanceAdjustedUnit.length - 1
+ ];
+
+
+ this.chart.options.scales.y.min = minElevation - elevationPadding;
+ this.chart.options.scales.y.max = maxElevation + elevationPadding;
+ }
+ this.chart.update();
+ }
+
+}
\ No newline at end of file
diff --git a/web/src/lib/vendor/maplibre-elevation-profile/tools.ts b/web/src/lib/vendor/maplibre-elevation-profile/tools.ts
new file mode 100644
index 00000000..42eb549f
--- /dev/null
+++ b/web/src/lib/vendor/maplibre-elevation-profile/tools.ts
@@ -0,0 +1,47 @@
+import { haversineDistance } from "$lib/models/gpx/utils";
+import type {
+ Position
+} from "geojson";
+
+
+export function haversineCumulatedDistanceWgs84(path: Position[]): number[] {
+ if (path.length < 2) {
+ return [];
+ }
+
+ let totalDistance = 0;
+ const distances: number[] = [0];
+
+ for (let i = 0; i < path.length - 1; i++) {
+ const [lon1, lat1] = path[i];
+ const [lon2, lat2] = path[i + 1];
+
+ totalDistance += haversineDistance(lat1, lon1, lat2, lon2);
+ distances.push(totalDistance);
+ }
+
+ return distances; // Array of cumulative distances in meters
+}
+
+export function smoothElevations(positions: Position[], windowSize: number): Position[] {
+ // Ensure windowSize is valid (at least 1)
+ if (windowSize < 1) throw new Error("Window size must be at least 1.");
+
+ // Create a new array with smoothed elevations
+ return positions.map((pos, i, arr) => {
+ const start = Math.max(0, i - Math.floor(windowSize / 2)); // Start index for the window
+ const end = Math.min(arr.length, i + Math.floor(windowSize / 2) + 1); // End index for the window
+ const segment = arr.slice(start, end); // Extract the positions in the window
+
+ // Calculate the weighted moving average of elevations
+ const weights = segment.map((_, idx) => idx + 1); // Increasing weights: 1, 2, 3...
+ const elevations = segment.map(p => p[2]); // Extract elevations
+ const weightedSum = elevations.reduce((sum, elevation, idx) => sum + elevation * weights[idx], 0);
+ const weightTotal = weights.reduce((sum, weight) => sum + weight, 0);
+
+ const smoothedElevation = weightedSum / weightTotal; // Weighted average elevation
+
+ // Return a new Position with the smoothed elevation
+ return [pos[0], pos[1], smoothedElevation] as Position;
+ });
+}
\ No newline at end of file
diff --git a/web/src/routes/+error.svelte b/web/src/routes/+error.svelte
index 0e7ad993..793786a0 100644
--- a/web/src/routes/+error.svelte
+++ b/web/src/routes/+error.svelte
@@ -7,7 +7,7 @@
style="height: calc(100dvh - 120px); font-size: 4rem"
>
- import MapWithElevation from "$lib/components/trail/map_with_elevation.svelte";
+ import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
import TrailInfoPanel from "$lib/components/trail/trail_info_panel.svelte";
import { trail } from "$lib/stores/trail_store";
import "$lib/vendor/leaflet-elevation/src/index.css";
- import type { Marker } from "leaflet";
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
import "leaflet/dist/leaflet.css";
+ import * as M from "maplibre-gl";
import "photoswipe/style.css";
import { _ } from "svelte-i18n";
- let markers: Marker[];
+ let markers: M.Marker[];
@@ -18,7 +18,7 @@
-
+
diff --git a/web/src/routes/tmp/+page.svelte b/web/src/routes/tmp/+page.svelte
new file mode 100644
index 00000000..3507c190
--- /dev/null
+++ b/web/src/routes/tmp/+page.svelte
@@ -0,0 +1,8 @@
+
+
+
diff --git a/web/src/routes/tmp/+page.ts b/web/src/routes/tmp/+page.ts
new file mode 100644
index 00000000..37a18b6e
--- /dev/null
+++ b/web/src/routes/tmp/+page.ts
@@ -0,0 +1,9 @@
+import type { Trail } from "$lib/models/trail";
+import { trails_show } from "$lib/stores/trail_store";
+
+export const load = async ({ params, fetch }) => {
+ const t: Trail = await trails_show("yesm2tqc6jok8jq", true, fetch)
+
+
+ return { trail: t }
+};
\ No newline at end of file
diff --git a/web/src/routes/trail/edit/[id]/+page.svelte b/web/src/routes/trail/edit/[id]/+page.svelte
index 1d36d933..6a1589dc 100644
--- a/web/src/routes/trail/edit/[id]/+page.svelte
+++ b/web/src/routes/trail/edit/[id]/+page.svelte
@@ -296,7 +296,6 @@
elevation_loss: $form.elevation_loss,
duration: $form.duration ? $form.duration * 60 : undefined,
});
- console.log(selectedFile.type);
log.expand.gpx_data = gpxData;
const blob = new Blob([gpxData], { type: selectedFile.type });
diff --git a/web/tailwind.config.js b/web/tailwind.config.js
index 25d14472..db213aca 100644
--- a/web/tailwind.config.js
+++ b/web/tailwind.config.js
@@ -5,7 +5,9 @@ export default {
extend: {
colors: {
'background': 'rgba(var(--background))',
+ 'background-inverse': 'rgba(var(--background-inverse))',
'content': 'rgba(var(--content))',
+ 'content-inverse': 'rgba(var(--content-inverse))',
'primary': 'rgba(var(--primary))',
'primary-hover': 'rgba(var(--primary-hover))',