mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-10-17 08:42:28 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bfd319758 | ||
|
|
187296a277 | ||
|
|
55a8b020e0 | ||
|
|
1ba5436af3 | ||
|
|
2e1ca00afa | ||
|
|
97d597e139 | ||
|
|
41b9116c6a |
No files matched your search
@@ -15,19 +15,31 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: |
|
cache-dependency-path: website/package-lock.json
|
||||||
gpx/package-lock.json
|
|
||||||
website/package-lock.json
|
|
||||||
|
|
||||||
- name: Install dependencies for gpx
|
- name: Install Rust
|
||||||
run: npm install --prefix gpx
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: wasm32-unknown-unknown
|
||||||
|
|
||||||
- name: Build gpx
|
- name: Install wasm-pack
|
||||||
run: npm run build --prefix gpx
|
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
|
- name: Install dependencies for website
|
||||||
run: npm install --prefix website
|
run: npm install --prefix website
|
||||||
|
|
||||||
|
- name: Test website
|
||||||
|
run: npm test --prefix website
|
||||||
|
|
||||||
- name: Create env file
|
- name: Create env file
|
||||||
run: |
|
run: |
|
||||||
touch website/.env
|
touch website/.env
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
target/
|
||||||
Generated
+5252
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
|
## 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,
|
- `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`),
|
||||||
- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application.
|
- `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
|
```bash
|
||||||
cd gpx-rs
|
cd wasm
|
||||||
wasm-pack build wasm --target bundler --out-dir ../pkg
|
wasm-pack build --target bundler
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running the website
|
### 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]
|
[package]
|
||||||
members = ["engine", "wasm"]
|
name = "gpx-engine"
|
||||||
resolver = "3"
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
[profile.release]
|
[dependencies]
|
||||||
debug = true
|
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
|
# gpx-rs
|
||||||
|
|
||||||
- `engine`: the GPX engine, plain Rust. Tested natively: `cargo test`.
|
The GPX engine: plain Rust, no knowledge of who uses it. Tested natively with `cargo test` (from
|
||||||
- `wasm`: the interface to the web page, compiled to WebAssembly.
|
here or from the root).
|
||||||
|
|
||||||
```
|
- `src/core`, `src/engine`, `src/storage`: the engine itself.
|
||||||
wasm-pack build wasm --target bundler --out-dir ../pkg
|
- `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
|
||||||
The interface is tested in Node (it needs `wasm-pack`, which fetches the test runner the first time):
|
bytes, results to JS objects or JSON, slices to typed arrays or raw bytes.
|
||||||
|
|
||||||
```
|
|
||||||
wasm-pack test --node wasm
|
|
||||||
```
|
|
||||||
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,7 +0,0 @@
|
|||||||
mod core;
|
|
||||||
mod engine;
|
|
||||||
mod storage;
|
|
||||||
|
|
||||||
pub use core::*;
|
|
||||||
pub use engine::*;
|
|
||||||
pub use storage::*;
|
|
||||||
@@ -0,0 +1,629 @@
|
|||||||
|
//! 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 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_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 `lng` and `lat`, it starts with a track and a segment that
|
||||||
|
/// hold that trackpoint (at `ele`, 0 by default).
|
||||||
|
pub fn new_file<H: History>(
|
||||||
|
engine: &mut Engine<H>,
|
||||||
|
name: &str,
|
||||||
|
lng: Option<f64>,
|
||||||
|
lat: Option<f64>,
|
||||||
|
ele: Option<f64>,
|
||||||
|
) -> 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 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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. 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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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::Route(engine::Route {
|
||||||
|
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 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,547 @@
|
|||||||
|
//! 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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>,
|
||||||
|
},
|
||||||
|
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::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::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,
|
||||||
|
},
|
||||||
|
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::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::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>,
|
||||||
|
tracks: Vec<TrackNode>,
|
||||||
|
waypoints: Vec<WaypointNode>,
|
||||||
|
waypoints_rev: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct TrackNode {
|
||||||
|
id: String,
|
||||||
|
#[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>,
|
||||||
|
segments: Vec<SegmentNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct SegmentNode {
|
||||||
|
id: String,
|
||||||
|
rev: String,
|
||||||
|
length: 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(),
|
||||||
|
tracks: file
|
||||||
|
.tracks
|
||||||
|
.iter()
|
||||||
|
.map(|trk| TrackNode {
|
||||||
|
id: trk.id.0.to_string(),
|
||||||
|
name: trk.name.clone(),
|
||||||
|
desc: trk.desc.clone(),
|
||||||
|
color: trk.color.clone(),
|
||||||
|
opacity: trk.opacity,
|
||||||
|
width: trk.width,
|
||||||
|
segments: trk
|
||||||
|
.segments
|
||||||
|
.iter()
|
||||||
|
.map(|seg| SegmentNode {
|
||||||
|
id: seg.id.0.to_string(),
|
||||||
|
rev: seg.rev_id.0.to_string(),
|
||||||
|
length: seg.len as f64,
|
||||||
|
})
|
||||||
|
.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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,242 @@
|
|||||||
|
//! 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, and with `as_route` that the segments are written as routes (`rte`)
|
||||||
|
/// instead of tracks: the anchors are the route points, and the trackpoints between them the
|
||||||
|
/// points of their path.
|
||||||
|
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,
|
||||||
|
slope,
|
||||||
|
slope_segment_slope,
|
||||||
|
slope_segment_distance,
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// Timestamps in milliseconds since the epoch (`NO_TIME` when missing), see `optional_buffers`.
|
||||||
|
pub fn timestamps<H: History>(engine: &Engine<H>) -> Option<&[i64]> {
|
||||||
|
engine.statistics().time.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
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.
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
-1
@@ -9,7 +9,7 @@ pub struct Load<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Load<'_> {
|
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)
|
let mut file = parse(self.data, categories)
|
||||||
.map_err(|err| CommandError::InvalidData(err.to_string()))?;
|
.map_err(|err| CommandError::InvalidData(err.to_string()))?;
|
||||||
if file.info.name.trim().is_empty() {
|
if file.info.name.trim().is_empty() {
|
||||||
@@ -6,6 +6,7 @@ mod load;
|
|||||||
mod move_elements;
|
mod move_elements;
|
||||||
mod new;
|
mod new;
|
||||||
mod paste;
|
mod paste;
|
||||||
|
mod sync;
|
||||||
mod transfer;
|
mod transfer;
|
||||||
|
|
||||||
pub use delete::*;
|
pub use delete::*;
|
||||||
@@ -16,4 +17,5 @@ pub use load::*;
|
|||||||
pub use move_elements::*;
|
pub use move_elements::*;
|
||||||
pub use new::*;
|
pub use new::*;
|
||||||
pub use paste::*;
|
pub use paste::*;
|
||||||
|
pub use sync::*;
|
||||||
pub use transfer::*;
|
pub use transfer::*;
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,148 @@
|
|||||||
|
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 when the data has none (see [`Load`]).
|
||||||
|
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 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 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,
|
||||||
|
}
|
||||||
|
.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]);
|
||||||
|
assert_eq!(fx.files[&first].info.name, "from disk");
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
File renamed without changes.
@@ -24,6 +24,7 @@ pub enum Command<'a> {
|
|||||||
New(New<'a>),
|
New(New<'a>),
|
||||||
Load(Load<'a>),
|
Load(Load<'a>),
|
||||||
LoadFiles(LoadFiles<'a>),
|
LoadFiles(LoadFiles<'a>),
|
||||||
|
SyncFiles(SyncFiles<'a>),
|
||||||
Duplicate(Duplicate),
|
Duplicate(Duplicate),
|
||||||
Delete(Delete),
|
Delete(Delete),
|
||||||
Paste(Paste),
|
Paste(Paste),
|
||||||
@@ -58,6 +59,7 @@ impl Apply for Command<'_> {
|
|||||||
Command::New(c) => c.apply(state),
|
Command::New(c) => c.apply(state),
|
||||||
Command::Load(c) => c.apply(state),
|
Command::Load(c) => c.apply(state),
|
||||||
Command::LoadFiles(c) => c.apply(state),
|
Command::LoadFiles(c) => c.apply(state),
|
||||||
|
Command::SyncFiles(c) => c.apply(state),
|
||||||
Command::Duplicate(c) => c.apply(state),
|
Command::Duplicate(c) => c.apply(state),
|
||||||
Command::Delete(c) => c.apply(state),
|
Command::Delete(c) => c.apply(state),
|
||||||
Command::Paste(c) => c.apply(state),
|
Command::Paste(c) => c.apply(state),
|
||||||
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.
+2
-1
@@ -5,7 +5,8 @@ use crate::{
|
|||||||
TrackSegment, TrackSegmentId, Trackpoint, Waypoint, delete_waypoints, update_selected,
|
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 {
|
pub enum CleanType {
|
||||||
Inside,
|
Inside,
|
||||||
Outside,
|
Outside,
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+114
-16
@@ -2,7 +2,7 @@ use std::{collections::HashSet, rc::Rc};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, CommandError, File, FileId, LngLat, LngLatBounds, Selection, State, Track, TrackId,
|
Apply, CommandError, File, FileId, LngLat, LngLatBounds, Selection, State, Track, TrackId,
|
||||||
TrackSegment, Waypoint, Waypoints, copy_waypoint, crossarc_lnglat, distance,
|
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
|
/// Splits the selected elements that are made of several segments into elements that have one
|
||||||
@@ -17,28 +17,43 @@ use crate::{
|
|||||||
/// are named after the track and numbered.
|
/// 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. 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)]
|
#[derive(Debug)]
|
||||||
pub struct Extract;
|
pub struct Extract {
|
||||||
|
pub keep_originals: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl Apply for Extract {
|
impl Apply for Extract {
|
||||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||||
let selection = state.selection.clone();
|
let selection = state.selection.clone();
|
||||||
match &selection {
|
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),
|
Selection::Track { file_id, trk_ids } => extract_tracks(state, *file_id, trk_ids),
|
||||||
_ => Err(CommandError::NothingToDo),
|
_ => 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
|
// the files that have something to extract, with the files that come out of them
|
||||||
let mut extracted: Vec<(FileId, Vec<File>)> = vec![];
|
let mut extracted: Vec<(FileId, Vec<File>)> = vec![];
|
||||||
for id in state.order.0.iter().filter(|id| file_ids.contains(id)) {
|
for id in state.order.0.iter().filter(|id| file_ids.contains(id)) {
|
||||||
let file = &state.files[id];
|
let file = &state.files[id];
|
||||||
let segments: usize = file.trk.iter().map(|track| track.trkseg.len()).sum();
|
let segments: usize = file.trk.iter().map(|track| track.trkseg.len()).sum();
|
||||||
if segments > 1 {
|
if segments > 1 {
|
||||||
extracted.push((*id, extract_file(file)));
|
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() {
|
if extracted.is_empty() {
|
||||||
@@ -48,13 +63,25 @@ fn extract_files(state: &mut State, file_ids: &HashSet<FileId>) -> Result<(), Co
|
|||||||
let mut order = vec![];
|
let mut order = vec![];
|
||||||
for id in state.order.0.iter() {
|
for id in state.order.0.iter() {
|
||||||
match extracted.iter().find(|(original, _)| original == id) {
|
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),
|
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 {
|
for (original, files) in extracted {
|
||||||
state.files.remove(&original);
|
if !keep_originals {
|
||||||
|
state.files.remove(&original);
|
||||||
|
}
|
||||||
selected.remove(&original);
|
selected.remove(&original);
|
||||||
for file in files {
|
for file in files {
|
||||||
selected.insert(file.id);
|
selected.insert(file.id);
|
||||||
@@ -372,7 +399,11 @@ mod tests {
|
|||||||
fx.files.insert(other_id, Rc::new(other));
|
fx.files.insert(other_id, Rc::new(other));
|
||||||
fx.order.0.push(other_id);
|
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
|
// in the place of the file, in order, named after the track
|
||||||
assert!(!fx.files.contains_key(&id));
|
assert!(!fx.files.contains_key(&id));
|
||||||
@@ -402,6 +433,48 @@ mod tests {
|
|||||||
if file_ids.len() == 3 && !file_ids.contains(&other_id)));
|
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]
|
#[test]
|
||||||
fn test_a_file_with_several_tracks_becomes_one_file_per_track() {
|
fn test_a_file_with_several_tracks_becomes_one_file_per_track() {
|
||||||
let original = file(
|
let original = file(
|
||||||
@@ -415,7 +488,11 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let ids: Vec<_> = original.trk.iter().map(|t| t.id).collect();
|
let ids: Vec<_> = original.trk.iter().map(|t| t.id).collect();
|
||||||
let (mut fx, _) = fixture(original);
|
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
|
// named after the track, or the file
|
||||||
assert_eq!(names(&fx), ["first", "route (2)", "third"]);
|
assert_eq!(names(&fx), ["first", "route (2)", "third"]);
|
||||||
@@ -451,7 +528,11 @@ mod tests {
|
|||||||
vec![shared, waypoint("lost", 100.0)],
|
vec![shared, waypoint("lost", 100.0)],
|
||||||
);
|
);
|
||||||
let (mut fx, _) = fixture(original);
|
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 first = &fx.files[&fx.order.0[0]];
|
||||||
let second = &fx.files[&fx.order.0[1]];
|
let second = &fx.files[&fx.order.0[1]];
|
||||||
// the tie goes to both, with different ids
|
// the tie goes to both, with different ids
|
||||||
@@ -480,7 +561,11 @@ mod tests {
|
|||||||
vec![waypoint("w", 1.0)],
|
vec![waypoint("w", 1.0)],
|
||||||
);
|
);
|
||||||
let (mut fx, _) = fixture(original);
|
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[0]].wpt.len(), 1);
|
||||||
assert_eq!(fx.files[&fx.order.0[1]].wpt.len(), 0);
|
assert_eq!(fx.files[&fx.order.0[1]].wpt.len(), 0);
|
||||||
}
|
}
|
||||||
@@ -503,7 +588,11 @@ mod tests {
|
|||||||
trk_ids: [ids[1], ids[2]].into(),
|
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 file = &fx.files[&id];
|
||||||
let names: Vec<_> = file.trk.iter().map(|t| t.info.name.clone()).collect();
|
let names: Vec<_> = file.trk.iter().map(|t| t.info.name.clone()).collect();
|
||||||
@@ -539,7 +628,10 @@ mod tests {
|
|||||||
let (mut fx, id) = fixture(original);
|
let (mut fx, id) = fixture(original);
|
||||||
let before = fx.files[&id].clone();
|
let before = fx.files[&id].clone();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Extract.apply(&mut fx.state()),
|
Extract {
|
||||||
|
keep_originals: false,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state()),
|
||||||
Err(CommandError::NothingToDo)
|
Err(CommandError::NothingToDo)
|
||||||
);
|
);
|
||||||
fx.selection = Selection::Track {
|
fx.selection = Selection::Track {
|
||||||
@@ -547,7 +639,10 @@ mod tests {
|
|||||||
trk_ids: [track_id].into(),
|
trk_ids: [track_id].into(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Extract.apply(&mut fx.state()),
|
Extract {
|
||||||
|
keep_originals: false,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state()),
|
||||||
Err(CommandError::NothingToDo)
|
Err(CommandError::NothingToDo)
|
||||||
);
|
);
|
||||||
for selection in [
|
for selection in [
|
||||||
@@ -561,7 +656,10 @@ mod tests {
|
|||||||
] {
|
] {
|
||||||
fx.selection = selection;
|
fx.selection = selection;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Extract.apply(&mut fx.state()),
|
Extract {
|
||||||
|
keep_originals: false,
|
||||||
|
}
|
||||||
|
.apply(&mut fx.state()),
|
||||||
Err(CommandError::NothingToDo)
|
Err(CommandError::NothingToDo)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
File renamed without changes.
+94
-3
@@ -2,10 +2,11 @@ use std::{collections::HashSet, rc::Rc};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, CommandError, FileId, GlobalStatistics, Selection, State, Statistics, Track, TrackId,
|
Apply, CommandError, FileId, GlobalStatistics, Selection, State, Statistics, Track, TrackId,
|
||||||
TrackSegment, TrackSegmentId, Waypoint, Waypoints, replace_trackpoints,
|
TrackSegment, TrackSegmentId, Waypoint, Waypoints, 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 {
|
pub enum MergeType {
|
||||||
/// The trackpoints of the elements are connected in a single segment.
|
/// The trackpoints of the elements are connected in a single segment.
|
||||||
Connect,
|
Connect,
|
||||||
@@ -17,7 +18,9 @@ pub enum MergeType {
|
|||||||
///
|
///
|
||||||
/// - files: the other files are deleted. The tracks of all of them are in the first file, as
|
/// - 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
|
/// 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.
|
/// 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
|
/// - 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.
|
/// connected in one segment with `Connect`. The other tracks are removed.
|
||||||
/// - segments of a track: they are all connected in a segment at the position of the first one
|
/// - segments of a track: they are all connected in a segment at the position of the first one
|
||||||
@@ -32,6 +35,8 @@ pub enum MergeType {
|
|||||||
pub struct Merge {
|
pub struct Merge {
|
||||||
pub type_: MergeType,
|
pub type_: MergeType,
|
||||||
pub remove_gaps: bool,
|
pub remove_gaps: bool,
|
||||||
|
/// Only for files: tracks and segments are merged in place anyway.
|
||||||
|
pub keep_originals: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Apply for Merge {
|
impl Apply for Merge {
|
||||||
@@ -109,6 +114,23 @@ impl Merge {
|
|||||||
target.trk = tracks;
|
target.trk = tracks;
|
||||||
target.wpt = Waypoints::default();
|
target.wpt = Waypoints::default();
|
||||||
target.wpt.insert_at(0, waypoints);
|
target.wpt.insert_at(0, waypoints);
|
||||||
|
if self.keep_originals {
|
||||||
|
// a new file, whose tracks and segments do not share their ids with the originals
|
||||||
|
target.id = Default::default();
|
||||||
|
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..] {
|
for id in &ordered[1..] {
|
||||||
state.files.remove(id);
|
state.files.remove(id);
|
||||||
}
|
}
|
||||||
@@ -273,6 +295,7 @@ mod tests {
|
|||||||
Merge {
|
Merge {
|
||||||
type_,
|
type_,
|
||||||
remove_gaps: false,
|
remove_gaps: false,
|
||||||
|
keep_originals: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,6 +363,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]
|
#[test]
|
||||||
fn test_connect_files() {
|
fn test_connect_files() {
|
||||||
let mut fx = Fixture::default();
|
let mut fx = Fixture::default();
|
||||||
@@ -429,6 +519,7 @@ mod tests {
|
|||||||
Merge {
|
Merge {
|
||||||
type_: MergeType::Connect,
|
type_: MergeType::Connect,
|
||||||
remove_gaps,
|
remove_gaps,
|
||||||
|
keep_originals: false,
|
||||||
}
|
}
|
||||||
.apply(&mut fx2.state())
|
.apply(&mut fx2.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
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.
Loaded 100 of 227 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user