mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-10-15 20:08:19 +00:00
Compare commits
161 Commits
layer-hove
...
drop-to-de
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d18f77bd57 | ||
![]() |
d5022c3ce2 | ||
![]() |
db881cbaf1 | ||
![]() |
4cacafa381 | ||
![]() |
c681029288 | ||
![]() |
f7d0bc1250 | ||
![]() |
ce974d7791 | ||
![]() |
01bf1274d9 | ||
![]() |
01a29226e5 | ||
![]() |
c0ac148a97 | ||
![]() |
c6e4796cdb | ||
![]() |
eba6989606 | ||
![]() |
eed13abeb0 | ||
![]() |
c36636652b | ||
![]() |
294ff5bedf | ||
![]() |
58415af7da | ||
![]() |
369c2a5fb6 | ||
![]() |
9eb716e36c | ||
![]() |
4c56468970 | ||
![]() |
9a2541b6f3 | ||
![]() |
2be5c837cb | ||
![]() |
43803717f4 | ||
![]() |
0d4376ee6f | ||
![]() |
7a72e3d44e | ||
![]() |
b8b74cc7de | ||
![]() |
ea3d10fcc3 | ||
![]() |
45bfac4f88 | ||
![]() |
74d37f1d45 | ||
![]() |
195671acb6 | ||
![]() |
2e1ead31ea | ||
![]() |
3af91213fe | ||
![]() |
6585f05ce3 | ||
![]() |
bcc29480c7 | ||
![]() |
c02d96e90f | ||
![]() |
56e4522da7 | ||
![]() |
5592cf47e0 | ||
![]() |
764c5030b9 | ||
![]() |
d4460f95dd | ||
![]() |
1483460ec6 | ||
![]() |
1a10ecc44b | ||
![]() |
f77793b7fe | ||
![]() |
a48da3fcf0 | ||
![]() |
9d13e9bcdc | ||
![]() |
484aeedbb1 | ||
![]() |
534b1ca8db | ||
![]() |
4d16efe62f | ||
![]() |
d3c11f6153 | ||
![]() |
2c62abd3eb | ||
![]() |
a735852898 | ||
![]() |
f94edf3e3a | ||
![]() |
930b4b84ed | ||
![]() |
553bc2e0a3 | ||
![]() |
aa50f1f2b0 | ||
![]() |
a27de23fa4 | ||
![]() |
deedd8c6c2 | ||
![]() |
5c4181498d | ||
![]() |
29a78e8af3 | ||
![]() |
4ebf2b6fa9 | ||
![]() |
1b741c3b2f | ||
![]() |
81484789b5 | ||
![]() |
c63e5cfb6b | ||
![]() |
8e37a308c3 | ||
![]() |
0baa956160 | ||
![]() |
b638863df3 | ||
![]() |
ec7629aea7 | ||
![]() |
3c7f78ae38 | ||
![]() |
4ca749d1cc | ||
![]() |
0883bfed03 | ||
![]() |
130c12bb73 | ||
![]() |
f513aa28ab | ||
![]() |
5e1244cc82 | ||
![]() |
4c17c3ddfe | ||
![]() |
5236fc5191 | ||
![]() |
1d443f0626 | ||
![]() |
c83b32e6ae | ||
![]() |
7adf660b76 | ||
![]() |
9a0d54c684 | ||
![]() |
3e57bdc7c8 | ||
![]() |
3cc4d569f1 | ||
![]() |
dc76c71ae2 | ||
![]() |
1190a471fb | ||
![]() |
84b1a42e30 | ||
![]() |
08ad9b4186 | ||
![]() |
b8128aaf86 | ||
![]() |
fb00220cc8 | ||
![]() |
723a0138b6 | ||
![]() |
10e328f2a3 | ||
![]() |
a1383a7e97 | ||
![]() |
56e229f000 | ||
![]() |
8511a18de1 | ||
![]() |
4b0e49d171 | ||
![]() |
14e9d3319d | ||
![]() |
39e6532c26 | ||
![]() |
b343123ca6 | ||
![]() |
3246742437 | ||
![]() |
3ce5391658 | ||
![]() |
71c88b15c6 | ||
![]() |
25a3df5756 | ||
![]() |
9d5391805d | ||
![]() |
7d801be682 | ||
![]() |
f846b03e74 | ||
![]() |
e48789bb75 | ||
![]() |
30cc709627 | ||
![]() |
9c85a014da | ||
![]() |
f55a3c0224 | ||
![]() |
8985623639 | ||
![]() |
9ba07ce1ed | ||
![]() |
2e50dea71d | ||
![]() |
0757efcb79 | ||
![]() |
5be02d1c36 | ||
![]() |
2612eb2e91 | ||
![]() |
2996f047d3 | ||
![]() |
96836228db | ||
![]() |
a3dc17d780 | ||
![]() |
e733c96c5a | ||
![]() |
1de5e9443e | ||
![]() |
481bc3b8a1 | ||
![]() |
ce5b0d87a9 | ||
![]() |
efa40edc80 | ||
![]() |
8497044473 | ||
![]() |
41ed9c06c1 | ||
![]() |
75dc8512e7 | ||
![]() |
979cdd1bac | ||
![]() |
056e7a3980 | ||
![]() |
47df6d8f80 | ||
![]() |
3cbfaba5e7 | ||
![]() |
24181b932a | ||
![]() |
666693f374 | ||
![]() |
0cb781176e | ||
![]() |
33f3b6cc32 | ||
![]() |
a1b5fe6352 | ||
![]() |
920e7901f4 | ||
![]() |
a23fea3d98 | ||
![]() |
a751ada6c5 | ||
![]() |
1cf1ce762e | ||
![]() |
833f3e58da | ||
![]() |
b9ca55c798 | ||
![]() |
766ebe0275 | ||
![]() |
d939ef2f60 | ||
![]() |
c1faea787a | ||
![]() |
e42dd6e144 | ||
![]() |
e10e2412c4 | ||
![]() |
b5fd8ea09b | ||
![]() |
1a4ae96782 | ||
![]() |
14ed58aaab | ||
![]() |
779c700d13 | ||
![]() |
31a2aa2fee | ||
![]() |
9d403c861f | ||
![]() |
49582dcddd | ||
![]() |
fa30739fd0 | ||
![]() |
3bc9ac4639 | ||
![]() |
84b3d29e2e | ||
![]() |
9327870d54 | ||
![]() |
f36194b336 | ||
![]() |
f34b23253e | ||
![]() |
cfa40238e4 | ||
![]() |
66b57e0013 | ||
![]() |
879b65953f | ||
![]() |
e800b2ebef | ||
![]() |
22e9c76a5b | ||
![]() |
d81d189cdf |
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build website
|
- name: Build website
|
||||||
env:
|
env:
|
||||||
BASE_PATH: '/${{ github.event.repository.name }}'
|
BASE_PATH: ''
|
||||||
run: |
|
run: |
|
||||||
npm run build --prefix website
|
npm run build --prefix website
|
||||||
|
|
||||||
|
@@ -3,11 +3,11 @@
|
|||||||
<img alt="Logo of gpx.studio." src="website/static/logo.svg">
|
<img alt="Logo of gpx.studio." src="website/static/logo.svg">
|
||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
**gpx.studio** is an online tool for creating and editing GPX files.
|
[**gpx.studio**](https://gpx.studio) is an online tool for creating and editing GPX files.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
This repository contains the source code of the new website, currently available [here](https://gpx.studio/gpx.studio).
|
This repository contains the source code of the website.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -72,6 +72,8 @@ This project has been made possible thanks to the following open source projects
|
|||||||
- [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) — beautiful and fast interactive maps
|
- [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) — beautiful and fast interactive maps
|
||||||
- [brouter](https://github.com/abrensch/brouter) — routing engine
|
- [brouter](https://github.com/abrensch/brouter) — routing engine
|
||||||
- [OpenStreetMap](https://www.openstreetmap.org) — map data used by Mapbox and brouter
|
- [OpenStreetMap](https://www.openstreetmap.org) — map data used by Mapbox and brouter
|
||||||
|
- Search:
|
||||||
|
- [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
72
gpx/package-lock.json
generated
72
gpx/package-lock.json
generated
@@ -7,15 +7,16 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "gpx",
|
"name": "gpx",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-xml-parser": "^4.4.0",
|
"fast-xml-parser": "^4.5.0",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"ts-node": "^10.9.2"
|
"ts-node": "^10.9.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/geojson": "^7946.0.14",
|
"@types/geojson": "^7946.0.14",
|
||||||
"@types/node": "^20.14.6",
|
"@types/node": "^20.16.10",
|
||||||
"typescript": "^5.4.5"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cspotcode/source-map-support": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
@@ -29,15 +30,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
|
|
||||||
"version": "0.3.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
|
||||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/resolve-uri": "^3.0.3",
|
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/resolve-uri": {
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
@@ -47,9 +39,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.4.15",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
|
"version": "0.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||||
|
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/resolve-uri": "^3.0.3",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
@@ -78,17 +79,17 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.14.6",
|
"version": "20.16.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||||
"integrity": "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==",
|
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.11.3",
|
"version": "8.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||||
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -97,9 +98,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn-walk": {
|
"node_modules/acorn-walk": {
|
||||||
"version": "8.3.2",
|
"version": "8.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||||
"integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
|
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.11.0"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
@@ -123,9 +127,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fast-xml-parser": {
|
"node_modules/fast-xml-parser": {
|
||||||
"version": "4.4.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz",
|
||||||
"integrity": "sha512-kLY3jFlwIYwBNDojclKsNAC12sfD6NwW74QB2CoNGPvtVxjliYehVunB3HYyNi+n4Tt1dAcgwYvmKF/Z18flqg==",
|
"integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -205,9 +209,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.4.5",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -217,9 +221,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||||
},
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
|
@@ -11,16 +11,17 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-xml-parser": "^4.4.0",
|
"fast-xml-parser": "^4.5.0",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"ts-node": "^10.9.2"
|
"ts-node": "^10.9.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
|
||||||
"build": "tsc"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/geojson": "^7946.0.14",
|
"@types/geojson": "^7946.0.14",
|
||||||
"@types/node": "^20.14.6",
|
"@types/node": "^20.16.10",
|
||||||
"typescript": "^5.4.5"
|
"typescript": "^5.6.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"postinstall": "npm run build"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
292
gpx/src/gpx.ts
292
gpx/src/gpx.ts
@@ -21,6 +21,7 @@ export abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
|
|||||||
abstract getEndTimestamp(): Date | undefined;
|
abstract getEndTimestamp(): Date | undefined;
|
||||||
abstract getStatistics(): GPXStatistics;
|
abstract getStatistics(): GPXStatistics;
|
||||||
abstract getSegments(): TrackSegment[];
|
abstract getSegments(): TrackSegment[];
|
||||||
|
abstract getTrackPoints(): TrackPoint[];
|
||||||
|
|
||||||
abstract toGeoJSON(): GeoJSON.Feature | GeoJSON.Feature[] | GeoJSON.FeatureCollection | GeoJSON.FeatureCollection[];
|
abstract toGeoJSON(): GeoJSON.Feature | GeoJSON.Feature[] | GeoJSON.FeatureCollection | GeoJSON.FeatureCollection[];
|
||||||
|
|
||||||
@@ -66,6 +67,10 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
|
|||||||
return this.children.flatMap((child) => child.getSegments());
|
return this.children.flatMap((child) => child.getSegments());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTrackPoints(): TrackPoint[] {
|
||||||
|
return this.children.flatMap((child) => child.getTrackPoints());
|
||||||
|
}
|
||||||
|
|
||||||
// Producers
|
// Producers
|
||||||
_reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date) {
|
_reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date) {
|
||||||
let og = getOriginal(this);
|
let og = getOriginal(this);
|
||||||
@@ -99,7 +104,7 @@ abstract class GPXTreeLeaf extends GPXTreeElement<GPXTreeLeaf> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A class that represents a GPX file
|
// A class that represents a GPX file
|
||||||
export class GPXFile extends GPXTreeNode<Track>{
|
export class GPXFile extends GPXTreeNode<Track> {
|
||||||
[immerable] = true;
|
[immerable] = true;
|
||||||
|
|
||||||
attributes: GPXFileAttributes;
|
attributes: GPXFileAttributes;
|
||||||
@@ -112,7 +117,15 @@ export class GPXFile extends GPXTreeNode<Track>{
|
|||||||
super();
|
super();
|
||||||
if (gpx) {
|
if (gpx) {
|
||||||
this.attributes = gpx.attributes
|
this.attributes = gpx.attributes
|
||||||
this.metadata = gpx.metadata;
|
this.metadata = gpx.metadata ?? {};
|
||||||
|
this.metadata.author = {
|
||||||
|
name: 'gpx.studio',
|
||||||
|
link: {
|
||||||
|
attributes: {
|
||||||
|
href: 'https://gpx.studio',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : [];
|
this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : [];
|
||||||
this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : [];
|
this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : [];
|
||||||
if (gpx.rte && gpx.rte.length > 0) {
|
if (gpx.rte && gpx.rte.length > 0) {
|
||||||
@@ -350,6 +363,36 @@ export class GPXFile extends GPXTreeNode<Track>{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createArtificialTimestamps(startTime: Date, totalTime: number, trackIndex?: number, segmentIndex?: number) {
|
||||||
|
let lastPoint = undefined;
|
||||||
|
this.trk.forEach((track, index) => {
|
||||||
|
if (trackIndex === undefined || trackIndex === index) {
|
||||||
|
track.createArtificialTimestamps(startTime, totalTime, lastPoint, segmentIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addElevation(elevations: number[], trackIndices?: number[], segmentIndices?: number[], waypointIndices?: number[]) {
|
||||||
|
let index = 0;
|
||||||
|
this.trk.forEach((track, trackIndex) => {
|
||||||
|
if (trackIndices === undefined || trackIndices.includes(trackIndex)) {
|
||||||
|
track.trkseg.forEach((segment, segmentIndex) => {
|
||||||
|
if (segmentIndices === undefined || segmentIndices.includes(segmentIndex)) {
|
||||||
|
segment.trkpt.forEach((point) => {
|
||||||
|
point.ele = elevations[index++];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.wpt.forEach((waypoint, waypointIndex) => {
|
||||||
|
if (waypointIndices === undefined || waypointIndices.includes(waypointIndex)) {
|
||||||
|
waypoint.ele = elevations[index++];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
elevations.splice(0, index);
|
||||||
|
}
|
||||||
|
|
||||||
setStyle(style: LineStyleExtension) {
|
setStyle(style: LineStyleExtension) {
|
||||||
this.trk.forEach((track) => {
|
this.trk.forEach((track) => {
|
||||||
track.setStyle(style);
|
track.setStyle(style);
|
||||||
@@ -422,8 +465,8 @@ export class Track extends GPXTreeNode<TrackSegment> {
|
|||||||
src?: string;
|
src?: string;
|
||||||
link?: Link;
|
link?: Link;
|
||||||
type?: string;
|
type?: string;
|
||||||
trkseg: TrackSegment[];
|
|
||||||
extensions?: TrackExtensions;
|
extensions?: TrackExtensions;
|
||||||
|
trkseg: TrackSegment[];
|
||||||
|
|
||||||
constructor(track?: TrackType & { _data?: any } | Track) {
|
constructor(track?: TrackType & { _data?: any } | Track) {
|
||||||
super();
|
super();
|
||||||
@@ -456,8 +499,8 @@ export class Track extends GPXTreeNode<TrackSegment> {
|
|||||||
src: this.src,
|
src: this.src,
|
||||||
link: cloneJSON(this.link),
|
link: cloneJSON(this.link),
|
||||||
type: this.type,
|
type: this.type,
|
||||||
trkseg: this.trkseg.map((seg) => seg.clone()),
|
|
||||||
extensions: cloneJSON(this.extensions),
|
extensions: cloneJSON(this.extensions),
|
||||||
|
trkseg: this.trkseg.map((seg) => seg.clone()),
|
||||||
_data: cloneJSON(this._data),
|
_data: cloneJSON(this._data),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -501,8 +544,8 @@ export class Track extends GPXTreeNode<TrackSegment> {
|
|||||||
src: this.src,
|
src: this.src,
|
||||||
link: this.link,
|
link: this.link,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
trkseg: this.trkseg.map((seg) => seg.toTrackSegmentType(exclude)),
|
|
||||||
extensions: this.extensions,
|
extensions: this.extensions,
|
||||||
|
trkseg: this.trkseg.map((seg) => seg.toTrackSegmentType(exclude)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,6 +624,17 @@ export class Track extends GPXTreeNode<TrackSegment> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createArtificialTimestamps(startTime: Date, totalTime: number, lastPoint: TrackPoint | undefined, segmentIndex?: number) {
|
||||||
|
this.trkseg.forEach((segment, index) => {
|
||||||
|
if (segmentIndex === undefined || segmentIndex === index) {
|
||||||
|
segment.createArtificialTimestamps(startTime, totalTime, lastPoint);
|
||||||
|
if (segment.trkpt.length > 0) {
|
||||||
|
lastPoint = segment.trkpt[segment.trkpt.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setStyle(style: LineStyleExtension, force: boolean = true) {
|
setStyle(style: LineStyleExtension, force: boolean = true) {
|
||||||
if (!this.extensions) {
|
if (!this.extensions) {
|
||||||
this.extensions = {};
|
this.extensions = {};
|
||||||
@@ -699,6 +753,11 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
|
|
||||||
// extensions
|
// extensions
|
||||||
if (points[i].extensions) {
|
if (points[i].extensions) {
|
||||||
|
if (points[i].extensions["gpxtpx:TrackPointExtension"] && points[i].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"]) {
|
||||||
|
let atemp = points[i].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"];
|
||||||
|
statistics.global.atemp.avg = (statistics.global.atemp.count * statistics.global.atemp.avg + atemp) / (statistics.global.atemp.count + 1);
|
||||||
|
statistics.global.atemp.count++;
|
||||||
|
}
|
||||||
if (points[i].extensions["gpxtpx:TrackPointExtension"] && points[i].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"]) {
|
if (points[i].extensions["gpxtpx:TrackPointExtension"] && points[i].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"]) {
|
||||||
let hr = points[i].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"];
|
let hr = points[i].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"];
|
||||||
statistics.global.hr.avg = (statistics.global.hr.count * statistics.global.hr.avg + hr) / (statistics.global.hr.count + 1);
|
statistics.global.hr.avg = (statistics.global.hr.count * statistics.global.hr.avg + hr) / (statistics.global.hr.count + 1);
|
||||||
@@ -709,17 +768,20 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
statistics.global.cad.avg = (statistics.global.cad.count * statistics.global.cad.avg + cad) / (statistics.global.cad.count + 1);
|
statistics.global.cad.avg = (statistics.global.cad.count * statistics.global.cad.avg + cad) / (statistics.global.cad.count + 1);
|
||||||
statistics.global.cad.count++;
|
statistics.global.cad.count++;
|
||||||
}
|
}
|
||||||
if (points[i].extensions["gpxtpx:TrackPointExtension"] && points[i].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"]) {
|
|
||||||
let atemp = points[i].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"];
|
|
||||||
statistics.global.atemp.avg = (statistics.global.atemp.count * statistics.global.atemp.avg + atemp) / (statistics.global.atemp.count + 1);
|
|
||||||
statistics.global.atemp.count++;
|
|
||||||
}
|
|
||||||
if (points[i].extensions["gpxpx:PowerExtension"] && points[i].extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"]) {
|
if (points[i].extensions["gpxpx:PowerExtension"] && points[i].extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"]) {
|
||||||
let power = points[i].extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"];
|
let power = points[i].extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"];
|
||||||
statistics.global.power.avg = (statistics.global.power.count * statistics.global.power.avg + power) / (statistics.global.power.count + 1);
|
statistics.global.power.avg = (statistics.global.power.count * statistics.global.power.avg + power) / (statistics.global.power.count + 1);
|
||||||
statistics.global.power.count++;
|
statistics.global.power.count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (i > 0 && points[i - 1].extensions && points[i - 1].extensions["gpxtpx:TrackPointExtension"] && points[i - 1].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"] && points[i - 1].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"].surface) {
|
||||||
|
let surface = points[i - 1].extensions["gpxtpx:TrackPointExtension"]["gpxtpx:Extensions"].surface;
|
||||||
|
if (statistics.global.surface[surface] === undefined) {
|
||||||
|
statistics.global.surface[surface] = 0;
|
||||||
|
}
|
||||||
|
statistics.global.surface[surface] += dist;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[statistics.local.slope.segment, statistics.local.slope.length] = this._computeSlopeSegments(statistics);
|
[statistics.local.slope.segment, statistics.local.slope.length] = this._computeSlopeSegments(statistics);
|
||||||
@@ -753,29 +815,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_computeSlopeSegments(statistics: GPXStatistics): [number[], number[]] {
|
_computeSlopeSegments(statistics: GPXStatistics): [number[], number[]] {
|
||||||
// x-coordinates are given by: statistics.local.distance.total[point._data.index] * 1000
|
let simplified = ramerDouglasPeucker(this.trkpt, 20, getElevationDistanceFunction(statistics));
|
||||||
// y-coordinates are given by: point.ele
|
|
||||||
// Compute the distance between point3 and the line defined by point1 and point2
|
|
||||||
function elevationDistance(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint): number {
|
|
||||||
if (point1.ele === undefined || point2.ele === undefined || point3.ele === undefined) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let x1 = statistics.local.distance.total[point1._data.index] * 1000;
|
|
||||||
let x2 = statistics.local.distance.total[point2._data.index] * 1000;
|
|
||||||
let x3 = statistics.local.distance.total[point3._data.index] * 1000;
|
|
||||||
let y1 = point1.ele;
|
|
||||||
let y2 = point2.ele;
|
|
||||||
let y3 = point3.ele;
|
|
||||||
|
|
||||||
let dist = Math.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2));
|
|
||||||
if (dist === 0) {
|
|
||||||
return Math.sqrt(Math.pow(x3 - x1, 2) + Math.pow(y3 - y1, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.abs((y2 - y1) * x3 - (x2 - x1) * y3 + x2 * y1 - y2 * x1) / dist;
|
|
||||||
}
|
|
||||||
|
|
||||||
let simplified = ramerDouglasPeucker(this.trkpt, 20, elevationDistance);
|
|
||||||
|
|
||||||
let slope = [];
|
let slope = [];
|
||||||
let length = [];
|
let length = [];
|
||||||
@@ -784,7 +824,7 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
let start = simplified[i].point._data.index;
|
let start = simplified[i].point._data.index;
|
||||||
let end = simplified[i + 1].point._data.index;
|
let end = simplified[i + 1].point._data.index;
|
||||||
let dist = statistics.local.distance.total[end] - statistics.local.distance.total[start];
|
let dist = statistics.local.distance.total[end] - statistics.local.distance.total[start];
|
||||||
let ele = simplified[i + 1].point.ele - simplified[i].point.ele;
|
let ele = (simplified[i + 1].point.ele ?? 0) - (simplified[i].point.ele ?? 0);
|
||||||
|
|
||||||
for (let j = start; j < end + (i + 1 === simplified.length - 1 ? 1 : 0); j++) {
|
for (let j = start; j < end + (i + 1 === simplified.length - 1 ? 1 : 0); j++) {
|
||||||
slope.push(0.1 * ele / dist);
|
slope.push(0.1 * ele / dist);
|
||||||
@@ -821,6 +861,10 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
return [this];
|
return [this];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTrackPoints(): TrackPoint[] {
|
||||||
|
return this.trkpt;
|
||||||
|
}
|
||||||
|
|
||||||
toGeoJSON(): GeoJSON.Feature {
|
toGeoJSON(): GeoJSON.Feature {
|
||||||
return {
|
return {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
@@ -851,22 +895,30 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
let trkpt = og.trkpt.slice();
|
let trkpt = og.trkpt.slice();
|
||||||
|
|
||||||
if (speed !== undefined || (trkpt.length > 0 && trkpt[0].time !== undefined)) {
|
if (speed !== undefined || (trkpt.length > 0 && trkpt[0].time !== undefined)) {
|
||||||
|
// Must handle timestamps (either segment has timestamps or the new points will have timestamps)
|
||||||
if (start > 0 && trkpt[0].time === undefined) {
|
if (start > 0 && trkpt[0].time === undefined) {
|
||||||
|
// Add timestamps to points before [start, end] because they are missing
|
||||||
trkpt.splice(0, 0, ...withTimestamps(trkpt.splice(0, start), speed, undefined, startTime));
|
trkpt.splice(0, 0, ...withTimestamps(trkpt.splice(0, start), speed, undefined, startTime));
|
||||||
}
|
}
|
||||||
if (points.length > 0) {
|
if (points.length > 0) {
|
||||||
|
// Adapt timestamps of the new points
|
||||||
let last = start > 0 ? trkpt[start - 1] : undefined;
|
let last = start > 0 ? trkpt[start - 1] : undefined;
|
||||||
if (points[0].time === undefined || (points.length > 1 && points[1].time === undefined)) {
|
if (points[0].time === undefined || (points.length > 1 && points[1].time === undefined)) {
|
||||||
|
// Add timestamps to the new points because they are missing
|
||||||
points = withTimestamps(points, speed, last, startTime);
|
points = withTimestamps(points, speed, last, startTime);
|
||||||
} else if (last !== undefined && points[0].time < last.time) {
|
} else if (last !== undefined && points[0].time < last.time) {
|
||||||
|
// Adapt timestamps of the new points because they are too early
|
||||||
points = withShiftedAndCompressedTimestamps(points, speed, 1, last);
|
points = withShiftedAndCompressedTimestamps(points, speed, 1, last);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (end < trkpt.length - 1) {
|
if (end < trkpt.length - 1) {
|
||||||
|
// Adapt timestamps of points after [start, end]
|
||||||
let last = points.length > 0 ? points[points.length - 1] : start > 0 ? trkpt[start - 1] : undefined;
|
let last = points.length > 0 ? points[points.length - 1] : start > 0 ? trkpt[start - 1] : undefined;
|
||||||
if (trkpt[end + 1].time === undefined) {
|
if (trkpt[end + 1].time === undefined) {
|
||||||
|
// Add timestamps to points after [start, end] because they are missing
|
||||||
trkpt.splice(end + 1, 0, ...withTimestamps(trkpt.splice(end + 1), speed, last, startTime));
|
trkpt.splice(end + 1, 0, ...withTimestamps(trkpt.splice(end + 1), speed, last, startTime));
|
||||||
} else if (last !== undefined && trkpt[end + 1].time < last.time) {
|
} else if (last !== undefined && trkpt[end + 1].time < last.time) {
|
||||||
|
// Adapt timestamps of points after [start, end] because they are too early
|
||||||
trkpt.splice(end + 1, 0, ...withShiftedAndCompressedTimestamps(trkpt.splice(end + 1), speed, 1, last));
|
trkpt.splice(end + 1, 0, ...withShiftedAndCompressedTimestamps(trkpt.splice(end + 1), speed, 1, last));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -944,6 +996,14 @@ export class TrackSegment extends GPXTreeLeaf {
|
|||||||
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createArtificialTimestamps(startTime: Date, totalTime: number, lastPoint: TrackPoint | undefined) {
|
||||||
|
let og = getOriginal(this); // Read as much as possible from the original object because it is faster
|
||||||
|
let slope = og._computeSlope();
|
||||||
|
let trkpt = withArtificialTimestamps(og.trkpt, totalTime, lastPoint, startTime, slope);
|
||||||
|
this.trkpt = freeze(trkpt); // Pre-freeze the array, faster as well
|
||||||
|
}
|
||||||
|
|
||||||
setHidden(hidden: boolean) {
|
setHidden(hidden: boolean) {
|
||||||
this._data.hidden = hidden;
|
this._data.hidden = hidden;
|
||||||
}
|
}
|
||||||
@@ -984,6 +1044,10 @@ export class TrackPoint {
|
|||||||
return this.attributes.lon;
|
return this.attributes.lon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTemperature(): number {
|
||||||
|
return this.extensions && this.extensions['gpxtpx:TrackPointExtension'] && this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:atemp'] ? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:atemp'] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
getHeartRate(): number {
|
getHeartRate(): number {
|
||||||
return this.extensions && this.extensions['gpxtpx:TrackPointExtension'] && this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:hr'] ? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:hr'] : undefined;
|
return this.extensions && this.extensions['gpxtpx:TrackPointExtension'] && this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:hr'] ? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:hr'] : undefined;
|
||||||
}
|
}
|
||||||
@@ -992,10 +1056,6 @@ export class TrackPoint {
|
|||||||
return this.extensions && this.extensions['gpxtpx:TrackPointExtension'] && this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:cad'] ? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:cad'] : undefined;
|
return this.extensions && this.extensions['gpxtpx:TrackPointExtension'] && this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:cad'] ? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:cad'] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTemperature(): number {
|
|
||||||
return this.extensions && this.extensions['gpxtpx:TrackPointExtension'] && this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:atemp'] ? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:atemp'] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPower(): number {
|
getPower(): number {
|
||||||
return this.extensions && this.extensions["gpxpx:PowerExtension"] && this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] ? this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] : undefined;
|
return this.extensions && this.extensions["gpxpx:PowerExtension"] && this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] ? this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] : undefined;
|
||||||
}
|
}
|
||||||
@@ -1032,15 +1092,15 @@ export class TrackPoint {
|
|||||||
"gpxpx:PowerExtension": {},
|
"gpxpx:PowerExtension": {},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if (this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"] && !exclude.includes('atemp')) {
|
||||||
|
trkpt.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"] = this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"];
|
||||||
|
}
|
||||||
if (this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"] && !exclude.includes('hr')) {
|
if (this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"] && !exclude.includes('hr')) {
|
||||||
trkpt.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"] = this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"];
|
trkpt.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"] = this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:hr"];
|
||||||
}
|
}
|
||||||
if (this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:cad"] && !exclude.includes('cad')) {
|
if (this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:cad"] && !exclude.includes('cad')) {
|
||||||
trkpt.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:cad"] = this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:cad"];
|
trkpt.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:cad"] = this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:cad"];
|
||||||
}
|
}
|
||||||
if (this.extensions["gpxtpx:TrackPointExtension"] && this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"] && !exclude.includes('atemp')) {
|
|
||||||
trkpt.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"] = this.extensions["gpxtpx:TrackPointExtension"]["gpxtpx:atemp"];
|
|
||||||
}
|
|
||||||
if (this.extensions["gpxpx:PowerExtension"] && this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] && !exclude.includes('power')) {
|
if (this.extensions["gpxpx:PowerExtension"] && this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] && !exclude.includes('power')) {
|
||||||
trkpt.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] = this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"];
|
trkpt.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"] = this.extensions["gpxpx:PowerExtension"]["gpxpx:PowerInWatts"];
|
||||||
}
|
}
|
||||||
@@ -1108,20 +1168,31 @@ export class Waypoint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toWaypointType(exclude: string[] = []): WaypointType {
|
toWaypointType(exclude: string[] = []): WaypointType {
|
||||||
let wpt: WaypointType = {
|
|
||||||
attributes: this.attributes,
|
|
||||||
ele: this.ele,
|
|
||||||
name: this.name,
|
|
||||||
cmt: this.cmt,
|
|
||||||
desc: this.desc,
|
|
||||||
link: this.link,
|
|
||||||
sym: this.sym,
|
|
||||||
type: this.type,
|
|
||||||
};
|
|
||||||
if (!exclude.includes('time')) {
|
if (!exclude.includes('time')) {
|
||||||
wpt = { ...wpt, time: this.time };
|
return {
|
||||||
|
attributes: this.attributes,
|
||||||
|
ele: this.ele,
|
||||||
|
time: this.time,
|
||||||
|
name: this.name,
|
||||||
|
cmt: this.cmt,
|
||||||
|
desc: this.desc,
|
||||||
|
link: this.link,
|
||||||
|
sym: this.sym,
|
||||||
|
type: this.type,
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
attributes: this.attributes,
|
||||||
|
ele: this.ele,
|
||||||
|
name: this.name,
|
||||||
|
cmt: this.cmt,
|
||||||
|
desc: this.desc,
|
||||||
|
link: this.link,
|
||||||
|
sym: this.sym,
|
||||||
|
type: this.type,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return wpt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clone(): Waypoint {
|
clone(): Waypoint {
|
||||||
@@ -1168,6 +1239,10 @@ export class GPXStatistics {
|
|||||||
southWest: Coordinates,
|
southWest: Coordinates,
|
||||||
northEast: Coordinates,
|
northEast: Coordinates,
|
||||||
},
|
},
|
||||||
|
atemp: {
|
||||||
|
avg: number,
|
||||||
|
count: number,
|
||||||
|
},
|
||||||
hr: {
|
hr: {
|
||||||
avg: number,
|
avg: number,
|
||||||
count: number,
|
count: number,
|
||||||
@@ -1176,14 +1251,11 @@ export class GPXStatistics {
|
|||||||
avg: number,
|
avg: number,
|
||||||
count: number,
|
count: number,
|
||||||
},
|
},
|
||||||
atemp: {
|
|
||||||
avg: number,
|
|
||||||
count: number,
|
|
||||||
},
|
|
||||||
power: {
|
power: {
|
||||||
avg: number,
|
avg: number,
|
||||||
count: number,
|
count: number,
|
||||||
}
|
},
|
||||||
|
surface: Record<string, number>,
|
||||||
};
|
};
|
||||||
local: {
|
local: {
|
||||||
points: TrackPoint[],
|
points: TrackPoint[],
|
||||||
@@ -1238,6 +1310,10 @@ export class GPXStatistics {
|
|||||||
lon: -180,
|
lon: -180,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
atemp: {
|
||||||
|
avg: 0,
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
hr: {
|
hr: {
|
||||||
avg: 0,
|
avg: 0,
|
||||||
count: 0,
|
count: 0,
|
||||||
@@ -1246,14 +1322,11 @@ export class GPXStatistics {
|
|||||||
avg: 0,
|
avg: 0,
|
||||||
count: 0,
|
count: 0,
|
||||||
},
|
},
|
||||||
atemp: {
|
|
||||||
avg: 0,
|
|
||||||
count: 0,
|
|
||||||
},
|
|
||||||
power: {
|
power: {
|
||||||
avg: 0,
|
avg: 0,
|
||||||
count: 0,
|
count: 0,
|
||||||
}
|
},
|
||||||
|
surface: {},
|
||||||
};
|
};
|
||||||
this.local = {
|
this.local = {
|
||||||
points: [],
|
points: [],
|
||||||
@@ -1315,17 +1388,34 @@ export class GPXStatistics {
|
|||||||
this.global.bounds.northEast.lat = Math.max(this.global.bounds.northEast.lat, other.global.bounds.northEast.lat);
|
this.global.bounds.northEast.lat = Math.max(this.global.bounds.northEast.lat, other.global.bounds.northEast.lat);
|
||||||
this.global.bounds.northEast.lon = Math.max(this.global.bounds.northEast.lon, other.global.bounds.northEast.lon);
|
this.global.bounds.northEast.lon = Math.max(this.global.bounds.northEast.lon, other.global.bounds.northEast.lon);
|
||||||
|
|
||||||
|
this.global.atemp.avg = (this.global.atemp.count * this.global.atemp.avg + other.global.atemp.count * other.global.atemp.avg) / Math.max(1, this.global.atemp.count + other.global.atemp.count);
|
||||||
|
this.global.atemp.count += other.global.atemp.count;
|
||||||
this.global.hr.avg = (this.global.hr.count * this.global.hr.avg + other.global.hr.count * other.global.hr.avg) / Math.max(1, this.global.hr.count + other.global.hr.count);
|
this.global.hr.avg = (this.global.hr.count * this.global.hr.avg + other.global.hr.count * other.global.hr.avg) / Math.max(1, this.global.hr.count + other.global.hr.count);
|
||||||
this.global.hr.count += other.global.hr.count;
|
this.global.hr.count += other.global.hr.count;
|
||||||
this.global.cad.avg = (this.global.cad.count * this.global.cad.avg + other.global.cad.count * other.global.cad.avg) / Math.max(1, this.global.cad.count + other.global.cad.count);
|
this.global.cad.avg = (this.global.cad.count * this.global.cad.avg + other.global.cad.count * other.global.cad.avg) / Math.max(1, this.global.cad.count + other.global.cad.count);
|
||||||
this.global.cad.count += other.global.cad.count;
|
this.global.cad.count += other.global.cad.count;
|
||||||
this.global.atemp.avg = (this.global.atemp.count * this.global.atemp.avg + other.global.atemp.count * other.global.atemp.avg) / Math.max(1, this.global.atemp.count + other.global.atemp.count);
|
|
||||||
this.global.atemp.count += other.global.atemp.count;
|
|
||||||
this.global.power.avg = (this.global.power.count * this.global.power.avg + other.global.power.count * other.global.power.avg) / Math.max(1, this.global.power.count + other.global.power.count);
|
this.global.power.avg = (this.global.power.count * this.global.power.avg + other.global.power.count * other.global.power.avg) / Math.max(1, this.global.power.count + other.global.power.count);
|
||||||
this.global.power.count += other.global.power.count;
|
this.global.power.count += other.global.power.count;
|
||||||
|
Object.keys(other.global.surface).forEach((surface) => {
|
||||||
|
if (this.global.surface[surface] === undefined) {
|
||||||
|
this.global.surface[surface] = 0;
|
||||||
|
}
|
||||||
|
this.global.surface[surface] += other.global.surface[surface];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
slice(start: number, end: number): GPXStatistics {
|
slice(start: number, end: number): GPXStatistics {
|
||||||
|
if (start < 0) {
|
||||||
|
start = 0;
|
||||||
|
} else if (start >= this.local.points.length) {
|
||||||
|
return new GPXStatistics();
|
||||||
|
}
|
||||||
|
if (end < start) {
|
||||||
|
return new GPXStatistics();
|
||||||
|
} else if (end >= this.local.points.length) {
|
||||||
|
end = this.local.points.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
let statistics = new GPXStatistics();
|
let statistics = new GPXStatistics();
|
||||||
|
|
||||||
statistics.local.points = this.local.points.slice(start, end + 1);
|
statistics.local.points = this.local.points.slice(start, end + 1);
|
||||||
@@ -1350,9 +1440,9 @@ export class GPXStatistics {
|
|||||||
statistics.global.bounds.northEast.lat = this.global.bounds.northEast.lat;
|
statistics.global.bounds.northEast.lat = this.global.bounds.northEast.lat;
|
||||||
statistics.global.bounds.northEast.lon = this.global.bounds.northEast.lon;
|
statistics.global.bounds.northEast.lon = this.global.bounds.northEast.lon;
|
||||||
|
|
||||||
|
statistics.global.atemp = this.global.atemp;
|
||||||
statistics.global.hr = this.global.hr;
|
statistics.global.hr = this.global.hr;
|
||||||
statistics.global.cad = this.global.cad;
|
statistics.global.cad = this.global.cad;
|
||||||
statistics.global.atemp = this.global.atemp;
|
|
||||||
statistics.global.power = this.global.power;
|
statistics.global.power = this.global.power;
|
||||||
|
|
||||||
return statistics;
|
return statistics;
|
||||||
@@ -1360,7 +1450,13 @@ export class GPXStatistics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const earthRadius = 6371008.8;
|
const earthRadius = 6371008.8;
|
||||||
export function distance(coord1: Coordinates, coord2: Coordinates): number {
|
export function distance(coord1: TrackPoint | Coordinates, coord2: TrackPoint | Coordinates): number {
|
||||||
|
if (coord1 instanceof TrackPoint) {
|
||||||
|
coord1 = coord1.getCoordinates();
|
||||||
|
}
|
||||||
|
if (coord2 instanceof TrackPoint) {
|
||||||
|
coord2 = coord2.getCoordinates();
|
||||||
|
}
|
||||||
const rad = Math.PI / 180;
|
const rad = Math.PI / 180;
|
||||||
const lat1 = coord1.lat * rad;
|
const lat1 = coord1.lat * rad;
|
||||||
const lat2 = coord2.lat * rad;
|
const lat2 = coord2.lat * rad;
|
||||||
@@ -1369,6 +1465,30 @@ export function distance(coord1: Coordinates, coord2: Coordinates): number {
|
|||||||
return maxMeters;
|
return maxMeters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getElevationDistanceFunction(statistics: GPXStatistics) {
|
||||||
|
// x-coordinates are given by: statistics.local.distance.total[point._data.index] * 1000
|
||||||
|
// y-coordinates are given by: point.ele
|
||||||
|
// Compute the distance between point3 and the line defined by point1 and point2
|
||||||
|
return (point1: TrackPoint, point2: TrackPoint, point3: TrackPoint) => {
|
||||||
|
if (point1.ele === undefined || point2.ele === undefined || point3.ele === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let x1 = statistics.local.distance.total[point1._data.index] * 1000;
|
||||||
|
let x2 = statistics.local.distance.total[point2._data.index] * 1000;
|
||||||
|
let x3 = statistics.local.distance.total[point3._data.index] * 1000;
|
||||||
|
let y1 = point1.ele;
|
||||||
|
let y2 = point2.ele;
|
||||||
|
let y3 = point3.ele;
|
||||||
|
|
||||||
|
let dist = Math.sqrt(Math.pow(y2 - y1, 2) + Math.pow(x2 - x1, 2));
|
||||||
|
if (dist === 0) {
|
||||||
|
return Math.sqrt(Math.pow(x3 - x1, 2) + Math.pow(y3 - y1, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.abs((y2 - y1) * x3 - (x2 - x1) * y3 + x2 * y1 - y2 * x1) / dist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function distanceWindowSmoothing(points: TrackPoint[], distanceWindow: number, accumulate: (index: number) => number, compute: (accumulated: number, start: number, end: number) => number, remove?: (index: number) => number): number[] {
|
function distanceWindowSmoothing(points: TrackPoint[], distanceWindow: number, accumulate: (index: number) => number, compute: (accumulated: number, start: number, end: number) => number, remove?: (index: number) => number): number[] {
|
||||||
let result = [];
|
let result = [];
|
||||||
|
|
||||||
@@ -1412,9 +1532,39 @@ function withTimestamps(points: TrackPoint[], speed: number, lastPoint: TrackPoi
|
|||||||
|
|
||||||
function withShiftedAndCompressedTimestamps(points: TrackPoint[], speed: number, ratio: number, lastPoint: TrackPoint): TrackPoint[] {
|
function withShiftedAndCompressedTimestamps(points: TrackPoint[], speed: number, ratio: number, lastPoint: TrackPoint): TrackPoint[] {
|
||||||
let start = getTimestamp(lastPoint, points[0], speed);
|
let start = getTimestamp(lastPoint, points[0], speed);
|
||||||
|
let last = points[0];
|
||||||
return points.map((point) => {
|
return points.map((point) => {
|
||||||
let pt = point.clone();
|
let pt = point.clone();
|
||||||
pt.time = new Date(start.getTime() + ratio * (point.time.getTime() - points[0].time.getTime()));
|
if (point.time === undefined) {
|
||||||
|
pt.time = getTimestamp(last, point, speed);
|
||||||
|
} else {
|
||||||
|
pt.time = new Date(start.getTime() + ratio * (point.time.getTime() - points[0].time.getTime()));
|
||||||
|
}
|
||||||
|
last = pt;
|
||||||
|
return pt;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function withArtificialTimestamps(points: TrackPoint[], totalTime: number, lastPoint: TrackPoint | undefined, startTime: Date, slope: number[]): TrackPoint[] {
|
||||||
|
let weight = [];
|
||||||
|
let totalWeight = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
|
let dist = distance(points[i].getCoordinates(), points[i + 1].getCoordinates());
|
||||||
|
let w = dist * (0.5 + 1 / (1 + Math.exp(- 0.2 * slope[i])));
|
||||||
|
weight.push(w);
|
||||||
|
totalWeight += w;
|
||||||
|
}
|
||||||
|
|
||||||
|
let last = lastPoint;
|
||||||
|
return points.map((point, i) => {
|
||||||
|
let pt = point.clone();
|
||||||
|
if (i === 0) {
|
||||||
|
pt.time = lastPoint?.time ?? startTime;
|
||||||
|
} else {
|
||||||
|
pt.time = new Date(last.time.getTime() + totalTime * 1000 * weight[i - 1] / totalWeight);
|
||||||
|
}
|
||||||
|
last = pt;
|
||||||
return pt;
|
return pt;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1445,8 +1595,8 @@ function convertRouteToTrack(route: RouteType): Track {
|
|||||||
src: route.src,
|
src: route.src,
|
||||||
link: route.link,
|
link: route.link,
|
||||||
type: route.type,
|
type: route.type,
|
||||||
trkseg: [],
|
|
||||||
extensions: route.extensions,
|
extensions: route.extensions,
|
||||||
|
trkseg: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (route.rtept) {
|
if (route.rtept) {
|
||||||
@@ -1462,6 +1612,8 @@ function convertRouteToTrack(route: RouteType): Track {
|
|||||||
} else {
|
} else {
|
||||||
segment.trkpt.push(new TrackPoint({
|
segment.trkpt.push(new TrackPoint({
|
||||||
attributes: rpt.attributes,
|
attributes: rpt.attributes,
|
||||||
|
ele: rpt.ele,
|
||||||
|
time: rpt.time,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1470,4 +1622,4 @@ function convertRouteToTrack(route: RouteType): Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
@@ -34,7 +34,7 @@ export function parseGPX(gpxData: string): GPXFile {
|
|||||||
return new Date(tagValue);
|
return new Date(tagValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagName === 'gpxtpx:hr' || tagName === 'gpxtpx:cad' || tagName === 'gpxtpx:atemp' || tagName === 'gpxpx:PowerInWatts' || tagName === 'opacity' || tagName === 'weight') {
|
if (tagName === 'gpxtpx:atemp' || tagName === 'gpxtpx:hr' || tagName === 'gpxtpx:cad' || tagName === 'gpxpx:PowerInWatts' || tagName === 'opacity' || tagName === 'weight') {
|
||||||
return parseFloat(tagValue);
|
return parseFloat(tagValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export function parseGPX(gpxData: string): GPXFile {
|
|||||||
return new GPXFile(parsed);
|
return new GPXFile(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildGPX(file: GPXFile, exclude: string[]): string {
|
export function buildGPX(file: GPXFile, exclude: string[] = []): string {
|
||||||
const gpx = file.toGPXFileType(exclude);
|
const gpx = file.toGPXFileType(exclude);
|
||||||
|
|
||||||
const builder = new XMLBuilder({
|
const builder = new XMLBuilder({
|
||||||
@@ -87,14 +87,6 @@ export function buildGPX(file: GPXFile, exclude: string[]): string {
|
|||||||
gpx.attributes['xmlns:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3';
|
gpx.attributes['xmlns:gpxx'] = 'http://www.garmin.com/xmlschemas/GpxExtensions/v3';
|
||||||
gpx.attributes['xmlns:gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1';
|
gpx.attributes['xmlns:gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1';
|
||||||
gpx.attributes['xmlns:gpx_style'] = 'http://www.topografix.com/GPX/gpx_style/0/2';
|
gpx.attributes['xmlns:gpx_style'] = 'http://www.topografix.com/GPX/gpx_style/0/2';
|
||||||
gpx.metadata.author = {
|
|
||||||
name: 'gpx.studio',
|
|
||||||
link: {
|
|
||||||
attributes: {
|
|
||||||
href: 'https://gpx.studio',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (gpx.trk.length === 1 && (gpx.trk[0].name === undefined || gpx.trk[0].name === '')) {
|
if (gpx.trk.length === 1 && (gpx.trk[0].name === undefined || gpx.trk[0].name === '')) {
|
||||||
gpx.trk[0].name = gpx.metadata.name;
|
gpx.trk[0].name = gpx.metadata.name;
|
||||||
@@ -107,6 +99,20 @@ export function buildGPX(file: GPXFile, exclude: string[]): string {
|
|||||||
encoding: "UTF-8",
|
encoding: "UTF-8",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
gpx
|
gpx: removeEmptyElements(gpx)
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeEmptyElements(obj: GPXFileType): GPXFileType {
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj[key] === null || obj[key] === undefined || obj[key] === '' || (Array.isArray(obj[key]) && obj[key].length === 0)) {
|
||||||
|
delete obj[key];
|
||||||
|
} else if (typeof obj[key] === 'object' && !(obj[key] instanceof Date)) {
|
||||||
|
removeEmptyElements(obj[key]);
|
||||||
|
if (Object.keys(obj[key]).length === 0) {
|
||||||
|
delete obj[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
}
|
}
|
@@ -101,4 +101,55 @@ function bearing(latA: number, lonA: number, latB: number, lonB: number): number
|
|||||||
// Finds the bearing from one lat / lon point to another.
|
// Finds the bearing from one lat / lon point to another.
|
||||||
return Math.atan2(Math.sin(lonB - lonA) * Math.cos(latB),
|
return Math.atan2(Math.sin(lonB - lonA) * Math.cos(latB),
|
||||||
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA));
|
Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function projectedPoint(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint | Coordinates): Coordinates {
|
||||||
|
return projected(point1.getCoordinates(), point2.getCoordinates(), point3 instanceof TrackPoint ? point3.getCoordinates() : point3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
|
||||||
|
// Calculates the point on the line defined by p1 and p2
|
||||||
|
// that is closest to the third point, p3.
|
||||||
|
// Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
|
||||||
|
|
||||||
|
const rad = Math.PI / 180;
|
||||||
|
const lat1 = coord1.lat * rad;
|
||||||
|
const lat2 = coord2.lat * rad;
|
||||||
|
const lat3 = coord3.lat * rad;
|
||||||
|
|
||||||
|
const lon1 = coord1.lon * rad;
|
||||||
|
const lon2 = coord2.lon * rad;
|
||||||
|
const lon3 = coord3.lon * rad;
|
||||||
|
|
||||||
|
// Prerequisites for the formulas
|
||||||
|
const bear12 = bearing(lat1, lon1, lat2, lon2);
|
||||||
|
const bear13 = bearing(lat1, lon1, lat3, lon3);
|
||||||
|
let dis13 = distance(lat1, lon1, lat3, lon3);
|
||||||
|
|
||||||
|
let diff = Math.abs(bear13 - bear12);
|
||||||
|
if (diff > Math.PI) {
|
||||||
|
diff = 2 * Math.PI - diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is relative bearing obtuse?
|
||||||
|
if (diff > (Math.PI / 2)) {
|
||||||
|
return coord1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the cross-track distance.
|
||||||
|
let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
|
||||||
|
|
||||||
|
// Is p4 beyond the arc?
|
||||||
|
let dis12 = distance(lat1, lon1, lat2, lon2);
|
||||||
|
let dis14 = Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
|
||||||
|
if (dis14 > dis12) {
|
||||||
|
return coord2;
|
||||||
|
} else {
|
||||||
|
// Determine the closest point (p4) on the great circle
|
||||||
|
const f = dis14 / earthRadius;
|
||||||
|
const lat4 = Math.asin(Math.sin(lat1) * Math.cos(f) + Math.cos(lat1) * Math.sin(f) * Math.cos(bear12));
|
||||||
|
const lon4 = lon1 + Math.atan2(Math.sin(bear12) * Math.sin(f) * Math.cos(lat1), Math.cos(f) - Math.sin(lat1) * Math.sin(lat4));
|
||||||
|
|
||||||
|
return { lat: lat4 / rad, lon: lon4 / rad };
|
||||||
|
}
|
||||||
}
|
}
|
@@ -58,8 +58,8 @@ export type TrackType = {
|
|||||||
src?: string;
|
src?: string;
|
||||||
link?: Link;
|
link?: Link;
|
||||||
type?: string;
|
type?: string;
|
||||||
trkseg: TrackSegmentType[];
|
|
||||||
extensions?: TrackExtensions;
|
extensions?: TrackExtensions;
|
||||||
|
trkseg: TrackSegmentType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TrackExtensions = {
|
export type TrackExtensions = {
|
||||||
@@ -89,9 +89,9 @@ export type TrackPointExtensions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TrackPointExtension = {
|
export type TrackPointExtension = {
|
||||||
|
'gpxtpx:atemp'?: number;
|
||||||
'gpxtpx:hr'?: number;
|
'gpxtpx:hr'?: number;
|
||||||
'gpxtpx:cad'?: number;
|
'gpxtpx:cad'?: number;
|
||||||
'gpxtpx:atemp'?: number;
|
|
||||||
'gpxtpx:Extensions'?: {
|
'gpxtpx:Extensions'?: {
|
||||||
surface?: string;
|
surface?: string;
|
||||||
};
|
};
|
||||||
|
253
gpx/test-data/with_routes.gpx
Normal file
253
gpx/test-data/with_routes.gpx
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="http://www.topografix.com/GPX/1/1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd"
|
||||||
|
xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1"
|
||||||
|
xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3"
|
||||||
|
xmlns:gpx_style="http://www.topografix.com/GPX/gpx_style/0/2" version="1.1" creator="https://gpx.studio">
|
||||||
|
<metadata>
|
||||||
|
<name>with_routes</name>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio"></link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<rte>
|
||||||
|
<name>route 1</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<rtept lat="50.790867" lon="4.404968">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.790714" lon="4.405036">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.790336" lon="4.405259">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.790165" lon="4.405331">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.790008" lon="4.405359">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.789818" lon="4.405359">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.789409" lon="4.40534">
|
||||||
|
<ele>107.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.789105" lon="4.405411">
|
||||||
|
<ele>106.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.788799" lon="4.405527">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.788645" lon="4.405606">
|
||||||
|
<ele>109.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.7885" lon="4.405711">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.78822" lon="4.405959">
|
||||||
|
<ele>112.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.787956" lon="4.406092">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.787814" lon="4.406143">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.787674" lon="4.406177">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.787451" lon="4.406199">
|
||||||
|
<ele>115.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.787297" lon="4.406177">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.78716" lon="4.406098">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.787045" lon="4.405984">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.786683" lon="4.405653">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.786538" lon="4.405543">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.78635" lon="4.405441">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.786275" lon="4.40542">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.786182" lon="4.405435">
|
||||||
|
<ele>116.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.786121" lon="4.405475">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.786042" lon="4.405558">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.785821" lon="4.405925">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.785672" lon="4.406119">
|
||||||
|
<ele>112.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.785516" lon="4.406256">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.785384" lon="4.406364">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.785126" lon="4.406475">
|
||||||
|
<ele>106.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.784697" lon="4.406537">
|
||||||
|
<ele>104.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.784591" lon="4.40657">
|
||||||
|
<ele>104.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.784507" lon="4.406612">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.784435" lon="4.40669">
|
||||||
|
<ele>103.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.784209" lon="4.407148">
|
||||||
|
<ele>103.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.784162" lon="4.407257">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.784077" lon="4.407372">
|
||||||
|
<ele>104.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.784006" lon="4.407435">
|
||||||
|
<ele>105.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.783924" lon="4.407471">
|
||||||
|
<ele>106.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.783837" lon="4.407486">
|
||||||
|
<ele>107.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.783771" lon="4.407472">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.783697" lon="4.407428">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.783626" lon="4.407363">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.783548" lon="4.407274">
|
||||||
|
<ele>110.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.783458" lon="4.407134">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.783123" lon="4.406435">
|
||||||
|
<ele>111.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.782982" lon="4.406168">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.782871" lon="4.406044">
|
||||||
|
<ele>113.3</ele>
|
||||||
|
</rtept>
|
||||||
|
</rte>
|
||||||
|
<rte>
|
||||||
|
<name>route 2</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<rtept lat="50.782212" lon="4.406377">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.782175" lon="4.406413">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.781749" lon="4.407018">
|
||||||
|
<ele>118.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.781654" lon="4.407316">
|
||||||
|
<ele>119.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.781563" lon="4.407764">
|
||||||
|
<ele>121.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.781487" lon="4.407984">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.781422" lon="4.408216">
|
||||||
|
<ele>122.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.781395" lon="4.408508">
|
||||||
|
<ele>123.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.781399" lon="4.409114">
|
||||||
|
<ele>126.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.781367" lon="4.409428">
|
||||||
|
<ele>128.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.781286" lon="4.409607">
|
||||||
|
<ele>129.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.78116" lon="4.409789">
|
||||||
|
<ele>130.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.780804" lon="4.409993">
|
||||||
|
<ele>130.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.780389" lon="4.410334">
|
||||||
|
<ele>131.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.780232" lon="4.410563">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.780094" lon="4.410827">
|
||||||
|
<ele>132.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.779723" lon="4.411582">
|
||||||
|
<ele>135.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.779591" lon="4.411791">
|
||||||
|
<ele>135.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.779125" lon="4.412435">
|
||||||
|
<ele>132.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.778676" lon="4.412979">
|
||||||
|
<ele>134.0</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.778194" lon="4.413466">
|
||||||
|
<ele>136.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.777427" lon="4.414302">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.777165" lon="4.414736">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.776927" lon="4.415201">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.776778" lon="4.415613">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.776553" lon="4.416425">
|
||||||
|
<ele>134.8</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.776326" lon="4.417304">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
</rtept>
|
||||||
|
<rtept lat="50.776129" lon="4.418383">
|
||||||
|
<ele>129.5</ele>
|
||||||
|
</rtept>
|
||||||
|
</rte>
|
||||||
|
</gpx>
|
4725
website/package-lock.json
generated
4725
website/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,61 +13,67 @@
|
|||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^3.2.2",
|
"@sveltejs/adapter-auto": "^3.2.5",
|
||||||
"@sveltejs/adapter-static": "^3.0.2",
|
"@sveltejs/adapter-static": "^3.0.5",
|
||||||
"@sveltejs/enhanced-img": "^0.3.0",
|
"@sveltejs/enhanced-img": "^0.3.8",
|
||||||
"@sveltejs/kit": "^2.5.17",
|
"@sveltejs/kit": "^2.6.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||||
"@types/eslint": "^8.56.10",
|
"@types/eslint": "^8.56.12",
|
||||||
"@types/events": "^3.0.3",
|
"@types/events": "^3.0.3",
|
||||||
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
|
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
|
||||||
"@types/mapbox-gl": "^3.1.0",
|
"@types/mapbox__tilebelt": "^1.0.4",
|
||||||
"@types/node": "^20.14.6",
|
"@types/mapbox-gl": "^3.4.0",
|
||||||
"@types/sanitize-html": "^2.11.0",
|
"@types/node": "^20.16.10",
|
||||||
|
"@types/png.js": "^0.2.3",
|
||||||
|
"@types/sanitize-html": "^2.13.0",
|
||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||||
"@typescript-eslint/parser": "^7.13.1",
|
"@typescript-eslint/parser": "^7.18.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-svelte": "^2.40.0",
|
"eslint-plugin-svelte": "^2.44.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"glob": "^10.4.3",
|
"glob": "^10.4.5",
|
||||||
"mdsvex": "^0.11.2",
|
"mdsvex": "^0.11.2",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.47",
|
||||||
"prettier": "^3.3.2",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-svelte": "^3.2.4",
|
"prettier-plugin-svelte": "^3.2.7",
|
||||||
"svelte": "^4.2.18",
|
"svelte": "^4.2.19",
|
||||||
"svelte-check": "^3.8.1",
|
"svelte-check": "^3.8.6",
|
||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.13",
|
||||||
"tslib": "^2.6.3",
|
"tslib": "^2.7.0",
|
||||||
"tsx": "^4.15.7",
|
"tsx": "^4.19.1",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.6.2",
|
||||||
"vite": "^5.3.1"
|
"vite": "^5.4.8",
|
||||||
|
"vite-plugin-node-polyfills": "^0.22.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@internationalized/date": "^3.5.4",
|
"@docsearch/js": "^3.6.2",
|
||||||
"@mapbox/mapbox-gl-geocoder": "^5.0.2",
|
"@internationalized/date": "^3.5.5",
|
||||||
|
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
|
||||||
"@mapbox/sphericalmercator": "^1.2.0",
|
"@mapbox/sphericalmercator": "^1.2.0",
|
||||||
|
"@mapbox/tilebelt": "^1.0.2",
|
||||||
"@types/mapbox__sphericalmercator": "^1.2.3",
|
"@types/mapbox__sphericalmercator": "^1.2.3",
|
||||||
"bits-ui": "^0.21.12",
|
"bits-ui": "^0.21.15",
|
||||||
"chart.js": "^4.4.3",
|
"chart.js": "^4.4.4",
|
||||||
"chartjs-plugin-zoom": "^2.0.1",
|
"chartjs-plugin-zoom": "^2.0.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dexie": "^4.0.7",
|
"dexie": "^4.0.8",
|
||||||
"gpx": "file:../gpx",
|
"gpx": "file:../gpx",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"lucide-static": "^0.427.0",
|
"lucide-static": "^0.427.0",
|
||||||
"lucide-svelte": "^0.427.0",
|
"lucide-svelte": "^0.427.0",
|
||||||
"mapbox-gl": "^3.4.0",
|
"mapbox-gl": "^3.7.0",
|
||||||
"mapillary-js": "^4.1.2",
|
"mapillary-js": "^4.1.2",
|
||||||
"mode-watcher": "^0.3.1",
|
"mode-watcher": "^0.3.1",
|
||||||
|
"png.js": "^0.2.1",
|
||||||
"sanitize-html": "^2.13.0",
|
"sanitize-html": "^2.13.0",
|
||||||
"sortablejs": "^1.15.2",
|
"sortablejs": "^1.15.3",
|
||||||
"svelte-i18n": "^4.0.0",
|
"svelte-i18n": "^4.0.0",
|
||||||
"svelte-sonner": "^0.3.24",
|
"svelte-sonner": "^0.3.28",
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwind-variants": "^0.2.1"
|
"tailwind-variants": "^0.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
--muted: 210 40% 96.1%;
|
--muted: 210 40% 96.1%;
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--muted-foreground: 215.4 16.3% 45%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
@@ -32,6 +32,8 @@
|
|||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--support: 220 15 130;
|
--support: 220 15 130;
|
||||||
|
|
||||||
|
--link: 0 110 180;
|
||||||
|
|
||||||
--ring: 222.2 84% 4.9%;
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
@@ -67,6 +69,8 @@
|
|||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
--support: 255 110 190;
|
--support: 255 110 190;
|
||||||
|
|
||||||
|
--link: 80 190 255;
|
||||||
|
|
||||||
--ring: hsl(212.7,26.8%,83.9);
|
--ring: hsl(212.7,26.8%,83.9);
|
||||||
}
|
}
|
||||||
|
68
website/src/hooks.server.js
Normal file
68
website/src/hooks.server.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { base } from '$app/paths';
|
||||||
|
import { languages } from '$lib/languages';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
|
export async function handle({ event, resolve }) {
|
||||||
|
const language = event.params.language ?? 'en';
|
||||||
|
const strings = await import(`./locales/${language}.json`);
|
||||||
|
|
||||||
|
const path = event.url.pathname;
|
||||||
|
const page = event.route.id?.replace('/[[language]]', '').split('/')[1] ?? 'home';
|
||||||
|
|
||||||
|
let title = strings.metadata[`${page}_title`];
|
||||||
|
const description = strings.metadata[`description`];
|
||||||
|
|
||||||
|
if (page === 'help' && event.params.guide) {
|
||||||
|
const [guide, subguide] = event.params.guide.split('/');
|
||||||
|
const guideModule = subguide
|
||||||
|
? await import(`./lib/docs/${language}/${guide}/${subguide}.mdx`)
|
||||||
|
: await import(`./lib/docs/${language}/${guide}.mdx`);
|
||||||
|
title = `${title} | ${guideModule.metadata.title}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlTag = `<html lang="${language}" translate="no">`;
|
||||||
|
|
||||||
|
let headTag = `<head>
|
||||||
|
<title>gpx.studio — ${title}</title>
|
||||||
|
<meta name="description" content="${description}" />
|
||||||
|
<meta property="og:title" content="gpx.studio — ${title}" />
|
||||||
|
<meta property="og:description" content="${description}" />
|
||||||
|
<meta name="twitter:title" content="gpx.studio — ${title}" />
|
||||||
|
<meta name="twitter:description" content="${description}" />
|
||||||
|
<meta property="og:image" content="https://gpx.studio${base}/og_logo.png" />
|
||||||
|
<meta property="og:url" content="https://gpx.studio/" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:site_name" content="gpx.studio" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:image" content="https://gpx.studio${base}/og_logo.png" />
|
||||||
|
<meta name="twitter:url" content="https://gpx.studio/" />
|
||||||
|
<meta name="twitter:site" content="@gpxstudio" />
|
||||||
|
<meta name="twitter:creator" content="@gpxstudio" />
|
||||||
|
<link rel="alternate" hreflang="x-default" href="https://gpx.studio${getURLForLanguage('en', path)}" />`;
|
||||||
|
|
||||||
|
for (let lang of Object.keys(languages)) {
|
||||||
|
headTag += ` <link rel="alternate" hreflang="${lang}" href="https://gpx.studio${getURLForLanguage(lang, path)}" />
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringsHTML = page === 'app' ? stringsToHTML(strings) : '';
|
||||||
|
|
||||||
|
const response = await resolve(event, {
|
||||||
|
transformPageChunk: ({ html }) => html.replace('<html>', htmlTag).replace('<head>', headTag).replace('</body>', `<div class="fixed -z-10 text-transparent">${stringsHTML}</div></body>`)
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringsToHTML(dictionary, strings = new Set(), root = true) {
|
||||||
|
Object.values(dictionary).forEach((value) => {
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
stringsToHTML(value, strings, false);
|
||||||
|
} else {
|
||||||
|
strings.add(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (root) {
|
||||||
|
return Array.from(strings).map((string) => `<p>${string}</p>`).join('');
|
||||||
|
}
|
||||||
|
}
|
1864
website/src/lib/assets/custom/bikerouter-gravel.json
Normal file
1864
website/src/lib/assets/custom/bikerouter-gravel.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 2.2 MiB |
@@ -1,9 +1,9 @@
|
|||||||
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
|
||||||
import { TramFront, Utensils, ShoppingBasket, Droplet, ShowerHead, Fuel, CircleParking, Fence, FerrisWheel, Bed, Mountain, Pickaxe, Store, TrainFront, Bus, Ship, Croissant, House, Tent, Wrench, Binoculars } from 'lucide-static';
|
import { TramFront, Utensils, ShoppingBasket, Droplet, ShowerHead, Fuel, CircleParking, Fence, FerrisWheel, Bed, Mountain, Pickaxe, Store, TrainFront, Bus, Ship, Croissant, House, Tent, Wrench, Binoculars } from 'lucide-static';
|
||||||
import { type AnySourceData, type Style } from 'mapbox-gl';
|
import { type Style } from 'mapbox-gl';
|
||||||
import ignFrTopo from './custom/ign-fr-topo.json';
|
import ignFrTopo from './custom/ign-fr-topo.json';
|
||||||
import ignFrPlan from './custom/ign-fr-plan.json';
|
import ignFrPlan from './custom/ign-fr-plan.json';
|
||||||
import ignFrSatellite from './custom/ign-fr-satellite.json';
|
import ignFrSatellite from './custom/ign-fr-satellite.json';
|
||||||
|
import bikerouterGravel from './custom/bikerouter-gravel.json';
|
||||||
|
|
||||||
export const basemaps: { [key: string]: string | Style; } = {
|
export const basemaps: { [key: string]: string | Style; } = {
|
||||||
mapboxOutdoors: 'mapbox://styles/mapbox/outdoors-v12',
|
mapboxOutdoors: 'mapbox://styles/mapbox/outdoors-v12',
|
||||||
@@ -15,7 +15,7 @@ export const basemaps: { [key: string]: string | Style; } = {
|
|||||||
type: 'raster',
|
type: 'raster',
|
||||||
tiles: ['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', 'https://b.tile.openstreetmap.org/{z}/{x}/{y}.png', 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
tiles: ['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', 'https://b.tile.openstreetmap.org/{z}/{x}/{y}.png', 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png'],
|
||||||
tileSize: 256,
|
tileSize: 256,
|
||||||
maxzoom: 18,
|
maxzoom: 19,
|
||||||
attribution: 'Map tiles by <a target="_top" rel="noopener" href="https://tile.openstreetmap.org/">OpenStreetMap tile servers</a>, under the <a target="_top" rel="noopener" href="https://operations.osmfoundation.org/policies/tiles/">tile usage policy</a>. Data by <a target="_top" rel="noopener" href="http://openstreetmap.org">OpenStreetMap</a>'
|
attribution: 'Map tiles by <a target="_top" rel="noopener" href="https://tile.openstreetmap.org/">OpenStreetMap tile servers</a>, under the <a target="_top" rel="noopener" href="https://operations.osmfoundation.org/policies/tiles/">tile usage policy</a>. Data by <a target="_top" rel="noopener" href="http://openstreetmap.org">OpenStreetMap</a>'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -66,7 +66,7 @@ export const basemaps: { [key: string]: string | Style; } = {
|
|||||||
type: 'raster',
|
type: 'raster',
|
||||||
tiles: ['https://a.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', 'https://b.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', 'https://c.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png'],
|
tiles: ['https://a.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', 'https://b.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', 'https://c.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png'],
|
||||||
tileSize: 256,
|
tileSize: 256,
|
||||||
maxzoom: 17,
|
maxzoom: 18,
|
||||||
attribution: '© <a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
attribution: '© <a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -167,23 +167,7 @@ export const basemaps: { [key: string]: string | Style; } = {
|
|||||||
source: 'ignEs',
|
source: 'ignEs',
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
ordnanceSurvey: {
|
ordnanceSurvey: "https://api.os.uk/maps/vector/v1/vts/resources/styles?srs=3857&key=piCT8WysfuC3xLSUW7sGLfrAAJoYDvQz",
|
||||||
version: 8,
|
|
||||||
sources: {
|
|
||||||
ordnanceSurvey: {
|
|
||||||
type: 'raster',
|
|
||||||
tiles: ['https://api.os.uk/maps/raster/v1/zxy/Outdoor_3857/{z}/{x}/{y}.png?key=piCT8WysfuC3xLSUW7sGLfrAAJoYDvQz'],
|
|
||||||
tileSize: 256,
|
|
||||||
maxzoom: 20,
|
|
||||||
attribution: '© <a href="http://www.ordnancesurvey.co.uk/" target="_blank">Ordnance Survey</a>'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
layers: [{
|
|
||||||
id: 'ordnanceSurvey',
|
|
||||||
type: 'raster',
|
|
||||||
source: 'ordnanceSurvey',
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
norwayTopo: {
|
norwayTopo: {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {
|
||||||
@@ -204,18 +188,49 @@ export const basemaps: { [key: string]: string | Style; } = {
|
|||||||
swedenTopo: {
|
swedenTopo: {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {
|
||||||
swedenTopo: {
|
swedenTopoWMTS: {
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
tiles: ['https://api.lantmateriet.se/open/topowebb-ccby/v1/wmts/token/1d54dd14-a28c-38a9-b6f3-b4ebfcc3c204/1.0.0/topowebb/default/3857/{z}/{y}/{x}.png'],
|
tiles: ['https://api.lantmateriet.se/open/topowebb-ccby/v1/wmts/token/1d54dd14-a28c-38a9-b6f3-b4ebfcc3c204/1.0.0/topowebb/default/3857/{z}/{y}/{x}.png'],
|
||||||
tileSize: 256,
|
tileSize: 256,
|
||||||
maxzoom: 14,
|
maxzoom: 14,
|
||||||
attribution: '© <a href="https://www.lantmateriet.se" target="_blank">Lantmäteriet</a>'
|
attribution: '© <a href="https://www.lantmateriet.se" target="_blank">Lantmäteriet</a>'
|
||||||
|
},
|
||||||
|
swedenTopoWMS: {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: ['https://minkarta.lantmateriet.se/map/topowebb?REQUEST=GetMap&SERVICE=WMS&VERSION=1.1.1&FORMAT=image%2Fpng&STYLES=&TRANSPARENT=false&LAYERS=topowebbkartan&TILED=true&MAP_RESOLUTION=180&WIDTH=512&HEIGHT=512&SRS=EPSG%3A3857&BBOX={bbox-epsg-3857}'],
|
||||||
|
tileSize: 512,
|
||||||
|
minzoom: 14,
|
||||||
|
maxzoom: 20,
|
||||||
|
attribution: '© <a href="https://www.lantmateriet.se" target="_blank">Lantmäteriet</a>'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
layers: [{
|
layers: [{
|
||||||
id: 'swedenTopo',
|
id: 'swedenTopoWMTS',
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
source: 'swedenTopo',
|
source: 'swedenTopoWMTS',
|
||||||
|
maxzoom: 14
|
||||||
|
}, {
|
||||||
|
id: 'swedenTopoWMS',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swedenTopoWMS',
|
||||||
|
minzoom: 14
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
swedenSatellite: {
|
||||||
|
version: 8,
|
||||||
|
sources: {
|
||||||
|
swedenSatellite: {
|
||||||
|
type: 'raster',
|
||||||
|
tiles: ['https://minkarta.lantmateriet.se/map/ortofoto?REQUEST=GetMap&SERVICE=WMS&VERSION=1.1.1&FORMAT=image%2Fpng&STYLES=&TRANSPARENT=false&LAYERS=Ortofoto_0.5%2COrtofoto_0.4%2COrtofoto_0.25%2COrtofoto_0.16&TILED=true&MAP_RESOLUTION=180&WIDTH=512&HEIGHT=512&SRS=EPSG%3A3857&BBOX={bbox-epsg-3857}'],
|
||||||
|
tileSize: 512,
|
||||||
|
maxzoom: 22,
|
||||||
|
attribution: '© <a href="https://www.lantmateriet.se" target="_blank">Lantmäteriet</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swedenSatellite',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swedenSatellite',
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
finlandTopo: {
|
finlandTopo: {
|
||||||
@@ -271,144 +286,309 @@ export const basemaps: { [key: string]: string | Style; } = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function extendBasemap(basemap: string | Style): string | Style {
|
export const overlays: { [key: string]: string | Style; } = {
|
||||||
if (typeof basemap === 'object') {
|
|
||||||
basemap["glyphs"] = "mapbox://fonts/mapbox/{fontstack}/{range}.pbf";
|
|
||||||
basemap["sprite"] = `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}`;
|
|
||||||
}
|
|
||||||
return basemap;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.values(basemaps).forEach(extendBasemap);
|
|
||||||
|
|
||||||
export const font: { [key: string]: string; } = {
|
|
||||||
swisstopoVector: 'Frutiger Neue Condensed Regular',
|
|
||||||
swisstopoSatellite: 'Frutiger Neue Condensed Regular',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const overlays: { [key: string]: AnySourceData; } = {
|
|
||||||
cyclOSMlite: {
|
cyclOSMlite: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://a.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png', 'https://b.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png', 'https://c.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
cyclOSMlite: {
|
||||||
maxzoom: 17,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
tiles: ['https://a.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png', 'https://b.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png', 'https://c.tile-cyclosm.openstreetmap.fr/cyclosm-lite/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 17,
|
||||||
|
attribution: '© <a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'cyclOSMlite',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'cyclOSMlite',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
|
bikerouterGravel: bikerouterGravel,
|
||||||
swisstopoSlope: {
|
swisstopoSlope: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.hangneigung-ueber_30/default/current/3857/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoSlope: {
|
||||||
maxzoom: 17,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>',
|
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.hangneigung-ueber_30/default/current/3857/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 17,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoSlope',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoSlope',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
swisstopoHiking: {
|
swisstopoHiking: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swisstlm3d-wanderwege/default/current/3857/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoHiking: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swisstlm3d-wanderwege/default/current/3857/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoHiking',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoHiking',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
swisstopoHikingClosures: {
|
swisstopoHikingClosures: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.wanderland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoHikingClosures: {
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
type: 'raster',
|
||||||
|
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.wanderland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoHikingClosures',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoHikingClosures',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
swisstopoCycling: {
|
swisstopoCycling: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.veloland/default/current/3857/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoCycling: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.veloland/default/current/3857/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoCycling',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoCycling',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
swisstopoCyclingClosures: {
|
swisstopoCyclingClosures: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.veloland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoCyclingClosures: {
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
type: 'raster',
|
||||||
|
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.veloland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoCyclingClosures',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoCyclingClosures',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
swisstopoMountainBike: {
|
swisstopoMountainBike: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.mountainbikeland/default/current/3857/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoMountainBike: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.astra.mountainbikeland/default/current/3857/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoMountainBike',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoMountainBike',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
swisstopoMountainBikeClosures: {
|
swisstopoMountainBikeClosures: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.mountainbikeland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoMountainBikeClosures: {
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
type: 'raster',
|
||||||
|
tiles: ['https://wms.geo.admin.ch/?version=1.3.0&service=WMS&request=GetMap&sld_version=1.1.0&layers=ch.astra.mountainbikeland-sperrungen_umleitungen&format=image/png&STYLE=default&bbox={bbox-epsg-3857}&width=256&height=256&crs=EPSG:3857&transparent=true'],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoMountainBikeClosures',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoMountainBikeClosures',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
swisstopoSkiTouring: {
|
swisstopoSkiTouring: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo-karto.skitouren/default/current/3857/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
swisstopoSkiTouring: {
|
||||||
maxzoom: 17,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
tiles: ['https://wmts.geo.admin.ch/1.0.0/ch.swisstopo-karto.skitouren/default/current/3857/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 17,
|
||||||
|
attribution: '© <a href="https://www.swisstopo.admin.ch" target="_blank">swisstopo</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'swisstopoSkiTouring',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'swisstopoSkiTouring',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
ignFrCadastre: {
|
ignFrCadastre: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TILEMATRIXSET=PM&TILEMATRIX={z}&TILECOL={x}&TILEROW={y}&LAYER=CADASTRALPARCELS.PARCELS&FORMAT=image/png&STYLE=normal'],
|
sources: {
|
||||||
tileSize: 256,
|
ignFrCadastre: {
|
||||||
maxzoom: 20,
|
type: 'raster',
|
||||||
attribution: 'IGN-F/Géoportail'
|
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TILEMATRIXSET=PM&TILEMATRIX={z}&TILECOL={x}&TILEROW={y}&LAYER=CADASTRALPARCELS.PARCELS&FORMAT=image/png&STYLE=normal'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 20,
|
||||||
|
attribution: 'IGN-F/Géoportail'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'ignFrCadastre',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'ignFrCadastre',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
ignSlope: {
|
ignSlope: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TileMatrixSet=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&Layer=GEOGRAPHICALGRIDSYSTEMS.SLOPES.MOUNTAIN&FORMAT=image/png&Style=normal'],
|
sources: {
|
||||||
tileSize: 256,
|
ignSlope: {
|
||||||
maxzoom: 17,
|
type: 'raster',
|
||||||
attribution: 'IGN-F/Géoportail'
|
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TileMatrixSet=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&Layer=GEOGRAPHICALGRIDSYSTEMS.SLOPES.MOUNTAIN&FORMAT=image/png&Style=normal'],
|
||||||
|
tileSize: 256,
|
||||||
|
attribution: 'IGN-F/Géoportail'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'ignSlope',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'ignSlope',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
ignSkiTouring: {
|
ignSkiTouring: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TileMatrixSet=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&Layer=TRACES.RANDO.HIVERNALE&FORMAT=image/png&Style=normal'],
|
sources: {
|
||||||
tileSize: 256,
|
ignSkiTouring: {
|
||||||
maxzoom: 16,
|
type: 'raster',
|
||||||
attribution: 'IGN-F/Géoportail'
|
tiles: ['https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetTile&TileMatrixSet=PM&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&Layer=TRACES.RANDO.HIVERNALE&FORMAT=image/png&Style=normal'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 16,
|
||||||
|
attribution: 'IGN-F/Géoportail'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'ignSkiTouring',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'ignSkiTouring',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
waymarkedTrailsHiking: {
|
waymarkedTrailsHiking: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
waymarkedTrailsHiking: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
tiles: ['https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'waymarkedTrailsHiking',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsHiking',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
waymarkedTrailsCycling: {
|
waymarkedTrailsCycling: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
waymarkedTrailsCycling: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
tiles: ['https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'waymarkedTrailsCycling',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsCycling',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
waymarkedTrailsMTB: {
|
waymarkedTrailsMTB: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://tile.waymarkedtrails.org/mtb/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
waymarkedTrailsMTB: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
tiles: ['https://tile.waymarkedtrails.org/mtb/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'waymarkedTrailsMTB',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsMTB',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
waymarkedTrailsSkating: {
|
waymarkedTrailsSkating: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://tile.waymarkedtrails.org/skating/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
waymarkedTrailsSkating: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
tiles: ['https://tile.waymarkedtrails.org/skating/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'waymarkedTrailsSkating',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsSkating',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
waymarkedTrailsHorseRiding: {
|
waymarkedTrailsHorseRiding: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://tile.waymarkedtrails.org/riding/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
waymarkedTrailsHorseRiding: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
tiles: ['https://tile.waymarkedtrails.org/riding/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'waymarkedTrailsHorseRiding',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsHorseRiding',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
waymarkedTrailsWinter: {
|
waymarkedTrailsWinter: {
|
||||||
type: 'raster',
|
version: 8,
|
||||||
tiles: ['https://tile.waymarkedtrails.org/slopes/{z}/{x}/{y}.png'],
|
sources: {
|
||||||
tileSize: 256,
|
waymarkedTrailsWinter: {
|
||||||
maxzoom: 18,
|
type: 'raster',
|
||||||
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
tiles: ['https://tile.waymarkedtrails.org/slopes/{z}/{x}/{y}.png'],
|
||||||
|
tileSize: 256,
|
||||||
|
maxzoom: 18,
|
||||||
|
attribution: '© <a href="https://www.waymarkedtrails.org" target="_blank">Waymarked Trails</a>'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layers: [{
|
||||||
|
id: 'waymarkedTrailsWinter',
|
||||||
|
type: 'raster',
|
||||||
|
source: 'waymarkedTrailsWinter',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -459,6 +639,7 @@ export const basemapTree: LayerTreeType = {
|
|||||||
},
|
},
|
||||||
sweden: {
|
sweden: {
|
||||||
swedenTopo: true,
|
swedenTopo: true,
|
||||||
|
swedenSatellite: true,
|
||||||
},
|
},
|
||||||
switzerland: {
|
switzerland: {
|
||||||
swisstopoRaster: true,
|
swisstopoRaster: true,
|
||||||
@@ -479,9 +660,6 @@ export const basemapTree: LayerTreeType = {
|
|||||||
export const overlayTree: LayerTreeType = {
|
export const overlayTree: LayerTreeType = {
|
||||||
overlays: {
|
overlays: {
|
||||||
world: {
|
world: {
|
||||||
cyclOSM: {
|
|
||||||
cyclOSMlite: true,
|
|
||||||
},
|
|
||||||
waymarked_trails: {
|
waymarked_trails: {
|
||||||
waymarkedTrailsHiking: true,
|
waymarkedTrailsHiking: true,
|
||||||
waymarkedTrailsCycling: true,
|
waymarkedTrailsCycling: true,
|
||||||
@@ -489,7 +667,9 @@ export const overlayTree: LayerTreeType = {
|
|||||||
waymarkedTrailsSkating: true,
|
waymarkedTrailsSkating: true,
|
||||||
waymarkedTrailsHorseRiding: true,
|
waymarkedTrailsHorseRiding: true,
|
||||||
waymarkedTrailsWinter: true,
|
waymarkedTrailsWinter: true,
|
||||||
}
|
},
|
||||||
|
cyclOSMlite: true,
|
||||||
|
bikerouterGravel: true,
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
@@ -563,9 +743,6 @@ export const defaultBasemap = 'mapboxOutdoors';
|
|||||||
export const defaultOverlays = {
|
export const defaultOverlays = {
|
||||||
overlays: {
|
overlays: {
|
||||||
world: {
|
world: {
|
||||||
cyclOSM: {
|
|
||||||
cyclOSMlite: false,
|
|
||||||
},
|
|
||||||
waymarked_trails: {
|
waymarked_trails: {
|
||||||
waymarkedTrailsHiking: false,
|
waymarkedTrailsHiking: false,
|
||||||
waymarkedTrailsCycling: false,
|
waymarkedTrailsCycling: false,
|
||||||
@@ -573,7 +750,9 @@ export const defaultOverlays = {
|
|||||||
waymarkedTrailsSkating: false,
|
waymarkedTrailsSkating: false,
|
||||||
waymarkedTrailsHorseRiding: false,
|
waymarkedTrailsHorseRiding: false,
|
||||||
waymarkedTrailsWinter: false,
|
waymarkedTrailsWinter: false,
|
||||||
}
|
},
|
||||||
|
cyclOSMlite: false,
|
||||||
|
bikerouterGravel: false,
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
@@ -679,6 +858,7 @@ export const defaultBasemapTree: LayerTreeType = {
|
|||||||
},
|
},
|
||||||
sweden: {
|
sweden: {
|
||||||
swedenTopo: false,
|
swedenTopo: false,
|
||||||
|
swedenSatellite: false,
|
||||||
},
|
},
|
||||||
switzerland: {
|
switzerland: {
|
||||||
swisstopoRaster: false,
|
swisstopoRaster: false,
|
||||||
@@ -699,9 +879,6 @@ export const defaultBasemapTree: LayerTreeType = {
|
|||||||
export const defaultOverlayTree: LayerTreeType = {
|
export const defaultOverlayTree: LayerTreeType = {
|
||||||
overlays: {
|
overlays: {
|
||||||
world: {
|
world: {
|
||||||
cyclOSM: {
|
|
||||||
cyclOSMlite: false,
|
|
||||||
},
|
|
||||||
waymarked_trails: {
|
waymarked_trails: {
|
||||||
waymarkedTrailsHiking: true,
|
waymarkedTrailsHiking: true,
|
||||||
waymarkedTrailsCycling: true,
|
waymarkedTrailsCycling: true,
|
||||||
@@ -709,7 +886,9 @@ export const defaultOverlayTree: LayerTreeType = {
|
|||||||
waymarkedTrailsSkating: false,
|
waymarkedTrailsSkating: false,
|
||||||
waymarkedTrailsHorseRiding: false,
|
waymarkedTrailsHorseRiding: false,
|
||||||
waymarkedTrailsWinter: false,
|
waymarkedTrailsWinter: false,
|
||||||
}
|
},
|
||||||
|
cyclOSMlite: false,
|
||||||
|
bikerouterGravel: false,
|
||||||
},
|
},
|
||||||
countries: {
|
countries: {
|
||||||
france: {
|
france: {
|
||||||
|
60
website/src/lib/components/AlgoliaDocSearch.svelte
Normal file
60
website/src/lib/components/AlgoliaDocSearch.svelte
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import docsearch from '@docsearch/js';
|
||||||
|
import '@docsearch/css';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { _, locale, waitLocale } from 'svelte-i18n';
|
||||||
|
|
||||||
|
let mounted = false;
|
||||||
|
|
||||||
|
function initDocsearch() {
|
||||||
|
docsearch({
|
||||||
|
appId: '21XLD94PE3',
|
||||||
|
apiKey: 'd2c1ed6cb0ed12adb2bd84eb2a38494d',
|
||||||
|
indexName: 'gpx',
|
||||||
|
container: '#docsearch',
|
||||||
|
searchParameters: {
|
||||||
|
facetFilters: ['lang:' + ($locale ?? 'en')]
|
||||||
|
},
|
||||||
|
placeholder: $_('docs.search.search'),
|
||||||
|
disableUserPersonalization: true,
|
||||||
|
translations: {
|
||||||
|
button: {
|
||||||
|
buttonText: $_('docs.search.search'),
|
||||||
|
buttonAriaLabel: $_('docs.search.search')
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
searchBox: {
|
||||||
|
resetButtonTitle: $_('docs.search.clear'),
|
||||||
|
resetButtonAriaLabel: $_('docs.search.clear'),
|
||||||
|
cancelButtonText: $_('docs.search.cancel'),
|
||||||
|
cancelButtonAriaLabel: $_('docs.search.cancel'),
|
||||||
|
searchInputLabel: $_('docs.search.search')
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
selectText: $_('docs.search.to_select'),
|
||||||
|
navigateText: $_('docs.search.to_navigate'),
|
||||||
|
closeText: $_('docs.search.to_close')
|
||||||
|
},
|
||||||
|
noResultsScreen: {
|
||||||
|
noResultsText: $_('docs.search.no_results'),
|
||||||
|
suggestedQueryText: $_('docs.search.no_results_suggestion')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
mounted = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
$: if (mounted && $locale) {
|
||||||
|
waitLocale().then(initDocsearch);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<link rel="preconnect" href="https://21XLD94PE3-dsn.algolia.net" crossorigin />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div id="docsearch" {...$$restProps}></div>
|
26
website/src/lib/components/ButtonWithTooltip.svelte
Normal file
26
website/src/lib/components/ButtonWithTooltip.svelte
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
|
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||||
|
|
||||||
|
export let variant:
|
||||||
|
| 'default'
|
||||||
|
| 'secondary'
|
||||||
|
| 'link'
|
||||||
|
| 'destructive'
|
||||||
|
| 'outline'
|
||||||
|
| 'ghost'
|
||||||
|
| undefined = 'default';
|
||||||
|
export let label: string;
|
||||||
|
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger asChild let:builder>
|
||||||
|
<Button builders={[builder]} {variant} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</Button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content {side}>
|
||||||
|
<span>{label}</span>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
@@ -104,7 +104,8 @@
|
|||||||
line: {
|
line: {
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
borderWidth: 2
|
borderWidth: 2,
|
||||||
|
cubicInterpolationMode: 'monotone'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
interaction: {
|
interaction: {
|
||||||
@@ -624,16 +625,14 @@
|
|||||||
type="single"
|
type="single"
|
||||||
bind:value={elevationFill}
|
bind:value={elevationFill}
|
||||||
>
|
>
|
||||||
<ToggleGroup.Item class="p-0 w-5 h-5" value="slope">
|
<ToggleGroup.Item class="p-0 w-5 h-5" value="slope" aria-label={$_('chart.show_slope')}>
|
||||||
<Tooltip side="left">
|
<Tooltip side="left" label={$_('chart.show_slope')}>
|
||||||
<TriangleRight slot="data" size="15" />
|
<TriangleRight size="15" />
|
||||||
<span slot="tooltip">{$_('chart.show_slope')}</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToggleGroup.Item>
|
</ToggleGroup.Item>
|
||||||
<ToggleGroup.Item class="p-0 w-5 h-5" value="surface">
|
<ToggleGroup.Item class="p-0 w-5 h-5" value="surface" aria-label={$_('chart.show_surface')}>
|
||||||
<Tooltip side="left">
|
<Tooltip side="left" label={$_('chart.show_surface')}>
|
||||||
<BrickWall slot="data" size="15" />
|
<BrickWall size="15" />
|
||||||
<span slot="tooltip">{$_('chart.show_surface')}</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToggleGroup.Item>
|
</ToggleGroup.Item>
|
||||||
</ToggleGroup.Root>
|
</ToggleGroup.Root>
|
||||||
@@ -644,36 +643,40 @@
|
|||||||
type="multiple"
|
type="multiple"
|
||||||
bind:value={additionalDatasets}
|
bind:value={additionalDatasets}
|
||||||
>
|
>
|
||||||
<ToggleGroup.Item class="p-0 w-5 h-5" value="speed">
|
<ToggleGroup.Item
|
||||||
<Tooltip side="left">
|
class="p-0 w-5 h-5"
|
||||||
<Zap slot="data" size="15" />
|
value="speed"
|
||||||
<span slot="tooltip"
|
aria-label={$velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}
|
||||||
>{$velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}</span
|
>
|
||||||
>
|
<Tooltip
|
||||||
|
side="left"
|
||||||
|
label={$velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}
|
||||||
|
>
|
||||||
|
<Zap size="15" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToggleGroup.Item>
|
</ToggleGroup.Item>
|
||||||
<ToggleGroup.Item class="p-0 w-5 h-5" value="hr">
|
<ToggleGroup.Item class="p-0 w-5 h-5" value="hr" aria-label={$_('chart.show_heartrate')}>
|
||||||
<Tooltip side="left">
|
<Tooltip side="left" label={$_('chart.show_heartrate')}>
|
||||||
<HeartPulse slot="data" size="15" />
|
<HeartPulse size="15" />
|
||||||
<span slot="tooltip">{$_('chart.show_heartrate')}</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToggleGroup.Item>
|
</ToggleGroup.Item>
|
||||||
<ToggleGroup.Item class="p-0 w-5 h-5" value="cad">
|
<ToggleGroup.Item class="p-0 w-5 h-5" value="cad" aria-label={$_('chart.show_cadence')}>
|
||||||
<Tooltip side="left">
|
<Tooltip side="left" label={$_('chart.show_cadence')}>
|
||||||
<Orbit slot="data" size="15" />
|
<Orbit size="15" />
|
||||||
<span slot="tooltip">{$_('chart.show_cadence')}</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToggleGroup.Item>
|
</ToggleGroup.Item>
|
||||||
<ToggleGroup.Item class="p-0 w-5 h-5" value="atemp">
|
<ToggleGroup.Item
|
||||||
<Tooltip side="left">
|
class="p-0 w-5 h-5"
|
||||||
<Thermometer slot="data" size="15" />
|
value="atemp"
|
||||||
<span slot="tooltip">{$_('chart.show_temperature')}</span>
|
aria-label={$_('chart.show_temperature')}
|
||||||
|
>
|
||||||
|
<Tooltip side="left" label={$_('chart.show_temperature')}>
|
||||||
|
<Thermometer size="15" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToggleGroup.Item>
|
</ToggleGroup.Item>
|
||||||
<ToggleGroup.Item class="p-0 w-5 h-5" value="power">
|
<ToggleGroup.Item class="p-0 w-5 h-5" value="power" aria-label={$_('chart.show_power')}>
|
||||||
<Tooltip side="left">
|
<Tooltip side="left" label={$_('chart.show_power')}>
|
||||||
<SquareActivity slot="data" size="15" />
|
<SquareActivity size="15" />
|
||||||
<span slot="tooltip">{$_('chart.show_power')}</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToggleGroup.Item>
|
</ToggleGroup.Item>
|
||||||
</ToggleGroup.Root>
|
</ToggleGroup.Root>
|
||||||
|
@@ -63,6 +63,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
hide.time = statistics.global.time.total === 0;
|
hide.time = statistics.global.time.total === 0;
|
||||||
|
hide.surface = !Object.keys(statistics.global.surface).some((key) => key !== 'unknown');
|
||||||
hide.hr = statistics.global.hr.count === 0;
|
hide.hr = statistics.global.hr.count === 0;
|
||||||
hide.cad = statistics.global.cad.count === 0;
|
hide.cad = statistics.global.cad.count === 0;
|
||||||
hide.atemp = statistics.global.atemp.count === 0;
|
hide.atemp = statistics.global.atemp.count === 0;
|
||||||
@@ -86,10 +87,10 @@
|
|||||||
class="fixed left-[50%] top-[50%] z-50 w-fit max-w-full translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-3 border bg-background p-3 shadow-lg rounded-md"
|
class="fixed left-[50%] top-[50%] z-50 w-fit max-w-full translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-3 border bg-background p-3 shadow-lg rounded-md"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="w-full flex flex-row items-center justify-center gap-4 border rounded-md p-2 bg-accent"
|
class="w-full flex flex-row items-center justify-center gap-4 border rounded-md p-2 bg-secondary"
|
||||||
>
|
>
|
||||||
<span>⚠️</span>
|
<span>⚠️</span>
|
||||||
<span class="max-w-96 text-sm">
|
<span class="max-w-[80%] text-sm">
|
||||||
{$_('menu.support_message')}
|
{$_('menu.support_message')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,7 +120,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-xl flex flex-col items-center gap-2">
|
<div
|
||||||
|
class="w-full max-w-xl flex flex-col items-center gap-2 {Object.values(hide).some((v) => !v)
|
||||||
|
? ''
|
||||||
|
: 'hidden'}"
|
||||||
|
>
|
||||||
<div class="w-full flex flex-row items-center gap-3">
|
<div class="w-full flex flex-row items-center gap-3">
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -139,7 +144,7 @@
|
|||||||
{$_('quantities.time')}
|
{$_('quantities.time')}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center gap-1.5">
|
<div class="flex flex-row items-center gap-1.5 {hide.surface ? 'hidden' : ''}">
|
||||||
<Checkbox id="export-surface" bind:checked={exportOptions.surface} />
|
<Checkbox id="export-surface" bind:checked={exportOptions.surface} />
|
||||||
<Label for="export-surface" class="flex flex-row items-center gap-1">
|
<Label for="export-surface" class="flex flex-row items-center gap-1">
|
||||||
<BrickWall size="16" />
|
<BrickWall size="16" />
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
<div class="mx-6 border-t">
|
<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="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">
|
<div class="grow flex flex-col items-start">
|
||||||
<Logo class="h-8" />
|
<Logo class="h-8" width="153" />
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 text-muted-foreground"
|
class="h-6 px-0 text-muted-foreground"
|
||||||
@@ -52,6 +52,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-start gap-1" id="contact">
|
<div class="flex flex-col items-start gap-1" id="contact">
|
||||||
<span class="font-semibold">{$_('homepage.contact')}</span>
|
<span class="font-semibold">{$_('homepage.contact')}</span>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
class="h-6 px-0 text-muted-foreground"
|
||||||
|
href="https://www.reddit.com/r/gpxstudio/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Logo company="reddit" class="h-4 mr-1 fill-muted-foreground" />
|
||||||
|
{$_('homepage.reddit')}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
class="h-6 px-0 text-muted-foreground"
|
class="h-6 px-0 text-muted-foreground"
|
||||||
|
@@ -36,48 +36,46 @@
|
|||||||
? 'flex-col justify-center'
|
? 'flex-col justify-center'
|
||||||
: 'flex-row w-full justify-between'} gap-4 p-0"
|
: 'flex-row w-full justify-between'} gap-4 p-0"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip label={$_('quantities.distance')}>
|
||||||
<span slot="data" class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<Ruler size="18" class="mr-1" />
|
<Ruler size="18" class="mr-1" />
|
||||||
<WithUnits value={statistics.global.distance.total} type="distance" />
|
<WithUnits value={statistics.global.distance.total} type="distance" />
|
||||||
</span>
|
</span>
|
||||||
<span slot="tooltip">{$_('quantities.distance')}</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip label={$_('quantities.elevation_gain_loss')}>
|
||||||
<span slot="data" class="flex flex-row items-center">
|
<span class="flex flex-row items-center">
|
||||||
<MoveUpRight size="18" class="mr-1" />
|
<MoveUpRight size="18" class="mr-1" />
|
||||||
<WithUnits value={statistics.global.elevation.gain} type="elevation" />
|
<WithUnits value={statistics.global.elevation.gain} type="elevation" />
|
||||||
<MoveDownRight size="18" class="mx-1" />
|
<MoveDownRight size="18" class="mx-1" />
|
||||||
<WithUnits value={statistics.global.elevation.loss} type="elevation" />
|
<WithUnits value={statistics.global.elevation.loss} type="elevation" />
|
||||||
</span>
|
</span>
|
||||||
<span slot="tooltip">{$_('quantities.elevation')}</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{#if panelSize > 120 || orientation === 'horizontal'}
|
{#if panelSize > 120 || orientation === 'horizontal'}
|
||||||
<Tooltip class={orientation === 'horizontal' ? 'hidden xs:block' : ''}>
|
<Tooltip
|
||||||
<span slot="data" class="flex flex-row items-center">
|
class={orientation === 'horizontal' ? 'hidden xs:block' : ''}
|
||||||
|
label="{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_(
|
||||||
|
'quantities.moving'
|
||||||
|
)} / {$_('quantities.total')})"
|
||||||
|
>
|
||||||
|
<span class="flex flex-row items-center">
|
||||||
<Zap size="18" class="mr-1" />
|
<Zap size="18" class="mr-1" />
|
||||||
<WithUnits value={statistics.global.speed.moving} type="speed" showUnits={false} />
|
<WithUnits value={statistics.global.speed.moving} type="speed" showUnits={false} />
|
||||||
<span class="mx-1">/</span>
|
<span class="mx-1">/</span>
|
||||||
<WithUnits value={statistics.global.speed.total} type="speed" />
|
<WithUnits value={statistics.global.speed.total} type="speed" />
|
||||||
</span>
|
</span>
|
||||||
<span slot="tooltip"
|
|
||||||
>{$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')} ({$_(
|
|
||||||
'quantities.moving'
|
|
||||||
)} / {$_('quantities.total')})</span
|
|
||||||
>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
{#if panelSize > 160 || orientation === 'horizontal'}
|
{#if panelSize > 160 || orientation === 'horizontal'}
|
||||||
<Tooltip class={orientation === 'horizontal' ? 'hidden md:block' : ''}>
|
<Tooltip
|
||||||
<span slot="data" class="flex flex-row items-center">
|
class={orientation === 'horizontal' ? 'hidden md:block' : ''}
|
||||||
|
label="{$_('quantities.time')} ({$_('quantities.moving')} / {$_('quantities.total')})"
|
||||||
|
>
|
||||||
|
<span class="flex flex-row items-center">
|
||||||
<Timer size="18" class="mr-1" />
|
<Timer size="18" class="mr-1" />
|
||||||
<WithUnits value={statistics.global.time.moving} type="time" />
|
<WithUnits value={statistics.global.time.moving} type="time" />
|
||||||
<span class="mx-1">/</span>
|
<span class="mx-1">/</span>
|
||||||
<WithUnits value={statistics.global.time.total} type="time" />
|
<WithUnits value={statistics.global.time.total} type="time" />
|
||||||
</span>
|
</span>
|
||||||
<span slot="tooltip"
|
|
||||||
>{$_('quantities.time')} ({$_('quantities.moving')} / {$_('quantities.total')})</span
|
|
||||||
>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
@@ -1,70 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { base } from '$app/paths';
|
|
||||||
import { page } from '$app/stores';
|
|
||||||
import { languages } from '$lib/languages';
|
|
||||||
import { _, isLoading } from 'svelte-i18n';
|
|
||||||
|
|
||||||
let location: string;
|
|
||||||
let title: string;
|
|
||||||
|
|
||||||
$: if ($page.route.id) {
|
|
||||||
location = $page.route.id;
|
|
||||||
Object.keys($page.params).forEach((param) => {
|
|
||||||
if (param !== 'language') {
|
|
||||||
location = location.replace(`[${param}]`, $page.params[param]);
|
|
||||||
location = location.replace(`[...${param}]`, $page.params[param]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
title = location.replace('/[...language]', '').split('/')[1] ?? 'home';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
{#if $isLoading}
|
|
||||||
<title>gpx.studio — the online GPX file editor</title>
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="View, edit and create GPX files online with advanced route planning capabilities and file processing tools, beautiful maps and detailed data visualizations."
|
|
||||||
/>
|
|
||||||
<meta property="og:title" content="gpx.studio — the online GPX file editor" />
|
|
||||||
<meta
|
|
||||||
property="og:description"
|
|
||||||
content="View, edit and create GPX files online with advanced route planning capabilities and file processing tools, beautiful maps and detailed data visualizations."
|
|
||||||
/>
|
|
||||||
<meta name="twitter:title" content="gpx.studio — the online GPX file editor" />
|
|
||||||
<meta
|
|
||||||
name="twitter:description"
|
|
||||||
content="View, edit and create GPX files online with advanced route planning capabilities and file processing tools, beautiful maps and detailed data visualizations."
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<title>gpx.studio — {$_(`metadata.${title}_title`)}</title>
|
|
||||||
<meta name="description" content={$_('metadata.description')} />
|
|
||||||
<meta property="og:title" content="gpx.studio — {$_(`metadata.${title}_title`)}" />
|
|
||||||
<meta property="og:description" content={$_('metadata.description')} />
|
|
||||||
<meta name="twitter:title" content="gpx.studio — {$_(`metadata.${title}_title`)}" />
|
|
||||||
<meta name="twitter:description" content={$_('metadata.description')} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<meta property="og:image" content="https://gpx.studio/og_logo.png" />
|
|
||||||
<meta property="og:url" content="https://gpx.studio/" />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:site_name" content="gpx.studio" />
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
|
||||||
<meta name="twitter:image" content="https://gpx.studio/og_logo.png" />
|
|
||||||
<meta name="twitter:url" content="https://gpx.studio/" />
|
|
||||||
<meta name="twitter:site" content="@gpxstudio" />
|
|
||||||
<meta name="twitter:creator" content="@gpxstudio" />
|
|
||||||
|
|
||||||
<link
|
|
||||||
rel="alternate"
|
|
||||||
hreflang="x-default"
|
|
||||||
href="https://gpx.studio{base}{location.replace('/[...language]', '')}"
|
|
||||||
/>
|
|
||||||
{#each Object.keys(languages) as lang}
|
|
||||||
<link
|
|
||||||
rel="alternate"
|
|
||||||
hreflang={lang}
|
|
||||||
href="https://gpx.studio{base}{location.replace('[...language]', lang)}"
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</svelte:head>
|
|
@@ -5,16 +5,14 @@
|
|||||||
export let link: string | undefined = undefined;
|
export let link: string | undefined = undefined;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="text-sm bg-muted rounded border flex flex-row items-center p-2 {$$props.class || ''}">
|
<div
|
||||||
|
class="text-sm bg-secondary rounded border flex flex-row items-center p-2 {$$props.class || ''}"
|
||||||
|
>
|
||||||
<CircleHelp size="16" class="w-4 mr-2 shrink-0 grow-0" />
|
<CircleHelp size="16" class="w-4 mr-2 shrink-0 grow-0" />
|
||||||
<div>
|
<div>
|
||||||
<slot />
|
<slot />
|
||||||
{#if link}
|
{#if link}
|
||||||
<a
|
<a href={link} target="_blank" class="text-sm text-link hover:underline">
|
||||||
href={link}
|
|
||||||
target="_blank"
|
|
||||||
class="text-sm text-blue-500 dark:text-blue-300 hover:underline"
|
|
||||||
>
|
|
||||||
{$_('menu.more')}
|
{$_('menu.more')}
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
import * as Select from '$lib/components/ui/select';
|
import * as Select from '$lib/components/ui/select';
|
||||||
import { languages } from '$lib/languages';
|
import { languages } from '$lib/languages';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
@@ -19,24 +20,32 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select.Root bind:selected>
|
<Select.Root bind:selected>
|
||||||
<Select.Trigger class="w-[180px] {$$props.class ?? ''}">
|
<Select.Trigger class="w-[180px] {$$props.class ?? ''}" aria-label={$_('menu.language')}>
|
||||||
<Languages size="16" />
|
<Languages size="16" />
|
||||||
<Select.Value class="ml-2 mr-auto" />
|
<Select.Value class="ml-2 mr-auto" />
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
{#each Object.entries(languages) as [lang, label]}
|
{#each Object.entries(languages) as [lang, label]}
|
||||||
<a href={getURLForLanguage(lang)}>
|
{#if $page.url.pathname.includes('404')}
|
||||||
<Select.Item value={lang}>{label}</Select.Item>
|
<a href={getURLForLanguage(lang, '/')}>
|
||||||
</a>
|
<Select.Item value={lang}>{label}</Select.Item>
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<a href={getURLForLanguage(lang, $page.url.pathname)}>
|
||||||
|
<Select.Item value={lang}>{label}</Select.Item>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
|
|
||||||
<!-- hidden links for svelte crawling -->
|
<!-- hidden links for svelte crawling -->
|
||||||
<div class="hidden">
|
<div class="hidden">
|
||||||
{#each Object.entries(languages) as [lang, label]}
|
{#if !$page.url.pathname.includes('404')}
|
||||||
<a href={getURLForLanguage(lang)}>
|
{#each Object.entries(languages) as [lang, label]}
|
||||||
{label}
|
<a href={getURLForLanguage(lang, $page.url.pathname)}>
|
||||||
</a>
|
{label}
|
||||||
{/each}
|
</a>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -60,4 +60,14 @@
|
|||||||
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
|
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
|
||||||
/></svg
|
/></svg
|
||||||
>
|
>
|
||||||
|
{:else if company === 'reddit'}
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="fill-foreground {$$restProps.class ?? ''}"
|
||||||
|
><title>Reddit</title><path
|
||||||
|
d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
@@ -52,7 +52,37 @@
|
|||||||
|
|
||||||
let newMap = new mapboxgl.Map({
|
let newMap = new mapboxgl.Map({
|
||||||
container: 'map',
|
container: 'map',
|
||||||
style: { version: 8, sources: {}, layers: [] },
|
style: {
|
||||||
|
version: 8,
|
||||||
|
sources: {},
|
||||||
|
layers: [],
|
||||||
|
imports: [
|
||||||
|
{
|
||||||
|
id: 'glyphs-and-sprite', // make Mapbox glyphs and sprite available to other styles
|
||||||
|
url: '',
|
||||||
|
data: {
|
||||||
|
version: 8,
|
||||||
|
sources: {},
|
||||||
|
layers: [],
|
||||||
|
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
|
||||||
|
sprite: `https://api.mapbox.com/styles/v1/mapbox/outdoors-v12/sprite?access_token=${PUBLIC_MAPBOX_TOKEN}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'basemap',
|
||||||
|
url: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'overlays',
|
||||||
|
url: '',
|
||||||
|
data: {
|
||||||
|
version: 8,
|
||||||
|
sources: {},
|
||||||
|
layers: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
zoom: 0,
|
zoom: 0,
|
||||||
hash: hash,
|
hash: hash,
|
||||||
language,
|
language,
|
||||||
@@ -62,6 +92,7 @@
|
|||||||
});
|
});
|
||||||
newMap.on('load', () => {
|
newMap.on('load', () => {
|
||||||
$map = newMap; // only set the store after the map has loaded
|
$map = newMap; // only set the store after the map has loaded
|
||||||
|
window._map = newMap; // entry point for extensions
|
||||||
scaleControl.setUnit($distanceUnits);
|
scaleControl.setUnit($distanceUnits);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,15 +109,42 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (geocoder) {
|
if (geocoder) {
|
||||||
newMap.addControl(
|
let geocoder = new MapboxGeocoder({
|
||||||
new MapboxGeocoder({
|
mapboxgl: mapboxgl,
|
||||||
accessToken: mapboxgl.accessToken,
|
enableEventLogging: false,
|
||||||
mapboxgl: mapboxgl,
|
collapsed: true,
|
||||||
collapsed: true,
|
flyTo: fitBoundsOptions,
|
||||||
flyTo: fitBoundsOptions,
|
language,
|
||||||
language
|
localGeocoder: () => [],
|
||||||
})
|
localGeocoderOnly: true,
|
||||||
);
|
externalGeocoder: (query: string) =>
|
||||||
|
fetch(
|
||||||
|
`https://nominatim.openstreetmap.org/search?format=json&q=${query}&limit=5&accept-language=${language}`
|
||||||
|
)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
return data.map((result: any) => {
|
||||||
|
return {
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [result.lon, result.lat]
|
||||||
|
},
|
||||||
|
place_name: result.display_name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let onKeyDown = geocoder._onKeyDown;
|
||||||
|
geocoder._onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
// Trigger search on Enter key only
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
onKeyDown.apply(geocoder, [{ target: geocoder._inputEl }]);
|
||||||
|
} else if (geocoder._typeahead.data.length > 0) {
|
||||||
|
geocoder._typeahead.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
newMap.addControl(geocoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (geolocate) {
|
if (geolocate) {
|
||||||
@@ -111,10 +169,12 @@
|
|||||||
tileSize: 512,
|
tileSize: 512,
|
||||||
maxzoom: 14
|
maxzoom: 14
|
||||||
});
|
});
|
||||||
newMap.setTerrain({
|
if (newMap.getPitch() > 0) {
|
||||||
source: 'mapbox-dem',
|
newMap.setTerrain({
|
||||||
exaggeration: newMap.getPitch() > 0 ? 1 : 0
|
source: 'mapbox-dem',
|
||||||
});
|
exaggeration: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
newMap.setFog({
|
newMap.setFog({
|
||||||
color: 'rgb(186, 210, 235)',
|
color: 'rgb(186, 210, 235)',
|
||||||
'high-color': 'rgb(36, 92, 223)',
|
'high-color': 'rgb(36, 92, 223)',
|
||||||
@@ -128,18 +188,7 @@
|
|||||||
exaggeration: 1
|
exaggeration: 1
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
newMap.setTerrain({
|
newMap.setTerrain(null);
|
||||||
source: 'mapbox-dem',
|
|
||||||
exaggeration: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// add dummy layer to place the overlay layers below
|
|
||||||
newMap.addLayer({
|
|
||||||
id: 'overlays',
|
|
||||||
type: 'background',
|
|
||||||
paint: {
|
|
||||||
'background-color': 'rgba(0, 0, 0, 0)'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -21,7 +21,7 @@
|
|||||||
Thermometer,
|
Thermometer,
|
||||||
Sun,
|
Sun,
|
||||||
Moon,
|
Moon,
|
||||||
Layers3,
|
Layers,
|
||||||
GalleryVertical,
|
GalleryVertical,
|
||||||
Languages,
|
Languages,
|
||||||
Settings,
|
Settings,
|
||||||
@@ -41,7 +41,8 @@
|
|||||||
FileStack,
|
FileStack,
|
||||||
FileX,
|
FileX,
|
||||||
BookOpenText,
|
BookOpenText,
|
||||||
ChartArea
|
ChartArea,
|
||||||
|
Maximize
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -54,7 +55,8 @@
|
|||||||
editMetadata,
|
editMetadata,
|
||||||
editStyle,
|
editStyle,
|
||||||
exportState,
|
exportState,
|
||||||
ExportState
|
ExportState,
|
||||||
|
centerMapOnSelection
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
copied,
|
copied,
|
||||||
@@ -126,13 +128,13 @@
|
|||||||
<div
|
<div
|
||||||
class="w-fit flex flex-row items-center justify-center p-1 bg-background rounded-b-md md:rounded-md pointer-events-auto shadow-md"
|
class="w-fit flex flex-row items-center justify-center p-1 bg-background rounded-b-md md:rounded-md pointer-events-auto shadow-md"
|
||||||
>
|
>
|
||||||
<a href="./" target="_blank">
|
<a href="./" target="_blank" class="shrink-0">
|
||||||
<Logo class="h-5 mt-0.5 mx-2 md:hidden" iconOnly={true} />
|
<Logo class="h-5 mt-0.5 mx-2 md:hidden" iconOnly={true} width="16" />
|
||||||
<Logo class="h-5 mt-0.5 mx-2 hidden md:block" />
|
<Logo class="h-5 mt-0.5 mx-2 hidden md:block" width="96" />
|
||||||
</a>
|
</a>
|
||||||
<Menubar.Root class="border-none h-fit p-0">
|
<Menubar.Root class="border-none h-fit p-0">
|
||||||
<Menubar.Menu>
|
<Menubar.Menu>
|
||||||
<Menubar.Trigger>
|
<Menubar.Trigger aria-label={$_('gpx.file')}>
|
||||||
<File size="18" class="md:hidden" />
|
<File size="18" class="md:hidden" />
|
||||||
<span class="hidden md:block">{$_('gpx.file')}</span>
|
<span class="hidden md:block">{$_('gpx.file')}</span>
|
||||||
</Menubar.Trigger>
|
</Menubar.Trigger>
|
||||||
@@ -185,7 +187,7 @@
|
|||||||
</Menubar.Content>
|
</Menubar.Content>
|
||||||
</Menubar.Menu>
|
</Menubar.Menu>
|
||||||
<Menubar.Menu>
|
<Menubar.Menu>
|
||||||
<Menubar.Trigger>
|
<Menubar.Trigger aria-label={$_('menu.edit')}>
|
||||||
<FilePen size="18" class="md:hidden" />
|
<FilePen size="18" class="md:hidden" />
|
||||||
<span class="hidden md:block">{$_('menu.edit')}</span>
|
<span class="hidden md:block">{$_('menu.edit')}</span>
|
||||||
</Menubar.Trigger>
|
</Menubar.Trigger>
|
||||||
@@ -241,12 +243,47 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<Shortcut key="H" ctrl={true} />
|
<Shortcut key="H" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
|
{#if $verticalFileView}
|
||||||
|
{#if $selection.getSelected().some((item) => item instanceof ListFileItem)}
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Item
|
||||||
|
on:click={() => dbUtils.addNewTrack($selection.getSelected()[0].getFileId())}
|
||||||
|
disabled={$selection.size !== 1}
|
||||||
|
>
|
||||||
|
<Plus size="16" class="mr-1" />
|
||||||
|
{$_('menu.new_track')}
|
||||||
|
</Menubar.Item>
|
||||||
|
{:else if $selection.getSelected().some((item) => item instanceof ListTrackItem)}
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Item
|
||||||
|
on:click={() => {
|
||||||
|
let item = $selection.getSelected()[0];
|
||||||
|
dbUtils.addNewSegment(item.getFileId(), item.getTrackIndex());
|
||||||
|
}}
|
||||||
|
disabled={$selection.size !== 1}
|
||||||
|
>
|
||||||
|
<Plus size="16" class="mr-1" />
|
||||||
|
{$_('menu.new_segment')}
|
||||||
|
</Menubar.Item>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item on:click={selectAll} disabled={$fileObservers.size == 0}>
|
<Menubar.Item on:click={selectAll} disabled={$fileObservers.size == 0}>
|
||||||
<FileStack size="16" class="mr-1" />
|
<FileStack size="16" class="mr-1" />
|
||||||
{$_('menu.select_all')}
|
{$_('menu.select_all')}
|
||||||
<Shortcut key="A" ctrl={true} />
|
<Shortcut key="A" ctrl={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
|
<Menubar.Item
|
||||||
|
on:click={() => {
|
||||||
|
if ($selection.size > 0) {
|
||||||
|
centerMapOnSelection();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Maximize size="16" class="mr-1" />
|
||||||
|
{$_('menu.center')}
|
||||||
|
<Shortcut key="⏎" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
{#if $verticalFileView}
|
{#if $verticalFileView}
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item on:click={copySelection} disabled={$selection.size === 0}>
|
<Menubar.Item on:click={copySelection} disabled={$selection.size === 0}>
|
||||||
@@ -280,7 +317,7 @@
|
|||||||
</Menubar.Content>
|
</Menubar.Content>
|
||||||
</Menubar.Menu>
|
</Menubar.Menu>
|
||||||
<Menubar.Menu>
|
<Menubar.Menu>
|
||||||
<Menubar.Trigger>
|
<Menubar.Trigger aria-label={$_('menu.view')}>
|
||||||
<View size="18" class="md:hidden" />
|
<View size="18" class="md:hidden" />
|
||||||
<span class="hidden md:block">{$_('menu.view')}</span>
|
<span class="hidden md:block">{$_('menu.view')}</span>
|
||||||
</Menubar.Trigger>
|
</Menubar.Trigger>
|
||||||
@@ -318,14 +355,14 @@
|
|||||||
</Menubar.Content>
|
</Menubar.Content>
|
||||||
</Menubar.Menu>
|
</Menubar.Menu>
|
||||||
<Menubar.Menu>
|
<Menubar.Menu>
|
||||||
<Menubar.Trigger>
|
<Menubar.Trigger aria-label={$_('menu.settings')}>
|
||||||
<Settings size="18" class="md:hidden" />
|
<Settings size="18" class="md:hidden" />
|
||||||
<span class="hidden md:block">
|
<span class="hidden md:block">
|
||||||
{$_('menu.settings')}
|
{$_('menu.settings')}
|
||||||
</span>
|
</span>
|
||||||
</Menubar.Trigger>
|
</Menubar.Trigger>
|
||||||
<Menubar.Content class="border-none"
|
<Menubar.Content class="border-none">
|
||||||
><Menubar.Sub>
|
<Menubar.Sub>
|
||||||
<Menubar.SubTrigger>
|
<Menubar.SubTrigger>
|
||||||
<Ruler size="16" class="mr-1" />{$_('menu.distance_units')}
|
<Ruler size="16" class="mr-1" />{$_('menu.distance_units')}
|
||||||
</Menubar.SubTrigger>
|
</Menubar.SubTrigger>
|
||||||
@@ -333,13 +370,14 @@
|
|||||||
<Menubar.RadioGroup bind:value={$distanceUnits}>
|
<Menubar.RadioGroup bind:value={$distanceUnits}>
|
||||||
<Menubar.RadioItem value="metric">{$_('menu.metric')}</Menubar.RadioItem>
|
<Menubar.RadioItem value="metric">{$_('menu.metric')}</Menubar.RadioItem>
|
||||||
<Menubar.RadioItem value="imperial">{$_('menu.imperial')}</Menubar.RadioItem>
|
<Menubar.RadioItem value="imperial">{$_('menu.imperial')}</Menubar.RadioItem>
|
||||||
|
<Menubar.RadioItem value="nautical">{$_('menu.nautical')}</Menubar.RadioItem>
|
||||||
</Menubar.RadioGroup>
|
</Menubar.RadioGroup>
|
||||||
</Menubar.SubContent>
|
</Menubar.SubContent>
|
||||||
</Menubar.Sub>
|
</Menubar.Sub>
|
||||||
<Menubar.Sub>
|
<Menubar.Sub>
|
||||||
<Menubar.SubTrigger
|
<Menubar.SubTrigger>
|
||||||
><Zap size="16" class="mr-1" />{$_('menu.velocity_units')}</Menubar.SubTrigger
|
<Zap size="16" class="mr-1" />{$_('menu.velocity_units')}
|
||||||
>
|
</Menubar.SubTrigger>
|
||||||
<Menubar.SubContent>
|
<Menubar.SubContent>
|
||||||
<Menubar.RadioGroup bind:value={$velocityUnits}>
|
<Menubar.RadioGroup bind:value={$velocityUnits}>
|
||||||
<Menubar.RadioItem value="speed">{$_('quantities.speed')}</Menubar.RadioItem>
|
<Menubar.RadioItem value="speed">{$_('quantities.speed')}</Menubar.RadioItem>
|
||||||
@@ -367,7 +405,7 @@
|
|||||||
<Menubar.SubContent>
|
<Menubar.SubContent>
|
||||||
<Menubar.RadioGroup bind:value={$locale}>
|
<Menubar.RadioGroup bind:value={$locale}>
|
||||||
{#each Object.entries(languages) as [lang, label]}
|
{#each Object.entries(languages) as [lang, label]}
|
||||||
<a href={getURLForLanguage(lang)}>
|
<a href={getURLForLanguage(lang, '/app')}>
|
||||||
<Menubar.RadioItem value={lang}>{label}</Menubar.RadioItem>
|
<Menubar.RadioItem value={lang}>{label}</Menubar.RadioItem>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -409,7 +447,7 @@
|
|||||||
</Menubar.SubContent>
|
</Menubar.SubContent>
|
||||||
</Menubar.Sub>
|
</Menubar.Sub>
|
||||||
<Menubar.Item on:click={() => (layerSettingsOpen = true)}>
|
<Menubar.Item on:click={() => (layerSettingsOpen = true)}>
|
||||||
<Layers3 size="16" class="mr-1" />
|
<Layers size="16" class="mr-1" />
|
||||||
{$_('menu.layers')}
|
{$_('menu.layers')}
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
</Menubar.Content>
|
</Menubar.Content>
|
||||||
@@ -421,6 +459,7 @@
|
|||||||
href="./help"
|
href="./help"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="cursor-default h-fit rounded-sm px-3 py-0.5"
|
class="cursor-default h-fit rounded-sm px-3 py-0.5"
|
||||||
|
aria-label={$_('menu.help')}
|
||||||
>
|
>
|
||||||
<BookOpenText size="18" class="md:hidden" />
|
<BookOpenText size="18" class="md:hidden" />
|
||||||
<span class="hidden md:block">
|
<span class="hidden md:block">
|
||||||
@@ -432,6 +471,7 @@
|
|||||||
href="https://ko-fi.com/gpxstudio"
|
href="https://ko-fi.com/gpxstudio"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="cursor-default h-fit rounded-sm font-bold text-support hover:text-support px-3 py-0.5"
|
class="cursor-default h-fit rounded-sm font-bold text-support hover:text-support px-3 py-0.5"
|
||||||
|
aria-label={$_('menu.donate')}
|
||||||
>
|
>
|
||||||
<HeartHandshake size="18" class="md:hidden" />
|
<HeartHandshake size="18" class="md:hidden" />
|
||||||
<span class="hidden md:flex flex-row items-center">
|
<span class="hidden md:flex flex-row items-center">
|
||||||
@@ -498,13 +538,16 @@
|
|||||||
} else {
|
} else {
|
||||||
dbUtils.undo();
|
dbUtils.undo();
|
||||||
}
|
}
|
||||||
} else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) {
|
|
||||||
if (e.shiftKey) {
|
|
||||||
dbUtils.deleteAllFiles();
|
|
||||||
} else {
|
|
||||||
dbUtils.deleteSelection();
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
} else if ((e.key === 'Backspace' || e.key === 'Delete') && (e.metaKey || e.ctrlKey)) {
|
||||||
|
if (!targetInput) {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
dbUtils.deleteAllFiles();
|
||||||
|
} else {
|
||||||
|
dbUtils.deleteSelection();
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) {
|
||||||
if (!targetInput) {
|
if (!targetInput) {
|
||||||
selectAll();
|
selectAll();
|
||||||
@@ -533,6 +576,10 @@
|
|||||||
dbUtils.setHiddenToSelection(true);
|
dbUtils.setHiddenToSelection(true);
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
if ($selection.size > 0) {
|
||||||
|
centerMapOnSelection();
|
||||||
|
}
|
||||||
} else if (e.key === 'F1') {
|
} else if (e.key === 'F1') {
|
||||||
switchBasemaps();
|
switchBasemaps();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@@ -15,6 +15,7 @@
|
|||||||
on:click={() => {
|
on:click={() => {
|
||||||
setMode(selectedMode === 'light' ? 'dark' : 'light');
|
setMode(selectedMode === 'light' ? 'dark' : 'light');
|
||||||
}}
|
}}
|
||||||
|
aria-label={$_('menu.mode')}
|
||||||
>
|
>
|
||||||
{#if selectedMode === 'light'}
|
{#if selectedMode === 'light'}
|
||||||
<Sun {size} />
|
<Sun {size} />
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Logo from '$lib/components/Logo.svelte';
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import AlgoliaDocSearch from '$lib/components/AlgoliaDocSearch.svelte';
|
||||||
import ModeSwitch from '$lib/components/ModeSwitch.svelte';
|
import ModeSwitch from '$lib/components/ModeSwitch.svelte';
|
||||||
import { BookOpenText, Home, Map } from 'lucide-svelte';
|
import { BookOpenText, Home, Map } from 'lucide-svelte';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
@@ -10,8 +11,8 @@
|
|||||||
<nav class="w-full sticky top-0 bg-background z-50">
|
<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">
|
<div class="mx-6 py-2 flex flex-row items-center border-b gap-4 sm:gap-8">
|
||||||
<a href={getURLForLanguage($locale, '/')} class="shrink-0 translate-y-0.5">
|
<a href={getURLForLanguage($locale, '/')} class="shrink-0 translate-y-0.5">
|
||||||
<Logo class="h-8 sm:hidden" iconOnly={true} />
|
<Logo class="h-8 sm:hidden" iconOnly={true} width="26" />
|
||||||
<Logo class="h-8 hidden sm:block" />
|
<Logo class="h-8 hidden sm:block" width="153" />
|
||||||
</a>
|
</a>
|
||||||
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/')}>
|
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/')}>
|
||||||
<Home size="18" class="mr-1.5" />
|
<Home size="18" class="mr-1.5" />
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
<BookOpenText size="18" class="mr-1.5" />
|
<BookOpenText size="18" class="mr-1.5" />
|
||||||
{$_('menu.help')}
|
{$_('menu.help')}
|
||||||
</Button>
|
</Button>
|
||||||
<ModeSwitch class="ml-auto" />
|
<AlgoliaDocSearch class="ml-auto" />
|
||||||
|
<ModeSwitch class="hidden xs:block" />
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@@ -1,26 +1,36 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { isMac, isSafari } from '$lib/utils';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
export let key: string;
|
export let key: string | undefined = undefined;
|
||||||
export let shift: boolean = false;
|
export let shift: boolean = false;
|
||||||
export let ctrl: boolean = false;
|
export let ctrl: boolean = false;
|
||||||
export let click: boolean = false;
|
export let click: boolean = false;
|
||||||
|
|
||||||
let isMac = false;
|
let mac = false;
|
||||||
let isSafari = false;
|
let safari = false;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
isMac = navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
|
mac = isMac();
|
||||||
isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
safari = isSafari();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="ml-auto pl-2 text-xs tracking-widest text-muted-foreground flex flex-row gap-0 items-baseline"
|
class="ml-auto pl-2 text-xs tracking-widest text-muted-foreground flex flex-row gap-0 items-baseline"
|
||||||
|
{...$$props}
|
||||||
>
|
>
|
||||||
<span>{shift ? '⇧' : ''}</span>
|
{#if shift}
|
||||||
<span>{ctrl ? (isMac && !isSafari ? '⌘' : $_('menu.ctrl') + '+') : ''}</span>
|
<span>⇧</span>
|
||||||
<span class={key === '+' ? 'font-medium text-sm/4' : ''}>{key}</span>
|
{/if}
|
||||||
<span>{click ? $_('menu.click') : ''}</span>
|
{#if ctrl}
|
||||||
|
<span>{mac && !safari ? '⌘' : $_('menu.ctrl') + '+'}</span>
|
||||||
|
{/if}
|
||||||
|
{#if key}
|
||||||
|
<span class={key === '+' ? 'font-medium text-sm/4' : ''}>{key}</span>
|
||||||
|
{/if}
|
||||||
|
{#if click}
|
||||||
|
<span>{$_('menu.click')}</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,14 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top';
|
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tooltip.Root>
|
<Tooltip.Root>
|
||||||
<Tooltip.Trigger {...$$restProps}>
|
<Tooltip.Trigger {...$$restProps} aria-label={label}>
|
||||||
<slot name="data" />
|
<slot />
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Content {side}>
|
<Tooltip.Content {side}>
|
||||||
<slot name="tooltip" />
|
<div class="flex flex-row items-center">
|
||||||
|
<span>{label}</span>
|
||||||
|
<slot name="extra" />
|
||||||
|
</div>
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
|
@@ -1,29 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
|
||||||
import { settings } from '$lib/db';
|
|
||||||
|
|
||||||
const { showWelcomeMessage } = settings;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<AlertDialog.Root
|
|
||||||
open={$showWelcomeMessage === true}
|
|
||||||
closeOnEscape={false}
|
|
||||||
closeOnOutsideClick={false}
|
|
||||||
onOpenChange={() => ($showWelcomeMessage = false)}
|
|
||||||
>
|
|
||||||
<AlertDialog.Trigger class="hidden"></AlertDialog.Trigger>
|
|
||||||
<AlertDialog.Content>
|
|
||||||
<AlertDialog.Header>
|
|
||||||
<AlertDialog.Title>
|
|
||||||
Welcome to the new version of <b>gpx.studio</b>!
|
|
||||||
</AlertDialog.Title>
|
|
||||||
<AlertDialog.Description class="space-y-1">
|
|
||||||
<p>The website is still under development and may contain bugs.</p>
|
|
||||||
<p>Please report any issues you find by email or on GitHub.</p>
|
|
||||||
</AlertDialog.Description>
|
|
||||||
</AlertDialog.Header>
|
|
||||||
<AlertDialog.Footer>
|
|
||||||
<AlertDialog.Action>Let's go!</AlertDialog.Action>
|
|
||||||
</AlertDialog.Footer>
|
|
||||||
</AlertDialog.Content>
|
|
||||||
</AlertDialog.Root>
|
|
@@ -2,9 +2,12 @@
|
|||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import {
|
import {
|
||||||
celsiusToFahrenheit,
|
celsiusToFahrenheit,
|
||||||
distancePerHourToSecondsPerDistance,
|
getConvertedDistance,
|
||||||
kilometersToMiles,
|
getConvertedElevation,
|
||||||
metersToFeet,
|
getConvertedVelocity,
|
||||||
|
getDistanceUnits,
|
||||||
|
getElevationUnits,
|
||||||
|
getVelocityUnits,
|
||||||
secondsToHHMMSS
|
secondsToHHMMSS
|
||||||
} from '$lib/units';
|
} from '$lib/units';
|
||||||
|
|
||||||
@@ -20,31 +23,18 @@
|
|||||||
|
|
||||||
<span class={$$props.class}>
|
<span class={$$props.class}>
|
||||||
{#if type === 'distance'}
|
{#if type === 'distance'}
|
||||||
{#if $distanceUnits === 'metric'}
|
{getConvertedDistance(value, $distanceUnits).toFixed(decimals ?? 2)}
|
||||||
{value.toFixed(decimals ?? 2)} {showUnits ? $_('units.kilometers') : ''}
|
{showUnits ? getDistanceUnits($distanceUnits) : ''}
|
||||||
{:else}
|
|
||||||
{kilometersToMiles(value).toFixed(decimals ?? 2)} {showUnits ? $_('units.miles') : ''}
|
|
||||||
{/if}
|
|
||||||
{:else if type === 'elevation'}
|
{:else if type === 'elevation'}
|
||||||
{#if $distanceUnits === 'metric'}
|
{getConvertedElevation(value, $distanceUnits).toFixed(decimals ?? 0)}
|
||||||
{value.toFixed(decimals ?? 0)} {showUnits ? $_('units.meters') : ''}
|
{showUnits ? getElevationUnits($distanceUnits) : ''}
|
||||||
{:else}
|
|
||||||
{metersToFeet(value).toFixed(decimals ?? 0)} {showUnits ? $_('units.feet') : ''}
|
|
||||||
{/if}
|
|
||||||
{:else if type === 'speed'}
|
{:else if type === 'speed'}
|
||||||
{#if $distanceUnits === 'metric'}
|
{#if $velocityUnits === 'speed'}
|
||||||
{#if $velocityUnits === 'speed'}
|
{getConvertedVelocity(value, $velocityUnits, $distanceUnits).toFixed(decimals ?? 2)}
|
||||||
{value.toFixed(decimals ?? 2)} {showUnits ? $_('units.kilometers_per_hour') : ''}
|
{showUnits ? getVelocityUnits($velocityUnits, $distanceUnits) : ''}
|
||||||
{:else}
|
|
||||||
{secondsToHHMMSS(distancePerHourToSecondsPerDistance(value))}
|
|
||||||
{showUnits ? $_('units.minutes_per_kilometer') : ''}
|
|
||||||
{/if}
|
|
||||||
{:else if $velocityUnits === 'speed'}
|
|
||||||
{kilometersToMiles(value).toFixed(decimals ?? 2)}
|
|
||||||
{showUnits ? $_('units.miles_per_hour') : ''}
|
|
||||||
{:else}
|
{:else}
|
||||||
{secondsToHHMMSS(distancePerHourToSecondsPerDistance(kilometersToMiles(value)))}
|
{secondsToHHMMSS(getConvertedVelocity(value, $velocityUnits, $distanceUnits))}
|
||||||
{showUnits ? $_('units.minutes_per_mile') : ''}
|
{showUnits ? getVelocityUnits($velocityUnits, $distanceUnits) : ''}
|
||||||
{/if}
|
{/if}
|
||||||
{:else if type === 'temperature'}
|
{:else if type === 'temperature'}
|
||||||
{#if $temperatureUnits === 'celsius'}
|
{#if $temperatureUnits === 'celsius'}
|
||||||
|
@@ -1,42 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment';
|
import { _ } from 'svelte-i18n';
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import { base } from '$app/paths';
|
|
||||||
import { _, locale } from 'svelte-i18n';
|
|
||||||
|
|
||||||
export let path: string;
|
export let module;
|
||||||
export let titleOnly: boolean = false;
|
|
||||||
|
|
||||||
let module = undefined;
|
|
||||||
let metadata: Record<string, any> = {};
|
|
||||||
|
|
||||||
const modules = import.meta.glob('/src/lib/docs/**/*.mdx');
|
|
||||||
|
|
||||||
function loadModule(path: string) {
|
|
||||||
modules[path]?.().then((mod) => {
|
|
||||||
module = mod.default;
|
|
||||||
metadata = mod.metadata;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if ($locale) {
|
|
||||||
if (modules.hasOwnProperty(`/src/lib/docs/${$locale}/${path}`)) {
|
|
||||||
loadModule(`/src/lib/docs/${$locale}/${path}`);
|
|
||||||
} else if (browser) {
|
|
||||||
goto(`${base}/404`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if module !== undefined}
|
<div class="markdown flex flex-col gap-3">
|
||||||
{#if titleOnly}
|
<svelte:component this={module} />
|
||||||
{metadata.title}
|
</div>
|
||||||
{:else}
|
|
||||||
<div class="markdown flex flex-col gap-3">
|
|
||||||
<svelte:component this={module} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
:global(.markdown) {
|
:global(.markdown) {
|
||||||
@@ -64,24 +34,24 @@
|
|||||||
@apply pt-1.5;
|
@apply pt-1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown p > button) {
|
:global(.markdown p > button, .markdown li > button) {
|
||||||
@apply border;
|
@apply border;
|
||||||
@apply rounded-md;
|
@apply rounded-md;
|
||||||
@apply px-1;
|
@apply px-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown > a) {
|
:global(.markdown > a) {
|
||||||
@apply text-blue-500;
|
@apply text-link;
|
||||||
@apply hover:underline;
|
@apply hover:underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown p > a) {
|
:global(.markdown p > a) {
|
||||||
@apply text-blue-500;
|
@apply text-link;
|
||||||
@apply hover:underline;
|
@apply hover:underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.markdown li > a) {
|
:global(.markdown li > a) {
|
||||||
@apply text-blue-500;
|
@apply text-link;
|
||||||
@apply hover:underline;
|
@apply hover:underline;
|
||||||
}
|
}
|
||||||
|
|
@@ -1,11 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let src;
|
export let src: 'getting-started/interface' | 'tools/routing' | 'tools/split';
|
||||||
export let alt: string;
|
export let alt: string;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col items-center py-6 w-full">
|
<div class="flex flex-col items-center py-6 w-full">
|
||||||
<div class="rounded-md overflow-clip shadow-xl mx-auto">
|
<div class="rounded-md overflow-hidden overflow-clip shadow-xl mx-auto">
|
||||||
<enhanced:img {src} {alt} class="w-full max-w-3xl" />
|
{#if src === 'getting-started/interface'}
|
||||||
|
<enhanced:img
|
||||||
|
src="/src/lib/assets/img/docs/getting-started/interface.png"
|
||||||
|
{alt}
|
||||||
|
class="w-full max-w-3xl"
|
||||||
|
/>
|
||||||
|
{:else if src === 'tools/routing'}
|
||||||
|
<enhanced:img
|
||||||
|
src="/src/lib/assets/img/docs/tools/routing.png"
|
||||||
|
{alt}
|
||||||
|
class="w-full max-w-3xl"
|
||||||
|
/>
|
||||||
|
{:else if src === 'tools/split'}
|
||||||
|
<enhanced:img src="/src/lib/assets/img/docs/tools/split.png" {alt} class="w-full max-w-3xl" />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<p class="text-center text-sm text-muted-foreground mt-2">{alt}</p>
|
<p class="text-center text-sm text-muted-foreground mt-2">{alt}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,8 +3,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="bg-accent border-l-8 {type === 'note'
|
class="bg-secondary border-l-8 {type === 'note'
|
||||||
? 'border-blue-500'
|
? 'border-link'
|
||||||
: 'border-destructive'} p-2 text-sm rounded-md"
|
: 'border-destructive'} p-2 text-sm rounded-md"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
div :global(a) {
|
div :global(a) {
|
||||||
@apply text-blue-500;
|
@apply text-link;
|
||||||
@apply hover:underline;
|
@apply hover:underline;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,14 +1,15 @@
|
|||||||
import { File, FilePen, View, type Icon, Settings, Pencil, MapPin, Scissors, CalendarClock, Group, Ungroup, Filter, SquareDashedMousePointer } from "lucide-svelte";
|
import { File, FilePen, View, type Icon, Settings, Pencil, MapPin, Scissors, CalendarClock, Group, Ungroup, Filter, SquareDashedMousePointer, MountainSnow } from "lucide-svelte";
|
||||||
import type { ComponentType } from "svelte";
|
import type { ComponentType } from "svelte";
|
||||||
|
|
||||||
export const guides: Record<string, string[]> = {
|
export const guides: Record<string, string[]> = {
|
||||||
'getting-started': [],
|
'getting-started': [],
|
||||||
menu: ['file', 'edit', 'view', 'settings'],
|
menu: ['file', 'edit', 'view', 'settings'],
|
||||||
'files-and-stats': [],
|
'files-and-stats': [],
|
||||||
toolbar: ['routing', 'poi', 'scissors', 'time', 'merge', 'extract', 'minify', 'clean'],
|
toolbar: ['routing', 'poi', 'scissors', 'time', 'merge', 'extract', 'elevation', 'minify', 'clean'],
|
||||||
'map-controls': [],
|
'map-controls': [],
|
||||||
'gpx': [],
|
'gpx': [],
|
||||||
'integration': [],
|
'integration': [],
|
||||||
|
'faq': [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const guideIcons: Record<string, string | ComponentType<Icon>> = {
|
export const guideIcons: Record<string, string | ComponentType<Icon>> = {
|
||||||
@@ -26,11 +27,13 @@ export const guideIcons: Record<string, string | ComponentType<Icon>> = {
|
|||||||
"time": CalendarClock,
|
"time": CalendarClock,
|
||||||
"merge": Group,
|
"merge": Group,
|
||||||
"extract": Ungroup,
|
"extract": Ungroup,
|
||||||
|
"elevation": MountainSnow,
|
||||||
"minify": Filter,
|
"minify": Filter,
|
||||||
"clean": SquareDashedMousePointer,
|
"clean": SquareDashedMousePointer,
|
||||||
"map-controls": "🗺",
|
"map-controls": "🗺",
|
||||||
"gpx": "💾",
|
"gpx": "💾",
|
||||||
"integration": "{ 👩💻 }",
|
"integration": "{ 👩💻 }",
|
||||||
|
"faq": "🔮",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getPreviousGuide(currentGuide: string): string | undefined {
|
export function getPreviousGuide(currentGuide: string): string | undefined {
|
||||||
|
@@ -20,8 +20,13 @@
|
|||||||
import type { GPXFile } from 'gpx';
|
import type { GPXFile } from 'gpx';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
import { ListFileItem } from '$lib/components/file-list/FileList';
|
import { ListFileItem } from '$lib/components/file-list/FileList';
|
||||||
import { allowedEmbeddingBasemaps, type EmbeddingOptions } from './Embedding';
|
import {
|
||||||
|
allowedEmbeddingBasemaps,
|
||||||
|
getFilesFromEmbeddingOptions,
|
||||||
|
type EmbeddingOptions
|
||||||
|
} from './Embedding';
|
||||||
import { mode, setMode } from 'mode-watcher';
|
import { mode, setMode } from 'mode-watcher';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
$embedding = true;
|
$embedding = true;
|
||||||
|
|
||||||
@@ -55,7 +60,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
let downloads: Promise<GPXFile | null>[] = [];
|
let downloads: Promise<GPXFile | null>[] = [];
|
||||||
options.files.forEach((url) => {
|
getFilesFromEmbeddingOptions(options).forEach((url) => {
|
||||||
downloads.push(
|
downloads.push(
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then((response) => response.blob())
|
.then((response) => response.blob())
|
||||||
@@ -176,7 +181,7 @@
|
|||||||
prevSettings.theme = $mode ?? 'system';
|
prevSettings.theme = $mode ?? 'system';
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if (options) {
|
$: if (browser && options) {
|
||||||
applyOptions();
|
applyOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +229,7 @@
|
|||||||
geolocate={false}
|
geolocate={false}
|
||||||
hash={useHash}
|
hash={useHash}
|
||||||
/>
|
/>
|
||||||
<OpenIn bind:files={options.files} />
|
<OpenIn bind:files={options.files} bind:ids={options.ids} />
|
||||||
<LayerControl />
|
<LayerControl />
|
||||||
<GPXLayers />
|
<GPXLayers />
|
||||||
{#if $fileObservers.size > 1}
|
{#if $fileObservers.size > 1}
|
||||||
|
@@ -1,31 +1,34 @@
|
|||||||
import { basemaps } from "$lib/assets/layers";
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
|
import { basemaps } from '$lib/assets/layers';
|
||||||
|
|
||||||
export type EmbeddingOptions = {
|
export type EmbeddingOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
files: string[];
|
files: string[];
|
||||||
|
ids: string[];
|
||||||
basemap: string;
|
basemap: string;
|
||||||
elevation: {
|
elevation: {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
height: number,
|
height: number;
|
||||||
controls: boolean,
|
controls: boolean;
|
||||||
fill: 'slope' | 'surface' | undefined,
|
fill: 'slope' | 'surface' | undefined;
|
||||||
speed: boolean,
|
speed: boolean;
|
||||||
hr: boolean,
|
hr: boolean;
|
||||||
cad: boolean,
|
cad: boolean;
|
||||||
temp: boolean,
|
temp: boolean;
|
||||||
power: boolean,
|
power: boolean;
|
||||||
},
|
};
|
||||||
distanceMarkers: boolean,
|
distanceMarkers: boolean;
|
||||||
directionMarkers: boolean,
|
directionMarkers: boolean;
|
||||||
distanceUnits: 'metric' | 'imperial',
|
distanceUnits: 'metric' | 'imperial' | 'nautical';
|
||||||
velocityUnits: 'speed' | 'pace',
|
velocityUnits: 'speed' | 'pace';
|
||||||
temperatureUnits: 'celsius' | 'fahrenheit',
|
temperatureUnits: 'celsius' | 'fahrenheit';
|
||||||
theme: 'system' | 'light' | 'dark',
|
theme: 'system' | 'light' | 'dark';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultEmbeddingOptions = {
|
export const defaultEmbeddingOptions = {
|
||||||
token: '',
|
token: '',
|
||||||
files: [],
|
files: [],
|
||||||
|
ids: [],
|
||||||
basemap: 'mapboxOutdoors',
|
basemap: 'mapboxOutdoors',
|
||||||
elevation: {
|
elevation: {
|
||||||
show: true,
|
show: true,
|
||||||
@@ -36,21 +39,24 @@ export const defaultEmbeddingOptions = {
|
|||||||
hr: false,
|
hr: false,
|
||||||
cad: false,
|
cad: false,
|
||||||
temp: false,
|
temp: false,
|
||||||
power: false,
|
power: false
|
||||||
},
|
},
|
||||||
distanceMarkers: false,
|
distanceMarkers: false,
|
||||||
directionMarkers: false,
|
directionMarkers: false,
|
||||||
distanceUnits: 'metric',
|
distanceUnits: 'metric',
|
||||||
velocityUnits: 'speed',
|
velocityUnits: 'speed',
|
||||||
temperatureUnits: 'celsius',
|
temperatureUnits: 'celsius',
|
||||||
theme: 'system',
|
theme: 'system'
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getDefaultEmbeddingOptions(): EmbeddingOptions {
|
export function getDefaultEmbeddingOptions(): EmbeddingOptions {
|
||||||
return JSON.parse(JSON.stringify(defaultEmbeddingOptions));
|
return JSON.parse(JSON.stringify(defaultEmbeddingOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMergedEmbeddingOptions(options: any, defaultOptions: any = defaultEmbeddingOptions): EmbeddingOptions {
|
export function getMergedEmbeddingOptions(
|
||||||
|
options: any,
|
||||||
|
defaultOptions: any = defaultEmbeddingOptions
|
||||||
|
): EmbeddingOptions {
|
||||||
const mergedOptions = JSON.parse(JSON.stringify(defaultOptions));
|
const mergedOptions = JSON.parse(JSON.stringify(defaultOptions));
|
||||||
for (const key in options) {
|
for (const key in options) {
|
||||||
if (typeof options[key] === 'object' && options[key] !== null && !Array.isArray(options[key])) {
|
if (typeof options[key] === 'object' && options[key] !== null && !Array.isArray(options[key])) {
|
||||||
@@ -62,10 +68,17 @@ export function getMergedEmbeddingOptions(options: any, defaultOptions: any = de
|
|||||||
return mergedOptions;
|
return mergedOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCleanedEmbeddingOptions(options: any, defaultOptions: any = defaultEmbeddingOptions): any {
|
export function getCleanedEmbeddingOptions(
|
||||||
|
options: any,
|
||||||
|
defaultOptions: any = defaultEmbeddingOptions
|
||||||
|
): any {
|
||||||
const cleanedOptions = JSON.parse(JSON.stringify(options));
|
const cleanedOptions = JSON.parse(JSON.stringify(options));
|
||||||
for (const key in cleanedOptions) {
|
for (const key in cleanedOptions) {
|
||||||
if (typeof cleanedOptions[key] === 'object' && cleanedOptions[key] !== null && !Array.isArray(cleanedOptions[key])) {
|
if (
|
||||||
|
typeof cleanedOptions[key] === 'object' &&
|
||||||
|
cleanedOptions[key] !== null &&
|
||||||
|
!Array.isArray(cleanedOptions[key])
|
||||||
|
) {
|
||||||
cleanedOptions[key] = getCleanedEmbeddingOptions(cleanedOptions[key], defaultOptions[key]);
|
cleanedOptions[key] = getCleanedEmbeddingOptions(cleanedOptions[key], defaultOptions[key]);
|
||||||
if (Object.keys(cleanedOptions[key]).length === 0) {
|
if (Object.keys(cleanedOptions[key]).length === 0) {
|
||||||
delete cleanedOptions[key];
|
delete cleanedOptions[key];
|
||||||
@@ -77,4 +90,59 @@ export function getCleanedEmbeddingOptions(options: any, defaultOptions: any = d
|
|||||||
return cleanedOptions;
|
return cleanedOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const allowedEmbeddingBasemaps = Object.keys(basemaps).filter(basemap => !['ordnanceSurvey'].includes(basemap));
|
export const allowedEmbeddingBasemaps = Object.keys(basemaps).filter(
|
||||||
|
(basemap) => !['ordnanceSurvey'].includes(basemap)
|
||||||
|
);
|
||||||
|
|
||||||
|
export function getFilesFromEmbeddingOptions(options: EmbeddingOptions): string[] {
|
||||||
|
return options.files.concat(options.ids.map((id) => getURLForGoogleDriveFile(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getURLForGoogleDriveFile(fileId: string): string {
|
||||||
|
return `https://www.googleapis.com/drive/v3/files/${fileId}?alt=media&key=AIzaSyA2ZadQob_hXiT2VaYIkAyafPvz_4ZMssk`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertOldEmbeddingOptions(options: URLSearchParams): any {
|
||||||
|
let newOptions: any = {
|
||||||
|
token: PUBLIC_MAPBOX_TOKEN,
|
||||||
|
files: [],
|
||||||
|
ids: [],
|
||||||
|
};
|
||||||
|
if (options.has('state')) {
|
||||||
|
let state = JSON.parse(options.get('state')!);
|
||||||
|
if (state.ids) {
|
||||||
|
newOptions.ids.push(...state.ids);
|
||||||
|
}
|
||||||
|
if (state.urls) {
|
||||||
|
newOptions.files.push(...state.urls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.has('source')) {
|
||||||
|
let basemap = options.get('source')!;
|
||||||
|
if (basemap === 'satellite') {
|
||||||
|
newOptions.basemap = 'mapboxSatellite';
|
||||||
|
} else if (basemap === 'otm') {
|
||||||
|
newOptions.basemap = 'openTopoMap';
|
||||||
|
} else if (basemap === 'ohm') {
|
||||||
|
newOptions.basemap = 'openHikingMap';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.has('imperial')) {
|
||||||
|
newOptions.distanceUnits = 'imperial';
|
||||||
|
}
|
||||||
|
if (options.has('running')) {
|
||||||
|
newOptions.velocityUnits = 'pace';
|
||||||
|
}
|
||||||
|
if (options.has('distance')) {
|
||||||
|
newOptions.distanceMarkers = true;
|
||||||
|
}
|
||||||
|
if (options.has('direction')) {
|
||||||
|
newOptions.directionMarkers = true;
|
||||||
|
}
|
||||||
|
if (options.has('slope')) {
|
||||||
|
newOptions.elevation = {
|
||||||
|
fill: 'slope'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return newOptions;
|
||||||
|
}
|
||||||
|
@@ -34,13 +34,21 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
let files = options.files[0];
|
let files = options.files[0];
|
||||||
$: if (files) {
|
$: {
|
||||||
let urls = files.split(',');
|
let urls = files.split(',');
|
||||||
urls = urls.filter((url) => url.length > 0);
|
urls = urls.filter((url) => url.length > 0);
|
||||||
if (JSON.stringify(urls) !== JSON.stringify(options.files)) {
|
if (JSON.stringify(urls) !== JSON.stringify(options.files)) {
|
||||||
options.files = urls;
|
options.files = urls;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let driveIds = '';
|
||||||
|
$: {
|
||||||
|
let ids = driveIds.split(',');
|
||||||
|
ids = ids.filter((id) => id.length > 0);
|
||||||
|
if (JSON.stringify(ids) !== JSON.stringify(options.ids)) {
|
||||||
|
options.ids = ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let manualCamera = false;
|
let manualCamera = false;
|
||||||
|
|
||||||
@@ -84,7 +92,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root>
|
<Card.Root id="embedding-playground">
|
||||||
<Card.Header>
|
<Card.Header>
|
||||||
<Card.Title>{$_('embedding.title')}</Card.Title>
|
<Card.Title>{$_('embedding.title')}</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
@@ -94,6 +102,8 @@
|
|||||||
<Input id="token" type="text" class="h-8" bind:value={options.token} />
|
<Input id="token" type="text" class="h-8" bind:value={options.token} />
|
||||||
<Label for="file_urls">{$_('embedding.file_urls')}</Label>
|
<Label for="file_urls">{$_('embedding.file_urls')}</Label>
|
||||||
<Input id="file_urls" type="text" class="h-8" bind:value={files} />
|
<Input id="file_urls" type="text" class="h-8" bind:value={files} />
|
||||||
|
<Label for="drive_ids">{$_('embedding.drive_ids')}</Label>
|
||||||
|
<Input id="drive_ids" type="text" class="h-8" bind:value={driveIds} />
|
||||||
<Label for="basemap">{$_('embedding.basemap')}</Label>
|
<Label for="basemap">{$_('embedding.basemap')}</Label>
|
||||||
<Select.Root
|
<Select.Root
|
||||||
selected={{ value: options.basemap, label: $_(`layers.label.${options.basemap}`) }}
|
selected={{ value: options.basemap, label: $_(`layers.label.${options.basemap}`) }}
|
||||||
@@ -214,6 +224,10 @@
|
|||||||
<RadioGroup.Item value="imperial" id="imperial" />
|
<RadioGroup.Item value="imperial" id="imperial" />
|
||||||
<Label for="imperial">{$_('menu.imperial')}</Label>
|
<Label for="imperial">{$_('menu.imperial')}</Label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="nautical" id="nautical" />
|
||||||
|
<Label for="nautical">{$_('menu.nautical')}</Label>
|
||||||
|
</div>
|
||||||
</RadioGroup.Root>
|
</RadioGroup.Root>
|
||||||
</Label>
|
</Label>
|
||||||
<Label class="flex flex-col items-start gap-2">
|
<Label class="flex flex-col items-start gap-2">
|
||||||
|
@@ -1,18 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import Logo from '$lib/components/Logo.svelte';
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
import { getURLForLanguage } from '$lib/utils';
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
|
|
||||||
export let files: string[];
|
export let files: string[];
|
||||||
|
export let ids: string[];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="absolute top-0 flex-wrap h-fit bg-background font-semibold rounded-md py-1 px-2 gap-1.5 xs:text-base mt-2.5 ml-2.5 mr-12"
|
class="absolute top-0 flex-wrap h-fit bg-background font-semibold rounded-md py-1 px-2 gap-1.5 xs:text-base mt-2.5 ml-2.5 mr-12"
|
||||||
href="{getURLForLanguage($locale, '/app')}?files={encodeURIComponent(JSON.stringify(files))}"
|
href="{getURLForLanguage($locale, '/app')}?{files.length > 0
|
||||||
target="_blank"
|
? `files=${encodeURIComponent(JSON.stringify(files))}`
|
||||||
|
: ''}{files.length > 0 && ids.length > 0 ? '&' : ''}{ids.length > 0
|
||||||
|
? `ids=${encodeURIComponent(JSON.stringify(ids))}`
|
||||||
|
: ''}"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
{$_('menu.open_in')}
|
{$_('menu.open_in')}
|
||||||
<Logo class="h-[18px] xs:h-5 translate-y-[1px]" />
|
<Logo class="h-[18px] xs:h-5 translate-y-[1px]" />
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -5,10 +5,17 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
|
import {
|
||||||
|
buildGPX,
|
||||||
|
GPXFile,
|
||||||
|
Track,
|
||||||
|
Waypoint,
|
||||||
|
type AnyGPXTreeElement,
|
||||||
|
type GPXTreeElement
|
||||||
|
} from 'gpx';
|
||||||
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
|
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
|
||||||
import Sortable from 'sortablejs/Sortable';
|
import Sortable from 'sortablejs/Sortable';
|
||||||
import { getFileIds, settings, type GPXFileWithStatistics } from '$lib/db';
|
import { getFile, getFileIds, settings, type GPXFileWithStatistics } from '$lib/db';
|
||||||
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||||
import FileListNodeStore from './FileListNodeStore.svelte';
|
import FileListNodeStore from './FileListNodeStore.svelte';
|
||||||
import FileListNode from './FileListNode.svelte';
|
import FileListNode from './FileListNode.svelte';
|
||||||
@@ -22,6 +29,7 @@
|
|||||||
type ListItem
|
type ListItem
|
||||||
} from './FileList';
|
} from './FileList';
|
||||||
import { selection } from './Selection';
|
import { selection } from './Selection';
|
||||||
|
import { isMac } from '$lib/utils';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
export let node:
|
export let node:
|
||||||
@@ -154,7 +162,7 @@
|
|||||||
direction: orientation,
|
direction: orientation,
|
||||||
forceAutoScrollFallback: true,
|
forceAutoScrollFallback: true,
|
||||||
multiDrag: true,
|
multiDrag: true,
|
||||||
multiDragKey: 'Meta',
|
multiDragKey: isMac() ? 'Meta' : 'Ctrl',
|
||||||
avoidImplicitDeselect: true,
|
avoidImplicitDeselect: true,
|
||||||
onSelect: updateToSelection,
|
onSelect: updateToSelection,
|
||||||
onDeselect: updateToSelection,
|
onDeselect: updateToSelection,
|
||||||
@@ -223,6 +231,22 @@
|
|||||||
|
|
||||||
moveItems(fromItem, toItem, fromItems, toItems);
|
moveItems(fromItem, toItem, fromItems, toItems);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
setData: function (dataTransfer: DataTransfer, dragEl: HTMLElement) {
|
||||||
|
if (sortableLevel === ListLevel.FILE) {
|
||||||
|
const fileId = dragEl.getAttribute('data-id');
|
||||||
|
const file = fileId ? getFile(fileId) : null;
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
const data = buildGPX(file);
|
||||||
|
dataTransfer.setData(
|
||||||
|
'DownloadURL',
|
||||||
|
`application/gpx+xml:${file.metadata.name}.gpx:data:text/octet-stream;charset=utf-8,${encodeURIComponent(data)}`
|
||||||
|
);
|
||||||
|
dataTransfer.dropEffect = 'copy';
|
||||||
|
dataTransfer.effectAllowed = 'copy';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Object.defineProperty(sortable, '_item', {
|
Object.defineProperty(sortable, '_item', {
|
||||||
|
@@ -15,6 +15,7 @@
|
|||||||
EyeOff,
|
EyeOff,
|
||||||
ClipboardCopy,
|
ClipboardCopy,
|
||||||
ClipboardPaste,
|
ClipboardPaste,
|
||||||
|
Maximize,
|
||||||
Scissors,
|
Scissors,
|
||||||
FileStack,
|
FileStack,
|
||||||
FileX
|
FileX
|
||||||
@@ -39,7 +40,15 @@
|
|||||||
} from './Selection';
|
} from './Selection';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { allHidden, editMetadata, editStyle, embedding, gpxLayers, map } from '$lib/stores';
|
import {
|
||||||
|
allHidden,
|
||||||
|
editMetadata,
|
||||||
|
editStyle,
|
||||||
|
embedding,
|
||||||
|
centerMapOnSelection,
|
||||||
|
gpxLayers,
|
||||||
|
map
|
||||||
|
} from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
GPXTreeElement,
|
GPXTreeElement,
|
||||||
Track,
|
Track,
|
||||||
@@ -239,10 +248,7 @@
|
|||||||
{#if item instanceof ListFileItem}
|
{#if item instanceof ListFileItem}
|
||||||
<ContextMenu.Item
|
<ContextMenu.Item
|
||||||
disabled={!singleSelection}
|
disabled={!singleSelection}
|
||||||
on:click={() =>
|
on:click={() => dbUtils.addNewTrack(item.getFileId())}
|
||||||
dbUtils.applyToFile(item.getFileId(), (file) =>
|
|
||||||
file.replaceTracks(file.trk.length, file.trk.length, [new Track()])
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Plus size="16" class="mr-1" />
|
<Plus size="16" class="mr-1" />
|
||||||
{$_('menu.new_track')}
|
{$_('menu.new_track')}
|
||||||
@@ -251,17 +257,7 @@
|
|||||||
{:else if item instanceof ListTrackItem}
|
{:else if item instanceof ListTrackItem}
|
||||||
<ContextMenu.Item
|
<ContextMenu.Item
|
||||||
disabled={!singleSelection}
|
disabled={!singleSelection}
|
||||||
on:click={() => {
|
on:click={() => dbUtils.addNewSegment(item.getFileId(), item.getTrackIndex())}
|
||||||
let trackIndex = item.getTrackIndex();
|
|
||||||
dbUtils.applyToFile(item.getFileId(), (file) =>
|
|
||||||
file.replaceTrackSegments(
|
|
||||||
trackIndex,
|
|
||||||
file.trk[trackIndex].trkseg.length,
|
|
||||||
file.trk[trackIndex].trkseg.length,
|
|
||||||
[new TrackSegment()]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Plus size="16" class="mr-1" />
|
<Plus size="16" class="mr-1" />
|
||||||
{$_('menu.new_segment')}
|
{$_('menu.new_segment')}
|
||||||
@@ -275,38 +271,41 @@
|
|||||||
{$_('menu.select_all')}
|
{$_('menu.select_all')}
|
||||||
<Shortcut key="A" ctrl={true} />
|
<Shortcut key="A" ctrl={true} />
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
<ContextMenu.Separator />
|
|
||||||
{/if}
|
{/if}
|
||||||
|
<ContextMenu.Item on:click={centerMapOnSelection}>
|
||||||
|
<Maximize size="16" class="mr-1" />
|
||||||
|
{$_('menu.center')}
|
||||||
|
<Shortcut key="⏎" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Separator />
|
||||||
|
<ContextMenu.Item on:click={dbUtils.duplicateSelection}>
|
||||||
|
<Copy size="16" class="mr-1" />
|
||||||
|
{$_('menu.duplicate')}
|
||||||
|
<Shortcut key="D" ctrl={true} /></ContextMenu.Item
|
||||||
|
>
|
||||||
{#if orientation === 'vertical'}
|
{#if orientation === 'vertical'}
|
||||||
<ContextMenu.Item on:click={dbUtils.duplicateSelection}>
|
<ContextMenu.Item on:click={copySelection}>
|
||||||
<Copy size="16" class="mr-1" />
|
<ClipboardCopy size="16" class="mr-1" />
|
||||||
{$_('menu.duplicate')}
|
{$_('menu.copy')}
|
||||||
<Shortcut key="D" ctrl={true} /></ContextMenu.Item
|
<Shortcut key="C" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item on:click={cutSelection}>
|
||||||
|
<Scissors size="16" class="mr-1" />
|
||||||
|
{$_('menu.cut')}
|
||||||
|
<Shortcut key="X" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item
|
||||||
|
disabled={$copied === undefined ||
|
||||||
|
$copied.length === 0 ||
|
||||||
|
!allowedPastes[$copied[0].level].includes(item.level)}
|
||||||
|
on:click={pasteSelection}
|
||||||
>
|
>
|
||||||
{#if orientation === 'vertical'}
|
<ClipboardPaste size="16" class="mr-1" />
|
||||||
<ContextMenu.Item on:click={copySelection}>
|
{$_('menu.paste')}
|
||||||
<ClipboardCopy size="16" class="mr-1" />
|
<Shortcut key="V" ctrl={true} />
|
||||||
{$_('menu.copy')}
|
</ContextMenu.Item>
|
||||||
<Shortcut key="C" ctrl={true} />
|
|
||||||
</ContextMenu.Item>
|
|
||||||
<ContextMenu.Item on:click={cutSelection}>
|
|
||||||
<Scissors size="16" class="mr-1" />
|
|
||||||
{$_('menu.cut')}
|
|
||||||
<Shortcut key="X" ctrl={true} />
|
|
||||||
</ContextMenu.Item>
|
|
||||||
<ContextMenu.Item
|
|
||||||
disabled={$copied === undefined ||
|
|
||||||
$copied.length === 0 ||
|
|
||||||
!allowedPastes[$copied[0].level].includes(item.level)}
|
|
||||||
on:click={pasteSelection}
|
|
||||||
>
|
|
||||||
<ClipboardPaste size="16" class="mr-1" />
|
|
||||||
{$_('menu.paste')}
|
|
||||||
<Shortcut key="V" ctrl={true} />
|
|
||||||
</ContextMenu.Item>
|
|
||||||
{/if}
|
|
||||||
<ContextMenu.Separator />
|
|
||||||
{/if}
|
{/if}
|
||||||
|
<ContextMenu.Separator />
|
||||||
<ContextMenu.Item on:click={dbUtils.deleteSelection}>
|
<ContextMenu.Item on:click={dbUtils.deleteSelection}>
|
||||||
{#if item instanceof ListFileItem}
|
{#if item instanceof ListFileItem}
|
||||||
<FileX size="16" class="mr-1" />
|
<FileX size="16" class="mr-1" />
|
||||||
|
@@ -1,62 +1,65 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Textarea } from '$lib/components/ui/textarea';
|
import { Textarea } from '$lib/components/ui/textarea';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
import * as Popover from '$lib/components/ui/popover';
|
import * as Popover from '$lib/components/ui/popover';
|
||||||
import { dbUtils } from '$lib/db';
|
import { dbUtils } from '$lib/db';
|
||||||
import { Save } from 'lucide-svelte';
|
import { Save } from 'lucide-svelte';
|
||||||
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
|
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
|
||||||
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import { editMetadata } from '$lib/stores';
|
import { editMetadata } from '$lib/stores';
|
||||||
|
|
||||||
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
||||||
export let item: ListItem;
|
export let item: ListItem;
|
||||||
export let open = false;
|
export let open = false;
|
||||||
|
|
||||||
let name: string =
|
let name: string =
|
||||||
node instanceof GPXFile
|
node instanceof GPXFile
|
||||||
? node.metadata.name ?? ''
|
? node.metadata.name ?? ''
|
||||||
: node instanceof Track
|
: node instanceof Track
|
||||||
? node.name ?? ''
|
? node.name ?? ''
|
||||||
: '';
|
: '';
|
||||||
let description: string =
|
let description: string =
|
||||||
node instanceof GPXFile
|
node instanceof GPXFile
|
||||||
? node.metadata.desc ?? ''
|
? node.metadata.desc ?? ''
|
||||||
: node instanceof Track
|
: node instanceof Track
|
||||||
? node.desc ?? ''
|
? node.desc ?? ''
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
$: if (!open) {
|
$: if (!open) {
|
||||||
$editMetadata = false;
|
$editMetadata = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Popover.Root bind:open>
|
<Popover.Root bind:open>
|
||||||
<Popover.Trigger />
|
<Popover.Trigger />
|
||||||
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
|
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
|
||||||
<Label for="name">{$_('menu.metadata.name')}</Label>
|
<Label for="name">{$_('menu.metadata.name')}</Label>
|
||||||
<Input bind:value={name} id="name" class="font-semibold h-8" />
|
<Input bind:value={name} id="name" class="font-semibold h-8" />
|
||||||
<Label for="description">{$_('menu.metadata.description')}</Label>
|
<Label for="description">{$_('menu.metadata.description')}</Label>
|
||||||
<Textarea bind:value={description} id="description" />
|
<Textarea bind:value={description} id="description" />
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
dbUtils.applyToFile(item.getFileId(), (file) => {
|
dbUtils.applyToFile(item.getFileId(), (file) => {
|
||||||
if (item instanceof ListFileItem && node instanceof GPXFile) {
|
if (item instanceof ListFileItem && node instanceof GPXFile) {
|
||||||
file.metadata.name = name;
|
file.metadata.name = name;
|
||||||
file.metadata.desc = description;
|
file.metadata.desc = description;
|
||||||
} else if (item instanceof ListTrackItem && node instanceof Track) {
|
if (file.trk.length === 1) {
|
||||||
file.trk[item.getTrackIndex()].name = name;
|
file.trk[0].name = name;
|
||||||
file.trk[item.getTrackIndex()].desc = description;
|
}
|
||||||
}
|
} else if (item instanceof ListTrackItem && node instanceof Track) {
|
||||||
});
|
file.trk[item.getTrackIndex()].name = name;
|
||||||
open = false;
|
file.trk[item.getTrackIndex()].desc = description;
|
||||||
}}
|
}
|
||||||
>
|
});
|
||||||
<Save size="16" class="mr-1" />
|
open = false;
|
||||||
{$_('menu.metadata.save')}
|
}}
|
||||||
</Button>
|
>
|
||||||
</Popover.Content>
|
<Save size="16" class="mr-1" />
|
||||||
|
{$_('menu.metadata.save')}
|
||||||
|
</Button>
|
||||||
|
</Popover.Content>
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
|
|
||||||
import { font } from "$lib/assets/layers";
|
|
||||||
import { settings } from "$lib/db";
|
import { settings } from "$lib/db";
|
||||||
import { gpxStatistics } from "$lib/stores";
|
import { gpxStatistics } from "$lib/stores";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
const { distanceMarkers, distanceUnits, currentBasemap } = settings;
|
const { distanceMarkers, distanceUnits } = settings;
|
||||||
|
|
||||||
export class DistanceMarkers {
|
export class DistanceMarkers {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
@@ -17,7 +16,7 @@ export class DistanceMarkers {
|
|||||||
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
|
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(distanceMarkers.subscribe(this.updateBinded));
|
this.unsubscribes.push(distanceMarkers.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(distanceUnits.subscribe(this.updateBinded));
|
this.unsubscribes.push(distanceUnits.subscribe(this.updateBinded));
|
||||||
this.map.on('style.load', this.updateBinded);
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
@@ -40,7 +39,7 @@ export class DistanceMarkers {
|
|||||||
layout: {
|
layout: {
|
||||||
'text-field': ['get', 'distance'],
|
'text-field': ['get', 'distance'],
|
||||||
'text-size': 14,
|
'text-size': 14,
|
||||||
'text-font': [font[get(currentBasemap)] ?? 'Open Sans Bold'],
|
'text-font': ['Open Sans Bold'],
|
||||||
'text-padding': 20,
|
'text-padding': 20,
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
|
@@ -7,7 +7,6 @@ import { addSelectItem, selectItem, selection } from "$lib/components/file-list/
|
|||||||
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
|
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
|
||||||
import type { Waypoint } from "gpx";
|
import type { Waypoint } from "gpx";
|
||||||
import { getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils";
|
import { getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils";
|
||||||
import { font } from "$lib/assets/layers";
|
|
||||||
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
|
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
|
||||||
import { MapPin, Square } from "lucide-static";
|
import { MapPin, Square } from "lucide-static";
|
||||||
import { getSymbolKey, symbols } from "$lib/assets/symbols";
|
import { getSymbolKey, symbols } from "$lib/assets/symbols";
|
||||||
@@ -66,7 +65,7 @@ function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
|
|||||||
</svg>`;
|
</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { directionMarkers, verticalFileView, currentBasemap, defaultOpacity, defaultWeight } = settings;
|
const { directionMarkers, verticalFileView, defaultOpacity, defaultWeight } = settings;
|
||||||
|
|
||||||
export class GPXLayer {
|
export class GPXLayer {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
@@ -82,6 +81,7 @@ export class GPXLayer {
|
|||||||
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
||||||
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.bind(this);
|
||||||
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
layerOnClickBinded: (e: any) => void = this.layerOnClick.bind(this);
|
||||||
|
layerOnContextMenuBinded: (e: any) => void = this.layerOnContextMenu.bind(this);
|
||||||
maybeHideWaypointPopupBinded: (e: any) => void = this.maybeHideWaypointPopup.bind(this);
|
maybeHideWaypointPopupBinded: (e: any) => void = this.maybeHideWaypointPopup.bind(this);
|
||||||
|
|
||||||
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
||||||
@@ -112,7 +112,7 @@ export class GPXLayer {
|
|||||||
}));
|
}));
|
||||||
this.draggable = get(currentTool) === Tool.WAYPOINT;
|
this.draggable = get(currentTool) === Tool.WAYPOINT;
|
||||||
|
|
||||||
this.map.on('style.load', this.updateBinded);
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
@@ -154,6 +154,7 @@ export class GPXLayer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.map.on('click', this.fileId, this.layerOnClickBinded);
|
this.map.on('click', this.fileId, this.layerOnClickBinded);
|
||||||
|
this.map.on('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
||||||
this.map.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
this.map.on('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
||||||
this.map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
this.map.on('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
}
|
}
|
||||||
@@ -170,7 +171,7 @@ export class GPXLayer {
|
|||||||
'text-keep-upright': false,
|
'text-keep-upright': false,
|
||||||
'text-max-angle': 361,
|
'text-max-angle': 361,
|
||||||
'text-allow-overlap': true,
|
'text-allow-overlap': true,
|
||||||
'text-font': [font[get(currentBasemap)] ?? 'Open Sans Bold'],
|
'text-font': ['Open Sans Bold'],
|
||||||
'symbol-placement': 'line',
|
'symbol-placement': 'line',
|
||||||
'symbol-spacing': 20,
|
'symbol-spacing': 20,
|
||||||
},
|
},
|
||||||
@@ -262,14 +263,16 @@ export class GPXLayer {
|
|||||||
marker.on('dragend', (e) => {
|
marker.on('dragend', (e) => {
|
||||||
resetCursor();
|
resetCursor();
|
||||||
marker.getElement().style.cursor = '';
|
marker.getElement().style.cursor = '';
|
||||||
dbUtils.applyToFile(this.fileId, (file) => {
|
getElevation([marker._waypoint]).then((ele) => {
|
||||||
let latLng = marker.getLngLat();
|
dbUtils.applyToFile(this.fileId, (file) => {
|
||||||
let wpt = file.wpt[marker._waypoint._data.index];
|
let latLng = marker.getLngLat();
|
||||||
wpt.setCoordinates({
|
let wpt = file.wpt[marker._waypoint._data.index];
|
||||||
lat: latLng.lat,
|
wpt.setCoordinates({
|
||||||
lon: latLng.lng
|
lat: latLng.lat,
|
||||||
|
lon: latLng.lng
|
||||||
|
});
|
||||||
|
wpt.ele = ele[0];
|
||||||
});
|
});
|
||||||
wpt.ele = getElevation(this.map, wpt.getCoordinates());
|
|
||||||
});
|
});
|
||||||
dragEndTimestamp = Date.now()
|
dragEndTimestamp = Date.now()
|
||||||
});
|
});
|
||||||
@@ -294,16 +297,17 @@ export class GPXLayer {
|
|||||||
|
|
||||||
updateMap(map: mapboxgl.Map) {
|
updateMap(map: mapboxgl.Map) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.map.on('style.load', this.updateBinded);
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
if (get(map)) {
|
if (get(map)) {
|
||||||
this.map.off('click', this.fileId, this.layerOnClickBinded);
|
this.map.off('click', this.fileId, this.layerOnClickBinded);
|
||||||
|
this.map.off('contextmenu', this.fileId, this.layerOnContextMenuBinded);
|
||||||
this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
this.map.off('mouseenter', this.fileId, this.layerOnMouseEnterBinded);
|
||||||
this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
this.map.off('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
this.map.off('style.load', this.updateBinded);
|
this.map.off('style.import.load', this.updateBinded);
|
||||||
|
|
||||||
if (this.map.getLayer(this.fileId + '-direction')) {
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
this.map.removeLayer(this.fileId + '-direction');
|
this.map.removeLayer(this.fileId + '-direction');
|
||||||
@@ -381,6 +385,12 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layerOnContextMenu(e: any) {
|
||||||
|
if (e.originalEvent.ctrlKey) {
|
||||||
|
this.layerOnClick(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showWaypointPopup(waypoint: Waypoint) {
|
showWaypointPopup(waypoint: Waypoint) {
|
||||||
if (get(currentPopupWaypoint) !== null) {
|
if (get(currentPopupWaypoint) !== null) {
|
||||||
this.hideWaypointPopup();
|
this.hideWaypointPopup();
|
||||||
|
@@ -24,13 +24,13 @@
|
|||||||
if (text === undefined) {
|
if (text === undefined) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
let sanitized = sanitizeHtml(text, {
|
return sanitizeHtml(text, {
|
||||||
allowedTags: ['a', 'br'],
|
allowedTags: ['a', 'br', 'img'],
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
a: ['href', 'target']
|
a: ['href', 'target'],
|
||||||
|
img: ['src']
|
||||||
}
|
}
|
||||||
}).trim();
|
}).trim();
|
||||||
return sanitized;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
>
|
>
|
||||||
<Trash2 size="16" class="mr-1" />
|
<Trash2 size="16" class="mr-1" />
|
||||||
{$_('menu.delete')}
|
{$_('menu.delete')}
|
||||||
<Shortcut key="" shift={true} click={true} />
|
<Shortcut shift={true} click={true} />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
@@ -99,7 +99,12 @@
|
|||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
div :global(a) {
|
div :global(a) {
|
||||||
@apply text-blue-500 dark:text-blue-300;
|
@apply text-link;
|
||||||
@apply hover:underline;
|
@apply hover:underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div :global(img) {
|
||||||
|
@apply my-0;
|
||||||
|
@apply rounded-md;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,417 +1,435 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||||
import {
|
import {
|
||||||
CirclePlus,
|
CirclePlus,
|
||||||
CircleX,
|
CircleX,
|
||||||
Minus,
|
Minus,
|
||||||
Pencil,
|
Pencil,
|
||||||
Plus,
|
Plus,
|
||||||
Save,
|
Save,
|
||||||
Trash2,
|
Trash2,
|
||||||
Move,
|
Move,
|
||||||
Map,
|
Map,
|
||||||
Layers2
|
Layers2
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import { defaultBasemap, extendBasemap, type CustomLayer } from '$lib/assets/layers';
|
import { defaultBasemap, type CustomLayer } from '$lib/assets/layers';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import Sortable from 'sortablejs/Sortable';
|
import Sortable from 'sortablejs/Sortable';
|
||||||
|
import { customBasemapUpdate } from './utils';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
customLayers,
|
customLayers,
|
||||||
selectedBasemapTree,
|
selectedBasemapTree,
|
||||||
selectedOverlayTree,
|
selectedOverlayTree,
|
||||||
currentBasemap,
|
currentBasemap,
|
||||||
previousBasemap,
|
previousBasemap,
|
||||||
currentOverlays,
|
currentOverlays,
|
||||||
previousOverlays,
|
previousOverlays,
|
||||||
customBasemapOrder,
|
customBasemapOrder,
|
||||||
customOverlayOrder
|
customOverlayOrder
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
let name: string = '';
|
let name: string = '';
|
||||||
let tileUrls: string[] = [''];
|
let tileUrls: string[] = [''];
|
||||||
let maxZoom: number = 20;
|
let maxZoom: number = 20;
|
||||||
let layerType: 'basemap' | 'overlay' = 'basemap';
|
let layerType: 'basemap' | 'overlay' = 'basemap';
|
||||||
let resourceType: 'raster' | 'vector' = 'raster';
|
let resourceType: 'raster' | 'vector' = 'raster';
|
||||||
|
|
||||||
let basemapContainer: HTMLElement;
|
let basemapContainer: HTMLElement;
|
||||||
let overlayContainer: HTMLElement;
|
let overlayContainer: HTMLElement;
|
||||||
|
|
||||||
let basemapSortable: Sortable;
|
let basemapSortable: Sortable;
|
||||||
let overlaySortable: Sortable;
|
let overlaySortable: Sortable;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($customBasemapOrder.length === 0) {
|
if ($customBasemapOrder.length === 0) {
|
||||||
$customBasemapOrder = Object.keys($customLayers).filter(
|
$customBasemapOrder = Object.keys($customLayers).filter(
|
||||||
(id) => $customLayers[id].layerType === 'basemap'
|
(id) => $customLayers[id].layerType === 'basemap'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if ($customOverlayOrder.length === 0) {
|
if ($customOverlayOrder.length === 0) {
|
||||||
$customOverlayOrder = Object.keys($customLayers).filter(
|
$customOverlayOrder = Object.keys($customLayers).filter(
|
||||||
(id) => $customLayers[id].layerType === 'overlay'
|
(id) => $customLayers[id].layerType === 'overlay'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
basemapSortable = Sortable.create(basemapContainer, {
|
basemapSortable = Sortable.create(basemapContainer, {
|
||||||
onSort: (e) => {
|
onSort: (e) => {
|
||||||
$customBasemapOrder = basemapSortable.toArray();
|
$customBasemapOrder = basemapSortable.toArray();
|
||||||
$selectedBasemapTree.basemaps['custom'] = $customBasemapOrder.reduce((acc, id) => {
|
$selectedBasemapTree.basemaps['custom'] = $customBasemapOrder.reduce((acc, id) => {
|
||||||
acc[id] = true;
|
acc[id] = true;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
overlaySortable = Sortable.create(overlayContainer, {
|
overlaySortable = Sortable.create(overlayContainer, {
|
||||||
onSort: (e) => {
|
onSort: (e) => {
|
||||||
$customOverlayOrder = overlaySortable.toArray();
|
$customOverlayOrder = overlaySortable.toArray();
|
||||||
$selectedOverlayTree.overlays['custom'] = $customOverlayOrder.reduce((acc, id) => {
|
$selectedOverlayTree.overlays['custom'] = $customOverlayOrder.reduce((acc, id) => {
|
||||||
acc[id] = true;
|
acc[id] = true;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
basemapSortable.sort($customBasemapOrder);
|
basemapSortable.sort($customBasemapOrder);
|
||||||
overlaySortable.sort($customOverlayOrder);
|
overlaySortable.sort($customOverlayOrder);
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
basemapSortable.destroy();
|
basemapSortable.destroy();
|
||||||
overlaySortable.destroy();
|
overlaySortable.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if (tileUrls[0].length > 0) {
|
$: if (tileUrls[0].length > 0) {
|
||||||
if (
|
if (
|
||||||
tileUrls[0].includes('.json') ||
|
tileUrls[0].includes('.json') ||
|
||||||
(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
|
(tileUrls[0].includes('api.mapbox.com/styles') && !tileUrls[0].includes('tiles'))
|
||||||
) {
|
) {
|
||||||
resourceType = 'vector';
|
resourceType = 'vector';
|
||||||
layerType = 'basemap';
|
} else {
|
||||||
} else {
|
resourceType = 'raster';
|
||||||
resourceType = 'raster';
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function createLayer() {
|
function createLayer() {
|
||||||
if (selectedLayerId && $customLayers[selectedLayerId].layerType !== layerType) {
|
if (selectedLayerId && $customLayers[selectedLayerId].layerType !== layerType) {
|
||||||
deleteLayer(selectedLayerId);
|
deleteLayer(selectedLayerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof maxZoom === 'string') {
|
if (typeof maxZoom === 'string') {
|
||||||
maxZoom = parseInt(maxZoom);
|
maxZoom = parseInt(maxZoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
let layerId = selectedLayerId ?? getLayerId();
|
let layerId = selectedLayerId ?? getLayerId();
|
||||||
let layer: CustomLayer = {
|
let layer: CustomLayer = {
|
||||||
id: layerId,
|
id: layerId,
|
||||||
name: name,
|
name: name,
|
||||||
tileUrls: tileUrls,
|
tileUrls: tileUrls.map((url) => decodeURI(url.trim())),
|
||||||
maxZoom: maxZoom,
|
maxZoom: maxZoom,
|
||||||
layerType: layerType,
|
layerType: layerType,
|
||||||
resourceType: resourceType,
|
resourceType: resourceType,
|
||||||
value: ''
|
value: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
if (resourceType === 'vector') {
|
if (resourceType === 'vector') {
|
||||||
layer.value = tileUrls[0];
|
layer.value = layer.tileUrls[0];
|
||||||
} else {
|
} else {
|
||||||
if (layerType === 'basemap') {
|
layer.value = {
|
||||||
layer.value = extendBasemap({
|
version: 8,
|
||||||
version: 8,
|
sources: {
|
||||||
sources: {
|
[layerId]: {
|
||||||
[layerId]: {
|
type: 'raster',
|
||||||
type: 'raster',
|
tiles: layer.tileUrls,
|
||||||
tiles: tileUrls,
|
tileSize: 256,
|
||||||
maxzoom: maxZoom
|
maxzoom: maxZoom
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
layers: [
|
layers: [
|
||||||
{
|
{
|
||||||
id: layerId,
|
id: layerId,
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
source: layerId
|
source: layerId
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
};
|
||||||
} else {
|
}
|
||||||
layer.value = {
|
$customLayers[layerId] = layer;
|
||||||
type: 'raster',
|
addLayer(layerId);
|
||||||
tiles: tileUrls,
|
selectedLayerId = undefined;
|
||||||
maxzoom: maxZoom
|
setDataFromSelectedLayer();
|
||||||
};
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
$customLayers[layerId] = layer;
|
|
||||||
addLayer(layerId);
|
|
||||||
selectedLayerId = undefined;
|
|
||||||
setDataFromSelectedLayer();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLayerId() {
|
function getLayerId() {
|
||||||
for (let id = 0; ; id++) {
|
for (let id = 0; ; id++) {
|
||||||
if (!$customLayers.hasOwnProperty(`custom-${id}`)) {
|
if (!$customLayers.hasOwnProperty(`custom-${id}`)) {
|
||||||
return `custom-${id}`;
|
return `custom-${id}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLayer(layerId: string) {
|
function addLayer(layerId: string) {
|
||||||
if (layerType === 'basemap') {
|
if (layerType === 'basemap') {
|
||||||
selectedBasemapTree.update(($tree) => {
|
selectedBasemapTree.update(($tree) => {
|
||||||
if (!$tree.basemaps.hasOwnProperty('custom')) {
|
if (!$tree.basemaps.hasOwnProperty('custom')) {
|
||||||
$tree.basemaps['custom'] = {};
|
$tree.basemaps['custom'] = {};
|
||||||
}
|
}
|
||||||
$tree.basemaps['custom'][layerId] = true;
|
$tree.basemaps['custom'][layerId] = true;
|
||||||
return $tree;
|
return $tree;
|
||||||
});
|
});
|
||||||
|
|
||||||
$currentBasemap = layerId;
|
if ($currentBasemap === layerId) {
|
||||||
|
$customBasemapUpdate++;
|
||||||
|
} else {
|
||||||
|
$currentBasemap = layerId;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$customBasemapOrder.includes(layerId)) {
|
if (!$customBasemapOrder.includes(layerId)) {
|
||||||
$customBasemapOrder = [...$customBasemapOrder, layerId];
|
$customBasemapOrder = [...$customBasemapOrder, layerId];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedOverlayTree.update(($tree) => {
|
selectedOverlayTree.update(($tree) => {
|
||||||
if (!$tree.overlays.hasOwnProperty('custom')) {
|
if (!$tree.overlays.hasOwnProperty('custom')) {
|
||||||
$tree.overlays['custom'] = {};
|
$tree.overlays['custom'] = {};
|
||||||
}
|
}
|
||||||
$tree.overlays['custom'][layerId] = true;
|
$tree.overlays['custom'][layerId] = true;
|
||||||
return $tree;
|
return $tree;
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($map && $map.getSource(layerId)) {
|
if (
|
||||||
// Reset source when updating an existing layer
|
$currentOverlays.overlays['custom'] &&
|
||||||
if ($map.getLayer(layerId)) {
|
$currentOverlays.overlays['custom'][layerId] &&
|
||||||
$map.removeLayer(layerId);
|
$map
|
||||||
}
|
) {
|
||||||
$map.removeSource(layerId);
|
try {
|
||||||
}
|
$map.removeImport(layerId);
|
||||||
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!$currentOverlays.overlays.hasOwnProperty('custom')) {
|
if (!$currentOverlays.overlays.hasOwnProperty('custom')) {
|
||||||
$currentOverlays.overlays['custom'] = {};
|
$currentOverlays.overlays['custom'] = {};
|
||||||
}
|
}
|
||||||
$currentOverlays.overlays['custom'][layerId] = true;
|
$currentOverlays.overlays['custom'][layerId] = true;
|
||||||
|
|
||||||
if (!$customOverlayOrder.includes(layerId)) {
|
if (!$customOverlayOrder.includes(layerId)) {
|
||||||
$customOverlayOrder = [...$customOverlayOrder, layerId];
|
$customOverlayOrder = [...$customOverlayOrder, layerId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function tryDeleteLayer(node: any, id: string): any {
|
function tryDeleteLayer(node: any, id: string): any {
|
||||||
if (node.hasOwnProperty(id)) {
|
if (node.hasOwnProperty(id)) {
|
||||||
delete node[id];
|
delete node[id];
|
||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteLayer(layerId: string) {
|
function deleteLayer(layerId: string) {
|
||||||
let layer = $customLayers[layerId];
|
let layer = $customLayers[layerId];
|
||||||
if (layer.layerType === 'basemap') {
|
if (layer.layerType === 'basemap') {
|
||||||
if (layerId === $currentBasemap) {
|
if (layerId === $currentBasemap) {
|
||||||
$currentBasemap = defaultBasemap;
|
$currentBasemap = defaultBasemap;
|
||||||
}
|
}
|
||||||
if (layerId === $previousBasemap) {
|
if (layerId === $previousBasemap) {
|
||||||
$previousBasemap = defaultBasemap;
|
$previousBasemap = defaultBasemap;
|
||||||
}
|
}
|
||||||
|
|
||||||
$selectedBasemapTree.basemaps['custom'] = tryDeleteLayer(
|
$selectedBasemapTree.basemaps['custom'] = tryDeleteLayer(
|
||||||
$selectedBasemapTree.basemaps['custom'],
|
$selectedBasemapTree.basemaps['custom'],
|
||||||
layerId
|
layerId
|
||||||
);
|
);
|
||||||
if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) {
|
if (Object.keys($selectedBasemapTree.basemaps['custom']).length === 0) {
|
||||||
$selectedBasemapTree.basemaps = tryDeleteLayer($selectedBasemapTree.basemaps, 'custom');
|
$selectedBasemapTree.basemaps = tryDeleteLayer(
|
||||||
}
|
$selectedBasemapTree.basemaps,
|
||||||
$customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId);
|
'custom'
|
||||||
} else {
|
);
|
||||||
$currentOverlays.overlays['custom'][layerId] = false;
|
}
|
||||||
if ($previousOverlays.overlays['custom']) {
|
$customBasemapOrder = $customBasemapOrder.filter((id) => id !== layerId);
|
||||||
$previousOverlays.overlays['custom'] = tryDeleteLayer(
|
} else {
|
||||||
$previousOverlays.overlays['custom'],
|
$currentOverlays.overlays['custom'][layerId] = false;
|
||||||
layerId
|
if ($previousOverlays.overlays['custom']) {
|
||||||
);
|
$previousOverlays.overlays['custom'] = tryDeleteLayer(
|
||||||
}
|
$previousOverlays.overlays['custom'],
|
||||||
|
layerId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$selectedOverlayTree.overlays['custom'] = tryDeleteLayer(
|
$selectedOverlayTree.overlays['custom'] = tryDeleteLayer(
|
||||||
$selectedOverlayTree.overlays['custom'],
|
$selectedOverlayTree.overlays['custom'],
|
||||||
layerId
|
layerId
|
||||||
);
|
);
|
||||||
if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) {
|
if (Object.keys($selectedOverlayTree.overlays['custom']).length === 0) {
|
||||||
$selectedOverlayTree.overlays = tryDeleteLayer($selectedOverlayTree.overlays, 'custom');
|
$selectedOverlayTree.overlays = tryDeleteLayer(
|
||||||
}
|
$selectedOverlayTree.overlays,
|
||||||
$customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId);
|
'custom'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$customOverlayOrder = $customOverlayOrder.filter((id) => id !== layerId);
|
||||||
|
|
||||||
if ($map) {
|
if (
|
||||||
if ($map.getLayer(layerId)) {
|
$currentOverlays.overlays['custom'] &&
|
||||||
$map.removeLayer(layerId);
|
$currentOverlays.overlays['custom'][layerId] &&
|
||||||
}
|
$map
|
||||||
if ($map.getSource(layerId)) {
|
) {
|
||||||
$map.removeSource(layerId);
|
try {
|
||||||
}
|
$map.removeImport(layerId);
|
||||||
}
|
} catch (e) {
|
||||||
}
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
$customLayers = tryDeleteLayer($customLayers, layerId);
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
$customLayers = tryDeleteLayer($customLayers, layerId);
|
||||||
|
}
|
||||||
|
|
||||||
let selectedLayerId: string | undefined = undefined;
|
let selectedLayerId: string | undefined = undefined;
|
||||||
|
|
||||||
function setDataFromSelectedLayer() {
|
function setDataFromSelectedLayer() {
|
||||||
if (selectedLayerId) {
|
if (selectedLayerId) {
|
||||||
const layer = $customLayers[selectedLayerId];
|
const layer = $customLayers[selectedLayerId];
|
||||||
name = layer.name;
|
name = layer.name;
|
||||||
tileUrls = layer.tileUrls;
|
tileUrls = layer.tileUrls;
|
||||||
maxZoom = layer.maxZoom;
|
maxZoom = layer.maxZoom;
|
||||||
layerType = layer.layerType;
|
layerType = layer.layerType;
|
||||||
resourceType = layer.resourceType;
|
resourceType = layer.resourceType;
|
||||||
} else {
|
} else {
|
||||||
name = '';
|
name = '';
|
||||||
tileUrls = [''];
|
tileUrls = [''];
|
||||||
maxZoom = 20;
|
maxZoom = 20;
|
||||||
layerType = 'basemap';
|
layerType = 'basemap';
|
||||||
resourceType = 'raster';
|
resourceType = 'raster';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: selectedLayerId, setDataFromSelectedLayer();
|
$: selectedLayerId, setDataFromSelectedLayer();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
{#if $customBasemapOrder.length > 0}
|
{#if $customBasemapOrder.length > 0}
|
||||||
<div class="flex flex-row items-center gap-1 font-semibold mb-2">
|
<div class="flex flex-row items-center gap-1 font-semibold mb-2">
|
||||||
<Map size="16" />
|
<Map size="16" />
|
||||||
{$_('layers.label.basemaps')}
|
{$_('layers.label.basemaps')}
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
bind:this={basemapContainer}
|
bind:this={basemapContainer}
|
||||||
class="ml-1.5 flex flex-col gap-1 {$customBasemapOrder.length > 0 ? 'mb-2' : ''}"
|
class="ml-1.5 flex flex-col gap-1 {$customBasemapOrder.length > 0 ? 'mb-2' : ''}"
|
||||||
>
|
>
|
||||||
{#each $customBasemapOrder as id (id)}
|
{#each $customBasemapOrder as id (id)}
|
||||||
<div class="flex flex-row items-center gap-2" data-id={id}>
|
<div class="flex flex-row items-center gap-2" data-id={id}>
|
||||||
<Move size="12" />
|
<Move size="12" />
|
||||||
<span class="grow">{$customLayers[id].name}</span>
|
<span class="grow">{$customLayers[id].name}</span>
|
||||||
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
|
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
|
||||||
<Pencil size="16" />
|
<Pencil size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
|
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
|
||||||
<Trash2 size="16" />
|
<Trash2 size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if $customOverlayOrder.length > 0}
|
{#if $customOverlayOrder.length > 0}
|
||||||
<div class="flex flex-row items-center gap-1 font-semibold mb-2">
|
<div class="flex flex-row items-center gap-1 font-semibold mb-2">
|
||||||
<Layers2 size="16" />
|
<Layers2 size="16" />
|
||||||
{$_('layers.label.overlays')}
|
{$_('layers.label.overlays')}
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<Separator />
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div
|
||||||
bind:this={overlayContainer}
|
bind:this={overlayContainer}
|
||||||
class="ml-1.5 flex flex-col gap-1 {$customOverlayOrder.length > 0 ? 'mb-2' : ''}"
|
class="ml-1.5 flex flex-col gap-1 {$customOverlayOrder.length > 0 ? 'mb-2' : ''}"
|
||||||
>
|
>
|
||||||
{#each $customOverlayOrder as id (id)}
|
{#each $customOverlayOrder as id (id)}
|
||||||
<div class="flex flex-row items-center gap-2" data-id={id}>
|
<div class="flex flex-row items-center gap-2" data-id={id}>
|
||||||
<Move size="12" />
|
<Move size="12" />
|
||||||
<span class="grow">{$customLayers[id].name}</span>
|
<span class="grow">{$customLayers[id].name}</span>
|
||||||
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
|
<Button variant="outline" on:click={() => (selectedLayerId = id)} class="p-1 h-7">
|
||||||
<Pencil size="16" />
|
<Pencil size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
|
<Button variant="outline" on:click={() => deleteLayer(id)} class="p-1 h-7">
|
||||||
<Trash2 size="16" />
|
<Trash2 size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header class="p-3">
|
<Card.Header class="p-3">
|
||||||
<Card.Title class="text-base">
|
<Card.Title class="text-base">
|
||||||
{#if selectedLayerId}
|
{#if selectedLayerId}
|
||||||
{$_('layers.custom_layers.edit')}
|
{$_('layers.custom_layers.edit')}
|
||||||
{:else}
|
{:else}
|
||||||
{$_('layers.custom_layers.new')}
|
{$_('layers.custom_layers.new')}
|
||||||
{/if}
|
{/if}
|
||||||
</Card.Title>
|
</Card.Title>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content class="p-3 pt-0">
|
<Card.Content class="p-3 pt-0">
|
||||||
<fieldset class="flex flex-col gap-2">
|
<fieldset class="flex flex-col gap-2">
|
||||||
<Label for="name">{$_('menu.metadata.name')}</Label>
|
<Label for="name">{$_('menu.metadata.name')}</Label>
|
||||||
<Input bind:value={name} id="name" class="h-8" />
|
<Input bind:value={name} id="name" class="h-8" />
|
||||||
<Label for="url">{$_('layers.custom_layers.urls')}</Label>
|
<Label for="url">{$_('layers.custom_layers.urls')}</Label>
|
||||||
{#each tileUrls as url, i}
|
{#each tileUrls as url, i}
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<Input
|
<Input
|
||||||
bind:value={tileUrls[i]}
|
bind:value={tileUrls[i]}
|
||||||
id="url"
|
id="url"
|
||||||
class="h-8"
|
class="h-8"
|
||||||
placeholder={$_('layers.custom_layers.url_placeholder')}
|
placeholder={$_('layers.custom_layers.url_placeholder')}
|
||||||
/>
|
/>
|
||||||
{#if tileUrls.length > 1}
|
{#if tileUrls.length > 1}
|
||||||
<Button
|
<Button
|
||||||
on:click={() => (tileUrls = tileUrls.filter((_, index) => index !== i))}
|
on:click={() =>
|
||||||
variant="outline"
|
(tileUrls = tileUrls.filter((_, index) => index !== i))}
|
||||||
class="p-1 h-8"
|
variant="outline"
|
||||||
>
|
class="p-1 h-8"
|
||||||
<Minus size="16" />
|
>
|
||||||
</Button>
|
<Minus size="16" />
|
||||||
{/if}
|
</Button>
|
||||||
{#if i === tileUrls.length - 1}
|
{/if}
|
||||||
<Button
|
{#if i === tileUrls.length - 1}
|
||||||
on:click={() => (tileUrls = [...tileUrls, ''])}
|
<Button
|
||||||
variant="outline"
|
on:click={() => (tileUrls = [...tileUrls, ''])}
|
||||||
class="p-1 h-8"
|
variant="outline"
|
||||||
>
|
class="p-1 h-8"
|
||||||
<Plus size="16" />
|
>
|
||||||
</Button>
|
<Plus size="16" />
|
||||||
{/if}
|
</Button>
|
||||||
</div>
|
{/if}
|
||||||
{/each}
|
</div>
|
||||||
{#if resourceType === 'raster'}
|
{/each}
|
||||||
<Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label>
|
{#if resourceType === 'raster'}
|
||||||
<Input type="number" bind:value={maxZoom} id="maxZoom" min={0} max={22} class="h-8" />
|
<Label for="maxZoom">{$_('layers.custom_layers.max_zoom')}</Label>
|
||||||
{/if}
|
<Input
|
||||||
<Label>{$_('layers.custom_layers.layer_type')}</Label>
|
type="number"
|
||||||
<RadioGroup.Root bind:value={layerType} class="flex flex-row">
|
bind:value={maxZoom}
|
||||||
<div class="flex items-center space-x-2">
|
id="maxZoom"
|
||||||
<RadioGroup.Item value="basemap" id="basemap" />
|
min={0}
|
||||||
<Label for="basemap">{$_('layers.custom_layers.basemap')}</Label>
|
max={22}
|
||||||
</div>
|
class="h-8"
|
||||||
<div class="flex items-center space-x-2">
|
/>
|
||||||
<RadioGroup.Item value="overlay" id="overlay" disabled={resourceType === 'vector'} />
|
{/if}
|
||||||
<Label for="overlay">{$_('layers.custom_layers.overlay')}</Label>
|
<Label>{$_('layers.custom_layers.layer_type')}</Label>
|
||||||
</div>
|
<RadioGroup.Root bind:value={layerType} class="flex flex-row">
|
||||||
</RadioGroup.Root>
|
<div class="flex items-center space-x-2">
|
||||||
{#if selectedLayerId}
|
<RadioGroup.Item value="basemap" id="basemap" />
|
||||||
<div class="mt-2 flex flex-row gap-2">
|
<Label for="basemap">{$_('layers.custom_layers.basemap')}</Label>
|
||||||
<Button variant="outline" on:click={createLayer} class="grow">
|
</div>
|
||||||
<Save size="16" class="mr-1" />
|
<div class="flex items-center space-x-2">
|
||||||
{$_('layers.custom_layers.update')}
|
<RadioGroup.Item value="overlay" id="overlay" />
|
||||||
</Button>
|
<Label for="overlay">{$_('layers.custom_layers.overlay')}</Label>
|
||||||
<Button variant="outline" on:click={() => (selectedLayerId = undefined)}>
|
</div>
|
||||||
<CircleX size="16" />
|
</RadioGroup.Root>
|
||||||
</Button>
|
{#if selectedLayerId}
|
||||||
</div>
|
<div class="mt-2 flex flex-row gap-2">
|
||||||
{:else}
|
<Button variant="outline" on:click={createLayer} class="grow">
|
||||||
<Button variant="outline" class="mt-2" on:click={createLayer}>
|
<Save size="16" class="mr-1" />
|
||||||
<CirclePlus size="16" class="mr-1" />
|
{$_('layers.custom_layers.update')}
|
||||||
{$_('layers.custom_layers.create')}
|
</Button>
|
||||||
</Button>
|
<Button variant="outline" on:click={() => (selectedLayerId = undefined)}>
|
||||||
{/if}
|
<CircleX size="16" />
|
||||||
</fieldset>
|
</Button>
|
||||||
</Card.Content>
|
</div>
|
||||||
</Card.Root>
|
{:else}
|
||||||
|
<Button variant="outline" class="mt-2" on:click={createLayer}>
|
||||||
|
<CirclePlus size="16" class="mr-1" />
|
||||||
|
{$_('layers.custom_layers.create')}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</fieldset>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import { getLayers } from './utils';
|
import { customBasemapUpdate, getLayers } from './utils';
|
||||||
import { OverpassLayer } from './OverpassLayer';
|
import { OverpassLayer } from './OverpassLayer';
|
||||||
import OverpassPopup from './OverpassPopup.svelte';
|
import OverpassPopup from './OverpassPopup.svelte';
|
||||||
|
|
||||||
@@ -35,33 +35,80 @@
|
|||||||
let basemap = basemaps.hasOwnProperty($currentBasemap)
|
let basemap = basemaps.hasOwnProperty($currentBasemap)
|
||||||
? basemaps[$currentBasemap]
|
? basemaps[$currentBasemap]
|
||||||
: $customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap];
|
: $customLayers[$currentBasemap]?.value ?? basemaps[defaultBasemap];
|
||||||
$map.setStyle(basemap, {
|
$map.removeImport('basemap');
|
||||||
diff: false
|
if (typeof basemap === 'string') {
|
||||||
});
|
$map.addImport({ id: 'basemap', url: basemap }, 'overlays');
|
||||||
|
} else {
|
||||||
|
$map.addImport(
|
||||||
|
{
|
||||||
|
id: 'basemap',
|
||||||
|
data: basemap
|
||||||
|
},
|
||||||
|
'overlays'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($map && $currentBasemap) {
|
$: if ($map && ($currentBasemap || $customBasemapUpdate)) {
|
||||||
setStyle();
|
setStyle();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($map && $currentOverlays) {
|
function addOverlay(id: string) {
|
||||||
// Add or remove overlay layers depending on the current overlays
|
try {
|
||||||
let overlayLayers = getLayers($currentOverlays);
|
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
|
||||||
Object.keys(overlayLayers).forEach((id) => {
|
if (typeof overlay === 'string') {
|
||||||
if (overlayLayers[id]) {
|
$map.addImport({ id, url: overlay });
|
||||||
if (!addOverlayLayer.hasOwnProperty(id)) {
|
} else {
|
||||||
addOverlayLayer[id] = addOverlayLayerForId(id);
|
if ($opacities.hasOwnProperty(id)) {
|
||||||
|
overlay = {
|
||||||
|
...overlay,
|
||||||
|
layers: overlay.layers.map((layer) => {
|
||||||
|
if (layer.type === 'raster') {
|
||||||
|
if (!layer.paint) {
|
||||||
|
layer.paint = {};
|
||||||
|
}
|
||||||
|
layer.paint['raster-opacity'] = $opacities[id];
|
||||||
|
}
|
||||||
|
return layer;
|
||||||
|
})
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (!$map.getLayer(id)) {
|
$map.addImport({
|
||||||
addOverlayLayer[id]();
|
id,
|
||||||
$map.on('style.load', addOverlayLayer[id]);
|
data: overlay
|
||||||
}
|
});
|
||||||
} else if ($map.getLayer(id)) {
|
|
||||||
$map.removeLayer(id);
|
|
||||||
$map.off('style.load', addOverlayLayer[id]);
|
|
||||||
}
|
}
|
||||||
});
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOverlays() {
|
||||||
|
if ($map && $currentOverlays) {
|
||||||
|
let overlayLayers = getLayers($currentOverlays);
|
||||||
|
try {
|
||||||
|
let activeOverlays = $map
|
||||||
|
.getStyle()
|
||||||
|
.imports.filter((i) => i.id !== 'basemap' && i.id !== 'overlays');
|
||||||
|
let toRemove = activeOverlays.filter((i) => !overlayLayers[i.id]);
|
||||||
|
toRemove.forEach((i) => {
|
||||||
|
$map.removeImport(i.id);
|
||||||
|
});
|
||||||
|
let toAdd = Object.entries(overlayLayers)
|
||||||
|
.filter(([id, selected]) => selected && !activeOverlays.some((j) => j.id === id))
|
||||||
|
.map(([id]) => id);
|
||||||
|
toAdd.forEach((id) => {
|
||||||
|
addOverlay(id);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to add sources and layers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if ($map && $currentOverlays) {
|
||||||
|
updateOverlays();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if ($map) {
|
$: if ($map) {
|
||||||
@@ -70,6 +117,7 @@
|
|||||||
}
|
}
|
||||||
overpassLayer = new OverpassLayer($map);
|
overpassLayer = new OverpassLayer($map);
|
||||||
overpassLayer.add();
|
overpassLayer.add();
|
||||||
|
$map.on('style.import.load', updateOverlays);
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedBasemap = writable(get(currentBasemap));
|
let selectedBasemap = writable(get(currentBasemap));
|
||||||
@@ -85,37 +133,6 @@
|
|||||||
selectedBasemap.set(value);
|
selectedBasemap.set(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
let addOverlayLayer: { [key: string]: () => void } = {};
|
|
||||||
function addOverlayLayerForId(id: string) {
|
|
||||||
return () => {
|
|
||||||
if ($map) {
|
|
||||||
try {
|
|
||||||
let overlay = $customLayers.hasOwnProperty(id) ? $customLayers[id].value : overlays[id];
|
|
||||||
if (!$map.getSource(id)) {
|
|
||||||
$map.addSource(id, overlay);
|
|
||||||
}
|
|
||||||
$map.addLayer(
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
type: overlay.type === 'raster' ? 'raster' : 'line',
|
|
||||||
source: id,
|
|
||||||
paint: {
|
|
||||||
...(id in $opacities
|
|
||||||
? overlay.type === 'raster'
|
|
||||||
? { 'raster-opacity': $opacities[id] }
|
|
||||||
: { 'line-opacity': $opacities[id] }
|
|
||||||
: {})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'overlays'
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// No reliable way to check if the map is ready to add sources and layers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let open = false;
|
let open = false;
|
||||||
function openLayerControl() {
|
function openLayerControl() {
|
||||||
open = true;
|
open = true;
|
||||||
|
@@ -9,8 +9,14 @@
|
|||||||
import * as Select from '$lib/components/ui/select';
|
import * as Select from '$lib/components/ui/select';
|
||||||
import { Slider } from '$lib/components/ui/slider';
|
import { Slider } from '$lib/components/ui/slider';
|
||||||
|
|
||||||
import { basemapTree, overlays, overlayTree, overpassTree } from '$lib/assets/layers';
|
import {
|
||||||
import { isSelected } from '$lib/components/layer-control/utils';
|
basemapTree,
|
||||||
|
defaultBasemap,
|
||||||
|
overlays,
|
||||||
|
overlayTree,
|
||||||
|
overpassTree
|
||||||
|
} from '$lib/assets/layers';
|
||||||
|
import { getLayers, isSelected, toggle } from '$lib/components/layer-control/utils';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
@@ -22,6 +28,7 @@
|
|||||||
selectedBasemapTree,
|
selectedBasemapTree,
|
||||||
selectedOverlayTree,
|
selectedOverlayTree,
|
||||||
selectedOverpassTree,
|
selectedOverpassTree,
|
||||||
|
currentBasemap,
|
||||||
currentOverlays,
|
currentOverlays,
|
||||||
customLayers,
|
customLayers,
|
||||||
opacities
|
opacities
|
||||||
@@ -46,6 +53,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if ($selectedBasemapTree && $currentBasemap) {
|
||||||
|
if (!isSelected($selectedBasemapTree, $currentBasemap)) {
|
||||||
|
if (!isSelected($selectedBasemapTree, defaultBasemap)) {
|
||||||
|
$selectedBasemapTree = toggle($selectedBasemapTree, defaultBasemap);
|
||||||
|
}
|
||||||
|
$currentBasemap = defaultBasemap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if ($selectedOverlayTree && $currentOverlays) {
|
||||||
|
let overlayLayers = getLayers($currentOverlays);
|
||||||
|
let toRemove = Object.entries(overlayLayers).filter(
|
||||||
|
([id, checked]) => checked && !isSelected($selectedOverlayTree, id)
|
||||||
|
);
|
||||||
|
if (toRemove.length > 0) {
|
||||||
|
currentOverlays.update((tree) => {
|
||||||
|
toRemove.forEach(([id]) => {
|
||||||
|
toggle(tree, id);
|
||||||
|
});
|
||||||
|
return tree;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$: if ($selectedOverlay) {
|
$: if ($selectedOverlay) {
|
||||||
setOpacityFromSelection();
|
setOpacityFromSelection();
|
||||||
}
|
}
|
||||||
|
@@ -46,6 +46,7 @@
|
|||||||
value={id}
|
value={id}
|
||||||
bind:checked={checked[id]}
|
bind:checked={checked[id]}
|
||||||
class="scale-90"
|
class="scale-90"
|
||||||
|
aria-label={$_(`layers.label.${id}`)}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<input id="{name}-{id}" type="radio" {name} value={id} bind:group={selected} />
|
<input id="{name}-{id}" type="radio" {name} value={id} bind:group={selected} />
|
||||||
|
@@ -50,7 +50,7 @@ export class OverpassLayer {
|
|||||||
|
|
||||||
add() {
|
add() {
|
||||||
this.map.on('moveend', this.queryIfNeededBinded);
|
this.map.on('moveend', this.queryIfNeededBinded);
|
||||||
this.map.on('style.load', this.updateBinded);
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
this.unsubscribes.push(data.subscribe(this.updateBinded));
|
this.unsubscribes.push(data.subscribe(this.updateBinded));
|
||||||
this.unsubscribes.push(currentOverpassQueries.subscribe(() => {
|
this.unsubscribes.push(currentOverpassQueries.subscribe(() => {
|
||||||
this.updateBinded();
|
this.updateBinded();
|
||||||
@@ -108,15 +108,19 @@ export class OverpassLayer {
|
|||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
this.map.off('moveend', this.queryIfNeededBinded);
|
this.map.off('moveend', this.queryIfNeededBinded);
|
||||||
this.map.off('style.load', this.updateBinded);
|
this.map.off('style.import.load', this.updateBinded);
|
||||||
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
this.unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||||
|
|
||||||
if (this.map.getLayer('overpass')) {
|
try {
|
||||||
this.map.removeLayer('overpass');
|
if (this.map.getLayer('overpass')) {
|
||||||
}
|
this.map.removeLayer('overpass');
|
||||||
|
}
|
||||||
|
|
||||||
if (this.map.getSource('overpass')) {
|
if (this.map.getSource('overpass')) {
|
||||||
this.map.removeSource('overpass');
|
this.map.removeSource('overpass');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// No reliable way to check if the map is ready to remove sources and layers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -62,11 +62,11 @@
|
|||||||
{#if key !== 'name' && !key.includes('image')}
|
{#if key !== 'name' && !key.includes('image')}
|
||||||
<span class="font-mono">{key}</span>
|
<span class="font-mono">{key}</span>
|
||||||
{#if key === 'website' || key === 'contact:website' || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
|
{#if key === 'website' || key === 'contact:website' || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
|
||||||
<a href={value} target="_blank" class="text-blue-500 underline">{value}</a>
|
<a href={value} target="_blank" class="text-link underline">{value}</a>
|
||||||
{:else if key === 'phone' || key === 'contact:phone'}
|
{:else if key === 'phone' || key === 'contact:phone'}
|
||||||
<a href={'tel:' + value} class="text-blue-500 underline">{value}</a>
|
<a href={'tel:' + value} class="text-link underline">{value}</a>
|
||||||
{:else if key === 'email' || key === 'contact:email'}
|
{:else if key === 'email' || key === 'contact:email'}
|
||||||
<a href={'mailto:' + value} class="text-blue-500 underline">{value}</a>
|
<a href={'mailto:' + value} class="text-link underline">{value}</a>
|
||||||
{:else}
|
{:else}
|
||||||
<span>{value}</span>
|
<span>{value}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import type { LayerTreeType } from "$lib/assets/layers";
|
import type { LayerTreeType } from "$lib/assets/layers";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
export function anySelectedLayer(node: LayerTreeType) {
|
export function anySelectedLayer(node: LayerTreeType) {
|
||||||
return Object.keys(node).find((id) => {
|
return Object.keys(node).find((id) => {
|
||||||
@@ -36,4 +37,17 @@ export function isSelected(node: LayerTreeType, id: string) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toggle(node: LayerTreeType, id: string) {
|
||||||
|
Object.keys(node).forEach((key) => {
|
||||||
|
if (key === id) {
|
||||||
|
node[key] = !node[key];
|
||||||
|
} else if (typeof node[key] !== "boolean") {
|
||||||
|
toggle(node[key], id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const customBasemapUpdate = writable(0);
|
@@ -1,11 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
|
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
|
||||||
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
import { Toggle } from '$lib/components/ui/toggle';
|
import { Toggle } from '$lib/components/ui/toggle';
|
||||||
import { PersonStanding, X } from 'lucide-svelte';
|
import { PersonStanding, X } from 'lucide-svelte';
|
||||||
import { MapillaryLayer } from './Mapillary';
|
import { MapillaryLayer } from './Mapillary';
|
||||||
import { GoogleRedirect } from './Google';
|
import { GoogleRedirect } from './Google';
|
||||||
import { map, streetViewEnabled } from '$lib/stores';
|
import { map, streetViewEnabled } from '$lib/stores';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
const { streetViewSource } = settings;
|
const { streetViewSource } = settings;
|
||||||
|
|
||||||
@@ -38,9 +40,15 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CustomControl class="w-[29px] h-[29px] shrink-0">
|
<CustomControl class="w-[29px] h-[29px] shrink-0">
|
||||||
<Toggle bind:pressed={$streetViewEnabled} class="w-full h-full rounded p-0">
|
<Tooltip class="w-full h-full" side="left" label={$_('menu.toggle_street_view')}>
|
||||||
<PersonStanding size="22" />
|
<Toggle
|
||||||
</Toggle>
|
bind:pressed={$streetViewEnabled}
|
||||||
|
class="w-full h-full rounded p-0"
|
||||||
|
aria-label={$_('menu.toggle_street_view')}
|
||||||
|
>
|
||||||
|
<PersonStanding size="22" />
|
||||||
|
</Toggle>
|
||||||
|
</Tooltip>
|
||||||
</CustomControl>
|
</CustomControl>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@@ -9,7 +9,8 @@
|
|||||||
Ungroup,
|
Ungroup,
|
||||||
MapPin,
|
MapPin,
|
||||||
Filter,
|
Filter,
|
||||||
Scissors
|
Scissors,
|
||||||
|
MountainSnow
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
@@ -21,37 +22,32 @@
|
|||||||
class="h-fit flex flex-col p-1 gap-1.5 bg-background rounded-r-md pointer-events-auto shadow-md {$$props.class ??
|
class="h-fit flex flex-col p-1 gap-1.5 bg-background rounded-r-md pointer-events-auto shadow-md {$$props.class ??
|
||||||
''}"
|
''}"
|
||||||
>
|
>
|
||||||
<ToolbarItem tool={Tool.ROUTING}>
|
<ToolbarItem tool={Tool.ROUTING} label={$_('toolbar.routing.tooltip')}>
|
||||||
<Pencil slot="icon" size="18" class="h-" />
|
<Pencil slot="icon" size="18" />
|
||||||
<span slot="tooltip">{$_('toolbar.routing.tooltip')}</span>
|
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.WAYPOINT}>
|
<ToolbarItem tool={Tool.WAYPOINT} label={$_('toolbar.waypoint.tooltip')}>
|
||||||
<MapPin slot="icon" size="18" />
|
<MapPin slot="icon" size="18" />
|
||||||
<span slot="tooltip">{$_('toolbar.waypoint.tooltip')}</span>
|
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.SCISSORS}>
|
<ToolbarItem tool={Tool.SCISSORS} label={$_('toolbar.scissors.tooltip')}>
|
||||||
<Scissors slot="icon" size="18" />
|
<Scissors slot="icon" size="18" />
|
||||||
<span slot="tooltip">{$_('toolbar.scissors.tooltip')}</span>
|
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.TIME}>
|
<ToolbarItem tool={Tool.TIME} label={$_('toolbar.time.tooltip')}>
|
||||||
<CalendarClock slot="icon" size="18" />
|
<CalendarClock slot="icon" size="18" />
|
||||||
<span slot="tooltip">{$_('toolbar.time.tooltip')}</span>
|
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.MERGE}>
|
<ToolbarItem tool={Tool.MERGE} label={$_('toolbar.merge.tooltip')}>
|
||||||
<Group slot="icon" size="18" />
|
<Group slot="icon" size="18" />
|
||||||
<span slot="tooltip">{$_('toolbar.merge.tooltip')}</span>
|
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.EXTRACT}>
|
<ToolbarItem tool={Tool.EXTRACT} label={$_('toolbar.extract.tooltip')}>
|
||||||
<Ungroup slot="icon" size="18" />
|
<Ungroup slot="icon" size="18" />
|
||||||
<span slot="tooltip">{$_('toolbar.extract.tooltip')}</span>
|
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.REDUCE}>
|
<ToolbarItem tool={Tool.ELEVATION} label={$_('toolbar.elevation.button')}>
|
||||||
|
<MountainSnow slot="icon" size="18" />
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem tool={Tool.REDUCE} label={$_('toolbar.reduce.tooltip')}>
|
||||||
<Filter slot="icon" size="18" />
|
<Filter slot="icon" size="18" />
|
||||||
<span slot="tooltip">{$_('toolbar.reduce.tooltip')}</span>
|
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
<ToolbarItem tool={Tool.CLEAN}>
|
<ToolbarItem tool={Tool.CLEAN} label={$_('toolbar.clean.tooltip')}>
|
||||||
<SquareDashedMousePointer slot="icon" size="18" />
|
<SquareDashedMousePointer slot="icon" size="18" />
|
||||||
<span slot="tooltip">{$_('toolbar.clean.tooltip')}</span>
|
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
</div>
|
</div>
|
||||||
<ToolbarItemMenu class={$$props.class ?? ''} />
|
<ToolbarItemMenu class={$$props.class ?? ''} />
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
import { currentTool, type Tool } from '$lib/stores';
|
import { currentTool, type Tool } from '$lib/stores';
|
||||||
|
|
||||||
export let tool: Tool;
|
export let tool: Tool;
|
||||||
|
export let label: string;
|
||||||
|
|
||||||
function toggleTool() {
|
function toggleTool() {
|
||||||
currentTool.update((current) => (current === tool ? null : tool));
|
currentTool.update((current) => (current === tool ? null : tool));
|
||||||
@@ -17,11 +18,12 @@
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="h-[26px] px-1 py-1.5 {$currentTool === tool ? 'bg-accent' : ''}"
|
class="h-[26px] px-1 py-1.5 {$currentTool === tool ? 'bg-accent' : ''}"
|
||||||
on:click={toggleTool}
|
on:click={toggleTool}
|
||||||
|
aria-label={label}
|
||||||
>
|
>
|
||||||
<slot name="icon" />
|
<slot name="icon" />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip.Trigger>
|
</Tooltip.Trigger>
|
||||||
<Tooltip.Content side="right">
|
<Tooltip.Content side="right">
|
||||||
<slot name="tooltip" />
|
<span>{label}</span>
|
||||||
</Tooltip.Content>
|
</Tooltip.Content>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
|
@@ -9,6 +9,7 @@
|
|||||||
import Time from '$lib/components/toolbar/tools/Time.svelte';
|
import Time from '$lib/components/toolbar/tools/Time.svelte';
|
||||||
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
|
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
|
||||||
import Extract from '$lib/components/toolbar/tools/Extract.svelte';
|
import Extract from '$lib/components/toolbar/tools/Extract.svelte';
|
||||||
|
import Elevation from '$lib/components/toolbar/tools/Elevation.svelte';
|
||||||
import Clean from '$lib/components/toolbar/tools/Clean.svelte';
|
import Clean from '$lib/components/toolbar/tools/Clean.svelte';
|
||||||
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
|
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
|
||||||
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
|
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
|
||||||
@@ -48,6 +49,8 @@
|
|||||||
<Time />
|
<Time />
|
||||||
{:else if $currentTool === Tool.MERGE}
|
{:else if $currentTool === Tool.MERGE}
|
||||||
<Merge />
|
<Merge />
|
||||||
|
{:else if $currentTool === Tool.ELEVATION}
|
||||||
|
<Elevation />
|
||||||
{:else if $currentTool === Tool.EXTRACT}
|
{:else if $currentTool === Tool.EXTRACT}
|
||||||
<Extract />
|
<Extract />
|
||||||
{:else if $currentTool === Tool.CLEAN}
|
{:else if $currentTool === Tool.CLEAN}
|
||||||
|
@@ -11,9 +11,9 @@
|
|||||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { resetCursor, setCrosshairCursor } from '$lib/utils';
|
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||||
import { Trash2 } from 'lucide-svelte';
|
import { Trash2 } from 'lucide-svelte';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
<Trash2 size="16" class="mr-1" />
|
<Trash2 size="16" class="mr-1" />
|
||||||
{$_('toolbar.clean.button')}
|
{$_('toolbar.clean.button')}
|
||||||
</Button>
|
</Button>
|
||||||
<Help link="./help/toolbar/clean">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/clean')}>
|
||||||
{#if validSelection}
|
{#if validSelection}
|
||||||
{$_('toolbar.clean.help')}
|
{$_('toolbar.clean.help')}
|
||||||
{:else}
|
{:else}
|
||||||
|
35
website/src/lib/components/toolbar/tools/Elevation.svelte
Normal file
35
website/src/lib/components/toolbar/tools/Elevation.svelte
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
|
import Help from '$lib/components/Help.svelte';
|
||||||
|
import { MountainSnow } from 'lucide-svelte';
|
||||||
|
import { dbUtils } from '$lib/db';
|
||||||
|
import { map } from '$lib/stores';
|
||||||
|
import { _, locale } from 'svelte-i18n';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
|
$: validSelection = $selection.size > 0;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="whitespace-normal h-fit"
|
||||||
|
disabled={!validSelection}
|
||||||
|
on:click={async () => {
|
||||||
|
if ($map) {
|
||||||
|
dbUtils.addElevationToSelection($map);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MountainSnow size="16" class="mr-1 shrink-0" />
|
||||||
|
{$_('toolbar.elevation.button')}
|
||||||
|
</Button>
|
||||||
|
<Help link={getURLForLanguage($locale, '/help/toolbar/elevation')}>
|
||||||
|
{#if validSelection}
|
||||||
|
{$_('toolbar.elevation.help')}
|
||||||
|
{:else}
|
||||||
|
{$_('toolbar.elevation.help_no_selection')}
|
||||||
|
{/if}
|
||||||
|
</Help>
|
||||||
|
</div>
|
@@ -11,7 +11,8 @@
|
|||||||
} from '$lib/components/file-list/FileList';
|
} from '$lib/components/file-list/FileList';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { dbUtils, getFile } from '$lib/db';
|
import { dbUtils, getFile } from '$lib/db';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
$: validSelection =
|
$: validSelection =
|
||||||
$selection.size > 0 &&
|
$selection.size > 0 &&
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
<Ungroup size="16" class="mr-1" />
|
<Ungroup size="16" class="mr-1" />
|
||||||
{$_('toolbar.extract.button')}
|
{$_('toolbar.extract.button')}
|
||||||
</Button>
|
</Button>
|
||||||
<Help link="./help/toolbar/extract">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/extract')}>
|
||||||
{#if validSelection}
|
{#if validSelection}
|
||||||
{$_('toolbar.extract.help')}
|
{$_('toolbar.extract.help')}
|
||||||
{:else}
|
{:else}
|
||||||
|
@@ -12,9 +12,11 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
import { dbUtils, getFile } from '$lib/db';
|
import { dbUtils, getFile } from '$lib/db';
|
||||||
import { Group } from 'lucide-svelte';
|
import { Group } from 'lucide-svelte';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||||
|
|
||||||
let canMergeTraces = false;
|
let canMergeTraces = false;
|
||||||
let canMergeContents = false;
|
let canMergeContents = false;
|
||||||
@@ -65,24 +67,39 @@
|
|||||||
</RadioGroup.Root>
|
</RadioGroup.Root>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
class="whitespace-normal h-fit"
|
||||||
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
|
disabled={(mergeType === MergeType.TRACES && !canMergeTraces) ||
|
||||||
(mergeType === MergeType.CONTENTS && !canMergeContents)}
|
(mergeType === MergeType.CONTENTS && !canMergeContents)}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
dbUtils.mergeSelection(mergeType === MergeType.TRACES);
|
dbUtils.mergeSelection(mergeType === MergeType.TRACES);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group size="16" class="mr-1" />
|
<Group size="16" class="mr-1 shrink-0" />
|
||||||
{$_('toolbar.merge.merge_selection')}
|
{$_('toolbar.merge.merge_selection')}
|
||||||
</Button>
|
</Button>
|
||||||
<Help link="./help/toolbar/merge">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/merge')}>
|
||||||
{#if mergeType === MergeType.TRACES && canMergeTraces}
|
{#if mergeType === MergeType.TRACES && canMergeTraces}
|
||||||
{$_('toolbar.merge.help_merge_traces')}
|
{$_('toolbar.merge.help_merge_traces')}
|
||||||
{:else if mergeType === MergeType.TRACES && !canMergeTraces}
|
{:else if mergeType === MergeType.TRACES && !canMergeTraces}
|
||||||
{$_('toolbar.merge.help_cannot_merge_traces')}
|
{$_('toolbar.merge.help_cannot_merge_traces')}
|
||||||
|
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
|
||||||
|
<Shortcut
|
||||||
|
ctrl={true}
|
||||||
|
click={true}
|
||||||
|
class="inline-flex text-muted-foreground text-xs border rounded p-0.5 gap-0"
|
||||||
|
/>
|
||||||
|
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
|
||||||
{:else if mergeType === MergeType.CONTENTS && canMergeContents}
|
{:else if mergeType === MergeType.CONTENTS && canMergeContents}
|
||||||
{$_('toolbar.merge.help_merge_contents')}
|
{$_('toolbar.merge.help_merge_contents')}
|
||||||
{:else if mergeType === MergeType.CONTENTS && !canMergeContents}
|
{:else if mergeType === MergeType.CONTENTS && !canMergeContents}
|
||||||
{$_('toolbar.merge.help_cannot_merge_contents')}
|
{$_('toolbar.merge.help_cannot_merge_contents')}
|
||||||
|
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[0]}
|
||||||
|
<Shortcut
|
||||||
|
ctrl={true}
|
||||||
|
click={true}
|
||||||
|
class="inline-flex text-muted-foreground text-xs border rounded p-0.5 gap-0"
|
||||||
|
/>
|
||||||
|
{$_('toolbar.merge.selection_tip').split('{KEYBOARD_SHORTCUT}')[1]}
|
||||||
{/if}
|
{/if}
|
||||||
</Help>
|
</Help>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -6,13 +6,14 @@
|
|||||||
import { ListItem, ListRootItem, ListTrackSegmentItem } from '$lib/components/file-list/FileList';
|
import { ListItem, ListRootItem, ListTrackSegmentItem } from '$lib/components/file-list/FileList';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { Filter } from 'lucide-svelte';
|
import { Filter } from 'lucide-svelte';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
import WithUnits from '$lib/components/WithUnits.svelte';
|
import WithUnits from '$lib/components/WithUnits.svelte';
|
||||||
import { dbUtils, fileObservers } from '$lib/db';
|
import { dbUtils, fileObservers } from '$lib/db';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
|
||||||
import { derived } from 'svelte/store';
|
import { derived } from 'svelte/store';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
let sliderValue = [50];
|
let sliderValue = [50];
|
||||||
let maxPoints = 0;
|
let maxPoints = 0;
|
||||||
@@ -153,18 +154,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<Label class="flex flex-row justify-between">
|
<Label class="flex flex-row justify-between">
|
||||||
<span>{$_('toolbar.reduce.tolerance')}</span>
|
<span>{$_('toolbar.reduce.tolerance')}</span>
|
||||||
<WithUnits value={tolerance / 1000} type="distance" decimals={3} />
|
<WithUnits value={tolerance / 1000} type="distance" decimals={3} class="font-normal" />
|
||||||
</Label>
|
</Label>
|
||||||
<Label class="flex flex-row justify-between">
|
<Label class="flex flex-row justify-between">
|
||||||
<span>{$_('toolbar.reduce.number_of_points')}</span>
|
<span>{$_('toolbar.reduce.number_of_points')}</span>
|
||||||
<span>{currentPoints}/{maxPoints}</span>
|
<span class="font-normal">{currentPoints}/{maxPoints}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Button variant="outline" disabled={!validSelection} on:click={reduce}>
|
<Button variant="outline" disabled={!validSelection} on:click={reduce}>
|
||||||
<Filter size="16" class="mr-1" />
|
<Filter size="16" class="mr-1" />
|
||||||
{$_('toolbar.reduce.button')}
|
{$_('toolbar.reduce.button')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Help link="./help/toolbar/minify">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/minify')}>
|
||||||
{#if validSelection}
|
{#if validSelection}
|
||||||
{$_('toolbar.reduce.help')}
|
{$_('toolbar.reduce.help')}
|
||||||
{:else}
|
{:else}
|
||||||
|
@@ -10,7 +10,8 @@
|
|||||||
import {
|
import {
|
||||||
distancePerHourToSecondsPerDistance,
|
distancePerHourToSecondsPerDistance,
|
||||||
getConvertedVelocity,
|
getConvertedVelocity,
|
||||||
milesToKilometers
|
milesToKilometers,
|
||||||
|
nauticalMilesToKilometers
|
||||||
} from '$lib/units';
|
} from '$lib/units';
|
||||||
import { CalendarDate, type DateValue } from '@internationalized/date';
|
import { CalendarDate, type DateValue } from '@internationalized/date';
|
||||||
import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from 'lucide-svelte';
|
import { CalendarClock, CirclePlay, CircleStop, CircleX, Timer, Zap } from 'lucide-svelte';
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
ListTrackSegmentItem
|
ListTrackSegmentItem
|
||||||
} from '$lib/components/file-list/FileList';
|
} from '$lib/components/file-list/FileList';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
let startDate: DateValue | undefined = undefined;
|
let startDate: DateValue | undefined = undefined;
|
||||||
let startTime: string | undefined = undefined;
|
let startTime: string | undefined = undefined;
|
||||||
@@ -32,11 +34,16 @@
|
|||||||
let endTime: string | undefined = undefined;
|
let endTime: string | undefined = undefined;
|
||||||
let movingTime: number | undefined = undefined;
|
let movingTime: number | undefined = undefined;
|
||||||
let speed: number | undefined = undefined;
|
let speed: number | undefined = undefined;
|
||||||
|
let artificial = false;
|
||||||
|
|
||||||
function toCalendarDate(date: Date): CalendarDate {
|
function toCalendarDate(date: Date): CalendarDate {
|
||||||
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toTimeString(date: Date): string {
|
||||||
|
return date.toTimeString().split(' ')[0];
|
||||||
|
}
|
||||||
|
|
||||||
const { velocityUnits, distanceUnits } = settings;
|
const { velocityUnits, distanceUnits } = settings;
|
||||||
|
|
||||||
function setSpeed(value: number) {
|
function setSpeed(value: number) {
|
||||||
@@ -50,14 +57,14 @@
|
|||||||
function setGPXData() {
|
function setGPXData() {
|
||||||
if ($gpxStatistics.global.time.start) {
|
if ($gpxStatistics.global.time.start) {
|
||||||
startDate = toCalendarDate($gpxStatistics.global.time.start);
|
startDate = toCalendarDate($gpxStatistics.global.time.start);
|
||||||
startTime = $gpxStatistics.global.time.start.toLocaleTimeString();
|
startTime = toTimeString($gpxStatistics.global.time.start);
|
||||||
} else {
|
} else {
|
||||||
startDate = undefined;
|
startDate = undefined;
|
||||||
startTime = undefined;
|
startTime = undefined;
|
||||||
}
|
}
|
||||||
if ($gpxStatistics.global.time.end) {
|
if ($gpxStatistics.global.time.end) {
|
||||||
endDate = toCalendarDate($gpxStatistics.global.time.end);
|
endDate = toCalendarDate($gpxStatistics.global.time.end);
|
||||||
endTime = $gpxStatistics.global.time.end.toLocaleTimeString();
|
endTime = toTimeString($gpxStatistics.global.time.end);
|
||||||
} else {
|
} else {
|
||||||
endDate = undefined;
|
endDate = undefined;
|
||||||
endTime = undefined;
|
endTime = undefined;
|
||||||
@@ -83,6 +90,9 @@
|
|||||||
return new Date();
|
return new Date();
|
||||||
}
|
}
|
||||||
let [hours, minutes, seconds] = time.split(':').map((x) => parseInt(x));
|
let [hours, minutes, seconds] = time.split(':').map((x) => parseInt(x));
|
||||||
|
if (seconds === undefined) {
|
||||||
|
seconds = 0;
|
||||||
|
}
|
||||||
return new Date(date.year, date.month - 1, date.day, hours, minutes, seconds);
|
return new Date(date.year, date.month - 1, date.day, hours, minutes, seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +108,7 @@
|
|||||||
: 1;
|
: 1;
|
||||||
let end = new Date(start.getTime() + ratio * movingTime * 1000);
|
let end = new Date(start.getTime() + ratio * movingTime * 1000);
|
||||||
endDate = toCalendarDate(end);
|
endDate = toCalendarDate(end);
|
||||||
endTime = end.toLocaleTimeString();
|
endTime = toTimeString(end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +124,7 @@
|
|||||||
: 1;
|
: 1;
|
||||||
let start = new Date(end.getTime() - ratio * movingTime * 1000);
|
let start = new Date(end.getTime() - ratio * movingTime * 1000);
|
||||||
startDate = toCalendarDate(start);
|
startDate = toCalendarDate(start);
|
||||||
startTime = start.toLocaleTimeString();
|
startTime = toTimeString(start);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +139,8 @@
|
|||||||
}
|
}
|
||||||
if ($distanceUnits === 'imperial') {
|
if ($distanceUnits === 'imperial') {
|
||||||
speedValue = milesToKilometers(speedValue);
|
speedValue = milesToKilometers(speedValue);
|
||||||
|
} else if ($distanceUnits === 'nautical') {
|
||||||
|
speedValue = nauticalMilesToKilometers(speedValue);
|
||||||
}
|
}
|
||||||
return speedValue;
|
return speedValue;
|
||||||
}
|
}
|
||||||
@@ -190,8 +202,10 @@
|
|||||||
<span class="text-sm shrink-0">
|
<span class="text-sm shrink-0">
|
||||||
{#if $distanceUnits === 'imperial'}
|
{#if $distanceUnits === 'imperial'}
|
||||||
{$_('units.miles_per_hour')}
|
{$_('units.miles_per_hour')}
|
||||||
{:else}
|
{:else if $distanceUnits === 'metric'}
|
||||||
{$_('units.kilometers_per_hour')}
|
{$_('units.kilometers_per_hour')}
|
||||||
|
{:else if $distanceUnits === 'nautical'}
|
||||||
|
{$_('units.knots')}
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -204,8 +218,10 @@
|
|||||||
<span class="text-sm shrink-0">
|
<span class="text-sm shrink-0">
|
||||||
{#if $distanceUnits === 'imperial'}
|
{#if $distanceUnits === 'imperial'}
|
||||||
{$_('units.minutes_per_mile')}
|
{$_('units.minutes_per_mile')}
|
||||||
{:else}
|
{:else if $distanceUnits === 'metric'}
|
||||||
{$_('units.minutes_per_kilometer')}
|
{$_('units.minutes_per_kilometer')}
|
||||||
|
{:else if $distanceUnits === 'nautical'}
|
||||||
|
{$_('units.minutes_per_nautical_mile')}
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -274,19 +290,19 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if $gpxStatistics.global.time.moving === 0 || $gpxStatistics.global.time.moving === undefined}
|
{#if $gpxStatistics.global.time.moving === 0 || $gpxStatistics.global.time.moving === undefined}
|
||||||
<div class="mt-0.5 flex flex-row gap-1 items-center hidden">
|
<div class="mt-0.5 flex flex-row gap-1 items-center">
|
||||||
<Checkbox id="artificial-time" disabled={!canUpdate} />
|
<Checkbox id="artificial-time" bind:checked={artificial} disabled={!canUpdate} />
|
||||||
<Label for="artificial-time">
|
<Label for="artificial-time">
|
||||||
{$_('toolbar.time.artificial')}
|
{$_('toolbar.time.artificial')}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2 items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!canUpdate}
|
disabled={!canUpdate}
|
||||||
class="grow"
|
class="grow whitespace-normal h-fit"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
let effectiveSpeed = getSpeed();
|
let effectiveSpeed = getSpeed();
|
||||||
if (startDate === undefined || startTime === undefined || effectiveSpeed === undefined) {
|
if (startDate === undefined || startTime === undefined || effectiveSpeed === undefined) {
|
||||||
@@ -309,34 +325,55 @@
|
|||||||
let fileId = item.getFileId();
|
let fileId = item.getFileId();
|
||||||
dbUtils.applyToFile(fileId, (file) => {
|
dbUtils.applyToFile(fileId, (file) => {
|
||||||
if (item instanceof ListFileItem) {
|
if (item instanceof ListFileItem) {
|
||||||
file.changeTimestamps(getDate(startDate, startTime), effectiveSpeed, ratio);
|
if (artificial) {
|
||||||
|
file.createArtificialTimestamps(getDate(startDate, startTime), movingTime);
|
||||||
|
} else {
|
||||||
|
file.changeTimestamps(getDate(startDate, startTime), effectiveSpeed, ratio);
|
||||||
|
}
|
||||||
} else if (item instanceof ListTrackItem) {
|
} else if (item instanceof ListTrackItem) {
|
||||||
file.changeTimestamps(
|
if (artificial) {
|
||||||
getDate(startDate, startTime),
|
file.createArtificialTimestamps(
|
||||||
effectiveSpeed,
|
getDate(startDate, startTime),
|
||||||
ratio,
|
movingTime,
|
||||||
item.getTrackIndex()
|
item.getTrackIndex()
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
file.changeTimestamps(
|
||||||
|
getDate(startDate, startTime),
|
||||||
|
effectiveSpeed,
|
||||||
|
ratio,
|
||||||
|
item.getTrackIndex()
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (item instanceof ListTrackSegmentItem) {
|
} else if (item instanceof ListTrackSegmentItem) {
|
||||||
file.changeTimestamps(
|
if (artificial) {
|
||||||
getDate(startDate, startTime),
|
file.createArtificialTimestamps(
|
||||||
effectiveSpeed,
|
getDate(startDate, startTime),
|
||||||
ratio,
|
movingTime,
|
||||||
item.getTrackIndex(),
|
item.getTrackIndex(),
|
||||||
item.getSegmentIndex()
|
item.getSegmentIndex()
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
file.changeTimestamps(
|
||||||
|
getDate(startDate, startTime),
|
||||||
|
effectiveSpeed,
|
||||||
|
ratio,
|
||||||
|
item.getTrackIndex(),
|
||||||
|
item.getSegmentIndex()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CalendarClock size="16" class="mr-1" />
|
<CalendarClock size="16" class="mr-1 shrink-0" />
|
||||||
{$_('toolbar.time.update')}
|
{$_('toolbar.time.update')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" on:click={setGPXData}>
|
<Button variant="outline" on:click={setGPXData}>
|
||||||
<CircleX size="16" />
|
<CircleX size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Help link="./help/toolbar/time">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/time')}>
|
||||||
{#if canUpdate}
|
{#if canUpdate}
|
||||||
{$_('toolbar.time.help')}
|
{$_('toolbar.time.help')}
|
||||||
{:else}
|
{:else}
|
||||||
|
@@ -19,8 +19,8 @@
|
|||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { map } from '$lib/stores';
|
import { map } from '$lib/stores';
|
||||||
import { resetCursor, setCrosshairCursor } from '$lib/utils';
|
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||||
import { CirclePlus, CircleX, Save } from 'lucide-svelte';
|
import { MapPin, CircleX, Save } from 'lucide-svelte';
|
||||||
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
import { getSymbolKey, symbols } from '$lib/assets/symbols';
|
||||||
|
|
||||||
let name: string;
|
let name: string;
|
||||||
@@ -181,12 +181,21 @@
|
|||||||
<div class="flex flex-col gap-3 w-full max-w-96 {$$props.class ?? ''}">
|
<div class="flex flex-col gap-3 w-full max-w-96 {$$props.class ?? ''}">
|
||||||
<fieldset class="flex flex-col gap-2">
|
<fieldset class="flex flex-col gap-2">
|
||||||
<Label for="name">{$_('menu.metadata.name')}</Label>
|
<Label for="name">{$_('menu.metadata.name')}</Label>
|
||||||
<Input bind:value={name} id="name" class="font-semibold h-8" />
|
<Input
|
||||||
|
bind:value={name}
|
||||||
|
id="name"
|
||||||
|
class="font-semibold h-8"
|
||||||
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
|
/>
|
||||||
<Label for="description">{$_('menu.metadata.description')}</Label>
|
<Label for="description">{$_('menu.metadata.description')}</Label>
|
||||||
<Textarea bind:value={description} id="description" />
|
<Textarea
|
||||||
|
bind:value={description}
|
||||||
|
id="description"
|
||||||
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
|
/>
|
||||||
<Label for="symbol">{$_('toolbar.waypoint.icon')}</Label>
|
<Label for="symbol">{$_('toolbar.waypoint.icon')}</Label>
|
||||||
<Select.Root bind:selected={selectedSymbol}>
|
<Select.Root bind:selected={selectedSymbol}>
|
||||||
<Select.Trigger id="symbol" class="w-full h-8">
|
<Select.Trigger id="symbol" class="w-full h-8" disabled={!canCreate && !$selectedWaypoint}>
|
||||||
<Select.Value />
|
<Select.Value />
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content class="max-h-60 overflow-y-scroll">
|
<Select.Content class="max-h-60 overflow-y-scroll">
|
||||||
@@ -209,9 +218,9 @@
|
|||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
<Label for="link">{$_('toolbar.waypoint.link')}</Label>
|
<Label for="link">{$_('toolbar.waypoint.link')}</Label>
|
||||||
<Input bind:value={link} id="link" class="h-8" />
|
<Input bind:value={link} id="link" class="h-8" disabled={!canCreate && !$selectedWaypoint} />
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<div>
|
<div class="grow">
|
||||||
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>
|
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>
|
||||||
<Input
|
<Input
|
||||||
bind:value={latitude}
|
bind:value={latitude}
|
||||||
@@ -221,9 +230,10 @@
|
|||||||
min={-90}
|
min={-90}
|
||||||
max={90}
|
max={90}
|
||||||
class="text-xs h-8"
|
class="text-xs h-8"
|
||||||
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="grow">
|
||||||
<Label for="longitude">{$_('toolbar.waypoint.longitude')}</Label>
|
<Label for="longitude">{$_('toolbar.waypoint.longitude')}</Label>
|
||||||
<Input
|
<Input
|
||||||
bind:value={longitude}
|
bind:value={longitude}
|
||||||
@@ -233,28 +243,28 @@
|
|||||||
min={-180}
|
min={-180}
|
||||||
max={180}
|
max={180}
|
||||||
class="text-xs h-8"
|
class="text-xs h-8"
|
||||||
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="flex flex-row flex-wrap gap-2">
|
<div class="flex flex-row gap-2 items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!canCreate && !$selectedWaypoint}
|
disabled={!canCreate && !$selectedWaypoint}
|
||||||
class="grow"
|
class="grow whitespace-normal h-fit"
|
||||||
on:click={createOrUpdateWaypoint}
|
on:click={createOrUpdateWaypoint}
|
||||||
>
|
>
|
||||||
{#if $selectedWaypoint}
|
{#if $selectedWaypoint}
|
||||||
<Save size="16" class="mr-1" />
|
<Save size="16" class="mr-1 shrink-0" />
|
||||||
{$_('menu.metadata.save')}
|
{$_('menu.metadata.save')}
|
||||||
{:else}
|
{:else}
|
||||||
<CirclePlus size="16" class="mr-1" />
|
<MapPin size="16" class="mr-1 shrink-0" />
|
||||||
{$_('toolbar.waypoint.create')}
|
{$_('toolbar.waypoint.create')}
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="ml-auto"
|
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
selectedWaypoint.set(undefined);
|
selectedWaypoint.set(undefined);
|
||||||
resetWaypointData();
|
resetWaypointData();
|
||||||
@@ -263,7 +273,7 @@
|
|||||||
<CircleX size="16" />
|
<CircleX size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Help link="./help/toolbar/poi">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/poi')}>
|
||||||
{#if $selectedWaypoint || canCreate}
|
{#if $selectedWaypoint || canCreate}
|
||||||
{$_('toolbar.waypoint.help')}
|
{$_('toolbar.waypoint.help')}
|
||||||
{:else}
|
{:else}
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import Help from '$lib/components/Help.svelte';
|
import Help from '$lib/components/Help.svelte';
|
||||||
|
import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte';
|
||||||
import Tooltip from '$lib/components/Tooltip.svelte';
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
import Shortcut from '$lib/components/Shortcut.svelte';
|
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||||
import {
|
import {
|
||||||
@@ -25,7 +26,7 @@
|
|||||||
import { dbUtils, getFile, getFileIds, settings } from '$lib/db';
|
import { dbUtils, getFile, getFileIds, settings } from '$lib/db';
|
||||||
import { brouterProfiles, routingProfileSelectItem } from './Routing';
|
import { brouterProfiles, routingProfileSelectItem } from './Routing';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
import { RoutingControls } from './RoutingControls';
|
import { RoutingControls } from './RoutingControls';
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { fileObservers } from '$lib/db';
|
import { fileObservers } from '$lib/db';
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
ListTrackSegmentItem,
|
ListTrackSegmentItem,
|
||||||
type ListItem
|
type ListItem
|
||||||
} from '$lib/components/file-list/FileList';
|
} from '$lib/components/file-list/FileList';
|
||||||
import { flyAndScale, resetCursor, setCrosshairCursor } from '$lib/utils';
|
import { flyAndScale, getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { TrackPoint } from 'gpx';
|
import { TrackPoint } from 'gpx';
|
||||||
|
|
||||||
@@ -105,7 +106,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if minimized}
|
{#if minimizable && minimized}
|
||||||
<div class="-m-1.5 -mb-2">
|
<div class="-m-1.5 -mb-2">
|
||||||
<Button variant="ghost" class="px-1 h-[26px]" on:click={() => (minimized = false)}>
|
<Button variant="ghost" class="px-1 h-[26px]" on:click={() => (minimized = false)}>
|
||||||
<SquareArrowOutDownRight size="18" />
|
<SquareArrowOutDownRight size="18" />
|
||||||
@@ -116,27 +117,24 @@
|
|||||||
class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}"
|
class="flex flex-col gap-3 w-full max-w-80 {$$props.class ?? ''}"
|
||||||
in:flyAndScale={{ x: -2, y: 0, duration: 50 }}
|
in:flyAndScale={{ x: -2, y: 0, duration: 50 }}
|
||||||
>
|
>
|
||||||
<div class="grow flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<Tooltip>
|
<Label class="flex flex-row justify-between items-center gap-2">
|
||||||
<Label slot="data" class="w-full flex flex-row justify-between items-center gap-2">
|
<span class="flex flex-row items-center gap-1">
|
||||||
<span class="flex flex-row gap-1">
|
{#if $routing}
|
||||||
{#if $routing}
|
<Route size="16" />
|
||||||
<Route size="16" />
|
{:else}
|
||||||
{:else}
|
<RouteOff size="16" />
|
||||||
<RouteOff size="16" />
|
{/if}
|
||||||
{/if}
|
{$_('toolbar.routing.use_routing')}
|
||||||
{$_('toolbar.routing.use_routing')}
|
|
||||||
</span>
|
|
||||||
<Switch class="scale-90" bind:checked={$routing} />
|
|
||||||
</Label>
|
|
||||||
<span slot="tooltip" class="flex flex-row items-center">
|
|
||||||
{$_('toolbar.routing.use_routing_tooltip')}
|
|
||||||
<Shortcut key="F5" />
|
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
<Tooltip label={$_('toolbar.routing.use_routing_tooltip')}>
|
||||||
|
<Switch class="scale-90" bind:checked={$routing} />
|
||||||
|
<Shortcut slot="extra" key="F5" />
|
||||||
|
</Tooltip>
|
||||||
|
</Label>
|
||||||
{#if $routing}
|
{#if $routing}
|
||||||
<div class="flex flex-col gap-3" in:slide>
|
<div class="flex flex-col gap-3" in:slide>
|
||||||
<Label class="w-full flex flex-row justify-between items-center gap-2">
|
<Label class="flex flex-row justify-between items-center gap-2">
|
||||||
<span class="shrink-0 flex flex-row items-center gap-1">
|
<span class="shrink-0 flex flex-row items-center gap-1">
|
||||||
{#if $routingProfileSelectItem.value.includes('bike') || $routingProfileSelectItem.value.includes('motorcycle')}
|
{#if $routingProfileSelectItem.value.includes('bike') || $routingProfileSelectItem.value.includes('motorcycle')}
|
||||||
<Bike size="16" />
|
<Bike size="16" />
|
||||||
@@ -162,81 +160,73 @@
|
|||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
</Label>
|
</Label>
|
||||||
<Label class="w-full flex flex-row justify-between items-center gap-2">
|
<Label class="flex flex-row justify-between items-center gap-2">
|
||||||
<span class="flex flex-row gap-1"
|
<span class="flex flex-row gap-1">
|
||||||
><TriangleAlert size="16" />{$_('toolbar.routing.allow_private')}</span
|
<TriangleAlert size="16" />
|
||||||
>
|
{$_('toolbar.routing.allow_private')}
|
||||||
|
</span>
|
||||||
<Switch class="scale-90" bind:checked={$privateRoads} />
|
<Switch class="scale-90" bind:checked={$privateRoads} />
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row flex-wrap justify-center gap-1">
|
<div class="flex flex-row flex-wrap justify-center gap-1">
|
||||||
<Tooltip>
|
<ButtonWithTooltip
|
||||||
<Button
|
label={$_('toolbar.routing.reverse.tooltip')}
|
||||||
slot="data"
|
variant="outline"
|
||||||
variant="outline"
|
class="flex flex-row gap-1 text-xs px-2"
|
||||||
class="flex flex-row gap-1 text-xs px-2"
|
disabled={!validSelection}
|
||||||
disabled={!validSelection}
|
on:click={dbUtils.reverseSelection}
|
||||||
on:click={dbUtils.reverseSelection}
|
>
|
||||||
>
|
<ArrowRightLeft size="12" />{$_('toolbar.routing.reverse.button')}
|
||||||
<ArrowRightLeft size="12" />{$_('toolbar.routing.reverse.button')}
|
</ButtonWithTooltip>
|
||||||
</Button>
|
<ButtonWithTooltip
|
||||||
<span slot="tooltip">{$_('toolbar.routing.reverse.tooltip')}</span>
|
label={$_('toolbar.routing.route_back_to_start.tooltip')}
|
||||||
</Tooltip>
|
variant="outline"
|
||||||
<Tooltip>
|
class="flex flex-row gap-1 text-xs px-2"
|
||||||
<Button
|
disabled={!validSelection}
|
||||||
slot="data"
|
on:click={() => {
|
||||||
variant="outline"
|
const selected = getOrderedSelection();
|
||||||
class="flex flex-row gap-1 text-xs px-2"
|
if (selected.length > 0) {
|
||||||
disabled={!validSelection}
|
const firstFileId = selected[0].getFileId();
|
||||||
on:click={() => {
|
const firstFile = getFile(firstFileId);
|
||||||
const selected = getOrderedSelection();
|
if (firstFile) {
|
||||||
if (selected.length > 0) {
|
let start = (() => {
|
||||||
const firstFileId = selected[0].getFileId();
|
if (selected[0] instanceof ListFileItem) {
|
||||||
const firstFile = getFile(firstFileId);
|
return firstFile.trk[0]?.trkseg[0]?.trkpt[0];
|
||||||
if (firstFile) {
|
} else if (selected[0] instanceof ListTrackItem) {
|
||||||
let start = (() => {
|
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[0]?.trkpt[0];
|
||||||
if (selected[0] instanceof ListFileItem) {
|
} else if (selected[0] instanceof ListTrackSegmentItem) {
|
||||||
return firstFile.trk[0]?.trkseg[0]?.trkpt[0];
|
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[
|
||||||
} else if (selected[0] instanceof ListTrackItem) {
|
selected[0].getSegmentIndex()
|
||||||
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[0]?.trkpt[0];
|
]?.trkpt[0];
|
||||||
} else if (selected[0] instanceof ListTrackSegmentItem) {
|
|
||||||
return firstFile.trk[selected[0].getTrackIndex()]?.trkseg[
|
|
||||||
selected[0].getSegmentIndex()
|
|
||||||
]?.trkpt[0];
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (start !== undefined) {
|
|
||||||
const lastFileId = selected[selected.length - 1].getFileId();
|
|
||||||
routingControls
|
|
||||||
.get(lastFileId)
|
|
||||||
?.appendAnchorWithCoordinates(start.getCoordinates());
|
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (start !== undefined) {
|
||||||
|
const lastFileId = selected[selected.length - 1].getFileId();
|
||||||
|
routingControls
|
||||||
|
.get(lastFileId)
|
||||||
|
?.appendAnchorWithCoordinates(start.getCoordinates());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
<Home size="12" />{$_('toolbar.routing.route_back_to_start.button')}
|
>
|
||||||
</Button>
|
<Home size="12" />{$_('toolbar.routing.route_back_to_start.button')}
|
||||||
<span slot="tooltip">{$_('toolbar.routing.route_back_to_start.tooltip')}</span>
|
</ButtonWithTooltip>
|
||||||
</Tooltip>
|
<ButtonWithTooltip
|
||||||
<Tooltip>
|
label={$_('toolbar.routing.round_trip.tooltip')}
|
||||||
<Button
|
variant="outline"
|
||||||
slot="data"
|
class="flex flex-row gap-1 text-xs px-2"
|
||||||
variant="outline"
|
disabled={!validSelection}
|
||||||
class="flex flex-row gap-1 text-xs px-2"
|
on:click={dbUtils.createRoundTripForSelection}
|
||||||
disabled={!validSelection}
|
>
|
||||||
on:click={dbUtils.createRoundTripForSelection}
|
<Repeat size="12" />{$_('toolbar.routing.round_trip.button')}
|
||||||
>
|
</ButtonWithTooltip>
|
||||||
<Repeat size="12" />{$_('toolbar.routing.round_trip.button')}
|
|
||||||
</Button>
|
|
||||||
<span slot="tooltip">{$_('toolbar.routing.round_trip.tooltip')}</span>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex flex-row gap-2 items-end justify-between">
|
<div class="w-full flex flex-row gap-2 items-end justify-between">
|
||||||
<Help link="./help/toolbar/routing">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/routing')}>
|
||||||
{#if !validSelection}
|
{#if !validSelection}
|
||||||
{$_('toolbar.routing.help_no_file')}
|
{$_('toolbar.routing.help_no_file')}
|
||||||
{:else}
|
{:else}
|
||||||
|
@@ -24,7 +24,7 @@ export const routingProfileSelectItem = writable({
|
|||||||
});
|
});
|
||||||
|
|
||||||
derived([routingProfile, locale, isLoading], ([profile, l, i]) => [profile, l, i]).subscribe(([profile, l, i]) => {
|
derived([routingProfile, locale, isLoading], ([profile, l, i]) => [profile, l, i]).subscribe(([profile, l, i]) => {
|
||||||
if (!i && profile !== '' && profile !== get(routingProfileSelectItem).value && l !== null) {
|
if (!i && profile !== '' && (profile !== get(routingProfileSelectItem).value || get(_)(`toolbar.routing.activities.${profile}`) !== get(routingProfileSelectItem).label) && l !== null) {
|
||||||
routingProfileSelectItem.update((item) => {
|
routingProfileSelectItem.update((item) => {
|
||||||
item.value = profile;
|
item.value = profile;
|
||||||
item.label = get(_)(`toolbar.routing.activities.${profile}`);
|
item.label = get(_)(`toolbar.routing.activities.${profile}`);
|
||||||
@@ -66,7 +66,7 @@ async function getRoute(points: Coordinates[], brouterProfile: string, privateRo
|
|||||||
const latIdx = messages[0].indexOf("Latitude");
|
const latIdx = messages[0].indexOf("Latitude");
|
||||||
const tagIdx = messages[0].indexOf("WayTags");
|
const tagIdx = messages[0].indexOf("WayTags");
|
||||||
let messageIdx = 1;
|
let messageIdx = 1;
|
||||||
let surface = messageIdx < messages.length ? getSurface(messages[messageIdx][tagIdx]) : "unknown";
|
let surface = messageIdx < messages.length ? getSurface(messages[messageIdx][tagIdx]) : undefined;
|
||||||
|
|
||||||
for (let i = 0; i < coordinates.length; i++) {
|
for (let i = 0; i < coordinates.length; i++) {
|
||||||
let coord = coordinates[i];
|
let coord = coordinates[i];
|
||||||
@@ -77,27 +77,30 @@ async function getRoute(points: Coordinates[], brouterProfile: string, privateRo
|
|||||||
},
|
},
|
||||||
ele: coord[2] ?? (i > 0 ? route[i - 1].ele : 0)
|
ele: coord[2] ?? (i > 0 ? route[i - 1].ele : 0)
|
||||||
}));
|
}));
|
||||||
route[route.length - 1].setSurface(surface)
|
|
||||||
|
|
||||||
if (messageIdx < messages.length &&
|
if (messageIdx < messages.length &&
|
||||||
coordinates[i][0] == Number(messages[messageIdx][lngIdx]) / 1000000 &&
|
coordinates[i][0] == Number(messages[messageIdx][lngIdx]) / 1000000 &&
|
||||||
coordinates[i][1] == Number(messages[messageIdx][latIdx]) / 1000000) {
|
coordinates[i][1] == Number(messages[messageIdx][latIdx]) / 1000000) {
|
||||||
messageIdx++;
|
messageIdx++;
|
||||||
|
|
||||||
if (messageIdx == messages.length) surface = "unknown";
|
if (messageIdx == messages.length) surface = undefined;
|
||||||
else surface = getSurface(messages[messageIdx][tagIdx]);
|
else surface = getSurface(messages[messageIdx][tagIdx]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (surface) {
|
||||||
|
route[route.length - 1].setSurface(surface);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSurface(message: string): string {
|
function getSurface(message: string): string | undefined {
|
||||||
const fields = message.split(" ");
|
const fields = message.split(" ");
|
||||||
for (let i = 0; i < fields.length; i++) if (fields[i].startsWith("surface=")) {
|
for (let i = 0; i < fields.length; i++) if (fields[i].startsWith("surface=")) {
|
||||||
return fields[i].substring(8);
|
return fields[i].substring(8);
|
||||||
}
|
}
|
||||||
return "unknown";
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getIntermediatePoints(points: Coordinates[]): Promise<TrackPoint[]> {
|
function getIntermediatePoints(points: Coordinates[]): Promise<TrackPoint[]> {
|
||||||
@@ -125,13 +128,10 @@ function getIntermediatePoints(points: Coordinates[]): Promise<TrackPoint[]> {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let m = get(map);
|
return getElevation(route).then((elevations) => {
|
||||||
route.forEach((point) => {
|
route.forEach((point, i) => {
|
||||||
point.setSurface("unknown");
|
point.ele = elevations[i];
|
||||||
if (m) {
|
});
|
||||||
point.ele = getElevation(m, point.getCoordinates());
|
return route;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve) => resolve(route));
|
|
||||||
}
|
}
|
@@ -30,7 +30,7 @@
|
|||||||
>
|
>
|
||||||
<Trash2 size="16" class="mr-1" />
|
<Trash2 size="16" class="mr-1" />
|
||||||
{$_('menu.delete')}
|
{$_('menu.delete')}
|
||||||
<Shortcut key="" shift={true} click={true} />
|
<Shortcut shift={true} click={true} />
|
||||||
</Button>
|
</Button>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, crossarcDistance } from "gpx";
|
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from "gpx";
|
||||||
import { get, writable, type Readable } from "svelte/store";
|
import { get, writable, type Readable } from "svelte/store";
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from "mapbox-gl";
|
||||||
import { route } from "./Routing";
|
import { route } from "./Routing";
|
||||||
@@ -81,7 +81,6 @@ export class RoutingControls {
|
|||||||
add() {
|
add() {
|
||||||
this.active = true;
|
this.active = true;
|
||||||
|
|
||||||
this.map.on('zoom', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
|
||||||
this.map.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
this.map.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
||||||
this.map.on('click', this.appendAnchorBinded);
|
this.map.on('click', this.appendAnchorBinded);
|
||||||
this.map.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
this.map.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
||||||
@@ -129,7 +128,6 @@ export class RoutingControls {
|
|||||||
for (let anchor of this.anchors) {
|
for (let anchor of this.anchors) {
|
||||||
anchor.marker.remove();
|
anchor.marker.remove();
|
||||||
}
|
}
|
||||||
this.map.off('zoom', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
|
||||||
this.map.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
this.map.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
|
||||||
this.map.off('click', this.appendAnchorBinded);
|
this.map.off('click', this.appendAnchorBinded);
|
||||||
this.map.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
this.map.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
|
||||||
@@ -187,11 +185,13 @@ export class RoutingControls {
|
|||||||
return (e: any) => {
|
return (e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (marker === this.temporaryAnchor.marker) {
|
|
||||||
|
if (Date.now() - this.lastDragEvent < 100) { // Prevent click event during drag
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Date.now() - this.lastDragEvent < 100) { // Prevent click event during drag
|
if (marker === this.temporaryAnchor.marker) {
|
||||||
|
this.turnIntoPermanentAnchor();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,14 +228,15 @@ export class RoutingControls {
|
|||||||
toggleAnchorsForZoomLevelAndBounds() { // Show markers only if they are in the current zoom level and bounds
|
toggleAnchorsForZoomLevelAndBounds() { // Show markers only if they are in the current zoom level and bounds
|
||||||
this.shownAnchors.splice(0, this.shownAnchors.length);
|
this.shownAnchors.splice(0, this.shownAnchors.length);
|
||||||
|
|
||||||
let southWest = this.map.unproject([0, this.map.getCanvas().height]);
|
let center = this.map.getCenter();
|
||||||
let northEast = this.map.unproject([this.map.getCanvas().width, 0]);
|
let bottomLeft = this.map.unproject([0, this.map.getCanvas().height]);
|
||||||
let bounds = new mapboxgl.LngLatBounds(southWest, northEast);
|
let topRight = this.map.unproject([this.map.getCanvas().width, 0]);
|
||||||
|
let diagonal = bottomLeft.distanceTo(topRight);
|
||||||
|
|
||||||
let zoom = this.map.getZoom();
|
let zoom = this.map.getZoom();
|
||||||
this.anchors.forEach((anchor) => {
|
this.anchors.forEach((anchor) => {
|
||||||
anchor.inZoom = anchor.point._data.zoom <= zoom;
|
anchor.inZoom = anchor.point._data.zoom <= zoom;
|
||||||
if (anchor.inZoom && bounds.contains(anchor.marker.getLngLat())) {
|
if (anchor.inZoom && center.distanceTo(anchor.marker.getLngLat()) < diagonal) {
|
||||||
anchor.marker.addTo(this.map);
|
anchor.marker.addTo(this.map);
|
||||||
this.shownAnchors.push(anchor);
|
this.shownAnchors.push(anchor);
|
||||||
} else {
|
} else {
|
||||||
@@ -335,14 +336,14 @@ export class RoutingControls {
|
|||||||
let file = get(this.file)?.file;
|
let file = get(this.file)?.file;
|
||||||
|
|
||||||
// Find the point closest to the temporary anchor
|
// Find the point closest to the temporary anchor
|
||||||
let minDistance = Number.MAX_VALUE;
|
let minDetails: any = { distance: Number.MAX_VALUE };
|
||||||
let minAnchor = this.temporaryAnchor as Anchor;
|
let minAnchor = this.temporaryAnchor as Anchor;
|
||||||
file?.forEachSegment((segment, trackIndex, segmentIndex) => {
|
file?.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
|
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
|
||||||
let details: any = {};
|
let details: any = {};
|
||||||
let closest = getClosestLinePoint(segment.trkpt, this.temporaryAnchor.point, details);
|
let closest = getClosestLinePoint(segment.trkpt, this.temporaryAnchor.point, details);
|
||||||
if (details.distance < minDistance) {
|
if (details.distance < minDetails.distance) {
|
||||||
minDistance = details.distance;
|
minDetails = details;
|
||||||
minAnchor = {
|
minAnchor = {
|
||||||
point: closest,
|
point: closest,
|
||||||
segment,
|
segment,
|
||||||
@@ -353,9 +354,65 @@ export class RoutingControls {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (minAnchor.point._data.anchor) {
|
||||||
|
minAnchor.point = minAnchor.point.clone();
|
||||||
|
if (minDetails.before) {
|
||||||
|
minAnchor.point._data.index = minAnchor.point._data.index + 0.5;
|
||||||
|
} else {
|
||||||
|
minAnchor.point._data.index = minAnchor.point._data.index - 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return minAnchor;
|
return minAnchor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
turnIntoPermanentAnchor() {
|
||||||
|
let file = get(this.file)?.file;
|
||||||
|
|
||||||
|
// Find the point closest to the temporary anchor
|
||||||
|
let minDetails: any = { distance: Number.MAX_VALUE };
|
||||||
|
let minInfo = {
|
||||||
|
point: this.temporaryAnchor.point,
|
||||||
|
trackIndex: -1,
|
||||||
|
segmentIndex: -1,
|
||||||
|
trkptIndex: -1
|
||||||
|
};
|
||||||
|
|
||||||
|
file?.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
|
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
|
||||||
|
let details: any = {};
|
||||||
|
getClosestLinePoint(segment.trkpt, this.temporaryAnchor.point, details);
|
||||||
|
if (details.distance < minDetails.distance) {
|
||||||
|
minDetails = details;
|
||||||
|
let before = details.before ? details.index : details.index - 1;
|
||||||
|
|
||||||
|
let projectedPt = projectedPoint(segment.trkpt[before], segment.trkpt[before + 1], this.temporaryAnchor.point);
|
||||||
|
let ratio = distance(segment.trkpt[before], projectedPt) / distance(segment.trkpt[before], segment.trkpt[before + 1]);
|
||||||
|
|
||||||
|
let point = segment.trkpt[before].clone();
|
||||||
|
point.setCoordinates(projectedPt);
|
||||||
|
point.ele = (1 - ratio) * (segment.trkpt[before].ele ?? 0) + ratio * (segment.trkpt[before + 1].ele ?? 0);
|
||||||
|
point.time = (segment.trkpt[before].time && segment.trkpt[before + 1].time) ? new Date((1 - ratio) * segment.trkpt[before].time.getTime() + ratio * segment.trkpt[before + 1].time.getTime()) : undefined;
|
||||||
|
point._data = {
|
||||||
|
anchor: true,
|
||||||
|
zoom: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
minInfo = {
|
||||||
|
point,
|
||||||
|
trackIndex,
|
||||||
|
segmentIndex,
|
||||||
|
trkptIndex: before + 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (minInfo.trackIndex !== -1) {
|
||||||
|
dbUtils.applyToFile(this.fileId, (file) => file.replaceTrackPoints(minInfo.trackIndex, minInfo.segmentIndex, minInfo.trkptIndex, minInfo.trkptIndex - 1, [minInfo.point]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getDeleteAnchor(anchor: Anchor) {
|
getDeleteAnchor(anchor: Anchor) {
|
||||||
return () => this.deleteAnchor(anchor);
|
return () => this.deleteAnchor(anchor);
|
||||||
}
|
}
|
||||||
|
@@ -17,11 +17,12 @@
|
|||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { gpxStatistics, map, slicedGPXStatistics, splitAs } from '$lib/stores';
|
import { gpxStatistics, map, slicedGPXStatistics, splitAs } from '$lib/stores';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
import { onDestroy, tick } from 'svelte';
|
import { onDestroy, tick } from 'svelte';
|
||||||
import { Crop } from 'lucide-svelte';
|
import { Crop } from 'lucide-svelte';
|
||||||
import { dbUtils } from '$lib/db';
|
import { dbUtils } from '$lib/db';
|
||||||
import { SplitControls } from './SplitControls';
|
import { SplitControls } from './SplitControls';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
let splitControls: SplitControls | undefined = undefined;
|
let splitControls: SplitControls | undefined = undefined;
|
||||||
let canCrop = false;
|
let canCrop = false;
|
||||||
@@ -37,8 +38,8 @@
|
|||||||
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
|
||||||
$gpxStatistics.local.points.length > 0;
|
$gpxStatistics.local.points.length > 0;
|
||||||
|
|
||||||
let maxSliderValue = 100;
|
let maxSliderValue = 1;
|
||||||
let sliderValues = [0, 100];
|
let sliderValues = [0, 1];
|
||||||
|
|
||||||
function updateCanCrop() {
|
function updateCanCrop() {
|
||||||
canCrop = sliderValues[0] != 0 || sliderValues[1] != maxSliderValue;
|
canCrop = sliderValues[0] != 0 || sliderValues[1] != maxSliderValue;
|
||||||
@@ -66,7 +67,7 @@
|
|||||||
if (validSelection && $gpxStatistics.local.points.length > 0) {
|
if (validSelection && $gpxStatistics.local.points.length > 0) {
|
||||||
maxSliderValue = $gpxStatistics.local.points.length - 1;
|
maxSliderValue = $gpxStatistics.local.points.length - 1;
|
||||||
} else {
|
} else {
|
||||||
maxSliderValue = 100;
|
maxSliderValue = 1;
|
||||||
}
|
}
|
||||||
await tick();
|
await tick();
|
||||||
sliderValues = [0, maxSliderValue];
|
sliderValues = [0, maxSliderValue];
|
||||||
@@ -135,7 +136,7 @@
|
|||||||
</Select.Content>
|
</Select.Content>
|
||||||
</Select.Root>
|
</Select.Root>
|
||||||
</Label>
|
</Label>
|
||||||
<Help link="./help/toolbar/scissors">
|
<Help link={getURLForLanguage($locale, '/help/toolbar/scissors')}>
|
||||||
{#if validSelection}
|
{#if validSelection}
|
||||||
{$_('toolbar.scissors.help')}
|
{$_('toolbar.scissors.help')}
|
||||||
{:else}
|
{:else}
|
||||||
|
@@ -49,9 +49,7 @@ export class SplitControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateControls() { // Update the markers when the files change
|
updateControls() { // Update the markers when the files change
|
||||||
|
|
||||||
let controlIndex = 0;
|
let controlIndex = 0;
|
||||||
|
|
||||||
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
let file = getFile(fileId);
|
let file = getFile(fileId);
|
||||||
|
|
||||||
@@ -61,6 +59,7 @@ export class SplitControls {
|
|||||||
for (let point of segment.trkpt.slice(1, -1)) { // Update the existing controls (could be improved by matching the existing controls with the new ones?)
|
for (let point of segment.trkpt.slice(1, -1)) { // Update the existing controls (could be improved by matching the existing controls with the new ones?)
|
||||||
if (point._data.anchor) {
|
if (point._data.anchor) {
|
||||||
if (controlIndex < this.controls.length) {
|
if (controlIndex < this.controls.length) {
|
||||||
|
this.controls[controlIndex].fileId = fileId;
|
||||||
this.controls[controlIndex].point = point;
|
this.controls[controlIndex].point = point;
|
||||||
this.controls[controlIndex].segment = segment;
|
this.controls[controlIndex].segment = segment;
|
||||||
this.controls[controlIndex].trackIndex = trackIndex;
|
this.controls[controlIndex].trackIndex = trackIndex;
|
||||||
@@ -117,7 +116,7 @@ export class SplitControls {
|
|||||||
createControl(point: TrackPoint, segment: TrackSegment, fileId: string, trackIndex: number, segmentIndex: number): ControlWithMarker {
|
createControl(point: TrackPoint, segment: TrackSegment, fileId: string, trackIndex: number, segmentIndex: number): ControlWithMarker {
|
||||||
let element = document.createElement('div');
|
let element = document.createElement('div');
|
||||||
element.className = `h-6 w-6 p-0.5 rounded-full bg-white border-2 border-black cursor-pointer`;
|
element.className = `h-6 w-6 p-0.5 rounded-full bg-white border-2 border-black cursor-pointer`;
|
||||||
element.innerHTML = Scissors.replace('width="24"', "").replace('height="24"', "");
|
element.innerHTML = Scissors.replace('width="24"', "").replace('height="24"', "").replace('stroke="currentColor"', 'stroke="black"');
|
||||||
|
|
||||||
let marker = new mapboxgl.Marker({
|
let marker = new mapboxgl.Marker({
|
||||||
draggable: true,
|
draggable: true,
|
||||||
@@ -137,7 +136,7 @@ export class SplitControls {
|
|||||||
|
|
||||||
marker.getElement().addEventListener('click', (e) => {
|
marker.getElement().addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dbUtils.split(fileId, trackIndex, segmentIndex, point.getCoordinates(), point._data.index);
|
dbUtils.split(control.fileId, control.trackIndex, control.segmentIndex, control.point.getCoordinates(), control.point._data.index);
|
||||||
});
|
});
|
||||||
|
|
||||||
return control;
|
return control;
|
||||||
|
@@ -10,7 +10,7 @@ import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simpli
|
|||||||
import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
|
import { SplitType } from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
|
||||||
import { getClosestLinePoint, getElevation } from '$lib/utils';
|
import { getClosestLinePoint, getElevation } from '$lib/utils';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import type mapboxgl from 'mapbox-gl';
|
||||||
|
|
||||||
enableMapSet();
|
enableMapSet();
|
||||||
enablePatches();
|
enablePatches();
|
||||||
@@ -80,7 +80,7 @@ export function dexieSettingStore<T>(key: string, initial: T, initialize: boolea
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const settings = {
|
export const settings = {
|
||||||
distanceUnits: dexieSettingStore<'metric' | 'imperial'>('distanceUnits', 'metric'),
|
distanceUnits: dexieSettingStore<'metric' | 'imperial' | 'nautical'>('distanceUnits', 'metric'),
|
||||||
velocityUnits: dexieSettingStore<'speed' | 'pace'>('velocityUnits', 'speed'),
|
velocityUnits: dexieSettingStore<'speed' | 'pace'>('velocityUnits', 'speed'),
|
||||||
temperatureUnits: dexieSettingStore<'celsius' | 'fahrenheit'>('temperatureUnits', 'celsius'),
|
temperatureUnits: dexieSettingStore<'celsius' | 'fahrenheit'>('temperatureUnits', 'celsius'),
|
||||||
elevationProfile: dexieSettingStore('elevationProfile', true),
|
elevationProfile: dexieSettingStore('elevationProfile', true),
|
||||||
@@ -111,7 +111,6 @@ export const settings = {
|
|||||||
defaultWeight: dexieSettingStore('defaultWeight', (browser && window.innerWidth < 600) ? 8 : 5),
|
defaultWeight: dexieSettingStore('defaultWeight', (browser && window.innerWidth < 600) ? 8 : 5),
|
||||||
bottomPanelSize: dexieSettingStore('bottomPanelSize', 170),
|
bottomPanelSize: dexieSettingStore('bottomPanelSize', 170),
|
||||||
rightPanelSize: dexieSettingStore('rightPanelSize', 240),
|
rightPanelSize: dexieSettingStore('rightPanelSize', 240),
|
||||||
showWelcomeMessage: dexieSettingStore('showWelcomeMessage', true, false),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber
|
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber
|
||||||
@@ -181,7 +180,7 @@ function dexieGPXFileStore(id: string): Readable<GPXFileWithStatistics> & { dest
|
|||||||
|
|
||||||
let statistics = new GPXStatisticsTree(gpx);
|
let statistics = new GPXStatisticsTree(gpx);
|
||||||
if (!fileState.has(id)) { // Update the map bounds for new files
|
if (!fileState.has(id)) { // Update the map bounds for new files
|
||||||
updateTargetMapBounds(statistics.getStatisticsFor(new ListFileItem(id)).global.bounds);
|
updateTargetMapBounds(id, statistics.getStatisticsFor(new ListFileItem(id)).global.bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
fileState.set(id, gpx);
|
fileState.set(id, gpx);
|
||||||
@@ -288,12 +287,12 @@ export const fileObservers: Writable<Map<string, Readable<GPXFileWithStatistics
|
|||||||
const fileState: Map<string, GPXFile> = new Map(); // Used to generate patches
|
const fileState: Map<string, GPXFile> = new Map(); // Used to generate patches
|
||||||
|
|
||||||
// Observe the file ids in the database, and maintain a map of file observers for the corresponding files
|
// Observe the file ids in the database, and maintain a map of file observers for the corresponding files
|
||||||
export function observeFilesFromDatabase() {
|
export function observeFilesFromDatabase(fitBounds: boolean) {
|
||||||
let initialize = true;
|
let initialize = true;
|
||||||
liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => {
|
liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => {
|
||||||
if (initialize) {
|
if (initialize) {
|
||||||
if (dbFileIds.length > 0) {
|
if (fitBounds && dbFileIds.length > 0) {
|
||||||
initTargetMapBounds(dbFileIds.length);
|
initTargetMapBounds(dbFileIds);
|
||||||
}
|
}
|
||||||
initialize = false;
|
initialize = false;
|
||||||
}
|
}
|
||||||
@@ -454,13 +453,14 @@ export const dbUtils = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
addMultiple: (files: GPXFile[]) => {
|
addMultiple: (files: GPXFile[]) => {
|
||||||
return applyGlobal((draft) => {
|
let ids = getFileIds(files.length);
|
||||||
let ids = getFileIds(files.length);
|
applyGlobal((draft) => {
|
||||||
files.forEach((file, index) => {
|
files.forEach((file, index) => {
|
||||||
file._data.id = ids[index];
|
file._data.id = ids[index];
|
||||||
draft.set(file._data.id, freeze(file));
|
draft.set(file._data.id, freeze(file));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
return ids;
|
||||||
},
|
},
|
||||||
applyToFile: (id: string, callback: (file: WritableDraft<GPXFile>) => void) => {
|
applyToFile: (id: string, callback: (file: WritableDraft<GPXFile>) => void) => {
|
||||||
applyToFiles([id], callback);
|
applyToFiles([id], callback);
|
||||||
@@ -513,8 +513,17 @@ export const dbUtils = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
addNewTrack: (fileId: string) => {
|
||||||
|
dbUtils.applyToFile(fileId, (file) => file.replaceTracks(file.trk.length, file.trk.length, [new Track()]));
|
||||||
|
},
|
||||||
|
addNewSegment: (fileId: string, trackIndex: number) => {
|
||||||
|
dbUtils.applyToFile(fileId, (file) => {
|
||||||
|
let track = file.trk[trackIndex];
|
||||||
|
track.replaceTrackSegments(track.trkseg.length, track.trkseg.length, [new TrackSegment()]);
|
||||||
|
});
|
||||||
|
},
|
||||||
reverseSelection: () => {
|
reverseSelection: () => {
|
||||||
if (!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) {
|
if (!get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints']) || get(gpxStatistics).local.points?.length <= 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
applyGlobal((draft) => {
|
applyGlobal((draft) => {
|
||||||
@@ -912,29 +921,30 @@ export const dbUtils = {
|
|||||||
if (m === null) {
|
if (m === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let ele = getElevation(m, waypoint.attributes);
|
getElevation([waypoint.attributes]).then((elevation) => {
|
||||||
if (item) {
|
if (item) {
|
||||||
dbUtils.applyToFile(item.getFileId(), (file) => {
|
dbUtils.applyToFile(item.getFileId(), (file) => {
|
||||||
let wpt = file.wpt[item.getWaypointIndex()];
|
let wpt = file.wpt[item.getWaypointIndex()];
|
||||||
wpt.name = waypoint.name;
|
wpt.name = waypoint.name;
|
||||||
wpt.desc = waypoint.desc;
|
wpt.desc = waypoint.desc;
|
||||||
wpt.cmt = waypoint.cmt;
|
wpt.cmt = waypoint.cmt;
|
||||||
wpt.sym = waypoint.sym;
|
wpt.sym = waypoint.sym;
|
||||||
wpt.link = waypoint.link;
|
wpt.link = waypoint.link;
|
||||||
wpt.setCoordinates(waypoint.attributes);
|
wpt.setCoordinates(waypoint.attributes);
|
||||||
wpt.ele = ele;
|
wpt.ele = elevation[0];
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let fileIds = new Set<string>();
|
let fileIds = new Set<string>();
|
||||||
get(selection).getSelected().forEach((item) => {
|
get(selection).getSelected().forEach((item) => {
|
||||||
fileIds.add(item.getFileId());
|
fileIds.add(item.getFileId());
|
||||||
});
|
});
|
||||||
let wpt = new Waypoint(waypoint);
|
let wpt = new Waypoint(waypoint);
|
||||||
wpt.ele = ele;
|
wpt.ele = elevation[0];
|
||||||
dbUtils.applyToFiles(Array.from(fileIds), (file) =>
|
dbUtils.applyToFiles(Array.from(fileIds), (file) =>
|
||||||
file.replaceWaypoints(file.wpt.length, file.wpt.length, [wpt])
|
file.replaceWaypoints(file.wpt.length, file.wpt.length, [wpt])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
setStyleToSelection: (style: LineStyleExtension) => {
|
setStyleToSelection: (style: LineStyleExtension) => {
|
||||||
if (get(selection).size === 0) {
|
if (get(selection).size === 0) {
|
||||||
@@ -1022,6 +1032,66 @@ export const dbUtils = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
addElevationToSelection: async (map: mapboxgl.Map) => {
|
||||||
|
if (get(selection).size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let points: (TrackPoint | Waypoint)[] = [];
|
||||||
|
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
|
let file = fileState.get(fileId);
|
||||||
|
if (file) {
|
||||||
|
if (level === ListLevel.FILE) {
|
||||||
|
points.push(...file.getTrackPoints());
|
||||||
|
points.push(...file.wpt);
|
||||||
|
} else if (level === ListLevel.TRACK) {
|
||||||
|
let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex());
|
||||||
|
trackIndices.forEach((trackIndex) => {
|
||||||
|
points.push(...file.trk[trackIndex].getTrackPoints());
|
||||||
|
});
|
||||||
|
} else if (level === ListLevel.SEGMENT) {
|
||||||
|
let trackIndex = (items[0] as ListTrackSegmentItem).getTrackIndex();
|
||||||
|
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex());
|
||||||
|
segmentIndices.forEach((segmentIndex) => {
|
||||||
|
points.push(...file.trk[trackIndex].trkseg[segmentIndex].getTrackPoints());
|
||||||
|
});
|
||||||
|
} else if (level === ListLevel.WAYPOINTS) {
|
||||||
|
points.push(...file.wpt);
|
||||||
|
} else if (level === ListLevel.WAYPOINT) {
|
||||||
|
let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex());
|
||||||
|
points.push(...waypointIndices.map((waypointIndex) => file.wpt[waypointIndex]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (points.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getElevation(points).then((elevations) => {
|
||||||
|
applyGlobal((draft) => {
|
||||||
|
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
|
let file = draft.get(fileId);
|
||||||
|
if (file) {
|
||||||
|
if (level === ListLevel.FILE) {
|
||||||
|
file.addElevation(elevations);
|
||||||
|
} else if (level === ListLevel.TRACK) {
|
||||||
|
let trackIndices = items.map((item) => (item as ListTrackItem).getTrackIndex());
|
||||||
|
file.addElevation(elevations, trackIndices, undefined, []);
|
||||||
|
} else if (level === ListLevel.SEGMENT) {
|
||||||
|
let trackIndices = [(items[0] as ListTrackSegmentItem).getTrackIndex()];
|
||||||
|
let segmentIndices = items.map((item) => (item as ListTrackSegmentItem).getSegmentIndex());
|
||||||
|
file.addElevation(elevations, trackIndices, segmentIndices, []);
|
||||||
|
} else if (level === ListLevel.WAYPOINTS) {
|
||||||
|
file.addElevation(elevations, [], [], undefined);
|
||||||
|
} else if (level === ListLevel.WAYPOINT) {
|
||||||
|
let waypointIndices = items.map((item) => (item as ListWaypointItem).getWaypointIndex());
|
||||||
|
file.addElevation(elevations, [], [], waypointIndices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
deleteSelectedFiles: () => {
|
deleteSelectedFiles: () => {
|
||||||
if (get(selection).size === 0) {
|
if (get(selection).size === 0) {
|
||||||
return;
|
return;
|
||||||
|
35
website/src/lib/docs/be/faq.mdx
Normal file
35
website/src/lib/docs/be/faq.mdx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
title: FAQ
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# { title }
|
||||||
|
|
||||||
|
### Do I need to donate to use the website?
|
||||||
|
|
||||||
|
No.
|
||||||
|
The website is free to use and always will be (as long as it is financially sustainable).
|
||||||
|
However, donations are appreciated and help keep the website running.
|
||||||
|
|
||||||
|
### Why is this route chosen over that one? _Or_ how can I add something to the map?
|
||||||
|
|
||||||
|
**gpx.studio** uses data from <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a>, which is an open and collaborative world map.
|
||||||
|
This means you can contribute to the map by adding or editing data on OpenStreetMap.
|
||||||
|
|
||||||
|
If you have never contributed to OpenStreetMap before, here is how you can suggest changes:
|
||||||
|
|
||||||
|
1. Go to the location where you want to add or edit data on the <a href="https://www.openstreetmap.org/" target="_blank">map</a>.
|
||||||
|
2. Use the <button>Query features</button> tool on the right to inspect the existing data.
|
||||||
|
3. Right-click on the location and select <button>Add a note here</button>.
|
||||||
|
4. Explain what is incorrect or missing in the note and click <button>Add note</button> to submit it.
|
||||||
|
|
||||||
|
Someone more experienced with OpenStreetMap will then review your note and make the necessary changes.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
More information on how to contribute to OpenStreetMap can be found <a href="https://wiki.openstreetmap.org/wiki/How_to_contribute" target="_blank">here</a>.
|
||||||
|
|
||||||
|
</DocsNote>
|
82
website/src/lib/docs/be/files-and-stats.mdx
Normal file
82
website/src/lib/docs/be/files-and-stats.mdx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
title: Files and statistics
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { TriangleRight, BrickWall, Zap, HeartPulse, Orbit, Thermometer, SquareActivity } from 'lucide-svelte';
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# { title }
|
||||||
|
|
||||||
|
## File list
|
||||||
|
|
||||||
|
Once you have [opened](./menu/file) files, they will be shown as tabs in the file list located at the bottom of the map.
|
||||||
|
You can reorder them by dragging and dropping the tabs.
|
||||||
|
And when many files are open, you can scroll through the list of tabs to navigate between them.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
When using a mouse, you need to hold <kbd>Shift</kbd> to scroll horizontally.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
### File selection
|
||||||
|
|
||||||
|
By clicking on a tab, you can switch between the files to inspect their statistics, and apply [edit actions](./menu/edit) and [tools](./toolbar) to them.
|
||||||
|
By holding the <kbd>Ctrl/Cmd</kbd> key, you can add files to the selection or remove them, and by holding <kbd>Shift</kbd>, you can select a range of files.
|
||||||
|
Most of the [edit actions](./menu/edit) and [tools](./toolbar) can be applied to multiple files at once.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
You can also navigate through the files using the arrow keys on your keyboard, and use <kbd>Shift</kbd> to add files to the selection.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
### Edit actions
|
||||||
|
|
||||||
|
By right-clicking on a file tab, you can access the same actions as in the [edit menu](./menu/edit).
|
||||||
|
|
||||||
|
### Vertical layout
|
||||||
|
|
||||||
|
As mentioned in the [view options section](./menu/view), you can switch between a horizontal and a vertical layout for the file list.
|
||||||
|
The vertical file list is useful when you have many files open, or files with multiple [tracks, segments, or points of interest](./gpx).
|
||||||
|
Indeed, this layout allows you to inspect the content of the files through collapsible sections.
|
||||||
|
|
||||||
|
You can also apply [edit actions](./menu/edit) and [tools](./toolbar) to internal file items.
|
||||||
|
Furthermore, you can drag and drop the inner items to reorder them, or move them in the hierarchy or even to another file.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
The size of the file list can be adjusted by dragging the separator between the map and the file list.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
## Elevation profile and statistics
|
||||||
|
|
||||||
|
At the bottom of the interface, you can find the elevation profile and statistics for the current selection.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
The size of the elevation profile can be adjusted by dragging the separator between the map and the elevation profile.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
### Interactive statistics
|
||||||
|
|
||||||
|
When hovering over the elevation profile, a tooltip will show statistics at the cursor position.
|
||||||
|
|
||||||
|
To get the statistics for a specific section of the elevation profile, you can drag a selection rectangle on the profile.
|
||||||
|
Click on the profile to reset the selection.
|
||||||
|
|
||||||
|
You can also use the mouse wheel to zoom in and out on the elevation profile, and move left and right by dragging the profile while holding the <kbd>Shift</kbd> key.
|
||||||
|
|
||||||
|
### Additional data
|
||||||
|
|
||||||
|
Using the buttons on the right of the elevation profile, you can optionally color the elevation profile by:
|
||||||
|
|
||||||
|
- **slope** <TriangleRight size="16" class="inline-block" style="margin-bottom: 2px" /> information computed from the elevation data, or
|
||||||
|
- **surface** <BrickWall size="16" class="inline-block" style="margin-bottom: 2px" /> data coming from <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a>'s <a href="https://wiki.openstreetmap.org/wiki/Key:surface" target="_blank">surface</a> tags.
|
||||||
|
This is only available for files created with **gpx.studio**.
|
||||||
|
|
||||||
|
If your selection includes it, you can also visualize: **speed** <Zap size="16" class="inline-block" style="margin-bottom: 2px" />, **heart rate** <HeartPulse size="16" class="inline-block" style="margin-bottom: 2px" />, **cadence** <Orbit size="16" class="inline-block" style="margin-bottom: 2px" />, **temperature** <Thermometer size="16" class="inline-block" style="margin-bottom: 2px" />, and **power** <SquareActivity size="16" class="inline-block" style="margin-bottom: 2px" /> data on the elevation profile.
|
37
website/src/lib/docs/be/getting-started.mdx
Normal file
37
website/src/lib/docs/be/getting-started.mdx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: Getting started
|
||||||
|
---
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# { title }
|
||||||
|
|
||||||
|
Welcome to the official guide for **gpx.studio**!
|
||||||
|
This guide will walk you through all the components and tools of the interface, helping you become a proficient user of the application.
|
||||||
|
|
||||||
|
<DocsImage src="getting-started/interface" alt="The gpx.studio interface." />
|
||||||
|
|
||||||
|
As shown in the screenshot above, the interface is divided into four main sections organized around the map.
|
||||||
|
Before we dive into the details of each section, let's have a quick overview of the interface.
|
||||||
|
|
||||||
|
## Menu
|
||||||
|
|
||||||
|
At the top of the interface, you will find the [main menu](./menu).
|
||||||
|
This is where you can access common actions such as opening, closing, and exporting files, undoing and redoing actions, and adjusting the application settings.
|
||||||
|
|
||||||
|
## Files and statistics
|
||||||
|
|
||||||
|
At the bottom of the interface, you will find the list of files currently open in the application.
|
||||||
|
You can click on a file to select it and display its statistics below the list.
|
||||||
|
In the [dedicated section](./files-and-stats), we will explain how to select multiple files and switch to a vertical layout for advanced file management.
|
||||||
|
|
||||||
|
## Toolbar
|
||||||
|
|
||||||
|
On the left side of the interface, you will find the [toolbar](./toolbar), which contains all the tools you can use to edit your files.
|
||||||
|
|
||||||
|
## Map controls
|
||||||
|
|
||||||
|
Finally, on the right side of the interface, you will find the [map controls](./map-controls).
|
||||||
|
These controls allow you to navigate the map, zoom in and out, and switch between different map styles.
|
34
website/src/lib/docs/be/gpx.mdx
Normal file
34
website/src/lib/docs/be/gpx.mdx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
title: GPX file format
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Waypoints, MapPin } from 'lucide-svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# { title }
|
||||||
|
|
||||||
|
The <a href="https://www.topografix.com/gpx.asp" target="_blank">GPX file format</a> is an open standard for exchanging GPS data between applications and GPS devices.
|
||||||
|
It essentially consists of a series of GPS points encoding one or multiple GPS traces, and, optionally, some points of interest.
|
||||||
|
|
||||||
|
GPX files may also contain metadata, of which the **name** and **description** fields are the most useful for users.
|
||||||
|
|
||||||
|
### <Waypoints size="16" class="inline-block" style="margin-bottom: 2px" /> Tracks, segments, and GPS points
|
||||||
|
|
||||||
|
As mentioned above, a GPX file can contain multiple GPS traces.
|
||||||
|
These are organized in a hierarchical structure, with tracks at the top level.
|
||||||
|
|
||||||
|
- A **track** is made of a sequence of disconnected segments.
|
||||||
|
Furthermore, it can contain metadata such as a **name**, a **description**, and **appearance properties**.
|
||||||
|
- A **segment** is a sequence of GPS points that form a continuous path.
|
||||||
|
- A **GPS point** is a location with a latitude, a longitude, and optionally a timestamp and an altitude.
|
||||||
|
Some devices also store additional information such as heart rate, cadence, temperature, and power.
|
||||||
|
|
||||||
|
In most cases, GPX files contain a single track with a single segment.
|
||||||
|
However, the hierarchy described above allows for more advanced use cases, such as planning multi-day trips with several variants for each day.
|
||||||
|
|
||||||
|
### <MapPin size="16" class="inline-block" style="margin-bottom: 2px" /> Points of interest
|
||||||
|
|
||||||
|
**Points of interest** (technically called _waypoints_) represent locations of interest to show either on a GPS device or on a digital map.
|
||||||
|
|
||||||
|
In addition to its coordinates, a point of interest can have a **name** and a **description**.
|
13
website/src/lib/docs/be/home/funding.mdx
Normal file
13
website/src/lib/docs/be/home/funding.mdx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
import { HeartHandshake } from 'lucide-svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## <HeartHandshake size="18" class="mr-1 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://mapbox.com" target="_blank">Mapbox</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! ❤️
|
5
website/src/lib/docs/be/home/mapbox.mdx
Normal file
5
website/src/lib/docs/be/home/mapbox.mdx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Mapbox is the company that provides some of the beautiful maps on this website.
|
||||||
|
They also develop the <a href="https://github.com/mapbox/mapbox-gl-js" target="_blank">map engine</a> which powers **gpx.studio**.
|
||||||
|
|
||||||
|
We are incredibly fortunate and grateful to be part of their <a href="https://mapbox.com/community" target="_blank">Community</a> program, which supports nonprofits, educational institutions, and positive impact organizations.
|
||||||
|
This partnership allows **gpx.studio** to benefit from Mapbox tools at discounted prices, greatly contributing to the financial viability of the project and enabling us to offer the best possible user experience.
|
12
website/src/lib/docs/be/home/translation.mdx
Normal file
12
website/src/lib/docs/be/home/translation.mdx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
import { Languages } from 'lucide-svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
## <Languages size="18" class="mr-1 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!
|
27
website/src/lib/docs/be/integration.mdx
Normal file
27
website/src/lib/docs/be/integration.mdx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: Integration
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
import EmbeddingPlayground from '$lib/components/embedding/EmbeddingPlayground.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# { title }
|
||||||
|
|
||||||
|
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://account.mapbox.com/auth/signup" target="_blank">Mapbox access token</a> to load the map, and
|
||||||
|
2. GPX files hosted on your server or on Google Drive, or accessible via a public URL.
|
||||||
|
|
||||||
|
You can then play with the configurator below to customize your map and generate the corresponding HTML code.
|
||||||
|
|
||||||
|
<DocsNote type="warning">
|
||||||
|
|
||||||
|
You will need to set up <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" target="_blank">Cross-Origin Resource Sharing (CORS)</a> headers on your server to allow <b>gpx.studio</b> to load your GPX files.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
<EmbeddingPlayground />
|
70
website/src/lib/docs/be/map-controls.mdx
Normal file
70
website/src/lib/docs/be/map-controls.mdx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
title: Map controls
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Plus, Minus, Diff, Compass, Search, LocateFixed, PersonStanding, Layers } from 'lucide-svelte';
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
import DocsLayers from '$lib/components/docs/DocsLayers.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# { title }
|
||||||
|
|
||||||
|
The map controls are located on the right side of the interface.
|
||||||
|
These controls allow you to navigate the map, zoom in and out, and switch between different map styles.
|
||||||
|
|
||||||
|
### <Diff size="16" class="inline-block" style="margin-bottom: 2px" /> Map navigation
|
||||||
|
|
||||||
|
The controls at the top allow you to zoom in <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> and out <Minus size="16" class="inline-block" style="margin-bottom: 2px" />, and to change the orientation and tilt of the map <Compass size="16" class="inline-block" style="margin-bottom: 2px" />.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
To control the orientation and tilt of the map, you can also drag the map while holding <kbd>Ctrl</kbd>.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
### <Search size="16" class="inline-block" style="margin-bottom: 2px" /> Search bar
|
||||||
|
|
||||||
|
You can use the search bar to look for an address and navigate to it on the map.
|
||||||
|
|
||||||
|
### <LocateFixed size="16" class="inline-block" style="margin-bottom: 2px" /> Locate button
|
||||||
|
|
||||||
|
The locate button centers the map on your current location.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
This only works if you have allowed your browser and <b>gpx.studio</b> to access your location.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
### <PersonStanding size="16" class="inline-block" style="margin-bottom: 2px" /> Street view
|
||||||
|
|
||||||
|
This button can be used to enable street view mode on the map.
|
||||||
|
Depending on the street view source chosen in the [settings](./menu/settings), street view imagery can be accessed differently.
|
||||||
|
|
||||||
|
- <a href="https://www.mapillary.com/" target="_blank">Mapillary</a>: the street view coverage will appear as green lines on the map. When zoomed in enough, green dots will show the exact locations where street view imagery is available. Hovering over a green dot will show the street view image at that location.
|
||||||
|
- <a href="https://www.google.com/streetview/" target="_blank">Google Street View</a>: click on the map to open a new tab with the street view imagery at that location.
|
||||||
|
|
||||||
|
### <Layers size="16" class="inline-block" style="margin-bottom: 2px" /> Map layers
|
||||||
|
|
||||||
|
The map layers button allows you to switch between different basemaps, and toggle map overlays and categories of points of interest.
|
||||||
|
|
||||||
|
- **Basemaps** are background maps that present the main geographic features of the world.
|
||||||
|
Depending on their purpose, basemaps have different styles and levels of detail.
|
||||||
|
Only one basemap can be displayed at a time.
|
||||||
|
- **Overlays** are additional layers that can be displayed on top of the basemap to provide complementary information.
|
||||||
|
- **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.mapbox.com/maps/outdoors" target="_blank">Mapbox Outdoors</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.
|
||||||
|
They can be enabled in the [map layer settings dialog](./menu/settings).
|
||||||
|
|
||||||
|
In these settings, you can also manage the opacity of the overlays.
|
||||||
|
|
||||||
|
For advanced users, it is possible to add custom basemaps and overlays by providing <a href="https://en.wikipedia.org/wiki/Web_Map_Tile_Service" target="_blank">WMTS</a>, <a href="https://en.wikipedia.org/wiki/Web_Map_Service" target="_blank">WMS</a>, or <a href="https://docs.mapbox.com/help/glossary/style/" target="_blank">Mapbox style JSON</a> URLs.
|
17
website/src/lib/docs/be/menu.mdx
Normal file
17
website/src/lib/docs/be/menu.mdx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
title: Menu
|
||||||
|
---
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# { title }
|
||||||
|
|
||||||
|
Асноўнае меню, размешчанае ў верхняй частцы інтэрфэйсу, забяспечвае доступ да дзеянняў, опцый і налад, падзеленых на некалькі катэгорый, якія тлумачацца асобна ў наступных раздзелах.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
Большасць з дзеянняў таксама можа быць выклікана з дапамогай спалучэння клавіш адлюстраваных у меню.
|
||||||
|
|
||||||
|
</DocsNote>
|
96
website/src/lib/docs/be/menu/edit.mdx
Normal file
96
website/src/lib/docs/be/menu/edit.mdx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
title: Edit actions
|
||||||
|
---
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Undo2, Redo2, Info, PaintBucket, EyeOff, FileStack, ClipboardCopy, Scissors, ClipboardPaste, Trash2, Maximize, Plus } from 'lucide-svelte';
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# { title }
|
||||||
|
|
||||||
|
Unlike the file actions, the edit actions can potentially modify the content of the currently selected files.
|
||||||
|
Moreover, when the vertical layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, and points of interest](../gpx).
|
||||||
|
Therefore, we will refer to the elements that can be modified by these actions as _file items_.
|
||||||
|
Note that except for the undo and redo actions, the edit actions are also accessible through the context menu (right-click) of the file items.
|
||||||
|
|
||||||
|
### <Undo2 size="16" class="inline-block" style="margin-bottom: 2px" /><Redo2 size="16" class="inline-block" style="margin-bottom: 2px" /> Undo and redo
|
||||||
|
|
||||||
|
Using these buttons, you can undo or redo the last actions you performed.
|
||||||
|
This applies to all actions of the interface but not to view options, application settings, or map navigation.
|
||||||
|
|
||||||
|
### <Info size="16" class="inline-block" style="margin-bottom: 2px" /> Info...
|
||||||
|
|
||||||
|
Open the information dialog of the currently selected file item, where you can see and edit its name and description.
|
||||||
|
|
||||||
|
### <PaintBucket size="16" class="inline-block" style="margin-bottom: 2px" /> Appearance...
|
||||||
|
|
||||||
|
Open the appearance dialog, where you can change the color, opacity, and width of the selected file items on the map.
|
||||||
|
|
||||||
|
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
|
||||||
|
|
||||||
|
Toggle the visibility of the selected file items on the map.
|
||||||
|
|
||||||
|
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New track
|
||||||
|
|
||||||
|
Create a new track in the selected file.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
This action is only available when the vertical layout of the files list is enabled.
|
||||||
|
Additionally, the selection must be a single file.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New segment
|
||||||
|
|
||||||
|
Create a new segment in the selected track.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
This action is only available when the vertical layout of the files list is enabled.
|
||||||
|
Additionally, the selection must be a single track.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
### <FileStack size="16" class="inline-block" style="margin-bottom: 2px" /> Select all
|
||||||
|
|
||||||
|
Add all file items in the current hierarchy level to the selection.
|
||||||
|
|
||||||
|
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
|
||||||
|
|
||||||
|
Center the map on the selected file items.
|
||||||
|
|
||||||
|
### <ClipboardCopy size="16" class="inline-block" style="margin-bottom: 2px" /> Copy
|
||||||
|
|
||||||
|
Copy the selected file items to the clipboard.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
This action is only available when the vertical layout of the files list is enabled.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
### <Scissors size="16" class="inline-block" style="margin-bottom: 2px" /> Cut
|
||||||
|
|
||||||
|
Cut the selected file items to the clipboard.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
This action is only available when the vertical layout of the files list is enabled.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
### <ClipboardPaste size="16" class="inline-block" style="margin-bottom: 2px" /> Paste
|
||||||
|
|
||||||
|
Paste the file items from the clipboard to the current hierarchy level if they are compatible with it.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
This action is only available when the vertical layout of the files list is enabled.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
### <Trash2 size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
||||||
|
|
||||||
|
Delete the selected file items.
|
52
website/src/lib/docs/be/menu/file.mdx
Normal file
52
website/src/lib/docs/be/menu/file.mdx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
title: File actions
|
||||||
|
---
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Plus, FolderOpen, Copy, FileX, Download } from 'lucide-svelte';
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# { title }
|
||||||
|
|
||||||
|
The file actions menu contains a set of pretty self-explanatory file operations.
|
||||||
|
|
||||||
|
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New
|
||||||
|
|
||||||
|
Create a new empty file.
|
||||||
|
|
||||||
|
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Open...
|
||||||
|
|
||||||
|
Open files from your computer.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
You can also drag and drop files directly from your file system into the window.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
### <Copy size="16" class="inline-block" style="margin-bottom: 2px" /> Duplicate
|
||||||
|
|
||||||
|
Create a copy of the currently selected files.
|
||||||
|
|
||||||
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close
|
||||||
|
|
||||||
|
Close the currently selected files.
|
||||||
|
|
||||||
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Close all
|
||||||
|
|
||||||
|
Close all files.
|
||||||
|
|
||||||
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
||||||
|
|
||||||
|
Open the export dialog to save the currently selected files to your computer.
|
||||||
|
|
||||||
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export all...
|
||||||
|
|
||||||
|
Open the export dialog to save all files to your computer.
|
||||||
|
|
||||||
|
<DocsNote type="warning">
|
||||||
|
|
||||||
|
If your download does not start after clicking the download button, please check your browser settings to allow downloads from <b>gpx.studio</b>.
|
||||||
|
|
||||||
|
</DocsNote>
|
50
website/src/lib/docs/be/menu/settings.mdx
Normal file
50
website/src/lib/docs/be/menu/settings.mdx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
title: Settings
|
||||||
|
---
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Ruler, Zap, Thermometer, Languages, Sun, PersonStanding, Layers } from 'lucide-svelte';
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# { title }
|
||||||
|
|
||||||
|
### <Ruler size="16" class="inline-block" style="margin-bottom: 2px" /> Distance units
|
||||||
|
|
||||||
|
Change the units used to display distances in the interface.
|
||||||
|
|
||||||
|
### <Zap size="16" class="inline-block" style="margin-bottom: 2px" /> Velocity units
|
||||||
|
|
||||||
|
Change the units used to display velocities in the interface.
|
||||||
|
You can choose between distance per hour or minutes per distance, which can be more suitable for running activities.
|
||||||
|
|
||||||
|
### <Thermometer size="16" class="inline-block" style="margin-bottom: 2px" /> Temperature units
|
||||||
|
|
||||||
|
Change the units used to display temperatures in the interface.
|
||||||
|
|
||||||
|
### <Languages size="16" class="inline-block" style="margin-bottom: 2px" /> Language
|
||||||
|
|
||||||
|
Change the language used in the interface.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
### <Sun size="16" class="inline-block" style="margin-bottom: 2px" /> Theme
|
||||||
|
|
||||||
|
Change the theme used in the interface.
|
||||||
|
|
||||||
|
### <PersonStanding size="16" class="inline-block" style="margin-bottom: 2px" /> Street view source
|
||||||
|
|
||||||
|
Change the source used for the [street view control](../map-controls).
|
||||||
|
The default one is <a href="https://www.mapillary.com" target="_blank">Mapillary</a>, but you can also use <a href="https://www.google.com/streetview/" target="_blank">Google Street View</a>.
|
||||||
|
Learn more about how to use the street view control in the [map controls section](../map-controls).
|
||||||
|
|
||||||
|
### <Layers size="16" class="inline-block" style="margin-bottom: 2px" /> Map layers...
|
||||||
|
|
||||||
|
Open a dialog where you can enable or disable map layers, add custom ones, change the opacity of overlays, and more.
|
||||||
|
More information about map layers can be found in the [map controls section](../map-controls).
|
48
website/src/lib/docs/be/menu/view.mdx
Normal file
48
website/src/lib/docs/be/menu/view.mdx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
title: View options
|
||||||
|
---
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ChartArea, GalleryVertical, Map, Layers2, Coins, Milestone, Box } from 'lucide-svelte';
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# { title }
|
||||||
|
|
||||||
|
This menu provides options to rearrange the interface and the map view.
|
||||||
|
|
||||||
|
### <ChartArea size="16" class="inline-block" style="margin-bottom: 2px" /> Elevation profile
|
||||||
|
|
||||||
|
Hide the elevation profile to make room for the map, or show it to inspect the current selection.
|
||||||
|
|
||||||
|
### <GalleryVertical size="16" class="inline-block" style="margin-bottom: 2px" /> Vertical file list
|
||||||
|
|
||||||
|
Switch between a vertical and a horizontal layout for the file list.
|
||||||
|
The [vertical file list](../files-and-stats) is useful when you have many files open, or files with multiple [tracks, segments, or points of interest](../gpx).
|
||||||
|
|
||||||
|
### <Map size="16" class="inline-block" style="margin-bottom: 2px" /> Switch to previous basemap
|
||||||
|
|
||||||
|
Change the basemap to the one previously selected through the [map layer control](../map-controls).
|
||||||
|
|
||||||
|
### <Layers2 size="16" class="inline-block" style="margin-bottom: 2px" /> Toggle overlays
|
||||||
|
|
||||||
|
Toggle the visibility of the map overlays selected through the [map layer control](../map-controls).
|
||||||
|
|
||||||
|
### <Coins size="16" class="inline-block" style="margin-bottom: 2px" /> Distance markers
|
||||||
|
|
||||||
|
Toggle the visibility of distance markers on the map.
|
||||||
|
They are displayed for the current selection, like the [elevation profile](../files-and-stats).
|
||||||
|
|
||||||
|
### <Milestone size="16" class="inline-block" style="margin-bottom: 2px" /> Direction arrows
|
||||||
|
|
||||||
|
Toggle the visibility of direction arrows on the map.
|
||||||
|
|
||||||
|
### <Box size="16" class="inline-block" style="margin-bottom: 2px" /> Toggle 3D
|
||||||
|
|
||||||
|
Enter or exit the 3D map view.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
To control the orientation and tilt of the map, you can also drag the map while holding <kbd>Ctrl</kbd>.
|
||||||
|
|
||||||
|
</DocsNote>
|
32
website/src/lib/docs/be/toolbar.mdx
Normal file
32
website/src/lib/docs/be/toolbar.mdx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
title: Toolbar
|
||||||
|
---
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
|
||||||
|
import { currentTool, Tool } from '$lib/stores';
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
currentTool.set(Tool.ROUTING);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
currentTool.set(null);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# { title }
|
||||||
|
|
||||||
|
Панэль інструментаў знаходзіцца з левага боку мапы і з'яўляецца сэрцам прыкладання, яна забяспечвае доступ да асноўных функцый **gpx.studio**.
|
||||||
|
Кожны інструмент прадстаўлены цэтлікам і можа быць актываваны пстрыкам на яго.
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-center text-foreground">
|
||||||
|
<div>
|
||||||
|
<Toolbar class="border rounded-md shadow-lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
З дапамогай [edit actions](./menu/edit), большасць інструментаў можа быць прыменена да некалькіх файлаў адначасова, а таксама для [inner tracks and segments](./gpx).
|
||||||
|
|
||||||
|
Наступныя секцыі апісваюць кожны інструмент больш дэтальна.
|
18
website/src/lib/docs/be/toolbar/clean.mdx
Normal file
18
website/src/lib/docs/be/toolbar/clean.mdx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
title: Clean
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { SquareDashedMousePointer } from 'lucide-svelte';
|
||||||
|
import Clean from '$lib/components/toolbar/tools/Clean.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# <SquareDashedMousePointer size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||||
|
|
||||||
|
When the clean tool is selected, dragging the map will create a rectangular selection.
|
||||||
|
|
||||||
|
Depending on the options selected in the dialog shown below, clicking the delete button will remove GPS points and/or [points of interest](../gpx) located either inside or outside the selection.
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
<Clean class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||||
|
</div>
|
24
website/src/lib/docs/be/toolbar/elevation.mdx
Normal file
24
website/src/lib/docs/be/toolbar/elevation.mdx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: Elevation
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { MountainSnow } from 'lucide-svelte';
|
||||||
|
import Elevation from '$lib/components/toolbar/tools/Elevation.svelte';
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# <MountainSnow size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||||
|
|
||||||
|
This tool allows you to add elevation data to traces and [points of interest](../gpx), or to replace the existing data.
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
<Elevation class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
Elevation data is provided by <a href="https://mapbox.com" target="_blank">Mapbox</a>.
|
||||||
|
You can learn more about its origin and accuracy in the <a href="https://docs.mapbox.com/data/tilesets/reference/mapbox-terrain-dem-v1/#elevation-data" target="_blank">documentation</a>.
|
||||||
|
|
||||||
|
</DocsNote>
|
26
website/src/lib/docs/be/toolbar/extract.mdx
Normal file
26
website/src/lib/docs/be/toolbar/extract.mdx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: Extract
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Ungroup } from 'lucide-svelte';
|
||||||
|
import Extract from '$lib/components/toolbar/tools/Extract.svelte';
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# <Ungroup size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||||
|
|
||||||
|
This tool allows you to extract [tracks (or segments)](../gpx) from files (or tracks) containing multiple of them.
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
<Extract class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Applying the tool to a file containing multiple tracks will create a new file for each of the tracks it contains.
|
||||||
|
Similarly, applying the tool to a track containing multiple segments will create (in the same file) a new track for each of the segments it contains.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
When extracting the tracks from a file containing <a href="../gpx">points of interest</a>, the tool will automatically assign each point of interest to the track it is closest to.
|
||||||
|
|
||||||
|
</DocsNote>
|
20
website/src/lib/docs/be/toolbar/merge.mdx
Normal file
20
website/src/lib/docs/be/toolbar/merge.mdx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
title: Merge
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Group } from 'lucide-svelte';
|
||||||
|
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# <Group size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||||
|
|
||||||
|
To use this tool, you need to [select](../files-and-stats) multiple files, [tracks, or segments](../gpx).
|
||||||
|
|
||||||
|
- If your goal is to create a single continuous trace from your selection, use the **Connect the traces** option and validate.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
<Merge class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||||
|
</div>
|
26
website/src/lib/docs/be/toolbar/minify.mdx
Normal file
26
website/src/lib/docs/be/toolbar/minify.mdx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: Minify
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Filter } from 'lucide-svelte';
|
||||||
|
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# <Filter size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||||
|
|
||||||
|
This tool can be used to reduce the number of GPS points in a trace, which can be useful for decreasing its size.
|
||||||
|
|
||||||
|
You can adjust the tolerance of the simplification algorithm using the slider, and see the number of points that will be kept, as well as the simplified trace on the map.
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
<Reduce class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
The tolerance value represents the maximum distance allowed between the original trace and the simplified trace.
|
||||||
|
You can read more about the algorithm used <a href="https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm" target="_blank">here</a>.
|
||||||
|
|
||||||
|
</DocsNote>
|
27
website/src/lib/docs/be/toolbar/poi.mdx
Normal file
27
website/src/lib/docs/be/toolbar/poi.mdx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: Points of interest
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { MapPin } from 'lucide-svelte';
|
||||||
|
import Waypoint from '$lib/components/toolbar/tools/Waypoint.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# <MapPin size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||||
|
|
||||||
|
[Points of interest](../gpx) can be added to GPX files to mark locations of interest on the map and display them on your GPS device.
|
||||||
|
|
||||||
|
### Creating a point of interest
|
||||||
|
|
||||||
|
To create a point of interest, fill in the form shown below.
|
||||||
|
You can choose the location of the point of interest either by clicking on the map or by entering the coordinates manually.
|
||||||
|
Validate the form when you are done.
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
<Waypoint class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Editing a point of interest
|
||||||
|
|
||||||
|
The form above can also be used to edit an existing point of interest after selecting it on the map.
|
||||||
|
If you only need to move the point of interest, you can drag it to the desired location.
|
84
website/src/lib/docs/be/toolbar/routing.mdx
Normal file
84
website/src/lib/docs/be/toolbar/routing.mdx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
title: Route planning and editing
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Pencil, Route, Bike, TriangleAlert, ArrowRightLeft, Home, Repeat, Trash2, CirclePlay, SquareArrowUpLeft } from 'lucide-svelte';
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||||
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# <Pencil size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||||
|
|
||||||
|
The route planning and editing tool allows you to create and edit routes by placing or moving anchor points on the map.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
As shown below, the tool dialog contains a few settings to control the routing behavior.
|
||||||
|
You can minimize the dialog to save space by clicking on <button><SquareArrowUpLeft size="16" class="inline-block" style="margin-bottom: 2px" /></button>.
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
<Routing minimizable={false} class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### <Route size="16" class="inline-block" style="margin-bottom: 2px" /> Routing
|
||||||
|
|
||||||
|
When routing is enabled, anchor points placed or moved on the map will be connected by a route calculated on the <a href="https://www.openstreetmap.org" target="_blank">OpenStreetMap</a> road network.
|
||||||
|
Disable routing to connect anchor points with straight lines.
|
||||||
|
This setting can also be toggled by pressing <kbd>F5</kbd>.
|
||||||
|
|
||||||
|
### <Bike size="16" class="inline-block" style="margin-bottom: 2px" /> Activity
|
||||||
|
|
||||||
|
Select the activity type to tailor the routes for.
|
||||||
|
|
||||||
|
### <TriangleAlert size="16" class="inline-block" style="margin-bottom: 2px" /> Allow private roads
|
||||||
|
|
||||||
|
When enabled, the routing engine will consider private roads when computing routes.
|
||||||
|
|
||||||
|
<DocsNote type="warning">
|
||||||
|
|
||||||
|
Only use this option if you have local knowledge of the area and have permission to use the roads in question.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
## Plotting and editing routes
|
||||||
|
|
||||||
|
Creating a route or extending an existing one is as simple as clicking on the map to place a new anchor point.
|
||||||
|
|
||||||
|
You can also drag an existing anchor point to reroute the segment connecting it with the previous and next anchor point.
|
||||||
|
|
||||||
|
Furthermore, new anchor points can be inserted between existing ones by hovering over the segment connecting them and dragging the anchor point that appears to the desired location.
|
||||||
|
On touch devices, you can tap on the segment to insert a new anchor point.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
When editing imported GPX files, an initial set of anchor points is created automatically.
|
||||||
|
To ease the editing process, the more the map is zoomed in, the more anchor points are displayed.
|
||||||
|
This allows the route to be edited at different levels of detail.
|
||||||
|
|
||||||
|
</DocsNote>
|
||||||
|
|
||||||
|
Finally, you can delete anchor points by clicking on them and selecting <button><Trash2 size="16" class="inline-block" style="margin-bottom: 4px" /> Delete</button> from the context menu.
|
||||||
|
|
||||||
|
<DocsImage src="tools/routing" alt="Anchor points allow you to easily edit a route." />
|
||||||
|
|
||||||
|
## Additional tools
|
||||||
|
|
||||||
|
The following tools automate some common route modification operations.
|
||||||
|
|
||||||
|
### <ArrowRightLeft size="16" class="inline-block" style="margin-bottom: 2px" /> Reverse
|
||||||
|
|
||||||
|
Reverse the direction of the route.
|
||||||
|
|
||||||
|
### <Home size="16" class="inline-block" style="margin-bottom: 2px" /> Back to start
|
||||||
|
|
||||||
|
Connect the last point of the route with the starting point, using the chosen routing settings.
|
||||||
|
|
||||||
|
### <Repeat size="16" class="inline-block" style="margin-bottom: 2px" /> Round trip
|
||||||
|
|
||||||
|
Return to the starting point by the same route.
|
||||||
|
|
||||||
|
### <CirclePlay size="16" class="inline-block" style="margin-bottom: 2px" /> Change the start of the loop
|
||||||
|
|
||||||
|
When the end point of the route is close enough to the start, you can change the start of the loop by clicking on any anchor point and selecting <button><CirclePlay size="16" class="inline-block" style="margin-bottom: 2px" /> Start loop here</button> from the context menu.
|
32
website/src/lib/docs/be/toolbar/scissors.mdx
Normal file
32
website/src/lib/docs/be/toolbar/scissors.mdx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
title: Crop and split
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ScissorsIcon } from 'lucide-svelte';
|
||||||
|
import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
|
||||||
|
import DocsImage from '$lib/components/docs/DocsImage.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# <ScissorsIcon size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||||
|
|
||||||
|
## Crop
|
||||||
|
|
||||||
|
Using the slider, you can define the part of the selected trace that you want to keep.
|
||||||
|
The start and end markers on the map and the [statistics and elevation profile](../files-and-stats) are updated in real time to reflect the selection.
|
||||||
|
Alternatively, you can drag a selection rectangle directly on the elevation profile.
|
||||||
|
Validate the selection when you are satisfied with the result.
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
<Scissors class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Split
|
||||||
|
|
||||||
|
To split the selected trace into two parts, click on one of the split markers displayed along the trace.
|
||||||
|
To split at a specific point of your choice, hover over the trace on the map.
|
||||||
|
Scissors will appear at the cursor position, showing that you can split the trace at that point.
|
||||||
|
|
||||||
|
You can choose to split the trace into two GPX files, or to keep the split parts in the same file as [tracks or segments](../gpx).
|
||||||
|
|
||||||
|
<DocsImage src="tools/split" alt="Hovering over the selected trace turns your cursor into scissors." />
|
27
website/src/lib/docs/be/toolbar/time.mdx
Normal file
27
website/src/lib/docs/be/toolbar/time.mdx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: Time
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { CalendarClock } from 'lucide-svelte';
|
||||||
|
import Time from '$lib/components/toolbar/tools/Time.svelte';
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# <CalendarClock size="24" class="inline-block" style="margin-bottom: 5px" /> { title }
|
||||||
|
|
||||||
|
This tool allows you to change or add timestamps to a trace.
|
||||||
|
You simply need to use the form shown below and validate it when you are done.
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-center">
|
||||||
|
<Time class="text-foreground p-3 border rounded-md shadow-lg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
When you edit the speed, the moving time is adapted accordingly in the form, and vice versa.
|
||||||
|
Similarly, when you edit the start time, the end time is updated to keep the same total duration, and vice versa.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
When using this tool with existing timestamps, changing the time or speed will simply shift, stretch, or compress them accordingly.
|
||||||
|
|
||||||
|
</DocsNote>
|
35
website/src/lib/docs/ca/faq.mdx
Normal file
35
website/src/lib/docs/ca/faq.mdx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
title: FAQ
|
||||||
|
---
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DocsNote from '$lib/components/docs/DocsNote.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# { title }
|
||||||
|
|
||||||
|
### Do I need to donate to use the website?
|
||||||
|
|
||||||
|
No.
|
||||||
|
The website is free to use and always will be (as long as it is financially sustainable).
|
||||||
|
However, donations are appreciated and help keep the website running.
|
||||||
|
|
||||||
|
### Why is this route chosen over that one? _Or_ how can I add something to the map?
|
||||||
|
|
||||||
|
**gpx.studio** uses data from <a href="https://www.openstreetmap.org/" target="_blank">OpenStreetMap</a>, which is an open and collaborative world map.
|
||||||
|
This means you can contribute to the map by adding or editing data on OpenStreetMap.
|
||||||
|
|
||||||
|
If you have never contributed to OpenStreetMap before, here is how you can suggest changes:
|
||||||
|
|
||||||
|
1. Go to the location where you want to add or edit data on the <a href="https://www.openstreetmap.org/" target="_blank">map</a>.
|
||||||
|
2. Use the <button>Query features</button> tool on the right to inspect the existing data.
|
||||||
|
3. Right-click on the location and select <button>Add a note here</button>.
|
||||||
|
4. Explain what is incorrect or missing in the note and click <button>Add note</button> to submit it.
|
||||||
|
|
||||||
|
Someone more experienced with OpenStreetMap will then review your note and make the necessary changes.
|
||||||
|
|
||||||
|
<DocsNote>
|
||||||
|
|
||||||
|
More information on how to contribute to OpenStreetMap can be found <a href="https://wiki.openstreetmap.org/wiki/How_to_contribute" target="_blank">here</a>.
|
||||||
|
|
||||||
|
</DocsNote>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user