mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-11-15 07:07:07 +00:00
Compare commits
95
Commits
desktop-beta
..
wasm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce8398f749 | ||
|
|
3c2055b50e | ||
|
|
1d13972014 | ||
|
|
890bcd765a | ||
|
|
4795513344 | ||
|
|
d84ca47745 | ||
|
|
9adb8e7221 | ||
|
|
a2b4d89150 | ||
|
|
77929cfa04 | ||
|
|
dc36de2a48 | ||
|
|
da9abbae5e | ||
|
|
3bb553d6b5 | ||
|
|
83b76bb838 | ||
|
|
42a55e6759 | ||
|
|
b085a1d476 | ||
|
|
840e943626 | ||
|
|
16e2e0f679 | ||
|
|
0f9557ea82 | ||
|
|
67a4c090bd | ||
|
|
606a0e974a | ||
|
|
e8c4eb92f9 | ||
|
|
f0851086c0 | ||
|
|
bec1ce4c9b | ||
|
|
b29c4cdb08 | ||
|
|
63ba8596a7 | ||
|
|
82d2f79da0 | ||
|
|
f642d60f42 | ||
|
|
24b877c4ee | ||
|
|
e07ec97e12 | ||
|
|
290f4d8c44 | ||
|
|
a48608840f | ||
|
|
df07892e76 | ||
|
|
594dd8979a | ||
|
|
29850c1c8f | ||
|
|
adbe4a2693 | ||
|
|
b843a18441 | ||
|
|
da8a9e3d84 | ||
|
|
c6bd5b06e8 | ||
|
|
c04367dd86 | ||
|
|
5e2f506120 | ||
|
|
c536e6247b | ||
|
|
594d714b7f | ||
|
|
543fecadb8 | ||
|
|
017d8521d6 | ||
|
|
78e9d45072 | ||
|
|
7651d859c4 | ||
|
|
ec448b72e2 | ||
|
|
bf946f179a | ||
|
|
9ff9b5475a | ||
|
|
55f1d275b7 | ||
|
|
1d534881f8 | ||
|
|
03cbacfbb4 | ||
|
|
9d0905959b | ||
|
|
b7ca462f58 | ||
|
|
9015da4876 | ||
|
|
f458a1104f | ||
|
|
caba9388af | ||
|
|
763a7406ce | ||
|
|
68c8933c35 | ||
|
|
84b1558b2e | ||
|
|
cd870f0186 | ||
|
|
9ac2a2b69c | ||
|
|
86aba8f4b1 | ||
|
|
74b674e8e8 | ||
|
|
f1904bc12f | ||
|
|
9afba53e5d | ||
|
|
fd76e0fa46 | ||
|
|
438a87f06f | ||
|
|
a994e16f51 | ||
|
|
639e7491eb | ||
|
|
891a08a3cc | ||
|
|
0c07164dec | ||
|
|
f133d55889 | ||
|
|
fbb636cb06 | ||
|
|
bddceb28aa | ||
|
|
bff84fa24c | ||
|
|
64914f9aa0 | ||
|
|
2abf46c747 | ||
|
|
888f41e979 | ||
|
|
17533586ef | ||
|
|
c6ad088947 | ||
|
|
87d6d5f30e | ||
|
|
dbe894b3a3 | ||
|
|
a921d4b9cd | ||
|
|
b7db73cf77 | ||
|
|
5868595a6c | ||
|
|
eb8e09b835 | ||
|
|
41dc853f65 | ||
|
|
27c0a57cc1 | ||
|
|
497ac120ec | ||
|
|
a486016d96 | ||
|
|
cc5f5653ef | ||
|
|
20222c450d | ||
|
|
69049e1345 | ||
|
|
ed6264444f |
No files matched your search
@@ -1,67 +0,0 @@
|
|||||||
name: Deploy to Cloudflare Pages
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: 'dev'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: cloudflare-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
deployments: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Install Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 24
|
|
||||||
cache: npm
|
|
||||||
cache-dependency-path: website/package-lock.json
|
|
||||||
|
|
||||||
- name: Install Rust
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
targets: wasm32-unknown-unknown
|
|
||||||
|
|
||||||
- name: Install wasm-pack
|
|
||||||
uses: taiki-e/install-action@v2
|
|
||||||
with:
|
|
||||||
tool: wasm-pack
|
|
||||||
|
|
||||||
- name: Test gpx-rs
|
|
||||||
run: cargo test
|
|
||||||
|
|
||||||
- name: Build wasm
|
|
||||||
run: wasm-pack build --target bundler
|
|
||||||
working-directory: wasm
|
|
||||||
|
|
||||||
- name: Install dependencies for website
|
|
||||||
run: npm install --prefix website
|
|
||||||
|
|
||||||
- name: Test website
|
|
||||||
run: npm test --prefix website
|
|
||||||
|
|
||||||
- name: Create env file
|
|
||||||
run: echo PUBLIC_MAPTILER_KEY=${{ secrets.PUBLIC_MAPTILER_KEY }} >> website/.env
|
|
||||||
|
|
||||||
- name: Build website
|
|
||||||
env:
|
|
||||||
BASE_PATH: ''
|
|
||||||
run: npm run build --prefix website
|
|
||||||
|
|
||||||
- name: Deploy to Cloudflare Pages
|
|
||||||
uses: cloudflare/wrangler-action@v3
|
|
||||||
with:
|
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
||||||
command: pages deploy website/build --project-name=${{ vars.CLOUDFLARE_PROJECT_NAME || 'gpxstudio' }} --branch=${{ github.ref_name }} --commit-dirty=true
|
|
||||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
@@ -15,31 +15,19 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: website/package-lock.json
|
cache-dependency-path: |
|
||||||
|
gpx/package-lock.json
|
||||||
|
website/package-lock.json
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install dependencies for gpx
|
||||||
uses: dtolnay/rust-toolchain@stable
|
run: npm install --prefix gpx
|
||||||
with:
|
|
||||||
targets: wasm32-unknown-unknown
|
|
||||||
|
|
||||||
- name: Install wasm-pack
|
- name: Build gpx
|
||||||
uses: taiki-e/install-action@v2
|
run: npm run build --prefix gpx
|
||||||
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,124 +0,0 @@
|
|||||||
name: Desktop beta
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: 'dev'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: desktop-beta-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- platform: macos-latest
|
|
||||||
rust_targets: aarch64-apple-darwin,x86_64-apple-darwin,wasm32-unknown-unknown
|
|
||||||
args: --target universal-apple-darwin
|
|
||||||
bundles: target/universal-apple-darwin/release/bundle/dmg/*.dmg
|
|
||||||
# the oldest supported Ubuntu, so that the AppImage runs on older systems too
|
|
||||||
- platform: ubuntu-22.04
|
|
||||||
rust_targets: wasm32-unknown-unknown
|
|
||||||
args: ''
|
|
||||||
bundles: |
|
|
||||||
target/release/bundle/deb/*.deb
|
|
||||||
target/release/bundle/rpm/*.rpm
|
|
||||||
target/release/bundle/appimage/*.AppImage
|
|
||||||
- platform: windows-latest
|
|
||||||
rust_targets: wasm32-unknown-unknown
|
|
||||||
args: ''
|
|
||||||
bundles: |
|
|
||||||
target/release/bundle/msi/*.msi
|
|
||||||
target/release/bundle/nsis/*.exe
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
|
||||||
|
|
||||||
- name: Install Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 24
|
|
||||||
cache: npm
|
|
||||||
cache-dependency-path: website/package-lock.json
|
|
||||||
|
|
||||||
- name: Install Rust
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
targets: ${{ matrix.rust_targets }}
|
|
||||||
|
|
||||||
- name: Cache Rust build
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
|
|
||||||
- name: Install wasm-pack
|
|
||||||
uses: taiki-e/install-action@v2
|
|
||||||
with:
|
|
||||||
tool: wasm-pack
|
|
||||||
|
|
||||||
# the desktop app does not use it, but the website depends on its package
|
|
||||||
- name: Build wasm
|
|
||||||
run: wasm-pack build --target bundler
|
|
||||||
working-directory: wasm
|
|
||||||
|
|
||||||
- name: Install dependencies for website
|
|
||||||
run: npm install --prefix website
|
|
||||||
|
|
||||||
# the key would be readable in the app: it is left empty, so only the MapTiler layer fails
|
|
||||||
- name: Create env file
|
|
||||||
run: echo "PUBLIC_MAPTILER_KEY=" >> website/.env
|
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
run: npx --yes @tauri-apps/cli@2 build ${{ matrix.args }}
|
|
||||||
working-directory: tauri
|
|
||||||
|
|
||||||
# after the build, which gives the app the website it embeds
|
|
||||||
- name: Test engine and app
|
|
||||||
run: cargo test -p gpx-engine -p gpx-studio-desktop
|
|
||||||
|
|
||||||
- name: Upload bundles
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: bundles-${{ matrix.platform }}
|
|
||||||
path: ${{ matrix.bundles }}
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
# replaces the beta release only once every platform is built
|
|
||||||
release:
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
GH_REPO: ${{ github.repository }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Download bundles
|
|
||||||
uses: actions/download-artifact@v5
|
|
||||||
with:
|
|
||||||
pattern: bundles-*
|
|
||||||
path: bundles
|
|
||||||
merge-multiple: true
|
|
||||||
|
|
||||||
- name: Publish beta release
|
|
||||||
# the bundles of a platform are in a folder per format (deb, msi…)
|
|
||||||
run: |
|
|
||||||
mapfile -t files < <(find bundles -type f)
|
|
||||||
gh release delete desktop-beta --cleanup-tag --yes || true
|
|
||||||
gh release create desktop-beta "${files[@]}" \
|
|
||||||
--prerelease \
|
|
||||||
--target ${{ github.sha }} \
|
|
||||||
--title "Desktop beta" \
|
|
||||||
--notes "Beta of the desktop app, built from ${{ github.sha }} on the dev branch. The builds are not signed yet: macOS and Windows will warn before opening them."
|
|
||||||
+1
-2
@@ -1,2 +1 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
target/
|
|
||||||
Generated
-5330
File diff suppressed because it is too large.
Load diff
@@ -1,5 +0,0 @@
|
|||||||
[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,20 +25,18 @@ Any help is greatly appreciated!
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
The code is split into four parts:
|
The code is split into two parts:
|
||||||
|
|
||||||
- `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`),
|
- `gpx-rs`: a Rust library, compiled to WebAssembly, for parsing and manipulating GPX files,
|
||||||
- `wasm`: the engine for the browser, compiled to WebAssembly,
|
- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application.
|
||||||
- `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 the website. See [`tauri`](tauri/README.md) for 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.
|
||||||
|
|
||||||
### Building the WebAssembly engine
|
### Building the `gpx-rs` library
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd wasm
|
cd gpx-rs
|
||||||
wasm-pack build --target bundler
|
wasm-pack build wasm --target bundler --out-dir ../pkg
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running the website
|
### Running the website
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
target/
|
||||||
|
pkg/
|
||||||
Generated
+794
@@ -0,0 +1,794 @@
|
|||||||
|
# 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"
|
||||||
+5
-10
@@ -1,11 +1,6 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "gpx-engine"
|
members = ["engine", "wasm"]
|
||||||
version = "0.1.0"
|
resolver = "3"
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
[profile.release]
|
||||||
chrono = "0.4.45"
|
debug = true
|
||||||
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"] }
|
|
||||||
+11
-7
@@ -1,10 +1,14 @@
|
|||||||
# gpx-rs
|
# gpx-rs
|
||||||
|
|
||||||
The GPX engine: plain Rust, no knowledge of who uses it. Tested natively with `cargo test` (from
|
- `engine`: the GPX engine, plain Rust. Tested natively: `cargo test`.
|
||||||
here or from the root).
|
- `wasm`: the interface to the web page, compiled to WebAssembly.
|
||||||
|
|
||||||
- `src/core`, `src/engine`, `src/storage`: the engine itself.
|
```
|
||||||
- `src/api`: the interface to the engine that the frontends (`../wasm`, `../tauri`) share. Each
|
wasm-pack build wasm --target bundler --out-dir ../pkg
|
||||||
function takes the engine and typed arguments, and gives plain data (`api::dto`, serde types, or
|
```
|
||||||
slices for the numeric buffers). A frontend only converts what crosses: ids from strings or
|
|
||||||
bytes, results to JS objects or JSON, slices to typed arrays or raw bytes.
|
The interface is tested in Node (it needs `wasm-pack`, which fetches the test runner the first time):
|
||||||
|
|
||||||
|
```
|
||||||
|
wasm-pack test --node wasm
|
||||||
|
```
|
||||||
@@ -1,505 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xmlns="http://www.topografix.com/GPX/1/1"
|
|
||||||
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd
|
|
||||||
http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd
|
|
||||||
http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd
|
|
||||||
http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd"
|
|
||||||
xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1"
|
|
||||||
xmlns:gpxpx="http://www.garmin.com/xmlschemas/PowerExtension/v1"
|
|
||||||
xmlns:gpx_style="http://www.topografix.com/GPX/gpx_style/0/2" version="1.1"
|
|
||||||
creator="https://gpx.studio">
|
|
||||||
<metadata>
|
|
||||||
<name>with_power</name>
|
|
||||||
<author>
|
|
||||||
<name>gpx.studio</name>
|
|
||||||
<link href="https://gpx.studio"></link>
|
|
||||||
</author>
|
|
||||||
</metadata>
|
|
||||||
<trk>
|
|
||||||
<name>with_power</name>
|
|
||||||
<type>Cycling</type>
|
|
||||||
<trkseg>
|
|
||||||
<trkpt lat="50.790867" lon="4.404968">
|
|
||||||
<ele>109.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.790714" lon="4.405036">
|
|
||||||
<ele>110.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.790336" lon="4.405259">
|
|
||||||
<ele>110.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.790165" lon="4.405331">
|
|
||||||
<ele>110.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.790008" lon="4.405359">
|
|
||||||
<ele>110.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.789818" lon="4.405359">
|
|
||||||
<ele>109.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.789409" lon="4.40534">
|
|
||||||
<ele>107.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.789105" lon="4.405411">
|
|
||||||
<ele>106.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.788799" lon="4.405527">
|
|
||||||
<ele>108.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.788645" lon="4.405606">
|
|
||||||
<ele>109.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.7885" lon="4.405711">
|
|
||||||
<ele>110.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.78822" lon="4.405959">
|
|
||||||
<ele>112.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.787956" lon="4.406092">
|
|
||||||
<ele>112.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.787814" lon="4.406143">
|
|
||||||
<ele>113.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.787674" lon="4.406177">
|
|
||||||
<ele>114.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.787451" lon="4.406199">
|
|
||||||
<ele>115.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.787297" lon="4.406177">
|
|
||||||
<ele>114.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.78716" lon="4.406098">
|
|
||||||
<ele>114.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.787045" lon="4.405984">
|
|
||||||
<ele>114.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.786683" lon="4.405653">
|
|
||||||
<ele>114.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.786538" lon="4.405543">
|
|
||||||
<ele>115.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.78635" lon="4.405441">
|
|
||||||
<ele>115.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.786275" lon="4.40542">
|
|
||||||
<ele>115.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.786182" lon="4.405435">
|
|
||||||
<ele>116.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.786121" lon="4.405475">
|
|
||||||
<ele>115.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.786042" lon="4.405558">
|
|
||||||
<ele>115.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.785821" lon="4.405925">
|
|
||||||
<ele>114.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.785672" lon="4.406119">
|
|
||||||
<ele>112.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.785516" lon="4.406256">
|
|
||||||
<ele>110.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.785384" lon="4.406364">
|
|
||||||
<ele>109.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.785126" lon="4.406475">
|
|
||||||
<ele>106.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.784697" lon="4.406537">
|
|
||||||
<ele>104.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.784591" lon="4.40657">
|
|
||||||
<ele>104.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.784507" lon="4.406612">
|
|
||||||
<ele>103.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.784435" lon="4.40669">
|
|
||||||
<ele>103.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.784209" lon="4.407148">
|
|
||||||
<ele>103.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.784162" lon="4.407257">
|
|
||||||
<ele>103.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.784077" lon="4.407372">
|
|
||||||
<ele>104.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.784006" lon="4.407435">
|
|
||||||
<ele>105.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.783924" lon="4.407471">
|
|
||||||
<ele>106.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.783837" lon="4.407486">
|
|
||||||
<ele>107.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.783771" lon="4.407472">
|
|
||||||
<ele>108.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.783697" lon="4.407428">
|
|
||||||
<ele>109.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.783626" lon="4.407363">
|
|
||||||
<ele>110.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.783548" lon="4.407274">
|
|
||||||
<ele>110.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.783458" lon="4.407134">
|
|
||||||
<ele>110.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.783123" lon="4.406435">
|
|
||||||
<ele>111.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.782982" lon="4.406168">
|
|
||||||
<ele>112.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.782871" lon="4.406044">
|
|
||||||
<ele>113.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.78279" lon="4.406021">
|
|
||||||
<ele>113.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.782714" lon="4.406018">
|
|
||||||
<ele>113.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.782607" lon="4.406047">
|
|
||||||
<ele>113.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.782405" lon="4.406194">
|
|
||||||
<ele>114.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.782175" lon="4.406413">
|
|
||||||
<ele>115.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.781749" lon="4.407018">
|
|
||||||
<ele>118.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.781654" lon="4.407316">
|
|
||||||
<ele>119.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.781563" lon="4.407764">
|
|
||||||
<ele>121.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.781487" lon="4.407984">
|
|
||||||
<ele>122.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.781422" lon="4.408216">
|
|
||||||
<ele>122.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.781395" lon="4.408508">
|
|
||||||
<ele>123.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.781399" lon="4.409114">
|
|
||||||
<ele>126.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.781367" lon="4.409428">
|
|
||||||
<ele>128.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.781286" lon="4.409607">
|
|
||||||
<ele>129.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.78116" lon="4.409789">
|
|
||||||
<ele>130.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.780804" lon="4.409993">
|
|
||||||
<ele>130.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.780389" lon="4.410334">
|
|
||||||
<ele>131.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.780232" lon="4.410563">
|
|
||||||
<ele>132.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.780094" lon="4.410827">
|
|
||||||
<ele>132.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.779723" lon="4.411582">
|
|
||||||
<ele>135.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.779591" lon="4.411791">
|
|
||||||
<ele>135.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.779125" lon="4.412435">
|
|
||||||
<ele>132.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.778676" lon="4.412979">
|
|
||||||
<ele>134.0</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.778194" lon="4.413466">
|
|
||||||
<ele>136.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.777427" lon="4.414302">
|
|
||||||
<ele>137.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.777165" lon="4.414736">
|
|
||||||
<ele>137.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.776927" lon="4.415201">
|
|
||||||
<ele>137.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.776778" lon="4.415613">
|
|
||||||
<ele>137.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.776553" lon="4.416425">
|
|
||||||
<ele>134.8</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.776326" lon="4.417304">
|
|
||||||
<ele>132.3</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
<trkpt lat="50.776129" lon="4.418383">
|
|
||||||
<ele>129.5</ele>
|
|
||||||
<extensions>
|
|
||||||
<gpxpx:PowerInWatts>210</gpxpx:PowerInWatts>
|
|
||||||
</extensions>
|
|
||||||
</trkpt>
|
|
||||||
</trkseg>
|
|
||||||
</trk>
|
|
||||||
</gpx>
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[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"] }
|
||||||
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.
@@ -6,7 +6,7 @@
|
|||||||
//! between them are kept: every part comes after the previous one in the new order, as late as the
|
//! between them are kept: every part comes after the previous one in the new order, as late as the
|
||||||
//! original one came after the part that followed it.
|
//! original one came after the part that followed it.
|
||||||
|
|
||||||
use crate::{Route, Track, TrackSegment, Trackpoint};
|
use crate::{Track, TrackSegment, Trackpoint};
|
||||||
|
|
||||||
/// The time of the first trackpoint of a segment.
|
/// The time of the first trackpoint of a segment.
|
||||||
pub fn start_time(segment: &TrackSegment) -> Option<i64> {
|
pub fn start_time(segment: &TrackSegment) -> Option<i64> {
|
||||||
@@ -94,19 +94,6 @@ pub fn reverse_segments(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reverses the routes of a file, and their trackpoints, like the segments of a track (see
|
|
||||||
/// [`reverse_segments`]).
|
|
||||||
pub fn reverse_routes(routes: &mut [Route], original_next: Option<i64>, new_previous: Option<i64>) {
|
|
||||||
routes.reverse();
|
|
||||||
let (mut next, mut previous) = (original_next, new_previous);
|
|
||||||
for route in routes.iter_mut() {
|
|
||||||
let original_start = start_time(&route.seg);
|
|
||||||
reverse_segment(&mut route.seg, next, previous);
|
|
||||||
next = original_start;
|
|
||||||
previous = end_time(&route.seg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reverses the tracks of a file, and their segments and trackpoints.
|
/// Reverses the tracks of a file, and their segments and trackpoints.
|
||||||
pub fn reverse_tracks(tracks: &mut [Track], original_next: Option<i64>, new_previous: Option<i64>) {
|
pub fn reverse_tracks(tracks: &mut [Track], original_next: Option<i64>, new_previous: Option<i64>) {
|
||||||
tracks.reverse();
|
tracks.reverse();
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,31 @@
|
|||||||
|
use crate::{Link, Track, Waypoints};
|
||||||
|
|
||||||
|
use super::common::uuid_id;
|
||||||
|
|
||||||
|
uuid_id!(FileId);
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct File {
|
||||||
|
pub id: FileId,
|
||||||
|
pub info: FileInfo,
|
||||||
|
pub trk: Vec<Track>,
|
||||||
|
pub wpt: Waypoints,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
|
pub struct FileInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub desc: Option<String>,
|
||||||
|
pub author: Option<Author>,
|
||||||
|
pub links: Vec<Link>,
|
||||||
|
pub time: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
|
pub struct Author {
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// `id@domain`, which the file gives as the two attributes of an `email` element.
|
||||||
|
pub email: Option<String>,
|
||||||
|
/// The author has a single link, unlike the other elements.
|
||||||
|
pub link: Option<Link>,
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ mod chunk;
|
|||||||
mod chunked;
|
mod chunked;
|
||||||
mod common;
|
mod common;
|
||||||
mod file;
|
mod file;
|
||||||
mod route;
|
|
||||||
mod segment;
|
mod segment;
|
||||||
mod track;
|
mod track;
|
||||||
mod trackpoint;
|
mod trackpoint;
|
||||||
@@ -15,7 +14,6 @@ pub use chunk::*;
|
|||||||
pub use chunked::*;
|
pub use chunked::*;
|
||||||
pub use common::*;
|
pub use common::*;
|
||||||
pub use file::*;
|
pub use file::*;
|
||||||
pub use route::*;
|
|
||||||
pub use segment::*;
|
pub use segment::*;
|
||||||
pub use track::*;
|
pub use track::*;
|
||||||
pub use trackpoint::*;
|
pub use trackpoint::*;
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Author, Chunk, File, Link, LngLat, Route, Track, TrackSegment, Trackpoint,
|
Author, Chunk, File, Link, LngLat, Track, TrackSegment, Trackpoint, TrackpointCategories,
|
||||||
TrackpointCategories, TrackpointChunk, Waypoint, WaypointChunk, distance,
|
TrackpointChunk, Waypoint, WaypointChunk, distance,
|
||||||
};
|
};
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use quick_xml::Error;
|
use quick_xml::Error;
|
||||||
@@ -18,7 +18,6 @@ enum GPXElement {
|
|||||||
Author(Author),
|
Author(Author),
|
||||||
Link(Link),
|
Link(Link),
|
||||||
Text,
|
Text,
|
||||||
/// A track, or a route, whose information is read the same way.
|
|
||||||
Track(Track),
|
Track(Track),
|
||||||
Segment(TrackSegment),
|
Segment(TrackSegment),
|
||||||
Trackpoint(Trackpoint),
|
Trackpoint(Trackpoint),
|
||||||
@@ -397,7 +396,7 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
|
|||||||
let mut stack: Vec<GPXElement> = vec![];
|
let mut stack: Vec<GPXElement> = vec![];
|
||||||
let mut trkpt_chunk = TrackpointChunk::default();
|
let mut trkpt_chunk = TrackpointChunk::default();
|
||||||
let mut wpt_chunk = WaypointChunk::default();
|
let mut wpt_chunk = WaypointChunk::default();
|
||||||
// The segment of a route is read by `RouteReader`. The points of the detailed
|
// Routes are read as tracks of a single segment, see `RouteReader`. The points of the detailed
|
||||||
// path that a route point comes with (`gpxx:rpt`) are collected until the route point is read.
|
// path that a route point comes with (`gpxx:rpt`) are collected until the route point is read.
|
||||||
let mut route = RouteReader::default();
|
let mut route = RouteReader::default();
|
||||||
let mut rpt_points: Vec<Trackpoint> = vec![];
|
let mut rpt_points: Vec<Trackpoint> = vec![];
|
||||||
@@ -531,12 +530,12 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"rte" => {
|
"rte" => {
|
||||||
if let Some(GPXElement::Track(trk)) = stack.pop() {
|
if let Some(GPXElement::Track(mut trk)) = stack.pop() {
|
||||||
gpx.rte.push(Route {
|
let trkseg = std::mem::take(&mut route).finish();
|
||||||
info: trk.info,
|
if !trkseg.is_empty() {
|
||||||
seg: std::mem::take(&mut route).finish(),
|
trk.trkseg.push(trkseg);
|
||||||
..Default::default()
|
}
|
||||||
});
|
gpx.trk.push(trk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"rtept" => {
|
"rtept" => {
|
||||||
@@ -741,37 +740,38 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_routes() {
|
fn test_parse_routes_as_tracks() {
|
||||||
let gpx = parse_data("with_routes");
|
let gpx = parse_data("with_routes");
|
||||||
|
|
||||||
assert!(gpx.trk.is_empty());
|
assert_eq!(gpx.trk.len(), 2);
|
||||||
assert_eq!(gpx.rte.len(), 2);
|
let trk = &gpx.trk[0];
|
||||||
let rte = &gpx.rte[0];
|
assert_eq!(trk.info.name.as_deref(), Some("route 1"));
|
||||||
assert_eq!(rte.info.name.as_deref(), Some("route 1"));
|
assert_eq!(trk.info.type_.as_deref(), Some("Cycling"));
|
||||||
assert_eq!(rte.info.type_.as_deref(), Some("Cycling"));
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
assert_eq!(rte.seg.len(), 49);
|
let trkseg = &trk.trkseg[0];
|
||||||
let trkpt = &rte.seg[0];
|
assert_eq!(trkseg.len(), 49);
|
||||||
|
let trkpt = &trkseg[0];
|
||||||
assert_eq!(trkpt.coordinates.lat, 50.790867);
|
assert_eq!(trkpt.coordinates.lat, 50.790867);
|
||||||
assert_eq!(trkpt.coordinates.lng, 4.404968);
|
assert_eq!(trkpt.coordinates.lng, 4.404968);
|
||||||
assert_eq!(trkpt.ele, 109.0);
|
assert_eq!(trkpt.ele, 109.0);
|
||||||
|
|
||||||
let rte = &gpx.rte[1];
|
let trk = &gpx.trk[1];
|
||||||
assert_eq!(rte.info.name.as_deref(), Some("route 2"));
|
assert_eq!(trk.info.name.as_deref(), Some("route 2"));
|
||||||
assert_eq!(rte.seg.len(), 28);
|
assert_eq!(trk.trkseg[0].len(), 28);
|
||||||
assert_ne!(gpx.rte[0].id, rte.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_route_point_extensions() {
|
fn test_parse_route_point_extensions() {
|
||||||
let gpx = parse_data("with_route_extensions");
|
let gpx = parse_data("with_route_extensions");
|
||||||
|
|
||||||
// a route without points has an empty segment
|
// a route without points has no segment
|
||||||
assert_eq!(gpx.rte.len(), 2);
|
assert_eq!(gpx.trk.len(), 2);
|
||||||
assert!(gpx.rte[1].seg.is_empty());
|
assert!(gpx.trk[1].trkseg.is_empty());
|
||||||
|
|
||||||
let rte = &gpx.rte[0];
|
let trk = &gpx.trk[0];
|
||||||
assert_eq!(rte.info.name.as_deref(), Some("with extensions"));
|
assert_eq!(trk.info.name.as_deref(), Some("with extensions"));
|
||||||
let points: Vec<_> = rte.seg.iter().collect();
|
assert_eq!(trk.trkseg.len(), 1);
|
||||||
|
let points: Vec<_> = trk.trkseg[0].iter().collect();
|
||||||
// the route points are kept, the points of the detailed path follow the one they come with
|
// the route points are kept, the points of the detailed path follow the one they come with
|
||||||
let coordinates: Vec<_> = points
|
let coordinates: Vec<_> = points
|
||||||
.iter()
|
.iter()
|
||||||
@@ -788,13 +788,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_route_points_are_the_anchors_and_nothing_else_is() {
|
fn test_route_points_are_the_anchors_and_nothing_else_is() {
|
||||||
let gpx = parse_data("with_route_extensions");
|
let gpx = parse_data("with_route_extensions");
|
||||||
let anchors: Vec<_> = gpx.rte[0].seg.iter().map(|p| p.anchor).collect();
|
let anchors: Vec<_> = gpx.trk[0].trkseg[0].iter().map(|p| p.anchor).collect();
|
||||||
assert_eq!(anchors, [Some(0), None, None, Some(0), Some(0)]);
|
assert_eq!(anchors, [Some(0), None, None, Some(0), Some(0)]);
|
||||||
|
|
||||||
// no anchor is computed for the points of a route that has none in between
|
// no anchor is computed for the points of a route that has none in between
|
||||||
let gpx = parse_data("with_routes");
|
let gpx = parse_data("with_routes");
|
||||||
for rte in &gpx.rte {
|
for trk in &gpx.trk {
|
||||||
assert!(rte.seg.iter().all(|p| p.anchor == Some(0)));
|
let segment = &trk.trkseg[0];
|
||||||
|
assert!(segment.iter().all(|p| p.anchor == Some(0)));
|
||||||
}
|
}
|
||||||
// while a track gets the anchors that its shape needs
|
// while a track gets the anchors that its shape needs
|
||||||
let track = parse_data("with_tracks");
|
let track = parse_data("with_tracks");
|
||||||
@@ -806,14 +807,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_parse_the_points_of_the_detailed_path_of_a_route() {
|
fn test_parse_the_points_of_the_detailed_path_of_a_route() {
|
||||||
let gpx = parse_data("with_route_points");
|
let gpx = parse_data("with_route_points");
|
||||||
assert_eq!(gpx.rte.len(), 1);
|
assert_eq!(gpx.trk.len(), 1);
|
||||||
let rte = &gpx.rte[0];
|
let trk = &gpx.trk[0];
|
||||||
assert_eq!(rte.info.name.as_deref(), Some("Lausanne"));
|
assert_eq!(trk.info.name.as_deref(), Some("Lausanne"));
|
||||||
assert_eq!(rte.info.cmt.as_deref(), Some("comment"));
|
assert_eq!(trk.info.cmt.as_deref(), Some("comment"));
|
||||||
assert_eq!(rte.info.desc.as_deref(), Some("description"));
|
assert_eq!(trk.info.desc.as_deref(), Some("description"));
|
||||||
assert_eq!(rte.info.type_.as_deref(), Some("Cycling"));
|
assert_eq!(trk.info.type_.as_deref(), Some("Cycling"));
|
||||||
|
|
||||||
let points: Vec<_> = rte.seg.iter().cloned().collect();
|
let points: Vec<_> = trk.trkseg[0].iter().cloned().collect();
|
||||||
// 3 route points and 4 points between them, the repeated one being kept
|
// 3 route points and 4 points between them, the repeated one being kept
|
||||||
assert_eq!(points.len(), 7);
|
assert_eq!(points.len(), 7);
|
||||||
let kinds: Vec<_> = points.iter().map(|p| p.anchor.is_some()).collect();
|
let kinds: Vec<_> = points.iter().map(|p| p.anchor.is_some()).collect();
|
||||||
@@ -845,7 +846,7 @@ mod tests {
|
|||||||
</rtept>
|
</rtept>
|
||||||
</rte></gpx>"#;
|
</rte></gpx>"#;
|
||||||
let gpx = parse(data.as_bytes(), &mut Default::default()).unwrap();
|
let gpx = parse(data.as_bytes(), &mut Default::default()).unwrap();
|
||||||
let points: Vec<_> = gpx.rte[0].seg.iter().cloned().collect();
|
let points: Vec<_> = gpx.trk[0].trkseg[0].iter().cloned().collect();
|
||||||
let coordinates: Vec<_> = points.iter().map(|p| p.coordinates.lat).collect();
|
let coordinates: Vec<_> = points.iter().map(|p| p.coordinates.lat).collect();
|
||||||
assert_eq!(coordinates, [1.0, 2.0, 2.1, 2.2]);
|
assert_eq!(coordinates, [1.0, 2.0, 2.1, 2.2]);
|
||||||
// the route point is an anchor, and so is the end of the path
|
// the route point is an anchor, and so is the end of the path
|
||||||
@@ -860,7 +861,7 @@ mod tests {
|
|||||||
<gpxx:RoutePointExtension><gpxx:rpt lat="1.5" lon="1.5"/></gpxx:RoutePointExtension>
|
<gpxx:RoutePointExtension><gpxx:rpt lat="1.5" lon="1.5"/></gpxx:RoutePointExtension>
|
||||||
</extensions></rtept></rte></gpx>"#;
|
</extensions></rtept></rte></gpx>"#;
|
||||||
let gpx = parse(data.as_bytes(), &mut Default::default()).unwrap();
|
let gpx = parse(data.as_bytes(), &mut Default::default()).unwrap();
|
||||||
let anchors: Vec<_> = gpx.rte[0].seg.iter().map(|p| p.anchor).collect();
|
let anchors: Vec<_> = gpx.trk[0].trkseg[0].iter().map(|p| p.anchor).collect();
|
||||||
assert_eq!(anchors, [Some(0), Some(0)]);
|
assert_eq!(anchors, [Some(0), Some(0)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -929,14 +930,14 @@ mod tests {
|
|||||||
assert_eq!(waypoints[0].links[2].text.as_deref(), Some("three"));
|
assert_eq!(waypoints[0].links[2].text.as_deref(), Some("three"));
|
||||||
assert!(waypoints[1].links.is_empty());
|
assert!(waypoints[1].links.is_empty());
|
||||||
|
|
||||||
// tracks, and routes
|
// tracks, and routes which are read as tracks
|
||||||
assert_eq!((gpx.trk.len(), gpx.rte.len()), (1, 1));
|
assert_eq!(gpx.trk.len(), 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
hrefs(&gpx.trk[0].info.links),
|
hrefs(&gpx.trk[0].info.links),
|
||||||
["https://example.com/trk-1", "https://example.com/trk-2"]
|
["https://example.com/trk-1", "https://example.com/trk-2"]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
hrefs(&gpx.rte[0].info.links),
|
hrefs(&gpx.trk[1].info.links),
|
||||||
["https://example.com/rte-1", "https://example.com/rte-2"]
|
["https://example.com/rte-1", "https://example.com/rte-2"]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1222,19 +1223,6 @@ mod tests {
|
|||||||
assert!(trkpt.power.is_some_and(|p| p == 200));
|
assert!(trkpt.power.is_some_and(|p| p == 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_trackpoint_power_3() {
|
|
||||||
let gpx = parse_data("with_power_3");
|
|
||||||
|
|
||||||
assert_eq!(gpx.trk.len(), 1);
|
|
||||||
let trk = &gpx.trk[0];
|
|
||||||
assert_eq!(trk.trkseg.len(), 1);
|
|
||||||
let trkseg = &trk.trkseg[0];
|
|
||||||
assert!(!trkseg.is_empty());
|
|
||||||
let trkpt = &trkseg[0];
|
|
||||||
assert!(trkpt.power.is_some_and(|p| p == 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_trackpoint_atemp() {
|
fn test_parse_trackpoint_atemp() {
|
||||||
let gpx = parse_data("with_temp");
|
let gpx = parse_data("with_temp");
|
||||||
@@ -8,6 +8,9 @@ use crate::{File, Link, Track, Trackpoint, TrackpointCategories, Waypoint};
|
|||||||
|
|
||||||
/// What the written file contains: the data of the trackpoints that are `false` are left out.
|
/// What the written file contains: the data of the trackpoints that are `false` are left out.
|
||||||
/// The same type tells which data a file has, see [`File::exportable_data`].
|
/// The same type tells which data a file has, see [`File::exportable_data`].
|
||||||
|
///
|
||||||
|
/// With `as_route`, the file is written with routes instead of tracks, see [`write`]. It is not
|
||||||
|
/// data: it is off in `ALL` and `NONE`, and in what [`File::exportable_data`] gives.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct ExportOptions {
|
pub struct ExportOptions {
|
||||||
pub time: bool,
|
pub time: bool,
|
||||||
@@ -17,6 +20,8 @@ pub struct ExportOptions {
|
|||||||
pub power: bool,
|
pub power: bool,
|
||||||
/// The OpenStreetMap data: surface, highway, SAC scale and MTB scale.
|
/// The OpenStreetMap data: surface, highway, SAC scale and MTB scale.
|
||||||
pub osm: bool,
|
pub osm: bool,
|
||||||
|
/// Writes every segment as a route (`rte`) instead of a track.
|
||||||
|
pub as_route: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExportOptions {
|
impl ExportOptions {
|
||||||
@@ -31,6 +36,7 @@ impl ExportOptions {
|
|||||||
atemp: value,
|
atemp: value,
|
||||||
power: value,
|
power: value,
|
||||||
osm: value,
|
osm: value,
|
||||||
|
as_route: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +49,7 @@ impl ExportOptions {
|
|||||||
atemp: self.atemp || other.atemp,
|
atemp: self.atemp || other.atemp,
|
||||||
power: self.power || other.power,
|
power: self.power || other.power,
|
||||||
osm: self.osm || other.osm,
|
osm: self.osm || other.osm,
|
||||||
|
as_route: self.as_route || other.as_route,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +59,12 @@ impl File {
|
|||||||
/// options of [`write`] can leave out.
|
/// options of [`write`] can leave out.
|
||||||
pub fn exportable_data(&self) -> ExportOptions {
|
pub fn exportable_data(&self) -> ExportOptions {
|
||||||
let mut data = ExportOptions::NONE;
|
let mut data = ExportOptions::NONE;
|
||||||
for point in self.segments().flat_map(|seg| seg.iter()) {
|
for point in self
|
||||||
|
.trk
|
||||||
|
.iter()
|
||||||
|
.flat_map(|trk| &trk.trkseg)
|
||||||
|
.flat_map(|seg| seg.iter())
|
||||||
|
{
|
||||||
data.time |= point.time.is_some();
|
data.time |= point.time.is_some();
|
||||||
data.hr |= point.hr.is_some();
|
data.hr |= point.hr.is_some();
|
||||||
data.cad |= point.cad.is_some();
|
data.cad |= point.cad.is_some();
|
||||||
@@ -70,10 +82,6 @@ impl File {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The highest zoom level from which an anchor is shown that makes it a route point, see [`write`]:
|
|
||||||
/// the anchors that only appear when zooming in further are details of the path.
|
|
||||||
pub const ROUTE_POINT_MAX_ZOOM: u8 = 12;
|
|
||||||
|
|
||||||
const SCHEMA_LOCATION: &str = "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd";
|
const SCHEMA_LOCATION: &str = "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd";
|
||||||
|
|
||||||
type XmlWriter = Writer<Vec<u8>>;
|
type XmlWriter = Writer<Vec<u8>>;
|
||||||
@@ -81,10 +89,10 @@ type XmlWriter = Writer<Vec<u8>>;
|
|||||||
/// Writes a file as GPX 1.1, in UTF-8. The surface, highway, SAC scale and MTB scale of the trackpoints are
|
/// Writes a file as GPX 1.1, in UTF-8. The surface, highway, SAC scale and MTB scale of the trackpoints are
|
||||||
/// the names of their codes in `categories`.
|
/// the names of their codes in `categories`.
|
||||||
///
|
///
|
||||||
/// The routes are written as routes (`rte`): the anchors of their segment that are shown from the
|
/// The segments are written as tracks, or with `options.as_route` as routes: a segment becomes a
|
||||||
/// zoom level [`ROUTE_POINT_MAX_ZOOM`] or a lower one are the `rtept`, and the trackpoints between
|
/// `rte` that has the information of its track, its anchors are the `rtept` and the trackpoints
|
||||||
/// two of them are the `gpxx:rpt` of the `rtept` before them (which only have a position, unlike
|
/// between two anchors are the `gpxx:rpt` of the `rtept` before them (which only have a
|
||||||
/// the `rtept`). The first and last trackpoints always are `rtept`.
|
/// position, unlike the `rtept`). The first and last trackpoints always are `rtept`.
|
||||||
pub fn write(file: &File, categories: &TrackpointCategories, options: ExportOptions) -> Vec<u8> {
|
pub fn write(file: &File, categories: &TrackpointCategories, options: ExportOptions) -> Vec<u8> {
|
||||||
let mut writer = Writer::new_with_indent(Vec::new(), b' ', 4);
|
let mut writer = Writer::new_with_indent(Vec::new(), b' ', 4);
|
||||||
// writing to memory does not fail
|
// writing to memory does not fail
|
||||||
@@ -92,9 +100,9 @@ pub fn write(file: &File, categories: &TrackpointCategories, options: ExportOpti
|
|||||||
writer.into_inner()
|
writer.into_inner()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_file<'a>(
|
fn write_file(
|
||||||
w: &mut XmlWriter,
|
w: &mut XmlWriter,
|
||||||
file: &'a File,
|
file: &File,
|
||||||
categories: &TrackpointCategories,
|
categories: &TrackpointCategories,
|
||||||
options: ExportOptions,
|
options: ExportOptions,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
@@ -128,18 +136,20 @@ fn write_file<'a>(
|
|||||||
for wpt in file.wpt.iter() {
|
for wpt in file.wpt.iter() {
|
||||||
write_waypoint(w, wpt, options)?;
|
write_waypoint(w, wpt, options)?;
|
||||||
}
|
}
|
||||||
// a single unnamed route or track is named like the file
|
|
||||||
let alone = file.rte.len() + file.trk.len() == 1;
|
|
||||||
let name = |info: &'a crate::TrackInfo| match &info.name {
|
|
||||||
Some(name) if !name.is_empty() => Some(name.as_str()),
|
|
||||||
_ if alone => Some(file.info.name.as_str()),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
for rte in &file.rte {
|
|
||||||
write_route(w, &rte.info, &rte.seg, name(&rte.info), categories, options)?;
|
|
||||||
}
|
|
||||||
for trk in &file.trk {
|
for trk in &file.trk {
|
||||||
write_track(w, trk, name(&trk.info), categories, options)?;
|
// a single unnamed track is named like the file
|
||||||
|
let name = match (&trk.info.name, file.trk.len()) {
|
||||||
|
(Some(name), _) if !name.is_empty() => Some(name.as_str()),
|
||||||
|
(_, 1) => Some(file.info.name.as_str()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if options.as_route {
|
||||||
|
for segment in trk.trkseg.iter().filter(|segment| !segment.is_empty()) {
|
||||||
|
write_route(w, trk, segment, name, categories, options)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
write_track(w, trk, name, categories, options)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
@@ -301,28 +311,22 @@ fn write_track(
|
|||||||
|
|
||||||
fn write_route(
|
fn write_route(
|
||||||
w: &mut XmlWriter,
|
w: &mut XmlWriter,
|
||||||
info: &crate::TrackInfo,
|
trk: &Track,
|
||||||
segment: &crate::TrackSegment,
|
segment: &crate::TrackSegment,
|
||||||
name: Option<&str>,
|
name: Option<&str>,
|
||||||
categories: &TrackpointCategories,
|
categories: &TrackpointCategories,
|
||||||
options: ExportOptions,
|
options: ExportOptions,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
w.create_element("rte").write_inner_content(|w| {
|
w.create_element("rte").write_inner_content(|w| {
|
||||||
write_track_info(w, name, info)?;
|
write_track_info(w, name, &trk.info)?;
|
||||||
let points: Vec<&Trackpoint> = segment.iter().collect();
|
let points: Vec<&Trackpoint> = segment.iter().collect();
|
||||||
let last = points.len().saturating_sub(1);
|
let last = points.len() - 1;
|
||||||
let is_route_point = |i: usize| {
|
let is_anchor = |i: usize| i == 0 || i == last || points[i].anchor.is_some();
|
||||||
i == 0
|
|
||||||
|| i == last
|
|
||||||
|| points[i]
|
|
||||||
.anchor
|
|
||||||
.is_some_and(|zoom| zoom <= ROUTE_POINT_MAX_ZOOM)
|
|
||||||
};
|
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < points.len() {
|
while i < points.len() {
|
||||||
// the trackpoints up to the next route point
|
// the trackpoints up to the next anchor
|
||||||
let mut next = i + 1;
|
let mut next = i + 1;
|
||||||
while next < points.len() && !is_route_point(next) {
|
while next < points.len() && !is_anchor(next) {
|
||||||
next += 1;
|
next += 1;
|
||||||
}
|
}
|
||||||
write_point(
|
write_point(
|
||||||
@@ -402,10 +406,9 @@ fn write_point(
|
|||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
// `PowerInWatts` is itself the root element of Garmin's power extension, without a
|
|
||||||
// wrapper
|
|
||||||
if let Some(power) = power {
|
if let Some(power) = power {
|
||||||
number(w, "gpxpx:PowerInWatts", power)?;
|
w.create_element("gpxpx:PowerExtension")
|
||||||
|
.write_inner_content(|w| number(w, "gpxpx:PowerInWatts", power))?;
|
||||||
}
|
}
|
||||||
if !path.is_empty() {
|
if !path.is_empty() {
|
||||||
w.create_element("gpxx:RoutePointExtension")
|
w.create_element("gpxx:RoutePointExtension")
|
||||||
@@ -452,7 +455,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn points(file: &File) -> Vec<Trackpoint> {
|
fn points(file: &File) -> Vec<Trackpoint> {
|
||||||
file.segments()
|
file.trk
|
||||||
|
.iter()
|
||||||
|
.flat_map(|trk| &trk.trkseg)
|
||||||
.flat_map(|seg| seg.iter().cloned())
|
.flat_map(|seg| seg.iter().cloned())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -512,7 +517,6 @@ mod tests {
|
|||||||
"with_temp",
|
"with_temp",
|
||||||
"with_power_1",
|
"with_power_1",
|
||||||
"with_power_2",
|
"with_power_2",
|
||||||
"with_power_3",
|
|
||||||
"with_surface",
|
"with_surface",
|
||||||
"with_highway",
|
"with_highway",
|
||||||
"with_style",
|
"with_style",
|
||||||
@@ -580,16 +584,6 @@ mod tests {
|
|||||||
assert_eq!(again.trk[0].info.name.as_deref(), Some("a <b> & c"));
|
assert_eq!(again.trk[0].info.name.as_deref(), Some("a <b> & c"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_power_is_written_without_a_wrapper() {
|
|
||||||
let (file, categories) = read("with_power_2");
|
|
||||||
let text: String = written_text(&file, &categories, ExportOptions::ALL)
|
|
||||||
.split_whitespace()
|
|
||||||
.collect();
|
|
||||||
assert!(text.contains("<extensions><gpxpx:PowerInWatts>200</gpxpx:PowerInWatts>"));
|
|
||||||
assert!(!text.contains("<gpxpx:PowerExtension>"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_exportable_data() {
|
fn test_exportable_data() {
|
||||||
let (file, _) = read("with_time");
|
let (file, _) = read("with_time");
|
||||||
@@ -601,20 +595,11 @@ mod tests {
|
|||||||
assert!(file.exportable_data().power);
|
assert!(file.exportable_data().power);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The file with each segment of its tracks as a route, which has the information of its
|
fn as_route() -> ExportOptions {
|
||||||
/// track.
|
ExportOptions {
|
||||||
fn as_routes(mut file: File) -> File {
|
as_route: true,
|
||||||
file.rte = std::mem::take(&mut file.trk)
|
..ExportOptions::ALL
|
||||||
.into_iter()
|
}
|
||||||
.flat_map(|trk| {
|
|
||||||
trk.trkseg.into_iter().map(move |seg| crate::Route {
|
|
||||||
info: trk.info.clone(),
|
|
||||||
seg,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
file
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn count(text: &str, pattern: &str) -> usize {
|
fn count(text: &str, pattern: &str) -> usize {
|
||||||
@@ -622,17 +607,19 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_routes_are_written_with_their_route_points_and_their_path() {
|
fn test_export_as_route_writes_routes_instead_of_tracks() {
|
||||||
let (file, categories) = read("with_route_points");
|
let (file, categories) = read("with_route_points");
|
||||||
let text = written_text(&file, &categories, ExportOptions::ALL);
|
let text = written_text(&file, &categories, as_route());
|
||||||
|
|
||||||
assert_eq!(count(&text, "<rte>"), 1);
|
assert_eq!(count(&text, "<rte>"), 1);
|
||||||
assert_eq!(count(&text, "<trk>"), 0);
|
assert_eq!(count(&text, "<trk>"), 0);
|
||||||
|
assert_eq!(count(&text, "<trkseg>"), 0);
|
||||||
assert_eq!(count(&text, "<trkpt"), 0);
|
assert_eq!(count(&text, "<trkpt"), 0);
|
||||||
// the anchors are the route points, the trackpoints between them the points of their path
|
// the anchors are the route points, the trackpoints between them the points of their path
|
||||||
assert_eq!(count(&text, "<rtept"), 3);
|
assert_eq!(count(&text, "<rtept"), 3);
|
||||||
assert_eq!(count(&text, "<gpxx:rpt"), 4);
|
assert_eq!(count(&text, "<gpxx:rpt"), 4);
|
||||||
assert_eq!(count(&text, "<gpxx:RoutePointExtension>"), 2);
|
assert_eq!(count(&text, "<gpxx:RoutePointExtension>"), 2);
|
||||||
|
// a route has the information of its track
|
||||||
assert!(text.contains("<name>Lausanne</name>"));
|
assert!(text.contains("<name>Lausanne</name>"));
|
||||||
assert!(text.contains("<cmt>comment</cmt>"));
|
assert!(text.contains("<cmt>comment</cmt>"));
|
||||||
assert!(text.contains("<desc>description</desc>"));
|
assert!(text.contains("<desc>description</desc>"));
|
||||||
@@ -647,14 +634,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_a_route_survives_being_written() {
|
fn test_a_route_survives_being_exported_as_a_route() {
|
||||||
let (file, mut categories) = read("with_route_points");
|
let (file, mut categories) = read("with_route_points");
|
||||||
let written = write(&file, &categories, ExportOptions::ALL);
|
let written = write(&file, &categories, as_route());
|
||||||
let again = parse(&written, &mut categories).unwrap();
|
let again = parse(&written, &mut categories).unwrap();
|
||||||
|
|
||||||
assert_eq!(again.rte.len(), 1);
|
assert_eq!(again.trk.len(), 1);
|
||||||
assert_eq!(again.rte[0].info, file.rte[0].info);
|
assert_eq!(again.trk[0].info, file.trk[0].info);
|
||||||
let (a, b) = (&file.rte[0].seg, &again.rte[0].seg);
|
let (a, b) = (&file.trk[0].trkseg[0], &again.trk[0].trkseg[0]);
|
||||||
assert_eq!(a.len(), b.len());
|
assert_eq!(a.len(), b.len());
|
||||||
for (a, b) in a.iter().zip(b.iter()) {
|
for (a, b) in a.iter().zip(b.iter()) {
|
||||||
assert_eq!(a.coordinates.lat, b.coordinates.lat);
|
assert_eq!(a.coordinates.lat, b.coordinates.lat);
|
||||||
@@ -672,121 +659,85 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_the_anchors_shown_up_to_zoom_12_are_the_route_points() {
|
fn test_tracks_become_one_route_per_segment_with_the_information_of_the_track() {
|
||||||
let (file, mut categories) = read("with_tracks_and_segments");
|
let (file, mut categories) = read("with_tracks_and_segments");
|
||||||
let file = as_routes(file);
|
let segments: usize = file.trk.iter().map(|t| t.trkseg.len()).sum();
|
||||||
assert!(file.rte.len() > 2);
|
assert!(segments > 2);
|
||||||
let written = write(&file, &categories, ExportOptions::ALL);
|
let written = write(&file, &categories, as_route());
|
||||||
|
let text = String::from_utf8(written.clone()).unwrap();
|
||||||
|
assert_eq!(count(&text, "<rte>"), segments);
|
||||||
|
assert_eq!(count(&text, "<trk>"), 0);
|
||||||
|
// every route starts and ends with a route point
|
||||||
|
assert_eq!(count(&text, "</rte>"), segments);
|
||||||
|
|
||||||
let again = parse(&written, &mut categories).unwrap();
|
let again = parse(&written, &mut categories).unwrap();
|
||||||
assert_eq!(again.rte.len(), file.rte.len());
|
assert_eq!(again.trk.len(), segments);
|
||||||
let anchors = |s: &crate::TrackSegment, max_zoom: u8| {
|
let mut routes = again.trk.iter();
|
||||||
s.iter()
|
for track in &file.trk {
|
||||||
.enumerate()
|
for segment in &track.trkseg {
|
||||||
.filter(|(_, p)| p.anchor.is_some_and(|zoom| zoom <= max_zoom))
|
let route = routes.next().unwrap();
|
||||||
.map(|(i, _)| i)
|
// the information of the track, repeated for each of its segments
|
||||||
.collect::<Vec<_>>()
|
assert_eq!(route.info.name, track.info.name);
|
||||||
};
|
assert_eq!(route.info.type_, track.info.type_);
|
||||||
for (a, b) in file.rte.iter().zip(&again.rte) {
|
let (a, b) = (segment, &route.trkseg[0]);
|
||||||
assert_eq!(a.info.name, b.info.name);
|
assert_eq!(a.len(), b.len());
|
||||||
assert_eq!(a.seg.len(), b.seg.len());
|
for (a, b) in a.iter().zip(b.iter()) {
|
||||||
// the route points come back as anchors, the other anchors as points of the path
|
assert_eq!(
|
||||||
assert_eq!(
|
(a.coordinates.lat, a.coordinates.lng),
|
||||||
anchors(&a.seg, ROUTE_POINT_MAX_ZOOM),
|
(b.coordinates.lat, b.coordinates.lng)
|
||||||
anchors(&b.seg, u8::MAX)
|
);
|
||||||
);
|
}
|
||||||
|
// the anchors of the segment are the ones of its route
|
||||||
|
let anchors = |s: &crate::TrackSegment| {
|
||||||
|
s.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, p)| p.anchor.is_some())
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
assert_eq!(anchors(a), anchors(b));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_anchors_shown_from_a_higher_zoom_are_points_of_the_path() {
|
|
||||||
let (file, categories) = read("simple");
|
|
||||||
let mut file = as_routes(file);
|
|
||||||
let segment = &mut file.rte[0].seg;
|
|
||||||
segment.update_all(|_, point| point.anchor = None);
|
|
||||||
let len = segment.len();
|
|
||||||
segment.set_anchor(5, ROUTE_POINT_MAX_ZOOM);
|
|
||||||
segment.set_anchor(10, ROUTE_POINT_MAX_ZOOM + 1);
|
|
||||||
let text = written_text(&file, &categories, ExportOptions::ALL);
|
|
||||||
// the ends, and the anchor shown from zoom 12
|
|
||||||
assert_eq!(count(&text, "<rtept"), 3);
|
|
||||||
assert_eq!(count(&text, "<gpxx:rpt"), len - 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_the_ends_of_a_route_are_route_points_even_without_anchors() {
|
fn test_the_ends_of_a_route_are_route_points_even_without_anchors() {
|
||||||
let (file, categories) = read("simple");
|
let (mut file, categories) = read("simple");
|
||||||
let mut file = as_routes(file);
|
file.trk[0].trkseg[0].update_all(|_, point| point.anchor = None);
|
||||||
file.rte[0].seg.update_all(|_, point| point.anchor = None);
|
let len = file.trk[0].trkseg[0].len();
|
||||||
let len = file.rte[0].seg.len();
|
let text = written_text(&file, &categories, as_route());
|
||||||
let text = written_text(&file, &categories, ExportOptions::ALL);
|
|
||||||
assert_eq!(count(&text, "<rtept"), 2);
|
assert_eq!(count(&text, "<rtept"), 2);
|
||||||
assert_eq!(count(&text, "<gpxx:rpt"), len - 2);
|
assert_eq!(count(&text, "<gpxx:rpt"), len - 2);
|
||||||
// and a single trackpoint is a route of one point
|
// and a single trackpoint is a route of one point
|
||||||
file.rte[0].seg.splice(1, len, vec![]);
|
file.trk[0].trkseg[0].splice(1, len, vec![]);
|
||||||
let text = written_text(&file, &categories, ExportOptions::ALL);
|
let text = written_text(&file, &categories, as_route());
|
||||||
assert_eq!(count(&text, "<rtept"), 1);
|
assert_eq!(count(&text, "<rtept"), 1);
|
||||||
assert_eq!(count(&text, "<gpxx:rpt"), 0);
|
assert_eq!(count(&text, "<gpxx:rpt"), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_routes_keep_their_style() {
|
fn test_empty_segments_are_not_routes() {
|
||||||
|
let (mut file, categories) = read("simple");
|
||||||
|
file.trk[0].trkseg.push(crate::TrackSegment::default());
|
||||||
|
let text = written_text(&file, &categories, as_route());
|
||||||
|
assert_eq!(count(&text, "<rte>"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_routes_have_the_style_of_their_track() {
|
||||||
let (file, categories) = read("with_style");
|
let (file, categories) = read("with_style");
|
||||||
let file = as_routes(file);
|
let text = written_text(&file, &categories, as_route());
|
||||||
let text = written_text(&file, &categories, ExportOptions::ALL);
|
|
||||||
assert!(count(&text, "<gpx_style:line>") >= 1);
|
assert!(count(&text, "<gpx_style:line>") >= 1);
|
||||||
assert_eq!(count(&text, "<gpx_style:line>"), count(&text, "<rte>"));
|
assert_eq!(count(&text, "<gpx_style:line>"), count(&text, "<rte>"));
|
||||||
let again = parse(text.as_bytes(), &mut Default::default()).unwrap();
|
let again = parse(text.as_bytes(), &mut Default::default()).unwrap();
|
||||||
assert_eq!(again.rte[0].info.color, file.rte[0].info.color);
|
assert_eq!(again.trk[0].info.color, file.trk[0].info.color);
|
||||||
assert_eq!(again.rte[0].info.width, file.rte[0].info.width);
|
assert_eq!(again.trk[0].info.width, file.trk[0].info.width);
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_routes_are_written_as_routes_before_the_tracks() {
|
|
||||||
let (mut file, categories) = read("with_tracks");
|
|
||||||
let (routes, _) = read("with_routes");
|
|
||||||
file.rte = routes.rte.clone();
|
|
||||||
let text = written_text(&file, &categories, ExportOptions::ALL);
|
|
||||||
assert_eq!(count(&text, "<rte>"), routes.rte.len());
|
|
||||||
assert_eq!(count(&text, "<trk>"), file.trk.len());
|
|
||||||
assert!(text.rfind("</rte>").unwrap() < text.find("<trk>").unwrap());
|
|
||||||
|
|
||||||
let again = parse(text.as_bytes(), &mut categories.clone()).unwrap();
|
|
||||||
assert_eq!(again.rte.len(), file.rte.len());
|
|
||||||
assert_eq!(again.trk.len(), file.trk.len());
|
|
||||||
for (a, b) in file.rte.iter().zip(&again.rte) {
|
|
||||||
assert_eq!(a.info, b.info);
|
|
||||||
same_points(
|
|
||||||
&a.seg.iter().cloned().collect::<Vec<_>>(),
|
|
||||||
&b.seg.iter().cloned().collect::<Vec<_>>(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
same_points(&points(&file), &points(&again));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_an_empty_route_is_written_and_a_single_route_is_named_like_the_file() {
|
|
||||||
let mut file = File::default();
|
|
||||||
file.info.name = "file".into();
|
|
||||||
file.rte.push(crate::Route::default());
|
|
||||||
let text = written_text(&file, &Default::default(), ExportOptions::ALL);
|
|
||||||
assert_eq!(count(&text, "<rte>"), 1);
|
|
||||||
assert_eq!(count(&text, "<rtept"), 0);
|
|
||||||
let again = parse(text.as_bytes(), &mut Default::default()).unwrap();
|
|
||||||
assert_eq!(again.rte.len(), 1);
|
|
||||||
assert_eq!(again.rte[0].info.name.as_deref(), Some("file"));
|
|
||||||
|
|
||||||
// not when there are others
|
|
||||||
file.trk.push(Track::default());
|
|
||||||
let text = written_text(&file, &Default::default(), ExportOptions::ALL);
|
|
||||||
// the metadata only
|
|
||||||
assert_eq!(count(&text, "<name>file</name>"), 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_route_points_keep_their_data_and_the_options_apply_to_them() {
|
fn test_route_points_keep_their_data_and_the_options_apply_to_them() {
|
||||||
let (file, categories) = read("with_hr");
|
let (file, categories) = read("with_hr");
|
||||||
let file = as_routes(file);
|
let with_all = written_text(&file, &categories, as_route());
|
||||||
let with_all = written_text(&file, &categories, ExportOptions::ALL);
|
|
||||||
assert!(with_all.contains("<gpxtpx:hr>"));
|
assert!(with_all.contains("<gpxtpx:hr>"));
|
||||||
let without = written_text(
|
let without = written_text(
|
||||||
&file,
|
&file,
|
||||||
@@ -794,10 +745,22 @@ mod tests {
|
|||||||
ExportOptions {
|
ExportOptions {
|
||||||
hr: false,
|
hr: false,
|
||||||
time: false,
|
time: false,
|
||||||
|
as_route: true,
|
||||||
..ExportOptions::ALL
|
..ExportOptions::ALL
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert!(!without.contains("<gpxtpx:hr>"));
|
assert!(!without.contains("<gpxtpx:hr>"));
|
||||||
assert!(!without.contains("<time>"));
|
assert!(!without.contains("<time>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_the_route_option_is_not_data() {
|
||||||
|
let (file, _) = read("with_route_points");
|
||||||
|
assert!(!file.exportable_data().as_route);
|
||||||
|
// it is a way to write the data, never part of them
|
||||||
|
let all = std::hint::black_box(ExportOptions::ALL);
|
||||||
|
assert!(!all.as_route && !ExportOptions::NONE.as_route);
|
||||||
|
assert!(as_route().union(ExportOptions::NONE).as_route);
|
||||||
|
assert!(!ExportOptions::ALL.union(ExportOptions::NONE).as_route);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -36,11 +36,4 @@ pub enum Action<'a> {
|
|||||||
file_ids: Vec<FileId>,
|
file_ids: Vec<FileId>,
|
||||||
index: usize,
|
index: usize,
|
||||||
},
|
},
|
||||||
/// Names a file in all the states of the history, as if it had always been called so (see
|
|
||||||
/// [`crate::History::rename`]). Not undoable: for a file that is given a name from the outside,
|
|
||||||
/// like the name of the file it is written to.
|
|
||||||
Rename {
|
|
||||||
file_id: FileId,
|
|
||||||
name: String,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
File renamed without changes.
+7
-24
@@ -1,6 +1,4 @@
|
|||||||
use crate::{
|
use crate::{Apply, CommandError, Edit, Editor, File, State, Track, update_selected};
|
||||||
Apply, CommandError, Edit, Editor, File, Route, State, Track, TrackInfo, update_selected,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Metadata<'a> {
|
pub struct Metadata<'a> {
|
||||||
@@ -15,33 +13,21 @@ impl Apply for Metadata<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Metadata<'_> {
|
|
||||||
fn set(&self, info: &mut TrackInfo) -> Edit {
|
|
||||||
info.name = Some(self.name.to_owned());
|
|
||||||
info.desc = Some(self.desc.to_owned());
|
|
||||||
Edit::Changed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Editor for Metadata<'_> {
|
impl Editor for Metadata<'_> {
|
||||||
fn file(&mut self, file: &mut File) -> Edit {
|
fn file(&mut self, file: &mut File) -> Edit {
|
||||||
file.info.name = self.name.to_owned();
|
file.info.name = self.name.to_owned();
|
||||||
file.info.desc = Some(self.desc.to_owned());
|
file.info.desc = Some(self.desc.to_owned());
|
||||||
// a single route or track is named like the file
|
// a single track is named like the file
|
||||||
match (file.rte.as_mut_slice(), file.trk.as_mut_slice()) {
|
if let [track] = file.trk.as_mut_slice() {
|
||||||
([route], []) => route.info.name = Some(self.name.to_owned()),
|
track.info.name = Some(self.name.to_owned());
|
||||||
([], [track]) => track.info.name = Some(self.name.to_owned()),
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
Edit::Changed
|
Edit::Changed
|
||||||
}
|
}
|
||||||
|
|
||||||
fn route(&mut self, route: &mut Route) -> Edit {
|
|
||||||
self.set(&mut route.info)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn track(&mut self, track: &mut Track) -> Edit {
|
fn track(&mut self, track: &mut Track) -> Edit {
|
||||||
self.set(&mut track.info)
|
track.info.name = Some(self.name.to_owned());
|
||||||
|
track.info.desc = Some(self.desc.to_owned());
|
||||||
|
Edit::Changed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,14 +45,12 @@ mod tests {
|
|||||||
New {
|
New {
|
||||||
name: "first",
|
name: "first",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
New {
|
New {
|
||||||
name: "second",
|
name: "second",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap(); // now selected
|
.unwrap(); // now selected
|
||||||
@@ -124,7 +108,6 @@ mod tests {
|
|||||||
New {
|
New {
|
||||||
name: "file",
|
name: "file",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
mod convert;
|
|
||||||
mod metadata;
|
mod metadata;
|
||||||
mod new_route;
|
|
||||||
mod new_track;
|
mod new_track;
|
||||||
mod new_track_segment;
|
mod new_track_segment;
|
||||||
mod style;
|
mod style;
|
||||||
|
|
||||||
pub use convert::*;
|
|
||||||
pub use metadata::*;
|
pub use metadata::*;
|
||||||
pub use new_route::*;
|
|
||||||
pub use new_track::*;
|
pub use new_track::*;
|
||||||
pub use new_track_segment::*;
|
pub use new_track_segment::*;
|
||||||
pub use style::*;
|
pub use style::*;
|
||||||
+11
-34
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Apply, CommandError, Edit, Editor, File, State, Track, TrackInfo, update_selected};
|
use crate::{Apply, CommandError, Edit, Editor, File, State, Track, update_selected};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct NewTrack;
|
pub struct NewTrack;
|
||||||
@@ -12,41 +12,20 @@ impl Apply for NewTrack {
|
|||||||
|
|
||||||
impl Editor for NewTrack {
|
impl Editor for NewTrack {
|
||||||
fn file(&mut self, file: &mut File) -> Edit {
|
fn file(&mut self, file: &mut File) -> Edit {
|
||||||
file.trk.push(Track {
|
let mut track = Track::default();
|
||||||
info: common_style(file),
|
track.info.color = common(file, |t| &t.info.color);
|
||||||
..Default::default()
|
track.info.opacity = common(file, |t| &t.info.opacity);
|
||||||
});
|
track.info.width = common(file, |t| &t.info.width);
|
||||||
|
file.trk.push(track);
|
||||||
Edit::Changed
|
Edit::Changed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The style that all the routes and tracks of the file agree on: each attribute is set if they
|
/// Returns the value of a style attribute if all existing tracks agree on it.
|
||||||
/// all have the same value for it.
|
fn common<T: Clone + PartialEq>(file: &File, get: impl Fn(&Track) -> &Option<T>) -> Option<T> {
|
||||||
pub(super) fn common_style(file: &File) -> TrackInfo {
|
let mut tracks = file.trk.iter();
|
||||||
let infos: Vec<&TrackInfo> = file
|
let first = get(tracks.next()?);
|
||||||
.rte
|
tracks.all(|t| get(t) == first).then(|| first.clone())?
|
||||||
.iter()
|
|
||||||
.map(|rte| &rte.info)
|
|
||||||
.chain(file.trk.iter().map(|trk| &trk.info))
|
|
||||||
.collect();
|
|
||||||
TrackInfo {
|
|
||||||
color: common(&infos, |info| &info.color),
|
|
||||||
opacity: common(&infos, |info| &info.opacity),
|
|
||||||
width: common(&infos, |info| &info.width),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the value of a style attribute if all the infos agree on it.
|
|
||||||
fn common<T: Clone + PartialEq>(
|
|
||||||
infos: &[&TrackInfo],
|
|
||||||
get: impl Fn(&TrackInfo) -> &Option<T>,
|
|
||||||
) -> Option<T> {
|
|
||||||
let mut infos = infos.iter();
|
|
||||||
let first = get(infos.next()?);
|
|
||||||
infos
|
|
||||||
.all(|info| get(info) == first)
|
|
||||||
.then(|| first.clone())?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -61,7 +40,6 @@ mod tests {
|
|||||||
New {
|
New {
|
||||||
name: "file",
|
name: "file",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -78,7 +56,6 @@ mod tests {
|
|||||||
New {
|
New {
|
||||||
name: "file",
|
name: "file",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
-1
@@ -29,7 +29,6 @@ mod tests {
|
|||||||
New {
|
New {
|
||||||
name: "file",
|
name: "file",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
+6
-16
@@ -1,4 +1,4 @@
|
|||||||
use crate::{Apply, CommandError, Edit, Editor, Route, State, Track, TrackInfo, update_selected};
|
use crate::{Apply, CommandError, Edit, Editor, State, Track, update_selected};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Style<'a> {
|
pub struct Style<'a> {
|
||||||
@@ -14,31 +14,21 @@ impl Apply for Style<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Style<'_> {
|
impl Editor for Style<'_> {
|
||||||
fn set(&self, info: &mut TrackInfo) -> Edit {
|
fn track(&mut self, track: &mut Track) -> Edit {
|
||||||
if let Some(color) = self.color {
|
if let Some(color) = self.color {
|
||||||
info.color = Some(color.to_owned());
|
track.info.color = Some(color.to_owned());
|
||||||
}
|
}
|
||||||
if self.opacity.is_some() {
|
if self.opacity.is_some() {
|
||||||
info.opacity = self.opacity;
|
track.info.opacity = self.opacity;
|
||||||
}
|
}
|
||||||
if self.width.is_some() {
|
if self.width.is_some() {
|
||||||
info.width = self.width;
|
track.info.width = self.width;
|
||||||
}
|
}
|
||||||
Edit::Changed
|
Edit::Changed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Editor for Style<'_> {
|
|
||||||
fn route(&mut self, route: &mut Route) -> Edit {
|
|
||||||
self.set(&mut route.info)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn track(&mut self, track: &mut Track) -> Edit {
|
|
||||||
self.set(&mut track.info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
+1
-18
@@ -3,7 +3,7 @@ use std::rc::Rc;
|
|||||||
use crate::{Apply, CommandError, FileId, Files, Selection, State, Waypoint};
|
use crate::{Apply, CommandError, FileId, Files, Selection, State, Waypoint};
|
||||||
|
|
||||||
/// Deletes the selected elements. With `whole_files`, the files holding the selected elements
|
/// Deletes the selected elements. With `whole_files`, the files holding the selected elements
|
||||||
/// are deleted instead, even if only a route, a track or a waypoint is selected.
|
/// are deleted instead, even if only a track or a waypoint is selected.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Delete {
|
pub struct Delete {
|
||||||
pub whole_files: bool,
|
pub whole_files: bool,
|
||||||
@@ -23,21 +23,6 @@ impl Apply for Delete {
|
|||||||
state.order.0.retain(|id| !file_ids.contains(id));
|
state.order.0.retain(|id| !file_ids.contains(id));
|
||||||
Selection::Empty
|
Selection::Empty
|
||||||
}
|
}
|
||||||
Selection::Route { file_id, rte_ids } => {
|
|
||||||
let file = state
|
|
||||||
.files
|
|
||||||
.get_mut(file_id)
|
|
||||||
.ok_or(CommandError::NothingToDo)?;
|
|
||||||
let file = Rc::make_mut(file);
|
|
||||||
let len = file.rte.len();
|
|
||||||
file.rte.retain(|rte| !rte_ids.contains(&rte.id));
|
|
||||||
if file.rte.len() == len {
|
|
||||||
return Err(CommandError::NothingToDo);
|
|
||||||
}
|
|
||||||
Selection::File {
|
|
||||||
file_ids: [*file_id].into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Selection::Track { file_id, trk_ids } => {
|
Selection::Track { file_id, trk_ids } => {
|
||||||
let file = state
|
let file = state
|
||||||
.files
|
.files
|
||||||
@@ -146,7 +131,6 @@ mod tests {
|
|||||||
crate::New {
|
crate::New {
|
||||||
name: "keep",
|
name: "keep",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -281,7 +265,6 @@ mod tests {
|
|||||||
crate::New {
|
crate::New {
|
||||||
name: "keep",
|
name: "keep",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
-2
@@ -31,14 +31,12 @@ mod tests {
|
|||||||
crate::New {
|
crate::New {
|
||||||
name: "a",
|
name: "a",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
crate::New {
|
crate::New {
|
||||||
name: "b",
|
name: "b",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
File renamed without changes.
+2
-27
@@ -1,8 +1,8 @@
|
|||||||
use std::{collections::HashSet, rc::Rc};
|
use std::{collections::HashSet, rc::Rc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, CommandError, FileId, Files, Selection, State, Waypoint, copy_file, copy_route,
|
Apply, CommandError, FileId, Files, Selection, State, Waypoint, copy_file, copy_segment,
|
||||||
copy_segment, copy_track, copy_waypoint,
|
copy_track, copy_waypoint,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -35,30 +35,6 @@ impl Apply for Duplicate {
|
|||||||
state.order.0.splice(at..at, order);
|
state.order.0.splice(at..at, order);
|
||||||
Selection::File { file_ids: copies }
|
Selection::File { file_ids: copies }
|
||||||
}
|
}
|
||||||
Selection::Route { file_id, rte_ids } => {
|
|
||||||
let file = state
|
|
||||||
.files
|
|
||||||
.get_mut(file_id)
|
|
||||||
.ok_or(CommandError::NothingToDo)?;
|
|
||||||
let file = Rc::make_mut(file);
|
|
||||||
let mut copies = HashSet::new();
|
|
||||||
append_copies(
|
|
||||||
&mut file.rte,
|
|
||||||
|rte| rte_ids.contains(&rte.id),
|
|
||||||
|rte| {
|
|
||||||
let copy = copy_route(rte);
|
|
||||||
copies.insert(copy.id);
|
|
||||||
copy
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if copies.is_empty() {
|
|
||||||
return Err(CommandError::NothingToDo);
|
|
||||||
}
|
|
||||||
Selection::Route {
|
|
||||||
file_id: *file_id,
|
|
||||||
rte_ids: copies,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Selection::Track { file_id, trk_ids } => {
|
Selection::Track { file_id, trk_ids } => {
|
||||||
let file = state
|
let file = state
|
||||||
.files
|
.files
|
||||||
@@ -217,7 +193,6 @@ mod tests {
|
|||||||
crate::New {
|
crate::New {
|
||||||
name,
|
name,
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
+1
-4
@@ -9,7 +9,7 @@ pub struct Load<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Load<'_> {
|
impl Load<'_> {
|
||||||
pub(crate) fn parse(self, categories: &mut TrackpointCategories) -> Result<File, CommandError> {
|
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() {
|
||||||
@@ -85,7 +85,6 @@ mod tests {
|
|||||||
crate::New {
|
crate::New {
|
||||||
name: "keep",
|
name: "keep",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -141,7 +140,6 @@ mod tests {
|
|||||||
crate::New {
|
crate::New {
|
||||||
name: "before",
|
name: "before",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -191,7 +189,6 @@ mod tests {
|
|||||||
crate::New {
|
crate::New {
|
||||||
name: "keep",
|
name: "keep",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -6,7 +6,6 @@ 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::*;
|
||||||
@@ -17,5 +16,4 @@ 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::*;
|
||||||
+3
-9
@@ -9,8 +9,6 @@ use crate::{
|
|||||||
pub enum MoveTarget {
|
pub enum MoveTarget {
|
||||||
/// The list of files.
|
/// The list of files.
|
||||||
Files { index: usize },
|
Files { index: usize },
|
||||||
/// The routes of a file.
|
|
||||||
Routes { file_id: FileId, index: usize },
|
|
||||||
/// The tracks of a file.
|
/// The tracks of a file.
|
||||||
Tracks { file_id: FileId, index: usize },
|
Tracks { file_id: FileId, index: usize },
|
||||||
/// The segments of a track.
|
/// The segments of a track.
|
||||||
@@ -28,10 +26,9 @@ pub enum MoveTarget {
|
|||||||
///
|
///
|
||||||
/// What can go where:
|
/// What can go where:
|
||||||
/// - files: among the files;
|
/// - files: among the files;
|
||||||
/// - routes and tracks: among the files (each one becomes a file), or among the routes or the
|
/// - tracks: among the files (each track becomes a file), or among the tracks of a file;
|
||||||
/// tracks of a file, where they keep their kind (see [`Destination`]);
|
/// - segments: among the files (each segment becomes a file), among the tracks of a file (each
|
||||||
/// - segments: among the files (each segment becomes a file), among the routes or the tracks of a
|
/// segment becomes a track), or among the segments of a track;
|
||||||
/// file (each segment becomes a route or a track), or among the segments of a track;
|
|
||||||
/// - waypoints, or all the waypoints of a file (the waypoints node): among the waypoints of a
|
/// - waypoints, or all the waypoints of a file (the waypoints node): among the waypoints of a
|
||||||
/// file.
|
/// file.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -44,9 +41,6 @@ impl From<MoveTarget> for Destination {
|
|||||||
fn from(target: MoveTarget) -> Self {
|
fn from(target: MoveTarget) -> Self {
|
||||||
match target {
|
match target {
|
||||||
MoveTarget::Files { index } => Destination::Files(Place::Index(index)),
|
MoveTarget::Files { index } => Destination::Files(Place::Index(index)),
|
||||||
MoveTarget::Routes { file_id, index } => {
|
|
||||||
Destination::Routes(file_id, Place::Index(index))
|
|
||||||
}
|
|
||||||
MoveTarget::Tracks { file_id, index } => {
|
MoveTarget::Tracks { file_id, index } => {
|
||||||
Destination::Tracks(file_id, Place::Index(index))
|
Destination::Tracks(file_id, Place::Index(index))
|
||||||
}
|
}
|
||||||
+6
-53
@@ -1,16 +1,12 @@
|
|||||||
use crate::{
|
use crate::{Apply, CommandError, File, LngLat, State, Track, TrackSegment, Trackpoint, produce};
|
||||||
Apply, CommandError, File, LngLat, Route, State, Track, TrackSegment, Trackpoint, produce,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Creates an empty file and selects it. With `route`, the file starts with a route, which is
|
/// Creates an empty file and selects it. With a `trackpoint`, the file starts with a track and a
|
||||||
/// empty. With a `trackpoint`, the file starts with a segment that holds it, the one of its route
|
/// segment that hold it: it is how the routing tool starts a route on an empty map, in a single
|
||||||
/// with `route` or of a track otherwise: it is how the routing tool starts a route on an empty map,
|
/// undo step.
|
||||||
/// in a single undo step.
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct New<'a> {
|
pub struct New<'a> {
|
||||||
pub name: &'a str,
|
pub name: &'a str,
|
||||||
pub trackpoint: Option<NewTrackpoint>,
|
pub trackpoint: Option<NewTrackpoint>,
|
||||||
pub route: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
@@ -25,8 +21,8 @@ impl Apply for New<'_> {
|
|||||||
produce(state, |_| {
|
produce(state, |_| {
|
||||||
let mut file = File::default();
|
let mut file = File::default();
|
||||||
file.info.name = self.name.to_owned();
|
file.info.name = self.name.to_owned();
|
||||||
let mut segment = TrackSegment::default();
|
|
||||||
if let Some(point) = self.trackpoint {
|
if let Some(point) = self.trackpoint {
|
||||||
|
let mut segment = TrackSegment::default();
|
||||||
// the ends of a segment are anchors of the routing tool
|
// the ends of a segment are anchors of the routing tool
|
||||||
segment.splice(
|
segment.splice(
|
||||||
0,
|
0,
|
||||||
@@ -40,13 +36,6 @@ impl Apply for New<'_> {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
}
|
|
||||||
if self.route {
|
|
||||||
file.rte.push(Route {
|
|
||||||
seg: segment,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
} else if self.trackpoint.is_some() {
|
|
||||||
file.trk.push(Track {
|
file.trk.push(Track {
|
||||||
trkseg: vec![segment],
|
trkseg: vec![segment],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -70,8 +59,7 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
New {
|
New {
|
||||||
name: "new",
|
name: "new",
|
||||||
trackpoint: None,
|
trackpoint: None
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.is_ok()
|
.is_ok()
|
||||||
@@ -95,7 +83,6 @@ mod tests {
|
|||||||
New {
|
New {
|
||||||
name: "new",
|
name: "new",
|
||||||
trackpoint: Some(point),
|
trackpoint: Some(point),
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -111,38 +98,4 @@ mod tests {
|
|||||||
assert_eq!(segment[0].anchor, Some(0));
|
assert_eq!(segment[0].anchor, Some(0));
|
||||||
assert_eq!(fx.selected_files(), [file.id].into());
|
assert_eq!(fx.selected_files(), [file.id].into());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_new_with_a_route() {
|
|
||||||
let mut fx = Fixture::default();
|
|
||||||
New {
|
|
||||||
name: "new",
|
|
||||||
trackpoint: None,
|
|
||||||
route: true,
|
|
||||||
}
|
|
||||||
.apply(&mut fx.state())
|
|
||||||
.unwrap();
|
|
||||||
let file = fx.files.values().next().unwrap();
|
|
||||||
assert_eq!((file.rte.len(), file.trk.len()), (1, 0));
|
|
||||||
assert!(file.rte[0].seg.is_empty());
|
|
||||||
assert_eq!(fx.selected_files(), [file.id].into());
|
|
||||||
|
|
||||||
// the trackpoint goes to the route
|
|
||||||
let mut fx = Fixture::default();
|
|
||||||
New {
|
|
||||||
name: "new",
|
|
||||||
trackpoint: Some(NewTrackpoint {
|
|
||||||
lng: 1.0,
|
|
||||||
lat: 2.0,
|
|
||||||
ele: 3.0,
|
|
||||||
}),
|
|
||||||
route: true,
|
|
||||||
}
|
|
||||||
.apply(&mut fx.state())
|
|
||||||
.unwrap();
|
|
||||||
let file = fx.files.values().next().unwrap();
|
|
||||||
assert_eq!((file.rte.len(), file.trk.len()), (1, 0));
|
|
||||||
assert_eq!(file.rte[0].seg.len(), 1);
|
|
||||||
assert_eq!(file.rte[0].seg[0].anchor, Some(0));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+10
-41
@@ -1,21 +1,18 @@
|
|||||||
use std::{collections::HashSet, hash::Hash};
|
use std::{collections::HashSet, hash::Hash};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, ClipboardContent, CommandError, Destination, FileId, Place, RouteId, Selection, State,
|
Apply, ClipboardContent, CommandError, Destination, FileId, Place, Selection, State, TrackId,
|
||||||
TrackId, TrackSegmentId, WaypointId, transfer, waypoint_ids,
|
TrackSegmentId, WaypointId, transfer, waypoint_ids,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Pastes what was copied or cut (see [`crate::Clipboard`]).
|
/// Pastes what was copied or cut (see [`crate::Clipboard`]).
|
||||||
///
|
///
|
||||||
/// Where it goes depends on the selection, see [`crate::Clipboard::can_paste`]:
|
/// Where it goes depends on the selection, see [`crate::Clipboard::can_paste`]:
|
||||||
/// - nothing selected: the files are copied, and routes, tracks and segments become new files
|
/// - nothing selected: the files are copied, and tracks and segments become new files (named
|
||||||
/// (named like their route or track, or like the file they come from);
|
/// like their track, or like the file they come from);
|
||||||
/// - a file: the routes and tracks are added to it, segments become new tracks in it, and the
|
/// - a file: the tracks are added to it, segments become new tracks in it, and the waypoints are
|
||||||
/// waypoints are added to its own;
|
/// added to its own;
|
||||||
/// - a route: routes, and segments as new routes, go after it in its file, tracks go before the
|
/// - a track: the segments are added to it, tracks go after it in its file;
|
||||||
/// other tracks (right after the routes);
|
|
||||||
/// - a track: the segments are added to it, tracks go after it in its file, and routes after the
|
|
||||||
/// other routes;
|
|
||||||
/// - a segment: the segments go after it in its track;
|
/// - a segment: the segments go after it in its track;
|
||||||
/// - the waypoints or a waypoint: the waypoints are added to their file, after the waypoint.
|
/// - the waypoints or a waypoint: the waypoints are added to their file, after the waypoint.
|
||||||
///
|
///
|
||||||
@@ -49,14 +46,6 @@ impl Apply for Paste {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn route_ids_of(state: &State, file_id: FileId) -> Vec<RouteId> {
|
|
||||||
state
|
|
||||||
.files
|
|
||||||
.get(&file_id)
|
|
||||||
.map(|file| file.rte.iter().map(|rte| rte.id).collect())
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn track_ids_of(state: &State, file_id: FileId) -> Vec<TrackId> {
|
fn track_ids_of(state: &State, file_id: FileId) -> Vec<TrackId> {
|
||||||
state
|
state
|
||||||
.files
|
.files
|
||||||
@@ -90,34 +79,14 @@ fn destination(state: &State, content: &ClipboardContent) -> Result<Destination,
|
|||||||
};
|
};
|
||||||
Ok(match (content, &*state.selection) {
|
Ok(match (content, &*state.selection) {
|
||||||
(ClipboardContent::Files(_), _) => Destination::Files(Place::End),
|
(ClipboardContent::Files(_), _) => Destination::Files(Place::End),
|
||||||
(
|
(ClipboardContent::Tracks(_) | ClipboardContent::Segments(_), Selection::Empty) => {
|
||||||
ClipboardContent::Routes(_)
|
Destination::Files(Place::End)
|
||||||
| ClipboardContent::Tracks(_)
|
|
||||||
| ClipboardContent::Segments(_),
|
|
||||||
Selection::Empty,
|
|
||||||
) => Destination::Files(Place::End),
|
|
||||||
(ClipboardContent::Routes(_), Selection::File { file_ids }) => {
|
|
||||||
Destination::Routes(last_file(file_ids)?, Place::End)
|
|
||||||
}
|
|
||||||
// routes and tracks keep their kind (see `Destination`)
|
|
||||||
(
|
|
||||||
ClipboardContent::Routes(_)
|
|
||||||
| ClipboardContent::Tracks(_)
|
|
||||||
| ClipboardContent::Segments(_),
|
|
||||||
Selection::Route { file_id, rte_ids },
|
|
||||||
) => {
|
|
||||||
let last =
|
|
||||||
last_selected(route_ids_of(state, *file_id).into_iter(), rte_ids).ok_or(nothing)?;
|
|
||||||
Destination::Routes(*file_id, Place::After(last))
|
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
ClipboardContent::Tracks(_) | ClipboardContent::Segments(_),
|
ClipboardContent::Tracks(_) | ClipboardContent::Segments(_),
|
||||||
Selection::File { file_ids },
|
Selection::File { file_ids },
|
||||||
) => Destination::Tracks(last_file(file_ids)?, Place::End),
|
) => Destination::Tracks(last_file(file_ids)?, Place::End),
|
||||||
(
|
(ClipboardContent::Tracks(_), Selection::Track { file_id, trk_ids }) => {
|
||||||
ClipboardContent::Routes(_) | ClipboardContent::Tracks(_),
|
|
||||||
Selection::Track { file_id, trk_ids },
|
|
||||||
) => {
|
|
||||||
let last =
|
let last =
|
||||||
last_selected(track_ids_of(state, *file_id).into_iter(), trk_ids).ok_or(nothing)?;
|
last_selected(track_ids_of(state, *file_id).into_iter(), trk_ids).ok_or(nothing)?;
|
||||||
Destination::Tracks(*file_id, Place::After(last))
|
Destination::Tracks(*file_id, Place::After(last))
|
||||||
+30
-131
@@ -5,9 +5,9 @@
|
|||||||
use std::{collections::HashSet, rc::Rc};
|
use std::{collections::HashSet, rc::Rc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ClipboardContent, ClipboardRoute, ClipboardSegment, ClipboardTrack, CommandError, File, FileId,
|
ClipboardContent, ClipboardSegment, ClipboardTrack, CommandError, File, FileId, Selection,
|
||||||
Route, RouteId, Selection, State, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint,
|
State, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint, WaypointId, copy_file,
|
||||||
WaypointId, copy_file, copy_route, copy_segment, copy_track, copy_waypoint,
|
copy_segment, copy_track, copy_waypoint,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Where elements are put, among the others of the same list.
|
/// Where elements are put, among the others of the same list.
|
||||||
@@ -37,20 +37,15 @@ impl<T: Copy + PartialEq> Place<T> {
|
|||||||
|
|
||||||
/// Where elements go. What can go where:
|
/// Where elements go. What can go where:
|
||||||
/// - files: among the files;
|
/// - files: among the files;
|
||||||
/// - routes: among the files (each route becomes a file, named like the route, or like the file
|
|
||||||
/// it comes from), or among the routes of a file. Among the tracks of a file, they stay routes,
|
|
||||||
/// which come before the tracks: they go after the other routes;
|
|
||||||
/// - tracks: among the files (each track becomes a file, named like the track, or like the file
|
/// - tracks: among the files (each track becomes a file, named like the track, or like the file
|
||||||
/// it comes from), or among the tracks of a file. Among the routes of a file, they stay tracks,
|
/// it comes from), or among the tracks of a file;
|
||||||
/// which come after the routes: they go before the other tracks;
|
|
||||||
/// - segments: among the files (each segment becomes a file with a track, named like its track,
|
/// - segments: among the files (each segment becomes a file with a track, named like its track,
|
||||||
/// or like its file), among the routes or the tracks of a file (each segment becomes a route or
|
/// or like its file), among the tracks of a file (each segment becomes a track), or among the
|
||||||
/// a track), or among the segments of a track;
|
/// segments of a track;
|
||||||
/// - waypoints: among the waypoints of a file.
|
/// - waypoints: among the waypoints of a file.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum Destination {
|
pub enum Destination {
|
||||||
Files(Place<FileId>),
|
Files(Place<FileId>),
|
||||||
Routes(FileId, Place<RouteId>),
|
|
||||||
Tracks(FileId, Place<TrackId>),
|
Tracks(FileId, Place<TrackId>),
|
||||||
Segments(FileId, TrackId, Place<TrackSegmentId>),
|
Segments(FileId, TrackId, Place<TrackSegmentId>),
|
||||||
Waypoints(FileId, Place<WaypointId>),
|
Waypoints(FileId, Place<WaypointId>),
|
||||||
@@ -98,14 +93,6 @@ pub fn remove_elements(state: &mut State, content: &ClipboardContent) {
|
|||||||
state.files.retain(|id, _| !ids.contains(id));
|
state.files.retain(|id, _| !ids.contains(id));
|
||||||
state.order.0.retain(|id| !ids.contains(id));
|
state.order.0.retain(|id| !ids.contains(id));
|
||||||
}
|
}
|
||||||
ClipboardContent::Routes(routes) => {
|
|
||||||
let ids: HashSet<RouteId> = routes.iter().map(|rte| rte.route.id).collect();
|
|
||||||
for file in state.files.values_mut() {
|
|
||||||
if file.rte.iter().any(|rte| ids.contains(&rte.id)) {
|
|
||||||
Rc::make_mut(file).rte.retain(|rte| !ids.contains(&rte.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ClipboardContent::Tracks(tracks) => {
|
ClipboardContent::Tracks(tracks) => {
|
||||||
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.track.id).collect();
|
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.track.id).collect();
|
||||||
for file in state.files.values_mut() {
|
for file in state.files.values_mut() {
|
||||||
@@ -154,9 +141,7 @@ pub fn transfer(
|
|||||||
// the destination has to be there, before anything is changed
|
// the destination has to be there, before anything is changed
|
||||||
let exists = match to {
|
let exists = match to {
|
||||||
Destination::Files(_) => true,
|
Destination::Files(_) => true,
|
||||||
Destination::Routes(file_id, _) | Destination::Tracks(file_id, _) => {
|
Destination::Tracks(file_id, _) => state.files.contains_key(&file_id),
|
||||||
state.files.contains_key(&file_id)
|
|
||||||
}
|
|
||||||
Destination::Segments(file_id, trk_id, _) => state
|
Destination::Segments(file_id, trk_id, _) => state
|
||||||
.files
|
.files
|
||||||
.get(&file_id)
|
.get(&file_id)
|
||||||
@@ -167,21 +152,17 @@ pub fn transfer(
|
|||||||
(content, to),
|
(content, to),
|
||||||
(ClipboardContent::Files(_), Destination::Files(_))
|
(ClipboardContent::Files(_), Destination::Files(_))
|
||||||
| (
|
| (
|
||||||
ClipboardContent::Routes(_) | ClipboardContent::Tracks(_),
|
ClipboardContent::Tracks(_),
|
||||||
Destination::Files(_) | Destination::Routes(..) | Destination::Tracks(..)
|
Destination::Files(_) | Destination::Tracks(..)
|
||||||
)
|
)
|
||||||
| (
|
| (
|
||||||
ClipboardContent::Segments(_),
|
ClipboardContent::Segments(_),
|
||||||
Destination::Files(_)
|
Destination::Files(_) | Destination::Tracks(..) | Destination::Segments(..),
|
||||||
| Destination::Routes(..)
|
|
||||||
| Destination::Tracks(..)
|
|
||||||
| Destination::Segments(..),
|
|
||||||
)
|
)
|
||||||
| (ClipboardContent::Waypoints(_), Destination::Waypoints(..))
|
| (ClipboardContent::Waypoints(_), Destination::Waypoints(..))
|
||||||
);
|
);
|
||||||
let empty = match content {
|
let empty = match content {
|
||||||
ClipboardContent::Files(files) => files.is_empty(),
|
ClipboardContent::Files(files) => files.is_empty(),
|
||||||
ClipboardContent::Routes(routes) => routes.is_empty(),
|
|
||||||
ClipboardContent::Tracks(tracks) => tracks.is_empty(),
|
ClipboardContent::Tracks(tracks) => tracks.is_empty(),
|
||||||
ClipboardContent::Segments(segments) => segments.is_empty(),
|
ClipboardContent::Segments(segments) => segments.is_empty(),
|
||||||
ClipboardContent::Waypoints(waypoints) => waypoints.is_empty(),
|
ClipboardContent::Waypoints(waypoints) => waypoints.is_empty(),
|
||||||
@@ -208,7 +189,6 @@ pub fn transfer(
|
|||||||
.collect();
|
.collect();
|
||||||
Ok(add_files(state, files, place))
|
Ok(add_files(state, files, place))
|
||||||
}
|
}
|
||||||
(ClipboardContent::Routes(routes), to) => transfer_routes(state, routes, copy, to),
|
|
||||||
(ClipboardContent::Tracks(tracks), to) => transfer_tracks(state, tracks, copy, to),
|
(ClipboardContent::Tracks(tracks), to) => transfer_tracks(state, tracks, copy, to),
|
||||||
(ClipboardContent::Segments(segments), to) => transfer_segments(state, segments, copy, to),
|
(ClipboardContent::Segments(segments), to) => transfer_segments(state, segments, copy, to),
|
||||||
(ClipboardContent::Waypoints(waypoints), Destination::Waypoints(file_id, place)) => {
|
(ClipboardContent::Waypoints(waypoints), Destination::Waypoints(file_id, place)) => {
|
||||||
@@ -218,89 +198,6 @@ pub fn transfer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Puts the routes among the routes of a file, at `place`, and gives the selection of them.
|
|
||||||
fn insert_routes(
|
|
||||||
state: &mut State,
|
|
||||||
file_id: FileId,
|
|
||||||
place: Place<RouteId>,
|
|
||||||
routes: Vec<Route>,
|
|
||||||
) -> Result<Selection, CommandError> {
|
|
||||||
let ids: HashSet<RouteId> = routes.iter().map(|rte| rte.id).collect();
|
|
||||||
let file = file_mut(state, file_id)?;
|
|
||||||
let existing: Vec<RouteId> = file.rte.iter().map(|rte| rte.id).collect();
|
|
||||||
let at = place.position(&existing);
|
|
||||||
file.rte.splice(at..at, routes);
|
|
||||||
Ok(Selection::Route {
|
|
||||||
file_id,
|
|
||||||
rte_ids: ids,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transfer_routes(
|
|
||||||
state: &mut State,
|
|
||||||
routes: &[ClipboardRoute],
|
|
||||||
copy: bool,
|
|
||||||
to: Destination,
|
|
||||||
) -> Result<Selection, CommandError> {
|
|
||||||
let routes: Vec<(Route, &str)> = routes
|
|
||||||
.iter()
|
|
||||||
.map(|clip| {
|
|
||||||
let route = if copy {
|
|
||||||
copy_route(&clip.route)
|
|
||||||
} else {
|
|
||||||
clip.route.clone()
|
|
||||||
};
|
|
||||||
(route, clip.file_name.as_str())
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
match to {
|
|
||||||
Destination::Files(place) => {
|
|
||||||
let files = routes
|
|
||||||
.into_iter()
|
|
||||||
.map(|(route, file_name)| {
|
|
||||||
let name = route
|
|
||||||
.info
|
|
||||||
.name
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| file_name.to_owned());
|
|
||||||
Rc::new(File {
|
|
||||||
rte: vec![route],
|
|
||||||
..new_file(&name, vec![])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
Ok(add_files(state, files, place))
|
|
||||||
}
|
|
||||||
Destination::Routes(file_id, place) => {
|
|
||||||
let routes = routes.into_iter().map(|(route, _)| route).collect();
|
|
||||||
insert_routes(state, file_id, place, routes)
|
|
||||||
}
|
|
||||||
Destination::Tracks(file_id, _) => {
|
|
||||||
let routes = routes.into_iter().map(|(route, _)| route).collect();
|
|
||||||
insert_routes(state, file_id, Place::End, routes)
|
|
||||||
}
|
|
||||||
_ => Err(CommandError::NothingToDo),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Puts the tracks among the tracks of a file, at `place`, and gives the selection of them.
|
|
||||||
fn insert_tracks(
|
|
||||||
state: &mut State,
|
|
||||||
file_id: FileId,
|
|
||||||
place: Place<TrackId>,
|
|
||||||
tracks: Vec<Track>,
|
|
||||||
) -> Result<Selection, CommandError> {
|
|
||||||
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.id).collect();
|
|
||||||
let file = file_mut(state, file_id)?;
|
|
||||||
let existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
|
|
||||||
let at = place.position(&existing);
|
|
||||||
file.trk.splice(at..at, tracks);
|
|
||||||
Ok(Selection::Track {
|
|
||||||
file_id,
|
|
||||||
trk_ids: ids,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transfer_tracks(
|
fn transfer_tracks(
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
tracks: &[ClipboardTrack],
|
tracks: &[ClipboardTrack],
|
||||||
@@ -334,12 +231,16 @@ fn transfer_tracks(
|
|||||||
Ok(add_files(state, files, place))
|
Ok(add_files(state, files, place))
|
||||||
}
|
}
|
||||||
Destination::Tracks(file_id, place) => {
|
Destination::Tracks(file_id, place) => {
|
||||||
let tracks = tracks.into_iter().map(|(track, _)| track).collect();
|
let tracks: Vec<Track> = tracks.into_iter().map(|(track, _)| track).collect();
|
||||||
insert_tracks(state, file_id, place, tracks)
|
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.id).collect();
|
||||||
}
|
let file = file_mut(state, file_id)?;
|
||||||
Destination::Routes(file_id, _) => {
|
let existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
|
||||||
let tracks = tracks.into_iter().map(|(track, _)| track).collect();
|
let at = place.position(&existing);
|
||||||
insert_tracks(state, file_id, Place::Index(0), tracks)
|
file.trk.splice(at..at, tracks);
|
||||||
|
Ok(Selection::Track {
|
||||||
|
file_id,
|
||||||
|
trk_ids: ids,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
_ => Err(CommandError::NothingToDo),
|
_ => Err(CommandError::NothingToDo),
|
||||||
}
|
}
|
||||||
@@ -375,22 +276,20 @@ fn transfer_segments(
|
|||||||
.collect();
|
.collect();
|
||||||
Ok(add_files(state, files, place))
|
Ok(add_files(state, files, place))
|
||||||
}
|
}
|
||||||
Destination::Routes(file_id, place) => {
|
|
||||||
let routes = segments
|
|
||||||
.into_iter()
|
|
||||||
.map(|(seg, _)| Route {
|
|
||||||
seg,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
insert_routes(state, file_id, place, routes)
|
|
||||||
}
|
|
||||||
Destination::Tracks(file_id, place) => {
|
Destination::Tracks(file_id, place) => {
|
||||||
let tracks = segments
|
let tracks: Vec<Track> = segments
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(segment, _)| own_track(segment))
|
.map(|(segment, _)| own_track(segment))
|
||||||
.collect();
|
.collect();
|
||||||
insert_tracks(state, file_id, place, tracks)
|
let ids = tracks.iter().map(|trk| trk.id).collect();
|
||||||
|
let file = file_mut(state, file_id)?;
|
||||||
|
let existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
|
||||||
|
let at = place.position(&existing);
|
||||||
|
file.trk.splice(at..at, tracks);
|
||||||
|
Ok(Selection::Track {
|
||||||
|
file_id,
|
||||||
|
trk_ids: ids,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Destination::Segments(file_id, trk_id, place) => {
|
Destination::Segments(file_id, trk_id, place) => {
|
||||||
let segments: Vec<TrackSegment> =
|
let segments: Vec<TrackSegment> =
|
||||||
File renamed without changes.
@@ -4,8 +4,6 @@ mod file;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod fixture;
|
pub(crate) mod fixture;
|
||||||
mod pattern;
|
mod pattern;
|
||||||
#[cfg(test)]
|
|
||||||
mod routes_tests;
|
|
||||||
mod tools;
|
mod tools;
|
||||||
|
|
||||||
pub use apply::*;
|
pub use apply::*;
|
||||||
@@ -26,22 +24,19 @@ 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),
|
||||||
Move(Move),
|
Move(Move),
|
||||||
DeleteAll(DeleteAll),
|
DeleteAll(DeleteAll),
|
||||||
DeleteWaypoint(DeleteWaypoint),
|
DeleteWaypoint(DeleteWaypoint),
|
||||||
Convert(Convert),
|
|
||||||
Metadata(Metadata<'a>),
|
Metadata(Metadata<'a>),
|
||||||
Style(Style<'a>),
|
Style(Style<'a>),
|
||||||
NewRoute(NewRoute),
|
|
||||||
NewTrack(NewTrack),
|
NewTrack(NewTrack),
|
||||||
NewTrackSegment(NewTrackSegment),
|
NewTrackSegment(NewTrackSegment),
|
||||||
Reverse(Reverse),
|
Reverse(Reverse),
|
||||||
RoundTrip(RoundTrip),
|
RoundTrip(RoundTrip),
|
||||||
Reroute(Reroute<'a>),
|
Route(Route<'a>),
|
||||||
NewWaypoint(NewWaypoint<'a>),
|
NewWaypoint(NewWaypoint<'a>),
|
||||||
EditWaypoint(EditWaypoint<'a>),
|
EditWaypoint(EditWaypoint<'a>),
|
||||||
MoveWaypoint(MoveWaypoint),
|
MoveWaypoint(MoveWaypoint),
|
||||||
@@ -63,22 +58,19 @@ 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),
|
||||||
Command::Move(c) => c.apply(state),
|
Command::Move(c) => c.apply(state),
|
||||||
Command::DeleteAll(c) => c.apply(state),
|
Command::DeleteAll(c) => c.apply(state),
|
||||||
Command::DeleteWaypoint(c) => c.apply(state),
|
Command::DeleteWaypoint(c) => c.apply(state),
|
||||||
Command::Convert(c) => c.apply(state),
|
|
||||||
Command::Metadata(c) => c.apply(state),
|
Command::Metadata(c) => c.apply(state),
|
||||||
Command::Style(c) => c.apply(state),
|
Command::Style(c) => c.apply(state),
|
||||||
Command::NewRoute(c) => c.apply(state),
|
|
||||||
Command::NewTrack(c) => c.apply(state),
|
Command::NewTrack(c) => c.apply(state),
|
||||||
Command::NewTrackSegment(c) => c.apply(state),
|
Command::NewTrackSegment(c) => c.apply(state),
|
||||||
Command::Reverse(c) => c.apply(state),
|
Command::Reverse(c) => c.apply(state),
|
||||||
Command::RoundTrip(c) => c.apply(state),
|
Command::RoundTrip(c) => c.apply(state),
|
||||||
Command::Reroute(c) => c.apply(state),
|
Command::Route(c) => c.apply(state),
|
||||||
Command::NewWaypoint(c) => c.apply(state),
|
Command::NewWaypoint(c) => c.apply(state),
|
||||||
Command::EditWaypoint(c) => c.apply(state),
|
Command::EditWaypoint(c) => c.apply(state),
|
||||||
Command::MoveWaypoint(c) => c.apply(state),
|
Command::MoveWaypoint(c) => c.apply(state),
|
||||||
+2
-12
@@ -1,4 +1,4 @@
|
|||||||
use crate::{File, Route, Track, TrackSegment, Waypoint};
|
use crate::{File, Track, TrackSegment, Waypoint};
|
||||||
|
|
||||||
// The trackpoints of a segment are shared chunks, so copying elements is cheap.
|
// The trackpoints of a segment are shared chunks, so copying elements is cheap.
|
||||||
|
|
||||||
@@ -18,20 +18,10 @@ pub fn copy_track(track: &Track) -> Track {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A copy of the route and of its segment, with new ids.
|
/// A copy of the file and of its tracks and segments, with new ids.
|
||||||
pub fn copy_route(route: &Route) -> Route {
|
|
||||||
Route {
|
|
||||||
id: Default::default(),
|
|
||||||
seg: copy_segment(&route.seg),
|
|
||||||
..route.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A copy of the file and of its routes, tracks and segments, with new ids.
|
|
||||||
pub fn copy_file(file: &File) -> File {
|
pub fn copy_file(file: &File) -> File {
|
||||||
File {
|
File {
|
||||||
id: Default::default(),
|
id: Default::default(),
|
||||||
rte: file.rte.iter().map(copy_route).collect(),
|
|
||||||
trk: file.trk.iter().map(copy_track).collect(),
|
trk: file.trk.iter().map(copy_track).collect(),
|
||||||
..file.clone()
|
..file.clone()
|
||||||
}
|
}
|
||||||
File renamed without changes.
File renamed without changes.
+5
-5
@@ -18,7 +18,7 @@ pub fn update_segments(
|
|||||||
let file = files
|
let file = files
|
||||||
.entry(location.file_id)
|
.entry(location.file_id)
|
||||||
.or_insert_with(|| (*state.files[&location.file_id]).clone());
|
.or_insert_with(|| (*state.files[&location.file_id]).clone());
|
||||||
f(i, file.segment_mut(location.index));
|
f(i, &mut file.trk[location.trk].trkseg[location.seg]);
|
||||||
}
|
}
|
||||||
for (file_id, file) in files {
|
for (file_id, file) in files {
|
||||||
state.files.insert(file_id, Rc::new(file));
|
state.files.insert(file_id, Rc::new(file));
|
||||||
@@ -31,8 +31,6 @@ mod tests {
|
|||||||
|
|
||||||
use crate::engine::command::fixture::Fixture;
|
use crate::engine::command::fixture::Fixture;
|
||||||
|
|
||||||
use crate::SegmentIndex;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn fixture() -> (Fixture, FileId) {
|
fn fixture() -> (Fixture, FileId) {
|
||||||
@@ -51,11 +49,13 @@ mod tests {
|
|||||||
let locations = [
|
let locations = [
|
||||||
SegmentLocation {
|
SegmentLocation {
|
||||||
file_id: id,
|
file_id: id,
|
||||||
index: SegmentIndex::Track { trk: 1, seg: 0 },
|
trk: 1,
|
||||||
|
seg: 0,
|
||||||
},
|
},
|
||||||
SegmentLocation {
|
SegmentLocation {
|
||||||
file_id: id,
|
file_id: id,
|
||||||
index: SegmentIndex::Track { trk: 0, seg: 0 },
|
trk: 0,
|
||||||
|
seg: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let mut calls = vec![];
|
let mut calls = vec![];
|
||||||
+7
-22
@@ -1,6 +1,6 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{File, FileId, Files, Route, Selection, State, Track, TrackSegment, Waypoint};
|
use crate::{File, FileId, Files, Selection, State, Track, TrackSegment, Waypoint};
|
||||||
|
|
||||||
/// What an [`Editor`] hook did to the element it was given.
|
/// What an [`Editor`] hook did to the element it was given.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -21,19 +21,13 @@ impl Edit {
|
|||||||
|
|
||||||
/// Independent edit of each selected element, without needing the rest of the state.
|
/// Independent edit of each selected element, without needing the rest of the state.
|
||||||
///
|
///
|
||||||
/// A selected file, route, track or segment is passed to the hook of its own level. By default
|
/// A selected file, track or segment is passed to the hook of its own level. By default a
|
||||||
/// a file is forwarded to its routes and tracks, and a route or a track to its segments, so an
|
/// file is forwarded to its tracks and a track to its segments, so an editor only overrides
|
||||||
/// editor only overrides the level(s) it cares about (metadata: file, route and track, style:
|
/// the level(s) it cares about (metadata: file and track, style: track, reverse: segment).
|
||||||
/// route and track, reverse: segment). Waypoints are only edited when selected, never through
|
/// Waypoints are only edited when selected, never through their file.
|
||||||
/// their file.
|
|
||||||
pub trait Editor {
|
pub trait Editor {
|
||||||
fn file(&mut self, file: &mut File) -> Edit {
|
fn file(&mut self, file: &mut File) -> Edit {
|
||||||
let routes = edit_each(&mut file.rte, |rte| self.route(rte));
|
edit_each(&mut file.trk, |trk| self.track(trk))
|
||||||
routes.merge(edit_each(&mut file.trk, |trk| self.track(trk)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn route(&mut self, route: &mut Route) -> Edit {
|
|
||||||
edit_segment(self, &mut route.seg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn track(&mut self, track: &mut Track) -> Edit {
|
fn track(&mut self, track: &mut Track) -> Edit {
|
||||||
@@ -101,7 +95,7 @@ fn edit_waypoints<E: Editor + ?Sized>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Applies the editor to every selected file, route, track or segment.
|
/// Applies the editor to every selected file, track or segment.
|
||||||
pub fn update_selected<E: Editor>(state: &mut State, editor: &mut E) {
|
pub fn update_selected<E: Editor>(state: &mut State, editor: &mut E) {
|
||||||
match &*state.selection {
|
match &*state.selection {
|
||||||
Selection::File { file_ids } => {
|
Selection::File { file_ids } => {
|
||||||
@@ -109,15 +103,6 @@ pub fn update_selected<E: Editor>(state: &mut State, editor: &mut E) {
|
|||||||
update_file(state.files, *id, |file| editor.file(file));
|
update_file(state.files, *id, |file| editor.file(file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Selection::Route { file_id, rte_ids } => {
|
|
||||||
update_file(state.files, *file_id, |file| {
|
|
||||||
edit_where(
|
|
||||||
&mut file.rte,
|
|
||||||
|rte| rte_ids.contains(&rte.id),
|
|
||||||
|rte| editor.route(rte),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Selection::Track { file_id, trk_ids } => {
|
Selection::Track { file_id, trk_ids } => {
|
||||||
update_file(state.files, *file_id, |file| {
|
update_file(state.files, *file_id, |file| {
|
||||||
edit_where(
|
edit_where(
|
||||||
File renamed without changes.
+1
-1
@@ -24,7 +24,7 @@ impl Apply for ChangeLoopStart {
|
|||||||
let index = self.index as usize;
|
let index = self.index as usize;
|
||||||
let target = find_target(state, index, index)?.ok_or(CommandError::NothingToDo)?;
|
let target = find_target(state, index, index)?.ok_or(CommandError::NothingToDo)?;
|
||||||
let mut file = (*state.files[&target.location.file_id]).clone();
|
let mut file = (*state.files[&target.location.file_id]).clone();
|
||||||
let segment = file.segment_mut(target.location.index);
|
let segment = &mut file.trk[target.location.trk].trkseg[target.location.seg];
|
||||||
let (start, len) = (target.start, segment.len());
|
let (start, len) = (target.start, segment.len());
|
||||||
if start == 0 || start >= len {
|
if start == 0 || start >= len {
|
||||||
// it already is the start, or it is not a trackpoint
|
// it already is the start, or it is not a trackpoint
|
||||||
+22
-46
@@ -5,16 +5,15 @@ use crate::{
|
|||||||
TrackSegment, TrackSegmentId, Trackpoint, Waypoint, delete_waypoints, update_selected,
|
TrackSegment, TrackSegmentId, Trackpoint, Waypoint, delete_waypoints, update_selected,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub enum CleanType {
|
pub enum CleanType {
|
||||||
Inside,
|
Inside,
|
||||||
Outside,
|
Outside,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes the trackpoints and/or waypoints of the selection lying inside (or outside) a
|
/// Removes the trackpoints and/or waypoints of the selection lying inside (or outside) a
|
||||||
/// rectangle. A selected file cleans its routes, its tracks and its waypoints, a selected route,
|
/// rectangle. A selected file cleans its tracks and its waypoints, a selected track or segment
|
||||||
/// track or segment only trackpoints, selected waypoints only waypoints.
|
/// only trackpoints, selected waypoints only waypoints.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Clean {
|
pub struct Clean {
|
||||||
pub bounds: LngLatBounds,
|
pub bounds: LngLatBounds,
|
||||||
@@ -85,11 +84,6 @@ struct Cleaner<'a> {
|
|||||||
impl Editor for Cleaner<'_> {
|
impl Editor for Cleaner<'_> {
|
||||||
fn file(&mut self, file: &mut File) -> Edit {
|
fn file(&mut self, file: &mut File) -> Edit {
|
||||||
let mut edit = Edit::Unchanged;
|
let mut edit = Edit::Unchanged;
|
||||||
for rte in &mut file.rte {
|
|
||||||
if self.route(rte) == Edit::Changed {
|
|
||||||
edit = Edit::Changed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for trk in &mut file.trk {
|
for trk in &mut file.trk {
|
||||||
if self.track(trk) == Edit::Changed {
|
if self.track(trk) == Edit::Changed {
|
||||||
edit = Edit::Changed;
|
edit = Edit::Changed;
|
||||||
@@ -129,27 +123,36 @@ impl Editor for Cleaner<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes what the clean left empty: the emptied segments, the routes of these segments and the
|
/// Removes what the clean left empty: the emptied segments, the tracks that lost all their
|
||||||
/// tracks that lost all their segments. A file is never removed, even if nothing is left in it: it stays, empty. The
|
/// segments and the files that lost something and have nothing left. The selection falls back
|
||||||
/// selection falls back to the closest element still existing.
|
/// to the closest element still existing.
|
||||||
fn prune(state: &mut State, emptied: &HashSet<TrackSegmentId>, wpt_files: &HashSet<FileId>) {
|
fn prune(state: &mut State, emptied: &HashSet<TrackSegmentId>, wpt_files: &HashSet<FileId>) {
|
||||||
let ids: Vec<FileId> = state
|
let ids: Vec<FileId> = state
|
||||||
.files
|
.files
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(id, file)| {
|
.filter(|(id, file)| {
|
||||||
wpt_files.contains(id) || file.segments().any(|seg| emptied.contains(&seg.id))
|
wpt_files.contains(id)
|
||||||
|
|| file
|
||||||
|
.trk
|
||||||
|
.iter()
|
||||||
|
.any(|trk| trk.trkseg.iter().any(|seg| emptied.contains(&seg.id)))
|
||||||
})
|
})
|
||||||
.map(|(id, _)| *id)
|
.map(|(id, _)| *id)
|
||||||
.collect();
|
.collect();
|
||||||
for id in ids {
|
for id in ids {
|
||||||
let mut file = (*state.files[&id]).clone();
|
let mut file = (*state.files[&id]).clone();
|
||||||
file.rte.retain(|rte| !emptied.contains(&rte.seg.id));
|
let lost_something = !emptied.is_empty() || wpt_files.contains(&id);
|
||||||
file.trk.retain_mut(|trk| {
|
file.trk.retain_mut(|trk| {
|
||||||
let len = trk.trkseg.len();
|
let len = trk.trkseg.len();
|
||||||
trk.trkseg.retain(|seg| !emptied.contains(&seg.id));
|
trk.trkseg.retain(|seg| !emptied.contains(&seg.id));
|
||||||
trk.trkseg.len() == len || !trk.trkseg.is_empty()
|
trk.trkseg.len() == len || !trk.trkseg.is_empty()
|
||||||
});
|
});
|
||||||
state.files.insert(id, Rc::new(file));
|
if lost_something && file.trk.is_empty() && file.wpt.is_empty() {
|
||||||
|
state.files.remove(&id);
|
||||||
|
state.order.0.retain(|f| *f != id);
|
||||||
|
} else {
|
||||||
|
state.files.insert(id, Rc::new(file));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
*state.selection = fixed_selection(state);
|
*state.selection = fixed_selection(state);
|
||||||
}
|
}
|
||||||
@@ -169,27 +172,6 @@ fn fixed_selection(state: &State) -> Selection {
|
|||||||
Selection::File { file_ids }
|
Selection::File { file_ids }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Selection::Route { file_id, rte_ids } => {
|
|
||||||
let Some(file) = files.get(file_id) else {
|
|
||||||
return Selection::Empty;
|
|
||||||
};
|
|
||||||
let rte_ids: HashSet<_> = file
|
|
||||||
.rte
|
|
||||||
.iter()
|
|
||||||
.map(|rte| rte.id)
|
|
||||||
.filter(|id| rte_ids.contains(id))
|
|
||||||
.collect();
|
|
||||||
if rte_ids.is_empty() {
|
|
||||||
Selection::File {
|
|
||||||
file_ids: [*file_id].into(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Selection::Route {
|
|
||||||
file_id: *file_id,
|
|
||||||
rte_ids,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Selection::Track { file_id, trk_ids } => {
|
Selection::Track { file_id, trk_ids } => {
|
||||||
let Some(file) = files.get(file_id) else {
|
let Some(file) = files.get(file_id) else {
|
||||||
return Selection::Empty;
|
return Selection::Empty;
|
||||||
@@ -314,16 +296,10 @@ mod tests {
|
|||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(count(&fx, id), 0);
|
assert_eq!(count(&fx, id), 0);
|
||||||
// nothing is left in the file, which stays and is still selected
|
// nothing is left of the file
|
||||||
let file = &fx.files[&id];
|
assert!(!fx.files.contains_key(&id));
|
||||||
assert!(file.trk.is_empty() && file.wpt.is_empty());
|
assert!(fx.order.0.is_empty());
|
||||||
assert_eq!(fx.order.0, [id]);
|
assert_eq!(fx.selection, Selection::Empty);
|
||||||
assert_eq!(
|
|
||||||
fx.selection,
|
|
||||||
Selection::File {
|
|
||||||
file_ids: HashSet::from([id])
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
+8
-9
@@ -11,9 +11,9 @@ use crate::{
|
|||||||
/// Keeps the trackpoints `start` to `end` (both included) of the selection, which are counted
|
/// Keeps the trackpoints `start` to `end` (both included) of the selection, which are counted
|
||||||
/// over its segments one after the other (as in the `RoutingBuffer`), and removes the others.
|
/// over its segments one after the other (as in the `RoutingBuffer`), and removes the others.
|
||||||
///
|
///
|
||||||
/// The segments that have no trackpoint left are removed, and so are the routes of these segments.
|
/// The segments that have no trackpoint left are removed. So are the tracks that have no segment
|
||||||
/// So are the tracks that have no segment left, if tracks or files are selected (not if only
|
/// left, if tracks or files are selected (not if only segments are), and the files that have no
|
||||||
/// segments are), and the files that have no trackpoint left, if files are selected. A range that goes past the last trackpoint stops at it.
|
/// trackpoint left, if files are selected. A range that goes past the last trackpoint stops at it.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Crop {
|
pub struct Crop {
|
||||||
pub start: u32,
|
pub start: u32,
|
||||||
@@ -43,7 +43,8 @@ impl Apply for Crop {
|
|||||||
.segment_locations(state.files, &state.order.0)
|
.segment_locations(state.files, &state.order.0)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|location| {
|
.map(|location| {
|
||||||
let len = location.segment(state.files).len();
|
let len =
|
||||||
|
state.files[&location.file_id].trk[location.trk].trkseg[location.seg].len();
|
||||||
let keep = (len > 0 && start < offset + len && end >= offset)
|
let keep = (len > 0 && start < offset + len && end >= offset)
|
||||||
.then(|| (start.saturating_sub(offset), (end - offset).min(len - 1)));
|
.then(|| (start.saturating_sub(offset), (end - offset).min(len - 1)));
|
||||||
offset += len;
|
offset += len;
|
||||||
@@ -72,14 +73,14 @@ impl Apply for Crop {
|
|||||||
let mut removed: HashSet<TrackSegmentId> = HashSet::new();
|
let mut removed: HashSet<TrackSegmentId> = HashSet::new();
|
||||||
let mut had_points: HashSet<FileId> = HashSet::new();
|
let mut had_points: HashSet<FileId> = HashSet::new();
|
||||||
for plan in &plans {
|
for plan in &plans {
|
||||||
let SegmentLocation { file_id, index } = plan.location;
|
let SegmentLocation { file_id, trk, seg } = plan.location;
|
||||||
let file = files
|
let file = files
|
||||||
.entry(file_id)
|
.entry(file_id)
|
||||||
.or_insert_with(|| (*state.files[&file_id]).clone());
|
.or_insert_with(|| (*state.files[&file_id]).clone());
|
||||||
if plan.len > 0 {
|
if plan.len > 0 {
|
||||||
had_points.insert(file_id);
|
had_points.insert(file_id);
|
||||||
}
|
}
|
||||||
let segment = file.segment_mut(index);
|
let segment = &mut file.trk[trk].trkseg[seg];
|
||||||
match plan.keep {
|
match plan.keep {
|
||||||
None => {
|
None => {
|
||||||
removed.insert(segment.id);
|
removed.insert(segment.id);
|
||||||
@@ -90,8 +91,6 @@ impl Apply for Crop {
|
|||||||
|
|
||||||
let mut gone: Vec<FileId> = vec![];
|
let mut gone: Vec<FileId> = vec![];
|
||||||
for (file_id, file) in files.iter_mut() {
|
for (file_id, file) in files.iter_mut() {
|
||||||
// a route has its segment, or is not there
|
|
||||||
file.rte.retain(|route| !removed.contains(&route.seg.id));
|
|
||||||
// the tracks that lost segments
|
// the tracks that lost segments
|
||||||
let touched: Vec<bool> = file
|
let touched: Vec<bool> = file
|
||||||
.trk
|
.trk
|
||||||
@@ -109,7 +108,7 @@ impl Apply for Crop {
|
|||||||
file.trk
|
file.trk
|
||||||
.retain(|track| !(touched.next().unwrap() && track.trkseg.is_empty()));
|
.retain(|track| !(touched.next().unwrap() && track.trkseg.is_empty()));
|
||||||
}
|
}
|
||||||
if prune_files && file.has_no_path() && had_points.contains(file_id) {
|
if prune_files && file.trk.is_empty() && had_points.contains(file_id) {
|
||||||
gone.push(*file_id);
|
gone.push(*file_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File renamed without changes.
+3
-1
@@ -14,7 +14,9 @@ impl Apply for Elevation<'_> {
|
|||||||
.segment_locations(state.files, &state.order.0);
|
.segment_locations(state.files, &state.order.0);
|
||||||
let count: usize = locations
|
let count: usize = locations
|
||||||
.iter()
|
.iter()
|
||||||
.map(|location| location.segment(state.files).len())
|
.map(|location| {
|
||||||
|
state.files[&location.file_id].trk[location.trk].trkseg[location.seg].len()
|
||||||
|
})
|
||||||
.sum();
|
.sum();
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return Err(CommandError::NothingToDo);
|
return Err(CommandError::NothingToDo);
|
||||||
+71
-175
@@ -1,59 +1,44 @@
|
|||||||
use std::{collections::HashSet, rc::Rc};
|
use std::{collections::HashSet, rc::Rc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, CommandError, File, FileId, LngLat, LngLatBounds, Route, Selection, State, Track,
|
Apply, CommandError, File, FileId, LngLat, LngLatBounds, Selection, State, Track, TrackId,
|
||||||
TrackId, TrackSegment, Waypoint, Waypoints, copy_file, copy_waypoint, crossarc_lnglat,
|
TrackSegment, Waypoint, Waypoints, copy_waypoint, crossarc_lnglat, distance,
|
||||||
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
|
||||||
/// each:
|
/// each:
|
||||||
///
|
///
|
||||||
/// - files: a file with several routes and tracks becomes one file per route and per track, in
|
/// - files: a file with several tracks becomes one file per track, in which the track is split
|
||||||
/// which the track is split into one track per segment. A file with a single track becomes one
|
/// into one track per segment. A file with a single track becomes one file per segment. The new
|
||||||
/// file per segment. The new files are in the place of the file, which goes, and are selected.
|
/// files are in the place of the file, which goes, and are selected. They are named after the
|
||||||
/// They are named after the route or track, or the file, and numbered. The waypoints go to the files that have the segments
|
/// track, or the file, and numbered. The waypoints go to the files that have the segments
|
||||||
/// closest to them (to the first file if the file has no trackpoint).
|
/// closest to them (to the first file if the file has no trackpoint).
|
||||||
/// - tracks: each selected track is replaced by one track per segment, which are selected. They
|
/// - tracks: each selected track is replaced by one track per segment, which are selected. They
|
||||||
/// are named after the track and numbered.
|
/// are named after the track and numbered.
|
||||||
///
|
///
|
||||||
/// What has a single segment is left as it is, which routes always are. 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, self.keep_originals),
|
Selection::File { file_ids } => extract_files(state, file_ids),
|
||||||
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(
|
fn extract_files(state: &mut State, file_ids: &HashSet<FileId>) -> Result<(), CommandError> {
|
||||||
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];
|
||||||
if file.segments().count() > 1 {
|
let segments: usize = file.trk.iter().map(|track| track.trkseg.len()).sum();
|
||||||
let mut files = extract_file(file);
|
if segments > 1 {
|
||||||
if keep_originals {
|
extracted.push((*id, extract_file(file)));
|
||||||
// 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() {
|
||||||
@@ -63,25 +48,13 @@ fn extract_files(
|
|||||||
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)) => {
|
Some((_, files)) => order.extend(files.iter().map(|file| file.id)),
|
||||||
if keep_originals {
|
|
||||||
order.push(*id);
|
|
||||||
}
|
|
||||||
order.extend(files.iter().map(|file| file.id));
|
|
||||||
}
|
|
||||||
None => order.push(*id),
|
None => order.push(*id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// the files that are kept stay selected, unless the originals are kept: then only what is new is
|
let mut selected: HashSet<FileId> = file_ids.clone();
|
||||||
let mut selected: HashSet<FileId> = if keep_originals {
|
|
||||||
HashSet::new()
|
|
||||||
} else {
|
|
||||||
file_ids.clone()
|
|
||||||
};
|
|
||||||
for (original, files) in extracted {
|
for (original, files) in extracted {
|
||||||
if !keep_originals {
|
state.files.remove(&original);
|
||||||
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);
|
||||||
@@ -95,65 +68,60 @@ fn extract_files(
|
|||||||
|
|
||||||
/// The files that a file with several segments becomes.
|
/// The files that a file with several segments becomes.
|
||||||
fn extract_file(file: &File) -> Vec<File> {
|
fn extract_file(file: &File) -> Vec<File> {
|
||||||
// for each new file: its routes, its tracks and its name, and the segments that go with it
|
// for each new file: its tracks and its name, and the segments that go with it
|
||||||
struct Part {
|
struct Part {
|
||||||
routes: Vec<Route>,
|
|
||||||
tracks: Vec<Track>,
|
tracks: Vec<Track>,
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
let name = |name: &Option<String>, i: usize| {
|
let (parts, owner): (Vec<Part>, Vec<usize>) = if file.trk.len() > 1 {
|
||||||
name.clone()
|
// one file per track, the segments of a track follow it
|
||||||
.unwrap_or_else(|| format!("{} ({})", file.info.name, i + 1))
|
let parts = file
|
||||||
};
|
.trk
|
||||||
let (parts, owner): (Vec<Part>, Vec<usize>) =
|
.iter()
|
||||||
if file.rte.len() + file.trk.len() > 1 {
|
.enumerate()
|
||||||
// one file per route and per track, the segments of a track follow it
|
.map(|(i, track)| Part {
|
||||||
let routes = file.rte.iter().enumerate().map(|(i, route)| Part {
|
|
||||||
routes: vec![route.clone()],
|
|
||||||
tracks: vec![],
|
|
||||||
name: name(&route.info.name, i),
|
|
||||||
});
|
|
||||||
let tracks = file.trk.iter().enumerate().map(|(i, track)| Part {
|
|
||||||
routes: vec![],
|
|
||||||
tracks: split_track(track),
|
tracks: split_track(track),
|
||||||
name: name(&track.info.name, file.rte.len() + i),
|
name: track
|
||||||
});
|
.info
|
||||||
let parts = routes.chain(tracks).collect();
|
.name
|
||||||
let owner =
|
.clone()
|
||||||
(0..file.rte.len())
|
.unwrap_or_else(|| format!("{} ({})", file.info.name, i + 1)),
|
||||||
.chain(file.trk.iter().enumerate().flat_map(|(i, track)| {
|
})
|
||||||
track.trkseg.iter().map(move |_| file.rte.len() + i)
|
.collect();
|
||||||
}))
|
let owner = file
|
||||||
.collect();
|
.trk
|
||||||
(parts, owner)
|
.iter()
|
||||||
} else {
|
.enumerate()
|
||||||
// one file per segment
|
.flat_map(|(i, track)| track.trkseg.iter().map(move |_| i))
|
||||||
let track = &file.trk[0];
|
.collect();
|
||||||
let base = track
|
(parts, owner)
|
||||||
.info
|
} else {
|
||||||
.name
|
// one file per segment
|
||||||
.clone()
|
let track = &file.trk[0];
|
||||||
.unwrap_or_else(|| file.info.name.clone());
|
let base = track
|
||||||
let parts = track
|
.info
|
||||||
.trkseg
|
.name
|
||||||
.iter()
|
.clone()
|
||||||
.enumerate()
|
.unwrap_or_else(|| file.info.name.clone());
|
||||||
.map(|(i, segment)| Part {
|
let parts = track
|
||||||
routes: vec![],
|
.trkseg
|
||||||
tracks: vec![Track {
|
.iter()
|
||||||
id: if i == 0 { track.id } else { Default::default() },
|
.enumerate()
|
||||||
trkseg: vec![segment.clone()],
|
.map(|(i, segment)| Part {
|
||||||
..track.clone()
|
tracks: vec![Track {
|
||||||
}],
|
id: if i == 0 { track.id } else { Default::default() },
|
||||||
name: format!("{base} ({})", i + 1),
|
trkseg: vec![segment.clone()],
|
||||||
})
|
..track.clone()
|
||||||
.collect();
|
}],
|
||||||
let owner = (0..track.trkseg.len()).collect();
|
name: format!("{base} ({})", i + 1),
|
||||||
(parts, owner)
|
})
|
||||||
};
|
.collect();
|
||||||
|
let owner = (0..track.trkseg.len()).collect();
|
||||||
|
(parts, owner)
|
||||||
|
};
|
||||||
|
|
||||||
// the waypoints go with the segments that are the closest to them
|
// the waypoints go with the segments that are the closest to them
|
||||||
let segments: Vec<&TrackSegment> = file.segments().collect();
|
let segments: Vec<&TrackSegment> = file.trk.iter().flat_map(|track| &track.trkseg).collect();
|
||||||
let mut waypoints: Vec<Vec<Waypoint>> = vec![vec![]; parts.len()];
|
let mut waypoints: Vec<Vec<Waypoint>> = vec![vec![]; parts.len()];
|
||||||
for waypoint in file.wpt.iter() {
|
for waypoint in file.wpt.iter() {
|
||||||
let mut owners: Vec<usize> = closest_segments(&segments, waypoint.coordinates)
|
let mut owners: Vec<usize> = closest_segments(&segments, waypoint.coordinates)
|
||||||
@@ -181,7 +149,6 @@ fn extract_file(file: &File) -> Vec<File> {
|
|||||||
.map(|(part, waypoints)| {
|
.map(|(part, waypoints)| {
|
||||||
let mut extracted = File {
|
let mut extracted = File {
|
||||||
id: Default::default(),
|
id: Default::default(),
|
||||||
rte: part.routes,
|
|
||||||
trk: part.tracks,
|
trk: part.tracks,
|
||||||
wpt: Waypoints::default(),
|
wpt: Waypoints::default(),
|
||||||
..file.clone()
|
..file.clone()
|
||||||
@@ -405,11 +372,7 @@ 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 {
|
Extract.apply(&mut fx.state()).unwrap();
|
||||||
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));
|
||||||
@@ -439,48 +402,6 @@ 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(
|
||||||
@@ -494,11 +415,7 @@ 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 {
|
Extract.apply(&mut fx.state()).unwrap();
|
||||||
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"]);
|
||||||
@@ -534,11 +451,7 @@ 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 {
|
Extract.apply(&mut fx.state()).unwrap();
|
||||||
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
|
||||||
@@ -567,11 +480,7 @@ mod tests {
|
|||||||
vec![waypoint("w", 1.0)],
|
vec![waypoint("w", 1.0)],
|
||||||
);
|
);
|
||||||
let (mut fx, _) = fixture(original);
|
let (mut fx, _) = fixture(original);
|
||||||
Extract {
|
Extract.apply(&mut fx.state()).unwrap();
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -594,11 +503,7 @@ mod tests {
|
|||||||
trk_ids: [ids[1], ids[2]].into(),
|
trk_ids: [ids[1], ids[2]].into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Extract {
|
Extract.apply(&mut fx.state()).unwrap();
|
||||||
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();
|
||||||
@@ -634,10 +539,7 @@ 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 {
|
Extract.apply(&mut fx.state()),
|
||||||
keep_originals: false,
|
|
||||||
}
|
|
||||||
.apply(&mut fx.state()),
|
|
||||||
Err(CommandError::NothingToDo)
|
Err(CommandError::NothingToDo)
|
||||||
);
|
);
|
||||||
fx.selection = Selection::Track {
|
fx.selection = Selection::Track {
|
||||||
@@ -645,10 +547,7 @@ mod tests {
|
|||||||
trk_ids: [track_id].into(),
|
trk_ids: [track_id].into(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Extract {
|
Extract.apply(&mut fx.state()),
|
||||||
keep_originals: false,
|
|
||||||
}
|
|
||||||
.apply(&mut fx.state()),
|
|
||||||
Err(CommandError::NothingToDo)
|
Err(CommandError::NothingToDo)
|
||||||
);
|
);
|
||||||
for selection in [
|
for selection in [
|
||||||
@@ -662,10 +561,7 @@ mod tests {
|
|||||||
] {
|
] {
|
||||||
fx.selection = selection;
|
fx.selection = selection;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Extract {
|
Extract.apply(&mut fx.state()),
|
||||||
keep_originals: false,
|
|
||||||
}
|
|
||||||
.apply(&mut fx.state()),
|
|
||||||
Err(CommandError::NothingToDo)
|
Err(CommandError::NothingToDo)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+2
-2
@@ -39,7 +39,7 @@ impl Apply for InsertAnchor {
|
|||||||
.selection
|
.selection
|
||||||
.segment_locations(state.files, &state.order.0)
|
.segment_locations(state.files, &state.order.0)
|
||||||
{
|
{
|
||||||
let segment = location.segment(state.files);
|
let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg];
|
||||||
let mut points = segment.iter();
|
let mut points = segment.iter();
|
||||||
let Some(mut previous) = points.next() else {
|
let Some(mut previous) = points.next() else {
|
||||||
continue;
|
continue;
|
||||||
@@ -81,7 +81,7 @@ impl Apply for InsertAnchor {
|
|||||||
let closest = closest.ok_or(CommandError::NothingToDo)?;
|
let closest = closest.ok_or(CommandError::NothingToDo)?;
|
||||||
|
|
||||||
let mut file = (*state.files[&closest.location.file_id]).clone();
|
let mut file = (*state.files[&closest.location.file_id]).clone();
|
||||||
let segment = file.segment_mut(closest.location.index);
|
let segment = &mut file.trk[closest.location.trk].trkseg[closest.location.seg];
|
||||||
let before = closest.before;
|
let before = closest.before;
|
||||||
// on a trackpoint already, if it is closer than `SNAP_DISTANCE`
|
// on a trackpoint already, if it is closer than `SNAP_DISTANCE`
|
||||||
let snapped = if segment.len() == 1 {
|
let snapped = if segment.len() == 1 {
|
||||||
+16
-155
@@ -1,13 +1,11 @@
|
|||||||
use std::{collections::HashSet, rc::Rc};
|
use std::{collections::HashSet, rc::Rc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, CommandError, FileId, GlobalStatistics, Route, RouteId, Selection, State, Statistics,
|
Apply, CommandError, FileId, GlobalStatistics, Selection, State, Statistics, Track, TrackId,
|
||||||
Track, TrackId, TrackSegment, TrackSegmentId, Waypoint, Waypoints, copy_route, copy_track,
|
TrackSegment, TrackSegmentId, Waypoint, Waypoints, replace_trackpoints,
|
||||||
replace_trackpoints,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
#[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,16 +15,11 @@ pub enum MergeType {
|
|||||||
|
|
||||||
/// Merges the selected elements into the first of them:
|
/// Merges the selected elements into the first of them:
|
||||||
///
|
///
|
||||||
/// - files: the other files are deleted. The routes and tracks of all of them are in the first
|
/// - files: the other files are deleted. The tracks of all of them are in the first file, as
|
||||||
/// file, as they are with `Group`, or all connected in the first of them (a route or a track,
|
/// they are with `Group`, or all connected in the first track with `Connect`. The waypoints of
|
||||||
/// the routes of a file coming before its tracks) with `Connect`. The waypoints of
|
/// all of them are in the first file too, except the ones that are the same.
|
||||||
/// 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.
|
||||||
/// - routes of a file: the first route gets their trackpoints, all connected with `Connect`, and
|
|
||||||
/// the other routes are removed. A route has a single segment, so there is nothing to `Group`.
|
|
||||||
/// - segments of a track: they are all connected in a segment at the position of the first one
|
/// - segments of a track: they are all connected in a segment at the position of the first one
|
||||||
/// with `Connect`. With `Group` they are put one after the other, at that position.
|
/// with `Connect`. With `Group` they are put one after the other, at that position.
|
||||||
///
|
///
|
||||||
@@ -39,8 +32,6 @@ 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 {
|
||||||
@@ -48,7 +39,6 @@ impl Apply for Merge {
|
|||||||
let selection = state.selection.clone();
|
let selection = state.selection.clone();
|
||||||
match &selection {
|
match &selection {
|
||||||
Selection::File { file_ids } => self.merge_files(state, file_ids),
|
Selection::File { file_ids } => self.merge_files(state, file_ids),
|
||||||
Selection::Route { file_id, rte_ids } => self.merge_routes(state, *file_id, rte_ids),
|
|
||||||
Selection::Track { file_id, trk_ids } => self.merge_tracks(state, *file_id, trk_ids),
|
Selection::Track { file_id, trk_ids } => self.merge_tracks(state, *file_id, trk_ids),
|
||||||
Selection::TrackSegment {
|
Selection::TrackSegment {
|
||||||
file_id,
|
file_id,
|
||||||
@@ -82,26 +72,21 @@ impl Merge {
|
|||||||
return Err(CommandError::NothingToDo);
|
return Err(CommandError::NothingToDo);
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut routes: Vec<Route> = vec![];
|
|
||||||
let mut tracks: Vec<Track> = vec![];
|
let mut tracks: Vec<Track> = vec![];
|
||||||
let mut segments: Vec<TrackSegment> = vec![];
|
|
||||||
let mut waypoints: Vec<Waypoint> = vec![];
|
let mut waypoints: Vec<Waypoint> = vec![];
|
||||||
for id in &ordered {
|
for id in &ordered {
|
||||||
let file = &state.files[id];
|
let file = &state.files[id];
|
||||||
routes.extend(file.rte.iter().cloned());
|
|
||||||
tracks.extend(file.trk.iter().cloned());
|
tracks.extend(file.trk.iter().cloned());
|
||||||
segments.extend(file.segments().cloned());
|
|
||||||
for waypoint in file.wpt.iter() {
|
for waypoint in file.wpt.iter() {
|
||||||
if !waypoints.iter().any(|kept| kept.same_content(waypoint)) {
|
if !waypoints.iter().any(|kept| kept.same_content(waypoint)) {
|
||||||
waypoints.push(waypoint.clone());
|
waypoints.push(waypoint.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// the first route or track, which holds the connected segment
|
let segments: Vec<TrackSegment> = tracks
|
||||||
let first_is_route = ordered.iter().find_map(|id| {
|
.iter()
|
||||||
let file = &state.files[id];
|
.flat_map(|track| track.trkseg.iter().cloned())
|
||||||
(!file.has_no_path()).then(|| !file.rte.is_empty())
|
.collect();
|
||||||
});
|
|
||||||
|
|
||||||
match self.type_ {
|
match self.type_ {
|
||||||
MergeType::Group if ordered.len() == 1 => return Err(CommandError::NothingToDo),
|
MergeType::Group if ordered.len() == 1 => return Err(CommandError::NothingToDo),
|
||||||
@@ -109,49 +94,21 @@ impl Merge {
|
|||||||
return Err(CommandError::NothingToDo);
|
return Err(CommandError::NothingToDo);
|
||||||
}
|
}
|
||||||
MergeType::Connect if !segments.is_empty() => {
|
MergeType::Connect if !segments.is_empty() => {
|
||||||
// a single route or track, which is the first one, with the connected segment
|
// a single track, which is the first one, with the connected segment
|
||||||
let merged = connect(&segments, segments[0].id, self.remove_gaps);
|
let merged = connect(&segments, segments[0].id, self.remove_gaps);
|
||||||
if first_is_route == Some(true) {
|
let first = Track {
|
||||||
let first = Route {
|
trkseg: vec![merged],
|
||||||
seg: merged,
|
..tracks[0].clone()
|
||||||
..routes[0].clone()
|
};
|
||||||
};
|
tracks = vec![first];
|
||||||
(routes, tracks) = (vec![first], vec![]);
|
|
||||||
} else {
|
|
||||||
let first = Track {
|
|
||||||
trkseg: vec![merged],
|
|
||||||
..tracks[0].clone()
|
|
||||||
};
|
|
||||||
(routes, tracks) = (vec![], vec![first]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut target = (*state.files[&target_id]).clone();
|
let mut target = (*state.files[&target_id]).clone();
|
||||||
target.rte = routes;
|
|
||||||
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 routes, tracks and segments do not share their ids with the
|
|
||||||
// originals
|
|
||||||
target.id = Default::default();
|
|
||||||
target.rte = target.rte.iter().map(copy_route).collect();
|
|
||||||
target.trk = target.trk.iter().map(copy_track).collect();
|
|
||||||
let at = state
|
|
||||||
.order
|
|
||||||
.0
|
|
||||||
.iter()
|
|
||||||
.rposition(|id| file_ids.contains(id))
|
|
||||||
.map_or(state.order.0.len(), |i| i + 1);
|
|
||||||
state.order.0.insert(at, target.id);
|
|
||||||
*state.selection = Selection::File {
|
|
||||||
file_ids: [target.id].into(),
|
|
||||||
};
|
|
||||||
state.files.insert(target.id, Rc::new(target));
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
for id in &ordered[1..] {
|
for id in &ordered[1..] {
|
||||||
state.files.remove(id);
|
state.files.remove(id);
|
||||||
}
|
}
|
||||||
@@ -160,33 +117,6 @@ impl Merge {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_routes(
|
|
||||||
&self,
|
|
||||||
state: &mut State,
|
|
||||||
file_id: FileId,
|
|
||||||
rte_ids: &HashSet<RouteId>,
|
|
||||||
) -> Result<(), CommandError> {
|
|
||||||
let file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
|
|
||||||
let selected: Vec<usize> = (0..file.rte.len())
|
|
||||||
.filter(|i| rte_ids.contains(&file.rte[*i].id))
|
|
||||||
.collect();
|
|
||||||
if selected.len() < 2 || self.type_ == MergeType::Group {
|
|
||||||
return Err(CommandError::NothingToDo);
|
|
||||||
}
|
|
||||||
let first = selected[0];
|
|
||||||
let segments: Vec<TrackSegment> =
|
|
||||||
selected.iter().map(|i| file.rte[*i].seg.clone()).collect();
|
|
||||||
let mut file = (**file).clone();
|
|
||||||
file.rte[first].seg = connect(&segments, segments[0].id, self.remove_gaps);
|
|
||||||
let mut index = 0;
|
|
||||||
file.rte.retain(|_| {
|
|
||||||
index += 1;
|
|
||||||
!selected[1..].contains(&(index - 1))
|
|
||||||
});
|
|
||||||
state.files.insert(file_id, Rc::new(file));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_tracks(
|
fn merge_tracks(
|
||||||
&self,
|
&self,
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
@@ -343,7 +273,6 @@ mod tests {
|
|||||||
Merge {
|
Merge {
|
||||||
type_,
|
type_,
|
||||||
remove_gaps: false,
|
remove_gaps: false,
|
||||||
keep_originals: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,73 +340,6 @@ 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();
|
||||||
@@ -567,7 +429,6 @@ 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.
+1
-8
@@ -58,8 +58,7 @@ impl Apply for NewWaypoint<'_> {
|
|||||||
.filter(|id| file_ids.contains(id))
|
.filter(|id| file_ids.contains(id))
|
||||||
.copied()
|
.copied()
|
||||||
.collect(),
|
.collect(),
|
||||||
Selection::Route { file_id, .. }
|
Selection::Track { file_id, .. }
|
||||||
| Selection::Track { file_id, .. }
|
|
||||||
| Selection::TrackSegment { file_id, .. }
|
| Selection::TrackSegment { file_id, .. }
|
||||||
| Selection::Waypoints { file_id }
|
| Selection::Waypoints { file_id }
|
||||||
| Selection::Waypoint { file_id, .. } => vec![*file_id],
|
| Selection::Waypoint { file_id, .. } => vec![*file_id],
|
||||||
@@ -119,14 +118,12 @@ mod tests {
|
|||||||
New {
|
New {
|
||||||
name: "a",
|
name: "a",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
New {
|
New {
|
||||||
name: "b",
|
name: "b",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -172,7 +169,6 @@ mod tests {
|
|||||||
New {
|
New {
|
||||||
name: "a",
|
name: "a",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -200,7 +196,6 @@ mod tests {
|
|||||||
New {
|
New {
|
||||||
name,
|
name,
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -239,7 +234,6 @@ mod tests {
|
|||||||
New {
|
New {
|
||||||
name: "a",
|
name: "a",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -256,7 +250,6 @@ mod tests {
|
|||||||
New {
|
New {
|
||||||
name: "a",
|
name: "a",
|
||||||
trackpoint: None,
|
trackpoint: None,
|
||||||
route: false,
|
|
||||||
}
|
}
|
||||||
.apply(&mut fx.state())
|
.apply(&mut fx.state())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
+1
-1
@@ -23,7 +23,7 @@ impl Apply for Reduce {
|
|||||||
let mut changed = vec![];
|
let mut changed = vec![];
|
||||||
let mut kept_points = vec![];
|
let mut kept_points = vec![];
|
||||||
for location in &locations {
|
for location in &locations {
|
||||||
let segment = location.segment(state.files);
|
let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg];
|
||||||
let kept = reduce_indices(segment, self.tolerance);
|
let kept = reduce_indices(segment, self.tolerance);
|
||||||
if kept.len() == segment.len() {
|
if kept.len() == segment.len() {
|
||||||
continue;
|
continue;
|
||||||
+9
-21
@@ -1,15 +1,14 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, CommandError, File, FileId, RouteId, Selection, State, TrackId, TrackSegmentId,
|
Apply, CommandError, File, FileId, Selection, State, TrackId, TrackSegmentId, end_time,
|
||||||
end_time, reverse_routes, reverse_segment, reverse_segments, reverse_tracks, start_time,
|
reverse_segment, reverse_segments, reverse_tracks, start_time, track_end_time,
|
||||||
track_end_time, track_start_time,
|
track_start_time,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Reverses the direction of the selected elements: the whole of each selected file (its routes,
|
/// Reverses the direction of the selected elements: the whole of each selected file (its tracks,
|
||||||
/// its tracks, the segments of its tracks and their trackpoints are in the opposite order; the
|
/// the segments of its tracks and their trackpoints are in the opposite order), of each selected
|
||||||
/// routes and the tracks are two wholes, as the routes stay before the tracks), of each selected
|
/// track (its segments and their trackpoints), or each selected segment on its own (its
|
||||||
/// track (its segments and their trackpoints), or each selected route or segment on its own (its
|
|
||||||
/// trackpoints). The timestamps are mirrored, see [`reverse_segment`].
|
/// trackpoints). The timestamps are mirrored, see [`reverse_segment`].
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Reverse;
|
pub struct Reverse;
|
||||||
@@ -21,7 +20,9 @@ impl Apply for Reverse {
|
|||||||
.segment_locations(state.files, &state.order.0);
|
.segment_locations(state.files, &state.order.0);
|
||||||
let points: usize = locations
|
let points: usize = locations
|
||||||
.iter()
|
.iter()
|
||||||
.map(|location| location.segment(state.files).len())
|
.map(|location| {
|
||||||
|
state.files[&location.file_id].trk[location.trk].trkseg[location.seg].len()
|
||||||
|
})
|
||||||
.sum();
|
.sum();
|
||||||
if points <= 1 {
|
if points <= 1 {
|
||||||
return Err(CommandError::NothingToDo);
|
return Err(CommandError::NothingToDo);
|
||||||
@@ -39,18 +40,12 @@ impl Apply for Reverse {
|
|||||||
Selection::File { file_ids } => {
|
Selection::File { file_ids } => {
|
||||||
for file_id in file_ids {
|
for file_id in file_ids {
|
||||||
edit(file_id, &|file| {
|
edit(file_id, &|file| {
|
||||||
let end = file.rte.last().and_then(|rte| end_time(&rte.seg));
|
|
||||||
let start = file.rte.first().and_then(|rte| start_time(&rte.seg));
|
|
||||||
reverse_routes(&mut file.rte, end, start);
|
|
||||||
let end = file.trk.last().and_then(track_end_time);
|
let end = file.trk.last().and_then(track_end_time);
|
||||||
let start = file.trk.first().and_then(track_start_time);
|
let start = file.trk.first().and_then(track_start_time);
|
||||||
reverse_tracks(&mut file.trk, end, start);
|
reverse_tracks(&mut file.trk, end, start);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Selection::Route { file_id, rte_ids } => edit(file_id, &|file| {
|
|
||||||
reverse_selected_routes(file, rte_ids);
|
|
||||||
}),
|
|
||||||
Selection::Track { file_id, trk_ids } => edit(file_id, &|file| {
|
Selection::Track { file_id, trk_ids } => edit(file_id, &|file| {
|
||||||
reverse_selected_tracks(file, trk_ids);
|
reverse_selected_tracks(file, trk_ids);
|
||||||
}),
|
}),
|
||||||
@@ -71,13 +66,6 @@ impl Apply for Reverse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reverse_selected_routes(file: &mut File, ids: &std::collections::HashSet<RouteId>) {
|
|
||||||
for route in file.rte.iter_mut().filter(|route| ids.contains(&route.id)) {
|
|
||||||
let (end, start) = (end_time(&route.seg), start_time(&route.seg));
|
|
||||||
reverse_segment(&mut route.seg, end, start);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reverse_selected_tracks(file: &mut File, ids: &std::collections::HashSet<TrackId>) {
|
fn reverse_selected_tracks(file: &mut File, ids: &std::collections::HashSet<TrackId>) {
|
||||||
for track in file.trk.iter_mut().filter(|track| ids.contains(&track.id)) {
|
for track in file.trk.iter_mut().filter(|track| ids.contains(&track.id)) {
|
||||||
let end = track.trkseg.last().and_then(end_time);
|
let end = track.trkseg.last().and_then(end_time);
|
||||||
+1
-1
@@ -14,7 +14,7 @@ impl Apply for RoundTrip {
|
|||||||
.selection
|
.selection
|
||||||
.segment_locations(state.files, &state.order.0)
|
.segment_locations(state.files, &state.order.0)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|l| l.segment(state.files).len() >= 2)
|
.filter(|l| state.files[&l.file_id].trk[l.trk].trkseg[l.seg].len() >= 2)
|
||||||
.collect();
|
.collect();
|
||||||
if locations.is_empty() {
|
if locations.is_empty() {
|
||||||
return Err(CommandError::NothingToDo);
|
return Err(CommandError::NothingToDo);
|
||||||
+14
-17
@@ -1,11 +1,11 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, Categories, CommandError, FileId, SegmentIndex, SegmentLocation, Selection, State,
|
Apply, Categories, CommandError, FileId, SegmentLocation, Selection, State, Statistics, Track,
|
||||||
Statistics, Track, TrackSegment, Trackpoint, distance, replace_trackpoints,
|
TrackSegment, Trackpoint, distance, replace_trackpoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The values of an OSM attribute (surface, highway...) of the new trackpoints of a [`Reroute`], as
|
/// The values of an OSM attribute (surface, highway...) of the new trackpoints of a [`Route`], as
|
||||||
/// the intervals of consecutive trackpoints that share one: `starts[i]` is the index of the first
|
/// the intervals of consecutive trackpoints that share one: `starts[i]` is the index of the first
|
||||||
/// trackpoint of the interval `i`, and `values[i]` its value, 0 when unknown, else 1 + the index
|
/// trackpoint of the interval `i`, and `values[i]` its value, 0 when unknown, else 1 + the index
|
||||||
/// of its name in `names`. No intervals means that the attribute is unknown everywhere.
|
/// of its name in `names`. No intervals means that the attribute is unknown everywhere.
|
||||||
@@ -64,7 +64,7 @@ impl RouteCategory<'_> {
|
|||||||
/// anchors anyway. The ones right before and after the range, between which the route goes, become
|
/// anchors anyway. The ones right before and after the range, between which the route goes, become
|
||||||
/// shown at every zoom level as well.
|
/// shown at every zoom level as well.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Reroute<'a> {
|
pub struct Route<'a> {
|
||||||
pub start: u32,
|
pub start: u32,
|
||||||
pub end: u32,
|
pub end: u32,
|
||||||
pub lng: &'a [f64],
|
pub lng: &'a [f64],
|
||||||
@@ -89,7 +89,7 @@ pub(super) struct Target {
|
|||||||
pub end: usize,
|
pub end: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Apply for Reroute<'_> {
|
impl Apply for Route<'_> {
|
||||||
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
fn apply(self, state: &mut State) -> Result<(), CommandError> {
|
||||||
let (start, end) = (self.start as usize, self.end as usize);
|
let (start, end) = (self.start as usize, self.end as usize);
|
||||||
let len = self.lng.len();
|
let len = self.lng.len();
|
||||||
@@ -152,17 +152,14 @@ impl Apply for Reroute<'_> {
|
|||||||
}
|
}
|
||||||
let seg = file.trk[trk].trkseg.len() - 1;
|
let seg = file.trk[trk].trkseg.len() - 1;
|
||||||
Target {
|
Target {
|
||||||
location: SegmentLocation {
|
location: SegmentLocation { file_id, trk, seg },
|
||||||
file_id,
|
|
||||||
index: SegmentIndex::Track { trk, seg },
|
|
||||||
},
|
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let segment = file.segment_mut(target.location.index);
|
let segment = &mut file.trk[target.location.trk].trkseg[target.location.seg];
|
||||||
let new_len = points.len();
|
let new_len = points.len();
|
||||||
let (speed, start_time) = speed_and_start_time(segment, &target, &points);
|
let (speed, start_time) = speed_and_start_time(segment, &target, &points);
|
||||||
let has_times = !segment.is_empty() && segment[0].time.is_some();
|
let has_times = !segment.is_empty() && segment[0].time.is_some();
|
||||||
@@ -211,7 +208,7 @@ pub(super) fn find_target(
|
|||||||
let segments: Vec<(SegmentLocation, usize)> = locations
|
let segments: Vec<(SegmentLocation, usize)> = locations
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|location| {
|
.map(|location| {
|
||||||
let segment = location.segment(state.files);
|
let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg];
|
||||||
(location, segment.len())
|
(location, segment.len())
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -385,8 +382,8 @@ mod tests {
|
|||||||
lng: &'a [f64],
|
lng: &'a [f64],
|
||||||
lat: &'a [f64],
|
lat: &'a [f64],
|
||||||
ele: &'a [f64],
|
ele: &'a [f64],
|
||||||
) -> Reroute<'a> {
|
) -> Route<'a> {
|
||||||
Reroute {
|
Route {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
lng,
|
lng,
|
||||||
@@ -462,7 +459,7 @@ mod tests {
|
|||||||
route(0, 0, &[1.0], &[], &[]).apply(&mut fx.state()),
|
route(0, 0, &[1.0], &[], &[]).apply(&mut fx.state()),
|
||||||
route(0, len + 1, &[], &[], &[]).apply(&mut fx.state()),
|
route(0, len + 1, &[], &[], &[]).apply(&mut fx.state()),
|
||||||
route(len + 1, len + 1, &[1.0], &[1.0], &[1.0]).apply(&mut fx.state()),
|
route(len + 1, len + 1, &[1.0], &[1.0], &[1.0]).apply(&mut fx.state()),
|
||||||
Reroute {
|
Route {
|
||||||
anchors: &[1],
|
anchors: &[1],
|
||||||
..route(0, 0, &[1.0], &[1.0], &[1.0])
|
..route(0, 0, &[1.0], &[1.0], &[1.0])
|
||||||
}
|
}
|
||||||
@@ -592,7 +589,7 @@ mod tests {
|
|||||||
|
|
||||||
// 5 new points replacing the 2 in the middle, two of them are anchors
|
// 5 new points replacing the 2 in the middle, two of them are anchors
|
||||||
let (lng, lat, ele) = ([1.0; 5], [2.0; 5], [3.0; 5]);
|
let (lng, lat, ele) = ([1.0; 5], [2.0; 5], [3.0; 5]);
|
||||||
Reroute {
|
Route {
|
||||||
anchors: &[1, 3],
|
anchors: &[1, 3],
|
||||||
..route(1, 3, &lng, &lat, &ele)
|
..route(1, 3, &lng, &lat, &ele)
|
||||||
}
|
}
|
||||||
@@ -642,7 +639,7 @@ mod tests {
|
|||||||
let names = ["asphalt".to_string(), "gravel".to_string()];
|
let names = ["asphalt".to_string(), "gravel".to_string()];
|
||||||
let (lng, lat, ele) = ([1.0; 4], [2.0; 4], [3.0; 4]);
|
let (lng, lat, ele) = ([1.0; 4], [2.0; 4], [3.0; 4]);
|
||||||
// asphalt, asphalt, unknown, gravel
|
// asphalt, asphalt, unknown, gravel
|
||||||
Reroute {
|
Route {
|
||||||
surface: RouteCategory {
|
surface: RouteCategory {
|
||||||
starts: &[0, 2, 3],
|
starts: &[0, 2, 3],
|
||||||
values: &[1, 0, 2],
|
values: &[1, 0, 2],
|
||||||
@@ -686,7 +683,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
for surface in broken {
|
for surface in broken {
|
||||||
let result = Reroute {
|
let result = Route {
|
||||||
surface,
|
surface,
|
||||||
..route(0, 0, &lng, &lat, &ele)
|
..route(0, 0, &lng, &lat, &ele)
|
||||||
}
|
}
|
||||||
+59
-178
@@ -1,44 +1,35 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Apply, CommandError, File, Route, SegmentIndex, Selection, State, Track, TrackSegment,
|
Apply, CommandError, File, State, Track, TrackSegment, Waypoints, copy_segment, copy_track,
|
||||||
Waypoints, copy_file, copy_route, copy_segment, copy_track, copy_waypoint,
|
copy_waypoint,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::route::find_target;
|
use super::route::find_target;
|
||||||
|
|
||||||
/// What is cut in two where the trackpoint is.
|
/// What is cut in two where the trackpoint is.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub enum SplitType {
|
pub enum SplitType {
|
||||||
/// The file becomes two files, which both have the waypoints (copies, in the second one).
|
/// The file becomes two files, which both have the waypoints (copies, in the second one).
|
||||||
Files,
|
Files,
|
||||||
/// The route becomes two routes.
|
|
||||||
Routes,
|
|
||||||
/// The track becomes two tracks.
|
/// The track becomes two tracks.
|
||||||
Tracks,
|
Tracks,
|
||||||
/// The segment of a track becomes two segments.
|
/// The segment becomes two segments.
|
||||||
Segments,
|
Segments,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cuts the file, the route, the track or the segment of the trackpoint `at` of the selection (see
|
/// Cuts the file, the track or the segment of the trackpoint `at` of the selection (see
|
||||||
/// `RoutingBuffer`) in two, there: the trackpoint is the last one of the first part and the first
|
/// `RoutingBuffer`) in two, there: the trackpoint is the last one of the first part and the first
|
||||||
/// one of the second part. Cutting a route needs the trackpoint to be in a route, and cutting a
|
/// one of the second part.
|
||||||
/// track or a segment needs it to be in a track: there is nothing to do otherwise.
|
|
||||||
///
|
///
|
||||||
/// The first part keeps the ids of what is cut, the second one has new ids, as the elements that
|
/// The first part keeps the ids of what is cut, the second one has new ids, as the elements that
|
||||||
/// come after the trackpoint (the following segments of the track, the following routes and
|
/// come after the trackpoint (the following segments of the track, the following tracks of the
|
||||||
/// tracks of the file, the routes coming before the tracks), which are moved to it. When the file
|
/// file), which are moved to it. When the file is cut, the new file comes right after it. The
|
||||||
/// is cut, the new file comes right after it. The selection does not change.
|
/// selection does not change.
|
||||||
///
|
|
||||||
/// With `keep_originals`, a file that is cut (`SplitType::Files`) is left as it is: its two parts
|
|
||||||
/// are two new files, with new ids, which come right after it and are selected. Cutting a route, a
|
|
||||||
/// track or a segment changes the file in place anyway, so the flag has no effect on them.
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Split {
|
pub struct Split {
|
||||||
pub at: u32,
|
pub at: u32,
|
||||||
pub split_type: SplitType,
|
pub split_type: SplitType,
|
||||||
pub keep_originals: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Apply for Split {
|
impl Apply for Split {
|
||||||
@@ -46,115 +37,69 @@ impl Apply for Split {
|
|||||||
let at = self.at as usize;
|
let at = self.at as usize;
|
||||||
let target = find_target(state, at, at)?.ok_or(CommandError::NothingToDo)?;
|
let target = find_target(state, at, at)?.ok_or(CommandError::NothingToDo)?;
|
||||||
let location = target.location;
|
let location = target.location;
|
||||||
let (file_id, index) = (location.file_id, target.start);
|
let (file_id, trk, seg, index) =
|
||||||
let in_route = matches!(location.index, SegmentIndex::Route(_));
|
(location.file_id, location.trk, location.seg, target.start);
|
||||||
let fits = match self.split_type {
|
|
||||||
SplitType::Files => true,
|
|
||||||
SplitType::Routes => in_route,
|
|
||||||
SplitType::Tracks | SplitType::Segments => !in_route,
|
|
||||||
};
|
|
||||||
if !fits {
|
|
||||||
return Err(CommandError::NothingToDo);
|
|
||||||
}
|
|
||||||
let mut file = (*state.files[&file_id]).clone();
|
let mut file = (*state.files[&file_id]).clone();
|
||||||
if index >= file.segment(location.index).len() {
|
if index >= file.trk[trk].trkseg[seg].len() {
|
||||||
return Err(CommandError::InvalidData(
|
return Err(CommandError::InvalidData(
|
||||||
"trackpoint out of the segment".into(),
|
"trackpoint out of the segment".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// with `SplitType::Files`, what goes to the second file: what comes after the trackpoint
|
let track = file.trk[trk].clone();
|
||||||
let mut second_file = File {
|
let segment = &track.trkseg[seg];
|
||||||
id: Default::default(),
|
let (first, second) = (keep_up_to(segment, index), keep_from(segment, index));
|
||||||
rte: vec![],
|
// the track is cut: what is before the segment, and its first part, then the second part
|
||||||
trk: vec![],
|
// and what is after it
|
||||||
wpt: Waypoints::default(),
|
let split_track = || {
|
||||||
..file.clone()
|
let mut first_track = track.clone();
|
||||||
|
first_track.trkseg.truncate(seg);
|
||||||
|
first_track.trkseg.push(first.clone());
|
||||||
|
let mut second_track = Track {
|
||||||
|
id: Default::default(),
|
||||||
|
trkseg: vec![second.clone()],
|
||||||
|
..track.clone()
|
||||||
|
};
|
||||||
|
second_track
|
||||||
|
.trkseg
|
||||||
|
.extend(track.trkseg[seg + 1..].iter().map(copy_segment));
|
||||||
|
(first_track, second_track)
|
||||||
};
|
};
|
||||||
match location.index {
|
|
||||||
SegmentIndex::Route(rte) => {
|
match self.split_type {
|
||||||
let route = file.rte[rte].clone();
|
SplitType::Segments => {
|
||||||
let first = Route {
|
file.trk[trk].trkseg.splice(seg..=seg, [first, second]);
|
||||||
seg: keep_up_to(&route.seg, index),
|
|
||||||
..route.clone()
|
|
||||||
};
|
|
||||||
let second = Route {
|
|
||||||
id: Default::default(),
|
|
||||||
seg: keep_from(&route.seg, index),
|
|
||||||
..route.clone()
|
|
||||||
};
|
|
||||||
if self.split_type == SplitType::Routes {
|
|
||||||
file.rte.splice(rte..=rte, [first, second]);
|
|
||||||
state.files.insert(file_id, Rc::new(file));
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
// the following routes, and the tracks, which come after the routes
|
|
||||||
second_file.rte = [second]
|
|
||||||
.into_iter()
|
|
||||||
.chain(file.rte[rte + 1..].iter().map(copy_route))
|
|
||||||
.collect();
|
|
||||||
second_file.trk = file.trk.iter().map(copy_track).collect();
|
|
||||||
file.rte.truncate(rte);
|
|
||||||
file.rte.push(first);
|
|
||||||
file.trk.clear();
|
|
||||||
}
|
}
|
||||||
SegmentIndex::Track { trk, seg } => {
|
SplitType::Tracks => {
|
||||||
let track = file.trk[trk].clone();
|
let (first_track, second_track) = split_track();
|
||||||
let segment = &track.trkseg[seg];
|
file.trk.splice(trk..=trk, [first_track, second_track]);
|
||||||
let (first, second) = (keep_up_to(segment, index), keep_from(segment, index));
|
}
|
||||||
if self.split_type == SplitType::Segments {
|
SplitType::Files => {
|
||||||
file.trk[trk].trkseg.splice(seg..=seg, [first, second]);
|
let (first_track, second_track) = split_track();
|
||||||
state.files.insert(file_id, Rc::new(file));
|
let mut second_file = File {
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
// the track is cut: what is before the segment, and its first part, then the
|
|
||||||
// second part and what is after it
|
|
||||||
let mut first_track = track.clone();
|
|
||||||
first_track.trkseg.truncate(seg);
|
|
||||||
first_track.trkseg.push(first);
|
|
||||||
let mut second_track = Track {
|
|
||||||
id: Default::default(),
|
id: Default::default(),
|
||||||
trkseg: vec![second],
|
trk: vec![second_track],
|
||||||
..track.clone()
|
wpt: Waypoints::default(),
|
||||||
|
..file.clone()
|
||||||
};
|
};
|
||||||
second_track
|
second_file
|
||||||
.trkseg
|
.trk
|
||||||
.extend(track.trkseg[seg + 1..].iter().map(copy_segment));
|
.extend(file.trk[trk + 1..].iter().map(copy_track));
|
||||||
if self.split_type == SplitType::Tracks {
|
second_file
|
||||||
file.trk.splice(trk..=trk, [first_track, second_track]);
|
.wpt
|
||||||
state.files.insert(file_id, Rc::new(file));
|
.insert_at(0, file.wpt.iter().map(copy_waypoint).collect());
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
second_file.trk = [second_track]
|
|
||||||
.into_iter()
|
|
||||||
.chain(file.trk[trk + 1..].iter().map(copy_track))
|
|
||||||
.collect();
|
|
||||||
file.trk.truncate(trk);
|
file.trk.truncate(trk);
|
||||||
file.trk.push(first_track);
|
file.trk.push(first_track);
|
||||||
|
let position = state.order.0.iter().position(|id| *id == file_id);
|
||||||
|
state.order.0.insert(
|
||||||
|
position.map_or(state.order.0.len(), |p| p + 1),
|
||||||
|
second_file.id,
|
||||||
|
);
|
||||||
|
state.files.insert(second_file.id, Rc::new(second_file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
state.files.insert(file_id, Rc::new(file));
|
||||||
// the file is cut: both parts have the waypoints
|
|
||||||
second_file
|
|
||||||
.wpt
|
|
||||||
.insert_at(0, file.wpt.iter().map(copy_waypoint).collect());
|
|
||||||
let position = state.order.0.iter().position(|id| *id == file_id);
|
|
||||||
let at = position.map_or(state.order.0.len(), |p| p + 1);
|
|
||||||
if self.keep_originals {
|
|
||||||
// the parts are new files: nothing is shared by id with the file, nor between them
|
|
||||||
let first_file = copy_file(&file);
|
|
||||||
let ids = [first_file.id, second_file.id];
|
|
||||||
state.order.0.splice(at..at, ids);
|
|
||||||
state.files.insert(first_file.id, Rc::new(first_file));
|
|
||||||
state.files.insert(second_file.id, Rc::new(second_file));
|
|
||||||
*state.selection = Selection::File {
|
|
||||||
file_ids: ids.into(),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
state.order.0.insert(at, second_file.id);
|
|
||||||
state.files.insert(second_file.id, Rc::new(second_file));
|
|
||||||
state.files.insert(file_id, Rc::new(file));
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,11 +146,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn split(at: u32, split_type: SplitType) -> Split {
|
fn split(at: u32, split_type: SplitType) -> Split {
|
||||||
Split {
|
Split { at, split_type }
|
||||||
at,
|
|
||||||
split_type,
|
|
||||||
keep_originals: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lngs(segment: &TrackSegment) -> Vec<f64> {
|
fn lngs(segment: &TrackSegment) -> Vec<f64> {
|
||||||
@@ -374,66 +315,6 @@ mod tests {
|
|||||||
assert!(matches!(&fx.selection, Selection::File { file_ids } if file_ids.contains(&id)));
|
assert!(matches!(&fx.selection, Selection::File { file_ids } if file_ids.contains(&id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_split_files_keeping_the_originals() {
|
|
||||||
let (mut fx, id) = loaded();
|
|
||||||
let before = fx.files[&id].clone();
|
|
||||||
let (trk, seg) = long_segment(&before);
|
|
||||||
let at = index(&before, trk, seg, 1);
|
|
||||||
|
|
||||||
Split {
|
|
||||||
at,
|
|
||||||
split_type: SplitType::Files,
|
|
||||||
keep_originals: true,
|
|
||||||
}
|
|
||||||
.apply(&mut fx.state())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// the file is untouched, the two parts come right after it
|
|
||||||
assert!(Rc::ptr_eq(&fx.files[&id], &before));
|
|
||||||
assert_eq!(fx.files.len(), 3);
|
|
||||||
assert_eq!(fx.order.0[0], id);
|
|
||||||
let (first_id, second_id) = (fx.order.0[1], fx.order.0[2]);
|
|
||||||
let (a, b) = (&fx.files[&first_id], &fx.files[&second_id]);
|
|
||||||
let all = path(&before);
|
|
||||||
assert_eq!(path(a), all[..=at as usize]);
|
|
||||||
assert_eq!(path(b), all[at as usize..]);
|
|
||||||
// nothing is shared by id with the file, nor between the parts
|
|
||||||
let ids = |f: &File| -> HashSet<_> {
|
|
||||||
f.trk
|
|
||||||
.iter()
|
|
||||||
.flat_map(|t| std::iter::once(t.id.0).chain(t.trkseg.iter().map(|s| s.id.0)))
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
assert!(ids(a).is_disjoint(&ids(&before)));
|
|
||||||
assert!(ids(b).is_disjoint(&ids(&before)));
|
|
||||||
assert!(ids(a).is_disjoint(&ids(b)));
|
|
||||||
// the new files are selected
|
|
||||||
assert!(matches!(&fx.selection, Selection::File { file_ids }
|
|
||||||
if *file_ids == HashSet::from([first_id, second_id])));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_keeping_the_originals_does_not_matter_when_cutting_a_segment() {
|
|
||||||
let (mut fx, id) = loaded();
|
|
||||||
let before = fx.files[&id].clone();
|
|
||||||
let (trk, seg) = long_segment(&before);
|
|
||||||
|
|
||||||
Split {
|
|
||||||
at: index(&before, trk, seg, 2),
|
|
||||||
split_type: SplitType::Segments,
|
|
||||||
keep_originals: true,
|
|
||||||
}
|
|
||||||
.apply(&mut fx.state())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(fx.files.len(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
fx.files[&id].trk[trk].trkseg.len(),
|
|
||||||
before.trk[trk].trkseg.len() + 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_or_nothing() {
|
fn test_invalid_or_nothing() {
|
||||||
let (mut fx, id) = loaded();
|
let (mut fx, id) = loaded();
|
||||||
+4
-2
@@ -45,8 +45,10 @@ impl Apply for Time {
|
|||||||
let locations = state
|
let locations = state
|
||||||
.selection
|
.selection
|
||||||
.segment_locations(state.files, &state.order.0);
|
.segment_locations(state.files, &state.order.0);
|
||||||
let segments: Vec<&TrackSegment> =
|
let segments: Vec<&TrackSegment> = locations
|
||||||
locations.iter().map(|l| l.segment(state.files)).collect();
|
.iter()
|
||||||
|
.map(|l| &state.files[&l.file_id].trk[l.trk].trkseg[l.seg])
|
||||||
|
.collect();
|
||||||
if segments.iter().all(|segment| segment.is_empty()) {
|
if segments.iter().all(|segment| segment.is_empty()) {
|
||||||
return Err(CommandError::NothingToDo);
|
return Err(CommandError::NothingToDo);
|
||||||
}
|
}
|
||||||
+2
-2
@@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet};
|
|||||||
use crate::{FileId, FileWaypointsRevisionId, Files, TrackSegmentId, TrackSegmentRevisionId};
|
use crate::{FileId, FileWaypointsRevisionId, Files, TrackSegmentId, TrackSegmentRevisionId};
|
||||||
|
|
||||||
/// Coordinates buffers, as flat `[lng, lat, lng, lat, ...]` arrays so that they can be handed
|
/// Coordinates buffers, as flat `[lng, lat, lng, lat, ...]` arrays so that they can be handed
|
||||||
/// to the UI without per-point calls: one per segment of a route or track (its trackpoints) and one per file
|
/// to the UI without per-point calls: one per track segment (its trackpoints) and one per file
|
||||||
/// (its waypoints). A buffer is only rebuilt when the revision of its segment (or the waypoints
|
/// (its waypoints). A buffer is only rebuilt when the revision of its segment (or the waypoints
|
||||||
/// of its file) changes, so editing a segment does not touch the buffers of the others.
|
/// of its file) changes, so editing a segment does not touch the buffers of the others.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
@@ -30,7 +30,7 @@ impl CoordinatesCache {
|
|||||||
self.waypoints
|
self.waypoints
|
||||||
.insert(file.id, (file.wpt.rev_id, coordinates));
|
.insert(file.id, (file.wpt.rev_id, coordinates));
|
||||||
}
|
}
|
||||||
for seg in file.segments() {
|
for seg in file.trk.iter().flat_map(|trk| &trk.trkseg) {
|
||||||
segments.insert(seg.id);
|
segments.insert(seg.id);
|
||||||
if self
|
if self
|
||||||
.segments
|
.segments
|
||||||
Loaded 100 of 699 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user