mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-06 22:04:06 +00:00
Compare commits
59
Commits
fde6bafc10
...
wasm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da8a9e3d84 | ||
|
|
c6bd5b06e8 | ||
|
|
c04367dd86 | ||
|
|
5e2f506120 | ||
|
|
c536e6247b | ||
|
|
594d714b7f | ||
|
|
543fecadb8 | ||
|
|
017d8521d6 | ||
|
|
78e9d45072 | ||
|
|
7651d859c4 | ||
|
|
ec448b72e2 | ||
|
|
bf946f179a | ||
|
|
9ff9b5475a | ||
|
|
55f1d275b7 | ||
|
|
1d534881f8 | ||
|
|
03cbacfbb4 | ||
|
|
9d0905959b | ||
|
|
b7ca462f58 | ||
|
|
9015da4876 | ||
|
|
f458a1104f | ||
|
|
caba9388af | ||
|
|
763a7406ce | ||
|
|
68c8933c35 | ||
|
|
84b1558b2e | ||
|
|
cd870f0186 | ||
|
|
9ac2a2b69c | ||
|
|
86aba8f4b1 | ||
|
|
74b674e8e8 | ||
|
|
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 |
@@ -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,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx version="1.1" creator="gpx.studio" xmlns="http://www.topografix.com/GPX/1/1"
|
||||
xmlns:ext="https://example.com/ext/0.1">
|
||||
<metadata>
|
||||
<name>self closing points</name>
|
||||
<desc>Points without children are self-closing elements.</desc>
|
||||
<extensions>
|
||||
<ext:version>2.0</ext:version>
|
||||
<ext:paths/>
|
||||
</extensions>
|
||||
</metadata>
|
||||
<wpt lat="50.0" lon="4.0"/>
|
||||
<wpt lat="50.1" lon="4.1"><name>with children</name></wpt>
|
||||
<wpt lat="50.2" lon="4.2"/>
|
||||
<trk>
|
||||
<name>track</name>
|
||||
<trkseg>
|
||||
<trkpt lat="50.00" lon="4.00"/>
|
||||
<trkpt lat="50.01" lon="4.01"><ele>12.5</ele></trkpt>
|
||||
<trkpt lat="50.02" lon="4.02"/>
|
||||
</trkseg>
|
||||
<trkseg>
|
||||
<trkpt lat="51.00" lon="5.00"/>
|
||||
</trkseg>
|
||||
</trk>
|
||||
</gpx>
|
||||
@@ -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,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx xmlns="http://www.topografix.com/GPX/1/1"
|
||||
xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" version="1.1" creator="https://gpx.studio">
|
||||
<metadata>
|
||||
<name>with_highway</name>
|
||||
</metadata>
|
||||
<trk>
|
||||
<name>with_highway</name>
|
||||
<trkseg>
|
||||
<trkpt lat="50.790867" lon="4.404968">
|
||||
<ele>109.0</ele>
|
||||
<extensions>
|
||||
<gpxtpx:TrackPointExtension>
|
||||
<gpxtpx:Extensions>
|
||||
<surface>asphalt</surface>
|
||||
<highway>residential</highway>
|
||||
</gpxtpx:Extensions>
|
||||
</gpxtpx:TrackPointExtension>
|
||||
</extensions>
|
||||
</trkpt>
|
||||
<trkpt lat="50.791000" lon="4.405100">
|
||||
<ele>110.0</ele>
|
||||
<extensions>
|
||||
<gpxtpx:TrackPointExtension>
|
||||
<gpxtpx:Extensions>
|
||||
<surface>asphalt</surface>
|
||||
<highway>residential</highway>
|
||||
</gpxtpx:Extensions>
|
||||
</gpxtpx:TrackPointExtension>
|
||||
</extensions>
|
||||
</trkpt>
|
||||
<trkpt lat="50.791200" lon="4.405300">
|
||||
<ele>111.0</ele>
|
||||
</trkpt>
|
||||
<trkpt lat="50.791400" lon="4.405500">
|
||||
<ele>112.0</ele>
|
||||
<extensions>
|
||||
<gpxtpx:TrackPointExtension>
|
||||
<gpxtpx:Extensions>
|
||||
<surface>gravel</surface>
|
||||
<highway>track</highway>
|
||||
<sac_scale>mountain_hiking</sac_scale>
|
||||
<mtb_scale>1</mtb_scale>
|
||||
</gpxtpx:Extensions>
|
||||
</gpxtpx:TrackPointExtension>
|
||||
</extensions>
|
||||
</trkpt>
|
||||
<trkpt lat="50.791600" lon="4.405700">
|
||||
<ele>113.0</ele>
|
||||
<extensions>
|
||||
<gpxtpx:TrackPointExtension>
|
||||
<gpxtpx:Extensions>
|
||||
<surface>gravel</surface>
|
||||
<sac_scale>hiking</sac_scale>
|
||||
<mtb_scale>1</mtb_scale>
|
||||
</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_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,96 @@
|
||||
/// Maps the values of a category of trackpoint data (the surface, the highway...) to small codes,
|
||||
/// so that the trackpoints only hold a `u8` instead of a string.
|
||||
///
|
||||
/// The codes are the positions of the names in the order of first appearance. The table only
|
||||
/// grows: a code never changes meaning, so it stays valid in every state of the history.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Categories(Vec<String>);
|
||||
|
||||
impl Categories {
|
||||
/// Maximum number of distinct values.
|
||||
pub const CAPACITY: usize = u8::MAX as usize;
|
||||
|
||||
/// The code of `name`, which is added to the table if it is new. `None` if the table is full.
|
||||
pub fn code(&mut self, name: &str) -> Option<u8> {
|
||||
let index = match self.0.iter().position(|known| known == name) {
|
||||
Some(index) => index,
|
||||
None if self.0.len() < Self::CAPACITY => {
|
||||
self.0.push(name.to_owned());
|
||||
self.0.len() - 1
|
||||
}
|
||||
None => return None,
|
||||
};
|
||||
u8::try_from(index).ok()
|
||||
}
|
||||
|
||||
/// The name of a code, `None` if it is unknown.
|
||||
pub fn name(&self, code: u8) -> Option<&str> {
|
||||
self.0.get(usize::from(code)).map(String::as_str)
|
||||
}
|
||||
|
||||
/// All the names, in the order of their codes.
|
||||
pub fn names(&self) -> &[String] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The categories of the data of the trackpoints. There is one table per engine, shared by all
|
||||
/// its files.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct TrackpointCategories {
|
||||
pub surface: Categories,
|
||||
pub highway: Categories,
|
||||
pub sac_scale: Categories,
|
||||
pub mtb_scale: Categories,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_codes_follow_the_order_of_appearance() {
|
||||
let mut categories = Categories::default();
|
||||
assert_eq!(categories.code("asphalt"), Some(0));
|
||||
assert_eq!(categories.code("gravel"), Some(1));
|
||||
// a known name keeps its code
|
||||
assert_eq!(categories.code("asphalt"), Some(0));
|
||||
assert_eq!(categories.names(), ["asphalt", "gravel"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_names_of_codes() {
|
||||
let mut categories = Categories::default();
|
||||
let code = categories.code("asphalt").unwrap();
|
||||
assert_eq!(categories.name(code), Some("asphalt"));
|
||||
assert_eq!(categories.name(code + 1), None);
|
||||
assert_eq!(Categories::default().name(0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_a_full_table_gives_no_code_to_new_names() {
|
||||
let mut categories = Categories::default();
|
||||
for i in 0..Categories::CAPACITY {
|
||||
assert_eq!(categories.code(&i.to_string()), Some(i as u8));
|
||||
}
|
||||
assert_eq!(categories.code("one too many"), None);
|
||||
assert_eq!(categories.names().len(), Categories::CAPACITY);
|
||||
// the known ones still have their code, the last one included
|
||||
assert_eq!(categories.code("0"), Some(0));
|
||||
assert_eq!(categories.code("254"), Some(254));
|
||||
assert_eq!(categories.name(254), Some("254"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_categories_are_independent() {
|
||||
let mut categories = TrackpointCategories::default();
|
||||
categories.surface.code("asphalt");
|
||||
assert_eq!(categories.highway.code("path"), Some(0));
|
||||
assert_eq!(categories.sac_scale.code("hiking"), Some(0));
|
||||
assert_eq!(categories.mtb_scale.code("1"), Some(0));
|
||||
assert_eq!(categories.surface.names(), ["asphalt"]);
|
||||
assert_eq!(categories.highway.names(), ["path"]);
|
||||
assert_eq!(categories.sac_scale.names(), ["hiking"]);
|
||||
assert_eq!(categories.mtb_scale.names(), ["1"]);
|
||||
}
|
||||
}
|
||||
@@ -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, Clone, Copy)]
|
||||
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,17 @@
|
||||
mod categories;
|
||||
mod chunk;
|
||||
mod common;
|
||||
mod file;
|
||||
mod segment;
|
||||
mod track;
|
||||
mod trackpoint;
|
||||
mod waypoint;
|
||||
|
||||
pub use categories::*;
|
||||
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,20 @@
|
||||
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>,
|
||||
/// Code of the surface, see [`crate::TrackpointCategories`].
|
||||
pub surface: Option<u8>,
|
||||
/// Code of the highway, see [`crate::TrackpointCategories`].
|
||||
pub highway: Option<u8>,
|
||||
/// Code of the SAC hiking scale, see [`crate::TrackpointCategories`].
|
||||
pub sac_scale: Option<u8>,
|
||||
/// Code of the mountain biking scale, see [`crate::TrackpointCategories`].
|
||||
pub mtb_scale: Option<u8>,
|
||||
}
|
||||
@@ -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,757 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Author, File, Link, LngLat, Track, TrackSegment, Trackpoint, TrackpointCategories,
|
||||
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,
|
||||
Surface,
|
||||
Highway,
|
||||
SacScale,
|
||||
MtbScale,
|
||||
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
|
||||
}
|
||||
|
||||
/// Parses a GPX file. The surface, highway, SAC scale and MTB scale of the trackpoints are stored as codes of
|
||||
/// `categories`, which learns the values it does not know yet.
|
||||
pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> 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),
|
||||
e if e.ends_with("power") => stack.push(GPXElement::Power),
|
||||
e if e.ends_with("PowerInWatts") => stack.push(GPXElement::Power),
|
||||
"surface" => stack.push(GPXElement::Surface),
|
||||
"highway" => stack.push(GPXElement::Highway),
|
||||
"sac_scale" => stack.push(GPXElement::SacScale),
|
||||
"mtb_scale" => stack.push(GPXElement::MtbScale),
|
||||
"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),
|
||||
_ => (),
|
||||
},
|
||||
// Self-closing points (`<trkpt lat=".." lon=".."/>`), which have no children
|
||||
Ok(Event::Empty(e)) => match e.name().as_ref() {
|
||||
"trkpt" => {
|
||||
if let Some(GPXElement::Segment(trkseg)) = stack.last_mut() {
|
||||
trkpt_chunk.trkpt.push(Trackpoint {
|
||||
coordinates: parse_coordinates(e.attributes()),
|
||||
..Default::default()
|
||||
});
|
||||
if trkpt_chunk.is_full() {
|
||||
trkseg.push(std::mem::take(&mut trkpt_chunk));
|
||||
}
|
||||
}
|
||||
}
|
||||
"wpt" => {
|
||||
wpt_chunk.wpt.push(Waypoint {
|
||||
coordinates: parse_coordinates(e.attributes()),
|
||||
..Default::default()
|
||||
});
|
||||
if wpt_chunk.is_full() {
|
||||
gpx.wpt.push(Rc::new(std::mem::take(&mut wpt_chunk)));
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
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::Surface) => {
|
||||
stack.pop();
|
||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||
trkpt.surface = categories.surface.code(&e);
|
||||
}
|
||||
}
|
||||
Some(GPXElement::Highway) => {
|
||||
stack.pop();
|
||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||
trkpt.highway = categories.highway.code(&e);
|
||||
}
|
||||
}
|
||||
Some(GPXElement::SacScale) => {
|
||||
stack.pop();
|
||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||
trkpt.sac_scale = categories.sac_scale.code(&e);
|
||||
}
|
||||
}
|
||||
Some(GPXElement::MtbScale) => {
|
||||
stack.pop();
|
||||
if let Some(GPXElement::Trackpoint(trkpt)) = stack.last_mut() {
|
||||
trkpt.mtb_scale = categories.mtb_scale.code(&e);
|
||||
}
|
||||
}
|
||||
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(e.to_string());
|
||||
}
|
||||
}
|
||||
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(),
|
||||
&mut Default::default(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_self_closing_points() {
|
||||
let gpx = parse_data("self_closing_points");
|
||||
|
||||
assert_eq!(gpx.info.name, "self closing points");
|
||||
let wpt: Vec<_> = gpx.wpt.iter().flat_map(|chunk| &chunk.wpt).collect();
|
||||
assert_eq!(wpt.len(), 3);
|
||||
assert_eq!(wpt[0].coordinates.lat, 50.0);
|
||||
assert_eq!(wpt[0].name, None);
|
||||
assert_eq!(wpt[1].name.as_deref(), Some("with children"));
|
||||
assert_eq!(wpt[2].coordinates.lng, 4.2);
|
||||
|
||||
assert_eq!(gpx.trk.len(), 1);
|
||||
let trk = &gpx.trk[0];
|
||||
assert_eq!(trk.trkseg.len(), 2);
|
||||
let seg = &trk.trkseg[0];
|
||||
assert_eq!(seg.len(), 3);
|
||||
assert_eq!(seg[0].coordinates.lat, 50.0);
|
||||
assert_eq!(seg[0].ele, 0.0);
|
||||
assert_eq!(seg[1].ele, 12.5);
|
||||
assert_eq!(seg[2].coordinates.lng, 4.02);
|
||||
assert_eq!(trk.trkseg[1].len(), 1);
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
|
||||
fn parse_data_with_categories(name: &str) -> (File, TrackpointCategories) {
|
||||
let mut categories = TrackpointCategories::default();
|
||||
let file = parse(
|
||||
&std::fs::read(format!("data/{name}.gpx")).unwrap(),
|
||||
&mut categories,
|
||||
)
|
||||
.unwrap();
|
||||
(file, categories)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_trackpoint_surface() {
|
||||
let (gpx, categories) = parse_data_with_categories("with_surface");
|
||||
|
||||
let trkseg = &gpx.trk[0].trkseg[0];
|
||||
assert_eq!(trkseg.len(), 80);
|
||||
// the codes follow the order of appearance in the file
|
||||
assert_eq!(categories.surface.names(), ["asphalt", "cobblestone"]);
|
||||
let asphalt = Some(0);
|
||||
let cobblestone = Some(1);
|
||||
assert_eq!(trkseg.iter().filter(|p| p.surface == asphalt).count(), 79);
|
||||
assert_eq!(
|
||||
trkseg.iter().filter(|p| p.surface == cobblestone).count(),
|
||||
1
|
||||
);
|
||||
// no highway in this file
|
||||
assert!(trkseg.iter().all(|trkpt| trkpt.highway.is_none()));
|
||||
assert!(categories.highway.names().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_trackpoint_surface_highway_and_scales() {
|
||||
let (gpx, categories) = parse_data_with_categories("with_highway");
|
||||
|
||||
let trkseg = &gpx.trk[0].trkseg[0];
|
||||
let names = |code: Option<u8>, names: &crate::Categories| {
|
||||
code.and_then(|code| names.name(code)).map(str::to_owned)
|
||||
};
|
||||
let values: Vec<_> = trkseg
|
||||
.iter()
|
||||
.map(|trkpt| {
|
||||
[
|
||||
names(trkpt.surface, &categories.surface),
|
||||
names(trkpt.highway, &categories.highway),
|
||||
names(trkpt.sac_scale, &categories.sac_scale),
|
||||
names(trkpt.mtb_scale, &categories.mtb_scale),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
let some = |s: &str| Some(s.to_owned());
|
||||
assert_eq!(
|
||||
values,
|
||||
[
|
||||
[some("asphalt"), some("residential"), None, None],
|
||||
[some("asphalt"), some("residential"), None, None],
|
||||
[None, None, None, None],
|
||||
[
|
||||
some("gravel"),
|
||||
some("track"),
|
||||
some("mountain_hiking"),
|
||||
some("1")
|
||||
],
|
||||
[some("gravel"), None, some("hiking"), some("1")],
|
||||
]
|
||||
);
|
||||
// the tables are separate
|
||||
assert_eq!(categories.sac_scale.names(), ["mountain_hiking", "hiking"]);
|
||||
assert_eq!(categories.mtb_scale.names(), ["1"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_shares_the_categories_between_files() {
|
||||
let mut categories = TrackpointCategories::default();
|
||||
let mut parsed = |name: &str| {
|
||||
parse(
|
||||
&std::fs::read(format!("data/{name}.gpx")).unwrap(),
|
||||
&mut categories,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
let first = parsed("with_highway");
|
||||
let second = parsed("with_surface");
|
||||
let code = |file: &File, i: usize| file.trk[0].trkseg[0][i].surface;
|
||||
// "asphalt" is the same in both, "cobblestone" comes after "gravel"
|
||||
assert_eq!(code(&first, 0), Some(0));
|
||||
assert_eq!(code(&second, 0), Some(0));
|
||||
assert_eq!(
|
||||
categories.surface.names(),
|
||||
["asphalt", "gravel", "cobblestone"]
|
||||
);
|
||||
}
|
||||
|
||||
#[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,504 @@
|
||||
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.global.hr.add(cur.hr.map(f64::from));
|
||||
self.global.cad.add(cur.cad.map(f64::from));
|
||||
self.global.atemp.add(cur.atemp.map(f64::from));
|
||||
self.global.power.add(cur.power.map(f64::from));
|
||||
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(10_000),
|
||||
|i, j| time_diff(trkseg[j].time, trkseg[i].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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Average of an optional measure (heart rate, cadence...) over the trackpoints that have it.
|
||||
/// Keeps the sum so that averages can be merged.
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Average {
|
||||
pub sum: f64,
|
||||
pub count: u32,
|
||||
}
|
||||
|
||||
impl Average {
|
||||
pub fn add(&mut self, value: Option<f64>) {
|
||||
if let Some(value) = value {
|
||||
self.sum += value;
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: &Average) {
|
||||
self.sum += other.sum;
|
||||
self.count += other.count;
|
||||
}
|
||||
|
||||
pub fn avg(&self) -> Option<f64> {
|
||||
(self.count > 0).then(|| self.sum / self.count as f64)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct GlobalStatistics {
|
||||
pub total_distance: f64,
|
||||
pub moving_distance: Option<f64>,
|
||||
pub total_time: Option<i64>,
|
||||
pub moving_time: Option<i64>,
|
||||
pub elevation_gain: f64,
|
||||
pub elevation_loss: f64,
|
||||
pub start_time: Option<i64>,
|
||||
pub end_time: Option<i64>,
|
||||
pub bounds: LngLatBounds,
|
||||
pub hr: Average,
|
||||
pub cad: Average,
|
||||
pub atemp: Average,
|
||||
pub power: Average,
|
||||
}
|
||||
|
||||
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);
|
||||
self.hr.merge(&other.hr);
|
||||
self.cad.merge(&other.cad);
|
||||
self.atemp.merge(&other.atemp);
|
||||
self.power.merge(&other.power);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct TrackpointStatistics {
|
||||
pub total_distance: f64,
|
||||
pub moving_distance: Option<f64>,
|
||||
pub total_time: Option<i64>,
|
||||
pub moving_time: Option<i64>,
|
||||
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(), &mut Default::default()).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(), &mut Default::default()).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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_average() {
|
||||
let mut average = Average::default();
|
||||
assert_eq!(average.avg(), None);
|
||||
average.add(None);
|
||||
assert_eq!(average.avg(), None);
|
||||
average.add(Some(100.0));
|
||||
average.add(Some(110.0));
|
||||
assert_eq!(average.avg(), Some(105.0));
|
||||
|
||||
let mut other = Average::default();
|
||||
other.add(Some(130.0));
|
||||
average.merge(&other);
|
||||
assert_eq!(average.count, 3);
|
||||
assert_eq!(average.avg(), Some(113.33333333333333));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_measures_of_the_trackpoints_are_averaged() {
|
||||
for (path, measure) in [
|
||||
("data/with_hr.gpx", 0),
|
||||
("data/with_cad.gpx", 1),
|
||||
("data/with_temp.gpx", 2),
|
||||
("data/with_power_1.gpx", 3),
|
||||
] {
|
||||
let data = std::fs::read(path).unwrap();
|
||||
let gpx = parse(&data, &mut Default::default()).unwrap();
|
||||
let trkseg = &gpx.trk[0].trkseg[0];
|
||||
let global = Statistics::compute(trkseg).global;
|
||||
let values: Vec<f64> = trkseg
|
||||
.iter()
|
||||
.filter_map(|trkpt| match measure {
|
||||
0 => trkpt.hr.map(f64::from),
|
||||
1 => trkpt.cad.map(f64::from),
|
||||
2 => trkpt.atemp.map(f64::from),
|
||||
_ => trkpt.power.map(f64::from),
|
||||
})
|
||||
.collect();
|
||||
let average = [global.hr, global.cad, global.atemp, global.power][measure];
|
||||
assert!(!values.is_empty(), "{path}");
|
||||
assert_eq!(average.count as usize, values.len(), "{path}");
|
||||
let expected = values.iter().sum::<f64>() / values.len() as f64;
|
||||
assert!((average.avg().unwrap() - expected).abs() < 1e-9, "{path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,203 @@
|
||||
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<i64> {
|
||||
Some(a? - b?)
|
||||
}
|
||||
|
||||
/// Computes the speed for a given distance in kilometers and a time in milliseconds
|
||||
pub fn speed(distance: f64, time: i64) -> 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_time_diff_and_speed_with_long_durations() {
|
||||
// 40 days in milliseconds, more than an i32 can hold
|
||||
let forty_days = 40 * 24 * 3_600_000_i64;
|
||||
assert!(forty_days > i64::from(i32::MAX));
|
||||
assert_eq!(time_diff(Some(forty_days), Some(0)), Some(forty_days));
|
||||
assert_eq!(time_diff(Some(0), Some(forty_days)), Some(-forty_days));
|
||||
assert_eq!(time_diff(None, Some(1)), None);
|
||||
assert_eq!(time_diff(Some(1), None), None);
|
||||
// 1 km in one hour
|
||||
assert!((speed(1.0, 3_600_000) - 1.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[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,39 @@
|
||||
use crate::{Command, FileId, SelectMode, Selection};
|
||||
|
||||
/// 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,
|
||||
/// Selects files, tracks, segments or waypoints (or the waypoints node of one file),
|
||||
/// ignoring what does not exist: a selection left with nothing is empty, which deselects
|
||||
/// everything. See [`SelectMode`] for how it combines with the current selection.
|
||||
Select {
|
||||
selection: Selection,
|
||||
mode: SelectMode,
|
||||
},
|
||||
/// Selects all the elements of the same kind as the selected ones, in the same place (see
|
||||
/// [`Selection::all_at_level`]). Does nothing when there is nothing more to select.
|
||||
SelectAll,
|
||||
/// Moves the selection to the next (`down`) or previous element of the same kind, like the
|
||||
/// arrow keys do in the file list (see [`Selection::neighbour`]). With `add`, the element is
|
||||
/// added to the selection (shift + arrow), otherwise it replaces it.
|
||||
ArrowSelect {
|
||||
down: bool,
|
||||
add: bool,
|
||||
},
|
||||
/// Puts the selected elements in the clipboard, to be pasted by [`crate::Paste`]. Does
|
||||
/// nothing when nothing is selected.
|
||||
Copy,
|
||||
/// Like `Copy`, but the elements are moved instead of copied when they are pasted.
|
||||
Cut,
|
||||
/// 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,72 @@
|
||||
use crate::{Apply, CommandError, State};
|
||||
use crate::{
|
||||
Clean, Crop, Delete, DeleteAll, DeleteWaypoint, Duplicate, EditWaypoint, Elevation, Extract,
|
||||
Load, LoadFiles, Merge, Metadata, Move, MoveWaypoint, New, NewTrack, NewTrackSegment,
|
||||
NewWaypoint, Paste, 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>),
|
||||
LoadFiles(LoadFiles<'a>),
|
||||
Duplicate(Duplicate),
|
||||
Delete(Delete),
|
||||
Paste(Paste),
|
||||
Move(Move),
|
||||
DeleteAll(DeleteAll),
|
||||
DeleteWaypoint(DeleteWaypoint),
|
||||
Metadata(Metadata<'a>),
|
||||
Style(Style<'a>),
|
||||
NewTrack(NewTrack),
|
||||
NewTrackSegment(NewTrackSegment),
|
||||
Reverse(Reverse),
|
||||
SpliceTrackpoints(SpliceTrackpoints<'a>),
|
||||
NewWaypoint(NewWaypoint<'a>),
|
||||
EditWaypoint(EditWaypoint<'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::LoadFiles(c) => c.apply(state),
|
||||
Command::Duplicate(c) => c.apply(state),
|
||||
Command::Delete(c) => c.apply(state),
|
||||
Command::Paste(c) => c.apply(state),
|
||||
Command::Move(c) => c.apply(state),
|
||||
Command::DeleteAll(c) => c.apply(state),
|
||||
Command::DeleteWaypoint(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::EditWaypoint(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,146 @@
|
||||
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());
|
||||
// a single track is named like the file
|
||||
if let [track] = file.trk.as_mut_slice() {
|
||||
track.info.name = Some(self.name.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, NewTrack, 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,
|
||||
name: "file",
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
/// A new file with `nb_tracks` tracks (without name), selected.
|
||||
fn file_with_tracks(fx: &mut Fixture, nb_tracks: usize) {
|
||||
New { name: "file" }.apply(&mut fx.state()).unwrap();
|
||||
for _ in 0..nb_tracks {
|
||||
NewTrack.apply(&mut fx.state()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn rename(fx: &mut Fixture) {
|
||||
Metadata {
|
||||
name: "renamed",
|
||||
desc: "",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_track_is_renamed_like_the_file() {
|
||||
let mut fx = Fixture::default();
|
||||
file_with_tracks(&mut fx, 1);
|
||||
rename(&mut fx);
|
||||
let file = fx.files.values().next().unwrap();
|
||||
assert_eq!(file.info.name, "renamed");
|
||||
assert_eq!(file.trk[0].info.name.as_deref(), Some("renamed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_track_with_a_name_is_renamed_too() {
|
||||
let mut fx = Fixture::default();
|
||||
file_with_tracks(&mut fx, 1);
|
||||
let id = *fx.files.keys().next().unwrap();
|
||||
std::rc::Rc::make_mut(fx.files.get_mut(&id).unwrap()).trk[0]
|
||||
.info
|
||||
.name = Some("track".to_owned());
|
||||
rename(&mut fx);
|
||||
let file = &fx.files[&id];
|
||||
assert_eq!(file.info.name, "renamed");
|
||||
assert_eq!(file.trk[0].info.name.as_deref(), Some("renamed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_several_tracks_are_not_renamed() {
|
||||
let mut fx = Fixture::default();
|
||||
file_with_tracks(&mut fx, 2);
|
||||
rename(&mut fx);
|
||||
let file = fx.files.values().next().unwrap();
|
||||
assert_eq!(file.info.name, "renamed");
|
||||
assert!(file.trk.iter().all(|trk| trk.info.name.is_none()));
|
||||
}
|
||||
}
|
||||
@@ -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,71 @@
|
||||
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,
|
||||
name: "file",
|
||||
}
|
||||
.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,284 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, FileId, Selection, StackEntry, State, Waypoint, edit_waypoint_chunks,
|
||||
};
|
||||
|
||||
/// Deletes the selected elements. With `whole_files`, the files holding the selected elements
|
||||
/// are deleted instead, even if only a track or a waypoint is selected.
|
||||
#[derive(Debug)]
|
||||
pub struct Delete {
|
||||
pub whole_files: bool,
|
||||
}
|
||||
|
||||
impl Apply for Delete {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let files = state.selection.to_files();
|
||||
let selection = if self.whole_files {
|
||||
&files
|
||||
} else {
|
||||
&*state.selection
|
||||
};
|
||||
let next = match 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(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) 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,
|
||||
name: "file",
|
||||
}
|
||||
.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 { whole_files: false }.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 { whole_files: false }
|
||||
.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 { whole_files: false }
|
||||
.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 { whole_files: false }
|
||||
.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 { whole_files: false }
|
||||
.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 { whole_files: false }
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert!(fx.files[&id].wpt.is_empty());
|
||||
fx.selection = Selection::Waypoints { file_id: id };
|
||||
assert_eq!(
|
||||
Delete { whole_files: false }.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 { whole_files: false }.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_whole_files_of_the_selected_elements() {
|
||||
for selection in [
|
||||
|id: FileId, fx: &Fixture| Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: HashSet::from([fx.files[&id].trk[0].id]),
|
||||
},
|
||||
|id: FileId, fx: &Fixture| Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: fx.files[&id].trk[0].id,
|
||||
trkseg_ids: HashSet::from([fx.files[&id].trk[0].trkseg[0].id]),
|
||||
},
|
||||
|id: FileId, _: &Fixture| Selection::Waypoints { file_id: id },
|
||||
|id: FileId, _: &Fixture| Selection::File {
|
||||
file_ids: HashSet::from([id]),
|
||||
},
|
||||
] {
|
||||
let (mut fx, id) = loaded();
|
||||
crate::New { name: "keep" }.apply(&mut fx.state()).unwrap();
|
||||
let keep = fx.order.0[1];
|
||||
fx.selection = selection(id, &fx);
|
||||
Delete { whole_files: true }.apply(&mut fx.state()).unwrap();
|
||||
assert_eq!(fx.order.0, vec![keep]);
|
||||
assert!(!fx.files.contains_key(&id));
|
||||
assert!(matches!(fx.selection, Selection::Empty));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_whole_files_nothing_selected() {
|
||||
let mut fx = Fixture::default();
|
||||
assert_eq!(
|
||||
Delete { whole_files: true }.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,121 @@
|
||||
use crate::{Apply, CommandError, FileId, Selection, State, WaypointId, delete_waypoints};
|
||||
|
||||
/// Deletes a waypoint of a file, whatever is selected (what the delete button of its popup does).
|
||||
/// If it was the only selected element, its file gets selected.
|
||||
#[derive(Debug)]
|
||||
pub struct DeleteWaypoint {
|
||||
pub file_id: FileId,
|
||||
pub waypoint_id: WaypointId,
|
||||
}
|
||||
|
||||
impl Apply for DeleteWaypoint {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
delete_waypoints(state.files, self.file_id, |wpt| wpt.id == self.waypoint_id)?;
|
||||
if let Selection::Waypoint { file_id, wpt_ids } = &*state.selection
|
||||
&& *file_id == self.file_id
|
||||
&& wpt_ids.iter().all(|id| *id == self.waypoint_id)
|
||||
{
|
||||
*state.selection = Selection::File {
|
||||
file_ids: [self.file_id].into(),
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
File, Selection, Waypoint, WaypointChunk, engine::command::fixture::Fixture, waypoint_ids,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deletes_one_waypoint_without_touching_the_selection() {
|
||||
let mut fx = Fixture::default();
|
||||
let mut file = File::default();
|
||||
file.wpt.push(Rc::new(WaypointChunk {
|
||||
wpt: (0..3)
|
||||
.map(|i| Waypoint {
|
||||
name: Some(format!("w{i}")),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}));
|
||||
let id = file.id;
|
||||
let ids: Vec<_> = waypoint_ids(&file).collect();
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.order.0.push(id);
|
||||
fx.selection = Selection::File {
|
||||
file_ids: [id].into(),
|
||||
};
|
||||
|
||||
DeleteWaypoint {
|
||||
file_id: id,
|
||||
waypoint_id: ids[1],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let names: Vec<_> = fx.files[&id]
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.map(|wpt| wpt.name.clone().unwrap())
|
||||
.collect();
|
||||
assert_eq!(names, ["w0", "w2"]);
|
||||
assert_eq!(
|
||||
fx.selection,
|
||||
Selection::File {
|
||||
file_ids: [id].into()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deleting_the_selected_waypoint_selects_its_file() {
|
||||
let mut fx = Fixture::default();
|
||||
let mut file = File::default();
|
||||
file.wpt.push(Rc::new(WaypointChunk {
|
||||
wpt: vec![Waypoint::default(), Waypoint::default()],
|
||||
..Default::default()
|
||||
}));
|
||||
let id = file.id;
|
||||
let ids: Vec<_> = waypoint_ids(&file).collect();
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.selection = Selection::Waypoint {
|
||||
file_id: id,
|
||||
wpt_ids: [ids[0]].into(),
|
||||
};
|
||||
DeleteWaypoint {
|
||||
file_id: id,
|
||||
waypoint_id: ids[0],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
fx.selection,
|
||||
Selection::File {
|
||||
file_ids: [id].into()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_waypoint_is_nothing_to_do() {
|
||||
let mut fx = Fixture::default();
|
||||
let file = File::default();
|
||||
let id = file.id;
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
let result = DeleteWaypoint {
|
||||
file_id: id,
|
||||
waypoint_id: WaypointId::default(),
|
||||
}
|
||||
.apply(&mut fx.state());
|
||||
assert!(matches!(result, Err(CommandError::NothingToDo)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, FileId, Selection, StackEntry, State, Waypoint, copy_file, copy_segment,
|
||||
copy_track, copy_waypoint, insert_waypoints,
|
||||
};
|
||||
|
||||
#[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 selected: Vec<&Waypoint> = file
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.filter(|wpt| filter(wpt))
|
||||
.collect();
|
||||
let last = selected.last().ok_or(CommandError::NothingToDo)?.id;
|
||||
let copies: Vec<Waypoint> = selected.into_iter().map(copy_waypoint).collect();
|
||||
let copy_ids = copies.iter().map(|wpt| wpt.id).collect();
|
||||
|
||||
// The copies go right after the last selected waypoint.
|
||||
let mut file = (**file).clone();
|
||||
insert_waypoints(&mut file, Some(last), copies);
|
||||
files.insert(file_id, Rc::new(file));
|
||||
Ok(Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: copy_ids,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
#[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,
|
||||
name: "file",
|
||||
}
|
||||
.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,220 @@
|
||||
use crate::{Apply, CommandError, File, State, TrackpointCategories, parse, produce};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Load<'a> {
|
||||
pub data: &'a [u8],
|
||||
/// Name of the file when the data has none (typically the name of the file on disk, without
|
||||
/// its extension).
|
||||
pub name: &'a str,
|
||||
}
|
||||
|
||||
impl Load<'_> {
|
||||
fn parse(self, categories: &mut TrackpointCategories) -> Result<File, CommandError> {
|
||||
let mut file = parse(self.data, categories)
|
||||
.map_err(|err| CommandError::InvalidData(err.to_string()))?;
|
||||
if file.info.name.trim().is_empty() {
|
||||
file.info.name = self.name.to_owned();
|
||||
}
|
||||
Ok(file)
|
||||
}
|
||||
}
|
||||
|
||||
impl Apply for Load<'_> {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
LoadFiles { files: vec![self] }.apply(state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads several files at once, as a single command (one undo step): the files are added at the
|
||||
/// end of the list, in the given order, and the first one is selected.
|
||||
///
|
||||
/// The files that cannot be read are skipped, unless none of them can: then nothing is loaded
|
||||
/// and the error is the one of the first file.
|
||||
#[derive(Debug)]
|
||||
pub struct LoadFiles<'a> {
|
||||
pub files: Vec<Load<'a>>,
|
||||
}
|
||||
|
||||
impl Apply for LoadFiles<'_> {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let mut files = Vec::with_capacity(self.files.len());
|
||||
let mut first_error = None;
|
||||
for load in self.files {
|
||||
match load.parse(state.categories) {
|
||||
Ok(file) => files.push(file),
|
||||
Err(error) => {
|
||||
first_error.get_or_insert(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
if files.is_empty() {
|
||||
return Err(first_error.unwrap_or(CommandError::NothingToDo));
|
||||
}
|
||||
produce(state, |_| files);
|
||||
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,
|
||||
name: "file",
|
||||
}
|
||||
.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>",
|
||||
name: "file",
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
fn load_with_name(data: &[u8]) -> String {
|
||||
let mut fx = Fixture::default();
|
||||
Load {
|
||||
data,
|
||||
name: "from disk",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
fx.files.values().next().unwrap().info.name.clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_keeps_the_name_of_the_file() {
|
||||
let data = br#"<gpx version="1.1"><metadata><name>in the file</name></metadata></gpx>"#;
|
||||
assert_eq!(load_with_name(data), "in the file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_without_name_uses_the_given_name() {
|
||||
let without = br#"<gpx version="1.1"><trk><trkseg></trkseg></trk></gpx>"#;
|
||||
assert_eq!(load_with_name(without), "from disk");
|
||||
let empty = br#"<gpx version="1.1"><metadata><name> </name></metadata></gpx>"#;
|
||||
assert_eq!(load_with_name(empty), "from disk");
|
||||
}
|
||||
|
||||
fn gpx(name: &str) -> Vec<u8> {
|
||||
format!(r#"<gpx version="1.1"><metadata><name>{name}</name></metadata></gpx>"#).into_bytes()
|
||||
}
|
||||
|
||||
fn load<'a>(data: &'a [u8], name: &'a str) -> Load<'a> {
|
||||
Load { data, name }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_several_files() {
|
||||
let mut fx = Fixture::default();
|
||||
crate::New { name: "before" }
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let before = fx.order.0[0];
|
||||
let (a, b, c) = (gpx("a"), gpx("b"), gpx("c"));
|
||||
|
||||
LoadFiles {
|
||||
files: vec![load(&a, "x"), load(&b, "x"), load(&c, "x")],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
// added after the others, in the order they were given
|
||||
let names: Vec<_> = fx
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.map(|id| fx.files[id].info.name.clone())
|
||||
.collect();
|
||||
assert_eq!(names, ["before", "a", "b", "c"]);
|
||||
assert_eq!(fx.order.0[0], before);
|
||||
// the first one is selected
|
||||
assert_eq!(fx.selected_files(), [fx.order.0[1]].into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_several_files_skips_the_invalid_ones() {
|
||||
let mut fx = Fixture::default();
|
||||
let (a, c) = (gpx("a"), gpx("c"));
|
||||
LoadFiles {
|
||||
files: vec![load(&a, "x"), load(b"<gpx><trk></gpx>", "x"), load(&c, "x")],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let names: Vec<_> = fx
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.map(|id| fx.files[id].info.name.clone())
|
||||
.collect();
|
||||
assert_eq!(names, ["a", "c"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_several_invalid_files_changes_nothing() {
|
||||
let mut fx = Fixture::default();
|
||||
crate::New { name: "keep" }.apply(&mut fx.state()).unwrap();
|
||||
let selected = fx.selected_files();
|
||||
|
||||
let result = LoadFiles {
|
||||
files: vec![
|
||||
load(b"<gpx><trk></gpx>", "x"),
|
||||
load(b"<gpx><wpt></gpx>", "y"),
|
||||
],
|
||||
}
|
||||
.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);
|
||||
|
||||
// nothing to load
|
||||
assert_eq!(
|
||||
LoadFiles { files: vec![] }.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_each_file_without_name_gets_its_own_name() {
|
||||
let mut fx = Fixture::default();
|
||||
let unnamed = br#"<gpx version="1.1"><trk><trkseg></trkseg></trk></gpx>"#;
|
||||
LoadFiles {
|
||||
files: vec![load(unnamed, "first"), load(unnamed, "second")],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let names: Vec<_> = fx
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.map(|id| fx.files[id].info.name.clone())
|
||||
.collect();
|
||||
assert_eq!(names, ["first", "second"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
mod delete;
|
||||
mod delete_all;
|
||||
mod delete_waypoint;
|
||||
mod duplicate;
|
||||
mod load;
|
||||
mod move_elements;
|
||||
mod new;
|
||||
mod paste;
|
||||
mod transfer;
|
||||
|
||||
pub use delete::*;
|
||||
pub use delete_all::*;
|
||||
pub use delete_waypoint::*;
|
||||
pub use duplicate::*;
|
||||
pub use load::*;
|
||||
pub use move_elements::*;
|
||||
pub use new::*;
|
||||
pub use paste::*;
|
||||
pub use transfer::*;
|
||||
@@ -0,0 +1,447 @@
|
||||
use crate::{
|
||||
Apply, Clipboard, CommandError, Destination, FileId, Place, Selection, State, TrackId, transfer,
|
||||
};
|
||||
|
||||
/// Where moved elements go: a list of the file tree, and the position in it. The position is
|
||||
/// counted among the elements of the list that are not moved, like the position of a dragged
|
||||
/// element in the list it is dropped in.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MoveTarget {
|
||||
/// The list of files.
|
||||
Files { index: usize },
|
||||
/// The tracks of a file.
|
||||
Tracks { file_id: FileId, index: usize },
|
||||
/// The segments of a track.
|
||||
Segments {
|
||||
file_id: FileId,
|
||||
trk_id: TrackId,
|
||||
index: usize,
|
||||
},
|
||||
/// The waypoints of a file.
|
||||
Waypoints { file_id: FileId, index: usize },
|
||||
}
|
||||
|
||||
/// Moves elements to a place of the file tree (what drag and drop does), with their ids. The
|
||||
/// moved elements are selected.
|
||||
///
|
||||
/// What can go where:
|
||||
/// - files: among the files;
|
||||
/// - tracks: among the files (each track becomes a file), or among the tracks of a file;
|
||||
/// - segments: among the files (each segment becomes a file), among the tracks of a file (each
|
||||
/// segment becomes a track), or among the segments of a track;
|
||||
/// - waypoints, or all the waypoints of a file (the waypoints node): among the waypoints of a
|
||||
/// file.
|
||||
#[derive(Debug)]
|
||||
pub struct Move {
|
||||
pub what: Selection,
|
||||
pub to: MoveTarget,
|
||||
}
|
||||
|
||||
impl From<MoveTarget> for Destination {
|
||||
fn from(target: MoveTarget) -> Self {
|
||||
match target {
|
||||
MoveTarget::Files { index } => Destination::Files(Place::Index(index)),
|
||||
MoveTarget::Tracks { file_id, index } => {
|
||||
Destination::Tracks(file_id, Place::Index(index))
|
||||
}
|
||||
MoveTarget::Segments {
|
||||
file_id,
|
||||
trk_id,
|
||||
index,
|
||||
} => Destination::Segments(file_id, trk_id, Place::Index(index)),
|
||||
MoveTarget::Waypoints { file_id, index } => {
|
||||
Destination::Waypoints(file_id, Place::Index(index))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Apply for Move {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let clipboard = Clipboard::new(&self.what, state.files, &state.order.0, true)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
*state.selection = transfer(state, &clipboard.content, true, self.to.into())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
File, Track, TrackInfo, TrackSegment, TrackSegmentId, Waypoint, WaypointChunk, WaypointId,
|
||||
engine::command::fixture::Fixture, new_file, waypoint_ids,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A file with the given tracks (name, number of segments) and number of waypoints.
|
||||
fn file(name: &str, tracks: &[(&str, usize)], waypoints: usize) -> File {
|
||||
let mut file = new_file(
|
||||
name,
|
||||
tracks
|
||||
.iter()
|
||||
.map(|(name, segments)| Track {
|
||||
info: TrackInfo {
|
||||
name: Some((*name).to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
trkseg: (0..*segments).map(|_| TrackSegment::default()).collect(),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
if waypoints > 0 {
|
||||
file.wpt = vec![Rc::new(WaypointChunk {
|
||||
wpt: (0..waypoints).map(|_| Waypoint::default()).collect(),
|
||||
..Default::default()
|
||||
})];
|
||||
}
|
||||
file
|
||||
}
|
||||
|
||||
fn add(fx: &mut Fixture, file: File) -> FileId {
|
||||
let id = file.id;
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.order.0.push(id);
|
||||
id
|
||||
}
|
||||
|
||||
fn track_ids(fx: &Fixture, id: FileId) -> Vec<TrackId> {
|
||||
fx.files[&id].trk.iter().map(|trk| trk.id).collect()
|
||||
}
|
||||
|
||||
fn segment_ids(fx: &Fixture, id: FileId, trk: usize) -> Vec<TrackSegmentId> {
|
||||
fx.files[&id].trk[trk].trkseg.iter().map(|s| s.id).collect()
|
||||
}
|
||||
|
||||
fn wpt_ids(fx: &Fixture, id: FileId) -> Vec<WaypointId> {
|
||||
waypoint_ids(&fx.files[&id]).collect()
|
||||
}
|
||||
|
||||
fn do_move(fx: &mut Fixture, what: Selection, to: MoveTarget) -> Result<(), CommandError> {
|
||||
Move { what, to }.apply(&mut fx.state())
|
||||
}
|
||||
|
||||
fn tracks(file_id: FileId, ids: &[TrackId]) -> Selection {
|
||||
Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn segments(file_id: FileId, trk_id: TrackId, ids: &[TrackSegmentId]) -> Selection {
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn waypoints(file_id: FileId, ids: &[WaypointId]) -> Selection {
|
||||
Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn files(ids: &[FileId]) -> Selection {
|
||||
Selection::File {
|
||||
file_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reorder_tracks_in_a_file() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("a", &[("0", 1), ("1", 1), ("2", 1), ("3", 1)], 0),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
(fx, a, t)
|
||||
};
|
||||
let at = |a, index| MoveTarget::Tracks { file_id: a, index };
|
||||
|
||||
// the position counts the tracks that stay
|
||||
let (mut fx, a, t) = build();
|
||||
do_move(&mut fx, tracks(a, &[t[0]]), at(a, 1)).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1], t[0], t[2], t[3]]);
|
||||
assert_eq!(fx.selection, tracks(a, &[t[0]]));
|
||||
|
||||
let (mut fx, a, t) = build();
|
||||
do_move(&mut fx, tracks(a, &[t[3]]), at(a, 0)).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[3], t[0], t[1], t[2]]);
|
||||
|
||||
// several at once, as a block, in their order; past the end is the end
|
||||
let (mut fx, a, t) = build();
|
||||
do_move(&mut fx, tracks(a, &[t[1], t[0]]), at(a, 99)).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[2], t[3], t[0], t[1]]);
|
||||
assert_eq!(fx.selection, tracks(a, &[t[0], t[1]]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_tracks_to_another_file_or_to_new_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[("t0", 1), ("t1", 1)], 0));
|
||||
let b = add(&mut fx, file("b", &[("b0", 1), ("b1", 1)], 0));
|
||||
let c = add(&mut fx, file("c", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
let b_tracks = track_ids(&fx, b);
|
||||
|
||||
do_move(
|
||||
&mut fx,
|
||||
tracks(a, &[t[0]]),
|
||||
MoveTarget::Tracks {
|
||||
file_id: b,
|
||||
index: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1]]);
|
||||
assert_eq!(track_ids(&fx, b), vec![b_tracks[0], t[0], b_tracks[1]]);
|
||||
assert_eq!(fx.selection, tracks(b, &[t[0]]));
|
||||
|
||||
// as a new file, at a position of the list of files
|
||||
do_move(&mut fx, tracks(a, &[t[1]]), MoveTarget::Files { index: 1 }).unwrap();
|
||||
assert!(track_ids(&fx, a).is_empty());
|
||||
assert_eq!(fx.order.0.len(), 4);
|
||||
let new = fx.order.0[1];
|
||||
assert_eq!(fx.order.0[0], a);
|
||||
assert_eq!(fx.order.0[2..], [b, c]);
|
||||
assert_eq!(track_ids(&fx, new), vec![t[1]]);
|
||||
assert_eq!(fx.files[&new].info.name, "t1");
|
||||
assert_eq!(fx.selection, files(&[new]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_segments() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[("t0", 3), ("t1", 1)], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
(fx, a, t)
|
||||
};
|
||||
|
||||
// within a track
|
||||
let (mut fx, a, t) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
do_move(
|
||||
&mut fx,
|
||||
segments(a, t[0], &[s[2]]),
|
||||
MoveTarget::Segments {
|
||||
file_id: a,
|
||||
trk_id: t[0],
|
||||
index: 0,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[2], s[0], s[1]]);
|
||||
assert_eq!(fx.selection, segments(a, t[0], &[s[2]]));
|
||||
|
||||
// to another track
|
||||
let (mut fx, a, t) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let other = segment_ids(&fx, a, 1);
|
||||
do_move(
|
||||
&mut fx,
|
||||
segments(a, t[0], &[s[0], s[1]]),
|
||||
MoveTarget::Segments {
|
||||
file_id: a,
|
||||
trk_id: t[1],
|
||||
index: 0,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[2]]);
|
||||
assert_eq!(segment_ids(&fx, a, 1), vec![s[0], s[1], other[0]]);
|
||||
|
||||
// as new tracks of a file
|
||||
let (mut fx, a, t) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
do_move(
|
||||
&mut fx,
|
||||
segments(a, t[0], &[s[1]]),
|
||||
MoveTarget::Tracks {
|
||||
file_id: a,
|
||||
index: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(fx.files[&a].trk.len(), 3);
|
||||
assert_eq!(segment_ids(&fx, a, 1), vec![s[1]]);
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[0], s[2]]);
|
||||
assert_eq!(fx.files[&a].trk[0].id, t[0]);
|
||||
assert_eq!(fx.files[&a].trk[2].id, t[1]);
|
||||
|
||||
// as new files
|
||||
let (mut fx, a, t) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
do_move(
|
||||
&mut fx,
|
||||
segments(a, t[0], &[s[0], s[1]]),
|
||||
MoveTarget::Files { index: 0 },
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(fx.order.0.len(), 3);
|
||||
assert_eq!(fx.order.0[2], a);
|
||||
assert_eq!(fx.files[&fx.order.0[0]].info.name, "t0");
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[2]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_waypoints() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 4));
|
||||
let b = add(&mut fx, file("b", &[], 2));
|
||||
(fx, a, b)
|
||||
};
|
||||
let at = |file_id, index| MoveTarget::Waypoints { file_id, index };
|
||||
|
||||
// within a file
|
||||
let (mut fx, a, _) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
do_move(&mut fx, waypoints(a, &[w[3]]), at(a, 1)).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), vec![w[0], w[3], w[1], w[2]]);
|
||||
assert_eq!(fx.selection, waypoints(a, &[w[3]]));
|
||||
|
||||
// to the start of another file
|
||||
let (mut fx, a, b) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = wpt_ids(&fx, b);
|
||||
do_move(&mut fx, waypoints(a, &[w[0], w[1]]), at(b, 0)).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), w[2..].to_vec());
|
||||
assert_eq!(wpt_ids(&fx, b), [&w[..2], &before[..]].concat());
|
||||
|
||||
// all the waypoints of a file
|
||||
let (mut fx, a, b) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = wpt_ids(&fx, b);
|
||||
do_move(&mut fx, Selection::Waypoints { file_id: a }, at(b, 1)).unwrap();
|
||||
assert!(wpt_ids(&fx, a).is_empty());
|
||||
assert_eq!(
|
||||
wpt_ids(&fx, b),
|
||||
[&before[..1], &w[..], &before[1..]].concat()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let c = add(&mut fx, file("c", &[], 0));
|
||||
do_move(&mut fx, files(&[c]), MoveTarget::Files { index: 0 }).unwrap();
|
||||
assert_eq!(fx.order.0, vec![c, a, b]);
|
||||
do_move(&mut fx, files(&[c, a]), MoveTarget::Files { index: 1 }).unwrap();
|
||||
assert_eq!(fx.order.0, vec![b, c, a]);
|
||||
assert_eq!(fx.selection, files(&[c, a]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_what_cannot_be_moved() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[("t", 1)], 1));
|
||||
let t = track_ids(&fx, a);
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = fx.files[&a].clone();
|
||||
let selection = fx.selection.clone();
|
||||
|
||||
let invalid = [
|
||||
// the wrong kind of list
|
||||
(
|
||||
files(&[a]),
|
||||
MoveTarget::Tracks {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
tracks(a, &t),
|
||||
MoveTarget::Waypoints {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
tracks(a, &t),
|
||||
MoveTarget::Segments {
|
||||
file_id: a,
|
||||
trk_id: t[0],
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
segments(a, t[0], &s),
|
||||
MoveTarget::Waypoints {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
waypoints(a, &w),
|
||||
MoveTarget::Tracks {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(waypoints(a, &w), MoveTarget::Files { index: 0 }),
|
||||
(
|
||||
Selection::Waypoints { file_id: a },
|
||||
MoveTarget::Files { index: 0 },
|
||||
),
|
||||
(Selection::Empty, MoveTarget::Files { index: 0 }),
|
||||
// elements that do not exist
|
||||
(
|
||||
tracks(a, &[TrackId::default()]),
|
||||
MoveTarget::Files { index: 0 },
|
||||
),
|
||||
(
|
||||
waypoints(a, &[WaypointId::default()]),
|
||||
MoveTarget::Waypoints {
|
||||
file_id: a,
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(files(&[FileId::default()]), MoveTarget::Files { index: 0 }),
|
||||
// a place that does not exist
|
||||
(
|
||||
tracks(a, &t),
|
||||
MoveTarget::Tracks {
|
||||
file_id: FileId::default(),
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
segments(a, t[0], &s),
|
||||
MoveTarget::Segments {
|
||||
file_id: a,
|
||||
trk_id: TrackId::default(),
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
(
|
||||
waypoints(a, &w),
|
||||
MoveTarget::Waypoints {
|
||||
file_id: FileId::default(),
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
];
|
||||
for (what, to) in invalid {
|
||||
assert_eq!(
|
||||
do_move(&mut fx, what.clone(), to),
|
||||
Err(CommandError::NothingToDo),
|
||||
"{what:?} to {to:?}"
|
||||
);
|
||||
// nothing changed
|
||||
assert!(Rc::ptr_eq(&fx.files[&a], &before));
|
||||
assert_eq!(fx.order.0, vec![a]);
|
||||
assert_eq!(fx.selection, selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,790 @@
|
||||
use std::{collections::HashSet, hash::Hash};
|
||||
|
||||
use crate::{
|
||||
Apply, ClipboardContent, CommandError, Destination, FileId, Place, Selection, State, TrackId,
|
||||
TrackSegmentId, WaypointId, transfer, waypoint_ids,
|
||||
};
|
||||
|
||||
/// Pastes what was copied or cut (see [`crate::Clipboard`]).
|
||||
///
|
||||
/// Where it goes depends on the selection, see [`crate::Clipboard::can_paste`]:
|
||||
/// - nothing selected: the files are copied, and tracks and segments become new files (named
|
||||
/// like their track, or like the file they come from);
|
||||
/// - a file: the tracks are added to it, segments become new tracks in it, and the waypoints are
|
||||
/// added to its own;
|
||||
/// - a track: the segments are added to it, tracks go after it in its file;
|
||||
/// - a segment: the segments go after it in its track;
|
||||
/// - the waypoints or a waypoint: the waypoints are added to their file, after the waypoint.
|
||||
///
|
||||
/// Copies are made, with new ids, and the pasted elements are selected. What was cut is moved
|
||||
/// instead, with its ids, the first time: what is still where it was is removed.
|
||||
///
|
||||
/// What is pasted is always the elements as they were when they were copied or cut, even if they
|
||||
/// were changed or deleted since. The clipboard is kept so that it can be pasted again: after
|
||||
/// what was cut was moved it is not cut anymore, and the next pastes make copies.
|
||||
#[derive(Debug)]
|
||||
pub struct Paste;
|
||||
|
||||
impl Apply for Paste {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let clipboard = state
|
||||
.clipboard
|
||||
.clone()
|
||||
.filter(|clipboard| clipboard.can_paste(state.selection))
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let cut = clipboard.cut;
|
||||
// always the elements as they were when they were copied or cut
|
||||
let to = destination(state, &clipboard.content)?;
|
||||
let next = transfer(state, &clipboard.content, cut, to)?;
|
||||
|
||||
if cut && let Some(clipboard) = state.clipboard.as_mut() {
|
||||
// moved: the next pastes make copies
|
||||
clipboard.cut = false;
|
||||
}
|
||||
*state.selection = next;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn track_ids_of(state: &State, file_id: FileId) -> Vec<TrackId> {
|
||||
state
|
||||
.files
|
||||
.get(&file_id)
|
||||
.map(|file| file.trk.iter().map(|trk| trk.id).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn segment_ids_of(state: &State, file_id: FileId, trk_id: TrackId) -> Vec<TrackSegmentId> {
|
||||
state
|
||||
.files
|
||||
.get(&file_id)
|
||||
.and_then(|file| file.trk.iter().find(|trk| trk.id == trk_id))
|
||||
.map(|trk| trk.trkseg.iter().map(|seg| seg.id).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn waypoint_ids_of(state: &State, file_id: FileId) -> Vec<WaypointId> {
|
||||
state
|
||||
.files
|
||||
.get(&file_id)
|
||||
.map(|file| waypoint_ids(file).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Where the content goes, depending on what is selected.
|
||||
fn destination(state: &State, content: &ClipboardContent) -> Result<Destination, CommandError> {
|
||||
let nothing = CommandError::NothingToDo;
|
||||
let last_file = |file_ids: &HashSet<FileId>| {
|
||||
last_selected(state.order.0.iter().copied(), file_ids).ok_or(CommandError::NothingToDo)
|
||||
};
|
||||
Ok(match (content, &*state.selection) {
|
||||
(ClipboardContent::Files(_), _) => Destination::Files(Place::End),
|
||||
(ClipboardContent::Tracks(_) | ClipboardContent::Segments(_), Selection::Empty) => {
|
||||
Destination::Files(Place::End)
|
||||
}
|
||||
(
|
||||
ClipboardContent::Tracks(_) | ClipboardContent::Segments(_),
|
||||
Selection::File { file_ids },
|
||||
) => Destination::Tracks(last_file(file_ids)?, Place::End),
|
||||
(ClipboardContent::Tracks(_), Selection::Track { file_id, trk_ids }) => {
|
||||
let last =
|
||||
last_selected(track_ids_of(state, *file_id).into_iter(), trk_ids).ok_or(nothing)?;
|
||||
Destination::Tracks(*file_id, Place::After(last))
|
||||
}
|
||||
(ClipboardContent::Segments(_), Selection::Track { file_id, trk_ids }) => {
|
||||
let last =
|
||||
last_selected(track_ids_of(state, *file_id).into_iter(), trk_ids).ok_or(nothing)?;
|
||||
Destination::Segments(*file_id, last, Place::End)
|
||||
}
|
||||
(
|
||||
ClipboardContent::Segments(_),
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
},
|
||||
) => {
|
||||
let last = last_selected(
|
||||
segment_ids_of(state, *file_id, *trk_id).into_iter(),
|
||||
trkseg_ids,
|
||||
)
|
||||
.ok_or(nothing)?;
|
||||
Destination::Segments(*file_id, *trk_id, Place::After(last))
|
||||
}
|
||||
(ClipboardContent::Waypoints(_), Selection::File { file_ids }) => {
|
||||
Destination::Waypoints(last_file(file_ids)?, Place::End)
|
||||
}
|
||||
(ClipboardContent::Waypoints(_), Selection::Waypoints { file_id }) => {
|
||||
Destination::Waypoints(*file_id, Place::End)
|
||||
}
|
||||
(ClipboardContent::Waypoints(_), Selection::Waypoint { file_id, wpt_ids }) => {
|
||||
let last = last_selected(waypoint_ids_of(state, *file_id).into_iter(), wpt_ids)
|
||||
.ok_or(nothing)?;
|
||||
Destination::Waypoints(*file_id, Place::After(last))
|
||||
}
|
||||
_ => return Err(nothing),
|
||||
})
|
||||
}
|
||||
|
||||
/// The last of the `selected` ids in the order of `all`: the element a paste goes after.
|
||||
fn last_selected<T: Copy + Eq + Hash>(
|
||||
all: impl Iterator<Item = T>,
|
||||
selected: &HashSet<T>,
|
||||
) -> Option<T> {
|
||||
all.filter(|id| selected.contains(id)).last()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Clipboard, ClipboardIds, File, FileId, Track, TrackId, TrackInfo, TrackSegment,
|
||||
TrackSegmentId, Waypoint, WaypointId, engine::command::fixture::Fixture, new_file,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A file with the given tracks (name, number of segments) and number of waypoints.
|
||||
fn file(name: &str, tracks: &[(Option<&str>, usize)], waypoints: usize) -> File {
|
||||
let mut file = new_file(
|
||||
name,
|
||||
tracks
|
||||
.iter()
|
||||
.map(|(name, segments)| Track {
|
||||
info: TrackInfo {
|
||||
name: name.map(str::to_owned),
|
||||
..Default::default()
|
||||
},
|
||||
trkseg: (0..*segments).map(|_| TrackSegment::default()).collect(),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
if waypoints > 0 {
|
||||
file.wpt = vec![Rc::new(crate::WaypointChunk {
|
||||
wpt: (0..waypoints).map(|_| Waypoint::default()).collect(),
|
||||
..Default::default()
|
||||
})];
|
||||
}
|
||||
file
|
||||
}
|
||||
|
||||
fn add(fx: &mut Fixture, file: File) -> FileId {
|
||||
let id = file.id;
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.order.0.push(id);
|
||||
id
|
||||
}
|
||||
|
||||
fn track_ids(fx: &Fixture, id: FileId) -> Vec<TrackId> {
|
||||
fx.files[&id].trk.iter().map(|trk| trk.id).collect()
|
||||
}
|
||||
|
||||
fn segment_ids(fx: &Fixture, id: FileId, trk: usize) -> Vec<TrackSegmentId> {
|
||||
fx.files[&id].trk[trk].trkseg.iter().map(|s| s.id).collect()
|
||||
}
|
||||
|
||||
fn wpt_ids(fx: &Fixture, id: FileId) -> Vec<WaypointId> {
|
||||
waypoint_ids(&fx.files[&id]).collect()
|
||||
}
|
||||
|
||||
fn names(fx: &Fixture) -> Vec<String> {
|
||||
fx.order
|
||||
.0
|
||||
.iter()
|
||||
.map(|id| fx.files[id].info.name.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn copy(fx: &mut Fixture, selection: Selection, cut: bool) {
|
||||
fx.clipboard = Clipboard::new(&selection, &fx.files, &fx.order.0, cut);
|
||||
}
|
||||
|
||||
/// The ids of what is in the clipboard, and whether it is cut.
|
||||
fn clipboard_state(fx: &Fixture) -> Option<(ClipboardIds, bool)> {
|
||||
fx.clipboard.as_ref().map(|c| (c.content.ids(), c.cut))
|
||||
}
|
||||
|
||||
fn paste(fx: &mut Fixture) -> Result<(), CommandError> {
|
||||
Paste.apply(&mut fx.state())
|
||||
}
|
||||
|
||||
fn tracks(file_id: FileId, ids: &[TrackId]) -> Selection {
|
||||
Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn segments(file_id: FileId, trk_id: TrackId, ids: &[TrackSegmentId]) -> Selection {
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn waypoints(file_id: FileId, ids: &[WaypointId]) -> Selection {
|
||||
Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn files(ids: &[FileId]) -> Selection {
|
||||
Selection::File {
|
||||
file_ids: ids.iter().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_copied_tracks_into_a_file() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("a", &[(Some("t0"), 1), (Some("t1"), 2), (None, 1)], 0),
|
||||
);
|
||||
let b = add(&mut fx, file("b", &[(Some("b0"), 1)], 0));
|
||||
let source = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[source[0], source[1]]), false);
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
|
||||
// the source is untouched, copies with new ids are added at the end
|
||||
assert_eq!(track_ids(&fx, a), source);
|
||||
let in_b = track_ids(&fx, b);
|
||||
assert_eq!(in_b.len(), 3);
|
||||
assert!(!source.contains(&in_b[1]) && !source.contains(&in_b[2]));
|
||||
let names: Vec<_> = fx.files[&b]
|
||||
.trk
|
||||
.iter()
|
||||
.map(|t| t.info.name.clone())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
[Some("b0".into()), Some("t0".into()), Some("t1".into())]
|
||||
);
|
||||
// the segments are copies too
|
||||
assert_eq!(fx.files[&b].trk[2].trkseg.len(), 2);
|
||||
assert_ne!(segment_ids(&fx, b, 2), segment_ids(&fx, a, 1));
|
||||
// the pasted tracks are selected, and the clipboard is still there
|
||||
assert_eq!(fx.selection, tracks(b, &in_b[1..]));
|
||||
assert_eq!(
|
||||
clipboard_state(&fx),
|
||||
Some((ClipboardIds::Tracks(vec![source[0], source[1]]), false))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_cut_tracks_moves_them() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t0"), 1), (Some("t1"), 1)], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let source = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[source[0]]), true);
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
|
||||
assert_eq!(track_ids(&fx, a), vec![source[1]]);
|
||||
assert_eq!(track_ids(&fx, b), vec![source[0]]);
|
||||
assert_eq!(fx.selection, tracks(b, &[source[0]]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_tracks_after_the_selected_track() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file(
|
||||
"a",
|
||||
&[
|
||||
(Some("0"), 1),
|
||||
(Some("1"), 1),
|
||||
(Some("2"), 1),
|
||||
(Some("3"), 1),
|
||||
],
|
||||
0,
|
||||
),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
// copy: the block goes after the selected track
|
||||
copy(&mut fx, tracks(a, &[t[0], t[1]]), false);
|
||||
fx.selection = tracks(a, &[t[2]]);
|
||||
paste(&mut fx).unwrap();
|
||||
let names: Vec<_> = fx.files[&a]
|
||||
.trk
|
||||
.iter()
|
||||
.map(|t| t.info.name.clone().unwrap())
|
||||
.collect();
|
||||
assert_eq!(names, ["0", "1", "2", "0", "1", "3"]);
|
||||
|
||||
// cut: moved after the selected track
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("a", &[(Some("0"), 1), (Some("1"), 1), (Some("2"), 1)], 0),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
fx.selection = tracks(a, &[t[2]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1], t[2], t[0]]);
|
||||
|
||||
// the selected track is cut itself: the tracks go at the end
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("a", &[(Some("0"), 1), (Some("1"), 1), (Some("2"), 1)], 0),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
fx.selection = tracks(a, &[t[0]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1], t[2], t[0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_tracks_as_new_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("source", &[(Some("named"), 1), (None, 1)], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0], t[1]]), false);
|
||||
fx.selection = Selection::Empty;
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
|
||||
// named like the track, or like the file it comes from
|
||||
assert_eq!(names(&fx), ["source", "named", "source"]);
|
||||
assert_eq!(fx.files.len(), 3);
|
||||
let new = &fx.order.0[1..];
|
||||
assert!(
|
||||
matches!(&fx.selection, Selection::File { file_ids } if file_ids.len() == 2 && new.iter().all(|id| file_ids.contains(id)))
|
||||
);
|
||||
assert_eq!(fx.files[&new[0]].trk.len(), 1);
|
||||
// the source keeps its tracks
|
||||
assert_eq!(track_ids(&fx, a), t);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_cut_tracks_as_new_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("source", &[(Some("one"), 1), (Some("two"), 1)], 0),
|
||||
);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1]]);
|
||||
let new = fx.order.0[1];
|
||||
assert_eq!(track_ids(&fx, new), vec![t[0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_segments() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t0"), 3), (Some("t1"), 1)], 0));
|
||||
(fx, a)
|
||||
};
|
||||
|
||||
// at the end of the selected track
|
||||
let (mut fx, a) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, segments(a, t[0], &[s[0]]), false);
|
||||
fx.selection = tracks(a, &[t[1]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(segment_ids(&fx, a, 0), s);
|
||||
let in_t1 = segment_ids(&fx, a, 1);
|
||||
assert_eq!(in_t1.len(), 2);
|
||||
assert_ne!(in_t1[1], s[0]);
|
||||
assert_eq!(fx.selection, segments(a, t[1], &in_t1[1..]));
|
||||
|
||||
// after the selected segment, moved
|
||||
let (mut fx, a) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, segments(a, t[0], &[s[0]]), true);
|
||||
fx.selection = segments(a, t[0], &[s[1]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(segment_ids(&fx, a, 0), vec![s[1], s[0], s[2]]);
|
||||
|
||||
// each segment becomes a track of the selected file
|
||||
let (mut fx, a) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, segments(a, t[0], &[s[0], s[1]]), false);
|
||||
fx.selection = files(&[a]);
|
||||
paste(&mut fx).unwrap();
|
||||
let tracks_now = track_ids(&fx, a);
|
||||
assert_eq!(tracks_now.len(), 4);
|
||||
assert!(
|
||||
fx.files[&a].trk[2..]
|
||||
.iter()
|
||||
.all(|trk| trk.trkseg.len() == 1)
|
||||
);
|
||||
assert_eq!(fx.selection, tracks(a, &tracks_now[2..]));
|
||||
|
||||
// each segment becomes a file, named like its track
|
||||
let (mut fx, a) = build();
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, segments(a, t[0], &[s[0], s[1]]), false);
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(names(&fx), ["a", "t0", "t0"]);
|
||||
assert!(
|
||||
fx.order.0[1..]
|
||||
.iter()
|
||||
.all(|id| fx.files[id].trk[0].trkseg.len() == 1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_waypoints() {
|
||||
let build = || {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 4));
|
||||
let b = add(&mut fx, file("b", &[], 2));
|
||||
(fx, a, b)
|
||||
};
|
||||
|
||||
// to the end of another file
|
||||
let (mut fx, a, b) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = wpt_ids(&fx, b);
|
||||
copy(&mut fx, waypoints(a, &[w[1], w[2]]), false);
|
||||
fx.selection = files(&[b]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), w);
|
||||
let now = wpt_ids(&fx, b);
|
||||
assert_eq!(now.len(), 4);
|
||||
assert_eq!(&now[..2], &before[..]);
|
||||
assert!(!w.contains(&now[2]));
|
||||
assert_eq!(fx.selection, waypoints(b, &now[2..]));
|
||||
|
||||
// after the selected waypoint
|
||||
let (mut fx, a, b) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
let before = wpt_ids(&fx, b);
|
||||
copy(&mut fx, waypoints(a, &[w[0]]), true);
|
||||
fx.selection = waypoints(b, &[before[0]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), w[1..].to_vec());
|
||||
assert_eq!(wpt_ids(&fx, b), vec![before[0], w[0], before[1]]);
|
||||
|
||||
// within the file, after a waypoint
|
||||
let (mut fx, a, _) = build();
|
||||
let w = wpt_ids(&fx, a);
|
||||
copy(&mut fx, waypoints(a, &[w[0], w[1]]), true);
|
||||
fx.selection = waypoints(a, &[w[2]]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), vec![w[2], w[0], w[1], w[3]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_all_the_waypoints_of_a_file() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 3));
|
||||
let b = add(&mut fx, file("b", &[], 1));
|
||||
let w = wpt_ids(&fx, a);
|
||||
copy(&mut fx, Selection::Waypoints { file_id: a }, false);
|
||||
fx.selection = Selection::Waypoints { file_id: b };
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, a), w);
|
||||
assert_eq!(wpt_ids(&fx, b).len(), 4);
|
||||
|
||||
// moved: the source has none left
|
||||
copy(&mut fx, Selection::Waypoints { file_id: a }, true);
|
||||
fx.selection = files(&[b]);
|
||||
paste(&mut fx).unwrap();
|
||||
assert!(wpt_ids(&fx, a).is_empty());
|
||||
assert_eq!(&wpt_ids(&fx, b)[4..], &w[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 1)], 1));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let c = add(&mut fx, file("c", &[], 0));
|
||||
copy(&mut fx, files(&[b, a]), false);
|
||||
fx.selection = files(&[c]);
|
||||
paste(&mut fx).unwrap();
|
||||
// copies, in the order of the files, at the end
|
||||
assert_eq!(names(&fx), ["a", "b", "c", "a", "b"]);
|
||||
assert_eq!(fx.files.len(), 5);
|
||||
assert_eq!(fx.files[&fx.order.0[3]].trk.len(), 1);
|
||||
assert_ne!(track_ids(&fx, fx.order.0[3]), track_ids(&fx, a));
|
||||
assert!(matches!(&fx.selection, Selection::File { file_ids } if file_ids.len() == 2));
|
||||
|
||||
// cut: moved to the end, they keep their ids
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let c = add(&mut fx, file("c", &[], 0));
|
||||
copy(&mut fx, files(&[a]), true);
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.order.0, vec![b, c, a]);
|
||||
assert_eq!(fx.selection, files(&[a]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nothing_to_paste() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 1)], 1));
|
||||
let t = track_ids(&fx, a);
|
||||
let w = wpt_ids(&fx, a);
|
||||
fx.selection = files(&[a]);
|
||||
|
||||
// an empty clipboard
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
|
||||
// a target that does not accept it (tracks cannot go to waypoints), the clipboard stays
|
||||
copy(&mut fx, tracks(a, &t), false);
|
||||
fx.selection = waypoints(a, &w);
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
assert!(fx.clipboard.is_some());
|
||||
assert_eq!(fx.selection, waypoints(a, &w));
|
||||
|
||||
// nothing to copy: what does not exist is not put in the clipboard
|
||||
copy(&mut fx, tracks(a, &[TrackId::default()]), false);
|
||||
fx.selection = files(&[a]);
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
copy(&mut fx, waypoints(a, &[WaypointId::default()]), false);
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
copy(&mut fx, files(&[FileId::default()]), false);
|
||||
fx.selection = Selection::Empty;
|
||||
assert_eq!(paste(&mut fx), Err(CommandError::NothingToDo));
|
||||
assert_eq!(fx.files.len(), 1);
|
||||
assert_eq!(fx.order.0, vec![a]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_a_copy_several_times() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t0"), 1), (Some("t1"), 1)], 1));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), false);
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
paste(&mut fx).unwrap();
|
||||
paste(&mut fx).unwrap();
|
||||
|
||||
// each paste made a copy with new ids, and the source is untouched
|
||||
let in_b = track_ids(&fx, b);
|
||||
assert_eq!(in_b.len(), 3);
|
||||
assert_eq!(in_b.iter().collect::<HashSet<_>>().len(), 3);
|
||||
assert!(in_b.iter().all(|id| !t.contains(id)));
|
||||
assert_eq!(track_ids(&fx, a), t);
|
||||
assert!(fx.clipboard.is_some());
|
||||
|
||||
// the selection is the last copy: the next paste goes after it
|
||||
assert_eq!(fx.selection, tracks(b, &[in_b[2]]));
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(track_ids(&fx, b).len(), 4);
|
||||
|
||||
// the same with other elements
|
||||
let w = wpt_ids(&fx, a);
|
||||
copy(&mut fx, waypoints(a, &w), false);
|
||||
fx.selection = files(&[b]);
|
||||
paste(&mut fx).unwrap();
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, b).len(), 2);
|
||||
assert_eq!(wpt_ids(&fx, a), w);
|
||||
|
||||
copy(&mut fx, files(&[b]), false);
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.files.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_a_cut_moves_the_first_time_then_copies() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t0"), 1), (Some("t1"), 1)], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let c = add(&mut fx, file("c", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
// moved with its id, and what is in the clipboard now is a copy of it
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1]]);
|
||||
assert_eq!(track_ids(&fx, b), vec![t[0]]);
|
||||
assert_eq!(
|
||||
clipboard_state(&fx),
|
||||
Some((ClipboardIds::Tracks(vec![t[0]]), false))
|
||||
);
|
||||
|
||||
fx.selection = files(&[c]);
|
||||
paste(&mut fx).unwrap();
|
||||
paste(&mut fx).unwrap();
|
||||
// copies: the moved track stays where it was moved to
|
||||
assert_eq!(track_ids(&fx, b), vec![t[0]]);
|
||||
let in_c = track_ids(&fx, c);
|
||||
assert_eq!(in_c.len(), 2);
|
||||
assert!(!in_c.contains(&t[0]));
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_cut_files_several_times() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
copy(&mut fx, files(&[a]), true);
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.order.0, vec![b, a]);
|
||||
// the next pastes copy it
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.files.len(), 3);
|
||||
assert_eq!(fx.order.0[..2], [b, a]);
|
||||
}
|
||||
|
||||
/// Renames the first track of the file, as an edit would.
|
||||
fn rename_track(fx: &mut Fixture, id: FileId, name: &str) {
|
||||
let file = std::rc::Rc::make_mut(fx.files.get_mut(&id).unwrap());
|
||||
file.trk[0].info.name = Some(name.to_owned());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_the_elements_as_they_were_when_copied() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("original"), 1)], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), false);
|
||||
rename_track(&mut fx, a, "edited");
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
let pasted = &fx.files[&b].trk[0];
|
||||
assert_eq!(pasted.info.name.as_deref(), Some("original"));
|
||||
// the source keeps what was done to it
|
||||
assert_eq!(fx.files[&a].trk[0].info.name.as_deref(), Some("edited"));
|
||||
|
||||
// and each time
|
||||
paste(&mut fx).unwrap();
|
||||
assert!(
|
||||
fx.files[&b]
|
||||
.trk
|
||||
.iter()
|
||||
.all(|trk| trk.info.name.as_deref() == Some("original"))
|
||||
);
|
||||
assert_eq!(fx.files[&b].trk.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_a_cut_as_it_was_when_cut() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(
|
||||
&mut fx,
|
||||
file("a", &[(Some("original"), 1), (Some("other"), 1)], 0),
|
||||
);
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
rename_track(&mut fx, a, "edited");
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
// what is in the clipboard is moved, the edited version is not what is pasted
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1]]);
|
||||
assert_eq!(track_ids(&fx, b), vec![t[0]]);
|
||||
assert_eq!(fx.files[&b].trk[0].info.name.as_deref(), Some("original"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_after_the_elements_were_deleted() {
|
||||
let delete_file = |fx: &mut Fixture, id: FileId| {
|
||||
fx.files.remove(&id);
|
||||
fx.order.0.retain(|other| *other != id);
|
||||
};
|
||||
|
||||
// copied tracks, waypoints, segments and files are still there to be pasted
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 2)], 2));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
let s = segment_ids(&fx, a, 0);
|
||||
let w = wpt_ids(&fx, a);
|
||||
|
||||
copy(&mut fx, tracks(a, &t), false);
|
||||
fx.selection = files(&[b]);
|
||||
delete_file(&mut fx, a);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.files[&b].trk.len(), 1);
|
||||
assert_eq!(fx.files[&b].trk[0].trkseg.len(), 2);
|
||||
// the copy of a track comes from a file that is gone: named like the track
|
||||
fx.selection = Selection::Empty;
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(names(&fx), ["b", "t"]);
|
||||
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 2)], 2));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let (t, s, w) = (track_ids(&fx, a), segment_ids(&fx, a, 0), wpt_ids(&fx, a));
|
||||
copy(&mut fx, segments(a, t[0], &s), false);
|
||||
fx.selection = files(&[b]);
|
||||
delete_file(&mut fx, a);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.files[&b].trk.len(), 2);
|
||||
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 2)], 2));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let w = wpt_ids(&fx, a);
|
||||
copy(&mut fx, waypoints(a, &w), false);
|
||||
fx.selection = files(&[b]);
|
||||
delete_file(&mut fx, a);
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(wpt_ids(&fx, b).len(), 2);
|
||||
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t"), 1)], 0));
|
||||
copy(&mut fx, files(&[a]), false);
|
||||
fx.selection = Selection::Empty;
|
||||
delete_file(&mut fx, a);
|
||||
assert!(fx.files.is_empty());
|
||||
paste(&mut fx).unwrap();
|
||||
paste(&mut fx).unwrap();
|
||||
assert_eq!(fx.files.len(), 2);
|
||||
assert_eq!(names(&fx), ["a", "a"]);
|
||||
let _ = (s, w);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paste_a_cut_after_the_elements_were_deleted() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = add(&mut fx, file("a", &[(Some("t0"), 1), (Some("t1"), 1)], 0));
|
||||
let b = add(&mut fx, file("b", &[], 0));
|
||||
let t = track_ids(&fx, a);
|
||||
copy(&mut fx, tracks(a, &[t[0]]), true);
|
||||
// deleted before it is pasted
|
||||
std::rc::Rc::make_mut(fx.files.get_mut(&a).unwrap())
|
||||
.trk
|
||||
.retain(|trk| trk.id != t[0]);
|
||||
fx.selection = files(&[b]);
|
||||
|
||||
paste(&mut fx).unwrap();
|
||||
// nothing to remove, the track is where it was pasted, with its id
|
||||
assert_eq!(track_ids(&fx, a), vec![t[1]]);
|
||||
assert_eq!(track_ids(&fx, b), vec![t[0]]);
|
||||
assert_eq!(
|
||||
clipboard_state(&fx),
|
||||
Some((ClipboardIds::Tracks(vec![t[0]]), false))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
//! Copying or moving elements to a place of the file tree: what paste and move are made of.
|
||||
//! The elements come as the content of a clipboard, and a destination says where they go among
|
||||
//! the elements already there.
|
||||
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
ClipboardContent, ClipboardSegment, ClipboardTrack, CommandError, File, FileId, Selection,
|
||||
State, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint, WaypointId, copy_file,
|
||||
copy_segment, copy_track, copy_waypoint, edit_waypoint_chunks, insert_waypoints_at,
|
||||
};
|
||||
|
||||
/// Where elements are put, among the others of the same list.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Place<T> {
|
||||
/// After all of them.
|
||||
End,
|
||||
/// Right after the one with this id, or at the end if it is not there (anymore).
|
||||
After(T),
|
||||
/// At this position (at the end if it is past it).
|
||||
Index(usize),
|
||||
}
|
||||
|
||||
impl<T: Copy + PartialEq> Place<T> {
|
||||
/// The position in the list of `ids`.
|
||||
pub fn position(&self, ids: &[T]) -> usize {
|
||||
match self {
|
||||
Place::End => ids.len(),
|
||||
Place::After(id) => ids
|
||||
.iter()
|
||||
.position(|other| other == id)
|
||||
.map_or(ids.len(), |i| i + 1),
|
||||
Place::Index(index) => (*index).min(ids.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where elements go. What can go where:
|
||||
/// - files: among the files;
|
||||
/// - tracks: among the files (each track becomes a file, named like the track, or like the file
|
||||
/// it comes from), or among the tracks of a file;
|
||||
/// - segments: among the files (each segment becomes a file with a track, named like its track,
|
||||
/// or like its file), among the tracks of a file (each segment becomes a track), or among the
|
||||
/// segments of a track;
|
||||
/// - waypoints: among the waypoints of a file.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Destination {
|
||||
Files(Place<FileId>),
|
||||
Tracks(FileId, Place<TrackId>),
|
||||
Segments(FileId, TrackId, Place<TrackSegmentId>),
|
||||
Waypoints(FileId, Place<WaypointId>),
|
||||
}
|
||||
|
||||
pub fn waypoint_ids(file: &File) -> impl Iterator<Item = WaypointId> + '_ {
|
||||
file.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.map(|wpt| wpt.id)
|
||||
}
|
||||
|
||||
fn file_mut<'a>(state: &'a mut State, id: FileId) -> Result<&'a mut File, CommandError> {
|
||||
state
|
||||
.files
|
||||
.get_mut(&id)
|
||||
.map(Rc::make_mut)
|
||||
.ok_or(CommandError::NothingToDo)
|
||||
}
|
||||
|
||||
pub fn new_file(name: &str, trk: Vec<Track>) -> File {
|
||||
let mut file = File {
|
||||
trk,
|
||||
..Default::default()
|
||||
};
|
||||
file.info.name = name.to_owned();
|
||||
file
|
||||
}
|
||||
|
||||
/// Adds the files to the state, at `place` in the order of the files.
|
||||
fn add_files(state: &mut State, files: Vec<Rc<File>>, place: Place<FileId>) -> Selection {
|
||||
let at = place.position(&state.order.0);
|
||||
let ids: Vec<FileId> = files.iter().map(|file| file.id).collect();
|
||||
for file in files {
|
||||
state.files.insert(file.id, file);
|
||||
}
|
||||
state.order.0.splice(at..at, ids.iter().copied());
|
||||
Selection::File {
|
||||
file_ids: ids.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the elements of the content from the files, wherever they are.
|
||||
pub fn remove_elements(state: &mut State, content: &ClipboardContent) {
|
||||
match content {
|
||||
ClipboardContent::Files(files) => {
|
||||
let ids: HashSet<FileId> = files.iter().map(|file| file.id).collect();
|
||||
state.files.retain(|id, _| !ids.contains(id));
|
||||
state.order.0.retain(|id| !ids.contains(id));
|
||||
}
|
||||
ClipboardContent::Tracks(tracks) => {
|
||||
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.track.id).collect();
|
||||
for file in state.files.values_mut() {
|
||||
if file.trk.iter().any(|trk| ids.contains(&trk.id)) {
|
||||
Rc::make_mut(file).trk.retain(|trk| !ids.contains(&trk.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
ClipboardContent::Segments(segments) => {
|
||||
let ids: HashSet<TrackSegmentId> = segments.iter().map(|seg| seg.segment.id).collect();
|
||||
for file in state.files.values_mut() {
|
||||
let contains = |trk: &Track| trk.trkseg.iter().any(|seg| ids.contains(&seg.id));
|
||||
if file.trk.iter().any(contains) {
|
||||
for trk in Rc::make_mut(file).trk.iter_mut() {
|
||||
trk.trkseg.retain(|seg| !ids.contains(&seg.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ClipboardContent::Waypoints(waypoints) => {
|
||||
let ids: HashSet<WaypointId> = waypoints.iter().map(|wpt| wpt.id).collect();
|
||||
for file in state.files.values_mut() {
|
||||
if waypoint_ids(file).any(|id| ids.contains(&id)) {
|
||||
edit_waypoint_chunks(
|
||||
Rc::make_mut(file),
|
||||
|wpt| ids.contains(&wpt.id),
|
||||
|wpts| {
|
||||
wpts.retain(|wpt| !ids.contains(&wpt.id));
|
||||
true
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts the elements of the content at the destination: copies of them, or the elements
|
||||
/// themselves, with their ids, when `cut` (they are removed from where they are first). Returns
|
||||
/// the selection of what was put.
|
||||
pub fn transfer(
|
||||
state: &mut State,
|
||||
content: &ClipboardContent,
|
||||
cut: bool,
|
||||
to: Destination,
|
||||
) -> Result<Selection, CommandError> {
|
||||
// the destination has to be there, before anything is changed
|
||||
let exists = match to {
|
||||
Destination::Files(_) => true,
|
||||
Destination::Tracks(file_id, _) => state.files.contains_key(&file_id),
|
||||
Destination::Segments(file_id, trk_id, _) => state
|
||||
.files
|
||||
.get(&file_id)
|
||||
.is_some_and(|file| file.trk.iter().any(|trk| trk.id == trk_id)),
|
||||
Destination::Waypoints(file_id, _) => state.files.contains_key(&file_id),
|
||||
};
|
||||
let valid = match (content, to) {
|
||||
(ClipboardContent::Files(_), Destination::Files(_))
|
||||
| (ClipboardContent::Tracks(_), Destination::Files(_) | Destination::Tracks(..))
|
||||
| (
|
||||
ClipboardContent::Segments(_),
|
||||
Destination::Files(_) | Destination::Tracks(..) | Destination::Segments(..),
|
||||
)
|
||||
| (ClipboardContent::Waypoints(_), Destination::Waypoints(..)) => true,
|
||||
_ => false,
|
||||
};
|
||||
let empty = match content {
|
||||
ClipboardContent::Files(files) => files.is_empty(),
|
||||
ClipboardContent::Tracks(tracks) => tracks.is_empty(),
|
||||
ClipboardContent::Segments(segments) => segments.is_empty(),
|
||||
ClipboardContent::Waypoints(waypoints) => waypoints.is_empty(),
|
||||
};
|
||||
if !exists || !valid || empty {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
if cut {
|
||||
remove_elements(state, content);
|
||||
}
|
||||
let copy = !cut;
|
||||
match (content, to) {
|
||||
(ClipboardContent::Files(files), Destination::Files(place)) => {
|
||||
let files = files
|
||||
.iter()
|
||||
.map(|file| {
|
||||
if copy {
|
||||
Rc::new(copy_file(file))
|
||||
} else {
|
||||
file.clone()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
(ClipboardContent::Tracks(tracks), to) => transfer_tracks(state, tracks, copy, to),
|
||||
(ClipboardContent::Segments(segments), to) => transfer_segments(state, segments, copy, to),
|
||||
(ClipboardContent::Waypoints(waypoints), Destination::Waypoints(file_id, place)) => {
|
||||
transfer_waypoints(state, waypoints, copy, file_id, place)
|
||||
}
|
||||
_ => Err(CommandError::NothingToDo),
|
||||
}
|
||||
}
|
||||
|
||||
fn transfer_tracks(
|
||||
state: &mut State,
|
||||
tracks: &[ClipboardTrack],
|
||||
copy: bool,
|
||||
to: Destination,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let tracks: Vec<(Track, &str)> = tracks
|
||||
.iter()
|
||||
.map(|clip| {
|
||||
let track = if copy {
|
||||
copy_track(&clip.track)
|
||||
} else {
|
||||
clip.track.clone()
|
||||
};
|
||||
(track, clip.file_name.as_str())
|
||||
})
|
||||
.collect();
|
||||
match to {
|
||||
Destination::Files(place) => {
|
||||
let files = tracks
|
||||
.into_iter()
|
||||
.map(|(track, file_name)| {
|
||||
let name = track
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| file_name.to_owned());
|
||||
Rc::new(new_file(&name, vec![track]))
|
||||
})
|
||||
.collect();
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
Destination::Tracks(file_id, place) => {
|
||||
let tracks: Vec<Track> = tracks.into_iter().map(|(track, _)| track).collect();
|
||||
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.id).collect();
|
||||
let file = file_mut(state, file_id)?;
|
||||
let existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
|
||||
let at = place.position(&existing);
|
||||
file.trk.splice(at..at, tracks);
|
||||
Ok(Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids,
|
||||
})
|
||||
}
|
||||
_ => Err(CommandError::NothingToDo),
|
||||
}
|
||||
}
|
||||
|
||||
fn transfer_segments(
|
||||
state: &mut State,
|
||||
segments: &[ClipboardSegment],
|
||||
copy: bool,
|
||||
to: Destination,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let segments: Vec<(TrackSegment, &str)> = segments
|
||||
.iter()
|
||||
.map(|clip| {
|
||||
let segment = if copy {
|
||||
copy_segment(&clip.segment)
|
||||
} else {
|
||||
clip.segment.clone()
|
||||
};
|
||||
(segment, clip.name.as_str())
|
||||
})
|
||||
.collect();
|
||||
// a segment on its own needs a track
|
||||
let own_track = |segment: TrackSegment| Track {
|
||||
trkseg: vec![segment],
|
||||
..Default::default()
|
||||
};
|
||||
match to {
|
||||
Destination::Files(place) => {
|
||||
let files = segments
|
||||
.into_iter()
|
||||
.map(|(segment, name)| Rc::new(new_file(name, vec![own_track(segment)])))
|
||||
.collect();
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
Destination::Tracks(file_id, place) => {
|
||||
let tracks: Vec<Track> = segments
|
||||
.into_iter()
|
||||
.map(|(segment, _)| own_track(segment))
|
||||
.collect();
|
||||
let ids = tracks.iter().map(|trk| trk.id).collect();
|
||||
let file = file_mut(state, file_id)?;
|
||||
let existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
|
||||
let at = place.position(&existing);
|
||||
file.trk.splice(at..at, tracks);
|
||||
Ok(Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids,
|
||||
})
|
||||
}
|
||||
Destination::Segments(file_id, trk_id, place) => {
|
||||
let segments: Vec<TrackSegment> =
|
||||
segments.into_iter().map(|(segment, _)| segment).collect();
|
||||
let ids: HashSet<TrackSegmentId> = segments.iter().map(|seg| seg.id).collect();
|
||||
let trk = file_mut(state, file_id)?
|
||||
.trk
|
||||
.iter_mut()
|
||||
.find(|trk| trk.id == trk_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let existing: Vec<TrackSegmentId> = trk.trkseg.iter().map(|seg| seg.id).collect();
|
||||
let at = place.position(&existing);
|
||||
trk.trkseg.splice(at..at, segments);
|
||||
Ok(Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: ids,
|
||||
})
|
||||
}
|
||||
Destination::Waypoints(..) => Err(CommandError::NothingToDo),
|
||||
}
|
||||
}
|
||||
|
||||
fn transfer_waypoints(
|
||||
state: &mut State,
|
||||
waypoints: &[Waypoint],
|
||||
copy: bool,
|
||||
file_id: FileId,
|
||||
place: Place<WaypointId>,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let waypoints: Vec<Waypoint> = waypoints
|
||||
.iter()
|
||||
.map(|wpt| {
|
||||
if copy {
|
||||
copy_waypoint(wpt)
|
||||
} else {
|
||||
wpt.clone()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let ids = waypoints.iter().map(|wpt| wpt.id).collect();
|
||||
let file = file_mut(state, file_id)?;
|
||||
let existing: Vec<WaypointId> = waypoint_ids(file).collect();
|
||||
insert_waypoints_at(file, place.position(&existing), waypoints);
|
||||
Ok(Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: ids,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_place_position() {
|
||||
let ids = [1, 2, 3];
|
||||
assert_eq!(Place::End.position(&ids), 3);
|
||||
assert_eq!(Place::After(1).position(&ids), 1);
|
||||
assert_eq!(Place::After(3).position(&ids), 3);
|
||||
// not there: at the end
|
||||
assert_eq!(Place::After(9).position(&ids), 3);
|
||||
assert_eq!(Place::Index(0).position(&ids), 0);
|
||||
assert_eq!(Place::Index(2).position(&ids), 2);
|
||||
assert_eq!(Place::Index(10).position(&ids), 3);
|
||||
assert_eq!(Place::<i32>::End.position(&[]), 0);
|
||||
assert_eq!(Place::<i32>::Index(4).position(&[]), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Shared helpers for the tests of the commands.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{Clipboard, FileId, FileOrder, Selection, StackEntry, State, TrackpointCategories};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Fixture {
|
||||
pub files: StackEntry,
|
||||
pub selection: Selection,
|
||||
pub order: FileOrder,
|
||||
pub clipboard: Option<Clipboard>,
|
||||
pub categories: TrackpointCategories,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
pub fn state(&mut self) -> State<'_> {
|
||||
State {
|
||||
categories: &mut self.categories,
|
||||
files: &mut self.files,
|
||||
selection: &mut self.selection,
|
||||
order: &mut self.order,
|
||||
clipboard: &mut self.clipboard,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,35 @@
|
||||
use crate::{File, Track, TrackSegment, Waypoint};
|
||||
|
||||
// The trackpoints of a segment are shared chunks, so copying elements is cheap.
|
||||
|
||||
/// A copy of the segment, with a new id.
|
||||
pub fn copy_segment(segment: &TrackSegment) -> TrackSegment {
|
||||
let mut copy = segment.clone();
|
||||
copy.id = Default::default();
|
||||
copy
|
||||
}
|
||||
|
||||
/// A copy of the track and of its segments, with new ids.
|
||||
pub fn copy_track(track: &Track) -> Track {
|
||||
Track {
|
||||
id: Default::default(),
|
||||
trkseg: track.trkseg.iter().map(copy_segment).collect(),
|
||||
..track.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// A copy of the file and of its tracks and segments, with new ids.
|
||||
pub fn copy_file(file: &File) -> File {
|
||||
File {
|
||||
id: Default::default(),
|
||||
trk: file.trk.iter().map(copy_track).collect(),
|
||||
..file.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// A copy of the waypoint, with a new id.
|
||||
pub fn copy_waypoint(waypoint: &Waypoint) -> Waypoint {
|
||||
let mut copy = waypoint.clone();
|
||||
copy.id = Default::default();
|
||||
copy
|
||||
}
|
||||
@@ -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,218 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{File, Waypoint, WaypointChunk, WaypointId};
|
||||
|
||||
/// Inserts waypoints in the file, right after the waypoint `after`, or at the end if there is
|
||||
/// none (or if it is not in the file).
|
||||
///
|
||||
/// See [`insert_waypoints_at`].
|
||||
pub fn insert_waypoints(file: &mut File, after: Option<WaypointId>, waypoints: Vec<Waypoint>) {
|
||||
let index = after
|
||||
.and_then(|after| {
|
||||
file.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.position(|wpt| wpt.id == after)
|
||||
})
|
||||
.map_or(usize::MAX, |i| i + 1);
|
||||
insert_waypoints_at(file, index, waypoints);
|
||||
}
|
||||
|
||||
/// Inserts waypoints in the file, so that the first one is at `index` among the waypoints of
|
||||
/// the file (at the end if the index is past it).
|
||||
///
|
||||
/// Only the chunk holding the waypoint that was at `index` is cut (when the waypoints are not
|
||||
/// inserted between two chunks), all the other chunks are kept as they are.
|
||||
pub fn insert_waypoints_at(file: &mut File, index: usize, waypoints: Vec<Waypoint>) {
|
||||
if waypoints.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut inserted = Vec::new();
|
||||
let mut chunk = WaypointChunk::default();
|
||||
for wpt in waypoints {
|
||||
chunk.wpt.push(wpt);
|
||||
if chunk.is_full() {
|
||||
inserted.push(Rc::new(std::mem::take(&mut chunk)));
|
||||
}
|
||||
}
|
||||
if !chunk.wpt.is_empty() {
|
||||
inserted.push(Rc::new(chunk));
|
||||
}
|
||||
|
||||
// the chunk holding the waypoint at `index`, and the position of that waypoint in it
|
||||
let mut start = 0;
|
||||
let position = file.wpt.iter().enumerate().find_map(|(i, chunk)| {
|
||||
let offset = index
|
||||
.checked_sub(start)
|
||||
.filter(|offset| *offset < chunk.wpt.len());
|
||||
start += chunk.wpt.len();
|
||||
offset.map(|offset| (i, offset))
|
||||
});
|
||||
match position {
|
||||
None => file.wpt.extend(inserted),
|
||||
Some((i, 0)) => {
|
||||
file.wpt.splice(i..i, inserted);
|
||||
}
|
||||
Some((i, offset)) => {
|
||||
let cut = &file.wpt[i];
|
||||
let part = |wpt: &[Waypoint]| {
|
||||
Rc::new(WaypointChunk {
|
||||
wpt: wpt.to_vec(),
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
let mut chunks = vec![part(&cut.wpt[..offset])];
|
||||
chunks.extend(inserted);
|
||||
chunks.push(part(&cut.wpt[offset..]));
|
||||
file.wpt.splice(i..=i, chunks);
|
||||
}
|
||||
}
|
||||
file.wpt_rev_id = Default::default();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn waypoint() -> Waypoint {
|
||||
Waypoint::default()
|
||||
}
|
||||
|
||||
fn file_with(chunks: &[usize]) -> (File, Vec<WaypointId>) {
|
||||
let mut file = File::default();
|
||||
let mut ids = vec![];
|
||||
for n in chunks {
|
||||
let wpt: Vec<_> = (0..*n).map(|_| waypoint()).collect();
|
||||
ids.extend(wpt.iter().map(|w| w.id));
|
||||
file.wpt.push(Rc::new(WaypointChunk {
|
||||
wpt,
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
(file, ids)
|
||||
}
|
||||
|
||||
fn ids(file: &File) -> Vec<WaypointId> {
|
||||
file.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| chunk.wpt.iter().map(|w| w.id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_at_the_end() {
|
||||
let (mut file, before) = file_with(&[2, 1]);
|
||||
let rev = file.wpt_rev_id;
|
||||
let new = vec![waypoint(), waypoint()];
|
||||
let new_ids: Vec<_> = new.iter().map(|w| w.id).collect();
|
||||
insert_waypoints(&mut file, None, new);
|
||||
assert_eq!(ids(&file), [before.clone(), new_ids].concat());
|
||||
// the chunks that were there are kept
|
||||
assert_eq!(file.wpt.len(), 3);
|
||||
assert_ne!(file.wpt_rev_id, rev);
|
||||
// an unknown waypoint is an insertion at the end too
|
||||
let (mut file, before) = file_with(&[2]);
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
insert_waypoints(&mut file, Some(WaypointId::default()), vec![one]);
|
||||
assert_eq!(ids(&file), [before, vec![one_id]].concat());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_after_the_last_waypoint_of_a_chunk_keeps_the_chunks() {
|
||||
let (mut file, before) = file_with(&[2, 2]);
|
||||
let first = file.wpt[0].clone();
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
insert_waypoints(&mut file, Some(before[1]), vec![one]);
|
||||
assert_eq!(ids(&file), [&before[..2], &[one_id], &before[2..]].concat());
|
||||
// 2 chunks + the inserted one, nothing was cut
|
||||
assert_eq!(file.wpt.len(), 3);
|
||||
assert!(Rc::ptr_eq(&file.wpt[0], &first));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_in_the_middle_of_a_chunk_cuts_it() {
|
||||
let (mut file, before) = file_with(&[3, 1]);
|
||||
let last = file.wpt[1].clone();
|
||||
let new = vec![waypoint(), waypoint()];
|
||||
let new_ids: Vec<_> = new.iter().map(|w| w.id).collect();
|
||||
insert_waypoints(&mut file, Some(before[0]), new);
|
||||
assert_eq!(
|
||||
ids(&file),
|
||||
[&before[..1], &new_ids[..], &before[1..]].concat()
|
||||
);
|
||||
// the cut chunk, the inserted one and the end of the cut one, then the untouched chunk
|
||||
assert_eq!(file.wpt.len(), 4);
|
||||
assert!(Rc::ptr_eq(&file.wpt[3], &last));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_nothing() {
|
||||
let (mut file, before) = file_with(&[2]);
|
||||
let rev = file.wpt_rev_id;
|
||||
insert_waypoints(&mut file, Some(before[0]), vec![]);
|
||||
assert_eq!(ids(&file), before);
|
||||
assert_eq!(file.wpt_rev_id, rev);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_many_fills_chunks() {
|
||||
let mut file = File::default();
|
||||
let new: Vec<_> = (0..300).map(|_| waypoint()).collect();
|
||||
let new_ids: Vec<_> = new.iter().map(|w| w.id).collect();
|
||||
insert_waypoints(&mut file, None, new);
|
||||
assert_eq!(ids(&file), new_ids);
|
||||
assert_eq!(file.wpt.len(), 3);
|
||||
assert!(file.wpt[..2].iter().all(|chunk| chunk.is_full()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_at_an_index() {
|
||||
// at the start
|
||||
let (mut file, before) = file_with(&[2, 1]);
|
||||
let first = file.wpt[0].clone();
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
insert_waypoints_at(&mut file, 0, vec![one]);
|
||||
assert_eq!(ids(&file), [vec![one_id], before.clone()].concat());
|
||||
// the existing chunks are kept
|
||||
assert_eq!(file.wpt.len(), 3);
|
||||
assert!(Rc::ptr_eq(&file.wpt[1], &first));
|
||||
|
||||
// between two chunks
|
||||
let (mut file, before) = file_with(&[2, 1]);
|
||||
let (first, last) = (file.wpt[0].clone(), file.wpt[1].clone());
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
insert_waypoints_at(&mut file, 2, vec![one]);
|
||||
assert_eq!(ids(&file), [&before[..2], &[one_id], &before[2..]].concat());
|
||||
assert_eq!(file.wpt.len(), 3);
|
||||
assert!(Rc::ptr_eq(&file.wpt[0], &first) && Rc::ptr_eq(&file.wpt[2], &last));
|
||||
|
||||
// inside a chunk
|
||||
let (mut file, before) = file_with(&[3]);
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
insert_waypoints_at(&mut file, 1, vec![one]);
|
||||
assert_eq!(ids(&file), [&before[..1], &[one_id], &before[1..]].concat());
|
||||
assert_eq!(file.wpt.len(), 3);
|
||||
|
||||
// at the end, or past it
|
||||
for index in [3, 100] {
|
||||
let (mut file, before) = file_with(&[3]);
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
insert_waypoints_at(&mut file, index, vec![one]);
|
||||
assert_eq!(ids(&file), [before, vec![one_id]].concat());
|
||||
}
|
||||
|
||||
// in a file without waypoints
|
||||
let mut file = File::default();
|
||||
let one = waypoint();
|
||||
let one_id = one.id;
|
||||
insert_waypoints_at(&mut file, 0, vec![one]);
|
||||
assert_eq!(ids(&file), vec![one_id]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
mod copy;
|
||||
mod edit_waypoint_chunks;
|
||||
mod insert_waypoints;
|
||||
mod produce;
|
||||
mod update_selected;
|
||||
mod update_waypoint;
|
||||
|
||||
pub use copy::*;
|
||||
pub use edit_waypoint_chunks::*;
|
||||
pub use insert_waypoints::*;
|
||||
pub use produce::*;
|
||||
pub use update_selected::*;
|
||||
pub use update_waypoint::*;
|
||||
@@ -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, &mut Default::default()).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,30 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{CommandError, FileId, State, Waypoint, WaypointId, edit_waypoint_chunks};
|
||||
|
||||
/// Changes one waypoint of a file with `f`, whatever is selected. Nothing to do if the file or
|
||||
/// the waypoint does not exist.
|
||||
pub fn update_waypoint(
|
||||
state: &mut State,
|
||||
file_id: FileId,
|
||||
waypoint_id: WaypointId,
|
||||
f: impl Fn(&mut Waypoint),
|
||||
) -> Result<(), CommandError> {
|
||||
let file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let mut file = (**file).clone();
|
||||
let changed = edit_waypoint_chunks(
|
||||
&mut file,
|
||||
|wpt| wpt.id == waypoint_id,
|
||||
|wpts| {
|
||||
wpts.iter_mut()
|
||||
.filter(|wpt| wpt.id == waypoint_id)
|
||||
.for_each(&f);
|
||||
true
|
||||
},
|
||||
);
|
||||
if !changed {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, Edit, Editor, File, FileId, LngLat, LngLatBounds, Selection, State,
|
||||
TrackSegment, TrackSegmentId, Trackpoint, Waypoint, delete_waypoints, edit_waypoint_chunks,
|
||||
update_selected,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CleanType {
|
||||
Inside,
|
||||
Outside,
|
||||
}
|
||||
|
||||
/// Removes the trackpoints and/or waypoints of the selection lying inside (or outside) a
|
||||
/// rectangle. A selected file cleans its tracks and its waypoints, a selected track or segment
|
||||
/// only trackpoints, selected waypoints only waypoints.
|
||||
#[derive(Debug)]
|
||||
pub struct Clean {
|
||||
pub bounds: LngLatBounds,
|
||||
pub type_: CleanType,
|
||||
pub trkpt: bool,
|
||||
pub wpt: bool,
|
||||
}
|
||||
|
||||
impl Clean {
|
||||
fn contains(&self, c: LngLat) -> bool {
|
||||
(self.bounds.sw.lng..=self.bounds.ne.lng).contains(&c.lng)
|
||||
&& (self.bounds.sw.lat..=self.bounds.ne.lat).contains(&c.lat)
|
||||
}
|
||||
|
||||
fn removes(&self, c: LngLat) -> bool {
|
||||
self.contains(c) == (self.type_ == CleanType::Inside)
|
||||
}
|
||||
}
|
||||
|
||||
impl Apply for Clean {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
match &*state.selection {
|
||||
Selection::Waypoint { file_id, .. } | Selection::Waypoints { file_id } => {
|
||||
let file_id = *file_id;
|
||||
if !self.wpt {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
let wpt_ids = match &*state.selection {
|
||||
Selection::Waypoint { wpt_ids, .. } => Some(wpt_ids.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let selected =
|
||||
|wpt: &Waypoint| wpt_ids.as_ref().is_none_or(|ids| ids.contains(&wpt.id));
|
||||
*state.selection = delete_waypoints(state.files, file_id, |wpt| {
|
||||
selected(wpt) && self.removes(wpt.coordinates)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
Selection::Empty => Err(CommandError::NothingToDo),
|
||||
_ => {
|
||||
let mut cleaner = Cleaner {
|
||||
clean: &self,
|
||||
changed: false,
|
||||
emptied: HashSet::new(),
|
||||
wpt_files: HashSet::new(),
|
||||
};
|
||||
update_selected(state, &mut cleaner);
|
||||
if cleaner.changed {
|
||||
prune(state, &cleaner.emptied, &cleaner.wpt_files);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CommandError::NothingToDo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Cleaner<'a> {
|
||||
clean: &'a Clean,
|
||||
changed: bool,
|
||||
/// Segments left without trackpoints by the clean.
|
||||
emptied: HashSet<TrackSegmentId>,
|
||||
/// Files that lost waypoints.
|
||||
wpt_files: HashSet<FileId>,
|
||||
}
|
||||
|
||||
impl Editor for Cleaner<'_> {
|
||||
fn file(&mut self, file: &mut File) -> Edit {
|
||||
let mut edit = Edit::Unchanged;
|
||||
for trk in &mut file.trk {
|
||||
if self.track(trk) == Edit::Changed {
|
||||
edit = Edit::Changed;
|
||||
}
|
||||
}
|
||||
if self.clean.wpt
|
||||
&& edit_waypoint_chunks(
|
||||
file,
|
||||
|wpt| self.clean.removes(wpt.coordinates),
|
||||
|wpts| {
|
||||
wpts.retain(|wpt| !self.clean.removes(wpt.coordinates));
|
||||
true
|
||||
},
|
||||
)
|
||||
{
|
||||
edit = Edit::Changed;
|
||||
self.wpt_files.insert(file.id);
|
||||
}
|
||||
self.changed |= edit == Edit::Changed;
|
||||
edit
|
||||
}
|
||||
|
||||
fn segment(&mut self, segment: &mut TrackSegment) -> Edit {
|
||||
if !self.clean.trkpt || !segment.iter().any(|pt| self.clean.removes(pt.coordinates)) {
|
||||
return Edit::Unchanged;
|
||||
}
|
||||
let kept: Vec<Trackpoint> = segment
|
||||
.iter()
|
||||
.filter(|pt| !self.clean.removes(pt.coordinates))
|
||||
.cloned()
|
||||
.collect();
|
||||
if kept.is_empty() {
|
||||
self.emptied.insert(segment.id);
|
||||
}
|
||||
segment.splice(0, segment.len(), kept);
|
||||
self.changed = true;
|
||||
Edit::Changed
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes what the clean left empty: the emptied segments, the tracks that lost all their
|
||||
/// segments and the files that lost something and have nothing left. The selection falls back
|
||||
/// to the closest element still existing.
|
||||
fn prune(state: &mut State, emptied: &HashSet<TrackSegmentId>, wpt_files: &HashSet<FileId>) {
|
||||
let ids: Vec<FileId> = state
|
||||
.files
|
||||
.iter()
|
||||
.filter(|(id, file)| {
|
||||
wpt_files.contains(id)
|
||||
|| file
|
||||
.trk
|
||||
.iter()
|
||||
.any(|trk| trk.trkseg.iter().any(|seg| emptied.contains(&seg.id)))
|
||||
})
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
for id in ids {
|
||||
let mut file = (*state.files[&id]).clone();
|
||||
let lost_something = !emptied.is_empty() || wpt_files.contains(&id);
|
||||
file.trk.retain_mut(|trk| {
|
||||
let len = trk.trkseg.len();
|
||||
trk.trkseg.retain(|seg| !emptied.contains(&seg.id));
|
||||
trk.trkseg.len() == len || !trk.trkseg.is_empty()
|
||||
});
|
||||
if lost_something && file.trk.is_empty() && file.wpt.is_empty() {
|
||||
state.files.remove(&id);
|
||||
state.order.0.retain(|f| *f != id);
|
||||
} else {
|
||||
state.files.insert(id, Rc::new(file));
|
||||
}
|
||||
}
|
||||
*state.selection = fixed_selection(state);
|
||||
}
|
||||
|
||||
fn fixed_selection(state: &State) -> Selection {
|
||||
let files = &*state.files;
|
||||
match &*state.selection {
|
||||
Selection::File { file_ids } => {
|
||||
let file_ids: HashSet<_> = file_ids
|
||||
.iter()
|
||||
.filter(|id| files.contains_key(id))
|
||||
.copied()
|
||||
.collect();
|
||||
if file_ids.is_empty() {
|
||||
Selection::Empty
|
||||
} else {
|
||||
Selection::File { file_ids }
|
||||
}
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let Some(file) = files.get(file_id) else {
|
||||
return Selection::Empty;
|
||||
};
|
||||
let trk_ids: HashSet<_> = file
|
||||
.trk
|
||||
.iter()
|
||||
.map(|trk| trk.id)
|
||||
.filter(|id| trk_ids.contains(id))
|
||||
.collect();
|
||||
if trk_ids.is_empty() {
|
||||
Selection::File {
|
||||
file_ids: [*file_id].into(),
|
||||
}
|
||||
} else {
|
||||
Selection::Track {
|
||||
file_id: *file_id,
|
||||
trk_ids,
|
||||
}
|
||||
}
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => {
|
||||
let Some(file) = files.get(file_id) else {
|
||||
return Selection::Empty;
|
||||
};
|
||||
let Some(trk) = file.trk.iter().find(|trk| trk.id == *trk_id) else {
|
||||
return Selection::File {
|
||||
file_ids: [*file_id].into(),
|
||||
};
|
||||
};
|
||||
let trkseg_ids: HashSet<_> = trk
|
||||
.trkseg
|
||||
.iter()
|
||||
.map(|seg| seg.id)
|
||||
.filter(|id| trkseg_ids.contains(id))
|
||||
.collect();
|
||||
if trkseg_ids.is_empty() {
|
||||
Selection::Track {
|
||||
file_id: *file_id,
|
||||
trk_ids: [*trk_id].into(),
|
||||
}
|
||||
} else {
|
||||
Selection::TrackSegment {
|
||||
file_id: *file_id,
|
||||
trk_id: *trk_id,
|
||||
trkseg_ids,
|
||||
}
|
||||
}
|
||||
}
|
||||
Selection::Waypoints { file_id } | Selection::Waypoint { file_id, .. } => {
|
||||
if files.contains_key(file_id) {
|
||||
Selection::File {
|
||||
file_ids: [*file_id].into(),
|
||||
}
|
||||
} else {
|
||||
Selection::Empty
|
||||
}
|
||||
}
|
||||
Selection::Empty => Selection::Empty,
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let id = fx.order.0[0];
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([id]),
|
||||
};
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
fn bounds(w: f64, s: f64, e: f64, n: f64) -> LngLatBounds {
|
||||
LngLatBounds {
|
||||
sw: LngLat { lng: w, lat: s },
|
||||
ne: LngLat { lng: e, lat: n },
|
||||
}
|
||||
}
|
||||
|
||||
fn clean(bounds: LngLatBounds, type_: CleanType) -> Clean {
|
||||
Clean {
|
||||
bounds,
|
||||
type_,
|
||||
trkpt: true,
|
||||
wpt: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn count(fx: &Fixture, id: FileId) -> usize {
|
||||
fx.files
|
||||
.get(&id)
|
||||
.into_iter()
|
||||
.flat_map(|file| &file.trk)
|
||||
.flat_map(|trk| &trk.trkseg)
|
||||
.map(|seg| seg.len())
|
||||
.sum()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inside_everything_removes_all_trackpoints() {
|
||||
let (mut fx, id) = loaded();
|
||||
assert!(count(&fx, id) > 0);
|
||||
clean(bounds(-180.0, -90.0, 180.0, 90.0), CleanType::Inside)
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(count(&fx, id), 0);
|
||||
// nothing is left of the file
|
||||
assert!(!fx.files.contains_key(&id));
|
||||
assert!(fx.order.0.is_empty());
|
||||
assert_eq!(fx.selection, Selection::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_emptied_segments_and_tracks_are_removed() {
|
||||
let (mut fx, id) = loaded();
|
||||
let before = fx.files[&id].clone();
|
||||
let seg = &before.trk[0].trkseg[0];
|
||||
let pts: Vec<_> = seg.iter().map(|p| p.coordinates).collect();
|
||||
let mut b = LngLatBounds::default();
|
||||
pts.iter().for_each(|c| b.extend(*c));
|
||||
clean(b, CleanType::Inside).apply(&mut fx.state()).unwrap();
|
||||
let after = &fx.files[&id];
|
||||
assert!(after.trk.iter().all(|t| !t.trkseg.is_empty()));
|
||||
assert!(
|
||||
after
|
||||
.trk
|
||||
.iter()
|
||||
.flat_map(|t| &t.trkseg)
|
||||
.all(|s| !s.is_empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_outside_everything_changes_nothing() {
|
||||
let (mut fx, id) = loaded();
|
||||
let before = fx.files[&id].clone();
|
||||
let result =
|
||||
clean(bounds(-180.0, -90.0, 180.0, 90.0), CleanType::Outside).apply(&mut fx.state());
|
||||
assert!(matches!(result, Err(CommandError::NothingToDo)));
|
||||
assert_eq!(fx.files[&id], before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partial_clean_only_removes_points_inside_and_bumps_revision() {
|
||||
let (mut fx, id) = loaded();
|
||||
let before = fx.files[&id].clone();
|
||||
let first = before.trk[0].trkseg[0].iter().next().unwrap().coordinates;
|
||||
let total = count(&fx, id);
|
||||
// tiny rectangle around the first point
|
||||
let b = bounds(
|
||||
first.lng - 1e-9,
|
||||
first.lat - 1e-9,
|
||||
first.lng + 1e-9,
|
||||
first.lat + 1e-9,
|
||||
);
|
||||
clean(b, CleanType::Inside).apply(&mut fx.state()).unwrap();
|
||||
let removed = total - count(&fx, id);
|
||||
assert!(removed >= 1);
|
||||
let seg = &fx.files[&id].trk[0].trkseg[0];
|
||||
assert_ne!(seg.rev_id, before.trk[0].trkseg[0].rev_id);
|
||||
assert!(seg.iter().all(|pt| !b_contains(&b, pt.coordinates)));
|
||||
}
|
||||
|
||||
fn b_contains(b: &LngLatBounds, c: LngLat) -> bool {
|
||||
c.lng >= b.sw.lng && c.lng <= b.ne.lng && c.lat >= b.sw.lat && c.lat <= b.ne.lat
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_waypoints_flag_and_selection() {
|
||||
let (mut fx, id) = loaded();
|
||||
let mut file = (*fx.files[&id]).clone();
|
||||
file.wpt = vec![std::rc::Rc::new(crate::WaypointChunk {
|
||||
wpt: vec![Waypoint::default(), Waypoint::default()],
|
||||
..Default::default()
|
||||
})];
|
||||
let ids: Vec<_> = file.wpt[0].wpt.iter().map(|w| w.id).collect();
|
||||
fx.files.insert(id, std::rc::Rc::new(file));
|
||||
let everywhere = bounds(-180.0, -90.0, 180.0, 90.0);
|
||||
let wpt_count = |fx: &Fixture| {
|
||||
fx.files
|
||||
.get(&id)
|
||||
.map_or(0, |f| f.wpt.iter().map(|c| c.wpt.len()).sum::<usize>())
|
||||
};
|
||||
|
||||
// trackpoints only: waypoints are kept
|
||||
Clean {
|
||||
wpt: false,
|
||||
..clean(everywhere, CleanType::Inside)
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(wpt_count(&fx), 2);
|
||||
|
||||
// one selected waypoint
|
||||
fx.selection = Selection::Waypoint {
|
||||
file_id: id,
|
||||
wpt_ids: HashSet::from([ids[0]]),
|
||||
};
|
||||
clean(everywhere, CleanType::Inside)
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(wpt_count(&fx), 1);
|
||||
|
||||
// whole file
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([id]),
|
||||
};
|
||||
clean(everywhere, CleanType::Inside)
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(wpt_count(&fx), 0);
|
||||
}
|
||||
}
|
||||
@@ -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,106 @@
|
||||
use crate::{Apply, CommandError, FileId, State, WaypointId, set_waypoint_fields, update_waypoint};
|
||||
|
||||
/// Changes a waypoint of a file, whatever is selected (what the form of the waypoint tool does):
|
||||
/// its name, description, icon, link, position and elevation. The strings that are empty remove
|
||||
/// the field.
|
||||
#[derive(Debug)]
|
||||
pub struct EditWaypoint<'a> {
|
||||
pub file_id: FileId,
|
||||
pub waypoint_id: WaypointId,
|
||||
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 EditWaypoint<'_> {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
update_waypoint(state, self.file_id, self.waypoint_id, |wpt| {
|
||||
set_waypoint_fields(
|
||||
wpt,
|
||||
(self.lng, self.lat, self.ele),
|
||||
self.name,
|
||||
self.desc,
|
||||
self.icon,
|
||||
self.link,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
File, Link, Waypoint, WaypointChunk, engine::command::fixture::Fixture, waypoint_ids,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_edit_the_given_waypoint() {
|
||||
let mut fx = Fixture::default();
|
||||
let mut file = File::default();
|
||||
file.wpt.push(Rc::new(WaypointChunk {
|
||||
wpt: vec![
|
||||
Waypoint {
|
||||
name: Some("old".into()),
|
||||
desc: Some("old description".into()),
|
||||
cmt: Some("old comment".into()),
|
||||
sym: Some("Flag".into()),
|
||||
link: Some(Link {
|
||||
href: "https://old".into(),
|
||||
text: None,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
Waypoint {
|
||||
name: Some("other".into()),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}));
|
||||
let id = file.id;
|
||||
let ids: Vec<_> = waypoint_ids(&file).collect();
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
|
||||
EditWaypoint {
|
||||
file_id: id,
|
||||
waypoint_id: ids[0],
|
||||
lng: 4.5,
|
||||
lat: 50.5,
|
||||
ele: 12.0,
|
||||
name: "new",
|
||||
desc: "new description",
|
||||
icon: "Summit",
|
||||
link: "",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let wpts: Vec<_> = fx.files[&id]
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|c| c.wpt.iter())
|
||||
.collect();
|
||||
let edited = wpts[0];
|
||||
assert_eq!(edited.id, ids[0]);
|
||||
assert_eq!(edited.name.as_deref(), Some("new"));
|
||||
assert_eq!(edited.desc.as_deref(), Some("new description"));
|
||||
assert_eq!(edited.cmt.as_deref(), Some("new description"));
|
||||
assert_eq!(edited.sym.as_deref(), Some("Summit"));
|
||||
// an empty link removes it
|
||||
assert!(edited.link.is_none());
|
||||
assert_eq!(
|
||||
(edited.coordinates.lng, edited.coordinates.lat, edited.ele),
|
||||
(4.5, 50.5, 12.0)
|
||||
);
|
||||
// the other waypoint is as it was
|
||||
assert_eq!(wpts[1].name.as_deref(), Some("other"));
|
||||
}
|
||||
}
|
||||
@@ -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,25 @@
|
||||
mod clean;
|
||||
mod crop;
|
||||
mod edit_waypoint;
|
||||
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 edit_waypoint::*;
|
||||
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,111 @@
|
||||
use crate::{Apply, CommandError, FileId, LngLat, State, WaypointId, update_waypoint};
|
||||
|
||||
/// Moves a waypoint of a file, whatever is selected (what a drag does): only its position and
|
||||
/// elevation change.
|
||||
#[derive(Debug)]
|
||||
pub struct MoveWaypoint {
|
||||
pub file_id: FileId,
|
||||
pub waypoint_id: WaypointId,
|
||||
pub lng: f64,
|
||||
pub lat: f64,
|
||||
pub ele: f64,
|
||||
}
|
||||
|
||||
impl Apply for MoveWaypoint {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
update_waypoint(state, self.file_id, self.waypoint_id, |wpt| {
|
||||
wpt.coordinates = LngLat {
|
||||
lng: self.lng,
|
||||
lat: self.lat,
|
||||
};
|
||||
wpt.ele = self.ele;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
File, Selection, Waypoint, WaypointChunk, engine::command::fixture::Fixture, waypoint_ids,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn fixture() -> (Fixture, crate::FileId, Vec<crate::WaypointId>) {
|
||||
let mut fx = Fixture::default();
|
||||
let mut file = File::default();
|
||||
file.wpt.push(Rc::new(WaypointChunk {
|
||||
wpt: (0..3)
|
||||
.map(|i| Waypoint {
|
||||
name: Some(format!("w{i}")),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}));
|
||||
let id = file.id;
|
||||
let ids = waypoint_ids(&file).collect();
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.order.0.push(id);
|
||||
(fx, id, ids)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_only_the_given_waypoint_moves_whatever_is_selected() {
|
||||
let (mut fx, id, ids) = fixture();
|
||||
let rev = fx.files[&id].wpt_rev_id;
|
||||
|
||||
MoveWaypoint {
|
||||
file_id: id,
|
||||
waypoint_id: ids[1],
|
||||
lng: 4.0,
|
||||
lat: 50.0,
|
||||
ele: 42.0,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let wpts: Vec<_> = fx.files[&id]
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|c| c.wpt.iter())
|
||||
.collect();
|
||||
assert_eq!(wpts.len(), 3);
|
||||
assert_eq!(
|
||||
(
|
||||
wpts[1].coordinates.lng,
|
||||
wpts[1].coordinates.lat,
|
||||
wpts[1].ele
|
||||
),
|
||||
(4.0, 50.0, 42.0)
|
||||
);
|
||||
// the other fields and the other waypoints are as they were
|
||||
assert_eq!(wpts[1].name.as_deref(), Some("w1"));
|
||||
assert_eq!(wpts[1].id, ids[1]);
|
||||
assert_eq!((wpts[0].coordinates.lng, wpts[0].ele), (0.0, 0.0));
|
||||
assert_eq!((wpts[2].coordinates.lat, wpts[2].ele), (0.0, 0.0));
|
||||
// the coordinates changed, which is noticed by what is derived from them
|
||||
assert_ne!(fx.files[&id].wpt_rev_id, rev);
|
||||
assert_eq!(fx.selection, Selection::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_waypoint_or_file_is_nothing_to_do() {
|
||||
let (mut fx, id, ids) = fixture();
|
||||
let before = fx.files[&id].clone();
|
||||
for (file_id, waypoint_id) in [(id, WaypointId::default()), (FileId::default(), ids[0])] {
|
||||
let result = MoveWaypoint {
|
||||
file_id,
|
||||
waypoint_id,
|
||||
lng: 1.0,
|
||||
lat: 1.0,
|
||||
ele: 1.0,
|
||||
}
|
||||
.apply(&mut fx.state());
|
||||
assert!(matches!(result, Err(CommandError::NothingToDo)));
|
||||
}
|
||||
assert!(Rc::ptr_eq(&fx.files[&id], &before));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, File, FileId, Link, LngLat, Selection, State, Waypoint,
|
||||
insert_waypoints_at,
|
||||
};
|
||||
|
||||
/// Adds a waypoint at the end of the waypoints of each selected file (or of the file of the
|
||||
/// selected elements). The strings are empty when the waypoint does not have the field.
|
||||
#[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,
|
||||
}
|
||||
|
||||
fn non_empty(text: &str) -> Option<String> {
|
||||
(!text.is_empty()).then(|| text.to_owned())
|
||||
}
|
||||
|
||||
/// Sets the fields of a waypoint: the strings that are empty remove the field.
|
||||
pub fn set_waypoint_fields(
|
||||
waypoint: &mut Waypoint,
|
||||
position: (f64, f64, f64),
|
||||
name: &str,
|
||||
desc: &str,
|
||||
icon: &str,
|
||||
link: &str,
|
||||
) {
|
||||
let (lng, lat, ele) = position;
|
||||
waypoint.coordinates = LngLat { lng, lat };
|
||||
waypoint.ele = ele;
|
||||
waypoint.name = non_empty(name);
|
||||
waypoint.desc = non_empty(desc);
|
||||
// the description is also the comment, as it is what the other applications show
|
||||
waypoint.cmt = non_empty(desc);
|
||||
waypoint.sym = non_empty(icon);
|
||||
waypoint.link = non_empty(link).map(|href| Link { href, text: None });
|
||||
}
|
||||
|
||||
impl Apply for NewWaypoint<'_> {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let file_ids: Vec<FileId> = match &*state.selection {
|
||||
Selection::Empty => vec![],
|
||||
Selection::File { file_ids } => state
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.filter(|id| file_ids.contains(id))
|
||||
.copied()
|
||||
.collect(),
|
||||
Selection::Track { file_id, .. }
|
||||
| Selection::TrackSegment { file_id, .. }
|
||||
| Selection::Waypoints { file_id }
|
||||
| Selection::Waypoint { file_id, .. } => vec![*file_id],
|
||||
};
|
||||
let file_ids: Vec<FileId> = file_ids
|
||||
.into_iter()
|
||||
.filter(|id| state.files.contains_key(id))
|
||||
.collect();
|
||||
if file_ids.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
|
||||
for id in file_ids {
|
||||
let mut waypoint = Waypoint::default();
|
||||
set_waypoint_fields(
|
||||
&mut waypoint,
|
||||
(self.lng, self.lat, self.ele),
|
||||
self.name,
|
||||
self.desc,
|
||||
self.icon,
|
||||
self.link,
|
||||
);
|
||||
let file: &mut File = Rc::make_mut(state.files.get_mut(&id).unwrap());
|
||||
insert_waypoints_at(file, usize::MAX, vec![waypoint]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{New, TrackId, TrackSegmentId, engine::command::fixture::Fixture, waypoint_ids};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn new_waypoint(name: &str) -> NewWaypoint<'_> {
|
||||
NewWaypoint {
|
||||
lng: 4.5,
|
||||
lat: 50.25,
|
||||
ele: 123.0,
|
||||
name,
|
||||
desc: "a description",
|
||||
icon: "Summit",
|
||||
link: "https://example.com",
|
||||
}
|
||||
}
|
||||
|
||||
fn waypoints(fx: &Fixture, id: FileId) -> Vec<Waypoint> {
|
||||
fx.files[&id]
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| chunk.wpt.iter().cloned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_waypoint_in_the_selected_file() {
|
||||
let mut fx = Fixture::default();
|
||||
New { name: "a" }.apply(&mut fx.state()).unwrap();
|
||||
New { name: "b" }.apply(&mut fx.state()).unwrap();
|
||||
let (a, b) = (fx.order.0[0], fx.order.0[1]);
|
||||
let rev = fx.files[&b].wpt_rev_id;
|
||||
|
||||
// b is selected
|
||||
new_waypoint("peak").apply(&mut fx.state()).unwrap();
|
||||
assert!(waypoints(&fx, a).is_empty());
|
||||
let created = waypoints(&fx, b);
|
||||
assert_eq!(created.len(), 1);
|
||||
let wpt = &created[0];
|
||||
assert_eq!(
|
||||
(wpt.coordinates.lng, wpt.coordinates.lat, wpt.ele),
|
||||
(4.5, 50.25, 123.0)
|
||||
);
|
||||
assert_eq!(wpt.name.as_deref(), Some("peak"));
|
||||
assert_eq!(wpt.desc.as_deref(), Some("a description"));
|
||||
assert_eq!(wpt.cmt.as_deref(), Some("a description"));
|
||||
assert_eq!(wpt.sym.as_deref(), Some("Summit"));
|
||||
assert_eq!(
|
||||
wpt.link.as_ref().map(|l| l.href.as_str()),
|
||||
Some("https://example.com")
|
||||
);
|
||||
assert_ne!(fx.files[&b].wpt_rev_id, rev);
|
||||
|
||||
// added after the others
|
||||
let mut second = new_waypoint("second");
|
||||
second.lng = 5.0;
|
||||
second.apply(&mut fx.state()).unwrap();
|
||||
let names: Vec<_> = waypoints(&fx, b)
|
||||
.iter()
|
||||
.map(|w| w.name.clone().unwrap())
|
||||
.collect();
|
||||
assert_eq!(names, ["peak", "second"]);
|
||||
// and not selected
|
||||
assert_eq!(fx.selected_files(), [b].into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_fields_are_not_set() {
|
||||
let mut fx = Fixture::default();
|
||||
New { name: "a" }.apply(&mut fx.state()).unwrap();
|
||||
let a = fx.order.0[0];
|
||||
NewWaypoint {
|
||||
lng: 1.0,
|
||||
lat: 2.0,
|
||||
ele: 0.0,
|
||||
name: "",
|
||||
desc: "",
|
||||
icon: "",
|
||||
link: "",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let wpt = &waypoints(&fx, a)[0];
|
||||
assert!(wpt.name.is_none() && wpt.desc.is_none() && wpt.cmt.is_none());
|
||||
assert!(wpt.sym.is_none() && wpt.link.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_each_selected_file_gets_its_own_waypoint() {
|
||||
let mut fx = Fixture::default();
|
||||
for name in ["a", "b", "c"] {
|
||||
New { name }.apply(&mut fx.state()).unwrap();
|
||||
}
|
||||
let ids = fx.order.0.clone();
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([ids[0], ids[2]]),
|
||||
};
|
||||
new_waypoint("w").apply(&mut fx.state()).unwrap();
|
||||
assert_eq!(waypoints(&fx, ids[0]).len(), 1);
|
||||
assert!(waypoints(&fx, ids[1]).is_empty());
|
||||
assert_eq!(waypoints(&fx, ids[2]).len(), 1);
|
||||
// not the same waypoint
|
||||
assert_ne!(waypoints(&fx, ids[0])[0].id, waypoints(&fx, ids[2])[0].id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_file_of_the_selected_elements_is_used() {
|
||||
for selection in [
|
||||
|id| Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [TrackId::default()].into(),
|
||||
},
|
||||
|id| Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: TrackId::default(),
|
||||
trkseg_ids: [TrackSegmentId::default()].into(),
|
||||
},
|
||||
|id| Selection::Waypoints { file_id: id },
|
||||
|id| Selection::Waypoint {
|
||||
file_id: id,
|
||||
wpt_ids: HashSet::new(),
|
||||
},
|
||||
] {
|
||||
let mut fx = Fixture::default();
|
||||
New { name: "a" }.apply(&mut fx.state()).unwrap();
|
||||
let a = fx.order.0[0];
|
||||
fx.selection = selection(a);
|
||||
new_waypoint("w").apply(&mut fx.state()).unwrap();
|
||||
assert_eq!(waypoint_ids(&fx.files[&a]).count(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nothing_selected_or_unknown_file() {
|
||||
let mut fx = Fixture::default();
|
||||
New { name: "a" }.apply(&mut fx.state()).unwrap();
|
||||
fx.selection = Selection::Empty;
|
||||
assert_eq!(
|
||||
new_waypoint("w").apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
fx.selection = Selection::Waypoints {
|
||||
file_id: FileId::default(),
|
||||
};
|
||||
assert_eq!(
|
||||
new_waypoint("w").apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
assert_eq!(fx.files.len(), 1);
|
||||
}
|
||||
}
|
||||
@@ -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,315 @@
|
||||
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,
|
||||
name: "file",
|
||||
}
|
||||
.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,
|
||||
name: "file",
|
||||
}
|
||||
.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,157 @@
|
||||
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,
|
||||
name: "file",
|
||||
}
|
||||
.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,158 @@
|
||||
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 desc: Option<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 desc: Option<String>,
|
||||
/// Style of the track, only when the file defines it.
|
||||
pub color: Option<String>,
|
||||
pub opacity: Option<f64>,
|
||||
pub width: Option<f64>,
|
||||
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>,
|
||||
pub sym: Option<String>,
|
||||
}
|
||||
|
||||
impl FileStructure {
|
||||
pub fn new(file: &File) -> Self {
|
||||
Self {
|
||||
id: file.id,
|
||||
name: file.info.name.clone(),
|
||||
desc: file.info.desc.clone(),
|
||||
tracks: file
|
||||
.trk
|
||||
.iter()
|
||||
.map(|trk| TrackNode {
|
||||
id: trk.id,
|
||||
name: trk.info.name.clone(),
|
||||
desc: trk.info.desc.clone(),
|
||||
color: trk.info.color.clone(),
|
||||
opacity: trk.info.opacity,
|
||||
width: trk.info.width,
|
||||
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(),
|
||||
sym: wpt.sym.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,
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = &fx.files[&fx.order.0[0]];
|
||||
let node = FileStructure::new(file);
|
||||
assert_eq!(
|
||||
(node.id, &node.name, &node.desc),
|
||||
(file.id, &file.info.name, &file.info.desc)
|
||||
);
|
||||
assert_eq!(node.tracks.len(), file.trk.len());
|
||||
for (n, t) in node.tracks.iter().zip(&file.trk) {
|
||||
assert_eq!((n.id, &n.name, &n.desc), (t.id, &t.info.name, &t.info.desc));
|
||||
assert_eq!(
|
||||
(&n.color, n.opacity, n.width),
|
||||
(&t.info.color, t.info.opacity, t.info.width)
|
||||
);
|
||||
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,92 @@
|
||||
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,
|
||||
name: "file",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
Load {
|
||||
data: &data,
|
||||
name: "file",
|
||||
}
|
||||
.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,482 @@
|
||||
use crate::{GlobalStatistics, Statistics, TrackSegment, sum_options};
|
||||
|
||||
/// Value of the timestamps of the trackpoints that have none.
|
||||
pub const NO_TIME: i64 = i64::MIN;
|
||||
|
||||
/// Flattened statistics of the selected segments, one entry per trackpoint, in selection order.
|
||||
///
|
||||
/// The distance, time, moving and elevation values are cumulative over the whole selection (the
|
||||
/// second segment continues where the first one ended), so the statistics of any range of
|
||||
/// trackpoints are the difference between its two ends: see [`StatisticsBuffer::slice`].
|
||||
/// Times are in milliseconds, distances in kilometers, speeds in km/h. Missing times are
|
||||
/// [`NO_TIME`], missing measures are NaN.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StatisticsBuffer {
|
||||
/// Statistics of the whole selection.
|
||||
pub global: GlobalStatistics,
|
||||
pub total_distance: Vec<f64>,
|
||||
pub moving_distance: Vec<f64>,
|
||||
pub total_time: Vec<i64>,
|
||||
pub moving_time: Vec<i64>,
|
||||
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>,
|
||||
pub lng: Vec<f64>,
|
||||
pub lat: Vec<f64>,
|
||||
pub ele: Vec<f64>,
|
||||
/// Timestamps, in milliseconds since the epoch.
|
||||
pub time: Vec<i64>,
|
||||
pub hr: Vec<f64>,
|
||||
pub cad: Vec<f64>,
|
||||
pub atemp: Vec<f64>,
|
||||
pub power: Vec<f64>,
|
||||
/// Surface of the trackpoints: 0 when unknown, else 1 + its code in the categories of the
|
||||
/// engine (so that it is never 0 for a known surface).
|
||||
pub surface: Vec<u8>,
|
||||
/// Highway of the trackpoints, as the surface.
|
||||
pub highway: Vec<u8>,
|
||||
/// SAC hiking scale of the trackpoints, as the surface.
|
||||
pub sac_scale: Vec<u8>,
|
||||
/// Mountain biking scale of the trackpoints, as the surface.
|
||||
pub mtb_scale: Vec<u8>,
|
||||
}
|
||||
|
||||
/// 0 for an unknown value, else the code plus one. The categories hold at most 255 values, so it
|
||||
/// fits.
|
||||
fn unknown_or_next(code: Option<u8>) -> u8 {
|
||||
code.and_then(|code| code.checked_add(1)).unwrap_or(0)
|
||||
}
|
||||
|
||||
impl StatisticsBuffer {
|
||||
pub fn len(&self) -> usize {
|
||||
self.total_distance.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.total_distance.is_empty()
|
||||
}
|
||||
|
||||
/// `selected`: the segments and their statistics, in order.
|
||||
pub fn update(&mut self, selected: &[(&TrackSegment, &Statistics)]) {
|
||||
self.global = GlobalStatistics::default();
|
||||
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();
|
||||
self.lng.clear();
|
||||
self.lat.clear();
|
||||
self.ele.clear();
|
||||
self.time.clear();
|
||||
self.hr.clear();
|
||||
self.cad.clear();
|
||||
self.atemp.clear();
|
||||
self.power.clear();
|
||||
self.surface.clear();
|
||||
self.highway.clear();
|
||||
self.sac_scale.clear();
|
||||
self.mtb_scale.clear();
|
||||
|
||||
let optional = |value: Option<f64>| value.unwrap_or(f64::NAN);
|
||||
|
||||
for (segment, stats) in selected {
|
||||
for (trkpt, trkpt_stats) in segment.iter().zip(stats.local.iter()) {
|
||||
let cumul_stats = &self.global;
|
||||
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);
|
||||
self.lng.push(trkpt.coordinates.lng);
|
||||
self.lat.push(trkpt.coordinates.lat);
|
||||
self.ele.push(trkpt.ele);
|
||||
self.time.push(trkpt.time.unwrap_or(NO_TIME));
|
||||
self.hr.push(optional(trkpt.hr.map(f64::from)));
|
||||
self.cad.push(optional(trkpt.cad.map(f64::from)));
|
||||
self.atemp.push(optional(trkpt.atemp.map(f64::from)));
|
||||
self.power.push(optional(trkpt.power.map(f64::from)));
|
||||
self.surface.push(unknown_or_next(trkpt.surface));
|
||||
self.highway.push(unknown_or_next(trkpt.highway));
|
||||
self.sac_scale.push(unknown_or_next(trkpt.sac_scale));
|
||||
self.mtb_scale.push(unknown_or_next(trkpt.mtb_scale));
|
||||
}
|
||||
self.global.merge(&stats.global);
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics of the trackpoints from `start` to `end` (both included) of the selection, for
|
||||
/// example the part of an elevation profile the user dragged over. `None` if the range is
|
||||
/// not inside the selection.
|
||||
///
|
||||
/// It is done in constant time, from the cumulative values at both ends: the bounds and
|
||||
/// the averages are the ones of the whole selection, they are not computed for the range.
|
||||
pub fn slice(&self, start: usize, end: usize) -> Option<GlobalStatistics> {
|
||||
if start > end || end >= self.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let delta = |values: &[f64]| values[end] - values[start];
|
||||
let delta_time = |values: &[i64]| values[end] - values[start];
|
||||
let time = |i: usize| (self.time[i] != NO_TIME).then_some(self.time[i]);
|
||||
Some(GlobalStatistics {
|
||||
total_distance: delta(&self.total_distance),
|
||||
moving_distance: self
|
||||
.global
|
||||
.moving_distance
|
||||
.map(|_| delta(&self.moving_distance)),
|
||||
total_time: self.global.total_time.map(|_| delta_time(&self.total_time)),
|
||||
moving_time: self
|
||||
.global
|
||||
.moving_time
|
||||
.map(|_| delta_time(&self.moving_time)),
|
||||
elevation_gain: delta(&self.elevation_gain),
|
||||
elevation_loss: delta(&self.elevation_loss),
|
||||
start_time: time(start),
|
||||
end_time: time(end),
|
||||
bounds: self.global.bounds,
|
||||
hr: self.global.hr,
|
||||
cad: self.global.cad,
|
||||
atemp: self.global.atemp,
|
||||
power: self.global.power,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::parse;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn computed(path: &str) -> (TrackSegment, Statistics) {
|
||||
let data = std::fs::read(path).unwrap();
|
||||
let file = parse(&data, &mut Default::default()).unwrap();
|
||||
let segment = file.trk[0].trkseg[0].clone();
|
||||
let stats = Statistics::compute(&segment);
|
||||
(segment, stats)
|
||||
}
|
||||
|
||||
fn all_lengths(buffer: &StatisticsBuffer) -> Vec<usize> {
|
||||
vec![
|
||||
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(),
|
||||
buffer.lng.len(),
|
||||
buffer.lat.len(),
|
||||
buffer.ele.len(),
|
||||
buffer.time.len(),
|
||||
buffer.hr.len(),
|
||||
buffer.cad.len(),
|
||||
buffer.atemp.len(),
|
||||
buffer.power.len(),
|
||||
buffer.surface.len(),
|
||||
buffer.highway.len(),
|
||||
buffer.sac_scale.len(),
|
||||
buffer.mtb_scale.len(),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty() {
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[]);
|
||||
assert!(buffer.is_empty());
|
||||
assert!(buffer.speed.is_empty());
|
||||
assert_eq!(buffer.global.total_distance, 0.0);
|
||||
assert!(buffer.slice(0, 0).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_segment_matches_local_stats() {
|
||||
let (segment, s) = computed("data/simple.gpx");
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s)]);
|
||||
|
||||
let n = s.local.len();
|
||||
assert!(all_lengths(&buffer).iter().all(|len| *len == n));
|
||||
assert_eq!(buffer.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);
|
||||
}
|
||||
// the trackpoints
|
||||
for (i, trkpt) in segment.iter().enumerate() {
|
||||
assert_eq!(buffer.lng[i], trkpt.coordinates.lng);
|
||||
assert_eq!(buffer.lat[i], trkpt.coordinates.lat);
|
||||
assert_eq!(buffer.ele[i], trkpt.ele);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_is_the_merge_of_the_segments() {
|
||||
let (segment, s) = computed("data/simple.gpx");
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s), (&segment, &s)]);
|
||||
|
||||
let global = &buffer.global;
|
||||
assert!((global.total_distance - 2.0 * s.global.total_distance).abs() < 1e-9);
|
||||
assert!((global.elevation_gain - 2.0 * s.global.elevation_gain).abs() < 1e-9);
|
||||
assert!((global.elevation_loss - 2.0 * s.global.elevation_loss).abs() < 1e-9);
|
||||
// and it is the last cumulative value of the points
|
||||
assert_eq!(
|
||||
global.total_distance,
|
||||
*buffer.total_distance.last().unwrap()
|
||||
);
|
||||
|
||||
buffer.update(&[(&segment, &s)]);
|
||||
assert_eq!(buffer.global.total_distance, s.global.total_distance);
|
||||
buffer.update(&[]);
|
||||
assert_eq!(buffer.global.total_distance, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_distances_accumulate_over_segments() {
|
||||
let (segment, s) = computed("data/simple.gpx");
|
||||
let n = s.local.len();
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s), (&segment, &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 (segment, s) = computed("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(&[(&segment, &s), (&segment, &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]));
|
||||
assert!(buffer.time.iter().all(|t| *t != NO_TIME));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_times_and_measures_are_nan() {
|
||||
let (segment, s) = computed("data/simple.gpx");
|
||||
assert!(segment.iter().all(|trkpt| trkpt.time.is_none()));
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s)]);
|
||||
assert!(buffer.time.iter().all(|t| *t == NO_TIME));
|
||||
assert!(buffer.hr.iter().all(|v| v.is_nan()));
|
||||
assert_eq!(buffer.global.hr.count, 0);
|
||||
assert_eq!(buffer.global.total_time, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_surface_and_highway_codes() {
|
||||
let (segment, s) = computed("data/with_highway.gpx");
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s)]);
|
||||
|
||||
// 0 for the trackpoints that have none, else the code of the engine plus one
|
||||
assert_eq!(buffer.surface, [1, 1, 0, 2, 2]);
|
||||
assert_eq!(buffer.highway, [1, 1, 0, 2, 0]);
|
||||
assert_eq!(buffer.sac_scale, [0, 0, 0, 1, 2]);
|
||||
assert_eq!(buffer.mtb_scale, [0, 0, 0, 1, 1]);
|
||||
assert!(all_lengths(&buffer).iter().all(|len| *len == 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codes_of_the_trackpoints_are_used_as_they_are() {
|
||||
let (mut segment, _) = computed("data/simple.gpx");
|
||||
let mut point = segment[0].clone();
|
||||
point.surface = Some(7);
|
||||
point.highway = Some(254);
|
||||
point.sac_scale = Some(0);
|
||||
point.mtb_scale = Some(3);
|
||||
segment.splice(0, 1, vec![point]);
|
||||
let s = Statistics::compute(&segment);
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s)]);
|
||||
assert_eq!((buffer.surface[0], buffer.highway[0]), (8, 255));
|
||||
assert_eq!((buffer.sac_scale[0], buffer.mtb_scale[0]), (1, 4));
|
||||
assert_eq!((buffer.surface[1], buffer.highway[1]), (0, 0));
|
||||
assert_eq!((buffer.sac_scale[1], buffer.mtb_scale[1]), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_codes_without_surface_and_highway() {
|
||||
let (segment, s) = computed("data/simple.gpx");
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s)]);
|
||||
assert_eq!(buffer.surface.len(), s.local.len());
|
||||
assert!(buffer.surface.iter().all(|c| *c == 0));
|
||||
assert!(buffer.highway.iter().all(|c| *c == 0));
|
||||
assert!(buffer.sac_scale.iter().all(|c| *c == 0));
|
||||
assert!(buffer.mtb_scale.iter().all(|c| *c == 0));
|
||||
// and they are reset by the next update
|
||||
buffer.update(&[]);
|
||||
assert!(buffer.surface.is_empty() && buffer.highway.is_empty());
|
||||
assert!(buffer.sac_scale.is_empty() && buffer.mtb_scale.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_replaces_previous_content() {
|
||||
let (segment, s) = computed("data/simple.gpx");
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s)]);
|
||||
buffer.update(&[(&segment, &s)]);
|
||||
assert_eq!(buffer.total_distance.len(), s.local.len());
|
||||
assert!(all_lengths(&buffer).iter().all(|len| *len == s.local.len()));
|
||||
buffer.update(&[]);
|
||||
assert!(buffer.total_distance.is_empty());
|
||||
assert!(all_lengths(&buffer).iter().all(|len| *len == 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slice_of_the_whole_selection_is_the_global_statistics() {
|
||||
let (segment, s) = computed("data/with_time.gpx");
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s), (&segment, &s)]);
|
||||
|
||||
let slice = buffer.slice(0, buffer.len() - 1).unwrap();
|
||||
let global = &buffer.global;
|
||||
assert!((slice.total_distance - global.total_distance).abs() < 1e-9);
|
||||
assert!((slice.elevation_gain - global.elevation_gain).abs() < 1e-9);
|
||||
assert!((slice.elevation_loss - global.elevation_loss).abs() < 1e-9);
|
||||
assert_eq!(slice.total_time, global.total_time);
|
||||
assert_eq!(slice.moving_time, global.moving_time);
|
||||
assert_eq!(slice.start_time, global.start_time);
|
||||
assert_eq!(slice.end_time, global.end_time);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slice_of_a_range() {
|
||||
let (segment, s) = computed("data/with_time.gpx");
|
||||
let n = s.local.len();
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s)]);
|
||||
|
||||
let (start, end) = (n / 4, n / 2);
|
||||
let slice = buffer.slice(start, end).unwrap();
|
||||
assert!(
|
||||
(slice.total_distance - (buffer.total_distance[end] - buffer.total_distance[start]))
|
||||
.abs()
|
||||
< 1e-12
|
||||
);
|
||||
assert_eq!(
|
||||
slice.total_time,
|
||||
Some(buffer.total_time[end] - buffer.total_time[start])
|
||||
);
|
||||
assert_eq!(
|
||||
slice.moving_time,
|
||||
Some(buffer.moving_time[end] - buffer.moving_time[start])
|
||||
);
|
||||
assert_eq!(slice.start_time, Some(buffer.time[start]));
|
||||
assert_eq!(slice.end_time, Some(buffer.time[end]));
|
||||
assert!(slice.total_distance < buffer.global.total_distance);
|
||||
assert!(slice.total_speed().is_some());
|
||||
|
||||
// a single point has nothing between its ends
|
||||
let point = buffer.slice(start, start).unwrap();
|
||||
assert_eq!(point.total_distance, 0.0);
|
||||
assert_eq!(point.total_time, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slice_keeps_the_bounds_and_averages_of_the_selection() {
|
||||
let data = std::fs::read("data/with_hr.gpx").unwrap();
|
||||
let file = parse(&data, &mut Default::default()).unwrap();
|
||||
let segment = file.trk[0].trkseg[0].clone();
|
||||
let s = Statistics::compute(&segment);
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s)]);
|
||||
assert!(buffer.global.hr.count > 0);
|
||||
|
||||
let slice = buffer.slice(1, 3).unwrap();
|
||||
let (global, bounds) = (&buffer.global, &slice.bounds);
|
||||
assert_eq!(slice.hr, global.hr);
|
||||
assert_eq!(slice.cad, global.cad);
|
||||
assert_eq!(slice.atemp, global.atemp);
|
||||
assert_eq!(slice.power, global.power);
|
||||
assert_eq!(
|
||||
(bounds.sw.lng, bounds.sw.lat),
|
||||
(global.bounds.sw.lng, global.bounds.sw.lat)
|
||||
);
|
||||
assert_eq!(
|
||||
(bounds.ne.lng, bounds.ne.lat),
|
||||
(global.bounds.ne.lng, global.bounds.ne.lat)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_durations_are_not_limited_to_24_days() {
|
||||
// 30 days of recording, in milliseconds
|
||||
let (segment, mut s) = computed("data/with_time.gpx");
|
||||
let month = 30 * 24 * 3_600_000_i64;
|
||||
s.global.total_time = Some(month);
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s), (&segment, &s)]);
|
||||
assert_eq!(buffer.global.total_time, Some(2 * month));
|
||||
assert!(2 * month > i64::from(i32::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slice_across_segments() {
|
||||
let (segment, s) = computed("data/simple.gpx");
|
||||
let n = s.local.len();
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s), (&segment, &s)]);
|
||||
|
||||
// from the middle of the first segment to the middle of the second one
|
||||
let slice = buffer.slice(n / 2, n + n / 2).unwrap();
|
||||
let expected = buffer.total_distance[n + n / 2] - buffer.total_distance[n / 2];
|
||||
assert!((slice.total_distance - expected).abs() < 1e-9);
|
||||
assert_eq!(slice.total_time, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slice_out_of_range() {
|
||||
let (segment, s) = computed("data/simple.gpx");
|
||||
let mut buffer = StatisticsBuffer::default();
|
||||
buffer.update(&[(&segment, &s)]);
|
||||
let n = buffer.len();
|
||||
assert!(buffer.slice(0, n).is_none());
|
||||
assert!(buffer.slice(n, n).is_none());
|
||||
assert!(buffer.slice(5, 4).is_none());
|
||||
assert!(buffer.slice(0, n - 1).is_some());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
use std::collections::{HashMap, HashSet, hash_map::Entry};
|
||||
|
||||
use crate::{
|
||||
File, FileId, GlobalStatistics, Selection, StackEntry, Statistics, Track, TrackSegment,
|
||||
TrackSegmentRevisionId,
|
||||
};
|
||||
|
||||
/// A segment and its statistics.
|
||||
pub type SelectedSegment<'a> = (&'a TrackSegment, &'a Statistics);
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics of the segments of the selection.
|
||||
pub fn get<'a>(
|
||||
&'a self,
|
||||
state: Option<&'a StackEntry>,
|
||||
selection: &Selection,
|
||||
) -> Vec<&'a Statistics> {
|
||||
self.selected(state, selection, &[])
|
||||
.into_iter()
|
||||
.map(|(_, stats)| stats)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The segments of the selection with their statistics. The segments of several selected
|
||||
/// files follow `order` (the files missing from it come last).
|
||||
pub fn selected<'a>(
|
||||
&'a self,
|
||||
state: Option<&'a StackEntry>,
|
||||
selection: &Selection,
|
||||
order: &[FileId],
|
||||
) -> Vec<SelectedSegment<'a>> {
|
||||
let mut selected = vec![];
|
||||
if let Some(state) = state {
|
||||
match selection {
|
||||
Selection::Empty => (),
|
||||
Selection::File { file_ids } => {
|
||||
let ordered = order
|
||||
.iter()
|
||||
.filter(|id| file_ids.contains(id))
|
||||
.chain(file_ids.iter().filter(|id| !order.contains(id)));
|
||||
for id in ordered {
|
||||
if let Some(file) = state.get(id) {
|
||||
self.add_file_stats(file, &mut selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 selected);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Selection::Waypoints { file_id } => {
|
||||
if let Some(file) = state.get(file_id) {
|
||||
self.add_file_stats(file, &mut selected);
|
||||
}
|
||||
}
|
||||
Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: _,
|
||||
} => {
|
||||
if let Some(file) = state.get(file_id) {
|
||||
self.add_file_stats(file, &mut selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
selected
|
||||
}
|
||||
|
||||
/// Global statistics of a file: its segments merged, in order.
|
||||
pub fn file_global(&self, file: &File) -> GlobalStatistics {
|
||||
let mut selected = vec![];
|
||||
self.add_file_stats(file, &mut selected);
|
||||
let mut global = GlobalStatistics::default();
|
||||
for (_, stats) in selected {
|
||||
global.merge(&stats.global);
|
||||
}
|
||||
global
|
||||
}
|
||||
|
||||
fn add_file_stats<'a>(&'a self, file: &'a File, selected: &mut Vec<SelectedSegment<'a>>) {
|
||||
for trk in file.trk.iter() {
|
||||
self.add_track_stats(trk, selected);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_track_stats<'a>(&'a self, trk: &'a Track, selected: &mut Vec<SelectedSegment<'a>>) {
|
||||
for trkseg in trk.trkseg.iter() {
|
||||
self.add_segment_stats(trkseg, selected);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_segment_stats<'a>(
|
||||
&'a self,
|
||||
trkseg: &'a TrackSegment,
|
||||
selected: &mut Vec<SelectedSegment<'a>>,
|
||||
) {
|
||||
if let Some(stats) = self.map.get(&trkseg.rev_id) {
|
||||
selected.push((trkseg, stats));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
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, &mut Default::default()).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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_segments_of_several_files_follow_the_given_order() {
|
||||
let (mut state, first) = state("data/simple.gpx");
|
||||
let (other, second) = {
|
||||
let data = std::fs::read("data/with_time.gpx").unwrap();
|
||||
let file = Rc::new(parse(&data, &mut Default::default()).unwrap());
|
||||
(file.clone(), file)
|
||||
};
|
||||
state.insert(other.id, other);
|
||||
let mut cache = StatisticsCache::default();
|
||||
cache.update(Some(&state));
|
||||
let selection = Selection::File {
|
||||
file_ids: HashSet::from([first.id, second.id]),
|
||||
};
|
||||
|
||||
let ids = |order: &[FileId]| -> Vec<_> {
|
||||
cache
|
||||
.selected(Some(&state), &selection, order)
|
||||
.iter()
|
||||
.map(|(segment, _)| segment.id)
|
||||
.collect()
|
||||
};
|
||||
let (a, b) = (first.trk[0].trkseg[0].id, second.trk[0].trkseg[0].id);
|
||||
assert_eq!(ids(&[first.id, second.id]), vec![a, b]);
|
||||
assert_eq!(ids(&[second.id, first.id]), vec![b, a]);
|
||||
// the files that are not in the order come last
|
||||
assert_eq!(ids(&[second.id]), vec![b, a]);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,479 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
File, FileId, Selection, StackEntry, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint,
|
||||
WaypointId,
|
||||
};
|
||||
|
||||
/// A track in the clipboard, with the name of the file it comes from (the name of the new file
|
||||
/// when it is pasted on its own).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClipboardTrack {
|
||||
pub track: Track,
|
||||
pub file_name: String,
|
||||
}
|
||||
|
||||
/// A segment in the clipboard, with the name of its track, or of its file when the track has
|
||||
/// none (the name of the new file when it is pasted on its own).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClipboardSegment {
|
||||
pub segment: TrackSegment,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// The elements that were copied or cut, in the order of the files.
|
||||
///
|
||||
/// It holds the elements themselves, not references to them: they are cheap to keep (files are
|
||||
/// shared, and so are the trackpoints of tracks and segments), and the clipboard stays valid
|
||||
/// when the elements are changed or deleted.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ClipboardContent {
|
||||
Files(Vec<Rc<File>>),
|
||||
Tracks(Vec<ClipboardTrack>),
|
||||
Segments(Vec<ClipboardSegment>),
|
||||
Waypoints(Vec<Waypoint>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct ClipboardId(Uuid);
|
||||
|
||||
impl Default for ClipboardId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
/// What was copied or cut, to be pasted, as many times as needed.
|
||||
///
|
||||
/// What is pasted is always the elements as they were when they were copied or cut, whatever
|
||||
/// happened to them since, even if they were deleted. Pasting what was copied makes copies, with
|
||||
/// new ids. What was cut is moved, with its ids, the first time it is pasted (the elements that
|
||||
/// are still there are removed): the clipboard is not cut anymore after that, and the next
|
||||
/// pastes make copies.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Clipboard {
|
||||
id: ClipboardId,
|
||||
pub content: ClipboardContent,
|
||||
pub cut: bool,
|
||||
}
|
||||
|
||||
/// Two clipboards are the same when they come from the same copy, and are cut the same way.
|
||||
impl PartialEq for Clipboard {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id && self.cut == other.cut
|
||||
}
|
||||
}
|
||||
|
||||
impl Clipboard {
|
||||
/// The clipboard for the selected elements, `None` if nothing is selected (or if what is
|
||||
/// selected does not exist).
|
||||
pub fn new(
|
||||
selection: &Selection,
|
||||
files: &StackEntry,
|
||||
order: &[FileId],
|
||||
cut: bool,
|
||||
) -> Option<Clipboard> {
|
||||
let content = match selection {
|
||||
Selection::Empty => return None,
|
||||
Selection::File { file_ids } => ClipboardContent::Files(
|
||||
order
|
||||
.iter()
|
||||
.filter(|id| file_ids.contains(id))
|
||||
.filter_map(|id| files.get(id).cloned())
|
||||
.collect(),
|
||||
),
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = files.get(file_id)?;
|
||||
ClipboardContent::Tracks(
|
||||
file.trk
|
||||
.iter()
|
||||
.filter(|trk| trk_ids.contains(&trk.id))
|
||||
.map(|trk| ClipboardTrack {
|
||||
track: trk.clone(),
|
||||
file_name: file.info.name.clone(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => {
|
||||
let file = files.get(file_id)?;
|
||||
let trk = file.trk.iter().find(|trk| trk.id == *trk_id)?;
|
||||
let name = trk
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| file.info.name.clone());
|
||||
ClipboardContent::Segments(
|
||||
trk.trkseg
|
||||
.iter()
|
||||
.filter(|seg| trkseg_ids.contains(&seg.id))
|
||||
.map(|seg| ClipboardSegment {
|
||||
segment: seg.clone(),
|
||||
name: name.clone(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
Selection::Waypoints { file_id } => ClipboardContent::Waypoints(
|
||||
files
|
||||
.get(file_id)?
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| chunk.wpt.iter().cloned())
|
||||
.collect(),
|
||||
),
|
||||
Selection::Waypoint { file_id, wpt_ids } => ClipboardContent::Waypoints(
|
||||
files
|
||||
.get(file_id)?
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.filter(|wpt| wpt_ids.contains(&wpt.id))
|
||||
.cloned()
|
||||
.collect(),
|
||||
),
|
||||
};
|
||||
let clipboard = Clipboard {
|
||||
id: ClipboardId::default(),
|
||||
content,
|
||||
cut,
|
||||
};
|
||||
(!clipboard.is_empty()).then_some(clipboard)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match &self.content {
|
||||
ClipboardContent::Files(files) => files.is_empty(),
|
||||
ClipboardContent::Tracks(tracks) => tracks.is_empty(),
|
||||
ClipboardContent::Segments(segments) => segments.is_empty(),
|
||||
ClipboardContent::Waypoints(waypoints) => waypoints.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the content can be pasted when `target` is selected: files and tracks can be
|
||||
/// pasted anywhere above them in the file tree, and nothing selected stands for the list of
|
||||
/// files.
|
||||
///
|
||||
/// | copied | pasted when selected |
|
||||
/// |-----------|------------------------------------------|
|
||||
/// | files | nothing, a file |
|
||||
/// | tracks | nothing, a file, a track |
|
||||
/// | segments | nothing, a file, a track, a segment |
|
||||
/// | waypoints | a file, the waypoints, a waypoint |
|
||||
pub fn can_paste(&self, target: &Selection) -> bool {
|
||||
use ClipboardContent as C;
|
||||
use Selection::*;
|
||||
matches!(
|
||||
(&self.content, target),
|
||||
(C::Files(_), Empty | File { .. })
|
||||
| (C::Tracks(_), Empty | File { .. } | Track { .. })
|
||||
| (
|
||||
C::Segments(_),
|
||||
Empty | File { .. } | Track { .. } | TrackSegment { .. }
|
||||
)
|
||||
| (
|
||||
C::Waypoints(_),
|
||||
File { .. } | Waypoints { .. } | Waypoint { .. }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The ids of the elements of the content, by kind.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ClipboardIds {
|
||||
Files(Vec<FileId>),
|
||||
Tracks(Vec<TrackId>),
|
||||
Segments(Vec<TrackSegmentId>),
|
||||
Waypoints(Vec<WaypointId>),
|
||||
}
|
||||
|
||||
impl ClipboardContent {
|
||||
pub fn ids(&self) -> ClipboardIds {
|
||||
match self {
|
||||
ClipboardContent::Files(files) => {
|
||||
ClipboardIds::Files(files.iter().map(|file| file.id).collect())
|
||||
}
|
||||
ClipboardContent::Tracks(tracks) => {
|
||||
ClipboardIds::Tracks(tracks.iter().map(|trk| trk.track.id).collect())
|
||||
}
|
||||
ClipboardContent::Segments(segments) => {
|
||||
ClipboardIds::Segments(segments.iter().map(|seg| seg.segment.id).collect())
|
||||
}
|
||||
ClipboardContent::Waypoints(waypoints) => {
|
||||
ClipboardIds::Waypoints(waypoints.iter().map(|wpt| wpt.id).collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{TrackInfo, WaypointChunk};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A file with 3 tracks (named "t0", "t1", "t2") of 2 segments and 3 waypoints.
|
||||
fn file(name: &str) -> File {
|
||||
let mut file = File::default();
|
||||
file.info.name = name.to_owned();
|
||||
for i in 0..3 {
|
||||
file.trk.push(Track {
|
||||
info: TrackInfo {
|
||||
name: (i < 2).then(|| format!("t{i}")),
|
||||
..Default::default()
|
||||
},
|
||||
trkseg: vec![TrackSegment::default(), TrackSegment::default()],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
file.wpt.push(Rc::new(WaypointChunk {
|
||||
wpt: vec![Waypoint::default(), Waypoint::default()],
|
||||
..Default::default()
|
||||
}));
|
||||
file.wpt.push(Rc::new(WaypointChunk {
|
||||
wpt: vec![Waypoint::default()],
|
||||
..Default::default()
|
||||
}));
|
||||
file
|
||||
}
|
||||
|
||||
fn files_of(files: Vec<File>) -> StackEntry {
|
||||
files
|
||||
.into_iter()
|
||||
.map(|file| (file.id, Rc::new(file)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn order_of(files: &StackEntry) -> Vec<FileId> {
|
||||
files.keys().copied().collect()
|
||||
}
|
||||
|
||||
fn tracks(file: &File, ids: &[usize]) -> Selection {
|
||||
Selection::Track {
|
||||
file_id: file.id,
|
||||
trk_ids: ids.iter().map(|i| file.trk[*i].id).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_clipboard_holds_what_is_selected() {
|
||||
let file = file("source");
|
||||
let (t0, t2) = (file.trk[0].id, file.trk[2].id);
|
||||
let (s0, s1) = (file.trk[1].trkseg[0].id, file.trk[1].trkseg[1].id);
|
||||
let wpt: Vec<_> = file
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|c| c.wpt.iter().map(|w| w.id))
|
||||
.collect();
|
||||
let files = files_of(vec![file.clone()]);
|
||||
|
||||
// tracks, in the order of the file, with the name of their file
|
||||
let clipboard =
|
||||
Clipboard::new(&tracks(&file, &[2, 0]), &files, &order_of(&files), false).unwrap();
|
||||
assert!(!clipboard.cut);
|
||||
match &clipboard.content {
|
||||
ClipboardContent::Tracks(copied) => {
|
||||
assert_eq!(
|
||||
copied.iter().map(|c| c.track.id).collect::<Vec<_>>(),
|
||||
vec![t0, t2]
|
||||
);
|
||||
assert!(copied.iter().all(|c| c.file_name == "source"));
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
|
||||
// segments, with the name of their track, or else of their file
|
||||
let segments = |trk: usize, ids: &[TrackSegmentId]| Selection::TrackSegment {
|
||||
file_id: file.id,
|
||||
trk_id: file.trk[trk].id,
|
||||
trkseg_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
match Clipboard::new(&segments(1, &[s1, s0]), &files, &order_of(&files), true)
|
||||
.unwrap()
|
||||
.content
|
||||
{
|
||||
ClipboardContent::Segments(copied) => {
|
||||
assert_eq!(
|
||||
copied.iter().map(|c| c.segment.id).collect::<Vec<_>>(),
|
||||
vec![s0, s1]
|
||||
);
|
||||
assert!(copied.iter().all(|c| c.name == "t1"));
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
let unnamed = file.trk[2].trkseg[0].id;
|
||||
match Clipboard::new(&segments(2, &[unnamed]), &files, &order_of(&files), true)
|
||||
.unwrap()
|
||||
.content
|
||||
{
|
||||
ClipboardContent::Segments(copied) => assert_eq!(copied[0].name, "source"),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
|
||||
// waypoints: some of them, or all of them
|
||||
let some = Selection::Waypoint {
|
||||
file_id: file.id,
|
||||
wpt_ids: [wpt[2], wpt[0]].into(),
|
||||
};
|
||||
assert_eq!(
|
||||
Clipboard::new(&some, &files, &order_of(&files), false)
|
||||
.unwrap()
|
||||
.content
|
||||
.ids(),
|
||||
ClipboardIds::Waypoints(vec![wpt[0], wpt[2]])
|
||||
);
|
||||
assert_eq!(
|
||||
Clipboard::new(
|
||||
&Selection::Waypoints { file_id: file.id },
|
||||
&files,
|
||||
&order_of(&files),
|
||||
false
|
||||
)
|
||||
.unwrap()
|
||||
.content
|
||||
.ids(),
|
||||
ClipboardIds::Waypoints(wpt)
|
||||
);
|
||||
|
||||
// files
|
||||
let selection = Selection::File {
|
||||
file_ids: [file.id].into(),
|
||||
};
|
||||
assert_eq!(
|
||||
Clipboard::new(&selection, &files, &order_of(&files), false)
|
||||
.unwrap()
|
||||
.content
|
||||
.ids(),
|
||||
ClipboardIds::Files(vec![file.id])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nothing_to_copy() {
|
||||
let file = file("f");
|
||||
let files = files_of(vec![file.clone()]);
|
||||
assert!(Clipboard::new(&Selection::Empty, &files, &order_of(&files), false).is_none());
|
||||
// elements that do not exist
|
||||
let unknown = Selection::Track {
|
||||
file_id: FileId::default(),
|
||||
trk_ids: [TrackId::default()].into(),
|
||||
};
|
||||
assert!(Clipboard::new(&unknown, &files, &order_of(&files), false).is_none());
|
||||
let unknown = Selection::Track {
|
||||
file_id: file.id,
|
||||
trk_ids: [TrackId::default()].into(),
|
||||
};
|
||||
assert!(Clipboard::new(&unknown, &files, &order_of(&files), false).is_none());
|
||||
assert!(
|
||||
Clipboard::new(
|
||||
&Selection::File {
|
||||
file_ids: [FileId::default()].into()
|
||||
},
|
||||
&files,
|
||||
&order_of(&files),
|
||||
false
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
// a file without waypoints
|
||||
let empty = files_of(vec![File::default()]);
|
||||
let id = *empty.keys().next().unwrap();
|
||||
assert!(
|
||||
Clipboard::new(
|
||||
&Selection::Waypoints { file_id: id },
|
||||
&empty,
|
||||
&order_of(&empty),
|
||||
true
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_clipboards_are_the_same_when_they_come_from_the_same_copy() {
|
||||
let file = file("f");
|
||||
let files = files_of(vec![file.clone()]);
|
||||
let selection = tracks(&file, &[0]);
|
||||
let clipboard = Clipboard::new(&selection, &files, &order_of(&files), false).unwrap();
|
||||
assert_eq!(clipboard, clipboard.clone());
|
||||
// the same elements copied again make another clipboard
|
||||
assert_ne!(
|
||||
clipboard,
|
||||
Clipboard::new(&selection, &files, &order_of(&files), false).unwrap()
|
||||
);
|
||||
// and it is not the same once it is not cut
|
||||
let mut copy = clipboard.clone();
|
||||
copy.cut = true;
|
||||
assert_ne!(clipboard, copy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_clipboard_survives_what_happens_to_the_files() {
|
||||
let file = file("f");
|
||||
let mut files = files_of(vec![file.clone()]);
|
||||
let clipboard =
|
||||
Clipboard::new(&tracks(&file, &[0, 1]), &files, &order_of(&files), true).unwrap();
|
||||
files.clear();
|
||||
assert!(!clipboard.is_empty());
|
||||
assert_eq!(
|
||||
clipboard.content.ids(),
|
||||
ClipboardIds::Tracks(vec![file.trk[0].id, file.trk[1].id])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_where_things_can_be_pasted() {
|
||||
let file = file("f");
|
||||
let files = files_of(vec![file.clone()]);
|
||||
let selection_of = |selection: Selection| {
|
||||
Clipboard::new(&selection, &files, &order_of(&files), false).unwrap()
|
||||
};
|
||||
let file_selection = Selection::File {
|
||||
file_ids: [file.id].into(),
|
||||
};
|
||||
let track = tracks(&file, &[0]);
|
||||
let segment = Selection::TrackSegment {
|
||||
file_id: file.id,
|
||||
trk_id: file.trk[0].id,
|
||||
trkseg_ids: [file.trk[0].trkseg[0].id].into(),
|
||||
};
|
||||
let waypoints = Selection::Waypoints { file_id: file.id };
|
||||
let waypoint = Selection::Waypoint {
|
||||
file_id: file.id,
|
||||
wpt_ids: [file.wpt[0].wpt[0].id].into(),
|
||||
};
|
||||
let targets = [
|
||||
Selection::Empty,
|
||||
file_selection.clone(),
|
||||
track.clone(),
|
||||
segment.clone(),
|
||||
waypoints.clone(),
|
||||
waypoint.clone(),
|
||||
];
|
||||
let expected = [
|
||||
// nothing, file, track, segment, waypoints, waypoint
|
||||
(file_selection, [true, true, false, false, false, false]),
|
||||
(track, [true, true, true, false, false, false]),
|
||||
(segment, [true, true, true, true, false, false]),
|
||||
(waypoints, [false, true, false, false, true, true]),
|
||||
(waypoint, [false, true, false, false, true, true]),
|
||||
];
|
||||
for (copied, allowed) in expected {
|
||||
let clipboard = selection_of(copied.clone());
|
||||
for (target, allowed) in targets.iter().zip(allowed) {
|
||||
assert_eq!(
|
||||
clipboard.can_paste(target),
|
||||
allowed,
|
||||
"{copied:?} on {target:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
mod clipboard;
|
||||
mod order;
|
||||
mod selection;
|
||||
mod stack;
|
||||
|
||||
pub use clipboard::*;
|
||||
pub use order::*;
|
||||
pub use selection::*;
|
||||
pub use stack::*;
|
||||
|
||||
pub struct State<'a> {
|
||||
pub categories: &'a mut crate::TrackpointCategories,
|
||||
pub files: &'a mut StackEntry,
|
||||
pub selection: &'a mut Selection,
|
||||
pub order: &'a mut FileOrder,
|
||||
pub clipboard: &'a mut Option<Clipboard>,
|
||||
}
|
||||
@@ -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,759 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{FileId, StackEntry, TrackId, TrackSegmentId, WaypointId};
|
||||
|
||||
/// How a new selection combines with the current one.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SelectMode {
|
||||
/// The new selection replaces the current one.
|
||||
Replace,
|
||||
/// The elements are added when they are of the same kind and in the same place as the
|
||||
/// current selection (see [`Selection::extend`]), otherwise they replace it.
|
||||
Add,
|
||||
/// Like `Add`, but the elements already selected are removed instead (see
|
||||
/// [`Selection::toggle`]).
|
||||
Toggle,
|
||||
}
|
||||
|
||||
#[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 what does not exist anymore (e.g. after an undo): files, and the tracks, segments
|
||||
/// and waypoints of the files that remain. A selection left with nothing becomes empty.
|
||||
pub fn retain_existing(&mut self, files: &StackEntry) {
|
||||
let empty = match self {
|
||||
Selection::Empty => false,
|
||||
Selection::File { file_ids } => {
|
||||
file_ids.retain(|id| files.contains_key(id));
|
||||
file_ids.is_empty()
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => match files.get(file_id) {
|
||||
Some(file) => {
|
||||
trk_ids.retain(|id| file.trk.iter().any(|trk| trk.id == *id));
|
||||
trk_ids.is_empty()
|
||||
}
|
||||
None => true,
|
||||
},
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => match files
|
||||
.get(file_id)
|
||||
.and_then(|file| file.trk.iter().find(|trk| trk.id == *trk_id))
|
||||
{
|
||||
Some(trk) => {
|
||||
trkseg_ids.retain(|id| trk.trkseg.iter().any(|seg| seg.id == *id));
|
||||
trkseg_ids.is_empty()
|
||||
}
|
||||
None => true,
|
||||
},
|
||||
Selection::Waypoints { file_id } => !files.contains_key(file_id),
|
||||
Selection::Waypoint { file_id, wpt_ids } => match files.get(file_id) {
|
||||
Some(file) => {
|
||||
wpt_ids.retain(|id| {
|
||||
file.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.any(|wpt| wpt.id == *id)
|
||||
});
|
||||
wpt_ids.is_empty()
|
||||
}
|
||||
None => true,
|
||||
},
|
||||
};
|
||||
if empty {
|
||||
*self = Selection::Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds `other` to the selection: the elements are merged when they are of the same kind and
|
||||
/// in the same place (same file, same track for segments), otherwise `other` replaces the
|
||||
/// selection.
|
||||
pub fn extend(&mut self, other: Selection) {
|
||||
match (&mut *self, other) {
|
||||
(Selection::File { file_ids }, Selection::File { file_ids: other }) => {
|
||||
file_ids.extend(other)
|
||||
}
|
||||
(
|
||||
Selection::Track { file_id, trk_ids },
|
||||
Selection::Track {
|
||||
file_id: other_file,
|
||||
trk_ids: other,
|
||||
},
|
||||
) if *file_id == other_file => trk_ids.extend(other),
|
||||
(
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
},
|
||||
Selection::TrackSegment {
|
||||
file_id: other_file,
|
||||
trk_id: other_trk,
|
||||
trkseg_ids: other,
|
||||
},
|
||||
) if *file_id == other_file && *trk_id == other_trk => trkseg_ids.extend(other),
|
||||
(Selection::Waypoints { file_id }, Selection::Waypoints { file_id: other })
|
||||
if *file_id == other => {}
|
||||
(
|
||||
Selection::Waypoint { file_id, wpt_ids },
|
||||
Selection::Waypoint {
|
||||
file_id: other_file,
|
||||
wpt_ids: other,
|
||||
},
|
||||
) if *file_id == other_file => wpt_ids.extend(other),
|
||||
(selection, other) => *selection = other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`Selection::extend`], but the elements of `other` that are already selected are
|
||||
/// removed from the selection instead. A selection left with nothing is empty.
|
||||
pub fn toggle(&mut self, other: Selection) {
|
||||
fn toggle_ids<T: std::hash::Hash + Eq>(ids: &mut HashSet<T>, other: HashSet<T>) {
|
||||
for id in other {
|
||||
if !ids.remove(&id) {
|
||||
ids.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
let empty = match (&mut *self, other) {
|
||||
(Selection::File { file_ids }, Selection::File { file_ids: other }) => {
|
||||
toggle_ids(file_ids, other);
|
||||
file_ids.is_empty()
|
||||
}
|
||||
(
|
||||
Selection::Track { file_id, trk_ids },
|
||||
Selection::Track {
|
||||
file_id: other_file,
|
||||
trk_ids: other,
|
||||
},
|
||||
) if *file_id == other_file => {
|
||||
toggle_ids(trk_ids, other);
|
||||
trk_ids.is_empty()
|
||||
}
|
||||
(
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
},
|
||||
Selection::TrackSegment {
|
||||
file_id: other_file,
|
||||
trk_id: other_trk,
|
||||
trkseg_ids: other,
|
||||
},
|
||||
) if *file_id == other_file && *trk_id == other_trk => {
|
||||
toggle_ids(trkseg_ids, other);
|
||||
trkseg_ids.is_empty()
|
||||
}
|
||||
(Selection::Waypoints { file_id }, Selection::Waypoints { file_id: other })
|
||||
if *file_id == other =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(
|
||||
Selection::Waypoint { file_id, wpt_ids },
|
||||
Selection::Waypoint {
|
||||
file_id: other_file,
|
||||
wpt_ids: other,
|
||||
},
|
||||
) if *file_id == other_file => {
|
||||
toggle_ids(wpt_ids, other);
|
||||
wpt_ids.is_empty()
|
||||
}
|
||||
(selection, other) => {
|
||||
*selection = other;
|
||||
false
|
||||
}
|
||||
};
|
||||
if empty {
|
||||
*self = Selection::Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Position of the first and last of the `selected` ids among the `all` ids.
|
||||
fn selected_range<T: PartialEq>(
|
||||
all: impl Iterator<Item = T>,
|
||||
selected: &HashSet<T>,
|
||||
) -> Option<(usize, usize)>
|
||||
where
|
||||
T: std::hash::Hash + Eq,
|
||||
{
|
||||
let mut range: Option<(usize, usize)> = None;
|
||||
for (i, id) in all.enumerate() {
|
||||
if selected.contains(&id) {
|
||||
range = Some(range.map_or((i, i), |(first, _)| (first, i)));
|
||||
}
|
||||
}
|
||||
range
|
||||
}
|
||||
|
||||
/// The id before or after the selected ones among `ids`, without wrapping around.
|
||||
fn neighbour<T: Copy + PartialEq + std::hash::Hash + Eq>(
|
||||
ids: &[T],
|
||||
selected: &HashSet<T>,
|
||||
down: bool,
|
||||
) -> Option<T> {
|
||||
let (first, last) = selected_range(ids.iter().copied(), selected)?;
|
||||
if down {
|
||||
ids.get(last + 1).copied()
|
||||
} else {
|
||||
first.checked_sub(1).map(|i| ids[i])
|
||||
}
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
/// The files holding the selected elements, as a selection of files.
|
||||
pub fn to_files(&self) -> Selection {
|
||||
match self {
|
||||
Selection::Empty => Selection::Empty,
|
||||
Selection::File { .. } => self.clone(),
|
||||
Selection::Track { file_id, .. }
|
||||
| Selection::TrackSegment { file_id, .. }
|
||||
| Selection::Waypoints { file_id }
|
||||
| Selection::Waypoint { file_id, .. } => Selection::File {
|
||||
file_ids: [*file_id].into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects all the elements of the same kind as the selected ones, in the same place: all
|
||||
/// the files, all the tracks of the file, all the segments of the track, all the waypoints
|
||||
/// of the file. All the files when nothing is selected. `None` when there is nothing to
|
||||
/// select (the waypoints node, or a file that does not exist).
|
||||
pub fn all_at_level(&self, files: &StackEntry, order: &[FileId]) -> Option<Selection> {
|
||||
match self {
|
||||
Selection::Empty | Selection::File { .. } => Some(Selection::File {
|
||||
file_ids: order.iter().copied().collect(),
|
||||
}),
|
||||
Selection::Track { file_id, .. } => Some(Selection::Track {
|
||||
file_id: *file_id,
|
||||
trk_ids: files.get(file_id)?.trk.iter().map(|trk| trk.id).collect(),
|
||||
}),
|
||||
Selection::TrackSegment {
|
||||
file_id, trk_id, ..
|
||||
} => {
|
||||
let trk = files
|
||||
.get(file_id)?
|
||||
.trk
|
||||
.iter()
|
||||
.find(|trk| trk.id == *trk_id)?;
|
||||
Some(Selection::TrackSegment {
|
||||
file_id: *file_id,
|
||||
trk_id: *trk_id,
|
||||
trkseg_ids: trk.trkseg.iter().map(|seg| seg.id).collect(),
|
||||
})
|
||||
}
|
||||
Selection::Waypoints { .. } => None,
|
||||
Selection::Waypoint { file_id, .. } => Some(Selection::Waypoint {
|
||||
file_id: *file_id,
|
||||
wpt_ids: files
|
||||
.get(file_id)?
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.map(|wpt| wpt.id)
|
||||
.collect(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// The element next to the selected ones, as a selection of that one element: the next one
|
||||
/// (`down`) or the previous one among the elements of the same kind, in the same place.
|
||||
/// The tracks, segments and waypoints stop at their ends, the files wrap around and skip
|
||||
/// the ones that are already selected. `None` when there is no such element.
|
||||
pub fn neighbour(&self, files: &StackEntry, order: &[FileId], down: bool) -> Option<Selection> {
|
||||
match self {
|
||||
Selection::Empty | Selection::Waypoints { .. } => None,
|
||||
Selection::File { file_ids } => {
|
||||
let (first, last) = selected_range(order.iter(), &file_ids.iter().collect())?;
|
||||
let limit = if down { last } else { first };
|
||||
let n = order.len();
|
||||
let mut i = limit;
|
||||
loop {
|
||||
i = if down { (i + 1) % n } else { (i + n - 1) % n };
|
||||
if i == limit {
|
||||
return None;
|
||||
}
|
||||
if !file_ids.contains(&order[i]) {
|
||||
return Some(Selection::File {
|
||||
file_ids: [order[i]].into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let ids: Vec<TrackId> = files.get(file_id)?.trk.iter().map(|t| t.id).collect();
|
||||
Some(Selection::Track {
|
||||
file_id: *file_id,
|
||||
trk_ids: [neighbour(&ids, trk_ids, down)?].into(),
|
||||
})
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => {
|
||||
let trk = files
|
||||
.get(file_id)?
|
||||
.trk
|
||||
.iter()
|
||||
.find(|trk| trk.id == *trk_id)?;
|
||||
let ids: Vec<TrackSegmentId> = trk.trkseg.iter().map(|seg| seg.id).collect();
|
||||
Some(Selection::TrackSegment {
|
||||
file_id: *file_id,
|
||||
trk_id: *trk_id,
|
||||
trkseg_ids: [neighbour(&ids, trkseg_ids, down)?].into(),
|
||||
})
|
||||
}
|
||||
Selection::Waypoint { file_id, wpt_ids } => {
|
||||
let ids: Vec<WaypointId> = files
|
||||
.get(file_id)?
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.map(|wpt| wpt.id)
|
||||
.collect();
|
||||
Some(Selection::Waypoint {
|
||||
file_id: *file_id,
|
||||
wpt_ids: [neighbour(&ids, wpt_ids, down)?].into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{File, Track, TrackSegment, Waypoint, WaypointChunk};
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retain_existing_elements() {
|
||||
let mut file = File::default();
|
||||
file.trk.push(Track {
|
||||
trkseg: vec![TrackSegment::default()],
|
||||
..Default::default()
|
||||
});
|
||||
file.wpt.push(Rc::new(WaypointChunk {
|
||||
wpt: vec![Waypoint::default()],
|
||||
..Default::default()
|
||||
}));
|
||||
let (file_id, trk_id) = (file.id, file.trk[0].id);
|
||||
let (seg_id, wpt_id) = (file.trk[0].trkseg[0].id, file.wpt[0].wpt[0].id);
|
||||
let mut files = StackEntry::default();
|
||||
files.insert(file_id, Rc::new(file));
|
||||
|
||||
let mut selection = Selection::Track {
|
||||
file_id,
|
||||
trk_ids: [trk_id, TrackId::default()].into(),
|
||||
};
|
||||
selection.retain_existing(&files);
|
||||
assert_eq!(
|
||||
selection,
|
||||
Selection::Track {
|
||||
file_id,
|
||||
trk_ids: [trk_id].into()
|
||||
}
|
||||
);
|
||||
|
||||
let mut selection = Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: [seg_id, TrackSegmentId::default()].into(),
|
||||
};
|
||||
selection.retain_existing(&files);
|
||||
assert!(
|
||||
matches!(&selection, Selection::TrackSegment { trkseg_ids, .. } if trkseg_ids.len() == 1)
|
||||
);
|
||||
// a segment under the wrong track
|
||||
let mut selection = Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id: TrackId::default(),
|
||||
trkseg_ids: [seg_id].into(),
|
||||
};
|
||||
selection.retain_existing(&files);
|
||||
assert_eq!(selection, Selection::Empty);
|
||||
|
||||
let mut selection = Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: [wpt_id, WaypointId::default()].into(),
|
||||
};
|
||||
selection.retain_existing(&files);
|
||||
assert!(matches!(&selection, Selection::Waypoint { wpt_ids, .. } if wpt_ids.len() == 1));
|
||||
let mut selection = Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: [WaypointId::default()].into(),
|
||||
};
|
||||
selection.retain_existing(&files);
|
||||
assert_eq!(selection, Selection::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend() {
|
||||
let file_id = FileId::default();
|
||||
let (a, b) = (TrackId::default(), TrackId::default());
|
||||
let tracks = |ids: &[TrackId]| Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
|
||||
// same kind and place: merged
|
||||
let mut selection = tracks(&[a]);
|
||||
selection.extend(tracks(&[b]));
|
||||
assert_eq!(selection, tracks(&[a, b]));
|
||||
|
||||
// another file, or another kind: replaced
|
||||
let mut selection = tracks(&[a]);
|
||||
let other = Selection::Track {
|
||||
file_id: FileId::default(),
|
||||
trk_ids: [b].into(),
|
||||
};
|
||||
selection.extend(other.clone());
|
||||
assert_eq!(selection, other);
|
||||
selection.extend(Selection::Waypoints { file_id });
|
||||
assert_eq!(selection, Selection::Waypoints { file_id });
|
||||
|
||||
// segments of another track: replaced
|
||||
let seg = TrackSegmentId::default();
|
||||
let segments = |trk_id| Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: [seg].into(),
|
||||
};
|
||||
let mut selection = segments(a);
|
||||
selection.extend(segments(b));
|
||||
assert_eq!(selection, segments(b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle() {
|
||||
let file_id = FileId::default();
|
||||
let (a, b) = (TrackId::default(), TrackId::default());
|
||||
let tracks = |ids: &[TrackId]| Selection::Track {
|
||||
file_id,
|
||||
trk_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
|
||||
// same kind and place: toggled one by one
|
||||
let mut selection = tracks(&[a]);
|
||||
selection.toggle(tracks(&[b]));
|
||||
assert_eq!(selection, tracks(&[a, b]));
|
||||
selection.toggle(tracks(&[a]));
|
||||
assert_eq!(selection, tracks(&[b]));
|
||||
selection.toggle(tracks(&[a, b]));
|
||||
assert_eq!(selection, tracks(&[a]));
|
||||
|
||||
// nothing left: empty
|
||||
selection.toggle(tracks(&[a]));
|
||||
assert_eq!(selection, Selection::Empty);
|
||||
|
||||
// another file, or another kind: replaced
|
||||
let mut selection = tracks(&[a]);
|
||||
selection.toggle(Selection::Waypoints { file_id });
|
||||
assert_eq!(selection, Selection::Waypoints { file_id });
|
||||
// the waypoints node toggles itself off
|
||||
selection.toggle(Selection::Waypoints { file_id });
|
||||
assert_eq!(selection, Selection::Empty);
|
||||
|
||||
// files
|
||||
let (f1, f2) = (FileId::default(), FileId::default());
|
||||
let files = |ids: &[FileId]| Selection::File {
|
||||
file_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
let mut selection = files(&[f1]);
|
||||
selection.toggle(files(&[f2]));
|
||||
assert_eq!(selection, files(&[f1, f2]));
|
||||
selection.toggle(files(&[f1]));
|
||||
assert_eq!(selection, files(&[f2]));
|
||||
}
|
||||
|
||||
/// A file with 3 tracks of 2 segments each and 3 waypoints, with the ids of everything.
|
||||
struct Tree {
|
||||
files: StackEntry,
|
||||
order: Vec<FileId>,
|
||||
file: FileId,
|
||||
tracks: Vec<TrackId>,
|
||||
segments: Vec<Vec<TrackSegmentId>>,
|
||||
waypoints: Vec<WaypointId>,
|
||||
}
|
||||
|
||||
fn tree() -> Tree {
|
||||
let mut file = File::default();
|
||||
for _ in 0..3 {
|
||||
file.trk.push(Track {
|
||||
trkseg: vec![TrackSegment::default(), TrackSegment::default()],
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
file.wpt.push(Rc::new(WaypointChunk {
|
||||
wpt: vec![Waypoint::default(), Waypoint::default()],
|
||||
..Default::default()
|
||||
}));
|
||||
file.wpt.push(Rc::new(WaypointChunk {
|
||||
wpt: vec![Waypoint::default()],
|
||||
..Default::default()
|
||||
}));
|
||||
let tree = Tree {
|
||||
file: file.id,
|
||||
tracks: file.trk.iter().map(|t| t.id).collect(),
|
||||
segments: file
|
||||
.trk
|
||||
.iter()
|
||||
.map(|t| t.trkseg.iter().map(|s| s.id).collect())
|
||||
.collect(),
|
||||
waypoints: file
|
||||
.wpt
|
||||
.iter()
|
||||
.flat_map(|chunk| &chunk.wpt)
|
||||
.map(|w| w.id)
|
||||
.collect(),
|
||||
order: vec![file.id],
|
||||
files: StackEntry::from([(file.id, Rc::new(file))]),
|
||||
};
|
||||
tree
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_at_level() {
|
||||
let t = tree();
|
||||
let other = FileId::default();
|
||||
let order = [other, t.file];
|
||||
let all = |selection: &Selection| selection.all_at_level(&t.files, &order);
|
||||
|
||||
// the files, in the order
|
||||
let all_files = Selection::File {
|
||||
file_ids: [other, t.file].into(),
|
||||
};
|
||||
assert_eq!(all(&Selection::Empty), Some(all_files.clone()));
|
||||
assert_eq!(
|
||||
all(&Selection::File {
|
||||
file_ids: [t.file].into()
|
||||
}),
|
||||
Some(all_files)
|
||||
);
|
||||
|
||||
// the siblings of the selected elements
|
||||
assert_eq!(
|
||||
all(&Selection::Track {
|
||||
file_id: t.file,
|
||||
trk_ids: [t.tracks[1]].into()
|
||||
}),
|
||||
Some(Selection::Track {
|
||||
file_id: t.file,
|
||||
trk_ids: t.tracks.iter().copied().collect()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
all(&Selection::TrackSegment {
|
||||
file_id: t.file,
|
||||
trk_id: t.tracks[2],
|
||||
trkseg_ids: [t.segments[2][0]].into()
|
||||
}),
|
||||
Some(Selection::TrackSegment {
|
||||
file_id: t.file,
|
||||
trk_id: t.tracks[2],
|
||||
trkseg_ids: t.segments[2].iter().copied().collect()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
all(&Selection::Waypoint {
|
||||
file_id: t.file,
|
||||
wpt_ids: [t.waypoints[0]].into()
|
||||
}),
|
||||
Some(Selection::Waypoint {
|
||||
file_id: t.file,
|
||||
wpt_ids: t.waypoints.iter().copied().collect()
|
||||
})
|
||||
);
|
||||
|
||||
// nothing to select
|
||||
assert_eq!(all(&Selection::Waypoints { file_id: t.file }), None);
|
||||
let unknown = FileId::default();
|
||||
assert_eq!(
|
||||
all(&Selection::Track {
|
||||
file_id: unknown,
|
||||
trk_ids: HashSet::new()
|
||||
}),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neighbour_of_tracks_stops_at_the_ends() {
|
||||
let t = tree();
|
||||
let tracks = |ids: &[TrackId]| Selection::Track {
|
||||
file_id: t.file,
|
||||
trk_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
let next = |selection: &Selection, down| selection.neighbour(&t.files, &t.order, down);
|
||||
|
||||
assert_eq!(
|
||||
next(&tracks(&[t.tracks[1]]), true),
|
||||
Some(tracks(&[t.tracks[2]]))
|
||||
);
|
||||
assert_eq!(
|
||||
next(&tracks(&[t.tracks[1]]), false),
|
||||
Some(tracks(&[t.tracks[0]]))
|
||||
);
|
||||
assert_eq!(next(&tracks(&[t.tracks[2]]), true), None);
|
||||
assert_eq!(next(&tracks(&[t.tracks[0]]), false), None);
|
||||
// from the end of the selected range, whatever its holes
|
||||
let range = tracks(&[t.tracks[0], t.tracks[1]]);
|
||||
assert_eq!(next(&range, true), Some(tracks(&[t.tracks[2]])));
|
||||
assert_eq!(next(&range, false), None);
|
||||
let holes = tracks(&[t.tracks[0], t.tracks[2]]);
|
||||
assert_eq!(next(&holes, true), None);
|
||||
assert_eq!(next(&holes, false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neighbour_of_segments_and_waypoints() {
|
||||
let t = tree();
|
||||
let segments = |trk: usize, seg: usize| Selection::TrackSegment {
|
||||
file_id: t.file,
|
||||
trk_id: t.tracks[trk],
|
||||
trkseg_ids: [t.segments[trk][seg]].into(),
|
||||
};
|
||||
let next = |selection: &Selection, down| selection.neighbour(&t.files, &t.order, down);
|
||||
// the segments stay in their track
|
||||
assert_eq!(next(&segments(1, 0), true), Some(segments(1, 1)));
|
||||
assert_eq!(next(&segments(1, 1), true), None);
|
||||
assert_eq!(next(&segments(1, 1), false), Some(segments(1, 0)));
|
||||
assert_eq!(next(&segments(1, 0), false), None);
|
||||
|
||||
// the waypoints are in the order of the file, across its chunks
|
||||
let waypoint = |i: usize| Selection::Waypoint {
|
||||
file_id: t.file,
|
||||
wpt_ids: [t.waypoints[i]].into(),
|
||||
};
|
||||
assert_eq!(next(&waypoint(1), true), Some(waypoint(2)));
|
||||
assert_eq!(next(&waypoint(2), true), None);
|
||||
assert_eq!(next(&waypoint(0), false), None);
|
||||
assert_eq!(next(&waypoint(2), false), Some(waypoint(1)));
|
||||
|
||||
assert_eq!(next(&Selection::Waypoints { file_id: t.file }, true), None);
|
||||
assert_eq!(next(&Selection::Empty, true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_neighbour_of_files_wraps_around_and_skips_the_selected() {
|
||||
let files = StackEntry::default();
|
||||
let [a, b, c] = [FileId::default(), FileId::default(), FileId::default()];
|
||||
let order = [a, b, c];
|
||||
let select = |ids: &[FileId]| Selection::File {
|
||||
file_ids: ids.iter().copied().collect(),
|
||||
};
|
||||
let next = |ids: &[FileId], down| select(ids).neighbour(&files, &order, down);
|
||||
|
||||
assert_eq!(next(&[a], true), Some(select(&[b])));
|
||||
assert_eq!(next(&[c], true), Some(select(&[a])));
|
||||
assert_eq!(next(&[a], false), Some(select(&[c])));
|
||||
assert_eq!(next(&[b], false), Some(select(&[a])));
|
||||
// from the end of the selection, skipping the files already selected
|
||||
assert_eq!(next(&[a, b], true), Some(select(&[c])));
|
||||
assert_eq!(next(&[a, b], false), Some(select(&[c])));
|
||||
assert_eq!(next(&[c, a], true), Some(select(&[b])));
|
||||
// everything is selected
|
||||
assert_eq!(next(&[a, b, c], true), None);
|
||||
// none of them is in the order
|
||||
assert_eq!(next(&[FileId::default()], true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_files() {
|
||||
let (file_id, trk_id) = (FileId::default(), TrackId::default());
|
||||
let file = Selection::File {
|
||||
file_ids: [file_id].into(),
|
||||
};
|
||||
assert_eq!(Selection::Empty.to_files(), Selection::Empty);
|
||||
assert_eq!(file.to_files(), file);
|
||||
assert_eq!(
|
||||
Selection::Track {
|
||||
file_id,
|
||||
trk_ids: [trk_id].into()
|
||||
}
|
||||
.to_files(),
|
||||
file
|
||||
);
|
||||
assert_eq!(
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids: HashSet::new()
|
||||
}
|
||||
.to_files(),
|
||||
file
|
||||
);
|
||||
assert_eq!(Selection::Waypoints { file_id }.to_files(), file);
|
||||
assert_eq!(
|
||||
Selection::Waypoint {
|
||||
file_id,
|
||||
wpt_ids: HashSet::new()
|
||||
}
|
||||
.to_files(),
|
||||
file
|
||||
);
|
||||
let several = Selection::File {
|
||||
file_ids: [file_id, FileId::default()].into(),
|
||||
};
|
||||
assert_eq!(several.to_files(), several);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
File diff suppressed because it is too large
Load Diff
Generated
+21
-1
@@ -17,6 +17,7 @@
|
||||
"dexie": "^4.0.11",
|
||||
"file-saver": "^2.0.5",
|
||||
"gpx": "file:../gpx",
|
||||
"gpx-rs": "file:../gpx-rs/pkg",
|
||||
"immer": "^10.1.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mapillary-js": "^4.1.2",
|
||||
@@ -70,7 +71,8 @@
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vaul-svelte": "^1.0.0-next.7",
|
||||
"vite": "^6.3.5"
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-wasm": "^3.6.0"
|
||||
}
|
||||
},
|
||||
"../gpx": {
|
||||
@@ -89,6 +91,10 @@
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
},
|
||||
"../gpx-rs/pkg": {
|
||||
"name": "gpx-rs",
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"../gpx/node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"dev": true,
|
||||
@@ -5492,6 +5498,10 @@
|
||||
"resolved": "../gpx",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/gpx-rs": {
|
||||
"resolved": "../gpx-rs/pkg",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"dev": true,
|
||||
@@ -7716,6 +7726,16 @@
|
||||
"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": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"postbuild": "npx tsx src/lib/scripts/sitemap.ts",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"test": "node --import tsx --test \"src/**/*.test.ts\"",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore && eslint .",
|
||||
"format": "prettier --write . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore"
|
||||
@@ -59,7 +60,8 @@
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vaul-svelte": "^1.0.0-next.7",
|
||||
"vite": "^6.3.5"
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-wasm": "^3.6.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
@@ -72,6 +74,7 @@
|
||||
"dexie": "^4.0.11",
|
||||
"file-saver": "^2.0.5",
|
||||
"gpx": "file:../gpx",
|
||||
"gpx-rs": "file:../gpx-rs/pkg",
|
||||
"immer": "^10.1.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mapillary-js": "^4.1.2",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { readable } from 'svelte/store';
|
||||
import { engine, FileStateCollectionObserver, type FileState, type Selection } from '$lib/engine';
|
||||
import { isSelectionHidden } from '$lib/selection-helpers';
|
||||
|
||||
/**
|
||||
* Whether all the selected elements are hidden. It follows the selection and, through the
|
||||
* observer of the files, the state of every file (hiding, editing).
|
||||
*/
|
||||
export const allHidden = readable(false, (set) => {
|
||||
let selection: Selection = { type: 'empty' };
|
||||
const states = new Map<string, FileState>();
|
||||
const unsubscribes = new Map<string, () => void>();
|
||||
|
||||
const update = () => set(isSelectionHidden(selection, states));
|
||||
|
||||
const unsubscribeSelection = engine.selection.subscribe(($selection) => {
|
||||
selection = $selection;
|
||||
update();
|
||||
});
|
||||
const observer = new FileStateCollectionObserver(
|
||||
(files) => {
|
||||
files.forEach((store, fileId) => {
|
||||
unsubscribes.set(
|
||||
fileId,
|
||||
store.subscribe(($file) => {
|
||||
states.set(fileId, $file);
|
||||
update();
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
(fileId) => {
|
||||
unsubscribes.get(fileId)?.();
|
||||
unsubscribes.delete(fileId);
|
||||
states.delete(fileId);
|
||||
update();
|
||||
},
|
||||
() => {
|
||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
unsubscribes.clear();
|
||||
states.clear();
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribeSelection();
|
||||
observer.destroy();
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { closestPointIndex } from './closest-point';
|
||||
|
||||
describe('closestPointIndex', () => {
|
||||
const line = [0, 0, 1, 0, 2, 0, 2, 1];
|
||||
|
||||
it('has no point on an empty polyline', () => {
|
||||
assert.equal(closestPointIndex([], { lng: 0, lat: 0 }), undefined);
|
||||
});
|
||||
|
||||
it('is the only point of a polyline of one point', () => {
|
||||
assert.equal(closestPointIndex([3, 4], { lng: 0, lat: 0 }), 0);
|
||||
});
|
||||
|
||||
it('is the nearest end of the nearest segment', () => {
|
||||
assert.equal(closestPointIndex(line, { lng: 0.2, lat: 0.1 }), 0);
|
||||
assert.equal(closestPointIndex(line, { lng: 0.8, lat: 0.1 }), 1);
|
||||
assert.equal(closestPointIndex(line, { lng: 1.6, lat: -0.1 }), 2);
|
||||
assert.equal(closestPointIndex(line, { lng: 2.1, lat: 0.9 }), 3);
|
||||
});
|
||||
|
||||
it('uses the distance to the segment, not to its ends', () => {
|
||||
// far from every point but right next to the long segment
|
||||
const coordinates = [0, 0, 10, 0, 10, 5];
|
||||
assert.equal(closestPointIndex(coordinates, { lng: 7, lat: 0.01 }), 1);
|
||||
assert.equal(closestPointIndex(coordinates, { lng: 3, lat: 0.01 }), 0);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user