95 Commits
Author SHA1 Message Date
vcoppe ce8398f749 renaming 2026-10-14 08:17:51 +02:00
vcoppe 3c2055b50e improve flow 2026-10-14 08:07:58 +02:00
vcoppe 1d13972014 fix 2026-10-11 11:19:57 +02:00
vcoppe 890bcd765a refactoring 2026-10-11 11:18:36 +02:00
vcoppe 4795513344 reorder methods 2026-10-11 10:58:15 +02:00
vcoppe d84ca47745 refactoring 2026-10-11 10:54:06 +02:00
vcoppe 9adb8e7221 outdated state 2026-10-11 10:48:43 +02:00
vcoppe a2b4d89150 cross-tab sync 2026-10-10 11:07:07 +02:00
vcoppe 77929cfa04 per-chunk categories storage 2026-10-09 17:55:07 +02:00
vcoppe dc36de2a48 import and export of routes, ref #192 2026-10-09 09:26:38 +02:00
vcoppe da9abbae5e refactoring and tests 2026-10-09 08:58:00 +02:00
vcoppe 3bb553d6b5 remove unused fields 2026-10-08 21:19:53 +02:00
vcoppe 83b76bb838 fix link fields 2026-10-08 21:13:44 +02:00
vcoppe 42a55e6759 progress 2026-10-08 21:04:29 +02:00
vcoppe b085a1d476 cleaning 2026-10-08 19:04:46 +02:00
vcoppe 840e943626 update readme 2026-10-08 18:41:27 +02:00
vcoppe 16e2e0f679 remove todo 2026-10-08 18:37:46 +02:00
vcoppe 0f9557ea82 remove gpx js package 2026-10-08 18:37:40 +02:00
vcoppe 67a4c090bd progress 2026-10-08 18:34:19 +02:00
vcoppe 606a0e974a export 2026-10-08 09:24:15 +02:00
vcoppe e8c4eb92f9 handle corrupt time data 2026-10-08 08:59:53 +02:00
vcoppe f0851086c0 fix reactivity 2026-10-08 08:47:14 +02:00
vcoppe bec1ce4c9b reduce 2026-10-08 08:36:29 +02:00
vcoppe b29c4cdb08 fix bounds for waypoints 2026-10-08 08:15:22 +02:00
vcoppe 63ba8596a7 time tool 2026-10-08 08:12:44 +02:00
vcoppe 82d2f79da0 progress 2026-10-07 22:20:40 +02:00
vcoppe f642d60f42 merge 2026-10-07 19:57:27 +02:00
vcoppe 24b877c4ee split 2026-10-07 19:38:21 +02:00
vcoppe e07ec97e12 crop 2026-10-07 18:54:00 +02:00
vcoppe 290f4d8c44 reverse 2026-10-07 18:45:56 +02:00
vcoppe a48608840f reverse 2026-10-07 18:36:00 +02:00
vcoppe df07892e76 refactoring 2026-10-07 18:22:30 +02:00
vcoppe 594dd8979a routing 2026-10-07 17:58:17 +02:00
vcoppe 29850c1c8f speed fix 2026-10-07 09:35:27 +02:00
vcoppe adbe4a2693 copy on wasm side to avoid any data corruption 2026-10-07 08:57:37 +02:00
vcoppe b843a18441 reduce data exchanges across boundary 2026-10-07 08:45:50 +02:00
vcoppe da8a9e3d84 handle self closing elements 2026-10-04 22:01:23 +02:00
vcoppe c6bd5b06e8 clean tool 2026-10-04 20:38:46 +02:00
vcoppe c04367dd86 progress 2026-10-04 19:19:53 +02:00
vcoppe 5e2f506120 progress 2026-10-04 19:14:41 +02:00
vcoppe c536e6247b progress 2026-10-04 17:29:57 +02:00
vcoppe 594d714b7f small fix 2026-10-04 17:19:35 +02:00
vcoppe 543fecadb8 fix popups 2026-10-04 17:09:41 +02:00
vcoppe 017d8521d6 use engine info 2026-10-04 16:27:03 +02:00
vcoppe 78e9d45072 small fix 2026-10-04 16:21:48 +02:00
vcoppe 7651d859c4 progress 2026-10-04 16:11:44 +02:00
vcoppe ec448b72e2 progress 2026-10-04 15:47:09 +02:00
vcoppe bf946f179a progress 2026-10-04 15:26:58 +02:00
vcoppe 9ff9b5475a progress 2026-10-04 15:03:37 +02:00
vcoppe 55f1d275b7 progress 2026-10-04 14:49:53 +02:00
vcoppe 1d534881f8 progress 2026-10-04 13:18:18 +02:00
vcoppe 03cbacfbb4 fix 2026-10-04 12:33:15 +02:00
vcoppe 9d0905959b fix markers 2026-10-04 12:23:42 +02:00
vcoppe b7ca462f58 progress 2026-10-04 11:52:46 +02:00
vcoppe 9015da4876 fixes 2026-10-04 11:07:12 +02:00
vcoppe f458a1104f progress 2026-10-04 11:00:46 +02:00
vcoppe caba9388af progress 2026-10-04 10:29:39 +02:00
vcoppe 763a7406ce progress 2026-10-04 10:15:51 +02:00
vcoppe 68c8933c35 progress 2026-10-04 09:55:08 +02:00
vcoppe 84b1558b2e progress 2026-10-04 09:50:34 +02:00
vcoppe cd870f0186 visibility 2026-10-04 09:40:17 +02:00
vcoppe 9ac2a2b69c progress 2026-10-04 08:53:00 +02:00
vcoppe 86aba8f4b1 progress 2026-10-04 08:26:15 +02:00
vcoppe 74b674e8e8 progress 2026-10-04 08:21:39 +02:00
vcoppe f1904bc12f cleaning 2026-10-03 20:54:05 +02:00
vcoppe 9afba53e5d progress 2026-10-03 19:50:38 +02:00
vcoppe fd76e0fa46 progress 2026-10-03 19:17:58 +02:00
vcoppe 438a87f06f progress 2026-10-03 19:08:56 +02:00
vcoppe a994e16f51 splice 2026-10-03 18:03:13 +02:00
vcoppe 639e7491eb merge methods into 1 2026-10-03 17:47:04 +02:00
vcoppe 891a08a3cc progress 2026-10-03 17:42:26 +02:00
vcoppe 0c07164dec remove (copy) in name 2026-10-03 17:26:53 +02:00
vcoppe f133d55889 progress 2026-10-03 17:21:16 +02:00
vcoppe fbb636cb06 progress 2026-10-03 16:52:21 +02:00
vcoppe bddceb28aa keep style info 2026-10-03 16:43:53 +02:00
vcoppe bff84fa24c progress 2026-10-03 16:40:17 +02:00
vcoppe 64914f9aa0 progress 2026-10-03 16:16:47 +02:00
vcoppe 2abf46c747 progress 2026-10-03 15:33:44 +02:00
vcoppe 888f41e979 refactoring 2026-10-03 13:56:16 +02:00
vcoppe 17533586ef progress 2026-10-03 13:41:58 +02:00
vcoppe c6ad088947 fix 2026-10-03 13:40:49 +02:00
vcoppe 87d6d5f30e change type 2026-10-03 11:49:10 +02:00
vcoppe dbe894b3a3 reorganize 2026-10-03 10:03:22 +02:00
vcoppe a921d4b9cd fixes 2026-10-02 18:50:10 +02:00
vcoppe b7db73cf77 progress 2026-10-02 18:06:45 +02:00
vcoppe 5868595a6c progress 2026-10-01 18:42:31 +02:00
vcoppe eb8e09b835 progress 2026-09-30 19:19:23 +02:00
vcoppe 41dc853f65 progress 2026-09-25 12:09:58 +02:00
vcoppe 27c0a57cc1 progress 2026-09-24 20:55:30 +02:00
vcoppe 497ac120ec progress 2026-09-23 19:53:39 +02:00
vcoppe a486016d96 progress 2026-09-21 23:49:57 +02:00
vcoppe cc5f5653ef progress 2026-09-20 13:58:27 +02:00
vcoppe 20222c450d parsing continued 2026-09-18 09:50:04 +02:00
vcoppe 69049e1345 parsing start 2026-09-18 09:03:51 +02:00
vcoppe ed6264444f wasm first steps 2026-07-14 18:36:30 +02:00
699 changed files with 33158 additions and 26634 deletions

No files matched your search

-67
View File
@@ -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 }}
+7 -19
View File
@@ -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
-124
View File
@@ -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
View File
@@ -1,2 +1 @@
.DS_Store .DS_Store
target/
Generated
-5330
View File
File diff suppressed because it is too large. Load diff
-5
View File
@@ -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"
+7 -9
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
target/
pkg/
+794
View File
@@ -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
View File
@@ -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
View File
@@ -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
```
-505
View File
@@ -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>
+11
View File
@@ -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.
@@ -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.
+31
View File
@@ -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.
@@ -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.
@@ -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::*;
@@ -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();
@@ -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();
@@ -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;
@@ -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();
@@ -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();
@@ -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();
@@ -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::*;
@@ -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))
} }
@@ -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));
}
} }
@@ -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))
@@ -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> =
@@ -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),
@@ -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()
} }
@@ -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![];
@@ -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(
@@ -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
@@ -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]
@@ -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);
} }
} }
@@ -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);
@@ -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)
); );
} }
@@ -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 {
@@ -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();
@@ -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();
@@ -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;
@@ -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);
@@ -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);
@@ -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)
} }
@@ -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();
@@ -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);
} }
@@ -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