Compare commits
51 Commits
maplibre
...
3dce5dc617
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dce5dc617 | ||
|
|
84b90e1026 | ||
|
|
d507586eed | ||
|
|
57afaedf83 | ||
|
|
48063b9066 | ||
|
|
452d356599 | ||
|
|
25eda8041e | ||
|
|
ae4d5356eb | ||
|
|
3343bb906e | ||
|
|
7b17900160 | ||
|
|
d5f1fe1c7b | ||
|
|
553f73f992 | ||
|
|
c8cedf2e2c | ||
|
|
a751817847 | ||
|
|
d1ef12db8d | ||
|
|
43d73edf29 | ||
|
|
5a0b8c376c | ||
|
|
9743fd460e | ||
|
|
f70f92a176 | ||
|
|
1a4175446c | ||
|
|
ed6dfab4c1 | ||
|
|
6a6e1105c0 | ||
|
|
1677fe254b | ||
|
|
02efe708c2 | ||
|
|
7dc834f506 | ||
|
|
57c4958ff2 | ||
|
|
03e59a8cce | ||
|
|
f3d18f09a0 | ||
|
|
c1dbd984e6 | ||
|
|
e4f227221d | ||
|
|
34139974aa | ||
|
|
408b2422e6 | ||
|
|
b59cb9e200 | ||
|
|
bd5cb65d0f | ||
|
|
4cfe487af0 | ||
|
|
4da2e39e32 | ||
|
|
5ff11a32c9 | ||
|
|
01a7ec916e | ||
|
|
dd94a7d613 | ||
|
|
089b88c62d | ||
|
|
9c6e03f4a8 | ||
|
|
2a4dfe010e | ||
|
|
f42a916c25 | ||
|
|
772b810fa8 | ||
|
|
4d4d10d5c2 | ||
|
|
0e4c7dbe64 | ||
|
|
a01ca79a82 | ||
|
|
c91baf7c83 | ||
|
|
5062de8ddf | ||
|
|
9ca46b9d35 | ||
|
|
7c2e24bbc4 |
2
.github/FUNDING.yml
vendored
@@ -1 +1 @@
|
||||
ko_fi: gpxstudio
|
||||
open_collective: gpxstudio
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
[**gpx.studio**](https://gpx.studio) is an online tool for creating and editing GPX files.
|
||||
|
||||

|
||||

|
||||
|
||||
This repository contains the source code of the website.
|
||||
|
||||
@@ -69,8 +69,8 @@ This project has been made possible thanks to the following open source projects
|
||||
- [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) — fast GPX file parsing
|
||||
- [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree
|
||||
- Mapping:
|
||||
- [MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js) — beautiful and fast interactive maps
|
||||
- [brouter](https://github.com/abrensch/brouter) — routing engine
|
||||
- [MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js) — beautiful and fast interactive map rendering
|
||||
- [GraphHopper](https://github.com/graphhopper/graphhopper) — routing engine
|
||||
- [OpenStreetMap](https://www.openstreetmap.org) — map data used by most of the map layers, and by the routing engine
|
||||
- Search:
|
||||
- [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation
|
||||
|
||||
@@ -1398,10 +1398,7 @@ export class TrackPoint {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
setExtensions(extensions: Record<string, string>) {
|
||||
if (Object.keys(extensions).length === 0) {
|
||||
return;
|
||||
}
|
||||
setExtension(key: string, value: string) {
|
||||
if (!this.extensions) {
|
||||
this.extensions = {};
|
||||
}
|
||||
@@ -1411,8 +1408,12 @@ export class TrackPoint {
|
||||
if (!this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions']) {
|
||||
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'] = {};
|
||||
}
|
||||
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'][key] = value;
|
||||
}
|
||||
|
||||
setExtensions(extensions: Record<string, string>) {
|
||||
Object.entries(extensions).forEach(([key, value]) => {
|
||||
this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:Extensions'][key] = value;
|
||||
this.setExtension(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
109
website/package-lock.json
generated
@@ -22,7 +22,7 @@
|
||||
"immer": "^10.1.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mapillary-js": "^4.1.2",
|
||||
"maplibre-gl": "^5.16.0",
|
||||
"maplibre-gl": "^5.21.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"sortablejs": "^1.15.6",
|
||||
"tailwind-merge": "^3.3.0"
|
||||
@@ -1611,31 +1611,6 @@
|
||||
"svelte": "^5"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"license": "ISC",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"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",
|
||||
@@ -1670,7 +1645,8 @@
|
||||
"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=="
|
||||
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/vector-tile": {
|
||||
"version": "2.0.4",
|
||||
@@ -1704,10 +1680,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/geojson-vt": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz",
|
||||
"integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==",
|
||||
"license": "ISC"
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.0.4.tgz",
|
||||
"integrity": "sha512-HYv3POhMRCdhP3UPPATM/hfcy6/WuVIf5FKboH8u/ZuFMTnAIcSVlq5nfOqroLokd925w2QtE7YwquFOIacwVQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"kdbush": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/maplibre-gl-geocoder": {
|
||||
"version": "1.9.4",
|
||||
@@ -1729,9 +1708,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||
"version": "24.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.4.1.tgz",
|
||||
"integrity": "sha512-UKhA4qv1h30XT768ccSv5NjNCX+dgfoq2qlLVmKejspPcSQTYD4SrVucgqegmYcKcmwf06wcNAa/kRd0NHWbUg==",
|
||||
"version": "24.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.7.0.tgz",
|
||||
"integrity": "sha512-Ed7rcKYU5iELfablg9Mj+TVCsXsPBgdMyXPRAxb2v7oWg9YJnpQdZ5msDs1LESu/mtXy3Z48Vdppv2t/x5kAhw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
|
||||
@@ -1749,18 +1728,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/mlt": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.2.tgz",
|
||||
"integrity": "sha512-SQKdJ909VGROkA6ovJgtHNs9YXV4YXUPS+VaZ50I2Mt951SLlUm2Cv34x5Xwc1HiFlsd3h2Yrs5cn7xzqBmENw==",
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.8.tgz",
|
||||
"integrity": "sha512-8vtfYGidr1rNkv5IwIoU2lfe3Oy+Wa8HluzQYcQi9cveU9K3pweAal/poQj4GJ0K/EW4bTQp2wVAs09g2yDRZg==",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/vt-pbf": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.2.1.tgz",
|
||||
"integrity": "sha512-IxZBGq/+9cqf2qdWlFuQ+ZfoMhWpxDUGQZ/poPHOJBvwMUT1GuxLo6HgYTou+xxtsOsjfbcjI8PZaPCtmt97rA==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.0.tgz",
|
||||
"integrity": "sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "^1.1.0",
|
||||
@@ -1772,6 +1751,12 @@
|
||||
"supercluster": "^8.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/vt-pbf/node_modules/@maplibre/geojson-vt": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz",
|
||||
"integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@maplibre/vt-pbf/node_modules/pbf": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
|
||||
@@ -2606,15 +2591,6 @@
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hammerjs": {
|
||||
"version": "2.0.46",
|
||||
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz",
|
||||
@@ -2702,6 +2678,7 @@
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||
"integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
@@ -4605,12 +4582,6 @@
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -5282,7 +5253,8 @@
|
||||
"node_modules/kdbush": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
||||
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="
|
||||
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
@@ -5683,33 +5655,29 @@
|
||||
"integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ=="
|
||||
},
|
||||
"node_modules/maplibre-gl": {
|
||||
"version": "5.16.0",
|
||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.16.0.tgz",
|
||||
"integrity": "sha512-/VDY89nr4jgLJyzmhy325cG6VUI02WkZ/UfVuDbG/piXzo6ODnM+omDFIwWY8tsEsBG26DNDmNMn3Y2ikHsBiA==",
|
||||
"version": "5.21.1",
|
||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.21.1.tgz",
|
||||
"integrity": "sha512-zto1RTnFkOpOO1bm93ElCXF1huey2N4LvXaGLMFcYAu9txh0OhGIdX1q3LZLkrMKgMxMeYduaQo+DVNzg098fg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/geojson-rewind": "^0.5.2",
|
||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||
"@mapbox/point-geometry": "^1.1.0",
|
||||
"@mapbox/tiny-sdf": "^2.0.7",
|
||||
"@mapbox/unitbezier": "^0.0.1",
|
||||
"@mapbox/vector-tile": "^2.0.4",
|
||||
"@mapbox/whoots-js": "^3.1.0",
|
||||
"@maplibre/maplibre-gl-style-spec": "^24.4.1",
|
||||
"@maplibre/mlt": "^1.1.2",
|
||||
"@maplibre/vt-pbf": "^4.2.0",
|
||||
"@maplibre/geojson-vt": "^6.0.4",
|
||||
"@maplibre/maplibre-gl-style-spec": "^24.7.0",
|
||||
"@maplibre/mlt": "^1.1.8",
|
||||
"@maplibre/vt-pbf": "^4.3.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/geojson-vt": "3.2.5",
|
||||
"@types/supercluster": "^7.1.3",
|
||||
"earcut": "^3.0.2",
|
||||
"geojson-vt": "^4.0.2",
|
||||
"gl-matrix": "^3.4.4",
|
||||
"kdbush": "^4.0.2",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"pbf": "^4.0.1",
|
||||
"potpack": "^2.1.0",
|
||||
"quickselect": "^3.0.0",
|
||||
"supercluster": "^8.0.1",
|
||||
"tinyqueue": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6627,7 +6595,8 @@
|
||||
"node_modules/quickselect": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="
|
||||
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
@@ -7356,6 +7325,7 @@
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
|
||||
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"kdbush": "^4.0.2"
|
||||
}
|
||||
@@ -7814,7 +7784,8 @@
|
||||
"node_modules/tinyqueue": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="
|
||||
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
"immer": "^10.1.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mapillary-js": "^4.1.2",
|
||||
"maplibre-gl": "^5.16.0",
|
||||
"maplibre-gl": "^5.21.1",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"sortablejs": "^1.15.6",
|
||||
"tailwind-merge": "^3.3.0"
|
||||
|
||||
|
Before Width: | Height: | Size: 2.0 MiB |
BIN
website/src/lib/assets/img/docs/getting-started/interface.webp
Normal file
|
After Width: | Height: | Size: 339 KiB |
|
Before Width: | Height: | Size: 768 KiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 596 KiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 5.4 MiB |
BIN
website/src/lib/assets/img/home/map-overlay.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 3.6 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 6.9 MiB |
|
Before Width: | Height: | Size: 448 KiB After Width: | Height: | Size: 348 KiB |
@@ -31,6 +31,7 @@ import bikerouterGravel from './custom/bikerouter-gravel.json';
|
||||
export const maptilerKeyPlaceHolder = 'MAPTILER_KEY';
|
||||
|
||||
export const basemaps: { [key: string]: string | StyleSpecification } = {
|
||||
maptilerStreets: `https://api.maptiler.com/maps/streets-v4/style.json?key=${maptilerKeyPlaceHolder}`,
|
||||
maptilerTopo: `https://api.maptiler.com/maps/topo-v4/style.json?key=${maptilerKeyPlaceHolder}`,
|
||||
maptilerOutdoors: `https://api.maptiler.com/maps/outdoor-v4/style.json?key=${maptilerKeyPlaceHolder}`,
|
||||
maptilerSatellite: `https://api.maptiler.com/maps/hybrid-v4/style.json?key=${maptilerKeyPlaceHolder}`,
|
||||
@@ -776,6 +777,7 @@ export type LayerTreeType = { [key: string]: LayerTreeType | boolean };
|
||||
export const basemapTree: LayerTreeType = {
|
||||
basemaps: {
|
||||
world: {
|
||||
maptilerStreets: true,
|
||||
maptilerTopo: true,
|
||||
maptilerOutdoors: true,
|
||||
maptilerSatellite: true,
|
||||
@@ -911,7 +913,7 @@ export const overpassTree: LayerTreeType = {
|
||||
};
|
||||
|
||||
// Default basemap used
|
||||
export const defaultBasemap = 'maptilerTopo';
|
||||
export const defaultBasemap = 'maptilerStreets';
|
||||
|
||||
// Default overlays used (none)
|
||||
export const defaultOverlays: LayerTreeType = {
|
||||
@@ -1000,6 +1002,7 @@ export const defaultOverpassQueries: LayerTreeType = {
|
||||
export const defaultBasemapTree: LayerTreeType = {
|
||||
basemaps: {
|
||||
world: {
|
||||
maptilerStreets: true,
|
||||
maptilerTopo: true,
|
||||
maptilerOutdoors: true,
|
||||
maptilerSatellite: true,
|
||||
|
||||
@@ -64,3 +64,9 @@
|
||||
</svelte:head>
|
||||
|
||||
<div id="docsearch" class={props.class ?? ''}></div>
|
||||
|
||||
<style>
|
||||
#docsearch :global(button) {
|
||||
margin-left: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} {variant} class={className} {onclick}>
|
||||
<Button {...props} {variant} class="bg-inherit {className}" {onclick}>
|
||||
{@render children()}
|
||||
</Button>
|
||||
{/snippet}
|
||||
|
||||
@@ -1,116 +1,118 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import LanguageSelect from '$lib/components/LanguageSelect.svelte';
|
||||
import ModeSwitch from '$lib/components/ModeSwitch.svelte';
|
||||
import Logo from '$lib/components/Logo.svelte';
|
||||
import { AtSign, BookOpenText, Heart, House, Map } from '@lucide/svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
</script>
|
||||
|
||||
<footer class="w-full">
|
||||
<div class="mx-6 border-t">
|
||||
<div class="mx-12 py-10 flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
|
||||
<div class="grow flex flex-col items-start">
|
||||
<Logo class="h-8" width="153" />
|
||||
<footer class="w-full px-12 py-10 border-t flex flex-col items-center">
|
||||
<div class="w-full max-w-5xl flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
|
||||
<div class="grow flex flex-col items-start">
|
||||
<Logo class="h-8" width="153" />
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE"
|
||||
target="_blank"
|
||||
>
|
||||
MIT © 2026 gpx.studio
|
||||
</Button>
|
||||
<div class="mt-3 flex flex-row gap-1.5">
|
||||
<LanguageSelect />
|
||||
<ModeSwitch />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow max-w-2xl flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<span class="font-semibold">{i18n._('homepage.website')}</span>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="https://github.com/gpxstudio/gpx.studio/blob/main/LICENSE"
|
||||
href={getURLForLanguage(i18n.lang, '/')}
|
||||
>
|
||||
<House size="16" />
|
||||
{i18n._('homepage.home')}
|
||||
</Button>
|
||||
<Button
|
||||
data-sveltekit-reload
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href={getURLForLanguage(i18n.lang, '/app')}
|
||||
>
|
||||
<Map size="16" />
|
||||
{i18n._('homepage.app')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href={getURLForLanguage(i18n.lang, '/help')}
|
||||
>
|
||||
<BookOpenText size="16" />
|
||||
{i18n._('menu.help')}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-1" id="contact">
|
||||
<span class="font-semibold">{i18n._('homepage.contact')}</span>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="https://www.reddit.com/r/gpxstudio/"
|
||||
target="_blank"
|
||||
>
|
||||
MIT © 2026 gpx.studio
|
||||
<Logo company="reddit" class="h-4 fill-muted-foreground" />
|
||||
{i18n._('homepage.reddit')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="https://facebook.com/gpx.studio"
|
||||
target="_blank"
|
||||
>
|
||||
<Logo company="facebook" class="h-4 fill-muted-foreground" />
|
||||
{i18n._('homepage.facebook')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="mailto:hello@gpx.studio"
|
||||
target="_blank"
|
||||
>
|
||||
<AtSign size="16" />
|
||||
{i18n._('homepage.email')}
|
||||
</Button>
|
||||
<LanguageSelect class="w-40 mt-3" />
|
||||
</div>
|
||||
<div class="grow max-w-2xl flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<span class="font-semibold">{i18n._('homepage.website')}</span>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href={getURLForLanguage(i18n.lang, '/')}
|
||||
>
|
||||
<House size="16" />
|
||||
{i18n._('homepage.home')}
|
||||
</Button>
|
||||
<Button
|
||||
data-sveltekit-reload
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href={getURLForLanguage(i18n.lang, '/app')}
|
||||
>
|
||||
<Map size="16" />
|
||||
{i18n._('homepage.app')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href={getURLForLanguage(i18n.lang, '/help')}
|
||||
>
|
||||
<BookOpenText size="16" />
|
||||
{i18n._('menu.help')}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-1" id="contact">
|
||||
<span class="font-semibold">{i18n._('homepage.contact')}</span>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="https://www.reddit.com/r/gpxstudio/"
|
||||
target="_blank"
|
||||
>
|
||||
<Logo company="reddit" class="h-4 fill-muted-foreground" />
|
||||
{i18n._('homepage.reddit')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="https://facebook.com/gpx.studio"
|
||||
target="_blank"
|
||||
>
|
||||
<Logo company="facebook" class="h-4 fill-muted-foreground" />
|
||||
{i18n._('homepage.facebook')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="mailto:hello@gpx.studio"
|
||||
target="_blank"
|
||||
>
|
||||
<AtSign size="16" />
|
||||
{i18n._('homepage.email')}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<span class="font-semibold">{i18n._('homepage.contribute')}</span>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="https://ko-fi.com/gpxstudio"
|
||||
target="_blank"
|
||||
>
|
||||
<Heart size="16" />
|
||||
{i18n._('menu.donate')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="https://crowdin.com/project/gpxstudio"
|
||||
target="_blank"
|
||||
>
|
||||
<Logo company="crowdin" class="h-4 fill-muted-foreground" />
|
||||
{i18n._('homepage.crowdin')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="https://github.com/gpxstudio/gpx.studio"
|
||||
target="_blank"
|
||||
>
|
||||
<Logo company="github" class="h-4 fill-muted-foreground" />
|
||||
{i18n._('homepage.github')}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<span class="font-semibold">{i18n._('homepage.contribute')}</span>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="https://opencollective.com/gpxstudio"
|
||||
target="_blank"
|
||||
>
|
||||
<Heart size="16" />
|
||||
{i18n._('menu.donate')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="https://crowdin.com/project/gpxstudio"
|
||||
target="_blank"
|
||||
>
|
||||
<Logo company="crowdin" class="h-4 fill-muted-foreground" />
|
||||
{i18n._('homepage.crowdin')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 has-[>svg]:px-0 text-muted-foreground"
|
||||
href="https://github.com/gpxstudio/gpx.studio"
|
||||
target="_blank"
|
||||
>
|
||||
<Logo company="github" class="h-4 fill-muted-foreground" />
|
||||
{i18n._('homepage.github')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,16 +12,17 @@
|
||||
|
||||
const { velocityUnits } = settings;
|
||||
|
||||
let panelHeight: number = $state(0);
|
||||
let panelWidth: number = $state(0);
|
||||
|
||||
let {
|
||||
gpxStatistics,
|
||||
slicedGPXStatistics,
|
||||
orientation,
|
||||
panelSize,
|
||||
}: {
|
||||
gpxStatistics: Readable<GPXStatisticsGroup>;
|
||||
slicedGPXStatistics: Readable<[GPXGlobalStatistics, number, number] | undefined>;
|
||||
orientation: 'horizontal' | 'vertical';
|
||||
panelSize: number;
|
||||
} = $props();
|
||||
|
||||
let statistics = $derived(
|
||||
@@ -32,58 +33,60 @@
|
||||
<Card.Root
|
||||
class="h-full {orientation === 'vertical'
|
||||
? 'min-w-40 sm:min-w-44'
|
||||
: 'w-full h-10'} border-none shadow-none p-0 text-sm sm:text-base"
|
||||
: 'w-full h-fit my-1'} border-none shadow-none p-0 text-sm sm:text-base bg-transparent"
|
||||
>
|
||||
<Card.Content
|
||||
class="h-full flex {orientation === 'vertical'
|
||||
? 'flex-col justify-center'
|
||||
: 'flex-row w-full justify-evenly'} gap-4 p-0"
|
||||
>
|
||||
<Tooltip label={i18n._('quantities.distance')}>
|
||||
<span class="flex flex-row items-center">
|
||||
<Ruler size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.distance.total} type="distance" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip label={i18n._('quantities.elevation_gain_loss')}>
|
||||
<span class="flex flex-row items-center">
|
||||
<MoveUpRight size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.elevation.gain} type="elevation" />
|
||||
<MoveDownRight size="16" class="mx-1" />
|
||||
<WithUnits value={statistics.elevation.loss} type="elevation" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
{#if panelSize > 120 || orientation === 'horizontal'}
|
||||
<Tooltip
|
||||
class={orientation === 'horizontal' ? 'hidden xs:block' : ''}
|
||||
label="{$velocityUnits === 'speed'
|
||||
? i18n._('quantities.speed')
|
||||
: i18n._('quantities.pace')} ({i18n._('quantities.moving')} / {i18n._(
|
||||
'quantities.total'
|
||||
)})"
|
||||
>
|
||||
<Card.Content class="h-full p-0">
|
||||
<div
|
||||
bind:clientHeight={panelHeight}
|
||||
bind:clientWidth={panelWidth}
|
||||
class="flex {orientation === 'vertical'
|
||||
? 'flex-col h-full justify-center'
|
||||
: 'flex-row w-full justify-evenly'} gap-4"
|
||||
>
|
||||
<Tooltip label={i18n._('quantities.distance')}>
|
||||
<span class="flex flex-row items-center">
|
||||
<Zap size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.speed.moving} type="speed" showUnits={false} />
|
||||
<span class="mx-1">/</span>
|
||||
<WithUnits value={statistics.speed.total} type="speed" />
|
||||
<Ruler size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.distance.total} type="distance" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if panelSize > 160 || orientation === 'horizontal'}
|
||||
<Tooltip
|
||||
class={orientation === 'horizontal' ? 'hidden md:block' : ''}
|
||||
label="{i18n._('quantities.time')} ({i18n._('quantities.moving')} / {i18n._(
|
||||
'quantities.total'
|
||||
)})"
|
||||
>
|
||||
<Tooltip label={i18n._('quantities.elevation_gain_loss')}>
|
||||
<span class="flex flex-row items-center">
|
||||
<Timer size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.time.moving} type="time" />
|
||||
<span class="mx-1">/</span>
|
||||
<WithUnits value={statistics.time.total} type="time" />
|
||||
<MoveUpRight size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.elevation.gain} type="elevation" />
|
||||
<MoveDownRight size="16" class="mx-1" />
|
||||
<WithUnits value={statistics.elevation.loss} type="elevation" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if panelHeight > 120 || (orientation === 'horizontal' && panelWidth > 450)}
|
||||
<Tooltip
|
||||
label="{$velocityUnits === 'speed'
|
||||
? i18n._('quantities.speed')
|
||||
: i18n._('quantities.pace')} ({i18n._('quantities.moving')} / {i18n._(
|
||||
'quantities.total'
|
||||
)})"
|
||||
>
|
||||
<span class="flex flex-row items-center">
|
||||
<Zap size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.speed.moving} type="speed" showUnits={false} />
|
||||
<span class="mx-1">/</span>
|
||||
<WithUnits value={statistics.speed.total} type="speed" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{#if panelHeight > 160 || (orientation === 'horizontal' && panelWidth > 620)}
|
||||
<Tooltip
|
||||
label="{i18n._('quantities.time')} ({i18n._('quantities.moving')} / {i18n._(
|
||||
'quantities.total'
|
||||
)})"
|
||||
>
|
||||
<span class="flex flex-row items-center">
|
||||
<Timer size="16" class="mr-1" />
|
||||
<WithUnits value={statistics.time.moving} type="time" />
|
||||
<span class="mx-1">/</span>
|
||||
<WithUnits value={statistics.time.total} type="time" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
@@ -5,16 +5,10 @@
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
import { Languages } from '@lucide/svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
}: {
|
||||
class?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Select.Root type="single" value={i18n.lang}>
|
||||
<Select.Trigger class="min-w-[180px] {className}" aria-label={i18n._('menu.language')}>
|
||||
<Select.Trigger class="w-[180px] px-2" aria-label={i18n._('menu.language')}>
|
||||
<Languages size="16" />
|
||||
<span class="mr-auto">
|
||||
{languages[i18n.lang]}
|
||||
|
||||
@@ -375,7 +375,7 @@
|
||||
<Menubar.Item inset onclick={() => map.toggle3D()}>
|
||||
<Box size="16" />
|
||||
{i18n._('menu.toggle_3d')}
|
||||
<Shortcut key="{i18n._('menu.ctrl')} {i18n._('menu.drag')}" />
|
||||
<Shortcut key={i18n._('menu.right_click_drag')} />
|
||||
</Menubar.Item>
|
||||
</Menubar.Content>
|
||||
</Menubar.Menu>
|
||||
@@ -515,7 +515,7 @@
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
href="https://ko-fi.com/gpxstudio"
|
||||
href="https://opencollective.com/gpxstudio"
|
||||
target="_blank"
|
||||
class="cursor-default h-fit rounded-sm font-bold text-support hover:text-support px-3 py-0.5"
|
||||
aria-label={i18n._('menu.donate')}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</script>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class={className}
|
||||
onclick={() => {
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
<script lang="ts">
|
||||
import Logo from '$lib/components/Logo.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import AlgoliaDocSearch from '$lib/components/AlgoliaDocSearch.svelte';
|
||||
import ModeSwitch from '$lib/components/ModeSwitch.svelte';
|
||||
import { BookOpenText, House, Map } from '@lucide/svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
</script>
|
||||
|
||||
<nav class="w-full sticky top-0 bg-background z-50">
|
||||
<div class="mx-6 py-2 flex flex-row items-center border-b gap-4 sm:gap-8">
|
||||
<a href={getURLForLanguage(i18n.lang, '/')} class="shrink-0 translate-y-0.5">
|
||||
<Logo class="h-8 sm:hidden" iconOnly={true} width="26" />
|
||||
<Logo class="h-8 hidden sm:block" width="153" />
|
||||
<nav class="sticky top-0 w-full px-12 py-2 bg-background z-50 flex flex-col items-center border-b">
|
||||
<div class="w-full max-w-5xl flex flex-row items-center gap-4 sm:gap-8">
|
||||
<a
|
||||
href={getURLForLanguage(i18n.lang, '/')}
|
||||
class="shrink-0 translate-y-0.25 justify-self-start"
|
||||
>
|
||||
<Logo class="h-8 xs:hidden" iconOnly={true} width="26" />
|
||||
<Logo class="h-8 hidden xs:block" width="153" />
|
||||
</a>
|
||||
<Button
|
||||
variant="link"
|
||||
class="text-base px-0 has-[>svg]:px-0"
|
||||
class="text-base px-0 has-[>svg]:px-0 ml-auto"
|
||||
href={getURLForLanguage(i18n.lang, '/')}
|
||||
>
|
||||
<House size="18" />
|
||||
@@ -39,7 +40,5 @@
|
||||
<BookOpenText size="18" />
|
||||
{i18n._('menu.help')}
|
||||
</Button>
|
||||
<AlgoliaDocSearch class="ml-auto" />
|
||||
<ModeSwitch class="hidden xs:inline-flex" />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
@apply text-foreground;
|
||||
@apply text-3xl;
|
||||
@apply font-semibold;
|
||||
@apply mb-3 pt-6;
|
||||
@apply mb-3;
|
||||
}
|
||||
|
||||
:global(.markdown h2) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="rounded-md overflow-hidden overflow-clip shadow-xl mx-auto">
|
||||
{#if src === 'getting-started/interface'}
|
||||
<enhanced:img
|
||||
src="/src/lib/assets/img/docs/getting-started/interface.png"
|
||||
src="/src/lib/assets/img/docs/getting-started/interface.webp"
|
||||
{alt}
|
||||
class="w-full max-w-3xl"
|
||||
/>
|
||||
@@ -20,13 +20,13 @@
|
||||
<enhanced:img
|
||||
src="/src/lib/assets/img/docs/tools/routing.png"
|
||||
{alt}
|
||||
class="w-full max-w-3xl"
|
||||
class="w-full max-w-lg"
|
||||
/>
|
||||
{:else if src === 'tools/split'}
|
||||
<enhanced:img
|
||||
src="/src/lib/assets/img/docs/tools/split.png"
|
||||
{alt}
|
||||
class="w-full max-w-3xl"
|
||||
class="w-full max-w-lg"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -68,14 +68,14 @@
|
||||
<canvas bind:this={overlay} class="w-full h-full absolute pointer-events-none"></canvas>
|
||||
<canvas bind:this={canvas} class="w-full h-full absolute"></canvas>
|
||||
{#if showControls}
|
||||
<div class="absolute bottom-10 right-1.5">
|
||||
<div class="absolute bottom-9 right-2.5">
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<ButtonWithTooltip
|
||||
label={i18n._('chart.settings')}
|
||||
variant="outline"
|
||||
side="left"
|
||||
class="w-7 h-7 p-0 flex justify-center opacity-70 hover:opacity-100 transition-opacity duration-300 hover:bg-background"
|
||||
class="w-7 h-7 p-0 flex justify-center opacity-70 hover:opacity-100 transition-opacity duration-300 bg-background"
|
||||
>
|
||||
<ChartNoAxesColumn size="18" />
|
||||
</ButtonWithTooltip>
|
||||
|
||||
@@ -117,13 +117,12 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="{options.elevation.show ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"
|
||||
class="{options.elevation.show ? '' : 'h-10'} flex flex-row gap-2 p-2 sm:px-4"
|
||||
style={options.elevation.show ? `height: ${options.elevation.height}px` : ''}
|
||||
>
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
panelSize={options.elevation.height}
|
||||
orientation={options.elevation.show ? 'vertical' : 'horizontal'}
|
||||
/>
|
||||
{#if options.elevation.show}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const defaultEmbeddingOptions = {
|
||||
key: '',
|
||||
files: [],
|
||||
ids: [],
|
||||
basemap: 'maptilerTopo',
|
||||
basemap: 'maptilerStreets',
|
||||
elevation: {
|
||||
show: true,
|
||||
height: 170,
|
||||
@@ -90,6 +90,9 @@ export function getCleanedEmbeddingOptions(
|
||||
delete cleanedOptions[key];
|
||||
}
|
||||
}
|
||||
if (cleanedOptions['key'] && cleanedOptions['key'] === PUBLIC_MAPTILER_KEY) {
|
||||
delete cleanedOptions['key'];
|
||||
}
|
||||
return cleanedOptions;
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,11 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full flex flex-row flex-wrap gap-2">
|
||||
<Button class="bg-support grow" href="https://ko-fi.com/gpxstudio" target="_blank">
|
||||
<Button
|
||||
class="bg-support grow"
|
||||
href="https://opencollective.com/gpxstudio"
|
||||
target="_blank"
|
||||
>
|
||||
{i18n._('menu.support_button')}
|
||||
<span>🙏</span>
|
||||
</Button>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { get } from 'svelte/store';
|
||||
import { map } from '$lib/components/map/map';
|
||||
import { allHidden } from '$lib/logic/hidden';
|
||||
import type { GeoJSONSource } from 'maplibre-gl';
|
||||
import { ANCHOR_LAYER_KEY } from '../style';
|
||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
||||
|
||||
const { distanceMarkers, distanceUnits } = settings;
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import { fileActions } from '$lib/logic/file-actions';
|
||||
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
|
||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
||||
import { gpxColors } from './gpx-layers';
|
||||
import { gpxColors } from '$lib/components/map/gpx-layer/gpx-layers';
|
||||
|
||||
const colors = [
|
||||
'#ff0000',
|
||||
@@ -251,7 +251,7 @@ export class GPXLayer {
|
||||
source: this.fileId,
|
||||
layout: {
|
||||
'text-field': '»',
|
||||
'text-offset': [0, -0.1],
|
||||
'text-offset': [0, -0.06],
|
||||
'text-keep-upright': false,
|
||||
'text-max-angle': 361,
|
||||
'text-allow-overlap': true,
|
||||
@@ -261,7 +261,6 @@ export class GPXLayer {
|
||||
},
|
||||
paint: {
|
||||
'text-color': 'white',
|
||||
'text-opacity': 0.7,
|
||||
'text-halo-width': 0.2,
|
||||
'text-halo-color': 'white',
|
||||
},
|
||||
|
||||
@@ -31,7 +31,7 @@ export class StartEndMarkers {
|
||||
unsubscribes: (() => void)[] = [];
|
||||
|
||||
constructor() {
|
||||
map.onLoad(() => this.update());
|
||||
map.onLoad((map_) => map_.on('style.load', this.updateBinded));
|
||||
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
|
||||
this.unsubscribes.push(slicedGPXStatistics.subscribe(this.updateBinded));
|
||||
this.unsubscribes.push(hoveredPoint.subscribe(this.updateBinded));
|
||||
|
||||
@@ -7,7 +7,7 @@ import { MapPopup } from '$lib/components/map/map-popup';
|
||||
import { settings } from '$lib/logic/settings';
|
||||
import { db } from '$lib/db';
|
||||
import type { GeoJSONSource } from 'maplibre-gl';
|
||||
import { ANCHOR_LAYER_KEY } from '../style';
|
||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
||||
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
||||
import { loadSVGIcon } from '$lib/utils';
|
||||
|
||||
@@ -24,7 +24,7 @@ liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
|
||||
});
|
||||
|
||||
export class OverpassLayer {
|
||||
overpassUrl = 'https://maps.mail.ru/osm/tools/overpass/api/interpreter';
|
||||
overpassUrl = 'https://overpass.private.coffee/api/interpreter';
|
||||
minZoom = 12;
|
||||
queryZoom = 12;
|
||||
expirationTime = 7 * 24 * 3600 * 1000;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { LayerTreeType } from '$lib/assets/layers';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export function anySelectedLayer(node: LayerTreeType) {
|
||||
return (
|
||||
|
||||
@@ -54,7 +54,7 @@ export class MapLibreGLMap {
|
||||
zoom: 0,
|
||||
hash: hash,
|
||||
boxZoom: false,
|
||||
maxPitch: 85,
|
||||
maxPitch: 90,
|
||||
});
|
||||
this.layerEventManager = new MapLayerEventManager(map);
|
||||
map.addControl(
|
||||
|
||||
@@ -2,7 +2,7 @@ import maplibregl, { type LayerSpecification, type VectorSourceSpecification } f
|
||||
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
|
||||
import 'mapillary-js/dist/mapillary.css';
|
||||
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
|
||||
import { ANCHOR_LAYER_KEY } from '../style';
|
||||
import { ANCHOR_LAYER_KEY } from '$lib/components/map/style';
|
||||
import type { MapLayerEventManager } from '$lib/components/map/map-layer-event-manager';
|
||||
|
||||
const mapillarySource: VectorSourceSpecification = {
|
||||
|
||||
@@ -85,9 +85,11 @@ export class StyleManager {
|
||||
|
||||
this.merge(style, basemapStyle);
|
||||
|
||||
const terrain = this.getCurrentTerrain();
|
||||
style.sources[terrain.source] = terrainSources[terrain.source];
|
||||
style.terrain = terrain.exaggeration > 0 ? terrain : undefined;
|
||||
if (this._maptilerKey !== '') {
|
||||
const terrain = this.getCurrentTerrain();
|
||||
style.sources[terrain.source] = terrainSources[terrain.source];
|
||||
style.terrain = terrain.exaggeration > 0 ? terrain : undefined;
|
||||
}
|
||||
|
||||
style.layers.push(...anchorLayers);
|
||||
|
||||
@@ -152,6 +154,7 @@ export class StyleManager {
|
||||
}
|
||||
|
||||
updateTerrain() {
|
||||
if (this._maptilerKey === '') return;
|
||||
const map_ = get(this._map);
|
||||
if (!map_) return;
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ export class RoutingControls {
|
||||
fileUnsubscribe: () => void = () => {};
|
||||
unsubscribes: Function[] = [];
|
||||
|
||||
updateControlsBinded: () => void = this.updateControls.bind(this);
|
||||
appendAnchorBinded: (e: MapMouseEvent) => void = this.appendAnchor.bind(this);
|
||||
|
||||
draggedAnchorIndex: number | null = null;
|
||||
@@ -129,10 +130,11 @@ export class RoutingControls {
|
||||
|
||||
this.loadIcons();
|
||||
|
||||
map_.on('style.load', this.updateControlsBinded);
|
||||
map_.on('click', this.appendAnchorBinded);
|
||||
layerEventManager.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
||||
|
||||
this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this));
|
||||
this.fileUnsubscribe = this.file.subscribe(this.updateControlsBinded);
|
||||
}
|
||||
|
||||
updateControls() {
|
||||
@@ -232,6 +234,7 @@ export class RoutingControls {
|
||||
|
||||
this.active = false;
|
||||
|
||||
map_?.off('style.load', this.updateControlsBinded);
|
||||
map_?.off('click', this.appendAnchorBinded);
|
||||
layerEventManager?.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
||||
map_?.off('mousemove', this.updateTemporaryAnchorBinded);
|
||||
@@ -683,17 +686,7 @@ export class RoutingControls {
|
||||
try {
|
||||
response = await route(targetTrackPoints.map((trkpt) => trkpt.getCoordinates()));
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('from-position not mapped in existing datafile')) {
|
||||
toast.error(i18n._('toolbar.routing.error.from'));
|
||||
} else if (e.message.includes('via1-position not mapped in existing datafile')) {
|
||||
toast.error(i18n._('toolbar.routing.error.via'));
|
||||
} else if (e.message.includes('to-position not mapped in existing datafile')) {
|
||||
toast.error(i18n._('toolbar.routing.error.to'));
|
||||
} else if (e.message.includes('Time-out')) {
|
||||
toast.error(i18n._('toolbar.routing.error.timeout'));
|
||||
} else {
|
||||
toast.error(e.message);
|
||||
}
|
||||
toast.error(i18n._(e.message, e.message));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,37 +6,213 @@ import { get } from 'svelte/store';
|
||||
|
||||
const { routing, routingProfile, privateRoads } = settings;
|
||||
|
||||
export const routingProfiles: { [key: string]: string } = {
|
||||
bike: 'Trekking-dry',
|
||||
racing_bike: 'fastbike',
|
||||
gravel_bike: 'gravel',
|
||||
mountain_bike: 'MTB',
|
||||
foot: 'Hiking-Alpine-SAC6',
|
||||
motorcycle: 'Car-FastEco',
|
||||
water: 'river',
|
||||
railway: 'rail',
|
||||
export type RoutingProfile = {
|
||||
engine: 'graphhopper' | 'brouter';
|
||||
profile: string;
|
||||
};
|
||||
|
||||
export const routingProfiles: { [key: string]: RoutingProfile } = {
|
||||
bike: { engine: 'graphhopper', profile: 'bike' },
|
||||
racing_bike: { engine: 'graphhopper', profile: 'racingbike' },
|
||||
gravel_bike: { engine: 'graphhopper', profile: 'gravelbike' },
|
||||
mountain_bike: { engine: 'graphhopper', profile: 'mtb' },
|
||||
foot: { engine: 'graphhopper', profile: 'foot' },
|
||||
motorcycle: { engine: 'graphhopper', profile: 'motorcycle' },
|
||||
water: { engine: 'brouter', profile: 'river' },
|
||||
railway: { engine: 'brouter', profile: 'rail' },
|
||||
};
|
||||
|
||||
export function route(points: Coordinates[]): Promise<TrackPoint[]> {
|
||||
if (get(routing)) {
|
||||
return getRoute(points, routingProfiles[get(routingProfile)], get(privateRoads));
|
||||
const profile = routingProfiles[get(routingProfile)];
|
||||
if (profile.engine === 'graphhopper') {
|
||||
return getGraphHopperRoute(points, profile.profile, get(privateRoads));
|
||||
} else {
|
||||
return getBRouterRoute(points, profile.profile);
|
||||
}
|
||||
} else {
|
||||
return getIntermediatePoints(points);
|
||||
}
|
||||
}
|
||||
|
||||
async function getRoute(
|
||||
const graphhopperDetails = ['road_class', 'surface', 'hike_rating', 'mtb_rating'];
|
||||
const hikeRatingToSACScale: { [key: string]: string } = {
|
||||
'1': 'hiking',
|
||||
'2': 'mountain_hiking',
|
||||
'3': 'demanding_mountain_hiking',
|
||||
'4': 'alpine_hiking',
|
||||
'5': 'demanding_alpine_hiking',
|
||||
'6': 'difficult_alpine_hiking',
|
||||
};
|
||||
const mtbRatingToScale: { [key: string]: string } = {
|
||||
'1': '0',
|
||||
'2': '1',
|
||||
'3': '2',
|
||||
'4': '3',
|
||||
'5': '4',
|
||||
'6': '5',
|
||||
'7': '6',
|
||||
};
|
||||
|
||||
const graphhopperBlockPrivateCustomModels: { [key: string]: any } = {
|
||||
bike: {
|
||||
priority: [
|
||||
{
|
||||
if: 'bike_road_access == PRIVATE',
|
||||
multiply_by: '0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
racingbike: {
|
||||
priority: [
|
||||
{
|
||||
if: 'bike_road_access == PRIVATE',
|
||||
multiply_by: '0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
gravelbike: {
|
||||
priority: [
|
||||
{
|
||||
if: 'bike_road_access == PRIVATE',
|
||||
multiply_by: '0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
mtb: {
|
||||
priority: [
|
||||
{
|
||||
if: 'bike_road_access == PRIVATE',
|
||||
multiply_by: '0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
foot: {
|
||||
priority: [
|
||||
{
|
||||
if: 'foot_road_access == PRIVATE',
|
||||
multiply_by: '0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
motorcycle: {
|
||||
priority: [
|
||||
{
|
||||
if: 'road_access == PRIVATE',
|
||||
multiply_by: '0.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
async function getGraphHopperRoute(
|
||||
points: Coordinates[],
|
||||
brouterProfile: string,
|
||||
graphHopperProfile: string,
|
||||
privateRoads: boolean
|
||||
): Promise<TrackPoint[]> {
|
||||
let url = `https://brouter.gpx.studio?lonlats=${points.map((point) => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile + (privateRoads ? '-private' : '')}&format=geojson&alternativeidx=0`;
|
||||
let response = await fetch('https://graphhopper.gpx.studio/route', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
points: points.map((point) => [point.lon, point.lat]),
|
||||
profile: graphHopperProfile,
|
||||
elevation: true,
|
||||
points_encoded: false,
|
||||
details: graphhopperDetails,
|
||||
custom_model: privateRoads
|
||||
? {}
|
||||
: graphhopperBlockPrivateCustomModels[graphHopperProfile] || {},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
if (error.message.includes('Cannot find point 0')) {
|
||||
throw new Error('toolbar.routing.error.from');
|
||||
} else if (error.message.includes('Cannot find point 1')) {
|
||||
if (points.length == 3) {
|
||||
throw new Error('toolbar.routing.error.via');
|
||||
} else {
|
||||
throw new Error('toolbar.routing.error.to');
|
||||
}
|
||||
} else if (error.hints[0].details.includes('PointDistanceExceededException')) {
|
||||
throw new Error('toolbar.routing.error.distance');
|
||||
} else if (error.hints[0].details.includes('ConnectionNotFoundException')) {
|
||||
throw new Error('toolbar.routing.error.connection');
|
||||
} else {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
let json = await response.json();
|
||||
|
||||
let route: TrackPoint[] = [];
|
||||
let coordinates = json.paths[0].points.coordinates;
|
||||
let details = json.paths[0].details;
|
||||
|
||||
for (let i = 0; i < coordinates.length; i++) {
|
||||
route.push(
|
||||
new TrackPoint({
|
||||
attributes: {
|
||||
lat: coordinates[i][1],
|
||||
lon: coordinates[i][0],
|
||||
},
|
||||
ele: coordinates[i][2] ?? (i > 0 ? route[i - 1].ele : 0),
|
||||
extensions: {},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
for (let key of graphhopperDetails) {
|
||||
let detail = details[key];
|
||||
for (let i = 0; i < detail.length; i++) {
|
||||
for (let j = detail[i][0]; j < detail[i][1] + (i == detail.length - 1); j++) {
|
||||
if (detail[i][2] !== undefined && detail[i][2] !== 'missing') {
|
||||
if (key === 'road_class') {
|
||||
route[j].setExtension('highway', detail[i][2]);
|
||||
} else if (key === 'hike_rating') {
|
||||
const sacScale = hikeRatingToSACScale[detail[i][2]];
|
||||
if (sacScale) {
|
||||
route[j].setExtension('sac_scale', sacScale);
|
||||
}
|
||||
} else if (key === 'mtb_rating') {
|
||||
const mtbScale = mtbRatingToScale[detail[i][2]];
|
||||
if (mtbScale) {
|
||||
route[j].setExtension('mtb_scale', mtbScale);
|
||||
}
|
||||
} else if (key === 'surface' && detail[i][2] !== 'other') {
|
||||
route[j].setExtension('surface', detail[i][2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
async function getBRouterRoute(
|
||||
points: Coordinates[],
|
||||
brouterProfile: string
|
||||
): Promise<TrackPoint[]> {
|
||||
let url = `https://brouter.de/brouter?lonlats=${points.map((point) => `${point.lon.toFixed(8)},${point.lat.toFixed(8)}`).join('|')}&profile=${brouterProfile}&format=geojson&alternativeidx=0`;
|
||||
|
||||
let response = await fetch(url);
|
||||
|
||||
// Check if the response is ok
|
||||
if (!response.ok) {
|
||||
throw new Error(`${await response.text()}`);
|
||||
const error = await response.text();
|
||||
if (error.includes('from-position not mapped in existing datafile')) {
|
||||
throw new Error('toolbar.routing.error.from');
|
||||
} else if (error.includes('via1-position not mapped in existing datafile')) {
|
||||
throw new Error('toolbar.routing.error.via');
|
||||
} else if (error.includes('to-position not mapped in existing datafile')) {
|
||||
throw new Error('toolbar.routing.error.to');
|
||||
} else if (error.includes('Time-out')) {
|
||||
throw new Error('toolbar.routing.error.timeout');
|
||||
} else {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
let geojson = await response.json();
|
||||
@@ -52,14 +228,13 @@ async function getRoute(
|
||||
let tags = messageIdx < messages.length ? getTags(messages[messageIdx][tagIdx]) : {};
|
||||
|
||||
for (let i = 0; i < coordinates.length; i++) {
|
||||
let coord = coordinates[i];
|
||||
route.push(
|
||||
new TrackPoint({
|
||||
attributes: {
|
||||
lat: coord[1],
|
||||
lon: coord[0],
|
||||
lat: coordinates[i][1],
|
||||
lon: coordinates[i][0],
|
||||
},
|
||||
ele: coord[2] ?? (i > 0 ? route[i - 1].ele : 0),
|
||||
ele: coordinates[i][2] ?? (i > 0 ? route[i - 1].ele : 0),
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -90,15 +90,12 @@ You can also use the mouse wheel to zoom in and out on the elevation profile, an
|
||||
{elevationFill}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center -mt-6">
|
||||
<div class="h-10 w-fit">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
panelSize={120}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
### Additional data
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<script>
|
||||
import { HeartHandshake } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <HeartHandshake size="18" class="inline-block align-baseline" /> Help keep the website free (and ad-free)
|
||||
|
||||
Each time you add or move GPS points, our servers calculate the best route on the road network.
|
||||
We also use APIs from <a href="https://maptiler.com" target="_blank">MapTiler</a> to display beautiful maps, retrieve elevation data and allow you to search for places.
|
||||
|
||||
Unfortunately, this is expensive.
|
||||
If you enjoy using this tool and find it valuable, please consider making a small donation to help keep the website free and ad-free.
|
||||
|
||||
Thank you very much for your support! ❤️
|
||||
@@ -1,12 +0,0 @@
|
||||
<script>
|
||||
import { Languages } from '@lucide/svelte';
|
||||
</script>
|
||||
|
||||
## <Languages size="18" class="inline-block align-baseline" /> Translation
|
||||
|
||||
The website is translated by volunteers using a collaborative translation platform.
|
||||
You can contribute by adding or improving translations on our <a href="https://crowdin.com/project/gpxstudio" target="_blank">Crowdin project</a>.
|
||||
|
||||
If you would like to start translating into a new language, please <a href="#contact">get in touch</a>.
|
||||
|
||||
Any help is greatly appreciated!
|
||||
@@ -12,8 +12,8 @@ title: Integration
|
||||
You can use **gpx.studio** to create maps showing your GPX files and embed them in your website.
|
||||
|
||||
All you need is:
|
||||
1. A <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler key</a> to load the map, and
|
||||
1. GPX files hosted on your server or on Google Drive, or accessible via a public URL.
|
||||
1. GPX files hosted on your server or on Google Drive, or accessible via a public URL;
|
||||
1. *Optional:* a <a href="https://cloud.maptiler.com/auth/widget?next=https://cloud.maptiler.com/maps/" target="_blank">MapTiler key</a> to load MapTiler maps.
|
||||
|
||||
You can then play with the configurator below to customize your map and generate the corresponding HTML code.
|
||||
|
||||
|
||||
@@ -56,10 +56,12 @@ Only one basemap can be displayed at a time.
|
||||
- **Points of interest** can be added to the map to show different categories of places, such as shops, restaurants, or accommodations.
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<DocsLayers />
|
||||
<span class="text-sm text-center mt-2">
|
||||
Hover over the map to show the <a href="https://hiking.waymarkedtrails.org" target="_blank">Waymarked Trails hiking</a> overlay on top of the <a href="https://www.maptiler.com/maps/outdoor-topo/" target="_blank">MapTiler Topo</a> basemap.
|
||||
</span>
|
||||
<DocsLayers />
|
||||
<span class="text-sm text-center mt-2">
|
||||
|
||||
Hover over the map to show the <a href="https://hiking.waymarkedtrails.org" target="_blank">Waymarked Trails hiking</a> overlay on top of the <a href="https://www.maptiler.com/maps/outdoor-topo/" target="_blank">MapTiler Topo</a> basemap.
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
A large collection of global and local basemaps and overlays is available in **gpx.studio**, as well as a selection of point-of-interest categories.
|
||||
|
||||
@@ -5,6 +5,7 @@ title: Merge
|
||||
<script>
|
||||
import { Group } from '@lucide/svelte';
|
||||
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
|
||||
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||
</script>
|
||||
|
||||
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||
@@ -15,6 +16,13 @@ To use this tool, you need to [select](../files-and-stats) multiple files, [trac
|
||||
- The second option can be used to create or manage files with multiple [tracks or segments](../gpx).
|
||||
Merging files (or tracks) will result in a single file (or track) containing all tracks (or segments) from the selection.
|
||||
|
||||
<DocsNote>
|
||||
|
||||
Selected items are merged in the order they appear in the files list.
|
||||
Reorder items by drag-and-drop if needed.
|
||||
|
||||
</DocsNote>
|
||||
|
||||
<div class="flex flex-row justify-center">
|
||||
<Merge class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,7 @@ import {
|
||||
defaultOverpassQueries,
|
||||
defaultOverpassTree,
|
||||
defaultTerrainSource,
|
||||
overlays,
|
||||
overpassQueryData,
|
||||
overpassTree,
|
||||
type CustomLayer,
|
||||
type LayerTreeType,
|
||||
} from '$lib/assets/layers';
|
||||
@@ -161,23 +160,42 @@ function getLayerValidator(allowed: Record<string, any>, fallback: string) {
|
||||
: fallback;
|
||||
}
|
||||
|
||||
function filterLayerTree(t: LayerTreeType, allowed: Record<string, any>): LayerTreeType {
|
||||
function filterLayerTree(t: LayerTreeType, allowed: LayerTreeType | undefined): LayerTreeType {
|
||||
const filtered: LayerTreeType = {};
|
||||
Object.entries(t).forEach(([key, value]) => {
|
||||
if (typeof value === 'object') {
|
||||
filtered[key] = filterLayerTree(value, allowed);
|
||||
} else if (
|
||||
allowed.hasOwnProperty(key) ||
|
||||
key.startsWith('custom-') ||
|
||||
key.startsWith('extension-')
|
||||
) {
|
||||
filtered[key] = value;
|
||||
const values = Object.values(t);
|
||||
if (values.length == 0) return filtered;
|
||||
if (typeof values[0] === 'boolean') {
|
||||
if (allowed) {
|
||||
Object.keys(allowed).forEach((key) => {
|
||||
if (Object.hasOwn(t, key)) {
|
||||
filtered[key] = t[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Object.entries(t).forEach(([key, value]) => {
|
||||
if (
|
||||
!Object.hasOwn(filtered, key) &&
|
||||
(key.startsWith('custom-') || key.startsWith('extension-'))
|
||||
) {
|
||||
filtered[key] = value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Object.entries(t).forEach(([key, value]) => {
|
||||
if (typeof value === 'object') {
|
||||
filtered[key] = filterLayerTree(
|
||||
value,
|
||||
typeof allowed === 'object' && typeof allowed[key] === 'object'
|
||||
? allowed[key]
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function getLayerTreeValidator(allowed: Record<string, any>) {
|
||||
function getLayerTreeValidator(allowed: LayerTreeType) {
|
||||
return (value: LayerTreeType) => filterLayerTree(value, allowed);
|
||||
}
|
||||
|
||||
@@ -185,7 +203,7 @@ type DistanceUnits = 'metric' | 'imperial' | 'nautical';
|
||||
type VelocityUnits = 'speed' | 'pace';
|
||||
type TemperatureUnits = 'celsius' | 'fahrenheit';
|
||||
type AdditionalDataset = 'speed' | 'hr' | 'cad' | 'atemp' | 'power';
|
||||
type ElevationFill = 'slope' | 'surface' | undefined;
|
||||
type ElevationFill = 'slope' | 'surface' | 'highway' | undefined;
|
||||
type RoutingProfile =
|
||||
| 'bike'
|
||||
| 'racing_bike'
|
||||
@@ -223,7 +241,7 @@ export const settings = {
|
||||
elevationFill: new Setting<ElevationFill>(
|
||||
'elevationFill',
|
||||
undefined,
|
||||
getValueValidator(['slope', 'surface', undefined], undefined)
|
||||
getValueValidator(['slope', 'surface', 'highway', undefined], undefined)
|
||||
),
|
||||
treeFileView: new Setting<boolean>('fileView', false),
|
||||
minimizeRoutingMenu: new Setting('minimizeRoutingMenu', false),
|
||||
@@ -254,37 +272,37 @@ export const settings = {
|
||||
previousBasemap: new Setting(
|
||||
'previousBasemap',
|
||||
defaultBasemap,
|
||||
getLayerValidator(Object.keys(basemaps), defaultBasemap)
|
||||
getLayerValidator(basemaps, defaultBasemap)
|
||||
),
|
||||
selectedBasemapTree: new Setting(
|
||||
'selectedBasemapTree',
|
||||
defaultBasemapTree,
|
||||
getLayerTreeValidator(basemaps)
|
||||
getLayerTreeValidator(defaultBasemapTree)
|
||||
),
|
||||
currentOverlays: new SettingInitOnFirstRead(
|
||||
'currentOverlays',
|
||||
defaultOverlays,
|
||||
getLayerTreeValidator(overlays)
|
||||
getLayerTreeValidator(defaultOverlayTree)
|
||||
),
|
||||
previousOverlays: new Setting(
|
||||
'previousOverlays',
|
||||
defaultOverlays,
|
||||
getLayerTreeValidator(overlays)
|
||||
getLayerTreeValidator(defaultOverlayTree)
|
||||
),
|
||||
selectedOverlayTree: new Setting(
|
||||
'selectedOverlayTree',
|
||||
defaultOverlayTree,
|
||||
getLayerTreeValidator(overlays)
|
||||
getLayerTreeValidator(defaultOverlayTree)
|
||||
),
|
||||
currentOverpassQueries: new SettingInitOnFirstRead(
|
||||
'currentOverpassQueries',
|
||||
defaultOverpassQueries,
|
||||
getLayerTreeValidator(overpassQueryData)
|
||||
getLayerTreeValidator(overpassTree)
|
||||
),
|
||||
selectedOverpassTree: new Setting(
|
||||
'selectedOverpassTree',
|
||||
defaultOverpassTree,
|
||||
getLayerTreeValidator(overpassQueryData)
|
||||
getLayerTreeValidator(overpassTree)
|
||||
),
|
||||
opacities: new Setting('opacities', defaultOpacities),
|
||||
customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}),
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"ctrl": "Ctrl",
|
||||
"click": "Click",
|
||||
"drag": "Drag",
|
||||
"right_click_drag": "Right-click drag",
|
||||
"metadata": {
|
||||
"button": "Info...",
|
||||
"name": "Name",
|
||||
@@ -190,6 +191,8 @@
|
||||
"from": "The start point is too far from the nearest road",
|
||||
"via": "The via point is too far from the nearest road",
|
||||
"to": "The end point is too far from the nearest road",
|
||||
"distance": "The end point is too far from the start point",
|
||||
"connection": "No connection found between the points",
|
||||
"timeout": "Route calculation took too long, try adding points closer together"
|
||||
}
|
||||
},
|
||||
@@ -300,6 +303,7 @@
|
||||
"switzerland": "Switzerland",
|
||||
"united_kingdom": "United Kingdom",
|
||||
"united_states": "United States",
|
||||
"maptilerStreets": "MapTiler Streets",
|
||||
"maptilerTopo": "MapTiler Topo",
|
||||
"maptilerOutdoors": "MapTiler Outdoors",
|
||||
"maptilerSatellite": "MapTiler Satellite",
|
||||
@@ -489,7 +493,7 @@
|
||||
"email": "Email",
|
||||
"contribute": "Contribute",
|
||||
"supported_by": "supported by",
|
||||
"support_button": "Support gpx.studio on Ko-fi",
|
||||
"features": "Features",
|
||||
"route_planning": "Route planning",
|
||||
"route_planning_description": "An intuitive interface to create itineraries tailored to each sport, based on OpenStreetMap data.",
|
||||
"file_processing": "Advanced file processing",
|
||||
@@ -498,8 +502,15 @@
|
||||
"maps_description": "A large collection of basemaps, overlays and points of interest to help you craft your next outdoor adventure, or visualize your latest achievement.",
|
||||
"data_visualization": "Data visualization",
|
||||
"data_visualization_description": "An interactive elevation profile with detailed statistics to analyze recorded activities and future objectives.",
|
||||
"identity": "Free, ad-free and open source",
|
||||
"identity_description": "The website is free to use, without ads, and the source code is publicly available on GitHub. This is only possible thanks to the incredible support of the community."
|
||||
"philosophy": "Philosophy",
|
||||
"foss": "Free, ad-free and open source",
|
||||
"foss_description": "The website is free to use, without ads, and the source code is publicly available on GitHub.",
|
||||
"privacy": "Privacy-friendly",
|
||||
"privacy_description": "Your GPX files never leave your browser. No tracking, no data collection.",
|
||||
"community": "Made possible by the community",
|
||||
"community_description": "gpx.studio has an amazing community that has covered its costs through donations for years, while shaping the project through feature suggestions, bug reports, and translations into many languages.",
|
||||
"support_button": "Support gpx.studio on Open Collective",
|
||||
"translate_button": "Help translate the website on Crowdin"
|
||||
},
|
||||
"docs": {
|
||||
"translate": "Improve the translation on Crowdin",
|
||||
@@ -524,7 +535,7 @@
|
||||
},
|
||||
"embedding": {
|
||||
"title": "Create your own map",
|
||||
"maptiler_key": "MapTiler key",
|
||||
"maptiler_key": "MapTiler key (optional, only required for MapTiler maps)",
|
||||
"file_urls": "File URLs (separated by commas)",
|
||||
"drive_ids": "Google Drive file IDs (separated by commas)",
|
||||
"basemap": "Basemap",
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
"update": "更新图层"
|
||||
},
|
||||
"opacity": "图层透明度",
|
||||
"terrain": "Terrain source",
|
||||
"terrain": "地形来源",
|
||||
"label": {
|
||||
"basemaps": "底图",
|
||||
"overlays": "叠加层",
|
||||
@@ -326,7 +326,7 @@
|
||||
"usgs": "USGS",
|
||||
"bikerouterGravel": "bikerouter.de Gravel",
|
||||
"cyclOSMlite": "CyclOSM Lite",
|
||||
"mapterhornHillshade": "Mapterhorn Hillshade",
|
||||
"mapterhornHillshade": "山体阴影",
|
||||
"openRailwayMap": "OpenRailwayMap",
|
||||
"swisstopoSlope": "Swisstopo Slope",
|
||||
"swisstopoHiking": "Swisstopo Hiking",
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import DocsContainer from '$lib/components/docs/DocsContainer.svelte';
|
||||
import Logo from '$lib/components/Logo.svelte';
|
||||
import ElevationProfile from '$lib/components/elevation-profile/ElevationProfile.svelte';
|
||||
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
|
||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import {
|
||||
BookOpenText,
|
||||
Heart,
|
||||
HeartHandshake,
|
||||
ChartArea,
|
||||
Map,
|
||||
PencilRuler,
|
||||
PenLine,
|
||||
Route,
|
||||
Scale,
|
||||
HatGlasses,
|
||||
Languages,
|
||||
ExternalLink,
|
||||
} from '@lucide/svelte';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
import { exampleGPXFile } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
|
||||
import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
|
||||
import { currentTool, Tool } from '$lib/components/toolbar/tools';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
data,
|
||||
}: {
|
||||
data: {
|
||||
fundingModule: Promise<any>;
|
||||
translationModule: Promise<any>;
|
||||
};
|
||||
} = $props();
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
let hoveredPoint = writable(null);
|
||||
@@ -53,17 +45,17 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-24 my-24">
|
||||
<div class="-mt-12 sm:mt-0">
|
||||
<div class="px-12 w-full flex flex-col items-center">
|
||||
<div class="flex flex-col gap-6 items-center max-w-3xl">
|
||||
<h1 class="text-4xl sm:text-6xl font-black text-center">
|
||||
<div class="w-full px-12 flex flex-col items-center">
|
||||
<div class="w-full max-w-5xl flex flex-col items-center">
|
||||
<div class="mt-12 flex flex-col xs:items-center gap-12">
|
||||
<div class="flex flex-col xs:items-center gap-6 max-w-3xl">
|
||||
<h1 class="text-4xl xs:text-5xl sm:text-6xl xs:text-center font-black">
|
||||
{i18n._('metadata.home_title')}
|
||||
</h1>
|
||||
<div class="text-lg sm:text-xl text-muted-foreground text-center">
|
||||
<div class="text-lg sm:text-xl text-muted-foreground xs:text-center">
|
||||
{i18n._('metadata.description')}
|
||||
</div>
|
||||
<div class="w-full flex flex-row justify-center gap-3">
|
||||
<div class="w-full flex flex-row xs:justify-center gap-3">
|
||||
<Button
|
||||
data-sveltekit-reload
|
||||
href={getURLForLanguage(i18n.lang, '/app')}
|
||||
@@ -82,193 +74,179 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative overflow-hidden">
|
||||
<enhanced:img
|
||||
src="/src/lib/assets/img/home/routing.png"
|
||||
alt="Screenshot of the gpx.studio map in 3D."
|
||||
class="w-full min-w-[1200px] ml-[20%] -translate-x-[20%]"
|
||||
src="/src/lib/assets/img/docs/getting-started/interface.webp"
|
||||
alt="The gpx.studio interface."
|
||||
class="rounded-xl shadow-2xl w-full"
|
||||
/>
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-background via-transparent to-background"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-12 sm:px-24 w-full flex flex-col items-center">
|
||||
<div
|
||||
class="flex flex-col md:flex-row gap-x-12 gap-y-6 items-center justify-between max-w-5xl"
|
||||
>
|
||||
<div class="markdown text-center">
|
||||
<h1>
|
||||
<Route size="24" class="inline-block align-baseline" />
|
||||
{i18n._('homepage.route_planning')}
|
||||
</h1>
|
||||
<p class="text-muted-foreground">{i18n._('homepage.route_planning_description')}</p>
|
||||
<h2>
|
||||
{i18n._('homepage.features')}
|
||||
</h2>
|
||||
<div class="grid md:grid-cols-2 gap-12 border-t pt-6">
|
||||
<div class="grid md:grid-rows-subgrid md:row-start-1 md:row-end-3 gap-4">
|
||||
<div>
|
||||
<h3>
|
||||
<Route size="20" class="inline-block align-baseline" />
|
||||
{i18n._('homepage.route_planning')}
|
||||
</h3>
|
||||
<p>
|
||||
{i18n._('homepage.route_planning_description')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="p-3 border rounded-xl shadow-xl origin-top-left scale-45 xs:scale-75 md:scale-45 lg:scale-70 absolute top-1.5 left-1.5 bg-background"
|
||||
>
|
||||
<Routing minimizable={false} />
|
||||
</div>
|
||||
<enhanced:img
|
||||
src="/src/lib/assets/img/docs/tools/routing.png"
|
||||
alt="Route planning illustration."
|
||||
class="h-full object-cover rounded-xl shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 w-fit rounded-md border shadow-xl md:shrink-0">
|
||||
<Routing minimizable={false} />
|
||||
<div class="grid md:grid-rows-subgrid md:row-start-1 md:row-end-3 gap-4">
|
||||
<div>
|
||||
<h3>
|
||||
<Map size="20" class="inline-block align-baseline" />
|
||||
{i18n._('homepage.maps')}
|
||||
</h3>
|
||||
<p>{i18n._('homepage.maps_description')}</p>
|
||||
</div>
|
||||
<enhanced:img
|
||||
src="/src/lib/assets/img/home/map-overlay.png"
|
||||
alt="3D map with multiple layers and points of interest."
|
||||
class="h-full object-cover rounded-xl shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid md:grid-rows-subgrid md:row-start-3 md:row-end-5 gap-4">
|
||||
<div>
|
||||
<h3>
|
||||
<ChartArea size="20" class="inline-block align-baseline" />
|
||||
{i18n._('homepage.data_visualization')}
|
||||
</h3>
|
||||
<p>
|
||||
{i18n._('homepage.data_visualization_description')}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-full aspect-3/2 overflow-hidden flex flex-col gap-2 rounded-xl pt-6 pb-4 px-6 bg-secondary/50 border shadow-lg"
|
||||
>
|
||||
<div class="grow">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
/>
|
||||
</div>
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid md:grid-rows-subgrid md:row-start-3 md:row-end-5 gap-4">
|
||||
<div>
|
||||
<h3>
|
||||
<PencilRuler size="20" class="inline-block align-baseline" />
|
||||
{i18n._('homepage.file_processing')}
|
||||
</h3>
|
||||
<p>
|
||||
{i18n._('homepage.file_processing_description')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="p-3 border rounded-xl shadow-xl origin-top-right scale-45 xs:scale-75 md:scale-45 lg:scale-70 absolute top-1.5 right-1.5 bg-background"
|
||||
>
|
||||
<Scissors />
|
||||
</div>
|
||||
<enhanced:img
|
||||
src="/src/lib/assets/img/docs/tools/split.png"
|
||||
alt="Splitting illustration."
|
||||
class="h-full object-cover rounded-xl shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-12 sm:px-24 w-full flex flex-col items-center">
|
||||
<div
|
||||
class="flex flex-col md:flex-row gap-x-12 gap-y-6 items-center justify-between max-w-5xl"
|
||||
>
|
||||
<div class="markdown text-center md:hidden">
|
||||
<h1>
|
||||
<PencilRuler size="24" class="inline-block align-baseline" />
|
||||
{i18n._('homepage.file_processing')}
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
{i18n._('homepage.file_processing_description')}
|
||||
<h2>
|
||||
{i18n._('homepage.philosophy')}
|
||||
</h2>
|
||||
<div class="w-full grid md:grid-cols-2 gap-12 border-t pt-6">
|
||||
<div class="w-full">
|
||||
<h3>
|
||||
<Scale size="20" class="inline-block align-baseline" />
|
||||
{i18n._('homepage.foss')}
|
||||
</h3>
|
||||
<p>
|
||||
{i18n._('homepage.foss_description')}
|
||||
<Button
|
||||
variant="link"
|
||||
href="https://github.com/gpxstudio/gpx.studio"
|
||||
target="_blank"
|
||||
class="p-0 has-[>svg]:p-0 h-fit"
|
||||
>
|
||||
<ExternalLink size="20" class="inline-block align-baseline" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
<div class="relative md:shrink-0 max-w-[400px]">
|
||||
<Toolbar />
|
||||
<div>
|
||||
<h3>
|
||||
<HatGlasses size="20" class="inline-block align-baseline" />
|
||||
{i18n._('homepage.privacy')}
|
||||
</h3>
|
||||
<p>{i18n._('homepage.privacy_description')}</p>
|
||||
</div>
|
||||
<div class="markdown text-center hidden md:block">
|
||||
<h1>
|
||||
<PencilRuler size="24" class="inline-block align-baseline" />
|
||||
{i18n._('homepage.file_processing')}
|
||||
</h1>
|
||||
<p class="text-muted-foreground">
|
||||
{i18n._('homepage.file_processing_description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-12 sm:px-24 w-full flex flex-col items-center">
|
||||
<div
|
||||
class="markdown flex flex-col md:flex-row gap-x-12 gap-y-6 items-center justify-between max-w-5xl"
|
||||
>
|
||||
<div class="markdown text-center">
|
||||
<h1>
|
||||
<Map size="24" class="inline-block align-baseline" />
|
||||
{i18n._('homepage.maps')}
|
||||
</h1>
|
||||
<p class="text-muted-foreground">{i18n._('homepage.maps_description')}</p>
|
||||
</div>
|
||||
<div
|
||||
class="relative w-full max-w-[320px] aspect-square rounded-2xl shadow-xl overflow-clip"
|
||||
>
|
||||
<enhanced:img
|
||||
src="/src/lib/assets/img/home/maptiler-topo.png"
|
||||
alt="MapTiler Topo map screenshot."
|
||||
class="absolute"
|
||||
style="clip-path: inset(0 50% 50% 0);"
|
||||
/>
|
||||
<enhanced:img
|
||||
src="/src/lib/assets/img/home/maptiler-satellite.png"
|
||||
alt="MapTiler Satellite map screenshot."
|
||||
class="absolute"
|
||||
style="clip-path: inset(0 0 50% 50%);"
|
||||
/>
|
||||
<enhanced:img
|
||||
src="/src/lib/assets/img/home/ign.png"
|
||||
alt="IGN map screenshot."
|
||||
class="absolute"
|
||||
style="clip-path: inset(50% 50% 0 0);"
|
||||
/>
|
||||
<enhanced:img
|
||||
src="/src/lib/assets/img/home/cyclosm.png"
|
||||
alt="CyclOSM map screenshot."
|
||||
class="absolute"
|
||||
style="clip-path: inset(50% 0 0 50%);"
|
||||
/>
|
||||
<enhanced:img
|
||||
src="/src/lib/assets/img/home/waymarked.png"
|
||||
alt="Waymarked Trails map screenshot."
|
||||
class="absolute"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-8 md:px-12">
|
||||
<div class="markdown text-center px-4 md:px-12">
|
||||
<h1>
|
||||
<ChartArea size="24" class="inline-block align-baseline" />
|
||||
{i18n._('homepage.data_visualization')}
|
||||
</h1>
|
||||
<p class="text-muted-foreground mb-6">
|
||||
{i18n._('homepage.data_visualization_description')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="h-48 w-full">
|
||||
<ElevationProfile
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
{hoveredPoint}
|
||||
{additionalDatasets}
|
||||
{elevationFill}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="h-10 w-fit">
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
panelSize={192}
|
||||
orientation={'horizontal'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-12 sm:px-24 w-full flex flex-col items-center">
|
||||
<div
|
||||
class="flex flex-col md:flex-row gap-x-12 gap-y-6 items-center justify-between max-w-5xl"
|
||||
>
|
||||
<div class="markdown text-center md:hidden">
|
||||
<h1>
|
||||
<Scale size="24" class="inline-block align-baseline" />
|
||||
{i18n._('homepage.identity')}
|
||||
</h1>
|
||||
<p class="text-muted-foreground">{i18n._('homepage.identity_description')}</p>
|
||||
</div>
|
||||
<a href="https://github.com/gpxstudio/gpx.studio" target="_blank">
|
||||
<Logo class="h-32" company="github" />
|
||||
</a>
|
||||
<div class="markdown text-center hidden md:block">
|
||||
<h1>
|
||||
<Scale size="24" class="inline-block align-baseline" />
|
||||
{i18n._('homepage.identity')}
|
||||
</h1>
|
||||
<p class="text-muted-foreground">{i18n._('homepage.identity_description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-12 w-full">
|
||||
<div class="w-full max-w-7xl mx-auto rounded-2xl shadow-xl overflow-hidden overflow-clip">
|
||||
<enhanced:img
|
||||
src="/src/lib/assets/img/home/map.png"
|
||||
alt="Screenshot of the gpx.studio map in 3D."
|
||||
class="min-w-[800px] ml-[15%] -translate-x-[15%]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="px-12 md:px-24 flex flex-row flex-wrap lg:flex-nowrap items-center justify-center -space-y-0.5 lg:-space-x-6"
|
||||
>
|
||||
<div
|
||||
class="grow max-w-xl flex flex-col items-center gap-6 p-8 border rounded-2xl shadow-xl -rotate-1 lg:rotate-1"
|
||||
>
|
||||
{#await data.fundingModule then fundingModule}
|
||||
<DocsContainer module={fundingModule.default} />
|
||||
{/await}
|
||||
<Button href="https://ko-fi.com/gpxstudio" target="_blank" class="text-base">
|
||||
<Heart size="16" fill="var(--support)" color="var(--support)" />
|
||||
<span>{i18n._('homepage.support_button')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
class="grow max-w-lg mx-6 h-fit bg-background flex flex-col items-center gap-6 p-8 border rounded-2xl shadow-xl rotate-1 lg:-rotate-1"
|
||||
class="md:text-center flex flex-col md:items-center mt-12 mb-24 p-6 border bg-secondary/50 rounded-xl"
|
||||
>
|
||||
{#await data.translationModule then translationModule}
|
||||
<DocsContainer module={translationModule.default} />
|
||||
{/await}
|
||||
<Button href="https://crowdin.com/project/gpxstudio" target="_blank" class="text-base">
|
||||
<PenLine size="16" />
|
||||
<span>{i18n._('homepage.contribute')}</span>
|
||||
</Button>
|
||||
<h3>
|
||||
{i18n._('homepage.community')}
|
||||
</h3>
|
||||
<p class="md:max-w-3/4">{i18n._('homepage.community_description')}</p>
|
||||
<HeartHandshake size="80" class="mt-6 self-center" />
|
||||
<div class="flex flex-row flex-wrap gap-4 justify-center mt-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
href="https://opencollective.com/gpxstudio"
|
||||
target="_blank"
|
||||
class="text-support text-base max-w-full h-auto whitespace-normal"
|
||||
>
|
||||
<span>{i18n._('homepage.support_button')}</span>
|
||||
<Heart size="16" fill="var(--support)" color="var(--support)" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
href="https://crowdin.com/project/gpxstudio"
|
||||
target="_blank"
|
||||
class="text-base max-w-full h-auto whitespace-normal"
|
||||
>
|
||||
<Languages size="16" />
|
||||
<span>{i18n._('homepage.translate_button')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
@reference "../../app.css";
|
||||
|
||||
div :global(h2) {
|
||||
@apply text-center text-4xl font-extrabold mt-24 mb-6;
|
||||
}
|
||||
|
||||
div :global(h3) {
|
||||
@apply text-2xl pt-0 font-semibold mb-3;
|
||||
}
|
||||
|
||||
div :global(p) {
|
||||
@apply text-muted-foreground;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
function getModule(language: string | undefined, guide: string) {
|
||||
language = language ?? 'en';
|
||||
return import(`./../../lib/docs/${language}/home/${guide}.mdx`);
|
||||
}
|
||||
|
||||
export async function load({ params }) {
|
||||
const { language } = params;
|
||||
|
||||
return {
|
||||
fundingModule: getModule(language, 'funding'),
|
||||
translationModule: getModule(language, 'translation'),
|
||||
};
|
||||
}
|
||||
@@ -135,13 +135,12 @@
|
||||
bind:offsetWidth={bottomPanelWidth}
|
||||
class="flex {bottomPanelOrientation == 'vertical'
|
||||
? 'flex-col'
|
||||
: 'flex-row py-2'} gap-1 px-2"
|
||||
: 'flex-row py-2'} gap-1 px-4"
|
||||
style={$elevationProfile ? `height: ${$bottomPanelSize}px` : ''}
|
||||
>
|
||||
<GPXStatistics
|
||||
{gpxStatistics}
|
||||
{slicedGPXStatistics}
|
||||
panelSize={$bottomPanelSize}
|
||||
orientation={bottomPanelOrientation == 'horizontal' ? 'vertical' : 'horizontal'}
|
||||
/>
|
||||
{#if $elevationProfile}
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
return;
|
||||
}
|
||||
embeddingOptions = getMergedEmbeddingOptions(options);
|
||||
if (embeddingOptions.key === '' && embeddingOptions.basemap.startsWith('maptiler')) {
|
||||
embeddingOptions.basemap = 'openStreetMap';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import AlgoliaDocSearch from '$lib/components/AlgoliaDocSearch.svelte';
|
||||
import { getURLForLanguage } from '$lib/utils';
|
||||
import { i18n } from '$lib/i18n.svelte';
|
||||
import { page } from '$app/state';
|
||||
@@ -17,37 +18,40 @@
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="grow px-12 pt-6 pb-12 flex flex-row gap-24">
|
||||
<div
|
||||
class="hidden md:flex flex-col gap-2 w-40 sticky mt-[27px] top-[108px] self-start shrink-0"
|
||||
>
|
||||
{#each Object.keys(guides) as guide}
|
||||
<Button
|
||||
variant="link"
|
||||
href={getURLForLanguage(i18n.lang, `/help/${guide}`)}
|
||||
class="min-h-5 h-fit p-0 w-fit text-muted-foreground hover:text-foreground hover:no-underline font-normal hover:font-semibold items-start whitespace-normal {page
|
||||
.params.guide === guide
|
||||
? 'font-semibold text-foreground'
|
||||
: ''}"
|
||||
>
|
||||
{data.guideTitles[guide]}
|
||||
</Button>
|
||||
{#each guides[guide] as subGuide}
|
||||
<div class="grow flex flex-col items-center p-12">
|
||||
<div class="max-w-5xl flex flex-row gap-24">
|
||||
<div class="hidden md:flex flex-col gap-2 w-40 sticky top-[101px] self-start shrink-0">
|
||||
<div class="mb-2">
|
||||
<AlgoliaDocSearch />
|
||||
</div>
|
||||
{#each Object.keys(guides) as guide}
|
||||
<Button
|
||||
variant="link"
|
||||
href={getURLForLanguage(i18n.lang, `/help/${guide}/${subGuide}`)}
|
||||
class="min-h-5 h-fit p-0 w-fit text-muted-foreground hover:text-foreground hover:no-underline font-normal hover:font-semibold items-start whitespace-normal ml-3 {page
|
||||
.params.guide ===
|
||||
guide + '/' + subGuide
|
||||
href={getURLForLanguage(i18n.lang, `/help/${guide}`)}
|
||||
class="min-h-5 h-fit p-0 w-fit text-muted-foreground hover:text-foreground hover:no-underline font-normal hover:font-semibold items-start whitespace-normal {page
|
||||
.params.guide === guide
|
||||
? 'font-semibold text-foreground'
|
||||
: ''}"
|
||||
>
|
||||
{data.guideTitles[`${guide}/${subGuide}`]}
|
||||
{data.guideTitles[guide]}
|
||||
</Button>
|
||||
{#each guides[guide] as subGuide}
|
||||
<Button
|
||||
variant="link"
|
||||
href={getURLForLanguage(i18n.lang, `/help/${guide}/${subGuide}`)}
|
||||
class="min-h-5 h-fit p-0 w-fit text-muted-foreground hover:text-foreground hover:no-underline font-normal hover:font-semibold items-start whitespace-normal ml-3 {page
|
||||
.params.guide ===
|
||||
guide + '/' + subGuide
|
||||
? 'font-semibold text-foreground'
|
||||
: ''}"
|
||||
>
|
||||
{data.guideTitles[`${guide}/${subGuide}`]}
|
||||
</Button>
|
||||
{/each}
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="grow">
|
||||
{@render children()}
|
||||
</div>
|
||||
<div class="grow">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
href="https://github.com/gpxstudio/gpx.studio/edit/dev/website/src/lib/docs/en/{page
|
||||
.params.guide}.mdx"
|
||||
target="_blank"
|
||||
class="p-0 h-6 ml-auto text-link"
|
||||
class="p-0 has-[>svg]:px-0 h-6 ml-auto text-link"
|
||||
>
|
||||
<PenLine size="16" />
|
||||
Edit this page on GitHub
|
||||
|
||||