first commit
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.DS_Store
|
||||||
|
website/build
|
||||||
|
website/node_modules
|
||||||
|
website/.svelte-kit
|
||||||
|
website/package
|
||||||
|
website/.env
|
||||||
|
website/.env.*
|
||||||
|
website/!.env.example
|
||||||
|
website/vite.config.js.timestamp-*
|
||||||
|
website/vite.config.ts.timestamp-*
|
||||||
|
gpx/node_modules
|
||||||
|
gpx/dist
|
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 gpx.studio
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
80
README.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="website/static/logo-dark.svg">
|
||||||
|
<img alt="Logo of gpx.studio." src="website/static/logo.svg">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
[**gpx.studio**](https://gpx.studio) is an online tool for creating and editing GPX files.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This repository contains the source code of the website.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Please create an issue if you find a bug or have a feature request.
|
||||||
|
|
||||||
|
Code contributions are also welcome, but except for obvious bug fixes, please open an issue first to discuss the changes you would like to make.
|
||||||
|
|
||||||
|
## Translation
|
||||||
|
|
||||||
|
The website is translated by volunteers on a collaborative translation platform.
|
||||||
|
You can help complete and improve the translations by joining the [Crowdin project](https://crowdin.com/project/gpxstudio).
|
||||||
|
If you would like to start the translation in a new language, please contact me or create an issue.
|
||||||
|
|
||||||
|
Any help is greatly appreciated!
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
The code is split into two parts:
|
||||||
|
- `gpx`: a Typescript library for parsing and manipulating GPX files,
|
||||||
|
- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application.
|
||||||
|
|
||||||
|
You will need [Node.js](https://nodejs.org/) to build and run these two parts.
|
||||||
|
|
||||||
|
### Building the `gpx` library
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd gpx
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the website
|
||||||
|
|
||||||
|
To be able to load the map, you will need to create your own <a href="https://account.mapbox.com/auth/signup" target="_blank">Mapbox access token</a> and store it in a `.env` file in the `website` directory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd website
|
||||||
|
echo PUBLIC_MAPBOX_TOKEN={YOUR_MAPBOX_TOKEN} >> .env
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
This project has been made possible thanks to the following open source projects:
|
||||||
|
|
||||||
|
- Development:
|
||||||
|
- [Svelte](https://github.com/sveltejs/svelte) and [SvelteKit](https://github.com/sveltejs/kit) — seamless development experience
|
||||||
|
- [MDsveX](https://github.com/pngwn/MDsveX) — allowing a Markdown-based documentation
|
||||||
|
- [svelte-i18n](https://github.com/kaisermann/svelte-i18n) — easy localization
|
||||||
|
- Design:
|
||||||
|
- [shadcn-svelte](https://github.com/huntabyte/shadcn-svelte) — beautiful components
|
||||||
|
- [lucide-svelte](https://github.com/lucide-icons/lucide/tree/main/packages/lucide-svelte) — beautiful icons
|
||||||
|
- [tailwindcss](https://github.com/tailwindlabs/tailwindcss) — easy styling
|
||||||
|
- [Chart.js](https://github.com/chartjs/Chart.js) — beautiful and fast charts
|
||||||
|
- Logic:
|
||||||
|
- [immer](https://github.com/immerjs/immer) — complex state management
|
||||||
|
- [Dexie.js](https://github.com/dexie/Dexie.js) — IndexedDB wrapper
|
||||||
|
- [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) — fast GPX file parsing
|
||||||
|
- [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree
|
||||||
|
- Mapping:
|
||||||
|
- [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) — beautiful and fast interactive maps
|
||||||
|
- [brouter](https://github.com/abrensch/brouter) — routing engine
|
||||||
|
- [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
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
5
crowdin.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
files:
|
||||||
|
- source: /website/src/locales/en.json
|
||||||
|
translation: /website/src/locales/%two_letters_code%.json
|
||||||
|
- source: /website/src/lib/docs/en/**/*.mdx
|
||||||
|
translation: /website/src/lib/docs/%two_letters_code%/**/%original_file_name%
|
242
gpx/package-lock.json
generated
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
{
|
||||||
|
"name": "gpx",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "gpx",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fast-xml-parser": "^4.5.0",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"ts-node": "^10.9.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/geojson": "^7946.0.14",
|
||||||
|
"@types/node": "^20.16.10",
|
||||||
|
"typescript": "^5.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
|
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": "0.3.9"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||||
|
"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": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node12": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node14": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node16": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz",
|
||||||
|
"integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.16.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||||
|
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.19.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/acorn": {
|
||||||
|
"version": "8.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||||
|
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||||
|
"bin": {
|
||||||
|
"acorn": "bin/acorn"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/acorn-walk": {
|
||||||
|
"version": "8.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||||
|
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/arg": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
|
||||||
|
},
|
||||||
|
"node_modules/create-require": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||||
|
},
|
||||||
|
"node_modules/diff": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-xml-parser": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/naturalintelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"strnum": "^1.0.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"fxparser": "src/cli/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/make-error": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
|
||||||
|
},
|
||||||
|
"node_modules/strnum": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
|
||||||
|
},
|
||||||
|
"node_modules/ts-node": {
|
||||||
|
"version": "10.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
|
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
|
"@tsconfig/node10": "^1.0.7",
|
||||||
|
"@tsconfig/node12": "^1.0.7",
|
||||||
|
"@tsconfig/node14": "^1.0.0",
|
||||||
|
"@tsconfig/node16": "^1.0.2",
|
||||||
|
"acorn": "^8.4.1",
|
||||||
|
"acorn-walk": "^8.1.1",
|
||||||
|
"arg": "^4.1.0",
|
||||||
|
"create-require": "^1.1.0",
|
||||||
|
"diff": "^4.0.1",
|
||||||
|
"make-error": "^1.1.1",
|
||||||
|
"v8-compile-cache-lib": "^3.0.1",
|
||||||
|
"yn": "3.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ts-node": "dist/bin.js",
|
||||||
|
"ts-node-cwd": "dist/bin-cwd.js",
|
||||||
|
"ts-node-esm": "dist/bin-esm.js",
|
||||||
|
"ts-node-script": "dist/bin-script.js",
|
||||||
|
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||||
|
"ts-script": "dist/bin-script-deprecated.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@swc/core": ">=1.2.50",
|
||||||
|
"@swc/wasm": ">=1.2.50",
|
||||||
|
"@types/node": "*",
|
||||||
|
"typescript": ">=2.7"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@swc/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/wasm": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||||
|
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.19.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||||
|
},
|
||||||
|
"node_modules/v8-compile-cache-lib": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="
|
||||||
|
},
|
||||||
|
"node_modules/yn": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
gpx/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "gpx",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"exports": "./dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/gpxstudio/gpx.studio.git",
|
||||||
|
"directory": "gpx"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fast-xml-parser": "^4.5.0",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"ts-node": "^10.9.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/geojson": "^7946.0.14",
|
||||||
|
"@types/node": "^20.16.10",
|
||||||
|
"typescript": "^5.6.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"postinstall": "npm run build"
|
||||||
|
}
|
||||||
|
}
|
1625
gpx/src/gpx.ts
Normal file
5
gpx/src/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './gpx';
|
||||||
|
export { Coordinates, LineStyleExtension, WaypointType } from './types';
|
||||||
|
export { parseGPX, buildGPX } from './io';
|
||||||
|
export * from './simplify';
|
||||||
|
|
118
gpx/src/io.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { XMLParser, XMLBuilder } from "fast-xml-parser";
|
||||||
|
import { GPXFileType } from "./types";
|
||||||
|
import { GPXFile } from "./gpx";
|
||||||
|
|
||||||
|
export function parseGPX(gpxData: string): GPXFile {
|
||||||
|
const parser = new XMLParser({
|
||||||
|
ignoreAttributes: false,
|
||||||
|
attributeNamePrefix: "",
|
||||||
|
attributesGroupName: 'attributes',
|
||||||
|
isArray(name: string) {
|
||||||
|
return name === 'trk' || name === 'trkseg' || name === 'trkpt' || name === 'wpt' || name === 'rte' || name === 'rtept' || name === 'gpxx:rpt';
|
||||||
|
},
|
||||||
|
attributeValueProcessor(attrName, attrValue, jPath) {
|
||||||
|
if (attrName === 'lat' || attrName === 'lon') {
|
||||||
|
return parseFloat(attrValue);
|
||||||
|
}
|
||||||
|
return attrValue;
|
||||||
|
},
|
||||||
|
transformTagName(tagName: string) {
|
||||||
|
if (tagName === 'power') {
|
||||||
|
// Transform the simple <power> tag to the more complex <gpxpx:PowerExtension> tag, the nested <gpxpx:PowerInWatts> tag is then handled by the tagValueProcessor
|
||||||
|
return 'gpxpx:PowerExtension';
|
||||||
|
}
|
||||||
|
return tagName;
|
||||||
|
},
|
||||||
|
parseTagValue: false,
|
||||||
|
tagValueProcessor(tagName, tagValue, jPath, hasAttributes, isLeafNode) {
|
||||||
|
if (isLeafNode) {
|
||||||
|
if (tagName === 'ele') {
|
||||||
|
return parseFloat(tagValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagName === 'time') {
|
||||||
|
return new Date(tagValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagName === 'gpxtpx:atemp' || tagName === 'gpxtpx:hr' || tagName === 'gpxtpx:cad' || tagName === 'gpxpx:PowerInWatts' || tagName === 'opacity' || tagName === 'weight') {
|
||||||
|
return parseFloat(tagValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagName === 'gpxpx:PowerExtension') {
|
||||||
|
// Finish the transformation of the simple <power> tag to the more complex <gpxpx:PowerExtension> tag
|
||||||
|
// Note that this only targets the transformed <power> tag, since it must be a leaf node
|
||||||
|
return {
|
||||||
|
'gpxpx:PowerInWatts': parseFloat(tagValue)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagValue;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed: GPXFileType = parser.parse(gpxData).gpx;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (parsed.metadata === "") {
|
||||||
|
parsed.metadata = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GPXFile(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildGPX(file: GPXFile, exclude: string[]): string {
|
||||||
|
const gpx = file.toGPXFileType(exclude);
|
||||||
|
|
||||||
|
const builder = new XMLBuilder({
|
||||||
|
format: true,
|
||||||
|
ignoreAttributes: false,
|
||||||
|
attributeNamePrefix: "",
|
||||||
|
attributesGroupName: 'attributes',
|
||||||
|
suppressEmptyNode: true,
|
||||||
|
tagValueProcessor: (tagName: string, tagValue: unknown): string => {
|
||||||
|
if (tagValue instanceof Date) {
|
||||||
|
return tagValue.toISOString();
|
||||||
|
}
|
||||||
|
return tagValue.toString();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
gpx.attributes.creator = gpx.attributes.creator ?? 'https://gpx.studio';
|
||||||
|
gpx.attributes['version'] = '1.1';
|
||||||
|
gpx.attributes['xmlns'] = 'http://www.topografix.com/GPX/1/1';
|
||||||
|
gpx.attributes['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance';
|
||||||
|
gpx.attributes['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.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd';
|
||||||
|
gpx.attributes['xmlns:gpxtpx'] = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1';
|
||||||
|
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:gpx_style'] = 'http://www.topografix.com/GPX/gpx_style/0/2';
|
||||||
|
|
||||||
|
if (gpx.trk.length === 1 && (gpx.trk[0].name === undefined || gpx.trk[0].name === '')) {
|
||||||
|
gpx.trk[0].name = gpx.metadata.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build({
|
||||||
|
"?xml": {
|
||||||
|
attributes: {
|
||||||
|
version: "1.0",
|
||||||
|
encoding: "UTF-8",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}
|
155
gpx/src/simplify.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { TrackPoint } from "./gpx";
|
||||||
|
import { Coordinates } from "./types";
|
||||||
|
|
||||||
|
export type SimplifiedTrackPoint = { point: TrackPoint, distance?: number };
|
||||||
|
|
||||||
|
const earthRadius = 6371008.8;
|
||||||
|
|
||||||
|
export function ramerDouglasPeucker(points: TrackPoint[], epsilon: number = 50, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = crossarcDistance): SimplifiedTrackPoint[] {
|
||||||
|
if (points.length == 0) {
|
||||||
|
return [];
|
||||||
|
} else if (points.length == 1) {
|
||||||
|
return [{
|
||||||
|
point: points[0]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
let simplified = [{
|
||||||
|
point: points[0]
|
||||||
|
}];
|
||||||
|
ramerDouglasPeuckerRecursive(points, epsilon, measure, 0, points.length - 1, simplified);
|
||||||
|
simplified.push({
|
||||||
|
point: points[points.length - 1]
|
||||||
|
});
|
||||||
|
return simplified;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number, start: number, end: number, simplified: SimplifiedTrackPoint[]) {
|
||||||
|
let largest = {
|
||||||
|
index: 0,
|
||||||
|
distance: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = start + 1; i < end; i++) {
|
||||||
|
let distance = measure(points[start], points[end], points[i]);
|
||||||
|
if (distance > largest.distance) {
|
||||||
|
largest.index = i;
|
||||||
|
largest.distance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (largest.distance > epsilon && largest.index != 0) {
|
||||||
|
ramerDouglasPeuckerRecursive(points, epsilon, measure, start, largest.index, simplified);
|
||||||
|
simplified.push({ point: points[largest.index], distance: largest.distance });
|
||||||
|
ramerDouglasPeuckerRecursive(points, epsilon, measure, largest.index, end, simplified);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function crossarcDistance(point1: TrackPoint, point2: TrackPoint, point3: TrackPoint | Coordinates): number {
|
||||||
|
return crossarc(point1.getCoordinates(), point2.getCoordinates(), point3 instanceof TrackPoint ? point3.getCoordinates() : point3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
|
||||||
|
// Calculates the shortest distance in meters
|
||||||
|
// between an arc (defined by p1 and p2) and a 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 dis13;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 distance(lat2, lon2, lat3, lon3);
|
||||||
|
} else {
|
||||||
|
return Math.abs(dxt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function distance(latA: number, lonA: number, latB: number, lonB: number): number {
|
||||||
|
// Finds the distance between two lat / lon points.
|
||||||
|
return Math.acos(Math.sin(latA) * Math.sin(latB) + Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)) * earthRadius;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function bearing(latA: number, lonA: number, latB: number, lonB: number): number {
|
||||||
|
// Finds the bearing from one lat / lon point to another.
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
127
gpx/src/types.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
export type GPXFileType = {
|
||||||
|
attributes: GPXFileAttributes;
|
||||||
|
metadata: Metadata;
|
||||||
|
wpt: WaypointType[];
|
||||||
|
trk: TrackType[];
|
||||||
|
rte: RouteType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GPXFileAttributes = {
|
||||||
|
creator?: string;
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Metadata = {
|
||||||
|
name?: string;
|
||||||
|
desc?: string;
|
||||||
|
author?: Author;
|
||||||
|
link?: Link;
|
||||||
|
time?: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Link = {
|
||||||
|
attributes: LinkAttributes;
|
||||||
|
text?: string;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LinkAttributes = {
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WaypointType = {
|
||||||
|
attributes: Coordinates;
|
||||||
|
ele?: number;
|
||||||
|
time?: Date;
|
||||||
|
name?: string;
|
||||||
|
cmt?: string;
|
||||||
|
desc?: string;
|
||||||
|
link?: Link;
|
||||||
|
sym?: string;
|
||||||
|
type?: string;
|
||||||
|
extensions?: WaypointExtensions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WaypointExtensions = {
|
||||||
|
'gpxx:RoutePointExtension'?: RoutePointExtension;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Coordinates = {
|
||||||
|
lat: number;
|
||||||
|
lon: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TrackType = {
|
||||||
|
name?: string;
|
||||||
|
cmt?: string;
|
||||||
|
desc?: string;
|
||||||
|
src?: string;
|
||||||
|
link?: Link;
|
||||||
|
type?: string;
|
||||||
|
extensions?: TrackExtensions;
|
||||||
|
trkseg: TrackSegmentType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TrackExtensions = {
|
||||||
|
'gpx_style:line'?: LineStyleExtension;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LineStyleExtension = {
|
||||||
|
color?: string;
|
||||||
|
opacity?: number;
|
||||||
|
weight?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TrackSegmentType = {
|
||||||
|
trkpt: TrackPointType[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TrackPointType = {
|
||||||
|
attributes: Coordinates;
|
||||||
|
ele?: number;
|
||||||
|
time?: Date;
|
||||||
|
extensions?: TrackPointExtensions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TrackPointExtensions = {
|
||||||
|
'gpxtpx:TrackPointExtension'?: TrackPointExtension;
|
||||||
|
'gpxpx:PowerExtension'?: PowerExtension;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TrackPointExtension = {
|
||||||
|
'gpxtpx:atemp'?: number;
|
||||||
|
'gpxtpx:hr'?: number;
|
||||||
|
'gpxtpx:cad'?: number;
|
||||||
|
'gpxtpx:Extensions'?: {
|
||||||
|
surface?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PowerExtension = {
|
||||||
|
'gpxpx:PowerInWatts'?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Author = {
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
link?: Link;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RouteType = {
|
||||||
|
name?: string;
|
||||||
|
cmt?: string;
|
||||||
|
desc?: string;
|
||||||
|
src?: string;
|
||||||
|
link?: Link;
|
||||||
|
type?: string;
|
||||||
|
extensions?: TrackExtensions;
|
||||||
|
rtept: WaypointType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RoutePointExtension = {
|
||||||
|
'gpxx:rpt'?: GPXXRoutePoint[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GPXXRoutePoint = {
|
||||||
|
attributes: Coordinates;
|
||||||
|
}
|
260
gpx/test-data/simple.gpx
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
<?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>simple</name>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio"></link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<trk>
|
||||||
|
<name>simple</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.790867" lon="4.404968">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790714" lon="4.405036">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790336" lon="4.405259">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790165" lon="4.405331">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790008" lon="4.405359">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789818" lon="4.405359">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789409" lon="4.40534">
|
||||||
|
<ele>107.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789105" lon="4.405411">
|
||||||
|
<ele>106.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788799" lon="4.405527">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788645" lon="4.405606">
|
||||||
|
<ele>109.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.7885" lon="4.405711">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78822" lon="4.405959">
|
||||||
|
<ele>112.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787956" lon="4.406092">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787814" lon="4.406143">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787674" lon="4.406177">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787451" lon="4.406199">
|
||||||
|
<ele>115.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787297" lon="4.406177">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78716" lon="4.406098">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787045" lon="4.405984">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786683" lon="4.405653">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786538" lon="4.405543">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78635" lon="4.405441">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786275" lon="4.40542">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786182" lon="4.405435">
|
||||||
|
<ele>116.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786121" lon="4.405475">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786042" lon="4.405558">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785821" lon="4.405925">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785672" lon="4.406119">
|
||||||
|
<ele>112.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785516" lon="4.406256">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785384" lon="4.406364">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785126" lon="4.406475">
|
||||||
|
<ele>106.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784697" lon="4.406537">
|
||||||
|
<ele>104.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784591" lon="4.40657">
|
||||||
|
<ele>104.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784507" lon="4.406612">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784435" lon="4.40669">
|
||||||
|
<ele>103.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784209" lon="4.407148">
|
||||||
|
<ele>103.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784162" lon="4.407257">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784077" lon="4.407372">
|
||||||
|
<ele>104.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784006" lon="4.407435">
|
||||||
|
<ele>105.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783924" lon="4.407471">
|
||||||
|
<ele>106.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783837" lon="4.407486">
|
||||||
|
<ele>107.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783771" lon="4.407472">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783697" lon="4.407428">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783626" lon="4.407363">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783548" lon="4.407274">
|
||||||
|
<ele>110.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783458" lon="4.407134">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783123" lon="4.406435">
|
||||||
|
<ele>111.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782982" lon="4.406168">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782871" lon="4.406044">
|
||||||
|
<ele>113.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78279" lon="4.406021">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782714" lon="4.406018">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782607" lon="4.406047">
|
||||||
|
<ele>113.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782405" lon="4.406194">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782175" lon="4.406413">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781749" lon="4.407018">
|
||||||
|
<ele>118.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781654" lon="4.407316">
|
||||||
|
<ele>119.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781563" lon="4.407764">
|
||||||
|
<ele>121.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781487" lon="4.407984">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781422" lon="4.408216">
|
||||||
|
<ele>122.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781395" lon="4.408508">
|
||||||
|
<ele>123.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781399" lon="4.409114">
|
||||||
|
<ele>126.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781367" lon="4.409428">
|
||||||
|
<ele>128.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781286" lon="4.409607">
|
||||||
|
<ele>129.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78116" lon="4.409789">
|
||||||
|
<ele>130.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780804" lon="4.409993">
|
||||||
|
<ele>130.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780389" lon="4.410334">
|
||||||
|
<ele>131.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780232" lon="4.410563">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780094" lon="4.410827">
|
||||||
|
<ele>132.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779723" lon="4.411582">
|
||||||
|
<ele>135.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779591" lon="4.411791">
|
||||||
|
<ele>135.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779125" lon="4.412435">
|
||||||
|
<ele>132.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778676" lon="4.412979">
|
||||||
|
<ele>134.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778194" lon="4.413466">
|
||||||
|
<ele>136.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777427" lon="4.414302">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777165" lon="4.414736">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776927" lon="4.415201">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776778" lon="4.415613">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776553" lon="4.416425">
|
||||||
|
<ele>134.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776326" lon="4.417304">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776129" lon="4.418383">
|
||||||
|
<ele>129.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>
|
660
gpx/test-data/with_cad.gpx
Normal file
@ -0,0 +1,660 @@
|
|||||||
|
<?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_cad</name>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio"></link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<trk>
|
||||||
|
<name>with_cad</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.790867" lon="4.404968">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790714" lon="4.405036">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790336" lon="4.405259">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790165" lon="4.405331">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790008" lon="4.405359">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789818" lon="4.405359">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789409" lon="4.40534">
|
||||||
|
<ele>107.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789105" lon="4.405411">
|
||||||
|
<ele>106.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788799" lon="4.405527">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788645" lon="4.405606">
|
||||||
|
<ele>109.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.7885" lon="4.405711">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78822" lon="4.405959">
|
||||||
|
<ele>112.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787956" lon="4.406092">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787814" lon="4.406143">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787674" lon="4.406177">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787451" lon="4.406199">
|
||||||
|
<ele>115.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787297" lon="4.406177">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78716" lon="4.406098">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787045" lon="4.405984">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786683" lon="4.405653">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786538" lon="4.405543">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78635" lon="4.405441">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786275" lon="4.40542">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786182" lon="4.405435">
|
||||||
|
<ele>116.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786121" lon="4.405475">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786042" lon="4.405558">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785821" lon="4.405925">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785672" lon="4.406119">
|
||||||
|
<ele>112.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785516" lon="4.406256">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785384" lon="4.406364">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785126" lon="4.406475">
|
||||||
|
<ele>106.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784697" lon="4.406537">
|
||||||
|
<ele>104.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784591" lon="4.40657">
|
||||||
|
<ele>104.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784507" lon="4.406612">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784435" lon="4.40669">
|
||||||
|
<ele>103.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784209" lon="4.407148">
|
||||||
|
<ele>103.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784162" lon="4.407257">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784077" lon="4.407372">
|
||||||
|
<ele>104.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784006" lon="4.407435">
|
||||||
|
<ele>105.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783924" lon="4.407471">
|
||||||
|
<ele>106.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783837" lon="4.407486">
|
||||||
|
<ele>107.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783771" lon="4.407472">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783697" lon="4.407428">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783626" lon="4.407363">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783548" lon="4.407274">
|
||||||
|
<ele>110.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783458" lon="4.407134">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783123" lon="4.406435">
|
||||||
|
<ele>111.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782982" lon="4.406168">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782871" lon="4.406044">
|
||||||
|
<ele>113.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78279" lon="4.406021">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782714" lon="4.406018">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782607" lon="4.406047">
|
||||||
|
<ele>113.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782405" lon="4.406194">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782175" lon="4.406413">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781749" lon="4.407018">
|
||||||
|
<ele>118.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781654" lon="4.407316">
|
||||||
|
<ele>119.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781563" lon="4.407764">
|
||||||
|
<ele>121.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781487" lon="4.407984">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781422" lon="4.408216">
|
||||||
|
<ele>122.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781395" lon="4.408508">
|
||||||
|
<ele>123.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781399" lon="4.409114">
|
||||||
|
<ele>126.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781367" lon="4.409428">
|
||||||
|
<ele>128.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781286" lon="4.409607">
|
||||||
|
<ele>129.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78116" lon="4.409789">
|
||||||
|
<ele>130.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780804" lon="4.409993">
|
||||||
|
<ele>130.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780389" lon="4.410334">
|
||||||
|
<ele>131.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780232" lon="4.410563">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780094" lon="4.410827">
|
||||||
|
<ele>132.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779723" lon="4.411582">
|
||||||
|
<ele>135.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779591" lon="4.411791">
|
||||||
|
<ele>135.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779125" lon="4.412435">
|
||||||
|
<ele>132.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778676" lon="4.412979">
|
||||||
|
<ele>134.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778194" lon="4.413466">
|
||||||
|
<ele>136.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777427" lon="4.414302">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777165" lon="4.414736">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776927" lon="4.415201">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776778" lon="4.415613">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776553" lon="4.416425">
|
||||||
|
<ele>134.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776326" lon="4.417304">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>80</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776129" lon="4.418383">
|
||||||
|
<ele>129.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:cad>90</gpxtpx:cad>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>
|
660
gpx/test-data/with_hr.gpx
Normal file
@ -0,0 +1,660 @@
|
|||||||
|
<?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_hr</name>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio"></link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<trk>
|
||||||
|
<name>with_hr</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.790867" lon="4.404968">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790714" lon="4.405036">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790336" lon="4.405259">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790165" lon="4.405331">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790008" lon="4.405359">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789818" lon="4.405359">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789409" lon="4.40534">
|
||||||
|
<ele>107.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789105" lon="4.405411">
|
||||||
|
<ele>106.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788799" lon="4.405527">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788645" lon="4.405606">
|
||||||
|
<ele>109.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.7885" lon="4.405711">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78822" lon="4.405959">
|
||||||
|
<ele>112.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787956" lon="4.406092">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787814" lon="4.406143">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787674" lon="4.406177">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787451" lon="4.406199">
|
||||||
|
<ele>115.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787297" lon="4.406177">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78716" lon="4.406098">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787045" lon="4.405984">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786683" lon="4.405653">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786538" lon="4.405543">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78635" lon="4.405441">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786275" lon="4.40542">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786182" lon="4.405435">
|
||||||
|
<ele>116.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786121" lon="4.405475">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786042" lon="4.405558">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785821" lon="4.405925">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785672" lon="4.406119">
|
||||||
|
<ele>112.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785516" lon="4.406256">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785384" lon="4.406364">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785126" lon="4.406475">
|
||||||
|
<ele>106.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784697" lon="4.406537">
|
||||||
|
<ele>104.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784591" lon="4.40657">
|
||||||
|
<ele>104.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784507" lon="4.406612">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784435" lon="4.40669">
|
||||||
|
<ele>103.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784209" lon="4.407148">
|
||||||
|
<ele>103.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784162" lon="4.407257">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784077" lon="4.407372">
|
||||||
|
<ele>104.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784006" lon="4.407435">
|
||||||
|
<ele>105.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783924" lon="4.407471">
|
||||||
|
<ele>106.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783837" lon="4.407486">
|
||||||
|
<ele>107.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783771" lon="4.407472">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783697" lon="4.407428">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783626" lon="4.407363">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783548" lon="4.407274">
|
||||||
|
<ele>110.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783458" lon="4.407134">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783123" lon="4.406435">
|
||||||
|
<ele>111.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782982" lon="4.406168">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782871" lon="4.406044">
|
||||||
|
<ele>113.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78279" lon="4.406021">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782714" lon="4.406018">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782607" lon="4.406047">
|
||||||
|
<ele>113.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782405" lon="4.406194">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782175" lon="4.406413">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781749" lon="4.407018">
|
||||||
|
<ele>118.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781654" lon="4.407316">
|
||||||
|
<ele>119.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781563" lon="4.407764">
|
||||||
|
<ele>121.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781487" lon="4.407984">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781422" lon="4.408216">
|
||||||
|
<ele>122.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781395" lon="4.408508">
|
||||||
|
<ele>123.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781399" lon="4.409114">
|
||||||
|
<ele>126.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781367" lon="4.409428">
|
||||||
|
<ele>128.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781286" lon="4.409607">
|
||||||
|
<ele>129.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78116" lon="4.409789">
|
||||||
|
<ele>130.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780804" lon="4.409993">
|
||||||
|
<ele>130.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780389" lon="4.410334">
|
||||||
|
<ele>131.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780232" lon="4.410563">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780094" lon="4.410827">
|
||||||
|
<ele>132.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779723" lon="4.411582">
|
||||||
|
<ele>135.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779591" lon="4.411791">
|
||||||
|
<ele>135.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779125" lon="4.412435">
|
||||||
|
<ele>132.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778676" lon="4.412979">
|
||||||
|
<ele>134.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778194" lon="4.413466">
|
||||||
|
<ele>136.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777427" lon="4.414302">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777165" lon="4.414736">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776927" lon="4.415201">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776778" lon="4.415613">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776553" lon="4.416425">
|
||||||
|
<ele>134.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776326" lon="4.417304">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>150</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776129" lon="4.418383">
|
||||||
|
<ele>129.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:hr>160</gpxtpx:hr>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>
|
500
gpx/test-data/with_power_1.gpx
Normal file
@ -0,0 +1,500 @@
|
|||||||
|
<?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_power</name>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio"></link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<trk>
|
||||||
|
<name>with_power</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.790867" lon="4.404968">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790714" lon="4.405036">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790336" lon="4.405259">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790165" lon="4.405331">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790008" lon="4.405359">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789818" lon="4.405359">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789409" lon="4.40534">
|
||||||
|
<ele>107.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789105" lon="4.405411">
|
||||||
|
<ele>106.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788799" lon="4.405527">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788645" lon="4.405606">
|
||||||
|
<ele>109.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.7885" lon="4.405711">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78822" lon="4.405959">
|
||||||
|
<ele>112.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787956" lon="4.406092">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787814" lon="4.406143">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787674" lon="4.406177">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787451" lon="4.406199">
|
||||||
|
<ele>115.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787297" lon="4.406177">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78716" lon="4.406098">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787045" lon="4.405984">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786683" lon="4.405653">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786538" lon="4.405543">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78635" lon="4.405441">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786275" lon="4.40542">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786182" lon="4.405435">
|
||||||
|
<ele>116.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786121" lon="4.405475">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786042" lon="4.405558">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785821" lon="4.405925">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785672" lon="4.406119">
|
||||||
|
<ele>112.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785516" lon="4.406256">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785384" lon="4.406364">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785126" lon="4.406475">
|
||||||
|
<ele>106.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784697" lon="4.406537">
|
||||||
|
<ele>104.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784591" lon="4.40657">
|
||||||
|
<ele>104.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784507" lon="4.406612">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784435" lon="4.40669">
|
||||||
|
<ele>103.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784209" lon="4.407148">
|
||||||
|
<ele>103.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784162" lon="4.407257">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784077" lon="4.407372">
|
||||||
|
<ele>104.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784006" lon="4.407435">
|
||||||
|
<ele>105.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783924" lon="4.407471">
|
||||||
|
<ele>106.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783837" lon="4.407486">
|
||||||
|
<ele>107.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783771" lon="4.407472">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783697" lon="4.407428">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783626" lon="4.407363">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783548" lon="4.407274">
|
||||||
|
<ele>110.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783458" lon="4.407134">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783123" lon="4.406435">
|
||||||
|
<ele>111.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782982" lon="4.406168">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782871" lon="4.406044">
|
||||||
|
<ele>113.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78279" lon="4.406021">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782714" lon="4.406018">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782607" lon="4.406047">
|
||||||
|
<ele>113.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782405" lon="4.406194">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782175" lon="4.406413">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781749" lon="4.407018">
|
||||||
|
<ele>118.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781654" lon="4.407316">
|
||||||
|
<ele>119.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781563" lon="4.407764">
|
||||||
|
<ele>121.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781487" lon="4.407984">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781422" lon="4.408216">
|
||||||
|
<ele>122.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781395" lon="4.408508">
|
||||||
|
<ele>123.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781399" lon="4.409114">
|
||||||
|
<ele>126.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781367" lon="4.409428">
|
||||||
|
<ele>128.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781286" lon="4.409607">
|
||||||
|
<ele>129.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78116" lon="4.409789">
|
||||||
|
<ele>130.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780804" lon="4.409993">
|
||||||
|
<ele>130.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780389" lon="4.410334">
|
||||||
|
<ele>131.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780232" lon="4.410563">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780094" lon="4.410827">
|
||||||
|
<ele>132.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779723" lon="4.411582">
|
||||||
|
<ele>135.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779591" lon="4.411791">
|
||||||
|
<ele>135.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779125" lon="4.412435">
|
||||||
|
<ele>132.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778676" lon="4.412979">
|
||||||
|
<ele>134.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778194" lon="4.413466">
|
||||||
|
<ele>136.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777427" lon="4.414302">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777165" lon="4.414736">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776927" lon="4.415201">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776778" lon="4.415613">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776553" lon="4.416425">
|
||||||
|
<ele>134.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776326" lon="4.417304">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>200</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776129" lon="4.418383">
|
||||||
|
<ele>129.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<power>210</power>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>
|
660
gpx/test-data/with_power_2.gpx
Normal file
@ -0,0 +1,660 @@
|
|||||||
|
<?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_power</name>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio"></link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<trk>
|
||||||
|
<name>with_power</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.790867" lon="4.404968">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790714" lon="4.405036">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790336" lon="4.405259">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790165" lon="4.405331">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790008" lon="4.405359">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789818" lon="4.405359">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789409" lon="4.40534">
|
||||||
|
<ele>107.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789105" lon="4.405411">
|
||||||
|
<ele>106.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788799" lon="4.405527">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788645" lon="4.405606">
|
||||||
|
<ele>109.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.7885" lon="4.405711">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78822" lon="4.405959">
|
||||||
|
<ele>112.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787956" lon="4.406092">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787814" lon="4.406143">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787674" lon="4.406177">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787451" lon="4.406199">
|
||||||
|
<ele>115.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787297" lon="4.406177">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78716" lon="4.406098">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787045" lon="4.405984">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786683" lon="4.405653">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786538" lon="4.405543">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78635" lon="4.405441">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786275" lon="4.40542">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786182" lon="4.405435">
|
||||||
|
<ele>116.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786121" lon="4.405475">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786042" lon="4.405558">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785821" lon="4.405925">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785672" lon="4.406119">
|
||||||
|
<ele>112.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785516" lon="4.406256">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785384" lon="4.406364">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785126" lon="4.406475">
|
||||||
|
<ele>106.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784697" lon="4.406537">
|
||||||
|
<ele>104.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784591" lon="4.40657">
|
||||||
|
<ele>104.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784507" lon="4.406612">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784435" lon="4.40669">
|
||||||
|
<ele>103.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784209" lon="4.407148">
|
||||||
|
<ele>103.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784162" lon="4.407257">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784077" lon="4.407372">
|
||||||
|
<ele>104.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784006" lon="4.407435">
|
||||||
|
<ele>105.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783924" lon="4.407471">
|
||||||
|
<ele>106.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783837" lon="4.407486">
|
||||||
|
<ele>107.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783771" lon="4.407472">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783697" lon="4.407428">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783626" lon="4.407363">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783548" lon="4.407274">
|
||||||
|
<ele>110.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783458" lon="4.407134">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783123" lon="4.406435">
|
||||||
|
<ele>111.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782982" lon="4.406168">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782871" lon="4.406044">
|
||||||
|
<ele>113.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78279" lon="4.406021">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782714" lon="4.406018">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782607" lon="4.406047">
|
||||||
|
<ele>113.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782405" lon="4.406194">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782175" lon="4.406413">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781749" lon="4.407018">
|
||||||
|
<ele>118.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781654" lon="4.407316">
|
||||||
|
<ele>119.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781563" lon="4.407764">
|
||||||
|
<ele>121.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781487" lon="4.407984">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781422" lon="4.408216">
|
||||||
|
<ele>122.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781395" lon="4.408508">
|
||||||
|
<ele>123.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781399" lon="4.409114">
|
||||||
|
<ele>126.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781367" lon="4.409428">
|
||||||
|
<ele>128.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781286" lon="4.409607">
|
||||||
|
<ele>129.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78116" lon="4.409789">
|
||||||
|
<ele>130.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780804" lon="4.409993">
|
||||||
|
<ele>130.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780389" lon="4.410334">
|
||||||
|
<ele>131.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780232" lon="4.410563">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780094" lon="4.410827">
|
||||||
|
<ele>132.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779723" lon="4.411582">
|
||||||
|
<ele>135.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779591" lon="4.411791">
|
||||||
|
<ele>135.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779125" lon="4.412435">
|
||||||
|
<ele>132.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778676" lon="4.412979">
|
||||||
|
<ele>134.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778194" lon="4.413466">
|
||||||
|
<ele>136.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777427" lon="4.414302">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777165" lon="4.414736">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776927" lon="4.415201">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776778" lon="4.415613">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776553" lon="4.416425">
|
||||||
|
<ele>134.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776326" lon="4.417304">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776129" lon="4.418383">
|
||||||
|
<ele>129.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxpx:PowerExtension>
|
||||||
|
<gpxpx:PowerInWatts>210</gpxpx:PowerInWatts>
|
||||||
|
</gpxpx:PowerExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>
|
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>
|
253
gpx/test-data/with_segments.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_segments</name>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio"></link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<trk>
|
||||||
|
<name>with_segments</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.790867" lon="4.404968">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790714" lon="4.405036">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790336" lon="4.405259">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790165" lon="4.405331">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790008" lon="4.405359">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789818" lon="4.405359">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789409" lon="4.40534">
|
||||||
|
<ele>107.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789105" lon="4.405411">
|
||||||
|
<ele>106.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788799" lon="4.405527">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788645" lon="4.405606">
|
||||||
|
<ele>109.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.7885" lon="4.405711">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78822" lon="4.405959">
|
||||||
|
<ele>112.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787956" lon="4.406092">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787814" lon="4.406143">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787674" lon="4.406177">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787451" lon="4.406199">
|
||||||
|
<ele>115.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787297" lon="4.406177">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78716" lon="4.406098">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787045" lon="4.405984">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786683" lon="4.405653">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786538" lon="4.405543">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78635" lon="4.405441">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786275" lon="4.40542">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786182" lon="4.405435">
|
||||||
|
<ele>116.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786121" lon="4.405475">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786042" lon="4.405558">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785821" lon="4.405925">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785672" lon="4.406119">
|
||||||
|
<ele>112.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785516" lon="4.406256">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785384" lon="4.406364">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785126" lon="4.406475">
|
||||||
|
<ele>106.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784697" lon="4.406537">
|
||||||
|
<ele>104.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784591" lon="4.40657">
|
||||||
|
<ele>104.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784507" lon="4.406612">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784435" lon="4.40669">
|
||||||
|
<ele>103.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784209" lon="4.407148">
|
||||||
|
<ele>103.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784162" lon="4.407257">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784077" lon="4.407372">
|
||||||
|
<ele>104.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784006" lon="4.407435">
|
||||||
|
<ele>105.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783924" lon="4.407471">
|
||||||
|
<ele>106.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783837" lon="4.407486">
|
||||||
|
<ele>107.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783771" lon="4.407472">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783697" lon="4.407428">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783626" lon="4.407363">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783548" lon="4.407274">
|
||||||
|
<ele>110.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783458" lon="4.407134">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783123" lon="4.406435">
|
||||||
|
<ele>111.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782982" lon="4.406168">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782871" lon="4.406044">
|
||||||
|
<ele>113.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.782212" lon="4.406377">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782175" lon="4.406413">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781749" lon="4.407018">
|
||||||
|
<ele>118.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781654" lon="4.407316">
|
||||||
|
<ele>119.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781563" lon="4.407764">
|
||||||
|
<ele>121.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781487" lon="4.407984">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781422" lon="4.408216">
|
||||||
|
<ele>122.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781395" lon="4.408508">
|
||||||
|
<ele>123.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781399" lon="4.409114">
|
||||||
|
<ele>126.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781367" lon="4.409428">
|
||||||
|
<ele>128.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781286" lon="4.409607">
|
||||||
|
<ele>129.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78116" lon="4.409789">
|
||||||
|
<ele>130.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780804" lon="4.409993">
|
||||||
|
<ele>130.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780389" lon="4.410334">
|
||||||
|
<ele>131.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780232" lon="4.410563">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780094" lon="4.410827">
|
||||||
|
<ele>132.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779723" lon="4.411582">
|
||||||
|
<ele>135.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779591" lon="4.411791">
|
||||||
|
<ele>135.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779125" lon="4.412435">
|
||||||
|
<ele>132.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778676" lon="4.412979">
|
||||||
|
<ele>134.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778194" lon="4.413466">
|
||||||
|
<ele>136.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777427" lon="4.414302">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777165" lon="4.414736">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776927" lon="4.415201">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776778" lon="4.415613">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776553" lon="4.416425">
|
||||||
|
<ele>134.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776326" lon="4.417304">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776129" lon="4.418383">
|
||||||
|
<ele>129.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>
|
267
gpx/test-data/with_style.gpx
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
<?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_style</name>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio"></link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<trk>
|
||||||
|
<name>with_style</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<extensions>
|
||||||
|
<gpx_style:line>
|
||||||
|
<color>#2d3ee9</color>
|
||||||
|
<opacity>0.5</opacity>
|
||||||
|
<weight>6</weight>
|
||||||
|
</gpx_style:line>
|
||||||
|
</extensions>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.790867" lon="4.404968">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790714" lon="4.405036">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790336" lon="4.405259">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790165" lon="4.405331">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790008" lon="4.405359">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789818" lon="4.405359">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789409" lon="4.40534">
|
||||||
|
<ele>107.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789105" lon="4.405411">
|
||||||
|
<ele>106.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788799" lon="4.405527">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788645" lon="4.405606">
|
||||||
|
<ele>109.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.7885" lon="4.405711">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78822" lon="4.405959">
|
||||||
|
<ele>112.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787956" lon="4.406092">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787814" lon="4.406143">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787674" lon="4.406177">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787451" lon="4.406199">
|
||||||
|
<ele>115.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787297" lon="4.406177">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78716" lon="4.406098">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787045" lon="4.405984">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786683" lon="4.405653">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786538" lon="4.405543">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78635" lon="4.405441">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786275" lon="4.40542">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786182" lon="4.405435">
|
||||||
|
<ele>116.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786121" lon="4.405475">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786042" lon="4.405558">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785821" lon="4.405925">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785672" lon="4.406119">
|
||||||
|
<ele>112.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785516" lon="4.406256">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785384" lon="4.406364">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785126" lon="4.406475">
|
||||||
|
<ele>106.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784697" lon="4.406537">
|
||||||
|
<ele>104.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784591" lon="4.40657">
|
||||||
|
<ele>104.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784507" lon="4.406612">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784435" lon="4.40669">
|
||||||
|
<ele>103.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784209" lon="4.407148">
|
||||||
|
<ele>103.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784162" lon="4.407257">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784077" lon="4.407372">
|
||||||
|
<ele>104.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784006" lon="4.407435">
|
||||||
|
<ele>105.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783924" lon="4.407471">
|
||||||
|
<ele>106.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783837" lon="4.407486">
|
||||||
|
<ele>107.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783771" lon="4.407472">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783697" lon="4.407428">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783626" lon="4.407363">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783548" lon="4.407274">
|
||||||
|
<ele>110.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783458" lon="4.407134">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783123" lon="4.406435">
|
||||||
|
<ele>111.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782982" lon="4.406168">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782871" lon="4.406044">
|
||||||
|
<ele>113.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78279" lon="4.406021">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782714" lon="4.406018">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782607" lon="4.406047">
|
||||||
|
<ele>113.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782405" lon="4.406194">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782175" lon="4.406413">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781749" lon="4.407018">
|
||||||
|
<ele>118.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781654" lon="4.407316">
|
||||||
|
<ele>119.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781563" lon="4.407764">
|
||||||
|
<ele>121.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781487" lon="4.407984">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781422" lon="4.408216">
|
||||||
|
<ele>122.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781395" lon="4.408508">
|
||||||
|
<ele>123.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781399" lon="4.409114">
|
||||||
|
<ele>126.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781367" lon="4.409428">
|
||||||
|
<ele>128.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781286" lon="4.409607">
|
||||||
|
<ele>129.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78116" lon="4.409789">
|
||||||
|
<ele>130.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780804" lon="4.409993">
|
||||||
|
<ele>130.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780389" lon="4.410334">
|
||||||
|
<ele>131.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780232" lon="4.410563">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780094" lon="4.410827">
|
||||||
|
<ele>132.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779723" lon="4.411582">
|
||||||
|
<ele>135.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779591" lon="4.411791">
|
||||||
|
<ele>135.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779125" lon="4.412435">
|
||||||
|
<ele>132.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778676" lon="4.412979">
|
||||||
|
<ele>134.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778194" lon="4.413466">
|
||||||
|
<ele>136.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777427" lon="4.414302">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777165" lon="4.414736">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776927" lon="4.415201">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776778" lon="4.415613">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776553" lon="4.416425">
|
||||||
|
<ele>134.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776326" lon="4.417304">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776129" lon="4.418383">
|
||||||
|
<ele>129.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>
|
820
gpx/test-data/with_surface.gpx
Normal file
@ -0,0 +1,820 @@
|
|||||||
|
<?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_surface</name>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio"></link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<trk>
|
||||||
|
<name>with_surface</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.790867" lon="4.404968">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790714" lon="4.405036">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790336" lon="4.405259">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790165" lon="4.405331">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790008" lon="4.405359">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789818" lon="4.405359">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789409" lon="4.40534">
|
||||||
|
<ele>107.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789105" lon="4.405411">
|
||||||
|
<ele>106.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788799" lon="4.405527">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788645" lon="4.405606">
|
||||||
|
<ele>109.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.7885" lon="4.405711">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78822" lon="4.405959">
|
||||||
|
<ele>112.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787956" lon="4.406092">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787814" lon="4.406143">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787674" lon="4.406177">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787451" lon="4.406199">
|
||||||
|
<ele>115.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787297" lon="4.406177">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78716" lon="4.406098">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787045" lon="4.405984">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786683" lon="4.405653">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786538" lon="4.405543">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78635" lon="4.405441">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786275" lon="4.40542">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786182" lon="4.405435">
|
||||||
|
<ele>116.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786121" lon="4.405475">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786042" lon="4.405558">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785821" lon="4.405925">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785672" lon="4.406119">
|
||||||
|
<ele>112.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785516" lon="4.406256">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785384" lon="4.406364">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785126" lon="4.406475">
|
||||||
|
<ele>106.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784697" lon="4.406537">
|
||||||
|
<ele>104.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784591" lon="4.40657">
|
||||||
|
<ele>104.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784507" lon="4.406612">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784435" lon="4.40669">
|
||||||
|
<ele>103.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784209" lon="4.407148">
|
||||||
|
<ele>103.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784162" lon="4.407257">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784077" lon="4.407372">
|
||||||
|
<ele>104.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784006" lon="4.407435">
|
||||||
|
<ele>105.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783924" lon="4.407471">
|
||||||
|
<ele>106.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783837" lon="4.407486">
|
||||||
|
<ele>107.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783771" lon="4.407472">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783697" lon="4.407428">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783626" lon="4.407363">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783548" lon="4.407274">
|
||||||
|
<ele>110.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783458" lon="4.407134">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783123" lon="4.406435">
|
||||||
|
<ele>111.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782982" lon="4.406168">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782871" lon="4.406044">
|
||||||
|
<ele>113.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78279" lon="4.406021">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782714" lon="4.406018">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782607" lon="4.406047">
|
||||||
|
<ele>113.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782405" lon="4.406194">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782175" lon="4.406413">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781749" lon="4.407018">
|
||||||
|
<ele>118.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781654" lon="4.407316">
|
||||||
|
<ele>119.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781563" lon="4.407764">
|
||||||
|
<ele>121.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781487" lon="4.407984">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781422" lon="4.408216">
|
||||||
|
<ele>122.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781395" lon="4.408508">
|
||||||
|
<ele>123.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781399" lon="4.409114">
|
||||||
|
<ele>126.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781367" lon="4.409428">
|
||||||
|
<ele>128.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781286" lon="4.409607">
|
||||||
|
<ele>129.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78116" lon="4.409789">
|
||||||
|
<ele>130.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780804" lon="4.409993">
|
||||||
|
<ele>130.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780389" lon="4.410334">
|
||||||
|
<ele>131.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780232" lon="4.410563">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780094" lon="4.410827">
|
||||||
|
<ele>132.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779723" lon="4.411582">
|
||||||
|
<ele>135.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779591" lon="4.411791">
|
||||||
|
<ele>135.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779125" lon="4.412435">
|
||||||
|
<ele>132.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778676" lon="4.412979">
|
||||||
|
<ele>134.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778194" lon="4.413466">
|
||||||
|
<ele>136.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777427" lon="4.414302">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777165" lon="4.414736">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776927" lon="4.415201">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776778" lon="4.415613">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776553" lon="4.416425">
|
||||||
|
<ele>134.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776326" lon="4.417304">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>asphalt</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776129" lon="4.418383">
|
||||||
|
<ele>129.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:Extensions>
|
||||||
|
<surface>cobblestone</surface>
|
||||||
|
</gpxtpx:Extensions>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>
|
660
gpx/test-data/with_temp.gpx
Normal file
@ -0,0 +1,660 @@
|
|||||||
|
<?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_temp</name>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio"></link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<trk>
|
||||||
|
<name>with_temp</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.790867" lon="4.404968">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790714" lon="4.405036">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790336" lon="4.405259">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790165" lon="4.405331">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790008" lon="4.405359">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789818" lon="4.405359">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789409" lon="4.40534">
|
||||||
|
<ele>107.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789105" lon="4.405411">
|
||||||
|
<ele>106.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788799" lon="4.405527">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788645" lon="4.405606">
|
||||||
|
<ele>109.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.7885" lon="4.405711">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78822" lon="4.405959">
|
||||||
|
<ele>112.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787956" lon="4.406092">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787814" lon="4.406143">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787674" lon="4.406177">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787451" lon="4.406199">
|
||||||
|
<ele>115.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787297" lon="4.406177">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78716" lon="4.406098">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787045" lon="4.405984">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786683" lon="4.405653">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786538" lon="4.405543">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78635" lon="4.405441">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786275" lon="4.40542">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786182" lon="4.405435">
|
||||||
|
<ele>116.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786121" lon="4.405475">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786042" lon="4.405558">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785821" lon="4.405925">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785672" lon="4.406119">
|
||||||
|
<ele>112.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785516" lon="4.406256">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785384" lon="4.406364">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785126" lon="4.406475">
|
||||||
|
<ele>106.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784697" lon="4.406537">
|
||||||
|
<ele>104.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784591" lon="4.40657">
|
||||||
|
<ele>104.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784507" lon="4.406612">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784435" lon="4.40669">
|
||||||
|
<ele>103.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784209" lon="4.407148">
|
||||||
|
<ele>103.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784162" lon="4.407257">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784077" lon="4.407372">
|
||||||
|
<ele>104.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784006" lon="4.407435">
|
||||||
|
<ele>105.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783924" lon="4.407471">
|
||||||
|
<ele>106.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783837" lon="4.407486">
|
||||||
|
<ele>107.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783771" lon="4.407472">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783697" lon="4.407428">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783626" lon="4.407363">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783548" lon="4.407274">
|
||||||
|
<ele>110.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783458" lon="4.407134">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783123" lon="4.406435">
|
||||||
|
<ele>111.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782982" lon="4.406168">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782871" lon="4.406044">
|
||||||
|
<ele>113.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78279" lon="4.406021">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782714" lon="4.406018">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782607" lon="4.406047">
|
||||||
|
<ele>113.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782405" lon="4.406194">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782175" lon="4.406413">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781749" lon="4.407018">
|
||||||
|
<ele>118.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781654" lon="4.407316">
|
||||||
|
<ele>119.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781563" lon="4.407764">
|
||||||
|
<ele>121.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781487" lon="4.407984">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781422" lon="4.408216">
|
||||||
|
<ele>122.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781395" lon="4.408508">
|
||||||
|
<ele>123.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781399" lon="4.409114">
|
||||||
|
<ele>126.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781367" lon="4.409428">
|
||||||
|
<ele>128.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781286" lon="4.409607">
|
||||||
|
<ele>129.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78116" lon="4.409789">
|
||||||
|
<ele>130.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780804" lon="4.409993">
|
||||||
|
<ele>130.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780389" lon="4.410334">
|
||||||
|
<ele>131.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780232" lon="4.410563">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780094" lon="4.410827">
|
||||||
|
<ele>132.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779723" lon="4.411582">
|
||||||
|
<ele>135.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779591" lon="4.411791">
|
||||||
|
<ele>135.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779125" lon="4.412435">
|
||||||
|
<ele>132.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778676" lon="4.412979">
|
||||||
|
<ele>134.0</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778194" lon="4.413466">
|
||||||
|
<ele>136.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777427" lon="4.414302">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777165" lon="4.414736">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776927" lon="4.415201">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776778" lon="4.415613">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776553" lon="4.416425">
|
||||||
|
<ele>134.8</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776326" lon="4.417304">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>21</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776129" lon="4.418383">
|
||||||
|
<ele>129.5</ele>
|
||||||
|
<extensions>
|
||||||
|
<gpxtpx:TrackPointExtension>
|
||||||
|
<gpxtpx:atemp>22</gpxtpx:atemp>
|
||||||
|
</gpxtpx:TrackPointExtension>
|
||||||
|
</extensions>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>
|
340
gpx/test-data/with_time.gpx
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
<?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_time</name>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio"></link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<trk>
|
||||||
|
<name>with_time</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.790867" lon="4.404968">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<time>2023-12-31T23:00:00.000Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790714" lon="4.405036">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<time>2023-12-31T23:00:03.180Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790336" lon="4.405259">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<time>2023-12-31T23:00:11.254Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790165" lon="4.405331">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<time>2023-12-31T23:00:14.795Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790008" lon="4.405359">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<time>2023-12-31T23:00:17.957Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789818" lon="4.405359">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<time>2023-12-31T23:00:21.759Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789409" lon="4.40534">
|
||||||
|
<ele>107.0</ele>
|
||||||
|
<time>2023-12-31T23:00:29.948Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789105" lon="4.405411">
|
||||||
|
<ele>106.0</ele>
|
||||||
|
<time>2023-12-31T23:00:36.098Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788799" lon="4.405527">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<time>2023-12-31T23:00:42.396Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788645" lon="4.405606">
|
||||||
|
<ele>109.8</ele>
|
||||||
|
<time>2023-12-31T23:00:45.636Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.7885" lon="4.405711">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<time>2023-12-31T23:00:48.827Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78822" lon="4.405959">
|
||||||
|
<ele>112.0</ele>
|
||||||
|
<time>2023-12-31T23:00:55.249Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787956" lon="4.406092">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<time>2023-12-31T23:01:00.794Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787814" lon="4.406143">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<time>2023-12-31T23:01:03.708Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787674" lon="4.406177">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<time>2023-12-31T23:01:06.542Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787451" lon="4.406199">
|
||||||
|
<ele>115.3</ele>
|
||||||
|
<time>2023-12-31T23:01:11.014Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787297" lon="4.406177">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<time>2023-12-31T23:01:14.108Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78716" lon="4.406098">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<time>2023-12-31T23:01:17.026Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787045" lon="4.405984">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<time>2023-12-31T23:01:19.742Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786683" lon="4.405653">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<time>2023-12-31T23:01:28.110Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786538" lon="4.405543">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
<time>2023-12-31T23:01:31.328Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78635" lon="4.405441">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<time>2023-12-31T23:01:35.306Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786275" lon="4.40542">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<time>2023-12-31T23:01:36.830Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786182" lon="4.405435">
|
||||||
|
<ele>116.0</ele>
|
||||||
|
<time>2023-12-31T23:01:38.701Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786121" lon="4.405475">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<time>2023-12-31T23:01:40.022Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786042" lon="4.405558">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
<time>2023-12-31T23:01:41.920Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785821" lon="4.405925">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<time>2023-12-31T23:01:48.333Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785672" lon="4.406119">
|
||||||
|
<ele>112.5</ele>
|
||||||
|
<time>2023-12-31T23:01:52.195Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785516" lon="4.406256">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<time>2023-12-31T23:01:55.766Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785384" lon="4.406364">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<time>2023-12-31T23:01:58.740Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785126" lon="4.406475">
|
||||||
|
<ele>106.3</ele>
|
||||||
|
<time>2023-12-31T23:02:04.091Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784697" lon="4.406537">
|
||||||
|
<ele>104.3</ele>
|
||||||
|
<time>2023-12-31T23:02:12.713Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784591" lon="4.40657">
|
||||||
|
<ele>104.0</ele>
|
||||||
|
<time>2023-12-31T23:02:14.875Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784507" lon="4.406612">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<time>2023-12-31T23:02:16.638Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784435" lon="4.40669">
|
||||||
|
<ele>103.3</ele>
|
||||||
|
<time>2023-12-31T23:02:18.384Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784209" lon="4.407148">
|
||||||
|
<ele>103.5</ele>
|
||||||
|
<time>2023-12-31T23:02:25.735Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784162" lon="4.407257">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<time>2023-12-31T23:02:27.404Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784077" lon="4.407372">
|
||||||
|
<ele>104.8</ele>
|
||||||
|
<time>2023-12-31T23:02:29.642Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784006" lon="4.407435">
|
||||||
|
<ele>105.8</ele>
|
||||||
|
<time>2023-12-31T23:02:31.271Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783924" lon="4.407471">
|
||||||
|
<ele>106.8</ele>
|
||||||
|
<time>2023-12-31T23:02:32.974Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783837" lon="4.407486">
|
||||||
|
<ele>107.8</ele>
|
||||||
|
<time>2023-12-31T23:02:34.725Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783771" lon="4.407472">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<time>2023-12-31T23:02:36.057Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783697" lon="4.407428">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<time>2023-12-31T23:02:37.639Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783626" lon="4.407363">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<time>2023-12-31T23:02:39.280Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783548" lon="4.407274">
|
||||||
|
<ele>110.5</ele>
|
||||||
|
<time>2023-12-31T23:02:41.205Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783458" lon="4.407134">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<time>2023-12-31T23:02:43.731Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783123" lon="4.406435">
|
||||||
|
<ele>111.8</ele>
|
||||||
|
<time>2023-12-31T23:02:54.830Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782982" lon="4.406168">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<time>2023-12-31T23:02:59.232Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782871" lon="4.406044">
|
||||||
|
<ele>113.3</ele>
|
||||||
|
<time>2023-12-31T23:03:01.951Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78279" lon="4.406021">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<time>2023-12-31T23:03:03.598Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782714" lon="4.406018">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<time>2023-12-31T23:03:05.119Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782607" lon="4.406047">
|
||||||
|
<ele>113.8</ele>
|
||||||
|
<time>2023-12-31T23:03:07.291Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782405" lon="4.406194">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<time>2023-12-31T23:03:11.741Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782175" lon="4.406413">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<time>2023-12-31T23:03:17.114Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781749" lon="4.407018">
|
||||||
|
<ele>118.5</ele>
|
||||||
|
<time>2023-12-31T23:03:28.573Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781654" lon="4.407316">
|
||||||
|
<ele>119.5</ele>
|
||||||
|
<time>2023-12-31T23:03:32.796Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781563" lon="4.407764">
|
||||||
|
<ele>121.3</ele>
|
||||||
|
<time>2023-12-31T23:03:38.750Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781487" lon="4.407984">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
<time>2023-12-31T23:03:41.922Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781422" lon="4.408216">
|
||||||
|
<ele>122.8</ele>
|
||||||
|
<time>2023-12-31T23:03:45.133Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781395" lon="4.408508">
|
||||||
|
<ele>123.5</ele>
|
||||||
|
<time>2023-12-31T23:03:48.867Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781399" lon="4.409114">
|
||||||
|
<ele>126.3</ele>
|
||||||
|
<time>2023-12-31T23:03:56.536Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781367" lon="4.409428">
|
||||||
|
<ele>128.0</ele>
|
||||||
|
<time>2023-12-31T23:04:00.561Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781286" lon="4.409607">
|
||||||
|
<ele>129.0</ele>
|
||||||
|
<time>2023-12-31T23:04:03.346Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78116" lon="4.409789">
|
||||||
|
<ele>130.0</ele>
|
||||||
|
<time>2023-12-31T23:04:06.761Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780804" lon="4.409993">
|
||||||
|
<ele>130.8</ele>
|
||||||
|
<time>2023-12-31T23:04:14.339Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780389" lon="4.410334">
|
||||||
|
<ele>131.8</ele>
|
||||||
|
<time>2023-12-31T23:04:23.699Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780232" lon="4.410563">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<time>2023-12-31T23:04:27.973Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780094" lon="4.410827">
|
||||||
|
<ele>132.8</ele>
|
||||||
|
<time>2023-12-31T23:04:32.307Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779723" lon="4.411582">
|
||||||
|
<ele>135.8</ele>
|
||||||
|
<time>2023-12-31T23:04:44.408Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779591" lon="4.411791">
|
||||||
|
<ele>135.5</ele>
|
||||||
|
<time>2023-12-31T23:04:48.146Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779125" lon="4.412435">
|
||||||
|
<ele>132.5</ele>
|
||||||
|
<time>2023-12-31T23:05:00.532Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778676" lon="4.412979">
|
||||||
|
<ele>134.0</ele>
|
||||||
|
<time>2023-12-31T23:05:11.852Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778194" lon="4.413466">
|
||||||
|
<ele>136.8</ele>
|
||||||
|
<time>2023-12-31T23:05:23.300Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777427" lon="4.414302">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<time>2023-12-31T23:05:41.944Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777165" lon="4.414736">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<time>2023-12-31T23:05:49.538Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776927" lon="4.415201">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<time>2023-12-31T23:05:57.109Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776778" lon="4.415613">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<time>2023-12-31T23:06:03.116Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776553" lon="4.416425">
|
||||||
|
<ele>134.8</ele>
|
||||||
|
<time>2023-12-31T23:06:14.336Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776326" lon="4.417304">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<time>2023-12-31T23:06:26.353Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776129" lon="4.418383">
|
||||||
|
<ele>129.5</ele>
|
||||||
|
<time>2023-12-31T23:06:40.567Z</time>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>
|
257
gpx/test-data/with_tracks.gpx
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
<?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_tracks</name>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio"></link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<trk>
|
||||||
|
<name>track 1</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.790867" lon="4.404968">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790714" lon="4.405036">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790336" lon="4.405259">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790165" lon="4.405331">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790008" lon="4.405359">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789818" lon="4.405359">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789409" lon="4.40534">
|
||||||
|
<ele>107.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789105" lon="4.405411">
|
||||||
|
<ele>106.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788799" lon="4.405527">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788645" lon="4.405606">
|
||||||
|
<ele>109.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.7885" lon="4.405711">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78822" lon="4.405959">
|
||||||
|
<ele>112.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787956" lon="4.406092">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787814" lon="4.406143">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787674" lon="4.406177">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787451" lon="4.406199">
|
||||||
|
<ele>115.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787297" lon="4.406177">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78716" lon="4.406098">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787045" lon="4.405984">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786683" lon="4.405653">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786538" lon="4.405543">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78635" lon="4.405441">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786275" lon="4.40542">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786182" lon="4.405435">
|
||||||
|
<ele>116.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786121" lon="4.405475">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786042" lon="4.405558">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785821" lon="4.405925">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785672" lon="4.406119">
|
||||||
|
<ele>112.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785516" lon="4.406256">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785384" lon="4.406364">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785126" lon="4.406475">
|
||||||
|
<ele>106.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784697" lon="4.406537">
|
||||||
|
<ele>104.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784591" lon="4.40657">
|
||||||
|
<ele>104.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784507" lon="4.406612">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784435" lon="4.40669">
|
||||||
|
<ele>103.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784209" lon="4.407148">
|
||||||
|
<ele>103.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784162" lon="4.407257">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784077" lon="4.407372">
|
||||||
|
<ele>104.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784006" lon="4.407435">
|
||||||
|
<ele>105.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783924" lon="4.407471">
|
||||||
|
<ele>106.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783837" lon="4.407486">
|
||||||
|
<ele>107.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783771" lon="4.407472">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783697" lon="4.407428">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783626" lon="4.407363">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783548" lon="4.407274">
|
||||||
|
<ele>110.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783458" lon="4.407134">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783123" lon="4.406435">
|
||||||
|
<ele>111.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782982" lon="4.406168">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782871" lon="4.406044">
|
||||||
|
<ele>113.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
<trk>
|
||||||
|
<name>track 2</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.782212" lon="4.406377">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782175" lon="4.406413">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781749" lon="4.407018">
|
||||||
|
<ele>118.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781654" lon="4.407316">
|
||||||
|
<ele>119.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781563" lon="4.407764">
|
||||||
|
<ele>121.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781487" lon="4.407984">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781422" lon="4.408216">
|
||||||
|
<ele>122.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781395" lon="4.408508">
|
||||||
|
<ele>123.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781399" lon="4.409114">
|
||||||
|
<ele>126.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781367" lon="4.409428">
|
||||||
|
<ele>128.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781286" lon="4.409607">
|
||||||
|
<ele>129.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78116" lon="4.409789">
|
||||||
|
<ele>130.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780804" lon="4.409993">
|
||||||
|
<ele>130.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780389" lon="4.410334">
|
||||||
|
<ele>131.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780232" lon="4.410563">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780094" lon="4.410827">
|
||||||
|
<ele>132.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779723" lon="4.411582">
|
||||||
|
<ele>135.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779591" lon="4.411791">
|
||||||
|
<ele>135.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779125" lon="4.412435">
|
||||||
|
<ele>132.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778676" lon="4.412979">
|
||||||
|
<ele>134.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778194" lon="4.413466">
|
||||||
|
<ele>136.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777427" lon="4.414302">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777165" lon="4.414736">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776927" lon="4.415201">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776778" lon="4.415613">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776553" lon="4.416425">
|
||||||
|
<ele>134.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776326" lon="4.417304">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776129" lon="4.418383">
|
||||||
|
<ele>129.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>
|
346
gpx/test-data/with_tracks_and_segments.gpx
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
<?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_tracks_and_segments</name>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio"></link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<trk>
|
||||||
|
<name>track 1</name>
|
||||||
|
<type>Running</type>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.790867" lon="4.404968">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<time>2023-12-31T23:00:00.000Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790714" lon="4.405036">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<time>2023-12-31T23:00:03.180Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790336" lon="4.405259">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<time>2023-12-31T23:00:11.254Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790165" lon="4.405331">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<time>2023-12-31T23:00:14.795Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790008" lon="4.405359">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
<time>2023-12-31T23:00:17.957Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789818" lon="4.405359">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<time>2023-12-31T23:00:21.759Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789409" lon="4.40534">
|
||||||
|
<ele>107.0</ele>
|
||||||
|
<time>2023-12-31T23:00:29.948Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789105" lon="4.405411">
|
||||||
|
<ele>106.0</ele>
|
||||||
|
<time>2023-12-31T23:00:36.098Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788799" lon="4.405527">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<time>2023-12-31T23:00:42.396Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788645" lon="4.405606">
|
||||||
|
<ele>109.8</ele>
|
||||||
|
<time>2023-12-31T23:00:45.636Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.7885" lon="4.405711">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<time>2023-12-31T23:00:48.827Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78822" lon="4.405959">
|
||||||
|
<ele>112.0</ele>
|
||||||
|
<time>2023-12-31T23:00:55.249Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787956" lon="4.406092">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<time>2023-12-31T23:01:00.794Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787814" lon="4.406143">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
<time>2023-12-31T23:01:03.708Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787674" lon="4.406177">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<time>2023-12-31T23:01:06.542Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787451" lon="4.406199">
|
||||||
|
<ele>115.3</ele>
|
||||||
|
<time>2023-12-31T23:01:11.014Z</time>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.78727108169855" lon="4.406133681127736">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
<time>2023-12-31T23:01:14.708Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787297" lon="4.406177">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
<time>2023-12-31T23:01:15.462Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78716" lon="4.406098">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<time>2023-12-31T23:01:18.380Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787045" lon="4.405984">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
<time>2023-12-31T23:01:21.096Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786683" lon="4.405653">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<time>2023-12-31T23:01:29.464Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786538" lon="4.405543">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
<time>2023-12-31T23:01:32.682Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78635" lon="4.405441">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<time>2023-12-31T23:01:36.660Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786275" lon="4.40542">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<time>2023-12-31T23:01:38.184Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786182" lon="4.405435">
|
||||||
|
<ele>116.0</ele>
|
||||||
|
<time>2023-12-31T23:01:40.055Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786121" lon="4.405475">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<time>2023-12-31T23:01:41.376Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786042" lon="4.405558">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
<time>2023-12-31T23:01:43.274Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785821" lon="4.405925">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
<time>2023-12-31T23:01:49.687Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785672" lon="4.406119">
|
||||||
|
<ele>112.5</ele>
|
||||||
|
<time>2023-12-31T23:01:53.549Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785516" lon="4.406256">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<time>2023-12-31T23:01:57.120Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785384" lon="4.406364">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
<time>2023-12-31T23:02:00.094Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785126" lon="4.406475">
|
||||||
|
<ele>106.3</ele>
|
||||||
|
<time>2023-12-31T23:02:05.445Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784697" lon="4.406537">
|
||||||
|
<ele>104.3</ele>
|
||||||
|
<time>2023-12-31T23:02:14.067Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784591" lon="4.40657">
|
||||||
|
<ele>104.0</ele>
|
||||||
|
<time>2023-12-31T23:02:16.229Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784507" lon="4.406612">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<time>2023-12-31T23:02:17.992Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784435" lon="4.40669">
|
||||||
|
<ele>103.3</ele>
|
||||||
|
<time>2023-12-31T23:02:19.738Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784209" lon="4.407148">
|
||||||
|
<ele>103.5</ele>
|
||||||
|
<time>2023-12-31T23:02:27.089Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784162" lon="4.407257">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
<time>2023-12-31T23:02:28.758Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784077" lon="4.407372">
|
||||||
|
<ele>104.8</ele>
|
||||||
|
<time>2023-12-31T23:02:30.996Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784006" lon="4.407435">
|
||||||
|
<ele>105.8</ele>
|
||||||
|
<time>2023-12-31T23:02:32.625Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783924" lon="4.407471">
|
||||||
|
<ele>106.8</ele>
|
||||||
|
<time>2023-12-31T23:02:34.328Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783837" lon="4.407486">
|
||||||
|
<ele>107.8</ele>
|
||||||
|
<time>2023-12-31T23:02:36.079Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783771" lon="4.407472">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
<time>2023-12-31T23:02:37.411Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783697" lon="4.407428">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
<time>2023-12-31T23:02:38.993Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783626" lon="4.407363">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
<time>2023-12-31T23:02:40.634Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783548" lon="4.407274">
|
||||||
|
<ele>110.5</ele>
|
||||||
|
<time>2023-12-31T23:02:42.559Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783458" lon="4.407134">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
<time>2023-12-31T23:02:45.085Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783123" lon="4.406435">
|
||||||
|
<ele>111.8</ele>
|
||||||
|
<time>2023-12-31T23:02:56.184Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782982" lon="4.406168">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
<time>2023-12-31T23:03:00.586Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782871" lon="4.406044">
|
||||||
|
<ele>113.3</ele>
|
||||||
|
<time>2023-12-31T23:03:03.305Z</time>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
<trk>
|
||||||
|
<name>track 2</name>
|
||||||
|
<type>Running</type>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.782212" lon="4.406377">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
<time>2023-12-31T23:03:17.151Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782175" lon="4.406413">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
<time>2023-12-31T23:03:18.020Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781749" lon="4.407018">
|
||||||
|
<ele>118.5</ele>
|
||||||
|
<time>2023-12-31T23:03:29.479Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781654" lon="4.407316">
|
||||||
|
<ele>119.5</ele>
|
||||||
|
<time>2023-12-31T23:03:33.702Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781563" lon="4.407764">
|
||||||
|
<ele>121.3</ele>
|
||||||
|
<time>2023-12-31T23:03:39.656Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781487" lon="4.407984">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
<time>2023-12-31T23:03:42.828Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781422" lon="4.408216">
|
||||||
|
<ele>122.8</ele>
|
||||||
|
<time>2023-12-31T23:03:46.039Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781395" lon="4.408508">
|
||||||
|
<ele>123.5</ele>
|
||||||
|
<time>2023-12-31T23:03:49.773Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781399" lon="4.409114">
|
||||||
|
<ele>126.3</ele>
|
||||||
|
<time>2023-12-31T23:03:57.442Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781367" lon="4.409428">
|
||||||
|
<ele>128.0</ele>
|
||||||
|
<time>2023-12-31T23:04:01.467Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781286" lon="4.409607">
|
||||||
|
<ele>129.0</ele>
|
||||||
|
<time>2023-12-31T23:04:04.252Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78116" lon="4.409789">
|
||||||
|
<ele>130.0</ele>
|
||||||
|
<time>2023-12-31T23:04:07.667Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780804" lon="4.409993">
|
||||||
|
<ele>130.8</ele>
|
||||||
|
<time>2023-12-31T23:04:15.245Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780389" lon="4.410334">
|
||||||
|
<ele>131.8</ele>
|
||||||
|
<time>2023-12-31T23:04:24.605Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780232" lon="4.410563">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<time>2023-12-31T23:04:28.879Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780094" lon="4.410827">
|
||||||
|
<ele>132.8</ele>
|
||||||
|
<time>2023-12-31T23:04:33.213Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779723" lon="4.411582">
|
||||||
|
<ele>135.8</ele>
|
||||||
|
<time>2023-12-31T23:04:45.314Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779591" lon="4.411791">
|
||||||
|
<ele>135.5</ele>
|
||||||
|
<time>2023-12-31T23:04:49.052Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779125" lon="4.412435">
|
||||||
|
<ele>132.5</ele>
|
||||||
|
<time>2023-12-31T23:05:01.438Z</time>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.77906316558724" lon="4.412547477922485">
|
||||||
|
<ele>133.3</ele>
|
||||||
|
<time>2023-12-31T23:05:03.324Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778676" lon="4.412979">
|
||||||
|
<ele>134.0</ele>
|
||||||
|
<time>2023-12-31T23:05:12.804Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778194" lon="4.413466">
|
||||||
|
<ele>136.8</ele>
|
||||||
|
<time>2023-12-31T23:05:24.252Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777427" lon="4.414302">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<time>2023-12-31T23:05:42.896Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777165" lon="4.414736">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<time>2023-12-31T23:05:50.490Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776927" lon="4.415201">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
<time>2023-12-31T23:05:58.061Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776778" lon="4.415613">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
<time>2023-12-31T23:06:04.068Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776553" lon="4.416425">
|
||||||
|
<ele>134.8</ele>
|
||||||
|
<time>2023-12-31T23:06:15.288Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776326" lon="4.417304">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
<time>2023-12-31T23:06:27.305Z</time>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776129" lon="4.418383">
|
||||||
|
<ele>129.5</ele>
|
||||||
|
<time>2023-12-31T23:06:41.519Z</time>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>
|
267
gpx/test-data/with_waypoint.gpx
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
<?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_waypoint</name>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio"></link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<wpt lat="50.7836710064975" lon="4.410764082658738">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
<name>Waypoint</name>
|
||||||
|
<cmt>Comment</cmt>
|
||||||
|
<desc>Description</desc>
|
||||||
|
<sym>Bike Trail</sym>
|
||||||
|
</wpt>
|
||||||
|
<trk>
|
||||||
|
<name>with_waypoint</name>
|
||||||
|
<type>Cycling</type>
|
||||||
|
<trkseg>
|
||||||
|
<trkpt lat="50.790867" lon="4.404968">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790714" lon="4.405036">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790336" lon="4.405259">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790165" lon="4.405331">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.790008" lon="4.405359">
|
||||||
|
<ele>110.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789818" lon="4.405359">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789409" lon="4.40534">
|
||||||
|
<ele>107.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.789105" lon="4.405411">
|
||||||
|
<ele>106.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788799" lon="4.405527">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.788645" lon="4.405606">
|
||||||
|
<ele>109.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.7885" lon="4.405711">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78822" lon="4.405959">
|
||||||
|
<ele>112.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787956" lon="4.406092">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787814" lon="4.406143">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787674" lon="4.406177">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787451" lon="4.406199">
|
||||||
|
<ele>115.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787297" lon="4.406177">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78716" lon="4.406098">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.787045" lon="4.405984">
|
||||||
|
<ele>114.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786683" lon="4.405653">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786538" lon="4.405543">
|
||||||
|
<ele>115.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78635" lon="4.405441">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786275" lon="4.40542">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786182" lon="4.405435">
|
||||||
|
<ele>116.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786121" lon="4.405475">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.786042" lon="4.405558">
|
||||||
|
<ele>115.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785821" lon="4.405925">
|
||||||
|
<ele>114.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785672" lon="4.406119">
|
||||||
|
<ele>112.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785516" lon="4.406256">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785384" lon="4.406364">
|
||||||
|
<ele>109.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.785126" lon="4.406475">
|
||||||
|
<ele>106.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784697" lon="4.406537">
|
||||||
|
<ele>104.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784591" lon="4.40657">
|
||||||
|
<ele>104.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784507" lon="4.406612">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784435" lon="4.40669">
|
||||||
|
<ele>103.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784209" lon="4.407148">
|
||||||
|
<ele>103.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784162" lon="4.407257">
|
||||||
|
<ele>103.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784077" lon="4.407372">
|
||||||
|
<ele>104.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.784006" lon="4.407435">
|
||||||
|
<ele>105.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783924" lon="4.407471">
|
||||||
|
<ele>106.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783837" lon="4.407486">
|
||||||
|
<ele>107.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783771" lon="4.407472">
|
||||||
|
<ele>108.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783697" lon="4.407428">
|
||||||
|
<ele>109.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783626" lon="4.407363">
|
||||||
|
<ele>110.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783548" lon="4.407274">
|
||||||
|
<ele>110.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783458" lon="4.407134">
|
||||||
|
<ele>110.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.783123" lon="4.406435">
|
||||||
|
<ele>111.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782982" lon="4.406168">
|
||||||
|
<ele>112.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782871" lon="4.406044">
|
||||||
|
<ele>113.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78279" lon="4.406021">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782714" lon="4.406018">
|
||||||
|
<ele>113.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782607" lon="4.406047">
|
||||||
|
<ele>113.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782405" lon="4.406194">
|
||||||
|
<ele>114.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.782175" lon="4.406413">
|
||||||
|
<ele>115.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781749" lon="4.407018">
|
||||||
|
<ele>118.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781654" lon="4.407316">
|
||||||
|
<ele>119.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781563" lon="4.407764">
|
||||||
|
<ele>121.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781487" lon="4.407984">
|
||||||
|
<ele>122.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781422" lon="4.408216">
|
||||||
|
<ele>122.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781395" lon="4.408508">
|
||||||
|
<ele>123.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781399" lon="4.409114">
|
||||||
|
<ele>126.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781367" lon="4.409428">
|
||||||
|
<ele>128.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.781286" lon="4.409607">
|
||||||
|
<ele>129.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.78116" lon="4.409789">
|
||||||
|
<ele>130.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780804" lon="4.409993">
|
||||||
|
<ele>130.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780389" lon="4.410334">
|
||||||
|
<ele>131.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780232" lon="4.410563">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.780094" lon="4.410827">
|
||||||
|
<ele>132.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779723" lon="4.411582">
|
||||||
|
<ele>135.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779591" lon="4.411791">
|
||||||
|
<ele>135.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.779125" lon="4.412435">
|
||||||
|
<ele>132.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778676" lon="4.412979">
|
||||||
|
<ele>134.0</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.778194" lon="4.413466">
|
||||||
|
<ele>136.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777427" lon="4.414302">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.777165" lon="4.414736">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776927" lon="4.415201">
|
||||||
|
<ele>137.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776778" lon="4.415613">
|
||||||
|
<ele>137.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776553" lon="4.416425">
|
||||||
|
<ele>134.8</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776326" lon="4.417304">
|
||||||
|
<ele>132.3</ele>
|
||||||
|
</trkpt>
|
||||||
|
<trkpt lat="50.776129" lon="4.418383">
|
||||||
|
<ele>129.5</ele>
|
||||||
|
</trkpt>
|
||||||
|
</trkseg>
|
||||||
|
</trk>
|
||||||
|
</gpx>
|
12
gpx/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ES2020",
|
||||||
|
"target": "ES2015",
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
}
|
13
website/.eslintignore
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/build
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
31
website/.eslintrc.cjs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/** @type { import("eslint").Linter.Config } */
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:svelte/recommended',
|
||||||
|
'prettier'
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['@typescript-eslint'],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
extraFileExtensions: ['.svelte']
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2017: true,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.svelte'],
|
||||||
|
parser: 'svelte-eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@typescript-eslint/parser'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
1
website/.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
4
website/.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
8
website/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
|
}
|
14
website/components.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/app.css",
|
||||||
|
"baseColor": "slate"
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "$lib/components",
|
||||||
|
"utils": "$lib/utils"
|
||||||
|
},
|
||||||
|
"typescript": true
|
||||||
|
}
|
9536
website/package-lock.json
generated
Normal file
79
website/package.json
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"name": "website",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"postbuild": "npx tsx src/lib/sitemap.ts",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"lint": "prettier --check . && eslint .",
|
||||||
|
"format": "prettier --write ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "^3.2.5",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.5",
|
||||||
|
"@sveltejs/enhanced-img": "^0.3.8",
|
||||||
|
"@sveltejs/kit": "^2.6.1",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||||
|
"@types/eslint": "^8.56.12",
|
||||||
|
"@types/events": "^3.0.3",
|
||||||
|
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
|
||||||
|
"@types/mapbox__tilebelt": "^1.0.4",
|
||||||
|
"@types/mapbox-gl": "^3.4.0",
|
||||||
|
"@types/node": "^20.16.10",
|
||||||
|
"@types/png.js": "^0.2.3",
|
||||||
|
"@types/sanitize-html": "^2.13.0",
|
||||||
|
"@types/sortablejs": "^1.15.8",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||||
|
"@typescript-eslint/parser": "^7.18.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-svelte": "^2.44.1",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"glob": "^10.4.5",
|
||||||
|
"mdsvex": "^0.11.2",
|
||||||
|
"postcss": "^8.4.47",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"prettier-plugin-svelte": "^3.2.7",
|
||||||
|
"svelte": "^4.2.19",
|
||||||
|
"svelte-check": "^3.8.6",
|
||||||
|
"tailwindcss": "^3.4.13",
|
||||||
|
"tslib": "^2.7.0",
|
||||||
|
"tsx": "^4.19.1",
|
||||||
|
"typescript": "^5.6.2",
|
||||||
|
"vite": "^5.4.8",
|
||||||
|
"vite-plugin-node-polyfills": "^0.22.0"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@docsearch/js": "^3.6.2",
|
||||||
|
"@internationalized/date": "^3.5.5",
|
||||||
|
"@mapbox/mapbox-gl-geocoder": "^5.0.3",
|
||||||
|
"@mapbox/sphericalmercator": "^1.2.0",
|
||||||
|
"@mapbox/tilebelt": "^1.0.2",
|
||||||
|
"@types/mapbox__sphericalmercator": "^1.2.3",
|
||||||
|
"bits-ui": "^0.21.15",
|
||||||
|
"chart.js": "^4.4.4",
|
||||||
|
"chartjs-plugin-zoom": "^2.0.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"dexie": "^4.0.8",
|
||||||
|
"gpx": "file:../gpx",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"lucide-static": "^0.427.0",
|
||||||
|
"lucide-svelte": "^0.427.0",
|
||||||
|
"mapbox-gl": "^3.7.0",
|
||||||
|
"mapillary-js": "^4.1.2",
|
||||||
|
"mode-watcher": "^0.3.1",
|
||||||
|
"png.js": "^0.2.1",
|
||||||
|
"sanitize-html": "^2.13.0",
|
||||||
|
"sortablejs": "^1.15.3",
|
||||||
|
"svelte-i18n": "^4.0.0",
|
||||||
|
"svelte-sonner": "^0.3.28",
|
||||||
|
"tailwind-merge": "^2.5.2",
|
||||||
|
"tailwind-variants": "^0.2.1"
|
||||||
|
}
|
||||||
|
}
|
6
website/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
13
website/src/app.d.ts
vendored
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
// See https://kit.svelte.dev/docs/types#app
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
15
website/src/app.html
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
86
website/src/app.pcss
Executable file
@ -0,0 +1,86 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 45%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--accent: 210 40% 92%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--destructive: 0 72.2% 50.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--support: 220 15 130;
|
||||||
|
|
||||||
|
--link: 0 110 180;
|
||||||
|
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--accent: 217.2 32.6% 30%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--support: 255 110 190;
|
||||||
|
|
||||||
|
--link: 80 190 255;
|
||||||
|
|
||||||
|
--ring: hsl(212.7,26.8%,83.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
68
website/src/hooks.server.js
Executable 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 ?? 'fr';
|
||||||
|
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.rnmkcy.eu — ${title}</title>
|
||||||
|
<meta name="description" content="${description}" />
|
||||||
|
<meta property="og:title" content="gpx.rnmkcy.eu — ${title}" />
|
||||||
|
<meta property="og:description" content="${description}" />
|
||||||
|
<meta name="twitter:title" content="gpx.rnmkcy.eu — ${title}" />
|
||||||
|
<meta name="twitter:description" content="${description}" />
|
||||||
|
<meta property="og:image" content="https://gpx.rnmkcy.eu${base}/og_logo.png" />
|
||||||
|
<meta property="og:url" content="https://gpx.rnmkcy.eu/" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:site_name" content="gpx.rnmkcy.eu" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:image" content="https://gpx.rnmkcy.eu${base}/og_logo.png" />
|
||||||
|
<meta name="twitter:url" content="https://gpx.rnmkcy.eu/" />
|
||||||
|
<meta name="twitter:site" content="@gpxstudio" />
|
||||||
|
<meta name="twitter:creator" content="@gpxstudio" />
|
||||||
|
<link rel="alternate" hreflang="x-default" href="https://gpx.rnmkcy.eu${getURLForLanguage('fr', path)}" />`;
|
||||||
|
|
||||||
|
for (let lang of Object.keys(languages)) {
|
||||||
|
headTag += ` <link rel="alternate" hreflang="${lang}" href="https://gpx.rnmkcy.eu${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
Executable file
15495
website/src/lib/assets/custom/ign-fr-plan.json
Executable file
10837
website/src/lib/assets/custom/ign-fr-satellite.json
Executable file
15495
website/src/lib/assets/custom/ign-fr-topo.json
Executable file
2
website/src/lib/assets/example.ts
Executable file
BIN
website/src/lib/assets/img/docs/getting-started/interface.png
Executable file
After Width: | Height: | Size: 2.2 MiB |
BIN
website/src/lib/assets/img/docs/tools/routing.png
Executable file
After Width: | Height: | Size: 768 KiB |
BIN
website/src/lib/assets/img/docs/tools/split.png
Executable file
After Width: | Height: | Size: 596 KiB |
BIN
website/src/lib/assets/img/home/cyclosm.png
Executable file
After Width: | Height: | Size: 4.3 MiB |
BIN
website/src/lib/assets/img/home/ign.png
Executable file
After Width: | Height: | Size: 5.4 MiB |
BIN
website/src/lib/assets/img/home/map.png
Executable file
After Width: | Height: | Size: 2.9 MiB |
BIN
website/src/lib/assets/img/home/mapbox-outdoors.png
Executable file
After Width: | Height: | Size: 1.5 MiB |
BIN
website/src/lib/assets/img/home/mapbox-satellite.png
Executable file
After Width: | Height: | Size: 3.6 MiB |
BIN
website/src/lib/assets/img/home/routing.png
Executable file
After Width: | Height: | Size: 6.9 MiB |
BIN
website/src/lib/assets/img/home/waymarked.png
Executable file
After Width: | Height: | Size: 448 KiB |
1246
website/src/lib/assets/layers.ts
Executable file
31
website/src/lib/assets/surfaces.ts
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
export const surfaceColors: { [key: string]: string } = {
|
||||||
|
'missing': '#d1d1d1',
|
||||||
|
'paved': '#8c8c8c',
|
||||||
|
'unpaved': '#6b443a',
|
||||||
|
'asphalt': '#8c8c8c',
|
||||||
|
'concrete': '#8c8c8c',
|
||||||
|
'chipseal': '#8c8c8c',
|
||||||
|
'cobblestone': '#ffd991',
|
||||||
|
'unhewn_cobblestone': '#ffd991',
|
||||||
|
'paving_stones': '#8c8c8c',
|
||||||
|
'stepping_stones': '#c7b2db',
|
||||||
|
'sett': '#ffd991',
|
||||||
|
'metal': '#8c8c8c',
|
||||||
|
'wood': '#6b443a',
|
||||||
|
'compacted': '#ffffa8',
|
||||||
|
'fine_gravel': '#ffffa8',
|
||||||
|
'gravel': '#ffffa8',
|
||||||
|
'pebblestone': '#ffffa8',
|
||||||
|
'rock': '#ffd991',
|
||||||
|
'dirt': '#ffffa8',
|
||||||
|
'ground': '#6b443a',
|
||||||
|
'earth': '#6b443a',
|
||||||
|
'snow': '#bdfffc',
|
||||||
|
'ice': '#bdfffc',
|
||||||
|
'salt': '#b6c0f2',
|
||||||
|
'mud': '#6b443a',
|
||||||
|
'sand': '#ffffc4',
|
||||||
|
'woodchips': '#6b443a',
|
||||||
|
'grass': '#61b55c',
|
||||||
|
'grass_paver': '#61b55c'
|
||||||
|
}
|
60
website/src/lib/assets/symbols.ts
Executable file
@ -0,0 +1,60 @@
|
|||||||
|
import { Landmark, Icon, Shell, Bike, Building, Tent, Car, Wrench, ShoppingBasket, Droplet, DoorOpen, Trees, Fuel, Home, Info, TreeDeciduous, CircleParking, Cross, Utensils, Construction, BrickWall, ShowerHead, Mountain, Phone, TrainFront, Bed, Binoculars, TriangleAlert, Anchor } from "lucide-svelte";
|
||||||
|
import { Landmark as LandmarkSvg, Shell as ShellSvg, Bike as BikeSvg, Building as BuildingSvg, Tent as TentSvg, Car as CarSvg, Wrench as WrenchSvg, ShoppingBasket as ShoppingBasketSvg, Droplet as DropletSvg, DoorOpen as DoorOpenSvg, Trees as TreesSvg, Fuel as FuelSvg, Home as HomeSvg, Info as InfoSvg, TreeDeciduous as TreeDeciduousSvg, CircleParking as CircleParkingSvg, Cross as CrossSvg, Utensils as UtensilsSvg, Construction as ConstructionSvg, BrickWall as BrickWallSvg, ShowerHead as ShowerHeadSvg, Mountain as MountainSvg, Phone as PhoneSvg, TrainFront as TrainFrontSvg, Bed as BedSvg, Binoculars as BinocularsSvg, TriangleAlert as TriangleAlertSvg, Anchor as AnchorSvg } from "lucide-static";
|
||||||
|
import type { ComponentType } from "svelte";
|
||||||
|
|
||||||
|
export type Symbol = {
|
||||||
|
value: string;
|
||||||
|
icon?: ComponentType<Icon>;
|
||||||
|
iconSvg?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const symbols: { [key: string]: Symbol } = {
|
||||||
|
alert: { value: 'Alert', icon: TriangleAlert, iconSvg: TriangleAlertSvg },
|
||||||
|
anchor: { value: 'Anchor', icon: Anchor, iconSvg: AnchorSvg },
|
||||||
|
bank: { value: 'Bank', icon: Landmark, iconSvg: LandmarkSvg },
|
||||||
|
beach: { value: 'Beach', icon: Shell, iconSvg: ShellSvg },
|
||||||
|
bike_trail: { value: 'Bike Trail', icon: Bike, iconSvg: BikeSvg },
|
||||||
|
binoculars: { value: 'Binoculars', icon: Binoculars, iconSvg: BinocularsSvg },
|
||||||
|
bridge: { value: 'Bridge' },
|
||||||
|
building: { value: 'Building', icon: Building, iconSvg: BuildingSvg },
|
||||||
|
campground: { value: 'Campground', icon: Tent, iconSvg: TentSvg },
|
||||||
|
car: { value: 'Car', icon: Car, iconSvg: CarSvg },
|
||||||
|
car_repair: { value: 'Car Repair', icon: Wrench, iconSvg: WrenchSvg },
|
||||||
|
convenience_store: { value: 'Convenience Store', icon: ShoppingBasket, iconSvg: ShoppingBasketSvg },
|
||||||
|
crossing: { value: 'Crossing' },
|
||||||
|
department_store: { value: 'Department Store', icon: ShoppingBasket, iconSvg: ShoppingBasketSvg },
|
||||||
|
drinking_water: { value: 'Drinking Water', icon: Droplet, iconSvg: DropletSvg },
|
||||||
|
exit: { value: 'Exit', icon: DoorOpen, iconSvg: DoorOpenSvg },
|
||||||
|
lodge: { value: 'Lodge', icon: Home, iconSvg: HomeSvg },
|
||||||
|
lodging: { value: 'Lodging', icon: Bed, iconSvg: BedSvg },
|
||||||
|
forest: { value: 'Forest', icon: Trees, iconSvg: TreesSvg },
|
||||||
|
gas_station: { value: 'Gas Station', icon: Fuel, iconSvg: FuelSvg },
|
||||||
|
ground_transportation: { value: 'Ground Transportation', icon: TrainFront, iconSvg: TrainFrontSvg },
|
||||||
|
hotel: { value: 'Hotel', icon: Bed, iconSvg: BedSvg },
|
||||||
|
house: { value: 'House', icon: Home, iconSvg: HomeSvg },
|
||||||
|
information: { value: 'Information', icon: Info, iconSvg: InfoSvg },
|
||||||
|
park: { value: 'Park', icon: TreeDeciduous, iconSvg: TreeDeciduousSvg },
|
||||||
|
parking_area: { value: 'Parking Area', icon: CircleParking, iconSvg: CircleParkingSvg },
|
||||||
|
pharmacy: { value: 'Pharmacy', icon: Cross, iconSvg: CrossSvg },
|
||||||
|
picnic_area: { value: 'Picnic Area', icon: Utensils, iconSvg: UtensilsSvg },
|
||||||
|
restaurant: { value: 'Restaurant', icon: Utensils, iconSvg: UtensilsSvg },
|
||||||
|
restricted_area: { value: 'Restricted Area', icon: Construction, iconSvg: ConstructionSvg },
|
||||||
|
restroom: { value: 'Restroom' },
|
||||||
|
road: { value: 'Road', icon: BrickWall, iconSvg: BrickWallSvg },
|
||||||
|
scenic_area: { value: 'Scenic Area', icon: Binoculars, iconSvg: BinocularsSvg },
|
||||||
|
shelter: { value: 'Shelter', icon: Tent, iconSvg: TentSvg },
|
||||||
|
shopping_center: { value: 'Shopping Center', icon: ShoppingBasket },
|
||||||
|
shower: { value: 'Shower', icon: ShowerHead, iconSvg: ShowerHeadSvg },
|
||||||
|
summit: { value: 'Summit', icon: Mountain, iconSvg: MountainSvg },
|
||||||
|
telephone: { value: 'Telephone', icon: Phone, iconSvg: PhoneSvg },
|
||||||
|
tunnel: { value: 'Tunnel' },
|
||||||
|
water_source: { value: 'Water Source', icon: Droplet, iconSvg: DropletSvg },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getSymbolKey(value: string | undefined): string | undefined {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
return Object.keys(symbols).find(key => symbols[key].value === value);
|
||||||
|
}
|
||||||
|
}
|
60
website/src/lib/components/AlgoliaDocSearch.svelte
Executable 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 ?? 'fr')]
|
||||||
|
},
|
||||||
|
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
Executable 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} on:click>
|
||||||
|
<slot />
|
||||||
|
</Button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content {side}>
|
||||||
|
<span>{label}</span>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
685
website/src/lib/components/ElevationProfile.svelte
Executable file
@ -0,0 +1,685 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as ToggleGroup from '$lib/components/ui/toggle-group';
|
||||||
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
|
import Chart from 'chart.js/auto';
|
||||||
|
import mapboxgl from 'mapbox-gl';
|
||||||
|
import { map } from '$lib/stores';
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
import {
|
||||||
|
BrickWall,
|
||||||
|
TriangleRight,
|
||||||
|
HeartPulse,
|
||||||
|
Orbit,
|
||||||
|
SquareActivity,
|
||||||
|
Thermometer,
|
||||||
|
Zap
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
import { surfaceColors } from '$lib/assets/surfaces';
|
||||||
|
import { _, locale } from 'svelte-i18n';
|
||||||
|
import {
|
||||||
|
getCadenceUnits,
|
||||||
|
getCadenceWithUnits,
|
||||||
|
getConvertedDistance,
|
||||||
|
getConvertedElevation,
|
||||||
|
getConvertedTemperature,
|
||||||
|
getConvertedVelocity,
|
||||||
|
getDistanceUnits,
|
||||||
|
getDistanceWithUnits,
|
||||||
|
getElevationWithUnits,
|
||||||
|
getHeartRateUnits,
|
||||||
|
getHeartRateWithUnits,
|
||||||
|
getPowerUnits,
|
||||||
|
getPowerWithUnits,
|
||||||
|
getTemperatureUnits,
|
||||||
|
getTemperatureWithUnits,
|
||||||
|
getVelocityUnits,
|
||||||
|
getVelocityWithUnits,
|
||||||
|
secondsToHHMMSS
|
||||||
|
} from '$lib/units';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
import { DateFormatter } from '@internationalized/date';
|
||||||
|
import type { GPXStatistics } from 'gpx';
|
||||||
|
import { settings } from '$lib/db';
|
||||||
|
import { mode } from 'mode-watcher';
|
||||||
|
|
||||||
|
export let gpxStatistics: Writable<GPXStatistics>;
|
||||||
|
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
|
||||||
|
export let panelSize: number;
|
||||||
|
export let additionalDatasets: string[];
|
||||||
|
export let elevationFill: 'slope' | 'surface' | undefined;
|
||||||
|
export let showControls: boolean = true;
|
||||||
|
|
||||||
|
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
|
||||||
|
|
||||||
|
let df: DateFormatter;
|
||||||
|
|
||||||
|
$: if ($locale) {
|
||||||
|
df = new DateFormatter($locale, {
|
||||||
|
dateStyle: 'medium',
|
||||||
|
timeStyle: 'medium'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let canvas: HTMLCanvasElement;
|
||||||
|
let showAdditionalScales = true;
|
||||||
|
let updateShowAdditionalScales = () => {
|
||||||
|
showAdditionalScales = canvas.width / window.devicePixelRatio >= 600;
|
||||||
|
};
|
||||||
|
let overlay: HTMLCanvasElement;
|
||||||
|
let chart: Chart;
|
||||||
|
|
||||||
|
Chart.defaults.font.family =
|
||||||
|
'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; // Tailwind CSS font
|
||||||
|
|
||||||
|
let marker: mapboxgl.Marker | null = null;
|
||||||
|
let dragging = false;
|
||||||
|
let panning = false;
|
||||||
|
|
||||||
|
let options = {
|
||||||
|
animation: false,
|
||||||
|
parsing: false,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'linear',
|
||||||
|
ticks: {
|
||||||
|
callback: function (value: number, index: number, ticks: { value: number }[]) {
|
||||||
|
if (index === ticks.length - 1) {
|
||||||
|
return `${value.toFixed(1).replace(/\.0+$/, '')}`;
|
||||||
|
}
|
||||||
|
return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
ticks: {
|
||||||
|
callback: function (value: number) {
|
||||||
|
return getElevationWithUnits(value, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
datasets: {
|
||||||
|
line: {
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0.4,
|
||||||
|
borderWidth: 2,
|
||||||
|
cubicInterpolationMode: 'monotone'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
mode: 'nearest',
|
||||||
|
axis: 'x',
|
||||||
|
intersect: false
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
decimation: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: () => !dragging && !panning,
|
||||||
|
callbacks: {
|
||||||
|
title: function () {
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
label: function (context: Chart.TooltipContext) {
|
||||||
|
let point = context.raw;
|
||||||
|
if (context.datasetIndex === 0) {
|
||||||
|
if ($map && marker) {
|
||||||
|
if (dragging) {
|
||||||
|
marker.remove();
|
||||||
|
} else {
|
||||||
|
marker.setLngLat(point.coordinates);
|
||||||
|
marker.addTo($map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${$_('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
|
||||||
|
} else if (context.datasetIndex === 1) {
|
||||||
|
return `${$velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')}: ${getVelocityWithUnits(point.y, false)}`;
|
||||||
|
} else if (context.datasetIndex === 2) {
|
||||||
|
return `${$_('quantities.heartrate')}: ${getHeartRateWithUnits(point.y)}`;
|
||||||
|
} else if (context.datasetIndex === 3) {
|
||||||
|
return `${$_('quantities.cadence')}: ${getCadenceWithUnits(point.y)}`;
|
||||||
|
} else if (context.datasetIndex === 4) {
|
||||||
|
return `${$_('quantities.temperature')}: ${getTemperatureWithUnits(point.y, false)}`;
|
||||||
|
} else if (context.datasetIndex === 5) {
|
||||||
|
return `${$_('quantities.power')}: ${getPowerWithUnits(point.y)}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
afterBody: function (contexts: Chart.TooltipContext[]) {
|
||||||
|
let context = contexts.filter((context) => context.datasetIndex === 0);
|
||||||
|
if (context.length === 0) return;
|
||||||
|
let point = context[0].raw;
|
||||||
|
let slope = {
|
||||||
|
at: point.slope.at.toFixed(1),
|
||||||
|
segment: point.slope.segment.toFixed(1),
|
||||||
|
length: getDistanceWithUnits(point.slope.length)
|
||||||
|
};
|
||||||
|
let surface = point.surface ? point.surface : 'unknown';
|
||||||
|
|
||||||
|
let labels = [
|
||||||
|
` ${$_('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
|
||||||
|
` ${$_('quantities.slope')}: ${slope.at} %${elevationFill === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}`
|
||||||
|
];
|
||||||
|
|
||||||
|
if (elevationFill === 'surface') {
|
||||||
|
labels.push(
|
||||||
|
` ${$_('quantities.surface')}: ${$_(`toolbar.routing.surface.${surface}`)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (point.time) {
|
||||||
|
labels.push(` ${$_('quantities.time')}: ${df.format(point.time)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
pan: {
|
||||||
|
enabled: true,
|
||||||
|
mode: 'x',
|
||||||
|
modifierKey: 'shift',
|
||||||
|
onPanStart: function () {
|
||||||
|
// hide tooltip
|
||||||
|
panning = true;
|
||||||
|
$slicedGPXStatistics = undefined;
|
||||||
|
},
|
||||||
|
onPanComplete: function () {
|
||||||
|
panning = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
wheel: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
mode: 'x',
|
||||||
|
onZoomStart: function ({ chart, event }: { chart: Chart; event: any }) {
|
||||||
|
if (
|
||||||
|
event.deltaY < 0 &&
|
||||||
|
Math.abs(
|
||||||
|
chart.getInitialScaleBounds().x.max / chart.options.plugins.zoom.limits.x.minRange -
|
||||||
|
chart.getZoomLevel()
|
||||||
|
) < 0.01
|
||||||
|
) {
|
||||||
|
// Disable wheel pan if zoomed in to the max, and zooming in
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$slicedGPXStatistics = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
x: {
|
||||||
|
min: 'original',
|
||||||
|
max: 'original',
|
||||||
|
minRange: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stacked: false,
|
||||||
|
onResize: function () {
|
||||||
|
updateOverlay();
|
||||||
|
updateShowAdditionalScales();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let datasets: {
|
||||||
|
[key: string]: {
|
||||||
|
id: string;
|
||||||
|
getLabel: () => string;
|
||||||
|
getUnits: () => string;
|
||||||
|
};
|
||||||
|
} = {
|
||||||
|
speed: {
|
||||||
|
id: 'speed',
|
||||||
|
getLabel: () => ($velocityUnits === 'speed' ? $_('quantities.speed') : $_('quantities.pace')),
|
||||||
|
getUnits: () => getVelocityUnits()
|
||||||
|
},
|
||||||
|
hr: {
|
||||||
|
id: 'hr',
|
||||||
|
getLabel: () => $_('quantities.heartrate'),
|
||||||
|
getUnits: () => getHeartRateUnits()
|
||||||
|
},
|
||||||
|
cad: {
|
||||||
|
id: 'cad',
|
||||||
|
getLabel: () => $_('quantities.cadence'),
|
||||||
|
getUnits: () => getCadenceUnits()
|
||||||
|
},
|
||||||
|
atemp: {
|
||||||
|
id: 'atemp',
|
||||||
|
getLabel: () => $_('quantities.temperature'),
|
||||||
|
getUnits: () => getTemperatureUnits()
|
||||||
|
},
|
||||||
|
power: {
|
||||||
|
id: 'power',
|
||||||
|
getLabel: () => $_('quantities.power'),
|
||||||
|
getUnits: () => getPowerUnits()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let [id, dataset] of Object.entries(datasets)) {
|
||||||
|
options.scales[`y${id}`] = {
|
||||||
|
type: 'linear',
|
||||||
|
position: 'right',
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: dataset.getLabel() + ' (' + dataset.getUnits() + ')',
|
||||||
|
padding: {
|
||||||
|
top: 6,
|
||||||
|
bottom: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
reverse: () => id === 'speed' && $velocityUnits === 'pace',
|
||||||
|
display: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
options.scales.yspeed['ticks'] = {
|
||||||
|
callback: function (value: number) {
|
||||||
|
if ($velocityUnits === 'speed') {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return secondsToHHMMSS(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
Chart.register((await import('chartjs-plugin-zoom')).default); // dynamic import to avoid SSR and 'window is not defined' error
|
||||||
|
|
||||||
|
chart = new Chart(canvas, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
datasets: []
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
id: 'toggleMarker',
|
||||||
|
events: ['mouseout'],
|
||||||
|
afterEvent: function (chart: Chart, args: { event: Chart.ChartEvent }) {
|
||||||
|
if (args.event.type === 'mouseout') {
|
||||||
|
if ($map && marker) {
|
||||||
|
marker.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Map marker to show on hover
|
||||||
|
let element = document.createElement('div');
|
||||||
|
element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
|
||||||
|
marker = new mapboxgl.Marker({
|
||||||
|
element
|
||||||
|
});
|
||||||
|
|
||||||
|
updateShowAdditionalScales();
|
||||||
|
|
||||||
|
let startIndex = 0;
|
||||||
|
let endIndex = 0;
|
||||||
|
function getIndex(evt) {
|
||||||
|
const points = chart.getElementsAtEventForMode(
|
||||||
|
evt,
|
||||||
|
'x',
|
||||||
|
{
|
||||||
|
intersect: false
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if (points.length === 0) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
if (evt.x - rect.left <= chart.chartArea.left) {
|
||||||
|
return 0;
|
||||||
|
} else if (evt.x - rect.left >= chart.chartArea.right) {
|
||||||
|
return $gpxStatistics.local.points.length - 1;
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let point = points.find((point) => point.element.raw);
|
||||||
|
if (point) {
|
||||||
|
return point.element.raw.index;
|
||||||
|
} else {
|
||||||
|
return points[0].index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dragStarted = false;
|
||||||
|
function onMouseDown(evt) {
|
||||||
|
if (evt.shiftKey) {
|
||||||
|
// Panning interaction
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dragStarted = true;
|
||||||
|
canvas.style.cursor = 'col-resize';
|
||||||
|
startIndex = getIndex(evt);
|
||||||
|
}
|
||||||
|
function onMouseMove(evt) {
|
||||||
|
if (dragStarted) {
|
||||||
|
dragging = true;
|
||||||
|
endIndex = getIndex(evt);
|
||||||
|
if (endIndex !== undefined) {
|
||||||
|
if (startIndex === undefined) {
|
||||||
|
startIndex = endIndex;
|
||||||
|
} else if (startIndex !== endIndex) {
|
||||||
|
$slicedGPXStatistics = [
|
||||||
|
$gpxStatistics.slice(Math.min(startIndex, endIndex), Math.max(startIndex, endIndex)),
|
||||||
|
Math.min(startIndex, endIndex),
|
||||||
|
Math.max(startIndex, endIndex)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onMouseUp(evt) {
|
||||||
|
dragStarted = false;
|
||||||
|
dragging = false;
|
||||||
|
canvas.style.cursor = '';
|
||||||
|
endIndex = getIndex(evt);
|
||||||
|
if (startIndex === endIndex) {
|
||||||
|
$slicedGPXStatistics = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.addEventListener('pointerdown', onMouseDown);
|
||||||
|
canvas.addEventListener('pointermove', onMouseMove);
|
||||||
|
canvas.addEventListener('pointerup', onMouseUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
$: if (chart && $distanceUnits && $velocityUnits && $temperatureUnits) {
|
||||||
|
let data = $gpxStatistics;
|
||||||
|
|
||||||
|
// update data
|
||||||
|
chart.data.datasets[0] = {
|
||||||
|
label: $_('quantities.elevation'),
|
||||||
|
data: data.local.points.map((point, index) => {
|
||||||
|
return {
|
||||||
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
|
y: point.ele ? getConvertedElevation(point.ele) : 0,
|
||||||
|
time: point.time,
|
||||||
|
slope: {
|
||||||
|
at: data.local.slope.at[index],
|
||||||
|
segment: data.local.slope.segment[index],
|
||||||
|
length: data.local.slope.length[index]
|
||||||
|
},
|
||||||
|
surface: point.getSurface(),
|
||||||
|
coordinates: point.getCoordinates(),
|
||||||
|
index: index
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
normalized: true,
|
||||||
|
fill: 'start',
|
||||||
|
order: 1
|
||||||
|
};
|
||||||
|
chart.data.datasets[1] = {
|
||||||
|
label: datasets.speed.getLabel(),
|
||||||
|
data: data.local.points.map((point, index) => {
|
||||||
|
return {
|
||||||
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
|
y: getConvertedVelocity(data.local.speed[index]),
|
||||||
|
index: index
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
normalized: true,
|
||||||
|
yAxisID: `y${datasets.speed.id}`,
|
||||||
|
hidden: true
|
||||||
|
};
|
||||||
|
chart.data.datasets[2] = {
|
||||||
|
label: datasets.hr.getLabel(),
|
||||||
|
data: data.local.points.map((point, index) => {
|
||||||
|
return {
|
||||||
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
|
y: point.getHeartRate(),
|
||||||
|
index: index
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
normalized: true,
|
||||||
|
yAxisID: `y${datasets.hr.id}`,
|
||||||
|
hidden: true
|
||||||
|
};
|
||||||
|
chart.data.datasets[3] = {
|
||||||
|
label: datasets.cad.getLabel(),
|
||||||
|
data: data.local.points.map((point, index) => {
|
||||||
|
return {
|
||||||
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
|
y: point.getCadence(),
|
||||||
|
index: index
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
normalized: true,
|
||||||
|
yAxisID: `y${datasets.cad.id}`,
|
||||||
|
hidden: true
|
||||||
|
};
|
||||||
|
chart.data.datasets[4] = {
|
||||||
|
label: datasets.atemp.getLabel(),
|
||||||
|
data: data.local.points.map((point, index) => {
|
||||||
|
return {
|
||||||
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
|
y: getConvertedTemperature(point.getTemperature()),
|
||||||
|
index: index
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
normalized: true,
|
||||||
|
yAxisID: `y${datasets.atemp.id}`,
|
||||||
|
hidden: true
|
||||||
|
};
|
||||||
|
chart.data.datasets[5] = {
|
||||||
|
label: datasets.power.getLabel(),
|
||||||
|
data: data.local.points.map((point, index) => {
|
||||||
|
return {
|
||||||
|
x: getConvertedDistance(data.local.distance.total[index]),
|
||||||
|
y: point.getPower(),
|
||||||
|
index: index
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
normalized: true,
|
||||||
|
yAxisID: `y${datasets.power.id}`,
|
||||||
|
hidden: true
|
||||||
|
};
|
||||||
|
chart.options.scales.x['min'] = 0;
|
||||||
|
chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);
|
||||||
|
|
||||||
|
// update units
|
||||||
|
for (let [id, dataset] of Object.entries(datasets)) {
|
||||||
|
chart.options.scales[`y${id}`].title.text =
|
||||||
|
dataset.getLabel() + ' (' + dataset.getUnits() + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxSlope = 20;
|
||||||
|
function slopeFillCallback(context) {
|
||||||
|
let slope = context.p0.raw.slope.segment;
|
||||||
|
if (slope > maxSlope) {
|
||||||
|
slope = maxSlope;
|
||||||
|
} else if (slope < -maxSlope) {
|
||||||
|
slope = -maxSlope;
|
||||||
|
}
|
||||||
|
|
||||||
|
let v = slope / maxSlope;
|
||||||
|
v = 1 / (1 + Math.exp(-6 * v));
|
||||||
|
v = v - 0.5;
|
||||||
|
|
||||||
|
let hue = ((0.5 - v) * 120).toString(10);
|
||||||
|
let lightness = 90 - Math.abs(v) * 70;
|
||||||
|
|
||||||
|
return ['hsl(', hue, ',70%,', lightness, '%)'].join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function surfaceFillCallback(context) {
|
||||||
|
let surface = context.p0.raw.surface;
|
||||||
|
return surfaceColors[surface] ? surfaceColors[surface] : surfaceColors.missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (chart) {
|
||||||
|
if (elevationFill === 'slope') {
|
||||||
|
chart.data.datasets[0]['segment'] = {
|
||||||
|
backgroundColor: slopeFillCallback
|
||||||
|
};
|
||||||
|
} else if (elevationFill === 'surface') {
|
||||||
|
chart.data.datasets[0]['segment'] = {
|
||||||
|
backgroundColor: surfaceFillCallback
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
chart.data.datasets[0]['segment'] = {};
|
||||||
|
}
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (additionalDatasets && chart) {
|
||||||
|
let includeSpeed = additionalDatasets.includes('speed');
|
||||||
|
let includeHeartRate = additionalDatasets.includes('hr');
|
||||||
|
let includeCadence = additionalDatasets.includes('cad');
|
||||||
|
let includeTemperature = additionalDatasets.includes('atemp');
|
||||||
|
let includePower = additionalDatasets.includes('power');
|
||||||
|
if (chart.data.datasets.length > 0) {
|
||||||
|
chart.data.datasets[1].hidden = !includeSpeed;
|
||||||
|
chart.data.datasets[2].hidden = !includeHeartRate;
|
||||||
|
chart.data.datasets[3].hidden = !includeCadence;
|
||||||
|
chart.data.datasets[4].hidden = !includeTemperature;
|
||||||
|
chart.data.datasets[5].hidden = !includePower;
|
||||||
|
}
|
||||||
|
chart.options.scales[`y${datasets.speed.id}`].display = includeSpeed && showAdditionalScales;
|
||||||
|
chart.options.scales[`y${datasets.hr.id}`].display = includeHeartRate && showAdditionalScales;
|
||||||
|
chart.options.scales[`y${datasets.cad.id}`].display = includeCadence && showAdditionalScales;
|
||||||
|
chart.options.scales[`y${datasets.atemp.id}`].display =
|
||||||
|
includeTemperature && showAdditionalScales;
|
||||||
|
chart.options.scales[`y${datasets.power.id}`].display = includePower && showAdditionalScales;
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOverlay() {
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.width = canvas.width / window.devicePixelRatio;
|
||||||
|
overlay.height = canvas.height / window.devicePixelRatio;
|
||||||
|
|
||||||
|
if ($slicedGPXStatistics) {
|
||||||
|
let startIndex = $slicedGPXStatistics[1];
|
||||||
|
let endIndex = $slicedGPXStatistics[2];
|
||||||
|
|
||||||
|
// Draw selection rectangle
|
||||||
|
let selectionContext = overlay.getContext('2d');
|
||||||
|
if (selectionContext) {
|
||||||
|
selectionContext.fillStyle = $mode === 'dark' ? 'white' : 'black';
|
||||||
|
selectionContext.globalAlpha = $mode === 'dark' ? 0.2 : 0.1;
|
||||||
|
selectionContext.clearRect(0, 0, overlay.width, overlay.height);
|
||||||
|
|
||||||
|
let startPixel = chart.scales.x.getPixelForValue(
|
||||||
|
getConvertedDistance($gpxStatistics.local.distance.total[startIndex])
|
||||||
|
);
|
||||||
|
let endPixel = chart.scales.x.getPixelForValue(
|
||||||
|
getConvertedDistance($gpxStatistics.local.distance.total[endIndex])
|
||||||
|
);
|
||||||
|
|
||||||
|
selectionContext.fillRect(
|
||||||
|
startPixel,
|
||||||
|
chart.chartArea.top,
|
||||||
|
endPixel - startPixel,
|
||||||
|
chart.chartArea.bottom - chart.chartArea.top
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (overlay) {
|
||||||
|
let selectionContext = overlay.getContext('2d');
|
||||||
|
if (selectionContext) {
|
||||||
|
selectionContext.clearRect(0, 0, overlay.width, overlay.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: $slicedGPXStatistics, $mode, updateOverlay();
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (chart) {
|
||||||
|
chart.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="h-full grow min-w-0 flex flex-row gap-4 items-center {$$props.class ?? ''}">
|
||||||
|
<div class="grow h-full min-w-0 relative">
|
||||||
|
<canvas bind:this={overlay} class=" w-full h-full absolute pointer-events-none"></canvas>
|
||||||
|
<canvas bind:this={canvas} class="w-full h-full"></canvas>
|
||||||
|
</div>
|
||||||
|
{#if showControls}
|
||||||
|
<div class="h-full flex flex-col justify-center" style="width: {panelSize > 158 ? 22 : 42}px">
|
||||||
|
<ToggleGroup.Root
|
||||||
|
class="{panelSize > 158
|
||||||
|
? 'flex-col'
|
||||||
|
: 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-t-md"
|
||||||
|
type="single"
|
||||||
|
bind:value={elevationFill}
|
||||||
|
>
|
||||||
|
<ToggleGroup.Item class="p-0 w-5 h-5" value="slope" aria-label={$_('chart.show_slope')}>
|
||||||
|
<Tooltip side="left" label={$_('chart.show_slope')}>
|
||||||
|
<TriangleRight size="15" />
|
||||||
|
</Tooltip>
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
<ToggleGroup.Item class="p-0 w-5 h-5" value="surface" aria-label={$_('chart.show_surface')}>
|
||||||
|
<Tooltip side="left" label={$_('chart.show_surface')}>
|
||||||
|
<BrickWall size="15" />
|
||||||
|
</Tooltip>
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
</ToggleGroup.Root>
|
||||||
|
<ToggleGroup.Root
|
||||||
|
class="{panelSize > 158
|
||||||
|
? 'flex-col'
|
||||||
|
: 'flex-row'} flex-wrap gap-0 min-h-0 content-center border rounded-b-md -mt-[1px]"
|
||||||
|
type="multiple"
|
||||||
|
bind:value={additionalDatasets}
|
||||||
|
>
|
||||||
|
<ToggleGroup.Item
|
||||||
|
class="p-0 w-5 h-5"
|
||||||
|
value="speed"
|
||||||
|
aria-label={$velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
side="left"
|
||||||
|
label={$velocityUnits === 'speed' ? $_('chart.show_speed') : $_('chart.show_pace')}
|
||||||
|
>
|
||||||
|
<Zap size="15" />
|
||||||
|
</Tooltip>
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
<ToggleGroup.Item class="p-0 w-5 h-5" value="hr" aria-label={$_('chart.show_heartrate')}>
|
||||||
|
<Tooltip side="left" label={$_('chart.show_heartrate')}>
|
||||||
|
<HeartPulse size="15" />
|
||||||
|
</Tooltip>
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
<ToggleGroup.Item class="p-0 w-5 h-5" value="cad" aria-label={$_('chart.show_cadence')}>
|
||||||
|
<Tooltip side="left" label={$_('chart.show_cadence')}>
|
||||||
|
<Orbit size="15" />
|
||||||
|
</Tooltip>
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
<ToggleGroup.Item
|
||||||
|
class="p-0 w-5 h-5"
|
||||||
|
value="atemp"
|
||||||
|
aria-label={$_('chart.show_temperature')}
|
||||||
|
>
|
||||||
|
<Tooltip side="left" label={$_('chart.show_temperature')}>
|
||||||
|
<Thermometer size="15" />
|
||||||
|
</Tooltip>
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
<ToggleGroup.Item class="p-0 w-5 h-5" value="power" aria-label={$_('chart.show_power')}>
|
||||||
|
<Tooltip side="left" label={$_('chart.show_power')}>
|
||||||
|
<SquareActivity size="15" />
|
||||||
|
</Tooltip>
|
||||||
|
</ToggleGroup.Item>
|
||||||
|
</ToggleGroup.Root>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
186
website/src/lib/components/Export.svelte
Executable file
@ -0,0 +1,186 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Label } from '$lib/components/ui/label';
|
||||||
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
|
import { Dialog } from 'bits-ui';
|
||||||
|
import {
|
||||||
|
currentTool,
|
||||||
|
exportAllFiles,
|
||||||
|
exportSelectedFiles,
|
||||||
|
ExportState,
|
||||||
|
exportState,
|
||||||
|
gpxStatistics
|
||||||
|
} from '$lib/stores';
|
||||||
|
import { fileObservers } from '$lib/db';
|
||||||
|
import {
|
||||||
|
Download,
|
||||||
|
Zap,
|
||||||
|
BrickWall,
|
||||||
|
HeartPulse,
|
||||||
|
Orbit,
|
||||||
|
Thermometer,
|
||||||
|
SquareActivity
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { selection } from './file-list/Selection';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { GPXStatistics } from 'gpx';
|
||||||
|
import { ListRootItem } from './file-list/FileList';
|
||||||
|
|
||||||
|
let open = false;
|
||||||
|
let exportOptions: Record<string, boolean> = {
|
||||||
|
time: true,
|
||||||
|
surface: true,
|
||||||
|
hr: true,
|
||||||
|
cad: true,
|
||||||
|
atemp: true,
|
||||||
|
power: true
|
||||||
|
};
|
||||||
|
let hide: Record<string, boolean> = {
|
||||||
|
time: false,
|
||||||
|
surface: false,
|
||||||
|
hr: false,
|
||||||
|
cad: false,
|
||||||
|
atemp: false,
|
||||||
|
power: false
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if ($exportState !== ExportState.NONE) {
|
||||||
|
open = true;
|
||||||
|
$currentTool = null;
|
||||||
|
|
||||||
|
let statistics = $gpxStatistics;
|
||||||
|
if ($exportState === ExportState.ALL) {
|
||||||
|
statistics = Array.from($fileObservers.values())
|
||||||
|
.map((file) => get(file)?.statistics)
|
||||||
|
.reduce((acc, cur) => {
|
||||||
|
if (cur !== undefined) {
|
||||||
|
acc.mergeWith(cur.getStatisticsFor(new ListRootItem()));
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, new GPXStatistics());
|
||||||
|
}
|
||||||
|
|
||||||
|
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.cad = statistics.global.cad.count === 0;
|
||||||
|
hide.atemp = statistics.global.atemp.count === 0;
|
||||||
|
hide.power = statistics.global.power.count === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: exclude = Object.keys(exportOptions).filter((key) => !exportOptions[key]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog.Root
|
||||||
|
bind:open
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
$exportState = ExportState.NONE;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog.Trigger class="hidden" />
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Content
|
||||||
|
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 style="display: none"
|
||||||
|
class="w-full flex flex-row items-center justify-center gap-4 border rounded-md p-2 bg-secondary"
|
||||||
|
>
|
||||||
|
<span>⚠️</span>
|
||||||
|
<span class="max-w-[80%] text-sm">
|
||||||
|
{$_('menu.support_message')}
|
||||||
|
</span>
|
||||||
|
<Button class="bg-support grow" href="https://ko-fi.com/gpxstudio" target="_blank">
|
||||||
|
{$_('menu.support_button')}
|
||||||
|
<span class="ml-2">🙏</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex flex-row flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="grow"
|
||||||
|
on:click={() => {
|
||||||
|
if ($exportState === ExportState.SELECTION) {
|
||||||
|
exportSelectedFiles(exclude);
|
||||||
|
} else if ($exportState === ExportState.ALL) {
|
||||||
|
exportAllFiles(exclude);
|
||||||
|
}
|
||||||
|
open = false;
|
||||||
|
$exportState = ExportState.NONE;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Download size="16" class="mr-1" />
|
||||||
|
{#if $fileObservers.size === 1 || ($exportState === ExportState.SELECTION && $selection.size === 1)}
|
||||||
|
{$_('menu.download_file')}
|
||||||
|
{:else}
|
||||||
|
{$_('menu.download_files')}
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<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="grow">
|
||||||
|
<Separator />
|
||||||
|
</div>
|
||||||
|
<Label class="shrink-0">
|
||||||
|
{$_('menu.export_options')}
|
||||||
|
</Label>
|
||||||
|
<div class="grow">
|
||||||
|
<Separator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row flex-wrap justify-center gap-x-6 gap-y-2">
|
||||||
|
<div class="flex flex-row items-center gap-1.5 {hide.time ? 'hidden' : ''}">
|
||||||
|
<Checkbox id="export-time" bind:checked={exportOptions.time} />
|
||||||
|
<Label for="export-time" class="flex flex-row items-center gap-1">
|
||||||
|
<Zap size="16" />
|
||||||
|
{$_('quantities.time')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-1.5 {hide.surface ? 'hidden' : ''}">
|
||||||
|
<Checkbox id="export-surface" bind:checked={exportOptions.surface} />
|
||||||
|
<Label for="export-surface" class="flex flex-row items-center gap-1">
|
||||||
|
<BrickWall size="16" />
|
||||||
|
{$_('quantities.surface')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-1.5 {hide.hr ? 'hidden' : ''}">
|
||||||
|
<Checkbox id="export-heartrate" bind:checked={exportOptions.hr} />
|
||||||
|
<Label for="export-heartrate" class="flex flex-row items-center gap-1">
|
||||||
|
<HeartPulse size="16" />
|
||||||
|
{$_('quantities.heartrate')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-1.5 {hide.cad ? 'hidden' : ''}">
|
||||||
|
<Checkbox id="export-cadence" bind:checked={exportOptions.cad} />
|
||||||
|
<Label for="export-cadence" class="flex flex-row items-center gap-1">
|
||||||
|
<Orbit size="16" />
|
||||||
|
{$_('quantities.cadence')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-1.5 {hide.atemp ? 'hidden' : ''}">
|
||||||
|
<Checkbox id="export-temperature" bind:checked={exportOptions.atemp} />
|
||||||
|
<Label for="export-temperature" class="flex flex-row items-center gap-1">
|
||||||
|
<Thermometer size="16" />
|
||||||
|
{$_('quantities.temperature')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-1.5 {hide.power ? 'hidden' : ''}">
|
||||||
|
<Checkbox id="export-power" bind:checked={exportOptions.power} />
|
||||||
|
<Label for="export-power" class="flex flex-row items-center gap-1">
|
||||||
|
<SquareActivity size="16" />
|
||||||
|
{$_('quantities.power')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
125
website/src/lib/components/Footer.svelte
Executable file
@ -0,0 +1,125 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import LanguageSelect from '$lib/components/LanguageSelect.svelte';
|
||||||
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
|
import { AtSign, BookOpenText, Heart, Home, Map } from 'lucide-svelte';
|
||||||
|
import { _, locale } from 'svelte-i18n';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<footer class="w-full">
|
||||||
|
<div class="mx-6 border-t">
|
||||||
|
<div class="mx-12 py-10 flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
|
||||||
|
<div class="grow flex flex-col items-start">
|
||||||
|
<Logo class="h-8" width="153" />
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
class="h-6 px-0 text-muted-foreground"
|
||||||
|
href="https://github.com/gpxstudio/gpx.rnmkcy.eu/blob/main/LICENSE"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
MIT © 2024 gpx.rnmkcy.eu
|
||||||
|
</Button>
|
||||||
|
<LanguageSelect class="w-40 mt-3" />
|
||||||
|
</div>
|
||||||
|
<div class="grow max-w-2xl flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
|
||||||
|
<div class="flex flex-col items-start gap-1">
|
||||||
|
<span class="font-semibold">{$_('homepage.website')}</span>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
class="h-6 px-0 text-muted-foreground"
|
||||||
|
href={getURLForLanguage($locale, '/')}
|
||||||
|
>
|
||||||
|
<Home size="16" class="mr-1" />
|
||||||
|
{$_('homepage.home')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
class="h-6 px-0 text-muted-foreground"
|
||||||
|
href={getURLForLanguage($locale, '/app')}
|
||||||
|
>
|
||||||
|
<Map size="16" class="mr-1" />
|
||||||
|
{$_('homepage.app')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
class="h-6 px-0 text-muted-foreground"
|
||||||
|
href={getURLForLanguage($locale, '/help')}
|
||||||
|
>
|
||||||
|
<BookOpenText size="16" class="mr-1" />
|
||||||
|
{$_('menu.help')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-start gap-1" id="contact">
|
||||||
|
<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
|
||||||
|
variant="link"
|
||||||
|
class="h-6 px-0 text-muted-foreground"
|
||||||
|
href="https://facebook.com/gpx.rnmkcy.eu"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Logo company="facebook" class="h-4 mr-1 fill-muted-foreground" />
|
||||||
|
{$_('homepage.facebook')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
class="h-6 px-0 text-muted-foreground"
|
||||||
|
href="https://x.com/gpxstudio"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Logo company="x" class="h-4 mr-1 fill-muted-foreground" />
|
||||||
|
{$_('homepage.x')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
class="h-6 px-0 text-muted-foreground"
|
||||||
|
href="mailto:hello@gpx.rnmkcy.eu"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<AtSign size="16" class="mr-1" />
|
||||||
|
{$_('homepage.email')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-start gap-1">
|
||||||
|
<span class="font-semibold">{$_('homepage.contribute')}</span>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
class="h-6 px-0 text-muted-foreground"
|
||||||
|
href="https://ko-fi.com/gpxstudio"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Heart size="16" class="mr-1" />
|
||||||
|
{$_('menu.donate')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
class="h-6 px-0 text-muted-foreground"
|
||||||
|
href="https://crowdin.com/project/gpxstudio"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Logo company="crowdin" class="h-4 mr-1 fill-muted-foreground" />
|
||||||
|
{$_('homepage.crowdin')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
class="h-6 px-0 text-muted-foreground"
|
||||||
|
href="https://github.com/gpxstudio/gpx.rnmkcy.eu"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<Logo company="github" class="h-4 mr-1 fill-muted-foreground" />
|
||||||
|
{$_('homepage.github')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
82
website/src/lib/components/GPXStatistics.svelte
Executable file
@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import Tooltip from '$lib/components/Tooltip.svelte';
|
||||||
|
import WithUnits from '$lib/components/WithUnits.svelte';
|
||||||
|
|
||||||
|
import { MoveDownRight, MoveUpRight, Ruler, Timer, Zap } from 'lucide-svelte';
|
||||||
|
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
import type { GPXStatistics } from 'gpx';
|
||||||
|
import type { Writable } from 'svelte/store';
|
||||||
|
import { settings } from '$lib/db';
|
||||||
|
|
||||||
|
export let gpxStatistics: Writable<GPXStatistics>;
|
||||||
|
export let slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
|
||||||
|
export let orientation: 'horizontal' | 'vertical';
|
||||||
|
export let panelSize: number;
|
||||||
|
|
||||||
|
const { velocityUnits } = settings;
|
||||||
|
|
||||||
|
let statistics: GPXStatistics;
|
||||||
|
|
||||||
|
$: if ($slicedGPXStatistics !== undefined) {
|
||||||
|
statistics = $slicedGPXStatistics[0];
|
||||||
|
} else {
|
||||||
|
statistics = $gpxStatistics;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card.Root
|
||||||
|
class="h-full {orientation === 'vertical'
|
||||||
|
? 'min-w-44 sm:min-w-52 text-sm sm:text-base'
|
||||||
|
: 'w-full'} border-none shadow-none"
|
||||||
|
>
|
||||||
|
<Card.Content
|
||||||
|
class="h-full flex {orientation === 'vertical'
|
||||||
|
? 'flex-col justify-center'
|
||||||
|
: 'flex-row w-full justify-between'} gap-4 p-0"
|
||||||
|
>
|
||||||
|
<Tooltip label={$_('quantities.distance')}>
|
||||||
|
<span class="flex flex-row items-center">
|
||||||
|
<Ruler size="18" class="mr-1" />
|
||||||
|
<WithUnits value={statistics.global.distance.total} type="distance" />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label={$_('quantities.elevation_gain_loss')}>
|
||||||
|
<span class="flex flex-row items-center">
|
||||||
|
<MoveUpRight size="18" class="mr-1" />
|
||||||
|
<WithUnits value={statistics.global.elevation.gain} type="elevation" />
|
||||||
|
<MoveDownRight size="18" class="mx-1" />
|
||||||
|
<WithUnits value={statistics.global.elevation.loss} type="elevation" />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
{#if panelSize > 120 || orientation === 'horizontal'}
|
||||||
|
<Tooltip
|
||||||
|
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" />
|
||||||
|
<WithUnits value={statistics.global.speed.moving} type="speed" showUnits={false} />
|
||||||
|
<span class="mx-1">/</span>
|
||||||
|
<WithUnits value={statistics.global.speed.total} type="speed" />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
{#if panelSize > 160 || orientation === 'horizontal'}
|
||||||
|
<Tooltip
|
||||||
|
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" />
|
||||||
|
<WithUnits value={statistics.global.time.moving} type="time" />
|
||||||
|
<span class="mx-1">/</span>
|
||||||
|
<WithUnits value={statistics.global.time.total} type="time" />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
20
website/src/lib/components/Help.svelte
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { CircleHelp } from 'lucide-svelte';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
|
export let link: string | undefined = undefined;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
<div>
|
||||||
|
<slot />
|
||||||
|
{#if link}
|
||||||
|
<a href={link} target="_blank" class="text-sm text-link hover:underline">
|
||||||
|
{$_('menu.more')}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
51
website/src/lib/components/LanguageSelect.svelte
Executable file
@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import * as Select from '$lib/components/ui/select';
|
||||||
|
import { languages } from '$lib/languages';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
import { Languages } from 'lucide-svelte';
|
||||||
|
import { _, locale } from 'svelte-i18n';
|
||||||
|
|
||||||
|
let selected = {
|
||||||
|
value: '',
|
||||||
|
label: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if ($locale) {
|
||||||
|
selected = {
|
||||||
|
value: $locale,
|
||||||
|
label: languages[$locale]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Select.Root bind:selected>
|
||||||
|
<Select.Trigger class="w-[180px] {$$props.class ?? ''}" aria-label={$_('menu.language')}>
|
||||||
|
<Languages size="16" />
|
||||||
|
<Select.Value class="ml-2 mr-auto" />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{#each Object.entries(languages) as [lang, label]}
|
||||||
|
{#if $page.url.pathname.includes('404')}
|
||||||
|
<a href={getURLForLanguage(lang, '/')}>
|
||||||
|
<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}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
|
||||||
|
<!-- hidden links for svelte crawling -->
|
||||||
|
<div class="hidden">
|
||||||
|
{#if !$page.url.pathname.includes('404')}
|
||||||
|
{#each Object.entries(languages) as [lang, label]}
|
||||||
|
<a href={getURLForLanguage(lang, $page.url.pathname)}>
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
73
website/src/lib/components/Logo.svelte
Executable file
@ -0,0 +1,73 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { base } from '$app/paths';
|
||||||
|
import { mode, systemPrefersMode } from 'mode-watcher';
|
||||||
|
|
||||||
|
export let iconOnly = false;
|
||||||
|
export let company = 'gpx.rnmkcy.eu';
|
||||||
|
|
||||||
|
$: effectiveMode = $mode ?? $systemPrefersMode ?? 'light';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if company === 'gpx.rnmkcy.eu'}
|
||||||
|
<img
|
||||||
|
src="{base}/{iconOnly ? 'icon' : 'logo'}{effectiveMode === 'dark' ? '-dark' : ''}.svg"
|
||||||
|
alt="Logo of gpx.rnmkcy.eu."
|
||||||
|
{...$$restProps}
|
||||||
|
/>
|
||||||
|
{:else if company === 'mapbox'}
|
||||||
|
<img
|
||||||
|
src="{base}/mapbox-logo-{effectiveMode === 'dark' ? 'white' : 'black'}.svg"
|
||||||
|
alt="Logo of Mapbox."
|
||||||
|
{...$$restProps}
|
||||||
|
/>
|
||||||
|
{:else if company === 'github'}
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="fill-foreground {$$restProps.class ?? ''}"
|
||||||
|
><title>GitHub</title><path
|
||||||
|
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
{:else if company === 'crowdin'}
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="fill-foreground {$$restProps.class ?? ''}"
|
||||||
|
><title>Crowdin</title><path
|
||||||
|
d="M16.119 17.793a2.619 2.619 0 0 1-1.667-.562c-.546-.436-1.004-1.09-1.018-1.858-.008-.388.414-.388.414-.388l1.018-.008c.332.008.43.47.445.586.128 1.04.717 1.495 1.168 1.702.273.123.204.513-.362.528zm-5.695-5.287L8.5 12.252c-.867-.214-.844-.982-.807-1.247a5.119 5.119 0 0 1 .814-2.125c.545-.804 1.303-1.508 2.29-2.073 1.856-1.074 4.45-1.673 7.31-1.673 2.09 0 4.256.27 4.29.27.197.025.328.213.333.437a.377.377 0 0 1-.355.393l-.92-.01c-2.902 0-4.968.394-6.506 1.248-1.527.837-2.57 2.117-3.287 4.012-.076.163-.335 1.12-1.24 1.022zm2.533 7.823c-1.44 0-2.797-.622-3.825-1.746-.87-.96-1.397-1.931-1.493-3.164-.06-.813.3-1.094.788-1.044l1.988.218c.45.092.75.34.825.854.397 2.736 2.122 3.814 3.15 4.046.18.042.292.157.283.365a.412.412 0 0 1-.322.398c-.458.074-.936.073-1.394.073zm-4.101 2.418a14.216 14.216 0 0 1-2.307-.214c-1.202-.214-2.208-.582-3.072-1.13C1.41 20.095.163 17.786.014 15.048c-.037-.65-.11-1.89 1.427-1.797.638.033 1.653.343 2.368.548.887.247 1.314.933 1.314 1.608 0 3.858 3.494 6.408 5.02 6.408.654 0 .414.701.127.779-.502.136-1.15.153-1.413.153zM3.525 11.419c-.605-.109-1.194-.358-1.768-.5C-.018 10.479.284 8.688.45 8.196c1.617-4.757 6.746-6.35 10.887-6.773 3.898-.4 7.978-.092 11.778.967.31.083 1.269.327.718.891-.35.358-1.7-.016-2.073-.041-2.23-.167-4.434-.192-6.656.15-2.349.357-4.768 1.099-6.71 2.665-.938.758-1.76 1.723-2.313 2.866-.144.3-.256.6-.354.9-.11.327-.47 1.91-2.215 1.6zm9.94.917c.332-1.488 1.81-3.848 6.385-3.686 1.05.033.57.749.052.731-2.586-.09-3.815 1.578-4.457 3.27-.219.546-.68.626-1.271.53-.415-.074-.866-.123-.71-.846Z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
{:else if company === 'facebook'}
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="fill-foreground {$$restProps.class ?? ''}"
|
||||||
|
><title>Facebook</title><path
|
||||||
|
d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
{:else if company === 'x'}
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="fill-foreground {$$restProps.class ?? ''}"
|
||||||
|
><title>X</title><path
|
||||||
|
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
|
||||||
|
>
|
||||||
|
{: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}
|
380
website/src/lib/components/Map.svelte
Executable file
@ -0,0 +1,380 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
|
||||||
|
import mapboxgl from 'mapbox-gl';
|
||||||
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
|
||||||
|
import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder';
|
||||||
|
import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css';
|
||||||
|
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { map } from '$lib/stores';
|
||||||
|
import { settings } from '$lib/db';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
|
export let accessToken = PUBLIC_MAPBOX_TOKEN;
|
||||||
|
export let geolocate = true;
|
||||||
|
export let geocoder = true;
|
||||||
|
export let hash = true;
|
||||||
|
|
||||||
|
mapboxgl.accessToken = accessToken;
|
||||||
|
|
||||||
|
let webgl2Supported = true;
|
||||||
|
let fitBoundsOptions: mapboxgl.FitBoundsOptions = {
|
||||||
|
maxZoom: 15,
|
||||||
|
linear: true,
|
||||||
|
easing: () => 1
|
||||||
|
};
|
||||||
|
|
||||||
|
const { distanceUnits, elevationProfile, verticalFileView, bottomPanelSize, rightPanelSize } =
|
||||||
|
settings;
|
||||||
|
let scaleControl = new mapboxgl.ScaleControl({
|
||||||
|
unit: $distanceUnits
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
let gl = document.createElement('canvas').getContext('webgl2');
|
||||||
|
if (!gl) {
|
||||||
|
webgl2Supported = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let language = $page.params.language;
|
||||||
|
if (language === 'zh') {
|
||||||
|
language = 'zh-Hans';
|
||||||
|
} else if (language?.includes('-')) {
|
||||||
|
language = language.split('-')[0];
|
||||||
|
} else if (language === '' || language === undefined) {
|
||||||
|
language = 'fr';
|
||||||
|
}
|
||||||
|
|
||||||
|
let newMap = new mapboxgl.Map({
|
||||||
|
container: 'map',
|
||||||
|
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,
|
||||||
|
hash: hash,
|
||||||
|
language,
|
||||||
|
attributionControl: false,
|
||||||
|
logoPosition: 'bottom-right',
|
||||||
|
boxZoom: false
|
||||||
|
});
|
||||||
|
newMap.on('load', () => {
|
||||||
|
$map = newMap; // only set the store after the map has loaded
|
||||||
|
window._map = newMap; // entry point for extensions
|
||||||
|
scaleControl.setUnit($distanceUnits);
|
||||||
|
});
|
||||||
|
|
||||||
|
newMap.addControl(
|
||||||
|
new mapboxgl.AttributionControl({
|
||||||
|
compact: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
newMap.addControl(
|
||||||
|
new mapboxgl.NavigationControl({
|
||||||
|
visualizePitch: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (geocoder) {
|
||||||
|
let geocoder = new MapboxGeocoder({
|
||||||
|
mapboxgl: mapboxgl,
|
||||||
|
enableEventLogging: false,
|
||||||
|
collapsed: true,
|
||||||
|
flyTo: fitBoundsOptions,
|
||||||
|
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) {
|
||||||
|
newMap.addControl(
|
||||||
|
new mapboxgl.GeolocateControl({
|
||||||
|
positionOptions: {
|
||||||
|
enableHighAccuracy: true
|
||||||
|
},
|
||||||
|
fitBoundsOptions,
|
||||||
|
trackUserLocation: true,
|
||||||
|
showUserHeading: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
newMap.addControl(scaleControl);
|
||||||
|
|
||||||
|
newMap.on('style.load', () => {
|
||||||
|
newMap.addSource('mapbox-dem', {
|
||||||
|
type: 'raster-dem',
|
||||||
|
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
||||||
|
tileSize: 512,
|
||||||
|
maxzoom: 14
|
||||||
|
});
|
||||||
|
if (newMap.getPitch() > 0) {
|
||||||
|
newMap.setTerrain({
|
||||||
|
source: 'mapbox-dem',
|
||||||
|
exaggeration: 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
newMap.setFog({
|
||||||
|
color: 'rgb(186, 210, 235)',
|
||||||
|
'high-color': 'rgb(36, 92, 223)',
|
||||||
|
'horizon-blend': 0.1,
|
||||||
|
'space-color': 'rgb(156, 240, 255)'
|
||||||
|
});
|
||||||
|
newMap.on('pitch', () => {
|
||||||
|
if (newMap.getPitch() > 0) {
|
||||||
|
newMap.setTerrain({
|
||||||
|
source: 'mapbox-dem',
|
||||||
|
exaggeration: 1
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
newMap.setTerrain(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if ($map) {
|
||||||
|
$map.remove();
|
||||||
|
$map = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$: if (
|
||||||
|
$map &&
|
||||||
|
(!$verticalFileView || !$elevationProfile || $bottomPanelSize || $rightPanelSize)
|
||||||
|
) {
|
||||||
|
$map.resize();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div {...$$restProps}>
|
||||||
|
<div id="map" class="h-full {webgl2Supported ? '' : 'hidden'}"></div>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center gap-3 h-full {webgl2Supported ? 'hidden' : ''}"
|
||||||
|
>
|
||||||
|
<p>{$_('webgl2_required')}</p>
|
||||||
|
<Button href="https://get.webgl.org/webgl2/" target="_blank">
|
||||||
|
{$_('enable_webgl2')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
div :global(.mapboxgl-map) {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-ctrl-top-right > .mapboxgl-ctrl) {
|
||||||
|
@apply shadow-md;
|
||||||
|
@apply bg-background;
|
||||||
|
@apply text-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-ctrl-icon) {
|
||||||
|
@apply dark:brightness-[4.7];
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-ctrl-geocoder) {
|
||||||
|
@apply flex;
|
||||||
|
@apply flex-row;
|
||||||
|
@apply w-fit;
|
||||||
|
@apply min-w-fit;
|
||||||
|
@apply items-center;
|
||||||
|
@apply shadow-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.suggestions) {
|
||||||
|
@apply shadow-md;
|
||||||
|
@apply bg-background;
|
||||||
|
@apply text-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-ctrl-geocoder .suggestions > li > a) {
|
||||||
|
@apply text-foreground;
|
||||||
|
@apply hover:text-accent-foreground;
|
||||||
|
@apply hover:bg-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-ctrl-geocoder .suggestions > .active > a) {
|
||||||
|
@apply bg-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-ctrl-geocoder--button) {
|
||||||
|
@apply bg-transparent;
|
||||||
|
@apply hover:bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-ctrl-geocoder--icon) {
|
||||||
|
@apply fill-foreground;
|
||||||
|
@apply hover:fill-accent-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-ctrl-geocoder--icon-search) {
|
||||||
|
@apply relative;
|
||||||
|
@apply top-0;
|
||||||
|
@apply left-0;
|
||||||
|
@apply my-2;
|
||||||
|
@apply w-[29px];
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-ctrl-geocoder--input) {
|
||||||
|
@apply relative;
|
||||||
|
@apply w-64;
|
||||||
|
@apply py-0;
|
||||||
|
@apply pl-2;
|
||||||
|
@apply focus:outline-none;
|
||||||
|
@apply transition-[width];
|
||||||
|
@apply duration-200;
|
||||||
|
@apply text-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-ctrl-geocoder--collapsed .mapboxgl-ctrl-geocoder--input) {
|
||||||
|
@apply w-0;
|
||||||
|
@apply p-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-ctrl-top-right) {
|
||||||
|
@apply z-40;
|
||||||
|
@apply flex;
|
||||||
|
@apply flex-col;
|
||||||
|
@apply items-end;
|
||||||
|
@apply h-full;
|
||||||
|
@apply overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal :global(.mapboxgl-ctrl-bottom-left) {
|
||||||
|
@apply bottom-[42px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal :global(.mapboxgl-ctrl-bottom-right) {
|
||||||
|
@apply bottom-[42px];
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-ctrl-attrib) {
|
||||||
|
@apply dark:bg-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-compact-show.mapboxgl-ctrl-attrib) {
|
||||||
|
@apply dark:bg-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-ctrl-attrib-button) {
|
||||||
|
@apply dark:bg-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-compact-show .mapboxgl-ctrl-attrib-button) {
|
||||||
|
@apply dark:bg-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-ctrl-attrib a) {
|
||||||
|
@apply text-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-popup) {
|
||||||
|
@apply w-fit;
|
||||||
|
@apply z-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-popup-content) {
|
||||||
|
@apply p-0;
|
||||||
|
@apply bg-transparent;
|
||||||
|
@apply shadow-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-popup-anchor-top .mapboxgl-popup-tip) {
|
||||||
|
@apply border-b-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip) {
|
||||||
|
@apply border-b-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip) {
|
||||||
|
@apply border-b-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip) {
|
||||||
|
@apply border-t-background;
|
||||||
|
@apply drop-shadow-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip) {
|
||||||
|
@apply border-t-background;
|
||||||
|
@apply drop-shadow-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip) {
|
||||||
|
@apply border-t-background;
|
||||||
|
@apply drop-shadow-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-popup-anchor-left .mapboxgl-popup-tip) {
|
||||||
|
@apply border-r-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
div :global(.mapboxgl-popup-anchor-right .mapboxgl-popup-tip) {
|
||||||
|
@apply border-l-background;
|
||||||
|
}
|
||||||
|
</style>
|
612
website/src/lib/components/Menu.svelte
Executable file
@ -0,0 +1,612 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Menubar from '$lib/components/ui/menubar/index.js';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
|
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Copy,
|
||||||
|
Download,
|
||||||
|
Undo2,
|
||||||
|
Redo2,
|
||||||
|
Trash2,
|
||||||
|
Heart,
|
||||||
|
Map,
|
||||||
|
Layers2,
|
||||||
|
Box,
|
||||||
|
Milestone,
|
||||||
|
Coins,
|
||||||
|
Ruler,
|
||||||
|
Zap,
|
||||||
|
Thermometer,
|
||||||
|
Sun,
|
||||||
|
Moon,
|
||||||
|
Layers,
|
||||||
|
GalleryVertical,
|
||||||
|
Languages,
|
||||||
|
Settings,
|
||||||
|
Info,
|
||||||
|
File,
|
||||||
|
View,
|
||||||
|
FilePen,
|
||||||
|
HeartHandshake,
|
||||||
|
PersonStanding,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
ClipboardCopy,
|
||||||
|
Scissors,
|
||||||
|
ClipboardPaste,
|
||||||
|
PaintBucket,
|
||||||
|
FolderOpen,
|
||||||
|
FileStack,
|
||||||
|
FileX,
|
||||||
|
BookOpenText,
|
||||||
|
ChartArea,
|
||||||
|
Maximize
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
|
import {
|
||||||
|
map,
|
||||||
|
triggerFileInput,
|
||||||
|
createFile,
|
||||||
|
loadFiles,
|
||||||
|
updateSelectionFromKey,
|
||||||
|
allHidden,
|
||||||
|
editMetadata,
|
||||||
|
editStyle,
|
||||||
|
exportState,
|
||||||
|
ExportState,
|
||||||
|
centerMapOnSelection
|
||||||
|
} from '$lib/stores';
|
||||||
|
import {
|
||||||
|
copied,
|
||||||
|
copySelection,
|
||||||
|
cutSelection,
|
||||||
|
pasteSelection,
|
||||||
|
selectAll,
|
||||||
|
selection
|
||||||
|
} from '$lib/components/file-list/Selection';
|
||||||
|
import { derived } from 'svelte/store';
|
||||||
|
import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db';
|
||||||
|
import { anySelectedLayer } from '$lib/components/layer-control/utils';
|
||||||
|
import { defaultOverlays } from '$lib/assets/layers';
|
||||||
|
import LayerControlSettings from '$lib/components/layer-control/LayerControlSettings.svelte';
|
||||||
|
import { allowedPastes, ListFileItem, ListTrackItem } from '$lib/components/file-list/FileList';
|
||||||
|
import Export from '$lib/components/Export.svelte';
|
||||||
|
import { mode, setMode, systemPrefersMode } from 'mode-watcher';
|
||||||
|
import { _, locale } from 'svelte-i18n';
|
||||||
|
import { languages } from '$lib/languages';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
|
||||||
|
const {
|
||||||
|
distanceUnits,
|
||||||
|
velocityUnits,
|
||||||
|
temperatureUnits,
|
||||||
|
elevationProfile,
|
||||||
|
verticalFileView,
|
||||||
|
currentBasemap,
|
||||||
|
previousBasemap,
|
||||||
|
currentOverlays,
|
||||||
|
previousOverlays,
|
||||||
|
distanceMarkers,
|
||||||
|
directionMarkers,
|
||||||
|
streetViewSource,
|
||||||
|
routing
|
||||||
|
} = settings;
|
||||||
|
|
||||||
|
let undoDisabled = derived(canUndo, ($canUndo) => !$canUndo);
|
||||||
|
let redoDisabled = derived(canRedo, ($canRedo) => !$canRedo);
|
||||||
|
|
||||||
|
function switchBasemaps() {
|
||||||
|
[$currentBasemap, $previousBasemap] = [$previousBasemap, $currentBasemap];
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOverlays() {
|
||||||
|
if (anySelectedLayer($currentOverlays)) {
|
||||||
|
[$currentOverlays, $previousOverlays] = [defaultOverlays, $currentOverlays];
|
||||||
|
} else {
|
||||||
|
[$currentOverlays, $previousOverlays] = [$previousOverlays, defaultOverlays];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle3D() {
|
||||||
|
if ($map) {
|
||||||
|
if ($map.getPitch() === 0) {
|
||||||
|
$map.easeTo({ pitch: 70 });
|
||||||
|
} else {
|
||||||
|
$map.easeTo({ pitch: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let layerSettingsOpen = false;
|
||||||
|
|
||||||
|
$: selectedMode = $mode ?? $systemPrefersMode ?? 'light';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="absolute md:top-2 left-0 right-0 z-20 flex flex-row justify-center pointer-events-none">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<a href="./" target="_blank" class="shrink-0">
|
||||||
|
<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" width="96" />
|
||||||
|
</a>
|
||||||
|
<Menubar.Root class="border-none h-fit p-0">
|
||||||
|
<Menubar.Menu>
|
||||||
|
<Menubar.Trigger aria-label={$_('gpx.file')}>
|
||||||
|
<File size="18" class="md:hidden" />
|
||||||
|
<span class="hidden md:block">{$_('gpx.file')}</span>
|
||||||
|
</Menubar.Trigger>
|
||||||
|
<Menubar.Content class="border-none">
|
||||||
|
<Menubar.Item on:click={createFile}>
|
||||||
|
<Plus size="16" class="mr-1" />
|
||||||
|
{$_('menu.new')}
|
||||||
|
<Shortcut key="+" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Item on:click={triggerFileInput}>
|
||||||
|
<FolderOpen size="16" class="mr-1" />
|
||||||
|
{$_('menu.open')}
|
||||||
|
<Shortcut key="O" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Item on:click={dbUtils.duplicateSelection} disabled={$selection.size == 0}>
|
||||||
|
<Copy size="16" class="mr-1" />
|
||||||
|
{$_('menu.duplicate')}
|
||||||
|
<Shortcut key="D" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Item on:click={dbUtils.deleteSelectedFiles} disabled={$selection.size == 0}>
|
||||||
|
<FileX size="16" class="mr-1" />
|
||||||
|
{$_('menu.close')}
|
||||||
|
<Shortcut key="⌫" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Item on:click={dbUtils.deleteAllFiles} disabled={$fileObservers.size == 0}>
|
||||||
|
<FileX size="16" class="mr-1" />
|
||||||
|
{$_('menu.close_all')}
|
||||||
|
<Shortcut key="⌫" ctrl={true} shift={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Item
|
||||||
|
on:click={() => ($exportState = ExportState.SELECTION)}
|
||||||
|
disabled={$selection.size == 0}
|
||||||
|
>
|
||||||
|
<Download size="16" class="mr-1" />
|
||||||
|
{$_('menu.export')}
|
||||||
|
<Shortcut key="S" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Item
|
||||||
|
on:click={() => ($exportState = ExportState.ALL)}
|
||||||
|
disabled={$fileObservers.size == 0}
|
||||||
|
>
|
||||||
|
<Download size="16" class="mr-1" />
|
||||||
|
{$_('menu.export_all')}
|
||||||
|
<Shortcut key="S" ctrl={true} shift={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
</Menubar.Content>
|
||||||
|
</Menubar.Menu>
|
||||||
|
<Menubar.Menu>
|
||||||
|
<Menubar.Trigger aria-label={$_('menu.edit')}>
|
||||||
|
<FilePen size="18" class="md:hidden" />
|
||||||
|
<span class="hidden md:block">{$_('menu.edit')}</span>
|
||||||
|
</Menubar.Trigger>
|
||||||
|
<Menubar.Content class="border-none">
|
||||||
|
<Menubar.Item on:click={dbUtils.undo} disabled={$undoDisabled}>
|
||||||
|
<Undo2 size="16" class="mr-1" />
|
||||||
|
{$_('menu.undo')}
|
||||||
|
<Shortcut key="Z" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Item on:click={dbUtils.redo} disabled={$redoDisabled}>
|
||||||
|
<Redo2 size="16" class="mr-1" />
|
||||||
|
{$_('menu.redo')}
|
||||||
|
<Shortcut key="Z" ctrl={true} shift={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Item
|
||||||
|
disabled={$selection.size !== 1 ||
|
||||||
|
!$selection
|
||||||
|
.getSelected()
|
||||||
|
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)}
|
||||||
|
on:click={() => ($editMetadata = true)}
|
||||||
|
>
|
||||||
|
<Info size="16" class="mr-1" />
|
||||||
|
{$_('menu.metadata.button')}
|
||||||
|
<Shortcut key="I" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Item
|
||||||
|
disabled={$selection.size === 0 ||
|
||||||
|
!$selection
|
||||||
|
.getSelected()
|
||||||
|
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)}
|
||||||
|
on:click={() => ($editStyle = true)}
|
||||||
|
>
|
||||||
|
<PaintBucket size="16" class="mr-1" />
|
||||||
|
{$_('menu.style.button')}
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Item
|
||||||
|
on:click={() => {
|
||||||
|
if ($allHidden) {
|
||||||
|
dbUtils.setHiddenToSelection(false);
|
||||||
|
} else {
|
||||||
|
dbUtils.setHiddenToSelection(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={$selection.size == 0}
|
||||||
|
>
|
||||||
|
{#if $allHidden}
|
||||||
|
<Eye size="16" class="mr-1" />
|
||||||
|
{$_('menu.unhide')}
|
||||||
|
{:else}
|
||||||
|
<EyeOff size="16" class="mr-1" />
|
||||||
|
{$_('menu.hide')}
|
||||||
|
{/if}
|
||||||
|
<Shortcut key="H" ctrl={true} />
|
||||||
|
</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.Item on:click={selectAll} disabled={$fileObservers.size == 0}>
|
||||||
|
<FileStack size="16" class="mr-1" />
|
||||||
|
{$_('menu.select_all')}
|
||||||
|
<Shortcut key="A" ctrl={true} />
|
||||||
|
</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}
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Item on:click={copySelection} disabled={$selection.size === 0}>
|
||||||
|
<ClipboardCopy size="16" class="mr-1" />
|
||||||
|
{$_('menu.copy')}
|
||||||
|
<Shortcut key="C" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Item on:click={cutSelection} disabled={$selection.size === 0}>
|
||||||
|
<Scissors size="16" class="mr-1" />
|
||||||
|
{$_('menu.cut')}
|
||||||
|
<Shortcut key="X" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Item
|
||||||
|
disabled={$copied === undefined ||
|
||||||
|
$copied.length === 0 ||
|
||||||
|
($selection.size > 0 &&
|
||||||
|
!allowedPastes[$copied[0].level].includes($selection.getSelected().pop()?.level))}
|
||||||
|
on:click={pasteSelection}
|
||||||
|
>
|
||||||
|
<ClipboardPaste size="16" class="mr-1" />
|
||||||
|
{$_('menu.paste')}
|
||||||
|
<Shortcut key="V" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
{/if}
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Item on:click={dbUtils.deleteSelection} disabled={$selection.size == 0}>
|
||||||
|
<Trash2 size="16" class="mr-1" />
|
||||||
|
{$_('menu.delete')}
|
||||||
|
<Shortcut key="⌫" ctrl={true} />
|
||||||
|
</Menubar.Item>
|
||||||
|
</Menubar.Content>
|
||||||
|
</Menubar.Menu>
|
||||||
|
<Menubar.Menu>
|
||||||
|
<Menubar.Trigger aria-label={$_('menu.view')}>
|
||||||
|
<View size="18" class="md:hidden" />
|
||||||
|
<span class="hidden md:block">{$_('menu.view')}</span>
|
||||||
|
</Menubar.Trigger>
|
||||||
|
<Menubar.Content class="border-none">
|
||||||
|
<Menubar.CheckboxItem bind:checked={$elevationProfile}>
|
||||||
|
<ChartArea size="16" class="mr-1" />
|
||||||
|
{$_('menu.elevation_profile')}
|
||||||
|
<Shortcut key="P" ctrl={true} />
|
||||||
|
</Menubar.CheckboxItem>
|
||||||
|
<Menubar.CheckboxItem bind:checked={$verticalFileView}>
|
||||||
|
<GalleryVertical size="16" class="mr-1" />
|
||||||
|
{$_('menu.vertical_file_view')}
|
||||||
|
<Shortcut key="L" ctrl={true} />
|
||||||
|
</Menubar.CheckboxItem>
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Item inset on:click={switchBasemaps}>
|
||||||
|
<Map size="16" class="mr-1" />{$_('menu.switch_basemap')}<Shortcut key="F1" />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Item inset on:click={toggleOverlays}>
|
||||||
|
<Layers2 size="16" class="mr-1" />{$_('menu.toggle_overlays')}<Shortcut key="F2" />
|
||||||
|
</Menubar.Item>
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.CheckboxItem bind:checked={$distanceMarkers}>
|
||||||
|
<Coins size="16" class="mr-1" />{$_('menu.distance_markers')}<Shortcut key="F3" />
|
||||||
|
</Menubar.CheckboxItem>
|
||||||
|
<Menubar.CheckboxItem bind:checked={$directionMarkers}>
|
||||||
|
<Milestone size="16" class="mr-1" />{$_('menu.direction_markers')}<Shortcut key="F4" />
|
||||||
|
</Menubar.CheckboxItem>
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Item inset on:click={toggle3D}>
|
||||||
|
<Box size="16" class="mr-1" />
|
||||||
|
{$_('menu.toggle_3d')}
|
||||||
|
<Shortcut key="{$_('menu.ctrl')}+{$_('menu.drag')}" />
|
||||||
|
</Menubar.Item>
|
||||||
|
</Menubar.Content>
|
||||||
|
</Menubar.Menu>
|
||||||
|
<Menubar.Menu>
|
||||||
|
<Menubar.Trigger aria-label={$_('menu.settings')}>
|
||||||
|
<Settings size="18" class="md:hidden" />
|
||||||
|
<span class="hidden md:block">
|
||||||
|
{$_('menu.settings')}
|
||||||
|
</span>
|
||||||
|
</Menubar.Trigger>
|
||||||
|
<Menubar.Content class="border-none">
|
||||||
|
<Menubar.Sub>
|
||||||
|
<Menubar.SubTrigger>
|
||||||
|
<Ruler size="16" class="mr-1" />{$_('menu.distance_units')}
|
||||||
|
</Menubar.SubTrigger>
|
||||||
|
<Menubar.SubContent>
|
||||||
|
<Menubar.RadioGroup bind:value={$distanceUnits}>
|
||||||
|
<Menubar.RadioItem value="metric">{$_('menu.metric')}</Menubar.RadioItem>
|
||||||
|
<Menubar.RadioItem value="imperial">{$_('menu.imperial')}</Menubar.RadioItem>
|
||||||
|
<Menubar.RadioItem value="nautical">{$_('menu.nautical')}</Menubar.RadioItem>
|
||||||
|
</Menubar.RadioGroup>
|
||||||
|
</Menubar.SubContent>
|
||||||
|
</Menubar.Sub>
|
||||||
|
<Menubar.Sub>
|
||||||
|
<Menubar.SubTrigger>
|
||||||
|
<Zap size="16" class="mr-1" />{$_('menu.velocity_units')}
|
||||||
|
</Menubar.SubTrigger>
|
||||||
|
<Menubar.SubContent>
|
||||||
|
<Menubar.RadioGroup bind:value={$velocityUnits}>
|
||||||
|
<Menubar.RadioItem value="speed">{$_('quantities.speed')}</Menubar.RadioItem>
|
||||||
|
<Menubar.RadioItem value="pace">{$_('quantities.pace')}</Menubar.RadioItem>
|
||||||
|
</Menubar.RadioGroup>
|
||||||
|
</Menubar.SubContent>
|
||||||
|
</Menubar.Sub>
|
||||||
|
<Menubar.Sub>
|
||||||
|
<Menubar.SubTrigger>
|
||||||
|
<Thermometer size="16" class="mr-1" />{$_('menu.temperature_units')}
|
||||||
|
</Menubar.SubTrigger>
|
||||||
|
<Menubar.SubContent>
|
||||||
|
<Menubar.RadioGroup bind:value={$temperatureUnits}>
|
||||||
|
<Menubar.RadioItem value="celsius">{$_('menu.celsius')}</Menubar.RadioItem>
|
||||||
|
<Menubar.RadioItem value="fahrenheit">{$_('menu.fahrenheit')}</Menubar.RadioItem>
|
||||||
|
</Menubar.RadioGroup>
|
||||||
|
</Menubar.SubContent>
|
||||||
|
</Menubar.Sub>
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Sub>
|
||||||
|
<Menubar.SubTrigger>
|
||||||
|
<Languages size="16" class="mr-1" />
|
||||||
|
{$_('menu.language')}
|
||||||
|
</Menubar.SubTrigger>
|
||||||
|
<Menubar.SubContent>
|
||||||
|
<Menubar.RadioGroup bind:value={$locale}>
|
||||||
|
{#each Object.entries(languages) as [lang, label]}
|
||||||
|
<a href={getURLForLanguage(lang, '/app')}>
|
||||||
|
<Menubar.RadioItem value={lang}>{label}</Menubar.RadioItem>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</Menubar.RadioGroup>
|
||||||
|
</Menubar.SubContent>
|
||||||
|
</Menubar.Sub>
|
||||||
|
<Menubar.Sub>
|
||||||
|
<Menubar.SubTrigger>
|
||||||
|
{#if selectedMode === 'light'}
|
||||||
|
<Sun size="16" class="mr-1" />
|
||||||
|
{:else}
|
||||||
|
<Moon size="16" class="mr-1" />
|
||||||
|
{/if}
|
||||||
|
{$_('menu.mode')}
|
||||||
|
</Menubar.SubTrigger>
|
||||||
|
<Menubar.SubContent>
|
||||||
|
<Menubar.RadioGroup
|
||||||
|
bind:value={selectedMode}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setMode(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menubar.RadioItem value="light">{$_('menu.light')}</Menubar.RadioItem>
|
||||||
|
<Menubar.RadioItem value="dark">{$_('menu.dark')}</Menubar.RadioItem>
|
||||||
|
</Menubar.RadioGroup>
|
||||||
|
</Menubar.SubContent>
|
||||||
|
</Menubar.Sub>
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Sub>
|
||||||
|
<Menubar.SubTrigger>
|
||||||
|
<PersonStanding size="16" class="mr-1" />
|
||||||
|
{$_('menu.street_view_source')}
|
||||||
|
</Menubar.SubTrigger>
|
||||||
|
<Menubar.SubContent>
|
||||||
|
<Menubar.RadioGroup bind:value={$streetViewSource}>
|
||||||
|
<Menubar.RadioItem value="mapillary">{$_('menu.mapillary')}</Menubar.RadioItem>
|
||||||
|
<Menubar.RadioItem value="google">{$_('menu.google')}</Menubar.RadioItem>
|
||||||
|
</Menubar.RadioGroup>
|
||||||
|
</Menubar.SubContent>
|
||||||
|
</Menubar.Sub>
|
||||||
|
<Menubar.Item on:click={() => (layerSettingsOpen = true)}>
|
||||||
|
<Layers size="16" class="mr-1" />
|
||||||
|
{$_('menu.layers')}
|
||||||
|
</Menubar.Item>
|
||||||
|
</Menubar.Content>
|
||||||
|
</Menubar.Menu>
|
||||||
|
</Menubar.Root>
|
||||||
|
<div class="h-fit flex flex-row items-center ml-1 gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
href="./help"
|
||||||
|
target="_blank"
|
||||||
|
class="cursor-default h-fit rounded-sm px-3 py-0.5"
|
||||||
|
aria-label={$_('menu.help')}
|
||||||
|
>
|
||||||
|
<BookOpenText size="18" class="md:hidden" />
|
||||||
|
<span class="hidden md:block">
|
||||||
|
{$_('menu.help')}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Export />
|
||||||
|
<LayerControlSettings bind:open={layerSettingsOpen} />
|
||||||
|
|
||||||
|
<svelte:window
|
||||||
|
on:keydown={(e) => {
|
||||||
|
let targetInput =
|
||||||
|
e.target.tagName === 'INPUT' ||
|
||||||
|
e.target.tagName === 'TEXTAREA' ||
|
||||||
|
e.target.tagName === 'SELECT' ||
|
||||||
|
e.target.role === 'combobox' ||
|
||||||
|
e.target.role === 'radio' ||
|
||||||
|
e.target.role === 'menu' ||
|
||||||
|
e.target.role === 'menuitem' ||
|
||||||
|
e.target.role === 'menuitemradio' ||
|
||||||
|
e.target.role === 'menuitemcheckbox';
|
||||||
|
|
||||||
|
if (e.key === '+' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
createFile();
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'o' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
triggerFileInput();
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'd' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
dbUtils.duplicateSelection();
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'c' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
if (!targetInput) {
|
||||||
|
copySelection();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
} else if (e.key === 'x' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
if (!targetInput) {
|
||||||
|
cutSelection();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
} else if (e.key === 'v' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
if (!targetInput) {
|
||||||
|
pasteSelection();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
} else if ((e.key === 's' || e.key == 'S') && (e.metaKey || e.ctrlKey)) {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
if ($fileObservers.size > 0) {
|
||||||
|
$exportState = ExportState.ALL;
|
||||||
|
}
|
||||||
|
} else if ($selection.size > 0) {
|
||||||
|
$exportState = ExportState.SELECTION;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
} else if ((e.key === 'z' || e.key == 'Z') && (e.metaKey || e.ctrlKey)) {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
dbUtils.redo();
|
||||||
|
} else {
|
||||||
|
dbUtils.undo();
|
||||||
|
}
|
||||||
|
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)) {
|
||||||
|
if (!targetInput) {
|
||||||
|
selectAll();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
} else if (e.key === 'i' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
if (
|
||||||
|
$selection.size === 1 &&
|
||||||
|
$selection
|
||||||
|
.getSelected()
|
||||||
|
.every((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
|
||||||
|
) {
|
||||||
|
$editMetadata = true;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'p' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
$elevationProfile = !$elevationProfile;
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'l' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
$verticalFileView = !$verticalFileView;
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'h' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
if ($allHidden) {
|
||||||
|
dbUtils.setHiddenToSelection(false);
|
||||||
|
} else {
|
||||||
|
dbUtils.setHiddenToSelection(true);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||||
|
if ($selection.size > 0) {
|
||||||
|
centerMapOnSelection();
|
||||||
|
}
|
||||||
|
} else if (e.key === 'F1') {
|
||||||
|
switchBasemaps();
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'F2') {
|
||||||
|
toggleOverlays();
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'F3') {
|
||||||
|
$distanceMarkers = !$distanceMarkers;
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'F4') {
|
||||||
|
$directionMarkers = !$directionMarkers;
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (e.key === 'F5') {
|
||||||
|
$routing = !$routing;
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (
|
||||||
|
e.key === 'ArrowRight' ||
|
||||||
|
e.key === 'ArrowDown' ||
|
||||||
|
e.key === 'ArrowLeft' ||
|
||||||
|
e.key === 'ArrowUp'
|
||||||
|
) {
|
||||||
|
if (!targetInput) {
|
||||||
|
updateSelectionFromKey(e.key === 'ArrowRight' || e.key === 'ArrowDown', e.shiftKey);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:dragover={(e) => e.preventDefault()}
|
||||||
|
on:drop={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.dataTransfer.files.length > 0) {
|
||||||
|
loadFiles(e.dataTransfer.files);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
div :global(button) {
|
||||||
|
@apply hover:bg-accent;
|
||||||
|
@apply px-3;
|
||||||
|
@apply py-0.5;
|
||||||
|
}
|
||||||
|
</style>
|
25
website/src/lib/components/ModeSwitch.svelte
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Moon, Sun } from 'lucide-svelte';
|
||||||
|
import { mode, setMode, systemPrefersMode } from 'mode-watcher';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
|
export let size = '20';
|
||||||
|
|
||||||
|
$: selectedMode = $mode ?? $systemPrefersMode ?? 'light';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="h-8 px-1.5 {$$props.class ?? ''}"
|
||||||
|
on:click={() => {
|
||||||
|
setMode(selectedMode === 'light' ? 'dark' : 'light');
|
||||||
|
}}
|
||||||
|
aria-label={$_('menu.mode')}
|
||||||
|
>
|
||||||
|
{#if selectedMode === 'light'}
|
||||||
|
<Sun {size} />
|
||||||
|
{:else}
|
||||||
|
<Moon {size} />
|
||||||
|
{/if}
|
||||||
|
</Button>
|
32
website/src/lib/components/Nav.svelte
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import AlgoliaDocSearch from '$lib/components/AlgoliaDocSearch.svelte';
|
||||||
|
import ModeSwitch from '$lib/components/ModeSwitch.svelte';
|
||||||
|
import { BookOpenText, Home, Map } from 'lucide-svelte';
|
||||||
|
import { _, locale } from 'svelte-i18n';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="w-full sticky top-0 bg-background z-50">
|
||||||
|
<div class="mx-6 py-2 flex flex-row items-center border-b gap-4 sm:gap-8">
|
||||||
|
<a href={getURLForLanguage($locale, '/')} class="shrink-0 translate-y-0.5">
|
||||||
|
<Logo class="h-8 sm:hidden" iconOnly={true} width="26" />
|
||||||
|
<Logo class="h-8 hidden sm:block" width="153" />
|
||||||
|
</a>
|
||||||
|
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/')}>
|
||||||
|
<Home size="18" class="mr-1.5" />
|
||||||
|
{$_('homepage.home')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/app')}>
|
||||||
|
<Map size="18" class="mr-1.5" />
|
||||||
|
{$_('homepage.app')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="link" class="text-base px-0" href={getURLForLanguage($locale, '/help')}>
|
||||||
|
<BookOpenText size="18" class="mr-1.5" />
|
||||||
|
{$_('menu.help')}
|
||||||
|
</Button>
|
||||||
|
<AlgoliaDocSearch class="ml-auto" />
|
||||||
|
<ModeSwitch class="hidden xs:block" />
|
||||||
|
</div>
|
||||||
|
</nav>
|
41
website/src/lib/components/Resizer.svelte
Executable file
@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let orientation: 'col' | 'row' = 'col';
|
||||||
|
|
||||||
|
export let after: number;
|
||||||
|
export let minAfter: number = 0;
|
||||||
|
export let maxAfter: number = Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
|
function handleMouseDown(event: PointerEvent) {
|
||||||
|
const startX = event.clientX;
|
||||||
|
const startY = event.clientY;
|
||||||
|
const startAfter = after;
|
||||||
|
|
||||||
|
const handleMouseMove = (event: PointerEvent) => {
|
||||||
|
const newAfter =
|
||||||
|
startAfter + (orientation === 'col' ? startX - event.clientX : startY - event.clientY);
|
||||||
|
if (newAfter >= minAfter && newAfter <= maxAfter) {
|
||||||
|
after = newAfter;
|
||||||
|
} else if (newAfter < minAfter && after !== minAfter) {
|
||||||
|
after = minAfter;
|
||||||
|
} else if (newAfter > maxAfter && after !== maxAfter) {
|
||||||
|
after = maxAfter;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
window.removeEventListener('pointermove', handleMouseMove);
|
||||||
|
window.removeEventListener('pointerup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', handleMouseMove);
|
||||||
|
window.addEventListener('pointerup', handleMouseUp);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<div
|
||||||
|
class="{orientation === 'col'
|
||||||
|
? 'w-1 h-full cursor-col-resize border-l'
|
||||||
|
: 'w-full h-1 cursor-row-resize border-t'} {orientation}"
|
||||||
|
on:pointerdown={handleMouseDown}
|
||||||
|
/>
|
36
website/src/lib/components/Shortcut.svelte
Executable file
@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { isMac, isSafari } from '$lib/utils';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
|
export let key: string | undefined = undefined;
|
||||||
|
export let shift: boolean = false;
|
||||||
|
export let ctrl: boolean = false;
|
||||||
|
export let click: boolean = false;
|
||||||
|
|
||||||
|
let mac = false;
|
||||||
|
let safari = false;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
mac = isMac();
|
||||||
|
safari = isSafari();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="ml-auto pl-2 text-xs tracking-widest text-muted-foreground flex flex-row gap-0 items-baseline"
|
||||||
|
{...$$props}
|
||||||
|
>
|
||||||
|
{#if shift}
|
||||||
|
<span>⇧</span>
|
||||||
|
{/if}
|
||||||
|
{#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>
|
18
website/src/lib/components/Tooltip.svelte
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
export let side: 'top' | 'right' | 'bottom' | 'left' = 'top';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger {...$$restProps} aria-label={label}>
|
||||||
|
<slot />
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content {side}>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<span>{label}</span>
|
||||||
|
<slot name="extra" />
|
||||||
|
</div>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
48
website/src/lib/components/WithUnits.svelte
Executable file
@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { settings } from '$lib/db';
|
||||||
|
import {
|
||||||
|
celsiusToFahrenheit,
|
||||||
|
getConvertedDistance,
|
||||||
|
getConvertedElevation,
|
||||||
|
getConvertedVelocity,
|
||||||
|
getDistanceUnits,
|
||||||
|
getElevationUnits,
|
||||||
|
getVelocityUnits,
|
||||||
|
secondsToHHMMSS
|
||||||
|
} from '$lib/units';
|
||||||
|
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
|
export let value: number;
|
||||||
|
export let type: 'distance' | 'elevation' | 'speed' | 'temperature' | 'time';
|
||||||
|
export let showUnits: boolean = true;
|
||||||
|
export let decimals: number | undefined = undefined;
|
||||||
|
|
||||||
|
const { distanceUnits, velocityUnits, temperatureUnits } = settings;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class={$$props.class}>
|
||||||
|
{#if type === 'distance'}
|
||||||
|
{getConvertedDistance(value, $distanceUnits).toFixed(decimals ?? 2)}
|
||||||
|
{showUnits ? getDistanceUnits($distanceUnits) : ''}
|
||||||
|
{:else if type === 'elevation'}
|
||||||
|
{getConvertedElevation(value, $distanceUnits).toFixed(decimals ?? 0)}
|
||||||
|
{showUnits ? getElevationUnits($distanceUnits) : ''}
|
||||||
|
{:else if type === 'speed'}
|
||||||
|
{#if $velocityUnits === 'speed'}
|
||||||
|
{getConvertedVelocity(value, $velocityUnits, $distanceUnits).toFixed(decimals ?? 2)}
|
||||||
|
{showUnits ? getVelocityUnits($velocityUnits, $distanceUnits) : ''}
|
||||||
|
{:else}
|
||||||
|
{secondsToHHMMSS(getConvertedVelocity(value, $velocityUnits, $distanceUnits))}
|
||||||
|
{showUnits ? getVelocityUnits($velocityUnits, $distanceUnits) : ''}
|
||||||
|
{/if}
|
||||||
|
{:else if type === 'temperature'}
|
||||||
|
{#if $temperatureUnits === 'celsius'}
|
||||||
|
{value} {showUnits ? $_('units.celsius') : ''}
|
||||||
|
{:else}
|
||||||
|
{celsiusToFahrenheit(value)} {showUnits ? $_('units.fahrenheit') : ''}
|
||||||
|
{/if}
|
||||||
|
{:else if type === 'time'}
|
||||||
|
{secondsToHHMMSS(value)}
|
||||||
|
{/if}
|
||||||
|
</span>
|
20
website/src/lib/components/collapsible-tree/CollapsibleTree.svelte
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { setContext } from 'svelte';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export let defaultState: 'open' | 'closed' = 'open';
|
||||||
|
export let side: 'left' | 'right' = 'right';
|
||||||
|
export let nohover: boolean = false;
|
||||||
|
export let slotInsideTrigger: boolean = true;
|
||||||
|
|
||||||
|
let open = writable<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
setContext('collapsible-tree-default-state', defaultState);
|
||||||
|
setContext('collapsible-tree-state', open);
|
||||||
|
setContext('collapsible-tree-side', side);
|
||||||
|
setContext('collapsible-tree-nohover', nohover);
|
||||||
|
setContext('collapsible-tree-parent-id', 'root');
|
||||||
|
setContext('collapsible-tree-slot-inside-trigger', slotInsideTrigger);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
97
website/src/lib/components/collapsible-tree/CollapsibleTreeNode.svelte
Executable file
@ -0,0 +1,97 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-svelte';
|
||||||
|
import { getContext, onMount, setContext } from 'svelte';
|
||||||
|
import { get, type Writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export let id: string | number;
|
||||||
|
|
||||||
|
let defaultState = getContext<'open' | 'closed'>('collapsible-tree-default-state');
|
||||||
|
let open = getContext<Writable<Record<string, boolean>>>('collapsible-tree-state');
|
||||||
|
let side = getContext<'left' | 'right'>('collapsible-tree-side');
|
||||||
|
let nohover = getContext<boolean>('collapsible-tree-nohover');
|
||||||
|
let slotInsideTrigger = getContext<boolean>('collapsible-tree-slot-inside-trigger');
|
||||||
|
let parentId = getContext<string>('collapsible-tree-parent-id');
|
||||||
|
|
||||||
|
let fullId = `${parentId}.${id}`;
|
||||||
|
setContext('collapsible-tree-parent-id', fullId);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!get(open).hasOwnProperty(fullId)) {
|
||||||
|
open.update((value) => {
|
||||||
|
value[fullId] = defaultState === 'open';
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function openNode() {
|
||||||
|
open.update((value) => {
|
||||||
|
value[fullId] = true;
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Collapsible.Root bind:open={$open[fullId]} class={$$props.class ?? ''}>
|
||||||
|
{#if slotInsideTrigger}
|
||||||
|
<Collapsible.Trigger class="w-full">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="w-full flex flex-row {side === 'right'
|
||||||
|
? 'justify-between'
|
||||||
|
: 'justify-start'} py-0 px-1 h-fit {nohover
|
||||||
|
? 'hover:bg-background'
|
||||||
|
: ''} pointer-events-none"
|
||||||
|
>
|
||||||
|
{#if side === 'left'}
|
||||||
|
{#if $open[fullId]}
|
||||||
|
<ChevronDown size="16" class="shrink-0" />
|
||||||
|
{:else}
|
||||||
|
<ChevronRight size="16" class="shrink-0" />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
<slot name="trigger" />
|
||||||
|
{#if side === 'right'}
|
||||||
|
{#if $open[fullId]}
|
||||||
|
<ChevronDown size="16" class="shrink-0" />
|
||||||
|
{:else}
|
||||||
|
<ChevronLeft size="16" class="shrink-0" />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
{:else}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="w-full flex flex-row {side === 'right'
|
||||||
|
? 'justify-between'
|
||||||
|
: 'justify-start'} py-0 px-1 h-fit {nohover ? 'hover:bg-background' : ''}"
|
||||||
|
>
|
||||||
|
{#if side === 'left'}
|
||||||
|
<Collapsible.Trigger>
|
||||||
|
{#if $open[fullId]}
|
||||||
|
<ChevronDown size="16" class="shrink-0" />
|
||||||
|
{:else}
|
||||||
|
<ChevronRight size="16" class="shrink-0" />
|
||||||
|
{/if}
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
{/if}
|
||||||
|
<slot name="trigger" />
|
||||||
|
{#if side === 'right'}
|
||||||
|
<Collapsible.Trigger>
|
||||||
|
{#if $open[fullId]}
|
||||||
|
<ChevronDown size="16" class="shrink-0" />
|
||||||
|
{:else}
|
||||||
|
<ChevronLeft size="16" class="shrink-0" />
|
||||||
|
{/if}
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Collapsible.Content class="ml-2">
|
||||||
|
<slot name="content" />
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
2
website/src/lib/components/collapsible-tree/index.ts
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
export { default as CollapsibleTree } from './CollapsibleTree.svelte';
|
||||||
|
export { default as CollapsibleTreeNode } from './CollapsibleTreeNode.svelte';
|
27
website/src/lib/components/custom-control/CustomControl.svelte
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CustomControl from './CustomControl';
|
||||||
|
import { map } from '$lib/stores';
|
||||||
|
|
||||||
|
export let position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' = 'top-right';
|
||||||
|
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let control: CustomControl | undefined = undefined;
|
||||||
|
|
||||||
|
$: if ($map && container) {
|
||||||
|
if (position.includes('right')) container.classList.add('float-right');
|
||||||
|
else container.classList.add('float-left');
|
||||||
|
container.classList.remove('hidden');
|
||||||
|
if (control === undefined) {
|
||||||
|
control = new CustomControl(container);
|
||||||
|
}
|
||||||
|
$map.addControl(control, position);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={container}
|
||||||
|
class="{$$props.class ||
|
||||||
|
''} clear-both translate-0 m-[10px] mb-0 last:mb-[10px] pointer-events-auto bg-background rounded shadow-md hidden"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
20
website/src/lib/components/custom-control/CustomControl.ts
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
import { type Map, type IControl } from 'mapbox-gl';
|
||||||
|
|
||||||
|
export default class CustomControl implements IControl {
|
||||||
|
_map: Map | undefined;
|
||||||
|
_container: HTMLElement;
|
||||||
|
|
||||||
|
constructor(container: HTMLElement) {
|
||||||
|
this._container = container;
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdd(map: Map): HTMLElement {
|
||||||
|
this._map = map;
|
||||||
|
return this._container;
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove() {
|
||||||
|
this._container?.parentNode?.removeChild(this._container);
|
||||||
|
this._map = undefined;
|
||||||
|
}
|
||||||
|
}
|
82
website/src/lib/components/docs/DocsContainer.svelte
Executable file
@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
|
export let module;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="markdown flex flex-col gap-3">
|
||||||
|
<svelte:component this={module} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
:global(.markdown) {
|
||||||
|
@apply text-muted-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.markdown h1) {
|
||||||
|
@apply text-foreground;
|
||||||
|
@apply text-3xl;
|
||||||
|
@apply font-semibold;
|
||||||
|
@apply mb-3 pt-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.markdown h2) {
|
||||||
|
@apply text-foreground;
|
||||||
|
@apply text-2xl;
|
||||||
|
@apply font-semibold;
|
||||||
|
@apply pt-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.markdown h3) {
|
||||||
|
@apply text-foreground;
|
||||||
|
@apply text-lg;
|
||||||
|
@apply font-semibold;
|
||||||
|
@apply pt-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.markdown p > button, .markdown li > button) {
|
||||||
|
@apply border;
|
||||||
|
@apply rounded-md;
|
||||||
|
@apply px-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.markdown > a) {
|
||||||
|
@apply text-link;
|
||||||
|
@apply hover:underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.markdown p > a) {
|
||||||
|
@apply text-link;
|
||||||
|
@apply hover:underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.markdown li > a) {
|
||||||
|
@apply text-link;
|
||||||
|
@apply hover:underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.markdown kbd) {
|
||||||
|
@apply p-1;
|
||||||
|
@apply rounded-md;
|
||||||
|
@apply border;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.markdown ul) {
|
||||||
|
@apply list-disc;
|
||||||
|
@apply pl-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.markdown ol) {
|
||||||
|
@apply list-decimal;
|
||||||
|
@apply pl-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.markdown li) {
|
||||||
|
@apply mt-1;
|
||||||
|
@apply first:mt-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.markdown hr) {
|
||||||
|
@apply my-5;
|
||||||
|
}
|
||||||
|
</style>
|
25
website/src/lib/components/docs/DocsImage.svelte
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let src: 'getting-started/interface' | 'tools/routing' | 'tools/split';
|
||||||
|
export let alt: string;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center py-6 w-full">
|
||||||
|
<div class="rounded-md overflow-hidden overflow-clip shadow-xl mx-auto">
|
||||||
|
{#if src === 'getting-started/interface'}
|
||||||
|
<enhanced:img
|
||||||
|
src="/src/lib/assets/img/docs/getting-started/interface.png"
|
||||||
|
{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>
|
||||||
|
<p class="text-center text-sm text-muted-foreground mt-2">{alt}</p>
|
||||||
|
</div>
|
13
website/src/lib/components/docs/DocsLayers.svelte
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import mapboxOutdoorsMap from '$lib/assets/img/home/mapbox-outdoors.png?enhanced';
|
||||||
|
import waymarkedMap from '$lib/assets/img/home/waymarked.png?enhanced';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative h-80 aspect-square rounded-2xl shadow-xl overflow-clip">
|
||||||
|
<enhanced:img src={mapboxOutdoorsMap} alt="Mapbox Outdoors map screenshot." class="absolute" />
|
||||||
|
<enhanced:img
|
||||||
|
src={waymarkedMap}
|
||||||
|
alt="Waymarked Trails map screenshot."
|
||||||
|
class="absolute opacity-0 hover:opacity-100 transition-opacity duration-200"
|
||||||
|
/>
|
||||||
|
</div>
|
18
website/src/lib/components/docs/DocsNote.svelte
Executable file
@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let type: 'note' | 'warning' = 'note';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="bg-secondary border-l-8 {type === 'note'
|
||||||
|
? 'border-link'
|
||||||
|
: 'border-destructive'} p-2 text-sm rounded-md"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
div :global(a) {
|
||||||
|
@apply text-link;
|
||||||
|
@apply hover:underline;
|
||||||
|
}
|
||||||
|
</style>
|
99
website/src/lib/components/docs/docs.ts
Executable file
@ -0,0 +1,99 @@
|
|||||||
|
import { File, FilePen, View, type Icon, Settings, Pencil, MapPin, Scissors, CalendarClock, Group, Ungroup, Filter, SquareDashedMousePointer, MountainSnow } from "lucide-svelte";
|
||||||
|
import type { ComponentType } from "svelte";
|
||||||
|
|
||||||
|
export const guides: Record<string, string[]> = {
|
||||||
|
'getting-started': [],
|
||||||
|
menu: ['file', 'edit', 'view', 'settings'],
|
||||||
|
'files-and-stats': [],
|
||||||
|
toolbar: ['routing', 'poi', 'scissors', 'time', 'merge', 'extract', 'elevation', 'minify', 'clean'],
|
||||||
|
'map-controls': [],
|
||||||
|
'gpx': [],
|
||||||
|
'integration': [],
|
||||||
|
'faq': [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const guideIcons: Record<string, string | ComponentType<Icon>> = {
|
||||||
|
"getting-started": "🚀",
|
||||||
|
"menu": "📂 ⚙️",
|
||||||
|
"file": File,
|
||||||
|
"edit": FilePen,
|
||||||
|
"view": View,
|
||||||
|
"settings": Settings,
|
||||||
|
"files-and-stats": "🗂 📈",
|
||||||
|
"toolbar": "🧰",
|
||||||
|
"routing": Pencil,
|
||||||
|
"poi": MapPin,
|
||||||
|
"scissors": Scissors,
|
||||||
|
"time": CalendarClock,
|
||||||
|
"merge": Group,
|
||||||
|
"extract": Ungroup,
|
||||||
|
"elevation": MountainSnow,
|
||||||
|
"minify": Filter,
|
||||||
|
"clean": SquareDashedMousePointer,
|
||||||
|
"map-controls": "🗺",
|
||||||
|
"gpx": "💾",
|
||||||
|
"integration": "{ 👩💻 }",
|
||||||
|
"faq": "🔮",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPreviousGuide(currentGuide: string): string | undefined {
|
||||||
|
let subguides = currentGuide.split('/');
|
||||||
|
|
||||||
|
if (subguides.length === 1) {
|
||||||
|
let keys = Object.keys(guides);
|
||||||
|
let index = keys.indexOf(currentGuide);
|
||||||
|
if (index === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let previousGuide = keys[index - 1];
|
||||||
|
if (previousGuide === undefined) {
|
||||||
|
return undefined;
|
||||||
|
} else if (guides[previousGuide].length === 0) {
|
||||||
|
return previousGuide;
|
||||||
|
} else {
|
||||||
|
return `${previousGuide}/${guides[previousGuide][guides[previousGuide].length - 1]}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (guides.hasOwnProperty(subguides[0])) {
|
||||||
|
let subguideIndex = guides[subguides[0]].indexOf(subguides[1]);
|
||||||
|
if (subguideIndex > 0) {
|
||||||
|
return `${subguides[0]}/${guides[subguides[0]][subguideIndex - 1]}`;
|
||||||
|
} else {
|
||||||
|
return subguides[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNextGuide(currentGuide: string): string | undefined {
|
||||||
|
let subguides = currentGuide.split('/');
|
||||||
|
|
||||||
|
if (subguides.length === 1) {
|
||||||
|
if (guides.hasOwnProperty(currentGuide)) {
|
||||||
|
if (guides[currentGuide].length === 0) {
|
||||||
|
let keys = Object.keys(guides);
|
||||||
|
let index = keys.indexOf(currentGuide);
|
||||||
|
return keys[index + 1];
|
||||||
|
} else {
|
||||||
|
return `${currentGuide}/${guides[currentGuide][0]}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (guides.hasOwnProperty(subguides[0])) {
|
||||||
|
let subguideIndex = guides[subguides[0]].indexOf(subguides[1]);
|
||||||
|
if (subguideIndex < guides[subguides[0]].length - 1) {
|
||||||
|
return `${subguides[0]}/${guides[subguides[0]][subguideIndex + 1]}`;
|
||||||
|
} else {
|
||||||
|
let keys = Object.keys(guides);
|
||||||
|
let index = keys.indexOf(subguides[0]);
|
||||||
|
return keys[index + 1];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
269
website/src/lib/components/embedding/Embedding.svelte
Executable file
@ -0,0 +1,269 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import GPXLayers from '$lib/components/gpx-layer/GPXLayers.svelte';
|
||||||
|
import ElevationProfile from '$lib/components/ElevationProfile.svelte';
|
||||||
|
import FileList from '$lib/components/file-list/FileList.svelte';
|
||||||
|
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
|
||||||
|
import Map from '$lib/components/Map.svelte';
|
||||||
|
import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
|
||||||
|
import OpenIn from '$lib/components/embedding/OpenIn.svelte';
|
||||||
|
import {
|
||||||
|
gpxStatistics,
|
||||||
|
slicedGPXStatistics,
|
||||||
|
embedding,
|
||||||
|
loadFile,
|
||||||
|
map,
|
||||||
|
updateGPXData
|
||||||
|
} from '$lib/stores';
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
import { fileObservers, settings, GPXStatisticsTree } from '$lib/db';
|
||||||
|
import { readable } from 'svelte/store';
|
||||||
|
import type { GPXFile } from 'gpx';
|
||||||
|
import { selection } from '$lib/components/file-list/Selection';
|
||||||
|
import { ListFileItem } from '$lib/components/file-list/FileList';
|
||||||
|
import {
|
||||||
|
allowedEmbeddingBasemaps,
|
||||||
|
getFilesFromEmbeddingOptions,
|
||||||
|
type EmbeddingOptions
|
||||||
|
} from './Embedding';
|
||||||
|
import { mode, setMode } from 'mode-watcher';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
$embedding = true;
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentBasemap,
|
||||||
|
distanceUnits,
|
||||||
|
velocityUnits,
|
||||||
|
temperatureUnits,
|
||||||
|
fileOrder,
|
||||||
|
distanceMarkers,
|
||||||
|
directionMarkers
|
||||||
|
} = settings;
|
||||||
|
|
||||||
|
export let useHash = true;
|
||||||
|
export let options: EmbeddingOptions;
|
||||||
|
export let hash: string;
|
||||||
|
|
||||||
|
let prevSettings = {
|
||||||
|
distanceMarkers: false,
|
||||||
|
directionMarkers: false,
|
||||||
|
distanceUnits: 'metric',
|
||||||
|
velocityUnits: 'speed',
|
||||||
|
temperatureUnits: 'celsius',
|
||||||
|
theme: 'system'
|
||||||
|
};
|
||||||
|
|
||||||
|
function applyOptions() {
|
||||||
|
fileObservers.update(($fileObservers) => {
|
||||||
|
$fileObservers.clear();
|
||||||
|
return $fileObservers;
|
||||||
|
});
|
||||||
|
|
||||||
|
let downloads: Promise<GPXFile | null>[] = [];
|
||||||
|
getFilesFromEmbeddingOptions(options).forEach((url) => {
|
||||||
|
downloads.push(
|
||||||
|
fetch(url)
|
||||||
|
.then((response) => response.blob())
|
||||||
|
.then((blob) => new File([blob], url.split('/').pop() ?? url))
|
||||||
|
.then(loadFile)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(downloads).then((files) => {
|
||||||
|
let ids: string[] = [];
|
||||||
|
let bounds = {
|
||||||
|
southWest: {
|
||||||
|
lat: 90,
|
||||||
|
lon: 180
|
||||||
|
},
|
||||||
|
northEast: {
|
||||||
|
lat: -90,
|
||||||
|
lon: -180
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fileObservers.update(($fileObservers) => {
|
||||||
|
files.forEach((file, index) => {
|
||||||
|
if (file === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = `gpx-${index}-embed`;
|
||||||
|
file._data.id = id;
|
||||||
|
let statistics = new GPXStatisticsTree(file);
|
||||||
|
|
||||||
|
$fileObservers.set(
|
||||||
|
id,
|
||||||
|
readable({
|
||||||
|
file,
|
||||||
|
statistics
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
ids.push(id);
|
||||||
|
let fileBounds = statistics.getStatisticsFor(new ListFileItem(id)).global.bounds;
|
||||||
|
|
||||||
|
bounds.southWest.lat = Math.min(bounds.southWest.lat, fileBounds.southWest.lat);
|
||||||
|
bounds.southWest.lon = Math.min(bounds.southWest.lon, fileBounds.southWest.lon);
|
||||||
|
bounds.northEast.lat = Math.max(bounds.northEast.lat, fileBounds.northEast.lat);
|
||||||
|
bounds.northEast.lon = Math.max(bounds.northEast.lon, fileBounds.northEast.lon);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $fileObservers;
|
||||||
|
});
|
||||||
|
|
||||||
|
$fileOrder = [...$fileOrder.filter((id) => !id.includes('embed')), ...ids];
|
||||||
|
|
||||||
|
selection.update(($selection) => {
|
||||||
|
$selection.clear();
|
||||||
|
ids.forEach((id) => {
|
||||||
|
$selection.toggle(new ListFileItem(id));
|
||||||
|
});
|
||||||
|
return $selection;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hash.length === 0) {
|
||||||
|
map.subscribe(($map) => {
|
||||||
|
if ($map) {
|
||||||
|
$map.fitBounds(
|
||||||
|
[
|
||||||
|
bounds.southWest.lon,
|
||||||
|
bounds.southWest.lat,
|
||||||
|
bounds.northEast.lon,
|
||||||
|
bounds.northEast.lat
|
||||||
|
],
|
||||||
|
{
|
||||||
|
padding: 80,
|
||||||
|
linear: true,
|
||||||
|
easing: () => 1
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.basemap !== $currentBasemap && allowedEmbeddingBasemaps.includes(options.basemap)) {
|
||||||
|
$currentBasemap = options.basemap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.distanceMarkers !== $distanceMarkers) {
|
||||||
|
$distanceMarkers = options.distanceMarkers;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.directionMarkers !== $directionMarkers) {
|
||||||
|
$directionMarkers = options.directionMarkers;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.distanceUnits !== $distanceUnits) {
|
||||||
|
$distanceUnits = options.distanceUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.velocityUnits !== $velocityUnits) {
|
||||||
|
$velocityUnits = options.velocityUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.temperatureUnits !== $temperatureUnits) {
|
||||||
|
$temperatureUnits = options.temperatureUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.theme !== $mode) {
|
||||||
|
setMode(options.theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
prevSettings.distanceMarkers = $distanceMarkers;
|
||||||
|
prevSettings.directionMarkers = $directionMarkers;
|
||||||
|
prevSettings.distanceUnits = $distanceUnits;
|
||||||
|
prevSettings.velocityUnits = $velocityUnits;
|
||||||
|
prevSettings.temperatureUnits = $temperatureUnits;
|
||||||
|
prevSettings.theme = $mode ?? 'system';
|
||||||
|
});
|
||||||
|
|
||||||
|
$: if (browser && options) {
|
||||||
|
applyOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if ($fileOrder) {
|
||||||
|
updateGPXData();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if ($distanceMarkers !== prevSettings.distanceMarkers) {
|
||||||
|
$distanceMarkers = prevSettings.distanceMarkers;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($directionMarkers !== prevSettings.directionMarkers) {
|
||||||
|
$directionMarkers = prevSettings.directionMarkers;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($distanceUnits !== prevSettings.distanceUnits) {
|
||||||
|
$distanceUnits = prevSettings.distanceUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($velocityUnits !== prevSettings.velocityUnits) {
|
||||||
|
$velocityUnits = prevSettings.velocityUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($temperatureUnits !== prevSettings.temperatureUnits) {
|
||||||
|
$temperatureUnits = prevSettings.temperatureUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mode !== prevSettings.theme) {
|
||||||
|
setMode(prevSettings.theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
$selection.clear();
|
||||||
|
$fileObservers.clear();
|
||||||
|
$fileOrder = $fileOrder.filter((id) => !id.includes('embed'));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="absolute flex flex-col h-full w-full border rounded-xl overflow-clip">
|
||||||
|
<div class="grow relative">
|
||||||
|
<Map
|
||||||
|
class="h-full {$fileObservers.size > 1 ? 'horizontal' : ''}"
|
||||||
|
accessToken={options.token}
|
||||||
|
geocoder={false}
|
||||||
|
geolocate={false}
|
||||||
|
hash={useHash}
|
||||||
|
/>
|
||||||
|
<OpenIn bind:files={options.files} bind:ids={options.ids} />
|
||||||
|
<LayerControl />
|
||||||
|
<GPXLayers />
|
||||||
|
{#if $fileObservers.size > 1}
|
||||||
|
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
|
||||||
|
<FileList orientation="horizontal" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="{options.elevation.show ? '' : 'h-10'} flex flex-row gap-2 px-2 sm:px-4"
|
||||||
|
style={options.elevation.show ? `height: ${options.elevation.height}px` : ''}
|
||||||
|
>
|
||||||
|
<GPXStatistics
|
||||||
|
{gpxStatistics}
|
||||||
|
{slicedGPXStatistics}
|
||||||
|
panelSize={options.elevation.height}
|
||||||
|
orientation={options.elevation.show ? 'vertical' : 'horizontal'}
|
||||||
|
/>
|
||||||
|
{#if options.elevation.show}
|
||||||
|
<ElevationProfile
|
||||||
|
{gpxStatistics}
|
||||||
|
{slicedGPXStatistics}
|
||||||
|
additionalDatasets={[
|
||||||
|
options.elevation.speed ? 'speed' : null,
|
||||||
|
options.elevation.hr ? 'hr' : null,
|
||||||
|
options.elevation.cad ? 'cad' : null,
|
||||||
|
options.elevation.temp ? 'temp' : null,
|
||||||
|
options.elevation.power ? 'power' : null
|
||||||
|
].filter((dataset) => dataset !== null)}
|
||||||
|
elevationFill={options.elevation.fill}
|
||||||
|
panelSize={options.elevation.height}
|
||||||
|
showControls={options.elevation.controls}
|
||||||
|
class="py-2"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
148
website/src/lib/components/embedding/Embedding.ts
Executable file
@ -0,0 +1,148 @@
|
|||||||
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
|
import { basemaps } from '$lib/assets/layers';
|
||||||
|
|
||||||
|
export type EmbeddingOptions = {
|
||||||
|
token: string;
|
||||||
|
files: string[];
|
||||||
|
ids: string[];
|
||||||
|
basemap: string;
|
||||||
|
elevation: {
|
||||||
|
show: boolean;
|
||||||
|
height: number;
|
||||||
|
controls: boolean;
|
||||||
|
fill: 'slope' | 'surface' | undefined;
|
||||||
|
speed: boolean;
|
||||||
|
hr: boolean;
|
||||||
|
cad: boolean;
|
||||||
|
temp: boolean;
|
||||||
|
power: boolean;
|
||||||
|
};
|
||||||
|
distanceMarkers: boolean;
|
||||||
|
directionMarkers: boolean;
|
||||||
|
distanceUnits: 'metric' | 'imperial' | 'nautical';
|
||||||
|
velocityUnits: 'speed' | 'pace';
|
||||||
|
temperatureUnits: 'celsius' | 'fahrenheit';
|
||||||
|
theme: 'system' | 'light' | 'dark';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultEmbeddingOptions = {
|
||||||
|
token: '',
|
||||||
|
files: [],
|
||||||
|
ids: [],
|
||||||
|
basemap: 'mapboxOutdoors',
|
||||||
|
elevation: {
|
||||||
|
show: true,
|
||||||
|
height: 170,
|
||||||
|
controls: true,
|
||||||
|
fill: undefined,
|
||||||
|
speed: false,
|
||||||
|
hr: false,
|
||||||
|
cad: false,
|
||||||
|
temp: false,
|
||||||
|
power: false
|
||||||
|
},
|
||||||
|
distanceMarkers: false,
|
||||||
|
directionMarkers: false,
|
||||||
|
distanceUnits: 'metric',
|
||||||
|
velocityUnits: 'speed',
|
||||||
|
temperatureUnits: 'celsius',
|
||||||
|
theme: 'system'
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getDefaultEmbeddingOptions(): EmbeddingOptions {
|
||||||
|
return JSON.parse(JSON.stringify(defaultEmbeddingOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMergedEmbeddingOptions(
|
||||||
|
options: any,
|
||||||
|
defaultOptions: any = defaultEmbeddingOptions
|
||||||
|
): EmbeddingOptions {
|
||||||
|
const mergedOptions = JSON.parse(JSON.stringify(defaultOptions));
|
||||||
|
for (const key in options) {
|
||||||
|
if (typeof options[key] === 'object' && options[key] !== null && !Array.isArray(options[key])) {
|
||||||
|
mergedOptions[key] = getMergedEmbeddingOptions(options[key], defaultOptions[key]);
|
||||||
|
} else {
|
||||||
|
mergedOptions[key] = options[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mergedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCleanedEmbeddingOptions(
|
||||||
|
options: any,
|
||||||
|
defaultOptions: any = defaultEmbeddingOptions
|
||||||
|
): any {
|
||||||
|
const cleanedOptions = JSON.parse(JSON.stringify(options));
|
||||||
|
for (const key in cleanedOptions) {
|
||||||
|
if (
|
||||||
|
typeof cleanedOptions[key] === 'object' &&
|
||||||
|
cleanedOptions[key] !== null &&
|
||||||
|
!Array.isArray(cleanedOptions[key])
|
||||||
|
) {
|
||||||
|
cleanedOptions[key] = getCleanedEmbeddingOptions(cleanedOptions[key], defaultOptions[key]);
|
||||||
|
if (Object.keys(cleanedOptions[key]).length === 0) {
|
||||||
|
delete cleanedOptions[key];
|
||||||
|
}
|
||||||
|
} else if (JSON.stringify(cleanedOptions[key]) === JSON.stringify(defaultOptions[key])) {
|
||||||
|
delete cleanedOptions[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleanedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
327
website/src/lib/components/embedding/EmbeddingPlayground.svelte
Executable file
@ -0,0 +1,327 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { Label } from '$lib/components/ui/label';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import * as Select from '$lib/components/ui/select';
|
||||||
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
|
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||||
|
import {
|
||||||
|
Zap,
|
||||||
|
HeartPulse,
|
||||||
|
Orbit,
|
||||||
|
Thermometer,
|
||||||
|
SquareActivity,
|
||||||
|
Coins,
|
||||||
|
Milestone,
|
||||||
|
Video
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
import {
|
||||||
|
allowedEmbeddingBasemaps,
|
||||||
|
getCleanedEmbeddingOptions,
|
||||||
|
getDefaultEmbeddingOptions
|
||||||
|
} from './Embedding';
|
||||||
|
import { PUBLIC_MAPBOX_TOKEN } from '$env/static/public';
|
||||||
|
import Embedding from './Embedding.svelte';
|
||||||
|
import { map } from '$lib/stores';
|
||||||
|
import { tick } from 'svelte';
|
||||||
|
import { base } from '$app/paths';
|
||||||
|
|
||||||
|
let options = getDefaultEmbeddingOptions();
|
||||||
|
options.token = 'YOUR_MAPBOX_TOKEN';
|
||||||
|
options.files = [
|
||||||
|
'https://raw.githubusercontent.com/gpxstudio/gpx.rnmkcy.eu/main/gpx/test-data/simple.gpx'
|
||||||
|
];
|
||||||
|
|
||||||
|
let files = options.files[0];
|
||||||
|
$: {
|
||||||
|
let urls = files.split(',');
|
||||||
|
urls = urls.filter((url) => url.length > 0);
|
||||||
|
if (JSON.stringify(urls) !== JSON.stringify(options.files)) {
|
||||||
|
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 zoom = '0';
|
||||||
|
let lat = '0';
|
||||||
|
let lon = '0';
|
||||||
|
let bearing = '0';
|
||||||
|
let pitch = '0';
|
||||||
|
|
||||||
|
$: hash = manualCamera ? `#${zoom}/${lat}/${lon}/${bearing}/${pitch}` : '';
|
||||||
|
|
||||||
|
$: iframeOptions =
|
||||||
|
options.token.length === 0 || options.token === 'YOUR_MAPBOX_TOKEN'
|
||||||
|
? Object.assign({}, options, { token: PUBLIC_MAPBOX_TOKEN })
|
||||||
|
: options;
|
||||||
|
|
||||||
|
async function resizeMap() {
|
||||||
|
if ($map) {
|
||||||
|
await tick();
|
||||||
|
$map.resize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (options.elevation.height || options.elevation.show) {
|
||||||
|
resizeMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCamera() {
|
||||||
|
if ($map) {
|
||||||
|
let center = $map.getCenter();
|
||||||
|
lat = center.lat.toFixed(4);
|
||||||
|
lon = center.lng.toFixed(4);
|
||||||
|
zoom = $map.getZoom().toFixed(2);
|
||||||
|
bearing = $map.getBearing().toFixed(1);
|
||||||
|
pitch = $map.getPitch().toFixed(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if ($map) {
|
||||||
|
$map.on('moveend', updateCamera);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card.Root id="embedding-playground">
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>{$_('embedding.title')}</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<fieldset class="flex flex-col gap-3">
|
||||||
|
<Label for="token">{$_('embedding.mapbox_token')}</Label>
|
||||||
|
<Input id="token" type="text" class="h-8" bind:value={options.token} />
|
||||||
|
<Label for="file_urls">{$_('embedding.file_urls')}</Label>
|
||||||
|
<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>
|
||||||
|
<Select.Root
|
||||||
|
selected={{ value: options.basemap, label: $_(`layers.label.${options.basemap}`) }}
|
||||||
|
onSelectedChange={(selected) => {
|
||||||
|
if (selected?.value) {
|
||||||
|
options.basemap = selected?.value;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Trigger id="basemap" class="w-full h-8">
|
||||||
|
<Select.Value />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content class="max-h-60 overflow-y-scroll">
|
||||||
|
{#each allowedEmbeddingBasemaps as basemap}
|
||||||
|
<Select.Item value={basemap}>{$_(`layers.label.${basemap}`)}</Select.Item>
|
||||||
|
{/each}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<Label for="profile">{$_('menu.elevation_profile')}</Label>
|
||||||
|
<Checkbox id="profile" bind:checked={options.elevation.show} />
|
||||||
|
</div>
|
||||||
|
{#if options.elevation.show}
|
||||||
|
<div class="grid grid-cols-2 gap-x-6 gap-y-3 rounded-md border p-3 mt-1">
|
||||||
|
<Label class="flex flex-row items-center gap-2">
|
||||||
|
{$_('embedding.height')}
|
||||||
|
<Input type="number" bind:value={options.elevation.height} class="h-8 w-20" />
|
||||||
|
</Label>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<span class="shrink-0">
|
||||||
|
{$_('embedding.fill_by')}
|
||||||
|
</span>
|
||||||
|
<Select.Root
|
||||||
|
selected={{ value: 'none', label: $_('embedding.none') }}
|
||||||
|
onSelectedChange={(selected) => {
|
||||||
|
let value = selected?.value;
|
||||||
|
if (value === 'none') {
|
||||||
|
options.elevation.fill = undefined;
|
||||||
|
} else if (value === 'slope' || value === 'surface') {
|
||||||
|
options.elevation.fill = value;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Trigger class="grow h-8">
|
||||||
|
<Select.Value />
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Item value="slope">{$_('quantities.slope')}</Select.Item>
|
||||||
|
<Select.Item value="surface">{$_('quantities.surface')}</Select.Item>
|
||||||
|
<Select.Item value="none">{$_('embedding.none')}</Select.Item>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<Checkbox id="controls" bind:checked={options.elevation.controls} />
|
||||||
|
<Label for="controls">{$_('embedding.show_controls')}</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<Checkbox id="show-speed" bind:checked={options.elevation.speed} />
|
||||||
|
<Label for="show-speed" class="flex flex-row items-center gap-1">
|
||||||
|
<Zap size="16" />
|
||||||
|
{$_('chart.show_speed')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<Checkbox id="show-hr" bind:checked={options.elevation.hr} />
|
||||||
|
<Label for="show-hr" class="flex flex-row items-center gap-1">
|
||||||
|
<HeartPulse size="16" />
|
||||||
|
{$_('chart.show_heartrate')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<Checkbox id="show-cad" bind:checked={options.elevation.cad} />
|
||||||
|
<Label for="show-cad" class="flex flex-row items-center gap-1">
|
||||||
|
<Orbit size="16" />
|
||||||
|
{$_('chart.show_cadence')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<Checkbox id="show-temp" bind:checked={options.elevation.temp} />
|
||||||
|
<Label for="show-temp" class="flex flex-row items-center gap-1">
|
||||||
|
<Thermometer size="16" />
|
||||||
|
{$_('chart.show_temperature')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<Checkbox id="show-power" bind:checked={options.elevation.power} />
|
||||||
|
<Label for="show-power" class="flex flex-row items-center gap-1">
|
||||||
|
<SquareActivity size="16" />
|
||||||
|
{$_('chart.show_power')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<Checkbox id="distance-markers" bind:checked={options.distanceMarkers} />
|
||||||
|
<Label for="distance-markers" class="flex flex-row items-center gap-1">
|
||||||
|
<Coins size="16" />
|
||||||
|
{$_('menu.distance_markers')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<Checkbox id="direction-markers" bind:checked={options.directionMarkers} />
|
||||||
|
<Label for="direction-markers" class="flex flex-row items-center gap-1">
|
||||||
|
<Milestone size="16" />
|
||||||
|
{$_('menu.direction_markers')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row flex-wrap justify-between gap-3">
|
||||||
|
<Label class="flex flex-col items-start gap-2">
|
||||||
|
{$_('menu.distance_units')}
|
||||||
|
<RadioGroup.Root bind:value={options.distanceUnits}>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="metric" id="metric" />
|
||||||
|
<Label for="metric">{$_('menu.metric')}</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="imperial" id="imperial" />
|
||||||
|
<Label for="imperial">{$_('menu.imperial')}</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="nautical" id="nautical" />
|
||||||
|
<Label for="nautical">{$_('menu.nautical')}</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
</Label>
|
||||||
|
<Label class="flex flex-col items-start gap-2">
|
||||||
|
{$_('menu.velocity_units')}
|
||||||
|
<RadioGroup.Root bind:value={options.velocityUnits}>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="speed" id="speed" />
|
||||||
|
<Label for="speed">{$_('quantities.speed')}</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="pace" id="pace" />
|
||||||
|
<Label for="pace">{$_('quantities.pace')}</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
</Label>
|
||||||
|
<Label class="flex flex-col items-start gap-2">
|
||||||
|
{$_('menu.temperature_units')}
|
||||||
|
<RadioGroup.Root bind:value={options.temperatureUnits}>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="celsius" id="celsius" />
|
||||||
|
<Label for="celsius">{$_('menu.celsius')}</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="fahrenheit" id="fahrenheit" />
|
||||||
|
<Label for="fahrenheit">{$_('menu.fahrenheit')}</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<Label class="flex flex-col items-start gap-2">
|
||||||
|
{$_('menu.mode')}
|
||||||
|
<RadioGroup.Root bind:value={options.theme} class="flex flex-row">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="system" id="system" />
|
||||||
|
<Label for="system">{$_('menu.system')}</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="light" id="light" />
|
||||||
|
<Label for="light">{$_('menu.light')}</Label>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<RadioGroup.Item value="dark" id="dark" />
|
||||||
|
<Label for="dark">{$_('menu.dark')}</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup.Root>
|
||||||
|
</Label>
|
||||||
|
<div class="flex flex-col gap-3 p-3 border rounded-md">
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<Checkbox id="manual-camera" bind:checked={manualCamera} />
|
||||||
|
<Label for="manual-camera" class="flex flex-row items-center gap-1">
|
||||||
|
<Video size="16" />
|
||||||
|
{$_('embedding.manual_camera')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{$_('embedding.manual_camera_description')}
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-row flex-wrap items-center gap-6">
|
||||||
|
<Label class="flex flex-col gap-1">
|
||||||
|
<span>{$_('embedding.latitude')}</span>
|
||||||
|
<span>{lat}</span>
|
||||||
|
</Label>
|
||||||
|
<Label class="flex flex-col gap-1">
|
||||||
|
<span>{$_('embedding.longitude')}</span>
|
||||||
|
<span>{lon}</span>
|
||||||
|
</Label>
|
||||||
|
<Label class="flex flex-col gap-1">
|
||||||
|
<span>{$_('embedding.zoom')}</span>
|
||||||
|
<span>{zoom}</span>
|
||||||
|
</Label>
|
||||||
|
<Label class="flex flex-col gap-1">
|
||||||
|
<span>{$_('embedding.bearing')}</span>
|
||||||
|
<span>{bearing}</span>
|
||||||
|
</Label>
|
||||||
|
<Label class="flex flex-col gap-1">
|
||||||
|
<span>{$_('embedding.pitch')}</span>
|
||||||
|
<span>{pitch}</span>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Label>
|
||||||
|
{$_('embedding.preview')}
|
||||||
|
</Label>
|
||||||
|
<div class="relative h-[600px]">
|
||||||
|
<Embedding bind:options={iframeOptions} bind:hash useHash={false} />
|
||||||
|
</div>
|
||||||
|
<Label>
|
||||||
|
{$_('embedding.code')}
|
||||||
|
</Label>
|
||||||
|
<pre class="bg-primary text-primary-foreground p-3 rounded-md whitespace-normal break-all">
|
||||||
|
<code class="language-html">
|
||||||
|
{`<iframe src="https://gpx.rnmkcy.eu${base}/embed?options=${encodeURIComponent(JSON.stringify(getCleanedEmbeddingOptions(options)))}${hash}" width="100%" height="600px" frameborder="0" style="outline: none;"/>`}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</fieldset>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
23
website/src/lib/components/embedding/OpenIn.svelte
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import Logo from '$lib/components/Logo.svelte';
|
||||||
|
import { getURLForLanguage } from '$lib/utils';
|
||||||
|
import { _, locale } from 'svelte-i18n';
|
||||||
|
|
||||||
|
export let files: string[];
|
||||||
|
export let ids: string[];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
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"
|
||||||
|
href="{getURLForLanguage($locale, '/app')}?{files.length > 0
|
||||||
|
? `files=${encodeURIComponent(JSON.stringify(files))}`
|
||||||
|
: ''}{files.length > 0 && ids.length > 0 ? '&' : ''}{ids.length > 0
|
||||||
|
? `ids=${encodeURIComponent(JSON.stringify(ids))}`
|
||||||
|
: ''}"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{$_('menu.open_in')}
|
||||||
|
<Logo class="h-[18px] xs:h-5 translate-y-[1px]" />
|
||||||
|
</Button>
|
89
website/src/lib/components/file-list/FileList.svelte
Executable file
@ -0,0 +1,89 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index';
|
||||||
|
import * as ContextMenu from '$lib/components/ui/context-menu';
|
||||||
|
import FileListNode from './FileListNode.svelte';
|
||||||
|
import { fileObservers, settings } from '$lib/db';
|
||||||
|
import { setContext } from 'svelte';
|
||||||
|
import { ListFileItem, ListLevel, ListRootItem, allowedPastes } from './FileList';
|
||||||
|
import { copied, pasteSelection, selectAll, selection } from './Selection';
|
||||||
|
import { ClipboardPaste, FileStack, Plus } from 'lucide-svelte';
|
||||||
|
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { createFile } from '$lib/stores';
|
||||||
|
|
||||||
|
export let orientation: 'vertical' | 'horizontal';
|
||||||
|
export let recursive = false;
|
||||||
|
|
||||||
|
setContext('orientation', orientation);
|
||||||
|
setContext('recursive', recursive);
|
||||||
|
|
||||||
|
const { verticalFileView } = settings;
|
||||||
|
|
||||||
|
verticalFileView.subscribe(($vertical) => {
|
||||||
|
if ($vertical) {
|
||||||
|
selection.update(($selection) => {
|
||||||
|
$selection.forEach((item) => {
|
||||||
|
if ($selection.hasAnyChildren(item, false)) {
|
||||||
|
$selection.toggle(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return $selection;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
selection.update(($selection) => {
|
||||||
|
$selection.forEach((item) => {
|
||||||
|
if (!(item instanceof ListFileItem)) {
|
||||||
|
$selection.toggle(item);
|
||||||
|
$selection.set(new ListFileItem(item.getFileId()), true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return $selection;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ScrollArea
|
||||||
|
class="shrink-0 {orientation === 'vertical' ? 'p-0 pr-3' : 'h-10 px-1'}"
|
||||||
|
{orientation}
|
||||||
|
scrollbarXClasses={orientation === 'vertical' ? '' : 'mt-1 h-2'}
|
||||||
|
scrollbarYClasses={orientation === 'vertical' ? '' : ''}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex {orientation === 'vertical'
|
||||||
|
? 'flex-col py-1 pl-1 min-h-screen'
|
||||||
|
: 'flex-row'} {$$props.class ?? ''}"
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<FileListNode bind:node={$fileObservers} item={new ListRootItem()} />
|
||||||
|
{#if orientation === 'vertical'}
|
||||||
|
<ContextMenu.Root>
|
||||||
|
<ContextMenu.Trigger class="grow" />
|
||||||
|
<ContextMenu.Content>
|
||||||
|
<ContextMenu.Item on:click={createFile}>
|
||||||
|
<Plus size="16" class="mr-1" />
|
||||||
|
{$_('menu.new_file')}
|
||||||
|
<Shortcut key="+" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Separator />
|
||||||
|
<ContextMenu.Item on:click={selectAll} disabled={$fileObservers.size === 0}>
|
||||||
|
<FileStack size="16" class="mr-1" />
|
||||||
|
{$_('menu.select_all')}
|
||||||
|
<Shortcut key="A" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Separator />
|
||||||
|
<ContextMenu.Item
|
||||||
|
disabled={$copied === undefined ||
|
||||||
|
$copied.length === 0 ||
|
||||||
|
!allowedPastes[$copied[0].level].includes(ListLevel.ROOT)}
|
||||||
|
on:click={pasteSelection}
|
||||||
|
>
|
||||||
|
<ClipboardPaste size="16" class="mr-1" />
|
||||||
|
{$_('menu.paste')}
|
||||||
|
<Shortcut key="V" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</ContextMenu.Content>
|
||||||
|
</ContextMenu.Root>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
440
website/src/lib/components/file-list/FileList.ts
Executable file
@ -0,0 +1,440 @@
|
|||||||
|
import { dbUtils, getFile } from "$lib/db";
|
||||||
|
import { freeze } from "immer";
|
||||||
|
import { GPXFile, Track, TrackSegment, Waypoint } from "gpx";
|
||||||
|
import { selection } from "./Selection";
|
||||||
|
import { newGPXFile } from "$lib/stores";
|
||||||
|
|
||||||
|
export enum ListLevel {
|
||||||
|
ROOT,
|
||||||
|
FILE,
|
||||||
|
TRACK,
|
||||||
|
SEGMENT,
|
||||||
|
WAYPOINTS,
|
||||||
|
WAYPOINT
|
||||||
|
}
|
||||||
|
|
||||||
|
export const allowedMoves: Record<ListLevel, ListLevel[]> = {
|
||||||
|
[ListLevel.ROOT]: [],
|
||||||
|
[ListLevel.FILE]: [ListLevel.FILE],
|
||||||
|
[ListLevel.TRACK]: [ListLevel.FILE, ListLevel.TRACK],
|
||||||
|
[ListLevel.SEGMENT]: [ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
|
||||||
|
[ListLevel.WAYPOINTS]: [ListLevel.WAYPOINTS],
|
||||||
|
[ListLevel.WAYPOINT]: [ListLevel.WAYPOINTS, ListLevel.WAYPOINT]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const allowedPastes: Record<ListLevel, ListLevel[]> = {
|
||||||
|
[ListLevel.ROOT]: [],
|
||||||
|
[ListLevel.FILE]: [ListLevel.ROOT, ListLevel.FILE],
|
||||||
|
[ListLevel.TRACK]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK],
|
||||||
|
[ListLevel.SEGMENT]: [ListLevel.ROOT, ListLevel.FILE, ListLevel.TRACK, ListLevel.SEGMENT],
|
||||||
|
[ListLevel.WAYPOINTS]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT],
|
||||||
|
[ListLevel.WAYPOINT]: [ListLevel.FILE, ListLevel.WAYPOINTS, ListLevel.WAYPOINT]
|
||||||
|
};
|
||||||
|
|
||||||
|
export abstract class ListItem {
|
||||||
|
level: ListLevel;
|
||||||
|
|
||||||
|
constructor(level: ListLevel) {
|
||||||
|
this.level = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getId(): string | number;
|
||||||
|
abstract getFullId(): string;
|
||||||
|
abstract getIdAtLevel(level: ListLevel): string | number | undefined;
|
||||||
|
abstract getFileId(): string;
|
||||||
|
abstract getParent(): ListItem;
|
||||||
|
abstract extend(id: string | number): ListItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ListRootItem extends ListItem {
|
||||||
|
constructor() {
|
||||||
|
super(ListLevel.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): string {
|
||||||
|
return 'root';
|
||||||
|
}
|
||||||
|
|
||||||
|
getFullId(): string {
|
||||||
|
return 'root';
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileId(): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
getParent(): ListItem {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(id: string): ListFileItem {
|
||||||
|
return new ListFileItem(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ListFileItem extends ListItem {
|
||||||
|
fileId: string;
|
||||||
|
|
||||||
|
constructor(fileId: string) {
|
||||||
|
super(ListLevel.FILE);
|
||||||
|
this.fileId = fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): string {
|
||||||
|
return this.fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFullId(): string {
|
||||||
|
return this.fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||||
|
switch (level) {
|
||||||
|
case ListLevel.ROOT:
|
||||||
|
return this.fileId;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileId(): string {
|
||||||
|
return this.fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getParent(): ListItem {
|
||||||
|
return new ListRootItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(id: number | 'waypoints'): ListTrackItem | ListWaypointsItem {
|
||||||
|
if (id === 'waypoints') {
|
||||||
|
return new ListWaypointsItem(this.fileId);
|
||||||
|
} else {
|
||||||
|
return new ListTrackItem(this.fileId, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ListTrackItem extends ListItem {
|
||||||
|
fileId: string;
|
||||||
|
trackIndex: number;
|
||||||
|
|
||||||
|
constructor(fileId: string, trackIndex: number) {
|
||||||
|
super(ListLevel.TRACK);
|
||||||
|
this.fileId = fileId;
|
||||||
|
this.trackIndex = trackIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): number {
|
||||||
|
return this.trackIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFullId(): string {
|
||||||
|
return `${this.fileId}-track-${this.trackIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||||
|
switch (level) {
|
||||||
|
case ListLevel.ROOT:
|
||||||
|
return this.fileId;
|
||||||
|
case ListLevel.FILE:
|
||||||
|
return this.trackIndex;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileId(): string {
|
||||||
|
return this.fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTrackIndex(): number {
|
||||||
|
return this.trackIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getParent(): ListItem {
|
||||||
|
return new ListFileItem(this.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(id: number): ListTrackSegmentItem {
|
||||||
|
return new ListTrackSegmentItem(this.fileId, this.trackIndex, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ListTrackSegmentItem extends ListItem {
|
||||||
|
fileId: string;
|
||||||
|
trackIndex: number;
|
||||||
|
segmentIndex: number;
|
||||||
|
|
||||||
|
constructor(fileId: string, trackIndex: number, segmentIndex: number) {
|
||||||
|
super(ListLevel.SEGMENT);
|
||||||
|
this.fileId = fileId;
|
||||||
|
this.trackIndex = trackIndex;
|
||||||
|
this.segmentIndex = segmentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): number {
|
||||||
|
return this.segmentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFullId(): string {
|
||||||
|
return `${this.fileId}-track-${this.trackIndex}--${this.segmentIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||||
|
switch (level) {
|
||||||
|
case ListLevel.ROOT:
|
||||||
|
return this.fileId;
|
||||||
|
case ListLevel.FILE:
|
||||||
|
return this.trackIndex;
|
||||||
|
case ListLevel.TRACK:
|
||||||
|
return this.segmentIndex;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileId(): string {
|
||||||
|
return this.fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTrackIndex(): number {
|
||||||
|
return this.trackIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSegmentIndex(): number {
|
||||||
|
return this.segmentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getParent(): ListItem {
|
||||||
|
return new ListTrackItem(this.fileId, this.trackIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(): ListTrackSegmentItem {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ListWaypointsItem extends ListItem {
|
||||||
|
fileId: string;
|
||||||
|
|
||||||
|
constructor(fileId: string) {
|
||||||
|
super(ListLevel.WAYPOINTS);
|
||||||
|
this.fileId = fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): string {
|
||||||
|
return 'waypoints';
|
||||||
|
}
|
||||||
|
|
||||||
|
getFullId(): string {
|
||||||
|
return `${this.fileId}-waypoints`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||||
|
switch (level) {
|
||||||
|
case ListLevel.ROOT:
|
||||||
|
return this.fileId;
|
||||||
|
case ListLevel.FILE:
|
||||||
|
return 'waypoints';
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileId(): string {
|
||||||
|
return this.fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getParent(): ListItem {
|
||||||
|
return new ListFileItem(this.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(id: number): ListWaypointItem {
|
||||||
|
return new ListWaypointItem(this.fileId, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ListWaypointItem extends ListItem {
|
||||||
|
fileId: string;
|
||||||
|
waypointIndex: number;
|
||||||
|
|
||||||
|
constructor(fileId: string, waypointIndex: number) {
|
||||||
|
super(ListLevel.WAYPOINT);
|
||||||
|
this.fileId = fileId;
|
||||||
|
this.waypointIndex = waypointIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(): number {
|
||||||
|
return this.waypointIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFullId(): string {
|
||||||
|
return `${this.fileId}-waypoint-${this.waypointIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIdAtLevel(level: ListLevel): string | number | undefined {
|
||||||
|
switch (level) {
|
||||||
|
case ListLevel.ROOT:
|
||||||
|
return this.fileId;
|
||||||
|
case ListLevel.FILE:
|
||||||
|
return 'waypoints';
|
||||||
|
case ListLevel.WAYPOINTS:
|
||||||
|
return this.waypointIndex;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileId(): string {
|
||||||
|
return this.fileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getWaypointIndex(): number {
|
||||||
|
return this.waypointIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getParent(): ListItem {
|
||||||
|
return new ListWaypointsItem(this.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
extend(): ListWaypointItem {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortItems(items: ListItem[], reverse: boolean = false) {
|
||||||
|
items.sort((a, b) => {
|
||||||
|
if (a instanceof ListTrackItem && b instanceof ListTrackItem) {
|
||||||
|
return a.getTrackIndex() - b.getTrackIndex();
|
||||||
|
} else if (a instanceof ListTrackSegmentItem && b instanceof ListTrackSegmentItem) {
|
||||||
|
return a.getSegmentIndex() - b.getSegmentIndex();
|
||||||
|
} else if (a instanceof ListWaypointItem && b instanceof ListWaypointItem) {
|
||||||
|
return a.getWaypointIndex() - b.getWaypointIndex();
|
||||||
|
}
|
||||||
|
return a.level - b.level;
|
||||||
|
});
|
||||||
|
if (reverse) {
|
||||||
|
items.reverse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moveItems(fromParent: ListItem, toParent: ListItem, fromItems: ListItem[], toItems: ListItem[], remove: boolean = true) {
|
||||||
|
if (fromItems.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sortItems(fromItems, false);
|
||||||
|
sortItems(toItems, false);
|
||||||
|
|
||||||
|
let context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[] = [];
|
||||||
|
fromItems.forEach((item) => {
|
||||||
|
let file = getFile(item.getFileId());
|
||||||
|
if (file) {
|
||||||
|
if (item instanceof ListFileItem) {
|
||||||
|
context.push(file.clone());
|
||||||
|
} else if (item instanceof ListTrackItem && item.getTrackIndex() < file.trk.length) {
|
||||||
|
context.push(file.trk[item.getTrackIndex()].clone());
|
||||||
|
} else if (item instanceof ListTrackSegmentItem && item.getTrackIndex() < file.trk.length && item.getSegmentIndex() < file.trk[item.getTrackIndex()].trkseg.length) {
|
||||||
|
context.push(file.trk[item.getTrackIndex()].trkseg[item.getSegmentIndex()].clone());
|
||||||
|
} else if (item instanceof ListWaypointsItem) {
|
||||||
|
context.push(file.wpt.map((wpt) => wpt.clone()));
|
||||||
|
} else if (item instanceof ListWaypointItem && item.getWaypointIndex() < file.wpt.length) {
|
||||||
|
context.push(file.wpt[item.getWaypointIndex()].clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (remove && !(fromParent instanceof ListRootItem)) {
|
||||||
|
sortItems(fromItems, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let files = [fromParent.getFileId(), toParent.getFileId()];
|
||||||
|
let callbacks = [
|
||||||
|
(file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
|
||||||
|
fromItems.forEach((item) => {
|
||||||
|
if (item instanceof ListTrackItem) {
|
||||||
|
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex(), []);
|
||||||
|
} else if (item instanceof ListTrackSegmentItem) {
|
||||||
|
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex(), []);
|
||||||
|
} else if (item instanceof ListWaypointsItem) {
|
||||||
|
file.replaceWaypoints(0, file.wpt.length - 1, []);
|
||||||
|
} else if (item instanceof ListWaypointItem) {
|
||||||
|
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex(), []);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(file, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
|
||||||
|
toItems.forEach((item, i) => {
|
||||||
|
if (item instanceof ListTrackItem) {
|
||||||
|
if (context[i] instanceof Track) {
|
||||||
|
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [context[i]]);
|
||||||
|
} else if (context[i] instanceof TrackSegment) {
|
||||||
|
file.replaceTracks(item.getTrackIndex(), item.getTrackIndex() - 1, [new Track({
|
||||||
|
trkseg: [context[i]]
|
||||||
|
})]);
|
||||||
|
}
|
||||||
|
} else if (item instanceof ListTrackSegmentItem && context[i] instanceof TrackSegment) {
|
||||||
|
file.replaceTrackSegments(item.getTrackIndex(), item.getSegmentIndex(), item.getSegmentIndex() - 1, [context[i]]);
|
||||||
|
} else if (item instanceof ListWaypointsItem) {
|
||||||
|
if (Array.isArray(context[i]) && context[i].length > 0 && context[i][0] instanceof Waypoint) {
|
||||||
|
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, context[i]);
|
||||||
|
} else if (context[i] instanceof Waypoint) {
|
||||||
|
file.replaceWaypoints(file.wpt.length, file.wpt.length - 1, [context[i]]);
|
||||||
|
}
|
||||||
|
} else if (item instanceof ListWaypointItem && context[i] instanceof Waypoint) {
|
||||||
|
file.replaceWaypoints(item.getWaypointIndex(), item.getWaypointIndex() - 1, [context[i]]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (fromParent instanceof ListRootItem) {
|
||||||
|
files = [];
|
||||||
|
callbacks = [];
|
||||||
|
} else if (!remove) {
|
||||||
|
files.splice(0, 1);
|
||||||
|
callbacks.splice(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
dbUtils.applyEachToFilesAndGlobal(files, callbacks, (files, context: (GPXFile | Track | TrackSegment | Waypoint[] | Waypoint)[]) => {
|
||||||
|
toItems.forEach((item, i) => {
|
||||||
|
if (item instanceof ListFileItem) {
|
||||||
|
if (context[i] instanceof GPXFile) {
|
||||||
|
let newFile = context[i];
|
||||||
|
if (remove) {
|
||||||
|
files.delete(newFile._data.id);
|
||||||
|
}
|
||||||
|
newFile._data.id = item.getFileId();
|
||||||
|
files.set(item.getFileId(), freeze(newFile));
|
||||||
|
} else if (context[i] instanceof Track) {
|
||||||
|
let newFile = newGPXFile();
|
||||||
|
newFile._data.id = item.getFileId();
|
||||||
|
if (context[i].name) {
|
||||||
|
newFile.metadata.name = context[i].name;
|
||||||
|
}
|
||||||
|
newFile.replaceTracks(0, 0, [context[i]]);
|
||||||
|
files.set(item.getFileId(), freeze(newFile));
|
||||||
|
} else if (context[i] instanceof TrackSegment) {
|
||||||
|
let newFile = newGPXFile();
|
||||||
|
newFile._data.id = item.getFileId();
|
||||||
|
newFile.replaceTracks(0, 0, [new Track({
|
||||||
|
trkseg: [context[i]]
|
||||||
|
})]);
|
||||||
|
files.set(item.getFileId(), freeze(newFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, context);
|
||||||
|
|
||||||
|
selection.update(($selection) => {
|
||||||
|
$selection.clear();
|
||||||
|
toItems.forEach((item) => {
|
||||||
|
$selection.set(item, true);
|
||||||
|
});
|
||||||
|
return $selection;
|
||||||
|
});
|
||||||
|
}
|
83
website/src/lib/components/file-list/FileListNode.svelte
Executable file
@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
GPXFile,
|
||||||
|
Track,
|
||||||
|
TrackSegment,
|
||||||
|
Waypoint,
|
||||||
|
type AnyGPXTreeElement,
|
||||||
|
type GPXTreeElement
|
||||||
|
} from 'gpx';
|
||||||
|
import { CollapsibleTreeNode } from '$lib/components/collapsible-tree/index';
|
||||||
|
import { settings, type GPXFileWithStatistics } from '$lib/db';
|
||||||
|
import { get, type Readable } from 'svelte/store';
|
||||||
|
import FileListNodeContent from './FileListNodeContent.svelte';
|
||||||
|
import FileListNodeLabel from './FileListNodeLabel.svelte';
|
||||||
|
import { afterUpdate, getContext } from 'svelte';
|
||||||
|
import {
|
||||||
|
ListFileItem,
|
||||||
|
ListTrackSegmentItem,
|
||||||
|
ListWaypointItem,
|
||||||
|
ListWaypointsItem,
|
||||||
|
type ListItem,
|
||||||
|
type ListTrackItem
|
||||||
|
} from './FileList';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { selection } from './Selection';
|
||||||
|
|
||||||
|
export let node:
|
||||||
|
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
||||||
|
| GPXTreeElement<AnyGPXTreeElement>
|
||||||
|
| Waypoint[]
|
||||||
|
| Waypoint;
|
||||||
|
export let item: ListItem;
|
||||||
|
|
||||||
|
let recursive = getContext<boolean>('recursive');
|
||||||
|
|
||||||
|
let collapsible: CollapsibleTreeNode;
|
||||||
|
|
||||||
|
$: label =
|
||||||
|
node instanceof GPXFile && item instanceof ListFileItem
|
||||||
|
? node.metadata.name
|
||||||
|
: node instanceof Track
|
||||||
|
? node.name ?? `${$_('gpx.track')} ${(item as ListTrackItem).trackIndex + 1}`
|
||||||
|
: node instanceof TrackSegment
|
||||||
|
? `${$_('gpx.segment')} ${(item as ListTrackSegmentItem).segmentIndex + 1}`
|
||||||
|
: node instanceof Waypoint
|
||||||
|
? node.name ?? `${$_('gpx.waypoint')} ${(item as ListWaypointItem).waypointIndex + 1}`
|
||||||
|
: node instanceof GPXFile && item instanceof ListWaypointsItem
|
||||||
|
? $_('gpx.waypoints')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const { verticalFileView } = settings;
|
||||||
|
|
||||||
|
function openIfSelectedChild() {
|
||||||
|
if (collapsible && get(verticalFileView) && $selection.hasAnyChildren(item, false)) {
|
||||||
|
collapsible.openNode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($selection) {
|
||||||
|
openIfSelectedChild();
|
||||||
|
}
|
||||||
|
|
||||||
|
afterUpdate(openIfSelectedChild);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if node instanceof Map}
|
||||||
|
<FileListNodeContent {node} {item} />
|
||||||
|
{:else if node instanceof TrackSegment}
|
||||||
|
<FileListNodeLabel {node} {item} {label} />
|
||||||
|
{:else if node instanceof Waypoint}
|
||||||
|
<FileListNodeLabel {node} {item} {label} />
|
||||||
|
{:else if recursive}
|
||||||
|
<CollapsibleTreeNode id={item.getId()} bind:this={collapsible}>
|
||||||
|
<FileListNodeLabel {node} {item} {label} slot="trigger" />
|
||||||
|
<div slot="content" class="ml-2">
|
||||||
|
{#key node}
|
||||||
|
<FileListNodeContent {node} {item} />
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
</CollapsibleTreeNode>
|
||||||
|
{:else}
|
||||||
|
<FileListNodeLabel {node} {item} {label} />
|
||||||
|
{/if}
|
374
website/src/lib/components/file-list/FileListNodeContent.svelte
Executable file
@ -0,0 +1,374 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
let dragging: Writable<ListLevel | null> = writable(null);
|
||||||
|
|
||||||
|
let updating = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
|
||||||
|
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
|
||||||
|
import Sortable from 'sortablejs/Sortable';
|
||||||
|
import { getFileIds, settings, type GPXFileWithStatistics } from '$lib/db';
|
||||||
|
import { get, writable, type Readable, type Writable } from 'svelte/store';
|
||||||
|
import FileListNodeStore from './FileListNodeStore.svelte';
|
||||||
|
import FileListNode from './FileListNode.svelte';
|
||||||
|
import {
|
||||||
|
ListFileItem,
|
||||||
|
ListLevel,
|
||||||
|
ListRootItem,
|
||||||
|
ListWaypointsItem,
|
||||||
|
allowedMoves,
|
||||||
|
moveItems,
|
||||||
|
type ListItem
|
||||||
|
} from './FileList';
|
||||||
|
import { selection } from './Selection';
|
||||||
|
import { isMac } from '$lib/utils';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
|
export let node:
|
||||||
|
| Map<string, Readable<GPXFileWithStatistics | undefined>>
|
||||||
|
| GPXTreeElement<AnyGPXTreeElement>
|
||||||
|
| Waypoint;
|
||||||
|
export let item: ListItem;
|
||||||
|
export let waypointRoot: boolean = false;
|
||||||
|
|
||||||
|
let container: HTMLElement;
|
||||||
|
let elements: { [id: string]: HTMLElement } = {};
|
||||||
|
let sortableLevel: ListLevel =
|
||||||
|
node instanceof Map
|
||||||
|
? ListLevel.FILE
|
||||||
|
: node instanceof GPXFile
|
||||||
|
? waypointRoot
|
||||||
|
? ListLevel.WAYPOINTS
|
||||||
|
: item instanceof ListWaypointsItem
|
||||||
|
? ListLevel.WAYPOINT
|
||||||
|
: ListLevel.TRACK
|
||||||
|
: node instanceof Track
|
||||||
|
? ListLevel.SEGMENT
|
||||||
|
: ListLevel.WAYPOINT;
|
||||||
|
let sortable: Sortable;
|
||||||
|
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
||||||
|
|
||||||
|
let destroyed = false;
|
||||||
|
let lastUpdateStart = 0;
|
||||||
|
function updateToSelection(e) {
|
||||||
|
if (destroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdateStart = Date.now();
|
||||||
|
setTimeout(() => {
|
||||||
|
if (Date.now() - lastUpdateStart >= 40) {
|
||||||
|
if (updating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updating = true;
|
||||||
|
// Sortable updates selection
|
||||||
|
let changed = getChangedIds();
|
||||||
|
if (changed.length > 0) {
|
||||||
|
selection.update(($selection) => {
|
||||||
|
$selection.clear();
|
||||||
|
Object.entries(elements).forEach(([id, element]) => {
|
||||||
|
$selection.set(
|
||||||
|
item.extend(getRealId(id)),
|
||||||
|
element.classList.contains('sortable-selected')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
e.originalEvent &&
|
||||||
|
!(
|
||||||
|
e.originalEvent.ctrlKey ||
|
||||||
|
e.originalEvent.metaKey ||
|
||||||
|
e.originalEvent.shiftKey
|
||||||
|
) &&
|
||||||
|
($selection.size > 1 ||
|
||||||
|
!$selection.has(item.extend(getRealId(changed[0]))))
|
||||||
|
) {
|
||||||
|
// Fix bug that sometimes causes a single select to be treated as a multi-select
|
||||||
|
$selection.clear();
|
||||||
|
$selection.set(item.extend(getRealId(changed[0])), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $selection;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updating = false;
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFromSelection() {
|
||||||
|
if (destroyed || updating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updating = true;
|
||||||
|
// Selection updates sortable
|
||||||
|
let changed = getChangedIds();
|
||||||
|
for (let id of changed) {
|
||||||
|
let element = elements[id];
|
||||||
|
if (element) {
|
||||||
|
if ($selection.has(item.extend(id))) {
|
||||||
|
Sortable.utils.select(element);
|
||||||
|
element.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'nearest'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Sortable.utils.deselect(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updating = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if ($selection) {
|
||||||
|
updateFromSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fileOrder } = settings;
|
||||||
|
function syncFileOrder() {
|
||||||
|
if (!sortable || sortableLevel !== ListLevel.FILE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentOrder = sortable.toArray();
|
||||||
|
if (currentOrder.length !== $fileOrder.length) {
|
||||||
|
sortable.sort($fileOrder);
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < currentOrder.length; i++) {
|
||||||
|
if (currentOrder[i] !== $fileOrder[i]) {
|
||||||
|
sortable.sort($fileOrder);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if ($fileOrder) {
|
||||||
|
syncFileOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSortable() {
|
||||||
|
sortable = Sortable.create(container, {
|
||||||
|
group: {
|
||||||
|
name: sortableLevel,
|
||||||
|
pull: allowedMoves[sortableLevel],
|
||||||
|
put: true
|
||||||
|
},
|
||||||
|
direction: orientation,
|
||||||
|
forceAutoScrollFallback: true,
|
||||||
|
multiDrag: true,
|
||||||
|
multiDragKey: isMac() ? 'Meta' : 'Ctrl',
|
||||||
|
avoidImplicitDeselect: true,
|
||||||
|
onSelect: updateToSelection,
|
||||||
|
onDeselect: updateToSelection,
|
||||||
|
onStart: () => {
|
||||||
|
dragging.set(sortableLevel);
|
||||||
|
},
|
||||||
|
onEnd: () => {
|
||||||
|
dragging.set(null);
|
||||||
|
},
|
||||||
|
onSort: (e) => {
|
||||||
|
if (sortableLevel === ListLevel.FILE) {
|
||||||
|
let newFileOrder = sortable.toArray();
|
||||||
|
if (newFileOrder.length !== get(fileOrder).length) {
|
||||||
|
fileOrder.set(newFileOrder);
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < newFileOrder.length; i++) {
|
||||||
|
if (newFileOrder[i] !== get(fileOrder)[i]) {
|
||||||
|
fileOrder.set(newFileOrder);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fromItem = Sortable.get(e.from)._item;
|
||||||
|
let toItem = Sortable.get(e.to)._item;
|
||||||
|
|
||||||
|
if (item === toItem && !(fromItem instanceof ListRootItem)) {
|
||||||
|
// Event is triggered on source and destination list, only handle it once
|
||||||
|
let fromItems = [];
|
||||||
|
let toItems = [];
|
||||||
|
|
||||||
|
if (Sortable.get(e.from)._waypointRoot) {
|
||||||
|
fromItems = [fromItem.extend('waypoints')];
|
||||||
|
} else {
|
||||||
|
let oldIndices: number[] =
|
||||||
|
e.oldIndicies.length > 0
|
||||||
|
? e.oldIndicies.map((i) => i.index)
|
||||||
|
: [e.oldIndex];
|
||||||
|
oldIndices = oldIndices.filter((i) => i >= 0);
|
||||||
|
oldIndices.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
fromItems = oldIndices.map((i) => fromItem.extend(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Sortable.get(e.from)._waypointRoot && Sortable.get(e.to)._waypointRoot) {
|
||||||
|
toItems = [toItem.extend('waypoints')];
|
||||||
|
} else {
|
||||||
|
if (Sortable.get(e.to)._waypointRoot) {
|
||||||
|
toItem = toItem.extend('waypoints');
|
||||||
|
}
|
||||||
|
|
||||||
|
let newIndices: number[] =
|
||||||
|
e.newIndicies.length > 0
|
||||||
|
? e.newIndicies.map((i) => i.index)
|
||||||
|
: [e.newIndex];
|
||||||
|
newIndices = newIndices.filter((i) => i >= 0);
|
||||||
|
newIndices.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
if (toItem instanceof ListRootItem) {
|
||||||
|
let newFileIds = getFileIds(newIndices.length);
|
||||||
|
toItems = newIndices.map((i, index) => {
|
||||||
|
$fileOrder.splice(i, 0, newFileIds[index]);
|
||||||
|
return item.extend(newFileIds[index]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toItems = newIndices.map((i) => toItem.extend(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveItems(fromItem, toItem, fromItems, toItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.defineProperty(sortable, '_item', {
|
||||||
|
value: item,
|
||||||
|
writable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(sortable, '_waypointRoot', {
|
||||||
|
value: waypointRoot,
|
||||||
|
writable: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
createSortable();
|
||||||
|
destroyed = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
elements = {};
|
||||||
|
container.childNodes.forEach((element) => {
|
||||||
|
if (element instanceof HTMLElement) {
|
||||||
|
let attr = element.getAttribute('data-id');
|
||||||
|
if (attr) {
|
||||||
|
if (node instanceof Map && !node.has(attr)) {
|
||||||
|
element.remove();
|
||||||
|
} else {
|
||||||
|
elements[attr] = element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
syncFileOrder();
|
||||||
|
updateFromSelection();
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
destroyed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
function getChangedIds() {
|
||||||
|
let changed: (string | number)[] = [];
|
||||||
|
Object.entries(elements).forEach(([id, element]) => {
|
||||||
|
let realId = getRealId(id);
|
||||||
|
let realItem = item.extend(realId);
|
||||||
|
let inSelection = get(selection).has(realItem);
|
||||||
|
let isSelected = element.classList.contains('sortable-selected');
|
||||||
|
if (inSelection !== isSelected) {
|
||||||
|
changed.push(realId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRealId(id: string | number) {
|
||||||
|
return sortableLevel === ListLevel.FILE || sortableLevel === ListLevel.WAYPOINTS
|
||||||
|
? id
|
||||||
|
: parseInt(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: canDrop = $dragging !== null && allowedMoves[$dragging].includes(sortableLevel);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={container}
|
||||||
|
class="sortable {orientation} flex {orientation === 'vertical'
|
||||||
|
? 'flex-col'
|
||||||
|
: 'flex-row gap-1'} {canDrop ? 'min-h-5' : ''}"
|
||||||
|
>
|
||||||
|
{#if node instanceof Map}
|
||||||
|
{#each node as [fileId, file] (fileId)}
|
||||||
|
<div data-id={fileId}>
|
||||||
|
<FileListNodeStore {file} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{:else if node instanceof GPXFile}
|
||||||
|
{#if item instanceof ListWaypointsItem}
|
||||||
|
{#each node.wpt as wpt, i (wpt)}
|
||||||
|
<div data-id={i} class="ml-1">
|
||||||
|
<FileListNode node={wpt} item={item.extend(i)} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{:else if waypointRoot}
|
||||||
|
{#if node.wpt.length > 0}
|
||||||
|
<div data-id="waypoints">
|
||||||
|
<FileListNode {node} item={item.extend('waypoints')} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
{#each node.children as child, i (child)}
|
||||||
|
<div data-id={i}>
|
||||||
|
<FileListNode node={child} item={item.extend(i)} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{:else if node instanceof Track}
|
||||||
|
{#each node.children as child, i (child)}
|
||||||
|
<div data-id={i} class="ml-1">
|
||||||
|
<FileListNode node={child} item={item.extend(i)} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if node instanceof GPXFile && item instanceof ListFileItem}
|
||||||
|
{#if !waypointRoot}
|
||||||
|
<svelte:self {node} {item} waypointRoot={true} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.sortable > div {
|
||||||
|
@apply rounded-md;
|
||||||
|
@apply h-fit;
|
||||||
|
@apply leading-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical :global(button) {
|
||||||
|
@apply hover:bg-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical :global(.sortable-selected button) {
|
||||||
|
@apply hover:bg-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical :global(.sortable-selected) {
|
||||||
|
@apply bg-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal :global(button) {
|
||||||
|
@apply bg-accent;
|
||||||
|
@apply hover:bg-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal :global(.sortable-selected button) {
|
||||||
|
@apply bg-background;
|
||||||
|
}
|
||||||
|
</style>
|
320
website/src/lib/components/file-list/FileListNodeLabel.svelte
Executable file
@ -0,0 +1,320 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import * as ContextMenu from '$lib/components/ui/context-menu';
|
||||||
|
import Shortcut from '$lib/components/Shortcut.svelte';
|
||||||
|
import { dbUtils, getFile } from '$lib/db';
|
||||||
|
import {
|
||||||
|
Copy,
|
||||||
|
Info,
|
||||||
|
MapPin,
|
||||||
|
PaintBucket,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
Waypoints,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
|
ClipboardCopy,
|
||||||
|
ClipboardPaste,
|
||||||
|
Maximize,
|
||||||
|
Scissors,
|
||||||
|
FileStack,
|
||||||
|
FileX
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
import {
|
||||||
|
ListFileItem,
|
||||||
|
ListLevel,
|
||||||
|
ListTrackItem,
|
||||||
|
ListWaypointItem,
|
||||||
|
allowedPastes,
|
||||||
|
type ListItem
|
||||||
|
} from './FileList';
|
||||||
|
import {
|
||||||
|
copied,
|
||||||
|
copySelection,
|
||||||
|
cut,
|
||||||
|
cutSelection,
|
||||||
|
pasteSelection,
|
||||||
|
selectAll,
|
||||||
|
selectItem,
|
||||||
|
selection
|
||||||
|
} from './Selection';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import {
|
||||||
|
allHidden,
|
||||||
|
editMetadata,
|
||||||
|
editStyle,
|
||||||
|
embedding,
|
||||||
|
centerMapOnSelection,
|
||||||
|
gpxLayers,
|
||||||
|
map
|
||||||
|
} from '$lib/stores';
|
||||||
|
import {
|
||||||
|
GPXTreeElement,
|
||||||
|
Track,
|
||||||
|
TrackSegment,
|
||||||
|
type AnyGPXTreeElement,
|
||||||
|
Waypoint,
|
||||||
|
GPXFile
|
||||||
|
} from 'gpx';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
import MetadataDialog from './MetadataDialog.svelte';
|
||||||
|
import StyleDialog from './StyleDialog.svelte';
|
||||||
|
|
||||||
|
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
||||||
|
export let item: ListItem;
|
||||||
|
export let label: string | undefined;
|
||||||
|
|
||||||
|
let orientation = getContext<'vertical' | 'horizontal'>('orientation');
|
||||||
|
|
||||||
|
$: singleSelection = $selection.size === 1;
|
||||||
|
|
||||||
|
let nodeColors: string[] = [];
|
||||||
|
|
||||||
|
$: if (node && $map) {
|
||||||
|
nodeColors = [];
|
||||||
|
|
||||||
|
if (node instanceof GPXFile) {
|
||||||
|
let style = node.getStyle();
|
||||||
|
|
||||||
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
|
if (layer) {
|
||||||
|
style.color.push(layer.layerColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
style.color.forEach((c) => {
|
||||||
|
if (!nodeColors.includes(c)) {
|
||||||
|
nodeColors.push(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (node instanceof Track) {
|
||||||
|
let style = node.getStyle();
|
||||||
|
if (style) {
|
||||||
|
if (style.color && !nodeColors.includes(style.color)) {
|
||||||
|
nodeColors.push(style.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nodeColors.length === 0) {
|
||||||
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
|
if (layer) {
|
||||||
|
nodeColors.push(layer.layerColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let openEditMetadata: boolean = false;
|
||||||
|
let openEditStyle: boolean = false;
|
||||||
|
|
||||||
|
$: openEditMetadata = $editMetadata && singleSelection && $selection.has(item);
|
||||||
|
$: openEditStyle =
|
||||||
|
$editStyle &&
|
||||||
|
$selection.has(item) &&
|
||||||
|
$selection.getSelected().findIndex((i) => i.getFullId() === item.getFullId()) === 0;
|
||||||
|
$: hidden = item.level === ListLevel.WAYPOINTS ? node._data.hiddenWpt : node._data.hidden;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<ContextMenu.Root
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (open) {
|
||||||
|
if (!get(selection).has(item)) {
|
||||||
|
selectItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContextMenu.Trigger class="grow truncate">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="relative w-full p-0 px-1 border-none overflow-hidden focus-visible:ring-0 focus-visible:ring-offset-0 {orientation ===
|
||||||
|
'vertical'
|
||||||
|
? 'h-fit'
|
||||||
|
: 'h-9 px-1.5 shadow-md'} pointer-events-auto"
|
||||||
|
>
|
||||||
|
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
|
||||||
|
<MetadataDialog bind:open={openEditMetadata} {node} {item} />
|
||||||
|
<StyleDialog bind:open={openEditStyle} {item} />
|
||||||
|
{/if}
|
||||||
|
{#if item.level === ListLevel.FILE || item.level === ListLevel.TRACK}
|
||||||
|
<div
|
||||||
|
class="absolute {orientation === 'vertical'
|
||||||
|
? 'top-0 bottom-0 right-1 w-1'
|
||||||
|
: 'top-0 h-1 left-0 right-0'}"
|
||||||
|
style="background:linear-gradient(to {orientation === 'vertical'
|
||||||
|
? 'bottom'
|
||||||
|
: 'right'},{nodeColors
|
||||||
|
.map(
|
||||||
|
(c, i) =>
|
||||||
|
`${c} ${Math.floor((100 * i) / nodeColors.length)}% ${Math.floor((100 * (i + 1)) / nodeColors.length)}%`
|
||||||
|
)
|
||||||
|
.join(',')})"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<span
|
||||||
|
class="w-full text-left truncate py-1 flex flex-row items-center {hidden
|
||||||
|
? 'text-muted-foreground'
|
||||||
|
: ''} {$cut && $copied?.some((i) => i.getFullId() === item.getFullId())
|
||||||
|
? 'text-muted-foreground'
|
||||||
|
: ''}"
|
||||||
|
on:contextmenu={(e) => {
|
||||||
|
if ($embedding) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
// Add to selection instead of opening context menu
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
$selection.toggle(item);
|
||||||
|
$selection = $selection;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:mouseenter={() => {
|
||||||
|
if (item instanceof ListWaypointItem) {
|
||||||
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
|
let file = getFile(item.getFileId());
|
||||||
|
if (layer && file) {
|
||||||
|
let waypoint = file.wpt[item.getWaypointIndex()];
|
||||||
|
if (waypoint) {
|
||||||
|
layer.showWaypointPopup(waypoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
on:mouseleave={() => {
|
||||||
|
if (item instanceof ListWaypointItem) {
|
||||||
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
|
if (layer) {
|
||||||
|
layer.hideWaypointPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if item.level === ListLevel.SEGMENT}
|
||||||
|
<Waypoints size="16" class="mr-1 shrink-0" />
|
||||||
|
{:else if item.level === ListLevel.WAYPOINT}
|
||||||
|
<MapPin size="16" class="mr-1 shrink-0" />
|
||||||
|
{/if}
|
||||||
|
<span class="grow select-none truncate {orientation === 'vertical' ? 'last:mr-2' : ''}">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
{#if hidden}
|
||||||
|
<EyeOff
|
||||||
|
size="12"
|
||||||
|
class="shrink-0 mt-1 ml-1 {orientation === 'vertical' ? 'mr-2' : ''} {item.level ===
|
||||||
|
ListLevel.SEGMENT || item.level === ListLevel.WAYPOINT
|
||||||
|
? 'mr-3'
|
||||||
|
: ''}"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</ContextMenu.Trigger>
|
||||||
|
<ContextMenu.Content>
|
||||||
|
{#if item instanceof ListFileItem || item instanceof ListTrackItem}
|
||||||
|
<ContextMenu.Item disabled={!singleSelection} on:click={() => ($editMetadata = true)}>
|
||||||
|
<Info size="16" class="mr-1" />
|
||||||
|
{$_('menu.metadata.button')}
|
||||||
|
<Shortcut key="I" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Item on:click={() => ($editStyle = true)}>
|
||||||
|
<PaintBucket size="16" class="mr-1" />
|
||||||
|
{$_('menu.style.button')}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
{/if}
|
||||||
|
<ContextMenu.Item
|
||||||
|
on:click={() => {
|
||||||
|
if ($allHidden) {
|
||||||
|
dbUtils.setHiddenToSelection(false);
|
||||||
|
} else {
|
||||||
|
dbUtils.setHiddenToSelection(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if $allHidden}
|
||||||
|
<Eye size="16" class="mr-1" />
|
||||||
|
{$_('menu.unhide')}
|
||||||
|
{:else}
|
||||||
|
<EyeOff size="16" class="mr-1" />
|
||||||
|
{$_('menu.hide')}
|
||||||
|
{/if}
|
||||||
|
<Shortcut key="H" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Separator />
|
||||||
|
{#if orientation === 'vertical'}
|
||||||
|
{#if item instanceof ListFileItem}
|
||||||
|
<ContextMenu.Item
|
||||||
|
disabled={!singleSelection}
|
||||||
|
on:click={() => dbUtils.addNewTrack(item.getFileId())}
|
||||||
|
>
|
||||||
|
<Plus size="16" class="mr-1" />
|
||||||
|
{$_('menu.new_track')}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Separator />
|
||||||
|
{:else if item instanceof ListTrackItem}
|
||||||
|
<ContextMenu.Item
|
||||||
|
disabled={!singleSelection}
|
||||||
|
on:click={() => dbUtils.addNewSegment(item.getFileId(), item.getTrackIndex())}
|
||||||
|
>
|
||||||
|
<Plus size="16" class="mr-1" />
|
||||||
|
{$_('menu.new_segment')}
|
||||||
|
</ContextMenu.Item>
|
||||||
|
<ContextMenu.Separator />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if item.level !== ListLevel.WAYPOINTS}
|
||||||
|
<ContextMenu.Item on:click={selectAll}>
|
||||||
|
<FileStack size="16" class="mr-1" />
|
||||||
|
{$_('menu.select_all')}
|
||||||
|
<Shortcut key="A" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
{/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'}
|
||||||
|
<ContextMenu.Item on:click={copySelection}>
|
||||||
|
<ClipboardCopy size="16" class="mr-1" />
|
||||||
|
{$_('menu.copy')}
|
||||||
|
<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 />
|
||||||
|
<ContextMenu.Item on:click={dbUtils.deleteSelection}>
|
||||||
|
{#if item instanceof ListFileItem}
|
||||||
|
<FileX size="16" class="mr-1" />
|
||||||
|
{$_('menu.close')}
|
||||||
|
{:else}
|
||||||
|
<Trash2 size="16" class="mr-1" />
|
||||||
|
{$_('menu.delete')}
|
||||||
|
{/if}
|
||||||
|
<Shortcut key="⌫" ctrl={true} />
|
||||||
|
</ContextMenu.Item>
|
||||||
|
</ContextMenu.Content>
|
||||||
|
</ContextMenu.Root>
|
23
website/src/lib/components/file-list/FileListNodeStore.svelte
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CollapsibleTree from '$lib/components/collapsible-tree/CollapsibleTree.svelte';
|
||||||
|
import FileListNode from '$lib/components/file-list/FileListNode.svelte';
|
||||||
|
|
||||||
|
import type { GPXFileWithStatistics } from '$lib/db';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import type { Readable } from 'svelte/store';
|
||||||
|
import { ListFileItem } from './FileList';
|
||||||
|
|
||||||
|
export let file: Readable<GPXFileWithStatistics | undefined>;
|
||||||
|
|
||||||
|
let recursive = getContext<boolean>('recursive');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $file}
|
||||||
|
{#if recursive}
|
||||||
|
<CollapsibleTree side="left" defaultState="closed" slotInsideTrigger={false}>
|
||||||
|
<FileListNode node={$file.file} item={new ListFileItem($file.file._data.id)} />
|
||||||
|
</CollapsibleTree>
|
||||||
|
{:else}
|
||||||
|
<FileListNode node={$file.file} item={new ListFileItem($file.file._data.id)} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
65
website/src/lib/components/file-list/MetadataDialog.svelte
Executable file
@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import { Textarea } from '$lib/components/ui/textarea';
|
||||||
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
|
import * as Popover from '$lib/components/ui/popover';
|
||||||
|
import { dbUtils } from '$lib/db';
|
||||||
|
import { Save } from 'lucide-svelte';
|
||||||
|
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
|
||||||
|
import { GPXTreeElement, Track, type AnyGPXTreeElement, Waypoint, GPXFile } from 'gpx';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { editMetadata } from '$lib/stores';
|
||||||
|
|
||||||
|
export let node: GPXTreeElement<AnyGPXTreeElement> | Waypoint[] | Waypoint;
|
||||||
|
export let item: ListItem;
|
||||||
|
export let open = false;
|
||||||
|
|
||||||
|
let name: string =
|
||||||
|
node instanceof GPXFile
|
||||||
|
? node.metadata.name ?? ''
|
||||||
|
: node instanceof Track
|
||||||
|
? node.name ?? ''
|
||||||
|
: '';
|
||||||
|
let description: string =
|
||||||
|
node instanceof GPXFile
|
||||||
|
? node.metadata.desc ?? ''
|
||||||
|
: node instanceof Track
|
||||||
|
? node.desc ?? ''
|
||||||
|
: '';
|
||||||
|
|
||||||
|
$: if (!open) {
|
||||||
|
$editMetadata = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Popover.Root bind:open>
|
||||||
|
<Popover.Trigger />
|
||||||
|
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
|
||||||
|
<Label for="name">{$_('menu.metadata.name')}</Label>
|
||||||
|
<Input bind:value={name} id="name" class="font-semibold h-8" />
|
||||||
|
<Label for="description">{$_('menu.metadata.description')}</Label>
|
||||||
|
<Textarea bind:value={description} id="description" />
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
on:click={() => {
|
||||||
|
dbUtils.applyToFile(item.getFileId(), (file) => {
|
||||||
|
if (item instanceof ListFileItem && node instanceof GPXFile) {
|
||||||
|
file.metadata.name = name;
|
||||||
|
file.metadata.desc = description;
|
||||||
|
if (file.trk.length === 1) {
|
||||||
|
file.trk[0].name = name;
|
||||||
|
}
|
||||||
|
} else if (item instanceof ListTrackItem && node instanceof Track) {
|
||||||
|
file.trk[item.getTrackIndex()].name = name;
|
||||||
|
file.trk[item.getTrackIndex()].desc = description;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
open = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Save size="16" class="mr-1" />
|
||||||
|
{$_('menu.metadata.save')}
|
||||||
|
</Button>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
315
website/src/lib/components/file-list/Selection.ts
Executable file
@ -0,0 +1,315 @@
|
|||||||
|
import { get, writable } from "svelte/store";
|
||||||
|
import { ListFileItem, ListItem, ListRootItem, ListTrackItem, ListTrackSegmentItem, ListWaypointItem, ListLevel, sortItems, ListWaypointsItem, moveItems } from "./FileList";
|
||||||
|
import { fileObservers, getFile, getFileIds, settings } from "$lib/db";
|
||||||
|
|
||||||
|
export class SelectionTreeType {
|
||||||
|
item: ListItem;
|
||||||
|
selected: boolean;
|
||||||
|
children: {
|
||||||
|
[key: string | number]: SelectionTreeType
|
||||||
|
};
|
||||||
|
size: number = 0;
|
||||||
|
|
||||||
|
constructor(item: ListItem) {
|
||||||
|
this.item = item;
|
||||||
|
this.selected = false;
|
||||||
|
this.children = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.selected = false;
|
||||||
|
for (let key in this.children) {
|
||||||
|
this.children[key].clear();
|
||||||
|
}
|
||||||
|
this.size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setOrToggle(item: ListItem, value?: boolean) {
|
||||||
|
if (item.level === this.item.level) {
|
||||||
|
let newSelected = value === undefined ? !this.selected : value;
|
||||||
|
if (this.selected !== newSelected) {
|
||||||
|
this.selected = newSelected;
|
||||||
|
this.size += this.selected ? 1 : -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let id = item.getIdAtLevel(this.item.level);
|
||||||
|
if (id !== undefined) {
|
||||||
|
if (!this.children.hasOwnProperty(id)) {
|
||||||
|
this.children[id] = new SelectionTreeType(this.item.extend(id));
|
||||||
|
}
|
||||||
|
this.size -= this.children[id].size;
|
||||||
|
this.children[id]._setOrToggle(item, value);
|
||||||
|
this.size += this.children[id].size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set(item: ListItem, value: boolean) {
|
||||||
|
this._setOrToggle(item, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(item: ListItem) {
|
||||||
|
this._setOrToggle(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
has(item: ListItem): boolean {
|
||||||
|
if (item.level === this.item.level) {
|
||||||
|
return this.selected;
|
||||||
|
} else {
|
||||||
|
let id = item.getIdAtLevel(this.item.level);
|
||||||
|
if (id !== undefined) {
|
||||||
|
if (this.children.hasOwnProperty(id)) {
|
||||||
|
return this.children[id].has(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAnyParent(item: ListItem, self: boolean = true): boolean {
|
||||||
|
if (this.selected && this.item.level <= item.level && (self || this.item.level < item.level)) {
|
||||||
|
return this.selected;
|
||||||
|
}
|
||||||
|
let id = item.getIdAtLevel(this.item.level);
|
||||||
|
if (id !== undefined) {
|
||||||
|
if (this.children.hasOwnProperty(id)) {
|
||||||
|
return this.children[id].hasAnyParent(item, self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
|
||||||
|
if (this.selected && this.item.level >= item.level && (self || this.item.level > item.level)) {
|
||||||
|
return this.selected;
|
||||||
|
}
|
||||||
|
let id = item.getIdAtLevel(this.item.level);
|
||||||
|
if (id !== undefined) {
|
||||||
|
if (ignoreIds === undefined || ignoreIds.indexOf(id) === -1) {
|
||||||
|
if (this.children.hasOwnProperty(id)) {
|
||||||
|
return this.children[id].hasAnyChildren(item, self, ignoreIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let key in this.children) {
|
||||||
|
if (ignoreIds === undefined || ignoreIds.indexOf(key) === -1) {
|
||||||
|
if (this.children[key].hasAnyChildren(item, self, ignoreIds)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelected(selection: ListItem[] = []): ListItem[] {
|
||||||
|
if (this.selected) {
|
||||||
|
selection.push(this.item);
|
||||||
|
}
|
||||||
|
for (let key in this.children) {
|
||||||
|
this.children[key].getSelected(selection);
|
||||||
|
}
|
||||||
|
return selection;
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(callback: (item: ListItem) => void) {
|
||||||
|
if (this.selected) {
|
||||||
|
callback(this.item);
|
||||||
|
}
|
||||||
|
for (let key in this.children) {
|
||||||
|
this.children[key].forEach(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getChild(id: string | number): SelectionTreeType | undefined {
|
||||||
|
return this.children[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteChild(id: string | number) {
|
||||||
|
if (this.children.hasOwnProperty(id)) {
|
||||||
|
this.size -= this.children[id].size;
|
||||||
|
delete this.children[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
|
||||||
|
|
||||||
|
export function selectItem(item: ListItem) {
|
||||||
|
selection.update(($selection) => {
|
||||||
|
$selection.clear();
|
||||||
|
$selection.set(item, true);
|
||||||
|
return $selection;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectFile(fileId: string) {
|
||||||
|
selectItem(new ListFileItem(fileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSelectItem(item: ListItem) {
|
||||||
|
selection.update(($selection) => {
|
||||||
|
$selection.toggle(item);
|
||||||
|
return $selection;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSelectFile(fileId: string) {
|
||||||
|
addSelectItem(new ListFileItem(fileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectAll() {
|
||||||
|
selection.update(($selection) => {
|
||||||
|
let item: ListItem = new ListRootItem();
|
||||||
|
$selection.forEach((i) => {
|
||||||
|
item = i;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (item instanceof ListRootItem || item instanceof ListFileItem) {
|
||||||
|
$selection.clear();
|
||||||
|
get(fileObservers).forEach((_file, fileId) => {
|
||||||
|
$selection.set(new ListFileItem(fileId), true);
|
||||||
|
});
|
||||||
|
} else if (item instanceof ListTrackItem) {
|
||||||
|
let file = getFile(item.getFileId());
|
||||||
|
if (file) {
|
||||||
|
file.trk.forEach((_track, trackId) => {
|
||||||
|
$selection.set(new ListTrackItem(item.getFileId(), trackId), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (item instanceof ListTrackSegmentItem) {
|
||||||
|
let file = getFile(item.getFileId());
|
||||||
|
if (file) {
|
||||||
|
file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
|
||||||
|
$selection.set(new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (item instanceof ListWaypointItem) {
|
||||||
|
let file = getFile(item.getFileId());
|
||||||
|
if (file) {
|
||||||
|
file.wpt.forEach((_waypoint, waypointId) => {
|
||||||
|
$selection.set(new ListWaypointItem(item.getFileId(), waypointId), true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $selection;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOrderedSelection(reverse: boolean = false): ListItem[] {
|
||||||
|
let selected: ListItem[] = [];
|
||||||
|
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
|
||||||
|
selected.push(...items);
|
||||||
|
}, reverse);
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyToOrderedItemsFromFile(selectedItems: ListItem[], callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) {
|
||||||
|
get(settings.fileOrder).forEach((fileId) => {
|
||||||
|
let level: ListLevel | undefined = undefined;
|
||||||
|
let items: ListItem[] = [];
|
||||||
|
selectedItems.forEach((item) => {
|
||||||
|
if (item.getFileId() === fileId) {
|
||||||
|
level = item.level;
|
||||||
|
if (item instanceof ListFileItem || item instanceof ListTrackItem || item instanceof ListTrackSegmentItem || item instanceof ListWaypointsItem || item instanceof ListWaypointItem) {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (items.length > 0) {
|
||||||
|
sortItems(items, reverse);
|
||||||
|
callback(fileId, level, items);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyToOrderedSelectedItemsFromFile(callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void, reverse: boolean = true) {
|
||||||
|
applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const copied = writable<ListItem[] | undefined>(undefined);
|
||||||
|
export const cut = writable(false);
|
||||||
|
|
||||||
|
export function copySelection(): boolean {
|
||||||
|
let selected = get(selection).getSelected();
|
||||||
|
if (selected.length > 0) {
|
||||||
|
copied.set(selected);
|
||||||
|
cut.set(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cutSelection() {
|
||||||
|
if (copySelection()) {
|
||||||
|
cut.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetCopied() {
|
||||||
|
copied.set(undefined);
|
||||||
|
cut.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pasteSelection() {
|
||||||
|
let fromItems = get(copied);
|
||||||
|
if (fromItems === undefined || fromItems.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected = get(selection).getSelected();
|
||||||
|
if (selected.length === 0) {
|
||||||
|
selected = [new ListRootItem()];
|
||||||
|
}
|
||||||
|
|
||||||
|
let fromParent = fromItems[0].getParent();
|
||||||
|
let toParent = selected[selected.length - 1];
|
||||||
|
|
||||||
|
let startIndex: number | undefined = undefined;
|
||||||
|
|
||||||
|
if (fromItems[0].level === toParent.level) {
|
||||||
|
if (toParent instanceof ListTrackItem || toParent instanceof ListTrackSegmentItem || toParent instanceof ListWaypointItem) {
|
||||||
|
startIndex = toParent.getId() + 1;
|
||||||
|
}
|
||||||
|
toParent = toParent.getParent();
|
||||||
|
}
|
||||||
|
|
||||||
|
let toItems: ListItem[] = [];
|
||||||
|
if (toParent.level === ListLevel.ROOT) {
|
||||||
|
let fileIds = getFileIds(fromItems.length);
|
||||||
|
fileIds.forEach((fileId) => {
|
||||||
|
toItems.push(new ListFileItem(fileId));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let toFile = getFile(toParent.getFileId());
|
||||||
|
if (toFile) {
|
||||||
|
fromItems.forEach((item, index) => {
|
||||||
|
if (toParent instanceof ListFileItem) {
|
||||||
|
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
|
||||||
|
toItems.push(new ListTrackItem(toParent.getFileId(), (startIndex ?? toFile.trk.length) + index));
|
||||||
|
} else if (item instanceof ListWaypointsItem) {
|
||||||
|
toItems.push(new ListWaypointsItem(toParent.getFileId()));
|
||||||
|
} else if (item instanceof ListWaypointItem) {
|
||||||
|
toItems.push(new ListWaypointItem(toParent.getFileId(), (startIndex ?? toFile.wpt.length) + index));
|
||||||
|
}
|
||||||
|
} else if (toParent instanceof ListTrackItem) {
|
||||||
|
if (item instanceof ListTrackSegmentItem) {
|
||||||
|
let toTrackIndex = toParent.getTrackIndex();
|
||||||
|
toItems.push(new ListTrackSegmentItem(toParent.getFileId(), toTrackIndex, (startIndex ?? toFile.trk[toTrackIndex].trkseg.length) + index));
|
||||||
|
}
|
||||||
|
} else if (toParent instanceof ListWaypointsItem) {
|
||||||
|
if (item instanceof ListWaypointItem) {
|
||||||
|
toItems.push(new ListWaypointItem(toParent.getFileId(), (startIndex ?? toFile.wpt.length) + index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromItems.length === toItems.length) {
|
||||||
|
moveItems(fromParent, toParent, fromItems, toItems, get(cut));
|
||||||
|
resetCopied();
|
||||||
|
}
|
||||||
|
}
|
167
website/src/lib/components/file-list/StyleDialog.svelte
Executable file
@ -0,0 +1,167 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
|
import { Slider } from '$lib/components/ui/slider';
|
||||||
|
import * as Popover from '$lib/components/ui/popover';
|
||||||
|
import { dbUtils, getFile, settings } from '$lib/db';
|
||||||
|
import { Save } from 'lucide-svelte';
|
||||||
|
import { ListFileItem, ListTrackItem, type ListItem } from './FileList';
|
||||||
|
import { selection } from './Selection';
|
||||||
|
import { editStyle, gpxLayers } from '$lib/stores';
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
|
export let item: ListItem;
|
||||||
|
export let open = false;
|
||||||
|
|
||||||
|
const { defaultOpacity, defaultWeight } = settings;
|
||||||
|
|
||||||
|
let colors: string[] = [];
|
||||||
|
let color: string | undefined = undefined;
|
||||||
|
let opacity: number[] = [];
|
||||||
|
let weight: number[] = [];
|
||||||
|
let colorChanged = false;
|
||||||
|
let opacityChanged = false;
|
||||||
|
let weightChanged = false;
|
||||||
|
|
||||||
|
function setStyleInputs() {
|
||||||
|
colors = [];
|
||||||
|
opacity = [];
|
||||||
|
weight = [];
|
||||||
|
|
||||||
|
$selection.forEach((item) => {
|
||||||
|
if (item instanceof ListFileItem) {
|
||||||
|
let file = getFile(item.getFileId());
|
||||||
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
|
if (file && layer) {
|
||||||
|
let style = file.getStyle();
|
||||||
|
style.color.push(layer.layerColor);
|
||||||
|
|
||||||
|
style.color.forEach((c) => {
|
||||||
|
if (!colors.includes(c)) {
|
||||||
|
colors.push(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
style.opacity.forEach((o) => {
|
||||||
|
if (!opacity.includes(o)) {
|
||||||
|
opacity.push(o);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
style.weight.forEach((w) => {
|
||||||
|
if (!weight.includes(w)) {
|
||||||
|
weight.push(w);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (item instanceof ListTrackItem) {
|
||||||
|
let file = getFile(item.getFileId());
|
||||||
|
let layer = gpxLayers.get(item.getFileId());
|
||||||
|
if (file && layer) {
|
||||||
|
let track = file.trk[item.getTrackIndex()];
|
||||||
|
let style = track.getStyle();
|
||||||
|
if (style) {
|
||||||
|
if (style.color && !colors.includes(style.color)) {
|
||||||
|
colors.push(style.color);
|
||||||
|
}
|
||||||
|
if (style.opacity && !opacity.includes(style.opacity)) {
|
||||||
|
opacity.push(style.opacity);
|
||||||
|
}
|
||||||
|
if (style.weight && !weight.includes(style.weight)) {
|
||||||
|
weight.push(style.weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!colors.includes(layer.layerColor)) {
|
||||||
|
colors.push(layer.layerColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
color = colors[0];
|
||||||
|
opacity = [opacity[0] ?? $defaultOpacity];
|
||||||
|
weight = [weight[0] ?? $defaultWeight];
|
||||||
|
|
||||||
|
colorChanged = false;
|
||||||
|
opacityChanged = false;
|
||||||
|
weightChanged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if ($selection && open) {
|
||||||
|
setStyleInputs();
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (!open) {
|
||||||
|
$editStyle = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Popover.Root bind:open>
|
||||||
|
<Popover.Trigger />
|
||||||
|
<Popover.Content side="top" sideOffset={22} alignOffset={30} class="flex flex-col gap-3">
|
||||||
|
<Label class="flex flex-row gap-2 items-center justify-between">
|
||||||
|
{$_('menu.style.color')}
|
||||||
|
<Input
|
||||||
|
bind:value={color}
|
||||||
|
type="color"
|
||||||
|
class="p-0 h-6 w-40"
|
||||||
|
on:change={() => (colorChanged = true)}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
<Label class="flex flex-row gap-2 items-center justify-between">
|
||||||
|
{$_('menu.style.opacity')}
|
||||||
|
<div class="w-40 p-2">
|
||||||
|
<Slider
|
||||||
|
bind:value={opacity}
|
||||||
|
min={0.3}
|
||||||
|
max={1}
|
||||||
|
step={0.1}
|
||||||
|
onValueChange={() => (opacityChanged = true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
<Label class="flex flex-row gap-2 items-center justify-between">
|
||||||
|
{$_('menu.style.width')}
|
||||||
|
<div class="w-40 p-2">
|
||||||
|
<Slider
|
||||||
|
bind:value={weight}
|
||||||
|
id="weight"
|
||||||
|
min={1}
|
||||||
|
max={10}
|
||||||
|
step={1}
|
||||||
|
onValueChange={() => (weightChanged = true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled={!colorChanged && !opacityChanged && !weightChanged}
|
||||||
|
on:click={() => {
|
||||||
|
let style = {};
|
||||||
|
if (colorChanged) {
|
||||||
|
style.color = color;
|
||||||
|
}
|
||||||
|
if (opacityChanged) {
|
||||||
|
style.opacity = opacity[0];
|
||||||
|
}
|
||||||
|
if (weightChanged) {
|
||||||
|
style.weight = weight[0];
|
||||||
|
}
|
||||||
|
dbUtils.setStyleToSelection(style);
|
||||||
|
|
||||||
|
if (item instanceof ListFileItem && $selection.size === gpxLayers.size) {
|
||||||
|
if (style.opacity) {
|
||||||
|
$defaultOpacity = style.opacity;
|
||||||
|
}
|
||||||
|
if (style.weight) {
|
||||||
|
$defaultWeight = style.weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Save size="16" class="mr-1" />
|
||||||
|
{$_('menu.metadata.save')}
|
||||||
|
</Button>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
95
website/src/lib/components/gpx-layer/DistanceMarkers.ts
Executable file
@ -0,0 +1,95 @@
|
|||||||
|
|
||||||
|
import { settings } from "$lib/db";
|
||||||
|
import { gpxStatistics } from "$lib/stores";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
|
||||||
|
const { distanceMarkers, distanceUnits } = settings;
|
||||||
|
|
||||||
|
export class DistanceMarkers {
|
||||||
|
map: mapboxgl.Map;
|
||||||
|
updateBinded: () => void = this.update.bind(this);
|
||||||
|
unsubscribes: (() => void)[] = [];
|
||||||
|
|
||||||
|
constructor(map: mapboxgl.Map) {
|
||||||
|
this.map = map;
|
||||||
|
|
||||||
|
this.unsubscribes.push(gpxStatistics.subscribe(this.updateBinded));
|
||||||
|
this.unsubscribes.push(distanceMarkers.subscribe(this.updateBinded));
|
||||||
|
this.unsubscribes.push(distanceUnits.subscribe(this.updateBinded));
|
||||||
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
try {
|
||||||
|
if (get(distanceMarkers)) {
|
||||||
|
let distanceSource = this.map.getSource('distance-markers');
|
||||||
|
if (distanceSource) {
|
||||||
|
distanceSource.setData(this.getDistanceMarkersGeoJSON());
|
||||||
|
} else {
|
||||||
|
this.map.addSource('distance-markers', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: this.getDistanceMarkersGeoJSON()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!this.map.getLayer('distance-markers')) {
|
||||||
|
this.map.addLayer({
|
||||||
|
id: 'distance-markers',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'distance-markers',
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'distance'],
|
||||||
|
'text-size': 14,
|
||||||
|
'text-font': ['Open Sans Bold'],
|
||||||
|
'text-padding': 20,
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': 'black',
|
||||||
|
'text-halo-width': 2,
|
||||||
|
'text-halo-color': 'white',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.map.moveLayer('distance-markers');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.map.getLayer('distance-markers')) {
|
||||||
|
this.map.removeLayer('distance-markers');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
this.unsubscribes.forEach(unsubscribe => unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
|
||||||
|
let statistics = get(gpxStatistics);
|
||||||
|
|
||||||
|
let features = [];
|
||||||
|
let currentTargetDistance = 1;
|
||||||
|
for (let i = 0; i < statistics.local.distance.total.length; i++) {
|
||||||
|
if (statistics.local.distance.total[i] >= currentTargetDistance * (get(distanceUnits) === 'metric' ? 1 : 1.60934)) {
|
||||||
|
let distance = currentTargetDistance.toFixed(0);
|
||||||
|
features.push({
|
||||||
|
type: 'Feature',
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [statistics.local.points[i].getLongitude(), statistics.local.points[i].getLatitude()]
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
distance,
|
||||||
|
}
|
||||||
|
} as GeoJSON.Feature);
|
||||||
|
currentTargetDistance += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
471
website/src/lib/components/gpx-layer/GPXLayer.ts
Executable file
@ -0,0 +1,471 @@
|
|||||||
|
import { currentTool, map, Tool } from "$lib/stores";
|
||||||
|
import { settings, type GPXFileWithStatistics, dbUtils } from "$lib/db";
|
||||||
|
import { get, type Readable } from "svelte/store";
|
||||||
|
import mapboxgl from "mapbox-gl";
|
||||||
|
import { currentPopupWaypoint, deleteWaypoint, waypointPopup } from "./WaypointPopup";
|
||||||
|
import { addSelectItem, selectItem, selection } from "$lib/components/file-list/Selection";
|
||||||
|
import { ListTrackSegmentItem, ListWaypointItem, ListWaypointsItem, ListTrackItem, ListFileItem, ListRootItem } from "$lib/components/file-list/FileList";
|
||||||
|
import type { Waypoint } from "gpx";
|
||||||
|
import { getElevation, resetCursor, setGrabbingCursor, setPointerCursor, setScissorsCursor } from "$lib/utils";
|
||||||
|
import { selectedWaypoint } from "$lib/components/toolbar/tools/Waypoint.svelte";
|
||||||
|
import { MapPin, Square } from "lucide-static";
|
||||||
|
import { getSymbolKey, symbols } from "$lib/assets/symbols";
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
'#ff0000',
|
||||||
|
'#0000ff',
|
||||||
|
'#46e646',
|
||||||
|
'#00ccff',
|
||||||
|
'#ff9900',
|
||||||
|
'#ff00ff',
|
||||||
|
'#ffff32',
|
||||||
|
'#288228',
|
||||||
|
'#9933ff',
|
||||||
|
'#50f0be',
|
||||||
|
'#8c645a'
|
||||||
|
];
|
||||||
|
|
||||||
|
const colorCount: { [key: string]: number } = {};
|
||||||
|
for (let color of colors) {
|
||||||
|
colorCount[color] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the color with the least amount of uses
|
||||||
|
function getColor() {
|
||||||
|
let color = colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b));
|
||||||
|
colorCount[color]++;
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decrementColor(color: string) {
|
||||||
|
if (colorCount.hasOwnProperty(color)) {
|
||||||
|
colorCount[color]--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarkerForSymbol(symbol: string | undefined, layerColor: string) {
|
||||||
|
let symbolSvg = symbol ? symbols[symbol]?.iconSvg : undefined;
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
${Square
|
||||||
|
.replace('width="24"', 'width="12"')
|
||||||
|
.replace('height="24"', 'height="12"')
|
||||||
|
.replace('stroke="currentColor"', 'stroke="SteelBlue"')
|
||||||
|
.replace('stroke-width="2"', 'stroke-width="1.5" x="9.6" y="0.4"')
|
||||||
|
.replace('fill="none"', `fill="${layerColor}"`)}
|
||||||
|
${MapPin
|
||||||
|
.replace('width="24"', '')
|
||||||
|
.replace('height="24"', '')
|
||||||
|
.replace('stroke="currentColor"', '')
|
||||||
|
.replace('path', `path fill="#3fb1ce" stroke="SteelBlue" stroke-width="1"`)
|
||||||
|
.replace('circle', `circle fill="${symbolSvg ? 'none' : 'white'}" stroke="${symbolSvg ? 'none' : 'white'}" stroke-width="2"`)}
|
||||||
|
${symbolSvg?.replace('width="24"', 'width="10"')
|
||||||
|
.replace('height="24"', 'height="10"')
|
||||||
|
.replace('stroke="currentColor"', 'stroke="white"')
|
||||||
|
.replace('stroke-width="2"', 'stroke-width="2.5" x="7" y="5"') ?? ''}
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { directionMarkers, verticalFileView, defaultOpacity, defaultWeight } = settings;
|
||||||
|
|
||||||
|
export class GPXLayer {
|
||||||
|
map: mapboxgl.Map;
|
||||||
|
fileId: string;
|
||||||
|
file: Readable<GPXFileWithStatistics | undefined>;
|
||||||
|
layerColor: string;
|
||||||
|
markers: mapboxgl.Marker[] = [];
|
||||||
|
selected: boolean = false;
|
||||||
|
draggable: boolean;
|
||||||
|
unsubscribe: Function[] = [];
|
||||||
|
|
||||||
|
updateBinded: () => void = this.update.bind(this);
|
||||||
|
layerOnMouseEnterBinded: (e: any) => void = this.layerOnMouseEnter.bind(this);
|
||||||
|
layerOnMouseLeaveBinded: () => void = this.layerOnMouseLeave.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);
|
||||||
|
|
||||||
|
constructor(map: mapboxgl.Map, fileId: string, file: Readable<GPXFileWithStatistics | undefined>) {
|
||||||
|
this.map = map;
|
||||||
|
this.fileId = fileId;
|
||||||
|
this.file = file;
|
||||||
|
this.layerColor = getColor();
|
||||||
|
this.unsubscribe.push(file.subscribe(this.updateBinded));
|
||||||
|
this.unsubscribe.push(selection.subscribe($selection => {
|
||||||
|
let newSelected = $selection.hasAnyChildren(new ListFileItem(this.fileId));
|
||||||
|
if (this.selected || newSelected) {
|
||||||
|
this.selected = newSelected;
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
if (newSelected) {
|
||||||
|
this.moveToFront();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
|
||||||
|
this.unsubscribe.push(currentTool.subscribe(tool => {
|
||||||
|
if (tool === Tool.WAYPOINT && !this.draggable) {
|
||||||
|
this.draggable = true;
|
||||||
|
this.markers.forEach(marker => marker.setDraggable(true));
|
||||||
|
} else if (tool !== Tool.WAYPOINT && this.draggable) {
|
||||||
|
this.draggable = false;
|
||||||
|
this.markers.forEach(marker => marker.setDraggable(false));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
this.draggable = get(currentTool) === Tool.WAYPOINT;
|
||||||
|
|
||||||
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
let file = get(this.file)?.file;
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file._data.style && file._data.style.color && this.layerColor !== `#${file._data.style.color}`) {
|
||||||
|
decrementColor(this.layerColor);
|
||||||
|
this.layerColor = `#${file._data.style.color}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let source = this.map.getSource(this.fileId);
|
||||||
|
if (source) {
|
||||||
|
source.setData(this.getGeoJSON());
|
||||||
|
} else {
|
||||||
|
this.map.addSource(this.fileId, {
|
||||||
|
type: 'geojson',
|
||||||
|
data: this.getGeoJSON()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.map.getLayer(this.fileId)) {
|
||||||
|
this.map.addLayer({
|
||||||
|
id: this.fileId,
|
||||||
|
type: 'line',
|
||||||
|
source: this.fileId,
|
||||||
|
layout: {
|
||||||
|
'line-join': 'round',
|
||||||
|
'line-cap': 'round'
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'line-color': ['get', 'color'],
|
||||||
|
'line-width': ['get', 'weight'],
|
||||||
|
'line-opacity': ['get', 'opacity']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get(directionMarkers)) {
|
||||||
|
if (!this.map.getLayer(this.fileId + '-direction')) {
|
||||||
|
this.map.addLayer({
|
||||||
|
id: this.fileId + '-direction',
|
||||||
|
type: 'symbol',
|
||||||
|
source: this.fileId,
|
||||||
|
layout: {
|
||||||
|
'text-field': '»',
|
||||||
|
'text-offset': [0, -0.1],
|
||||||
|
'text-keep-upright': false,
|
||||||
|
'text-max-angle': 361,
|
||||||
|
'text-allow-overlap': true,
|
||||||
|
'text-font': ['Open Sans Bold'],
|
||||||
|
'symbol-placement': 'line',
|
||||||
|
'symbol-spacing': 20,
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': 'white',
|
||||||
|
'text-opacity': 0.7,
|
||||||
|
'text-halo-width': 0.2,
|
||||||
|
'text-halo-color': 'white'
|
||||||
|
}
|
||||||
|
}, this.map.getLayer('distance-markers') ? 'distance-markers' : undefined);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
|
this.map.removeLayer(this.fileId + '-direction');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let visibleItems: [number, number][] = [];
|
||||||
|
file.forEachSegment((segment, trackIndex, segmentIndex) => {
|
||||||
|
if (!segment._data.hidden) {
|
||||||
|
visibleItems.push([trackIndex, segmentIndex]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.map.setFilter(this.fileId, ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false });
|
||||||
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
|
this.map.setFilter(this.fileId + '-direction', ['any', ...visibleItems.map(([trackIndex, segmentIndex]) => ['all', ['==', 'trackIndex', trackIndex], ['==', 'segmentIndex', segmentIndex]])], { validate: false });
|
||||||
|
}
|
||||||
|
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let markerIndex = 0;
|
||||||
|
|
||||||
|
if (get(selection).hasAnyChildren(new ListFileItem(this.fileId))) {
|
||||||
|
file.wpt.forEach((waypoint) => { // Update markers
|
||||||
|
let symbolKey = getSymbolKey(waypoint.sym);
|
||||||
|
if (markerIndex < this.markers.length) {
|
||||||
|
this.markers[markerIndex].getElement().innerHTML = getMarkerForSymbol(symbolKey, this.layerColor);
|
||||||
|
this.markers[markerIndex].setLngLat(waypoint.getCoordinates());
|
||||||
|
Object.defineProperty(this.markers[markerIndex], '_waypoint', { value: waypoint, writable: true });
|
||||||
|
} else {
|
||||||
|
let element = document.createElement('div');
|
||||||
|
element.classList.add('w-8', 'h-8', 'drop-shadow-xl');
|
||||||
|
element.innerHTML = getMarkerForSymbol(symbolKey, this.layerColor);
|
||||||
|
let marker = new mapboxgl.Marker({
|
||||||
|
draggable: this.draggable,
|
||||||
|
element,
|
||||||
|
anchor: 'bottom'
|
||||||
|
}).setLngLat(waypoint.getCoordinates());
|
||||||
|
Object.defineProperty(marker, '_waypoint', { value: waypoint, writable: true });
|
||||||
|
let dragEndTimestamp = 0;
|
||||||
|
marker.getElement().addEventListener('mouseover', (e) => {
|
||||||
|
if (marker._isDragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.showWaypointPopup(marker._waypoint);
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
marker.getElement().addEventListener('click', (e) => {
|
||||||
|
if (dragEndTimestamp && Date.now() - dragEndTimestamp < 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get(currentTool) === Tool.WAYPOINT && e.shiftKey) {
|
||||||
|
deleteWaypoint(this.fileId, marker._waypoint._data.index);
|
||||||
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get(verticalFileView)) {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), false)) {
|
||||||
|
addSelectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
|
||||||
|
} else {
|
||||||
|
selectItem(new ListWaypointItem(this.fileId, marker._waypoint._data.index));
|
||||||
|
}
|
||||||
|
} else if (get(currentTool) === Tool.WAYPOINT) {
|
||||||
|
selectedWaypoint.set([marker._waypoint, this.fileId]);
|
||||||
|
} else {
|
||||||
|
this.showWaypointPopup(marker._waypoint);
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
marker.on('dragstart', () => {
|
||||||
|
setGrabbingCursor();
|
||||||
|
marker.getElement().style.cursor = 'grabbing';
|
||||||
|
this.hideWaypointPopup();
|
||||||
|
});
|
||||||
|
marker.on('dragend', (e) => {
|
||||||
|
resetCursor();
|
||||||
|
marker.getElement().style.cursor = '';
|
||||||
|
getElevation([marker._waypoint]).then((ele) => {
|
||||||
|
dbUtils.applyToFile(this.fileId, (file) => {
|
||||||
|
let latLng = marker.getLngLat();
|
||||||
|
let wpt = file.wpt[marker._waypoint._data.index];
|
||||||
|
wpt.setCoordinates({
|
||||||
|
lat: latLng.lat,
|
||||||
|
lon: latLng.lng
|
||||||
|
});
|
||||||
|
wpt.ele = ele[0];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
dragEndTimestamp = Date.now()
|
||||||
|
});
|
||||||
|
this.markers.push(marker);
|
||||||
|
}
|
||||||
|
markerIndex++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
while (markerIndex < this.markers.length) { // Remove extra markers
|
||||||
|
this.markers.pop()?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.markers.forEach((marker) => {
|
||||||
|
if (!marker._waypoint._data.hidden) {
|
||||||
|
marker.addTo(this.map);
|
||||||
|
} else {
|
||||||
|
marker.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMap(map: mapboxgl.Map) {
|
||||||
|
this.map = map;
|
||||||
|
this.map.on('style.import.load', this.updateBinded);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
remove() {
|
||||||
|
if (get(map)) {
|
||||||
|
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('mouseleave', this.fileId, this.layerOnMouseLeaveBinded);
|
||||||
|
this.map.off('style.import.load', this.updateBinded);
|
||||||
|
|
||||||
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
|
this.map.removeLayer(this.fileId + '-direction');
|
||||||
|
}
|
||||||
|
if (this.map.getLayer(this.fileId)) {
|
||||||
|
this.map.removeLayer(this.fileId);
|
||||||
|
}
|
||||||
|
if (this.map.getSource(this.fileId)) {
|
||||||
|
this.map.removeSource(this.fileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.markers.forEach((marker) => {
|
||||||
|
marker.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.unsubscribe.forEach((unsubscribe) => unsubscribe());
|
||||||
|
|
||||||
|
decrementColor(this.layerColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveToFront() {
|
||||||
|
if (this.map.getLayer(this.fileId)) {
|
||||||
|
this.map.moveLayer(this.fileId);
|
||||||
|
}
|
||||||
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
|
this.map.moveLayer(this.fileId + '-direction', this.map.getLayer('distance-markers') ? 'distance-markers' : undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layerOnMouseEnter(e: any) {
|
||||||
|
let trackIndex = e.features[0].properties.trackIndex;
|
||||||
|
let segmentIndex = e.features[0].properties.segmentIndex;
|
||||||
|
|
||||||
|
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
|
||||||
|
setScissorsCursor();
|
||||||
|
} else {
|
||||||
|
setPointerCursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layerOnMouseLeave() {
|
||||||
|
resetCursor();
|
||||||
|
}
|
||||||
|
|
||||||
|
layerOnClick(e: any) {
|
||||||
|
if (get(currentTool) === Tool.ROUTING && get(selection).hasAnyChildren(new ListRootItem(), true, ['waypoints'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let trackIndex = e.features[0].properties.trackIndex;
|
||||||
|
let segmentIndex = e.features[0].properties.segmentIndex;
|
||||||
|
|
||||||
|
if (get(currentTool) === Tool.SCISSORS && get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex))) {
|
||||||
|
dbUtils.split(this.fileId, trackIndex, segmentIndex, { lat: e.lngLat.lat, lon: e.lngLat.lng });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = get(this.file)?.file;
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let item = undefined;
|
||||||
|
if (get(verticalFileView) && file.getSegments().length > 1) { // Select inner item
|
||||||
|
item = file.children[trackIndex].children.length > 1 ? new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex) : new ListTrackItem(this.fileId, trackIndex);
|
||||||
|
} else {
|
||||||
|
item = new ListFileItem(this.fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.originalEvent.ctrlKey || e.originalEvent.metaKey) {
|
||||||
|
addSelectItem(item);
|
||||||
|
} else {
|
||||||
|
selectItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layerOnContextMenu(e: any) {
|
||||||
|
if (e.originalEvent.ctrlKey) {
|
||||||
|
this.layerOnClick(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showWaypointPopup(waypoint: Waypoint) {
|
||||||
|
if (get(currentPopupWaypoint) !== null) {
|
||||||
|
this.hideWaypointPopup();
|
||||||
|
}
|
||||||
|
let marker = this.markers[waypoint._data.index];
|
||||||
|
if (marker) {
|
||||||
|
currentPopupWaypoint.set([waypoint, this.fileId]);
|
||||||
|
marker.setPopup(waypointPopup);
|
||||||
|
marker.togglePopup();
|
||||||
|
this.map.on('mousemove', this.maybeHideWaypointPopupBinded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeHideWaypointPopup(e: any) {
|
||||||
|
let waypoint = get(currentPopupWaypoint)?.[0];
|
||||||
|
if (waypoint) {
|
||||||
|
let marker = this.markers[waypoint._data.index];
|
||||||
|
if (marker) {
|
||||||
|
if (this.map.project(marker.getLngLat()).dist(this.map.project(e.lngLat)) > 100) {
|
||||||
|
this.hideWaypointPopup();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.hideWaypointPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideWaypointPopup() {
|
||||||
|
let waypoint = get(currentPopupWaypoint)?.[0];
|
||||||
|
if (waypoint) {
|
||||||
|
let marker = this.markers[waypoint._data.index];
|
||||||
|
marker?.getPopup()?.remove();
|
||||||
|
currentPopupWaypoint.set(null);
|
||||||
|
this.map.off('mousemove', this.maybeHideWaypointPopupBinded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getGeoJSON(): GeoJSON.FeatureCollection {
|
||||||
|
let file = get(this.file)?.file;
|
||||||
|
if (!file) {
|
||||||
|
return {
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = file.toGeoJSON();
|
||||||
|
|
||||||
|
let trackIndex = 0, segmentIndex = 0;
|
||||||
|
for (let feature of data.features) {
|
||||||
|
if (!feature.properties) {
|
||||||
|
feature.properties = {};
|
||||||
|
}
|
||||||
|
if (!feature.properties.color) {
|
||||||
|
feature.properties.color = this.layerColor;
|
||||||
|
}
|
||||||
|
if (!feature.properties.weight) {
|
||||||
|
feature.properties.weight = get(defaultWeight);
|
||||||
|
}
|
||||||
|
if (!feature.properties.opacity) {
|
||||||
|
feature.properties.opacity = get(defaultOpacity);
|
||||||
|
}
|
||||||
|
if (get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)) || get(selection).hasAnyChildren(new ListWaypointsItem(this.fileId), true)) {
|
||||||
|
feature.properties.weight = feature.properties.weight + 2;
|
||||||
|
feature.properties.opacity = Math.min(1, feature.properties.opacity + 0.1);
|
||||||
|
}
|
||||||
|
feature.properties.trackIndex = trackIndex;
|
||||||
|
feature.properties.segmentIndex = segmentIndex;
|
||||||
|
|
||||||
|
segmentIndex++;
|
||||||
|
if (segmentIndex >= file.trk[trackIndex].trkseg.length) {
|
||||||
|
segmentIndex = 0;
|
||||||
|
trackIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
58
website/src/lib/components/gpx-layer/GPXLayers.svelte
Executable file
@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { map, gpxLayers } from '$lib/stores';
|
||||||
|
import { GPXLayer } from './GPXLayer';
|
||||||
|
import WaypointPopup from './WaypointPopup.svelte';
|
||||||
|
import { fileObservers } from '$lib/db';
|
||||||
|
import { DistanceMarkers } from './DistanceMarkers';
|
||||||
|
import { StartEndMarkers } from './StartEndMarkers';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
let distanceMarkers: DistanceMarkers | undefined = undefined;
|
||||||
|
let startEndMarkers: StartEndMarkers | undefined = undefined;
|
||||||
|
|
||||||
|
$: if ($map && $fileObservers) {
|
||||||
|
// remove layers for deleted files
|
||||||
|
gpxLayers.forEach((layer, fileId) => {
|
||||||
|
if (!$fileObservers.has(fileId)) {
|
||||||
|
layer.remove();
|
||||||
|
gpxLayers.delete(fileId);
|
||||||
|
} else if ($map !== layer.map) {
|
||||||
|
layer.updateMap($map);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// add layers for new files
|
||||||
|
$fileObservers.forEach((file, fileId) => {
|
||||||
|
if (!gpxLayers.has(fileId)) {
|
||||||
|
gpxLayers.set(fileId, new GPXLayer($map, fileId, file));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if ($map) {
|
||||||
|
if (distanceMarkers) {
|
||||||
|
distanceMarkers.remove();
|
||||||
|
}
|
||||||
|
if (startEndMarkers) {
|
||||||
|
startEndMarkers.remove();
|
||||||
|
}
|
||||||
|
distanceMarkers = new DistanceMarkers($map);
|
||||||
|
startEndMarkers = new StartEndMarkers($map);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
gpxLayers.forEach((layer) => layer.remove());
|
||||||
|
gpxLayers.clear();
|
||||||
|
|
||||||
|
if (distanceMarkers) {
|
||||||
|
distanceMarkers.remove();
|
||||||
|
distanceMarkers = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startEndMarkers) {
|
||||||
|
startEndMarkers.remove();
|
||||||
|
startEndMarkers = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<WaypointPopup />
|