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
698 changed files with 32955 additions and 26431 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:
node-version: 24
cache: npm
cache-dependency-path: website/package-lock.json
cache-dependency-path: |
gpx/package-lock.json
website/package-lock.json
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: Install dependencies for gpx
run: npm install --prefix gpx
- name: Install wasm-pack
uses: taiki-e/install-action@v2
with:
tool: wasm-pack
- name: Test gpx-rs
run: cargo test
- name: Build wasm
run: wasm-pack build --target bundler
working-directory: wasm
- name: Build gpx
run: npm run build --prefix gpx
- name: Install dependencies for website
run: npm install --prefix website
- name: Test website
run: npm test --prefix website
- name: Create env file
run: |
touch website/.env
-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
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
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`),
- `wasm`: the engine for the browser, compiled to WebAssembly,
- `tauri`: the desktop app, which runs the engine natively,
- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application. It also is the UI of the desktop app.
- `gpx-rs`: a Rust library, compiled to WebAssembly, for parsing and manipulating GPX files,
- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application.
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
cd wasm
wasm-pack build --target bundler
cd gpx-rs
wasm-pack build wasm --target bundler --out-dir ../pkg
```
### 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]
name = "gpx-engine"
version = "0.1.0"
edition = "2024"
[workspace]
members = ["engine", "wasm"]
resolver = "3"
[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"] }
[profile.release]
debug = true
+11 -7
View File
@@ -1,10 +1,14 @@
# gpx-rs
The GPX engine: plain Rust, no knowledge of who uses it. Tested natively with `cargo test` (from
here or from the root).
- `engine`: the GPX engine, plain Rust. Tested natively: `cargo test`.
- `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
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.
```
wasm-pack build wasm --target bundler --out-dir ../pkg
```
The interface is tested in Node (it needs `wasm-pack`, which fetches the test runner the first time):
```
wasm-pack test --node wasm
```
-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
//! 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.
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.
pub fn reverse_tracks(tracks: &mut [Track], original_next: Option<i64>, new_previous: Option<i64>) {
tracks.reverse();
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+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 common;
mod file;
mod route;
mod segment;
mod track;
mod trackpoint;
@@ -15,7 +14,6 @@ pub use chunk::*;
pub use chunked::*;
pub use common::*;
pub use file::*;
pub use route::*;
pub use segment::*;
pub use track::*;
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::{
Author, Chunk, File, Link, LngLat, Route, Track, TrackSegment, Trackpoint,
TrackpointCategories, TrackpointChunk, Waypoint, WaypointChunk, distance,
Author, Chunk, File, Link, LngLat, Track, TrackSegment, Trackpoint, TrackpointCategories,
TrackpointChunk, Waypoint, WaypointChunk, distance,
};
use chrono::DateTime;
use quick_xml::Error;
@@ -18,7 +18,6 @@ enum GPXElement {
Author(Author),
Link(Link),
Text,
/// A track, or a route, whose information is read the same way.
Track(Track),
Segment(TrackSegment),
Trackpoint(Trackpoint),
@@ -397,7 +396,7 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
let mut stack: Vec<GPXElement> = vec![];
let mut trkpt_chunk = TrackpointChunk::default();
let mut wpt_chunk = WaypointChunk::default();
// 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.
let mut route = RouteReader::default();
let mut rpt_points: Vec<Trackpoint> = vec![];
@@ -531,12 +530,12 @@ pub fn parse(data: &[u8], categories: &mut TrackpointCategories) -> Result<File,
}
}
"rte" => {
if let Some(GPXElement::Track(trk)) = stack.pop() {
gpx.rte.push(Route {
info: trk.info,
seg: std::mem::take(&mut route).finish(),
..Default::default()
});
if let Some(GPXElement::Track(mut trk)) = stack.pop() {
let trkseg = std::mem::take(&mut route).finish();
if !trkseg.is_empty() {
trk.trkseg.push(trkseg);
}
gpx.trk.push(trk);
}
}
"rtept" => {
@@ -741,37 +740,38 @@ mod tests {
}
#[test]
fn test_parse_routes() {
fn test_parse_routes_as_tracks() {
let gpx = parse_data("with_routes");
assert!(gpx.trk.is_empty());
assert_eq!(gpx.rte.len(), 2);
let rte = &gpx.rte[0];
assert_eq!(rte.info.name.as_deref(), Some("route 1"));
assert_eq!(rte.info.type_.as_deref(), Some("Cycling"));
assert_eq!(rte.seg.len(), 49);
let trkpt = &rte.seg[0];
assert_eq!(gpx.trk.len(), 2);
let trk = &gpx.trk[0];
assert_eq!(trk.info.name.as_deref(), Some("route 1"));
assert_eq!(trk.info.type_.as_deref(), Some("Cycling"));
assert_eq!(trk.trkseg.len(), 1);
let trkseg = &trk.trkseg[0];
assert_eq!(trkseg.len(), 49);
let trkpt = &trkseg[0];
assert_eq!(trkpt.coordinates.lat, 50.790867);
assert_eq!(trkpt.coordinates.lng, 4.404968);
assert_eq!(trkpt.ele, 109.0);
let rte = &gpx.rte[1];
assert_eq!(rte.info.name.as_deref(), Some("route 2"));
assert_eq!(rte.seg.len(), 28);
assert_ne!(gpx.rte[0].id, rte.id);
let trk = &gpx.trk[1];
assert_eq!(trk.info.name.as_deref(), Some("route 2"));
assert_eq!(trk.trkseg[0].len(), 28);
}
#[test]
fn test_parse_route_point_extensions() {
let gpx = parse_data("with_route_extensions");
// a route without points has an empty segment
assert_eq!(gpx.rte.len(), 2);
assert!(gpx.rte[1].seg.is_empty());
// a route without points has no segment
assert_eq!(gpx.trk.len(), 2);
assert!(gpx.trk[1].trkseg.is_empty());
let rte = &gpx.rte[0];
assert_eq!(rte.info.name.as_deref(), Some("with extensions"));
let points: Vec<_> = rte.seg.iter().collect();
let trk = &gpx.trk[0];
assert_eq!(trk.info.name.as_deref(), Some("with extensions"));
assert_eq!(trk.trkseg.len(), 1);
let points: Vec<_> = trk.trkseg[0].iter().collect();
// the route points are kept, the points of the detailed path follow the one they come with
let coordinates: Vec<_> = points
.iter()
@@ -788,13 +788,14 @@ mod tests {
#[test]
fn test_route_points_are_the_anchors_and_nothing_else_is() {
let gpx = parse_data("with_route_extensions");
let anchors: Vec<_> = gpx.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)]);
// no anchor is computed for the points of a route that has none in between
let gpx = parse_data("with_routes");
for rte in &gpx.rte {
assert!(rte.seg.iter().all(|p| p.anchor == Some(0)));
for trk in &gpx.trk {
let segment = &trk.trkseg[0];
assert!(segment.iter().all(|p| p.anchor == Some(0)));
}
// while a track gets the anchors that its shape needs
let track = parse_data("with_tracks");
@@ -806,14 +807,14 @@ mod tests {
#[test]
fn test_parse_the_points_of_the_detailed_path_of_a_route() {
let gpx = parse_data("with_route_points");
assert_eq!(gpx.rte.len(), 1);
let rte = &gpx.rte[0];
assert_eq!(rte.info.name.as_deref(), Some("Lausanne"));
assert_eq!(rte.info.cmt.as_deref(), Some("comment"));
assert_eq!(rte.info.desc.as_deref(), Some("description"));
assert_eq!(rte.info.type_.as_deref(), Some("Cycling"));
assert_eq!(gpx.trk.len(), 1);
let trk = &gpx.trk[0];
assert_eq!(trk.info.name.as_deref(), Some("Lausanne"));
assert_eq!(trk.info.cmt.as_deref(), Some("comment"));
assert_eq!(trk.info.desc.as_deref(), Some("description"));
assert_eq!(trk.info.type_.as_deref(), Some("Cycling"));
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
assert_eq!(points.len(), 7);
let kinds: Vec<_> = points.iter().map(|p| p.anchor.is_some()).collect();
@@ -845,7 +846,7 @@ mod tests {
</rtept>
</rte></gpx>"#;
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();
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
@@ -860,7 +861,7 @@ mod tests {
<gpxx:RoutePointExtension><gpxx:rpt lat="1.5" lon="1.5"/></gpxx:RoutePointExtension>
</extensions></rtept></rte></gpx>"#;
let gpx = parse(data.as_bytes(), &mut Default::default()).unwrap();
let anchors: Vec<_> = gpx.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)]);
}
@@ -929,14 +930,14 @@ mod tests {
assert_eq!(waypoints[0].links[2].text.as_deref(), Some("three"));
assert!(waypoints[1].links.is_empty());
// tracks, and routes
assert_eq!((gpx.trk.len(), gpx.rte.len()), (1, 1));
// tracks, and routes which are read as tracks
assert_eq!(gpx.trk.len(), 2);
assert_eq!(
hrefs(&gpx.trk[0].info.links),
["https://example.com/trk-1", "https://example.com/trk-2"]
);
assert_eq!(
hrefs(&gpx.rte[0].info.links),
hrefs(&gpx.trk[1].info.links),
["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));
}
#[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]
fn test_parse_trackpoint_atemp() {
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.
/// The same type tells which data a file has, see [`File::exportable_data`].
///
/// With `as_route`, the file is written with routes instead of tracks, see [`write`]. It is not
/// data: it is off in `ALL` and `NONE`, and in what [`File::exportable_data`] gives.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExportOptions {
pub time: bool,
@@ -17,6 +20,8 @@ pub struct ExportOptions {
pub power: bool,
/// The OpenStreetMap data: surface, highway, SAC scale and MTB scale.
pub osm: bool,
/// Writes every segment as a route (`rte`) instead of a track.
pub as_route: bool,
}
impl ExportOptions {
@@ -31,6 +36,7 @@ impl ExportOptions {
atemp: value,
power: value,
osm: value,
as_route: false,
}
}
@@ -43,6 +49,7 @@ impl ExportOptions {
atemp: self.atemp || other.atemp,
power: self.power || other.power,
osm: self.osm || other.osm,
as_route: self.as_route || other.as_route,
}
}
}
@@ -52,7 +59,12 @@ impl File {
/// options of [`write`] can leave out.
pub fn exportable_data(&self) -> ExportOptions {
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.hr |= point.hr.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";
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
/// the names of their codes in `categories`.
///
/// The routes are written as routes (`rte`): the anchors of their segment that are shown from the
/// zoom level [`ROUTE_POINT_MAX_ZOOM`] or a lower one are the `rtept`, and the trackpoints between
/// two of them are the `gpxx:rpt` of the `rtept` before them (which only have a position, unlike
/// the `rtept`). The first and last trackpoints always are `rtept`.
/// The segments are written as tracks, or with `options.as_route` as routes: a segment becomes a
/// `rte` that has the information of its track, its anchors are the `rtept` and the trackpoints
/// between two anchors are the `gpxx:rpt` of the `rtept` before them (which only have a
/// position, unlike the `rtept`). The first and last trackpoints always are `rtept`.
pub fn write(file: &File, categories: &TrackpointCategories, options: ExportOptions) -> Vec<u8> {
let mut writer = Writer::new_with_indent(Vec::new(), b' ', 4);
// writing to memory does not fail
@@ -92,9 +100,9 @@ pub fn write(file: &File, categories: &TrackpointCategories, options: ExportOpti
writer.into_inner()
}
fn write_file<'a>(
fn write_file(
w: &mut XmlWriter,
file: &'a File,
file: &File,
categories: &TrackpointCategories,
options: ExportOptions,
) -> io::Result<()> {
@@ -128,18 +136,20 @@ fn write_file<'a>(
for wpt in file.wpt.iter() {
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()),
for trk in &file.trk {
// a single unnamed track is named like the file
let name = match (&trk.info.name, file.trk.len()) {
(Some(name), _) if !name.is_empty() => Some(name.as_str()),
(_, 1) => Some(file.info.name.as_str()),
_ => None,
};
for rte in &file.rte {
write_route(w, &rte.info, &rte.seg, name(&rte.info), categories, options)?;
if options.as_route {
for segment in trk.trkseg.iter().filter(|segment| !segment.is_empty()) {
write_route(w, trk, segment, name, categories, options)?;
}
} else {
write_track(w, trk, name, categories, options)?;
}
for trk in &file.trk {
write_track(w, trk, name(&trk.info), categories, options)?;
}
Ok(())
})?;
@@ -301,28 +311,22 @@ fn write_track(
fn write_route(
w: &mut XmlWriter,
info: &crate::TrackInfo,
trk: &Track,
segment: &crate::TrackSegment,
name: Option<&str>,
categories: &TrackpointCategories,
options: ExportOptions,
) -> io::Result<()> {
w.create_element("rte").write_inner_content(|w| {
write_track_info(w, name, info)?;
write_track_info(w, name, &trk.info)?;
let points: Vec<&Trackpoint> = segment.iter().collect();
let last = points.len().saturating_sub(1);
let is_route_point = |i: usize| {
i == 0
|| i == last
|| points[i]
.anchor
.is_some_and(|zoom| zoom <= ROUTE_POINT_MAX_ZOOM)
};
let last = points.len() - 1;
let is_anchor = |i: usize| i == 0 || i == last || points[i].anchor.is_some();
let mut i = 0;
while i < points.len() {
// the trackpoints up to the next route point
// the trackpoints up to the next anchor
let mut next = i + 1;
while next < points.len() && !is_route_point(next) {
while next < points.len() && !is_anchor(next) {
next += 1;
}
write_point(
@@ -402,10 +406,9 @@ fn write_point(
Ok(())
})?;
}
// `PowerInWatts` is itself the root element of Garmin's power extension, without a
// wrapper
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() {
w.create_element("gpxx:RoutePointExtension")
@@ -452,7 +455,9 @@ mod tests {
}
fn points(file: &File) -> Vec<Trackpoint> {
file.segments()
file.trk
.iter()
.flat_map(|trk| &trk.trkseg)
.flat_map(|seg| seg.iter().cloned())
.collect()
}
@@ -512,7 +517,6 @@ mod tests {
"with_temp",
"with_power_1",
"with_power_2",
"with_power_3",
"with_surface",
"with_highway",
"with_style",
@@ -580,16 +584,6 @@ mod tests {
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]
fn test_exportable_data() {
let (file, _) = read("with_time");
@@ -601,20 +595,11 @@ mod tests {
assert!(file.exportable_data().power);
}
/// The file with each segment of its tracks as a route, which has the information of its
/// track.
fn as_routes(mut file: File) -> File {
file.rte = std::mem::take(&mut file.trk)
.into_iter()
.flat_map(|trk| {
trk.trkseg.into_iter().map(move |seg| crate::Route {
info: trk.info.clone(),
seg,
..Default::default()
})
})
.collect();
file
fn as_route() -> ExportOptions {
ExportOptions {
as_route: true,
..ExportOptions::ALL
}
}
fn count(text: &str, pattern: &str) -> usize {
@@ -622,17 +607,19 @@ mod tests {
}
#[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 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, "<trk>"), 0);
assert_eq!(count(&text, "<trkseg>"), 0);
assert_eq!(count(&text, "<trkpt"), 0);
// the anchors are the route points, the trackpoints between them the points of their path
assert_eq!(count(&text, "<rtept"), 3);
assert_eq!(count(&text, "<gpxx:rpt"), 4);
assert_eq!(count(&text, "<gpxx:RoutePointExtension>"), 2);
// a route has the information of its track
assert!(text.contains("<name>Lausanne</name>"));
assert!(text.contains("<cmt>comment</cmt>"));
assert!(text.contains("<desc>description</desc>"));
@@ -647,14 +634,14 @@ mod tests {
}
#[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 written = write(&file, &categories, ExportOptions::ALL);
let written = write(&file, &categories, as_route());
let again = parse(&written, &mut categories).unwrap();
assert_eq!(again.rte.len(), 1);
assert_eq!(again.rte[0].info, file.rte[0].info);
let (a, b) = (&file.rte[0].seg, &again.rte[0].seg);
assert_eq!(again.trk.len(), 1);
assert_eq!(again.trk[0].info, file.trk[0].info);
let (a, b) = (&file.trk[0].trkseg[0], &again.trk[0].trkseg[0]);
assert_eq!(a.len(), b.len());
for (a, b) in a.iter().zip(b.iter()) {
assert_eq!(a.coordinates.lat, b.coordinates.lat);
@@ -672,121 +659,85 @@ mod tests {
}
#[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 = as_routes(file);
assert!(file.rte.len() > 2);
let written = write(&file, &categories, ExportOptions::ALL);
let segments: usize = file.trk.iter().map(|t| t.trkseg.len()).sum();
assert!(segments > 2);
let written = write(&file, &categories, as_route());
let text = String::from_utf8(written.clone()).unwrap();
assert_eq!(count(&text, "<rte>"), segments);
assert_eq!(count(&text, "<trk>"), 0);
// every route starts and ends with a route point
assert_eq!(count(&text, "</rte>"), segments);
let again = parse(&written, &mut categories).unwrap();
assert_eq!(again.rte.len(), file.rte.len());
let anchors = |s: &crate::TrackSegment, max_zoom: u8| {
assert_eq!(again.trk.len(), segments);
let mut routes = again.trk.iter();
for track in &file.trk {
for segment in &track.trkseg {
let route = routes.next().unwrap();
// the information of the track, repeated for each of its segments
assert_eq!(route.info.name, track.info.name);
assert_eq!(route.info.type_, track.info.type_);
let (a, b) = (segment, &route.trkseg[0]);
assert_eq!(a.len(), b.len());
for (a, b) in a.iter().zip(b.iter()) {
assert_eq!(
(a.coordinates.lat, a.coordinates.lng),
(b.coordinates.lat, b.coordinates.lng)
);
}
// the anchors of the segment are the ones of its route
let anchors = |s: &crate::TrackSegment| {
s.iter()
.enumerate()
.filter(|(_, p)| p.anchor.is_some_and(|zoom| zoom <= max_zoom))
.filter(|(_, p)| p.anchor.is_some())
.map(|(i, _)| i)
.collect::<Vec<_>>()
};
for (a, b) in file.rte.iter().zip(&again.rte) {
assert_eq!(a.info.name, b.info.name);
assert_eq!(a.seg.len(), b.seg.len());
// the route points come back as anchors, the other anchors as points of the path
assert_eq!(
anchors(&a.seg, ROUTE_POINT_MAX_ZOOM),
anchors(&b.seg, u8::MAX)
);
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]
fn test_the_ends_of_a_route_are_route_points_even_without_anchors() {
let (file, categories) = read("simple");
let mut file = as_routes(file);
file.rte[0].seg.update_all(|_, point| point.anchor = None);
let len = file.rte[0].seg.len();
let text = written_text(&file, &categories, ExportOptions::ALL);
let (mut file, categories) = read("simple");
file.trk[0].trkseg[0].update_all(|_, point| point.anchor = None);
let len = file.trk[0].trkseg[0].len();
let text = written_text(&file, &categories, as_route());
assert_eq!(count(&text, "<rtept"), 2);
assert_eq!(count(&text, "<gpxx:rpt"), len - 2);
// and a single trackpoint is a route of one point
file.rte[0].seg.splice(1, len, vec![]);
let text = written_text(&file, &categories, ExportOptions::ALL);
file.trk[0].trkseg[0].splice(1, len, vec![]);
let text = written_text(&file, &categories, as_route());
assert_eq!(count(&text, "<rtept"), 1);
assert_eq!(count(&text, "<gpxx:rpt"), 0);
}
#[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 = as_routes(file);
let text = written_text(&file, &categories, ExportOptions::ALL);
let text = written_text(&file, &categories, as_route());
assert!(count(&text, "<gpx_style:line>") >= 1);
assert_eq!(count(&text, "<gpx_style:line>"), count(&text, "<rte>"));
let again = parse(text.as_bytes(), &mut Default::default()).unwrap();
assert_eq!(again.rte[0].info.color, file.rte[0].info.color);
assert_eq!(again.rte[0].info.width, file.rte[0].info.width);
}
#[test]
fn test_routes_are_written_as_routes_before_the_tracks() {
let (mut file, categories) = read("with_tracks");
let (routes, _) = read("with_routes");
file.rte = routes.rte.clone();
let text = written_text(&file, &categories, ExportOptions::ALL);
assert_eq!(count(&text, "<rte>"), routes.rte.len());
assert_eq!(count(&text, "<trk>"), file.trk.len());
assert!(text.rfind("</rte>").unwrap() < text.find("<trk>").unwrap());
let again = parse(text.as_bytes(), &mut categories.clone()).unwrap();
assert_eq!(again.rte.len(), file.rte.len());
assert_eq!(again.trk.len(), file.trk.len());
for (a, b) in file.rte.iter().zip(&again.rte) {
assert_eq!(a.info, b.info);
same_points(
&a.seg.iter().cloned().collect::<Vec<_>>(),
&b.seg.iter().cloned().collect::<Vec<_>>(),
);
}
same_points(&points(&file), &points(&again));
}
#[test]
fn test_an_empty_route_is_written_and_a_single_route_is_named_like_the_file() {
let mut file = File::default();
file.info.name = "file".into();
file.rte.push(crate::Route::default());
let text = written_text(&file, &Default::default(), ExportOptions::ALL);
assert_eq!(count(&text, "<rte>"), 1);
assert_eq!(count(&text, "<rtept"), 0);
let again = parse(text.as_bytes(), &mut Default::default()).unwrap();
assert_eq!(again.rte.len(), 1);
assert_eq!(again.rte[0].info.name.as_deref(), Some("file"));
// not when there are others
file.trk.push(Track::default());
let text = written_text(&file, &Default::default(), ExportOptions::ALL);
// the metadata only
assert_eq!(count(&text, "<name>file</name>"), 1);
assert_eq!(again.trk[0].info.color, file.trk[0].info.color);
assert_eq!(again.trk[0].info.width, file.trk[0].info.width);
}
#[test]
fn test_route_points_keep_their_data_and_the_options_apply_to_them() {
let (file, categories) = read("with_hr");
let file = as_routes(file);
let with_all = written_text(&file, &categories, ExportOptions::ALL);
let with_all = written_text(&file, &categories, as_route());
assert!(with_all.contains("<gpxtpx:hr>"));
let without = written_text(
&file,
@@ -794,10 +745,22 @@ mod tests {
ExportOptions {
hr: false,
time: false,
as_route: true,
..ExportOptions::ALL
},
);
assert!(!without.contains("<gpxtpx:hr>"));
assert!(!without.contains("<time>"));
}
#[test]
fn test_the_route_option_is_not_data() {
let (file, _) = read("with_route_points");
assert!(!file.exportable_data().as_route);
// it is a way to write the data, never part of them
let all = std::hint::black_box(ExportOptions::ALL);
assert!(!all.as_route && !ExportOptions::NONE.as_route);
assert!(as_route().union(ExportOptions::NONE).as_route);
assert!(!ExportOptions::ALL.union(ExportOptions::NONE).as_route);
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -36,11 +36,4 @@ pub enum Action<'a> {
file_ids: Vec<FileId>,
index: usize,
},
/// Names a file in all the states of the history, as if it had always been called so (see
/// [`crate::History::rename`]). Not undoable: for a file that is given a name from the outside,
/// like the name of the file it is written to.
Rename {
file_id: FileId,
name: String,
},
}
File renamed without changes.
@@ -1,6 +1,4 @@
use crate::{
Apply, CommandError, Edit, Editor, File, Route, State, Track, TrackInfo, update_selected,
};
use crate::{Apply, CommandError, Edit, Editor, File, State, Track, update_selected};
#[derive(Debug)]
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<'_> {
fn file(&mut self, file: &mut File) -> Edit {
file.info.name = self.name.to_owned();
file.info.desc = Some(self.desc.to_owned());
// a single route or track is named like the file
match (file.rte.as_mut_slice(), file.trk.as_mut_slice()) {
([route], []) => route.info.name = Some(self.name.to_owned()),
([], [track]) => track.info.name = Some(self.name.to_owned()),
_ => {}
// a single track is named like the file
if let [track] = file.trk.as_mut_slice() {
track.info.name = Some(self.name.to_owned());
}
Edit::Changed
}
fn 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)
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 {
name: "first",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
New {
name: "second",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap(); // now selected
@@ -124,7 +108,6 @@ mod tests {
New {
name: "file",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -1,13 +1,9 @@
mod convert;
mod metadata;
mod new_route;
mod new_track;
mod new_track_segment;
mod style;
pub use convert::*;
pub use metadata::*;
pub use new_route::*;
pub use new_track::*;
pub use new_track_segment::*;
pub use style::*;
@@ -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)]
pub struct NewTrack;
@@ -12,41 +12,20 @@ impl Apply for NewTrack {
impl Editor for NewTrack {
fn file(&mut self, file: &mut File) -> Edit {
file.trk.push(Track {
info: common_style(file),
..Default::default()
});
let mut track = Track::default();
track.info.color = common(file, |t| &t.info.color);
track.info.opacity = common(file, |t| &t.info.opacity);
track.info.width = common(file, |t| &t.info.width);
file.trk.push(track);
Edit::Changed
}
}
/// The style that all the routes and tracks of the file agree on: each attribute is set if they
/// all have the same value for it.
pub(super) fn common_style(file: &File) -> TrackInfo {
let infos: Vec<&TrackInfo> = file
.rte
.iter()
.map(|rte| &rte.info)
.chain(file.trk.iter().map(|trk| &trk.info))
.collect();
TrackInfo {
color: common(&infos, |info| &info.color),
opacity: common(&infos, |info| &info.opacity),
width: common(&infos, |info| &info.width),
..Default::default()
}
}
/// Returns the value of a style attribute if all the infos agree on it.
fn common<T: Clone + PartialEq>(
infos: &[&TrackInfo],
get: impl Fn(&TrackInfo) -> &Option<T>,
) -> Option<T> {
let mut infos = infos.iter();
let first = get(infos.next()?);
infos
.all(|info| get(info) == first)
.then(|| first.clone())?
/// Returns the value of a style attribute if all existing tracks agree on it.
fn common<T: Clone + PartialEq>(file: &File, get: impl Fn(&Track) -> &Option<T>) -> Option<T> {
let mut tracks = file.trk.iter();
let first = get(tracks.next()?);
tracks.all(|t| get(t) == first).then(|| first.clone())?
}
#[cfg(test)]
@@ -61,7 +40,6 @@ mod tests {
New {
name: "file",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -78,7 +56,6 @@ mod tests {
New {
name: "file",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -29,7 +29,6 @@ mod tests {
New {
name: "file",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.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)]
pub struct Style<'a> {
@@ -14,31 +14,21 @@ impl Apply for Style<'_> {
}
}
impl Style<'_> {
fn set(&self, info: &mut TrackInfo) -> Edit {
impl Editor for Style<'_> {
fn track(&mut self, track: &mut Track) -> Edit {
if let Some(color) = self.color {
info.color = Some(color.to_owned());
track.info.color = Some(color.to_owned());
}
if self.opacity.is_some() {
info.opacity = self.opacity;
track.info.opacity = self.opacity;
}
if self.width.is_some() {
info.width = self.width;
track.info.width = self.width;
}
Edit::Changed
}
}
impl Editor for Style<'_> {
fn route(&mut self, route: &mut Route) -> Edit {
self.set(&mut route.info)
}
fn track(&mut self, track: &mut Track) -> Edit {
self.set(&mut track.info)
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
@@ -3,7 +3,7 @@ use std::rc::Rc;
use crate::{Apply, CommandError, FileId, Files, Selection, State, Waypoint};
/// Deletes the selected elements. With `whole_files`, the files holding the selected elements
/// are deleted instead, even if only a route, a track or a waypoint is selected.
/// are deleted instead, even if only a track or a waypoint is selected.
#[derive(Debug)]
pub struct Delete {
pub whole_files: bool,
@@ -23,21 +23,6 @@ impl Apply for Delete {
state.order.0.retain(|id| !file_ids.contains(id));
Selection::Empty
}
Selection::Route { file_id, rte_ids } => {
let file = state
.files
.get_mut(file_id)
.ok_or(CommandError::NothingToDo)?;
let file = Rc::make_mut(file);
let len = file.rte.len();
file.rte.retain(|rte| !rte_ids.contains(&rte.id));
if file.rte.len() == len {
return Err(CommandError::NothingToDo);
}
Selection::File {
file_ids: [*file_id].into(),
}
}
Selection::Track { file_id, trk_ids } => {
let file = state
.files
@@ -146,7 +131,6 @@ mod tests {
crate::New {
name: "keep",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -281,7 +265,6 @@ mod tests {
crate::New {
name: "keep",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -31,14 +31,12 @@ mod tests {
crate::New {
name: "a",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
crate::New {
name: "b",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -1,8 +1,8 @@
use std::{collections::HashSet, rc::Rc};
use crate::{
Apply, CommandError, FileId, Files, Selection, State, Waypoint, copy_file, copy_route,
copy_segment, copy_track, copy_waypoint,
Apply, CommandError, FileId, Files, Selection, State, Waypoint, copy_file, copy_segment,
copy_track, copy_waypoint,
};
#[derive(Debug)]
@@ -35,30 +35,6 @@ impl Apply for Duplicate {
state.order.0.splice(at..at, order);
Selection::File { file_ids: copies }
}
Selection::Route { file_id, rte_ids } => {
let file = state
.files
.get_mut(file_id)
.ok_or(CommandError::NothingToDo)?;
let file = Rc::make_mut(file);
let mut copies = HashSet::new();
append_copies(
&mut file.rte,
|rte| rte_ids.contains(&rte.id),
|rte| {
let copy = copy_route(rte);
copies.insert(copy.id);
copy
},
);
if copies.is_empty() {
return Err(CommandError::NothingToDo);
}
Selection::Route {
file_id: *file_id,
rte_ids: copies,
}
}
Selection::Track { file_id, trk_ids } => {
let file = state
.files
@@ -217,7 +193,6 @@ mod tests {
crate::New {
name,
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -9,7 +9,7 @@ pub struct Load<'a> {
}
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)
.map_err(|err| CommandError::InvalidData(err.to_string()))?;
if file.info.name.trim().is_empty() {
@@ -85,7 +85,6 @@ mod tests {
crate::New {
name: "keep",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -141,7 +140,6 @@ mod tests {
crate::New {
name: "before",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -191,7 +189,6 @@ mod tests {
crate::New {
name: "keep",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -6,7 +6,6 @@ mod load;
mod move_elements;
mod new;
mod paste;
mod sync;
mod transfer;
pub use delete::*;
@@ -17,5 +16,4 @@ pub use load::*;
pub use move_elements::*;
pub use new::*;
pub use paste::*;
pub use sync::*;
pub use transfer::*;
@@ -9,8 +9,6 @@ use crate::{
pub enum MoveTarget {
/// The list of files.
Files { index: usize },
/// The routes of a file.
Routes { file_id: FileId, index: usize },
/// The tracks of a file.
Tracks { file_id: FileId, index: usize },
/// The segments of a track.
@@ -28,10 +26,9 @@ pub enum MoveTarget {
///
/// What can go where:
/// - files: among the files;
/// - routes and tracks: among the files (each one becomes a file), or among the routes or the
/// tracks of a file, where they keep their kind (see [`Destination`]);
/// - segments: among the files (each segment becomes a file), among the routes or the tracks of a
/// file (each segment becomes a route or a track), or among the segments of a track;
/// - tracks: among the files (each track becomes a file), or among the tracks of a file;
/// - segments: among the files (each segment becomes a file), among the tracks of a file (each
/// segment becomes a track), or among the segments of a track;
/// - waypoints, or all the waypoints of a file (the waypoints node): among the waypoints of a
/// file.
#[derive(Debug)]
@@ -44,9 +41,6 @@ impl From<MoveTarget> for Destination {
fn from(target: MoveTarget) -> Self {
match target {
MoveTarget::Files { index } => Destination::Files(Place::Index(index)),
MoveTarget::Routes { file_id, index } => {
Destination::Routes(file_id, Place::Index(index))
}
MoveTarget::Tracks { file_id, index } => {
Destination::Tracks(file_id, Place::Index(index))
}
@@ -1,16 +1,12 @@
use crate::{
Apply, CommandError, File, LngLat, Route, State, Track, TrackSegment, Trackpoint, produce,
};
use crate::{Apply, CommandError, File, LngLat, State, Track, TrackSegment, Trackpoint, produce};
/// Creates an empty file and selects it. With `route`, the file starts with a route, which is
/// empty. With a `trackpoint`, the file starts with a segment that holds it, the one of its route
/// with `route` or of a track otherwise: it is how the routing tool starts a route on an empty map,
/// in a single undo step.
/// Creates an empty file and selects it. With a `trackpoint`, the file starts with a track and a
/// segment that hold it: it is how the routing tool starts a route on an empty map, in a single
/// undo step.
#[derive(Debug)]
pub struct New<'a> {
pub name: &'a str,
pub trackpoint: Option<NewTrackpoint>,
pub route: bool,
}
#[derive(Debug, Clone, Copy)]
@@ -25,8 +21,8 @@ impl Apply for New<'_> {
produce(state, |_| {
let mut file = File::default();
file.info.name = self.name.to_owned();
let mut segment = TrackSegment::default();
if let Some(point) = self.trackpoint {
let mut segment = TrackSegment::default();
// the ends of a segment are anchors of the routing tool
segment.splice(
0,
@@ -40,13 +36,6 @@ impl Apply for New<'_> {
..Default::default()
}],
);
}
if self.route {
file.rte.push(Route {
seg: segment,
..Default::default()
});
} else if self.trackpoint.is_some() {
file.trk.push(Track {
trkseg: vec![segment],
..Default::default()
@@ -70,8 +59,7 @@ mod tests {
assert!(
New {
name: "new",
trackpoint: None,
route: false,
trackpoint: None
}
.apply(&mut fx.state())
.is_ok()
@@ -95,7 +83,6 @@ mod tests {
New {
name: "new",
trackpoint: Some(point),
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -111,38 +98,4 @@ mod tests {
assert_eq!(segment[0].anchor, Some(0));
assert_eq!(fx.selected_files(), [file.id].into());
}
#[test]
fn test_new_with_a_route() {
let mut fx = Fixture::default();
New {
name: "new",
trackpoint: None,
route: true,
}
.apply(&mut fx.state())
.unwrap();
let file = fx.files.values().next().unwrap();
assert_eq!((file.rte.len(), file.trk.len()), (1, 0));
assert!(file.rte[0].seg.is_empty());
assert_eq!(fx.selected_files(), [file.id].into());
// the trackpoint goes to the route
let mut fx = Fixture::default();
New {
name: "new",
trackpoint: Some(NewTrackpoint {
lng: 1.0,
lat: 2.0,
ele: 3.0,
}),
route: true,
}
.apply(&mut fx.state())
.unwrap();
let file = fx.files.values().next().unwrap();
assert_eq!((file.rte.len(), file.trk.len()), (1, 0));
assert_eq!(file.rte[0].seg.len(), 1);
assert_eq!(file.rte[0].seg[0].anchor, Some(0));
}
}
@@ -1,21 +1,18 @@
use std::{collections::HashSet, hash::Hash};
use crate::{
Apply, ClipboardContent, CommandError, Destination, FileId, Place, RouteId, Selection, State,
TrackId, TrackSegmentId, WaypointId, transfer, waypoint_ids,
Apply, ClipboardContent, CommandError, Destination, FileId, Place, Selection, State, TrackId,
TrackSegmentId, WaypointId, transfer, waypoint_ids,
};
/// Pastes what was copied or cut (see [`crate::Clipboard`]).
///
/// Where it goes depends on the selection, see [`crate::Clipboard::can_paste`]:
/// - nothing selected: the files are copied, and routes, tracks and segments become new files
/// (named like their route or track, or like the file they come from);
/// - a file: the routes and tracks are added to it, segments become new tracks in it, and the
/// waypoints are added to its own;
/// - a route: routes, and segments as new routes, go after it in its file, tracks go before the
/// other tracks (right after the routes);
/// - a track: the segments are added to it, tracks go after it in its file, and routes after the
/// other routes;
/// - nothing selected: the files are copied, and tracks and segments become new files (named
/// like their track, or like the file they come from);
/// - a file: the tracks are added to it, segments become new tracks in it, and the waypoints are
/// added to its own;
/// - a track: the segments are added to it, tracks go after it in its file;
/// - a segment: the segments go after it in its track;
/// - the waypoints or a waypoint: the waypoints are added to their file, after the waypoint.
///
@@ -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> {
state
.files
@@ -90,34 +79,14 @@ fn destination(state: &State, content: &ClipboardContent) -> Result<Destination,
};
Ok(match (content, &*state.selection) {
(ClipboardContent::Files(_), _) => Destination::Files(Place::End),
(
ClipboardContent::Routes(_)
| ClipboardContent::Tracks(_)
| ClipboardContent::Segments(_),
Selection::Empty,
) => Destination::Files(Place::End),
(ClipboardContent::Routes(_), Selection::File { file_ids }) => {
Destination::Routes(last_file(file_ids)?, Place::End)
}
// routes and tracks keep their kind (see `Destination`)
(
ClipboardContent::Routes(_)
| ClipboardContent::Tracks(_)
| ClipboardContent::Segments(_),
Selection::Route { file_id, rte_ids },
) => {
let last =
last_selected(route_ids_of(state, *file_id).into_iter(), rte_ids).ok_or(nothing)?;
Destination::Routes(*file_id, Place::After(last))
(ClipboardContent::Tracks(_) | ClipboardContent::Segments(_), Selection::Empty) => {
Destination::Files(Place::End)
}
(
ClipboardContent::Tracks(_) | ClipboardContent::Segments(_),
Selection::File { file_ids },
) => Destination::Tracks(last_file(file_ids)?, Place::End),
(
ClipboardContent::Routes(_) | ClipboardContent::Tracks(_),
Selection::Track { file_id, trk_ids },
) => {
(ClipboardContent::Tracks(_), Selection::Track { file_id, trk_ids }) => {
let last =
last_selected(track_ids_of(state, *file_id).into_iter(), trk_ids).ok_or(nothing)?;
Destination::Tracks(*file_id, Place::After(last))
@@ -5,9 +5,9 @@
use std::{collections::HashSet, rc::Rc};
use crate::{
ClipboardContent, ClipboardRoute, ClipboardSegment, ClipboardTrack, CommandError, File, FileId,
Route, RouteId, Selection, State, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint,
WaypointId, copy_file, copy_route, copy_segment, copy_track, copy_waypoint,
ClipboardContent, ClipboardSegment, ClipboardTrack, CommandError, File, FileId, Selection,
State, Track, TrackId, TrackSegment, TrackSegmentId, Waypoint, WaypointId, copy_file,
copy_segment, copy_track, copy_waypoint,
};
/// 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:
/// - files: among the files;
/// - routes: among the files (each route becomes a file, named like the route, or like the file
/// it comes from), or among the routes of a file. Among the tracks of a file, they stay routes,
/// which come before the tracks: they go after the other routes;
/// - tracks: among the files (each track becomes a file, named like the track, or like the file
/// it comes from), or among the tracks of a file. Among the routes of a file, they stay tracks,
/// which come after the routes: they go before the other tracks;
/// it comes from), or among the tracks of a file;
/// - segments: among the files (each segment becomes a file with a track, named like its track,
/// or like its file), among the routes or the tracks of a file (each segment becomes a route or
/// a track), or among the segments of a track;
/// or like its file), among the tracks of a file (each segment becomes a track), or among the
/// segments of a track;
/// - waypoints: among the waypoints of a file.
#[derive(Debug, Clone, Copy)]
pub enum Destination {
Files(Place<FileId>),
Routes(FileId, Place<RouteId>),
Tracks(FileId, Place<TrackId>),
Segments(FileId, TrackId, Place<TrackSegmentId>),
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.order.0.retain(|id| !ids.contains(id));
}
ClipboardContent::Routes(routes) => {
let ids: HashSet<RouteId> = routes.iter().map(|rte| rte.route.id).collect();
for file in state.files.values_mut() {
if file.rte.iter().any(|rte| ids.contains(&rte.id)) {
Rc::make_mut(file).rte.retain(|rte| !ids.contains(&rte.id));
}
}
}
ClipboardContent::Tracks(tracks) => {
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.track.id).collect();
for file in state.files.values_mut() {
@@ -154,9 +141,7 @@ pub fn transfer(
// the destination has to be there, before anything is changed
let exists = match to {
Destination::Files(_) => true,
Destination::Routes(file_id, _) | Destination::Tracks(file_id, _) => {
state.files.contains_key(&file_id)
}
Destination::Tracks(file_id, _) => state.files.contains_key(&file_id),
Destination::Segments(file_id, trk_id, _) => state
.files
.get(&file_id)
@@ -167,21 +152,17 @@ pub fn transfer(
(content, to),
(ClipboardContent::Files(_), Destination::Files(_))
| (
ClipboardContent::Routes(_) | ClipboardContent::Tracks(_),
Destination::Files(_) | Destination::Routes(..) | Destination::Tracks(..)
ClipboardContent::Tracks(_),
Destination::Files(_) | Destination::Tracks(..)
)
| (
ClipboardContent::Segments(_),
Destination::Files(_)
| Destination::Routes(..)
| Destination::Tracks(..)
| Destination::Segments(..),
Destination::Files(_) | Destination::Tracks(..) | Destination::Segments(..),
)
| (ClipboardContent::Waypoints(_), Destination::Waypoints(..))
);
let empty = match content {
ClipboardContent::Files(files) => files.is_empty(),
ClipboardContent::Routes(routes) => routes.is_empty(),
ClipboardContent::Tracks(tracks) => tracks.is_empty(),
ClipboardContent::Segments(segments) => segments.is_empty(),
ClipboardContent::Waypoints(waypoints) => waypoints.is_empty(),
@@ -208,7 +189,6 @@ pub fn transfer(
.collect();
Ok(add_files(state, files, place))
}
(ClipboardContent::Routes(routes), to) => transfer_routes(state, routes, copy, to),
(ClipboardContent::Tracks(tracks), to) => transfer_tracks(state, tracks, copy, to),
(ClipboardContent::Segments(segments), to) => transfer_segments(state, segments, copy, to),
(ClipboardContent::Waypoints(waypoints), Destination::Waypoints(file_id, place)) => {
@@ -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(
state: &mut State,
tracks: &[ClipboardTrack],
@@ -334,12 +231,16 @@ fn transfer_tracks(
Ok(add_files(state, files, place))
}
Destination::Tracks(file_id, place) => {
let tracks = tracks.into_iter().map(|(track, _)| track).collect();
insert_tracks(state, file_id, place, tracks)
}
Destination::Routes(file_id, _) => {
let tracks = tracks.into_iter().map(|(track, _)| track).collect();
insert_tracks(state, file_id, Place::Index(0), tracks)
let tracks: Vec<Track> = tracks.into_iter().map(|(track, _)| track).collect();
let ids: HashSet<TrackId> = tracks.iter().map(|trk| trk.id).collect();
let file = file_mut(state, file_id)?;
let existing: Vec<TrackId> = file.trk.iter().map(|trk| trk.id).collect();
let at = place.position(&existing);
file.trk.splice(at..at, tracks);
Ok(Selection::Track {
file_id,
trk_ids: ids,
})
}
_ => Err(CommandError::NothingToDo),
}
@@ -375,22 +276,20 @@ fn transfer_segments(
.collect();
Ok(add_files(state, files, place))
}
Destination::Routes(file_id, place) => {
let routes = segments
.into_iter()
.map(|(seg, _)| Route {
seg,
..Default::default()
})
.collect();
insert_routes(state, file_id, place, routes)
}
Destination::Tracks(file_id, place) => {
let tracks = segments
let tracks: Vec<Track> = segments
.into_iter()
.map(|(segment, _)| own_track(segment))
.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) => {
let segments: Vec<TrackSegment> =
@@ -4,8 +4,6 @@ mod file;
#[cfg(test)]
pub(crate) mod fixture;
mod pattern;
#[cfg(test)]
mod routes_tests;
mod tools;
pub use apply::*;
@@ -26,22 +24,19 @@ pub enum Command<'a> {
New(New<'a>),
Load(Load<'a>),
LoadFiles(LoadFiles<'a>),
SyncFiles(SyncFiles<'a>),
Duplicate(Duplicate),
Delete(Delete),
Paste(Paste),
Move(Move),
DeleteAll(DeleteAll),
DeleteWaypoint(DeleteWaypoint),
Convert(Convert),
Metadata(Metadata<'a>),
Style(Style<'a>),
NewRoute(NewRoute),
NewTrack(NewTrack),
NewTrackSegment(NewTrackSegment),
Reverse(Reverse),
RoundTrip(RoundTrip),
Reroute(Reroute<'a>),
Route(Route<'a>),
NewWaypoint(NewWaypoint<'a>),
EditWaypoint(EditWaypoint<'a>),
MoveWaypoint(MoveWaypoint),
@@ -63,22 +58,19 @@ impl Apply for Command<'_> {
Command::New(c) => c.apply(state),
Command::Load(c) => c.apply(state),
Command::LoadFiles(c) => c.apply(state),
Command::SyncFiles(c) => c.apply(state),
Command::Duplicate(c) => c.apply(state),
Command::Delete(c) => c.apply(state),
Command::Paste(c) => c.apply(state),
Command::Move(c) => c.apply(state),
Command::DeleteAll(c) => c.apply(state),
Command::DeleteWaypoint(c) => c.apply(state),
Command::Convert(c) => c.apply(state),
Command::Metadata(c) => c.apply(state),
Command::Style(c) => c.apply(state),
Command::NewRoute(c) => c.apply(state),
Command::NewTrack(c) => c.apply(state),
Command::NewTrackSegment(c) => c.apply(state),
Command::Reverse(c) => c.apply(state),
Command::RoundTrip(c) => c.apply(state),
Command::Reroute(c) => c.apply(state),
Command::Route(c) => c.apply(state),
Command::NewWaypoint(c) => c.apply(state),
Command::EditWaypoint(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.
@@ -18,20 +18,10 @@ pub fn copy_track(track: &Track) -> Track {
}
}
/// A copy of the route and of its segment, with new ids.
pub fn copy_route(route: &Route) -> Route {
Route {
id: Default::default(),
seg: copy_segment(&route.seg),
..route.clone()
}
}
/// A copy of the file and of its routes, tracks and segments, with new ids.
/// A copy of the file and of its tracks and segments, with new ids.
pub fn copy_file(file: &File) -> File {
File {
id: Default::default(),
rte: file.rte.iter().map(copy_route).collect(),
trk: file.trk.iter().map(copy_track).collect(),
..file.clone()
}
@@ -18,7 +18,7 @@ pub fn update_segments(
let file = files
.entry(location.file_id)
.or_insert_with(|| (*state.files[&location.file_id]).clone());
f(i, file.segment_mut(location.index));
f(i, &mut file.trk[location.trk].trkseg[location.seg]);
}
for (file_id, file) in files {
state.files.insert(file_id, Rc::new(file));
@@ -31,8 +31,6 @@ mod tests {
use crate::engine::command::fixture::Fixture;
use crate::SegmentIndex;
use super::*;
fn fixture() -> (Fixture, FileId) {
@@ -51,11 +49,13 @@ mod tests {
let locations = [
SegmentLocation {
file_id: id,
index: SegmentIndex::Track { trk: 1, seg: 0 },
trk: 1,
seg: 0,
},
SegmentLocation {
file_id: id,
index: SegmentIndex::Track { trk: 0, seg: 0 },
trk: 0,
seg: 0,
},
];
let mut calls = vec![];
@@ -1,6 +1,6 @@
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.
#[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.
///
/// A selected file, route, track or segment is passed to the hook of its own level. By default
/// a file is forwarded to its routes and tracks, and a route or a track to its segments, so an
/// editor only overrides the level(s) it cares about (metadata: file, route and track, style:
/// route and track, reverse: segment). Waypoints are only edited when selected, never through
/// their file.
/// A selected file, track or segment is passed to the hook of its own level. By default a
/// file is forwarded to its tracks and a track to its segments, so an editor only overrides
/// the level(s) it cares about (metadata: file and track, style: track, reverse: segment).
/// Waypoints are only edited when selected, never through their file.
pub trait Editor {
fn file(&mut self, file: &mut File) -> Edit {
let routes = edit_each(&mut file.rte, |rte| self.route(rte));
routes.merge(edit_each(&mut file.trk, |trk| self.track(trk)))
}
fn route(&mut self, route: &mut Route) -> Edit {
edit_segment(self, &mut route.seg)
edit_each(&mut file.trk, |trk| self.track(trk))
}
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) {
match &*state.selection {
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));
}
}
Selection::Route { file_id, rte_ids } => {
update_file(state.files, *file_id, |file| {
edit_where(
&mut file.rte,
|rte| rte_ids.contains(&rte.id),
|rte| editor.route(rte),
)
});
}
Selection::Track { file_id, trk_ids } => {
update_file(state.files, *file_id, |file| {
edit_where(
@@ -24,7 +24,7 @@ impl Apply for ChangeLoopStart {
let index = self.index as usize;
let target = find_target(state, index, index)?.ok_or(CommandError::NothingToDo)?;
let mut file = (*state.files[&target.location.file_id]).clone();
let segment = 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());
if start == 0 || start >= len {
// 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,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CleanType {
Inside,
Outside,
}
/// Removes the trackpoints and/or waypoints of the selection lying inside (or outside) a
/// rectangle. A selected file cleans its routes, its tracks and its waypoints, a selected route,
/// track or segment only trackpoints, selected waypoints only waypoints.
/// rectangle. A selected file cleans its tracks and its waypoints, a selected track or segment
/// only trackpoints, selected waypoints only waypoints.
#[derive(Debug)]
pub struct Clean {
pub bounds: LngLatBounds,
@@ -85,11 +84,6 @@ struct Cleaner<'a> {
impl Editor for Cleaner<'_> {
fn file(&mut self, file: &mut File) -> Edit {
let mut edit = Edit::Unchanged;
for rte in &mut file.rte {
if self.route(rte) == Edit::Changed {
edit = Edit::Changed;
}
}
for trk in &mut file.trk {
if self.track(trk) == Edit::Changed {
edit = Edit::Changed;
@@ -129,28 +123,37 @@ impl Editor for Cleaner<'_> {
}
}
/// Removes what the clean left empty: the emptied segments, the routes of these segments and the
/// tracks that lost all their segments. A file is never removed, even if nothing is left in it: it stays, empty. The
/// selection falls back to the closest element still existing.
/// Removes what the clean left empty: the emptied segments, the tracks that lost all their
/// segments and the files that lost something and have nothing left. The selection falls back
/// to the closest element still existing.
fn prune(state: &mut State, emptied: &HashSet<TrackSegmentId>, wpt_files: &HashSet<FileId>) {
let ids: Vec<FileId> = state
.files
.iter()
.filter(|(id, file)| {
wpt_files.contains(id) || file.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)
.collect();
for id in ids {
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| {
let len = trk.trkseg.len();
trk.trkseg.retain(|seg| !emptied.contains(&seg.id));
trk.trkseg.len() == len || !trk.trkseg.is_empty()
});
if lost_something && file.trk.is_empty() && file.wpt.is_empty() {
state.files.remove(&id);
state.order.0.retain(|f| *f != id);
} else {
state.files.insert(id, Rc::new(file));
}
}
*state.selection = fixed_selection(state);
}
@@ -169,27 +172,6 @@ fn fixed_selection(state: &State) -> Selection {
Selection::File { file_ids }
}
}
Selection::Route { file_id, rte_ids } => {
let Some(file) = files.get(file_id) else {
return Selection::Empty;
};
let rte_ids: HashSet<_> = file
.rte
.iter()
.map(|rte| rte.id)
.filter(|id| rte_ids.contains(id))
.collect();
if rte_ids.is_empty() {
Selection::File {
file_ids: [*file_id].into(),
}
} else {
Selection::Route {
file_id: *file_id,
rte_ids,
}
}
}
Selection::Track { file_id, trk_ids } => {
let Some(file) = files.get(file_id) else {
return Selection::Empty;
@@ -314,16 +296,10 @@ mod tests {
.apply(&mut fx.state())
.unwrap();
assert_eq!(count(&fx, id), 0);
// nothing is left in the file, which stays and is still selected
let file = &fx.files[&id];
assert!(file.trk.is_empty() && file.wpt.is_empty());
assert_eq!(fx.order.0, [id]);
assert_eq!(
fx.selection,
Selection::File {
file_ids: HashSet::from([id])
}
);
// nothing is left of the file
assert!(!fx.files.contains_key(&id));
assert!(fx.order.0.is_empty());
assert_eq!(fx.selection, Selection::Empty);
}
#[test]
@@ -11,9 +11,9 @@ use crate::{
/// Keeps the trackpoints `start` to `end` (both included) of the selection, which are counted
/// over its segments one after the other (as in the `RoutingBuffer`), and removes the others.
///
/// The segments that have no trackpoint left are removed, and so are the routes of these segments.
/// So are the tracks that have no segment left, if tracks or files are selected (not if only
/// segments are), and the files that have no trackpoint left, if files are selected. A range that goes past the last trackpoint stops at it.
/// The segments that have no trackpoint left are removed. So are the tracks that have no segment
/// left, if tracks or files are selected (not if only segments are), and the files that have no
/// trackpoint left, if files are selected. A range that goes past the last trackpoint stops at it.
#[derive(Debug)]
pub struct Crop {
pub start: u32,
@@ -43,7 +43,8 @@ impl Apply for Crop {
.segment_locations(state.files, &state.order.0)
.into_iter()
.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)
.then(|| (start.saturating_sub(offset), (end - offset).min(len - 1)));
offset += len;
@@ -72,14 +73,14 @@ impl Apply for Crop {
let mut removed: HashSet<TrackSegmentId> = HashSet::new();
let mut had_points: HashSet<FileId> = HashSet::new();
for plan in &plans {
let SegmentLocation { file_id, index } = plan.location;
let SegmentLocation { file_id, trk, seg } = plan.location;
let file = files
.entry(file_id)
.or_insert_with(|| (*state.files[&file_id]).clone());
if plan.len > 0 {
had_points.insert(file_id);
}
let segment = file.segment_mut(index);
let segment = &mut file.trk[trk].trkseg[seg];
match plan.keep {
None => {
removed.insert(segment.id);
@@ -90,8 +91,6 @@ impl Apply for Crop {
let mut gone: Vec<FileId> = vec![];
for (file_id, file) in files.iter_mut() {
// a route has its segment, or is not there
file.rte.retain(|route| !removed.contains(&route.seg.id));
// the tracks that lost segments
let touched: Vec<bool> = file
.trk
@@ -109,7 +108,7 @@ impl Apply for Crop {
file.trk
.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);
}
}
@@ -14,7 +14,9 @@ impl Apply for Elevation<'_> {
.segment_locations(state.files, &state.order.0);
let count: usize = locations
.iter()
.map(|location| location.segment(state.files).len())
.map(|location| {
state.files[&location.file_id].trk[location.trk].trkseg[location.seg].len()
})
.sum();
if count == 0 {
return Err(CommandError::NothingToDo);
@@ -1,59 +1,44 @@
use std::{collections::HashSet, rc::Rc};
use crate::{
Apply, CommandError, File, FileId, LngLat, LngLatBounds, Route, Selection, State, Track,
TrackId, TrackSegment, Waypoint, Waypoints, copy_file, copy_waypoint, crossarc_lnglat,
distance,
Apply, CommandError, File, FileId, LngLat, LngLatBounds, Selection, State, Track, TrackId,
TrackSegment, Waypoint, Waypoints, copy_waypoint, crossarc_lnglat, distance,
};
/// Splits the selected elements that are made of several segments into elements that have one
/// each:
///
/// - files: a file with several routes and tracks becomes one file per route and per track, in
/// which the track is split into one track per segment. A file with a single track becomes one
/// file per segment. The new files are in the place of the file, which goes, and are selected.
/// They are named after the route or track, or the file, and numbered. The waypoints go to the files that have the segments
/// - files: a file with several tracks becomes one file per track, in which the track is split
/// into one track per segment. A file with a single track becomes one file per segment. The new
/// files are in the place of the file, which goes, and are selected. They are named after the
/// track, or the file, and numbered. The waypoints go to the files that have the segments
/// closest to them (to the first file if the file has no trackpoint).
/// - tracks: each selected track is replaced by one track per segment, which are selected. They
/// are named after the track and numbered.
///
/// What has a single segment is left as it is, which routes always are. There is nothing to do if nothing was changed.
///
/// With `keep_originals`, the files that are extracted stay where they are, and the new files come
/// right after each of them, and only the new files are selected. Extracting
/// tracks changes the file in place anyway, so the flag has no effect on it.
/// What has a single segment is left as it is. There is nothing to do if nothing was changed.
#[derive(Debug)]
pub struct Extract {
pub keep_originals: bool,
}
pub struct Extract;
impl Apply for Extract {
fn apply(self, state: &mut State) -> Result<(), CommandError> {
let selection = state.selection.clone();
match &selection {
Selection::File { file_ids } => extract_files(state, file_ids, 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),
_ => Err(CommandError::NothingToDo),
}
}
}
fn extract_files(
state: &mut State,
file_ids: &HashSet<FileId>,
keep_originals: bool,
) -> Result<(), CommandError> {
fn extract_files(state: &mut State, file_ids: &HashSet<FileId>) -> Result<(), CommandError> {
// the files that have something to extract, with the files that come out of them
let mut extracted: Vec<(FileId, Vec<File>)> = vec![];
for id in state.order.0.iter().filter(|id| file_ids.contains(id)) {
let file = &state.files[id];
if file.segments().count() > 1 {
let mut files = extract_file(file);
if keep_originals {
// the tracks and segments are the ones of the file, which stays: they need new ids
files = files.iter().map(copy_file).collect();
}
extracted.push((*id, files));
let segments: usize = file.trk.iter().map(|track| track.trkseg.len()).sum();
if segments > 1 {
extracted.push((*id, extract_file(file)));
}
}
if extracted.is_empty() {
@@ -63,25 +48,13 @@ fn extract_files(
let mut order = vec![];
for id in state.order.0.iter() {
match extracted.iter().find(|(original, _)| original == id) {
Some((_, files)) => {
if keep_originals {
order.push(*id);
}
order.extend(files.iter().map(|file| file.id));
}
Some((_, files)) => order.extend(files.iter().map(|file| file.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> = if keep_originals {
HashSet::new()
} else {
file_ids.clone()
};
let mut selected: HashSet<FileId> = file_ids.clone();
for (original, files) in extracted {
if !keep_originals {
state.files.remove(&original);
}
selected.remove(&original);
for file in files {
selected.insert(file.id);
@@ -95,35 +68,31 @@ fn extract_files(
/// The files that a file with several segments becomes.
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 {
routes: Vec<Route>,
tracks: Vec<Track>,
name: String,
}
let name = |name: &Option<String>, i: usize| {
name.clone()
.unwrap_or_else(|| format!("{} ({})", file.info.name, i + 1))
};
let (parts, owner): (Vec<Part>, Vec<usize>) =
if file.rte.len() + file.trk.len() > 1 {
// one file per route and per track, the segments of a track follow it
let routes = file.rte.iter().enumerate().map(|(i, route)| Part {
routes: vec![route.clone()],
tracks: vec![],
name: name(&route.info.name, i),
});
let tracks = file.trk.iter().enumerate().map(|(i, track)| Part {
routes: vec![],
let (parts, owner): (Vec<Part>, Vec<usize>) = if file.trk.len() > 1 {
// one file per track, the segments of a track follow it
let parts = file
.trk
.iter()
.enumerate()
.map(|(i, track)| Part {
tracks: split_track(track),
name: name(&track.info.name, file.rte.len() + i),
});
let parts = routes.chain(tracks).collect();
let owner =
(0..file.rte.len())
.chain(file.trk.iter().enumerate().flat_map(|(i, track)| {
track.trkseg.iter().map(move |_| file.rte.len() + i)
}))
name: track
.info
.name
.clone()
.unwrap_or_else(|| format!("{} ({})", file.info.name, i + 1)),
})
.collect();
let owner = file
.trk
.iter()
.enumerate()
.flat_map(|(i, track)| track.trkseg.iter().map(move |_| i))
.collect();
(parts, owner)
} else {
@@ -139,7 +108,6 @@ fn extract_file(file: &File) -> Vec<File> {
.iter()
.enumerate()
.map(|(i, segment)| Part {
routes: vec![],
tracks: vec![Track {
id: if i == 0 { track.id } else { Default::default() },
trkseg: vec![segment.clone()],
@@ -153,7 +121,7 @@ fn extract_file(file: &File) -> Vec<File> {
};
// the waypoints go with the segments that are the closest to them
let segments: Vec<&TrackSegment> = file.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()];
for waypoint in file.wpt.iter() {
let mut owners: Vec<usize> = closest_segments(&segments, waypoint.coordinates)
@@ -181,7 +149,6 @@ fn extract_file(file: &File) -> Vec<File> {
.map(|(part, waypoints)| {
let mut extracted = File {
id: Default::default(),
rte: part.routes,
trk: part.tracks,
wpt: Waypoints::default(),
..file.clone()
@@ -405,11 +372,7 @@ mod tests {
fx.files.insert(other_id, Rc::new(other));
fx.order.0.push(other_id);
Extract {
keep_originals: false,
}
.apply(&mut fx.state())
.unwrap();
Extract.apply(&mut fx.state()).unwrap();
// in the place of the file, in order, named after the track
assert!(!fx.files.contains_key(&id));
@@ -439,48 +402,6 @@ mod tests {
if file_ids.len() == 3 && !file_ids.contains(&other_id)));
}
#[test]
fn test_extracting_files_keeping_the_originals() {
let original = file(
"route",
vec![track(Some("climb"), &[0.0, 10.0, 20.0])],
vec![],
);
let segment_ids: Vec<_> = original.trk[0].trkseg.iter().map(|s| s.id).collect();
let (mut fx, id) = fixture(original);
let before = fx.files[&id].clone();
let single = file("single", vec![track(None, &[50.0])], vec![]);
let single_id = single.id;
fx.files.insert(single_id, Rc::new(single));
fx.order.0.push(single_id);
fx.selection = Selection::File {
file_ids: HashSet::from([id, single_id]),
};
Extract {
keep_originals: true,
}
.apply(&mut fx.state())
.unwrap();
// the file stays, the new ones come right after it
assert!(Rc::ptr_eq(&fx.files[&id], &before));
assert_eq!(
names(&fx),
["route", "climb (1)", "climb (2)", "climb (3)", "single"]
);
assert_eq!(fx.order.0[0], id);
assert_eq!(fx.order.0[4], single_id);
// the segments are copies, not the ones of the file
for new_id in &fx.order.0[1..4] {
let segment = fx.files[new_id].trk[0].trkseg[0].id;
assert!(!segment_ids.contains(&segment));
}
// only the new files are selected
assert!(matches!(&fx.selection, Selection::File { file_ids }
if *file_ids == fx.order.0[1..4].iter().copied().collect::<HashSet<_>>()));
}
#[test]
fn test_a_file_with_several_tracks_becomes_one_file_per_track() {
let original = file(
@@ -494,11 +415,7 @@ mod tests {
);
let ids: Vec<_> = original.trk.iter().map(|t| t.id).collect();
let (mut fx, _) = fixture(original);
Extract {
keep_originals: false,
}
.apply(&mut fx.state())
.unwrap();
Extract.apply(&mut fx.state()).unwrap();
// named after the track, or the file
assert_eq!(names(&fx), ["first", "route (2)", "third"]);
@@ -534,11 +451,7 @@ mod tests {
vec![shared, waypoint("lost", 100.0)],
);
let (mut fx, _) = fixture(original);
Extract {
keep_originals: false,
}
.apply(&mut fx.state())
.unwrap();
Extract.apply(&mut fx.state()).unwrap();
let first = &fx.files[&fx.order.0[0]];
let second = &fx.files[&fx.order.0[1]];
// the tie goes to both, with different ids
@@ -567,11 +480,7 @@ mod tests {
vec![waypoint("w", 1.0)],
);
let (mut fx, _) = fixture(original);
Extract {
keep_originals: false,
}
.apply(&mut fx.state())
.unwrap();
Extract.apply(&mut fx.state()).unwrap();
assert_eq!(fx.files[&fx.order.0[0]].wpt.len(), 1);
assert_eq!(fx.files[&fx.order.0[1]].wpt.len(), 0);
}
@@ -594,11 +503,7 @@ mod tests {
trk_ids: [ids[1], ids[2]].into(),
};
Extract {
keep_originals: false,
}
.apply(&mut fx.state())
.unwrap();
Extract.apply(&mut fx.state()).unwrap();
let file = &fx.files[&id];
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 before = fx.files[&id].clone();
assert_eq!(
Extract {
keep_originals: false,
}
.apply(&mut fx.state()),
Extract.apply(&mut fx.state()),
Err(CommandError::NothingToDo)
);
fx.selection = Selection::Track {
@@ -645,10 +547,7 @@ mod tests {
trk_ids: [track_id].into(),
};
assert_eq!(
Extract {
keep_originals: false,
}
.apply(&mut fx.state()),
Extract.apply(&mut fx.state()),
Err(CommandError::NothingToDo)
);
for selection in [
@@ -662,10 +561,7 @@ mod tests {
] {
fx.selection = selection;
assert_eq!(
Extract {
keep_originals: false,
}
.apply(&mut fx.state()),
Extract.apply(&mut fx.state()),
Err(CommandError::NothingToDo)
);
}
@@ -39,7 +39,7 @@ impl Apply for InsertAnchor {
.selection
.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 Some(mut previous) = points.next() else {
continue;
@@ -81,7 +81,7 @@ impl Apply for InsertAnchor {
let closest = closest.ok_or(CommandError::NothingToDo)?;
let mut file = (*state.files[&closest.location.file_id]).clone();
let segment = file.segment_mut(closest.location.index);
let segment = &mut file.trk[closest.location.trk].trkseg[closest.location.seg];
let before = closest.before;
// on a trackpoint already, if it is closer than `SNAP_DISTANCE`
let snapped = if segment.len() == 1 {
@@ -1,13 +1,11 @@
use std::{collections::HashSet, rc::Rc};
use crate::{
Apply, CommandError, FileId, GlobalStatistics, Route, RouteId, Selection, State, Statistics,
Track, TrackId, TrackSegment, TrackSegmentId, Waypoint, Waypoints, copy_route, copy_track,
replace_trackpoints,
Apply, CommandError, FileId, GlobalStatistics, Selection, State, Statistics, Track, TrackId,
TrackSegment, TrackSegmentId, Waypoint, Waypoints, replace_trackpoints,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeType {
/// The trackpoints of the elements are connected in a single segment.
Connect,
@@ -17,16 +15,11 @@ pub enum MergeType {
/// 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
/// file, as they are with `Group`, or all connected in the first of them (a route or a track,
/// the routes of a file coming before its tracks) with `Connect`. The waypoints of
/// all of them are in the first file too, except the ones that are the same. With
/// `keep_originals`, no file is deleted nor changed: the result is a new file (with the info of
/// the first one and new ids), which comes right after the last of the files and is selected.
/// - files: the other files are deleted. The tracks of all of them are in the first file, as
/// they are with `Group`, or all connected in the first track with `Connect`. The waypoints of
/// all of them are in the first file too, except the ones that are the same.
/// - tracks of a file: the first track gets their segments, as they are with `Group`, or all
/// connected in one segment with `Connect`. The other tracks are removed.
/// - routes of a file: the first route gets their trackpoints, all connected with `Connect`, and
/// the other routes are removed. A route has a single segment, so there is nothing to `Group`.
/// - segments of a track: they are all connected in a segment at the position of the first one
/// with `Connect`. With `Group` they are put one after the other, at that position.
///
@@ -39,8 +32,6 @@ pub enum MergeType {
pub struct Merge {
pub type_: MergeType,
pub remove_gaps: bool,
/// Only for files: tracks and segments are merged in place anyway.
pub keep_originals: bool,
}
impl Apply for Merge {
@@ -48,7 +39,6 @@ impl Apply for Merge {
let selection = state.selection.clone();
match &selection {
Selection::File { file_ids } => self.merge_files(state, file_ids),
Selection::Route { file_id, rte_ids } => self.merge_routes(state, *file_id, rte_ids),
Selection::Track { file_id, trk_ids } => self.merge_tracks(state, *file_id, trk_ids),
Selection::TrackSegment {
file_id,
@@ -82,26 +72,21 @@ impl Merge {
return Err(CommandError::NothingToDo);
};
let mut routes: Vec<Route> = vec![];
let mut tracks: Vec<Track> = vec![];
let mut segments: Vec<TrackSegment> = vec![];
let mut waypoints: Vec<Waypoint> = vec![];
for id in &ordered {
let file = &state.files[id];
routes.extend(file.rte.iter().cloned());
tracks.extend(file.trk.iter().cloned());
segments.extend(file.segments().cloned());
for waypoint in file.wpt.iter() {
if !waypoints.iter().any(|kept| kept.same_content(waypoint)) {
waypoints.push(waypoint.clone());
}
}
}
// the first route or track, which holds the connected segment
let first_is_route = ordered.iter().find_map(|id| {
let file = &state.files[id];
(!file.has_no_path()).then(|| !file.rte.is_empty())
});
let segments: Vec<TrackSegment> = tracks
.iter()
.flat_map(|track| track.trkseg.iter().cloned())
.collect();
match self.type_ {
MergeType::Group if ordered.len() == 1 => return Err(CommandError::NothingToDo),
@@ -109,49 +94,21 @@ impl Merge {
return Err(CommandError::NothingToDo);
}
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);
if first_is_route == Some(true) {
let first = Route {
seg: merged,
..routes[0].clone()
};
(routes, tracks) = (vec![first], vec![]);
} else {
let first = Track {
trkseg: vec![merged],
..tracks[0].clone()
};
(routes, tracks) = (vec![], vec![first]);
}
tracks = vec![first];
}
_ => {}
}
let mut target = (*state.files[&target_id]).clone();
target.rte = routes;
target.trk = tracks;
target.wpt = Waypoints::default();
target.wpt.insert_at(0, waypoints);
if self.keep_originals {
// a new file, whose routes, tracks and segments do not share their ids with the
// originals
target.id = Default::default();
target.rte = target.rte.iter().map(copy_route).collect();
target.trk = target.trk.iter().map(copy_track).collect();
let at = state
.order
.0
.iter()
.rposition(|id| file_ids.contains(id))
.map_or(state.order.0.len(), |i| i + 1);
state.order.0.insert(at, target.id);
*state.selection = Selection::File {
file_ids: [target.id].into(),
};
state.files.insert(target.id, Rc::new(target));
return Ok(());
}
for id in &ordered[1..] {
state.files.remove(id);
}
@@ -160,33 +117,6 @@ impl Merge {
Ok(())
}
fn merge_routes(
&self,
state: &mut State,
file_id: FileId,
rte_ids: &HashSet<RouteId>,
) -> Result<(), CommandError> {
let file = state.files.get(&file_id).ok_or(CommandError::NothingToDo)?;
let selected: Vec<usize> = (0..file.rte.len())
.filter(|i| rte_ids.contains(&file.rte[*i].id))
.collect();
if selected.len() < 2 || self.type_ == MergeType::Group {
return Err(CommandError::NothingToDo);
}
let first = selected[0];
let segments: Vec<TrackSegment> =
selected.iter().map(|i| file.rte[*i].seg.clone()).collect();
let mut file = (**file).clone();
file.rte[first].seg = connect(&segments, segments[0].id, self.remove_gaps);
let mut index = 0;
file.rte.retain(|_| {
index += 1;
!selected[1..].contains(&(index - 1))
});
state.files.insert(file_id, Rc::new(file));
Ok(())
}
fn merge_tracks(
&self,
state: &mut State,
@@ -343,7 +273,6 @@ mod tests {
Merge {
type_,
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]
fn test_connect_files() {
let mut fx = Fixture::default();
@@ -567,7 +429,6 @@ mod tests {
Merge {
type_: MergeType::Connect,
remove_gaps,
keep_originals: false,
}
.apply(&mut fx2.state())
.unwrap();
@@ -58,8 +58,7 @@ impl Apply for NewWaypoint<'_> {
.filter(|id| file_ids.contains(id))
.copied()
.collect(),
Selection::Route { file_id, .. }
| Selection::Track { file_id, .. }
Selection::Track { file_id, .. }
| Selection::TrackSegment { file_id, .. }
| Selection::Waypoints { file_id }
| Selection::Waypoint { file_id, .. } => vec![*file_id],
@@ -119,14 +118,12 @@ mod tests {
New {
name: "a",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
New {
name: "b",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -172,7 +169,6 @@ mod tests {
New {
name: "a",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -200,7 +196,6 @@ mod tests {
New {
name,
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -239,7 +234,6 @@ mod tests {
New {
name: "a",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -256,7 +250,6 @@ mod tests {
New {
name: "a",
trackpoint: None,
route: false,
}
.apply(&mut fx.state())
.unwrap();
@@ -23,7 +23,7 @@ impl Apply for Reduce {
let mut changed = vec![];
let mut kept_points = vec![];
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);
if kept.len() == segment.len() {
continue;
@@ -1,15 +1,14 @@
use std::rc::Rc;
use crate::{
Apply, CommandError, File, FileId, RouteId, Selection, State, TrackId, TrackSegmentId,
end_time, reverse_routes, reverse_segment, reverse_segments, reverse_tracks, start_time,
track_end_time, track_start_time,
Apply, CommandError, File, FileId, Selection, State, TrackId, TrackSegmentId, end_time,
reverse_segment, reverse_segments, reverse_tracks, start_time, track_end_time,
track_start_time,
};
/// Reverses the direction of the selected elements: the whole of each selected file (its routes,
/// its tracks, the segments of its tracks and their trackpoints are in the opposite order; the
/// 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 route or segment on its own (its
/// Reverses the direction of the selected elements: the whole of each selected file (its tracks,
/// the segments of its tracks and their trackpoints are in the opposite order), of each selected
/// track (its segments and their trackpoints), or each selected segment on its own (its
/// trackpoints). The timestamps are mirrored, see [`reverse_segment`].
#[derive(Debug)]
pub struct Reverse;
@@ -21,7 +20,9 @@ impl Apply for Reverse {
.segment_locations(state.files, &state.order.0);
let points: usize = locations
.iter()
.map(|location| location.segment(state.files).len())
.map(|location| {
state.files[&location.file_id].trk[location.trk].trkseg[location.seg].len()
})
.sum();
if points <= 1 {
return Err(CommandError::NothingToDo);
@@ -39,18 +40,12 @@ impl Apply for Reverse {
Selection::File { file_ids } => {
for file_id in file_ids {
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 start = file.trk.first().and_then(track_start_time);
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| {
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>) {
for track in file.trk.iter_mut().filter(|track| ids.contains(&track.id)) {
let end = track.trkseg.last().and_then(end_time);
@@ -14,7 +14,7 @@ impl Apply for RoundTrip {
.selection
.segment_locations(state.files, &state.order.0)
.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();
if locations.is_empty() {
return Err(CommandError::NothingToDo);
@@ -1,11 +1,11 @@
use std::rc::Rc;
use crate::{
Apply, Categories, CommandError, FileId, SegmentIndex, SegmentLocation, Selection, State,
Statistics, Track, TrackSegment, Trackpoint, distance, replace_trackpoints,
Apply, Categories, CommandError, FileId, SegmentLocation, Selection, State, Statistics, Track,
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
/// 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.
@@ -64,7 +64,7 @@ impl RouteCategory<'_> {
/// anchors anyway. The ones right before and after the range, between which the route goes, become
/// shown at every zoom level as well.
#[derive(Debug)]
pub struct Reroute<'a> {
pub struct Route<'a> {
pub start: u32,
pub end: u32,
pub lng: &'a [f64],
@@ -89,7 +89,7 @@ pub(super) struct Target {
pub end: usize,
}
impl Apply for Reroute<'_> {
impl Apply for Route<'_> {
fn apply(self, state: &mut State) -> Result<(), CommandError> {
let (start, end) = (self.start as usize, self.end as usize);
let len = self.lng.len();
@@ -152,17 +152,14 @@ impl Apply for Reroute<'_> {
}
let seg = file.trk[trk].trkseg.len() - 1;
Target {
location: SegmentLocation {
file_id,
index: SegmentIndex::Track { trk, seg },
},
location: SegmentLocation { file_id, trk, seg },
start: 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 (speed, start_time) = speed_and_start_time(segment, &target, &points);
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
.into_iter()
.map(|location| {
let segment = location.segment(state.files);
let segment = &state.files[&location.file_id].trk[location.trk].trkseg[location.seg];
(location, segment.len())
})
.collect();
@@ -385,8 +382,8 @@ mod tests {
lng: &'a [f64],
lat: &'a [f64],
ele: &'a [f64],
) -> Reroute<'a> {
Reroute {
) -> Route<'a> {
Route {
start,
end,
lng,
@@ -462,7 +459,7 @@ mod tests {
route(0, 0, &[1.0], &[], &[]).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()),
Reroute {
Route {
anchors: &[1],
..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
let (lng, lat, ele) = ([1.0; 5], [2.0; 5], [3.0; 5]);
Reroute {
Route {
anchors: &[1, 3],
..route(1, 3, &lng, &lat, &ele)
}
@@ -642,7 +639,7 @@ mod tests {
let names = ["asphalt".to_string(), "gravel".to_string()];
let (lng, lat, ele) = ([1.0; 4], [2.0; 4], [3.0; 4]);
// asphalt, asphalt, unknown, gravel
Reroute {
Route {
surface: RouteCategory {
starts: &[0, 2, 3],
values: &[1, 0, 2],
@@ -686,7 +683,7 @@ mod tests {
},
];
for surface in broken {
let result = Reroute {
let result = Route {
surface,
..route(0, 0, &lng, &lat, &ele)
}
@@ -1,44 +1,35 @@
use std::rc::Rc;
use crate::{
Apply, CommandError, File, Route, SegmentIndex, Selection, State, Track, TrackSegment,
Waypoints, copy_file, copy_route, copy_segment, copy_track, copy_waypoint,
Apply, CommandError, File, State, Track, TrackSegment, Waypoints, copy_segment, copy_track,
copy_waypoint,
};
use super::route::find_target;
/// What is cut in two where the trackpoint is.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SplitType {
/// The file becomes two files, which both have the waypoints (copies, in the second one).
Files,
/// The route becomes two routes.
Routes,
/// The track becomes two tracks.
Tracks,
/// The segment of a track becomes two segments.
/// The segment becomes two 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
/// one of the second part. Cutting a route needs the trackpoint to be in a route, and cutting a
/// track or a segment needs it to be in a track: there is nothing to do otherwise.
/// one of the second part.
///
/// 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
/// tracks of the file, the routes coming before the tracks), which are moved to it. When the file
/// is cut, the new file comes right after it. The 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.
/// come after the trackpoint (the following segments of the track, the following tracks of the
/// file), which are moved to it. When the file is cut, the new file comes right after it. The
/// selection does not change.
#[derive(Debug)]
pub struct Split {
pub at: u32,
pub split_type: SplitType,
pub keep_originals: bool,
}
impl Apply for Split {
@@ -46,115 +37,69 @@ impl Apply for Split {
let at = self.at as usize;
let target = find_target(state, at, at)?.ok_or(CommandError::NothingToDo)?;
let location = target.location;
let (file_id, index) = (location.file_id, target.start);
let in_route = matches!(location.index, SegmentIndex::Route(_));
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 (file_id, trk, seg, index) =
(location.file_id, location.trk, location.seg, target.start);
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(
"trackpoint out of the segment".into(),
));
}
// with `SplitType::Files`, what goes to the second file: what comes after the trackpoint
let mut second_file = File {
id: Default::default(),
rte: vec![],
trk: vec![],
wpt: Waypoints::default(),
..file.clone()
};
match location.index {
SegmentIndex::Route(rte) => {
let route = file.rte[rte].clone();
let first = Route {
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 } => {
let track = file.trk[trk].clone();
let segment = &track.trkseg[seg];
let (first, second) = (keep_up_to(segment, index), keep_from(segment, index));
if self.split_type == SplitType::Segments {
file.trk[trk].trkseg.splice(seg..=seg, [first, second]);
state.files.insert(file_id, Rc::new(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
// the track is cut: what is before the segment, and its first part, then the second part
// and what is after it
let split_track = || {
let mut first_track = track.clone();
first_track.trkseg.truncate(seg);
first_track.trkseg.push(first);
first_track.trkseg.push(first.clone());
let mut second_track = Track {
id: Default::default(),
trkseg: vec![second],
trkseg: vec![second.clone()],
..track.clone()
};
second_track
.trkseg
.extend(track.trkseg[seg + 1..].iter().map(copy_segment));
if self.split_type == SplitType::Tracks {
file.trk.splice(trk..=trk, [first_track, second_track]);
state.files.insert(file_id, Rc::new(file));
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.push(first_track);
}
}
(first_track, second_track)
};
// the file is cut: both parts have the waypoints
match self.split_type {
SplitType::Segments => {
file.trk[trk].trkseg.splice(seg..=seg, [first, second]);
}
SplitType::Tracks => {
let (first_track, second_track) = split_track();
file.trk.splice(trk..=trk, [first_track, second_track]);
}
SplitType::Files => {
let (first_track, second_track) = split_track();
let mut second_file = File {
id: Default::default(),
trk: vec![second_track],
wpt: Waypoints::default(),
..file.clone()
};
second_file
.trk
.extend(file.trk[trk + 1..].iter().map(copy_track));
second_file
.wpt
.insert_at(0, file.wpt.iter().map(copy_waypoint).collect());
file.trk.truncate(trk);
file.trk.push(first_track);
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.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.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));
}
}
state.files.insert(file_id, Rc::new(file));
Ok(())
}
}
@@ -201,11 +146,7 @@ mod tests {
}
fn split(at: u32, split_type: SplitType) -> Split {
Split {
at,
split_type,
keep_originals: false,
}
Split { at, split_type }
}
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)));
}
#[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]
fn test_invalid_or_nothing() {
let (mut fx, id) = loaded();
@@ -45,8 +45,10 @@ impl Apply for Time {
let locations = state
.selection
.segment_locations(state.files, &state.order.0);
let segments: Vec<&TrackSegment> =
locations.iter().map(|l| l.segment(state.files)).collect();
let segments: Vec<&TrackSegment> = locations
.iter()
.map(|l| &state.files[&l.file_id].trk[l.trk].trkseg[l.seg])
.collect();
if segments.iter().all(|segment| segment.is_empty()) {
return Err(CommandError::NothingToDo);
}
@@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet};
use crate::{FileId, FileWaypointsRevisionId, Files, TrackSegmentId, TrackSegmentRevisionId};
/// Coordinates buffers, as flat `[lng, lat, lng, lat, ...]` arrays so that they can be handed
/// to the UI without per-point calls: one per 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
/// of its file) changes, so editing a segment does not touch the buffers of the others.
#[derive(Debug, Default)]
@@ -30,7 +30,7 @@ impl CoordinatesCache {
self.waypoints
.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);
if self
.segments
Loaded 100 of 698 files, more files were not shown because too many files have changed in this diff. Show more