mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-04 12:58:23 +00:00
Compare commits
38
Commits
70bb125076
...
wasm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1904bc12f | ||
|
|
9afba53e5d | ||
|
|
fd76e0fa46 | ||
|
|
438a87f06f | ||
|
|
a994e16f51 | ||
|
|
639e7491eb | ||
|
|
891a08a3cc | ||
|
|
0c07164dec | ||
|
|
f133d55889 | ||
|
|
fbb636cb06 | ||
|
|
bddceb28aa | ||
|
|
bff84fa24c | ||
|
|
64914f9aa0 | ||
|
|
2abf46c747 | ||
|
|
888f41e979 | ||
|
|
17533586ef | ||
|
|
c6ad088947 | ||
|
|
87d6d5f30e | ||
|
|
dbe894b3a3 | ||
|
|
a921d4b9cd | ||
|
|
b7db73cf77 | ||
|
|
5868595a6c | ||
|
|
eb8e09b835 | ||
|
|
41dc853f65 | ||
|
|
27c0a57cc1 | ||
|
|
497ac120ec | ||
|
|
a486016d96 | ||
|
|
cc5f5653ef | ||
|
|
20222c450d | ||
|
|
69049e1345 | ||
|
|
ed6264444f | ||
|
|
86f4fc3394 | ||
|
|
565589fc94 | ||
|
|
4ef92e5abd | ||
|
|
7f8751fa38 | ||
|
|
befcba00b4 | ||
|
|
26e4b637a2 | ||
|
|
75e3bc6f93 |
@@ -70,8 +70,9 @@ This project has been made possible thanks to the following open source projects
|
|||||||
- [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree
|
- [SortableJS](https://github.com/SortableJS/Sortable) — creating a sortable file tree
|
||||||
- Mapping:
|
- Mapping:
|
||||||
- [MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js) — beautiful and fast interactive map rendering
|
- [MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js) — beautiful and fast interactive map rendering
|
||||||
- [GraphHopper](https://github.com/graphhopper/graphhopper) — routing engine
|
- [GraphHopper](https://github.com/graphhopper/graphhopper) — powerful routing engine
|
||||||
- [OpenStreetMap](https://www.openstreetmap.org) — map data used by most of the map layers, and by the routing engine
|
- [OpenStreetMap](https://www.openstreetmap.org) — open map data used by most of the map layers, and by the routing engine
|
||||||
|
- [Mapterhorn](https://github.com/mapterhorn/mapterhorn) — high-quality open terrain data used by some map layers (including for 3D), and by the routing engine
|
||||||
- Search:
|
- Search:
|
||||||
- [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation
|
- [DocSearch](https://github.com/algolia/docsearch) — search engine for the documentation
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
target/
|
||||||
|
pkg/
|
||||||
Generated
+398
@@ -0,0 +1,398 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console_error_panic_hook"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gpx-engine"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"quick-xml",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gpx-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"gpx-engine",
|
||||||
|
"js-sys",
|
||||||
|
"uuid",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.65"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.189"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.42.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.118"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.26.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.129"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.129"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.129"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.6",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.129"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.62.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement",
|
||||||
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
"windows-strings",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[workspace]
|
||||||
|
members = ["engine", "wasm"]
|
||||||
|
resolver = "3"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
```
|
||||||
|
wasm-pack build wasm --target bundler --out-dir ../pkg
|
||||||
|
```
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "gpx-engine"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = "0.4.45"
|
||||||
|
quick-xml = "0.42.0"
|
||||||
|
uuid = { version = "1.26.1", features = ["v4"] }
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
<?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>
|
||||||
|
<desc>description</desc>
|
||||||
|
<author>
|
||||||
|
<name>gpx.studio</name>
|
||||||
|
<link href="https://gpx.studio">
|
||||||
|
<text>link text</text>
|
||||||
|
</link>
|
||||||
|
</author>
|
||||||
|
</metadata>
|
||||||
|
<trk>
|
||||||
|
<name>track name</name>
|
||||||
|
<cmt>track comment</cmt>
|
||||||
|
<desc>track description</desc>
|
||||||
|
<src>track source</src>
|
||||||
|
<link href="https://gpx.studio">
|
||||||
|
<text>track link text</text>
|
||||||
|
</link>
|
||||||
|
<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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,665 @@
|
|||||||
|
<?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:gpxpx="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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
<gpx_style:color>2d3ee9</gpx_style:color>
|
||||||
|
<gpx_style:opacity>0.5</gpx_style:opacity>
|
||||||
|
<gpx_style:width>6</gpx_style:width>
|
||||||
|
</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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
<?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</name>
|
||||||
|
<cmt>waypoint comment</cmt>
|
||||||
|
<desc>waypoint description</desc>
|
||||||
|
<link href="https://gpx.studio">
|
||||||
|
<text>waypoint link text</text>
|
||||||
|
</link>
|
||||||
|
<sym>Bike Trail</sym>
|
||||||
|
<type>Bike Trail</type>
|
||||||
|
</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>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
mod simplify;
|
||||||
|
mod smooth;
|
||||||
|
|
||||||
|
pub use simplify::*;
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
use crate::{
|
||||||
|
core::gpx::{TrackSegment, TrackSegmentIndex},
|
||||||
|
core::utils::crossarc,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn ramer_douglas_peucker<F>(
|
||||||
|
trkseg: &TrackSegment,
|
||||||
|
mapping: &F,
|
||||||
|
epsilon: f64,
|
||||||
|
) -> Vec<TrackSegmentIndex>
|
||||||
|
where
|
||||||
|
F: Fn(TrackSegmentIndex) -> (f64, f64),
|
||||||
|
{
|
||||||
|
match trkseg.len() {
|
||||||
|
0 => vec![],
|
||||||
|
1 => vec![trkseg.first_index().unwrap()],
|
||||||
|
2 => vec![trkseg.first_index().unwrap(), trkseg.last_index().unwrap()],
|
||||||
|
_ => {
|
||||||
|
let first = trkseg.first_index().unwrap();
|
||||||
|
let last = trkseg.last_index().unwrap();
|
||||||
|
|
||||||
|
let mut indices = vec![first];
|
||||||
|
ramer_douglas_peucker_helper(trkseg, first, last, mapping, epsilon, &mut indices);
|
||||||
|
indices.push(last);
|
||||||
|
indices
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ramer_douglas_peucker_helper<F>(
|
||||||
|
trkseg: &TrackSegment,
|
||||||
|
start: TrackSegmentIndex,
|
||||||
|
end: TrackSegmentIndex,
|
||||||
|
mapping: &F,
|
||||||
|
epsilon: f64,
|
||||||
|
indices: &mut Vec<TrackSegmentIndex>,
|
||||||
|
) where
|
||||||
|
F: Fn(TrackSegmentIndex) -> (f64, f64),
|
||||||
|
{
|
||||||
|
let mut max_idx = None;
|
||||||
|
let mut max_dist = 0.0;
|
||||||
|
|
||||||
|
let start_pt = mapping(start);
|
||||||
|
let end_pt = mapping(end);
|
||||||
|
|
||||||
|
let mut cur = trkseg.next_index(Some(start));
|
||||||
|
while let Some(idx) = cur {
|
||||||
|
if idx == end {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pt = mapping(idx);
|
||||||
|
let dist = crossarc(start_pt.0, start_pt.1, end_pt.0, end_pt.1, pt.0, pt.1);
|
||||||
|
if dist > max_dist {
|
||||||
|
max_idx = Some(idx);
|
||||||
|
max_dist = dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
cur = trkseg.next_index(cur);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(idx) = max_idx.filter(|_| max_dist > epsilon) {
|
||||||
|
ramer_douglas_peucker_helper(trkseg, start, idx, mapping, epsilon, indices);
|
||||||
|
indices.push(idx);
|
||||||
|
ramer_douglas_peucker_helper(trkseg, idx, end, mapping, epsilon, indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{Trackpoint, TrackpointChunk};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn segment(eles: &[f64]) -> TrackSegment {
|
||||||
|
let mut trkseg = TrackSegment::default();
|
||||||
|
// several small chunks, to also exercise the chunk boundaries
|
||||||
|
for part in eles.chunks(3) {
|
||||||
|
let mut chunk = TrackpointChunk::default();
|
||||||
|
for ele in part {
|
||||||
|
chunk.trkpt.push(Trackpoint {
|
||||||
|
ele: *ele,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
trkseg.push(chunk);
|
||||||
|
}
|
||||||
|
trkseg
|
||||||
|
}
|
||||||
|
|
||||||
|
fn simplify(eles: &[f64], epsilon: f64) -> Vec<usize> {
|
||||||
|
let trkseg = segment(eles);
|
||||||
|
ramer_douglas_peucker(&trkseg, &|idx| (idx.flat as f64, trkseg[idx].ele), epsilon)
|
||||||
|
.iter()
|
||||||
|
.map(|idx| idx.flat)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_and_single_point() {
|
||||||
|
assert!(simplify(&[], 1.0).is_empty());
|
||||||
|
assert_eq!(simplify(&[5.0], 1.0), vec![0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_collinear_points_are_removed() {
|
||||||
|
let eles: Vec<f64> = (0..20).map(|i| i as f64 * 2.0).collect();
|
||||||
|
assert_eq!(simplify(&eles, 0.1), vec![0, 19]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_peak_is_kept_when_above_epsilon() {
|
||||||
|
let eles = [0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0];
|
||||||
|
assert_eq!(simplify(&eles, 5.0), vec![0, 3, 6]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_peak_is_dropped_when_below_epsilon() {
|
||||||
|
let eles = [0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0];
|
||||||
|
assert_eq!(simplify(&eles, 1.0), vec![0, 6]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_indices_are_sorted_and_keep_extremities() {
|
||||||
|
let eles = [0.0, 5.0, -3.0, 8.0, 1.0, 9.0, -4.0, 2.0, 7.0, 0.0];
|
||||||
|
let kept = simplify(&eles, 0.5);
|
||||||
|
assert_eq!(kept.first(), Some(&0));
|
||||||
|
assert_eq!(kept.last(), Some(&9));
|
||||||
|
assert!(kept.windows(2).all(|w| w[0] < w[1]));
|
||||||
|
// a larger epsilon never keeps more points
|
||||||
|
assert!(simplify(&eles, 5.0).len() <= kept.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
#[macro_export]
|
||||||
|
macro_rules! for_each_window {
|
||||||
|
(
|
||||||
|
$trkseg:expr,
|
||||||
|
$left:expr,
|
||||||
|
$right:expr,
|
||||||
|
$window:expr,
|
||||||
|
|$a:ident, $b:ident| $distance:expr,
|
||||||
|
|$i:ident, $l:ident, $r:ident| $body:block,
|
||||||
|
) => {{
|
||||||
|
let mut window_start = $left.unwrap_or_default();
|
||||||
|
let mut window_center = $left;
|
||||||
|
let mut window_end = $left.unwrap_or_default();
|
||||||
|
while let Some(idx) = window_center {
|
||||||
|
// advance left if needed
|
||||||
|
while window_start != idx && {
|
||||||
|
let $a = window_start;
|
||||||
|
let $b = idx;
|
||||||
|
$distance
|
||||||
|
} > $window {
|
||||||
|
if let Some(next) = $trkseg.next_index(Some(window_start)) {
|
||||||
|
if next == idx {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
window_start = next;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// advance right if needed
|
||||||
|
window_end = window_end.max(idx);
|
||||||
|
while let Some(next) = $trkseg.next_index(Some(window_end)) {
|
||||||
|
if window_end != idx && {
|
||||||
|
let $a = idx;
|
||||||
|
let $b = next;
|
||||||
|
$distance
|
||||||
|
} > $window {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
window_end = next;
|
||||||
|
}
|
||||||
|
// apply body
|
||||||
|
let $i = idx;
|
||||||
|
let $l = window_start;
|
||||||
|
let $r = window_end;
|
||||||
|
$body
|
||||||
|
// go next
|
||||||
|
if window_center == $right {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
window_center = $trkseg.next_index(window_center);
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{TrackSegment, Trackpoint, TrackpointChunk};
|
||||||
|
|
||||||
|
fn segment(eles: &[f64]) -> TrackSegment {
|
||||||
|
let mut trkseg = TrackSegment::default();
|
||||||
|
// several small chunks, to also exercise the chunk boundaries
|
||||||
|
for part in eles.chunks(3) {
|
||||||
|
let mut chunk = TrackpointChunk::default();
|
||||||
|
for ele in part {
|
||||||
|
chunk.trkpt.push(Trackpoint {
|
||||||
|
ele: *ele,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
trkseg.push(chunk);
|
||||||
|
}
|
||||||
|
trkseg
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns (center, left, right) flat indices for each visited center.
|
||||||
|
fn windows(len: usize, window: f64) -> Vec<(usize, usize, usize)> {
|
||||||
|
let trkseg = segment(&vec![0.0; len]);
|
||||||
|
let mut visited = vec![];
|
||||||
|
for_each_window!(
|
||||||
|
trkseg,
|
||||||
|
trkseg.first_index(),
|
||||||
|
trkseg.last_index(),
|
||||||
|
window,
|
||||||
|
|a, b| (b.flat - a.flat) as f64,
|
||||||
|
|i, l, r| {
|
||||||
|
visited.push((i.flat, l.flat, r.flat));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
visited
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_window_bounds() {
|
||||||
|
let visited = windows(10, 2.0);
|
||||||
|
assert_eq!(visited.len(), 10);
|
||||||
|
for (i, l, r) in visited {
|
||||||
|
assert_eq!(l, i.saturating_sub(2), "left of {i}");
|
||||||
|
assert_eq!(r, (i + 2).min(9), "right of {i}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_window_always_includes_neighbours() {
|
||||||
|
for (i, l, r) in windows(6, 0.0) {
|
||||||
|
assert_eq!((l, r), (i.saturating_sub(1), (i + 1).min(5)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_window_larger_than_segment() {
|
||||||
|
for (_, l, r) in windows(5, 100.0) {
|
||||||
|
assert_eq!((l, r), (0, 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_window_sub_range() {
|
||||||
|
let trkseg = segment(&[0.0; 10]);
|
||||||
|
let mut centers = vec![];
|
||||||
|
let mut first = trkseg.first_index();
|
||||||
|
for _ in 0..3 {
|
||||||
|
first = trkseg.next_index(first);
|
||||||
|
}
|
||||||
|
let mut last = first;
|
||||||
|
for _ in 0..2 {
|
||||||
|
last = trkseg.next_index(last);
|
||||||
|
}
|
||||||
|
for_each_window!(
|
||||||
|
trkseg,
|
||||||
|
first,
|
||||||
|
last,
|
||||||
|
1.0,
|
||||||
|
|a, b| (b.flat - a.flat) as f64,
|
||||||
|
|i, _l, _r| {
|
||||||
|
centers.push(i.flat);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(centers, vec![3, 4, 5]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{Trackpoint, Waypoint};
|
||||||
|
|
||||||
|
const MAX_TRKPT_CHUNK_SIZE: usize = 4096;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct TrackpointChunkId(Uuid);
|
||||||
|
|
||||||
|
impl Default for TrackpointChunkId {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Uuid::new_v4())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct TrackpointChunk {
|
||||||
|
pub id: TrackpointChunkId,
|
||||||
|
pub trkpt: Vec<Trackpoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for TrackpointChunk {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackpointChunk {
|
||||||
|
pub fn is_full(&self) -> bool {
|
||||||
|
self.trkpt.len() == MAX_TRKPT_CHUNK_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct WaypointChunkId(Uuid);
|
||||||
|
|
||||||
|
impl Default for WaypointChunkId {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Uuid::new_v4())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_WPT_CHUNK_SIZE: usize = 128;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct WaypointChunk {
|
||||||
|
pub id: WaypointChunkId,
|
||||||
|
pub wpt: Vec<Waypoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for WaypointChunk {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WaypointChunk {
|
||||||
|
pub fn is_full(&self) -> bool {
|
||||||
|
self.wpt.len() == MAX_WPT_CHUNK_SIZE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trackpoint_chunk_full() {
|
||||||
|
let mut chunk = TrackpointChunk::default();
|
||||||
|
assert!(!chunk.is_full());
|
||||||
|
for _ in 0..MAX_TRKPT_CHUNK_SIZE - 1 {
|
||||||
|
chunk.trkpt.push(Trackpoint::default());
|
||||||
|
}
|
||||||
|
assert!(!chunk.is_full());
|
||||||
|
chunk.trkpt.push(Trackpoint::default());
|
||||||
|
assert!(chunk.is_full());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_waypoint_chunk_full() {
|
||||||
|
let mut chunk = WaypointChunk::default();
|
||||||
|
assert!(!chunk.is_full());
|
||||||
|
for _ in 0..MAX_WPT_CHUNK_SIZE {
|
||||||
|
chunk.wpt.push(Waypoint::default());
|
||||||
|
}
|
||||||
|
assert!(chunk.is_full());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_chunk_equality_is_by_id() {
|
||||||
|
let a = TrackpointChunk::default();
|
||||||
|
let b = TrackpointChunk::default();
|
||||||
|
assert_ne!(a, b);
|
||||||
|
let c = TrackpointChunk {
|
||||||
|
id: TrackpointChunkId(a.id.0),
|
||||||
|
trkpt: vec![Trackpoint::default()],
|
||||||
|
};
|
||||||
|
assert_eq!(a, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Link {
|
||||||
|
pub href: String,
|
||||||
|
pub text: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
|
pub struct LngLat {
|
||||||
|
pub lng: f64,
|
||||||
|
pub lat: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LngLatBounds {
|
||||||
|
pub sw: LngLat,
|
||||||
|
pub ne: LngLat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LngLatBounds {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
sw: LngLat {
|
||||||
|
lng: 180.0,
|
||||||
|
lat: 90.0,
|
||||||
|
},
|
||||||
|
ne: LngLat {
|
||||||
|
lng: -180.0,
|
||||||
|
lat: -90.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LngLatBounds {
|
||||||
|
pub fn extend(&mut self, coordinates: LngLat) {
|
||||||
|
self.sw.lng = self.sw.lng.min(coordinates.lng);
|
||||||
|
self.sw.lat = self.sw.lat.min(coordinates.lat);
|
||||||
|
self.ne.lng = self.ne.lng.max(coordinates.lng);
|
||||||
|
self.ne.lat = self.ne.lat.max(coordinates.lat);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge(&mut self, other: &LngLatBounds) {
|
||||||
|
self.sw.lng = self.sw.lng.min(other.sw.lng);
|
||||||
|
self.sw.lat = self.sw.lat.min(other.sw.lat);
|
||||||
|
self.ne.lng = self.ne.lng.max(other.ne.lng);
|
||||||
|
self.ne.lat = self.ne.lat.max(other.ne.lat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bounds_extend() {
|
||||||
|
let mut bounds = LngLatBounds::default();
|
||||||
|
bounds.extend(LngLat {
|
||||||
|
lng: 4.0,
|
||||||
|
lat: 50.0,
|
||||||
|
});
|
||||||
|
assert_eq!((bounds.sw.lng, bounds.sw.lat), (4.0, 50.0));
|
||||||
|
assert_eq!((bounds.ne.lng, bounds.ne.lat), (4.0, 50.0));
|
||||||
|
|
||||||
|
bounds.extend(LngLat {
|
||||||
|
lng: 6.0,
|
||||||
|
lat: 45.0,
|
||||||
|
});
|
||||||
|
bounds.extend(LngLat {
|
||||||
|
lng: 5.0,
|
||||||
|
lat: 48.0,
|
||||||
|
});
|
||||||
|
assert_eq!((bounds.sw.lng, bounds.sw.lat), (4.0, 45.0));
|
||||||
|
assert_eq!((bounds.ne.lng, bounds.ne.lat), (6.0, 50.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bounds_merge() {
|
||||||
|
let mut a = LngLatBounds::default();
|
||||||
|
a.extend(LngLat { lng: 0.0, lat: 0.0 });
|
||||||
|
a.extend(LngLat { lng: 1.0, lat: 1.0 });
|
||||||
|
let mut b = LngLatBounds::default();
|
||||||
|
b.extend(LngLat {
|
||||||
|
lng: -2.0,
|
||||||
|
lat: 0.5,
|
||||||
|
});
|
||||||
|
b.extend(LngLat { lng: 0.5, lat: 3.0 });
|
||||||
|
|
||||||
|
a.merge(&b);
|
||||||
|
assert_eq!((a.sw.lng, a.sw.lat), (-2.0, 0.0));
|
||||||
|
assert_eq!((a.ne.lng, a.ne.lat), (1.0, 3.0));
|
||||||
|
|
||||||
|
// merging an empty bounds changes nothing
|
||||||
|
a.merge(&LngLatBounds::default());
|
||||||
|
assert_eq!((a.sw.lng, a.sw.lat), (-2.0, 0.0));
|
||||||
|
assert_eq!((a.ne.lng, a.ne.lat), (1.0, 3.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{Link, Track, WaypointChunk};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct FileId(pub Uuid);
|
||||||
|
|
||||||
|
impl Default for FileId {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Uuid::new_v4())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct FileWaypointsRevisionId(pub Uuid);
|
||||||
|
|
||||||
|
impl Default for FileWaypointsRevisionId {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Uuid::new_v4())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
|
pub struct File {
|
||||||
|
pub id: FileId,
|
||||||
|
pub info: FileInfo,
|
||||||
|
pub trk: Vec<Track>,
|
||||||
|
pub wpt: Vec<Rc<WaypointChunk>>,
|
||||||
|
pub wpt_rev_id: FileWaypointsRevisionId,
|
||||||
|
// TODO routes
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
|
pub struct FileInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub desc: Option<String>,
|
||||||
|
pub author: Option<Author>,
|
||||||
|
pub link: Option<Link>,
|
||||||
|
pub time: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
|
pub struct Author {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub link: Option<Link>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
mod chunk;
|
||||||
|
mod common;
|
||||||
|
mod file;
|
||||||
|
mod segment;
|
||||||
|
mod track;
|
||||||
|
mod trackpoint;
|
||||||
|
mod waypoint;
|
||||||
|
|
||||||
|
pub use chunk::*;
|
||||||
|
pub use common::*;
|
||||||
|
pub use file::*;
|
||||||
|
pub use segment::*;
|
||||||
|
pub use track::*;
|
||||||
|
pub use trackpoint::*;
|
||||||
|
pub use waypoint::*;
|
||||||
@@ -0,0 +1,454 @@
|
|||||||
|
use std::{ops::Index, rc::Rc};
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{Trackpoint, TrackpointChunk};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct TrackSegmentId(pub Uuid);
|
||||||
|
|
||||||
|
impl Default for TrackSegmentId {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Uuid::new_v4())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct TrackSegmentRevisionId(pub Uuid);
|
||||||
|
|
||||||
|
impl Default for TrackSegmentRevisionId {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Uuid::new_v4())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
|
pub struct TrackSegment {
|
||||||
|
pub id: TrackSegmentId,
|
||||||
|
pub rev_id: TrackSegmentRevisionId,
|
||||||
|
chunks: Vec<Rc<TrackpointChunk>>,
|
||||||
|
cumul_length: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackSegment {
|
||||||
|
pub fn push(&mut self, chunk: TrackpointChunk) {
|
||||||
|
self.push_shared(Rc::new(chunk));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_shared(&mut self, chunk: Rc<TrackpointChunk>) {
|
||||||
|
if chunk.trkpt.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.cumul_length
|
||||||
|
.push(self.cumul_length.last().copied().unwrap_or_default() + chunk.trkpt.len());
|
||||||
|
self.chunks.push(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces the points in `start..end` by `points`. Panics if the range is out of bounds.
|
||||||
|
///
|
||||||
|
/// Chunks that are not concerned are kept as they are (shared), only the chunks around the
|
||||||
|
/// range are copied and refilled.
|
||||||
|
pub fn splice(&mut self, start: usize, end: usize, points: Vec<Trackpoint>) {
|
||||||
|
assert!(
|
||||||
|
start <= end && end <= self.len(),
|
||||||
|
"splice range out of bounds"
|
||||||
|
);
|
||||||
|
let old = std::mem::take(&mut self.chunks);
|
||||||
|
self.cumul_length.clear();
|
||||||
|
|
||||||
|
let mut pending = TrackpointChunk::default();
|
||||||
|
let mut inserted = false;
|
||||||
|
let mut offset = 0;
|
||||||
|
for chunk in old {
|
||||||
|
let (lo, hi) = (offset, offset + chunk.trkpt.len());
|
||||||
|
offset = hi;
|
||||||
|
// a chunk ending at `start` is extended unless it is full, to avoid tiny chunks
|
||||||
|
if hi < start || (hi == start && chunk.is_full()) {
|
||||||
|
self.push_shared(chunk);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !inserted {
|
||||||
|
self.fill(&mut pending, chunk.trkpt[..start - lo].iter().cloned());
|
||||||
|
self.fill(&mut pending, points.iter().cloned());
|
||||||
|
inserted = true;
|
||||||
|
}
|
||||||
|
if hi <= end {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if lo >= end {
|
||||||
|
self.flush(&mut pending);
|
||||||
|
self.push_shared(chunk);
|
||||||
|
} else {
|
||||||
|
self.fill(&mut pending, chunk.trkpt[end - lo..].iter().cloned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !inserted {
|
||||||
|
self.fill(&mut pending, points);
|
||||||
|
}
|
||||||
|
self.flush(&mut pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill(
|
||||||
|
&mut self,
|
||||||
|
pending: &mut TrackpointChunk,
|
||||||
|
points: impl IntoIterator<Item = Trackpoint>,
|
||||||
|
) {
|
||||||
|
for trkpt in points {
|
||||||
|
pending.trkpt.push(trkpt);
|
||||||
|
if pending.is_full() {
|
||||||
|
self.flush(pending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self, pending: &mut TrackpointChunk) {
|
||||||
|
self.push(std::mem::take(pending));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.cumul_length.last().copied().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.chunks.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> TrackSegmentIterator<'_> {
|
||||||
|
TrackSegmentIterator::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn first_index(&self) -> Option<TrackSegmentIndex> {
|
||||||
|
self.next_index(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last_index(&self) -> Option<TrackSegmentIndex> {
|
||||||
|
self.prev_index(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_index(&self, cur: Option<TrackSegmentIndex>) -> Option<TrackSegmentIndex> {
|
||||||
|
let mut next = cur.map_or_default(|idx| TrackSegmentIndex {
|
||||||
|
chunk: idx.chunk,
|
||||||
|
pos: idx.pos + 1,
|
||||||
|
flat: idx.flat + 1,
|
||||||
|
});
|
||||||
|
loop {
|
||||||
|
if next.chunk >= self.chunks.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if next.pos == self.chunks[next.chunk].trkpt.len() {
|
||||||
|
next.chunk += 1;
|
||||||
|
next.pos = 0;
|
||||||
|
} else {
|
||||||
|
return Some(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev_index(&self, cur: Option<TrackSegmentIndex>) -> Option<TrackSegmentIndex> {
|
||||||
|
let mut prev = cur.unwrap_or(TrackSegmentIndex {
|
||||||
|
chunk: self.chunks.len(),
|
||||||
|
pos: 0,
|
||||||
|
flat: self.cumul_length.last().copied().unwrap_or_default(),
|
||||||
|
});
|
||||||
|
if prev.pos == 0 {
|
||||||
|
while prev.chunk > 0 {
|
||||||
|
prev.chunk -= 1;
|
||||||
|
if !self.chunks[prev.chunk].trkpt.is_empty() {
|
||||||
|
prev.pos = self.chunks[prev.chunk].trkpt.len() - 1;
|
||||||
|
prev.flat -= 1;
|
||||||
|
return Some(prev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
prev.pos -= 1;
|
||||||
|
prev.flat -= 1;
|
||||||
|
Some(prev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn locate(&self, idx: usize) -> Option<TrackSegmentIndex> {
|
||||||
|
let chunk = self.cumul_length.partition_point(|l| idx >= *l);
|
||||||
|
if chunk >= self.chunks.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let pos = if chunk > 0 {
|
||||||
|
idx - self.cumul_length[chunk - 1]
|
||||||
|
} else {
|
||||||
|
idx
|
||||||
|
};
|
||||||
|
if pos >= self.chunks[chunk].trkpt.len() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(TrackSegmentIndex {
|
||||||
|
chunk,
|
||||||
|
pos,
|
||||||
|
flat: idx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<TrackSegmentIndex> for TrackSegment {
|
||||||
|
type Output = Trackpoint;
|
||||||
|
|
||||||
|
fn index(&self, idx: TrackSegmentIndex) -> &Self::Output {
|
||||||
|
&self.chunks[idx.chunk].trkpt[idx.pos]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<usize> for TrackSegment {
|
||||||
|
type Output = Trackpoint;
|
||||||
|
|
||||||
|
fn index(&self, idx: usize) -> &Self::Output {
|
||||||
|
&self[self.locate(idx).unwrap()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, PartialOrd, Ord)]
|
||||||
|
pub struct TrackSegmentIndex {
|
||||||
|
pub chunk: usize,
|
||||||
|
pub pos: usize,
|
||||||
|
pub flat: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TrackSegmentIterator<'a> {
|
||||||
|
trkseg: &'a TrackSegment,
|
||||||
|
idx: Option<TrackSegmentIndex>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TrackSegmentIterator<'a> {
|
||||||
|
pub fn new(trkseg: &'a TrackSegment) -> Self {
|
||||||
|
Self {
|
||||||
|
trkseg,
|
||||||
|
idx: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for TrackSegmentIterator<'a> {
|
||||||
|
type Item = &'a Trackpoint;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.idx = self.trkseg.next_index(self.idx);
|
||||||
|
self.idx.map(|idx| &self.trkseg[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||||
|
let idx = self.idx.map_or_default(|idx| idx.flat) + n;
|
||||||
|
self.idx = self.trkseg.locate(idx);
|
||||||
|
self.idx.map(|idx| &self.trkseg[idx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn create_track_segment(nb_chunks: usize) -> TrackSegment {
|
||||||
|
let mut trkseg = TrackSegment::default();
|
||||||
|
let mut count = 0;
|
||||||
|
for n in 1..=nb_chunks {
|
||||||
|
let mut chunk = TrackpointChunk::default();
|
||||||
|
for _ in 0..n {
|
||||||
|
chunk.trkpt.push(Trackpoint {
|
||||||
|
ele: count as f64,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
trkseg.push(chunk);
|
||||||
|
}
|
||||||
|
trkseg
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eles(trkseg: &TrackSegment) -> Vec<f64> {
|
||||||
|
trkseg.iter().map(|p| p.ele).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn points(eles: &[f64]) -> Vec<Trackpoint> {
|
||||||
|
eles.iter()
|
||||||
|
.map(|&ele| Trackpoint {
|
||||||
|
ele,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_splice_replace_insert_delete_append() {
|
||||||
|
// chunks of 1..=5 points: [0] [1 2] [3 4 5] [6 7 8 9] [10..=14]
|
||||||
|
let mut trkseg = create_track_segment(5);
|
||||||
|
trkseg.splice(4, 8, points(&[-1.0, -2.0]));
|
||||||
|
let mut expected: Vec<f64> = (0..4).map(f64::from).collect();
|
||||||
|
expected.extend([-1.0, -2.0]);
|
||||||
|
expected.extend((8..15).map(f64::from));
|
||||||
|
assert_eq!(eles(&trkseg), expected);
|
||||||
|
assert_eq!(trkseg.len(), expected.len());
|
||||||
|
for i in 0..trkseg.len() {
|
||||||
|
assert_eq!(trkseg[i].ele, expected[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
trkseg.splice(2, 2, points(&[100.0]));
|
||||||
|
expected.insert(2, 100.0);
|
||||||
|
assert_eq!(eles(&trkseg), expected);
|
||||||
|
|
||||||
|
trkseg.splice(0, 3, vec![]);
|
||||||
|
expected.drain(..3);
|
||||||
|
assert_eq!(eles(&trkseg), expected);
|
||||||
|
|
||||||
|
let len = trkseg.len();
|
||||||
|
trkseg.splice(len, len, points(&[7.0, 8.0]));
|
||||||
|
expected.extend([7.0, 8.0]);
|
||||||
|
assert_eq!(eles(&trkseg), expected);
|
||||||
|
|
||||||
|
let len = trkseg.len();
|
||||||
|
trkseg.splice(0, len, vec![]);
|
||||||
|
assert_eq!(trkseg.len(), 0);
|
||||||
|
assert!(trkseg.first_index().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_splice_keeps_untouched_chunks_shared() {
|
||||||
|
let mut trkseg = create_track_segment(5);
|
||||||
|
let before = trkseg.chunks.clone();
|
||||||
|
// inside the third chunk only
|
||||||
|
trkseg.splice(4, 5, points(&[-1.0]));
|
||||||
|
assert!(Rc::ptr_eq(&trkseg.chunks[0], &before[0]));
|
||||||
|
assert!(Rc::ptr_eq(&trkseg.chunks[1], &before[1]));
|
||||||
|
assert!(Rc::ptr_eq(trkseg.chunks.last().unwrap(), &before[4]));
|
||||||
|
assert!(Rc::ptr_eq(
|
||||||
|
&trkseg.chunks[trkseg.chunks.len() - 2],
|
||||||
|
&before[3]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_splice_append_extends_last_chunk_and_respects_max_size() {
|
||||||
|
let mut trkseg = TrackSegment::default();
|
||||||
|
for i in 0..10_000 {
|
||||||
|
let len = trkseg.len();
|
||||||
|
trkseg.splice(len, len, points(&[i as f64]));
|
||||||
|
}
|
||||||
|
assert_eq!(trkseg.len(), 10_000);
|
||||||
|
assert!(trkseg.chunks.len() <= 3);
|
||||||
|
assert!(trkseg.chunks.iter().all(|c| c.trkpt.len() <= 4096));
|
||||||
|
assert_eq!(trkseg[9_999].ele, 9_999.0);
|
||||||
|
assert_eq!(*trkseg.cumul_length.last().unwrap(), 10_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_len() {
|
||||||
|
let nb_chunks = 10;
|
||||||
|
let trkseg = create_track_segment(nb_chunks);
|
||||||
|
assert_eq!(trkseg.len(), nb_chunks * (nb_chunks + 1) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_locate() {
|
||||||
|
let nb_chunks = 10;
|
||||||
|
let trkseg = create_track_segment(nb_chunks);
|
||||||
|
for i in 0..trkseg.len() {
|
||||||
|
let idx = trkseg.locate(i);
|
||||||
|
assert!(idx.is_some());
|
||||||
|
assert_eq!(trkseg[idx.unwrap()].ele, i as f64);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(trkseg.locate(trkseg.len()).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_index() {
|
||||||
|
let nb_chunks = 10;
|
||||||
|
let trkseg = create_track_segment(nb_chunks);
|
||||||
|
for i in 0..trkseg.len() {
|
||||||
|
assert_eq!(trkseg[i].ele, i as f64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_index_out_of_bounds_1() {
|
||||||
|
let nb_chunks = 10;
|
||||||
|
let trkseg = create_track_segment(nb_chunks);
|
||||||
|
let _ = trkseg[trkseg.len()];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_index_out_of_bounds_2() {
|
||||||
|
let nb_chunks = 10;
|
||||||
|
let trkseg = create_track_segment(nb_chunks);
|
||||||
|
let _ = trkseg[TrackSegmentIndex {
|
||||||
|
chunk: trkseg.chunks.len(),
|
||||||
|
pos: 0,
|
||||||
|
flat: 0,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_iter() {
|
||||||
|
let nb_chunks = 10;
|
||||||
|
let trkseg = create_track_segment(nb_chunks);
|
||||||
|
for (i, trkpt) in trkseg.iter().enumerate() {
|
||||||
|
assert!(std::ptr::eq(&trkseg[i], trkpt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_iter_nth() {
|
||||||
|
let nb_chunks = 10;
|
||||||
|
let trkseg = create_track_segment(nb_chunks);
|
||||||
|
let tenth = trkseg.iter().nth(10);
|
||||||
|
assert!(tenth.is_some());
|
||||||
|
assert!(std::ptr::eq(&trkseg[10], tenth.unwrap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_iter_skip() {
|
||||||
|
let nb_chunks = 10;
|
||||||
|
let trkseg = create_track_segment(nb_chunks);
|
||||||
|
for (i, trkpt) in trkseg.iter().enumerate().skip(5) {
|
||||||
|
assert!(std::ptr::eq(&trkseg[i], trkpt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_first_last_index() {
|
||||||
|
assert!(TrackSegment::default().first_index().is_none());
|
||||||
|
assert!(TrackSegment::default().last_index().is_none());
|
||||||
|
|
||||||
|
let trkseg = create_track_segment(5); // 15 points in chunks of 1..=5
|
||||||
|
let first = trkseg.first_index().unwrap();
|
||||||
|
let last = trkseg.last_index().unwrap();
|
||||||
|
assert_eq!((first.chunk, first.pos, first.flat), (0, 0, 0));
|
||||||
|
assert_eq!((last.chunk, last.pos, last.flat), (4, 4, 14));
|
||||||
|
assert_eq!(trkseg[last].ele, 14.0);
|
||||||
|
assert_eq!(trkseg.locate(14), Some(last));
|
||||||
|
assert!(trkseg.next_index(Some(last)).is_none());
|
||||||
|
assert!(trkseg.prev_index(Some(first)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prev_next_are_inverse() {
|
||||||
|
let trkseg = create_track_segment(6);
|
||||||
|
let mut idx = trkseg.last_index();
|
||||||
|
let mut visited = vec![];
|
||||||
|
while let Some(i) = idx {
|
||||||
|
assert_eq!(trkseg.locate(i.flat), Some(i));
|
||||||
|
visited.push(i.flat);
|
||||||
|
idx = trkseg.prev_index(Some(i));
|
||||||
|
}
|
||||||
|
assert_eq!(visited, (0..trkseg.len()).rev().collect::<Vec<_>>());
|
||||||
|
|
||||||
|
let mut idx = trkseg.first_index();
|
||||||
|
while let Some(i) = idx {
|
||||||
|
let next = trkseg.next_index(Some(i));
|
||||||
|
if let Some(n) = next {
|
||||||
|
assert_eq!(trkseg.prev_index(Some(n)), Some(i));
|
||||||
|
}
|
||||||
|
idx = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{Link, TrackSegment};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct TrackId(pub Uuid);
|
||||||
|
|
||||||
|
impl Default for TrackId {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Uuid::new_v4())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
|
pub struct Track {
|
||||||
|
pub id: TrackId,
|
||||||
|
pub info: TrackInfo,
|
||||||
|
pub trkseg: Vec<TrackSegment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
|
pub struct TrackInfo {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub cmt: Option<String>,
|
||||||
|
pub desc: Option<String>,
|
||||||
|
pub src: Option<String>,
|
||||||
|
pub link: Option<Link>,
|
||||||
|
pub type_: Option<String>,
|
||||||
|
pub color: Option<String>,
|
||||||
|
pub opacity: Option<f64>,
|
||||||
|
pub width: Option<f64>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use crate::LngLat;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Trackpoint {
|
||||||
|
pub coordinates: LngLat,
|
||||||
|
pub ele: f64,
|
||||||
|
pub time: Option<i64>,
|
||||||
|
pub atemp: Option<i16>,
|
||||||
|
pub hr: Option<u16>,
|
||||||
|
pub cad: Option<u16>,
|
||||||
|
pub power: Option<u16>,
|
||||||
|
// TODO OSM data? or store intervals at a higher level?
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{Link, LngLat};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct WaypointId(pub Uuid);
|
||||||
|
|
||||||
|
impl Default for WaypointId {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Uuid::new_v4())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Waypoint {
|
||||||
|
pub id: WaypointId,
|
||||||
|
pub coordinates: LngLat,
|
||||||
|
pub ele: f64,
|
||||||
|
pub time: Option<i64>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub cmt: Option<String>,
|
||||||
|
pub desc: Option<String>,
|
||||||
|
pub link: Option<Link>,
|
||||||
|
pub sym: Option<String>,
|
||||||
|
pub type_: Option<String>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mod parse;
|
||||||
|
|
||||||
|
pub use parse::*;
|
||||||
@@ -0,0 +1,579 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Author, File, Link, LngLat, Track, TrackSegment, Trackpoint, TrackpointChunk, Waypoint,
|
||||||
|
WaypointChunk,
|
||||||
|
};
|
||||||
|
use chrono::DateTime;
|
||||||
|
use quick_xml::Error;
|
||||||
|
use quick_xml::events::Event;
|
||||||
|
use quick_xml::events::attributes::Attributes;
|
||||||
|
use quick_xml::reader::Reader;
|
||||||
|
|
||||||
|
enum GPXElement {
|
||||||
|
Metadata,
|
||||||
|
Name,
|
||||||
|
Comment,
|
||||||
|
Description,
|
||||||
|
Source,
|
||||||
|
Author(Author),
|
||||||
|
Link(Link),
|
||||||
|
Text,
|
||||||
|
Track(Track),
|
||||||
|
Segment(TrackSegment),
|
||||||
|
Trackpoint(Trackpoint),
|
||||||
|
Waypoint(Waypoint),
|
||||||
|
Elevation,
|
||||||
|
Time,
|
||||||
|
Temperature,
|
||||||
|
Heartrate,
|
||||||
|
Cadence,
|
||||||
|
Power,
|
||||||
|
Symbol,
|
||||||
|
Type,
|
||||||
|
Color,
|
||||||
|
Opacity,
|
||||||
|
Width,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_coordinates(attributes: Attributes<'_>) -> LngLat {
|
||||||
|
let mut coordinates = LngLat::default();
|
||||||
|
for attr in attributes.flatten() {
|
||||||
|
match attr.key.as_ref() {
|
||||||
|
"lat" => coordinates.lat = attr.value.parse().unwrap_or_default(),
|
||||||
|
"lon" => coordinates.lng = attr.value.parse().unwrap_or_default(),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coordinates
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(data: &[u8]) -> Result<File, Error> {
|
||||||
|
let mut reader = Reader::from_reader(data);
|
||||||
|
let mut buf = vec![];
|
||||||
|
let mut gpx = File::default();
|
||||||
|
let mut stack: Vec<GPXElement> = vec![];
|
||||||
|
let mut trkpt_chunk = TrackpointChunk::default();
|
||||||
|
let mut wpt_chunk = WaypointChunk::default();
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(e)) => match e.name().as_ref() {
|
||||||
|
"metadata" => stack.push(GPXElement::Metadata),
|
||||||
|
"name" => stack.push(GPXElement::Name),
|
||||||
|
"cmt" => stack.push(GPXElement::Comment),
|
||||||
|
"desc" => stack.push(GPXElement::Description),
|
||||||
|
"src" => stack.push(GPXElement::Source),
|
||||||
|
"author" => stack.push(GPXElement::Author(Author::default())),
|
||||||
|
"link" => {
|
||||||
|
let mut link = Link::default();
|
||||||
|
for attr in e.attributes().flatten() {
|
||||||
|
if attr.key.as_ref() == "href" {
|
||||||
|
link.href = attr.value.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stack.push(GPXElement::Link(link));
|
||||||
|
}
|
||||||
|
"text" => stack.push(GPXElement::Text),
|
||||||
|
"trk" => stack.push(GPXElement::Track(Track::default())),
|
||||||
|
"trkseg" => {
|
||||||
|
stack.push(GPXElement::Segment(TrackSegment::default()));
|
||||||
|
}
|
||||||
|
"trkpt" => {
|
||||||
|
stack.push(GPXElement::Trackpoint(Trackpoint {
|
||||||
|
coordinates: parse_coordinates(e.attributes()),
|
||||||
|
..Default::default()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
"wpt" => {
|
||||||
|
stack.push(GPXElement::Waypoint(Waypoint {
|
||||||
|
coordinates: parse_coordinates(e.attributes()),
|
||||||
|
..Default::default()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
"ele" => stack.push(GPXElement::Elevation),
|
||||||
|
"time" => stack.push(GPXElement::Time),
|
||||||
|
e if e.ends_with("atemp") => stack.push(GPXElement::Temperature),
|
||||||
|
e if e.ends_with("hr") => stack.push(GPXElement::Heartrate),
|
||||||
|
e if e.ends_with("cad") => stack.push(GPXElement::Cadence),
|
||||||
|
"power" => stack.push(GPXElement::Power),
|
||||||
|
e if e.ends_with("PowerInWatts") => stack.push(GPXElement::Power),
|
||||||
|
"sym" => stack.push(GPXElement::Symbol),
|
||||||
|
"type" => stack.push(GPXElement::Type),
|
||||||
|
e if e.ends_with("color") => stack.push(GPXElement::Color),
|
||||||
|
e if e.ends_with("opacity") => stack.push(GPXElement::Opacity),
|
||||||
|
e if e.ends_with("width") => stack.push(GPXElement::Width),
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
Ok(Event::End(e)) => match e.name().as_ref() {
|
||||||
|
"gpx" => {
|
||||||
|
if !wpt_chunk.wpt.is_empty() {
|
||||||
|
gpx.wpt.push(Rc::new(std::mem::take(&mut wpt_chunk)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"metadata" => {
|
||||||
|
stack.pop();
|
||||||
|
}
|
||||||
|
"author" => {
|
||||||
|
if let Some(GPXElement::Author(author)) = stack.pop() {
|
||||||
|
gpx.info.author = Some(author);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"link" => {
|
||||||
|
if let Some(GPXElement::Link(link)) = stack.pop() {
|
||||||
|
match stack.last_mut() {
|
||||||
|
Some(GPXElement::Author(author)) => {
|
||||||
|
author.link = Some(link);
|
||||||
|
}
|
||||||
|
Some(GPXElement::Track(trk)) => {
|
||||||
|
trk.info.link = Some(link);
|
||||||
|
}
|
||||||
|
Some(GPXElement::Waypoint(wpt)) => {
|
||||||
|
wpt.link = Some(link);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"trk" => {
|
||||||
|
if let Some(GPXElement::Track(trk)) = stack.pop() {
|
||||||
|
gpx.trk.push(trk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"trkseg" => {
|
||||||
|
if let Some(GPXElement::Segment(mut trkseg)) = stack.pop()
|
||||||
|
&& let Some(GPXElement::Track(trk)) = stack.last_mut()
|
||||||
|
{
|
||||||
|
// `push` ignores the chunk if it is empty
|
||||||
|
trkseg.push(std::mem::take(&mut trkpt_chunk));
|
||||||
|
trk.trkseg.push(trkseg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"trkpt" => {
|
||||||
|
if let Some(GPXElement::Trackpoint(trkpt)) = stack.pop()
|
||||||
|
&& let Some(GPXElement::Segment(trkseg)) = stack.last_mut()
|
||||||
|
{
|
||||||
|
trkpt_chunk.trkpt.push(trkpt);
|
||||||
|
if trkpt_chunk.is_full() {
|
||||||
|
trkseg.push(std::mem::take(&mut trkpt_chunk));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"wpt" => {
|
||||||
|
if let Some(GPXElement::Waypoint(wpt)) = stack.pop() {
|
||||||
|
wpt_chunk.wpt.push(wpt);
|
||||||
|
if wpt_chunk.is_full() {
|
||||||
|
gpx.wpt.push(Rc::new(std::mem::take(&mut wpt_chunk)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
Ok(Event::Text(e)) => match stack.last_mut() {
|
||||||
|
Some(GPXElement::Name) => {
|
||||||
|
stack.pop();
|
||||||
|
match stack.last_mut() {
|
||||||
|
Some(GPXElement::Metadata) => {
|
||||||
|
gpx.info.name = e.to_string();
|
||||||
|
}
|
||||||
|
Some(GPXElement::Author(author)) => {
|
||||||
|
author.name = Some(e.to_string());
|
||||||
|
}
|
||||||
|
Some(GPXElement::Track(trk)) => {
|
||||||
|
trk.info.name = Some(e.to_string());
|
||||||
|
}
|
||||||
|
Some(GPXElement::Waypoint(wpt)) => {
|
||||||
|
wpt.name = Some(e.to_string());
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Comment) => {
|
||||||
|
stack.pop();
|
||||||
|
match stack.last_mut() {
|
||||||
|
Some(GPXElement::Track(trk)) => {
|
||||||
|
trk.info.cmt = Some(e.to_string());
|
||||||
|
}
|
||||||
|
Some(GPXElement::Waypoint(wpt)) => {
|
||||||
|
wpt.cmt = Some(e.to_string());
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Description) => {
|
||||||
|
stack.pop();
|
||||||
|
match stack.last_mut() {
|
||||||
|
Some(GPXElement::Metadata) => {
|
||||||
|
gpx.info.desc = Some(e.to_string());
|
||||||
|
}
|
||||||
|
Some(GPXElement::Track(trk)) => {
|
||||||
|
trk.info.desc = Some(e.to_string());
|
||||||
|
}
|
||||||
|
Some(GPXElement::Waypoint(wpt)) => {
|
||||||
|
wpt.desc = Some(e.to_string());
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Source) => {
|
||||||
|
stack.pop();
|
||||||
|
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
|
||||||
|
trk.info.src = Some(e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Text) => {
|
||||||
|
stack.pop();
|
||||||
|
if let Some(GPXElement::Link(link)) = stack.last_mut() {
|
||||||
|
link.text = Some(e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Elevation) => {
|
||||||
|
stack.pop();
|
||||||
|
match stack.last_mut() {
|
||||||
|
Some(GPXElement::Trackpoint(trkpt)) => {
|
||||||
|
trkpt.ele = e.parse().unwrap_or_default();
|
||||||
|
}
|
||||||
|
Some(GPXElement::Waypoint(wpt)) => {
|
||||||
|
wpt.ele = e.parse().unwrap_or_default();
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Time) => {
|
||||||
|
stack.pop();
|
||||||
|
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||||
|
trkpt.time = DateTime::parse_from_rfc3339(e.as_ref())
|
||||||
|
.ok()
|
||||||
|
.map(|time| time.timestamp_millis());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Temperature) => {
|
||||||
|
stack.pop();
|
||||||
|
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||||
|
trkpt.atemp = e.parse().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Heartrate) => {
|
||||||
|
stack.pop();
|
||||||
|
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||||
|
trkpt.hr = e.parse().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Cadence) => {
|
||||||
|
stack.pop();
|
||||||
|
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||||
|
trkpt.cad = e.parse().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Power) => {
|
||||||
|
stack.pop();
|
||||||
|
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||||
|
trkpt.power = e.parse().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Symbol) => {
|
||||||
|
stack.pop();
|
||||||
|
if let Some(GPXElement::Waypoint(wpt)) = stack.last_mut() {
|
||||||
|
wpt.sym = Some(e.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Type) => {
|
||||||
|
stack.pop();
|
||||||
|
match stack.last_mut() {
|
||||||
|
Some(GPXElement::Track(trk)) => {
|
||||||
|
trk.info.type_ = Some(e.to_string());
|
||||||
|
}
|
||||||
|
Some(GPXElement::Waypoint(wpt)) => {
|
||||||
|
wpt.type_ = Some(e.to_string());
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Color) => {
|
||||||
|
stack.pop();
|
||||||
|
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
|
||||||
|
trk.info.color = Some(format!("#{}", &*e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Opacity) => {
|
||||||
|
stack.pop();
|
||||||
|
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
|
||||||
|
trk.info.opacity = e.parse().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(GPXElement::Width) => {
|
||||||
|
stack.pop();
|
||||||
|
if let Some(GPXElement::Track(trk)) = stack.last_mut() {
|
||||||
|
trk.info.width = e.parse().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
Ok(gpx)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn parse_data(name: &str) -> File {
|
||||||
|
parse(&std::fs::read(format!("data/{name}.gpx")).unwrap()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_simple() {
|
||||||
|
let gpx = parse_data("simple");
|
||||||
|
|
||||||
|
assert_eq!(gpx.info.name, "simple");
|
||||||
|
assert!(gpx.info.desc.is_some_and(|d| d == "description"));
|
||||||
|
assert!(gpx.info.author.is_some());
|
||||||
|
let author = gpx.info.author.as_ref().unwrap();
|
||||||
|
assert!(author.name.as_ref().is_some_and(|n| n == "gpx.studio"));
|
||||||
|
assert!(author.link.is_some());
|
||||||
|
let link = author.link.as_ref().unwrap();
|
||||||
|
assert_eq!(link.href, "https://gpx.studio");
|
||||||
|
assert!(link.text.as_ref().is_some_and(|t| t == "link text"));
|
||||||
|
|
||||||
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
|
let trk = &gpx.trk[0];
|
||||||
|
assert!(trk.info.name.as_ref().is_some_and(|n| n == "track name"));
|
||||||
|
assert!(trk.info.cmt.as_ref().is_some_and(|c| c == "track comment"));
|
||||||
|
assert!(
|
||||||
|
trk.info
|
||||||
|
.desc
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|d| d == "track description")
|
||||||
|
);
|
||||||
|
assert!(trk.info.src.as_ref().is_some_and(|s| s == "track source"));
|
||||||
|
assert!(trk.info.link.is_some());
|
||||||
|
let link = trk.info.link.as_ref().unwrap();
|
||||||
|
assert_eq!(link.href, "https://gpx.studio");
|
||||||
|
assert!(link.text.as_ref().is_some_and(|t| t == "track link text"));
|
||||||
|
assert!(trk.info.type_.as_ref().is_some_and(|c| c == "Cycling"));
|
||||||
|
|
||||||
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
|
let trkseg = &trk.trkseg[0];
|
||||||
|
assert_eq!(trkseg.len(), 80);
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert_eq!(trkpt.coordinates.lat, 50.790867);
|
||||||
|
assert_eq!(trkpt.coordinates.lng, 4.404968);
|
||||||
|
assert_eq!(trkpt.ele, 109.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_tracks() {
|
||||||
|
let gpx = parse_data("with_tracks");
|
||||||
|
|
||||||
|
assert_eq!(gpx.trk.len(), 2);
|
||||||
|
let trk = &gpx.trk[0];
|
||||||
|
assert!(trk.info.name.as_ref().is_some_and(|n| n == "track 1"));
|
||||||
|
assert!(trk.info.type_.as_ref().is_some_and(|c| c == "Cycling"));
|
||||||
|
|
||||||
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
|
let trkseg = &trk.trkseg[0];
|
||||||
|
assert_eq!(trkseg.len(), 49);
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert_eq!(trkpt.coordinates.lat, 50.790867);
|
||||||
|
assert_eq!(trkpt.coordinates.lng, 4.404968);
|
||||||
|
assert_eq!(trkpt.ele, 109.0);
|
||||||
|
|
||||||
|
let trk = &gpx.trk[1];
|
||||||
|
assert!(trk.info.name.as_ref().is_some_and(|n| n == "track 2"));
|
||||||
|
assert!(trk.info.type_.as_ref().is_some_and(|c| c == "Cycling"));
|
||||||
|
|
||||||
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
|
let trkseg = &trk.trkseg[0];
|
||||||
|
assert_eq!(trkseg.len(), 28);
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert_eq!(trkpt.coordinates.lat, 50.782212);
|
||||||
|
assert_eq!(trkpt.coordinates.lng, 4.406377);
|
||||||
|
assert_eq!(trkpt.ele, 115.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_segments() {
|
||||||
|
let gpx = parse_data("with_segments");
|
||||||
|
|
||||||
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
|
let trk = &gpx.trk[0];
|
||||||
|
|
||||||
|
assert_eq!(trk.trkseg.len(), 2);
|
||||||
|
let trkseg = &trk.trkseg[0];
|
||||||
|
assert_eq!(trkseg.len(), 49);
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert_eq!(trkpt.coordinates.lat, 50.790867);
|
||||||
|
assert_eq!(trkpt.coordinates.lng, 4.404968);
|
||||||
|
assert_eq!(trkpt.ele, 109.0);
|
||||||
|
|
||||||
|
let trkseg = &trk.trkseg[1];
|
||||||
|
assert_eq!(trkseg.len(), 28);
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert_eq!(trkpt.coordinates.lat, 50.782212);
|
||||||
|
assert_eq!(trkpt.coordinates.lng, 4.406377);
|
||||||
|
assert_eq!(trkpt.ele, 115.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_tracks_and_segments() {
|
||||||
|
let gpx = parse_data("with_tracks_and_segments");
|
||||||
|
|
||||||
|
assert_eq!(gpx.trk.len(), 2);
|
||||||
|
let trk = &gpx.trk[0];
|
||||||
|
|
||||||
|
assert_eq!(trk.trkseg.len(), 2);
|
||||||
|
let trkseg = &trk.trkseg[0];
|
||||||
|
assert_eq!(trkseg.len(), 16);
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert_eq!(trkpt.coordinates.lat, 50.790867);
|
||||||
|
assert_eq!(trkpt.coordinates.lng, 4.404968);
|
||||||
|
assert_eq!(trkpt.ele, 109.0);
|
||||||
|
|
||||||
|
let trkseg = &trk.trkseg[1];
|
||||||
|
assert_eq!(trkseg.len(), 34);
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert_eq!(trkpt.coordinates.lat, 50.78727108169855);
|
||||||
|
assert_eq!(trkpt.coordinates.lng, 4.406133681127736);
|
||||||
|
assert_eq!(trkpt.ele, 115.0);
|
||||||
|
|
||||||
|
let trk = &gpx.trk[1];
|
||||||
|
|
||||||
|
assert_eq!(trk.trkseg.len(), 2);
|
||||||
|
let trkseg = &trk.trkseg[0];
|
||||||
|
assert_eq!(trkseg.len(), 19);
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert_eq!(trkpt.coordinates.lat, 50.782212);
|
||||||
|
assert_eq!(trkpt.coordinates.lng, 4.406377);
|
||||||
|
assert_eq!(trkpt.ele, 115.5);
|
||||||
|
|
||||||
|
let trkseg = &trk.trkseg[1];
|
||||||
|
assert_eq!(trkseg.len(), 10);
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert_eq!(trkpt.coordinates.lat, 50.77906316558724);
|
||||||
|
assert_eq!(trkpt.coordinates.lng, 4.412547477922485);
|
||||||
|
assert_eq!(trkpt.ele, 133.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_waypoint() {
|
||||||
|
let gpx = parse_data("with_waypoint");
|
||||||
|
|
||||||
|
assert_eq!(gpx.wpt.len(), 1);
|
||||||
|
let chunk = &gpx.wpt[0];
|
||||||
|
assert_eq!(chunk.wpt.len(), 1);
|
||||||
|
let wpt = &chunk.wpt[0];
|
||||||
|
assert_eq!(wpt.coordinates.lat, 50.7836710064975);
|
||||||
|
assert_eq!(wpt.coordinates.lng, 4.410764082658738);
|
||||||
|
assert!(wpt.name.as_ref().is_some_and(|n| n == "waypoint name"));
|
||||||
|
assert!(wpt.cmt.as_ref().is_some_and(|c| c == "waypoint comment"));
|
||||||
|
assert!(
|
||||||
|
wpt.desc
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|d| d == "waypoint description")
|
||||||
|
);
|
||||||
|
assert!(wpt.link.is_some());
|
||||||
|
let link = wpt.link.as_ref().unwrap();
|
||||||
|
assert_eq!(link.href, "https://gpx.studio");
|
||||||
|
assert!(
|
||||||
|
link.text
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|t| t == "waypoint link text")
|
||||||
|
);
|
||||||
|
assert!(wpt.sym.as_ref().is_some_and(|s| s == "Bike Trail"));
|
||||||
|
assert!(wpt.type_.as_ref().is_some_and(|t| t == "Bike Trail"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_trackpoint_time() {
|
||||||
|
let gpx = parse_data("with_time");
|
||||||
|
|
||||||
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
|
let trk = &gpx.trk[0];
|
||||||
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
|
let trkseg = &trk.trkseg[0];
|
||||||
|
assert!(!trkseg.is_empty());
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert!(trkpt.time.is_some_and(|t| t == 1704063600000));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_trackpoint_hr() {
|
||||||
|
let gpx = parse_data("with_hr");
|
||||||
|
|
||||||
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
|
let trk = &gpx.trk[0];
|
||||||
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
|
let trkseg = &trk.trkseg[0];
|
||||||
|
assert!(!trkseg.is_empty());
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert!(trkpt.hr.is_some_and(|h| h == 150));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_trackpoint_cad() {
|
||||||
|
let gpx = parse_data("with_cad");
|
||||||
|
|
||||||
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
|
let trk = &gpx.trk[0];
|
||||||
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
|
let trkseg = &trk.trkseg[0];
|
||||||
|
assert!(!trkseg.is_empty());
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert!(trkpt.cad.is_some_and(|c| c == 80));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_trackpoint_power_1() {
|
||||||
|
let gpx = parse_data("with_power_1");
|
||||||
|
|
||||||
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
|
let trk = &gpx.trk[0];
|
||||||
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
|
let trkseg = &trk.trkseg[0];
|
||||||
|
assert!(!trkseg.is_empty());
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert!(trkpt.power.is_some_and(|p| p == 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_trackpoint_power_2() {
|
||||||
|
let gpx = parse_data("with_power_2");
|
||||||
|
|
||||||
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
|
let trk = &gpx.trk[0];
|
||||||
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
|
let trkseg = &trk.trkseg[0];
|
||||||
|
assert!(!trkseg.is_empty());
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert!(trkpt.power.is_some_and(|p| p == 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_trackpoint_atemp() {
|
||||||
|
let gpx = parse_data("with_temp");
|
||||||
|
|
||||||
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
|
let trk = &gpx.trk[0];
|
||||||
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
|
let trkseg = &trk.trkseg[0];
|
||||||
|
assert!(!trkseg.is_empty());
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
|
assert!(trkpt.atemp.is_some_and(|t| t == 21));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_track_style() {
|
||||||
|
let gpx = parse_data("with_style");
|
||||||
|
|
||||||
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
|
let trk = &gpx.trk[0];
|
||||||
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
|
assert!(trk.info.color.as_ref().is_some_and(|c| c == "#2d3ee9"));
|
||||||
|
assert!(trk.info.opacity.is_some_and(|o| o == 0.5));
|
||||||
|
assert!(trk.info.width.is_some_and(|w| w == 6.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
mod algorithm;
|
||||||
|
pub(crate) mod gpx;
|
||||||
|
mod io;
|
||||||
|
pub(crate) mod statistics;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
pub use algorithm::*;
|
||||||
|
pub use gpx::*;
|
||||||
|
pub use io::*;
|
||||||
|
pub use statistics::*;
|
||||||
|
pub use utils::*;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
mod statistics;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
pub use statistics::*;
|
||||||
|
pub use utils::*;
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
use crate::{
|
||||||
|
LngLat, LngLatBounds, TrackSegment, TrackSegmentIndex, Trackpoint, distance, for_each_window,
|
||||||
|
max_options, min_options, ramer_douglas_peucker, slope, speed, sum_options, time_diff,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct Statistics {
|
||||||
|
pub global: GlobalStatistics,
|
||||||
|
pub local: Vec<TrackpointStatistics>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Statistics {
|
||||||
|
pub fn total_speed(&self) -> Option<f64> {
|
||||||
|
self.global.total_speed()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn moving_speed(&self) -> Option<f64> {
|
||||||
|
self.global.moving_speed()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute(trkseg: &TrackSegment) -> Self {
|
||||||
|
let mut stats = Self::default();
|
||||||
|
if trkseg.is_empty() {
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut prev = &trkseg[0];
|
||||||
|
for cur in trkseg.iter() {
|
||||||
|
stats.accumulate(prev, cur);
|
||||||
|
prev = cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.compute_smoothed_speed(trkseg);
|
||||||
|
stats.compute_smoothed_elevation_segments(trkseg);
|
||||||
|
stats.compute_smoothed_slope(trkseg);
|
||||||
|
|
||||||
|
stats
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accumulate(&mut self, prev: &Trackpoint, cur: &Trackpoint) {
|
||||||
|
self.accumulate_distance_and_time(prev, cur);
|
||||||
|
self.update_time_bounds(cur.time);
|
||||||
|
self.update_bounds(cur.coordinates);
|
||||||
|
self.local
|
||||||
|
.push(TrackpointStatistics::from_partial_stats(self));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accumulate_distance_and_time(&mut self, prev: &Trackpoint, cur: &Trackpoint) {
|
||||||
|
let dist = distance(prev.coordinates, cur.coordinates);
|
||||||
|
let time = time_diff(cur.time, prev.time);
|
||||||
|
|
||||||
|
self.global.total_distance += dist;
|
||||||
|
|
||||||
|
if let Some(time) = time {
|
||||||
|
let speed = speed(dist, time);
|
||||||
|
if (0.5..=1500.0).contains(&speed) {
|
||||||
|
self.global.moving_distance = self
|
||||||
|
.global
|
||||||
|
.moving_distance
|
||||||
|
.map_or(Some(dist), |d| Some(d + dist));
|
||||||
|
self.global.moving_time = self
|
||||||
|
.global
|
||||||
|
.moving_time
|
||||||
|
.map_or(Some(time), |t| Some(t + time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_time_bounds(&mut self, time: Option<i64>) {
|
||||||
|
if let Some(time) = time {
|
||||||
|
if self.global.start_time.is_none() {
|
||||||
|
self.global.start_time = Some(time);
|
||||||
|
}
|
||||||
|
self.global.end_time = Some(time);
|
||||||
|
self.global.total_time = time_diff(self.global.end_time, self.global.start_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_bounds(&mut self, coordinates: LngLat) {
|
||||||
|
self.global.bounds.extend(coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_smoothed_speed(&mut self, trkseg: &TrackSegment) {
|
||||||
|
for_each_window!(
|
||||||
|
trkseg,
|
||||||
|
trkseg.first_index(),
|
||||||
|
trkseg.last_index(),
|
||||||
|
Some(10000),
|
||||||
|
|i, j| time_diff(trkseg[i].time, trkseg[j].time),
|
||||||
|
|i, left, right| {
|
||||||
|
self.local[i.flat].speed =
|
||||||
|
time_diff(trkseg[right].time, trkseg[left].time).map(|t| {
|
||||||
|
speed(
|
||||||
|
self.local[right.flat].total_distance
|
||||||
|
- self.local[left.flat].total_distance,
|
||||||
|
t,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_smoothed_elevation_segments(&mut self, trkseg: &TrackSegment) {
|
||||||
|
let simplified = self.get_elevation_extremas(trkseg);
|
||||||
|
|
||||||
|
for i in 0..(simplified.len() - 1) {
|
||||||
|
let start = simplified[i];
|
||||||
|
let end = simplified[i + 1];
|
||||||
|
let last = i + 1 == simplified.len() - 1;
|
||||||
|
|
||||||
|
self.compute_smoothed_elevation_gain(trkseg, start, end, last);
|
||||||
|
|
||||||
|
let segment_dist =
|
||||||
|
self.local[end.flat].total_distance - self.local[start.flat].total_distance;
|
||||||
|
let segment_ele = trkseg[end].ele - trkseg[start].ele;
|
||||||
|
let segment_slope = slope(segment_ele, segment_dist);
|
||||||
|
for k in start.flat..(end.flat + last as usize) {
|
||||||
|
self.local[k].slope_segment = SlopeSegment {
|
||||||
|
slope: segment_slope,
|
||||||
|
distance: segment_dist,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_smoothed_elevation_gain(
|
||||||
|
&mut self,
|
||||||
|
trkseg: &TrackSegment,
|
||||||
|
start: TrackSegmentIndex,
|
||||||
|
end: TrackSegmentIndex,
|
||||||
|
last: bool,
|
||||||
|
) {
|
||||||
|
let mut cumul_ele = 0.0;
|
||||||
|
let mut current_left = start;
|
||||||
|
let mut current_right = Some(start);
|
||||||
|
let mut prev_smoothed_ele = trkseg[start].ele;
|
||||||
|
|
||||||
|
for_each_window!(
|
||||||
|
trkseg,
|
||||||
|
Some(start),
|
||||||
|
Some(end),
|
||||||
|
0.1,
|
||||||
|
|i, j| self.local[j.flat].total_distance - self.local[i.flat].total_distance,
|
||||||
|
|i, left, right| {
|
||||||
|
while current_left != left {
|
||||||
|
cumul_ele -= trkseg[current_left].ele;
|
||||||
|
current_left = trkseg.next_index(Some(current_left)).unwrap();
|
||||||
|
}
|
||||||
|
while let Some(current) = current_right {
|
||||||
|
if current > right {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cumul_ele += trkseg[current].ele;
|
||||||
|
current_right = trkseg.next_index(current_right);
|
||||||
|
}
|
||||||
|
|
||||||
|
let smoothed_ele = if i == start || i == end {
|
||||||
|
trkseg[i].ele
|
||||||
|
} else {
|
||||||
|
cumul_ele / (right.flat - left.flat + 1) as f64
|
||||||
|
};
|
||||||
|
|
||||||
|
let delta = smoothed_ele - prev_smoothed_ele;
|
||||||
|
if delta > 0.0 {
|
||||||
|
self.global.elevation_gain += delta;
|
||||||
|
} else if delta < 0.0 {
|
||||||
|
self.global.elevation_loss -= delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < end || last {
|
||||||
|
self.local[i.flat].elevation_gain = self.global.elevation_gain;
|
||||||
|
self.local[i.flat].elevation_loss = self.global.elevation_loss;
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_smoothed_ele = smoothed_ele;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_smoothed_slope(&mut self, trkseg: &TrackSegment) {
|
||||||
|
for_each_window!(
|
||||||
|
trkseg,
|
||||||
|
trkseg.first_index(),
|
||||||
|
trkseg.last_index(),
|
||||||
|
0.05,
|
||||||
|
|i, j| self.local[j.flat].total_distance - self.local[i.flat].total_distance,
|
||||||
|
|i, left, right| {
|
||||||
|
let dist =
|
||||||
|
self.local[right.flat].total_distance - self.local[left.flat].total_distance;
|
||||||
|
let ele = trkseg[right].ele - trkseg[left].ele;
|
||||||
|
self.local[i.flat].slope = slope(ele, dist);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_elevation_extremas(&self, trkseg: &TrackSegment) -> Vec<TrackSegmentIndex> {
|
||||||
|
ramer_douglas_peucker(
|
||||||
|
trkseg,
|
||||||
|
&|idx| {
|
||||||
|
(
|
||||||
|
self.local[idx.flat].total_distance * 1000.0,
|
||||||
|
trkseg[idx].ele,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
20.0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct GlobalStatistics {
|
||||||
|
pub total_distance: f64,
|
||||||
|
pub moving_distance: Option<f64>,
|
||||||
|
pub total_time: Option<i32>,
|
||||||
|
pub moving_time: Option<i32>,
|
||||||
|
pub elevation_gain: f64,
|
||||||
|
pub elevation_loss: f64,
|
||||||
|
pub start_time: Option<i64>,
|
||||||
|
pub end_time: Option<i64>,
|
||||||
|
pub bounds: LngLatBounds,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlobalStatistics {
|
||||||
|
pub fn total_speed(&self) -> Option<f64> {
|
||||||
|
self.total_time.map(|t| speed(self.total_distance, t))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn moving_speed(&self) -> Option<f64> {
|
||||||
|
self.moving_distance
|
||||||
|
.zip(self.moving_time)
|
||||||
|
.map(|(d, t)| speed(d, t))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge(&mut self, other: &GlobalStatistics) {
|
||||||
|
self.total_distance += other.total_distance;
|
||||||
|
self.moving_distance = sum_options(self.moving_distance, other.moving_distance);
|
||||||
|
self.total_time = sum_options(self.total_time, other.total_time);
|
||||||
|
self.moving_time = sum_options(self.moving_time, other.moving_time);
|
||||||
|
self.elevation_gain += other.elevation_gain;
|
||||||
|
self.elevation_loss += other.elevation_loss;
|
||||||
|
self.start_time = min_options(self.start_time, other.start_time);
|
||||||
|
self.end_time = max_options(self.end_time, other.end_time);
|
||||||
|
self.bounds.merge(&other.bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct TrackpointStatistics {
|
||||||
|
pub total_distance: f64,
|
||||||
|
pub moving_distance: Option<f64>,
|
||||||
|
pub total_time: Option<i32>,
|
||||||
|
pub moving_time: Option<i32>,
|
||||||
|
pub speed: Option<f64>,
|
||||||
|
pub elevation_gain: f64,
|
||||||
|
pub elevation_loss: f64,
|
||||||
|
pub slope: f64,
|
||||||
|
pub slope_segment: SlopeSegment,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrackpointStatistics {
|
||||||
|
fn from_partial_stats(stats: &Statistics) -> Self {
|
||||||
|
Self {
|
||||||
|
total_distance: stats.global.total_distance,
|
||||||
|
moving_distance: stats.global.moving_distance,
|
||||||
|
total_time: stats.global.total_time,
|
||||||
|
moving_time: stats.global.moving_time,
|
||||||
|
// stats below are computed later
|
||||||
|
speed: None,
|
||||||
|
elevation_gain: Default::default(),
|
||||||
|
elevation_loss: Default::default(),
|
||||||
|
slope: Default::default(),
|
||||||
|
slope_segment: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct SlopeSegment {
|
||||||
|
pub slope: f64,
|
||||||
|
pub distance: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{fs::File, io::Read};
|
||||||
|
|
||||||
|
use crate::parse;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compute_smoothed_speed() {
|
||||||
|
let mut f = File::open("data/with_time.gpx").unwrap();
|
||||||
|
let mut data = String::new();
|
||||||
|
let _ = f.read_to_string(&mut data);
|
||||||
|
let gpx = parse(data.as_bytes()).unwrap();
|
||||||
|
|
||||||
|
let trkseg = &gpx.trk[0].trkseg[0];
|
||||||
|
let stats = Statistics::compute(trkseg);
|
||||||
|
assert_eq!(stats.local.len(), trkseg.len());
|
||||||
|
for trkpt_stats in stats.local.iter() {
|
||||||
|
assert!(trkpt_stats.speed.is_some());
|
||||||
|
let speed = trkpt_stats.speed.unwrap();
|
||||||
|
assert_ne!(speed, f64::NAN);
|
||||||
|
assert!((speed - 20.0).abs() < 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(path: &str) -> crate::File {
|
||||||
|
let mut f = File::open(path).unwrap();
|
||||||
|
let mut data = String::new();
|
||||||
|
let _ = f.read_to_string(&mut data);
|
||||||
|
parse(data.as_bytes()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compute_empty_segment() {
|
||||||
|
let stats = Statistics::compute(&TrackSegment::default());
|
||||||
|
assert!(stats.local.is_empty());
|
||||||
|
assert_eq!(stats.global.total_distance, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compute_distance_and_bounds() {
|
||||||
|
let gpx = load("data/simple.gpx");
|
||||||
|
let trkseg = &gpx.trk[0].trkseg[0];
|
||||||
|
let stats = Statistics::compute(trkseg);
|
||||||
|
|
||||||
|
assert_eq!(stats.local.len(), trkseg.len());
|
||||||
|
assert_eq!(stats.local[0].total_distance, 0.0);
|
||||||
|
assert!(stats.global.total_distance > 0.0);
|
||||||
|
// cumulative distance never decreases and ends at the global distance
|
||||||
|
assert!(
|
||||||
|
stats
|
||||||
|
.local
|
||||||
|
.windows(2)
|
||||||
|
.all(|w| w[0].total_distance <= w[1].total_distance)
|
||||||
|
);
|
||||||
|
let last = stats.local.last().unwrap();
|
||||||
|
assert!((last.total_distance - stats.global.total_distance).abs() < 1e-9);
|
||||||
|
assert!(stats.global.elevation_gain >= 0.0);
|
||||||
|
assert!(stats.global.elevation_loss >= 0.0);
|
||||||
|
|
||||||
|
// every point lies inside the bounds
|
||||||
|
let b = &stats.global.bounds;
|
||||||
|
for trkpt in trkseg.iter() {
|
||||||
|
assert!(b.sw.lng <= trkpt.coordinates.lng && trkpt.coordinates.lng <= b.ne.lng);
|
||||||
|
assert!(b.sw.lat <= trkpt.coordinates.lat && trkpt.coordinates.lat <= b.ne.lat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_compute_time() {
|
||||||
|
let gpx = load("data/with_time.gpx");
|
||||||
|
let stats = Statistics::compute(&gpx.trk[0].trkseg[0]);
|
||||||
|
let total_time = stats.global.total_time.unwrap();
|
||||||
|
assert!(total_time > 0);
|
||||||
|
let speed = stats.global.total_speed().unwrap();
|
||||||
|
assert!((speed - 20.0).abs() < 0.5, "{speed}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_time_without_timestamps() {
|
||||||
|
let gpx = load("data/simple.gpx");
|
||||||
|
let stats = Statistics::compute(&gpx.trk[0].trkseg[0]);
|
||||||
|
assert!(stats.global.total_time.is_none());
|
||||||
|
assert!(stats.global.total_speed().is_none());
|
||||||
|
assert!(stats.local.iter().all(|s| s.speed.is_none()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_global_merge() {
|
||||||
|
let mut a = GlobalStatistics {
|
||||||
|
total_distance: 1.0,
|
||||||
|
elevation_gain: 10.0,
|
||||||
|
moving_time: Some(5),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
a.bounds.extend(crate::LngLat { lng: 0.0, lat: 0.0 });
|
||||||
|
let mut b = GlobalStatistics {
|
||||||
|
total_distance: 2.0,
|
||||||
|
elevation_loss: 4.0,
|
||||||
|
moving_distance: Some(1.5),
|
||||||
|
moving_time: Some(7),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
b.bounds.extend(crate::LngLat { lng: 2.0, lat: 3.0 });
|
||||||
|
|
||||||
|
a.merge(&b);
|
||||||
|
assert_eq!(a.total_distance, 3.0);
|
||||||
|
assert_eq!(a.elevation_gain, 10.0);
|
||||||
|
assert_eq!(a.elevation_loss, 4.0);
|
||||||
|
assert_eq!(a.moving_distance, Some(1.5));
|
||||||
|
assert_eq!(a.moving_time, Some(12));
|
||||||
|
assert_eq!(a.total_time, None);
|
||||||
|
|
||||||
|
let mut c = GlobalStatistics {
|
||||||
|
start_time: Some(1_000),
|
||||||
|
end_time: Some(4_000),
|
||||||
|
total_time: Some(3_000),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let d = GlobalStatistics {
|
||||||
|
start_time: Some(10_000),
|
||||||
|
end_time: Some(12_000),
|
||||||
|
total_time: Some(2_000),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
c.merge(&d);
|
||||||
|
// 3 s + 2 s, the gap between the two is ignored
|
||||||
|
assert_eq!(c.total_time, Some(5_000));
|
||||||
|
let mut cumul = GlobalStatistics::default();
|
||||||
|
cumul.merge(&c);
|
||||||
|
cumul.merge(&d);
|
||||||
|
assert_eq!(cumul.start_time, Some(1_000));
|
||||||
|
assert_eq!(cumul.end_time, Some(12_000));
|
||||||
|
assert_eq!(cumul.total_time, Some(7_000));
|
||||||
|
assert_eq!((a.bounds.ne.lng, a.bounds.ne.lat), (2.0, 3.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
use std::ops::Add;
|
||||||
|
|
||||||
|
pub fn combine_options<T>(a: Option<T>, b: Option<T>, f: impl FnOnce(T, T) -> T) -> Option<T> {
|
||||||
|
match (a, b) {
|
||||||
|
(Some(a), Some(b)) => Some(f(a, b)),
|
||||||
|
(Some(v), None) | (None, Some(v)) => Some(v),
|
||||||
|
(None, None) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sum_options<T>(a: Option<T>, b: Option<T>) -> Option<T>
|
||||||
|
where
|
||||||
|
T: Add<Output = T>,
|
||||||
|
{
|
||||||
|
combine_options(a, b, |a, b| a + b)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn min_options<T>(a: Option<T>, b: Option<T>) -> Option<T>
|
||||||
|
where
|
||||||
|
T: Ord,
|
||||||
|
{
|
||||||
|
combine_options(a, b, |a, b| a.min(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_options<T>(a: Option<T>, b: Option<T>) -> Option<T>
|
||||||
|
where
|
||||||
|
T: Ord,
|
||||||
|
{
|
||||||
|
combine_options(a, b, |a, b| a.max(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sum_options() {
|
||||||
|
assert_eq!(sum_options(Some(1), Some(2)), Some(3));
|
||||||
|
assert_eq!(sum_options(Some(1), None), Some(1));
|
||||||
|
assert_eq!(sum_options(None, Some(2)), Some(2));
|
||||||
|
assert_eq!(sum_options::<i32>(None, None), None);
|
||||||
|
assert_eq!(sum_options(Some(0.5), Some(0.25)), Some(0.75));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
|
use crate::LngLat;
|
||||||
|
|
||||||
|
const TO_RADIANS: f64 = PI / 180.0;
|
||||||
|
const EARTH_RADIUS: f64 = 6371.0088;
|
||||||
|
|
||||||
|
/// Computes the distance in kilometers between two coordinates using the Haversine formula
|
||||||
|
pub fn distance(p1: LngLat, p2: LngLat) -> f64 {
|
||||||
|
let lat1 = p1.lat * TO_RADIANS;
|
||||||
|
let lat2 = p2.lat * TO_RADIANS;
|
||||||
|
let delta_lat = lat2 - lat1;
|
||||||
|
let delta_lng = (p2.lng - p1.lng) * TO_RADIANS;
|
||||||
|
|
||||||
|
let a =
|
||||||
|
(delta_lat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (delta_lng / 2.0).sin().powi(2);
|
||||||
|
let c = 2.0 * a.min(1.0).sqrt().asin();
|
||||||
|
EARTH_RADIUS * c
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn time_diff(a: Option<i64>, b: Option<i64>) -> Option<i32> {
|
||||||
|
Some((a? - b?) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the speed for a given distance in kilometers and a time in milliseconds
|
||||||
|
pub fn speed(distance: f64, time: i32) -> f64 {
|
||||||
|
distance / (time as f64 / 3_600_000.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn slope(ele: f64, distance: f64) -> f64 {
|
||||||
|
if distance == 0.0 {
|
||||||
|
100.0
|
||||||
|
} else {
|
||||||
|
0.1 * ele / distance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const METERS_PER_LATITUDE_DEGREE: f64 = 111320.0;
|
||||||
|
|
||||||
|
/// Approximate planar projection, in meters, around the latitude of `origin` (ignores the
|
||||||
|
/// curvature of the earth).
|
||||||
|
struct Planar {
|
||||||
|
meters_per_longitude_degree: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Planar {
|
||||||
|
fn around(origin: LngLat) -> Self {
|
||||||
|
Self {
|
||||||
|
meters_per_longitude_degree: (origin.lat * TO_RADIANS).cos()
|
||||||
|
* METERS_PER_LATITUDE_DEGREE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_meters(&self, p: LngLat) -> (f64, f64) {
|
||||||
|
(
|
||||||
|
p.lng * self.meters_per_longitude_degree,
|
||||||
|
p.lat * METERS_PER_LATITUDE_DEGREE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_degrees(&self, (x, y): (f64, f64)) -> LngLat {
|
||||||
|
LngLat {
|
||||||
|
lng: x / self.meters_per_longitude_degree,
|
||||||
|
lat: y / METERS_PER_LATITUDE_DEGREE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Position, between 0 (at `(x1, y1)`) and 1 (at `(x2, y2)`), of the point of the segment that is
|
||||||
|
/// the closest to `(x3, y3)`. A degenerate segment (same ends) is a point.
|
||||||
|
fn closest_on_segment(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> f64 {
|
||||||
|
let dx = x2 - x1;
|
||||||
|
let dy = y2 - y1;
|
||||||
|
let segment_length_squared = dx * dx + dy * dy;
|
||||||
|
if segment_length_squared == 0.0 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
(((x3 - x1) * dx + (y3 - y1) * dy) / segment_length_squared).clamp(0.0, 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the point on the line segment defined by p1 and p2 that is closest to the third
|
||||||
|
/// point, p3. Uses simple planar geometry (ignores earth curvature).
|
||||||
|
pub fn projected(p1: LngLat, p2: LngLat, p3: LngLat) -> LngLat {
|
||||||
|
let planar = Planar::around(p1);
|
||||||
|
let (x1, y1) = planar.to_meters(p1);
|
||||||
|
let (x2, y2) = planar.to_meters(p2);
|
||||||
|
let (x3, y3) = planar.to_meters(p3);
|
||||||
|
let t = closest_on_segment(x1, y1, x2, y2, x3, y3);
|
||||||
|
planar.to_degrees((x1 + t * (x2 - x1), y1 + t * (y2 - y1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the perpendicular distance in meters between a line segment (defined by p1 and p2)
|
||||||
|
/// and a third point, p3. Uses simple planar geometry (ignores earth curvature).
|
||||||
|
pub fn crossarc_lnglat(p1: LngLat, p2: LngLat, p3: LngLat) -> f64 {
|
||||||
|
let planar = Planar::around(p1);
|
||||||
|
let (x1, y1) = planar.to_meters(p1);
|
||||||
|
let (x2, y2) = planar.to_meters(p2);
|
||||||
|
let (x3, y3) = planar.to_meters(p3);
|
||||||
|
crossarc(x1, y1, x2, y2, x3, y3)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Distance from the point `(x3, y3)` to the segment `(x1, y1)`-`(x2, y2)`, in the units of the
|
||||||
|
/// coordinates (planar geometry).
|
||||||
|
pub fn crossarc(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> f64 {
|
||||||
|
let t = closest_on_segment(x1, y1, x2, y2, x3, y3);
|
||||||
|
(x3 - (x1 + t * (x2 - x1))).hypot(y3 - (y1 + t * (y2 - y1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn p(lng: f64, lat: f64) -> LngLat {
|
||||||
|
LngLat { lng, lat }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_distance() {
|
||||||
|
assert_eq!(distance(p(4.0, 50.0), p(4.0, 50.0)), 0.0);
|
||||||
|
// one degree of latitude
|
||||||
|
let d = distance(p(0.0, 0.0), p(0.0, 1.0));
|
||||||
|
assert!((d - 111.195).abs() < 0.01, "{d}");
|
||||||
|
// symmetric
|
||||||
|
let a = p(4.40, 50.79);
|
||||||
|
let b = p(6.13, 45.90);
|
||||||
|
assert!((distance(a, b) - distance(b, a)).abs() < 1e-9);
|
||||||
|
// half the Earth's circumference, without NaN from rounding
|
||||||
|
let antipodal = distance(p(0.0, 0.0), p(180.0, 0.0));
|
||||||
|
assert!((antipodal - PI * EARTH_RADIUS).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_time_diff() {
|
||||||
|
assert_eq!(time_diff(Some(5000), Some(2000)), Some(3000));
|
||||||
|
assert_eq!(time_diff(Some(2000), Some(5000)), Some(-3000));
|
||||||
|
assert_eq!(time_diff(None, Some(1)), None);
|
||||||
|
assert_eq!(time_diff(Some(1), None), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_speed() {
|
||||||
|
// 1 km in 1 h
|
||||||
|
assert!((speed(1.0, 3_600_000) - 1.0).abs() < 1e-12);
|
||||||
|
// 10 km in 30 min
|
||||||
|
assert!((speed(10.0, 1_800_000) - 20.0).abs() < 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_slope() {
|
||||||
|
// 10 m of elevation over 100 m (distance is expressed in km, hence the 0.1 factor)
|
||||||
|
assert!((slope(10.0, 0.1) - 10.0).abs() < 1e-12);
|
||||||
|
assert!((slope(-5.0, 0.1) + 5.0).abs() < 1e-12);
|
||||||
|
assert_eq!(slope(0.0, 1.0), 0.0);
|
||||||
|
assert_eq!(slope(10.0, 0.0), 100.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_crossarc() {
|
||||||
|
// perpendicular distance to the segment (0,0)-(10,0)
|
||||||
|
assert!((crossarc(0.0, 0.0, 10.0, 0.0, 5.0, 3.0) - 3.0).abs() < 1e-12);
|
||||||
|
// beyond the ends: distance to the closest endpoint
|
||||||
|
assert!((crossarc(0.0, 0.0, 10.0, 0.0, 13.0, 4.0) - 5.0).abs() < 1e-12);
|
||||||
|
assert!((crossarc(0.0, 0.0, 10.0, 0.0, -3.0, 4.0) - 5.0).abs() < 1e-12);
|
||||||
|
// point on the segment
|
||||||
|
assert_eq!(crossarc(0.0, 0.0, 10.0, 0.0, 4.0, 0.0), 0.0);
|
||||||
|
// degenerate segment
|
||||||
|
assert!((crossarc(1.0, 1.0, 1.0, 1.0, 4.0, 5.0) - 5.0).abs() < 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_projected() {
|
||||||
|
let proj = projected(p(0.0, 0.0), p(1.0, 0.0), p(0.5, 1.0));
|
||||||
|
assert!((proj.lng - 0.5).abs() < 1e-9);
|
||||||
|
assert!(proj.lat.abs() < 1e-9);
|
||||||
|
// clamped to the segment
|
||||||
|
let proj = projected(p(0.0, 0.0), p(1.0, 0.0), p(2.0, 1.0));
|
||||||
|
assert!((proj.lng - 1.0).abs() < 1e-9);
|
||||||
|
// degenerate segment
|
||||||
|
let proj = projected(p(3.0, 4.0), p(3.0, 4.0), p(5.0, 6.0));
|
||||||
|
assert_eq!((proj.lng, proj.lat), (3.0, 4.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_crossarc_lnglat() {
|
||||||
|
// about 1 degree of latitude away from an east-west segment on the equator
|
||||||
|
let d = crossarc_lnglat(p(0.0, 0.0), p(1.0, 0.0), p(0.5, 1.0));
|
||||||
|
assert!((d - METERS_PER_LATITUDE_DEGREE).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
use crate::{Command, FileId};
|
||||||
|
|
||||||
|
/// Everything the frontend can ask the engine to do.
|
||||||
|
///
|
||||||
|
/// Only [`Action::Edit`] changes the files (and records an undo step); the others act on the
|
||||||
|
/// history, on the selection or on the file order.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Action<'a> {
|
||||||
|
Edit(Command<'a>),
|
||||||
|
Undo,
|
||||||
|
Redo,
|
||||||
|
Select {
|
||||||
|
file_ids: Vec<FileId>,
|
||||||
|
},
|
||||||
|
AddSelect {
|
||||||
|
file_ids: Vec<FileId>,
|
||||||
|
},
|
||||||
|
SelectAll,
|
||||||
|
/// Moves the files to `index` among the other files, in the given order. Not undoable.
|
||||||
|
Reorder {
|
||||||
|
file_ids: Vec<FileId>,
|
||||||
|
index: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::State;
|
||||||
|
|
||||||
|
/// The effect of a command on the engine state.
|
||||||
|
///
|
||||||
|
/// Each command is a small struct holding its arguments, with its effect implemented in its
|
||||||
|
/// own file. Effects are built from the reusable patterns in `pattern/`.
|
||||||
|
pub trait Apply {
|
||||||
|
fn apply(self, state: &mut State) -> Result<(), CommandError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum CommandError {
|
||||||
|
/// The command is not implemented yet.
|
||||||
|
NotImplemented(&'static str),
|
||||||
|
/// The command would not change anything.
|
||||||
|
NothingToDo,
|
||||||
|
/// The input data could not be parsed.
|
||||||
|
InvalidData(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for CommandError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::NotImplemented(name) => write!(f, "{name} is not implemented yet"),
|
||||||
|
Self::NothingToDo => write!(f, "nothing to do"),
|
||||||
|
Self::InvalidData(err) => write!(f, "invalid data: {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for CommandError {}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
use crate::{Apply, CommandError, State};
|
||||||
|
use crate::{
|
||||||
|
Clean, Crop, Delete, DeleteAll, Duplicate, Elevation, Extract, Load, Merge, Metadata,
|
||||||
|
MoveWaypoint, New, NewTrack, NewTrackSegment, NewWaypoint, Reverse, SpliceTrackpoints, Split,
|
||||||
|
Style, Time,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A user action that edits the files. Each variant wraps the command's own struct, whose
|
||||||
|
/// effect is implemented in its own file (see `Apply`).
|
||||||
|
///
|
||||||
|
/// Bulk data (coordinates, file bytes) is borrowed straight from the wasm-bindgen argument
|
||||||
|
/// buffers, so a command is built without copying; the engine only copies what it keeps.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Command<'a> {
|
||||||
|
New(New<'a>),
|
||||||
|
Load(Load<'a>),
|
||||||
|
Duplicate(Duplicate),
|
||||||
|
Delete(Delete),
|
||||||
|
DeleteAll(DeleteAll),
|
||||||
|
Metadata(Metadata<'a>),
|
||||||
|
Style(Style<'a>),
|
||||||
|
NewTrack(NewTrack),
|
||||||
|
NewTrackSegment(NewTrackSegment),
|
||||||
|
Reverse(Reverse),
|
||||||
|
SpliceTrackpoints(SpliceTrackpoints<'a>),
|
||||||
|
NewWaypoint(NewWaypoint<'a>),
|
||||||
|
MoveWaypoint(MoveWaypoint),
|
||||||
|
Crop(Crop),
|
||||||
|
Split(Split),
|
||||||
|
Time(Time),
|
||||||
|
Merge(Merge),
|
||||||
|
Extract(Extract),
|
||||||
|
Elevation(Elevation<'a>),
|
||||||
|
Clean(Clean),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apply for Command<'_> {
|
||||||
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||||
|
match self {
|
||||||
|
Command::New(c) => c.apply(state),
|
||||||
|
Command::Load(c) => c.apply(state),
|
||||||
|
Command::Duplicate(c) => c.apply(state),
|
||||||
|
Command::Delete(c) => c.apply(state),
|
||||||
|
Command::DeleteAll(c) => c.apply(state),
|
||||||
|
Command::Metadata(c) => c.apply(state),
|
||||||
|
Command::Style(c) => c.apply(state),
|
||||||
|
Command::NewTrack(c) => c.apply(state),
|
||||||
|
Command::NewTrackSegment(c) => c.apply(state),
|
||||||
|
Command::Reverse(c) => c.apply(state),
|
||||||
|
Command::SpliceTrackpoints(c) => c.apply(state),
|
||||||
|
Command::NewWaypoint(c) => c.apply(state),
|
||||||
|
Command::MoveWaypoint(c) => c.apply(state),
|
||||||
|
Command::Crop(c) => c.apply(state),
|
||||||
|
Command::Split(c) => c.apply(state),
|
||||||
|
Command::Time(c) => c.apply(state),
|
||||||
|
Command::Merge(c) => c.apply(state),
|
||||||
|
Command::Extract(c) => c.apply(state),
|
||||||
|
Command::Elevation(c) => c.apply(state),
|
||||||
|
Command::Clean(c) => c.apply(state),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
use crate::{Apply, CommandError, Edit, Editor, File, State, Track, update_selected};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Metadata<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub desc: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apply for Metadata<'_> {
|
||||||
|
fn apply(mut self, state: &mut State) -> Result<(), CommandError> {
|
||||||
|
update_selected(state, &mut self);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Editor for Metadata<'_> {
|
||||||
|
fn file(&mut self, file: &mut File) -> Edit {
|
||||||
|
file.info.name = self.name.to_owned();
|
||||||
|
file.info.desc = Some(self.desc.to_owned());
|
||||||
|
Edit::Changed
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track(&mut self, track: &mut Track) -> Edit {
|
||||||
|
track.info.name = Some(self.name.to_owned());
|
||||||
|
track.info.desc = Some(self.desc.to_owned());
|
||||||
|
Edit::Changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::{New, Selection, engine::command::fixture::Fixture};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_metadata_applies_to_selected_file() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
New { name: "first" }.apply(&mut fx.state()).unwrap();
|
||||||
|
New { name: "second" }.apply(&mut fx.state()).unwrap(); // now selected
|
||||||
|
let selected = *fx.selected_files().iter().next().unwrap();
|
||||||
|
|
||||||
|
Metadata {
|
||||||
|
name: "renamed",
|
||||||
|
desc: "description",
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for (id, file) in fx.files.iter() {
|
||||||
|
if *id == selected {
|
||||||
|
assert_eq!(file.info.name, "renamed");
|
||||||
|
assert_eq!(file.info.desc.as_deref(), Some("description"));
|
||||||
|
} else {
|
||||||
|
assert_eq!(file.info.name, "first");
|
||||||
|
assert_eq!(file.info.desc, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_metadata_applies_to_selected_track() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let data = std::fs::read("data/with_tracks.gpx").unwrap();
|
||||||
|
crate::Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||||
|
let file = fx.files.values().next().unwrap().clone();
|
||||||
|
fx.selection = Selection::Track {
|
||||||
|
file_id: file.id,
|
||||||
|
trk_ids: HashSet::from([file.trk[0].id]),
|
||||||
|
};
|
||||||
|
|
||||||
|
Metadata {
|
||||||
|
name: "renamed",
|
||||||
|
desc: "",
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let after = &fx.files[&file.id];
|
||||||
|
assert_eq!(after.info.name, file.info.name);
|
||||||
|
assert_eq!(after.trk[0].info.name.as_deref(), Some("renamed"));
|
||||||
|
assert_eq!(after.trk[1].info, file.trk[1].info);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
mod metadata;
|
||||||
|
mod new_track;
|
||||||
|
mod new_track_segment;
|
||||||
|
mod style;
|
||||||
|
|
||||||
|
pub use metadata::*;
|
||||||
|
pub use new_track::*;
|
||||||
|
pub use new_track_segment::*;
|
||||||
|
pub use style::*;
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
use crate::{Apply, CommandError, Edit, Editor, File, State, Track, update_selected};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NewTrack;
|
||||||
|
|
||||||
|
impl Apply for NewTrack {
|
||||||
|
fn apply(mut self, state: &mut State) -> Result<(), CommandError> {
|
||||||
|
update_selected(state, &mut self);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Editor for NewTrack {
|
||||||
|
fn file(&mut self, file: &mut File) -> Edit {
|
||||||
|
let mut track = Track::default();
|
||||||
|
track.info.color = common(file, |t| &t.info.color);
|
||||||
|
track.info.opacity = common(file, |t| &t.info.opacity);
|
||||||
|
track.info.width = common(file, |t| &t.info.width);
|
||||||
|
file.trk.push(track);
|
||||||
|
Edit::Changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the value of a style attribute if all existing tracks agree on it.
|
||||||
|
fn common<T: Clone + PartialEq>(file: &File, get: impl Fn(&Track) -> &Option<T>) -> Option<T> {
|
||||||
|
let mut tracks = file.trk.iter();
|
||||||
|
let first = get(tracks.next()?);
|
||||||
|
tracks.all(|t| get(t) == first).then(|| first.clone())?
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{New, engine::command::fixture::Fixture};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_track() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
New { name: "file" }.apply(&mut fx.state()).unwrap();
|
||||||
|
NewTrack.apply(&mut fx.state()).unwrap();
|
||||||
|
NewTrack.apply(&mut fx.state()).unwrap();
|
||||||
|
let file = fx.files.values().next().unwrap();
|
||||||
|
assert_eq!(file.trk.len(), 2);
|
||||||
|
assert_ne!(file.trk[0].id, file.trk[1].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_track_inherits_agreeing_style_only() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
New { name: "file" }.apply(&mut fx.state()).unwrap();
|
||||||
|
NewTrack.apply(&mut fx.state()).unwrap();
|
||||||
|
NewTrack.apply(&mut fx.state()).unwrap();
|
||||||
|
let file = std::rc::Rc::make_mut(fx.files.values_mut().next().unwrap());
|
||||||
|
for (i, trk) in file.trk.iter_mut().enumerate() {
|
||||||
|
trk.info.color = Some("ff0000".into());
|
||||||
|
trk.info.opacity = Some(0.5 + i as f64 * 0.1);
|
||||||
|
}
|
||||||
|
NewTrack.apply(&mut fx.state()).unwrap();
|
||||||
|
let info = &fx.files.values().next().unwrap().trk[2].info;
|
||||||
|
assert_eq!(info.color.as_deref(), Some("ff0000"));
|
||||||
|
assert_eq!(info.opacity, None);
|
||||||
|
assert_eq!(info.width, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
use crate::{Apply, CommandError, Edit, Editor, State, Track, TrackSegment, update_selected};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NewTrackSegment;
|
||||||
|
|
||||||
|
impl Apply for NewTrackSegment {
|
||||||
|
fn apply(mut self, state: &mut State) -> Result<(), CommandError> {
|
||||||
|
update_selected(state, &mut self);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Editor for NewTrackSegment {
|
||||||
|
fn track(&mut self, track: &mut Track) -> Edit {
|
||||||
|
track.trkseg.push(TrackSegment::default());
|
||||||
|
Edit::Changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{New, NewTrack, engine::command::fixture::Fixture};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_track_segment_on_each_track_of_selected_file() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
New { name: "file" }.apply(&mut fx.state()).unwrap();
|
||||||
|
NewTrack.apply(&mut fx.state()).unwrap();
|
||||||
|
NewTrack.apply(&mut fx.state()).unwrap();
|
||||||
|
NewTrackSegment.apply(&mut fx.state()).unwrap();
|
||||||
|
let file = fx.files.values().next().unwrap();
|
||||||
|
assert!(file.trk.iter().all(|t| t.trkseg.len() == 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
use crate::{Apply, CommandError, Edit, Editor, State, Track, update_selected};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Style<'a> {
|
||||||
|
pub color: Option<&'a str>,
|
||||||
|
pub opacity: Option<f64>,
|
||||||
|
pub width: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apply for Style<'_> {
|
||||||
|
fn apply(mut self, state: &mut State) -> Result<(), CommandError> {
|
||||||
|
update_selected(state, &mut self);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Editor for Style<'_> {
|
||||||
|
fn track(&mut self, track: &mut Track) -> Edit {
|
||||||
|
if let Some(color) = self.color {
|
||||||
|
track.info.color = Some(color.to_owned());
|
||||||
|
}
|
||||||
|
if self.opacity.is_some() {
|
||||||
|
track.info.opacity = self.opacity;
|
||||||
|
}
|
||||||
|
if self.width.is_some() {
|
||||||
|
track.info.width = self.width;
|
||||||
|
}
|
||||||
|
Edit::Changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::{Selection, engine::command::fixture::Fixture};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_style_applies_to_all_tracks_of_selected_file_and_keeps_unset_fields() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let data = std::fs::read("data/with_style.gpx").unwrap();
|
||||||
|
crate::Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||||
|
let id = *fx.files.keys().next().unwrap();
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
assert!(!before.trk.is_empty());
|
||||||
|
fx.selection = Selection::File {
|
||||||
|
file_ids: HashSet::from([id]),
|
||||||
|
};
|
||||||
|
|
||||||
|
Style {
|
||||||
|
color: Some("ff0000"),
|
||||||
|
opacity: None,
|
||||||
|
width: Some(7.0),
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for (b, a) in before.trk.iter().zip(fx.files[&id].trk.iter()) {
|
||||||
|
assert_eq!(a.info.color.as_deref(), Some("ff0000"));
|
||||||
|
assert_eq!(a.info.width, Some(7.0));
|
||||||
|
assert_eq!(a.info.opacity, b.info.opacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Apply, CommandError, FileId, Selection, StackEntry, State, Waypoint, edit_waypoint_chunks,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Delete;
|
||||||
|
|
||||||
|
impl Apply for Delete {
|
||||||
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||||
|
let next = match &*state.selection {
|
||||||
|
Selection::File { file_ids } => {
|
||||||
|
state.files.retain(|id, _| !file_ids.contains(id));
|
||||||
|
state.order.0.retain(|id| !file_ids.contains(id));
|
||||||
|
Selection::Empty
|
||||||
|
}
|
||||||
|
Selection::Track { file_id, trk_ids } => {
|
||||||
|
let file = state
|
||||||
|
.files
|
||||||
|
.get_mut(file_id)
|
||||||
|
.ok_or(CommandError::NothingToDo)?;
|
||||||
|
let file = Rc::make_mut(file);
|
||||||
|
let len = file.trk.len();
|
||||||
|
file.trk.retain(|trk| !trk_ids.contains(&trk.id));
|
||||||
|
if file.trk.len() == len {
|
||||||
|
return Err(CommandError::NothingToDo);
|
||||||
|
}
|
||||||
|
Selection::File {
|
||||||
|
file_ids: [*file_id].into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selection::TrackSegment {
|
||||||
|
file_id,
|
||||||
|
trk_id,
|
||||||
|
trkseg_ids,
|
||||||
|
} => {
|
||||||
|
let file = state
|
||||||
|
.files
|
||||||
|
.get_mut(file_id)
|
||||||
|
.ok_or(CommandError::NothingToDo)?;
|
||||||
|
let file = Rc::make_mut(file);
|
||||||
|
let trk = file
|
||||||
|
.trk
|
||||||
|
.iter_mut()
|
||||||
|
.find(|trk| trk.id == *trk_id)
|
||||||
|
.ok_or(CommandError::NothingToDo)?;
|
||||||
|
let len = trk.trkseg.len();
|
||||||
|
trk.trkseg.retain(|seg| !trkseg_ids.contains(&seg.id));
|
||||||
|
if trk.trkseg.len() == len {
|
||||||
|
return Err(CommandError::NothingToDo);
|
||||||
|
}
|
||||||
|
Selection::Track {
|
||||||
|
file_id: *file_id,
|
||||||
|
trk_ids: [*trk_id].into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selection::Waypoints { file_id } => delete_waypoints(state.files, *file_id, |_| true)?,
|
||||||
|
Selection::Waypoint { file_id, wpt_ids } => {
|
||||||
|
delete_waypoints(state.files, *file_id, |wpt| wpt_ids.contains(&wpt.id))?
|
||||||
|
}
|
||||||
|
Selection::Empty => return Err(CommandError::NothingToDo),
|
||||||
|
};
|
||||||
|
*state.selection = next;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_waypoints(
|
||||||
|
files: &mut StackEntry,
|
||||||
|
file_id: FileId,
|
||||||
|
filter: impl Fn(&Waypoint) -> bool,
|
||||||
|
) -> Result<Selection, CommandError> {
|
||||||
|
let file = files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||||
|
let mut file = (**file).clone();
|
||||||
|
let changed = edit_waypoint_chunks(&mut file, &filter, |wpts| {
|
||||||
|
wpts.retain(|wpt| !filter(wpt));
|
||||||
|
true
|
||||||
|
});
|
||||||
|
if !changed {
|
||||||
|
return Err(CommandError::NothingToDo);
|
||||||
|
}
|
||||||
|
files.insert(file_id, Rc::new(file));
|
||||||
|
Ok(Selection::File {
|
||||||
|
file_ids: [file_id].into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::engine::command::fixture::Fixture;
|
||||||
|
use crate::{FileId, Load};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn loaded() -> (Fixture, FileId) {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||||
|
Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||||
|
let id = fx.order.0[0];
|
||||||
|
(fx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_nothing_selected() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
assert_eq!(
|
||||||
|
Delete.apply(&mut fx.state()),
|
||||||
|
Err(CommandError::NothingToDo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_selected_files() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
crate::New { name: "keep" }.apply(&mut fx.state()).unwrap();
|
||||||
|
let keep = fx.order.0[1];
|
||||||
|
fx.selection = Selection::File {
|
||||||
|
file_ids: HashSet::from([id]),
|
||||||
|
};
|
||||||
|
Delete.apply(&mut fx.state()).unwrap();
|
||||||
|
assert_eq!(fx.order.0, vec![keep]);
|
||||||
|
assert!(fx.files.contains_key(&keep) && !fx.files.contains_key(&id));
|
||||||
|
assert!(matches!(fx.selection, Selection::Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_selected_tracks() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let before = fx.files[&id].trk.len();
|
||||||
|
let trk_id = fx.files[&id].trk[0].id;
|
||||||
|
fx.selection = Selection::Track {
|
||||||
|
file_id: id,
|
||||||
|
trk_ids: HashSet::from([trk_id]),
|
||||||
|
};
|
||||||
|
Delete.apply(&mut fx.state()).unwrap();
|
||||||
|
let file = &fx.files[&id];
|
||||||
|
assert_eq!(file.trk.len(), before - 1);
|
||||||
|
assert!(file.trk.iter().all(|t| t.id != trk_id));
|
||||||
|
assert_eq!(fx.selected_files(), HashSet::from([id]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_selected_segments() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let trk = &fx.files[&id].trk[0];
|
||||||
|
let (trk_id, seg_id, before) = (trk.id, trk.trkseg[0].id, trk.trkseg.len());
|
||||||
|
fx.selection = Selection::TrackSegment {
|
||||||
|
file_id: id,
|
||||||
|
trk_id,
|
||||||
|
trkseg_ids: HashSet::from([seg_id]),
|
||||||
|
};
|
||||||
|
Delete.apply(&mut fx.state()).unwrap();
|
||||||
|
let trk = &fx.files[&id].trk[0];
|
||||||
|
assert_eq!(trk.trkseg.len(), before - 1);
|
||||||
|
assert!(trk.trkseg.iter().all(|s| s.id != seg_id));
|
||||||
|
assert!(
|
||||||
|
matches!(&fx.selection, Selection::Track { trk_ids, .. } if trk_ids.contains(&trk_id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_waypoints(fx: &mut Fixture, id: FileId, n: usize) -> Vec<crate::WaypointId> {
|
||||||
|
let wpts: Vec<_> = (0..n).map(|_| Waypoint::default()).collect();
|
||||||
|
let ids = wpts.iter().map(|w| w.id).collect();
|
||||||
|
let mut file = (*fx.files[&id]).clone();
|
||||||
|
file.wpt = vec![Rc::new(crate::WaypointChunk {
|
||||||
|
wpt: wpts,
|
||||||
|
..Default::default()
|
||||||
|
})];
|
||||||
|
fx.files.insert(id, Rc::new(file));
|
||||||
|
ids
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_selected_waypoints() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let ids = with_waypoints(&mut fx, id, 3);
|
||||||
|
fx.selection = Selection::Waypoint {
|
||||||
|
file_id: id,
|
||||||
|
wpt_ids: HashSet::from([ids[1]]),
|
||||||
|
};
|
||||||
|
let rev = fx.files[&id].wpt_rev_id;
|
||||||
|
Delete.apply(&mut fx.state()).unwrap();
|
||||||
|
assert_ne!(fx.files[&id].wpt_rev_id, rev);
|
||||||
|
let left: Vec<_> = fx.files[&id]
|
||||||
|
.wpt
|
||||||
|
.iter()
|
||||||
|
.flat_map(|c| c.wpt.iter().map(|w| w.id))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(left, vec![ids[0], ids[2]]);
|
||||||
|
assert_eq!(fx.selected_files(), HashSet::from([id]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_all_waypoints_of_file() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
with_waypoints(&mut fx, id, 3);
|
||||||
|
fx.selection = Selection::Waypoints { file_id: id };
|
||||||
|
Delete.apply(&mut fx.state()).unwrap();
|
||||||
|
assert!(fx.files[&id].wpt.is_empty());
|
||||||
|
fx.selection = Selection::Waypoints { file_id: id };
|
||||||
|
assert_eq!(
|
||||||
|
Delete.apply(&mut fx.state()),
|
||||||
|
Err(CommandError::NothingToDo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_unknown_ids_is_nothing_to_do() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
fx.selection = Selection::Track {
|
||||||
|
file_id: id,
|
||||||
|
trk_ids: HashSet::from([Default::default()]),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
Delete.apply(&mut fx.state()),
|
||||||
|
Err(CommandError::NothingToDo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
use crate::{Apply, CommandError, Selection, State};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DeleteAll;
|
||||||
|
|
||||||
|
impl Apply for DeleteAll {
|
||||||
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||||
|
if state.files.is_empty() {
|
||||||
|
return Err(CommandError::NothingToDo);
|
||||||
|
}
|
||||||
|
state.files.clear();
|
||||||
|
state.order.0.clear();
|
||||||
|
*state.selection = Selection::Empty;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::engine::command::fixture::Fixture;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_all() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
assert_eq!(
|
||||||
|
DeleteAll.apply(&mut fx.state()),
|
||||||
|
Err(CommandError::NothingToDo)
|
||||||
|
);
|
||||||
|
crate::New { name: "a" }.apply(&mut fx.state()).unwrap();
|
||||||
|
crate::New { name: "b" }.apply(&mut fx.state()).unwrap();
|
||||||
|
assert!(DeleteAll.apply(&mut fx.state()).is_ok());
|
||||||
|
assert!(fx.files.is_empty());
|
||||||
|
assert!(fx.order.0.is_empty());
|
||||||
|
assert!(matches!(fx.selection, Selection::Empty));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
use std::{collections::HashSet, rc::Rc};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Apply, CommandError, File, FileId, Selection, StackEntry, State, Track, TrackSegment, Waypoint,
|
||||||
|
WaypointChunk,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Duplicate;
|
||||||
|
|
||||||
|
impl Apply for Duplicate {
|
||||||
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||||
|
let next = match &*state.selection {
|
||||||
|
Selection::File { file_ids } => {
|
||||||
|
let mut copies = HashSet::new();
|
||||||
|
let mut order = Vec::new();
|
||||||
|
for id in state.order.0.iter().filter(|id| file_ids.contains(id)) {
|
||||||
|
let Some(file) = state.files.get(id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let copy = copy_file(file);
|
||||||
|
order.push(copy.id);
|
||||||
|
copies.insert(copy.id);
|
||||||
|
state.files.insert(copy.id, Rc::new(copy));
|
||||||
|
}
|
||||||
|
if copies.is_empty() {
|
||||||
|
return Err(CommandError::NothingToDo);
|
||||||
|
}
|
||||||
|
let at = state
|
||||||
|
.order
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.rposition(|id| file_ids.contains(id))
|
||||||
|
.map_or(state.order.0.len(), |i| i + 1);
|
||||||
|
state.order.0.splice(at..at, order);
|
||||||
|
Selection::File { file_ids: copies }
|
||||||
|
}
|
||||||
|
Selection::Track { file_id, trk_ids } => {
|
||||||
|
let file = state
|
||||||
|
.files
|
||||||
|
.get_mut(file_id)
|
||||||
|
.ok_or(CommandError::NothingToDo)?;
|
||||||
|
let file = Rc::make_mut(file);
|
||||||
|
let mut copies = HashSet::new();
|
||||||
|
append_copies(
|
||||||
|
&mut file.trk,
|
||||||
|
|trk| trk_ids.contains(&trk.id),
|
||||||
|
|trk| {
|
||||||
|
let copy = copy_track(trk);
|
||||||
|
copies.insert(copy.id);
|
||||||
|
copy
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if copies.is_empty() {
|
||||||
|
return Err(CommandError::NothingToDo);
|
||||||
|
}
|
||||||
|
Selection::Track {
|
||||||
|
file_id: *file_id,
|
||||||
|
trk_ids: copies,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selection::TrackSegment {
|
||||||
|
file_id,
|
||||||
|
trk_id,
|
||||||
|
trkseg_ids,
|
||||||
|
} => {
|
||||||
|
let file = state
|
||||||
|
.files
|
||||||
|
.get_mut(file_id)
|
||||||
|
.ok_or(CommandError::NothingToDo)?;
|
||||||
|
let file = Rc::make_mut(file);
|
||||||
|
let trk = file
|
||||||
|
.trk
|
||||||
|
.iter_mut()
|
||||||
|
.find(|trk| trk.id == *trk_id)
|
||||||
|
.ok_or(CommandError::NothingToDo)?;
|
||||||
|
let mut copies = HashSet::new();
|
||||||
|
append_copies(
|
||||||
|
&mut trk.trkseg,
|
||||||
|
|seg| trkseg_ids.contains(&seg.id),
|
||||||
|
|seg| {
|
||||||
|
let copy = copy_segment(seg);
|
||||||
|
copies.insert(copy.id);
|
||||||
|
copy
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if copies.is_empty() {
|
||||||
|
return Err(CommandError::NothingToDo);
|
||||||
|
}
|
||||||
|
Selection::TrackSegment {
|
||||||
|
file_id: *file_id,
|
||||||
|
trk_id: *trk_id,
|
||||||
|
trkseg_ids: copies,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selection::Waypoints { file_id } => {
|
||||||
|
duplicate_waypoints(state.files, *file_id, |_| true)?
|
||||||
|
}
|
||||||
|
Selection::Waypoint { file_id, wpt_ids } => {
|
||||||
|
duplicate_waypoints(state.files, *file_id, |wpt| wpt_ids.contains(&wpt.id))?
|
||||||
|
}
|
||||||
|
Selection::Empty => return Err(CommandError::NothingToDo),
|
||||||
|
};
|
||||||
|
*state.selection = next;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn duplicate_waypoints(
|
||||||
|
files: &mut StackEntry,
|
||||||
|
file_id: FileId,
|
||||||
|
filter: impl Fn(&Waypoint) -> bool,
|
||||||
|
) -> Result<Selection, CommandError> {
|
||||||
|
let file = files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||||
|
let mut file = (**file).clone();
|
||||||
|
let last_chunk = file
|
||||||
|
.wpt
|
||||||
|
.iter()
|
||||||
|
.rposition(|chunk| chunk.wpt.iter().any(&filter))
|
||||||
|
.ok_or(CommandError::NothingToDo)?;
|
||||||
|
|
||||||
|
let mut copies = HashSet::new();
|
||||||
|
let mut inserted = Vec::new();
|
||||||
|
let mut chunk = WaypointChunk::default();
|
||||||
|
for wpt in file
|
||||||
|
.wpt
|
||||||
|
.iter()
|
||||||
|
.flat_map(|chunk| &chunk.wpt)
|
||||||
|
.filter(|wpt| filter(wpt))
|
||||||
|
{
|
||||||
|
let mut copy = wpt.clone();
|
||||||
|
copy.id = Default::default();
|
||||||
|
copies.insert(copy.id);
|
||||||
|
chunk.wpt.push(copy);
|
||||||
|
if chunk.is_full() {
|
||||||
|
inserted.push(Rc::new(std::mem::take(&mut chunk)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !chunk.wpt.is_empty() {
|
||||||
|
inserted.push(Rc::new(chunk));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The copies go right after the last selected waypoint: only the chunk holding it is cut
|
||||||
|
// (when waypoints follow it), all the other chunks are kept as they are.
|
||||||
|
let cut = &file.wpt[last_chunk];
|
||||||
|
let split = cut.wpt.iter().rposition(&filter).unwrap() + 1;
|
||||||
|
let replacement = if split == cut.wpt.len() {
|
||||||
|
let mut chunks = vec![cut.clone()];
|
||||||
|
chunks.extend(inserted);
|
||||||
|
chunks
|
||||||
|
} else {
|
||||||
|
let part = |wpt: &[Waypoint]| {
|
||||||
|
Rc::new(WaypointChunk {
|
||||||
|
wpt: wpt.to_vec(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let mut chunks = vec![part(&cut.wpt[..split])];
|
||||||
|
chunks.extend(inserted);
|
||||||
|
chunks.push(part(&cut.wpt[split..]));
|
||||||
|
chunks
|
||||||
|
};
|
||||||
|
file.wpt.splice(last_chunk..=last_chunk, replacement);
|
||||||
|
file.wpt_rev_id = Default::default();
|
||||||
|
files.insert(file_id, Rc::new(file));
|
||||||
|
Ok(Selection::Waypoint {
|
||||||
|
file_id,
|
||||||
|
wpt_ids: copies,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts a copy of each item matching `filter`, as a block after the last matching item.
|
||||||
|
fn append_copies<T>(items: &mut Vec<T>, filter: impl Fn(&T) -> bool, copy: impl FnMut(&T) -> T) {
|
||||||
|
let Some(last) = items.iter().rposition(&filter) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let copies: Vec<T> = items.iter().filter(|item| filter(item)).map(copy).collect();
|
||||||
|
items.splice(last + 1..last + 1, copies);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track points are shared chunks, so copies are cheap.
|
||||||
|
fn copy_segment(segment: &TrackSegment) -> TrackSegment {
|
||||||
|
let mut copy = segment.clone();
|
||||||
|
copy.id = Default::default();
|
||||||
|
copy
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_track(track: &Track) -> Track {
|
||||||
|
Track {
|
||||||
|
id: Default::default(),
|
||||||
|
trkseg: track.trkseg.iter().map(copy_segment).collect(),
|
||||||
|
..track.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_file(file: &File) -> File {
|
||||||
|
File {
|
||||||
|
id: Default::default(),
|
||||||
|
trk: file.trk.iter().map(copy_track).collect(),
|
||||||
|
..file.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::engine::command::fixture::Fixture;
|
||||||
|
use crate::{FileId, Load};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn loaded() -> (Fixture, FileId) {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||||
|
Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||||
|
let id = fx.order.0[0];
|
||||||
|
(fx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duplicate_nothing_selected() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
assert_eq!(
|
||||||
|
Duplicate.apply(&mut fx.state()),
|
||||||
|
Err(CommandError::NothingToDo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duplicate_file() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
Duplicate.apply(&mut fx.state()).unwrap();
|
||||||
|
assert_eq!(fx.files.len(), 2);
|
||||||
|
let copy_id = fx.order.0[1];
|
||||||
|
assert_eq!(fx.selected_files(), [copy_id].into());
|
||||||
|
let (orig, copy) = (&fx.files[&id], &fx.files[©_id]);
|
||||||
|
assert_ne!(copy_id, id);
|
||||||
|
assert_eq!(copy.info.name, orig.info.name);
|
||||||
|
assert_eq!(copy.trk.len(), orig.trk.len());
|
||||||
|
for (a, b) in orig.trk.iter().zip(©.trk) {
|
||||||
|
assert_ne!(a.id, b.id);
|
||||||
|
assert_eq!(a.info, b.info);
|
||||||
|
for (a, b) in a.trkseg.iter().zip(&b.trkseg) {
|
||||||
|
assert_ne!(a.id, b.id);
|
||||||
|
assert_eq!(a.len(), b.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duplicate_files_go_after_last_selected() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
for name in ["a", "b", "c"] {
|
||||||
|
crate::New { name }.apply(&mut fx.state()).unwrap();
|
||||||
|
}
|
||||||
|
let [a, b, c] = [fx.order.0[0], fx.order.0[1], fx.order.0[2]];
|
||||||
|
fx.selection = Selection::File {
|
||||||
|
file_ids: [a, b].into(),
|
||||||
|
};
|
||||||
|
Duplicate.apply(&mut fx.state()).unwrap();
|
||||||
|
assert_eq!(fx.order.0.len(), 5);
|
||||||
|
assert_eq!(fx.order.0[..2], [a, b]);
|
||||||
|
assert_eq!(fx.order.0[4], c);
|
||||||
|
assert_eq!(
|
||||||
|
fx.selected_files(),
|
||||||
|
fx.order.0[2..4].iter().copied().collect()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duplicate_track_appends_copy() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let before = fx.files[&id].trk.len();
|
||||||
|
let (trk_id, second_id) = (fx.files[&id].trk[0].id, fx.files[&id].trk[1].id);
|
||||||
|
fx.selection = Selection::Track {
|
||||||
|
file_id: id,
|
||||||
|
trk_ids: [trk_id].into(),
|
||||||
|
};
|
||||||
|
Duplicate.apply(&mut fx.state()).unwrap();
|
||||||
|
let file = &fx.files[&id];
|
||||||
|
assert_eq!(file.trk.len(), before + 1);
|
||||||
|
assert_eq!(file.trk[0].id, trk_id);
|
||||||
|
assert_ne!(file.trk[1].id, trk_id);
|
||||||
|
// copies go right after the selected track, not at the end
|
||||||
|
assert_eq!(file.trk[2].id, second_id);
|
||||||
|
assert!(
|
||||||
|
matches!(&fx.selection, Selection::Track { trk_ids, .. } if trk_ids.contains(&file.trk[1].id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duplicate_selected_waypoints() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let wpts: Vec<_> = (0..3).map(|_| Waypoint::default()).collect();
|
||||||
|
let ids: Vec<_> = wpts.iter().map(|w| w.id).collect();
|
||||||
|
let mut file = (*fx.files[&id]).clone();
|
||||||
|
file.wpt = vec![Rc::new(crate::WaypointChunk {
|
||||||
|
wpt: wpts,
|
||||||
|
..Default::default()
|
||||||
|
})];
|
||||||
|
fx.files.insert(id, Rc::new(file));
|
||||||
|
let original_chunk = fx.files[&id].wpt[0].clone();
|
||||||
|
|
||||||
|
fx.selection = Selection::Waypoint {
|
||||||
|
file_id: id,
|
||||||
|
wpt_ids: HashSet::from([ids[0]]),
|
||||||
|
};
|
||||||
|
let rev = fx.files[&id].wpt_rev_id;
|
||||||
|
Duplicate.apply(&mut fx.state()).unwrap();
|
||||||
|
assert_ne!(fx.files[&id].wpt_rev_id, rev);
|
||||||
|
let all: Vec<_> = fx.files[&id]
|
||||||
|
.wpt
|
||||||
|
.iter()
|
||||||
|
.flat_map(|c| c.wpt.iter().map(|w| w.id))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(all.len(), 4);
|
||||||
|
assert!(
|
||||||
|
matches!(&fx.selection, Selection::Waypoint { wpt_ids, .. } if wpt_ids == &HashSet::from([all[1]]))
|
||||||
|
);
|
||||||
|
// the chunk is cut after the selected waypoint: [w0] [copy] [w1 w2]
|
||||||
|
assert_eq!(all[..], [ids[0], all[1], ids[1], ids[2]]);
|
||||||
|
assert_eq!(fx.files[&id].wpt.len(), 3);
|
||||||
|
assert_ne!(fx.files[&id].wpt[0].id, original_chunk.id);
|
||||||
|
|
||||||
|
// selecting the last waypoint of a chunk does not cut it
|
||||||
|
let last = fx.files[&id].wpt.last().unwrap().wpt.last().unwrap().id;
|
||||||
|
let chunks = fx.files[&id].wpt.clone();
|
||||||
|
fx.selection = Selection::Waypoint {
|
||||||
|
file_id: id,
|
||||||
|
wpt_ids: HashSet::from([last]),
|
||||||
|
};
|
||||||
|
Duplicate.apply(&mut fx.state()).unwrap();
|
||||||
|
let file = &fx.files[&id];
|
||||||
|
assert_eq!(file.wpt.len(), 4);
|
||||||
|
assert!(
|
||||||
|
file.wpt[..3]
|
||||||
|
.iter()
|
||||||
|
.zip(&chunks)
|
||||||
|
.all(|(a, b)| Rc::ptr_eq(a, b))
|
||||||
|
);
|
||||||
|
|
||||||
|
fx.selection = Selection::Waypoints { file_id: id };
|
||||||
|
Duplicate.apply(&mut fx.state()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
fx.files[&id].wpt.iter().map(|c| c.wpt.len()).sum::<usize>(),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duplicate_segment_appends_copy() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let trk = &fx.files[&id].trk[0];
|
||||||
|
let (trk_id, seg_id, before) = (trk.id, trk.trkseg[0].id, trk.trkseg.len());
|
||||||
|
fx.selection = Selection::TrackSegment {
|
||||||
|
file_id: id,
|
||||||
|
trk_id,
|
||||||
|
trkseg_ids: [seg_id].into(),
|
||||||
|
};
|
||||||
|
Duplicate.apply(&mut fx.state()).unwrap();
|
||||||
|
let trk = &fx.files[&id].trk[0];
|
||||||
|
assert_eq!(trk.trkseg.len(), before + 1);
|
||||||
|
assert_eq!(trk.trkseg[0].id, seg_id);
|
||||||
|
assert_ne!(trk.trkseg[1].id, seg_id);
|
||||||
|
assert_eq!(trk.trkseg[0].len(), trk.trkseg[1].len());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
use crate::{Apply, CommandError, State, parse, produce};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Load<'a> {
|
||||||
|
pub data: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apply for Load<'_> {
|
||||||
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||||
|
let file = parse(self.data).map_err(|err| CommandError::InvalidData(err.to_string()))?;
|
||||||
|
produce(state, |_| vec![file]);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::engine::command::fixture::Fixture;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let data = std::fs::read("data/with_tracks.gpx").unwrap();
|
||||||
|
assert!(Load { data: &data }.apply(&mut fx.state()).is_ok());
|
||||||
|
assert_eq!(fx.files.len(), 1);
|
||||||
|
let file = fx.files.values().next().unwrap();
|
||||||
|
assert!(!file.trk.is_empty());
|
||||||
|
assert_eq!(fx.selected_files(), [file.id].into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_invalid_file_changes_nothing() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
crate::New { name: "keep" }.apply(&mut fx.state()).unwrap();
|
||||||
|
let selected = fx.selected_files();
|
||||||
|
|
||||||
|
let result = Load {
|
||||||
|
data: b"<gpx><trk></gpx>",
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state());
|
||||||
|
assert!(matches!(result, Err(CommandError::InvalidData(_))));
|
||||||
|
assert_eq!(fx.files.len(), 1);
|
||||||
|
assert_eq!(fx.order.0.len(), 1);
|
||||||
|
assert_eq!(fx.selected_files(), selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
mod delete;
|
||||||
|
mod delete_all;
|
||||||
|
mod duplicate;
|
||||||
|
mod load;
|
||||||
|
mod new;
|
||||||
|
|
||||||
|
pub use delete::*;
|
||||||
|
pub use delete_all::*;
|
||||||
|
pub use duplicate::*;
|
||||||
|
pub use load::*;
|
||||||
|
pub use new::*;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
use crate::{Apply, CommandError, File, State, produce};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct New<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apply for New<'_> {
|
||||||
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||||
|
produce(state, |_| {
|
||||||
|
let mut file = File::default();
|
||||||
|
file.info.name = self.name.to_owned();
|
||||||
|
vec![file]
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::engine::command::fixture::Fixture;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
assert!(New { name: "new" }.apply(&mut fx.state()).is_ok());
|
||||||
|
assert_eq!(fx.files.len(), 1);
|
||||||
|
let file = fx.files.values().next().unwrap();
|
||||||
|
assert_eq!(file.info.name, "new");
|
||||||
|
assert!(file.trk.is_empty());
|
||||||
|
assert_eq!(fx.selected_files(), [file.id].into());
|
||||||
|
assert_eq!(fx.order.0, vec![file.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//! Shared helpers for the tests of the commands.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::{FileId, FileOrder, Selection, StackEntry, State};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Fixture {
|
||||||
|
pub files: StackEntry,
|
||||||
|
pub selection: Selection,
|
||||||
|
pub order: FileOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fixture {
|
||||||
|
pub fn state(&mut self) -> State<'_> {
|
||||||
|
State {
|
||||||
|
files: &mut self.files,
|
||||||
|
selection: &mut self.selection,
|
||||||
|
order: &mut self.order,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ids of the selected files, empty unless the selection is a file selection.
|
||||||
|
pub fn selected_files(&self) -> HashSet<FileId> {
|
||||||
|
match &self.selection {
|
||||||
|
Selection::File { file_ids } => file_ids.clone(),
|
||||||
|
_ => HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
mod apply;
|
||||||
|
mod command;
|
||||||
|
mod edit;
|
||||||
|
mod file;
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod fixture;
|
||||||
|
mod pattern;
|
||||||
|
mod tools;
|
||||||
|
|
||||||
|
pub use apply::*;
|
||||||
|
pub use command::*;
|
||||||
|
pub use edit::*;
|
||||||
|
pub use file::*;
|
||||||
|
pub use pattern::*;
|
||||||
|
pub use tools::*;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::{File, Waypoint, WaypointChunk};
|
||||||
|
|
||||||
|
/// Edits the waypoints of the chunks containing a waypoint accepted by `filter`.
|
||||||
|
///
|
||||||
|
/// `f` receives a copy of the chunk's waypoints and returns whether it changed them. A
|
||||||
|
/// changed chunk is replaced by a new one (with a new id, so that derived data is recomputed)
|
||||||
|
/// and chunks left empty are dropped. Returns whether anything changed.
|
||||||
|
pub fn edit_waypoint_chunks(
|
||||||
|
file: &mut File,
|
||||||
|
filter: impl Fn(&Waypoint) -> bool,
|
||||||
|
mut f: impl FnMut(&mut Vec<Waypoint>) -> bool,
|
||||||
|
) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
|
let mut chunks = Vec::with_capacity(file.wpt.len());
|
||||||
|
for chunk in file.wpt.drain(..) {
|
||||||
|
if !chunk.wpt.iter().any(&filter) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut wpt = chunk.wpt.clone();
|
||||||
|
if !f(&mut wpt) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
if !wpt.is_empty() {
|
||||||
|
chunks.push(Rc::new(WaypointChunk {
|
||||||
|
wpt,
|
||||||
|
..Default::default()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file.wpt = chunks;
|
||||||
|
if changed {
|
||||||
|
file.wpt_rev_id = Default::default();
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
mod edit_waypoint_chunks;
|
||||||
|
mod produce;
|
||||||
|
mod update_selected;
|
||||||
|
|
||||||
|
pub use edit_waypoint_chunks::*;
|
||||||
|
pub use produce::*;
|
||||||
|
pub use update_selected::*;
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::{File, Selection, State};
|
||||||
|
|
||||||
|
/// Pure producer: adds the files returned by `f` to the state and selects the first one.
|
||||||
|
pub fn produce<F>(state: &mut State, f: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(&State) -> Vec<File>,
|
||||||
|
{
|
||||||
|
*state.selection = Selection::Empty;
|
||||||
|
for file in f(state) {
|
||||||
|
let file_id = file.id;
|
||||||
|
state.files.insert(file_id, Rc::new(file));
|
||||||
|
state.order.0.push(file_id);
|
||||||
|
if matches!(*state.selection, Selection::Empty) {
|
||||||
|
*state.selection = Selection::File {
|
||||||
|
file_ids: [file_id].into(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::engine::command::fixture::Fixture;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn named(name: &str) -> File {
|
||||||
|
let mut file = File::default();
|
||||||
|
file.info.name = name.to_string();
|
||||||
|
file
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_produce_adds_orders_and_selects_first() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let (a, b) = (named("a"), named("b"));
|
||||||
|
let (id_a, id_b) = (a.id, b.id);
|
||||||
|
produce(&mut fx.state(), |_| vec![a, b]);
|
||||||
|
|
||||||
|
assert_eq!(fx.files.len(), 2);
|
||||||
|
assert_eq!(fx.order.0, vec![id_a, id_b]);
|
||||||
|
assert_eq!(fx.selected_files(), HashSet::from([id_a]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_produce_replaces_selection_and_keeps_order() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let a = named("a");
|
||||||
|
let id_a = a.id;
|
||||||
|
produce(&mut fx.state(), |_| vec![a]);
|
||||||
|
let b = named("b");
|
||||||
|
let id_b = b.id;
|
||||||
|
produce(&mut fx.state(), |_| vec![b]);
|
||||||
|
|
||||||
|
assert_eq!(fx.order.0, vec![id_a, id_b]);
|
||||||
|
assert_eq!(fx.selected_files(), HashSet::from([id_b]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_produce_nothing_clears_selection() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
produce(&mut fx.state(), |_| vec![named("a")]);
|
||||||
|
produce(&mut fx.state(), |_| vec![]);
|
||||||
|
assert!(matches!(fx.selection, Selection::Empty));
|
||||||
|
assert_eq!(fx.files.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_produce_sees_current_state() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
produce(&mut fx.state(), |_| vec![named("a")]);
|
||||||
|
produce(&mut fx.state(), |state| {
|
||||||
|
assert_eq!(state.files.len(), 1);
|
||||||
|
vec![]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
File, FileId, Selection, StackEntry, State, Track, TrackSegment, Waypoint, edit_waypoint_chunks,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// What an [`Editor`] hook did to the element it was given.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Edit {
|
||||||
|
Unchanged,
|
||||||
|
Changed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Edit {
|
||||||
|
fn merge(self, other: Edit) -> Edit {
|
||||||
|
if self == Edit::Changed || other == Edit::Changed {
|
||||||
|
Edit::Changed
|
||||||
|
} else {
|
||||||
|
Edit::Unchanged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Independent edit of each selected element, without needing the rest of the state.
|
||||||
|
///
|
||||||
|
/// A selected file, track or segment is passed to the hook of its own level. By default a
|
||||||
|
/// file is forwarded to its tracks and a track to its segments, so an editor only overrides
|
||||||
|
/// the level(s) it cares about (metadata: file and track, style: track, reverse: segment).
|
||||||
|
/// Waypoints are only edited when selected, never through their file.
|
||||||
|
pub trait Editor {
|
||||||
|
fn file(&mut self, file: &mut File) -> Edit {
|
||||||
|
edit_each(&mut file.trk, |trk| self.track(trk))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track(&mut self, track: &mut Track) -> Edit {
|
||||||
|
edit_each(&mut track.trkseg, |seg| edit_segment(self, seg))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn segment(&mut self, _segment: &mut TrackSegment) -> Edit {
|
||||||
|
Edit::Unchanged
|
||||||
|
}
|
||||||
|
|
||||||
|
fn waypoint(&mut self, _waypoint: &mut Waypoint) -> Edit {
|
||||||
|
Edit::Unchanged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calls the segment hook and gives the segment a new revision id if it changed, so that
|
||||||
|
/// derived data (statistics) is recomputed.
|
||||||
|
fn edit_segment<E: Editor + ?Sized>(editor: &mut E, segment: &mut TrackSegment) -> Edit {
|
||||||
|
let edit = editor.segment(segment);
|
||||||
|
if edit == Edit::Changed {
|
||||||
|
segment.rev_id = Default::default();
|
||||||
|
}
|
||||||
|
edit
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edit_each<T>(items: &mut [T], f: impl FnMut(&mut T) -> Edit) -> Edit {
|
||||||
|
edit_where(items, |_| true, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edit_where<T>(
|
||||||
|
items: &mut [T],
|
||||||
|
filter: impl Fn(&T) -> bool,
|
||||||
|
mut f: impl FnMut(&mut T) -> Edit,
|
||||||
|
) -> Edit {
|
||||||
|
items
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|item| filter(item))
|
||||||
|
.fold(Edit::Unchanged, |acc, item| acc.merge(f(item)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Edits a copy of the file (cheap, track points are shared chunks) and stores it back as a
|
||||||
|
/// new snapshot only if something changed.
|
||||||
|
fn update_file(files: &mut StackEntry, id: FileId, f: impl FnOnce(&mut File) -> Edit) {
|
||||||
|
let Some(current) = files.get(&id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut file = (**current).clone();
|
||||||
|
if f(&mut file) == Edit::Changed {
|
||||||
|
files.insert(id, Rc::new(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edit_waypoints<E: Editor + ?Sized>(
|
||||||
|
file: &mut File,
|
||||||
|
filter: impl Fn(&Waypoint) -> bool,
|
||||||
|
editor: &mut E,
|
||||||
|
) -> Edit {
|
||||||
|
let changed = edit_waypoint_chunks(file, &filter, |wpts| {
|
||||||
|
edit_where(wpts, &filter, |wpt| editor.waypoint(wpt)) == Edit::Changed
|
||||||
|
});
|
||||||
|
if changed {
|
||||||
|
Edit::Changed
|
||||||
|
} else {
|
||||||
|
Edit::Unchanged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies the editor to every selected file, track or segment.
|
||||||
|
pub fn update_selected<E: Editor>(state: &mut State, editor: &mut E) {
|
||||||
|
match &*state.selection {
|
||||||
|
Selection::File { file_ids } => {
|
||||||
|
for id in file_ids {
|
||||||
|
update_file(state.files, *id, |file| editor.file(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selection::Track { file_id, trk_ids } => {
|
||||||
|
update_file(state.files, *file_id, |file| {
|
||||||
|
edit_where(
|
||||||
|
&mut file.trk,
|
||||||
|
|trk| trk_ids.contains(&trk.id),
|
||||||
|
|trk| editor.track(trk),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Selection::TrackSegment {
|
||||||
|
file_id,
|
||||||
|
trk_id,
|
||||||
|
trkseg_ids,
|
||||||
|
} => {
|
||||||
|
update_file(state.files, *file_id, |file| {
|
||||||
|
edit_where(
|
||||||
|
&mut file.trk,
|
||||||
|
|trk| trk.id == *trk_id,
|
||||||
|
|trk| {
|
||||||
|
edit_where(
|
||||||
|
&mut trk.trkseg,
|
||||||
|
|seg| trkseg_ids.contains(&seg.id),
|
||||||
|
|seg| edit_segment(editor, seg),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Selection::Waypoints { file_id } => {
|
||||||
|
update_file(state.files, *file_id, |file| {
|
||||||
|
edit_waypoints(file, |_| true, editor)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Selection::Waypoint { file_id, wpt_ids } => {
|
||||||
|
update_file(state.files, *file_id, |file| {
|
||||||
|
edit_waypoints(file, |wpt| wpt_ids.contains(&wpt.id), editor)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Selection::Empty => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::engine::command::fixture::Fixture;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
struct Rename;
|
||||||
|
|
||||||
|
impl Editor for Rename {
|
||||||
|
fn file(&mut self, file: &mut File) -> Edit {
|
||||||
|
file.info.name.push('!');
|
||||||
|
Edit::Changed
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track(&mut self, track: &mut Track) -> Edit {
|
||||||
|
track.info.name = Some("track".to_string());
|
||||||
|
Edit::Changed
|
||||||
|
}
|
||||||
|
|
||||||
|
fn segment(&mut self, _: &mut TrackSegment) -> Edit {
|
||||||
|
Edit::Changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Nothing;
|
||||||
|
impl Editor for Nothing {}
|
||||||
|
|
||||||
|
fn fixture_with_tracks() -> (Fixture, FileId) {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||||
|
let file = Rc::new(crate::parse(&data).unwrap());
|
||||||
|
let id = file.id;
|
||||||
|
fx.files.insert(id, file);
|
||||||
|
(fx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_only_selected_files_are_updated() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let (a, b, c) = (File::default(), File::default(), File::default());
|
||||||
|
let ids = [a.id, b.id, c.id];
|
||||||
|
for file in [a, b, c] {
|
||||||
|
fx.files.insert(file.id, Rc::new(file));
|
||||||
|
}
|
||||||
|
let untouched = fx.files[&ids[2]].clone();
|
||||||
|
fx.selection = Selection::File {
|
||||||
|
file_ids: HashSet::from([ids[0], ids[1]]),
|
||||||
|
};
|
||||||
|
|
||||||
|
update_selected(&mut fx.state(), &mut Rename);
|
||||||
|
|
||||||
|
assert_eq!(fx.files[&ids[0]].info.name, "!");
|
||||||
|
assert_eq!(fx.files[&ids[1]].info.name, "!");
|
||||||
|
assert!(Rc::ptr_eq(&fx.files[&ids[2]], &untouched));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unchanged_file_keeps_its_snapshot() {
|
||||||
|
let (mut fx, id) = fixture_with_tracks();
|
||||||
|
fx.selection = Selection::File {
|
||||||
|
file_ids: HashSet::from([id]),
|
||||||
|
};
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
update_selected(&mut fx.state(), &mut Nothing);
|
||||||
|
assert!(Rc::ptr_eq(&fx.files[&id], &before));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_track_selection_uses_track_hook() {
|
||||||
|
let (mut fx, id) = fixture_with_tracks();
|
||||||
|
let trk_id = fx.files[&id].trk[0].id;
|
||||||
|
fx.selection = Selection::Track {
|
||||||
|
file_id: id,
|
||||||
|
trk_ids: HashSet::from([trk_id]),
|
||||||
|
};
|
||||||
|
update_selected(&mut fx.state(), &mut Rename);
|
||||||
|
|
||||||
|
let file = &fx.files[&id];
|
||||||
|
assert_eq!(file.info.name, "with_tracks_and_segments");
|
||||||
|
assert_eq!(file.trk[0].info.name.as_deref(), Some("track"));
|
||||||
|
assert_ne!(file.trk[1].info.name.as_deref(), Some("track"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_segment_selection_bumps_revision_id() {
|
||||||
|
let (mut fx, id) = fixture_with_tracks();
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
let trk = &before.trk[0];
|
||||||
|
fx.selection = Selection::TrackSegment {
|
||||||
|
file_id: id,
|
||||||
|
trk_id: trk.id,
|
||||||
|
trkseg_ids: HashSet::from([trk.trkseg[0].id]),
|
||||||
|
};
|
||||||
|
update_selected(&mut fx.state(), &mut Rename);
|
||||||
|
|
||||||
|
let after = &fx.files[&id];
|
||||||
|
assert_ne!(after.trk[0].trkseg[0].rev_id, trk.trkseg[0].rev_id);
|
||||||
|
assert_eq!(after.trk[0].trkseg[1].rev_id, trk.trkseg[1].rev_id);
|
||||||
|
assert_eq!(
|
||||||
|
after.trk[1].trkseg[0].rev_id,
|
||||||
|
before.trk[1].trkseg[0].rev_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_file_selection_cascades_to_segments() {
|
||||||
|
struct SegmentsOnly;
|
||||||
|
impl Editor for SegmentsOnly {
|
||||||
|
fn segment(&mut self, _: &mut TrackSegment) -> Edit {
|
||||||
|
Edit::Changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (mut fx, id) = fixture_with_tracks();
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
fx.selection = Selection::File {
|
||||||
|
file_ids: HashSet::from([id]),
|
||||||
|
};
|
||||||
|
update_selected(&mut fx.state(), &mut SegmentsOnly);
|
||||||
|
for (b, a) in before.trk.iter().zip(fx.files[&id].trk.iter()) {
|
||||||
|
for (b, a) in b.trkseg.iter().zip(a.trkseg.iter()) {
|
||||||
|
assert_ne!(b.rev_id, a.rev_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_waypoint_selection_edits_only_selected_waypoints() {
|
||||||
|
struct Name;
|
||||||
|
impl Editor for Name {
|
||||||
|
fn waypoint(&mut self, wpt: &mut Waypoint) -> Edit {
|
||||||
|
wpt.name = Some("x".into());
|
||||||
|
Edit::Changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (mut fx, id) = fixture_with_tracks();
|
||||||
|
let wpts: Vec<_> = (0..3).map(|_| Waypoint::default()).collect();
|
||||||
|
let ids: Vec<_> = wpts.iter().map(|w| w.id).collect();
|
||||||
|
let mut file = (*fx.files[&id]).clone();
|
||||||
|
file.wpt = vec![Rc::new(crate::WaypointChunk {
|
||||||
|
wpt: wpts,
|
||||||
|
..Default::default()
|
||||||
|
})];
|
||||||
|
fx.files.insert(id, Rc::new(file));
|
||||||
|
let chunk_id = fx.files[&id].wpt[0].id;
|
||||||
|
|
||||||
|
// A file selection never touches waypoints.
|
||||||
|
fx.selection = Selection::File {
|
||||||
|
file_ids: HashSet::from([id]),
|
||||||
|
};
|
||||||
|
update_selected(&mut fx.state(), &mut Name);
|
||||||
|
assert!(fx.files[&id].wpt[0].wpt.iter().all(|w| w.name.is_none()));
|
||||||
|
|
||||||
|
fx.selection = Selection::Waypoint {
|
||||||
|
file_id: id,
|
||||||
|
wpt_ids: HashSet::from([ids[1]]),
|
||||||
|
};
|
||||||
|
let rev = fx.files[&id].wpt_rev_id;
|
||||||
|
update_selected(&mut fx.state(), &mut Name);
|
||||||
|
assert_ne!(fx.files[&id].wpt_rev_id, rev);
|
||||||
|
let chunk = &fx.files[&id].wpt[0];
|
||||||
|
assert_ne!(chunk.id, chunk_id);
|
||||||
|
let named: Vec<_> = chunk.wpt.iter().map(|w| w.name.is_some()).collect();
|
||||||
|
assert_eq!(named, [false, true, false]);
|
||||||
|
|
||||||
|
fx.selection = Selection::Waypoints { file_id: id };
|
||||||
|
update_selected(&mut fx.state(), &mut Name);
|
||||||
|
assert!(fx.files[&id].wpt[0].wpt.iter().all(|w| w.name.is_some()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_missing_files_and_other_selections_are_ignored() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let file = File::default();
|
||||||
|
let id = file.id;
|
||||||
|
fx.files.insert(id, Rc::new(file));
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
|
||||||
|
for selection in [
|
||||||
|
Selection::File {
|
||||||
|
file_ids: HashSet::from([FileId::default()]),
|
||||||
|
},
|
||||||
|
Selection::Empty,
|
||||||
|
] {
|
||||||
|
fx.selection = selection;
|
||||||
|
update_selected(&mut fx.state(), &mut Rename);
|
||||||
|
assert!(Rc::ptr_eq(&fx.files[&id], &before));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
use crate::{Apply, CommandError, LngLatBounds, State};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CleanType {
|
||||||
|
Inside,
|
||||||
|
Outside,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Clean {
|
||||||
|
pub bounds: LngLatBounds,
|
||||||
|
pub type_: CleanType,
|
||||||
|
pub trkpt: bool,
|
||||||
|
pub wpt: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apply for Clean {
|
||||||
|
fn apply(self, _state: &mut State) -> Result<(), CommandError> {
|
||||||
|
// TODO
|
||||||
|
Err(CommandError::NotImplemented("clean"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use crate::{Apply, CommandError, State};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Crop {
|
||||||
|
pub start: u32,
|
||||||
|
pub end: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apply for Crop {
|
||||||
|
fn apply(self, _state: &mut State) -> Result<(), CommandError> {
|
||||||
|
// TODO
|
||||||
|
Err(CommandError::NotImplemented("crop"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use crate::{Apply, CommandError, State};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Elevation<'a> {
|
||||||
|
pub ele: &'a [f64],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apply for Elevation<'_> {
|
||||||
|
fn apply(self, _state: &mut State) -> Result<(), CommandError> {
|
||||||
|
// TODO
|
||||||
|
Err(CommandError::NotImplemented("elevation"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
use crate::{Apply, CommandError, State};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Extract;
|
||||||
|
|
||||||
|
impl Apply for Extract {
|
||||||
|
fn apply(self, _state: &mut State) -> Result<(), CommandError> {
|
||||||
|
// TODO
|
||||||
|
Err(CommandError::NotImplemented("extract"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
use crate::{Apply, CommandError, State};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum MergeType {
|
||||||
|
Connect,
|
||||||
|
Group,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Merge {
|
||||||
|
pub type_: MergeType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apply for Merge {
|
||||||
|
fn apply(self, _state: &mut State) -> Result<(), CommandError> {
|
||||||
|
// TODO
|
||||||
|
Err(CommandError::NotImplemented("merge"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
mod clean;
|
||||||
|
mod crop;
|
||||||
|
mod elevation;
|
||||||
|
mod extract;
|
||||||
|
mod merge;
|
||||||
|
mod move_waypoint;
|
||||||
|
mod new_waypoint;
|
||||||
|
mod reverse;
|
||||||
|
mod splice_trackpoints;
|
||||||
|
mod split;
|
||||||
|
mod time;
|
||||||
|
|
||||||
|
pub use clean::*;
|
||||||
|
pub use crop::*;
|
||||||
|
pub use elevation::*;
|
||||||
|
pub use extract::*;
|
||||||
|
pub use merge::*;
|
||||||
|
pub use move_waypoint::*;
|
||||||
|
pub use new_waypoint::*;
|
||||||
|
pub use reverse::*;
|
||||||
|
pub use splice_trackpoints::*;
|
||||||
|
pub use split::*;
|
||||||
|
pub use time::*;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
use crate::{Apply, CommandError, State};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MoveWaypoint {
|
||||||
|
pub lng: f64,
|
||||||
|
pub lat: f64,
|
||||||
|
pub ele: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apply for MoveWaypoint {
|
||||||
|
fn apply(self, _state: &mut State) -> Result<(), CommandError> {
|
||||||
|
// TODO
|
||||||
|
Err(CommandError::NotImplemented("move waypoint"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
use crate::{Apply, CommandError, State};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NewWaypoint<'a> {
|
||||||
|
pub lng: f64,
|
||||||
|
pub lat: f64,
|
||||||
|
pub ele: f64,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub desc: &'a str,
|
||||||
|
pub icon: &'a str,
|
||||||
|
pub link: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apply for NewWaypoint<'_> {
|
||||||
|
fn apply(self, _state: &mut State) -> Result<(), CommandError> {
|
||||||
|
// TODO
|
||||||
|
Err(CommandError::NotImplemented("new waypoint"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
use crate::{Apply, CommandError, State};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Reverse;
|
||||||
|
|
||||||
|
impl Apply for Reverse {
|
||||||
|
fn apply(self, _state: &mut State) -> Result<(), CommandError> {
|
||||||
|
// TODO
|
||||||
|
Err(CommandError::NotImplemented("reverse"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::{Apply, CommandError, File, FileId, LngLat, Selection, State, Trackpoint};
|
||||||
|
|
||||||
|
/// Replaces the trackpoints `start..end` of the last track segment of the selection by the
|
||||||
|
/// given ones (a pure insertion when `start == end`, a pure removal when there are no new
|
||||||
|
/// points).
|
||||||
|
///
|
||||||
|
/// The selection can be made of several files, tracks or track segments: the indices are
|
||||||
|
/// meant for its very last segment, in file order, then track order, then segment order.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SpliceTrackpoints<'a> {
|
||||||
|
pub start: u32,
|
||||||
|
pub end: u32,
|
||||||
|
pub lng: &'a [f64],
|
||||||
|
pub lat: &'a [f64],
|
||||||
|
pub ele: &'a [f64],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apply for SpliceTrackpoints<'_> {
|
||||||
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||||
|
let (start, end) = (self.start as usize, self.end as usize);
|
||||||
|
if start > end || self.lng.len() != self.lat.len() || self.lat.len() != self.ele.len() {
|
||||||
|
return Err(CommandError::InvalidData(
|
||||||
|
"invalid trackpoints range".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let (file_id, trk, seg) = last_segment(state).ok_or(CommandError::NothingToDo)?;
|
||||||
|
let mut file = (*state.files[&file_id]).clone();
|
||||||
|
let segment = &mut file.trk[trk].trkseg[seg];
|
||||||
|
if end > segment.len() {
|
||||||
|
return Err(CommandError::InvalidData(
|
||||||
|
"trackpoints range out of bounds".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if start == end && self.lng.is_empty() {
|
||||||
|
return Err(CommandError::NothingToDo);
|
||||||
|
}
|
||||||
|
let points = (0..self.lng.len())
|
||||||
|
.map(|i| Trackpoint {
|
||||||
|
coordinates: LngLat {
|
||||||
|
lng: self.lng[i],
|
||||||
|
lat: self.lat[i],
|
||||||
|
},
|
||||||
|
ele: self.ele[i],
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
segment.splice(start, end, points);
|
||||||
|
segment.rev_id = Default::default();
|
||||||
|
state.files.insert(file_id, Rc::new(file));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Position (file, track index, segment index) of the last segment of the selection.
|
||||||
|
fn last_segment(state: &State) -> Option<(FileId, usize, usize)> {
|
||||||
|
let last_of_track = |file: &File, trk: usize| file.trk[trk].trkseg.len().checked_sub(1);
|
||||||
|
let last_of_file = |file: &File, filter: &dyn Fn(usize) -> bool| {
|
||||||
|
(0..file.trk.len())
|
||||||
|
.rev()
|
||||||
|
.filter(|&trk| filter(trk))
|
||||||
|
.find_map(|trk| last_of_track(file, trk).map(|seg| (trk, seg)))
|
||||||
|
};
|
||||||
|
match &*state.selection {
|
||||||
|
Selection::File { file_ids } => state
|
||||||
|
.order
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.filter(|id| file_ids.contains(id))
|
||||||
|
.find_map(|id| {
|
||||||
|
let (trk, seg) = last_of_file(state.files.get(id)?, &|_| true)?;
|
||||||
|
Some((*id, trk, seg))
|
||||||
|
}),
|
||||||
|
Selection::Track { file_id, trk_ids } => {
|
||||||
|
let file = state.files.get(file_id)?;
|
||||||
|
let (trk, seg) = last_of_file(file, &|trk| trk_ids.contains(&file.trk[trk].id))?;
|
||||||
|
Some((*file_id, trk, seg))
|
||||||
|
}
|
||||||
|
Selection::TrackSegment {
|
||||||
|
file_id,
|
||||||
|
trk_id,
|
||||||
|
trkseg_ids,
|
||||||
|
} => {
|
||||||
|
let file = state.files.get(file_id)?;
|
||||||
|
let trk = file.trk.iter().position(|trk| trk.id == *trk_id)?;
|
||||||
|
let seg = file.trk[trk]
|
||||||
|
.trkseg
|
||||||
|
.iter()
|
||||||
|
.rposition(|seg| trkseg_ids.contains(&seg.id))?;
|
||||||
|
Some((*file_id, trk, seg))
|
||||||
|
}
|
||||||
|
Selection::Empty | Selection::Waypoints { .. } | Selection::Waypoint { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::{FileId, Load, engine::command::fixture::Fixture};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn loaded() -> (Fixture, FileId) {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||||
|
Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||||
|
let id = fx.order.0[0];
|
||||||
|
let trk = &fx.files[&id].trk[0];
|
||||||
|
fx.selection = Selection::TrackSegment {
|
||||||
|
file_id: id,
|
||||||
|
trk_id: trk.id,
|
||||||
|
trkseg_ids: HashSet::from([trk.trkseg[0].id]),
|
||||||
|
};
|
||||||
|
(fx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn splice<'a>(
|
||||||
|
start: u32,
|
||||||
|
end: u32,
|
||||||
|
lng: &'a [f64],
|
||||||
|
lat: &'a [f64],
|
||||||
|
ele: &'a [f64],
|
||||||
|
) -> SpliceTrackpoints<'a> {
|
||||||
|
SpliceTrackpoints {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
lng,
|
||||||
|
lat,
|
||||||
|
ele,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_splice_replaces_points_of_selected_segment_only() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
let len = before.trk[0].trkseg[0].len();
|
||||||
|
assert!(len >= 3);
|
||||||
|
|
||||||
|
splice(1, 3, &[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0], &[7.0, 8.0, 9.0])
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let after = &fx.files[&id];
|
||||||
|
let (old, new) = (&before.trk[0].trkseg[0], &after.trk[0].trkseg[0]);
|
||||||
|
assert_eq!(new.len(), len + 1);
|
||||||
|
assert_eq!(new[0].coordinates.lng, old[0].coordinates.lng);
|
||||||
|
assert_eq!(new[1].coordinates.lng, 1.0);
|
||||||
|
assert_eq!(new[3].coordinates.lat, 6.0);
|
||||||
|
assert_eq!(new[3].ele, 9.0);
|
||||||
|
assert_eq!(new[4].coordinates.lng, old[3].coordinates.lng);
|
||||||
|
assert_ne!(new.rev_id, old.rev_id);
|
||||||
|
assert_eq!(new.id, old.id);
|
||||||
|
// other segments and tracks keep their revision
|
||||||
|
for (b, a) in before.trk.iter().zip(&after.trk) {
|
||||||
|
for (b, a) in b.trkseg.iter().zip(&a.trkseg) {
|
||||||
|
if a.id != new.id {
|
||||||
|
assert_eq!(a.rev_id, b.rev_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_splice_append_and_remove() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let len = fx.files[&id].trk[0].trkseg[0].len() as u32;
|
||||||
|
splice(len, len, &[1.0], &[2.0], &[3.0])
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
let seg = &fx.files[&id].trk[0].trkseg[0];
|
||||||
|
assert_eq!(seg.len(), len as usize + 1);
|
||||||
|
assert_eq!(seg[len as usize].ele, 3.0);
|
||||||
|
|
||||||
|
splice(0, len + 1, &[], &[], &[])
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(fx.files[&id].trk[0].trkseg[0].len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_splice_invalid_arguments_change_nothing() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let before = fx.files[&id].clone();
|
||||||
|
let len = before.trk[0].trkseg[0].len() as u32;
|
||||||
|
let results = [
|
||||||
|
splice(2, 1, &[], &[], &[]).apply(&mut fx.state()),
|
||||||
|
splice(0, 0, &[1.0], &[], &[]).apply(&mut fx.state()),
|
||||||
|
splice(0, len + 1, &[], &[], &[]).apply(&mut fx.state()),
|
||||||
|
];
|
||||||
|
assert!(
|
||||||
|
results
|
||||||
|
.iter()
|
||||||
|
.all(|r| matches!(r, Err(CommandError::InvalidData(_))))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
splice(0, 0, &[], &[], &[]).apply(&mut fx.state()),
|
||||||
|
Err(CommandError::NothingToDo)
|
||||||
|
);
|
||||||
|
assert!(std::rc::Rc::ptr_eq(&fx.files[&id], &before));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_splice_needs_a_segment_selection() {
|
||||||
|
let (mut fx, _) = loaded();
|
||||||
|
fx.selection = Selection::Empty;
|
||||||
|
assert_eq!(
|
||||||
|
splice(0, 0, &[1.0], &[1.0], &[1.0]).apply(&mut fx.state()),
|
||||||
|
Err(CommandError::NothingToDo)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_splice_targets_last_segment_of_the_selection() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let file = fx.files[&id].clone();
|
||||||
|
assert!(file.trk.len() >= 2);
|
||||||
|
let last_trk = file.trk.len() - 1;
|
||||||
|
let last_seg = file.trk[last_trk].trkseg.len() - 1;
|
||||||
|
|
||||||
|
// whole file selected: last segment of its last track
|
||||||
|
fx.selection = Selection::File {
|
||||||
|
file_ids: HashSet::from([id]),
|
||||||
|
};
|
||||||
|
splice(0, 0, &[1.0], &[2.0], &[3.0])
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
let after = &fx.files[&id];
|
||||||
|
for (t, (b, a)) in file.trk.iter().zip(&after.trk).enumerate() {
|
||||||
|
for (s, (b, a)) in b.trkseg.iter().zip(&a.trkseg).enumerate() {
|
||||||
|
let target = (t, s) == (last_trk, last_seg);
|
||||||
|
assert_eq!(a.len(), b.len() + usize::from(target));
|
||||||
|
assert_eq!(a.rev_id != b.rev_id, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// several tracks selected: last selected one (in file order, whatever the set order)
|
||||||
|
let ids: Vec<_> = file.trk.iter().map(|t| t.id).collect();
|
||||||
|
fx.selection = Selection::Track {
|
||||||
|
file_id: id,
|
||||||
|
trk_ids: HashSet::from([ids[1], ids[0]]),
|
||||||
|
};
|
||||||
|
let len = |fx: &Fixture, t: usize| {
|
||||||
|
let trk = &fx.files[&id].trk[t];
|
||||||
|
trk.trkseg.last().unwrap().len()
|
||||||
|
};
|
||||||
|
let (l0, l1) = (len(&fx, 0), len(&fx, 1));
|
||||||
|
splice(0, 0, &[1.0], &[2.0], &[3.0])
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(len(&fx, 0), l0);
|
||||||
|
assert_eq!(len(&fx, 1), l1 + 1);
|
||||||
|
|
||||||
|
// several segments selected
|
||||||
|
let trk = &fx.files[&id].trk[0];
|
||||||
|
let seg_ids: Vec<_> = trk.trkseg.iter().map(|s| s.id).collect();
|
||||||
|
if seg_ids.len() >= 2 {
|
||||||
|
fx.selection = Selection::TrackSegment {
|
||||||
|
file_id: id,
|
||||||
|
trk_id: trk.id,
|
||||||
|
trkseg_ids: seg_ids.iter().copied().collect(),
|
||||||
|
};
|
||||||
|
let lens: Vec<_> = trk.trkseg.iter().map(|s| s.len()).collect();
|
||||||
|
splice(0, 0, &[1.0], &[2.0], &[3.0])
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
let after: Vec<_> = fx.files[&id].trk[0]
|
||||||
|
.trkseg
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.len())
|
||||||
|
.collect();
|
||||||
|
let n = lens.len();
|
||||||
|
assert_eq!(after[..n - 1], lens[..n - 1]);
|
||||||
|
assert_eq!(after[n - 1], lens[n - 1] + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_splice_on_multiple_files_uses_last_in_file_order() {
|
||||||
|
let (mut fx, first) = loaded();
|
||||||
|
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||||
|
Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||||
|
let second = fx.order.0[1];
|
||||||
|
fx.selection = Selection::File {
|
||||||
|
file_ids: HashSet::from([second, first]),
|
||||||
|
};
|
||||||
|
let total = |fx: &Fixture, id: FileId| -> usize {
|
||||||
|
fx.files[&id]
|
||||||
|
.trk
|
||||||
|
.iter()
|
||||||
|
.flat_map(|t| &t.trkseg)
|
||||||
|
.map(|s| s.len())
|
||||||
|
.sum()
|
||||||
|
};
|
||||||
|
let (a, b) = (total(&fx, first), total(&fx, second));
|
||||||
|
splice(0, 0, &[1.0], &[2.0], &[3.0])
|
||||||
|
.apply(&mut fx.state())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(total(&fx, first), a);
|
||||||
|
assert_eq!(total(&fx, second), b + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
use crate::{Apply, CommandError, State};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Split {
|
||||||
|
pub at: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Apply for Split {
|
||||||
|
fn apply(self, _state: &mut State) -> Result<(), CommandError> {
|
||||||
|
// TODO
|
||||||
|
Err(CommandError::NotImplemented("split"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
use crate::{Apply, CommandError, State};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Time;
|
||||||
|
|
||||||
|
impl Apply for Time {
|
||||||
|
fn apply(self, _state: &mut State) -> Result<(), CommandError> {
|
||||||
|
// TODO
|
||||||
|
Err(CommandError::NotImplemented("time"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
FileId, FileWaypointsRevisionId, StackEntry as Files, TrackSegmentId, TrackSegmentRevisionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Coordinates buffers, as flat `[lng, lat, lng, lat, ...]` arrays so that they can be handed
|
||||||
|
/// to the UI without per-point calls: one per track segment (its trackpoints) and one per file
|
||||||
|
/// (its waypoints). A buffer is only rebuilt when the revision of its segment (or the waypoints
|
||||||
|
/// of its file) changes, so editing a segment does not touch the buffers of the others.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct CoordinatesCache {
|
||||||
|
segments: HashMap<TrackSegmentId, (TrackSegmentRevisionId, Vec<f64>)>,
|
||||||
|
waypoints: HashMap<FileId, (FileWaypointsRevisionId, Vec<f64>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CoordinatesCache {
|
||||||
|
pub fn update(&mut self, files: Option<&Files>) {
|
||||||
|
let mut segments = HashSet::new();
|
||||||
|
let mut waypoints = HashSet::new();
|
||||||
|
for file in files.into_iter().flat_map(|files| files.values()) {
|
||||||
|
waypoints.insert(file.id);
|
||||||
|
if self
|
||||||
|
.waypoints
|
||||||
|
.get(&file.id)
|
||||||
|
.is_none_or(|(r, _)| *r != file.wpt_rev_id)
|
||||||
|
{
|
||||||
|
let mut coordinates = Vec::new();
|
||||||
|
for wpt in file.wpt.iter().flat_map(|chunk| &chunk.wpt) {
|
||||||
|
coordinates.extend([wpt.coordinates.lng, wpt.coordinates.lat]);
|
||||||
|
}
|
||||||
|
self.waypoints
|
||||||
|
.insert(file.id, (file.wpt_rev_id, coordinates));
|
||||||
|
}
|
||||||
|
for seg in file.trk.iter().flat_map(|trk| &trk.trkseg) {
|
||||||
|
segments.insert(seg.id);
|
||||||
|
if self
|
||||||
|
.segments
|
||||||
|
.get(&seg.id)
|
||||||
|
.is_none_or(|(r, _)| *r != seg.rev_id)
|
||||||
|
{
|
||||||
|
let mut coordinates = Vec::with_capacity(seg.len() * 2);
|
||||||
|
for trkpt in seg.iter() {
|
||||||
|
coordinates.extend([trkpt.coordinates.lng, trkpt.coordinates.lat]);
|
||||||
|
}
|
||||||
|
self.segments.insert(seg.id, (seg.rev_id, coordinates));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.segments.retain(|id, _| segments.contains(id));
|
||||||
|
self.waypoints.retain(|id, _| waypoints.contains(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coordinates of the trackpoints of a segment, empty if it does not exist.
|
||||||
|
pub fn segment(&self, id: &TrackSegmentId) -> &[f64] {
|
||||||
|
self.segments.get(id).map_or(&[], |(_, c)| c)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coordinates of the waypoints of a file, empty if it does not exist.
|
||||||
|
pub fn waypoints(&self, id: &FileId) -> &[f64] {
|
||||||
|
self.waypoints.get(id).map_or(&[], |(_, c)| c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::{Apply, LngLat, Load, Waypoint, WaypointChunk, engine::command::fixture::Fixture};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn loaded() -> (Fixture, FileId) {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||||
|
Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||||
|
let id = fx.order.0[0];
|
||||||
|
(fx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_segment_buffers_match_trackpoints() {
|
||||||
|
let (fx, id) = loaded();
|
||||||
|
let mut cache = CoordinatesCache::default();
|
||||||
|
cache.update(Some(&fx.files));
|
||||||
|
for seg in fx.files[&id].trk.iter().flat_map(|t| &t.trkseg) {
|
||||||
|
let buffer = cache.segment(&seg.id);
|
||||||
|
assert_eq!(buffer.len(), seg.len() * 2);
|
||||||
|
for (i, p) in seg.iter().enumerate() {
|
||||||
|
assert_eq!(buffer[2 * i], p.coordinates.lng);
|
||||||
|
assert_eq!(buffer[2 * i + 1], p.coordinates.lat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(cache.segment(&TrackSegmentId::default()).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_only_changed_buffers_are_rebuilt_and_stale_ones_dropped() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let mut cache = CoordinatesCache::default();
|
||||||
|
cache.update(Some(&fx.files));
|
||||||
|
let ids: Vec<_> = fx.files[&id]
|
||||||
|
.trk
|
||||||
|
.iter()
|
||||||
|
.flat_map(|t| &t.trkseg)
|
||||||
|
.map(|s| s.id)
|
||||||
|
.collect();
|
||||||
|
assert!(ids.len() >= 2);
|
||||||
|
let ptr = |c: &CoordinatesCache, i: usize| c.segment(&ids[i]).as_ptr();
|
||||||
|
let (p0, p1) = (ptr(&cache, 0), ptr(&cache, 1));
|
||||||
|
|
||||||
|
let mut file = (*fx.files[&id]).clone();
|
||||||
|
let seg = &mut file.trk[0].trkseg[0];
|
||||||
|
seg.splice(0, 0, vec![Default::default()]);
|
||||||
|
seg.rev_id = Default::default();
|
||||||
|
let len = seg.len();
|
||||||
|
fx.files.insert(id, Rc::new(file));
|
||||||
|
cache.update(Some(&fx.files));
|
||||||
|
assert_eq!(cache.segment(&ids[0]).len(), len * 2);
|
||||||
|
assert_ne!(ptr(&cache, 0), p0);
|
||||||
|
assert_eq!(ptr(&cache, 1), p1);
|
||||||
|
|
||||||
|
cache.update(None);
|
||||||
|
assert!(cache.segment(&ids[0]).is_empty());
|
||||||
|
assert!(cache.waypoints(&id).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_waypoint_buffer() {
|
||||||
|
let (mut fx, id) = loaded();
|
||||||
|
let wpt = |lng, lat| Waypoint {
|
||||||
|
coordinates: LngLat { lng, lat },
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut file = (*fx.files[&id]).clone();
|
||||||
|
file.wpt_rev_id = Default::default();
|
||||||
|
file.wpt = vec![
|
||||||
|
Rc::new(WaypointChunk {
|
||||||
|
wpt: vec![wpt(1.0, 2.0)],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
Rc::new(WaypointChunk {
|
||||||
|
wpt: vec![wpt(3.0, 4.0)],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
fx.files.insert(id, Rc::new(file));
|
||||||
|
let mut cache = CoordinatesCache::default();
|
||||||
|
cache.update(Some(&fx.files));
|
||||||
|
assert_eq!(cache.waypoints(&id), [1.0, 2.0, 3.0, 4.0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
use crate::{
|
||||||
|
File, FileId, FileWaypointsRevisionId, TrackId, TrackSegmentId, TrackSegmentRevisionId,
|
||||||
|
WaypointId,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// What the UI needs to display a file: its name and the structure of its tracks and waypoints,
|
||||||
|
/// with the ids that reference every element. Coordinates are not part of it, they are read
|
||||||
|
/// from the buffers of the [`CoordinatesCache`](crate::CoordinatesCache) (the `i`-th waypoint
|
||||||
|
/// of a file is the `i`-th coordinates pair of its waypoint buffer, same for the trackpoints of
|
||||||
|
/// a segment).
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct FileStructure {
|
||||||
|
pub id: FileId,
|
||||||
|
pub name: String,
|
||||||
|
pub tracks: Vec<TrackNode>,
|
||||||
|
pub waypoints: Vec<WaypointNode>,
|
||||||
|
/// Changes whenever the waypoints of the file (hence their buffer) change.
|
||||||
|
pub wpt_rev_id: FileWaypointsRevisionId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct TrackNode {
|
||||||
|
pub id: TrackId,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub segments: Vec<SegmentNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct SegmentNode {
|
||||||
|
pub id: TrackSegmentId,
|
||||||
|
/// Changes whenever the trackpoints of the segment (hence their buffer) change.
|
||||||
|
pub rev_id: TrackSegmentRevisionId,
|
||||||
|
pub len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct WaypointNode {
|
||||||
|
pub id: WaypointId,
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileStructure {
|
||||||
|
pub fn new(file: &File) -> Self {
|
||||||
|
Self {
|
||||||
|
id: file.id,
|
||||||
|
name: file.info.name.clone(),
|
||||||
|
tracks: file
|
||||||
|
.trk
|
||||||
|
.iter()
|
||||||
|
.map(|trk| TrackNode {
|
||||||
|
id: trk.id,
|
||||||
|
name: trk.info.name.clone(),
|
||||||
|
segments: trk
|
||||||
|
.trkseg
|
||||||
|
.iter()
|
||||||
|
.map(|seg| SegmentNode {
|
||||||
|
id: seg.id,
|
||||||
|
rev_id: seg.rev_id,
|
||||||
|
len: seg.len(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
waypoints: file
|
||||||
|
.wpt
|
||||||
|
.iter()
|
||||||
|
.flat_map(|chunk| &chunk.wpt)
|
||||||
|
.map(|wpt| WaypointNode {
|
||||||
|
id: wpt.id,
|
||||||
|
name: wpt.name.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
wpt_rev_id: file.wpt_rev_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::{Apply, Load, Waypoint, WaypointChunk, engine::command::fixture::Fixture};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_structure_of_a_file() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||||
|
Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||||
|
let file = &fx.files[&fx.order.0[0]];
|
||||||
|
let node = FileStructure::new(file);
|
||||||
|
assert_eq!((node.id, &node.name), (file.id, &file.info.name));
|
||||||
|
assert_eq!(node.tracks.len(), file.trk.len());
|
||||||
|
for (n, t) in node.tracks.iter().zip(&file.trk) {
|
||||||
|
assert_eq!((n.id, &n.name), (t.id, &t.info.name));
|
||||||
|
assert_eq!(n.segments.len(), t.trkseg.len());
|
||||||
|
for (n, s) in n.segments.iter().zip(&t.trkseg) {
|
||||||
|
assert_eq!((n.id, n.rev_id, n.len), (s.id, s.rev_id, s.len()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_waypoints_in_buffer_order_and_revision() {
|
||||||
|
let mut file = crate::File::default();
|
||||||
|
let wpt = |n: &str| Waypoint {
|
||||||
|
name: Some(n.to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let before = file.wpt_rev_id;
|
||||||
|
file.wpt_rev_id = Default::default();
|
||||||
|
file.wpt = vec![
|
||||||
|
Rc::new(WaypointChunk {
|
||||||
|
wpt: vec![wpt("a"), wpt("b")],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
Rc::new(WaypointChunk {
|
||||||
|
wpt: vec![wpt("c")],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
let node = FileStructure::new(&file);
|
||||||
|
let names: Vec<_> = node
|
||||||
|
.waypoints
|
||||||
|
.iter()
|
||||||
|
.map(|w| w.name.clone().unwrap())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(names, ["a", "b", "c"]);
|
||||||
|
assert_ne!(node.wpt_rev_id, before);
|
||||||
|
assert_eq!(node.wpt_rev_id, file.wpt_rev_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{Diff, FileId, FileStructure, StackEntry as Files};
|
||||||
|
|
||||||
|
/// The structure of each file, only recomputed for the files that changed according to the
|
||||||
|
/// [`Diff`] of the stack.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct FileStructureCache {
|
||||||
|
map: HashMap<FileId, FileStructure>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileStructureCache {
|
||||||
|
/// Brings the cache in line with `files`, given what changed since the last update.
|
||||||
|
pub fn update(&mut self, files: Option<&Files>, diff: Option<&Diff>) {
|
||||||
|
if let Some(diff) = diff {
|
||||||
|
for id in diff.removed.iter() {
|
||||||
|
self.map.remove(id);
|
||||||
|
}
|
||||||
|
for id in diff.added.iter().chain(&diff.modified) {
|
||||||
|
if let Some(file) = files.and_then(|files| files.get(id)) {
|
||||||
|
self.map.insert(*id, FileStructure::new(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, id: &FileId) -> Option<&FileStructure> {
|
||||||
|
self.map.get(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::{Apply, Load, engine::command::fixture::Fixture};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_only_files_of_the_diff_are_recomputed() {
|
||||||
|
let mut fx = Fixture::default();
|
||||||
|
let data = std::fs::read("data/with_tracks_and_segments.gpx").unwrap();
|
||||||
|
Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||||
|
Load { data: &data }.apply(&mut fx.state()).unwrap();
|
||||||
|
let (a, b) = (fx.order.0[0], fx.order.0[1]);
|
||||||
|
|
||||||
|
let mut cache = FileStructureCache::default();
|
||||||
|
cache.update(
|
||||||
|
Some(&fx.files),
|
||||||
|
Some(&Diff {
|
||||||
|
added: vec![a, b],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let before = cache.get(&b).unwrap() as *const FileStructure;
|
||||||
|
|
||||||
|
let mut file = (*fx.files[&a]).clone();
|
||||||
|
file.info.name = "renamed".into();
|
||||||
|
fx.files.insert(a, Rc::new(file));
|
||||||
|
cache.update(
|
||||||
|
Some(&fx.files),
|
||||||
|
Some(&Diff {
|
||||||
|
modified: vec![a],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert_eq!(cache.get(&a).unwrap().name, "renamed");
|
||||||
|
assert!(std::ptr::eq(cache.get(&b).unwrap(), before));
|
||||||
|
|
||||||
|
fx.files.remove(&b);
|
||||||
|
cache.update(
|
||||||
|
Some(&fx.files),
|
||||||
|
Some(&Diff {
|
||||||
|
removed: vec![b],
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert!(cache.get(&b).is_none());
|
||||||
|
assert!(cache.get(&a).is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
mod coordinates_cache;
|
||||||
|
mod file_structure;
|
||||||
|
mod file_structure_cache;
|
||||||
|
mod statistics_buffer;
|
||||||
|
mod statistics_cache;
|
||||||
|
|
||||||
|
pub use coordinates_cache::*;
|
||||||
|
pub use file_structure::*;
|
||||||
|
pub use file_structure_cache::*;
|
||||||
|
pub use statistics_buffer::*;
|
||||||
|
pub use statistics_cache::*;
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
use crate::{GlobalStatistics, Statistics, sum_options};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct StatisticsBuffer {
|
||||||
|
pub total_distance: Vec<f64>,
|
||||||
|
pub moving_distance: Vec<f64>,
|
||||||
|
pub total_time: Vec<i32>,
|
||||||
|
pub moving_time: Vec<i32>,
|
||||||
|
pub speed: Vec<f64>,
|
||||||
|
pub elevation_gain: Vec<f64>,
|
||||||
|
pub elevation_loss: Vec<f64>,
|
||||||
|
pub slope: Vec<f64>,
|
||||||
|
pub slope_segment_slope: Vec<f64>,
|
||||||
|
pub slope_segment_distance: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatisticsBuffer {
|
||||||
|
pub fn update(&mut self, stats: &[&Statistics]) {
|
||||||
|
self.total_distance.clear();
|
||||||
|
self.moving_distance.clear();
|
||||||
|
self.total_time.clear();
|
||||||
|
self.moving_time.clear();
|
||||||
|
self.speed.clear();
|
||||||
|
self.elevation_gain.clear();
|
||||||
|
self.elevation_loss.clear();
|
||||||
|
self.slope.clear();
|
||||||
|
self.slope_segment_slope.clear();
|
||||||
|
self.slope_segment_distance.clear();
|
||||||
|
|
||||||
|
let mut cumul_stats = GlobalStatistics::default();
|
||||||
|
for stats in stats {
|
||||||
|
for trkpt_stats in stats.local.iter() {
|
||||||
|
self.total_distance
|
||||||
|
.push(cumul_stats.total_distance + trkpt_stats.total_distance);
|
||||||
|
self.moving_distance.push(
|
||||||
|
sum_options(cumul_stats.moving_distance, trkpt_stats.moving_distance)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
self.total_time.push(
|
||||||
|
sum_options(cumul_stats.total_time, trkpt_stats.total_time).unwrap_or_default(),
|
||||||
|
);
|
||||||
|
self.moving_time.push(
|
||||||
|
sum_options(cumul_stats.moving_time, trkpt_stats.moving_time)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
self.speed.push(trkpt_stats.speed.unwrap_or_default());
|
||||||
|
self.elevation_gain
|
||||||
|
.push(cumul_stats.elevation_gain + trkpt_stats.elevation_gain);
|
||||||
|
self.elevation_loss
|
||||||
|
.push(cumul_stats.elevation_loss + trkpt_stats.elevation_loss);
|
||||||
|
self.slope.push(trkpt_stats.slope);
|
||||||
|
self.slope_segment_slope
|
||||||
|
.push(trkpt_stats.slope_segment.slope);
|
||||||
|
self.slope_segment_distance
|
||||||
|
.push(trkpt_stats.slope_segment.distance);
|
||||||
|
}
|
||||||
|
cumul_stats.merge(&stats.global);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::parse;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn stats(path: &str) -> Statistics {
|
||||||
|
let data = std::fs::read(path).unwrap();
|
||||||
|
let file = parse(&data).unwrap();
|
||||||
|
Statistics::compute(&file.trk[0].trkseg[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty() {
|
||||||
|
let mut buffer = StatisticsBuffer::default();
|
||||||
|
buffer.update(&[]);
|
||||||
|
assert!(buffer.total_distance.is_empty());
|
||||||
|
assert!(buffer.speed.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_single_segment_matches_local_stats() {
|
||||||
|
let s = stats("data/simple.gpx");
|
||||||
|
let mut buffer = StatisticsBuffer::default();
|
||||||
|
buffer.update(&[&s]);
|
||||||
|
|
||||||
|
let n = s.local.len();
|
||||||
|
for len in [
|
||||||
|
buffer.total_distance.len(),
|
||||||
|
buffer.moving_distance.len(),
|
||||||
|
buffer.total_time.len(),
|
||||||
|
buffer.moving_time.len(),
|
||||||
|
buffer.speed.len(),
|
||||||
|
buffer.elevation_gain.len(),
|
||||||
|
buffer.elevation_loss.len(),
|
||||||
|
buffer.slope.len(),
|
||||||
|
buffer.slope_segment_slope.len(),
|
||||||
|
buffer.slope_segment_distance.len(),
|
||||||
|
] {
|
||||||
|
assert_eq!(len, n);
|
||||||
|
}
|
||||||
|
for (i, local) in s.local.iter().enumerate() {
|
||||||
|
assert_eq!(buffer.total_distance[i], local.total_distance);
|
||||||
|
assert_eq!(buffer.slope[i], local.slope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_distances_accumulate_over_segments() {
|
||||||
|
let s = stats("data/simple.gpx");
|
||||||
|
let n = s.local.len();
|
||||||
|
let mut buffer = StatisticsBuffer::default();
|
||||||
|
buffer.update(&[&s, &s]);
|
||||||
|
|
||||||
|
assert_eq!(buffer.total_distance.len(), 2 * n);
|
||||||
|
// the second segment starts where the first one ended
|
||||||
|
assert_eq!(buffer.total_distance[n], s.global.total_distance);
|
||||||
|
assert!((buffer.total_distance[2 * n - 1] - 2.0 * s.global.total_distance).abs() < 1e-9);
|
||||||
|
assert!(buffer.total_distance.windows(2).all(|w| w[0] <= w[1]));
|
||||||
|
assert!((buffer.elevation_gain[2 * n - 1] - 2.0 * s.global.elevation_gain).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_total_time_accumulates_over_segments() {
|
||||||
|
let s = stats("data/with_time.gpx");
|
||||||
|
let n = s.local.len();
|
||||||
|
let duration = s.global.total_time.unwrap();
|
||||||
|
assert!(duration > 0);
|
||||||
|
let mut buffer = StatisticsBuffer::default();
|
||||||
|
buffer.update(&[&s, &s]);
|
||||||
|
|
||||||
|
assert_eq!(buffer.total_time[n - 1], duration);
|
||||||
|
assert_eq!(buffer.total_time[n], duration);
|
||||||
|
assert_eq!(buffer.total_time[2 * n - 1], 2 * duration);
|
||||||
|
assert!(buffer.total_time.windows(2).all(|w| w[0] <= w[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_replaces_previous_content() {
|
||||||
|
let s = stats("data/simple.gpx");
|
||||||
|
let mut buffer = StatisticsBuffer::default();
|
||||||
|
buffer.update(&[&s]);
|
||||||
|
buffer.update(&[&s]);
|
||||||
|
assert_eq!(buffer.total_distance.len(), s.local.len());
|
||||||
|
buffer.update(&[]);
|
||||||
|
assert!(buffer.total_distance.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet, hash_map::Entry},
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{File, Selection, StackEntry, Statistics, Track, TrackSegment, TrackSegmentRevisionId};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct StatisticsCache {
|
||||||
|
map: HashMap<TrackSegmentRevisionId, Statistics>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatisticsCache {
|
||||||
|
pub fn update(&mut self, state: Option<&StackEntry>) {
|
||||||
|
let mut stale: HashSet<TrackSegmentRevisionId> = self.map.keys().copied().collect();
|
||||||
|
if let Some(state) = state {
|
||||||
|
for file in state.values() {
|
||||||
|
for trk in file.trk.iter() {
|
||||||
|
for trkseg in trk.trkseg.iter() {
|
||||||
|
match self.map.entry(trkseg.rev_id) {
|
||||||
|
Entry::Occupied(_) => {
|
||||||
|
stale.remove(&trkseg.rev_id);
|
||||||
|
}
|
||||||
|
Entry::Vacant(e) => {
|
||||||
|
e.insert(Statistics::compute(trkseg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id in stale.iter() {
|
||||||
|
self.map.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<'a>(
|
||||||
|
&'a self,
|
||||||
|
state: Option<&StackEntry>,
|
||||||
|
selection: &Selection,
|
||||||
|
) -> Vec<&'a Statistics> {
|
||||||
|
let mut stats = vec![];
|
||||||
|
if let Some(state) = state {
|
||||||
|
match selection {
|
||||||
|
Selection::Empty => (),
|
||||||
|
Selection::File { file_ids } => {
|
||||||
|
for id in file_ids.iter() {
|
||||||
|
if let Some(file) = state.get(id) {
|
||||||
|
self.add_file_stats(file, &mut stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selection::Track { file_id, trk_ids } => {
|
||||||
|
if let Some(file) = state.get(file_id) {
|
||||||
|
for trk in file.trk.iter() {
|
||||||
|
if trk_ids.contains(&trk.id) {
|
||||||
|
self.add_track_stats(trk, &mut stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selection::TrackSegment {
|
||||||
|
file_id,
|
||||||
|
trk_id,
|
||||||
|
trkseg_ids,
|
||||||
|
} => {
|
||||||
|
if let Some(file) = state.get(file_id) {
|
||||||
|
for trk in file.trk.iter() {
|
||||||
|
if *trk_id == trk.id {
|
||||||
|
for trkseg in trk.trkseg.iter() {
|
||||||
|
if trkseg_ids.contains(&trkseg.id) {
|
||||||
|
self.add_segment_stats(trkseg, &mut stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selection::Waypoints { file_id } => {
|
||||||
|
if let Some(file) = state.get(file_id) {
|
||||||
|
self.add_file_stats(file, &mut stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selection::Waypoint {
|
||||||
|
file_id,
|
||||||
|
wpt_ids: _,
|
||||||
|
} => {
|
||||||
|
if let Some(file) = state.get(file_id) {
|
||||||
|
self.add_file_stats(file, &mut stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stats
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_file_stats<'a>(&'a self, file: &Rc<File>, stats: &mut Vec<&'a Statistics>) {
|
||||||
|
for trk in file.trk.iter() {
|
||||||
|
self.add_track_stats(trk, stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_track_stats<'a>(&'a self, trk: &Track, stats: &mut Vec<&'a Statistics>) {
|
||||||
|
for trkseg in trk.trkseg.iter() {
|
||||||
|
self.add_segment_stats(trkseg, stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_segment_stats<'a>(&'a self, trkseg: &TrackSegment, stats: &mut Vec<&'a Statistics>) {
|
||||||
|
if let Some(s) = self.map.get(&trkseg.rev_id) {
|
||||||
|
stats.push(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::{FileId, parse};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn state(path: &str) -> (StackEntry, Rc<File>) {
|
||||||
|
let data = std::fs::read(path).unwrap();
|
||||||
|
let file = Rc::new(parse(&data).unwrap());
|
||||||
|
let mut state = StackEntry::default();
|
||||||
|
state.insert(file.id, file.clone());
|
||||||
|
(state, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_selection(id: FileId) -> Selection {
|
||||||
|
Selection::File {
|
||||||
|
file_ids: HashSet::from([id]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_update_and_select_file() {
|
||||||
|
let (state, file) = state("data/with_tracks_and_segments.gpx");
|
||||||
|
let nb_segments: usize = file.trk.iter().map(|t| t.trkseg.len()).sum();
|
||||||
|
let mut cache = StatisticsCache::default();
|
||||||
|
cache.update(Some(&state));
|
||||||
|
assert_eq!(cache.map.len(), nb_segments);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
cache.get(Some(&state), &file_selection(file.id)).len(),
|
||||||
|
nb_segments
|
||||||
|
);
|
||||||
|
assert!(cache.get(Some(&state), &Selection::Empty).is_empty());
|
||||||
|
assert!(cache.get(None, &file_selection(file.id)).is_empty());
|
||||||
|
// unknown file
|
||||||
|
assert!(
|
||||||
|
cache
|
||||||
|
.get(Some(&state), &file_selection(FileId::default()))
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_select_track_and_segment() {
|
||||||
|
let (state, file) = state("data/with_tracks_and_segments.gpx");
|
||||||
|
let mut cache = StatisticsCache::default();
|
||||||
|
cache.update(Some(&state));
|
||||||
|
|
||||||
|
let trk = &file.trk[0];
|
||||||
|
let selection = Selection::Track {
|
||||||
|
file_id: file.id,
|
||||||
|
trk_ids: HashSet::from([trk.id]),
|
||||||
|
};
|
||||||
|
assert_eq!(cache.get(Some(&state), &selection).len(), trk.trkseg.len());
|
||||||
|
|
||||||
|
let trkseg = &trk.trkseg[1];
|
||||||
|
let selection = Selection::TrackSegment {
|
||||||
|
file_id: file.id,
|
||||||
|
trk_id: trk.id,
|
||||||
|
trkseg_ids: HashSet::from([trkseg.id]),
|
||||||
|
};
|
||||||
|
let stats = cache.get(Some(&state), &selection);
|
||||||
|
assert_eq!(stats.len(), 1);
|
||||||
|
assert_eq!(stats[0].local.len(), trkseg.len());
|
||||||
|
|
||||||
|
// a segment id under the wrong track selects nothing
|
||||||
|
let selection = Selection::TrackSegment {
|
||||||
|
file_id: file.id,
|
||||||
|
trk_id: file.trk[1].id,
|
||||||
|
trkseg_ids: HashSet::from([trkseg.id]),
|
||||||
|
};
|
||||||
|
assert!(cache.get(Some(&state), &selection).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unchanged_segments_are_not_recomputed() {
|
||||||
|
let (state, file) = state("data/simple.gpx");
|
||||||
|
let mut cache = StatisticsCache::default();
|
||||||
|
cache.update(Some(&state));
|
||||||
|
let before: *const Statistics = cache.get(Some(&state), &file_selection(file.id))[0];
|
||||||
|
|
||||||
|
// a new snapshot of the file that shares its segments (same revision ids)
|
||||||
|
let mut next = StackEntry::default();
|
||||||
|
let renamed = {
|
||||||
|
let mut f = (*file).clone();
|
||||||
|
f.info.name = "other".to_string();
|
||||||
|
Rc::new(f)
|
||||||
|
};
|
||||||
|
next.insert(renamed.id, renamed);
|
||||||
|
cache.update(Some(&next));
|
||||||
|
let after: *const Statistics = cache.get(Some(&next), &file_selection(file.id))[0];
|
||||||
|
assert!(std::ptr::eq(before, after));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_modified_segment_is_recomputed_and_stale_entries_dropped() {
|
||||||
|
let (state, file) = state("data/simple.gpx");
|
||||||
|
let mut cache = StatisticsCache::default();
|
||||||
|
cache.update(Some(&state));
|
||||||
|
let old_rev = file.trk[0].trkseg[0].rev_id;
|
||||||
|
assert!(cache.map.contains_key(&old_rev));
|
||||||
|
|
||||||
|
let mut f = (*file).clone();
|
||||||
|
f.trk[0].trkseg[0].rev_id = Default::default();
|
||||||
|
let new_rev = f.trk[0].trkseg[0].rev_id;
|
||||||
|
let mut next = StackEntry::default();
|
||||||
|
next.insert(f.id, Rc::new(f));
|
||||||
|
cache.update(Some(&next));
|
||||||
|
assert!(cache.map.contains_key(&new_rev));
|
||||||
|
assert!(!cache.map.contains_key(&old_rev));
|
||||||
|
|
||||||
|
cache.update(None);
|
||||||
|
assert!(cache.map.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,449 @@
|
|||||||
|
// TODO license in every file
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Action, Apply, Command, CoordinatesCache, Diff, FileId, FileOrder, FileStructure,
|
||||||
|
FileStructureCache, Selection, Stack, State, StatisticsBuffer, StatisticsCache, TrackSegmentId,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Engine {
|
||||||
|
stack: Stack,
|
||||||
|
selection: Selection,
|
||||||
|
order: FileOrder,
|
||||||
|
statistics_cache: StatisticsCache,
|
||||||
|
coordinates_cache: CoordinatesCache,
|
||||||
|
structure_cache: FileStructureCache,
|
||||||
|
diff: Option<Diff>,
|
||||||
|
order_changed: bool,
|
||||||
|
selection_changed: bool,
|
||||||
|
statistics_buffer: StatisticsBuffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Engine {
|
||||||
|
pub fn statistics(&self) -> &StatisticsBuffer {
|
||||||
|
&self.statistics_buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Files in display order.
|
||||||
|
pub fn order(&self) -> &[FileId] {
|
||||||
|
&self.order.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Name, tracks, segments and waypoints (with their ids) of a file.
|
||||||
|
pub fn file_structure(&self, id: &FileId) -> Option<&FileStructure> {
|
||||||
|
self.structure_cache.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which files the last action added, removed or modified.
|
||||||
|
pub fn last_diff(&self) -> Option<&Diff> {
|
||||||
|
self.diff.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the last action changed the order of the files.
|
||||||
|
pub fn order_changed(&self) -> bool {
|
||||||
|
self.order_changed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Currently selected elements.
|
||||||
|
pub fn selection(&self) -> &Selection {
|
||||||
|
&self.selection
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the last action changed the selection.
|
||||||
|
pub fn selection_changed(&self) -> bool {
|
||||||
|
self.selection_changed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coordinates (`[lng, lat, ...]`) of the trackpoints of a segment.
|
||||||
|
pub fn segment_coordinates(&self, id: &TrackSegmentId) -> &[f64] {
|
||||||
|
self.coordinates_cache.segment(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coordinates (`[lng, lat, ...]`) of the waypoints of a file.
|
||||||
|
pub fn waypoint_coordinates(&self, id: &FileId) -> &[f64] {
|
||||||
|
self.coordinates_cache.waypoints(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single entry point for every action coming from the frontend. Returns whether anything
|
||||||
|
/// changed.
|
||||||
|
pub fn execute(&mut self, action: Action) -> bool {
|
||||||
|
let selection_before = self.selection.clone();
|
||||||
|
let order_before = self.order.0.clone();
|
||||||
|
// the files change iff there is a diff, but selection and order actions have none
|
||||||
|
self.diff = match action {
|
||||||
|
Action::Edit(command) => self.edit(command),
|
||||||
|
Action::Undo => self.stack.undo(),
|
||||||
|
Action::Redo => self.stack.redo(),
|
||||||
|
Action::Select { file_ids } => {
|
||||||
|
self.selection = Selection::File {
|
||||||
|
file_ids: file_ids.into_iter().collect(),
|
||||||
|
};
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Action::AddSelect { file_ids } => {
|
||||||
|
match &mut self.selection {
|
||||||
|
Selection::File { file_ids: ids } => ids.extend(file_ids),
|
||||||
|
selection => {
|
||||||
|
*selection = Selection::File {
|
||||||
|
file_ids: file_ids.into_iter().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Action::Reorder { file_ids, index } => {
|
||||||
|
self.order.move_files(&file_ids, index);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Action::SelectAll => {
|
||||||
|
self.selection = Selection::File {
|
||||||
|
file_ids: self.order.0.iter().copied().collect(),
|
||||||
|
};
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let changed = self.diff.is_some()
|
||||||
|
|| self.selection != selection_before
|
||||||
|
|| self.order.0 != order_before;
|
||||||
|
if changed {
|
||||||
|
self.refresh();
|
||||||
|
}
|
||||||
|
// after the refresh, which also syncs the order and selection with the files (undo, redo)
|
||||||
|
self.order_changed = self.order.0 != order_before;
|
||||||
|
self.selection_changed = self.selection != selection_before;
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edit(&mut self, command: Command) -> Option<Diff> {
|
||||||
|
self.stack.create_and_push_next(|files| {
|
||||||
|
let mut state = State {
|
||||||
|
files,
|
||||||
|
selection: &mut self.selection,
|
||||||
|
order: &mut self.order,
|
||||||
|
};
|
||||||
|
command.apply(&mut state).map_err(|err| err.to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Brings everything derived from the files back in line with the current stack entry.
|
||||||
|
fn refresh(&mut self) {
|
||||||
|
let current = self.stack.current();
|
||||||
|
match current {
|
||||||
|
Some(files) => {
|
||||||
|
// files coming back after an undo/redo are added at the end of the order
|
||||||
|
self.order.sync(files);
|
||||||
|
self.selection.retain_existing(files);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.order.0.clear();
|
||||||
|
self.selection = Selection::Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.statistics_cache.update(current);
|
||||||
|
self.coordinates_cache.update(current);
|
||||||
|
self.structure_cache.update(current, self.diff.as_ref());
|
||||||
|
self.statistics_buffer
|
||||||
|
.update(&self.statistics_cache.get(current, &self.selection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{Load, Metadata, New, NewTrack, Style};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn edit(engine: &mut Engine, command: Command) -> bool {
|
||||||
|
engine.execute(Action::Edit(command))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(engine: &mut Engine, name: &str) -> bool {
|
||||||
|
edit(engine, Command::New(New { name }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(engine: &mut Engine, path: &str) -> bool {
|
||||||
|
let data = std::fs::read(path).unwrap();
|
||||||
|
edit(engine, Command::Load(Load { data: &data }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected(engine: &Engine) -> Vec<FileId> {
|
||||||
|
match &engine.selection {
|
||||||
|
Selection::File { file_ids } => file_ids.iter().copied().collect(),
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_file() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
assert!(load(&mut engine, "data/simple.gpx"));
|
||||||
|
assert_eq!(engine.statistics_buffer.total_distance.len(), 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_file_has_no_statistics() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
assert!(new(&mut engine, "empty"));
|
||||||
|
assert_eq!(engine.stack.current().unwrap().len(), 1);
|
||||||
|
assert_eq!(engine.order.0.len(), 1);
|
||||||
|
assert!(engine.statistics().total_distance.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_statistics_follow_selection() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
assert!(load(&mut engine, "data/simple.gpx"));
|
||||||
|
let n = engine.statistics().total_distance.len();
|
||||||
|
assert!(n > 0);
|
||||||
|
|
||||||
|
// a newly created (empty) file becomes the selection
|
||||||
|
assert!(new(&mut engine, "empty"));
|
||||||
|
assert!(engine.statistics().total_distance.is_empty());
|
||||||
|
assert_eq!(engine.stack.current().unwrap().len(), 2);
|
||||||
|
|
||||||
|
// loading a file selects it again
|
||||||
|
assert!(load(&mut engine, "data/simple.gpx"));
|
||||||
|
assert_eq!(engine.statistics().total_distance.len(), n);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_load_is_rejected() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
assert!(!edit(
|
||||||
|
&mut engine,
|
||||||
|
Command::Load(Load {
|
||||||
|
data: b"<gpx><trk></gpx>"
|
||||||
|
})
|
||||||
|
));
|
||||||
|
assert!(engine.stack.current().is_none());
|
||||||
|
assert!(!engine.stack.can_undo());
|
||||||
|
assert!(engine.order.0.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_not_implemented_command_is_rejected() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
new(&mut engine, "file");
|
||||||
|
assert!(!edit(&mut engine, Command::Reverse(crate::Reverse)));
|
||||||
|
assert!(!engine.stack.can_redo());
|
||||||
|
assert_eq!(engine.stack.current().unwrap().len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_metadata_renames_selected_file() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
new(&mut engine, "before");
|
||||||
|
assert!(edit(
|
||||||
|
&mut engine,
|
||||||
|
Command::Metadata(Metadata {
|
||||||
|
name: "after",
|
||||||
|
desc: "about",
|
||||||
|
})
|
||||||
|
));
|
||||||
|
let file = engine.stack.current().unwrap().values().next().unwrap();
|
||||||
|
assert_eq!(file.info.name, "after");
|
||||||
|
assert_eq!(file.info.desc.as_deref(), Some("about"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_metadata_without_selection_does_not_modify_files() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
new(&mut engine, "file");
|
||||||
|
engine.selection = Selection::Empty;
|
||||||
|
edit(
|
||||||
|
&mut engine,
|
||||||
|
Command::Metadata(Metadata {
|
||||||
|
name: "renamed",
|
||||||
|
desc: "",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let file = engine.stack.current().unwrap().values().next().unwrap();
|
||||||
|
assert_eq!(file.info.name, "file");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_edit_changes_statistics_when_segments_change() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
load(&mut engine, "data/simple.gpx");
|
||||||
|
let before = engine
|
||||||
|
.statistics_cache
|
||||||
|
.get(engine.stack.current(), &engine.selection)[0] as *const _;
|
||||||
|
edit(&mut engine, Command::NewTrack(NewTrack));
|
||||||
|
edit(
|
||||||
|
&mut engine,
|
||||||
|
Command::Style(Style {
|
||||||
|
color: Some("ff0000"),
|
||||||
|
opacity: None,
|
||||||
|
width: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// styling does not touch the segments: their statistics are reused
|
||||||
|
let after = engine
|
||||||
|
.statistics_cache
|
||||||
|
.get(engine.stack.current(), &engine.selection)[0] as *const _;
|
||||||
|
assert!(std::ptr::eq(before, after));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_undo_redo_keep_selection_and_order_consistent() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
new(&mut engine, "a");
|
||||||
|
let a = selected(&engine)[0];
|
||||||
|
new(&mut engine, "b");
|
||||||
|
let b = selected(&engine)[0];
|
||||||
|
assert_eq!(engine.order.0, vec![a, b]);
|
||||||
|
|
||||||
|
assert!(engine.execute(Action::Undo));
|
||||||
|
assert_eq!(engine.order.0, vec![a]);
|
||||||
|
assert!(selected(&engine).is_empty());
|
||||||
|
|
||||||
|
assert!(engine.execute(Action::Redo));
|
||||||
|
assert_eq!(engine.order.0, vec![a, b]);
|
||||||
|
|
||||||
|
assert!(engine.execute(Action::Undo));
|
||||||
|
assert!(engine.execute(Action::Undo));
|
||||||
|
assert!(engine.order.0.is_empty());
|
||||||
|
assert!(!engine.execute(Action::Undo));
|
||||||
|
assert!(engine.stack.current().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reorder_is_not_undoable_and_returning_files_go_last() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
new(&mut engine, "a");
|
||||||
|
let a = engine.order.0[0];
|
||||||
|
new(&mut engine, "b");
|
||||||
|
let b = engine.order.0[1];
|
||||||
|
new(&mut engine, "c");
|
||||||
|
let c = engine.order.0[2];
|
||||||
|
|
||||||
|
assert!(engine.execute(Action::Reorder {
|
||||||
|
file_ids: vec![c],
|
||||||
|
index: 0
|
||||||
|
}));
|
||||||
|
assert_eq!(engine.order.0, vec![c, a, b]);
|
||||||
|
assert!(!engine.execute(Action::Reorder {
|
||||||
|
file_ids: vec![c],
|
||||||
|
index: 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
// undoing the creation of c does not undo the reorder
|
||||||
|
assert!(engine.execute(Action::Undo));
|
||||||
|
assert_eq!(engine.order.0, vec![a, b]);
|
||||||
|
// c comes back at the end
|
||||||
|
assert!(engine.execute(Action::Redo));
|
||||||
|
assert_eq!(engine.order.0, vec![a, b, c]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_structures_and_coordinates_follow_actions() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
assert!(engine.order().is_empty());
|
||||||
|
load(&mut engine, "data/simple.gpx");
|
||||||
|
let loaded_id = engine.order()[0];
|
||||||
|
let diff = engine.last_diff().unwrap();
|
||||||
|
assert_eq!(diff.added, vec![loaded_id]);
|
||||||
|
assert!(engine.order_changed());
|
||||||
|
|
||||||
|
new(&mut engine, "empty");
|
||||||
|
let empty_id = engine.order()[1];
|
||||||
|
// only the new file is reported, the other one is not recomputed
|
||||||
|
let diff = engine.last_diff().unwrap();
|
||||||
|
assert_eq!(diff.added, vec![empty_id]);
|
||||||
|
assert!(diff.modified.is_empty() && diff.removed.is_empty());
|
||||||
|
assert_eq!(engine.file_structure(&empty_id).unwrap().name, "empty");
|
||||||
|
|
||||||
|
let structure = engine.file_structure(&loaded_id).unwrap();
|
||||||
|
let seg = &structure.tracks[0].segments[0];
|
||||||
|
let (seg_id, len) = (seg.id, seg.len);
|
||||||
|
assert_eq!(engine.segment_coordinates(&seg_id).len(), len * 2);
|
||||||
|
assert!(engine.waypoint_coordinates(&loaded_id).is_empty());
|
||||||
|
|
||||||
|
// reordering only changes the order
|
||||||
|
assert!(engine.execute(Action::Reorder {
|
||||||
|
file_ids: vec![loaded_id],
|
||||||
|
index: 1
|
||||||
|
}));
|
||||||
|
assert_eq!(engine.order(), [empty_id, loaded_id]);
|
||||||
|
assert!(engine.last_diff().is_none());
|
||||||
|
assert!(engine.order_changed());
|
||||||
|
|
||||||
|
// an action that changes nothing reports nothing
|
||||||
|
assert!(!engine.execute(Action::Reorder {
|
||||||
|
file_ids: vec![loaded_id],
|
||||||
|
index: 1
|
||||||
|
}));
|
||||||
|
assert!(engine.last_diff().is_none());
|
||||||
|
assert!(!engine.order_changed());
|
||||||
|
|
||||||
|
// undo removes the empty file, redoing the load removes the other one too
|
||||||
|
assert!(engine.execute(Action::Undo));
|
||||||
|
let diff = engine.last_diff().unwrap();
|
||||||
|
assert_eq!(diff.removed, vec![empty_id]);
|
||||||
|
assert!(engine.order_changed());
|
||||||
|
assert!(engine.file_structure(&empty_id).is_none());
|
||||||
|
assert!(engine.execute(Action::Undo));
|
||||||
|
assert!(engine.order().is_empty());
|
||||||
|
assert!(engine.file_structure(&loaded_id).is_none());
|
||||||
|
assert!(engine.segment_coordinates(&seg_id).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_selection_changed_flag() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
new(&mut engine, "a");
|
||||||
|
let a = engine.order()[0];
|
||||||
|
// a new file is selected by the edit
|
||||||
|
assert!(engine.selection_changed());
|
||||||
|
assert!(!engine.execute(Action::Select { file_ids: vec![a] }));
|
||||||
|
assert!(!engine.selection_changed());
|
||||||
|
assert!(engine.execute(Action::Select { file_ids: vec![] }));
|
||||||
|
assert!(engine.selection_changed());
|
||||||
|
// undoing drops the selected file from the selection
|
||||||
|
engine.execute(Action::Select { file_ids: vec![a] });
|
||||||
|
assert!(engine.execute(Action::Undo));
|
||||||
|
assert!(engine.selection_changed());
|
||||||
|
assert_eq!(engine.selection(), &Selection::Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_undo_updates_statistics() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
load(&mut engine, "data/simple.gpx");
|
||||||
|
assert!(!engine.statistics().total_distance.is_empty());
|
||||||
|
engine.execute(Action::Undo);
|
||||||
|
assert!(engine.statistics().total_distance.is_empty());
|
||||||
|
engine.execute(Action::Redo);
|
||||||
|
engine.execute(Action::SelectAll);
|
||||||
|
assert_eq!(engine.statistics().total_distance.len(), 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_selection_actions() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
new(&mut engine, "a");
|
||||||
|
let a = selected(&engine)[0];
|
||||||
|
new(&mut engine, "b");
|
||||||
|
let b = selected(&engine)[0];
|
||||||
|
assert_eq!(selected(&engine), vec![b]);
|
||||||
|
|
||||||
|
assert!(engine.execute(Action::Select { file_ids: vec![a] }));
|
||||||
|
assert_eq!(selected(&engine), vec![a]);
|
||||||
|
|
||||||
|
assert!(engine.execute(Action::AddSelect { file_ids: vec![b] }));
|
||||||
|
assert_eq!(selected(&engine).len(), 2);
|
||||||
|
|
||||||
|
engine.execute(Action::Select { file_ids: vec![] });
|
||||||
|
assert!(engine.execute(Action::SelectAll));
|
||||||
|
assert_eq!(selected(&engine).len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_selection_does_not_create_undo_steps() {
|
||||||
|
let mut engine = Engine::default();
|
||||||
|
new(&mut engine, "a");
|
||||||
|
engine.execute(Action::SelectAll);
|
||||||
|
engine.execute(Action::Undo);
|
||||||
|
assert!(engine.stack.current().is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
mod action;
|
||||||
|
mod command;
|
||||||
|
mod derived;
|
||||||
|
mod engine;
|
||||||
|
mod state;
|
||||||
|
|
||||||
|
pub use action::*;
|
||||||
|
pub use command::*;
|
||||||
|
pub use derived::*;
|
||||||
|
pub use engine::*;
|
||||||
|
pub use state::*;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
mod order;
|
||||||
|
mod selection;
|
||||||
|
mod stack;
|
||||||
|
|
||||||
|
pub use order::*;
|
||||||
|
pub use selection::*;
|
||||||
|
pub use stack::*;
|
||||||
|
|
||||||
|
pub struct State<'a> {
|
||||||
|
pub files: &'a mut StackEntry,
|
||||||
|
pub selection: &'a mut Selection,
|
||||||
|
pub order: &'a mut FileOrder,
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::{FileId, StackEntry as Files};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct FileOrder(pub Vec<FileId>);
|
||||||
|
|
||||||
|
impl FileOrder {
|
||||||
|
/// Moves the given files, as a block and in the given order, so that they start at `index`
|
||||||
|
/// among the other files (clamped to the end). Unknown ids are ignored. Returns whether the
|
||||||
|
/// order changed.
|
||||||
|
pub fn move_files(&mut self, file_ids: &[FileId], index: usize) -> bool {
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
let moved: Vec<FileId> = file_ids
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|id| self.0.contains(id) && seen.insert(*id))
|
||||||
|
.collect();
|
||||||
|
let mut order: Vec<FileId> = self
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|id| !seen.contains(id))
|
||||||
|
.collect();
|
||||||
|
let index = index.min(order.len());
|
||||||
|
order.splice(index..index, moved);
|
||||||
|
let changed = order != self.0;
|
||||||
|
self.0 = order;
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drops the files that do not exist anymore and appends the new ones at the end.
|
||||||
|
pub fn sync(&mut self, files: &Files) {
|
||||||
|
self.0.retain(|id| files.contains_key(id));
|
||||||
|
let known: HashSet<FileId> = self.0.iter().copied().collect();
|
||||||
|
self.0
|
||||||
|
.extend(files.keys().filter(|id| !known.contains(id)).copied());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn ids() -> [FileId; 3] {
|
||||||
|
[FileId::default(), FileId::default(), FileId::default()]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_files_as_block_in_given_order() {
|
||||||
|
let [a, b, c] = ids();
|
||||||
|
let mut order = FileOrder(vec![a, b, c]);
|
||||||
|
assert!(order.move_files(&[c, a], 1));
|
||||||
|
// index counts among the files that are not moved
|
||||||
|
assert_eq!(order.0, vec![b, c, a]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_files_start_and_clamped_end() {
|
||||||
|
let [a, b, c] = ids();
|
||||||
|
let mut order = FileOrder(vec![a, b, c]);
|
||||||
|
assert!(order.move_files(&[c], 0));
|
||||||
|
assert_eq!(order.0, vec![c, a, b]);
|
||||||
|
assert!(order.move_files(&[c], 100));
|
||||||
|
assert_eq!(order.0, vec![a, b, c]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_files_ignores_unknown_and_duplicate_ids() {
|
||||||
|
let [a, b, c] = ids();
|
||||||
|
let mut order = FileOrder(vec![a, b, c]);
|
||||||
|
assert!(order.move_files(&[FileId::default(), b, b], 0));
|
||||||
|
assert_eq!(order.0, vec![b, a, c]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_files_unchanged() {
|
||||||
|
let [a, b, c] = ids();
|
||||||
|
let mut order = FileOrder(vec![a, b, c]);
|
||||||
|
assert!(!order.move_files(&[], 0));
|
||||||
|
assert!(!order.move_files(&[a, b], 0));
|
||||||
|
assert!(!order.move_files(&[FileId::default()], 0));
|
||||||
|
assert_eq!(order.0, vec![a, b, c]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sync_drops_missing_and_appends_new_at_the_end() {
|
||||||
|
let [a, b, c] = ids();
|
||||||
|
let mut files = Files::default();
|
||||||
|
for id in [b, c] {
|
||||||
|
files.insert(id, Default::default());
|
||||||
|
}
|
||||||
|
let mut order = FileOrder(vec![a, b]);
|
||||||
|
order.sync(&files);
|
||||||
|
assert_eq!(order.0, vec![b, c]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::{FileId, StackEntry, TrackId, TrackSegmentId, WaypointId};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Selection {
|
||||||
|
#[default]
|
||||||
|
Empty,
|
||||||
|
File {
|
||||||
|
file_ids: HashSet<FileId>,
|
||||||
|
},
|
||||||
|
Track {
|
||||||
|
file_id: FileId,
|
||||||
|
trk_ids: HashSet<TrackId>,
|
||||||
|
},
|
||||||
|
TrackSegment {
|
||||||
|
file_id: FileId,
|
||||||
|
trk_id: TrackId,
|
||||||
|
trkseg_ids: HashSet<TrackSegmentId>,
|
||||||
|
},
|
||||||
|
Waypoints {
|
||||||
|
file_id: FileId,
|
||||||
|
},
|
||||||
|
Waypoint {
|
||||||
|
file_id: FileId,
|
||||||
|
wpt_ids: HashSet<WaypointId>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Selection {
|
||||||
|
/// Drops the files that do not exist anymore (e.g. after an undo).
|
||||||
|
pub fn retain_existing(&mut self, files: &StackEntry) {
|
||||||
|
match self {
|
||||||
|
Selection::Empty => {}
|
||||||
|
Selection::File { file_ids } => {
|
||||||
|
file_ids.retain(|id| files.contains_key(id));
|
||||||
|
if file_ids.is_empty() {
|
||||||
|
*self = Selection::Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selection::Track { file_id, .. }
|
||||||
|
| Selection::TrackSegment { file_id, .. }
|
||||||
|
| Selection::Waypoints { file_id }
|
||||||
|
| Selection::Waypoint { file_id, .. } => {
|
||||||
|
if !files.contains_key(file_id) {
|
||||||
|
*self = Selection::Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::File;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_retain_existing() {
|
||||||
|
let file = File::default();
|
||||||
|
let (kept, gone) = (file.id, FileId::default());
|
||||||
|
let mut files = StackEntry::default();
|
||||||
|
files.insert(kept, Rc::new(file));
|
||||||
|
|
||||||
|
let mut selection = Selection::File {
|
||||||
|
file_ids: [kept, gone].into(),
|
||||||
|
};
|
||||||
|
selection.retain_existing(&files);
|
||||||
|
assert!(
|
||||||
|
matches!(&selection, Selection::File { file_ids } if file_ids.len() == 1 && file_ids.contains(&kept))
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut selection = Selection::File {
|
||||||
|
file_ids: [gone].into(),
|
||||||
|
};
|
||||||
|
selection.retain_existing(&files);
|
||||||
|
assert!(matches!(selection, Selection::Empty));
|
||||||
|
|
||||||
|
let mut selection = Selection::Waypoints { file_id: kept };
|
||||||
|
selection.retain_existing(&files);
|
||||||
|
assert!(matches!(selection, Selection::Waypoints { .. }));
|
||||||
|
let mut selection = Selection::Waypoints { file_id: gone };
|
||||||
|
selection.retain_existing(&files);
|
||||||
|
assert!(matches!(selection, Selection::Empty));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{File, FileId};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Stack {
|
||||||
|
entries: Vec<StackEntry>,
|
||||||
|
index: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stack {
|
||||||
|
pub fn current(&self) -> Option<&StackEntry> {
|
||||||
|
self.index.map_or_default(|i| self.get(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_and_push_next<F>(&mut self, f: F) -> Option<Diff>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut StackEntry) -> Result<(), String>,
|
||||||
|
{
|
||||||
|
self.record_diff(|stack| {
|
||||||
|
let mut next = stack.current().map_or_default(|c| c.clone());
|
||||||
|
if f(&mut next).is_ok() {
|
||||||
|
stack.push(next);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn undo(&mut self) -> Option<Diff> {
|
||||||
|
self.record_diff(|stack| {
|
||||||
|
if let Some(i) = stack.index {
|
||||||
|
if i == 0 {
|
||||||
|
stack.index = None;
|
||||||
|
} else {
|
||||||
|
stack.index = Some(i - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn redo(&mut self) -> Option<Diff> {
|
||||||
|
self.record_diff(|stack| match stack.index {
|
||||||
|
Some(i) => {
|
||||||
|
if i + 1 < stack.entries.len() {
|
||||||
|
stack.index = Some(i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
if !stack.entries.is_empty() {
|
||||||
|
stack.index = Some(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_undo(&self) -> bool {
|
||||||
|
self.index.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_redo(&self) -> bool {
|
||||||
|
match self.index {
|
||||||
|
Some(i) => i + 1 < self.entries.len(),
|
||||||
|
None => !self.entries.is_empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, index: usize) -> Option<&StackEntry> {
|
||||||
|
self.entries.get(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&mut self, entry: StackEntry) {
|
||||||
|
if let Some(i) = self.index {
|
||||||
|
self.entries.truncate(i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.entries.push(entry);
|
||||||
|
self.index = Some(self.entries.len() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_diff<F>(&mut self, f: F) -> Option<Diff>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Self),
|
||||||
|
{
|
||||||
|
let prev = self.index;
|
||||||
|
f(self);
|
||||||
|
let cur = self.index;
|
||||||
|
if prev == cur {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let prev = prev.map_or_default(|i| self.get(i));
|
||||||
|
let cur = cur.map_or_default(|i| self.get(i));
|
||||||
|
let prev_ids: HashSet<FileId> = prev.map_or_default(|e| e.keys().copied().collect());
|
||||||
|
let cur_ids: HashSet<FileId> = cur.map_or_default(|e| e.keys().copied().collect());
|
||||||
|
let mut modified = vec![];
|
||||||
|
for id in prev_ids.intersection(&cur_ids) {
|
||||||
|
let before = prev.map_or_default(|e| e.get(id));
|
||||||
|
let after = cur.map_or_default(|e| e.get(id));
|
||||||
|
if !before.zip(after).is_some_and(|(b, a)| Rc::ptr_eq(b, a)) && before != after {
|
||||||
|
modified.push(*id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Diff {
|
||||||
|
added: cur_ids.difference(&prev_ids).copied().collect(),
|
||||||
|
removed: prev_ids.difference(&cur_ids).copied().collect(),
|
||||||
|
modified,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type StackEntry = HashMap<FileId, Rc<File>>;
|
||||||
|
|
||||||
|
/// Which files were added, removed or modified by a change of the current stack entry.
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Diff {
|
||||||
|
pub added: Vec<FileId>,
|
||||||
|
pub removed: Vec<FileId>,
|
||||||
|
pub modified: Vec<FileId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_new_file() {
|
||||||
|
let mut stack = Stack::default();
|
||||||
|
|
||||||
|
assert!(!stack.can_undo());
|
||||||
|
assert!(!stack.can_redo());
|
||||||
|
assert!(stack.current().is_none());
|
||||||
|
|
||||||
|
let diff = stack.create_and_push_next(|e| {
|
||||||
|
let file = Rc::new(File::default());
|
||||||
|
e.insert(file.id, file);
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(stack.can_undo());
|
||||||
|
assert!(!stack.can_redo());
|
||||||
|
assert!(stack.current().is_some());
|
||||||
|
|
||||||
|
assert!(diff.is_some());
|
||||||
|
|
||||||
|
let diff = diff.unwrap();
|
||||||
|
assert_eq!(diff.added.len(), 1);
|
||||||
|
assert!(diff.removed.is_empty());
|
||||||
|
assert!(diff.modified.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_file(stack: &mut Stack) -> FileId {
|
||||||
|
let file = Rc::new(File::default());
|
||||||
|
let id = file.id;
|
||||||
|
stack
|
||||||
|
.create_and_push_next(|e| {
|
||||||
|
e.insert(id, file);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_failed_command_pushes_nothing() {
|
||||||
|
let mut stack = Stack::default();
|
||||||
|
let diff = stack.create_and_push_next(|e| {
|
||||||
|
e.insert(FileId::default(), Rc::new(File::default()));
|
||||||
|
Err("nope".to_string())
|
||||||
|
});
|
||||||
|
assert!(diff.is_none());
|
||||||
|
assert!(!stack.can_undo());
|
||||||
|
assert!(stack.current().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_undo_redo() {
|
||||||
|
let mut stack = Stack::default();
|
||||||
|
assert!(stack.undo().is_none());
|
||||||
|
assert!(stack.redo().is_none());
|
||||||
|
|
||||||
|
let id = add_file(&mut stack);
|
||||||
|
|
||||||
|
let diff = stack.undo().unwrap();
|
||||||
|
assert_eq!(diff.removed, vec![id]);
|
||||||
|
assert!(diff.added.is_empty());
|
||||||
|
assert!(stack.current().is_none());
|
||||||
|
assert!(!stack.can_undo());
|
||||||
|
assert!(stack.can_redo());
|
||||||
|
assert!(stack.undo().is_none());
|
||||||
|
|
||||||
|
let diff = stack.redo().unwrap();
|
||||||
|
assert_eq!(diff.added, vec![id]);
|
||||||
|
assert!(stack.current().unwrap().contains_key(&id));
|
||||||
|
assert!(stack.can_undo());
|
||||||
|
assert!(!stack.can_redo());
|
||||||
|
assert!(stack.redo().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_modified_and_removed() {
|
||||||
|
let mut stack = Stack::default();
|
||||||
|
let id = add_file(&mut stack);
|
||||||
|
|
||||||
|
let diff = stack
|
||||||
|
.create_and_push_next(|e| {
|
||||||
|
let mut file = (**e.get(&id).unwrap()).clone();
|
||||||
|
file.info.name = "renamed".to_string();
|
||||||
|
e.insert(id, Rc::new(file));
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(diff.modified, vec![id]);
|
||||||
|
assert!(diff.added.is_empty() && diff.removed.is_empty());
|
||||||
|
|
||||||
|
// an unchanged file is not reported as modified
|
||||||
|
let diff = stack.create_and_push_next(|_| Ok(())).unwrap();
|
||||||
|
assert!(diff.modified.is_empty());
|
||||||
|
|
||||||
|
let diff = stack
|
||||||
|
.create_and_push_next(|e| {
|
||||||
|
e.remove(&id);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(diff.removed, vec![id]);
|
||||||
|
assert!(stack.current().unwrap().is_empty());
|
||||||
|
|
||||||
|
// going back restores the renamed file
|
||||||
|
stack.undo();
|
||||||
|
assert_eq!(stack.current().unwrap()[&id].info.name, "renamed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_push_after_undo_drops_redo_branch() {
|
||||||
|
let mut stack = Stack::default();
|
||||||
|
let first = add_file(&mut stack);
|
||||||
|
let second = add_file(&mut stack);
|
||||||
|
|
||||||
|
stack.undo();
|
||||||
|
assert!(stack.can_redo());
|
||||||
|
assert!(!stack.current().unwrap().contains_key(&second));
|
||||||
|
|
||||||
|
let third = add_file(&mut stack);
|
||||||
|
assert!(!stack.can_redo());
|
||||||
|
assert!(stack.redo().is_none());
|
||||||
|
let current = stack.current().unwrap();
|
||||||
|
assert!(current.contains_key(&first));
|
||||||
|
assert!(current.contains_key(&third));
|
||||||
|
assert!(!current.contains_key(&second));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
mod core;
|
||||||
|
mod engine;
|
||||||
|
|
||||||
|
pub use core::*;
|
||||||
|
pub use engine::*;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "gpx-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
console_error_panic_hook = "0.1.7"
|
||||||
|
gpx-engine = { path = "../engine" }
|
||||||
|
js-sys = "0.3.106"
|
||||||
|
uuid = { version = "1.26.1", features = ["js"] }
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
@@ -0,0 +1,552 @@
|
|||||||
|
//! Frontier between the SvelteKit frontend and the engine.
|
||||||
|
//!
|
||||||
|
//! Conventions, chosen to keep calls cheap:
|
||||||
|
//! - Coordinates and elevations cross as `Float64Array` (`&[f64]`): one memcpy into linear
|
||||||
|
//! memory, no per-point calls. `lng`/`lat`/`ele` arrays must have the same length.
|
||||||
|
//! - File bytes cross as `Uint8Array` (`&[u8]`), strings as `&str`.
|
||||||
|
//! - File ids cross as one flat `Uint8Array` of concatenated 16-byte UUIDs (no `Array<string>`).
|
||||||
|
//! - Rectangles cross as four numbers instead of an object.
|
||||||
|
//! - Ids read from the file tree are hyphenated UUID strings; the functions reading buffers take
|
||||||
|
//! them as such.
|
||||||
|
//! - Every function returns `false` when its arguments are invalid or the command did nothing.
|
||||||
|
use std::cell::RefCell;
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use gpx_engine::{self as engine, Action, Command, Engine, FileId, LngLat, LngLatBounds};
|
||||||
|
use js_sys::{Array, Float64Array, Int32Array, Object, Reflect};
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum MergeType {
|
||||||
|
Connect,
|
||||||
|
Group,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum CleanType {
|
||||||
|
Inside,
|
||||||
|
Outside,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MergeType> for engine::MergeType {
|
||||||
|
fn from(t: MergeType) -> Self {
|
||||||
|
match t {
|
||||||
|
MergeType::Connect => Self::Connect,
|
||||||
|
MergeType::Group => Self::Group,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CleanType> for engine::CleanType {
|
||||||
|
fn from(t: CleanType) -> Self {
|
||||||
|
match t {
|
||||||
|
CleanType::Inside => Self::Inside,
|
||||||
|
CleanType::Outside => Self::Outside,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static ENGINE: RefCell<Option<Engine>> = const { RefCell::new(None) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute(action: Action) -> bool {
|
||||||
|
ENGINE.with(|engine| {
|
||||||
|
engine
|
||||||
|
.borrow_mut()
|
||||||
|
.as_mut()
|
||||||
|
.is_some_and(|engine| engine.execute(action))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edit(command: Command) -> bool {
|
||||||
|
execute(Action::Edit(command))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes concatenated 16-byte UUIDs.
|
||||||
|
fn file_ids(bytes: &[u8]) -> Option<Vec<FileId>> {
|
||||||
|
let (chunks, rest) = bytes.as_chunks::<16>();
|
||||||
|
rest.is_empty().then(|| {
|
||||||
|
chunks
|
||||||
|
.iter()
|
||||||
|
.map(|c| FileId(uuid::Uuid::from_bytes(*c)))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn same_len(a: &[f64], b: &[f64], c: &[f64]) -> bool {
|
||||||
|
a.len() == b.len() && b.len() == c.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn start() {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
ENGINE.with(|engine| {
|
||||||
|
*engine.borrow_mut() = Some(Engine::default());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statistics buffers
|
||||||
|
//
|
||||||
|
// These are views into wasm memory: they are invalidated by the next command (and any
|
||||||
|
// allocation), so read or copy them right away.
|
||||||
|
|
||||||
|
fn with_stats<T>(f: impl FnOnce(&engine::StatisticsBuffer) -> T) -> Option<T> {
|
||||||
|
ENGINE.with(|engine| engine.borrow().as_ref().map(|e| f(e.statistics())))
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! stats_getter {
|
||||||
|
($name:ident, $array:ident) => {
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn $name() -> $array {
|
||||||
|
with_stats(|s| unsafe { $array::view(&s.$name) })
|
||||||
|
.unwrap_or_else(|| $array::new_with_length(0))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
stats_getter!(total_distance, Float64Array);
|
||||||
|
stats_getter!(moving_distance, Float64Array);
|
||||||
|
stats_getter!(total_time, Int32Array);
|
||||||
|
stats_getter!(moving_time, Int32Array);
|
||||||
|
stats_getter!(speed, Float64Array);
|
||||||
|
stats_getter!(elevation_gain, Float64Array);
|
||||||
|
stats_getter!(elevation_loss, Float64Array);
|
||||||
|
stats_getter!(slope, Float64Array);
|
||||||
|
stats_getter!(slope_segment_slope, Float64Array);
|
||||||
|
stats_getter!(slope_segment_distance, Float64Array);
|
||||||
|
|
||||||
|
// File commands
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn new_file(name: &str) -> bool {
|
||||||
|
edit(Command::New(engine::New { name }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn load_file(data: &[u8]) -> bool {
|
||||||
|
edit(Command::Load(engine::Load { data }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn duplicate() -> bool {
|
||||||
|
edit(Command::Duplicate(engine::Duplicate))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn delete() -> bool {
|
||||||
|
edit(Command::Delete(engine::Delete))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn delete_all() -> bool {
|
||||||
|
edit(Command::DeleteAll(engine::DeleteAll))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the files to `index` among the other files, in the given order (not undoable).
|
||||||
|
/// `file_ids_bytes`: concatenated 16-byte UUIDs.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn reorder(file_ids_bytes: &[u8], index: usize) -> bool {
|
||||||
|
file_ids(file_ids_bytes).is_some_and(|file_ids| execute(Action::Reorder { file_ids, index }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit commands
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn metadata(name: &str, desc: &str) -> bool {
|
||||||
|
edit(Command::Metadata(engine::Metadata { name, desc }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn style(color: Option<String>, opacity: Option<f64>, width: Option<f64>) -> bool {
|
||||||
|
edit(Command::Style(engine::Style {
|
||||||
|
color: color.as_deref(),
|
||||||
|
opacity,
|
||||||
|
width,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn new_track() -> bool {
|
||||||
|
edit(Command::NewTrack(engine::NewTrack))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn new_track_segment() -> bool {
|
||||||
|
edit(Command::NewTrackSegment(engine::NewTrackSegment))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn reverse() -> bool {
|
||||||
|
edit(Command::Reverse(engine::Reverse))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn splice_trackpoints(start: u32, end: u32, lng: &[f64], lat: &[f64], ele: &[f64]) -> bool {
|
||||||
|
start <= end
|
||||||
|
&& same_len(lng, lat, ele)
|
||||||
|
&& edit(Command::SpliceTrackpoints(engine::SpliceTrackpoints {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
lng,
|
||||||
|
lat,
|
||||||
|
ele,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn new_waypoint(
|
||||||
|
lng: f64,
|
||||||
|
lat: f64,
|
||||||
|
ele: f64,
|
||||||
|
name: &str,
|
||||||
|
desc: &str,
|
||||||
|
icon: &str,
|
||||||
|
link: &str,
|
||||||
|
) -> bool {
|
||||||
|
edit(Command::NewWaypoint(engine::NewWaypoint {
|
||||||
|
lng,
|
||||||
|
lat,
|
||||||
|
ele,
|
||||||
|
name,
|
||||||
|
desc,
|
||||||
|
icon,
|
||||||
|
link,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn move_waypoint(lng: f64, lat: f64, ele: f64) -> bool {
|
||||||
|
edit(Command::MoveWaypoint(engine::MoveWaypoint {
|
||||||
|
lng,
|
||||||
|
lat,
|
||||||
|
ele,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn crop(start: u32, end: u32) -> bool {
|
||||||
|
start <= end && edit(Command::Crop(engine::Crop { start, end }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn split(at: u32) -> bool {
|
||||||
|
edit(Command::Split(engine::Split { at }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn time() -> bool {
|
||||||
|
edit(Command::Time(engine::Time))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn merge(type_: MergeType) -> bool {
|
||||||
|
edit(Command::Merge(engine::Merge {
|
||||||
|
type_: type_.into(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn extract() -> bool {
|
||||||
|
edit(Command::Extract(engine::Extract))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn elevation(ele: &[f64]) -> bool {
|
||||||
|
edit(Command::Elevation(engine::Elevation { ele }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn clean(
|
||||||
|
west: f64,
|
||||||
|
south: f64,
|
||||||
|
east: f64,
|
||||||
|
north: f64,
|
||||||
|
type_: CleanType,
|
||||||
|
trkpt: bool,
|
||||||
|
wpt: bool,
|
||||||
|
) -> bool {
|
||||||
|
edit(Command::Clean(engine::Clean {
|
||||||
|
bounds: LngLatBounds {
|
||||||
|
sw: LngLat {
|
||||||
|
lng: west,
|
||||||
|
lat: south,
|
||||||
|
},
|
||||||
|
ne: LngLat {
|
||||||
|
lng: east,
|
||||||
|
lat: north,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type_: type_.into(),
|
||||||
|
trkpt,
|
||||||
|
wpt,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo-redo
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn undo() -> bool {
|
||||||
|
execute(Action::Undo)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn redo() -> bool {
|
||||||
|
execute(Action::Redo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selection
|
||||||
|
|
||||||
|
/// `file_ids_bytes`: concatenated 16-byte UUIDs.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn select(file_ids_bytes: &[u8]) -> bool {
|
||||||
|
file_ids(file_ids_bytes).is_some_and(|file_ids| execute(Action::Select { file_ids }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `file_ids_bytes`: concatenated 16-byte UUIDs.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn add_select(file_ids_bytes: &[u8]) -> bool {
|
||||||
|
file_ids(file_ids_bytes).is_some_and(|file_ids| execute(Action::AddSelect { file_ids }))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn select_all() -> bool {
|
||||||
|
execute(Action::SelectAll)
|
||||||
|
}
|
||||||
|
|
||||||
|
// File order and structures
|
||||||
|
//
|
||||||
|
// The files are described by one ordered list of ids and one structure object per file, so that
|
||||||
|
// the UI can react to what changed only: after each action, `last_update` tells which
|
||||||
|
// structures to (re)read or drop and whether the order changed. Ids are UUID strings. The
|
||||||
|
// trackpoints of a segment and the waypoints of a file are in the same order as the coordinates
|
||||||
|
// of their buffers (see below).
|
||||||
|
|
||||||
|
#[wasm_bindgen(typescript_custom_section)]
|
||||||
|
const FILE_STRUCTURE_TS: &str = r#"
|
||||||
|
export type Selection =
|
||||||
|
| { type: 'empty' }
|
||||||
|
| { type: 'file'; fileIds: string[] }
|
||||||
|
| { type: 'track'; fileId: string; trackIds: string[] }
|
||||||
|
| { type: 'segment'; fileId: string; trackId: string; segmentIds: string[] }
|
||||||
|
| { type: 'waypoints'; fileId: string }
|
||||||
|
| { type: 'waypoint'; fileId: string; waypointIds: string[] };
|
||||||
|
export interface FilesUpdate {
|
||||||
|
orderChanged: boolean;
|
||||||
|
selectionChanged: boolean;
|
||||||
|
/** Files to read the structure of. */
|
||||||
|
added: string[];
|
||||||
|
/** Files whose structure changed: reread it. */
|
||||||
|
modified: string[];
|
||||||
|
/** Files that do not exist anymore: drop their structure. */
|
||||||
|
removed: string[];
|
||||||
|
}
|
||||||
|
export interface FileStructure {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
tracks: TrackNode[];
|
||||||
|
waypoints: WaypointNode[];
|
||||||
|
/** Changes when the waypoints of the file change: refetch their coordinates. */
|
||||||
|
waypointsRev: string;
|
||||||
|
}
|
||||||
|
export interface TrackNode {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
segments: SegmentNode[];
|
||||||
|
}
|
||||||
|
export interface SegmentNode {
|
||||||
|
id: string;
|
||||||
|
/** Changes when the trackpoints of the segment change: refetch their coordinates. */
|
||||||
|
rev: string;
|
||||||
|
/** Number of trackpoints. */
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
export interface WaypointNode {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
extern "C" {
|
||||||
|
#[wasm_bindgen(typescript_type = "Selection")]
|
||||||
|
pub type Selection;
|
||||||
|
#[wasm_bindgen(typescript_type = "FilesUpdate")]
|
||||||
|
pub type FilesUpdate;
|
||||||
|
#[wasm_bindgen(typescript_type = "FileStructure | undefined")]
|
||||||
|
pub type FileStructure;
|
||||||
|
#[wasm_bindgen(typescript_type = "string[]")]
|
||||||
|
pub type FileOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(object: &Object, key: &str, value: impl Into<JsValue>) {
|
||||||
|
Reflect::set(object, &key.into(), &value.into()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn array<T>(items: &[T], f: impl Fn(&T) -> JsValue) -> Array {
|
||||||
|
items.iter().map(f).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ids(items: &[FileId]) -> Array {
|
||||||
|
array(items, |id| id.0.to_string().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn named_node(id: uuid::Uuid, name: Option<&str>) -> Object {
|
||||||
|
let node = Object::new();
|
||||||
|
set(&node, "id", id.to_string());
|
||||||
|
if let Some(name) = name {
|
||||||
|
set(&node, "name", name);
|
||||||
|
}
|
||||||
|
node
|
||||||
|
}
|
||||||
|
|
||||||
|
fn structure_object(file: &engine::FileStructure) -> Object {
|
||||||
|
let node = named_node(file.id.0, Some(&file.name));
|
||||||
|
set(
|
||||||
|
&node,
|
||||||
|
"tracks",
|
||||||
|
array(&file.tracks, |trk| {
|
||||||
|
let node = named_node(trk.id.0, trk.name.as_deref());
|
||||||
|
set(
|
||||||
|
&node,
|
||||||
|
"segments",
|
||||||
|
array(&trk.segments, |seg| {
|
||||||
|
let node = named_node(seg.id.0, None);
|
||||||
|
set(&node, "rev", seg.rev_id.0.to_string());
|
||||||
|
set(&node, "length", seg.len as f64);
|
||||||
|
node.into()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
node.into()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
set(
|
||||||
|
&node,
|
||||||
|
"waypoints",
|
||||||
|
array(&file.waypoints, |wpt| {
|
||||||
|
named_node(wpt.id.0, wpt.name.as_deref()).into()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
set(&node, "waypointsRev", file.wpt_rev_id.0.to_string());
|
||||||
|
node
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_engine<T>(f: impl FnOnce(&Engine) -> T) -> Option<T> {
|
||||||
|
ENGINE.with(|engine| engine.borrow().as_ref().map(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ids of the files in display order.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn file_order() -> FileOrder {
|
||||||
|
let order = with_engine(|e| ids(e.order())).unwrap_or_default();
|
||||||
|
order.unchecked_into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structure of a file, `undefined` if the id is unknown.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn file_structure(file_id: &str) -> FileStructure {
|
||||||
|
let structure = uuid::Uuid::parse_str(file_id)
|
||||||
|
.ok()
|
||||||
|
.and_then(|id| {
|
||||||
|
with_engine(|e| e.file_structure(&FileId(id)).map(structure_object)).flatten()
|
||||||
|
})
|
||||||
|
.map_or(JsValue::UNDEFINED, JsValue::from);
|
||||||
|
structure.unchecked_into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uuids<T>(items: impl IntoIterator<Item = T>, uuid: impl Fn(T) -> uuid::Uuid) -> Array {
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| JsValue::from(uuid(item).to_string()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current selection (ids are UUID strings).
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn selection() -> Selection {
|
||||||
|
use engine::Selection as S;
|
||||||
|
let object = Object::new();
|
||||||
|
with_engine(|e| match e.selection() {
|
||||||
|
S::Empty => set(&object, "type", "empty"),
|
||||||
|
S::File { file_ids } => {
|
||||||
|
set(&object, "type", "file");
|
||||||
|
set(&object, "fileIds", uuids(file_ids, |id| id.0));
|
||||||
|
}
|
||||||
|
S::Track { file_id, trk_ids } => {
|
||||||
|
set(&object, "type", "track");
|
||||||
|
set(&object, "fileId", file_id.0.to_string());
|
||||||
|
set(&object, "trackIds", uuids(trk_ids, |id| id.0));
|
||||||
|
}
|
||||||
|
S::TrackSegment {
|
||||||
|
file_id,
|
||||||
|
trk_id,
|
||||||
|
trkseg_ids,
|
||||||
|
} => {
|
||||||
|
set(&object, "type", "segment");
|
||||||
|
set(&object, "fileId", file_id.0.to_string());
|
||||||
|
set(&object, "trackId", trk_id.0.to_string());
|
||||||
|
set(&object, "segmentIds", uuids(trkseg_ids, |id| id.0));
|
||||||
|
}
|
||||||
|
S::Waypoints { file_id } => {
|
||||||
|
set(&object, "type", "waypoints");
|
||||||
|
set(&object, "fileId", file_id.0.to_string());
|
||||||
|
}
|
||||||
|
S::Waypoint { file_id, wpt_ids } => {
|
||||||
|
set(&object, "type", "waypoint");
|
||||||
|
set(&object, "fileId", file_id.0.to_string());
|
||||||
|
set(&object, "waypointIds", uuids(wpt_ids, |id| id.0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
object.unchecked_into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the last action changed. Read it right after each action.
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn last_update() -> FilesUpdate {
|
||||||
|
let update = Object::new();
|
||||||
|
with_engine(|e| {
|
||||||
|
let diff = e.last_diff().cloned().unwrap_or_default();
|
||||||
|
set(&update, "orderChanged", e.order_changed());
|
||||||
|
set(&update, "selectionChanged", e.selection_changed());
|
||||||
|
set(&update, "added", ids(&diff.added));
|
||||||
|
set(&update, "modified", ids(&diff.modified));
|
||||||
|
set(&update, "removed", ids(&diff.removed));
|
||||||
|
});
|
||||||
|
update.unchecked_into()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coordinates buffers
|
||||||
|
//
|
||||||
|
// Like the statistics buffers, these are views into wasm memory, invalidated by the next
|
||||||
|
// command (and any allocation): read or copy them right away. They are flat `[lng, lat, ...]`
|
||||||
|
// arrays. Compare `rev` / `waypointsRev` of the file structures with the previous ones to know which
|
||||||
|
// buffers actually changed.
|
||||||
|
|
||||||
|
fn coordinates_view(f: impl FnOnce(&Engine) -> Option<&[f64]>) -> Float64Array {
|
||||||
|
ENGINE.with(|engine| match engine.borrow().as_ref().and_then(f) {
|
||||||
|
Some(coordinates) => unsafe { Float64Array::view(coordinates) },
|
||||||
|
None => Float64Array::new_with_length(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coordinates of the trackpoints of a segment (empty if the id is unknown).
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn segment_coordinates(segment_id: &str) -> Float64Array {
|
||||||
|
match uuid::Uuid::parse_str(segment_id) {
|
||||||
|
Ok(id) => coordinates_view(|e| Some(e.segment_coordinates(&engine::TrackSegmentId(id)))),
|
||||||
|
Err(_) => Float64Array::new_with_length(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coordinates of the waypoints of a file (empty if the id is unknown).
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn waypoint_coordinates(file_id: &str) -> Float64Array {
|
||||||
|
match uuid::Uuid::parse_str(file_id) {
|
||||||
|
Ok(id) => coordinates_view(|e| Some(e.waypoint_coordinates(&FileId(id)))),
|
||||||
|
Err(_) => Float64Array::new_with_length(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+21
-1
@@ -17,6 +17,7 @@
|
|||||||
"dexie": "^4.0.11",
|
"dexie": "^4.0.11",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"gpx": "file:../gpx",
|
"gpx": "file:../gpx",
|
||||||
|
"gpx-rs": "file:../gpx-rs/pkg",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"mapillary-js": "^4.1.2",
|
"mapillary-js": "^4.1.2",
|
||||||
@@ -70,7 +71,8 @@
|
|||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vaul-svelte": "^1.0.0-next.7",
|
"vaul-svelte": "^1.0.0-next.7",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5",
|
||||||
|
"vite-plugin-wasm": "^3.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../gpx": {
|
"../gpx": {
|
||||||
@@ -89,6 +91,10 @@
|
|||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../gpx-rs/pkg": {
|
||||||
|
"name": "gpx-rs",
|
||||||
|
"version": "0.1.0"
|
||||||
|
},
|
||||||
"../gpx/node_modules/@cspotcode/source-map-support": {
|
"../gpx/node_modules/@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5492,6 +5498,10 @@
|
|||||||
"resolved": "../gpx",
|
"resolved": "../gpx",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/gpx-rs": {
|
||||||
|
"resolved": "../gpx-rs/pkg",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -7716,6 +7726,16 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-plugin-wasm": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-mL/QPziiIA4RAA6DkaZZzOstdwbW5jO4Vz7Zenj0wieKWBlNvIvX5L5ljum9lcUX0ShNfBgCNLKTjNkRVVqcsw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite/node_modules/@esbuild/aix-ppc64": {
|
"node_modules/vite/node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||||
|
|||||||
@@ -59,7 +59,8 @@
|
|||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vaul-svelte": "^1.0.0-next.7",
|
"vaul-svelte": "^1.0.0-next.7",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5",
|
||||||
|
"vite-plugin-wasm": "^3.6.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -72,6 +73,7 @@
|
|||||||
"dexie": "^4.0.11",
|
"dexie": "^4.0.11",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"gpx": "file:../gpx",
|
"gpx": "file:../gpx",
|
||||||
|
"gpx-rs": "file:../gpx-rs/pkg",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"mapillary-js": "^4.1.2",
|
"mapillary-js": "^4.1.2",
|
||||||
|
|||||||
@@ -1449,7 +1449,7 @@ export const overpassQueryData: Record<string, OverpassQueryData> = {
|
|||||||
color: 'DarkBlue',
|
color: 'DarkBlue',
|
||||||
},
|
},
|
||||||
tags: {
|
tags: {
|
||||||
railway: 'station',
|
railway: ['station', 'halt'],
|
||||||
},
|
},
|
||||||
symbol: 'Ground Transportation',
|
symbol: 'Ground Transportation',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,26 +45,6 @@
|
|||||||
settings.initialize();
|
settings.initialize();
|
||||||
|
|
||||||
function applyOptions() {
|
function applyOptions() {
|
||||||
let downloads: Promise<GPXFile | null>[] = getFilesFromEmbeddingOptions(options).map(
|
|
||||||
(url) => {
|
|
||||||
return fetch(url)
|
|
||||||
.then((response) => response.blob())
|
|
||||||
.then((blob) => new File([blob], url.split('/').pop() ?? url))
|
|
||||||
.then(loadFile);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
Promise.all(downloads).then((answers) => {
|
|
||||||
const files = answers.filter((file) => file !== null) as GPXFile[];
|
|
||||||
let ids: string[] = [];
|
|
||||||
files.forEach((file, index) => {
|
|
||||||
let id = `gpx-${index}-embed`;
|
|
||||||
file._data.id = id;
|
|
||||||
ids.push(id);
|
|
||||||
});
|
|
||||||
fileStateCollection.setEmbeddedFiles(files);
|
|
||||||
$fileOrder = ids;
|
|
||||||
selection.selectAll();
|
|
||||||
});
|
|
||||||
if (allowedEmbeddingBasemaps.includes(options.basemap)) {
|
if (allowedEmbeddingBasemaps.includes(options.basemap)) {
|
||||||
$currentBasemap = options.basemap;
|
$currentBasemap = options.basemap;
|
||||||
}
|
}
|
||||||
@@ -90,6 +70,27 @@
|
|||||||
].filter((dataset) => dataset !== null)
|
].filter((dataset) => dataset !== null)
|
||||||
);
|
);
|
||||||
elevationFill.set(options.elevation.fill == 'none' ? undefined : options.elevation.fill);
|
elevationFill.set(options.elevation.fill == 'none' ? undefined : options.elevation.fill);
|
||||||
|
|
||||||
|
let downloads: Promise<GPXFile | null>[] = getFilesFromEmbeddingOptions(options).map(
|
||||||
|
(url) => {
|
||||||
|
return fetch(url)
|
||||||
|
.then((response) => response.blob())
|
||||||
|
.then((blob) => new File([blob], url.split('/').pop() ?? url))
|
||||||
|
.then(loadFile);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Promise.all(downloads).then((answers) => {
|
||||||
|
const files = answers.filter((file) => file !== null) as GPXFile[];
|
||||||
|
let ids: string[] = [];
|
||||||
|
files.forEach((file, index) => {
|
||||||
|
let id = `gpx-${index}-embed`;
|
||||||
|
file._data.id = id;
|
||||||
|
ids.push(id);
|
||||||
|
});
|
||||||
|
fileStateCollection.setEmbeddedFiles(files);
|
||||||
|
$fileOrder = ids;
|
||||||
|
selection.selectAll();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|||||||
@@ -88,6 +88,14 @@
|
|||||||
<span class="font-mono">{key}</span>
|
<span class="font-mono">{key}</span>
|
||||||
{#if key === 'website' || key.startsWith('website:') || key.endsWith(':website') || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
|
{#if key === 'website' || key.startsWith('website:') || key.endsWith(':website') || key === 'contact:facebook' || key === 'contact:instagram' || key === 'contact:twitter'}
|
||||||
<a href={value} target="_blank" class="text-link underline">{value}</a>
|
<a href={value} target="_blank" class="text-link underline">{value}</a>
|
||||||
|
{:else if key === 'wikipedia' || key.startsWith('wikipedia:') || key.endsWith(':wikipedia')}
|
||||||
|
<a
|
||||||
|
href="https://wikipedia.org/wiki/{value}"
|
||||||
|
target="_blank"
|
||||||
|
class="text-link underline"
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</a>
|
||||||
{:else if key === 'phone' || key === 'contact:phone'}
|
{:else if key === 'phone' || key === 'contact:phone'}
|
||||||
<a href={'tel:' + value} class="text-link underline">{value}</a>
|
<a href={'tel:' + value} class="text-link underline">{value}</a>
|
||||||
{:else if key === 'email' || key === 'contact:email'}
|
{:else if key === 'email' || key === 'contact:email'}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ liveQuery(() => db.overpassdata.toArray()).subscribe((pois) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export class OverpassLayer {
|
export class OverpassLayer {
|
||||||
overpassUrl = 'https://overpass.private.coffee/api/interpreter';
|
overpassUrl = 'https://overpass.gpx.studio/api/interpreter';
|
||||||
minZoom = 12;
|
minZoom = 12;
|
||||||
queryZoom = 12;
|
queryZoom = 12;
|
||||||
expirationTime = 7 * 24 * 3600 * 1000;
|
expirationTime = 7 * 24 * 3600 * 1000;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ Met de kaartlagen knop kun je wisselen tussen verschillende basiskaarten, en kaa
|
|||||||
<DocsLayers />
|
<DocsLayers />
|
||||||
<span class="text-sm text-center mt-2">
|
<span class="text-sm text-center mt-2">
|
||||||
|
|
||||||
Hover over the map to show the <a href="https://hiking.waymarkedtrails.org" target="_blank">Waymarked Trails hiking</a> overlay on top of the topographic basemap.
|
Beweeg over de kaart om de <a href="https://hiking.waymarkedtrails.org" target="_blank">Gemarkeerde sporen wandelen</a> overlay te tonen bovenop de topografische basiskaart.
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Wijzig de eenheden die worden gebruikt om afstanden in de interface weer te geve
|
|||||||
|
|
||||||
### <Zap size="16" class="inline-block" style="margin-bottom: 2px" /> Snelheidseenheden
|
### <Zap size="16" class="inline-block" style="margin-bottom: 2px" /> Snelheidseenheden
|
||||||
|
|
||||||
Verander de eenheden die worden gebruikt om velocities in de interface weer te geven.
|
Verander de eenheden die worden gebruikt om snelheden in de interface weer te geven.
|
||||||
Je kunt kiezen tussen afstand per uur of minuten per afstand, wat geschikter kan zijn voor het uitvoeren van activiteiten.
|
Je kunt kiezen tussen afstand per uur of minuten per afstand, wat geschikter kan zijn voor het uitvoeren van activiteiten.
|
||||||
|
|
||||||
### <Thermometer size="16" class="inline-block" style="margin-bottom: 2px" /> Temperatuureenheden
|
### <Thermometer size="16" class="inline-block" style="margin-bottom: 2px" /> Temperatuureenheden
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Met dit gereedschap kun je hoogtegegevens toevoegen aan sporen en [interessante
|
|||||||
|
|
||||||
<DocsNote>
|
<DocsNote>
|
||||||
|
|
||||||
Elevation data is provided by <a href="https://mapterhorn.com" target="_blank">Mapterhorn</a>.
|
Hoogtegegevens worden verstrekt door <a href="https://maptiler.com" target="_blank">Mapterhorn</a>.
|
||||||
You can learn more about its origin and accuracy in the <a href="https://mapterhorn.com/attribution/" target="_blank">documentation</a>.
|
Je kunt meer leren over de oorsprong en nauwkeurigheid in de <a href="https://mapterhorn.com/attribution/" target="_blank">documentatie</a>.
|
||||||
|
|
||||||
</DocsNote>
|
</DocsNote>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: FAQ
|
title: 常見問題
|
||||||
---
|
---
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -8,7 +8,7 @@ title: FAQ
|
|||||||
|
|
||||||
# { title }
|
# { title }
|
||||||
|
|
||||||
### Do I need to donate to use the website?
|
### 需要捐贈才能使用這網站嗎?
|
||||||
|
|
||||||
No.
|
No.
|
||||||
The website is free to use and always will be (as long as it is financially sustainable).
|
The website is free to use and always will be (as long as it is financially sustainable).
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: Files and statistics
|
title: 檔案和統計
|
||||||
---
|
---
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -19,7 +19,7 @@ title: Files and statistics
|
|||||||
|
|
||||||
# { title }
|
# { title }
|
||||||
|
|
||||||
## File list
|
## 檔案列表
|
||||||
|
|
||||||
Once you have [opened](./menu/file) files, they will be shown as tabs in the file list located at the bottom of the map.
|
Once you have [opened](./menu/file) files, they will be shown as tabs in the file list located at the bottom of the map.
|
||||||
You can reorder them by dragging and dropping the tabs.
|
You can reorder them by dragging and dropping the tabs.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Before we dive into the details of each section, let's have a quick overview of
|
|||||||
At the top of the interface, you will find the [main menu](./menu).
|
At the top of the interface, you will find the [main menu](./menu).
|
||||||
This is where you can access common actions such as opening, closing, and exporting files, undoing and redoing actions, and adjusting the application settings.
|
This is where you can access common actions such as opening, closing, and exporting files, undoing and redoing actions, and adjusting the application settings.
|
||||||
|
|
||||||
## Files and statistics
|
## 檔案和統計
|
||||||
|
|
||||||
At the bottom of the interface, you will find the list of files currently open in the application.
|
At the bottom of the interface, you will find the list of files currently open in the application.
|
||||||
You can click on a file to select it and display its statistics below the list.
|
You can click on a file to select it and display its statistics below the list.
|
||||||
|
|||||||
@@ -10,87 +10,87 @@ title: Edit actions
|
|||||||
# { title }
|
# { title }
|
||||||
|
|
||||||
與文件操作不同,編輯操作可能會修改目前選定文件的內容。
|
與文件操作不同,編輯操作可能會修改目前選定文件的內容。
|
||||||
Moreover, when the tree layout of the files list is enabled (see [Files and statistics](../files-and-stats)), they can also be applied to [tracks, segments, and points of interest](../gpx).
|
若啟用檔案 [樹形佈局](../files-and-stats),操作也可作用於相關 [軌跡、路段和興趣點](../gpx)。
|
||||||
Therefore, we will refer to the elements that can be modified by these actions as _file items_.
|
在文件中,所有可通過以上操作修改的項目被稱為 _檔案項_。
|
||||||
Note that except for the undo and redo actions, the edit actions are also accessible through the context menu (right-click) of the file items.
|
注意,除復原和重做外,其餘操作也可通過右鍵點選檔案項來喚出操作菜單。
|
||||||
|
|
||||||
### <Undo2 size="16" class="inline-block" style="margin-bottom: 2px" /><Redo2 size="16" class="inline-block" style="margin-bottom: 2px" /> Undo and redo
|
### <Undo2 size="16" class="inline-block" style="margin-bottom: 2px" /><Redo2 size="16" class="inline-block" style="margin-bottom: 2px" /> 復原和重做
|
||||||
|
|
||||||
Using these buttons, you can undo or redo the last actions you performed.
|
復原或重做上一次執行的操作。
|
||||||
This applies to all actions of the interface but not to view options, application settings, or map navigation.
|
只適用於在介面上直接進行的所有操作,不適用於顯示選項、應用設定或地圖導航。
|
||||||
|
|
||||||
### <Info size="16" class="inline-block" style="margin-bottom: 2px" /> Info...
|
### <Info size="16" class="inline-block" style="margin-bottom: 2px" /> 資訊…
|
||||||
|
|
||||||
Open the information dialog of the currently selected file item, where you can see and edit its name and description.
|
用於查看或編輯所選檔案項的名稱和說明資訊。用於查看或編輯所選檔案項的名稱和說明資訊。
|
||||||
|
|
||||||
### <PaintBucket size="16" class="inline-block" style="margin-bottom: 2px" /> Appearance...
|
### <PaintBucket size="16" class="inline-block" style="margin-bottom: 2px" /> 外觀
|
||||||
|
|
||||||
Open the appearance dialog, where you can change the color, opacity, and width of the selected file items on the map.
|
用於調整所選檔案項在地圖上顯示的軌跡顏色、不透明度和闊度。
|
||||||
|
|
||||||
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> Hide/unhide
|
### <EyeOff size="16" class="inline-block" style="margin-bottom: 2px" /> 隱藏/顯示
|
||||||
|
|
||||||
Toggle the visibility of the selected file items on the map.
|
切換所選檔案項在地圖上的可見性。
|
||||||
|
|
||||||
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New track
|
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> 新建軌跡
|
||||||
|
|
||||||
Create a new track in the selected file.
|
在所選檔案項中建立一條新軌跡。
|
||||||
|
|
||||||
<DocsNote>
|
<DocsNote>
|
||||||
|
|
||||||
This action is only available when the tree layout of the files list is enabled.
|
此操作僅在啟用樹形佈局時可用,
|
||||||
Additionally, the selection must be a single file.
|
所選內容必須是一個檔案。
|
||||||
|
|
||||||
</DocsNote>
|
</DocsNote>
|
||||||
|
|
||||||
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New segment
|
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> 新建路段
|
||||||
|
|
||||||
Create a new segment in the selected track.
|
在所選軌跡中建立一個新路段。
|
||||||
|
|
||||||
<DocsNote>
|
<DocsNote>
|
||||||
|
|
||||||
This action is only available when the tree layout of the files list is enabled.
|
此操作僅在啟用樹形佈局時可用,
|
||||||
Additionally, the selection must be a single track.
|
且所選內容必須是單條軌跡。
|
||||||
|
|
||||||
</DocsNote>
|
</DocsNote>
|
||||||
|
|
||||||
### <FileStack size="16" class="inline-block" style="margin-bottom: 2px" /> Select all
|
### <FileStack size="16" class="inline-block" style="margin-bottom: 2px" /> 全選
|
||||||
|
|
||||||
Add all file items in the current hierarchy level to the selection.
|
加入當前檔案層級中所有內容到選取中。
|
||||||
|
|
||||||
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> Center
|
### <Maximize size="16" class="inline-block" style="margin-bottom: 2px" /> 置中
|
||||||
|
|
||||||
Center the map on the selected file items.
|
根據所選檔案項調整地圖顯示中心。
|
||||||
|
|
||||||
### <ClipboardCopy size="16" class="inline-block" style="margin-bottom: 2px" /> Copy
|
### <ClipboardCopy size="16" class="inline-block" style="margin-bottom: 2px" /> 複製
|
||||||
|
|
||||||
Copy the selected file items to the clipboard.
|
複製所選檔案項到剪貼簿。
|
||||||
|
|
||||||
<DocsNote>
|
<DocsNote>
|
||||||
|
|
||||||
This action is only available when the tree layout of the files list is enabled.
|
此操作僅在啟用樹形佈局時可用,
|
||||||
|
|
||||||
</DocsNote>
|
</DocsNote>
|
||||||
|
|
||||||
### <Scissors size="16" class="inline-block" style="margin-bottom: 2px" /> Cut
|
### <Scissors size="16" class="inline-block" style="margin-bottom: 2px" /> 剪下
|
||||||
|
|
||||||
Cut the selected file items to the clipboard.
|
剪下所選檔案項到剪貼簿。
|
||||||
|
|
||||||
<DocsNote>
|
<DocsNote>
|
||||||
|
|
||||||
This action is only available when the tree layout of the files list is enabled.
|
此操作僅在啟用樹形佈局時可用,
|
||||||
|
|
||||||
</DocsNote>
|
</DocsNote>
|
||||||
|
|
||||||
### <ClipboardPaste size="16" class="inline-block" style="margin-bottom: 2px" /> Paste
|
### <ClipboardPaste size="16" class="inline-block" style="margin-bottom: 2px" /> 貼上
|
||||||
|
|
||||||
Paste the file items from the clipboard to the current hierarchy level if they are compatible with it.
|
如果剪貼簿中的內容與當前檔案層級相容,進行貼上操作。
|
||||||
|
|
||||||
<DocsNote>
|
<DocsNote>
|
||||||
|
|
||||||
This action is only available when the tree layout of the files list is enabled.
|
此操作僅在啟用樹形佈局時可用,
|
||||||
|
|
||||||
</DocsNote>
|
</DocsNote>
|
||||||
|
|
||||||
### <Trash2 size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
### <Trash2 size="16" class="inline-block" style="margin-bottom: 2px" /> 刪除
|
||||||
|
|
||||||
Delete the selected file items.
|
刪除所選檔案項。
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: File actions
|
title: 檔案
|
||||||
---
|
---
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -9,44 +9,44 @@ title: File actions
|
|||||||
|
|
||||||
# { title }
|
# { title }
|
||||||
|
|
||||||
The file actions menu contains a set of pretty self-explanatory file operations.
|
該菜單中的操作非常清晰。
|
||||||
|
|
||||||
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> New
|
### <Plus size="16" class="inline-block" style="margin-bottom: 2px" /> 新增
|
||||||
|
|
||||||
Create a new empty file.
|
創造一個新的空白檔案。
|
||||||
|
|
||||||
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> Open...
|
### <FolderOpen size="16" class="inline-block" style="margin-bottom: 2px" /> 開啟…
|
||||||
|
|
||||||
Open files from your computer.
|
開啟本地檔案。
|
||||||
|
|
||||||
<DocsNote>
|
<DocsNote>
|
||||||
|
|
||||||
You can also drag and drop files directly from your file system into the window.
|
支援將檔案直接拖放到窗口中。
|
||||||
|
|
||||||
</DocsNote>
|
</DocsNote>
|
||||||
|
|
||||||
### <Copy size="16" class="inline-block" style="margin-bottom: 2px" /> Duplicate
|
### <Copy size="16" class="inline-block" style="margin-bottom: 2px" /> 複製
|
||||||
|
|
||||||
Create a copy of the currently selected files.
|
創造當前選中檔案的副本。
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> 刪除
|
||||||
|
|
||||||
Delete the currently selected files.
|
刪除當前選中的檔案。
|
||||||
|
|
||||||
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> Delete all
|
### <FileX size="16" class="inline-block" style="margin-bottom: 2px" /> 全部刪除
|
||||||
|
|
||||||
Delete all files.
|
刪除全部檔案。
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> 匯出...
|
||||||
|
|
||||||
Open the export dialog to save the currently selected files to your computer.
|
保存當前選取檔案到本地。
|
||||||
|
|
||||||
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> Export all...
|
### <Download size="16" class="inline-block" style="margin-bottom: 2px" /> 匯出所有…
|
||||||
|
|
||||||
Open the export dialog to save all files to your computer.
|
保存所有檔案到本地。
|
||||||
|
|
||||||
<DocsNote type="warning">
|
<DocsNote type="warning">
|
||||||
|
|
||||||
If your download does not start after clicking the download button, please check your browser settings to allow downloads from <b>gpx.studio</b>.
|
點選下載按鈕後若無反應,請檢查瀏覽器設定以允許其從 <b>gpx.studio</b> 下載。
|
||||||
|
|
||||||
</DocsNote>
|
</DocsNote>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: Settings
|
title: 設定
|
||||||
---
|
---
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -9,7 +9,7 @@ title: Settings
|
|||||||
|
|
||||||
# { title }
|
# { title }
|
||||||
|
|
||||||
### <Ruler size="16" class="inline-block" style="margin-bottom: 2px" /> Distance units
|
### <Ruler size="16" class="inline-block" style="margin-bottom: 2px" /> 距離單位
|
||||||
|
|
||||||
Change the units used to display distances in the interface.
|
Change the units used to display distances in the interface.
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user