mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-27 10:37:17 +00:00
Compare commits
85
Commits
wasm
..
45f877ec78
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45f877ec78 | ||
|
|
0f957fb1f2 | ||
|
|
764d0b233a | ||
|
|
b6c0f5190a | ||
|
|
c842da8dc1 | ||
|
|
5bc35f30d9 | ||
|
|
873e2e1654 | ||
|
|
6c2d2879f4 | ||
|
|
f5670cdd8e | ||
|
|
741d35db1e | ||
|
|
6556c29492 | ||
|
|
7fa850e4c8 | ||
|
|
f9e8d4c644 | ||
|
|
a07da16163 | ||
|
|
4583874e7e | ||
|
|
48325e6016 | ||
|
|
9033401094 | ||
|
|
b64c192c4f | ||
|
|
9dd4af98a5 | ||
|
|
05510ece92 | ||
|
|
8bbcbdc097 | ||
|
|
3012f6bf73 | ||
|
|
5373d0f0d4 | ||
|
|
8fd47d8b18 | ||
|
|
a5c1f6ad33 | ||
|
|
3ef2bf135a | ||
|
|
adbf89e318 | ||
|
|
11709316d2 | ||
|
|
d52b61f707 | ||
|
|
358253b7a9 | ||
|
|
8d6885d58b | ||
|
|
128338aa05 | ||
|
|
c84e1c9a06 | ||
|
|
9f49119eec | ||
|
|
67a47cf065 | ||
|
|
e57949dc01 | ||
|
|
da92fd15d2 | ||
|
|
01443943b3 | ||
|
|
970cf7d1e1 | ||
|
|
593b43e433 | ||
|
|
8fa88680b3 | ||
|
|
b2495d1466 | ||
|
|
ed90ba6a1f | ||
|
|
b0a3c3a9ba | ||
|
|
005cc7b305 | ||
|
|
1bfd319758 | ||
|
|
187296a277 | ||
|
|
55a8b020e0 | ||
|
|
1ba5436af3 | ||
|
|
2e1ca00afa | ||
|
|
97d597e139 | ||
|
|
41b9116c6a | ||
|
|
6a0d434371 | ||
|
|
b319440a57 | ||
|
|
b0d78b5f3c | ||
|
|
2e8b2d6f30 | ||
|
|
5259d007c9 | ||
|
|
cb36725a46 | ||
|
|
deef1bab47 | ||
|
|
108880f8cd | ||
|
|
37a659831b | ||
|
|
fde6bafc10 | ||
|
|
4537ea4730 | ||
|
|
1954bbd764 | ||
|
|
9d2da80893 | ||
|
|
043d368c74 | ||
|
|
bb11d3375b | ||
|
|
bfe5961e27 | ||
|
|
17559913fd | ||
|
|
55590d68a6 | ||
|
|
8e9f16c460 | ||
|
|
c60b64f24f | ||
|
|
fdb6fe4e52 | ||
|
|
0bf168e67e | ||
|
|
d762a45eb9 | ||
|
|
548ab9a459 | ||
|
|
315c1f6a61 | ||
|
|
98257bee12 | ||
|
|
5561b7d9fe | ||
|
|
320887206c | ||
|
|
04a1bf6a55 | ||
|
|
0989371874 | ||
|
|
3dcd6e52d3 | ||
|
|
0f06d0d461 | ||
|
|
db33310a10 |
No files matched your search
@@ -15,19 +15,31 @@ jobs:
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: |
|
||||
gpx/package-lock.json
|
||||
website/package-lock.json
|
||||
cache-dependency-path: website/package-lock.json
|
||||
|
||||
- name: Install dependencies for gpx
|
||||
run: npm install --prefix gpx
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
|
||||
- name: Build gpx
|
||||
run: npm run build --prefix gpx
|
||||
- name: Install wasm-pack
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: wasm-pack
|
||||
|
||||
- name: Test gpx-rs
|
||||
run: cargo test
|
||||
|
||||
- name: Build wasm
|
||||
run: wasm-pack build --target bundler
|
||||
working-directory: wasm
|
||||
|
||||
- name: Install dependencies for website
|
||||
run: npm install --prefix website
|
||||
|
||||
- name: Test website
|
||||
run: npm test --prefix website
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
touch website/.env
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
.DS_Store
|
||||
target/
|
||||
Generated
+5330
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,5 @@
|
||||
[workspace]
|
||||
members = ["gpx-rs", "wasm", "tauri"]
|
||||
# `cargo test` at the root only tests the engine: the others need their own tooling
|
||||
default-members = ["gpx-rs"]
|
||||
resolver = "3"
|
||||
@@ -25,18 +25,20 @@ Any help is greatly appreciated!
|
||||
|
||||
## Development
|
||||
|
||||
The code is split into two parts:
|
||||
The code is split into four parts:
|
||||
|
||||
- `gpx-rs`: a Rust library, compiled to WebAssembly, for parsing and manipulating GPX files,
|
||||
- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application.
|
||||
- `gpx-rs`: a Rust library for parsing and manipulating GPX files (the engine), and the interface to it that the next two share (`gpx_engine::api`),
|
||||
- `wasm`: the engine for the browser, compiled to WebAssembly,
|
||||
- `tauri`: the desktop app, which runs the engine natively,
|
||||
- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application. It also is the UI of the desktop app.
|
||||
|
||||
You will need [Node.js](https://nodejs.org/), [Rust](https://rustup.rs/) and [wasm-pack](https://rustwasm.github.io/wasm-pack/) to build and run these two parts.
|
||||
You will need [Node.js](https://nodejs.org/), [Rust](https://rustup.rs/) and [wasm-pack](https://rustwasm.github.io/wasm-pack/) to build and run the website. See [`tauri`](tauri/README.md) for the desktop app.
|
||||
|
||||
### Building the `gpx-rs` library
|
||||
### Building the WebAssembly engine
|
||||
|
||||
```bash
|
||||
cd gpx-rs
|
||||
wasm-pack build wasm --target bundler --out-dir ../pkg
|
||||
cd wasm
|
||||
wasm-pack build --target bundler
|
||||
```
|
||||
|
||||
### Running the website
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
target/
|
||||
pkg/
|
||||
Generated
-794
@@ -1,794 +0,0 @@
|
||||
# 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 = "async-trait"
|
||||
version = "0.1.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-polyfill"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
|
||||
dependencies = [
|
||||
"critical-section",
|
||||
]
|
||||
|
||||
[[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 = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[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 = "cobs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[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 = "critical-section"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[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",
|
||||
"postcard",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpx-rs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"gpx-engine",
|
||||
"idb",
|
||||
"js-sys",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-bindgen-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hash32"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heapless"
|
||||
version = "0.7.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
|
||||
dependencies = [
|
||||
"atomic-polyfill",
|
||||
"hash32",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
"spin",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[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 = "idb"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6554f394e990a1af530a528a7fdcad6e01b29cb1b990f89df3ffd62cf15f7828"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[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 = "libm"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[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 = "minicov"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
|
||||
dependencies = [
|
||||
"cobs",
|
||||
"embedded-io 0.4.0",
|
||||
"embedded-io 0.6.1",
|
||||
"heapless",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[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 = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[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 = "spin"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[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 = "thiserror"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[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",
|
||||
"serde_core",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[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-futures"
|
||||
version = "0.4.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"tokio",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[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 = "wasm-bindgen-test"
|
||||
version = "0.3.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae7499dfd45780a0a91d7ee6bb9ac51970a4479a41a89da443fdda5a39547d42"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"cast",
|
||||
"js-sys",
|
||||
"libm",
|
||||
"minicov",
|
||||
"nu-ansi-term",
|
||||
"num-traits",
|
||||
"oorandom",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-bindgen-test-macro",
|
||||
"wasm-bindgen-test-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b84b5ac638bfb168196a1a461fcc8f46a294a18b1b6be52133b4e0db122cc9f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-shared"
|
||||
version = "0.2.129"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f692aa943ccd88363733b77063f32cfed5bc6cbea8e6e8b251b302f881606fe"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
+10
-5
@@ -1,6 +1,11 @@
|
||||
[workspace]
|
||||
members = ["engine", "wasm"]
|
||||
resolver = "3"
|
||||
[package]
|
||||
name = "gpx-engine"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
[dependencies]
|
||||
chrono = "0.4.45"
|
||||
postcard = { version = "1.1.3", features = ["use-std"] }
|
||||
quick-xml = "0.42.0"
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
uuid = { version = "1.26.1", features = ["serde", "v4"] }
|
||||
+7
-11
@@ -1,14 +1,10 @@
|
||||
# gpx-rs
|
||||
|
||||
- `engine`: the GPX engine, plain Rust. Tested natively: `cargo test`.
|
||||
- `wasm`: the interface to the web page, compiled to WebAssembly.
|
||||
The GPX engine: plain Rust, no knowledge of who uses it. Tested natively with `cargo test` (from
|
||||
here or from the root).
|
||||
|
||||
```
|
||||
wasm-pack build wasm --target bundler --out-dir ../pkg
|
||||
```
|
||||
|
||||
The interface is tested in Node (it needs `wasm-pack`, which fetches the test runner the first time):
|
||||
|
||||
```
|
||||
wasm-pack test --node wasm
|
||||
```
|
||||
- `src/core`, `src/engine`, `src/storage`: the engine itself.
|
||||
- `src/api`: the interface to the engine that the frontends (`../wasm`, `../tauri`) share. Each
|
||||
function takes the engine and typed arguments, and gives plain data (`api::dto`, serde types, or
|
||||
slices for the numeric buffers). A frontend only converts what crosses: ids from strings or
|
||||
bytes, results to JS objects or JSON, slices to typed arrays or raw bytes.
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,11 +0,0 @@
|
||||
[package]
|
||||
name = "gpx-engine"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.45"
|
||||
postcard = { version = "1.1.3", features = ["use-std"] }
|
||||
quick-xml = "0.42.0"
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
uuid = { version = "1.26.1", features = ["serde", "v4"] }
|
||||
@@ -1,31 +0,0 @@
|
||||
use crate::{Link, Track, Waypoints};
|
||||
|
||||
use super::common::uuid_id;
|
||||
|
||||
uuid_id!(FileId);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct File {
|
||||
pub id: FileId,
|
||||
pub info: FileInfo,
|
||||
pub trk: Vec<Track>,
|
||||
pub wpt: Waypoints,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct FileInfo {
|
||||
pub name: String,
|
||||
pub desc: Option<String>,
|
||||
pub author: Option<Author>,
|
||||
pub links: Vec<Link>,
|
||||
pub time: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Author {
|
||||
pub name: Option<String>,
|
||||
/// `id@domain`, which the file gives as the two attributes of an `email` element.
|
||||
pub email: Option<String>,
|
||||
/// The author has a single link, unlike the other elements.
|
||||
pub link: Option<Link>,
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mod core;
|
||||
mod engine;
|
||||
mod storage;
|
||||
|
||||
pub use core::*;
|
||||
pub use engine::*;
|
||||
pub use storage::*;
|
||||
@@ -0,0 +1,672 @@
|
||||
//! The functions that edit the files or change the selection.
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::dto::*;
|
||||
use crate::{self as engine, Action, Command, Engine, History, LngLat, LngLatBounds};
|
||||
|
||||
/// Executes an action and tells what it did.
|
||||
pub fn execute<H: History>(engine: &mut Engine<H>, action: Action) -> Outcome {
|
||||
match engine.execute(action) {
|
||||
Ok(outcome) => Outcome::new(&outcome, None),
|
||||
Err(error) => Outcome::failed(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit<H: History>(engine: &mut Engine<H>, command: Command) -> Outcome {
|
||||
execute(engine, Action::Edit(command))
|
||||
}
|
||||
|
||||
fn file_ids(ids: &[Uuid]) -> Vec<engine::FileId> {
|
||||
ids.iter().copied().map(engine::FileId).collect()
|
||||
}
|
||||
|
||||
// Selection
|
||||
|
||||
/// Selects files. Unknown files are ignored, and selecting nothing deselects everything.
|
||||
pub fn select<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
file_ids_: &[Uuid],
|
||||
mode: engine::SelectMode,
|
||||
) -> Outcome {
|
||||
select_elements(
|
||||
engine,
|
||||
engine::Selection::File {
|
||||
file_ids: file_ids(file_ids_).into_iter().collect(),
|
||||
},
|
||||
mode,
|
||||
)
|
||||
}
|
||||
|
||||
/// Selects elements: they are merged with the selection when it holds elements of the same kind in
|
||||
/// the same place (same file, same track for segments), otherwise they replace it.
|
||||
pub fn select_elements<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
selection: engine::Selection,
|
||||
mode: engine::SelectMode,
|
||||
) -> Outcome {
|
||||
execute(engine, Action::Select { selection, mode })
|
||||
}
|
||||
|
||||
/// Selects all the elements of the same kind as the selected ones, in the same place: all the
|
||||
/// files, the routes or the tracks of the file, the segments of the track, the waypoints of the file. All the
|
||||
/// files when nothing is selected.
|
||||
pub fn select_all<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
execute(engine, Action::SelectAll)
|
||||
}
|
||||
|
||||
/// Moves the selection to the next (`down`) or previous element of the same kind, like the arrow
|
||||
/// keys do in the file list. With `add` (shift + arrow), the element is added to the selection,
|
||||
/// otherwise it replaces it.
|
||||
pub fn arrow_select<H: History>(engine: &mut Engine<H>, down: bool, add: bool) -> Outcome {
|
||||
execute(engine, Action::ArrowSelect { down, add })
|
||||
}
|
||||
|
||||
pub fn select_routes<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
file_id: Uuid,
|
||||
route_ids: &[Uuid],
|
||||
mode: engine::SelectMode,
|
||||
) -> Outcome {
|
||||
let selection = engine::Selection::Route {
|
||||
file_id: engine::FileId(file_id),
|
||||
rte_ids: route_ids.iter().copied().map(engine::RouteId).collect(),
|
||||
};
|
||||
select_elements(engine, selection, mode)
|
||||
}
|
||||
|
||||
pub fn select_tracks<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
file_id: Uuid,
|
||||
track_ids: &[Uuid],
|
||||
mode: engine::SelectMode,
|
||||
) -> Outcome {
|
||||
let selection = engine::Selection::Track {
|
||||
file_id: engine::FileId(file_id),
|
||||
trk_ids: track_ids.iter().copied().map(engine::TrackId).collect(),
|
||||
};
|
||||
select_elements(engine, selection, mode)
|
||||
}
|
||||
|
||||
pub fn select_segments<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
file_id: Uuid,
|
||||
track_id: Uuid,
|
||||
segment_ids: &[Uuid],
|
||||
mode: engine::SelectMode,
|
||||
) -> Outcome {
|
||||
let selection = engine::Selection::TrackSegment {
|
||||
file_id: engine::FileId(file_id),
|
||||
trk_id: engine::TrackId(track_id),
|
||||
trkseg_ids: segment_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.map(engine::TrackSegmentId)
|
||||
.collect(),
|
||||
};
|
||||
select_elements(engine, selection, mode)
|
||||
}
|
||||
|
||||
/// Selects the node standing for all the waypoints of a file.
|
||||
pub fn select_waypoint_group<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
file_id: Uuid,
|
||||
mode: engine::SelectMode,
|
||||
) -> Outcome {
|
||||
let selection = engine::Selection::Waypoints {
|
||||
file_id: engine::FileId(file_id),
|
||||
};
|
||||
select_elements(engine, selection, mode)
|
||||
}
|
||||
|
||||
pub fn select_waypoints<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
file_id: Uuid,
|
||||
waypoint_ids: &[Uuid],
|
||||
mode: engine::SelectMode,
|
||||
) -> Outcome {
|
||||
let selection = engine::Selection::Waypoint {
|
||||
file_id: engine::FileId(file_id),
|
||||
wpt_ids: waypoint_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.map(engine::WaypointId)
|
||||
.collect(),
|
||||
};
|
||||
select_elements(engine, selection, mode)
|
||||
}
|
||||
|
||||
// History
|
||||
|
||||
pub fn undo<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
execute(engine, Action::Undo)
|
||||
}
|
||||
|
||||
pub fn redo<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
execute(engine, Action::Redo)
|
||||
}
|
||||
|
||||
// Files
|
||||
|
||||
/// Creates a file and selects it. With `route`, it starts with a route. With `lng` and `lat`, it
|
||||
/// starts with a segment that holds that trackpoint (at `ele`, 0 by default): the one of its route
|
||||
/// with `route`, of a track otherwise.
|
||||
pub fn new_file<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
name: &str,
|
||||
lng: Option<f64>,
|
||||
lat: Option<f64>,
|
||||
ele: Option<f64>,
|
||||
route: bool,
|
||||
) -> Outcome {
|
||||
let trackpoint = lng.zip(lat).map(|(lng, lat)| engine::NewTrackpoint {
|
||||
lng,
|
||||
lat,
|
||||
ele: ele.unwrap_or_default(),
|
||||
});
|
||||
edit(
|
||||
engine,
|
||||
Command::New(engine::New {
|
||||
name,
|
||||
trackpoint,
|
||||
route,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Loads files as a single command (one undo step). The files are in `data`, one after the other,
|
||||
/// `lengths` being the number of bytes of each one. `names`: the name of each file when its data
|
||||
/// has none (the name on disk, without extension). The files that cannot be read are skipped, the
|
||||
/// first one that was read is selected.
|
||||
pub fn load_files<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
data: &[u8],
|
||||
lengths: &[usize],
|
||||
names: &[String],
|
||||
) -> Outcome {
|
||||
if lengths.len() != names.len() {
|
||||
return Outcome::failed(format!(
|
||||
"{} lengths for {} names",
|
||||
lengths.len(),
|
||||
names.len()
|
||||
));
|
||||
}
|
||||
let mut files = Vec::with_capacity(names.len());
|
||||
let mut offset = 0;
|
||||
for (length, name) in lengths.iter().zip(names) {
|
||||
let end = offset + *length;
|
||||
let Some(data) = data.get(offset..end) else {
|
||||
return Outcome::failed(format!("the data of {name} is shorter than its length"));
|
||||
};
|
||||
files.push(engine::Load { data, name });
|
||||
offset = end;
|
||||
}
|
||||
edit(engine, Command::LoadFiles(engine::LoadFiles { files }))
|
||||
}
|
||||
|
||||
/// Brings changes of the files from the outside in (see [`engine::SyncFiles`]): each file is
|
||||
/// `(id, data, name)`, and replaces the file of that id, or is added if there is none; it is called
|
||||
/// `name`, whatever its data says. The files to `remove` are deleted. The selection is left alone.
|
||||
pub fn sync_files<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
files: &[(Uuid, &[u8], &str)],
|
||||
remove: &[Uuid],
|
||||
) -> Outcome {
|
||||
let files = files
|
||||
.iter()
|
||||
.map(|(id, data, name)| engine::ExternalFile {
|
||||
id: engine::FileId(*id),
|
||||
data,
|
||||
name,
|
||||
})
|
||||
.collect();
|
||||
edit(
|
||||
engine,
|
||||
Command::SyncFiles(engine::SyncFiles {
|
||||
files,
|
||||
remove: file_ids(remove),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn duplicate<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
edit(engine, Command::Duplicate(engine::Duplicate))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn delete<H: History>(engine: &mut Engine<H>, whole_files: bool) -> Outcome {
|
||||
edit(engine, Command::Delete(engine::Delete { whole_files }))
|
||||
}
|
||||
|
||||
pub fn delete_all<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
edit(engine, Command::DeleteAll(engine::DeleteAll))
|
||||
}
|
||||
|
||||
/// Moves the files to `index` among the other files, in the given order (not undoable).
|
||||
pub fn reorder<H: History>(engine: &mut Engine<H>, file_ids_: &[Uuid], index: usize) -> Outcome {
|
||||
execute(
|
||||
engine,
|
||||
Action::Reorder {
|
||||
file_ids: file_ids(file_ids_),
|
||||
index,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Names a file in all its states, as if it had always been called so (not undoable): for a file
|
||||
/// that is written to a file of another name, whose name it takes.
|
||||
pub fn rename_file<H: History>(engine: &mut Engine<H>, file_id: Uuid, name: &str) -> Outcome {
|
||||
execute(
|
||||
engine,
|
||||
Action::Rename {
|
||||
file_id: engine::FileId(file_id),
|
||||
name: name.to_owned(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Puts the selected elements in the clipboard. Nothing happens when nothing is selected.
|
||||
pub fn copy<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
execute(engine, Action::Copy)
|
||||
}
|
||||
|
||||
/// Like `copy`, but the elements are moved when they are pasted.
|
||||
pub fn cut<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
execute(engine, Action::Cut)
|
||||
}
|
||||
|
||||
/// Pastes the clipboard according to the selection (see `can_paste`). What is pasted is always the
|
||||
/// elements as they were when they were copied or cut, even if they were changed or deleted since.
|
||||
/// It can be pasted several times: what was copied is copied again, what was cut is moved the first
|
||||
/// time (and the clipboard is not cut after that), then it is copied.
|
||||
pub fn paste<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
edit(engine, Command::Paste(engine::Paste))
|
||||
}
|
||||
|
||||
/// Moves elements to a place of the file tree, keeping their ids, like dragging and dropping them
|
||||
/// does. The moved elements are selected.
|
||||
///
|
||||
/// Files can only go among the files; tracks among the files (each becomes a file) or the tracks
|
||||
/// of a file; segments among the files (each becomes a file), the tracks of a file (each becomes
|
||||
/// a track) or the segments of a track; waypoints, or all the waypoints of a file, among the
|
||||
/// waypoints of a file.
|
||||
pub fn move_elements<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
what: Selection,
|
||||
to: MoveTarget,
|
||||
) -> Outcome {
|
||||
edit(
|
||||
engine,
|
||||
Command::Move(engine::Move {
|
||||
what: what.into(),
|
||||
to: to.into(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn metadata<H: History>(engine: &mut Engine<H>, name: &str, desc: &str) -> Outcome {
|
||||
edit(engine, Command::Metadata(engine::Metadata { name, desc }))
|
||||
}
|
||||
|
||||
/// Only the given fields are changed.
|
||||
pub fn style<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
color: Option<&str>,
|
||||
opacity: Option<f64>,
|
||||
width: Option<f64>,
|
||||
) -> Outcome {
|
||||
edit(
|
||||
engine,
|
||||
Command::Style(engine::Style {
|
||||
color,
|
||||
opacity,
|
||||
width,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Converts the selected routes to tracks, or the selected tracks or segments to routes.
|
||||
pub fn convert<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
edit(engine, Command::Convert(engine::Convert))
|
||||
}
|
||||
|
||||
pub fn new_route<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
edit(engine, Command::NewRoute(engine::NewRoute))
|
||||
}
|
||||
|
||||
pub fn new_track<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
edit(engine, Command::NewTrack(engine::NewTrack))
|
||||
}
|
||||
|
||||
pub fn new_track_segment<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
edit(engine, Command::NewTrackSegment(engine::NewTrackSegment))
|
||||
}
|
||||
|
||||
// Waypoints. The strings that are empty are the fields that the waypoint does not have.
|
||||
|
||||
/// Adds a waypoint to each selected file (or to the file of the selected elements).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_waypoint<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
lng: f64,
|
||||
lat: f64,
|
||||
ele: f64,
|
||||
name: &str,
|
||||
desc: &str,
|
||||
icon: &str,
|
||||
link: &str,
|
||||
) -> Outcome {
|
||||
edit(
|
||||
engine,
|
||||
Command::NewWaypoint(engine::NewWaypoint {
|
||||
lng,
|
||||
lat,
|
||||
ele,
|
||||
name,
|
||||
desc,
|
||||
icon,
|
||||
link,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Moves a waypoint of a file, whatever is selected.
|
||||
pub fn move_waypoint<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
file_id: Uuid,
|
||||
waypoint_id: Uuid,
|
||||
lng: f64,
|
||||
lat: f64,
|
||||
ele: f64,
|
||||
) -> Outcome {
|
||||
edit(
|
||||
engine,
|
||||
Command::MoveWaypoint(engine::MoveWaypoint {
|
||||
file_id: engine::FileId(file_id),
|
||||
waypoint_id: engine::WaypointId(waypoint_id),
|
||||
lng,
|
||||
lat,
|
||||
ele,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Deletes a waypoint of a file, whatever is selected.
|
||||
pub fn delete_waypoint<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
file_id: Uuid,
|
||||
waypoint_id: Uuid,
|
||||
) -> Outcome {
|
||||
edit(
|
||||
engine,
|
||||
Command::DeleteWaypoint(engine::DeleteWaypoint {
|
||||
file_id: engine::FileId(file_id),
|
||||
waypoint_id: engine::WaypointId(waypoint_id),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Changes a waypoint of a file, whatever is selected: its name, description, icon, link,
|
||||
/// position and elevation.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn update_waypoint<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
file_id: Uuid,
|
||||
waypoint_id: Uuid,
|
||||
lng: f64,
|
||||
lat: f64,
|
||||
ele: f64,
|
||||
name: &str,
|
||||
desc: &str,
|
||||
icon: &str,
|
||||
link: &str,
|
||||
) -> Outcome {
|
||||
edit(
|
||||
engine,
|
||||
Command::EditWaypoint(engine::EditWaypoint {
|
||||
file_id: engine::FileId(file_id),
|
||||
waypoint_id: engine::WaypointId(waypoint_id),
|
||||
lng,
|
||||
lat,
|
||||
ele,
|
||||
name,
|
||||
desc,
|
||||
icon,
|
||||
link,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Tools
|
||||
|
||||
pub fn reverse<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
edit(engine, Command::Reverse(engine::Reverse))
|
||||
}
|
||||
|
||||
/// Adds to each selected segment a reversed copy of itself (without repeating its last point), so
|
||||
/// that it goes back to where it started.
|
||||
pub fn round_trip<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
edit(engine, Command::RoundTrip(engine::RoundTrip))
|
||||
}
|
||||
|
||||
/// Keeps the trackpoints `start` to `end` (both included) of the selection, counted over its
|
||||
/// segments one after the other, and removes the others (and what is left empty).
|
||||
pub fn crop<H: History>(engine: &mut Engine<H>, start: u32, end: u32) -> Outcome {
|
||||
if start > end {
|
||||
return Outcome::failed(format!("the range {start}..{end} is reversed"));
|
||||
}
|
||||
edit(engine, Command::Crop(engine::Crop { start, end }))
|
||||
}
|
||||
|
||||
/// Removes the trackpoints of the selection that are less than `tolerance` meters away from the
|
||||
/// line of their neighbours, except the ends of the segments.
|
||||
pub fn reduce<H: History>(engine: &mut Engine<H>, tolerance: f64) -> Outcome {
|
||||
edit(engine, Command::Reduce(engine::Reduce { tolerance }))
|
||||
}
|
||||
|
||||
/// Sets the timestamps of the selection, which starts at `start_time` (ms since the epoch): the
|
||||
/// trackpoints that have some keep their durations multiplied by `ratio`, the others follow the
|
||||
/// previous one at `speed` (km/h).
|
||||
pub fn change_timestamps<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
start_time: f64,
|
||||
speed: f64,
|
||||
ratio: f64,
|
||||
) -> Outcome {
|
||||
edit(
|
||||
engine,
|
||||
Command::Time(engine::Time {
|
||||
start_time: start_time as i64,
|
||||
kind: engine::TimeKind::Change { speed, ratio },
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Makes up timestamps for the selection, which starts at `start_time` (ms since the epoch) and
|
||||
/// lasts `total_time` seconds.
|
||||
pub fn create_artificial_timestamps<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
start_time: f64,
|
||||
total_time: f64,
|
||||
) -> Outcome {
|
||||
edit(
|
||||
engine,
|
||||
Command::Time(engine::Time {
|
||||
start_time: start_time as i64,
|
||||
kind: engine::TimeKind::Artificial { total_time },
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Merges the selection into its first element. `remove_gaps` brings the parts that are connected
|
||||
/// closer in time, if they have timestamps. Merged files are kept (and the result is a new file) if
|
||||
/// the history needs it, see [`History::KEEPS_ORIGINALS`].
|
||||
pub fn merge<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
type_: engine::MergeType,
|
||||
remove_gaps: bool,
|
||||
) -> Outcome {
|
||||
edit(
|
||||
engine,
|
||||
Command::Merge(engine::Merge {
|
||||
type_,
|
||||
remove_gaps,
|
||||
keep_originals: H::KEEPS_ORIGINALS,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Extracted files are kept (and the result is new files) if the history needs it, see
|
||||
/// [`History::KEEPS_ORIGINALS`].
|
||||
pub fn extract<H: History>(engine: &mut Engine<H>) -> Outcome {
|
||||
edit(
|
||||
engine,
|
||||
Command::Extract(engine::Extract {
|
||||
keep_originals: H::KEEPS_ORIGINALS,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets the elevation of the trackpoints of the selection, one elevation per trackpoint.
|
||||
pub fn elevation<H: History>(engine: &mut Engine<H>, ele: &[f64]) -> Outcome {
|
||||
edit(engine, Command::Elevation(engine::Elevation { ele }))
|
||||
}
|
||||
|
||||
/// Removes the trackpoints and/or waypoints of the selection inside (or outside) a rectangle.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn clean<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
west: f64,
|
||||
south: f64,
|
||||
east: f64,
|
||||
north: f64,
|
||||
type_: engine::CleanType,
|
||||
trkpt: bool,
|
||||
wpt: bool,
|
||||
) -> Outcome {
|
||||
edit(
|
||||
engine,
|
||||
Command::Clean(engine::Clean {
|
||||
bounds: LngLatBounds {
|
||||
sw: LngLat {
|
||||
lng: west,
|
||||
lat: south,
|
||||
},
|
||||
ne: LngLat {
|
||||
lng: east,
|
||||
lat: north,
|
||||
},
|
||||
},
|
||||
type_,
|
||||
trkpt,
|
||||
wpt,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Routing
|
||||
//
|
||||
// The indices of the trackpoints and of the anchors are the ones of the routing buffers: they only
|
||||
// mean something for the `revision` they were read with (`routing_revision`), and the commands
|
||||
// that use them do nothing if the selection is not the one of that revision anymore.
|
||||
|
||||
/// Makes the call `f`, unless the selection is not the one of the revision anymore.
|
||||
fn at_revision<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
revision: u32,
|
||||
f: impl FnOnce(&mut Engine<H>) -> Outcome,
|
||||
) -> Outcome {
|
||||
if engine.routing().revision == revision {
|
||||
f(engine)
|
||||
} else {
|
||||
Outcome::failed("the selection changed since the routing buffers were read")
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the trackpoints `start..end` of the selection by the given ones, in a single segment
|
||||
/// (a pure insertion if `start == end`, a pure removal without new points). `attributes` holds the
|
||||
/// surface, highway, SAC scale and MTB scale of the new points, as intervals, and `anchors` the
|
||||
/// indices among the new points of the ones that become anchors.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn route<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
revision: u32,
|
||||
start: u32,
|
||||
end: u32,
|
||||
lng: &[f64],
|
||||
lat: &[f64],
|
||||
ele: &[f64],
|
||||
attributes: &RouteAttributes,
|
||||
anchors: &[u32],
|
||||
) -> Outcome {
|
||||
if lng.len() != lat.len() || lat.len() != ele.len() {
|
||||
return Outcome::failed("lng, lat and ele differ in length");
|
||||
}
|
||||
at_revision(engine, revision, |engine| {
|
||||
edit(
|
||||
engine,
|
||||
Command::Reroute(engine::Reroute {
|
||||
start,
|
||||
end,
|
||||
lng,
|
||||
lat,
|
||||
ele,
|
||||
surface: attributes.surface.view(),
|
||||
highway: attributes.highway.view(),
|
||||
sac_scale: attributes.sac_scale.view(),
|
||||
mtb_scale: attributes.mtb_scale.view(),
|
||||
anchors,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Makes an anchor of the point of the selected segments that is the closest to the coordinates,
|
||||
/// inserting a trackpoint on the path if none is there.
|
||||
pub fn insert_anchor<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
revision: u32,
|
||||
lng: f64,
|
||||
lat: f64,
|
||||
) -> Outcome {
|
||||
at_revision(engine, revision, |engine| {
|
||||
edit(
|
||||
engine,
|
||||
Command::InsertAnchor(engine::InsertAnchor { lng, lat }),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Makes the trackpoint `index` of the selection the start of its segment, which is a loop (the
|
||||
/// trackpoints before it go to the end, and the loop closes on it).
|
||||
pub fn change_loop_start<H: History>(engine: &mut Engine<H>, revision: u32, index: u32) -> Outcome {
|
||||
at_revision(engine, revision, |engine| {
|
||||
edit(
|
||||
engine,
|
||||
Command::ChangeLoopStart(engine::ChangeLoopStart { index }),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Cuts the file, the route, the track or the segment (see `SplitType`) of the trackpoint `at` of the
|
||||
/// selection in two, there: the trackpoint ends the first part and starts the second one. A file that
|
||||
/// is cut is kept (and its two parts are new files) if the history needs it, see
|
||||
/// [`History::KEEPS_ORIGINALS`].
|
||||
pub fn split<H: History>(
|
||||
engine: &mut Engine<H>,
|
||||
revision: u32,
|
||||
at: u32,
|
||||
split_type: engine::SplitType,
|
||||
) -> Outcome {
|
||||
at_revision(engine, revision, |engine| {
|
||||
edit(
|
||||
engine,
|
||||
Command::Split(engine::Split {
|
||||
at,
|
||||
split_type,
|
||||
keep_originals: H::KEEPS_ORIGINALS,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
//! What crosses to the frontend, as plain data (serde): the shapes of the objects that the frontend
|
||||
//! reads, which `wasm/src/ts.rs` declares in TypeScript. Ids are hyphenated UUID strings, times
|
||||
//! are numbers (ms since the epoch), absent fields are skipped.
|
||||
|
||||
use crate as engine;
|
||||
use engine::{FileId, History};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn ids(items: &[FileId]) -> Vec<String> {
|
||||
items.iter().map(|id| id.0.to_string()).collect()
|
||||
}
|
||||
|
||||
// Outcome
|
||||
|
||||
/// What a call that edits the files or changes the selection did.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Outcome {
|
||||
pub changed: bool,
|
||||
order_changed: bool,
|
||||
selection_changed: bool,
|
||||
clipboard_changed: bool,
|
||||
added: Vec<String>,
|
||||
modified: Vec<String>,
|
||||
removed: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<String>,
|
||||
/// Whether the files changed in a way that has to be saved (not sent).
|
||||
#[serde(skip)]
|
||||
pub saves: bool,
|
||||
}
|
||||
|
||||
impl Outcome {
|
||||
pub fn new(outcome: &engine::Outcome, error: Option<String>) -> Self {
|
||||
let diff = outcome.diff.clone().unwrap_or_default();
|
||||
Self {
|
||||
changed: outcome.changed,
|
||||
order_changed: outcome.order_changed,
|
||||
selection_changed: outcome.selection_changed,
|
||||
clipboard_changed: outcome.clipboard_changed,
|
||||
added: ids(&diff.added),
|
||||
modified: ids(&diff.modified),
|
||||
removed: ids(&diff.removed),
|
||||
error,
|
||||
saves: outcome.changed && (outcome.diff.is_some() || outcome.order_changed),
|
||||
}
|
||||
}
|
||||
|
||||
/// The outcome of a frontend that knows nothing of the engine: all the files are added, and the
|
||||
/// order, the selection and the clipboard are to be read.
|
||||
pub fn everything<H: History>(engine: &engine::Engine<H>) -> Self {
|
||||
Self {
|
||||
changed: true,
|
||||
order_changed: true,
|
||||
selection_changed: true,
|
||||
clipboard_changed: true,
|
||||
added: ids(engine.order()),
|
||||
modified: vec![],
|
||||
removed: vec![],
|
||||
error: None,
|
||||
saves: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The outcome of a call that did nothing and has nothing to say.
|
||||
pub fn failed_none() -> Self {
|
||||
Self::new(&engine::Outcome::default(), None)
|
||||
}
|
||||
|
||||
/// The ids of the files that the call added.
|
||||
pub fn added(&self) -> &[String] {
|
||||
&self.added
|
||||
}
|
||||
|
||||
/// Whether the call did nothing and says why.
|
||||
pub fn has_error(&self) -> bool {
|
||||
self.error.is_some()
|
||||
}
|
||||
|
||||
/// What two calls that were made one after the other did together.
|
||||
pub fn merge(mut self, other: Self) -> Self {
|
||||
fn union(into: &mut Vec<String>, from: Vec<String>) {
|
||||
for id in from {
|
||||
if !into.contains(&id) {
|
||||
into.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.changed |= other.changed;
|
||||
self.order_changed |= other.order_changed;
|
||||
self.selection_changed |= other.selection_changed;
|
||||
self.clipboard_changed |= other.clipboard_changed;
|
||||
self.saves |= other.saves;
|
||||
// a file that was added and then modified is still an added one, and one that was added
|
||||
// and then removed is not there at all
|
||||
let added_before = self.added.clone();
|
||||
union(&mut self.added, other.added);
|
||||
for id in other.modified {
|
||||
if !self.added.contains(&id) && !self.modified.contains(&id) {
|
||||
self.modified.push(id);
|
||||
}
|
||||
}
|
||||
for id in other.removed {
|
||||
self.added.retain(|added| *added != id);
|
||||
self.modified.retain(|modified| *modified != id);
|
||||
if !added_before.contains(&id) && !self.removed.contains(&id) {
|
||||
self.removed.push(id);
|
||||
}
|
||||
}
|
||||
self.error = self.error.or(other.error);
|
||||
self
|
||||
}
|
||||
|
||||
/// What a call that does nothing returns.
|
||||
pub fn failed(message: impl Into<String>) -> Self {
|
||||
Self::new(&engine::Outcome::default(), Some(message.into()))
|
||||
}
|
||||
}
|
||||
|
||||
// Selection and clipboard
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(
|
||||
tag = "type",
|
||||
rename_all = "camelCase",
|
||||
rename_all_fields = "camelCase"
|
||||
)]
|
||||
pub enum Selection {
|
||||
Empty,
|
||||
File {
|
||||
file_ids: Vec<Uuid>,
|
||||
},
|
||||
Route {
|
||||
file_id: Uuid,
|
||||
route_ids: Vec<Uuid>,
|
||||
},
|
||||
Track {
|
||||
file_id: Uuid,
|
||||
track_ids: Vec<Uuid>,
|
||||
},
|
||||
Segment {
|
||||
file_id: Uuid,
|
||||
track_id: Uuid,
|
||||
segment_ids: Vec<Uuid>,
|
||||
},
|
||||
Waypoints {
|
||||
file_id: Uuid,
|
||||
},
|
||||
Waypoint {
|
||||
file_id: Uuid,
|
||||
waypoint_ids: Vec<Uuid>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<&engine::Selection> for Selection {
|
||||
fn from(selection: &engine::Selection) -> Self {
|
||||
use engine::Selection as S;
|
||||
match selection {
|
||||
S::Empty => Self::Empty,
|
||||
S::File { file_ids } => Self::File {
|
||||
file_ids: file_ids.iter().map(|id| id.0).collect(),
|
||||
},
|
||||
S::Route { file_id, rte_ids } => Self::Route {
|
||||
file_id: file_id.0,
|
||||
route_ids: rte_ids.iter().map(|id| id.0).collect(),
|
||||
},
|
||||
S::Track { file_id, trk_ids } => Self::Track {
|
||||
file_id: file_id.0,
|
||||
track_ids: trk_ids.iter().map(|id| id.0).collect(),
|
||||
},
|
||||
S::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => Self::Segment {
|
||||
file_id: file_id.0,
|
||||
track_id: trk_id.0,
|
||||
segment_ids: trkseg_ids.iter().map(|id| id.0).collect(),
|
||||
},
|
||||
S::Waypoints { file_id } => Self::Waypoints { file_id: file_id.0 },
|
||||
S::Waypoint { file_id, wpt_ids } => Self::Waypoint {
|
||||
file_id: file_id.0,
|
||||
waypoint_ids: wpt_ids.iter().map(|id| id.0).collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Selection> for engine::Selection {
|
||||
fn from(selection: Selection) -> Self {
|
||||
use engine::Selection as S;
|
||||
match selection {
|
||||
Selection::Empty => S::Empty,
|
||||
Selection::File { file_ids } => S::File {
|
||||
file_ids: file_ids.into_iter().map(FileId).collect(),
|
||||
},
|
||||
Selection::Route { file_id, route_ids } => S::Route {
|
||||
file_id: FileId(file_id),
|
||||
rte_ids: route_ids.into_iter().map(engine::RouteId).collect(),
|
||||
},
|
||||
Selection::Track { file_id, track_ids } => S::Track {
|
||||
file_id: FileId(file_id),
|
||||
trk_ids: track_ids.into_iter().map(engine::TrackId).collect(),
|
||||
},
|
||||
Selection::Segment {
|
||||
file_id,
|
||||
track_id,
|
||||
segment_ids,
|
||||
} => S::TrackSegment {
|
||||
file_id: FileId(file_id),
|
||||
trk_id: engine::TrackId(track_id),
|
||||
trkseg_ids: segment_ids
|
||||
.into_iter()
|
||||
.map(engine::TrackSegmentId)
|
||||
.collect(),
|
||||
},
|
||||
Selection::Waypoints { file_id } => S::Waypoints {
|
||||
file_id: FileId(file_id),
|
||||
},
|
||||
Selection::Waypoint {
|
||||
file_id,
|
||||
waypoint_ids,
|
||||
} => S::Waypoint {
|
||||
file_id: FileId(file_id),
|
||||
wpt_ids: waypoint_ids.into_iter().map(engine::WaypointId).collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where moved elements go. A negative index is the start, an infinite one the end (which JSON
|
||||
/// writes as `null`, so the frontend sends a very large number instead).
|
||||
#[derive(Deserialize)]
|
||||
#[serde(
|
||||
tag = "type",
|
||||
rename_all = "camelCase",
|
||||
rename_all_fields = "camelCase"
|
||||
)]
|
||||
pub enum MoveTarget {
|
||||
Files {
|
||||
index: f64,
|
||||
},
|
||||
Routes {
|
||||
file_id: Uuid,
|
||||
index: f64,
|
||||
},
|
||||
Tracks {
|
||||
file_id: Uuid,
|
||||
index: f64,
|
||||
},
|
||||
Segments {
|
||||
file_id: Uuid,
|
||||
track_id: Uuid,
|
||||
index: f64,
|
||||
},
|
||||
Waypoints {
|
||||
file_id: Uuid,
|
||||
index: f64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<MoveTarget> for engine::MoveTarget {
|
||||
fn from(target: MoveTarget) -> Self {
|
||||
use engine::MoveTarget as T;
|
||||
let index = |index: f64| index.max(0.0) as usize;
|
||||
match target {
|
||||
MoveTarget::Files { index: i } => T::Files { index: index(i) },
|
||||
MoveTarget::Routes { file_id, index: i } => T::Routes {
|
||||
file_id: FileId(file_id),
|
||||
index: index(i),
|
||||
},
|
||||
MoveTarget::Tracks { file_id, index: i } => T::Tracks {
|
||||
file_id: FileId(file_id),
|
||||
index: index(i),
|
||||
},
|
||||
MoveTarget::Segments {
|
||||
file_id,
|
||||
track_id,
|
||||
index: i,
|
||||
} => T::Segments {
|
||||
file_id: FileId(file_id),
|
||||
trk_id: engine::TrackId(track_id),
|
||||
index: index(i),
|
||||
},
|
||||
MoveTarget::Waypoints { file_id, index: i } => T::Waypoints {
|
||||
file_id: FileId(file_id),
|
||||
index: index(i),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Clipboard {
|
||||
#[serde(rename = "type")]
|
||||
kind: &'static str,
|
||||
ids: Vec<String>,
|
||||
cut: bool,
|
||||
}
|
||||
|
||||
impl Clipboard {
|
||||
pub fn new(clipboard: &engine::Clipboard) -> Self {
|
||||
let strings = |ids: Vec<Uuid>| ids.iter().map(Uuid::to_string).collect();
|
||||
let (kind, ids) = match clipboard.content.ids() {
|
||||
engine::ClipboardIds::Files(ids) => {
|
||||
("files", strings(ids.into_iter().map(|id| id.0).collect()))
|
||||
}
|
||||
engine::ClipboardIds::Routes(ids) => {
|
||||
("routes", strings(ids.into_iter().map(|id| id.0).collect()))
|
||||
}
|
||||
engine::ClipboardIds::Tracks(ids) => {
|
||||
("tracks", strings(ids.into_iter().map(|id| id.0).collect()))
|
||||
}
|
||||
engine::ClipboardIds::Segments(ids) => (
|
||||
"segments",
|
||||
strings(ids.into_iter().map(|id| id.0).collect()),
|
||||
),
|
||||
engine::ClipboardIds::Waypoints(ids) => (
|
||||
"waypoints",
|
||||
strings(ids.into_iter().map(|id| id.0).collect()),
|
||||
),
|
||||
};
|
||||
Self {
|
||||
kind,
|
||||
ids,
|
||||
cut: clipboard.cut,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Files
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileStructure {
|
||||
id: String,
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
desc: Option<String>,
|
||||
routes: Vec<RouteNode>,
|
||||
tracks: Vec<TrackNode>,
|
||||
waypoints: Vec<WaypointNode>,
|
||||
waypoints_rev: String,
|
||||
}
|
||||
|
||||
/// The name, description and style of a route or a track.
|
||||
#[derive(Serialize)]
|
||||
struct PathInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
desc: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
color: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
opacity: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
width: Option<f64>,
|
||||
}
|
||||
|
||||
impl From<&engine::PathInfo> for PathInfo {
|
||||
fn from(info: &engine::PathInfo) -> Self {
|
||||
Self {
|
||||
name: info.name.clone(),
|
||||
desc: info.desc.clone(),
|
||||
color: info.color.clone(),
|
||||
opacity: info.opacity,
|
||||
width: info.width,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RouteNode {
|
||||
id: String,
|
||||
#[serde(flatten)]
|
||||
info: PathInfo,
|
||||
segment: SegmentNode,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TrackNode {
|
||||
id: String,
|
||||
#[serde(flatten)]
|
||||
info: PathInfo,
|
||||
segments: Vec<SegmentNode>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SegmentNode {
|
||||
id: String,
|
||||
rev: String,
|
||||
length: f64,
|
||||
}
|
||||
|
||||
impl From<&engine::SegmentNode> for SegmentNode {
|
||||
fn from(seg: &engine::SegmentNode) -> Self {
|
||||
Self {
|
||||
id: seg.id.0.to_string(),
|
||||
rev: seg.rev_id.0.to_string(),
|
||||
length: seg.len as f64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct WaypointNode {
|
||||
id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
sym: Option<String>,
|
||||
}
|
||||
|
||||
impl FileStructure {
|
||||
pub fn new(file: &engine::FileStructure) -> Self {
|
||||
Self {
|
||||
id: file.id.0.to_string(),
|
||||
name: file.name.clone(),
|
||||
desc: file.desc.clone(),
|
||||
routes: file
|
||||
.routes
|
||||
.iter()
|
||||
.map(|rte| RouteNode {
|
||||
id: rte.id.0.to_string(),
|
||||
info: (&rte.info).into(),
|
||||
segment: (&rte.segment).into(),
|
||||
})
|
||||
.collect(),
|
||||
tracks: file
|
||||
.tracks
|
||||
.iter()
|
||||
.map(|trk| TrackNode {
|
||||
id: trk.id.0.to_string(),
|
||||
info: (&trk.info).into(),
|
||||
segments: trk.segments.iter().map(Into::into).collect(),
|
||||
})
|
||||
.collect(),
|
||||
waypoints: file
|
||||
.waypoints
|
||||
.iter()
|
||||
.map(|wpt| WaypointNode {
|
||||
id: wpt.id.0.to_string(),
|
||||
name: wpt.name.clone(),
|
||||
sym: wpt.sym.clone(),
|
||||
})
|
||||
.collect(),
|
||||
waypoints_rev: file.wpt_rev_id.0.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GlobalStatistics {
|
||||
total_distance: f64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
moving_distance: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
total_time: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
moving_time: Option<f64>,
|
||||
elevation_gain: f64,
|
||||
elevation_loss: f64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
start_time: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
end_time: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
total_speed: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
moving_speed: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
bounds: Option<Bounds>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Bounds {
|
||||
west: f64,
|
||||
south: f64,
|
||||
east: f64,
|
||||
north: f64,
|
||||
}
|
||||
|
||||
impl GlobalStatistics {
|
||||
pub fn new(stats: &engine::GlobalStatistics) -> Self {
|
||||
let seconds = |ms: Option<i64>| ms.map(|ms| ms as f64 / 1000.0);
|
||||
let (sw, ne) = (&stats.bounds.sw, &stats.bounds.ne);
|
||||
Self {
|
||||
total_distance: stats.total_distance,
|
||||
moving_distance: stats.moving_distance,
|
||||
total_time: seconds(stats.total_time),
|
||||
moving_time: seconds(stats.moving_time),
|
||||
elevation_gain: stats.elevation_gain,
|
||||
elevation_loss: stats.elevation_loss,
|
||||
start_time: stats.start_time.map(|t| t as f64),
|
||||
end_time: stats.end_time.map(|t| t as f64),
|
||||
total_speed: stats.total_speed(),
|
||||
moving_speed: stats.moving_speed(),
|
||||
bounds: (sw.lng <= ne.lng && sw.lat <= ne.lat).then_some(Bounds {
|
||||
west: sw.lng,
|
||||
south: sw.lat,
|
||||
east: ne.lng,
|
||||
north: ne.lat,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct WaypointDetails {
|
||||
id: String,
|
||||
lng: f64,
|
||||
lat: f64,
|
||||
ele: f64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
time: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
desc: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cmt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
sym: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
links: Option<Vec<Link>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Link {
|
||||
href: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
text: Option<String>,
|
||||
}
|
||||
|
||||
impl WaypointDetails {
|
||||
pub fn new(wpt: &engine::Waypoint) -> Self {
|
||||
Self {
|
||||
id: wpt.id.0.to_string(),
|
||||
lng: wpt.coordinates.lng,
|
||||
lat: wpt.coordinates.lat,
|
||||
ele: wpt.ele,
|
||||
time: wpt.time.map(|t| t as f64),
|
||||
name: wpt.name.clone(),
|
||||
desc: wpt.desc.clone(),
|
||||
cmt: wpt.cmt.clone(),
|
||||
sym: wpt.sym.clone(),
|
||||
links: (!wpt.links.is_empty()).then(|| {
|
||||
wpt.links
|
||||
.iter()
|
||||
.map(|link| Link {
|
||||
href: link.href.clone(),
|
||||
text: link.text.clone(),
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TrackpointDetails {
|
||||
lng: f64,
|
||||
lat: f64,
|
||||
ele: f64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
time: Option<f64>,
|
||||
}
|
||||
|
||||
impl TrackpointDetails {
|
||||
pub fn new(pt: &engine::Trackpoint) -> Self {
|
||||
Self {
|
||||
lng: pt.coordinates.lng,
|
||||
lat: pt.coordinates.lat,
|
||||
ele: pt.ele,
|
||||
time: pt.time.map(|t| t as f64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trackpoint of the selection: what the statistics buffers do not hold for each trackpoint.
|
||||
/// Absent fields are unknown.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SelectionPoint {
|
||||
/// Slope (%) at the trackpoint.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub slope: Option<f64>,
|
||||
/// ms since the epoch.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub time: Option<f64>,
|
||||
/// Slope (%) and length (km) of the smoothed elevation segment of the trackpoint.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub slope_segment_slope: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub slope_segment_distance: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub surface: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub highway: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sac_scale: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mtb_scale: Option<String>,
|
||||
}
|
||||
|
||||
// Routing
|
||||
|
||||
/// A category of the trackpoints of a route, as in `RouteCategory` of the engine.
|
||||
#[derive(Default, Deserialize)]
|
||||
pub struct RouteCategory {
|
||||
pub starts: Vec<u32>,
|
||||
pub values: Vec<u8>,
|
||||
pub names: Vec<String>,
|
||||
}
|
||||
|
||||
impl RouteCategory {
|
||||
pub fn view(&self) -> engine::RouteCategory<'_> {
|
||||
engine::RouteCategory {
|
||||
starts: &self.starts,
|
||||
values: &self.values,
|
||||
names: &self.names,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct RouteAttributes {
|
||||
pub surface: RouteCategory,
|
||||
pub highway: RouteCategory,
|
||||
pub sac_scale: RouteCategory,
|
||||
pub mtb_scale: RouteCategory,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//! The interface to the engine, which the interfaces to the frontend share: the web page
|
||||
//! (`wasm`) and the desktop app (`tauri`) each expose these functions, and only convert what
|
||||
//! crosses to their own way of passing data.
|
||||
//!
|
||||
//! - `commands`: the functions that edit the files or change the selection. Each returns an
|
||||
//! `Outcome`: whether anything changed, what changed (to update what the frontend keeps), and why
|
||||
//! nothing was done when that is so (the command had nothing to do, the data is invalid, an
|
||||
//! argument is wrong).
|
||||
//! - `queries`: the functions that read the state of the engine.
|
||||
//! - `dto`: the data that crosses, as serde types.
|
||||
//!
|
||||
//! Ids are `Uuid`s: how the frontend sends them (strings, bytes) is for the interfaces to parse.
|
||||
//! Coordinates and statistics are slices of numbers, which the interfaces copy to typed arrays.
|
||||
|
||||
pub mod commands;
|
||||
pub mod dto;
|
||||
pub mod queries;
|
||||
|
||||
pub use commands::*;
|
||||
pub use dto::*;
|
||||
pub use queries::*;
|
||||
@@ -0,0 +1,277 @@
|
||||
//! The functions that read the state of the engine.
|
||||
//!
|
||||
//! The files are described by one ordered list of ids and one structure per file, so that the UI
|
||||
//! can react to what changed only: after each command, its `Outcome` tells which structures to
|
||||
//! (re)read or drop and whether the order changed. The trackpoints of a segment and the waypoints
|
||||
//! of a file are in the same order as the coordinates of their buffers.
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::dto::*;
|
||||
use crate::{self as engine, Engine, FileId, History};
|
||||
|
||||
/// Ids of the files in display order.
|
||||
pub fn file_order<H: History>(engine: &Engine<H>) -> Vec<String> {
|
||||
ids(engine.order())
|
||||
}
|
||||
|
||||
/// Structure of a file, `None` if the id is unknown.
|
||||
pub fn file_structure<H: History>(engine: &Engine<H>, file_id: Uuid) -> Option<FileStructure> {
|
||||
engine
|
||||
.file_structure(&FileId(file_id))
|
||||
.map(FileStructure::new)
|
||||
}
|
||||
|
||||
/// Global statistics of a file, `None` if the id is unknown.
|
||||
pub fn file_statistics<H: History>(engine: &Engine<H>, file_id: Uuid) -> Option<GlobalStatistics> {
|
||||
engine
|
||||
.file_statistics(&FileId(file_id))
|
||||
.map(|stats| GlobalStatistics::new(&stats))
|
||||
}
|
||||
|
||||
/// The current selection.
|
||||
pub fn selection<H: History>(engine: &Engine<H>) -> Selection {
|
||||
engine.selection().into()
|
||||
}
|
||||
|
||||
/// What was copied or cut and is waiting to be pasted.
|
||||
pub fn clipboard<H: History>(engine: &Engine<H>) -> Option<Clipboard> {
|
||||
engine.clipboard().map(Clipboard::new)
|
||||
}
|
||||
|
||||
pub fn can_undo<H: History>(engine: &Engine<H>) -> bool {
|
||||
engine.can_undo()
|
||||
}
|
||||
|
||||
pub fn can_redo<H: History>(engine: &Engine<H>) -> bool {
|
||||
engine.can_redo()
|
||||
}
|
||||
|
||||
/// Whether the clipboard can be pasted with the current selection.
|
||||
pub fn can_paste<H: History>(engine: &Engine<H>) -> bool {
|
||||
engine.can_paste()
|
||||
}
|
||||
|
||||
/// All the data of a waypoint of a file, `None` if it does not exist.
|
||||
pub fn waypoint<H: History>(
|
||||
engine: &Engine<H>,
|
||||
file_id: Uuid,
|
||||
waypoint_id: Uuid,
|
||||
) -> Option<WaypointDetails> {
|
||||
engine
|
||||
.waypoint(&FileId(file_id), &engine::WaypointId(waypoint_id))
|
||||
.map(WaypointDetails::new)
|
||||
}
|
||||
|
||||
/// A trackpoint of a segment of a file, `None` if it does not exist.
|
||||
pub fn trackpoint<H: History>(
|
||||
engine: &Engine<H>,
|
||||
file_id: Uuid,
|
||||
segment_id: Uuid,
|
||||
index: usize,
|
||||
) -> Option<TrackpointDetails> {
|
||||
engine
|
||||
.trackpoint(&FileId(file_id), &engine::TrackSegmentId(segment_id), index)
|
||||
.map(TrackpointDetails::new)
|
||||
}
|
||||
|
||||
/// A file as GPX (UTF-8 bytes), `None` if it does not exist. The options say which data of the
|
||||
/// trackpoints to keep.
|
||||
pub fn export_file<H: History>(
|
||||
engine: &Engine<H>,
|
||||
file_id: Uuid,
|
||||
options: engine::ExportOptions,
|
||||
) -> Option<Vec<u8>> {
|
||||
engine.export(&FileId(file_id), options)
|
||||
}
|
||||
|
||||
/// The data that some files have, so that the ones that none has are not offered when exporting
|
||||
/// them: a bit for the time (1), heart rate (2), cadence (4), temperature (8), power (16) and the
|
||||
/// OpenStreetMap data (32).
|
||||
pub fn exportable_data<H: History>(engine: &Engine<H>, file_ids: &[Uuid]) -> u8 {
|
||||
let ids: Vec<FileId> = file_ids.iter().copied().map(FileId).collect();
|
||||
let data = engine.exportable_data(&ids);
|
||||
[
|
||||
data.time, data.hr, data.cad, data.atemp, data.power, data.osm,
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(0, |bits, (i, on)| bits | (u8::from(*on) << i))
|
||||
}
|
||||
|
||||
/// For each trackpoint of the selection (numbered as in the statistics), the highest tolerance
|
||||
/// (m) of `reduce` that keeps it: infinite for the ends of the segments.
|
||||
pub fn reduction_distances<H: History>(engine: &Engine<H>) -> Vec<f64> {
|
||||
engine.reduction_distances()
|
||||
}
|
||||
|
||||
// Coordinates, as flat `[lng, lat, ...]` slices. Compare `rev` / `waypointsRev` of the file
|
||||
// structures with the previous ones to know which ones actually changed.
|
||||
|
||||
/// Coordinates of the trackpoints of a segment (empty if the id is unknown).
|
||||
pub fn segment_coordinates<H: History>(engine: &Engine<H>, segment_id: Uuid) -> &[f64] {
|
||||
engine.segment_coordinates(&engine::TrackSegmentId(segment_id))
|
||||
}
|
||||
|
||||
/// Coordinates of the waypoints of a file (empty if the id is unknown).
|
||||
pub fn waypoint_coordinates<H: History>(engine: &Engine<H>, file_id: Uuid) -> &[f64] {
|
||||
engine.waypoint_coordinates(&FileId(file_id))
|
||||
}
|
||||
|
||||
// Statistics of the selection
|
||||
|
||||
/// Global statistics of the whole selection (the buffers below cover it).
|
||||
pub fn selection_statistics<H: History>(engine: &Engine<H>) -> GlobalStatistics {
|
||||
GlobalStatistics::new(&engine.statistics().global)
|
||||
}
|
||||
|
||||
/// Global statistics of the trackpoints from `start` to `end` (both included) of the selection,
|
||||
/// as indexed by the buffers. `None` when the range is not in the selection.
|
||||
pub fn slice_statistics<H: History>(
|
||||
engine: &Engine<H>,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) -> Option<GlobalStatistics> {
|
||||
engine
|
||||
.statistics()
|
||||
.slice(start, end)
|
||||
.map(|stats| GlobalStatistics::new(&stats))
|
||||
}
|
||||
|
||||
macro_rules! buffers {
|
||||
($($name:ident),*) => {$(
|
||||
pub fn $name<H: History>(engine: &Engine<H>) -> &[f64] {
|
||||
&engine.statistics().$name
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
// One entry per trackpoint of the selection.
|
||||
buffers!(
|
||||
total_distance,
|
||||
elevation_gain,
|
||||
elevation_loss,
|
||||
lng,
|
||||
lat,
|
||||
ele
|
||||
);
|
||||
|
||||
macro_rules! optional_buffers {
|
||||
($($name:ident: $type:ty),*) => {$(
|
||||
/// `None` when no trackpoint of the selection has the value, else an entry per trackpoint,
|
||||
/// the missing ones being NaN (`NO_TIME` for the timestamps, 0 before the first trackpoint
|
||||
/// that has them for the cumulative distances and durations).
|
||||
pub fn $name<H: History>(engine: &Engine<H>) -> Option<&[$type]> {
|
||||
engine.statistics().$name.as_deref()
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
optional_buffers!(moving_distance: f64, speed: f64, hr: f64, cad: f64, atemp: f64, power: f64);
|
||||
optional_buffers!(total_time: i64, moving_time: i64);
|
||||
|
||||
// The smoothed elevation segments of the selection, over which the slope is averaged, as
|
||||
// intervals of trackpoints: `slope_segment_starts` holds the index of the first trackpoint of each
|
||||
// segment, and `slope_segment_slopes` its slope (%). Their length is in `selection_point`.
|
||||
|
||||
pub fn slope_segment_starts<H: History>(engine: &Engine<H>) -> &[u32] {
|
||||
&engine.statistics().slope_segments.starts
|
||||
}
|
||||
|
||||
pub fn slope_segment_slopes<H: History>(engine: &Engine<H>) -> &[f64] {
|
||||
&engine.statistics().slope_segments.slopes
|
||||
}
|
||||
|
||||
/// What the buffers do not hold for each trackpoint of the selection, for the one at `index`
|
||||
/// (numbered as in the buffers), `None` if there is none: for example the one that is hovered on
|
||||
/// the elevation profile.
|
||||
pub fn selection_point<H: History>(engine: &Engine<H>, index: usize) -> Option<SelectionPoint> {
|
||||
let stats = engine.statistics();
|
||||
if index >= stats.len() {
|
||||
return None;
|
||||
}
|
||||
let categories = engine.categories();
|
||||
let name = |intervals: &engine::Intervals, categories: &engine::Categories| {
|
||||
let value = intervals.value_at(index);
|
||||
let code = value.checked_sub(1)?;
|
||||
categories.name(code).map(str::to_owned)
|
||||
};
|
||||
let segment = stats.slope_segments.at(index);
|
||||
let finite = |value: f64| value.is_finite().then_some(value);
|
||||
Some(SelectionPoint {
|
||||
slope: finite(stats.slope[index]),
|
||||
time: stats
|
||||
.time
|
||||
.as_ref()
|
||||
.map(|time| time[index])
|
||||
.filter(|time| *time != engine::NO_TIME)
|
||||
.map(|time| time as f64),
|
||||
slope_segment_slope: segment.and_then(|i| finite(stats.slope_segments.slopes[i])),
|
||||
slope_segment_distance: segment.and_then(|i| finite(stats.slope_segments.distances[i])),
|
||||
surface: name(&stats.surface, &categories.surface),
|
||||
highway: name(&stats.highway, &categories.highway),
|
||||
sac_scale: name(&stats.sac_scale, &categories.sac_scale),
|
||||
mtb_scale: name(&stats.mtb_scale, &categories.mtb_scale),
|
||||
})
|
||||
}
|
||||
|
||||
// Surface, highway, SAC scale and MTB scale of the trackpoints, as intervals of trackpoints that
|
||||
// have the same value: `<name>_starts` holds the index of the first trackpoint of each interval,
|
||||
// and `<name>_values` its value (0 when unknown, else 1 + the index in the names of the category).
|
||||
// A value that is unknown for every trackpoint is a single interval.
|
||||
macro_rules! categories {
|
||||
($starts:ident, $values:ident, $names:ident, $field:ident) => {
|
||||
pub fn $starts<H: History>(engine: &Engine<H>) -> &[u32] {
|
||||
&engine.statistics().$field.starts
|
||||
}
|
||||
|
||||
pub fn $values<H: History>(engine: &Engine<H>) -> &[u8] {
|
||||
&engine.statistics().$field.values
|
||||
}
|
||||
|
||||
/// The names of the category, in the order of their codes. The list only grows: a name
|
||||
/// keeps its index.
|
||||
pub fn $names<H: History>(engine: &Engine<H>) -> &[String] {
|
||||
engine.categories().$field.names()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
categories!(surface_starts, surface_values, surfaces, surface);
|
||||
categories!(highway_starts, highway_values, highways, highway);
|
||||
categories!(sac_scale_starts, sac_scale_values, sac_scales, sac_scale);
|
||||
categories!(mtb_scale_starts, mtb_scale_values, mtb_scales, mtb_scale);
|
||||
|
||||
// The anchors of the routing tool among the trackpoints of the selection (numbered as in the
|
||||
// statistics). The ends of the segments are always anchors.
|
||||
|
||||
/// The index in the selection of each anchor.
|
||||
pub fn anchor_indices<H: History>(engine: &Engine<H>) -> &[u32] {
|
||||
&engine.routing().anchor_indices
|
||||
}
|
||||
|
||||
/// The lowest map zoom level at which each anchor is shown.
|
||||
pub fn anchor_zooms<H: History>(engine: &Engine<H>) -> &[u8] {
|
||||
&engine.routing().anchor_zooms
|
||||
}
|
||||
|
||||
/// Index in the selection of the first trackpoint of each selected segment that has some.
|
||||
pub fn segment_starts<H: History>(engine: &Engine<H>) -> &[u32] {
|
||||
&engine.routing().segment_starts
|
||||
}
|
||||
|
||||
/// The id of each selected segment that has trackpoints, as `segment_starts`.
|
||||
pub fn segment_ids<H: History>(engine: &Engine<H>) -> Vec<String> {
|
||||
engine
|
||||
.routing()
|
||||
.segment_ids
|
||||
.iter()
|
||||
.map(|id| id.0.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Changes when the selected segments or their trackpoints change: the indices in the selection
|
||||
/// are only valid while it stays the same.
|
||||
pub fn routing_revision<H: History>(engine: &Engine<H>) -> u32 {
|
||||
engine.routing().revision
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -6,7 +6,7 @@
|
||||
//! between them are kept: every part comes after the previous one in the new order, as late as the
|
||||
//! original one came after the part that followed it.
|
||||
|
||||
use crate::{Track, TrackSegment, Trackpoint};
|
||||
use crate::{Route, Track, TrackSegment, Trackpoint};
|
||||
|
||||
/// The time of the first trackpoint of a segment.
|
||||
pub fn start_time(segment: &TrackSegment) -> Option<i64> {
|
||||
@@ -94,6 +94,19 @@ pub fn reverse_segments(
|
||||
}
|
||||
}
|
||||
|
||||
/// Reverses the routes of a file, and their trackpoints, like the segments of a track (see
|
||||
/// [`reverse_segments`]).
|
||||
pub fn reverse_routes(routes: &mut [Route], original_next: Option<i64>, new_previous: Option<i64>) {
|
||||
routes.reverse();
|
||||
let (mut next, mut previous) = (original_next, new_previous);
|
||||
for route in routes.iter_mut() {
|
||||
let original_start = start_time(&route.seg);
|
||||
reverse_segment(&mut route.seg, next, previous);
|
||||
next = original_start;
|
||||
previous = end_time(&route.seg);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reverses the tracks of a file, and their segments and trackpoints.
|
||||
pub fn reverse_tracks(tracks: &mut [Track], original_next: Option<i64>, new_previous: Option<i64>) {
|
||||
tracks.reverse();
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,81 @@
|
||||
use crate::{Link, Route, Track, TrackSegment, Waypoints};
|
||||
|
||||
use super::common::uuid_id;
|
||||
|
||||
uuid_id!(FileId);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct File {
|
||||
pub id: FileId,
|
||||
pub info: FileInfo,
|
||||
/// The routes come before the tracks, as in GPX: everything that goes through the segments
|
||||
/// of a file (statistics, the elevation profile...) takes the ones of the routes first.
|
||||
pub rte: Vec<Route>,
|
||||
pub trk: Vec<Track>,
|
||||
pub wpt: Waypoints,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct FileInfo {
|
||||
pub name: String,
|
||||
pub desc: Option<String>,
|
||||
pub author: Option<Author>,
|
||||
pub links: Vec<Link>,
|
||||
pub time: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Author {
|
||||
pub name: Option<String>,
|
||||
/// `id@domain`, which the file gives as the two attributes of an `email` element.
|
||||
pub email: Option<String>,
|
||||
/// The author has a single link, unlike the other elements.
|
||||
pub link: Option<Link>,
|
||||
}
|
||||
|
||||
/// Where a segment is in its file: the one of a route, or one of a track.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SegmentIndex {
|
||||
Route(usize),
|
||||
Track { trk: usize, seg: usize },
|
||||
}
|
||||
|
||||
impl File {
|
||||
/// The segments of the routes, then the ones of the tracks.
|
||||
pub fn segments(&self) -> impl Iterator<Item = &TrackSegment> {
|
||||
self.rte
|
||||
.iter()
|
||||
.map(|rte| &rte.seg)
|
||||
.chain(self.trk.iter().flat_map(|trk| &trk.trkseg))
|
||||
}
|
||||
|
||||
/// Where the segments are, in the order of [`File::segments`].
|
||||
pub fn segment_indices(&self) -> impl Iterator<Item = SegmentIndex> + '_ {
|
||||
(0..self.rte.len())
|
||||
.map(SegmentIndex::Route)
|
||||
.chain(self.trk.iter().enumerate().flat_map(|(trk, track)| {
|
||||
(0..track.trkseg.len()).map(move |seg| SegmentIndex::Track { trk, seg })
|
||||
}))
|
||||
}
|
||||
|
||||
/// The segment at `index`. Panics if there is none.
|
||||
pub fn segment(&self, index: SegmentIndex) -> &TrackSegment {
|
||||
match index {
|
||||
SegmentIndex::Route(rte) => &self.rte[rte].seg,
|
||||
SegmentIndex::Track { trk, seg } => &self.trk[trk].trkseg[seg],
|
||||
}
|
||||
}
|
||||
|
||||
/// The segment at `index`. Panics if there is none.
|
||||
pub fn segment_mut(&mut self, index: SegmentIndex) -> &mut TrackSegment {
|
||||
match index {
|
||||
SegmentIndex::Route(rte) => &mut self.rte[rte].seg,
|
||||
SegmentIndex::Track { trk, seg } => &mut self.trk[trk].trkseg[seg],
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the file has no route nor track.
|
||||
pub fn has_no_path(&self) -> bool {
|
||||
self.rte.is_empty() && self.trk.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ mod chunk;
|
||||
mod chunked;
|
||||
mod common;
|
||||
mod file;
|
||||
mod route;
|
||||
mod segment;
|
||||
mod track;
|
||||
mod trackpoint;
|
||||
@@ -14,6 +15,7 @@ pub use chunk::*;
|
||||
pub use chunked::*;
|
||||
pub use common::*;
|
||||
pub use file::*;
|
||||
pub use route::*;
|
||||
pub use segment::*;
|
||||
pub use track::*;
|
||||
pub use trackpoint::*;
|
||||
@@ -0,0 +1,15 @@
|
||||
use crate::{TrackInfo, TrackSegment};
|
||||
|
||||
use super::common::uuid_id;
|
||||
|
||||
uuid_id!(RouteId);
|
||||
|
||||
/// A route of the file (`rte`): like a track, but with a single segment, its points.
|
||||
///
|
||||
/// It has the information of a track: GPX gives the same to both.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Route {
|
||||
pub id: RouteId,
|
||||
pub info: TrackInfo,
|
||||
pub seg: TrackSegment,
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
Author, Chunk, File, Link, LngLat, Track, TrackSegment, Trackpoint, TrackpointCategories,
|
||||
TrackpointChunk, Waypoint, WaypointChunk, distance,
|
||||
Author, Chunk, File, Link, LngLat, Route, Track, TrackSegment, Trackpoint,
|
||||
TrackpointCategories, TrackpointChunk, Waypoint, WaypointChunk, distance,
|
||||
};
|
||||
use chrono::DateTime;
|
||||
use quick_xml::Error;
|
||||
@@ -18,6 +18,7 @@ enum GPXElement {
|
||||
Author(Author),
|
||||
Link(Link),
|
||||
Text,
|
||||
/// A track, or a route, whose information is read the same way.
|
||||
Track(Track),
|
||||
Segment(TrackSegment),
|
||||
Trackpoint(Trackpoint),
|
||||
@@ -396,7 +397,7 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
|
||||
let mut stack: Vec<GPXElement> = vec![];
|
||||
let mut trkpt_chunk = TrackpointChunk::default();
|
||||
let mut wpt_chunk = WaypointChunk::default();
|
||||
// Routes are read as tracks of a single segment, see `RouteReader`. The points of the detailed
|
||||
// The segment of a route is read by `RouteReader`. The points of the detailed
|
||||
// path that a route point comes with (`gpxx:rpt`) are collected until the route point is read.
|
||||
let mut route = RouteReader::default();
|
||||
let mut rpt_points: Vec<Trackpoint> = vec![];
|
||||
@@ -530,12 +531,12 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
|
||||
}
|
||||
}
|
||||
"rte" => {
|
||||
if let Some(GPXElement::Track(mut trk)) = stack.pop() {
|
||||
let trkseg = std::mem::take(&mut route).finish();
|
||||
if !trkseg.is_empty() {
|
||||
trk.trkseg.push(trkseg);
|
||||
}
|
||||
gpx.trk.push(trk);
|
||||
if let Some(GPXElement::Track(trk)) = stack.pop() {
|
||||
gpx.rte.push(Route {
|
||||
info: trk.info,
|
||||
seg: std::mem::take(&mut route).finish(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
"rtept" => {
|
||||
@@ -740,38 +741,37 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_routes_as_tracks() {
|
||||
fn test_parse_routes() {
|
||||
let gpx = parse_data("with_routes");
|
||||
|
||||
assert_eq!(gpx.trk.len(), 2);
|
||||
let trk = &gpx.trk[0];
|
||||
assert_eq!(trk.info.name.as_deref(), Some("route 1"));
|
||||
assert_eq!(trk.info.type_.as_deref(), Some("Cycling"));
|
||||
assert_eq!(trk.trkseg.len(), 1);
|
||||
let trkseg = &trk.trkseg[0];
|
||||
assert_eq!(trkseg.len(), 49);
|
||||
let trkpt = &trkseg[0];
|
||||
assert!(gpx.trk.is_empty());
|
||||
assert_eq!(gpx.rte.len(), 2);
|
||||
let rte = &gpx.rte[0];
|
||||
assert_eq!(rte.info.name.as_deref(), Some("route 1"));
|
||||
assert_eq!(rte.info.type_.as_deref(), Some("Cycling"));
|
||||
assert_eq!(rte.seg.len(), 49);
|
||||
let trkpt = &rte.seg[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_eq!(trk.info.name.as_deref(), Some("route 2"));
|
||||
assert_eq!(trk.trkseg[0].len(), 28);
|
||||
let rte = &gpx.rte[1];
|
||||
assert_eq!(rte.info.name.as_deref(), Some("route 2"));
|
||||
assert_eq!(rte.seg.len(), 28);
|
||||
assert_ne!(gpx.rte[0].id, rte.id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_route_point_extensions() {
|
||||
let gpx = parse_data("with_route_extensions");
|
||||
|
||||
// a route without points has no segment
|
||||
assert_eq!(gpx.trk.len(), 2);
|
||||
assert!(gpx.trk[1].trkseg.is_empty());
|
||||
// a route without points has an empty segment
|
||||
assert_eq!(gpx.rte.len(), 2);
|
||||
assert!(gpx.rte[1].seg.is_empty());
|
||||
|
||||
let trk = &gpx.trk[0];
|
||||
assert_eq!(trk.info.name.as_deref(), Some("with extensions"));
|
||||
assert_eq!(trk.trkseg.len(), 1);
|
||||
let points: Vec<_> = trk.trkseg[0].iter().collect();
|
||||
let rte = &gpx.rte[0];
|
||||
assert_eq!(rte.info.name.as_deref(), Some("with extensions"));
|
||||
let points: Vec<_> = rte.seg.iter().collect();
|
||||
// the route points are kept, the points of the detailed path follow the one they come with
|
||||
let coordinates: Vec<_> = points
|
||||
.iter()
|
||||
@@ -788,14 +788,13 @@ mod tests {
|
||||
#[test]
|
||||
fn test_route_points_are_the_anchors_and_nothing_else_is() {
|
||||
let gpx = parse_data("with_route_extensions");
|
||||
let anchors: Vec<_> = gpx.trk[0].trkseg[0].iter().map(|p| p.anchor).collect();
|
||||
let anchors: Vec<_> = gpx.rte[0].seg.iter().map(|p| p.anchor).collect();
|
||||
assert_eq!(anchors, [Some(0), None, None, Some(0), Some(0)]);
|
||||
|
||||
// no anchor is computed for the points of a route that has none in between
|
||||
let gpx = parse_data("with_routes");
|
||||
for trk in &gpx.trk {
|
||||
let segment = &trk.trkseg[0];
|
||||
assert!(segment.iter().all(|p| p.anchor == Some(0)));
|
||||
for rte in &gpx.rte {
|
||||
assert!(rte.seg.iter().all(|p| p.anchor == Some(0)));
|
||||
}
|
||||
// while a track gets the anchors that its shape needs
|
||||
let track = parse_data("with_tracks");
|
||||
@@ -807,14 +806,14 @@ mod tests {
|
||||
#[test]
|
||||
fn test_parse_the_points_of_the_detailed_path_of_a_route() {
|
||||
let gpx = parse_data("with_route_points");
|
||||
assert_eq!(gpx.trk.len(), 1);
|
||||
let trk = &gpx.trk[0];
|
||||
assert_eq!(trk.info.name.as_deref(), Some("Lausanne"));
|
||||
assert_eq!(trk.info.cmt.as_deref(), Some("comment"));
|
||||
assert_eq!(trk.info.desc.as_deref(), Some("description"));
|
||||
assert_eq!(trk.info.type_.as_deref(), Some("Cycling"));
|
||||
assert_eq!(gpx.rte.len(), 1);
|
||||
let rte = &gpx.rte[0];
|
||||
assert_eq!(rte.info.name.as_deref(), Some("Lausanne"));
|
||||
assert_eq!(rte.info.cmt.as_deref(), Some("comment"));
|
||||
assert_eq!(rte.info.desc.as_deref(), Some("description"));
|
||||
assert_eq!(rte.info.type_.as_deref(), Some("Cycling"));
|
||||
|
||||
let points: Vec<_> = trk.trkseg[0].iter().cloned().collect();
|
||||
let points: Vec<_> = rte.seg.iter().cloned().collect();
|
||||
// 3 route points and 4 points between them, the repeated one being kept
|
||||
assert_eq!(points.len(), 7);
|
||||
let kinds: Vec<_> = points.iter().map(|p| p.anchor.is_some()).collect();
|
||||
@@ -846,7 +845,7 @@ mod tests {
|
||||
</rtept>
|
||||
</rte></gpx>"#;
|
||||
let gpx = parse(data.as_bytes(), &mut Default::default()).unwrap();
|
||||
let points: Vec<_> = gpx.trk[0].trkseg[0].iter().cloned().collect();
|
||||
let points: Vec<_> = gpx.rte[0].seg.iter().cloned().collect();
|
||||
let coordinates: Vec<_> = points.iter().map(|p| p.coordinates.lat).collect();
|
||||
assert_eq!(coordinates, [1.0, 2.0, 2.1, 2.2]);
|
||||
// the route point is an anchor, and so is the end of the path
|
||||
@@ -861,7 +860,7 @@ mod tests {
|
||||
<gpxx:RoutePointExtension><gpxx:rpt lat="1.5" lon="1.5"/></gpxx:RoutePointExtension>
|
||||
</extensions></rtept></rte></gpx>"#;
|
||||
let gpx = parse(data.as_bytes(), &mut Default::default()).unwrap();
|
||||
let anchors: Vec<_> = gpx.trk[0].trkseg[0].iter().map(|p| p.anchor).collect();
|
||||
let anchors: Vec<_> = gpx.rte[0].seg.iter().map(|p| p.anchor).collect();
|
||||
assert_eq!(anchors, [Some(0), Some(0)]);
|
||||
}
|
||||
|
||||
@@ -930,14 +929,14 @@ mod tests {
|
||||
assert_eq!(waypoints[0].links[2].text.as_deref(), Some("three"));
|
||||
assert!(waypoints[1].links.is_empty());
|
||||
|
||||
// tracks, and routes which are read as tracks
|
||||
assert_eq!(gpx.trk.len(), 2);
|
||||
// tracks, and routes
|
||||
assert_eq!((gpx.trk.len(), gpx.rte.len()), (1, 1));
|
||||
assert_eq!(
|
||||
hrefs(&gpx.trk[0].info.links),
|
||||
["https://example.com/trk-1", "https://example.com/trk-2"]
|
||||
);
|
||||
assert_eq!(
|
||||
hrefs(&gpx.trk[1].info.links),
|
||||
hrefs(&gpx.rte[0].info.links),
|
||||
["https://example.com/rte-1", "https://example.com/rte-2"]
|
||||
);
|
||||
}
|
||||
@@ -8,9 +8,6 @@ use crate::{File, Link, Track, Trackpoint, TrackpointCategories, Waypoint};
|
||||
|
||||
/// What the written file contains: the data of the trackpoints that are `false` are left out.
|
||||
/// The same type tells which data a file has, see [`File::exportable_data`].
|
||||
///
|
||||
/// With `as_route`, the file is written with routes instead of tracks, see [`write`]. It is not
|
||||
/// data: it is off in `ALL` and `NONE`, and in what [`File::exportable_data`] gives.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ExportOptions {
|
||||
pub time: bool,
|
||||
@@ -20,8 +17,6 @@ pub struct ExportOptions {
|
||||
pub power: bool,
|
||||
/// The OpenStreetMap data: surface, highway, SAC scale and MTB scale.
|
||||
pub osm: bool,
|
||||
/// Writes every segment as a route (`rte`) instead of a track.
|
||||
pub as_route: bool,
|
||||
}
|
||||
|
||||
impl ExportOptions {
|
||||
@@ -36,7 +31,6 @@ impl ExportOptions {
|
||||
atemp: value,
|
||||
power: value,
|
||||
osm: value,
|
||||
as_route: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +43,6 @@ impl ExportOptions {
|
||||
atemp: self.atemp || other.atemp,
|
||||
power: self.power || other.power,
|
||||
osm: self.osm || other.osm,
|
||||
as_route: self.as_route || other.as_route,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,12 +52,7 @@ impl File {
|
||||
/// options of [`write`] can leave out.
|
||||
pub fn exportable_data(&self) -> ExportOptions {
|
||||
let mut data = ExportOptions::NONE;
|
||||
for point in self
|
||||
.trk
|
||||
.iter()
|
||||
.flat_map(|trk| &trk.trkseg)
|
||||
.flat_map(|seg| seg.iter())
|
||||
{
|
||||
for point in self.segments().flat_map(|seg| seg.iter()) {
|
||||
data.time |= point.time.is_some();
|
||||
data.hr |= point.hr.is_some();
|
||||
data.cad |= point.cad.is_some();
|
||||
@@ -82,6 +70,10 @@ impl File {
|
||||
}
|
||||
}
|
||||
|
||||
/// The highest zoom level from which an anchor is shown that makes it a route point, see [`write`]:
|
||||
/// the anchors that only appear when zooming in further are details of the path.
|
||||
pub const ROUTE_POINT_MAX_ZOOM: u8 = 12;
|
||||
|
||||
const SCHEMA_LOCATION: &str = "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd";
|
||||
|
||||
type XmlWriter = Writer<Vec<u8>>;
|
||||
@@ -89,10 +81,10 @@ type XmlWriter = Writer<Vec<u8>>;
|
||||
/// Writes a file as GPX 1.1, in UTF-8. The surface, highway, SAC scale and MTB scale of the trackpoints are
|
||||
/// the names of their codes in `categories`.
|
||||
///
|
||||
/// The segments are written as tracks, or with `options.as_route` as routes: a segment becomes a
|
||||
/// `rte` that has the information of its track, its anchors are the `rtept` and the trackpoints
|
||||
/// between two anchors are the `gpxx:rpt` of the `rtept` before them (which only have a
|
||||
/// position, unlike the `rtept`). The first and last trackpoints always are `rtept`.
|
||||
/// The routes are written as routes (`rte`): the anchors of their segment that are shown from the
|
||||
/// zoom level [`ROUTE_POINT_MAX_ZOOM`] or a lower one are the `rtept`, and the trackpoints between
|
||||
/// two of them are the `gpxx:rpt` of the `rtept` before them (which only have a position, unlike
|
||||
/// the `rtept`). The first and last trackpoints always are `rtept`.
|
||||
pub fn write(file: &File, categories: &TrackpointCategories, options: ExportOptions) -> Vec<u8> {
|
||||
let mut writer = Writer::new_with_indent(Vec::new(), b' ', 4);
|
||||
// writing to memory does not fail
|
||||
@@ -100,9 +92,9 @@ pub fn write(file: &File, categories: &TrackpointCategories, options: ExportOpti
|
||||
writer.into_inner()
|
||||
}
|
||||
|
||||
fn write_file(
|
||||
fn write_file<'a>(
|
||||
w: &mut XmlWriter,
|
||||
file: &File,
|
||||
file: &'a File,
|
||||
categories: &TrackpointCategories,
|
||||
options: ExportOptions,
|
||||
) -> io::Result<()> {
|
||||
@@ -136,20 +128,18 @@ fn write_file(
|
||||
for wpt in file.wpt.iter() {
|
||||
write_waypoint(w, wpt, options)?;
|
||||
}
|
||||
for trk in &file.trk {
|
||||
// a single unnamed track is named like the file
|
||||
let name = match (&trk.info.name, file.trk.len()) {
|
||||
(Some(name), _) if !name.is_empty() => Some(name.as_str()),
|
||||
(_, 1) => Some(file.info.name.as_str()),
|
||||
// a single unnamed route or track is named like the file
|
||||
let alone = file.rte.len() + file.trk.len() == 1;
|
||||
let name = |info: &'a crate::TrackInfo| match &info.name {
|
||||
Some(name) if !name.is_empty() => Some(name.as_str()),
|
||||
_ if alone => Some(file.info.name.as_str()),
|
||||
_ => None,
|
||||
};
|
||||
if options.as_route {
|
||||
for segment in trk.trkseg.iter().filter(|segment| !segment.is_empty()) {
|
||||
write_route(w, trk, segment, name, categories, options)?;
|
||||
}
|
||||
} else {
|
||||
write_track(w, trk, name, categories, options)?;
|
||||
for rte in &file.rte {
|
||||
write_route(w, &rte.info, &rte.seg, name(&rte.info), categories, options)?;
|
||||
}
|
||||
for trk in &file.trk {
|
||||
write_track(w, trk, name(&trk.info), categories, options)?;
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
@@ -311,22 +301,28 @@ fn write_track(
|
||||
|
||||
fn write_route(
|
||||
w: &mut XmlWriter,
|
||||
trk: &Track,
|
||||
info: &crate::TrackInfo,
|
||||
segment: &crate::TrackSegment,
|
||||
name: Option<&str>,
|
||||
categories: &TrackpointCategories,
|
||||
options: ExportOptions,
|
||||
) -> io::Result<()> {
|
||||
w.create_element("rte").write_inner_content(|w| {
|
||||
write_track_info(w, name, &trk.info)?;
|
||||
write_track_info(w, name, info)?;
|
||||
let points: Vec<&Trackpoint> = segment.iter().collect();
|
||||
let last = points.len() - 1;
|
||||
let is_anchor = |i: usize| i == 0 || i == last || points[i].anchor.is_some();
|
||||
let last = points.len().saturating_sub(1);
|
||||
let is_route_point = |i: usize| {
|
||||
i == 0
|
||||
|| i == last
|
||||
|| points[i]
|
||||
.anchor
|
||||
.is_some_and(|zoom| zoom <= ROUTE_POINT_MAX_ZOOM)
|
||||
};
|
||||
let mut i = 0;
|
||||
while i < points.len() {
|
||||
// the trackpoints up to the next anchor
|
||||
// the trackpoints up to the next route point
|
||||
let mut next = i + 1;
|
||||
while next < points.len() && !is_anchor(next) {
|
||||
while next < points.len() && !is_route_point(next) {
|
||||
next += 1;
|
||||
}
|
||||
write_point(
|
||||
@@ -455,9 +451,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn points(file: &File) -> Vec<Trackpoint> {
|
||||
file.trk
|
||||
.iter()
|
||||
.flat_map(|trk| &trk.trkseg)
|
||||
file.segments()
|
||||
.flat_map(|seg| seg.iter().cloned())
|
||||
.collect()
|
||||
}
|
||||
@@ -595,11 +589,20 @@ mod tests {
|
||||
assert!(file.exportable_data().power);
|
||||
}
|
||||
|
||||
fn as_route() -> ExportOptions {
|
||||
ExportOptions {
|
||||
as_route: true,
|
||||
..ExportOptions::ALL
|
||||
}
|
||||
/// The file with each segment of its tracks as a route, which has the information of its
|
||||
/// track.
|
||||
fn as_routes(mut file: File) -> File {
|
||||
file.rte = std::mem::take(&mut file.trk)
|
||||
.into_iter()
|
||||
.flat_map(|trk| {
|
||||
trk.trkseg.into_iter().map(move |seg| crate::Route {
|
||||
info: trk.info.clone(),
|
||||
seg,
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
file
|
||||
}
|
||||
|
||||
fn count(text: &str, pattern: &str) -> usize {
|
||||
@@ -607,19 +610,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_export_as_route_writes_routes_instead_of_tracks() {
|
||||
fn test_routes_are_written_with_their_route_points_and_their_path() {
|
||||
let (file, categories) = read("with_route_points");
|
||||
let text = written_text(&file, &categories, as_route());
|
||||
let text = written_text(&file, &categories, ExportOptions::ALL);
|
||||
|
||||
assert_eq!(count(&text, "<rte>"), 1);
|
||||
assert_eq!(count(&text, "<trk>"), 0);
|
||||
assert_eq!(count(&text, "<trkseg>"), 0);
|
||||
assert_eq!(count(&text, "<trkpt"), 0);
|
||||
// the anchors are the route points, the trackpoints between them the points of their path
|
||||
assert_eq!(count(&text, "<rtept"), 3);
|
||||
assert_eq!(count(&text, "<gpxx:rpt"), 4);
|
||||
assert_eq!(count(&text, "<gpxx:RoutePointExtension>"), 2);
|
||||
// a route has the information of its track
|
||||
assert!(text.contains("<name>Lausanne</name>"));
|
||||
assert!(text.contains("<cmt>comment</cmt>"));
|
||||
assert!(text.contains("<desc>description</desc>"));
|
||||
@@ -634,14 +635,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_a_route_survives_being_exported_as_a_route() {
|
||||
fn test_a_route_survives_being_written() {
|
||||
let (file, mut categories) = read("with_route_points");
|
||||
let written = write(&file, &categories, as_route());
|
||||
let written = write(&file, &categories, ExportOptions::ALL);
|
||||
let again = parse(&written, &mut categories).unwrap();
|
||||
|
||||
assert_eq!(again.trk.len(), 1);
|
||||
assert_eq!(again.trk[0].info, file.trk[0].info);
|
||||
let (a, b) = (&file.trk[0].trkseg[0], &again.trk[0].trkseg[0]);
|
||||
assert_eq!(again.rte.len(), 1);
|
||||
assert_eq!(again.rte[0].info, file.rte[0].info);
|
||||
let (a, b) = (&file.rte[0].seg, &again.rte[0].seg);
|
||||
assert_eq!(a.len(), b.len());
|
||||
for (a, b) in a.iter().zip(b.iter()) {
|
||||
assert_eq!(a.coordinates.lat, b.coordinates.lat);
|
||||
@@ -659,85 +660,121 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tracks_become_one_route_per_segment_with_the_information_of_the_track() {
|
||||
fn test_the_anchors_shown_up_to_zoom_12_are_the_route_points() {
|
||||
let (file, mut categories) = read("with_tracks_and_segments");
|
||||
let segments: usize = file.trk.iter().map(|t| t.trkseg.len()).sum();
|
||||
assert!(segments > 2);
|
||||
let written = write(&file, &categories, as_route());
|
||||
let text = String::from_utf8(written.clone()).unwrap();
|
||||
assert_eq!(count(&text, "<rte>"), segments);
|
||||
assert_eq!(count(&text, "<trk>"), 0);
|
||||
// every route starts and ends with a route point
|
||||
assert_eq!(count(&text, "</rte>"), segments);
|
||||
|
||||
let file = as_routes(file);
|
||||
assert!(file.rte.len() > 2);
|
||||
let written = write(&file, &categories, ExportOptions::ALL);
|
||||
let again = parse(&written, &mut categories).unwrap();
|
||||
assert_eq!(again.trk.len(), segments);
|
||||
let mut routes = again.trk.iter();
|
||||
for track in &file.trk {
|
||||
for segment in &track.trkseg {
|
||||
let route = routes.next().unwrap();
|
||||
// the information of the track, repeated for each of its segments
|
||||
assert_eq!(route.info.name, track.info.name);
|
||||
assert_eq!(route.info.type_, track.info.type_);
|
||||
let (a, b) = (segment, &route.trkseg[0]);
|
||||
assert_eq!(a.len(), b.len());
|
||||
for (a, b) in a.iter().zip(b.iter()) {
|
||||
assert_eq!(
|
||||
(a.coordinates.lat, a.coordinates.lng),
|
||||
(b.coordinates.lat, b.coordinates.lng)
|
||||
);
|
||||
}
|
||||
// the anchors of the segment are the ones of its route
|
||||
let anchors = |s: &crate::TrackSegment| {
|
||||
assert_eq!(again.rte.len(), file.rte.len());
|
||||
let anchors = |s: &crate::TrackSegment, max_zoom: u8| {
|
||||
s.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, p)| p.anchor.is_some())
|
||||
.filter(|(_, p)| p.anchor.is_some_and(|zoom| zoom <= max_zoom))
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(anchors(a), anchors(b));
|
||||
}
|
||||
for (a, b) in file.rte.iter().zip(&again.rte) {
|
||||
assert_eq!(a.info.name, b.info.name);
|
||||
assert_eq!(a.seg.len(), b.seg.len());
|
||||
// the route points come back as anchors, the other anchors as points of the path
|
||||
assert_eq!(
|
||||
anchors(&a.seg, ROUTE_POINT_MAX_ZOOM),
|
||||
anchors(&b.seg, u8::MAX)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anchors_shown_from_a_higher_zoom_are_points_of_the_path() {
|
||||
let (file, categories) = read("simple");
|
||||
let mut file = as_routes(file);
|
||||
let segment = &mut file.rte[0].seg;
|
||||
segment.update_all(|_, point| point.anchor = None);
|
||||
let len = segment.len();
|
||||
segment.set_anchor(5, ROUTE_POINT_MAX_ZOOM);
|
||||
segment.set_anchor(10, ROUTE_POINT_MAX_ZOOM + 1);
|
||||
let text = written_text(&file, &categories, ExportOptions::ALL);
|
||||
// the ends, and the anchor shown from zoom 12
|
||||
assert_eq!(count(&text, "<rtept"), 3);
|
||||
assert_eq!(count(&text, "<gpxx:rpt"), len - 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_ends_of_a_route_are_route_points_even_without_anchors() {
|
||||
let (mut file, categories) = read("simple");
|
||||
file.trk[0].trkseg[0].update_all(|_, point| point.anchor = None);
|
||||
let len = file.trk[0].trkseg[0].len();
|
||||
let text = written_text(&file, &categories, as_route());
|
||||
let (file, categories) = read("simple");
|
||||
let mut file = as_routes(file);
|
||||
file.rte[0].seg.update_all(|_, point| point.anchor = None);
|
||||
let len = file.rte[0].seg.len();
|
||||
let text = written_text(&file, &categories, ExportOptions::ALL);
|
||||
assert_eq!(count(&text, "<rtept"), 2);
|
||||
assert_eq!(count(&text, "<gpxx:rpt"), len - 2);
|
||||
// and a single trackpoint is a route of one point
|
||||
file.trk[0].trkseg[0].splice(1, len, vec![]);
|
||||
let text = written_text(&file, &categories, as_route());
|
||||
file.rte[0].seg.splice(1, len, vec![]);
|
||||
let text = written_text(&file, &categories, ExportOptions::ALL);
|
||||
assert_eq!(count(&text, "<rtept"), 1);
|
||||
assert_eq!(count(&text, "<gpxx:rpt"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_segments_are_not_routes() {
|
||||
let (mut file, categories) = read("simple");
|
||||
file.trk[0].trkseg.push(crate::TrackSegment::default());
|
||||
let text = written_text(&file, &categories, as_route());
|
||||
assert_eq!(count(&text, "<rte>"), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_routes_have_the_style_of_their_track() {
|
||||
fn test_routes_keep_their_style() {
|
||||
let (file, categories) = read("with_style");
|
||||
let text = written_text(&file, &categories, as_route());
|
||||
let file = as_routes(file);
|
||||
let text = written_text(&file, &categories, ExportOptions::ALL);
|
||||
assert!(count(&text, "<gpx_style:line>") >= 1);
|
||||
assert_eq!(count(&text, "<gpx_style:line>"), count(&text, "<rte>"));
|
||||
let again = parse(text.as_bytes(), &mut Default::default()).unwrap();
|
||||
assert_eq!(again.trk[0].info.color, file.trk[0].info.color);
|
||||
assert_eq!(again.trk[0].info.width, file.trk[0].info.width);
|
||||
assert_eq!(again.rte[0].info.color, file.rte[0].info.color);
|
||||
assert_eq!(again.rte[0].info.width, file.rte[0].info.width);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_routes_are_written_as_routes_before_the_tracks() {
|
||||
let (mut file, categories) = read("with_tracks");
|
||||
let (routes, _) = read("with_routes");
|
||||
file.rte = routes.rte.clone();
|
||||
let text = written_text(&file, &categories, ExportOptions::ALL);
|
||||
assert_eq!(count(&text, "<rte>"), routes.rte.len());
|
||||
assert_eq!(count(&text, "<trk>"), file.trk.len());
|
||||
assert!(text.rfind("</rte>").unwrap() < text.find("<trk>").unwrap());
|
||||
|
||||
let again = parse(text.as_bytes(), &mut categories.clone()).unwrap();
|
||||
assert_eq!(again.rte.len(), file.rte.len());
|
||||
assert_eq!(again.trk.len(), file.trk.len());
|
||||
for (a, b) in file.rte.iter().zip(&again.rte) {
|
||||
assert_eq!(a.info, b.info);
|
||||
same_points(
|
||||
&a.seg.iter().cloned().collect::<Vec<_>>(),
|
||||
&b.seg.iter().cloned().collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
same_points(&points(&file), &points(&again));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_an_empty_route_is_written_and_a_single_route_is_named_like_the_file() {
|
||||
let mut file = File::default();
|
||||
file.info.name = "file".into();
|
||||
file.rte.push(crate::Route::default());
|
||||
let text = written_text(&file, &Default::default(), ExportOptions::ALL);
|
||||
assert_eq!(count(&text, "<rte>"), 1);
|
||||
assert_eq!(count(&text, "<rtept"), 0);
|
||||
let again = parse(text.as_bytes(), &mut Default::default()).unwrap();
|
||||
assert_eq!(again.rte.len(), 1);
|
||||
assert_eq!(again.rte[0].info.name.as_deref(), Some("file"));
|
||||
|
||||
// not when there are others
|
||||
file.trk.push(Track::default());
|
||||
let text = written_text(&file, &Default::default(), ExportOptions::ALL);
|
||||
// the metadata only
|
||||
assert_eq!(count(&text, "<name>file</name>"), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_points_keep_their_data_and_the_options_apply_to_them() {
|
||||
let (file, categories) = read("with_hr");
|
||||
let with_all = written_text(&file, &categories, as_route());
|
||||
let file = as_routes(file);
|
||||
let with_all = written_text(&file, &categories, ExportOptions::ALL);
|
||||
assert!(with_all.contains("<gpxtpx:hr>"));
|
||||
let without = written_text(
|
||||
&file,
|
||||
@@ -745,22 +782,10 @@ mod tests {
|
||||
ExportOptions {
|
||||
hr: false,
|
||||
time: false,
|
||||
as_route: true,
|
||||
..ExportOptions::ALL
|
||||
},
|
||||
);
|
||||
assert!(!without.contains("<gpxtpx:hr>"));
|
||||
assert!(!without.contains("<time>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_route_option_is_not_data() {
|
||||
let (file, _) = read("with_route_points");
|
||||
assert!(!file.exportable_data().as_route);
|
||||
// it is a way to write the data, never part of them
|
||||
let all = std::hint::black_box(ExportOptions::ALL);
|
||||
assert!(!all.as_route && !ExportOptions::NONE.as_route);
|
||||
assert!(as_route().union(ExportOptions::NONE).as_route);
|
||||
assert!(!ExportOptions::ALL.union(ExportOptions::NONE).as_route);
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -36,4 +36,11 @@ pub enum Action<'a> {
|
||||
file_ids: Vec<FileId>,
|
||||
index: usize,
|
||||
},
|
||||
/// Names a file in all the states of the history, as if it had always been called so (see
|
||||
/// [`crate::History::rename`]). Not undoable: for a file that is given a name from the outside,
|
||||
/// like the name of the file it is written to.
|
||||
Rename {
|
||||
file_id: FileId,
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,128 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{Apply, CommandError, FileId, Route, Selection, State, Track};
|
||||
|
||||
/// Converts the selected routes to tracks, or the selected tracks or segments to routes, in their
|
||||
/// file:
|
||||
///
|
||||
/// - a route becomes a track with its segment. The new tracks go before the other tracks, right
|
||||
/// after the routes;
|
||||
/// - a track becomes one route per segment, as a route has a single segment, and goes away. A
|
||||
/// selected segment becomes a route and leaves its track, which stays even if it has no segment
|
||||
/// left. The routes have the information of their track, and go after the other routes.
|
||||
///
|
||||
/// The segments keep their ids, the new routes and tracks are selected. There is nothing to do
|
||||
/// for the other selections, or for tracks without segments.
|
||||
#[derive(Debug)]
|
||||
pub struct Convert;
|
||||
|
||||
impl Apply for Convert {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let selection = state.selection.clone();
|
||||
let next = match &selection {
|
||||
Selection::Route { file_id, rte_ids } => {
|
||||
let file = Rc::make_mut(
|
||||
state
|
||||
.files
|
||||
.get_mut(file_id)
|
||||
.ok_or(CommandError::NothingToDo)?,
|
||||
);
|
||||
let (converted, kept) = std::mem::take(&mut file.rte)
|
||||
.into_iter()
|
||||
.partition::<Vec<_>, _>(|rte| rte_ids.contains(&rte.id));
|
||||
file.rte = kept;
|
||||
let tracks: Vec<Track> = converted
|
||||
.into_iter()
|
||||
.map(|rte| Track {
|
||||
id: Default::default(),
|
||||
info: rte.info,
|
||||
trkseg: vec![rte.seg],
|
||||
})
|
||||
.collect();
|
||||
let trk_ids: HashSet<_> = tracks.iter().map(|trk| trk.id).collect();
|
||||
file.trk.splice(0..0, tracks);
|
||||
Selection::Track {
|
||||
file_id: *file_id,
|
||||
trk_ids,
|
||||
}
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = Rc::make_mut(
|
||||
state
|
||||
.files
|
||||
.get_mut(file_id)
|
||||
.ok_or(CommandError::NothingToDo)?,
|
||||
);
|
||||
let (converted, kept) = std::mem::take(&mut file.trk)
|
||||
.into_iter()
|
||||
.partition::<Vec<_>, _>(|trk| trk_ids.contains(&trk.id));
|
||||
file.trk = kept;
|
||||
let routes = converted.into_iter().flat_map(|trk| {
|
||||
trk.trkseg.into_iter().map(move |seg| Route {
|
||||
id: Default::default(),
|
||||
info: trk.info.clone(),
|
||||
seg,
|
||||
})
|
||||
});
|
||||
add_routes(&mut file.rte, *file_id, routes)
|
||||
}
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
trk_id,
|
||||
trkseg_ids,
|
||||
} => {
|
||||
let file = Rc::make_mut(
|
||||
state
|
||||
.files
|
||||
.get_mut(file_id)
|
||||
.ok_or(CommandError::NothingToDo)?,
|
||||
);
|
||||
let trk = file
|
||||
.trk
|
||||
.iter_mut()
|
||||
.find(|trk| trk.id == *trk_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let (converted, kept) = std::mem::take(&mut trk.trkseg)
|
||||
.into_iter()
|
||||
.partition::<Vec<_>, _>(|seg| trkseg_ids.contains(&seg.id));
|
||||
trk.trkseg = kept;
|
||||
let info = trk.info.clone();
|
||||
let routes = converted.into_iter().map(|seg| Route {
|
||||
id: Default::default(),
|
||||
info: info.clone(),
|
||||
seg,
|
||||
});
|
||||
add_routes(&mut file.rte, *file_id, routes)
|
||||
}
|
||||
Selection::Empty
|
||||
| Selection::File { .. }
|
||||
| Selection::Waypoints { .. }
|
||||
| Selection::Waypoint { .. } => return Err(CommandError::NothingToDo),
|
||||
};
|
||||
let empty = match &next {
|
||||
Selection::Route { rte_ids, .. } => rte_ids.is_empty(),
|
||||
Selection::Track { trk_ids, .. } => trk_ids.is_empty(),
|
||||
_ => true,
|
||||
};
|
||||
if empty {
|
||||
// nothing was converted (the changed copy of the file is dropped)
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
*state.selection = next;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the routes after the other routes, and gives the selection of them.
|
||||
fn add_routes(
|
||||
rte: &mut Vec<Route>,
|
||||
file_id: FileId,
|
||||
routes: impl Iterator<Item = Route>,
|
||||
) -> Selection {
|
||||
let start = rte.len();
|
||||
rte.extend(routes);
|
||||
Selection::Route {
|
||||
file_id,
|
||||
rte_ids: rte[start..].iter().map(|rte| rte.id).collect(),
|
||||
}
|
||||
}
|
||||
+24
-7
@@ -1,4 +1,6 @@
|
||||
use crate::{Apply, CommandError, Edit, Editor, File, State, Track, update_selected};
|
||||
use crate::{
|
||||
Apply, CommandError, Edit, Editor, File, Route, State, Track, TrackInfo, update_selected,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Metadata<'a> {
|
||||
@@ -13,21 +15,33 @@ impl Apply for Metadata<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Metadata<'_> {
|
||||
fn set(&self, info: &mut TrackInfo) -> Edit {
|
||||
info.name = Some(self.name.to_owned());
|
||||
info.desc = Some(self.desc.to_owned());
|
||||
Edit::Changed
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
// a single route or track is named like the file
|
||||
match (file.rte.as_mut_slice(), file.trk.as_mut_slice()) {
|
||||
([route], []) => route.info.name = Some(self.name.to_owned()),
|
||||
([], [track]) => track.info.name = Some(self.name.to_owned()),
|
||||
_ => {}
|
||||
}
|
||||
Edit::Changed
|
||||
}
|
||||
|
||||
fn route(&mut self, route: &mut Route) -> Edit {
|
||||
self.set(&mut route.info)
|
||||
}
|
||||
|
||||
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
|
||||
self.set(&mut track.info)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +59,14 @@ mod tests {
|
||||
New {
|
||||
name: "first",
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
New {
|
||||
name: "second",
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap(); // now selected
|
||||
@@ -108,6 +124,7 @@ mod tests {
|
||||
New {
|
||||
name: "file",
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
@@ -1,9 +1,13 @@
|
||||
mod convert;
|
||||
mod metadata;
|
||||
mod new_route;
|
||||
mod new_track;
|
||||
mod new_track_segment;
|
||||
mod style;
|
||||
|
||||
pub use convert::*;
|
||||
pub use metadata::*;
|
||||
pub use new_route::*;
|
||||
pub use new_track::*;
|
||||
pub use new_track_segment::*;
|
||||
pub use style::*;
|
||||
@@ -0,0 +1,55 @@
|
||||
use crate::{Apply, CommandError, Edit, Editor, File, Route, State, update_selected};
|
||||
|
||||
use super::new_track::common_style;
|
||||
|
||||
/// Adds an empty route to the selected files, with the style that their routes and tracks agree
|
||||
/// on (see [`crate::NewTrack`]).
|
||||
#[derive(Debug)]
|
||||
pub struct NewRoute;
|
||||
|
||||
impl Apply for NewRoute {
|
||||
fn apply(mut self, state: &mut State) -> Result<(), CommandError> {
|
||||
update_selected(state, &mut self);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor for NewRoute {
|
||||
fn file(&mut self, file: &mut File) -> Edit {
|
||||
file.rte.push(Route {
|
||||
info: common_style(file),
|
||||
..Default::default()
|
||||
});
|
||||
Edit::Changed
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{New, engine::command::fixture::Fixture};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_route_has_the_style_of_the_tracks_and_an_empty_segment() {
|
||||
let mut fx = Fixture::default();
|
||||
New {
|
||||
name: "file",
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
crate::NewTrack.apply(&mut fx.state()).unwrap();
|
||||
let file = std::rc::Rc::make_mut(fx.files.values_mut().next().unwrap());
|
||||
file.trk[0].info.color = Some("ff0000".into());
|
||||
NewRoute.apply(&mut fx.state()).unwrap();
|
||||
NewRoute.apply(&mut fx.state()).unwrap();
|
||||
|
||||
let file = fx.files.values().next().unwrap();
|
||||
assert_eq!(file.rte.len(), 2);
|
||||
assert_ne!(file.rte[0].id, file.rte[1].id);
|
||||
assert!(file.rte[0].seg.is_empty());
|
||||
assert_eq!(file.rte[1].info.color.as_deref(), Some("ff0000"));
|
||||
}
|
||||
}
|
||||
+34
-11
@@ -1,4 +1,4 @@
|
||||
use crate::{Apply, CommandError, Edit, Editor, File, State, Track, update_selected};
|
||||
use crate::{Apply, CommandError, Edit, Editor, File, State, Track, TrackInfo, update_selected};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NewTrack;
|
||||
@@ -12,20 +12,41 @@ impl Apply for NewTrack {
|
||||
|
||||
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);
|
||||
file.trk.push(Track {
|
||||
info: common_style(file),
|
||||
..Default::default()
|
||||
});
|
||||
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())?
|
||||
/// The style that all the routes and tracks of the file agree on: each attribute is set if they
|
||||
/// all have the same value for it.
|
||||
pub(super) fn common_style(file: &File) -> TrackInfo {
|
||||
let infos: Vec<&TrackInfo> = file
|
||||
.rte
|
||||
.iter()
|
||||
.map(|rte| &rte.info)
|
||||
.chain(file.trk.iter().map(|trk| &trk.info))
|
||||
.collect();
|
||||
TrackInfo {
|
||||
color: common(&infos, |info| &info.color),
|
||||
opacity: common(&infos, |info| &info.opacity),
|
||||
width: common(&infos, |info| &info.width),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the value of a style attribute if all the infos agree on it.
|
||||
fn common<T: Clone + PartialEq>(
|
||||
infos: &[&TrackInfo],
|
||||
get: impl Fn(&TrackInfo) -> &Option<T>,
|
||||
) -> Option<T> {
|
||||
let mut infos = infos.iter();
|
||||
let first = get(infos.next()?);
|
||||
infos
|
||||
.all(|info| get(info) == first)
|
||||
.then(|| first.clone())?
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -40,6 +61,7 @@ mod tests {
|
||||
New {
|
||||
name: "file",
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
@@ -56,6 +78,7 @@ mod tests {
|
||||
New {
|
||||
name: "file",
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
+1
@@ -29,6 +29,7 @@ mod tests {
|
||||
New {
|
||||
name: "file",
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
+16
-6
@@ -1,4 +1,4 @@
|
||||
use crate::{Apply, CommandError, Edit, Editor, State, Track, update_selected};
|
||||
use crate::{Apply, CommandError, Edit, Editor, Route, State, Track, TrackInfo, update_selected};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Style<'a> {
|
||||
@@ -14,21 +14,31 @@ impl Apply for Style<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor for Style<'_> {
|
||||
fn track(&mut self, track: &mut Track) -> Edit {
|
||||
impl Style<'_> {
|
||||
fn set(&self, info: &mut TrackInfo) -> Edit {
|
||||
if let Some(color) = self.color {
|
||||
track.info.color = Some(color.to_owned());
|
||||
info.color = Some(color.to_owned());
|
||||
}
|
||||
if self.opacity.is_some() {
|
||||
track.info.opacity = self.opacity;
|
||||
info.opacity = self.opacity;
|
||||
}
|
||||
if self.width.is_some() {
|
||||
track.info.width = self.width;
|
||||
info.width = self.width;
|
||||
}
|
||||
Edit::Changed
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor for Style<'_> {
|
||||
fn route(&mut self, route: &mut Route) -> Edit {
|
||||
self.set(&mut route.info)
|
||||
}
|
||||
|
||||
fn track(&mut self, track: &mut Track) -> Edit {
|
||||
self.set(&mut track.info)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
+18
-1
@@ -3,7 +3,7 @@ use std::rc::Rc;
|
||||
use crate::{Apply, CommandError, FileId, Files, Selection, State, Waypoint};
|
||||
|
||||
/// 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.
|
||||
/// are deleted instead, even if only a route, a track or a waypoint is selected.
|
||||
#[derive(Debug)]
|
||||
pub struct Delete {
|
||||
pub whole_files: bool,
|
||||
@@ -23,6 +23,21 @@ impl Apply for Delete {
|
||||
state.order.0.retain(|id| !file_ids.contains(id));
|
||||
Selection::Empty
|
||||
}
|
||||
Selection::Route { file_id, rte_ids } => {
|
||||
let file = state
|
||||
.files
|
||||
.get_mut(file_id)
|
||||
.ok_or(CommandError::NothingToDo)?;
|
||||
let file = Rc::make_mut(file);
|
||||
let len = file.rte.len();
|
||||
file.rte.retain(|rte| !rte_ids.contains(&rte.id));
|
||||
if file.rte.len() == len {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
Selection::File {
|
||||
file_ids: [*file_id].into(),
|
||||
}
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = state
|
||||
.files
|
||||
@@ -131,6 +146,7 @@ mod tests {
|
||||
crate::New {
|
||||
name: "keep",
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
@@ -265,6 +281,7 @@ mod tests {
|
||||
crate::New {
|
||||
name: "keep",
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
+2
@@ -31,12 +31,14 @@ mod tests {
|
||||
crate::New {
|
||||
name: "a",
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
crate::New {
|
||||
name: "b",
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
File renamed without changes.
+27
-2
@@ -1,8 +1,8 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, FileId, Files, Selection, State, Waypoint, copy_file, copy_segment,
|
||||
copy_track, copy_waypoint,
|
||||
Apply, CommandError, FileId, Files, Selection, State, Waypoint, copy_file, copy_route,
|
||||
copy_segment, copy_track, copy_waypoint,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -35,6 +35,30 @@ impl Apply for Duplicate {
|
||||
state.order.0.splice(at..at, order);
|
||||
Selection::File { file_ids: copies }
|
||||
}
|
||||
Selection::Route { file_id, rte_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.rte,
|
||||
|rte| rte_ids.contains(&rte.id),
|
||||
|rte| {
|
||||
let copy = copy_route(rte);
|
||||
copies.insert(copy.id);
|
||||
copy
|
||||
},
|
||||
);
|
||||
if copies.is_empty() {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
Selection::Route {
|
||||
file_id: *file_id,
|
||||
rte_ids: copies,
|
||||
}
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let file = state
|
||||
.files
|
||||
@@ -193,6 +217,7 @@ mod tests {
|
||||
crate::New {
|
||||
name,
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
+4
-1
@@ -9,7 +9,7 @@ pub struct Load<'a> {
|
||||
}
|
||||
|
||||
impl Load<'_> {
|
||||
fn parse(self, categories: &mut TrackpointCategories) -> Result<File, CommandError> {
|
||||
pub(crate) 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() {
|
||||
@@ -85,6 +85,7 @@ mod tests {
|
||||
crate::New {
|
||||
name: "keep",
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
@@ -140,6 +141,7 @@ mod tests {
|
||||
crate::New {
|
||||
name: "before",
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
@@ -189,6 +191,7 @@ mod tests {
|
||||
crate::New {
|
||||
name: "keep",
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
@@ -6,6 +6,7 @@ mod load;
|
||||
mod move_elements;
|
||||
mod new;
|
||||
mod paste;
|
||||
mod sync;
|
||||
mod transfer;
|
||||
|
||||
pub use delete::*;
|
||||
@@ -16,4 +17,5 @@ pub use load::*;
|
||||
pub use move_elements::*;
|
||||
pub use new::*;
|
||||
pub use paste::*;
|
||||
pub use sync::*;
|
||||
pub use transfer::*;
|
||||
+9
-3
@@ -9,6 +9,8 @@ use crate::{
|
||||
pub enum MoveTarget {
|
||||
/// The list of files.
|
||||
Files { index: usize },
|
||||
/// The routes of a file.
|
||||
Routes { file_id: FileId, index: usize },
|
||||
/// The tracks of a file.
|
||||
Tracks { file_id: FileId, index: usize },
|
||||
/// The segments of a track.
|
||||
@@ -26,9 +28,10 @@ pub enum MoveTarget {
|
||||
///
|
||||
/// 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;
|
||||
/// - routes and tracks: among the files (each one becomes a file), or among the routes or the
|
||||
/// tracks of a file, where they keep their kind (see [`Destination`]);
|
||||
/// - segments: among the files (each segment becomes a file), among the routes or the tracks of a
|
||||
/// file (each segment becomes a route or 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)]
|
||||
@@ -41,6 +44,9 @@ impl From<MoveTarget> for Destination {
|
||||
fn from(target: MoveTarget) -> Self {
|
||||
match target {
|
||||
MoveTarget::Files { index } => Destination::Files(Place::Index(index)),
|
||||
MoveTarget::Routes { file_id, index } => {
|
||||
Destination::Routes(file_id, Place::Index(index))
|
||||
}
|
||||
MoveTarget::Tracks { file_id, index } => {
|
||||
Destination::Tracks(file_id, Place::Index(index))
|
||||
}
|
||||
+53
-6
@@ -1,12 +1,16 @@
|
||||
use crate::{Apply, CommandError, File, LngLat, State, Track, TrackSegment, Trackpoint, produce};
|
||||
use crate::{
|
||||
Apply, CommandError, File, LngLat, Route, State, Track, TrackSegment, Trackpoint, produce,
|
||||
};
|
||||
|
||||
/// Creates an empty file and selects it. With a `trackpoint`, the file starts with a track and a
|
||||
/// segment that hold it: it is how the routing tool starts a route on an empty map, in a single
|
||||
/// undo step.
|
||||
/// Creates an empty file and selects it. With `route`, the file starts with a route, which is
|
||||
/// empty. With a `trackpoint`, the file starts with a segment that holds it, the one of its route
|
||||
/// with `route` or of a track otherwise: it is how the routing tool starts a route on an empty map,
|
||||
/// in a single undo step.
|
||||
#[derive(Debug)]
|
||||
pub struct New<'a> {
|
||||
pub name: &'a str,
|
||||
pub trackpoint: Option<NewTrackpoint>,
|
||||
pub route: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -21,8 +25,8 @@ impl Apply for New<'_> {
|
||||
produce(state, |_| {
|
||||
let mut file = File::default();
|
||||
file.info.name = self.name.to_owned();
|
||||
if let Some(point) = self.trackpoint {
|
||||
let mut segment = TrackSegment::default();
|
||||
if let Some(point) = self.trackpoint {
|
||||
// the ends of a segment are anchors of the routing tool
|
||||
segment.splice(
|
||||
0,
|
||||
@@ -36,6 +40,13 @@ impl Apply for New<'_> {
|
||||
..Default::default()
|
||||
}],
|
||||
);
|
||||
}
|
||||
if self.route {
|
||||
file.rte.push(Route {
|
||||
seg: segment,
|
||||
..Default::default()
|
||||
});
|
||||
} else if self.trackpoint.is_some() {
|
||||
file.trk.push(Track {
|
||||
trkseg: vec![segment],
|
||||
..Default::default()
|
||||
@@ -59,7 +70,8 @@ mod tests {
|
||||
assert!(
|
||||
New {
|
||||
name: "new",
|
||||
trackpoint: None
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.is_ok()
|
||||
@@ -83,6 +95,7 @@ mod tests {
|
||||
New {
|
||||
name: "new",
|
||||
trackpoint: Some(point),
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
@@ -98,4 +111,38 @@ mod tests {
|
||||
assert_eq!(segment[0].anchor, Some(0));
|
||||
assert_eq!(fx.selected_files(), [file.id].into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_with_a_route() {
|
||||
let mut fx = Fixture::default();
|
||||
New {
|
||||
name: "new",
|
||||
trackpoint: None,
|
||||
route: true,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = fx.files.values().next().unwrap();
|
||||
assert_eq!((file.rte.len(), file.trk.len()), (1, 0));
|
||||
assert!(file.rte[0].seg.is_empty());
|
||||
assert_eq!(fx.selected_files(), [file.id].into());
|
||||
|
||||
// the trackpoint goes to the route
|
||||
let mut fx = Fixture::default();
|
||||
New {
|
||||
name: "new",
|
||||
trackpoint: Some(NewTrackpoint {
|
||||
lng: 1.0,
|
||||
lat: 2.0,
|
||||
ele: 3.0,
|
||||
}),
|
||||
route: true,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = fx.files.values().next().unwrap();
|
||||
assert_eq!((file.rte.len(), file.trk.len()), (1, 0));
|
||||
assert_eq!(file.rte[0].seg.len(), 1);
|
||||
assert_eq!(file.rte[0].seg[0].anchor, Some(0));
|
||||
}
|
||||
}
|
||||
+41
-10
@@ -1,18 +1,21 @@
|
||||
use std::{collections::HashSet, hash::Hash};
|
||||
|
||||
use crate::{
|
||||
Apply, ClipboardContent, CommandError, Destination, FileId, Place, Selection, State, TrackId,
|
||||
TrackSegmentId, WaypointId, transfer, waypoint_ids,
|
||||
Apply, ClipboardContent, CommandError, Destination, FileId, Place, RouteId, 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;
|
||||
/// - nothing selected: the files are copied, and routes, tracks and segments become new files
|
||||
/// (named like their route or track, or like the file they come from);
|
||||
/// - a file: the routes and tracks are added to it, segments become new tracks in it, and the
|
||||
/// waypoints are added to its own;
|
||||
/// - a route: routes, and segments as new routes, go after it in its file, tracks go before the
|
||||
/// other tracks (right after the routes);
|
||||
/// - a track: the segments are added to it, tracks go after it in its file, and routes after the
|
||||
/// other routes;
|
||||
/// - 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.
|
||||
///
|
||||
@@ -46,6 +49,14 @@ impl Apply for Paste {
|
||||
}
|
||||
}
|
||||
|
||||
fn route_ids_of(state: &State, file_id: FileId) -> Vec<RouteId> {
|
||||
state
|
||||
.files
|
||||
.get(&file_id)
|
||||
.map(|file| file.rte.iter().map(|rte| rte.id).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn track_ids_of(state: &State, file_id: FileId) -> Vec<TrackId> {
|
||||
state
|
||||
.files
|
||||
@@ -79,14 +90,34 @@ fn destination(state: &State, content: &ClipboardContent) -> Result<Destination,
|
||||
};
|
||||
Ok(match (content, &*state.selection) {
|
||||
(ClipboardContent::Files(_), _) => Destination::Files(Place::End),
|
||||
(ClipboardContent::Tracks(_) | ClipboardContent::Segments(_), Selection::Empty) => {
|
||||
Destination::Files(Place::End)
|
||||
(
|
||||
ClipboardContent::Routes(_)
|
||||
| ClipboardContent::Tracks(_)
|
||||
| ClipboardContent::Segments(_),
|
||||
Selection::Empty,
|
||||
) => Destination::Files(Place::End),
|
||||
(ClipboardContent::Routes(_), Selection::File { file_ids }) => {
|
||||
Destination::Routes(last_file(file_ids)?, Place::End)
|
||||
}
|
||||
// routes and tracks keep their kind (see `Destination`)
|
||||
(
|
||||
ClipboardContent::Routes(_)
|
||||
| ClipboardContent::Tracks(_)
|
||||
| ClipboardContent::Segments(_),
|
||||
Selection::Route { file_id, rte_ids },
|
||||
) => {
|
||||
let last =
|
||||
last_selected(route_ids_of(state, *file_id).into_iter(), rte_ids).ok_or(nothing)?;
|
||||
Destination::Routes(*file_id, Place::After(last))
|
||||
}
|
||||
(
|
||||
ClipboardContent::Tracks(_) | ClipboardContent::Segments(_),
|
||||
Selection::File { file_ids },
|
||||
) => Destination::Tracks(last_file(file_ids)?, Place::End),
|
||||
(ClipboardContent::Tracks(_), Selection::Track { file_id, trk_ids }) => {
|
||||
(
|
||||
ClipboardContent::Routes(_) | 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))
|
||||
@@ -0,0 +1,155 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{Apply, CommandError, FileId, Load, State};
|
||||
|
||||
/// A file that comes from the outside (the file system), under the identity it has here.
|
||||
#[derive(Debug)]
|
||||
pub struct ExternalFile<'a> {
|
||||
pub id: FileId,
|
||||
pub data: &'a [u8],
|
||||
/// Name of the file outside (the name of the file on the disk, without its extension), which
|
||||
/// is its name here whatever the data says: a file has a single name.
|
||||
pub name: &'a str,
|
||||
}
|
||||
|
||||
/// Brings changes of the files from the outside in, as a single command (one undo step): the files
|
||||
/// that are given replace the ones of the same id (which stay where they are in the order, and keep
|
||||
/// their id), or are added at the end if there are none; the ones to `remove` are deleted.
|
||||
///
|
||||
/// Unlike [`crate::LoadFiles`], the files are named as they are outside (see [`ExternalFile`]), and
|
||||
/// the selection is left alone (what it holds of a file that was
|
||||
/// replaced or deleted is dropped by the engine afterwards, as with any edit).
|
||||
///
|
||||
/// The files that cannot be read are skipped, and the files that are here stay as they are. It is an
|
||||
/// error when nothing could be done.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SyncFiles<'a> {
|
||||
pub files: Vec<ExternalFile<'a>>,
|
||||
pub remove: Vec<FileId>,
|
||||
}
|
||||
|
||||
impl Apply for SyncFiles<'_> {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let mut changed = false;
|
||||
let mut first_error = None;
|
||||
for external in self.files {
|
||||
let load = Load {
|
||||
data: external.data,
|
||||
name: external.name,
|
||||
};
|
||||
match load.parse(state.categories) {
|
||||
Ok(mut file) => {
|
||||
file.id = external.id;
|
||||
if !external.name.trim().is_empty() {
|
||||
file.info.name = external.name.to_owned();
|
||||
}
|
||||
if state.files.insert(external.id, Rc::new(file)).is_none() {
|
||||
state.order.0.push(external.id);
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
Err(error) => {
|
||||
first_error.get_or_insert(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
for id in self.remove {
|
||||
if state.files.remove(&id).is_some() {
|
||||
state.order.0.retain(|other| *other != id);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(first_error.unwrap_or(CommandError::NothingToDo))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::engine::command::fixture::Fixture;
|
||||
use crate::{New, Selection};
|
||||
|
||||
use super::*;
|
||||
|
||||
const GPX: &[u8] = br#"<gpx version="1.1"><metadata><name>from disk</name></metadata></gpx>"#;
|
||||
|
||||
fn new_file(fx: &mut Fixture, name: &str) -> FileId {
|
||||
New {
|
||||
name,
|
||||
trackpoint: None,
|
||||
route: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
*fx.selected_files().iter().next().unwrap()
|
||||
}
|
||||
|
||||
fn external(id: FileId) -> ExternalFile<'static> {
|
||||
ExternalFile {
|
||||
id,
|
||||
data: GPX,
|
||||
name: "name",
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_replaces_a_file_in_place() {
|
||||
let mut fx = Fixture::default();
|
||||
let first = new_file(&mut fx, "first");
|
||||
let second = new_file(&mut fx, "second");
|
||||
SyncFiles {
|
||||
files: vec![external(first)],
|
||||
remove: vec![],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(fx.order.0, [first, second]);
|
||||
// named as it is outside, not as the data says
|
||||
assert_eq!(fx.files[&first].info.name, "name");
|
||||
assert_eq!(fx.files[&first].id, first);
|
||||
// the selection is not changed
|
||||
assert_eq!(fx.selected_files(), [second].into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_adds_and_removes_files() {
|
||||
let mut fx = Fixture::default();
|
||||
let first = new_file(&mut fx, "first");
|
||||
let added = FileId::default();
|
||||
SyncFiles {
|
||||
files: vec![external(added)],
|
||||
remove: vec![first],
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(fx.order.0, [added]);
|
||||
assert!(!fx.files.contains_key(&first));
|
||||
// what was selected is gone
|
||||
assert!(matches!(fx.selection, Selection::File { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync_with_nothing_to_do_or_invalid_data_is_an_error() {
|
||||
let mut fx = Fixture::default();
|
||||
assert_eq!(
|
||||
SyncFiles::default().apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
let invalid = SyncFiles {
|
||||
files: vec![ExternalFile {
|
||||
id: FileId::default(),
|
||||
data: b"<gpx><trk></gpx>",
|
||||
name: "x",
|
||||
}],
|
||||
remove: vec![],
|
||||
};
|
||||
assert!(matches!(
|
||||
invalid.apply(&mut fx.state()),
|
||||
Err(CommandError::InvalidData(_))
|
||||
));
|
||||
assert!(fx.files.is_empty());
|
||||
}
|
||||
}
|
||||
+131
-30
@@ -5,9 +5,9 @@
|
||||
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,
|
||||
ClipboardContent, ClipboardRoute, ClipboardSegment, ClipboardTrack, CommandError, File, FileId,
|
||||
Route, RouteId, Selection, State, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint,
|
||||
WaypointId, copy_file, copy_route, copy_segment, copy_track, copy_waypoint,
|
||||
};
|
||||
|
||||
/// Where elements are put, among the others of the same list.
|
||||
@@ -37,15 +37,20 @@ impl<T: Copy + PartialEq> Place<T> {
|
||||
|
||||
/// Where elements go. What can go where:
|
||||
/// - files: among the files;
|
||||
/// - routes: among the files (each route becomes a file, named like the route, or like the file
|
||||
/// it comes from), or among the routes of a file. Among the tracks of a file, they stay routes,
|
||||
/// which come before the tracks: they go after the other routes;
|
||||
/// - 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;
|
||||
/// it comes from), or among the tracks of a file. Among the routes of a file, they stay tracks,
|
||||
/// which come after the routes: they go before the other tracks;
|
||||
/// - 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;
|
||||
/// or like its file), among the routes or the tracks of a file (each segment becomes a route or
|
||||
/// 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>),
|
||||
Routes(FileId, Place<RouteId>),
|
||||
Tracks(FileId, Place<TrackId>),
|
||||
Segments(FileId, TrackId, Place<TrackSegmentId>),
|
||||
Waypoints(FileId, Place<WaypointId>),
|
||||
@@ -93,6 +98,14 @@ pub fn remove_elements(state: &mut State, content: &ClipboardContent) {
|
||||
state.files.retain(|id, _| !ids.contains(id));
|
||||
state.order.0.retain(|id| !ids.contains(id));
|
||||
}
|
||||
ClipboardContent::Routes(routes) => {
|
||||
let ids: HashSet<RouteId> = routes.iter().map(|rte| rte.route.id).collect();
|
||||
for file in state.files.values_mut() {
|
||||
if file.rte.iter().any(|rte| ids.contains(&rte.id)) {
|
||||
Rc::make_mut(file).rte.retain(|rte| !ids.contains(&rte.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
ClipboardContent::Tracks(tracks) => {
|
||||
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.track.id).collect();
|
||||
for file in state.files.values_mut() {
|
||||
@@ -141,7 +154,9 @@ pub fn transfer(
|
||||
// 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::Routes(file_id, _) | Destination::Tracks(file_id, _) => {
|
||||
state.files.contains_key(&file_id)
|
||||
}
|
||||
Destination::Segments(file_id, trk_id, _) => state
|
||||
.files
|
||||
.get(&file_id)
|
||||
@@ -152,17 +167,21 @@ pub fn transfer(
|
||||
(content, to),
|
||||
(ClipboardContent::Files(_), Destination::Files(_))
|
||||
| (
|
||||
ClipboardContent::Tracks(_),
|
||||
Destination::Files(_) | Destination::Tracks(..)
|
||||
ClipboardContent::Routes(_) | ClipboardContent::Tracks(_),
|
||||
Destination::Files(_) | Destination::Routes(..) | Destination::Tracks(..)
|
||||
)
|
||||
| (
|
||||
ClipboardContent::Segments(_),
|
||||
Destination::Files(_) | Destination::Tracks(..) | Destination::Segments(..),
|
||||
Destination::Files(_)
|
||||
| Destination::Routes(..)
|
||||
| Destination::Tracks(..)
|
||||
| Destination::Segments(..),
|
||||
)
|
||||
| (ClipboardContent::Waypoints(_), Destination::Waypoints(..))
|
||||
);
|
||||
let empty = match content {
|
||||
ClipboardContent::Files(files) => files.is_empty(),
|
||||
ClipboardContent::Routes(routes) => routes.is_empty(),
|
||||
ClipboardContent::Tracks(tracks) => tracks.is_empty(),
|
||||
ClipboardContent::Segments(segments) => segments.is_empty(),
|
||||
ClipboardContent::Waypoints(waypoints) => waypoints.is_empty(),
|
||||
@@ -189,6 +208,7 @@ pub fn transfer(
|
||||
.collect();
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
(ClipboardContent::Routes(routes), to) => transfer_routes(state, routes, copy, to),
|
||||
(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)) => {
|
||||
@@ -198,6 +218,89 @@ pub fn transfer(
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts the routes among the routes of a file, at `place`, and gives the selection of them.
|
||||
fn insert_routes(
|
||||
state: &mut State,
|
||||
file_id: FileId,
|
||||
place: Place<RouteId>,
|
||||
routes: Vec<Route>,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let ids: HashSet<RouteId> = routes.iter().map(|rte| rte.id).collect();
|
||||
let file = file_mut(state, file_id)?;
|
||||
let existing: Vec<RouteId> = file.rte.iter().map(|rte| rte.id).collect();
|
||||
let at = place.position(&existing);
|
||||
file.rte.splice(at..at, routes);
|
||||
Ok(Selection::Route {
|
||||
file_id,
|
||||
rte_ids: ids,
|
||||
})
|
||||
}
|
||||
|
||||
fn transfer_routes(
|
||||
state: &mut State,
|
||||
routes: &[ClipboardRoute],
|
||||
copy: bool,
|
||||
to: Destination,
|
||||
) -> Result<Selection, CommandError> {
|
||||
let routes: Vec<(Route, &str)> = routes
|
||||
.iter()
|
||||
.map(|clip| {
|
||||
let route = if copy {
|
||||
copy_route(&clip.route)
|
||||
} else {
|
||||
clip.route.clone()
|
||||
};
|
||||
(route, clip.file_name.as_str())
|
||||
})
|
||||
.collect();
|
||||
match to {
|
||||
Destination::Files(place) => {
|
||||
let files = routes
|
||||
.into_iter()
|
||||
.map(|(route, file_name)| {
|
||||
let name = route
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| file_name.to_owned());
|
||||
Rc::new(File {
|
||||
rte: vec![route],
|
||||
..new_file(&name, vec![])
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
Destination::Routes(file_id, place) => {
|
||||
let routes = routes.into_iter().map(|(route, _)| route).collect();
|
||||
insert_routes(state, file_id, place, routes)
|
||||
}
|
||||
Destination::Tracks(file_id, _) => {
|
||||
let routes = routes.into_iter().map(|(route, _)| route).collect();
|
||||
insert_routes(state, file_id, Place::End, routes)
|
||||
}
|
||||
_ => Err(CommandError::NothingToDo),
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts the tracks among the tracks of a file, at `place`, and gives the selection of them.
|
||||
fn insert_tracks(
|
||||
state: &mut State,
|
||||
file_id: FileId,
|
||||
place: Place<TrackId>,
|
||||
tracks: Vec<Track>,
|
||||
) -> Result<Selection, CommandError> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
fn transfer_tracks(
|
||||
state: &mut State,
|
||||
tracks: &[ClipboardTrack],
|
||||
@@ -231,16 +334,12 @@ fn transfer_tracks(
|
||||
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,
|
||||
})
|
||||
let tracks = tracks.into_iter().map(|(track, _)| track).collect();
|
||||
insert_tracks(state, file_id, place, tracks)
|
||||
}
|
||||
Destination::Routes(file_id, _) => {
|
||||
let tracks = tracks.into_iter().map(|(track, _)| track).collect();
|
||||
insert_tracks(state, file_id, Place::Index(0), tracks)
|
||||
}
|
||||
_ => Err(CommandError::NothingToDo),
|
||||
}
|
||||
@@ -276,20 +375,22 @@ fn transfer_segments(
|
||||
.collect();
|
||||
Ok(add_files(state, files, place))
|
||||
}
|
||||
Destination::Routes(file_id, place) => {
|
||||
let routes = segments
|
||||
.into_iter()
|
||||
.map(|(seg, _)| Route {
|
||||
seg,
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
insert_routes(state, file_id, place, routes)
|
||||
}
|
||||
Destination::Tracks(file_id, place) => {
|
||||
let tracks: Vec<Track> = segments
|
||||
let tracks = 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,
|
||||
})
|
||||
insert_tracks(state, file_id, place, tracks)
|
||||
}
|
||||
Destination::Segments(file_id, trk_id, place) => {
|
||||
let segments: Vec<TrackSegment> =
|
||||
File renamed without changes.
@@ -4,6 +4,8 @@ mod file;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod fixture;
|
||||
mod pattern;
|
||||
#[cfg(test)]
|
||||
mod routes_tests;
|
||||
mod tools;
|
||||
|
||||
pub use apply::*;
|
||||
@@ -24,19 +26,22 @@ pub enum Command<'a> {
|
||||
New(New<'a>),
|
||||
Load(Load<'a>),
|
||||
LoadFiles(LoadFiles<'a>),
|
||||
SyncFiles(SyncFiles<'a>),
|
||||
Duplicate(Duplicate),
|
||||
Delete(Delete),
|
||||
Paste(Paste),
|
||||
Move(Move),
|
||||
DeleteAll(DeleteAll),
|
||||
DeleteWaypoint(DeleteWaypoint),
|
||||
Convert(Convert),
|
||||
Metadata(Metadata<'a>),
|
||||
Style(Style<'a>),
|
||||
NewRoute(NewRoute),
|
||||
NewTrack(NewTrack),
|
||||
NewTrackSegment(NewTrackSegment),
|
||||
Reverse(Reverse),
|
||||
RoundTrip(RoundTrip),
|
||||
Route(Route<'a>),
|
||||
Reroute(Reroute<'a>),
|
||||
NewWaypoint(NewWaypoint<'a>),
|
||||
EditWaypoint(EditWaypoint<'a>),
|
||||
MoveWaypoint(MoveWaypoint),
|
||||
@@ -58,19 +63,22 @@ impl Apply for Command<'_> {
|
||||
Command::New(c) => c.apply(state),
|
||||
Command::Load(c) => c.apply(state),
|
||||
Command::LoadFiles(c) => c.apply(state),
|
||||
Command::SyncFiles(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::Convert(c) => c.apply(state),
|
||||
Command::Metadata(c) => c.apply(state),
|
||||
Command::Style(c) => c.apply(state),
|
||||
Command::NewRoute(c) => c.apply(state),
|
||||
Command::NewTrack(c) => c.apply(state),
|
||||
Command::NewTrackSegment(c) => c.apply(state),
|
||||
Command::Reverse(c) => c.apply(state),
|
||||
Command::RoundTrip(c) => c.apply(state),
|
||||
Command::Route(c) => c.apply(state),
|
||||
Command::Reroute(c) => c.apply(state),
|
||||
Command::NewWaypoint(c) => c.apply(state),
|
||||
Command::EditWaypoint(c) => c.apply(state),
|
||||
Command::MoveWaypoint(c) => c.apply(state),
|
||||
+12
-2
@@ -1,4 +1,4 @@
|
||||
use crate::{File, Track, TrackSegment, Waypoint};
|
||||
use crate::{File, Route, Track, TrackSegment, Waypoint};
|
||||
|
||||
// The trackpoints of a segment are shared chunks, so copying elements is cheap.
|
||||
|
||||
@@ -18,10 +18,20 @@ pub fn copy_track(track: &Track) -> Track {
|
||||
}
|
||||
}
|
||||
|
||||
/// A copy of the file and of its tracks and segments, with new ids.
|
||||
/// A copy of the route and of its segment, with new ids.
|
||||
pub fn copy_route(route: &Route) -> Route {
|
||||
Route {
|
||||
id: Default::default(),
|
||||
seg: copy_segment(&route.seg),
|
||||
..route.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// A copy of the file and of its routes, tracks and segments, with new ids.
|
||||
pub fn copy_file(file: &File) -> File {
|
||||
File {
|
||||
id: Default::default(),
|
||||
rte: file.rte.iter().map(copy_route).collect(),
|
||||
trk: file.trk.iter().map(copy_track).collect(),
|
||||
..file.clone()
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
+5
-5
@@ -18,7 +18,7 @@ pub fn update_segments(
|
||||
let file = files
|
||||
.entry(location.file_id)
|
||||
.or_insert_with(|| (*state.files[&location.file_id]).clone());
|
||||
f(i, &mut file.trk[location.trk].trkseg[location.seg]);
|
||||
f(i, file.segment_mut(location.index));
|
||||
}
|
||||
for (file_id, file) in files {
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
@@ -31,6 +31,8 @@ mod tests {
|
||||
|
||||
use crate::engine::command::fixture::Fixture;
|
||||
|
||||
use crate::SegmentIndex;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn fixture() -> (Fixture, FileId) {
|
||||
@@ -49,13 +51,11 @@ mod tests {
|
||||
let locations = [
|
||||
SegmentLocation {
|
||||
file_id: id,
|
||||
trk: 1,
|
||||
seg: 0,
|
||||
index: SegmentIndex::Track { trk: 1, seg: 0 },
|
||||
},
|
||||
SegmentLocation {
|
||||
file_id: id,
|
||||
trk: 0,
|
||||
seg: 0,
|
||||
index: SegmentIndex::Track { trk: 0, seg: 0 },
|
||||
},
|
||||
];
|
||||
let mut calls = vec![];
|
||||
+22
-7
@@ -1,6 +1,6 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{File, FileId, Files, Selection, State, Track, TrackSegment, Waypoint};
|
||||
use crate::{File, FileId, Files, Route, Selection, State, Track, TrackSegment, Waypoint};
|
||||
|
||||
/// What an [`Editor`] hook did to the element it was given.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -21,13 +21,19 @@ impl Edit {
|
||||
|
||||
/// 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.
|
||||
/// A selected file, route, track or segment is passed to the hook of its own level. By default
|
||||
/// a file is forwarded to its routes and tracks, and a route or a track to its segments, so an
|
||||
/// editor only overrides the level(s) it cares about (metadata: file, route and track, style:
|
||||
/// route and 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))
|
||||
let routes = edit_each(&mut file.rte, |rte| self.route(rte));
|
||||
routes.merge(edit_each(&mut file.trk, |trk| self.track(trk)))
|
||||
}
|
||||
|
||||
fn route(&mut self, route: &mut Route) -> Edit {
|
||||
edit_segment(self, &mut route.seg)
|
||||
}
|
||||
|
||||
fn track(&mut self, track: &mut Track) -> Edit {
|
||||
@@ -95,7 +101,7 @@ fn edit_waypoints<E: Editor + ?Sized>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the editor to every selected file, track or segment.
|
||||
/// Applies the editor to every selected file, route, track or segment.
|
||||
pub fn update_selected<E: Editor>(state: &mut State, editor: &mut E) {
|
||||
match &*state.selection {
|
||||
Selection::File { file_ids } => {
|
||||
@@ -103,6 +109,15 @@ pub fn update_selected<E: Editor>(state: &mut State, editor: &mut E) {
|
||||
update_file(state.files, *id, |file| editor.file(file));
|
||||
}
|
||||
}
|
||||
Selection::Route { file_id, rte_ids } => {
|
||||
update_file(state.files, *file_id, |file| {
|
||||
edit_where(
|
||||
&mut file.rte,
|
||||
|rte| rte_ids.contains(&rte.id),
|
||||
|rte| editor.route(rte),
|
||||
)
|
||||
});
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
update_file(state.files, *file_id, |file| {
|
||||
edit_where(
|
||||
File renamed without changes.
@@ -0,0 +1,684 @@
|
||||
//! The commands on routes: they work as on tracks, a route being a track of a single segment.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
Apply, Clean, CleanType, Clipboard, ClipboardContent, CommandError, Convert, Crop, Delete,
|
||||
Duplicate, Extract, File, FileId, LngLat, LngLatBounds, Merge, MergeType, Metadata, Move,
|
||||
MoveTarget, Paste, Reverse, RouteId, SegmentIndex, Selection, Split, SplitType, Style,
|
||||
engine::command::fixture::Fixture, parse,
|
||||
};
|
||||
|
||||
/// A file with the 2 routes of `with_routes` and the 2 tracks (of 2 segments) of
|
||||
/// `with_tracks_and_segments`, which is the only file and is in the order.
|
||||
fn fixture() -> (Fixture, FileId) {
|
||||
let read = |name: &str| {
|
||||
let data = std::fs::read(format!("data/{name}.gpx")).unwrap();
|
||||
parse(&data, &mut Default::default()).unwrap()
|
||||
};
|
||||
let mut file = read("with_tracks_and_segments");
|
||||
file.rte = read("with_routes").rte;
|
||||
assert_eq!((file.rte.len(), file.trk.len()), (2, 2));
|
||||
let mut fx = Fixture::default();
|
||||
let id = file.id;
|
||||
fx.files.insert(id, Rc::new(file));
|
||||
fx.order.0.push(id);
|
||||
(fx, id)
|
||||
}
|
||||
|
||||
fn file(fx: &Fixture, id: FileId) -> &File {
|
||||
&fx.files[&id]
|
||||
}
|
||||
|
||||
fn routes(fx: &Fixture, id: FileId, indices: &[usize]) -> Selection {
|
||||
Selection::Route {
|
||||
file_id: id,
|
||||
rte_ids: indices.iter().map(|i| file(fx, id).rte[*i].id).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn whole(id: FileId) -> Selection {
|
||||
Selection::File {
|
||||
file_ids: [id].into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn lengths(file: &File) -> Vec<usize> {
|
||||
file.segments().map(|segment| segment.len()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_the_segments_of_a_file_are_the_ones_of_its_routes_then_its_tracks() {
|
||||
let (fx, id) = fixture();
|
||||
let file = file(&fx, id);
|
||||
let locations = whole(id).segment_locations(&fx.files, &fx.order.0);
|
||||
let indices: Vec<SegmentIndex> = locations.iter().map(|l| l.index).collect();
|
||||
assert_eq!(
|
||||
indices,
|
||||
[
|
||||
SegmentIndex::Route(0),
|
||||
SegmentIndex::Route(1),
|
||||
SegmentIndex::Track { trk: 0, seg: 0 },
|
||||
SegmentIndex::Track { trk: 0, seg: 1 },
|
||||
SegmentIndex::Track { trk: 1, seg: 0 },
|
||||
SegmentIndex::Track { trk: 1, seg: 1 },
|
||||
]
|
||||
);
|
||||
// a selection of routes covers their segments only
|
||||
let selected = routes(&fx, id, &[1]).segment_locations(&fx.files, &fx.order.0);
|
||||
assert_eq!(selected.len(), 1);
|
||||
assert_eq!(selected[0].segment(&fx.files).id, file.rte[1].seg.id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_routes_are_selected_like_tracks() {
|
||||
let (fx, id) = fixture();
|
||||
let ids: Vec<RouteId> = file(&fx, id).rte.iter().map(|r| r.id).collect();
|
||||
|
||||
let mut selection = routes(&fx, id, &[0]);
|
||||
selection.extend(routes(&fx, id, &[1]));
|
||||
assert_eq!(selection, routes(&fx, id, &[0, 1]));
|
||||
selection.toggle(routes(&fx, id, &[0]));
|
||||
assert_eq!(selection, routes(&fx, id, &[1]));
|
||||
// routes and tracks are not selected together
|
||||
let track = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [file(&fx, id).trk[0].id].into(),
|
||||
};
|
||||
selection.extend(track.clone());
|
||||
assert_eq!(selection, track);
|
||||
|
||||
assert_eq!(
|
||||
routes(&fx, id, &[0]).all_at_level(&fx.files, &fx.order.0),
|
||||
Some(routes(&fx, id, &[0, 1]))
|
||||
);
|
||||
assert_eq!(
|
||||
routes(&fx, id, &[0]).neighbour(&fx.files, &fx.order.0, true),
|
||||
Some(routes(&fx, id, &[1]))
|
||||
);
|
||||
assert_eq!(
|
||||
routes(&fx, id, &[1]).neighbour(&fx.files, &fx.order.0, true),
|
||||
None
|
||||
);
|
||||
assert_eq!(routes(&fx, id, &[1]).to_files(), whole(id));
|
||||
|
||||
let mut gone = Selection::Route {
|
||||
file_id: id,
|
||||
rte_ids: [ids[0], RouteId::default()].into(),
|
||||
};
|
||||
gone.retain_existing(&fx.files);
|
||||
assert_eq!(gone, routes(&fx, id, &[0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_and_duplicate_routes() {
|
||||
let (mut fx, id) = fixture();
|
||||
let kept = file(&fx, id).rte[1].id;
|
||||
fx.selection = routes(&fx, id, &[0]);
|
||||
Delete { whole_files: false }
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(file(&fx, id).rte.len(), 1);
|
||||
assert_eq!(file(&fx, id).rte[0].id, kept);
|
||||
assert_eq!(file(&fx, id).trk.len(), 2);
|
||||
assert_eq!(fx.selection, whole(id));
|
||||
|
||||
fx.selection = routes(&fx, id, &[0]);
|
||||
Duplicate.apply(&mut fx.state()).unwrap();
|
||||
let file = file(&fx, id);
|
||||
assert_eq!(file.rte.len(), 2);
|
||||
assert_ne!(file.rte[1].id, kept);
|
||||
assert_ne!(file.rte[1].seg.id, file.rte[0].seg.id);
|
||||
assert_eq!(file.rte[1].info, file.rte[0].info);
|
||||
assert_eq!(
|
||||
fx.selection,
|
||||
Selection::Route {
|
||||
file_id: id,
|
||||
rte_ids: [file.rte[1].id].into()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_metadata_and_style_of_routes() {
|
||||
let (mut fx, id) = fixture();
|
||||
fx.selection = routes(&fx, id, &[1]);
|
||||
Metadata {
|
||||
name: "renamed",
|
||||
desc: "about",
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
Style {
|
||||
color: Some("00ff00"),
|
||||
opacity: None,
|
||||
width: Some(3.0),
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = file(&fx, id);
|
||||
let info = &file.rte[1].info;
|
||||
assert_eq!(info.name.as_deref(), Some("renamed"));
|
||||
assert_eq!(info.desc.as_deref(), Some("about"));
|
||||
assert_eq!(info.color.as_deref(), Some("00ff00"));
|
||||
assert_eq!(info.width, Some(3.0));
|
||||
assert_ne!(file.rte[0].info.name.as_deref(), Some("renamed"));
|
||||
|
||||
// the style of a file is the one of its routes and tracks
|
||||
fx.selection = whole(id);
|
||||
Style {
|
||||
color: Some("ff0000"),
|
||||
opacity: None,
|
||||
width: None,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = &fx.files[&id];
|
||||
assert!(
|
||||
file.rte
|
||||
.iter()
|
||||
.map(|r| &r.info)
|
||||
.chain(file.trk.iter().map(|t| &t.info))
|
||||
.all(|info| info.color.as_deref() == Some("ff0000"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_and_paste_routes() {
|
||||
let (mut fx, id) = fixture();
|
||||
let copied = file(&fx, id).rte[0].clone();
|
||||
fx.clipboard = Clipboard::new(&routes(&fx, id, &[0]), &fx.files, &fx.order.0, false);
|
||||
assert!(matches!(
|
||||
fx.clipboard.as_ref().unwrap().content,
|
||||
ClipboardContent::Routes(_)
|
||||
));
|
||||
|
||||
// after the selected route
|
||||
fx.selection = routes(&fx, id, &[1]);
|
||||
Paste.apply(&mut fx.state()).unwrap();
|
||||
let file = file(&fx, id);
|
||||
assert_eq!(file.rte.len(), 3);
|
||||
assert_ne!(file.rte[2].id, copied.id);
|
||||
assert_eq!(file.rte[2].seg.len(), copied.seg.len());
|
||||
assert!(
|
||||
matches!(&fx.selection, Selection::Route { rte_ids, .. } if rte_ids.contains(&file.rte[2].id))
|
||||
);
|
||||
|
||||
// nothing selected: a new file, named like the route
|
||||
fx.selection = Selection::Empty;
|
||||
Paste.apply(&mut fx.state()).unwrap();
|
||||
assert_eq!(fx.order.0.len(), 2);
|
||||
let new = &fx.files[&fx.order.0[1]];
|
||||
assert_eq!(new.rte.len(), 1);
|
||||
assert!(new.trk.is_empty());
|
||||
assert_eq!(Some(&new.info.name), copied.info.name.as_ref());
|
||||
|
||||
// a track selected: among the routes of its file
|
||||
fx.selection = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [fx.files[&id].trk[0].id].into(),
|
||||
};
|
||||
Paste.apply(&mut fx.state()).unwrap();
|
||||
assert_eq!(fx.files[&id].rte.len(), 4);
|
||||
|
||||
// segments do not go in a route, but become routes next to it
|
||||
let file = &fx.files[&id];
|
||||
let track = &file.trk[0];
|
||||
fx.clipboard = Clipboard::new(
|
||||
&Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: track.id,
|
||||
trkseg_ids: [track.trkseg[1].id].into(),
|
||||
},
|
||||
&fx.files,
|
||||
&fx.order.0,
|
||||
false,
|
||||
);
|
||||
fx.selection = routes(&fx, id, &[0]);
|
||||
Paste.apply(&mut fx.state()).unwrap();
|
||||
let file = &fx.files[&id];
|
||||
assert_eq!(file.rte.len(), 5);
|
||||
assert_eq!(file.rte[1].seg.len(), file.trk[0].trkseg[1].len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cut_tracks_pasted_on_a_route_go_first_among_the_tracks() {
|
||||
let (mut fx, id) = fixture();
|
||||
let moved = file(&fx, id).trk[1].id;
|
||||
fx.clipboard = Clipboard::new(
|
||||
&Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [moved].into(),
|
||||
},
|
||||
&fx.files,
|
||||
&fx.order.0,
|
||||
true,
|
||||
);
|
||||
fx.selection = routes(&fx, id, &[0]);
|
||||
Paste.apply(&mut fx.state()).unwrap();
|
||||
let file = file(&fx, id);
|
||||
assert_eq!(file.trk.len(), 2);
|
||||
assert_eq!(file.trk[0].id, moved);
|
||||
assert_eq!(file.rte.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_routes() {
|
||||
let (mut fx, id) = fixture();
|
||||
let ids: Vec<RouteId> = file(&fx, id).rte.iter().map(|r| r.id).collect();
|
||||
Move {
|
||||
what: routes(&fx, id, &[1]),
|
||||
to: MoveTarget::Routes {
|
||||
file_id: id,
|
||||
index: 0,
|
||||
},
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let order: Vec<RouteId> = file(&fx, id).rte.iter().map(|r| r.id).collect();
|
||||
assert_eq!(order, [ids[1], ids[0]]);
|
||||
|
||||
// to the files: a file of its own
|
||||
Move {
|
||||
what: routes(&fx, id, &[0]),
|
||||
to: MoveTarget::Files { index: 1 },
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(file(&fx, id).rte.len(), 1);
|
||||
let new = &fx.files[&fx.order.0[1]];
|
||||
assert_eq!(new.rte[0].id, ids[1]);
|
||||
|
||||
// a segment moved among the routes becomes one
|
||||
let track = &file(&fx, id).trk[0];
|
||||
let segment = track.trkseg[0].id;
|
||||
Move {
|
||||
what: Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: track.id,
|
||||
trkseg_ids: [segment].into(),
|
||||
},
|
||||
to: MoveTarget::Routes {
|
||||
file_id: id,
|
||||
index: 1,
|
||||
},
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = file(&fx, id);
|
||||
assert_eq!(file.rte[1].seg.id, segment);
|
||||
assert_eq!(file.trk[0].trkseg.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_routes_and_tracks_moved_among_each_other_keep_their_kind() {
|
||||
let (mut fx, id) = fixture();
|
||||
let route = file(&fx, id).rte[0].id;
|
||||
let track = file(&fx, id).trk[1].id;
|
||||
|
||||
// a route among the tracks: after the other routes, which come first
|
||||
Move {
|
||||
what: routes(&fx, id, &[0]),
|
||||
to: MoveTarget::Tracks {
|
||||
file_id: id,
|
||||
index: 1,
|
||||
},
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let moved = file(&fx, id);
|
||||
assert_eq!((moved.rte.len(), moved.trk.len()), (2, 2));
|
||||
assert_eq!(moved.rte[1].id, route);
|
||||
assert_eq!(
|
||||
fx.selection,
|
||||
Selection::Route {
|
||||
file_id: id,
|
||||
rte_ids: [route].into()
|
||||
}
|
||||
);
|
||||
|
||||
// a track among the routes: before the other tracks, which come after the routes
|
||||
Move {
|
||||
what: Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [track].into(),
|
||||
},
|
||||
to: MoveTarget::Routes {
|
||||
file_id: id,
|
||||
index: 0,
|
||||
},
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let moved = file(&fx, id);
|
||||
assert_eq!((moved.rte.len(), moved.trk.len()), (2, 2));
|
||||
assert_eq!(moved.trk[0].id, track);
|
||||
|
||||
// and copied routes pasted on a track stay routes
|
||||
fx.clipboard = Clipboard::new(&routes(&fx, id, &[0]), &fx.files, &fx.order.0, false);
|
||||
fx.selection = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [track].into(),
|
||||
};
|
||||
Paste.apply(&mut fx.state()).unwrap();
|
||||
let pasted = file(&fx, id);
|
||||
assert_eq!((pasted.rte.len(), pasted.trk.len()), (3, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_routes() {
|
||||
let (mut fx, id) = fixture();
|
||||
let before = lengths(file(&fx, id));
|
||||
let first = file(&fx, id).rte[0].id;
|
||||
fx.selection = routes(&fx, id, &[0, 1]);
|
||||
let merge = |type_| Merge {
|
||||
type_,
|
||||
remove_gaps: false,
|
||||
keep_originals: false,
|
||||
};
|
||||
assert_eq!(
|
||||
merge(MergeType::Group).apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
merge(MergeType::Connect).apply(&mut fx.state()).unwrap();
|
||||
let file = file(&fx, id);
|
||||
assert_eq!(file.rte.len(), 1);
|
||||
assert_eq!(file.rte[0].id, first);
|
||||
assert_eq!(file.rte[0].seg.len(), before[0] + before[1]);
|
||||
assert_eq!(file.trk.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_files_with_routes() {
|
||||
let (mut fx, id) = fixture();
|
||||
let other = File {
|
||||
rte: file(&fx, id).rte.iter().map(crate::copy_route).collect(),
|
||||
..File::default()
|
||||
};
|
||||
let other_id = other.id;
|
||||
fx.files.insert(other_id, Rc::new(other));
|
||||
fx.order.0.push(other_id);
|
||||
let total: usize = lengths(file(&fx, id)).iter().sum::<usize>()
|
||||
+ lengths(&fx.files[&other_id]).iter().sum::<usize>();
|
||||
|
||||
let mut grouped = Fixture {
|
||||
files: fx.files.clone(),
|
||||
order: fx.order.clone(),
|
||||
selection: Selection::File {
|
||||
file_ids: [id, other_id].into(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
Merge {
|
||||
type_: MergeType::Group,
|
||||
remove_gaps: false,
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut grouped.state())
|
||||
.unwrap();
|
||||
let merged = &grouped.files[&id];
|
||||
assert_eq!((merged.rte.len(), merged.trk.len()), (4, 2));
|
||||
assert!(!grouped.files.contains_key(&other_id));
|
||||
|
||||
// connected in the first route, which comes first
|
||||
fx.selection = Selection::File {
|
||||
file_ids: [id, other_id].into(),
|
||||
};
|
||||
Merge {
|
||||
type_: MergeType::Connect,
|
||||
remove_gaps: false,
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let merged = &fx.files[&id];
|
||||
assert_eq!((merged.rte.len(), merged.trk.len()), (1, 0));
|
||||
assert_eq!(merged.rte[0].seg.len(), total);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_a_file_with_routes() {
|
||||
let (mut fx, id) = fixture();
|
||||
let names: Vec<_> = file(&fx, id)
|
||||
.rte
|
||||
.iter()
|
||||
.map(|r| r.info.name.clone().unwrap())
|
||||
.collect();
|
||||
fx.selection = whole(id);
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
// one file per route and one per track
|
||||
assert_eq!(fx.order.0.len(), 4);
|
||||
let files: Vec<&File> = fx.order.0.iter().map(|id| &*fx.files[id]).collect();
|
||||
for (file, name) in files.iter().zip(&names) {
|
||||
assert_eq!((file.rte.len(), file.trk.len()), (1, 0));
|
||||
assert_eq!(&file.info.name, name);
|
||||
}
|
||||
for file in &files[2..] {
|
||||
assert_eq!((file.rte.len(), file.trk.len()), (0, 2));
|
||||
}
|
||||
|
||||
// a route has nothing to extract
|
||||
let (mut fx, id) = fixture();
|
||||
fx.selection = routes(&fx, id, &[0]);
|
||||
assert_eq!(
|
||||
Extract {
|
||||
keep_originals: false
|
||||
}
|
||||
.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_a_route() {
|
||||
let (mut fx, id) = fixture();
|
||||
let before = file(&fx, id).rte[1].clone();
|
||||
fx.selection = routes(&fx, id, &[1]);
|
||||
Split {
|
||||
at: 10,
|
||||
split_type: SplitType::Routes,
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
// two routes, which share the trackpoint
|
||||
let split = file(&fx, id);
|
||||
assert_eq!(split.rte.len(), 3);
|
||||
assert_eq!(split.rte[1].id, before.id);
|
||||
assert_ne!(split.rte[2].id, before.id);
|
||||
assert_eq!(split.rte[1].seg.len(), 11);
|
||||
assert_eq!(split.rte[2].seg.len(), before.seg.len() - 10);
|
||||
assert_eq!(split.rte[2].info, before.info);
|
||||
|
||||
// a route is not a track nor a segment, and the other way around
|
||||
let (mut fx, id) = fixture();
|
||||
fx.selection = whole(id);
|
||||
let segment_start = lengths(file(&fx, id))[..2].iter().sum::<usize>() as u32;
|
||||
for (at, split_type) in [
|
||||
(10, SplitType::Tracks),
|
||||
(10, SplitType::Segments),
|
||||
(segment_start + 10, SplitType::Routes),
|
||||
] {
|
||||
assert_eq!(
|
||||
Split {
|
||||
at,
|
||||
split_type,
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
assert_eq!(file(&fx, id).rte.len(), 2);
|
||||
|
||||
// into two files: the following routes and the tracks go to the second one
|
||||
let (mut fx, id) = fixture();
|
||||
let first_len = file(&fx, id).rte[0].seg.len();
|
||||
fx.selection = whole(id);
|
||||
Split {
|
||||
at: 5,
|
||||
split_type: SplitType::Files,
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(fx.order.0.len(), 2);
|
||||
let (a, b) = (&fx.files[&fx.order.0[0]], &fx.files[&fx.order.0[1]]);
|
||||
assert_eq!(a.id, id);
|
||||
assert_eq!((a.rte.len(), a.trk.len()), (1, 0));
|
||||
assert_eq!(a.rte[0].seg.len(), 6);
|
||||
assert_eq!((b.rte.len(), b.trk.len()), (2, 2));
|
||||
assert_eq!(b.rte[0].seg.len(), first_len - 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_crop_removes_the_routes_left_without_trackpoints() {
|
||||
let (mut fx, id) = fixture();
|
||||
let len = lengths(file(&fx, id));
|
||||
fx.selection = whole(id);
|
||||
// the second route only
|
||||
let start = len[0] as u32;
|
||||
Crop {
|
||||
start,
|
||||
end: start + len[1] as u32 - 1,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let file = file(&fx, id);
|
||||
assert_eq!((file.rte.len(), file.trk.len()), (1, 0));
|
||||
assert_eq!(file.rte[0].seg.len(), len[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clean_removes_the_routes_left_without_trackpoints() {
|
||||
let (mut fx, id) = fixture();
|
||||
fx.selection = routes(&fx, id, &[0]);
|
||||
let everywhere = LngLatBounds {
|
||||
sw: LngLat {
|
||||
lng: -180.0,
|
||||
lat: -90.0,
|
||||
},
|
||||
ne: LngLat {
|
||||
lng: 180.0,
|
||||
lat: 90.0,
|
||||
},
|
||||
};
|
||||
Clean {
|
||||
bounds: everywhere,
|
||||
type_: CleanType::Inside,
|
||||
trkpt: true,
|
||||
wpt: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(file(&fx, id).rte.len(), 1);
|
||||
assert_eq!(fx.selection, whole(id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reverse_a_route_and_the_routes_of_a_file() {
|
||||
let (mut fx, id) = fixture();
|
||||
let before = file(&fx, id).clone();
|
||||
fx.selection = routes(&fx, id, &[0]);
|
||||
Reverse.apply(&mut fx.state()).unwrap();
|
||||
let route = &file(&fx, id).rte[0].seg;
|
||||
let original = &before.rte[0].seg;
|
||||
assert_eq!(
|
||||
route[0].coordinates.lat,
|
||||
original[original.len() - 1].coordinates.lat
|
||||
);
|
||||
assert_eq!(file(&fx, id).rte[1].seg.rev_id, before.rte[1].seg.rev_id);
|
||||
|
||||
let (mut fx, id) = fixture();
|
||||
let before = file(&fx, id).clone();
|
||||
fx.selection = whole(id);
|
||||
Reverse.apply(&mut fx.state()).unwrap();
|
||||
let file = file(&fx, id);
|
||||
// the routes are in the opposite order, and still before the tracks
|
||||
assert_eq!(file.rte[0].id, before.rte[1].id);
|
||||
assert_eq!(file.trk[0].id, before.trk[1].id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_routes_to_tracks_and_back() {
|
||||
let (mut fx, id) = fixture();
|
||||
let before = file(&fx, id).clone();
|
||||
|
||||
// a route becomes a track, first among the tracks, with its segment and its information
|
||||
fx.selection = routes(&fx, id, &[1]);
|
||||
Convert.apply(&mut fx.state()).unwrap();
|
||||
let converted = file(&fx, id);
|
||||
assert_eq!((converted.rte.len(), converted.trk.len()), (1, 3));
|
||||
assert_eq!(converted.trk[0].info, before.rte[1].info);
|
||||
assert_eq!(converted.trk[0].trkseg.len(), 1);
|
||||
assert_eq!(converted.trk[0].trkseg[0].id, before.rte[1].seg.id);
|
||||
assert_eq!(
|
||||
fx.selection,
|
||||
Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [converted.trk[0].id].into()
|
||||
}
|
||||
);
|
||||
|
||||
// and back: a route again, after the other routes
|
||||
Convert.apply(&mut fx.state()).unwrap();
|
||||
let back = file(&fx, id);
|
||||
assert_eq!((back.rte.len(), back.trk.len()), (2, 2));
|
||||
assert_eq!(back.rte[1].seg.id, before.rte[1].seg.id);
|
||||
assert_eq!(back.rte[1].info, before.rte[1].info);
|
||||
assert!(
|
||||
matches!(&fx.selection, Selection::Route { rte_ids, .. } if rte_ids.contains(&back.rte[1].id))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_a_track_to_one_route_per_segment() {
|
||||
let (mut fx, id) = fixture();
|
||||
let track = file(&fx, id).trk[0].clone();
|
||||
assert_eq!(track.trkseg.len(), 2);
|
||||
fx.selection = Selection::Track {
|
||||
file_id: id,
|
||||
trk_ids: [track.id].into(),
|
||||
};
|
||||
Convert.apply(&mut fx.state()).unwrap();
|
||||
let converted = file(&fx, id);
|
||||
assert_eq!((converted.rte.len(), converted.trk.len()), (4, 1));
|
||||
for (route, segment) in converted.rte[2..].iter().zip(&track.trkseg) {
|
||||
assert_eq!(route.seg.id, segment.id);
|
||||
assert_eq!(route.info, track.info);
|
||||
}
|
||||
assert!(matches!(&fx.selection, Selection::Route { rte_ids, .. } if rte_ids.len() == 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_segments_to_routes_keeps_their_track() {
|
||||
let (mut fx, id) = fixture();
|
||||
let track = file(&fx, id).trk[1].clone();
|
||||
fx.selection = Selection::TrackSegment {
|
||||
file_id: id,
|
||||
trk_id: track.id,
|
||||
trkseg_ids: [track.trkseg[0].id, track.trkseg[1].id].into(),
|
||||
};
|
||||
Convert.apply(&mut fx.state()).unwrap();
|
||||
let converted = file(&fx, id);
|
||||
assert_eq!((converted.rte.len(), converted.trk.len()), (4, 2));
|
||||
assert!(converted.trk[1].trkseg.is_empty());
|
||||
assert_eq!(converted.rte[2].seg.id, track.trkseg[0].id);
|
||||
|
||||
// nothing to convert otherwise
|
||||
for selection in [
|
||||
whole(id),
|
||||
Selection::Empty,
|
||||
Selection::Waypoints { file_id: id },
|
||||
] {
|
||||
fx.selection = selection;
|
||||
assert_eq!(
|
||||
Convert.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -24,7 +24,7 @@ impl Apply for ChangeLoopStart {
|
||||
let index = self.index as usize;
|
||||
let target = find_target(state, index, index)?.ok_or(CommandError::NothingToDo)?;
|
||||
let mut file = (*state.files[&target.location.file_id]).clone();
|
||||
let segment = &mut file.trk[target.location.trk].trkseg[target.location.seg];
|
||||
let segment = file.segment_mut(target.location.index);
|
||||
let (start, len) = (target.start, segment.len());
|
||||
if start == 0 || start >= len {
|
||||
// it already is the start, or it is not a trackpoint
|
||||
+45
-21
@@ -5,15 +5,16 @@ use crate::{
|
||||
TrackSegment, TrackSegmentId, Trackpoint, Waypoint, delete_waypoints, update_selected,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
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.
|
||||
/// rectangle. A selected file cleans its routes, its tracks and its waypoints, a selected route,
|
||||
/// track or segment only trackpoints, selected waypoints only waypoints.
|
||||
#[derive(Debug)]
|
||||
pub struct Clean {
|
||||
pub bounds: LngLatBounds,
|
||||
@@ -84,6 +85,11 @@ struct Cleaner<'a> {
|
||||
impl Editor for Cleaner<'_> {
|
||||
fn file(&mut self, file: &mut File) -> Edit {
|
||||
let mut edit = Edit::Unchanged;
|
||||
for rte in &mut file.rte {
|
||||
if self.route(rte) == Edit::Changed {
|
||||
edit = Edit::Changed;
|
||||
}
|
||||
}
|
||||
for trk in &mut file.trk {
|
||||
if self.track(trk) == Edit::Changed {
|
||||
edit = Edit::Changed;
|
||||
@@ -123,37 +129,28 @@ impl Editor for Cleaner<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Removes what the clean left empty: the emptied segments, the routes of these segments and the
|
||||
/// tracks that lost all their segments. A file is never removed, even if nothing is left in it: it stays, empty. 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)))
|
||||
wpt_files.contains(id) || file.segments().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.rte.retain(|rte| !emptied.contains(&rte.seg.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);
|
||||
}
|
||||
|
||||
@@ -172,6 +169,27 @@ fn fixed_selection(state: &State) -> Selection {
|
||||
Selection::File { file_ids }
|
||||
}
|
||||
}
|
||||
Selection::Route { file_id, rte_ids } => {
|
||||
let Some(file) = files.get(file_id) else {
|
||||
return Selection::Empty;
|
||||
};
|
||||
let rte_ids: HashSet<_> = file
|
||||
.rte
|
||||
.iter()
|
||||
.map(|rte| rte.id)
|
||||
.filter(|id| rte_ids.contains(id))
|
||||
.collect();
|
||||
if rte_ids.is_empty() {
|
||||
Selection::File {
|
||||
file_ids: [*file_id].into(),
|
||||
}
|
||||
} else {
|
||||
Selection::Route {
|
||||
file_id: *file_id,
|
||||
rte_ids,
|
||||
}
|
||||
}
|
||||
}
|
||||
Selection::Track { file_id, trk_ids } => {
|
||||
let Some(file) = files.get(file_id) else {
|
||||
return Selection::Empty;
|
||||
@@ -296,10 +314,16 @@ mod tests {
|
||||
.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);
|
||||
// nothing is left in the file, which stays and is still selected
|
||||
let file = &fx.files[&id];
|
||||
assert!(file.trk.is_empty() && file.wpt.is_empty());
|
||||
assert_eq!(fx.order.0, [id]);
|
||||
assert_eq!(
|
||||
fx.selection,
|
||||
Selection::File {
|
||||
file_ids: HashSet::from([id])
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
+9
-8
@@ -11,9 +11,9 @@ use crate::{
|
||||
/// Keeps the trackpoints `start` to `end` (both included) of the selection, which are counted
|
||||
/// over its segments one after the other (as in the `RoutingBuffer`), and removes the others.
|
||||
///
|
||||
/// The segments that have no trackpoint left are removed. So are the tracks that have no segment
|
||||
/// left, if tracks or files are selected (not if only segments are), and the files that have no
|
||||
/// trackpoint left, if files are selected. A range that goes past the last trackpoint stops at it.
|
||||
/// The segments that have no trackpoint left are removed, and so are the routes of these segments.
|
||||
/// So are the tracks that have no segment left, if tracks or files are selected (not if only
|
||||
/// segments are), and the files that have no trackpoint left, if files are selected. A range that goes past the last trackpoint stops at it.
|
||||
#[derive(Debug)]
|
||||
pub struct Crop {
|
||||
pub start: u32,
|
||||
@@ -43,8 +43,7 @@ impl Apply for Crop {
|
||||
.segment_locations(state.files, &state.order.0)
|
||||
.into_iter()
|
||||
.map(|location| {
|
||||
let len =
|
||||
state.files[&location.file_id].trk[location.trk].trkseg[location.seg].len();
|
||||
let len = location.segment(state.files).len();
|
||||
let keep = (len > 0 && start < offset + len && end >= offset)
|
||||
.then(|| (start.saturating_sub(offset), (end - offset).min(len - 1)));
|
||||
offset += len;
|
||||
@@ -73,14 +72,14 @@ impl Apply for Crop {
|
||||
let mut removed: HashSet<TrackSegmentId> = HashSet::new();
|
||||
let mut had_points: HashSet<FileId> = HashSet::new();
|
||||
for plan in &plans {
|
||||
let SegmentLocation { file_id, trk, seg } = plan.location;
|
||||
let SegmentLocation { file_id, index } = plan.location;
|
||||
let file = files
|
||||
.entry(file_id)
|
||||
.or_insert_with(|| (*state.files[&file_id]).clone());
|
||||
if plan.len > 0 {
|
||||
had_points.insert(file_id);
|
||||
}
|
||||
let segment = &mut file.trk[trk].trkseg[seg];
|
||||
let segment = file.segment_mut(index);
|
||||
match plan.keep {
|
||||
None => {
|
||||
removed.insert(segment.id);
|
||||
@@ -91,6 +90,8 @@ impl Apply for Crop {
|
||||
|
||||
let mut gone: Vec<FileId> = vec![];
|
||||
for (file_id, file) in files.iter_mut() {
|
||||
// a route has its segment, or is not there
|
||||
file.rte.retain(|route| !removed.contains(&route.seg.id));
|
||||
// the tracks that lost segments
|
||||
let touched: Vec<bool> = file
|
||||
.trk
|
||||
@@ -108,7 +109,7 @@ impl Apply for Crop {
|
||||
file.trk
|
||||
.retain(|track| !(touched.next().unwrap() && track.trkseg.is_empty()));
|
||||
}
|
||||
if prune_files && file.trk.is_empty() && had_points.contains(file_id) {
|
||||
if prune_files && file.has_no_path() && had_points.contains(file_id) {
|
||||
gone.push(*file_id);
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+1
-3
@@ -14,9 +14,7 @@ impl Apply for Elevation<'_> {
|
||||
.segment_locations(state.files, &state.order.0);
|
||||
let count: usize = locations
|
||||
.iter()
|
||||
.map(|location| {
|
||||
state.files[&location.file_id].trk[location.trk].trkseg[location.seg].len()
|
||||
})
|
||||
.map(|location| location.segment(state.files).len())
|
||||
.sum();
|
||||
if count == 0 {
|
||||
return Err(CommandError::NothingToDo);
|
||||
+148
-44
@@ -1,44 +1,59 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, File, FileId, LngLat, LngLatBounds, Selection, State, Track, TrackId,
|
||||
TrackSegment, Waypoint, Waypoints, copy_waypoint, crossarc_lnglat, distance,
|
||||
Apply, CommandError, File, FileId, LngLat, LngLatBounds, Route, Selection, State, Track,
|
||||
TrackId, TrackSegment, Waypoint, Waypoints, copy_file, copy_waypoint, crossarc_lnglat,
|
||||
distance,
|
||||
};
|
||||
|
||||
/// Splits the selected elements that are made of several segments into elements that have one
|
||||
/// each:
|
||||
///
|
||||
/// - files: a file with several tracks becomes one file per track, in which the track is split
|
||||
/// into one track per segment. A file with a single track becomes one file per segment. The new
|
||||
/// files are in the place of the file, which goes, and are selected. They are named after the
|
||||
/// track, or the file, and numbered. The waypoints go to the files that have the segments
|
||||
/// - files: a file with several routes and tracks becomes one file per route and per track, in
|
||||
/// which the track is split into one track per segment. A file with a single track becomes one
|
||||
/// file per segment. The new files are in the place of the file, which goes, and are selected.
|
||||
/// They are named after the route or track, or the file, and numbered. The waypoints go to the files that have the segments
|
||||
/// closest to them (to the first file if the file has no trackpoint).
|
||||
/// - tracks: each selected track is replaced by one track per segment, which are selected. They
|
||||
/// are named after the track and numbered.
|
||||
///
|
||||
/// What has a single segment is left as it is. There is nothing to do if nothing was changed.
|
||||
/// What has a single segment is left as it is, which routes always are. There is nothing to do if nothing was changed.
|
||||
///
|
||||
/// With `keep_originals`, the files that are extracted stay where they are, and the new files come
|
||||
/// right after each of them, and only the new files are selected. Extracting
|
||||
/// tracks changes the file in place anyway, so the flag has no effect on it.
|
||||
#[derive(Debug)]
|
||||
pub struct Extract;
|
||||
pub struct Extract {
|
||||
pub keep_originals: bool,
|
||||
}
|
||||
|
||||
impl Apply for Extract {
|
||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||
let selection = state.selection.clone();
|
||||
match &selection {
|
||||
Selection::File { file_ids } => extract_files(state, file_ids),
|
||||
Selection::File { file_ids } => extract_files(state, file_ids, self.keep_originals),
|
||||
Selection::Track { file_id, trk_ids } => extract_tracks(state, *file_id, trk_ids),
|
||||
_ => Err(CommandError::NothingToDo),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_files(state: &mut State, file_ids: &HashSet<FileId>) -> Result<(), CommandError> {
|
||||
fn extract_files(
|
||||
state: &mut State,
|
||||
file_ids: &HashSet<FileId>,
|
||||
keep_originals: bool,
|
||||
) -> Result<(), CommandError> {
|
||||
// the files that have something to extract, with the files that come out of them
|
||||
let mut extracted: Vec<(FileId, Vec<File>)> = vec![];
|
||||
for id in state.order.0.iter().filter(|id| file_ids.contains(id)) {
|
||||
let file = &state.files[id];
|
||||
let segments: usize = file.trk.iter().map(|track| track.trkseg.len()).sum();
|
||||
if segments > 1 {
|
||||
extracted.push((*id, extract_file(file)));
|
||||
if file.segments().count() > 1 {
|
||||
let mut files = extract_file(file);
|
||||
if keep_originals {
|
||||
// the tracks and segments are the ones of the file, which stays: they need new ids
|
||||
files = files.iter().map(copy_file).collect();
|
||||
}
|
||||
extracted.push((*id, files));
|
||||
}
|
||||
}
|
||||
if extracted.is_empty() {
|
||||
@@ -48,13 +63,25 @@ fn extract_files(state: &mut State, file_ids: &HashSet<FileId>) -> Result<(), Co
|
||||
let mut order = vec![];
|
||||
for id in state.order.0.iter() {
|
||||
match extracted.iter().find(|(original, _)| original == id) {
|
||||
Some((_, files)) => order.extend(files.iter().map(|file| file.id)),
|
||||
Some((_, files)) => {
|
||||
if keep_originals {
|
||||
order.push(*id);
|
||||
}
|
||||
order.extend(files.iter().map(|file| file.id));
|
||||
}
|
||||
None => order.push(*id),
|
||||
}
|
||||
}
|
||||
let mut selected: HashSet<FileId> = file_ids.clone();
|
||||
// the files that are kept stay selected, unless the originals are kept: then only what is new is
|
||||
let mut selected: HashSet<FileId> = if keep_originals {
|
||||
HashSet::new()
|
||||
} else {
|
||||
file_ids.clone()
|
||||
};
|
||||
for (original, files) in extracted {
|
||||
if !keep_originals {
|
||||
state.files.remove(&original);
|
||||
}
|
||||
selected.remove(&original);
|
||||
for file in files {
|
||||
selected.insert(file.id);
|
||||
@@ -68,31 +95,35 @@ fn extract_files(state: &mut State, file_ids: &HashSet<FileId>) -> Result<(), Co
|
||||
|
||||
/// The files that a file with several segments becomes.
|
||||
fn extract_file(file: &File) -> Vec<File> {
|
||||
// for each new file: its tracks and its name, and the segments that go with it
|
||||
// for each new file: its routes, its tracks and its name, and the segments that go with it
|
||||
struct Part {
|
||||
routes: Vec<Route>,
|
||||
tracks: Vec<Track>,
|
||||
name: String,
|
||||
}
|
||||
let (parts, owner): (Vec<Part>, Vec<usize>) = if file.trk.len() > 1 {
|
||||
// one file per track, the segments of a track follow it
|
||||
let parts = file
|
||||
.trk
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, track)| Part {
|
||||
let name = |name: &Option<String>, i: usize| {
|
||||
name.clone()
|
||||
.unwrap_or_else(|| format!("{} ({})", file.info.name, i + 1))
|
||||
};
|
||||
let (parts, owner): (Vec<Part>, Vec<usize>) =
|
||||
if file.rte.len() + file.trk.len() > 1 {
|
||||
// one file per route and per track, the segments of a track follow it
|
||||
let routes = file.rte.iter().enumerate().map(|(i, route)| Part {
|
||||
routes: vec![route.clone()],
|
||||
tracks: vec![],
|
||||
name: name(&route.info.name, i),
|
||||
});
|
||||
let tracks = file.trk.iter().enumerate().map(|(i, track)| Part {
|
||||
routes: vec![],
|
||||
tracks: split_track(track),
|
||||
name: track
|
||||
.info
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("{} ({})", file.info.name, i + 1)),
|
||||
})
|
||||
.collect();
|
||||
let owner = file
|
||||
.trk
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(i, track)| track.trkseg.iter().map(move |_| i))
|
||||
name: name(&track.info.name, file.rte.len() + i),
|
||||
});
|
||||
let parts = routes.chain(tracks).collect();
|
||||
let owner =
|
||||
(0..file.rte.len())
|
||||
.chain(file.trk.iter().enumerate().flat_map(|(i, track)| {
|
||||
track.trkseg.iter().map(move |_| file.rte.len() + i)
|
||||
}))
|
||||
.collect();
|
||||
(parts, owner)
|
||||
} else {
|
||||
@@ -108,6 +139,7 @@ fn extract_file(file: &File) -> Vec<File> {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, segment)| Part {
|
||||
routes: vec![],
|
||||
tracks: vec![Track {
|
||||
id: if i == 0 { track.id } else { Default::default() },
|
||||
trkseg: vec![segment.clone()],
|
||||
@@ -121,7 +153,7 @@ fn extract_file(file: &File) -> Vec<File> {
|
||||
};
|
||||
|
||||
// the waypoints go with the segments that are the closest to them
|
||||
let segments: Vec<&TrackSegment> = file.trk.iter().flat_map(|track| &track.trkseg).collect();
|
||||
let segments: Vec<&TrackSegment> = file.segments().collect();
|
||||
let mut waypoints: Vec<Vec<Waypoint>> = vec![vec![]; parts.len()];
|
||||
for waypoint in file.wpt.iter() {
|
||||
let mut owners: Vec<usize> = closest_segments(&segments, waypoint.coordinates)
|
||||
@@ -149,6 +181,7 @@ fn extract_file(file: &File) -> Vec<File> {
|
||||
.map(|(part, waypoints)| {
|
||||
let mut extracted = File {
|
||||
id: Default::default(),
|
||||
rte: part.routes,
|
||||
trk: part.tracks,
|
||||
wpt: Waypoints::default(),
|
||||
..file.clone()
|
||||
@@ -372,7 +405,11 @@ mod tests {
|
||||
fx.files.insert(other_id, Rc::new(other));
|
||||
fx.order.0.push(other_id);
|
||||
|
||||
Extract.apply(&mut fx.state()).unwrap();
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
// in the place of the file, in order, named after the track
|
||||
assert!(!fx.files.contains_key(&id));
|
||||
@@ -402,6 +439,48 @@ mod tests {
|
||||
if file_ids.len() == 3 && !file_ids.contains(&other_id)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extracting_files_keeping_the_originals() {
|
||||
let original = file(
|
||||
"route",
|
||||
vec![track(Some("climb"), &[0.0, 10.0, 20.0])],
|
||||
vec![],
|
||||
);
|
||||
let segment_ids: Vec<_> = original.trk[0].trkseg.iter().map(|s| s.id).collect();
|
||||
let (mut fx, id) = fixture(original);
|
||||
let before = fx.files[&id].clone();
|
||||
let single = file("single", vec![track(None, &[50.0])], vec![]);
|
||||
let single_id = single.id;
|
||||
fx.files.insert(single_id, Rc::new(single));
|
||||
fx.order.0.push(single_id);
|
||||
fx.selection = Selection::File {
|
||||
file_ids: HashSet::from([id, single_id]),
|
||||
};
|
||||
|
||||
Extract {
|
||||
keep_originals: true,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
// the file stays, the new ones come right after it
|
||||
assert!(Rc::ptr_eq(&fx.files[&id], &before));
|
||||
assert_eq!(
|
||||
names(&fx),
|
||||
["route", "climb (1)", "climb (2)", "climb (3)", "single"]
|
||||
);
|
||||
assert_eq!(fx.order.0[0], id);
|
||||
assert_eq!(fx.order.0[4], single_id);
|
||||
// the segments are copies, not the ones of the file
|
||||
for new_id in &fx.order.0[1..4] {
|
||||
let segment = fx.files[new_id].trk[0].trkseg[0].id;
|
||||
assert!(!segment_ids.contains(&segment));
|
||||
}
|
||||
// only the new files are selected
|
||||
assert!(matches!(&fx.selection, Selection::File { file_ids }
|
||||
if *file_ids == fx.order.0[1..4].iter().copied().collect::<HashSet<_>>()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_a_file_with_several_tracks_becomes_one_file_per_track() {
|
||||
let original = file(
|
||||
@@ -415,7 +494,11 @@ mod tests {
|
||||
);
|
||||
let ids: Vec<_> = original.trk.iter().map(|t| t.id).collect();
|
||||
let (mut fx, _) = fixture(original);
|
||||
Extract.apply(&mut fx.state()).unwrap();
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
// named after the track, or the file
|
||||
assert_eq!(names(&fx), ["first", "route (2)", "third"]);
|
||||
@@ -451,7 +534,11 @@ mod tests {
|
||||
vec![shared, waypoint("lost", 100.0)],
|
||||
);
|
||||
let (mut fx, _) = fixture(original);
|
||||
Extract.apply(&mut fx.state()).unwrap();
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
let first = &fx.files[&fx.order.0[0]];
|
||||
let second = &fx.files[&fx.order.0[1]];
|
||||
// the tie goes to both, with different ids
|
||||
@@ -480,7 +567,11 @@ mod tests {
|
||||
vec![waypoint("w", 1.0)],
|
||||
);
|
||||
let (mut fx, _) = fixture(original);
|
||||
Extract.apply(&mut fx.state()).unwrap();
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
assert_eq!(fx.files[&fx.order.0[0]].wpt.len(), 1);
|
||||
assert_eq!(fx.files[&fx.order.0[1]].wpt.len(), 0);
|
||||
}
|
||||
@@ -503,7 +594,11 @@ mod tests {
|
||||
trk_ids: [ids[1], ids[2]].into(),
|
||||
};
|
||||
|
||||
Extract.apply(&mut fx.state()).unwrap();
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
let file = &fx.files[&id];
|
||||
let names: Vec<_> = file.trk.iter().map(|t| t.info.name.clone()).collect();
|
||||
@@ -539,7 +634,10 @@ mod tests {
|
||||
let (mut fx, id) = fixture(original);
|
||||
let before = fx.files[&id].clone();
|
||||
assert_eq!(
|
||||
Extract.apply(&mut fx.state()),
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
fx.selection = Selection::Track {
|
||||
@@ -547,7 +645,10 @@ mod tests {
|
||||
trk_ids: [track_id].into(),
|
||||
};
|
||||
assert_eq!(
|
||||
Extract.apply(&mut fx.state()),
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
for selection in [
|
||||
@@ -561,7 +662,10 @@ mod tests {
|
||||
] {
|
||||
fx.selection = selection;
|
||||
assert_eq!(
|
||||
Extract.apply(&mut fx.state()),
|
||||
Extract {
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx.state()),
|
||||
Err(CommandError::NothingToDo)
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -39,7 +39,7 @@ impl Apply for InsertAnchor {
|
||||
.selection
|
||||
.segment_locations(state.files, &state.order.0)
|
||||
{
|
||||
let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg];
|
||||
let segment = location.segment(state.files);
|
||||
let mut points = segment.iter();
|
||||
let Some(mut previous) = points.next() else {
|
||||
continue;
|
||||
@@ -81,7 +81,7 @@ impl Apply for InsertAnchor {
|
||||
let closest = closest.ok_or(CommandError::NothingToDo)?;
|
||||
|
||||
let mut file = (*state.files[&closest.location.file_id]).clone();
|
||||
let segment = &mut file.trk[closest.location.trk].trkseg[closest.location.seg];
|
||||
let segment = file.segment_mut(closest.location.index);
|
||||
let before = closest.before;
|
||||
// on a trackpoint already, if it is closer than `SNAP_DISTANCE`
|
||||
let snapped = if segment.len() == 1 {
|
||||
+151
-12
@@ -1,11 +1,13 @@
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
Apply, CommandError, FileId, GlobalStatistics, Selection, State, Statistics, Track, TrackId,
|
||||
TrackSegment, TrackSegmentId, Waypoint, Waypoints, replace_trackpoints,
|
||||
Apply, CommandError, FileId, GlobalStatistics, Route, RouteId, Selection, State, Statistics,
|
||||
Track, TrackId, TrackSegment, TrackSegmentId, Waypoint, Waypoints, copy_route, copy_track,
|
||||
replace_trackpoints,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum MergeType {
|
||||
/// The trackpoints of the elements are connected in a single segment.
|
||||
Connect,
|
||||
@@ -15,11 +17,16 @@ pub enum MergeType {
|
||||
|
||||
/// Merges the selected elements into the first of them:
|
||||
///
|
||||
/// - files: the other files are deleted. The tracks of all of them are in the first file, as
|
||||
/// they are with `Group`, or all connected in the first track with `Connect`. The waypoints of
|
||||
/// all of them are in the first file too, except the ones that are the same.
|
||||
/// - files: the other files are deleted. The routes and tracks of all of them are in the first
|
||||
/// file, as they are with `Group`, or all connected in the first of them (a route or a track,
|
||||
/// the routes of a file coming before its tracks) with `Connect`. The waypoints of
|
||||
/// all of them are in the first file too, except the ones that are the same. With
|
||||
/// `keep_originals`, no file is deleted nor changed: the result is a new file (with the info of
|
||||
/// the first one and new ids), which comes right after the last of the files and is selected.
|
||||
/// - tracks of a file: the first track gets their segments, as they are with `Group`, or all
|
||||
/// connected in one segment with `Connect`. The other tracks are removed.
|
||||
/// - routes of a file: the first route gets their trackpoints, all connected with `Connect`, and
|
||||
/// the other routes are removed. A route has a single segment, so there is nothing to `Group`.
|
||||
/// - segments of a track: they are all connected in a segment at the position of the first one
|
||||
/// with `Connect`. With `Group` they are put one after the other, at that position.
|
||||
///
|
||||
@@ -32,6 +39,8 @@ pub enum MergeType {
|
||||
pub struct Merge {
|
||||
pub type_: MergeType,
|
||||
pub remove_gaps: bool,
|
||||
/// Only for files: tracks and segments are merged in place anyway.
|
||||
pub keep_originals: bool,
|
||||
}
|
||||
|
||||
impl Apply for Merge {
|
||||
@@ -39,6 +48,7 @@ impl Apply for Merge {
|
||||
let selection = state.selection.clone();
|
||||
match &selection {
|
||||
Selection::File { file_ids } => self.merge_files(state, file_ids),
|
||||
Selection::Route { file_id, rte_ids } => self.merge_routes(state, *file_id, rte_ids),
|
||||
Selection::Track { file_id, trk_ids } => self.merge_tracks(state, *file_id, trk_ids),
|
||||
Selection::TrackSegment {
|
||||
file_id,
|
||||
@@ -72,21 +82,26 @@ impl Merge {
|
||||
return Err(CommandError::NothingToDo);
|
||||
};
|
||||
|
||||
let mut routes: Vec<Route> = vec![];
|
||||
let mut tracks: Vec<Track> = vec![];
|
||||
let mut segments: Vec<TrackSegment> = vec![];
|
||||
let mut waypoints: Vec<Waypoint> = vec![];
|
||||
for id in &ordered {
|
||||
let file = &state.files[id];
|
||||
routes.extend(file.rte.iter().cloned());
|
||||
tracks.extend(file.trk.iter().cloned());
|
||||
segments.extend(file.segments().cloned());
|
||||
for waypoint in file.wpt.iter() {
|
||||
if !waypoints.iter().any(|kept| kept.same_content(waypoint)) {
|
||||
waypoints.push(waypoint.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let segments: Vec<TrackSegment> = tracks
|
||||
.iter()
|
||||
.flat_map(|track| track.trkseg.iter().cloned())
|
||||
.collect();
|
||||
// the first route or track, which holds the connected segment
|
||||
let first_is_route = ordered.iter().find_map(|id| {
|
||||
let file = &state.files[id];
|
||||
(!file.has_no_path()).then(|| !file.rte.is_empty())
|
||||
});
|
||||
|
||||
match self.type_ {
|
||||
MergeType::Group if ordered.len() == 1 => return Err(CommandError::NothingToDo),
|
||||
@@ -94,21 +109,49 @@ impl Merge {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
MergeType::Connect if !segments.is_empty() => {
|
||||
// a single track, which is the first one, with the connected segment
|
||||
// a single route or track, which is the first one, with the connected segment
|
||||
let merged = connect(&segments, segments[0].id, self.remove_gaps);
|
||||
if first_is_route == Some(true) {
|
||||
let first = Route {
|
||||
seg: merged,
|
||||
..routes[0].clone()
|
||||
};
|
||||
(routes, tracks) = (vec![first], vec![]);
|
||||
} else {
|
||||
let first = Track {
|
||||
trkseg: vec![merged],
|
||||
..tracks[0].clone()
|
||||
};
|
||||
tracks = vec![first];
|
||||
(routes, tracks) = (vec![], vec![first]);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut target = (*state.files[&target_id]).clone();
|
||||
target.rte = routes;
|
||||
target.trk = tracks;
|
||||
target.wpt = Waypoints::default();
|
||||
target.wpt.insert_at(0, waypoints);
|
||||
if self.keep_originals {
|
||||
// a new file, whose routes, tracks and segments do not share their ids with the
|
||||
// originals
|
||||
target.id = Default::default();
|
||||
target.rte = target.rte.iter().map(copy_route).collect();
|
||||
target.trk = target.trk.iter().map(copy_track).collect();
|
||||
let at = state
|
||||
.order
|
||||
.0
|
||||
.iter()
|
||||
.rposition(|id| file_ids.contains(id))
|
||||
.map_or(state.order.0.len(), |i| i + 1);
|
||||
state.order.0.insert(at, target.id);
|
||||
*state.selection = Selection::File {
|
||||
file_ids: [target.id].into(),
|
||||
};
|
||||
state.files.insert(target.id, Rc::new(target));
|
||||
return Ok(());
|
||||
}
|
||||
for id in &ordered[1..] {
|
||||
state.files.remove(id);
|
||||
}
|
||||
@@ -117,6 +160,33 @@ impl Merge {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn merge_routes(
|
||||
&self,
|
||||
state: &mut State,
|
||||
file_id: FileId,
|
||||
rte_ids: &HashSet<RouteId>,
|
||||
) -> Result<(), CommandError> {
|
||||
let file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
||||
let selected: Vec<usize> = (0..file.rte.len())
|
||||
.filter(|i| rte_ids.contains(&file.rte[*i].id))
|
||||
.collect();
|
||||
if selected.len() < 2 || self.type_ == MergeType::Group {
|
||||
return Err(CommandError::NothingToDo);
|
||||
}
|
||||
let first = selected[0];
|
||||
let segments: Vec<TrackSegment> =
|
||||
selected.iter().map(|i| file.rte[*i].seg.clone()).collect();
|
||||
let mut file = (**file).clone();
|
||||
file.rte[first].seg = connect(&segments, segments[0].id, self.remove_gaps);
|
||||
let mut index = 0;
|
||||
file.rte.retain(|_| {
|
||||
index += 1;
|
||||
!selected[1..].contains(&(index - 1))
|
||||
});
|
||||
state.files.insert(file_id, Rc::new(file));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn merge_tracks(
|
||||
&self,
|
||||
state: &mut State,
|
||||
@@ -273,6 +343,7 @@ mod tests {
|
||||
Merge {
|
||||
type_,
|
||||
remove_gaps: false,
|
||||
keep_originals: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +411,73 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_files_keeping_the_originals() {
|
||||
for type_ in [MergeType::Group, MergeType::Connect] {
|
||||
let mut fx = Fixture::default();
|
||||
let a = load(&mut fx, "data/with_tracks_and_segments.gpx");
|
||||
let b = load(&mut fx, "data/simple.gpx");
|
||||
let c = load(&mut fx, "data/simple.gpx");
|
||||
let (before_a, before_b) = (fx.files[&a].clone(), fx.files[&b].clone());
|
||||
select_files(&mut fx, &[a, b]);
|
||||
|
||||
Merge {
|
||||
type_,
|
||||
remove_gaps: false,
|
||||
keep_originals: true,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
// the files are untouched, the new one comes right after the last of them
|
||||
assert!(Rc::ptr_eq(&fx.files[&a], &before_a));
|
||||
assert!(Rc::ptr_eq(&fx.files[&b], &before_b));
|
||||
assert_eq!(fx.files.len(), 4);
|
||||
assert_eq!(&fx.order.0[..2], [a, b]);
|
||||
assert_eq!(fx.order.0[3], c);
|
||||
let merged_id = fx.order.0[2];
|
||||
let merged = &fx.files[&merged_id];
|
||||
assert_eq!(points(merged), points(&before_a) + points(&before_b));
|
||||
assert_eq!(merged.info, before_a.info);
|
||||
// new ids for the tracks and segments
|
||||
let ids = |f: &File| -> Vec<_> {
|
||||
f.trk
|
||||
.iter()
|
||||
.flat_map(|t| std::iter::once(t.id.0).chain(t.trkseg.iter().map(|s| s.id.0)))
|
||||
.collect()
|
||||
};
|
||||
let originals: Vec<_> = ids(&before_a).into_iter().chain(ids(&before_b)).collect();
|
||||
assert!(ids(merged).iter().all(|id| !originals.contains(id)));
|
||||
// and it is the selection
|
||||
assert!(matches!(&fx.selection, Selection::File { file_ids }
|
||||
if *file_ids == HashSet::from([merged_id])));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keeping_the_originals_does_not_matter_when_merging_tracks() {
|
||||
let mut fx = Fixture::default();
|
||||
let a = load(&mut fx, "data/with_tracks_and_segments.gpx");
|
||||
let tracks: HashSet<_> = fx.files[&a].trk.iter().map(|t| t.id).collect();
|
||||
let before = fx.files[&a].trk.len();
|
||||
assert!(before > 1);
|
||||
fx.selection = Selection::Track {
|
||||
file_id: a,
|
||||
trk_ids: tracks,
|
||||
};
|
||||
|
||||
Merge {
|
||||
type_: MergeType::Group,
|
||||
remove_gaps: false,
|
||||
keep_originals: true,
|
||||
}
|
||||
.apply(&mut fx.state())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(fx.files.len(), 1);
|
||||
assert_eq!(fx.files[&a].trk.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connect_files() {
|
||||
let mut fx = Fixture::default();
|
||||
@@ -429,6 +567,7 @@ mod tests {
|
||||
Merge {
|
||||
type_: MergeType::Connect,
|
||||
remove_gaps,
|
||||
keep_originals: false,
|
||||
}
|
||||
.apply(&mut fx2.state())
|
||||
.unwrap();
|
||||
File renamed without changes.
File renamed without changes.
Loaded 100 of 350 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user