diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d354c062b..7b97a1c22 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,19 +15,31 @@ jobs: with: node-version: 24 cache: npm - cache-dependency-path: | - gpx/package-lock.json - website/package-lock.json + cache-dependency-path: website/package-lock.json - - name: Install dependencies for gpx - run: npm install --prefix gpx + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown - - name: Build gpx - run: npm run build --prefix gpx + - name: Install wasm-pack + uses: taiki-e/install-action@v2 + with: + tool: wasm-pack + + - name: Test gpx-rs + run: cargo test + + - name: Build wasm + run: wasm-pack build --target bundler + working-directory: wasm - name: Install dependencies for website run: npm install --prefix website + - name: Test website + run: npm test --prefix website + - name: Create env file run: | touch website/.env diff --git a/.gitignore b/.gitignore index 496ee2ca6..f2564bc44 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.DS_Store \ No newline at end of file +.DS_Store +target/ diff --git a/tauri/Cargo.lock b/Cargo.lock similarity index 95% rename from tauri/Cargo.lock rename to Cargo.lock index 26240fd50..04b7f2155 100644 --- a/tauri/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,17 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[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 = "atk" version = "0.18.2" @@ -283,10 +294,16 @@ dependencies = [ ] [[package]] -name = "cc" -version = "1.8.0" +name = "cast" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +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", @@ -321,9 +338,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.5" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" @@ -358,6 +375,16 @@ dependencies = [ "memchr", ] +[[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 = "cookie" version = "0.18.2" @@ -477,7 +504,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" dependencies = [ "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -506,7 +533,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -517,7 +544,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core", "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -550,7 +577,7 @@ dependencies = [ "defmt-parser", "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -589,7 +616,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -643,7 +670,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -666,7 +693,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -754,9 +781,9 @@ checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "equivalent" -version = "1.0.3" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" @@ -796,9 +823,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.14" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "flate2" @@ -841,7 +868,7 @@ checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -899,7 +926,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -1128,7 +1155,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -1181,6 +1208,22 @@ dependencies = [ "uuid", ] +[[package]] +name = "gpx-wasm" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "gpx-engine", + "idb", + "js-sys", + "serde", + "serde-wasm-bindgen", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", +] + [[package]] name = "gtk" version = "0.18.2" @@ -1230,7 +1273,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -1495,6 +1538,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "idb" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6554f394e990a1af530a528a7fdcad6e01b29cb1b990f89df3ffd62cf15f7828" +dependencies = [ + "indexmap 2.14.2", + "js-sys", + "num-traits", + "thiserror 2.0.21", + "tokio", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1625,7 +1683,7 @@ dependencies = [ "jiff-core", "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -1684,7 +1742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -1756,9 +1814,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.190" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libdbus-sys" @@ -1779,6 +1837,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.25" @@ -1841,6 +1905,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1929,6 +2003,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -1942,6 +2025,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1963,7 +2047,7 @@ dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -2168,6 +2252,12 @@ 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 = "option-ext" version = "0.2.0" @@ -2269,7 +2359,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -2436,9 +2526,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.107" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2454,9 +2544,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.47" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -2515,7 +2605,7 @@ checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -2659,7 +2749,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -2719,6 +2809,17 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -2736,7 +2837,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -2747,14 +2848,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] name = "serde_json" -version = "1.0.154" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -2771,7 +2872,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -2822,7 +2923,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -2844,7 +2945,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -3023,9 +3124,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.119" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -3034,9 +3135,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.9" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -3060,7 +3161,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -3124,7 +3225,7 @@ checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -3223,7 +3324,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.119", + "syn 2.0.118", "tauri-utils", "thiserror 2.0.21", "time", @@ -3241,7 +3342,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", "tauri-codegen", "tauri-utils", ] @@ -3381,7 +3482,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -3392,7 +3493,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] @@ -3443,9 +3544,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.53.3" +version = "1.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" dependencies = [ "bytes", "libc", @@ -3670,9 +3771,9 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" -version = "1.0.27" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" @@ -3713,9 +3814,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.28.0" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -3832,7 +3933,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", "wasm-bindgen-shared", ] @@ -3845,6 +3946,45 @@ 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 = "wasm-streams" version = "0.5.0" @@ -3954,7 +4094,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -4068,7 +4208,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -4079,7 +4219,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 2.0.118", ] [[package]] @@ -4333,7 +4473,7 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", "synstructure", ] @@ -4354,7 +4494,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", "synstructure", ] @@ -4388,7 +4528,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.9", + "syn 3.0.6", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 000000000..43d8f2060 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = ["gpx-rs", "wasm", "tauri"] +# `cargo test` at the root only tests the engine: the others need their own tooling +default-members = ["gpx-rs"] +resolver = "3" \ No newline at end of file diff --git a/README.md b/README.md index a6af9c34f..4a8f5f895 100644 --- a/README.md +++ b/README.md @@ -25,18 +25,20 @@ Any help is greatly appreciated! ## Development -The code is split into two parts: +The code is split into four parts: -- `gpx-rs`: a Rust library, compiled to WebAssembly, for parsing and manipulating GPX files, -- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application. +- `gpx-rs`: a Rust library for parsing and manipulating GPX files (the engine), and the interface to it that the next two share (`gpx_engine::api`), +- `wasm`: the engine for the browser, compiled to WebAssembly, +- `tauri`: the desktop app, which runs the engine natively, +- `website`: the website itself, which is a [SvelteKit](https://kit.svelte.dev/) application. It also is the UI of the desktop app. -You will need [Node.js](https://nodejs.org/), [Rust](https://rustup.rs/) and [wasm-pack](https://rustwasm.github.io/wasm-pack/) to build and run these two parts. +You will need [Node.js](https://nodejs.org/), [Rust](https://rustup.rs/) and [wasm-pack](https://rustwasm.github.io/wasm-pack/) to build and run the website. See [`tauri`](tauri/README.md) for the desktop app. -### Building the `gpx-rs` library +### Building the WebAssembly engine ```bash -cd gpx-rs -wasm-pack build wasm --target bundler --out-dir ../pkg +cd wasm +wasm-pack build --target bundler ``` ### Running the website diff --git a/gpx-rs/.gitignore b/gpx-rs/.gitignore deleted file mode 100644 index 38adc0efd..000000000 --- a/gpx-rs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target/ -pkg/ \ No newline at end of file diff --git a/gpx-rs/Cargo.lock b/gpx-rs/Cargo.lock deleted file mode 100644 index 62ebb9ec0..000000000 --- a/gpx-rs/Cargo.lock +++ /dev/null @@ -1,794 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "android_system_properties" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" -dependencies = [ - "libc", -] - -[[package]] -name = "async-trait" -version = "0.1.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", -] - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - -[[package]] -name = "cc" -version = "1.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror", -] - -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "find-msvc-tools" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" - -[[package]] -name = "futures-core" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" - -[[package]] -name = "futures-task" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" - -[[package]] -name = "futures-util" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi", -] - -[[package]] -name = "gpx-engine" -version = "0.1.0" -dependencies = [ - "chrono", - "postcard", - "quick-xml", - "serde", - "uuid", -] - -[[package]] -name = "gpx-rs" -version = "0.1.0" -dependencies = [ - "console_error_panic_hook", - "gpx-engine", - "idb", - "js-sys", - "uuid", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test", -] - -[[package]] -name = "hash32" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heapless" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" -dependencies = [ - "atomic-polyfill", - "hash32", - "rustc_version", - "serde", - "spin", - "stable_deref_trait", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idb" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6554f394e990a1af530a528a7fdcad6e01b29cb1b990f89df3ffd62cf15f7828" -dependencies = [ - "indexmap", - "js-sys", - "num-traits", - "thiserror", - "tokio", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "indexmap" -version = "2.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "minicov" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" -dependencies = [ - "cc", - "walkdir", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "heapless", - "serde", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quick-xml" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "spin" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" -dependencies = [ - "lock_api", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "syn" -version = "2.0.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "tokio" -version = "1.53.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" -dependencies = [ - "pin-project-lite", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "uuid" -version = "1.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" -dependencies = [ - "getrandom", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.129" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" -dependencies = [ - "js-sys", - "tokio", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.129" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.129" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 3.0.6", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.129" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-bindgen-test" -version = "0.3.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae7499dfd45780a0a91d7ee6bb9ac51970a4479a41a89da443fdda5a39547d42" -dependencies = [ - "async-trait", - "cast", - "js-sys", - "libm", - "minicov", - "nu-ansi-term", - "num-traits", - "oorandom", - "serde", - "serde_json", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test-macro", - "wasm-bindgen-test-shared", -] - -[[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b84b5ac638bfb168196a1a461fcc8f46a294a18b1b6be52133b4e0db122cc9f" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "wasm-bindgen-test-shared" -version = "0.2.129" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f692aa943ccd88363733b77063f32cfed5bc6cbea8e6e8b251b302f881606fe" - -[[package]] -name = "web-sys" -version = "0.3.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/gpx-rs/Cargo.toml b/gpx-rs/Cargo.toml index 7ff8215fa..f6e72066f 100644 --- a/gpx-rs/Cargo.toml +++ b/gpx-rs/Cargo.toml @@ -1,6 +1,11 @@ -[workspace] -members = ["engine", "wasm"] -resolver = "3" +[package] +name = "gpx-engine" +version = "0.1.0" +edition = "2024" -[profile.release] -debug = true +[dependencies] +chrono = "0.4.45" +postcard = { version = "1.1.3", features = ["use-std"] } +quick-xml = "0.42.0" +serde = { version = "1.0.229", features = ["derive"] } +uuid = { version = "1.26.1", features = ["serde", "v4"] } diff --git a/gpx-rs/README.md b/gpx-rs/README.md index 7645015bd..9473ae43b 100644 --- a/gpx-rs/README.md +++ b/gpx-rs/README.md @@ -1,14 +1,10 @@ # gpx-rs -- `engine`: the GPX engine, plain Rust. Tested natively: `cargo test`. -- `wasm`: the interface to the web page, compiled to WebAssembly. +The GPX engine: plain Rust, no knowledge of who uses it. Tested natively with `cargo test` (from +here or from the root). -``` -wasm-pack build wasm --target bundler --out-dir ../pkg -``` - -The interface is tested in Node (it needs `wasm-pack`, which fetches the test runner the first time): - -``` -wasm-pack test --node wasm -``` +- `src/core`, `src/engine`, `src/storage`: the engine itself. +- `src/api`: the interface to the engine that the frontends (`../wasm`, `../tauri`) share. Each + function takes the engine and typed arguments, and gives plain data (`api::dto`, serde types, or + slices for the numeric buffers). A frontend only converts what crosses: ids from strings or + bytes, results to JS objects or JSON, slices to typed arrays or raw bytes. diff --git a/gpx-rs/engine/data/self_closing_points.gpx b/gpx-rs/data/self_closing_points.gpx similarity index 100% rename from gpx-rs/engine/data/self_closing_points.gpx rename to gpx-rs/data/self_closing_points.gpx diff --git a/gpx-rs/engine/data/simple.gpx b/gpx-rs/data/simple.gpx similarity index 100% rename from gpx-rs/engine/data/simple.gpx rename to gpx-rs/data/simple.gpx diff --git a/gpx-rs/engine/data/with_cad.gpx b/gpx-rs/data/with_cad.gpx similarity index 100% rename from gpx-rs/engine/data/with_cad.gpx rename to gpx-rs/data/with_cad.gpx diff --git a/gpx-rs/engine/data/with_highway.gpx b/gpx-rs/data/with_highway.gpx similarity index 100% rename from gpx-rs/engine/data/with_highway.gpx rename to gpx-rs/data/with_highway.gpx diff --git a/gpx-rs/engine/data/with_hr.gpx b/gpx-rs/data/with_hr.gpx similarity index 100% rename from gpx-rs/engine/data/with_hr.gpx rename to gpx-rs/data/with_hr.gpx diff --git a/gpx-rs/engine/data/with_links.gpx b/gpx-rs/data/with_links.gpx similarity index 100% rename from gpx-rs/engine/data/with_links.gpx rename to gpx-rs/data/with_links.gpx diff --git a/gpx-rs/engine/data/with_power_1.gpx b/gpx-rs/data/with_power_1.gpx similarity index 100% rename from gpx-rs/engine/data/with_power_1.gpx rename to gpx-rs/data/with_power_1.gpx diff --git a/gpx-rs/engine/data/with_power_2.gpx b/gpx-rs/data/with_power_2.gpx similarity index 100% rename from gpx-rs/engine/data/with_power_2.gpx rename to gpx-rs/data/with_power_2.gpx diff --git a/gpx-rs/engine/data/with_route_extensions.gpx b/gpx-rs/data/with_route_extensions.gpx similarity index 100% rename from gpx-rs/engine/data/with_route_extensions.gpx rename to gpx-rs/data/with_route_extensions.gpx diff --git a/gpx-rs/engine/data/with_route_points.gpx b/gpx-rs/data/with_route_points.gpx similarity index 100% rename from gpx-rs/engine/data/with_route_points.gpx rename to gpx-rs/data/with_route_points.gpx diff --git a/gpx-rs/engine/data/with_routes.gpx b/gpx-rs/data/with_routes.gpx similarity index 100% rename from gpx-rs/engine/data/with_routes.gpx rename to gpx-rs/data/with_routes.gpx diff --git a/gpx-rs/engine/data/with_segments.gpx b/gpx-rs/data/with_segments.gpx similarity index 100% rename from gpx-rs/engine/data/with_segments.gpx rename to gpx-rs/data/with_segments.gpx diff --git a/gpx-rs/engine/data/with_style.gpx b/gpx-rs/data/with_style.gpx similarity index 100% rename from gpx-rs/engine/data/with_style.gpx rename to gpx-rs/data/with_style.gpx diff --git a/gpx-rs/engine/data/with_surface.gpx b/gpx-rs/data/with_surface.gpx similarity index 100% rename from gpx-rs/engine/data/with_surface.gpx rename to gpx-rs/data/with_surface.gpx diff --git a/gpx-rs/engine/data/with_temp.gpx b/gpx-rs/data/with_temp.gpx similarity index 100% rename from gpx-rs/engine/data/with_temp.gpx rename to gpx-rs/data/with_temp.gpx diff --git a/gpx-rs/engine/data/with_time.gpx b/gpx-rs/data/with_time.gpx similarity index 100% rename from gpx-rs/engine/data/with_time.gpx rename to gpx-rs/data/with_time.gpx diff --git a/gpx-rs/engine/data/with_tracks.gpx b/gpx-rs/data/with_tracks.gpx similarity index 100% rename from gpx-rs/engine/data/with_tracks.gpx rename to gpx-rs/data/with_tracks.gpx diff --git a/gpx-rs/engine/data/with_tracks_and_segments.gpx b/gpx-rs/data/with_tracks_and_segments.gpx similarity index 100% rename from gpx-rs/engine/data/with_tracks_and_segments.gpx rename to gpx-rs/data/with_tracks_and_segments.gpx diff --git a/gpx-rs/engine/data/with_waypoint.gpx b/gpx-rs/data/with_waypoint.gpx similarity index 100% rename from gpx-rs/engine/data/with_waypoint.gpx rename to gpx-rs/data/with_waypoint.gpx diff --git a/gpx-rs/engine/Cargo.toml b/gpx-rs/engine/Cargo.toml deleted file mode 100644 index f6e72066f..000000000 --- a/gpx-rs/engine/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "gpx-engine" -version = "0.1.0" -edition = "2024" - -[dependencies] -chrono = "0.4.45" -postcard = { version = "1.1.3", features = ["use-std"] } -quick-xml = "0.42.0" -serde = { version = "1.0.229", features = ["derive"] } -uuid = { version = "1.26.1", features = ["serde", "v4"] } diff --git a/gpx-rs/engine/src/lib.rs b/gpx-rs/engine/src/lib.rs deleted file mode 100644 index 8fef69875..000000000 --- a/gpx-rs/engine/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod core; -mod engine; -mod storage; - -pub use core::*; -pub use engine::*; -pub use storage::*; diff --git a/gpx-rs/src/api/commands.rs b/gpx-rs/src/api/commands.rs new file mode 100644 index 000000000..aeb29e8db --- /dev/null +++ b/gpx-rs/src/api/commands.rs @@ -0,0 +1,554 @@ +//! The functions that edit the files or change the selection. + +use uuid::Uuid; + +use super::dto::*; +use crate::{self as engine, Action, Command, Engine, LngLat, LngLatBounds}; + +/// Executes an action and tells what it did. +pub fn execute(engine: &mut Engine, action: Action) -> Outcome { + match engine.execute(action) { + Ok(outcome) => Outcome::new(&outcome, None), + Err(error) => Outcome::failed(error.to_string()), + } +} + +pub fn edit(engine: &mut Engine, command: Command) -> Outcome { + execute(engine, Action::Edit(command)) +} + +fn file_ids(ids: &[Uuid]) -> Vec { + ids.iter().copied().map(engine::FileId).collect() +} + +// Selection + +/// Selects files. Unknown files are ignored, and selecting nothing deselects everything. +pub fn select(engine: &mut Engine, file_ids_: &[Uuid], mode: engine::SelectMode) -> Outcome { + select_elements( + engine, + engine::Selection::File { + file_ids: file_ids(file_ids_).into_iter().collect(), + }, + mode, + ) +} + +/// Selects elements: they are merged with the selection when it holds elements of the same kind in +/// the same place (same file, same track for segments), otherwise they replace it. +pub fn select_elements( + engine: &mut Engine, + selection: engine::Selection, + mode: engine::SelectMode, +) -> Outcome { + execute(engine, Action::Select { selection, mode }) +} + +/// Selects all the elements of the same kind as the selected ones, in the same place: all the +/// files, the tracks of the file, the segments of the track, the waypoints of the file. All the +/// files when nothing is selected. +pub fn select_all(engine: &mut Engine) -> Outcome { + execute(engine, Action::SelectAll) +} + +/// Moves the selection to the next (`down`) or previous element of the same kind, like the arrow +/// keys do in the file list. With `add` (shift + arrow), the element is added to the selection, +/// otherwise it replaces it. +pub fn arrow_select(engine: &mut Engine, down: bool, add: bool) -> Outcome { + execute(engine, Action::ArrowSelect { down, add }) +} + +pub fn select_tracks( + engine: &mut Engine, + file_id: Uuid, + track_ids: &[Uuid], + mode: engine::SelectMode, +) -> Outcome { + let selection = engine::Selection::Track { + file_id: engine::FileId(file_id), + trk_ids: track_ids.iter().copied().map(engine::TrackId).collect(), + }; + select_elements(engine, selection, mode) +} + +pub fn select_segments( + engine: &mut Engine, + file_id: Uuid, + track_id: Uuid, + segment_ids: &[Uuid], + mode: engine::SelectMode, +) -> Outcome { + let selection = engine::Selection::TrackSegment { + file_id: engine::FileId(file_id), + trk_id: engine::TrackId(track_id), + trkseg_ids: segment_ids + .iter() + .copied() + .map(engine::TrackSegmentId) + .collect(), + }; + select_elements(engine, selection, mode) +} + +/// Selects the node standing for all the waypoints of a file. +pub fn select_waypoint_group( + engine: &mut Engine, + file_id: Uuid, + mode: engine::SelectMode, +) -> Outcome { + let selection = engine::Selection::Waypoints { + file_id: engine::FileId(file_id), + }; + select_elements(engine, selection, mode) +} + +pub fn select_waypoints( + engine: &mut Engine, + file_id: Uuid, + waypoint_ids: &[Uuid], + mode: engine::SelectMode, +) -> Outcome { + let selection = engine::Selection::Waypoint { + file_id: engine::FileId(file_id), + wpt_ids: waypoint_ids + .iter() + .copied() + .map(engine::WaypointId) + .collect(), + }; + select_elements(engine, selection, mode) +} + +// History + +pub fn undo(engine: &mut Engine) -> Outcome { + execute(engine, Action::Undo) +} + +pub fn redo(engine: &mut Engine) -> Outcome { + execute(engine, Action::Redo) +} + +// Files + +/// Creates a file and selects it. With `lng` and `lat`, it starts with a track and a segment that +/// hold that trackpoint (at `ele`, 0 by default). +pub fn new_file( + engine: &mut Engine, + name: &str, + lng: Option, + lat: Option, + ele: Option, +) -> Outcome { + let trackpoint = lng.zip(lat).map(|(lng, lat)| engine::NewTrackpoint { + lng, + lat, + ele: ele.unwrap_or_default(), + }); + edit(engine, Command::New(engine::New { name, trackpoint })) +} + +/// Loads files as a single command (one undo step). The files are in `data`, one after the other, +/// `lengths` being the number of bytes of each one. `names`: the name of each file when its data +/// has none (the name on disk, without extension). The files that cannot be read are skipped, the +/// first one that was read is selected. +pub fn load_files( + engine: &mut Engine, + data: &[u8], + lengths: &[usize], + names: &[String], +) -> Outcome { + if lengths.len() != names.len() { + return Outcome::failed(format!( + "{} lengths for {} names", + lengths.len(), + names.len() + )); + } + let mut files = Vec::with_capacity(names.len()); + let mut offset = 0; + for (length, name) in lengths.iter().zip(names) { + let end = offset + *length; + let Some(data) = data.get(offset..end) else { + return Outcome::failed(format!("the data of {name} is shorter than its length")); + }; + files.push(engine::Load { data, name }); + offset = end; + } + edit(engine, Command::LoadFiles(engine::LoadFiles { files })) +} + +pub fn duplicate(engine: &mut Engine) -> Outcome { + edit(engine, Command::Duplicate(engine::Duplicate)) +} + +/// Deletes the selected elements. With `whole_files`, the files holding the selected elements are +/// deleted instead, even if only a track or a waypoint is selected. +pub fn delete(engine: &mut Engine, whole_files: bool) -> Outcome { + edit(engine, Command::Delete(engine::Delete { whole_files })) +} + +pub fn delete_all(engine: &mut Engine) -> Outcome { + edit(engine, Command::DeleteAll(engine::DeleteAll)) +} + +/// Moves the files to `index` among the other files, in the given order (not undoable). +pub fn reorder(engine: &mut Engine, file_ids_: &[Uuid], index: usize) -> Outcome { + execute( + engine, + Action::Reorder { + file_ids: file_ids(file_ids_), + index, + }, + ) +} + +/// Puts the selected elements in the clipboard. Nothing happens when nothing is selected. +pub fn copy(engine: &mut Engine) -> Outcome { + execute(engine, Action::Copy) +} + +/// Like `copy`, but the elements are moved when they are pasted. +pub fn cut(engine: &mut Engine) -> Outcome { + execute(engine, Action::Cut) +} + +/// Pastes the clipboard according to the selection (see `can_paste`). What is pasted is always the +/// elements as they were when they were copied or cut, even if they were changed or deleted since. +/// It can be pasted several times: what was copied is copied again, what was cut is moved the first +/// time (and the clipboard is not cut after that), then it is copied. +pub fn paste(engine: &mut Engine) -> Outcome { + edit(engine, Command::Paste(engine::Paste)) +} + +/// Moves elements to a place of the file tree, keeping their ids, like dragging and dropping them +/// does. The moved elements are selected. +/// +/// Files can only go among the files; tracks among the files (each becomes a file) or the tracks +/// of a file; segments among the files (each becomes a file), the tracks of a file (each becomes +/// a track) or the segments of a track; waypoints, or all the waypoints of a file, among the +/// waypoints of a file. +pub fn move_elements(engine: &mut Engine, what: Selection, to: MoveTarget) -> Outcome { + edit( + engine, + Command::Move(engine::Move { + what: what.into(), + to: to.into(), + }), + ) +} + +pub fn metadata(engine: &mut Engine, name: &str, desc: &str) -> Outcome { + edit(engine, Command::Metadata(engine::Metadata { name, desc })) +} + +/// Only the given fields are changed. +pub fn style( + engine: &mut Engine, + color: Option<&str>, + opacity: Option, + width: Option, +) -> Outcome { + edit( + engine, + Command::Style(engine::Style { + color, + opacity, + width, + }), + ) +} + +pub fn new_track(engine: &mut Engine) -> Outcome { + edit(engine, Command::NewTrack(engine::NewTrack)) +} + +pub fn new_track_segment(engine: &mut Engine) -> Outcome { + edit(engine, Command::NewTrackSegment(engine::NewTrackSegment)) +} + +// Waypoints. The strings that are empty are the fields that the waypoint does not have. + +/// Adds a waypoint to each selected file (or to the file of the selected elements). +#[allow(clippy::too_many_arguments)] +pub fn new_waypoint( + engine: &mut Engine, + lng: f64, + lat: f64, + ele: f64, + name: &str, + desc: &str, + icon: &str, + link: &str, +) -> Outcome { + edit( + engine, + Command::NewWaypoint(engine::NewWaypoint { + lng, + lat, + ele, + name, + desc, + icon, + link, + }), + ) +} + +/// Moves a waypoint of a file, whatever is selected. +pub fn move_waypoint( + engine: &mut Engine, + file_id: Uuid, + waypoint_id: Uuid, + lng: f64, + lat: f64, + ele: f64, +) -> Outcome { + edit( + engine, + Command::MoveWaypoint(engine::MoveWaypoint { + file_id: engine::FileId(file_id), + waypoint_id: engine::WaypointId(waypoint_id), + lng, + lat, + ele, + }), + ) +} + +/// Deletes a waypoint of a file, whatever is selected. +pub fn delete_waypoint(engine: &mut Engine, file_id: Uuid, waypoint_id: Uuid) -> Outcome { + edit( + engine, + Command::DeleteWaypoint(engine::DeleteWaypoint { + file_id: engine::FileId(file_id), + waypoint_id: engine::WaypointId(waypoint_id), + }), + ) +} + +/// Changes a waypoint of a file, whatever is selected: its name, description, icon, link, +/// position and elevation. +#[allow(clippy::too_many_arguments)] +pub fn update_waypoint( + engine: &mut Engine, + file_id: Uuid, + waypoint_id: Uuid, + lng: f64, + lat: f64, + ele: f64, + name: &str, + desc: &str, + icon: &str, + link: &str, +) -> Outcome { + edit( + engine, + Command::EditWaypoint(engine::EditWaypoint { + file_id: engine::FileId(file_id), + waypoint_id: engine::WaypointId(waypoint_id), + lng, + lat, + ele, + name, + desc, + icon, + link, + }), + ) +} + +// Tools + +pub fn reverse(engine: &mut Engine) -> Outcome { + edit(engine, Command::Reverse(engine::Reverse)) +} + +/// Adds to each selected segment a reversed copy of itself (without repeating its last point), so +/// that it goes back to where it started. +pub fn round_trip(engine: &mut Engine) -> Outcome { + edit(engine, Command::RoundTrip(engine::RoundTrip)) +} + +/// Keeps the trackpoints `start` to `end` (both included) of the selection, counted over its +/// segments one after the other, and removes the others (and what is left empty). +pub fn crop(engine: &mut Engine, start: u32, end: u32) -> Outcome { + if start > end { + return Outcome::failed(format!("the range {start}..{end} is reversed")); + } + edit(engine, Command::Crop(engine::Crop { start, end })) +} + +/// Removes the trackpoints of the selection that are less than `tolerance` meters away from the +/// line of their neighbours, except the ends of the segments. +pub fn reduce(engine: &mut Engine, tolerance: f64) -> Outcome { + edit(engine, Command::Reduce(engine::Reduce { tolerance })) +} + +/// Sets the timestamps of the selection, which starts at `start_time` (ms since the epoch): the +/// trackpoints that have some keep their durations multiplied by `ratio`, the others follow the +/// previous one at `speed` (km/h). +pub fn change_timestamps(engine: &mut Engine, start_time: f64, speed: f64, ratio: f64) -> Outcome { + edit( + engine, + Command::Time(engine::Time { + start_time: start_time as i64, + kind: engine::TimeKind::Change { speed, ratio }, + }), + ) +} + +/// Makes up timestamps for the selection, which starts at `start_time` (ms since the epoch) and +/// lasts `total_time` seconds. +pub fn create_artificial_timestamps( + engine: &mut Engine, + start_time: f64, + total_time: f64, +) -> Outcome { + edit( + engine, + Command::Time(engine::Time { + start_time: start_time as i64, + kind: engine::TimeKind::Artificial { total_time }, + }), + ) +} + +/// Merges the selection into its first element. `remove_gaps` brings the parts that are connected +/// closer in time, if they have timestamps. +pub fn merge(engine: &mut Engine, type_: engine::MergeType, remove_gaps: bool) -> Outcome { + edit(engine, Command::Merge(engine::Merge { type_, remove_gaps })) +} + +pub fn extract(engine: &mut Engine) -> Outcome { + edit(engine, Command::Extract(engine::Extract)) +} + +/// Sets the elevation of the trackpoints of the selection, one elevation per trackpoint. +pub fn elevation(engine: &mut Engine, ele: &[f64]) -> Outcome { + edit(engine, Command::Elevation(engine::Elevation { ele })) +} + +/// Removes the trackpoints and/or waypoints of the selection inside (or outside) a rectangle. +#[allow(clippy::too_many_arguments)] +pub fn clean( + engine: &mut Engine, + west: f64, + south: f64, + east: f64, + north: f64, + type_: engine::CleanType, + trkpt: bool, + wpt: bool, +) -> Outcome { + edit( + engine, + Command::Clean(engine::Clean { + bounds: LngLatBounds { + sw: LngLat { + lng: west, + lat: south, + }, + ne: LngLat { + lng: east, + lat: north, + }, + }, + type_, + trkpt, + wpt, + }), + ) +} + +// Routing +// +// The indices of the trackpoints and of the anchors are the ones of the routing buffers: they only +// mean something for the `revision` they were read with (`routing_revision`), and the commands +// that use them do nothing if the selection is not the one of that revision anymore. + +/// Makes the call `f`, unless the selection is not the one of the revision anymore. +fn at_revision( + engine: &mut Engine, + revision: u32, + f: impl FnOnce(&mut Engine) -> Outcome, +) -> Outcome { + if engine.routing().revision == revision { + f(engine) + } else { + Outcome::failed("the selection changed since the routing buffers were read") + } +} + +/// Replaces the trackpoints `start..end` of the selection by the given ones, in a single segment +/// (a pure insertion if `start == end`, a pure removal without new points). `attributes` holds the +/// surface, highway, SAC scale and MTB scale of the new points, as intervals, and `anchors` the +/// indices among the new points of the ones that become anchors. +#[allow(clippy::too_many_arguments)] +pub fn route( + engine: &mut Engine, + revision: u32, + start: u32, + end: u32, + lng: &[f64], + lat: &[f64], + ele: &[f64], + attributes: &RouteAttributes, + anchors: &[u32], +) -> Outcome { + if lng.len() != lat.len() || lat.len() != ele.len() { + return Outcome::failed("lng, lat and ele differ in length"); + } + at_revision(engine, revision, |engine| { + edit( + engine, + Command::Route(engine::Route { + start, + end, + lng, + lat, + ele, + surface: attributes.surface.view(), + highway: attributes.highway.view(), + sac_scale: attributes.sac_scale.view(), + mtb_scale: attributes.mtb_scale.view(), + anchors, + }), + ) + }) +} + +/// Makes an anchor of the point of the selected segments that is the closest to the coordinates, +/// inserting a trackpoint on the path if none is there. +pub fn insert_anchor(engine: &mut Engine, revision: u32, lng: f64, lat: f64) -> Outcome { + at_revision(engine, revision, |engine| { + edit( + engine, + Command::InsertAnchor(engine::InsertAnchor { lng, lat }), + ) + }) +} + +/// Makes the trackpoint `index` of the selection the start of its segment, which is a loop (the +/// trackpoints before it go to the end, and the loop closes on it). +pub fn change_loop_start(engine: &mut Engine, revision: u32, index: u32) -> Outcome { + at_revision(engine, revision, |engine| { + edit( + engine, + Command::ChangeLoopStart(engine::ChangeLoopStart { index }), + ) + }) +} + +/// Cuts the file, the track or the segment (see `SplitType`) of the trackpoint `at` of the +/// selection in two, there: the trackpoint ends the first part and starts the second one. +pub fn split( + engine: &mut Engine, + revision: u32, + at: u32, + split_type: engine::SplitType, +) -> Outcome { + at_revision(engine, revision, |engine| { + edit(engine, Command::Split(engine::Split { at, split_type })) + }) +} diff --git a/tauri/src/dto.rs b/gpx-rs/src/api/dto.rs similarity index 85% rename from tauri/src/dto.rs rename to gpx-rs/src/api/dto.rs index f05671fc3..b922fe6ad 100644 --- a/tauri/src/dto.rs +++ b/gpx-rs/src/api/dto.rs @@ -1,32 +1,12 @@ -//! What crosses to the frontend, as JSON: the same shapes as the objects of the WASM interface -//! (`gpx-rs/wasm/src/ts.rs` holds their TypeScript types). +//! What crosses to the frontend, as plain data (serde): the shapes of the objects that the frontend +//! reads, which `wasm/src/ts.rs` declares in TypeScript. Ids are hyphenated UUID strings, times +//! are numbers (ms since the epoch), absent fields are skipped. -use gpx_engine::{self as engine, FileId}; +use crate as engine; +use engine::FileId; use serde::{Deserialize, Serialize}; use uuid::Uuid; -/// Bytes that cross as they are: typed arrays (little-endian), GPX files. -pub type Bytes = tauri::ipc::Response; - -pub fn f64s(values: &[f64]) -> Vec { - values.iter().flat_map(|v| v.to_le_bytes()).collect() -} - -pub fn i64s(values: &[i64]) -> Vec { - values.iter().flat_map(|v| v.to_le_bytes()).collect() -} - -pub fn u32s(values: &[u32]) -> Vec { - values.iter().flat_map(|v| v.to_le_bytes()).collect() -} - -/// An array that can be absent: the first byte tells whether it is there, the array follows. -pub fn optional(array: Option>) -> Bytes { - let mut bytes = vec![u8::from(array.is_some())]; - bytes.extend(array.unwrap_or_default()); - Bytes::new(bytes) -} - pub fn ids(items: &[FileId]) -> Vec { items.iter().map(|id| id.0.to_string()).collect() } @@ -46,6 +26,9 @@ pub struct Outcome { removed: Vec, #[serde(skip_serializing_if = "Option::is_none")] error: Option, + /// Whether the files changed in a way that has to be saved (not sent). + #[serde(skip)] + pub saves: bool, } impl Outcome { @@ -60,6 +43,7 @@ impl Outcome { modified: ids(&diff.modified), removed: ids(&diff.removed), error, + saves: outcome.changed && (outcome.diff.is_some() || outcome.order_changed), } } @@ -478,76 +462,6 @@ impl TrackpointDetails { } } -// Enumerations - -#[derive(Clone, Copy, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum SelectMode { - Replace, - Add, - Toggle, -} - -impl From for engine::SelectMode { - fn from(m: SelectMode) -> Self { - match m { - SelectMode::Replace => Self::Replace, - SelectMode::Add => Self::Add, - SelectMode::Toggle => Self::Toggle, - } - } -} - -#[derive(Clone, Copy, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum MergeType { - Connect, - Group, -} - -impl From for engine::MergeType { - fn from(t: MergeType) -> Self { - match t { - MergeType::Connect => Self::Connect, - MergeType::Group => Self::Group, - } - } -} - -#[derive(Clone, Copy, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum SplitType { - Files, - Tracks, - Segments, -} - -impl From for engine::SplitType { - fn from(t: SplitType) -> Self { - match t { - SplitType::Files => Self::Files, - SplitType::Tracks => Self::Tracks, - SplitType::Segments => Self::Segments, - } - } -} - -#[derive(Clone, Copy, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum CleanType { - Inside, - Outside, -} - -impl From for engine::CleanType { - fn from(t: CleanType) -> Self { - match t { - CleanType::Inside => Self::Inside, - CleanType::Outside => Self::Outside, - } - } -} - // Routing /// A category of the trackpoints of a route, as in `RouteCategory` of the engine. diff --git a/gpx-rs/src/api/mod.rs b/gpx-rs/src/api/mod.rs new file mode 100644 index 000000000..41ac8b906 --- /dev/null +++ b/gpx-rs/src/api/mod.rs @@ -0,0 +1,21 @@ +//! The interface to the engine, which the interfaces to the frontend share: the web page +//! (`wasm`) and the desktop app (`tauri`) each expose these functions, and only convert what +//! crosses to their own way of passing data. +//! +//! - `commands`: the functions that edit the files or change the selection. Each returns an +//! `Outcome`: whether anything changed, what changed (to update what the frontend keeps), and why +//! nothing was done when that is so (the command had nothing to do, the data is invalid, an +//! argument is wrong). +//! - `queries`: the functions that read the state of the engine. +//! - `dto`: the data that crosses, as serde types. +//! +//! Ids are `Uuid`s: how the frontend sends them (strings, bytes) is for the interfaces to parse. +//! Coordinates and statistics are slices of numbers, which the interfaces copy to typed arrays. + +pub mod commands; +pub mod dto; +pub mod queries; + +pub use commands::*; +pub use dto::*; +pub use queries::*; diff --git a/gpx-rs/src/api/queries.rs b/gpx-rs/src/api/queries.rs new file mode 100644 index 000000000..fd05ed0f0 --- /dev/null +++ b/gpx-rs/src/api/queries.rs @@ -0,0 +1,234 @@ +//! The functions that read the state of the engine. +//! +//! The files are described by one ordered list of ids and one structure per file, so that the UI +//! can react to what changed only: after each command, its `Outcome` tells which structures to +//! (re)read or drop and whether the order changed. The trackpoints of a segment and the waypoints +//! of a file are in the same order as the coordinates of their buffers. + +use uuid::Uuid; + +use super::dto::*; +use crate::{self as engine, Engine, FileId}; + +/// Ids of the files in display order. +pub fn file_order(engine: &Engine) -> Vec { + ids(engine.order()) +} + +/// Structure of a file, `None` if the id is unknown. +pub fn file_structure(engine: &Engine, file_id: Uuid) -> Option { + engine + .file_structure(&FileId(file_id)) + .map(FileStructure::new) +} + +/// Global statistics of a file, `None` if the id is unknown. +pub fn file_statistics(engine: &Engine, file_id: Uuid) -> Option { + engine + .file_statistics(&FileId(file_id)) + .map(|stats| GlobalStatistics::new(&stats)) +} + +/// The current selection. +pub fn selection(engine: &Engine) -> Selection { + engine.selection().into() +} + +/// What was copied or cut and is waiting to be pasted. +pub fn clipboard(engine: &Engine) -> Option { + engine.clipboard().map(Clipboard::new) +} + +pub fn can_undo(engine: &Engine) -> bool { + engine.can_undo() +} + +pub fn can_redo(engine: &Engine) -> bool { + engine.can_redo() +} + +/// Whether the clipboard can be pasted with the current selection. +pub fn can_paste(engine: &Engine) -> bool { + engine.can_paste() +} + +/// All the data of a waypoint of a file, `None` if it does not exist. +pub fn waypoint(engine: &Engine, file_id: Uuid, waypoint_id: Uuid) -> Option { + engine + .waypoint(&FileId(file_id), &engine::WaypointId(waypoint_id)) + .map(WaypointDetails::new) +} + +/// A trackpoint of a segment of a file, `None` if it does not exist. +pub fn trackpoint( + engine: &Engine, + file_id: Uuid, + segment_id: Uuid, + index: usize, +) -> Option { + engine + .trackpoint(&FileId(file_id), &engine::TrackSegmentId(segment_id), index) + .map(TrackpointDetails::new) +} + +/// A file as GPX (UTF-8 bytes), `None` if it does not exist. The options say which data of the +/// trackpoints to keep, and with `as_route` that the segments are written as routes (`rte`) +/// instead of tracks: the anchors are the route points, and the trackpoints between them the +/// points of their path. +pub fn export_file( + engine: &Engine, + file_id: Uuid, + options: engine::ExportOptions, +) -> Option> { + engine.export(&FileId(file_id), options) +} + +/// The data that some files have, so that the ones that none has are not offered when exporting +/// them: a bit for the time (1), heart rate (2), cadence (4), temperature (8), power (16) and the +/// OpenStreetMap data (32). +pub fn exportable_data(engine: &Engine, file_ids: &[Uuid]) -> u8 { + let ids: Vec = file_ids.iter().copied().map(FileId).collect(); + let data = engine.exportable_data(&ids); + [ + data.time, data.hr, data.cad, data.atemp, data.power, data.osm, + ] + .iter() + .enumerate() + .fold(0, |bits, (i, on)| bits | (u8::from(*on) << i)) +} + +/// For each trackpoint of the selection (numbered as in the statistics), the highest tolerance +/// (m) of `reduce` that keeps it: infinite for the ends of the segments. +pub fn reduction_distances(engine: &Engine) -> Vec { + engine.reduction_distances() +} + +// Coordinates, as flat `[lng, lat, ...]` slices. Compare `rev` / `waypointsRev` of the file +// structures with the previous ones to know which ones actually changed. + +/// Coordinates of the trackpoints of a segment (empty if the id is unknown). +pub fn segment_coordinates(engine: &Engine, segment_id: Uuid) -> &[f64] { + engine.segment_coordinates(&engine::TrackSegmentId(segment_id)) +} + +/// Coordinates of the waypoints of a file (empty if the id is unknown). +pub fn waypoint_coordinates(engine: &Engine, file_id: Uuid) -> &[f64] { + engine.waypoint_coordinates(&FileId(file_id)) +} + +// Statistics of the selection + +/// Global statistics of the whole selection (the buffers below cover it). +pub fn selection_statistics(engine: &Engine) -> GlobalStatistics { + GlobalStatistics::new(&engine.statistics().global) +} + +/// Global statistics of the trackpoints from `start` to `end` (both included) of the selection, +/// as indexed by the buffers. `None` when the range is not in the selection. +pub fn slice_statistics(engine: &Engine, start: usize, end: usize) -> Option { + engine + .statistics() + .slice(start, end) + .map(|stats| GlobalStatistics::new(&stats)) +} + +macro_rules! buffers { + ($($name:ident),*) => {$( + pub fn $name(engine: &Engine) -> &[f64] { + &engine.statistics().$name + } + )*}; +} + +// One entry per trackpoint of the selection. +buffers!( + total_distance, + elevation_gain, + elevation_loss, + slope, + slope_segment_slope, + slope_segment_distance, + lng, + lat, + ele +); + +macro_rules! optional_buffers { + ($($name:ident: $type:ty),*) => {$( + /// `None` when no trackpoint of the selection has the value, else an entry per trackpoint, + /// the missing ones being NaN (`NO_TIME` for the timestamps, 0 before the first trackpoint + /// that has them for the cumulative distances and durations). + pub fn $name(engine: &Engine) -> Option<&[$type]> { + engine.statistics().$name.as_deref() + } + )*}; +} + +optional_buffers!(moving_distance: f64, speed: f64, hr: f64, cad: f64, atemp: f64, power: f64); +optional_buffers!(total_time: i64, moving_time: i64); + +/// Timestamps in milliseconds since the epoch (`NO_TIME` when missing), see `optional_buffers`. +pub fn timestamps(engine: &Engine) -> Option<&[i64]> { + engine.statistics().time.as_deref() +} + +// Surface, highway, SAC scale and MTB scale of the trackpoints, as intervals of trackpoints that +// have the same value: `_starts` holds the index of the first trackpoint of each interval, +// and `_values` its value (0 when unknown, else 1 + the index in the names of the category). +// A value that is unknown for every trackpoint is a single interval. +macro_rules! categories { + ($starts:ident, $values:ident, $names:ident, $field:ident) => { + pub fn $starts(engine: &Engine) -> &[u32] { + &engine.statistics().$field.starts + } + + pub fn $values(engine: &Engine) -> &[u8] { + &engine.statistics().$field.values + } + + /// The names of the category, in the order of their codes. The list only grows: a name + /// keeps its index. + pub fn $names(engine: &Engine) -> &[String] { + engine.categories().$field.names() + } + }; +} + +categories!(surface_starts, surface_values, surfaces, surface); +categories!(highway_starts, highway_values, highways, highway); +categories!(sac_scale_starts, sac_scale_values, sac_scales, sac_scale); +categories!(mtb_scale_starts, mtb_scale_values, mtb_scales, mtb_scale); + +// The anchors of the routing tool among the trackpoints of the selection (numbered as in the +// statistics). The ends of the segments are always anchors. + +/// The index in the selection of each anchor. +pub fn anchor_indices(engine: &Engine) -> &[u32] { + &engine.routing().anchor_indices +} + +/// The lowest map zoom level at which each anchor is shown. +pub fn anchor_zooms(engine: &Engine) -> &[u8] { + &engine.routing().anchor_zooms +} + +/// Index in the selection of the first trackpoint of each selected segment that has some. +pub fn segment_starts(engine: &Engine) -> &[u32] { + &engine.routing().segment_starts +} + +/// The id of each selected segment that has trackpoints, as `segment_starts`. +pub fn segment_ids(engine: &Engine) -> Vec { + engine + .routing() + .segment_ids + .iter() + .map(|id| id.0.to_string()) + .collect() +} + +/// Changes when the selected segments or their trackpoints change: the indices in the selection +/// are only valid while it stays the same. +pub fn routing_revision(engine: &Engine) -> u32 { + engine.routing().revision +} diff --git a/gpx-rs/engine/src/core/algorithm/anchors.rs b/gpx-rs/src/core/algorithm/anchors.rs similarity index 100% rename from gpx-rs/engine/src/core/algorithm/anchors.rs rename to gpx-rs/src/core/algorithm/anchors.rs diff --git a/gpx-rs/engine/src/core/algorithm/mod.rs b/gpx-rs/src/core/algorithm/mod.rs similarity index 100% rename from gpx-rs/engine/src/core/algorithm/mod.rs rename to gpx-rs/src/core/algorithm/mod.rs diff --git a/gpx-rs/engine/src/core/algorithm/reverse.rs b/gpx-rs/src/core/algorithm/reverse.rs similarity index 100% rename from gpx-rs/engine/src/core/algorithm/reverse.rs rename to gpx-rs/src/core/algorithm/reverse.rs diff --git a/gpx-rs/engine/src/core/algorithm/simplify.rs b/gpx-rs/src/core/algorithm/simplify.rs similarity index 100% rename from gpx-rs/engine/src/core/algorithm/simplify.rs rename to gpx-rs/src/core/algorithm/simplify.rs diff --git a/gpx-rs/engine/src/core/algorithm/smooth.rs b/gpx-rs/src/core/algorithm/smooth.rs similarity index 100% rename from gpx-rs/engine/src/core/algorithm/smooth.rs rename to gpx-rs/src/core/algorithm/smooth.rs diff --git a/gpx-rs/engine/src/core/algorithm/timestamps.rs b/gpx-rs/src/core/algorithm/timestamps.rs similarity index 100% rename from gpx-rs/engine/src/core/algorithm/timestamps.rs rename to gpx-rs/src/core/algorithm/timestamps.rs diff --git a/gpx-rs/engine/src/core/gpx/categories.rs b/gpx-rs/src/core/gpx/categories.rs similarity index 100% rename from gpx-rs/engine/src/core/gpx/categories.rs rename to gpx-rs/src/core/gpx/categories.rs diff --git a/gpx-rs/engine/src/core/gpx/chunk.rs b/gpx-rs/src/core/gpx/chunk.rs similarity index 100% rename from gpx-rs/engine/src/core/gpx/chunk.rs rename to gpx-rs/src/core/gpx/chunk.rs diff --git a/gpx-rs/engine/src/core/gpx/chunked.rs b/gpx-rs/src/core/gpx/chunked.rs similarity index 100% rename from gpx-rs/engine/src/core/gpx/chunked.rs rename to gpx-rs/src/core/gpx/chunked.rs diff --git a/gpx-rs/engine/src/core/gpx/common.rs b/gpx-rs/src/core/gpx/common.rs similarity index 100% rename from gpx-rs/engine/src/core/gpx/common.rs rename to gpx-rs/src/core/gpx/common.rs diff --git a/gpx-rs/engine/src/core/gpx/file.rs b/gpx-rs/src/core/gpx/file.rs similarity index 100% rename from gpx-rs/engine/src/core/gpx/file.rs rename to gpx-rs/src/core/gpx/file.rs diff --git a/gpx-rs/engine/src/core/gpx/mod.rs b/gpx-rs/src/core/gpx/mod.rs similarity index 100% rename from gpx-rs/engine/src/core/gpx/mod.rs rename to gpx-rs/src/core/gpx/mod.rs diff --git a/gpx-rs/engine/src/core/gpx/segment.rs b/gpx-rs/src/core/gpx/segment.rs similarity index 100% rename from gpx-rs/engine/src/core/gpx/segment.rs rename to gpx-rs/src/core/gpx/segment.rs diff --git a/gpx-rs/engine/src/core/gpx/track.rs b/gpx-rs/src/core/gpx/track.rs similarity index 100% rename from gpx-rs/engine/src/core/gpx/track.rs rename to gpx-rs/src/core/gpx/track.rs diff --git a/gpx-rs/engine/src/core/gpx/trackpoint.rs b/gpx-rs/src/core/gpx/trackpoint.rs similarity index 100% rename from gpx-rs/engine/src/core/gpx/trackpoint.rs rename to gpx-rs/src/core/gpx/trackpoint.rs diff --git a/gpx-rs/engine/src/core/gpx/waypoint.rs b/gpx-rs/src/core/gpx/waypoint.rs similarity index 100% rename from gpx-rs/engine/src/core/gpx/waypoint.rs rename to gpx-rs/src/core/gpx/waypoint.rs diff --git a/gpx-rs/engine/src/core/gpx/waypoints.rs b/gpx-rs/src/core/gpx/waypoints.rs similarity index 100% rename from gpx-rs/engine/src/core/gpx/waypoints.rs rename to gpx-rs/src/core/gpx/waypoints.rs diff --git a/gpx-rs/engine/src/core/io/mod.rs b/gpx-rs/src/core/io/mod.rs similarity index 100% rename from gpx-rs/engine/src/core/io/mod.rs rename to gpx-rs/src/core/io/mod.rs diff --git a/gpx-rs/engine/src/core/io/parse.rs b/gpx-rs/src/core/io/parse.rs similarity index 100% rename from gpx-rs/engine/src/core/io/parse.rs rename to gpx-rs/src/core/io/parse.rs diff --git a/gpx-rs/engine/src/core/io/write.rs b/gpx-rs/src/core/io/write.rs similarity index 100% rename from gpx-rs/engine/src/core/io/write.rs rename to gpx-rs/src/core/io/write.rs diff --git a/gpx-rs/engine/src/core/mod.rs b/gpx-rs/src/core/mod.rs similarity index 100% rename from gpx-rs/engine/src/core/mod.rs rename to gpx-rs/src/core/mod.rs diff --git a/gpx-rs/engine/src/core/statistics/compute.rs b/gpx-rs/src/core/statistics/compute.rs similarity index 100% rename from gpx-rs/engine/src/core/statistics/compute.rs rename to gpx-rs/src/core/statistics/compute.rs diff --git a/gpx-rs/engine/src/core/statistics/mod.rs b/gpx-rs/src/core/statistics/mod.rs similarity index 100% rename from gpx-rs/engine/src/core/statistics/mod.rs rename to gpx-rs/src/core/statistics/mod.rs diff --git a/gpx-rs/engine/src/core/statistics/utils.rs b/gpx-rs/src/core/statistics/utils.rs similarity index 100% rename from gpx-rs/engine/src/core/statistics/utils.rs rename to gpx-rs/src/core/statistics/utils.rs diff --git a/gpx-rs/engine/src/core/utils.rs b/gpx-rs/src/core/utils.rs similarity index 100% rename from gpx-rs/engine/src/core/utils.rs rename to gpx-rs/src/core/utils.rs diff --git a/gpx-rs/engine/src/engine/action.rs b/gpx-rs/src/engine/action.rs similarity index 100% rename from gpx-rs/engine/src/engine/action.rs rename to gpx-rs/src/engine/action.rs diff --git a/gpx-rs/engine/src/engine/command/apply.rs b/gpx-rs/src/engine/command/apply.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/apply.rs rename to gpx-rs/src/engine/command/apply.rs diff --git a/gpx-rs/engine/src/engine/command/edit/metadata.rs b/gpx-rs/src/engine/command/edit/metadata.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/edit/metadata.rs rename to gpx-rs/src/engine/command/edit/metadata.rs diff --git a/gpx-rs/engine/src/engine/command/edit/mod.rs b/gpx-rs/src/engine/command/edit/mod.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/edit/mod.rs rename to gpx-rs/src/engine/command/edit/mod.rs diff --git a/gpx-rs/engine/src/engine/command/edit/new_track.rs b/gpx-rs/src/engine/command/edit/new_track.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/edit/new_track.rs rename to gpx-rs/src/engine/command/edit/new_track.rs diff --git a/gpx-rs/engine/src/engine/command/edit/new_track_segment.rs b/gpx-rs/src/engine/command/edit/new_track_segment.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/edit/new_track_segment.rs rename to gpx-rs/src/engine/command/edit/new_track_segment.rs diff --git a/gpx-rs/engine/src/engine/command/edit/style.rs b/gpx-rs/src/engine/command/edit/style.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/edit/style.rs rename to gpx-rs/src/engine/command/edit/style.rs diff --git a/gpx-rs/engine/src/engine/command/file/delete.rs b/gpx-rs/src/engine/command/file/delete.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/file/delete.rs rename to gpx-rs/src/engine/command/file/delete.rs diff --git a/gpx-rs/engine/src/engine/command/file/delete_all.rs b/gpx-rs/src/engine/command/file/delete_all.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/file/delete_all.rs rename to gpx-rs/src/engine/command/file/delete_all.rs diff --git a/gpx-rs/engine/src/engine/command/file/delete_waypoint.rs b/gpx-rs/src/engine/command/file/delete_waypoint.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/file/delete_waypoint.rs rename to gpx-rs/src/engine/command/file/delete_waypoint.rs diff --git a/gpx-rs/engine/src/engine/command/file/duplicate.rs b/gpx-rs/src/engine/command/file/duplicate.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/file/duplicate.rs rename to gpx-rs/src/engine/command/file/duplicate.rs diff --git a/gpx-rs/engine/src/engine/command/file/load.rs b/gpx-rs/src/engine/command/file/load.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/file/load.rs rename to gpx-rs/src/engine/command/file/load.rs diff --git a/gpx-rs/engine/src/engine/command/file/mod.rs b/gpx-rs/src/engine/command/file/mod.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/file/mod.rs rename to gpx-rs/src/engine/command/file/mod.rs diff --git a/gpx-rs/engine/src/engine/command/file/move_elements.rs b/gpx-rs/src/engine/command/file/move_elements.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/file/move_elements.rs rename to gpx-rs/src/engine/command/file/move_elements.rs diff --git a/gpx-rs/engine/src/engine/command/file/new.rs b/gpx-rs/src/engine/command/file/new.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/file/new.rs rename to gpx-rs/src/engine/command/file/new.rs diff --git a/gpx-rs/engine/src/engine/command/file/paste.rs b/gpx-rs/src/engine/command/file/paste.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/file/paste.rs rename to gpx-rs/src/engine/command/file/paste.rs diff --git a/gpx-rs/engine/src/engine/command/file/transfer.rs b/gpx-rs/src/engine/command/file/transfer.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/file/transfer.rs rename to gpx-rs/src/engine/command/file/transfer.rs diff --git a/gpx-rs/engine/src/engine/command/fixture.rs b/gpx-rs/src/engine/command/fixture.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/fixture.rs rename to gpx-rs/src/engine/command/fixture.rs diff --git a/gpx-rs/engine/src/engine/command/mod.rs b/gpx-rs/src/engine/command/mod.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/mod.rs rename to gpx-rs/src/engine/command/mod.rs diff --git a/gpx-rs/engine/src/engine/command/pattern/copy.rs b/gpx-rs/src/engine/command/pattern/copy.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/pattern/copy.rs rename to gpx-rs/src/engine/command/pattern/copy.rs diff --git a/gpx-rs/engine/src/engine/command/pattern/mod.rs b/gpx-rs/src/engine/command/pattern/mod.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/pattern/mod.rs rename to gpx-rs/src/engine/command/pattern/mod.rs diff --git a/gpx-rs/engine/src/engine/command/pattern/produce.rs b/gpx-rs/src/engine/command/pattern/produce.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/pattern/produce.rs rename to gpx-rs/src/engine/command/pattern/produce.rs diff --git a/gpx-rs/engine/src/engine/command/pattern/update_segments.rs b/gpx-rs/src/engine/command/pattern/update_segments.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/pattern/update_segments.rs rename to gpx-rs/src/engine/command/pattern/update_segments.rs diff --git a/gpx-rs/engine/src/engine/command/pattern/update_selected.rs b/gpx-rs/src/engine/command/pattern/update_selected.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/pattern/update_selected.rs rename to gpx-rs/src/engine/command/pattern/update_selected.rs diff --git a/gpx-rs/engine/src/engine/command/pattern/update_waypoint.rs b/gpx-rs/src/engine/command/pattern/update_waypoint.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/pattern/update_waypoint.rs rename to gpx-rs/src/engine/command/pattern/update_waypoint.rs diff --git a/gpx-rs/engine/src/engine/command/tools/change_loop_start.rs b/gpx-rs/src/engine/command/tools/change_loop_start.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/tools/change_loop_start.rs rename to gpx-rs/src/engine/command/tools/change_loop_start.rs diff --git a/gpx-rs/engine/src/engine/command/tools/clean.rs b/gpx-rs/src/engine/command/tools/clean.rs similarity index 99% rename from gpx-rs/engine/src/engine/command/tools/clean.rs rename to gpx-rs/src/engine/command/tools/clean.rs index c543d6ebb..3cf3715c7 100644 --- a/gpx-rs/engine/src/engine/command/tools/clean.rs +++ b/gpx-rs/src/engine/command/tools/clean.rs @@ -5,7 +5,8 @@ use crate::{ TrackSegment, TrackSegmentId, Trackpoint, Waypoint, delete_waypoints, update_selected, }; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "camelCase")] pub enum CleanType { Inside, Outside, diff --git a/gpx-rs/engine/src/engine/command/tools/crop.rs b/gpx-rs/src/engine/command/tools/crop.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/tools/crop.rs rename to gpx-rs/src/engine/command/tools/crop.rs diff --git a/gpx-rs/engine/src/engine/command/tools/edit_waypoint.rs b/gpx-rs/src/engine/command/tools/edit_waypoint.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/tools/edit_waypoint.rs rename to gpx-rs/src/engine/command/tools/edit_waypoint.rs diff --git a/gpx-rs/engine/src/engine/command/tools/elevation.rs b/gpx-rs/src/engine/command/tools/elevation.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/tools/elevation.rs rename to gpx-rs/src/engine/command/tools/elevation.rs diff --git a/gpx-rs/engine/src/engine/command/tools/extract.rs b/gpx-rs/src/engine/command/tools/extract.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/tools/extract.rs rename to gpx-rs/src/engine/command/tools/extract.rs diff --git a/gpx-rs/engine/src/engine/command/tools/insert_anchor.rs b/gpx-rs/src/engine/command/tools/insert_anchor.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/tools/insert_anchor.rs rename to gpx-rs/src/engine/command/tools/insert_anchor.rs diff --git a/gpx-rs/engine/src/engine/command/tools/merge.rs b/gpx-rs/src/engine/command/tools/merge.rs similarity index 99% rename from gpx-rs/engine/src/engine/command/tools/merge.rs rename to gpx-rs/src/engine/command/tools/merge.rs index 130ce5a68..9916ee929 100644 --- a/gpx-rs/engine/src/engine/command/tools/merge.rs +++ b/gpx-rs/src/engine/command/tools/merge.rs @@ -5,7 +5,8 @@ use crate::{ TrackSegment, TrackSegmentId, Waypoint, Waypoints, replace_trackpoints, }; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "camelCase")] pub enum MergeType { /// The trackpoints of the elements are connected in a single segment. Connect, diff --git a/gpx-rs/engine/src/engine/command/tools/mod.rs b/gpx-rs/src/engine/command/tools/mod.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/tools/mod.rs rename to gpx-rs/src/engine/command/tools/mod.rs diff --git a/gpx-rs/engine/src/engine/command/tools/move_waypoint.rs b/gpx-rs/src/engine/command/tools/move_waypoint.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/tools/move_waypoint.rs rename to gpx-rs/src/engine/command/tools/move_waypoint.rs diff --git a/gpx-rs/engine/src/engine/command/tools/new_waypoint.rs b/gpx-rs/src/engine/command/tools/new_waypoint.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/tools/new_waypoint.rs rename to gpx-rs/src/engine/command/tools/new_waypoint.rs diff --git a/gpx-rs/engine/src/engine/command/tools/reduce.rs b/gpx-rs/src/engine/command/tools/reduce.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/tools/reduce.rs rename to gpx-rs/src/engine/command/tools/reduce.rs diff --git a/gpx-rs/engine/src/engine/command/tools/reverse.rs b/gpx-rs/src/engine/command/tools/reverse.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/tools/reverse.rs rename to gpx-rs/src/engine/command/tools/reverse.rs diff --git a/gpx-rs/engine/src/engine/command/tools/round_trip.rs b/gpx-rs/src/engine/command/tools/round_trip.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/tools/round_trip.rs rename to gpx-rs/src/engine/command/tools/round_trip.rs diff --git a/gpx-rs/engine/src/engine/command/tools/route.rs b/gpx-rs/src/engine/command/tools/route.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/tools/route.rs rename to gpx-rs/src/engine/command/tools/route.rs diff --git a/gpx-rs/engine/src/engine/command/tools/split.rs b/gpx-rs/src/engine/command/tools/split.rs similarity index 99% rename from gpx-rs/engine/src/engine/command/tools/split.rs rename to gpx-rs/src/engine/command/tools/split.rs index 73f7e93bb..2130b073f 100644 --- a/gpx-rs/engine/src/engine/command/tools/split.rs +++ b/gpx-rs/src/engine/command/tools/split.rs @@ -8,7 +8,8 @@ use crate::{ use super::route::find_target; /// What is cut in two where the trackpoint is. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "camelCase")] pub enum SplitType { /// The file becomes two files, which both have the waypoints (copies, in the second one). Files, diff --git a/gpx-rs/engine/src/engine/command/tools/time.rs b/gpx-rs/src/engine/command/tools/time.rs similarity index 100% rename from gpx-rs/engine/src/engine/command/tools/time.rs rename to gpx-rs/src/engine/command/tools/time.rs diff --git a/gpx-rs/engine/src/engine/derived/coordinates_cache.rs b/gpx-rs/src/engine/derived/coordinates_cache.rs similarity index 100% rename from gpx-rs/engine/src/engine/derived/coordinates_cache.rs rename to gpx-rs/src/engine/derived/coordinates_cache.rs diff --git a/gpx-rs/engine/src/engine/derived/file_structure.rs b/gpx-rs/src/engine/derived/file_structure.rs similarity index 100% rename from gpx-rs/engine/src/engine/derived/file_structure.rs rename to gpx-rs/src/engine/derived/file_structure.rs diff --git a/gpx-rs/engine/src/engine/derived/file_structure_cache.rs b/gpx-rs/src/engine/derived/file_structure_cache.rs similarity index 100% rename from gpx-rs/engine/src/engine/derived/file_structure_cache.rs rename to gpx-rs/src/engine/derived/file_structure_cache.rs diff --git a/gpx-rs/engine/src/engine/derived/mod.rs b/gpx-rs/src/engine/derived/mod.rs similarity index 100% rename from gpx-rs/engine/src/engine/derived/mod.rs rename to gpx-rs/src/engine/derived/mod.rs diff --git a/gpx-rs/engine/src/engine/derived/routing_buffer.rs b/gpx-rs/src/engine/derived/routing_buffer.rs similarity index 100% rename from gpx-rs/engine/src/engine/derived/routing_buffer.rs rename to gpx-rs/src/engine/derived/routing_buffer.rs diff --git a/gpx-rs/engine/src/engine/derived/statistics_buffer.rs b/gpx-rs/src/engine/derived/statistics_buffer.rs similarity index 100% rename from gpx-rs/engine/src/engine/derived/statistics_buffer.rs rename to gpx-rs/src/engine/derived/statistics_buffer.rs diff --git a/gpx-rs/engine/src/engine/derived/statistics_cache.rs b/gpx-rs/src/engine/derived/statistics_cache.rs similarity index 100% rename from gpx-rs/engine/src/engine/derived/statistics_cache.rs rename to gpx-rs/src/engine/derived/statistics_cache.rs diff --git a/gpx-rs/engine/src/engine/mod.rs b/gpx-rs/src/engine/mod.rs similarity index 100% rename from gpx-rs/engine/src/engine/mod.rs rename to gpx-rs/src/engine/mod.rs diff --git a/gpx-rs/engine/src/engine/outcome.rs b/gpx-rs/src/engine/outcome.rs similarity index 100% rename from gpx-rs/engine/src/engine/outcome.rs rename to gpx-rs/src/engine/outcome.rs diff --git a/gpx-rs/engine/src/engine/state/clipboard.rs b/gpx-rs/src/engine/state/clipboard.rs similarity index 100% rename from gpx-rs/engine/src/engine/state/clipboard.rs rename to gpx-rs/src/engine/state/clipboard.rs diff --git a/gpx-rs/engine/src/engine/state/history.rs b/gpx-rs/src/engine/state/history.rs similarity index 100% rename from gpx-rs/engine/src/engine/state/history.rs rename to gpx-rs/src/engine/state/history.rs diff --git a/gpx-rs/engine/src/engine/state/mod.rs b/gpx-rs/src/engine/state/mod.rs similarity index 100% rename from gpx-rs/engine/src/engine/state/mod.rs rename to gpx-rs/src/engine/state/mod.rs diff --git a/gpx-rs/engine/src/engine/state/order.rs b/gpx-rs/src/engine/state/order.rs similarity index 100% rename from gpx-rs/engine/src/engine/state/order.rs rename to gpx-rs/src/engine/state/order.rs diff --git a/gpx-rs/engine/src/engine/state/selection.rs b/gpx-rs/src/engine/state/selection.rs similarity index 99% rename from gpx-rs/engine/src/engine/state/selection.rs rename to gpx-rs/src/engine/state/selection.rs index 15baa6e9c..bbd7cc108 100644 --- a/gpx-rs/engine/src/engine/state/selection.rs +++ b/gpx-rs/src/engine/state/selection.rs @@ -3,7 +3,8 @@ use std::collections::HashSet; use crate::{FileId, Files, TrackId, TrackSegmentId, WaypointId}; /// How a new selection combines with the current one. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "camelCase")] pub enum SelectMode { /// The new selection replaces the current one. Replace, diff --git a/gpx-rs/engine/src/engine/tests.rs b/gpx-rs/src/engine/tests.rs similarity index 100% rename from gpx-rs/engine/src/engine/tests.rs rename to gpx-rs/src/engine/tests.rs diff --git a/gpx-rs/src/lib.rs b/gpx-rs/src/lib.rs new file mode 100644 index 000000000..3cd415c8c --- /dev/null +++ b/gpx-rs/src/lib.rs @@ -0,0 +1,10 @@ +//! The GPX engine, and `api`, what the interfaces to it (the web page, the desktop app) share. + +pub mod api; +mod core; +mod engine; +mod storage; + +pub use core::*; +pub use engine::*; +pub use storage::*; diff --git a/gpx-rs/engine/src/storage/codec/mod.rs b/gpx-rs/src/storage/codec/mod.rs similarity index 100% rename from gpx-rs/engine/src/storage/codec/mod.rs rename to gpx-rs/src/storage/codec/mod.rs diff --git a/gpx-rs/engine/src/storage/codec/v1.rs b/gpx-rs/src/storage/codec/v1.rs similarity index 100% rename from gpx-rs/engine/src/storage/codec/v1.rs rename to gpx-rs/src/storage/codec/v1.rs diff --git a/gpx-rs/engine/src/storage/memory.rs b/gpx-rs/src/storage/memory.rs similarity index 100% rename from gpx-rs/engine/src/storage/memory.rs rename to gpx-rs/src/storage/memory.rs diff --git a/gpx-rs/engine/src/storage/mod.rs b/gpx-rs/src/storage/mod.rs similarity index 100% rename from gpx-rs/engine/src/storage/mod.rs rename to gpx-rs/src/storage/mod.rs diff --git a/gpx-rs/engine/src/storage/persister.rs b/gpx-rs/src/storage/persister.rs similarity index 100% rename from gpx-rs/engine/src/storage/persister.rs rename to gpx-rs/src/storage/persister.rs diff --git a/gpx-rs/wasm/src/command/routing.rs b/gpx-rs/wasm/src/command/routing.rs deleted file mode 100644 index 6fa5e1d76..000000000 --- a/gpx-rs/wasm/src/command/routing.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! The commands that refer to trackpoints by their index in the routing buffers. - -use gpx_engine::{self as engine, Command}; -use js_sys::{Array, Uint8Array, Uint32Array}; -use wasm_bindgen::prelude::*; - -use crate::convert::*; -use crate::ids::*; -use crate::queries::routing::with_routing; -use crate::session::*; -use crate::ts::*; -use crate::types::*; - -// Routing -// -// The indices of the trackpoints and of the anchors are the ones of the routing buffers: they only -// mean something for the `revision` they were read with (`routing_revision`), and the commands -// that use them do nothing if the selection is not the one of that revision anymore. - -/// Makes the call `f`, unless the selection is not the one of the revision anymore. -pub(crate) fn at_revision(revision: u32, f: impl FnOnce() -> Outcome) -> Outcome { - if with_routing(|r| r.revision == revision).unwrap_or(false) { - f() - } else { - fail("the selection changed since the routing buffers were read") - } -} - -/// A category of the trackpoints of a route: see `RouteCategory` in `engine`. -pub(crate) struct ParsedCategory { - starts: Vec, - values: Vec, - names: Vec, -} - -impl ParsedCategory { - fn parse(attributes: &JsValue, key: &str) -> Option { - let Some(category) = property(attributes, key) else { - return Some(Self { - starts: vec![], - values: vec![], - names: vec![], - }); - }; - Some(Self { - starts: property(&category, "starts")? - .unchecked_into::() - .to_vec(), - values: property(&category, "values")? - .unchecked_into::() - .to_vec(), - names: Array::from(&property(&category, "names")?) - .iter() - .map(|name| name.as_string()) - .collect::>()?, - }) - } - - fn view(&self) -> engine::RouteCategory<'_> { - engine::RouteCategory { - starts: &self.starts, - values: &self.values, - names: &self.names, - } - } -} - -/// Replaces the trackpoints `start..end` of the selection by the given ones, in a single segment -/// (see `RoutingBuffer`). A pure insertion if `start == end`, a pure removal without new points. -/// `attributes` holds the surface, highway, SAC scale and MTB scale of the new points, as -/// intervals, and `anchors` the indices among the new points of the ones that become anchors. -/// Does nothing, and says why, if the revision of the routing buffers is not the given one. -#[wasm_bindgen] -#[allow(clippy::too_many_arguments)] -pub fn route( - revision: u32, - start: u32, - end: u32, - lng: &[f64], - lat: &[f64], - ele: &[f64], - attributes: RouteAttributes, - anchors: &[u32], -) -> Outcome { - let parse = |key| ParsedCategory::parse(&attributes, key); - let (Some(surface), Some(highway), Some(sac_scale), Some(mtb_scale)) = ( - parse("surface"), - parse("highway"), - parse("sacScale"), - parse("mtbScale"), - ) else { - return fail("the attributes of the route are not valid"); - }; - if !same_len(lng, lat, ele) { - return fail("lng, lat and ele differ in length"); - } - at_revision(revision, || { - edit(Command::Route(engine::Route { - start, - end, - lng, - lat, - ele, - surface: surface.view(), - highway: highway.view(), - sac_scale: sac_scale.view(), - mtb_scale: mtb_scale.view(), - anchors, - })) - }) -} - -/// Makes an anchor of the point of the selected segments that is the closest to the coordinates, -/// inserting a trackpoint on the path if none is there. Does nothing, and says why, if the -/// revision of the routing buffers is not the given one. -#[wasm_bindgen] -pub fn insert_anchor(revision: u32, lng: f64, lat: f64) -> Outcome { - at_revision(revision, || { - edit(Command::InsertAnchor(engine::InsertAnchor { lng, lat })) - }) -} - -/// Makes the trackpoint `index` of the selection the start of its segment, which is a loop (the -/// trackpoints before it go to the end, and the loop closes on it). Does nothing, and says why, if the revision of the -/// routing buffers is not the given one. -#[wasm_bindgen] -pub fn change_loop_start(revision: u32, index: u32) -> Outcome { - at_revision(revision, || { - edit(Command::ChangeLoopStart(engine::ChangeLoopStart { index })) - }) -} - -/// Cuts the file, the track or the segment (see `SplitType`) of the trackpoint `at` of the -/// selection in two, there: the trackpoint ends the first part and starts the second one. Does -/// nothing, and returns false, if the revision of the routing buffers is not the given one. -#[wasm_bindgen] -pub fn split(revision: u32, at: u32, split_type: SplitType) -> Outcome { - at_revision(revision, || { - edit(Command::Split(engine::Split { - at, - split_type: split_type.into(), - })) - }) -} diff --git a/gpx-rs/wasm/src/command/waypoints.rs b/gpx-rs/wasm/src/command/waypoints.rs deleted file mode 100644 index b6058d0bc..000000000 --- a/gpx-rs/wasm/src/command/waypoints.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! Creating, editing, moving and deleting waypoints. - -use gpx_engine::{self as engine, Command}; -use wasm_bindgen::prelude::*; - -use crate::ts::Outcome; - -use crate::ids::*; -use crate::session::*; - -#[wasm_bindgen] -pub fn new_waypoint( - lng: f64, - lat: f64, - ele: f64, - name: &str, - desc: &str, - icon: &str, - link: &str, -) -> Outcome { - edit(Command::NewWaypoint(engine::NewWaypoint { - lng, - lat, - ele, - name, - desc, - icon, - link, - })) -} - -/// Moves a waypoint of a file, whatever is selected. -#[wasm_bindgen] -pub fn move_waypoint(file_id: &str, waypoint_id: &str, lng: f64, lat: f64, ele: f64) -> Outcome { - match (parse_file_id(file_id), parse_waypoint_id(waypoint_id)) { - (Some(file_id), Some(waypoint_id)) => edit(Command::MoveWaypoint(engine::MoveWaypoint { - file_id, - waypoint_id, - lng, - lat, - ele, - })), - _ => fail("the file or waypoint id is not a UUID"), - } -} - -/// Deletes a waypoint of a file, whatever is selected. -#[wasm_bindgen] -pub fn delete_waypoint(file_id: &str, waypoint_id: &str) -> Outcome { - match (parse_file_id(file_id), parse_waypoint_id(waypoint_id)) { - (Some(file_id), Some(waypoint_id)) => { - edit(Command::DeleteWaypoint(engine::DeleteWaypoint { - file_id, - waypoint_id, - })) - } - _ => fail("the file or waypoint id is not a UUID"), - } -} - -/// Changes a waypoint of a file, whatever is selected: its name, description, icon, link, position -/// and elevation. The strings that are empty remove the field. -#[wasm_bindgen] -#[allow(clippy::too_many_arguments)] -pub fn update_waypoint( - file_id: &str, - waypoint_id: &str, - lng: f64, - lat: f64, - ele: f64, - name: &str, - desc: &str, - icon: &str, - link: &str, -) -> Outcome { - match (parse_file_id(file_id), parse_waypoint_id(waypoint_id)) { - (Some(file_id), Some(waypoint_id)) => edit(Command::EditWaypoint(engine::EditWaypoint { - file_id, - waypoint_id, - lng, - lat, - ele, - name, - desc, - icon, - link, - })), - _ => fail("the file or waypoint id is not a UUID"), - } -} diff --git a/gpx-rs/wasm/src/convert.rs b/gpx-rs/wasm/src/convert.rs deleted file mode 100644 index 0c98d65d7..000000000 --- a/gpx-rs/wasm/src/convert.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! Turning engine types into JS objects and back. - -use gpx_engine::{self as engine, FileId}; -use js_sys::{Array, Object, Reflect}; -use wasm_bindgen::prelude::*; - -use crate::ts::*; - -pub(crate) fn property(object: &JsValue, key: &str) -> Option { - Reflect::get(object, &key.into()) - .ok() - .filter(|value| !value.is_undefined()) -} - -pub(crate) fn uuid_property(object: &JsValue, key: &str) -> Option { - uuid::Uuid::parse_str(&property(object, key)?.as_string()?).ok() -} - -pub(crate) fn uuids_property(object: &JsValue, key: &str) -> Option> { - Array::from(&property(object, key)?) - .iter() - .map(|id| uuid::Uuid::parse_str(&id.as_string()?).ok()) - .collect() -} - -/// The inverse of `selection_object`. -pub(crate) fn parse_selection(value: &JsValue) -> Option { - use engine::Selection as S; - let file_id = || uuid_property(value, "fileId").map(FileId); - Some(match property(value, "type")?.as_string()?.as_str() { - "empty" => S::Empty, - "file" => S::File { - file_ids: uuids_property(value, "fileIds")? - .into_iter() - .map(FileId) - .collect(), - }, - "track" => S::Track { - file_id: file_id()?, - trk_ids: uuids_property(value, "trackIds")? - .into_iter() - .map(engine::TrackId) - .collect(), - }, - "segment" => S::TrackSegment { - file_id: file_id()?, - trk_id: engine::TrackId(uuid_property(value, "trackId")?), - trkseg_ids: uuids_property(value, "segmentIds")? - .into_iter() - .map(engine::TrackSegmentId) - .collect(), - }, - "waypoints" => S::Waypoints { - file_id: file_id()?, - }, - "waypoint" => S::Waypoint { - file_id: file_id()?, - wpt_ids: uuids_property(value, "waypointIds")? - .into_iter() - .map(engine::WaypointId) - .collect(), - }, - _ => return None, - }) -} - -pub(crate) fn parse_move_target(value: &JsValue) -> Option { - use engine::MoveTarget as T; - // a negative index is the start, an infinite one is the end - let index = property(value, "index")?.as_f64()?.max(0.0) as usize; - let file_id = || uuid_property(value, "fileId").map(FileId); - Some(match property(value, "type")?.as_string()?.as_str() { - "files" => T::Files { index }, - "tracks" => T::Tracks { - file_id: file_id()?, - index, - }, - "segments" => T::Segments { - file_id: file_id()?, - trk_id: engine::TrackId(uuid_property(value, "trackId")?), - index, - }, - "waypoints" => T::Waypoints { - file_id: file_id()?, - index, - }, - _ => return None, - }) -} - -pub(crate) fn set(object: &Object, key: &str, value: impl Into) { - Reflect::set(object, &key.into(), &value.into()).unwrap(); -} - -pub(crate) fn array(items: &[T], f: impl Fn(&T) -> JsValue) -> Array { - items.iter().map(f).collect() -} - -pub(crate) fn ids(items: &[FileId]) -> Array { - array(items, |id| id.0.to_string().into()) -} - -pub(crate) fn named_node(id: uuid::Uuid, name: Option<&str>) -> Object { - let node = Object::new(); - set(&node, "id", id.to_string()); - if let Some(name) = name { - set(&node, "name", name); - } - node -} - -pub(crate) fn structure_object(file: &engine::FileStructure) -> Object { - let node = named_node(file.id.0, Some(&file.name)); - if let Some(desc) = &file.desc { - set(&node, "desc", desc.as_str()); - } - set( - &node, - "tracks", - array(&file.tracks, |trk| { - let node = named_node(trk.id.0, trk.name.as_deref()); - if let Some(desc) = &trk.desc { - set(&node, "desc", desc.as_str()); - } - if let Some(color) = &trk.color { - set(&node, "color", color.as_str()); - } - if let Some(opacity) = trk.opacity { - set(&node, "opacity", opacity); - } - if let Some(width) = trk.width { - set(&node, "width", width); - } - set( - &node, - "segments", - array(&trk.segments, |seg| { - let node = named_node(seg.id.0, None); - set(&node, "rev", seg.rev_id.0.to_string()); - set(&node, "length", seg.len as f64); - node.into() - }), - ); - node.into() - }), - ); - set( - &node, - "waypoints", - array(&file.waypoints, |wpt| { - let node = named_node(wpt.id.0, wpt.name.as_deref()); - if let Some(sym) = &wpt.sym { - set(&node, "sym", sym.as_str()); - } - node.into() - }), - ); - set(&node, "waypointsRev", file.wpt_rev_id.0.to_string()); - node -} - -pub(crate) fn statistics_object(stats: &engine::GlobalStatistics) -> GlobalStatistics { - let object = Object::new(); - let optional = |key: &str, value: Option| { - if let Some(value) = value { - set(&object, key, value); - } - }; - let seconds = |ms: Option| ms.map(|ms| ms as f64 / 1000.0); - set(&object, "totalDistance", stats.total_distance); - optional("movingDistance", stats.moving_distance); - optional("totalTime", seconds(stats.total_time)); - optional("movingTime", seconds(stats.moving_time)); - set(&object, "elevationGain", stats.elevation_gain); - set(&object, "elevationLoss", stats.elevation_loss); - optional("startTime", stats.start_time.map(|t| t as f64)); - optional("endTime", stats.end_time.map(|t| t as f64)); - optional("totalSpeed", stats.total_speed()); - optional("movingSpeed", stats.moving_speed()); - let (sw, ne) = (&stats.bounds.sw, &stats.bounds.ne); - if sw.lng <= ne.lng && sw.lat <= ne.lat { - let bounds = Object::new(); - set(&bounds, "west", sw.lng); - set(&bounds, "south", sw.lat); - set(&bounds, "east", ne.lng); - set(&bounds, "north", ne.lat); - set(&object, "bounds", bounds); - } - object.unchecked_into() -} - -pub(crate) fn uuids( - items: impl IntoIterator, - uuid: impl Fn(T) -> uuid::Uuid, -) -> Array { - items - .into_iter() - .map(|item| JsValue::from(uuid(item).to_string())) - .collect() -} - -pub(crate) fn selection_object(selection: &engine::Selection) -> Object { - use engine::Selection as S; - let object = Object::new(); - match selection { - S::Empty => set(&object, "type", "empty"), - S::File { file_ids } => { - set(&object, "type", "file"); - set(&object, "fileIds", uuids(file_ids, |id| id.0)); - } - S::Track { file_id, trk_ids } => { - set(&object, "type", "track"); - set(&object, "fileId", file_id.0.to_string()); - set(&object, "trackIds", uuids(trk_ids, |id| id.0)); - } - S::TrackSegment { - file_id, - trk_id, - trkseg_ids, - } => { - set(&object, "type", "segment"); - set(&object, "fileId", file_id.0.to_string()); - set(&object, "trackId", trk_id.0.to_string()); - set(&object, "segmentIds", uuids(trkseg_ids, |id| id.0)); - } - S::Waypoints { file_id } => { - set(&object, "type", "waypoints"); - set(&object, "fileId", file_id.0.to_string()); - } - S::Waypoint { file_id, wpt_ids } => { - set(&object, "type", "waypoint"); - set(&object, "fileId", file_id.0.to_string()); - set(&object, "waypointIds", uuids(wpt_ids, |id| id.0)); - } - } - object -} - -/// The outcome of a call. `error` is what it says about why nothing was done, if that is so. -pub(crate) fn outcome_object(outcome: &engine::Outcome, error: Option<&str>) -> Outcome { - let object = Object::new(); - let diff = outcome.diff.clone().unwrap_or_default(); - set(&object, "changed", outcome.changed); - set(&object, "orderChanged", outcome.order_changed); - set(&object, "selectionChanged", outcome.selection_changed); - set(&object, "clipboardChanged", outcome.clipboard_changed); - set(&object, "added", ids(&diff.added)); - set(&object, "modified", ids(&diff.modified)); - set(&object, "removed", ids(&diff.removed)); - if let Some(error) = error { - set(&object, "error", error); - } - object.unchecked_into() -} diff --git a/gpx-rs/wasm/src/ids.rs b/gpx-rs/wasm/src/ids.rs deleted file mode 100644 index 2f48e8d01..000000000 --- a/gpx-rs/wasm/src/ids.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Ids, which cross as strings (one id) or as concatenated 16-byte UUIDs (several). - -use gpx_engine::{self as engine, FileId}; - -pub(crate) fn uuid_list(bytes: &[u8]) -> Option> { - let (chunks, rest) = bytes.as_chunks::<16>(); - rest.is_empty() - .then(|| chunks.iter().map(|c| uuid::Uuid::from_bytes(*c)).collect()) -} - -pub(crate) fn file_ids(bytes: &[u8]) -> Option> { - uuid_list(bytes).map(|ids| ids.into_iter().map(FileId).collect()) -} - -pub(crate) fn parse_file_id(id: &str) -> Option { - uuid::Uuid::parse_str(id).ok().map(FileId) -} - -pub(crate) fn same_len(a: &[f64], b: &[f64], c: &[f64]) -> bool { - a.len() == b.len() && b.len() == c.len() -} - -pub(crate) fn parse_waypoint_id(id: &str) -> Option { - uuid::Uuid::parse_str(id).ok().map(engine::WaypointId) -} diff --git a/gpx-rs/wasm/src/queries/coordinates.rs b/gpx-rs/wasm/src/queries/coordinates.rs deleted file mode 100644 index 5448b7122..000000000 --- a/gpx-rs/wasm/src/queries/coordinates.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! The coordinates of the trackpoints and waypoints, as flat typed arrays. - -use gpx_engine::{self as engine, Engine, FileId}; -use js_sys::Float64Array; -use wasm_bindgen::prelude::*; - -use crate::session::*; - -// Coordinates buffers -// -// Like the statistics buffers, these are copies. They are flat `[lng, lat, ...]` arrays. Compare `rev` / `waypointsRev` of the file structures with the previous ones to know which -// buffers actually changed. - -pub(crate) fn coordinates_array(f: impl FnOnce(&Engine) -> Option<&[f64]>) -> Float64Array { - ENGINE.with(|engine| match engine.borrow().as_ref().and_then(f) { - Some(coordinates) => Float64Array::from(coordinates), - None => Float64Array::new_with_length(0), - }) -} - -/// Coordinates of the trackpoints of a segment (empty if the id is unknown). -#[wasm_bindgen] -pub fn segment_coordinates(segment_id: &str) -> Float64Array { - match uuid::Uuid::parse_str(segment_id) { - Ok(id) => coordinates_array(|e| Some(e.segment_coordinates(&engine::TrackSegmentId(id)))), - Err(_) => Float64Array::new_with_length(0), - } -} - -/// Coordinates of the waypoints of a file (empty if the id is unknown). -#[wasm_bindgen] -pub fn waypoint_coordinates(file_id: &str) -> Float64Array { - match uuid::Uuid::parse_str(file_id) { - Ok(id) => coordinates_array(|e| Some(e.waypoint_coordinates(&FileId(id)))), - Err(_) => Float64Array::new_with_length(0), - } -} diff --git a/gpx-rs/wasm/src/queries/files.rs b/gpx-rs/wasm/src/queries/files.rs deleted file mode 100644 index 400f06061..000000000 --- a/gpx-rs/wasm/src/queries/files.rs +++ /dev/null @@ -1,219 +0,0 @@ -//! The files, their structure and statistics, the selection and the clipboard. - -// File order and structures -// -// The files are described by one ordered list of ids and one structure object per file, so that -// the UI can react to what changed only: after each action, `last_update` tells which -// structures to (re)read or drop and whether the order changed. Ids are UUID strings. The -// trackpoints of a segment and the waypoints of a file are in the same order as the coordinates -// of their buffers (see below). - -use gpx_engine::{self as engine, FileId}; -use js_sys::Object; -use wasm_bindgen::prelude::*; - -use crate::convert::*; -use crate::ids::*; -use crate::queries::statistics::with_stats; -use crate::session::*; -use crate::ts::*; - -/// Ids of the files in display order. -#[wasm_bindgen] -pub fn file_order() -> FileOrder { - let order = with_engine(|e| ids(e.order())).unwrap_or_default(); - order.unchecked_into() -} - -/// Structure of a file, `undefined` if the id is unknown. -#[wasm_bindgen] -pub fn file_structure(file_id: &str) -> FileStructure { - let structure = uuid::Uuid::parse_str(file_id) - .ok() - .and_then(|id| { - with_engine(|e| e.file_structure(&FileId(id)).map(structure_object)).flatten() - }) - .map_or(JsValue::UNDEFINED, JsValue::from); - structure.unchecked_into() -} - -/// Global statistics of a file, `undefined` if the id is unknown. -#[wasm_bindgen] -pub fn file_statistics(file_id: &str) -> GlobalStatistics { - uuid::Uuid::parse_str(file_id) - .ok() - .and_then(|id| with_engine(|e| e.file_statistics(&FileId(id))).flatten()) - .map_or(JsValue::UNDEFINED.unchecked_into(), |stats| { - statistics_object(&stats) - }) -} - -/// Global statistics of the whole selection (the arrays of the statistics buffers cover it). -#[wasm_bindgen] -pub fn selection_statistics() -> GlobalStatistics { - with_stats(|buffer| statistics_object(&buffer.global)) - .unwrap_or_else(|| JsValue::UNDEFINED.unchecked_into()) -} - -/// A file as GPX (UTF-8 bytes), `undefined` if it does not exist. The options say which data of the trackpoints -/// to keep, and with `as_route` that the segments are written as routes (`rte`) instead of tracks: -/// the anchors are the route points, and the trackpoints between them the points of their path. -#[wasm_bindgen] -#[allow(clippy::too_many_arguments)] -pub fn export_file( - file_id: &str, - time: bool, - hr: bool, - cad: bool, - atemp: bool, - power: bool, - osm: bool, - as_route: bool, -) -> Option> { - let id = parse_file_id(file_id)?; - let options = engine::ExportOptions { - time, - hr, - cad, - atemp, - power, - osm, - as_route, - }; - with_engine(|e| e.export(&id, options)).flatten() -} - -/// The data that some files have, so that the ones that none has are not offered when exporting -/// them: a bit for the time (1), heart rate (2), cadence (4), temperature (8), power (16) and the -/// OpenStreetMap data (32). `file_ids` are concatenated UUIDs. -#[wasm_bindgen] -pub fn exportable_data(file_ids: &[u8]) -> u8 { - let Some(ids) = self::file_ids(file_ids) else { - return 0; - }; - let data = with_engine(|e| e.exportable_data(&ids)).unwrap_or(engine::ExportOptions::NONE); - [ - data.time, data.hr, data.cad, data.atemp, data.power, data.osm, - ] - .iter() - .enumerate() - .fold(0, |bits, (i, on)| bits | (u8::from(*on) << i)) -} - -/// Global statistics of the trackpoints from `start` to `end` (both included) of the selection, -/// as indexed by the statistics buffers. `undefined` when the range is not in the selection. -#[wasm_bindgen] -pub fn slice_statistics(start: usize, end: usize) -> GlobalStatistics { - with_stats(|buffer| buffer.slice(start, end)) - .flatten() - .map_or(JsValue::UNDEFINED.unchecked_into(), |stats| { - statistics_object(&stats) - }) -} - -/// The current selection (ids are UUID strings). -#[wasm_bindgen] -pub fn selection() -> Selection { - with_engine(|e| selection_object(e.selection())) - .unwrap_or_default() - .unchecked_into() -} - -/// All the data of a waypoint of a file, `undefined` if it does not exist. -#[wasm_bindgen] -pub fn waypoint(file_id: &str, waypoint_id: &str) -> WaypointDetails { - uuid::Uuid::parse_str(file_id) - .ok() - .zip(uuid::Uuid::parse_str(waypoint_id).ok()) - .and_then(|(file_id, id)| { - with_engine(|e| { - e.waypoint(&FileId(file_id), &engine::WaypointId(id)) - .map(|wpt| { - let object = Object::new(); - set(&object, "id", wpt.id.0.to_string()); - set(&object, "lng", wpt.coordinates.lng); - set(&object, "lat", wpt.coordinates.lat); - set(&object, "ele", wpt.ele); - if let Some(time) = wpt.time { - set(&object, "time", time as f64); - } - for (key, value) in [ - ("name", &wpt.name), - ("desc", &wpt.desc), - ("cmt", &wpt.cmt), - ("sym", &wpt.sym), - ] { - if let Some(value) = value { - set(&object, key, value.as_str()); - } - } - if !wpt.links.is_empty() { - set( - &object, - "links", - array(&wpt.links, |link| { - let link_object = Object::new(); - set(&link_object, "href", link.href.as_str()); - if let Some(text) = &link.text { - set(&link_object, "text", text.as_str()); - } - link_object.into() - }), - ); - } - JsValue::from(object) - }) - }) - .flatten() - }) - .unwrap_or(JsValue::UNDEFINED) - .unchecked_into() -} - -/// A trackpoint of a segment of a file, `undefined` if it does not exist. -#[wasm_bindgen] -pub fn trackpoint(file_id: &str, segment_id: &str, index: usize) -> TrackpointDetails { - parse_file_id(file_id) - .zip(uuid::Uuid::parse_str(segment_id).ok()) - .and_then(|(file_id, segment_id)| { - with_engine(|e| { - e.trackpoint(&file_id, &engine::TrackSegmentId(segment_id), index) - .map(|pt| { - let object = Object::new(); - set(&object, "lng", pt.coordinates.lng); - set(&object, "lat", pt.coordinates.lat); - set(&object, "ele", pt.ele); - if let Some(time) = pt.time { - set(&object, "time", time as f64); - } - JsValue::from(object) - }) - }) - .flatten() - }) - .unwrap_or(JsValue::UNDEFINED) - .unchecked_into() -} - -/// What was copied or cut and is waiting to be pasted, `undefined` if nothing: the kind of the -/// elements and their ids (UUID strings). -#[wasm_bindgen] -pub fn clipboard() -> Clipboard { - with_engine(|e| { - e.clipboard().map_or(JsValue::UNDEFINED, |clipboard| { - let object = Object::new(); - let (kind, ids) = match clipboard.content.ids() { - engine::ClipboardIds::Files(ids) => ("files", uuids(ids, |id| id.0)), - engine::ClipboardIds::Tracks(ids) => ("tracks", uuids(ids, |id| id.0)), - engine::ClipboardIds::Segments(ids) => ("segments", uuids(ids, |id| id.0)), - engine::ClipboardIds::Waypoints(ids) => ("waypoints", uuids(ids, |id| id.0)), - }; - set(&object, "type", kind); - set(&object, "ids", ids); - set(&object, "cut", clipboard.cut); - object.into() - }) - }) - .unwrap_or(JsValue::UNDEFINED) - .unchecked_into() -} diff --git a/gpx-rs/wasm/src/queries/statistics.rs b/gpx-rs/wasm/src/queries/statistics.rs deleted file mode 100644 index 76a645dba..000000000 --- a/gpx-rs/wasm/src/queries/statistics.rs +++ /dev/null @@ -1,142 +0,0 @@ -//! The statistics of the selection, as typed arrays, and the categories they refer to. - -use gpx_engine as engine; -use js_sys::{BigInt64Array, Float64Array, Uint8Array, Uint32Array}; -use wasm_bindgen::prelude::*; - -use crate::convert::*; -use crate::session::*; -use crate::ts::*; - -// Statistics buffers -// -// Each getter returns a copy of the buffer, as a new JS typed array. - -pub(crate) fn with_stats(f: impl FnOnce(&engine::StatisticsBuffer) -> T) -> Option { - ENGINE.with(|engine| engine.borrow().as_ref().map(|e| f(e.statistics()))) -} - -macro_rules! stats_getter { - ($name:ident, $array:ident) => { - #[wasm_bindgen] - pub fn $name() -> $array { - with_stats(|s| $array::from(&s.$name[..])).unwrap_or_else(|| $array::new_with_length(0)) - } - }; -} - -stats_getter!(total_distance, Float64Array); -stats_getter!(elevation_gain, Float64Array); -stats_getter!(elevation_loss, Float64Array); -stats_getter!(slope, Float64Array); -stats_getter!(slope_segment_slope, Float64Array); -stats_getter!(slope_segment_distance, Float64Array); -stats_getter!(lng, Float64Array); -stats_getter!(lat, Float64Array); -stats_getter!(ele, Float64Array); - -// The optional values are `undefined` when no trackpoint of the selection has one, else they have -// an entry per trackpoint, the missing ones being NaN (`NO_TIME` for the timestamps, 0 before the -// first trackpoint that has them for the cumulative distances and durations). -macro_rules! optional_stats_getter { - ($name:ident, $field:ident, $array:ident) => { - #[wasm_bindgen] - pub fn $name() -> Option<$array> { - with_stats(|s| s.$field.as_deref().map($array::from)).flatten() - } - }; -} - -optional_stats_getter!(moving_distance, moving_distance, Float64Array); -// Durations since the start of the selection, in milliseconds. -optional_stats_getter!(total_time, total_time, BigInt64Array); -optional_stats_getter!(moving_time, moving_time, BigInt64Array); -optional_stats_getter!(speed, speed, Float64Array); -// Timestamps in milliseconds since the epoch (`NO_TIME`, the smallest i64, when missing). -optional_stats_getter!(timestamps, time, BigInt64Array); - -/// Value of the timestamps of the trackpoints that have none. -#[wasm_bindgen] -pub fn no_time() -> i64 { - engine::NO_TIME -} - -optional_stats_getter!(hr, hr, Float64Array); -optional_stats_getter!(cad, cad, Float64Array); -optional_stats_getter!(atemp, atemp, Float64Array); -optional_stats_getter!(power, power, Float64Array); - -// Surface, highway, SAC scale and MTB scale of the trackpoints, as intervals of trackpoints that -// have the same value: `_starts` holds the index of the first trackpoint of each interval, -// and `_values` its value (0 when unknown, else 1 + the index in `surfaces()`, -// `highways()`, `sac_scales()` or `mtb_scales()`). A value that is unknown for every trackpoint is -// a single interval. -macro_rules! intervals_getters { - ($starts:ident, $values:ident, $field:ident) => { - #[wasm_bindgen] - pub fn $starts() -> Uint32Array { - with_stats(|s| Uint32Array::from(&s.$field.starts[..])) - .unwrap_or_else(|| Uint32Array::new_with_length(0)) - } - - #[wasm_bindgen] - pub fn $values() -> Uint8Array { - with_stats(|s| Uint8Array::from(&s.$field.values[..])) - .unwrap_or_else(|| Uint8Array::new_with_length(0)) - } - }; -} - -intervals_getters!(surface_starts, surface_values, surface); -intervals_getters!(highway_starts, highway_values, highway); -intervals_getters!(sac_scale_starts, sac_scale_values, sac_scale); -intervals_getters!(mtb_scale_starts, mtb_scale_values, mtb_scale); - -/// The names of the surfaces, in the order of their codes (see `surface_values`). The list only grows: -/// a name keeps its index. -#[wasm_bindgen] -pub fn surfaces() -> StringList { - with_engine(|e| array(e.categories().surface.names(), |name| name.as_str().into())) - .unwrap_or_default() - .unchecked_into() -} - -/// The names of the highways, in the order of their codes (see `highway_values`). -#[wasm_bindgen] -pub fn highways() -> StringList { - with_engine(|e| array(e.categories().highway.names(), |name| name.as_str().into())) - .unwrap_or_default() - .unchecked_into() -} - -/// The names of the SAC scales, in the order of their codes (see `sac_scale_values`). -#[wasm_bindgen] -pub fn sac_scales() -> StringList { - with_engine(|e| { - array(e.categories().sac_scale.names(), |name| { - name.as_str().into() - }) - }) - .unwrap_or_default() - .unchecked_into() -} - -/// The names of the MTB scales, in the order of their codes (see `mtb_scale_values`). -#[wasm_bindgen] -pub fn mtb_scales() -> StringList { - with_engine(|e| { - array(e.categories().mtb_scale.names(), |name| { - name.as_str().into() - }) - }) - .unwrap_or_default() - .unchecked_into() -} - -/// For each trackpoint of the selection (numbered as in the statistics), the highest tolerance -/// (m) of `reduce` that keeps it: infinite for the ends of the segments. -#[wasm_bindgen] -pub fn reduction_distances() -> Float64Array { - with_engine(|e| Float64Array::from(&e.reduction_distances()[..])) - .unwrap_or_else(|| Float64Array::new_with_length(0)) -} diff --git a/tauri/Cargo.toml b/tauri/Cargo.toml index 39013bcc2..e8395fa8d 100644 --- a/tauri/Cargo.toml +++ b/tauri/Cargo.toml @@ -19,6 +19,5 @@ tauri = { version = "2", features = [] } serde = { version = "1", features = ["derive"] } serde_json = "1" uuid = { version = "1", features = ["serde"] } -gpx-engine = { path = "../gpx-rs/engine" } +gpx-engine = { path = "../gpx-rs" } -[workspace] diff --git a/tauri/src/bytes.rs b/tauri/src/bytes.rs new file mode 100644 index 000000000..0bd9d45e6 --- /dev/null +++ b/tauri/src/bytes.rs @@ -0,0 +1,40 @@ +//! Typed arrays and files cross as raw bytes (little-endian), not as JSON. + +pub type Bytes = tauri::ipc::Response; + +pub fn f64s(values: &[f64]) -> Bytes { + Bytes::new( + values + .iter() + .flat_map(|v| v.to_le_bytes()) + .collect::>(), + ) +} + +pub fn u32s(values: &[u32]) -> Bytes { + Bytes::new( + values + .iter() + .flat_map(|v| v.to_le_bytes()) + .collect::>(), + ) +} + +pub fn u8s(values: &[u8]) -> Bytes { + Bytes::new(values.to_vec()) +} + +/// An array that can be absent: the first byte tells whether it is there, the array follows. +pub fn optional(array: Option>) -> Bytes { + let mut bytes = vec![u8::from(array.is_some())]; + bytes.extend(array.unwrap_or_default()); + Bytes::new(bytes) +} + +pub fn f64_bytes(values: &[f64]) -> Vec { + values.iter().flat_map(|v| v.to_le_bytes()).collect() +} + +pub fn i64_bytes(values: &[i64]) -> Vec { + values.iter().flat_map(|v| v.to_le_bytes()).collect() +} diff --git a/tauri/src/commands/edit.rs b/tauri/src/commands/edit.rs index c02397403..dd30604fd 100644 --- a/tauri/src/commands/edit.rs +++ b/tauri/src/commands/edit.rs @@ -1,22 +1,15 @@ //! Editing the metadata, the style, the structure and the waypoints of the files, and the tools: //! reversing, cropping, reducing, timestamps, merging, cleaning... -use gpx_engine::{self as engine, Command, LngLat, LngLatBounds}; +use gpx_engine::{api, CleanType, MergeType, SplitType}; use tauri::State; use uuid::Uuid; -use crate::dto::*; -use crate::state::{act, edit, AppState}; +use crate::state::AppState; #[tauri::command(async)] -pub fn metadata(state: State, name: String, desc: String) -> Outcome { - edit!( - state, - Command::Metadata(engine::Metadata { - name: &name, - desc: &desc, - }) - ) +pub fn metadata(state: State, name: String, desc: String) -> api::Outcome { + state.run(move |e| api::metadata(e, &name, &desc)) } #[tauri::command(async)] @@ -25,25 +18,18 @@ pub fn style( color: Option, opacity: Option, width: Option, -) -> Outcome { - edit!( - state, - Command::Style(engine::Style { - color: color.as_deref(), - opacity, - width, - }) - ) +) -> api::Outcome { + state.run(move |e| api::style(e, color.as_deref(), opacity, width)) } #[tauri::command(async)] -pub fn new_track(state: State) -> Outcome { - edit!(state, Command::NewTrack(engine::NewTrack)) +pub fn new_track(state: State) -> api::Outcome { + state.run(api::new_track) } #[tauri::command(async)] -pub fn new_track_segment(state: State) -> Outcome { - edit!(state, Command::NewTrackSegment(engine::NewTrackSegment)) +pub fn new_track_segment(state: State) -> api::Outcome { + state.run(api::new_track_segment) } // Waypoints @@ -59,19 +45,8 @@ pub fn new_waypoint( desc: String, icon: String, link: String, -) -> Outcome { - edit!( - state, - Command::NewWaypoint(engine::NewWaypoint { - lng, - lat, - ele, - name: &name, - desc: &desc, - icon: &icon, - link: &link, - }) - ) +) -> api::Outcome { + state.run(move |e| api::new_waypoint(e, lng, lat, ele, &name, &desc, &icon, &link)) } #[tauri::command(async)] @@ -82,28 +57,13 @@ pub fn move_waypoint( lng: f64, lat: f64, ele: f64, -) -> Outcome { - edit!( - state, - Command::MoveWaypoint(engine::MoveWaypoint { - file_id: engine::FileId(file_id), - waypoint_id: engine::WaypointId(waypoint_id), - lng, - lat, - ele, - }) - ) +) -> api::Outcome { + state.run(move |e| api::move_waypoint(e, file_id, waypoint_id, lng, lat, ele)) } #[tauri::command(async)] -pub fn delete_waypoint(state: State, file_id: Uuid, waypoint_id: Uuid) -> Outcome { - edit!( - state, - Command::DeleteWaypoint(engine::DeleteWaypoint { - file_id: engine::FileId(file_id), - waypoint_id: engine::WaypointId(waypoint_id), - }) - ) +pub fn delete_waypoint(state: State, file_id: Uuid, waypoint_id: Uuid) -> api::Outcome { + state.run(move |e| api::delete_waypoint(e, file_id, waypoint_id)) } #[tauri::command(async)] @@ -119,46 +79,43 @@ pub fn update_waypoint( desc: String, icon: String, link: String, -) -> Outcome { - edit!( - state, - Command::EditWaypoint(engine::EditWaypoint { - file_id: engine::FileId(file_id), - waypoint_id: engine::WaypointId(waypoint_id), +) -> api::Outcome { + state.run(move |e| { + api::update_waypoint( + e, + file_id, + waypoint_id, lng, lat, ele, - name: &name, - desc: &desc, - icon: &icon, - link: &link, - }) - ) + &name, + &desc, + &icon, + &link, + ) + }) } // Tools #[tauri::command(async)] -pub fn reverse(state: State) -> Outcome { - edit!(state, Command::Reverse(engine::Reverse)) +pub fn reverse(state: State) -> api::Outcome { + state.run(api::reverse) } #[tauri::command(async)] -pub fn round_trip(state: State) -> Outcome { - edit!(state, Command::RoundTrip(engine::RoundTrip)) +pub fn round_trip(state: State) -> api::Outcome { + state.run(api::round_trip) } #[tauri::command(async)] -pub fn crop(state: State, start: u32, end: u32) -> Outcome { - if start > end { - return Outcome::failed(format!("the range {start}..{end} is reversed")); - } - edit!(state, Command::Crop(engine::Crop { start, end })) +pub fn crop(state: State, start: u32, end: u32) -> api::Outcome { + state.run(move |e| api::crop(e, start, end)) } #[tauri::command(async)] -pub fn reduce(state: State, tolerance: f64) -> Outcome { - edit!(state, Command::Reduce(engine::Reduce { tolerance })) +pub fn reduce(state: State, tolerance: f64) -> api::Outcome { + state.run(move |e| api::reduce(e, tolerance)) } #[tauri::command(async)] @@ -167,14 +124,8 @@ pub fn change_timestamps( start_time: f64, speed: f64, ratio: f64, -) -> Outcome { - edit!( - state, - Command::Time(engine::Time { - start_time: start_time as i64, - kind: engine::TimeKind::Change { speed, ratio }, - }) - ) +) -> api::Outcome { + state.run(move |e| api::change_timestamps(e, start_time, speed, ratio)) } #[tauri::command(async)] @@ -182,35 +133,23 @@ pub fn create_artificial_timestamps( state: State, start_time: f64, total_time: f64, -) -> Outcome { - edit!( - state, - Command::Time(engine::Time { - start_time: start_time as i64, - kind: engine::TimeKind::Artificial { total_time }, - }) - ) +) -> api::Outcome { + state.run(move |e| api::create_artificial_timestamps(e, start_time, total_time)) } #[tauri::command(async)] -pub fn merge(state: State, merge_type: MergeType, remove_gaps: bool) -> Outcome { - edit!( - state, - Command::Merge(engine::Merge { - type_: merge_type.into(), - remove_gaps, - }) - ) +pub fn merge(state: State, merge_type: MergeType, remove_gaps: bool) -> api::Outcome { + state.run(move |e| api::merge(e, merge_type, remove_gaps)) } #[tauri::command(async)] -pub fn extract(state: State) -> Outcome { - edit!(state, Command::Extract(engine::Extract)) +pub fn extract(state: State) -> api::Outcome { + state.run(api::extract) } #[tauri::command(async)] -pub fn elevation(state: State, ele: Vec) -> Outcome { - edit!(state, Command::Elevation(engine::Elevation { ele: &ele })) +pub fn elevation(state: State, ele: Vec) -> api::Outcome { + state.run(move |e| api::elevation(e, &ele)) } #[tauri::command(async)] @@ -224,23 +163,56 @@ pub fn clean( clean_type: CleanType, trkpt: bool, wpt: bool, -) -> Outcome { - edit!( - state, - Command::Clean(engine::Clean { - bounds: LngLatBounds { - sw: LngLat { - lng: west, - lat: south, - }, - ne: LngLat { - lng: east, - lat: north, - }, - }, - type_: clean_type.into(), - trkpt, - wpt, - }) - ) +) -> api::Outcome { + state.run(move |e| api::clean(e, west, south, east, north, clean_type, trkpt, wpt)) +} + +// Routing + +#[tauri::command(async)] +#[allow(clippy::too_many_arguments)] +pub fn route( + state: State, + revision: u32, + start: u32, + end: u32, + lng: Vec, + lat: Vec, + ele: Vec, + attributes: api::RouteAttributes, + anchors: Vec, +) -> api::Outcome { + state.run(move |e| { + api::route( + e, + revision, + start, + end, + &lng, + &lat, + &ele, + &attributes, + &anchors, + ) + }) +} + +#[tauri::command(async)] +pub fn insert_anchor(state: State, revision: u32, lng: f64, lat: f64) -> api::Outcome { + state.run(move |e| api::insert_anchor(e, revision, lng, lat)) +} + +#[tauri::command(async)] +pub fn change_loop_start(state: State, revision: u32, index: u32) -> api::Outcome { + state.run(move |e| api::change_loop_start(e, revision, index)) +} + +#[tauri::command(async)] +pub fn split( + state: State, + revision: u32, + at: u32, + split_type: SplitType, +) -> api::Outcome { + state.run(move |e| api::split(e, revision, at, split_type)) } diff --git a/tauri/src/commands/files.rs b/tauri/src/commands/files.rs index b35c96755..01572f8f9 100644 --- a/tauri/src/commands/files.rs +++ b/tauri/src/commands/files.rs @@ -1,12 +1,11 @@ //! Creating, loading, deleting, moving, copying and pasting files and their elements, and undoing. -use gpx_engine::{self as engine, Action, Command}; +use gpx_engine::api; use tauri::ipc::{InvokeBody, Request}; use tauri::State; use uuid::Uuid; -use crate::dto::*; -use crate::state::{act, edit, AppState}; +use crate::state::AppState; #[tauri::command(async)] pub fn new_file( @@ -15,73 +14,35 @@ pub fn new_file( lng: Option, lat: Option, ele: Option, -) -> Outcome { - let trackpoint = lng.zip(lat).map(|(lng, lat)| engine::NewTrackpoint { - lng, - lat, - ele: ele.unwrap_or_default(), - }); - edit!( - state, - Command::New(engine::New { - name: &name, - trackpoint, - }) - ) +) -> api::Outcome { + state.run(move |e| api::new_file(e, &name, lng, lat, ele)) } -/// The files are in the body, one after the other, the headers `lengths` and `names` (JSON) telling -/// the number of bytes and the name of each one. +/// The files are in the body, one after the other, the headers `lengths` and `names` (JSON, +/// percent-encoded) telling the number of bytes and the name of each one. #[tauri::command(async)] -pub fn load_files(state: State, request: Request<'_>) -> Outcome { +pub fn load_files(state: State, request: Request<'_>) -> api::Outcome { let InvokeBody::Raw(data) = request.body() else { - return Outcome::failed("the files are missing"); + return api::Outcome::failed("the files are missing"); }; let header = |key: &str| { request .headers() .get(key) .and_then(|value| value.to_str().ok()) - .and_then(urlencoding_decode) + .and_then(percent_decode) }; let (Some(lengths), Some(names)) = ( header("lengths").and_then(|json| serde_json::from_str::>(&json).ok()), header("names").and_then(|json| serde_json::from_str::>(&json).ok()), ) else { - return Outcome::failed("the lengths or the names of the files are not valid"); + return api::Outcome::failed("the lengths or the names of the files are not valid"); }; - if lengths.len() != names.len() { - return Outcome::failed(format!( - "{} lengths for {} names", - lengths.len(), - names.len() - )); - } - let (data, mut offset) = (data.clone(), 0); - let mut ranges = Vec::with_capacity(names.len()); - for (length, name) in lengths.iter().zip(&names) { - let end = offset + length; - if end > data.len() { - return Outcome::failed(format!("the data of {name} is shorter than its length")); - } - ranges.push(offset..end); - offset = end; - } - edit!(state, { - let files = ranges - .into_iter() - .zip(&names) - .map(|(range, name)| engine::Load { - data: &data[range], - name, - }) - .collect(); - Command::LoadFiles(engine::LoadFiles { files }) - }) + let data = data.clone(); + state.run(move |e| api::load_files(e, &data, &lengths, &names)) } -/// Headers are ASCII: the frontend percent-encodes the JSON. -fn urlencoding_decode(value: &str) -> Option { +fn percent_decode(value: &str) -> Option { let bytes = value.as_bytes(); let mut decoded = Vec::with_capacity(bytes.len()); let mut i = 0; @@ -99,63 +60,55 @@ fn urlencoding_decode(value: &str) -> Option { } #[tauri::command(async)] -pub fn duplicate(state: State) -> Outcome { - edit!(state, Command::Duplicate(engine::Duplicate)) +pub fn duplicate(state: State) -> api::Outcome { + state.run(api::duplicate) } #[tauri::command(async)] -pub fn delete(state: State, whole_files: bool) -> Outcome { - edit!(state, Command::Delete(engine::Delete { whole_files })) +pub fn delete(state: State, whole_files: bool) -> api::Outcome { + state.run(move |e| api::delete(e, whole_files)) } #[tauri::command(async)] -pub fn delete_all(state: State) -> Outcome { - edit!(state, Command::DeleteAll(engine::DeleteAll)) +pub fn delete_all(state: State) -> api::Outcome { + state.run(api::delete_all) } #[tauri::command(async)] -pub fn reorder(state: State, file_ids: Vec, index: usize) -> Outcome { - act!( - state, - Action::Reorder { - file_ids: file_ids.into_iter().map(engine::FileId).collect(), - index, - } - ) +pub fn reorder(state: State, file_ids: Vec, index: usize) -> api::Outcome { + state.run(move |e| api::reorder(e, &file_ids, index)) } #[tauri::command(async)] -pub fn copy(state: State) -> Outcome { - act!(state, Action::Copy) +pub fn copy(state: State) -> api::Outcome { + state.run(api::copy) } #[tauri::command(async)] -pub fn cut(state: State) -> Outcome { - act!(state, Action::Cut) +pub fn cut(state: State) -> api::Outcome { + state.run(api::cut) } #[tauri::command(async)] -pub fn paste(state: State) -> Outcome { - edit!(state, Command::Paste(engine::Paste)) +pub fn paste(state: State) -> api::Outcome { + state.run(api::paste) } #[tauri::command(async)] -pub fn move_elements(state: State, what: Selection, to: MoveTarget) -> Outcome { - edit!( - state, - Command::Move(engine::Move { - what: what.into(), - to: to.into(), - }) - ) +pub fn move_elements( + state: State, + what: api::Selection, + to: api::MoveTarget, +) -> api::Outcome { + state.run(move |e| api::move_elements(e, what, to)) } #[tauri::command(async)] -pub fn undo(state: State) -> Outcome { - act!(state, Action::Undo) +pub fn undo(state: State) -> api::Outcome { + state.run(api::undo) } #[tauri::command(async)] -pub fn redo(state: State) -> Outcome { - act!(state, Action::Redo) +pub fn redo(state: State) -> api::Outcome { + state.run(api::redo) } diff --git a/tauri/src/commands/mod.rs b/tauri/src/commands/mod.rs index 9e4936727..ad227c01b 100644 --- a/tauri/src/commands/mod.rs +++ b/tauri/src/commands/mod.rs @@ -8,7 +8,6 @@ pub mod edit; pub mod files; pub mod queries; -pub mod routing; pub mod selection; pub mod statistics; pub mod storage; diff --git a/tauri/src/commands/queries.rs b/tauri/src/commands/queries.rs index 93d246e48..f58922583 100644 --- a/tauri/src/commands/queries.rs +++ b/tauri/src/commands/queries.rs @@ -1,54 +1,51 @@ //! The files, their structure and statistics, the selection, the clipboard, and the coordinates of //! the trackpoints and waypoints (flat `[lng, lat, ...]` little-endian `f64` arrays). -use gpx_engine::{self as engine, FileId}; +use gpx_engine::{api, ExportOptions}; use tauri::State; use uuid::Uuid; -use crate::dto::*; +use crate::bytes::*; use crate::state::AppState; #[tauri::command(async)] pub fn file_order(state: State) -> Vec { - state.read(|e| ids(e.order())) + state.read(api::file_order) } #[tauri::command(async)] -pub fn file_structure(state: State, file_id: Uuid) -> Option { - state.read(move |e| e.file_structure(&FileId(file_id)).map(FileStructure::new)) +pub fn file_structure(state: State, file_id: Uuid) -> Option { + state.read(move |e| api::file_structure(e, file_id)) } #[tauri::command(async)] -pub fn file_statistics(state: State, file_id: Uuid) -> Option { - state.read(move |e| { - e.file_statistics(&FileId(file_id)) - .map(|s| GlobalStatistics::new(&s)) - }) +pub fn file_statistics(state: State, file_id: Uuid) -> Option { + state.read(move |e| api::file_statistics(e, file_id)) } #[tauri::command(async)] -pub fn selection(state: State) -> Selection { - state.read(|e| e.selection().into()) +pub fn selection(state: State) -> api::Selection { + state.read(api::selection) } #[tauri::command(async)] -pub fn clipboard(state: State) -> Option { - state.read(|e| e.clipboard().map(Clipboard::new)) +pub fn clipboard(state: State) -> Option { + state.read(api::clipboard) } #[tauri::command(async)] pub fn can_undo(state: State) -> bool { - state.read(|e| e.can_undo()) + state.read(api::can_undo) } #[tauri::command(async)] pub fn can_redo(state: State) -> bool { - state.read(|e| e.can_redo()) + state.read(api::can_redo) } #[tauri::command(async)] pub fn can_paste(state: State) -> bool { - state.read(|e| e.can_paste()) + state.read(api::can_paste) } #[tauri::command(async)] @@ -56,11 +53,8 @@ pub fn waypoint( state: State, file_id: Uuid, waypoint_id: Uuid, -) -> Option { - state.read(move |e| { - e.waypoint(&FileId(file_id), &engine::WaypointId(waypoint_id)) - .map(WaypointDetails::new) - }) +) -> Option { + state.read(move |e| api::waypoint(e, file_id, waypoint_id)) } #[tauri::command(async)] @@ -69,11 +63,8 @@ pub fn trackpoint( file_id: Uuid, segment_id: Uuid, index: usize, -) -> Option { - state.read(move |e| { - e.trackpoint(&FileId(file_id), &engine::TrackSegmentId(segment_id), index) - .map(TrackpointDetails::new) - }) +) -> Option { + state.read(move |e| api::trackpoint(e, file_id, segment_id, index)) } /// A file as GPX (UTF-8 bytes), the first byte telling whether the file exists. @@ -90,7 +81,7 @@ pub fn export_file( osm: bool, as_route: bool, ) -> Bytes { - let options = engine::ExportOptions { + let options = ExportOptions { time, hr, cad, @@ -99,38 +90,25 @@ pub fn export_file( osm, as_route, }; - optional(state.read(move |e| e.export(&FileId(file_id), options))) + optional(state.read(move |e| api::export_file(e, file_id, options))) } -/// The data that some files have: a bit for the time (1), heart rate (2), cadence (4), temperature -/// (8), power (16) and the OpenStreetMap data (32). #[tauri::command(async)] pub fn exportable_data(state: State, file_ids: Vec) -> u8 { - state.read(move |e| { - let ids: Vec = file_ids.into_iter().map(FileId).collect(); - let data = e.exportable_data(&ids); - [ - data.time, data.hr, data.cad, data.atemp, data.power, data.osm, - ] - .iter() - .enumerate() - .fold(0, |bits, (i, on)| bits | (u8::from(*on) << i)) - }) + state.read(move |e| api::exportable_data(e, &file_ids)) } #[tauri::command(async)] pub fn reduction_distances(state: State) -> Bytes { - Bytes::new(state.read(|e| f64s(&e.reduction_distances()))) + f64s(&state.read(api::reduction_distances)) } #[tauri::command(async)] pub fn segment_coordinates(state: State, segment_id: Uuid) -> Bytes { - Bytes::new( - state.read(move |e| f64s(e.segment_coordinates(&engine::TrackSegmentId(segment_id)))), - ) + f64s(&state.read(move |e| api::segment_coordinates(e, segment_id).to_vec())) } #[tauri::command(async)] pub fn waypoint_coordinates(state: State, file_id: Uuid) -> Bytes { - Bytes::new(state.read(move |e| f64s(e.waypoint_coordinates(&FileId(file_id))))) + f64s(&state.read(move |e| api::waypoint_coordinates(e, file_id).to_vec())) } diff --git a/tauri/src/commands/routing.rs b/tauri/src/commands/routing.rs deleted file mode 100644 index 56fd9770f..000000000 --- a/tauri/src/commands/routing.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! The commands that refer to trackpoints by their index in the routing buffers, which only mean -//! something for the `revision` they were read with: the commands do nothing if the selection is -//! not the one of that revision anymore. - -use gpx_engine::{self as engine, Command}; -use tauri::State; - -use crate::dto::*; -use crate::state::{edit_at, AppState}; - -#[tauri::command(async)] -#[allow(clippy::too_many_arguments)] -pub fn route( - state: State, - revision: u32, - start: u32, - end: u32, - lng: Vec, - lat: Vec, - ele: Vec, - attributes: RouteAttributes, - anchors: Vec, -) -> Outcome { - if lng.len() != lat.len() || lat.len() != ele.len() { - return Outcome::failed("lng, lat and ele differ in length"); - } - edit_at!( - state, - revision, - Command::Route(engine::Route { - start, - end, - lng: &lng, - lat: &lat, - ele: &ele, - surface: attributes.surface.view(), - highway: attributes.highway.view(), - sac_scale: attributes.sac_scale.view(), - mtb_scale: attributes.mtb_scale.view(), - anchors: &anchors, - }) - ) -} - -#[tauri::command(async)] -pub fn insert_anchor(state: State, revision: u32, lng: f64, lat: f64) -> Outcome { - edit_at!( - state, - revision, - Command::InsertAnchor(engine::InsertAnchor { lng, lat }) - ) -} - -#[tauri::command(async)] -pub fn change_loop_start(state: State, revision: u32, index: u32) -> Outcome { - edit_at!( - state, - revision, - Command::ChangeLoopStart(engine::ChangeLoopStart { index }) - ) -} - -#[tauri::command(async)] -pub fn split(state: State, revision: u32, at: u32, split_type: SplitType) -> Outcome { - edit_at!( - state, - revision, - Command::Split(engine::Split { - at, - split_type: split_type.into(), - }) - ) -} diff --git a/tauri/src/commands/selection.rs b/tauri/src/commands/selection.rs index 6f7658952..8555e6dd8 100644 --- a/tauri/src/commands/selection.rs +++ b/tauri/src/commands/selection.rs @@ -1,49 +1,24 @@ //! Changing what is selected. -use gpx_engine::{self as engine, Action}; +use gpx_engine::{api, SelectMode}; use tauri::State; use uuid::Uuid; -use crate::dto::*; -use crate::state::{act, AppState}; +use crate::state::AppState; -fn select_elements( - state: &AppState, - selection: Option, - mode: SelectMode, -) -> Outcome { - match selection { - Some(selection) => act!( - state, - Action::Select { - selection, - mode: mode.into(), - } - ), - None => Outcome::failed("the ids are not UUIDs"), - } -} - -fn to_file_ids(ids: Vec) -> Vec { - ids.into_iter().map(engine::FileId).collect() +#[tauri::command(async)] +pub fn select(state: State, file_ids: Vec, mode: SelectMode) -> api::Outcome { + state.run(move |e| api::select(e, &file_ids, mode)) } #[tauri::command(async)] -pub fn select(state: State, file_ids: Vec, mode: SelectMode) -> Outcome { - let selection = engine::Selection::File { - file_ids: to_file_ids(file_ids).into_iter().collect(), - }; - select_elements(&state, Some(selection), mode) +pub fn select_all(state: State) -> api::Outcome { + state.run(api::select_all) } #[tauri::command(async)] -pub fn select_all(state: State) -> Outcome { - act!(state, Action::SelectAll) -} - -#[tauri::command(async)] -pub fn arrow_select(state: State, down: bool, add: bool) -> Outcome { - act!(state, Action::ArrowSelect { down, add }) +pub fn arrow_select(state: State, down: bool, add: bool) -> api::Outcome { + state.run(move |e| api::arrow_select(e, down, add)) } #[tauri::command(async)] @@ -52,12 +27,8 @@ pub fn select_tracks( file_id: Uuid, track_ids: Vec, mode: SelectMode, -) -> Outcome { - let selection = engine::Selection::Track { - file_id: engine::FileId(file_id), - trk_ids: track_ids.into_iter().map(engine::TrackId).collect(), - }; - select_elements(&state, Some(selection), mode) +) -> api::Outcome { + state.run(move |e| api::select_tracks(e, file_id, &track_ids, mode)) } #[tauri::command(async)] @@ -67,24 +38,17 @@ pub fn select_segments( track_id: Uuid, segment_ids: Vec, mode: SelectMode, -) -> Outcome { - let selection = engine::Selection::TrackSegment { - file_id: engine::FileId(file_id), - trk_id: engine::TrackId(track_id), - trkseg_ids: segment_ids - .into_iter() - .map(engine::TrackSegmentId) - .collect(), - }; - select_elements(&state, Some(selection), mode) +) -> api::Outcome { + state.run(move |e| api::select_segments(e, file_id, track_id, &segment_ids, mode)) } #[tauri::command(async)] -pub fn select_waypoint_group(state: State, file_id: Uuid, mode: SelectMode) -> Outcome { - let selection = engine::Selection::Waypoints { - file_id: engine::FileId(file_id), - }; - select_elements(&state, Some(selection), mode) +pub fn select_waypoint_group( + state: State, + file_id: Uuid, + mode: SelectMode, +) -> api::Outcome { + state.run(move |e| api::select_waypoint_group(e, file_id, mode)) } #[tauri::command(async)] @@ -93,10 +57,6 @@ pub fn select_waypoints( file_id: Uuid, waypoint_ids: Vec, mode: SelectMode, -) -> Outcome { - let selection = engine::Selection::Waypoint { - file_id: engine::FileId(file_id), - wpt_ids: waypoint_ids.into_iter().map(engine::WaypointId).collect(), - }; - select_elements(&state, Some(selection), mode) +) -> api::Outcome { + state.run(move |e| api::select_waypoints(e, file_id, &waypoint_ids, mode)) } diff --git a/tauri/src/commands/statistics.rs b/tauri/src/commands/statistics.rs index f5af50774..a20f5991e 100644 --- a/tauri/src/commands/statistics.rs +++ b/tauri/src/commands/statistics.rs @@ -2,16 +2,17 @@ //! (one per call, copies of the buffers of the engine). Optional ones start with a byte telling //! whether they are there. +use gpx_engine::api; use tauri::State; -use crate::dto::*; +use crate::bytes::*; use crate::state::AppState; macro_rules! f64_getters { ($($name:ident),*) => {$( #[tauri::command(async)] pub fn $name(state: State) -> Bytes { - Bytes::new(state.read(|e| f64s(&e.statistics().$name))) + f64s(&state.read(|e| api::$name(e).to_vec())) } )*}; } @@ -30,7 +31,7 @@ macro_rules! optional_f64_getters { ($($name:ident),*) => {$( #[tauri::command(async)] pub fn $name(state: State) -> Bytes { - optional(state.read(|e| e.statistics().$name.as_deref().map(f64s))) + optional(state.read(|e| api::$name(e).map(f64_bytes))) } )*}; } @@ -40,12 +41,12 @@ optional_f64_getters!(speed, hr, cad, atemp, power); /// Timestamps in milliseconds since the epoch (`NO_TIME` when missing), as `i64`. #[tauri::command(async)] pub fn timestamps(state: State) -> Bytes { - optional(state.read(|e| e.statistics().time.as_deref().map(i64s))) + optional(state.read(|e| api::timestamps(e).map(i64_bytes))) } #[tauri::command(async)] -pub fn selection_statistics(state: State) -> GlobalStatistics { - state.read(|e| GlobalStatistics::new(&e.statistics().global)) +pub fn selection_statistics(state: State) -> api::GlobalStatistics { + state.read(api::selection_statistics) } #[tauri::command(async)] @@ -53,83 +54,55 @@ pub fn slice_statistics( state: State, start: usize, end: usize, -) -> Option { - state.read(move |e| { - e.statistics() - .slice(start, end) - .map(|s| GlobalStatistics::new(&s)) - }) +) -> Option { + state.read(move |e| api::slice_statistics(e, start, end)) } -macro_rules! intervals_getters { - ($starts:ident, $values:ident, $field:ident) => { +macro_rules! category_getters { + ($starts:ident, $values:ident, $names:ident) => { #[tauri::command(async)] pub fn $starts(state: State) -> Bytes { - Bytes::new(state.read(|e| u32s(&e.statistics().$field.starts))) + u32s(&state.read(|e| api::$starts(e).to_vec())) } #[tauri::command(async)] pub fn $values(state: State) -> Bytes { - Bytes::new(state.read(|e| e.statistics().$field.values.clone())) + u8s(&state.read(|e| api::$values(e).to_vec())) + } + + #[tauri::command(async)] + pub fn $names(state: State) -> Vec { + state.read(|e| api::$names(e).to_vec()) } }; } -intervals_getters!(surface_starts, surface_values, surface); -intervals_getters!(highway_starts, highway_values, highway); -intervals_getters!(sac_scale_starts, sac_scale_values, sac_scale); -intervals_getters!(mtb_scale_starts, mtb_scale_values, mtb_scale); - -/// The names of the categories, in the order of their codes. The lists only grow. -#[tauri::command(async)] -pub fn surfaces(state: State) -> Vec { - state.read(|e| e.categories().surface.names().to_vec()) -} - -#[tauri::command(async)] -pub fn highways(state: State) -> Vec { - state.read(|e| e.categories().highway.names().to_vec()) -} - -#[tauri::command(async)] -pub fn sac_scales(state: State) -> Vec { - state.read(|e| e.categories().sac_scale.names().to_vec()) -} - -#[tauri::command(async)] -pub fn mtb_scales(state: State) -> Vec { - state.read(|e| e.categories().mtb_scale.names().to_vec()) -} - -// Routing buffers +category_getters!(surface_starts, surface_values, surfaces); +category_getters!(highway_starts, highway_values, highways); +category_getters!(sac_scale_starts, sac_scale_values, sac_scales); +category_getters!(mtb_scale_starts, mtb_scale_values, mtb_scales); #[tauri::command(async)] pub fn anchor_indices(state: State) -> Bytes { - Bytes::new(state.read(|e| u32s(&e.routing().anchor_indices))) + u32s(&state.read(|e| api::anchor_indices(e).to_vec())) } #[tauri::command(async)] pub fn anchor_zooms(state: State) -> Bytes { - Bytes::new(state.read(|e| e.routing().anchor_zooms.clone())) + u8s(&state.read(|e| api::anchor_zooms(e).to_vec())) } #[tauri::command(async)] pub fn segment_starts(state: State) -> Bytes { - Bytes::new(state.read(|e| u32s(&e.routing().segment_starts))) + u32s(&state.read(|e| api::segment_starts(e).to_vec())) } #[tauri::command(async)] pub fn segment_ids(state: State) -> Vec { - state.read(|e| { - e.routing() - .segment_ids - .iter() - .map(|id| id.0.to_string()) - .collect() - }) + state.read(api::segment_ids) } #[tauri::command(async)] pub fn routing_revision(state: State) -> u32 { - state.read(|e| e.routing().revision) + state.read(api::routing_revision) } diff --git a/tauri/src/commands/storage.rs b/tauri/src/commands/storage.rs index 15c113289..df0266e82 100644 --- a/tauri/src/commands/storage.rs +++ b/tauri/src/commands/storage.rs @@ -6,8 +6,8 @@ use serde::Serialize; use tauri::State; -use crate::dto::Outcome; use crate::state::AppState; +use gpx_engine::api::Outcome; /// What opening the storage found. #[derive(Serialize)] diff --git a/tauri/src/lib.rs b/tauri/src/lib.rs index 176905878..b9a21a9ce 100644 --- a/tauri/src/lib.rs +++ b/tauri/src/lib.rs @@ -1,8 +1,8 @@ //! The desktop shell of gpx.studio. The engine of the files (`gpx-rs/engine`) runs in this //! process, and the frontend talks to it through the commands of `commands`. +mod bytes; mod commands; -mod dto; mod state; use commands::*; @@ -59,10 +59,10 @@ fn with_engine(builder: tauri::Builder) -> tauri::Builder< edit::extract, edit::elevation, edit::clean, - routing::route, - routing::insert_anchor, - routing::change_loop_start, - routing::split, + edit::route, + edit::insert_anchor, + edit::change_loop_start, + edit::split, // queries queries::file_order, queries::file_structure, diff --git a/tauri/src/state.rs b/tauri/src/state.rs index 54f6477dd..9dc4a357d 100644 --- a/tauri/src/state.rs +++ b/tauri/src/state.rs @@ -8,14 +8,10 @@ use std::sync::mpsc::{self, Sender}; use std::sync::{Mutex, MutexGuard}; use std::thread; -use gpx_engine::{Action, Engine}; - -use crate::dto::Outcome; +use gpx_engine::Engine; type Job = Box; -/// What the commands talk to. The engine is the same as in the browser (`gpx-rs`); here it lives in -/// the process of the app, and the frontend reaches it through the commands only. pub struct AppState { jobs: Mutex>, /// The settings of the frontend, as JSON strings by key. @@ -65,43 +61,3 @@ impl AppState { self.settings.lock().unwrap() } } - -/// Executes an action and tells what it did, see `Outcome`. -pub fn execute(engine: &mut Engine, action: Action) -> Outcome { - match engine.execute(action) { - Ok(outcome) => Outcome::new(&outcome, None), - Err(error) => Outcome::failed(error.to_string()), - } -} - -/// Executes an action on the engine. The values the action uses are moved to the engine thread. -macro_rules! act { - ($state:expr, $action:expr) => { - $state.run(move |engine| $crate::state::execute(engine, $action)) - }; -} - -/// Executes a command (the command can borrow what it uses). -macro_rules! edit { - ($state:expr, $command:expr) => { - act!($state, ::gpx_engine::Action::Edit($command)) - }; -} - -/// Like `edit!`, but does nothing if the selection is not the one of `revision` anymore (the -/// indices of the routing buffers only mean something for their revision). -macro_rules! edit_at { - ($state:expr, $revision:expr, $command:expr) => {{ - let revision = $revision; - $state.run(move |engine| { - if engine.routing().revision != revision { - return $crate::dto::Outcome::failed( - "the selection changed since the routing buffers were read", - ); - } - $crate::state::execute(engine, ::gpx_engine::Action::Edit($command)) - }) - }}; -} - -pub(crate) use {act, edit, edit_at}; diff --git a/tauri/src/tests.rs b/tauri/src/tests.rs index 2fb486a70..2b9d061a3 100644 --- a/tauri/src/tests.rs +++ b/tauri/src/tests.rs @@ -60,7 +60,7 @@ impl App { let mut data = vec![]; let mut lengths = vec![]; for name in names { - let file = std::fs::read(format!("../gpx-rs/engine/data/{name}.gpx")).unwrap(); + let file = std::fs::read(format!("../gpx-rs/data/{name}.gpx")).unwrap(); lengths.push(file.len()); data.extend(file); } @@ -77,8 +77,10 @@ impl App { fn f64s(bytes: &[u8]) -> Vec { bytes - .chunks_exact(8) - .map(|chunk| f64::from_le_bytes(chunk.try_into().unwrap())) + .as_chunks::<8>() + .0 + .iter() + .map(|chunk| f64::from_le_bytes(*chunk)) .collect() } diff --git a/wasm/.gitignore b/wasm/.gitignore new file mode 100644 index 000000000..01d0a0845 --- /dev/null +++ b/wasm/.gitignore @@ -0,0 +1 @@ +pkg/ diff --git a/gpx-rs/wasm/Cargo.toml b/wasm/Cargo.toml similarity index 77% rename from gpx-rs/wasm/Cargo.toml rename to wasm/Cargo.toml index d7dcd03ef..ded307bb9 100644 --- a/gpx-rs/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "gpx-rs" +name = "gpx-wasm" version = "0.1.0" edition = "2024" @@ -8,9 +8,11 @@ crate-type = ["cdylib", "rlib"] [dependencies] console_error_panic_hook = "0.1.7" -gpx-engine = { path = "../engine" } +gpx-engine = { path = "../gpx-rs" } idb = "0.6.5" js-sys = "0.3.106" +serde = "1" +serde-wasm-bindgen = "0.6" uuid = { version = "1.26.1", features = ["js"] } wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4.79" diff --git a/wasm/README.md b/wasm/README.md new file mode 100644 index 000000000..949ca28bc --- /dev/null +++ b/wasm/README.md @@ -0,0 +1,20 @@ +# wasm + +The engine of [`../gpx-rs`](../gpx-rs) for the browser: WebAssembly, exposed to the page through +`wasm-bindgen`. Each exported function is a thin wrapper over `gpx_engine::api` (see `../gpx-rs/src/api`) +that parses what the page sends and turns the result into JS objects (`src/convert.rs`, with +`serde-wasm-bindgen`) or typed arrays. The storage in the browser (IndexedDB) is here too, as it +is specific to the browser. + +``` +wasm-pack build --target bundler +``` + +The website imports the result as `gpx-wasm` (`pkg/`). The functions are tested in Node (it needs +`wasm-pack`, which fetches the test runner the first time): + +``` +wasm-pack test --node +``` + +`src/ts.rs` declares the TypeScript types of the objects that cross. diff --git a/gpx-rs/wasm/src/command/edit.rs b/wasm/src/command/edit.rs similarity index 58% rename from gpx-rs/wasm/src/command/edit.rs rename to wasm/src/command/edit.rs index 1e2201ed1..d07f5e729 100644 --- a/gpx-rs/wasm/src/command/edit.rs +++ b/wasm/src/command/edit.rs @@ -1,34 +1,29 @@ //! Editing the metadata, the style and the structure of the files. -use gpx_engine::{self as engine, Command}; +use gpx_engine::api; use wasm_bindgen::prelude::*; -use crate::ts::Outcome; - use crate::session::*; +use crate::ts::Outcome; // Edit commands #[wasm_bindgen] pub fn metadata(name: &str, desc: &str) -> Outcome { - edit(Command::Metadata(engine::Metadata { name, desc })) + run(|e| api::metadata(e, name, desc)) } #[wasm_bindgen] pub fn style(color: Option, opacity: Option, width: Option) -> Outcome { - edit(Command::Style(engine::Style { - color: color.as_deref(), - opacity, - width, - })) + run(|e| api::style(e, color.as_deref(), opacity, width)) } #[wasm_bindgen] pub fn new_track() -> Outcome { - edit(Command::NewTrack(engine::NewTrack)) + run(api::new_track) } #[wasm_bindgen] pub fn new_track_segment() -> Outcome { - edit(Command::NewTrackSegment(engine::NewTrackSegment)) + run(api::new_track_segment) } diff --git a/gpx-rs/wasm/src/command/files.rs b/wasm/src/command/files.rs similarity index 67% rename from gpx-rs/wasm/src/command/files.rs rename to wasm/src/command/files.rs index 3d9361efa..e891502d2 100644 --- a/gpx-rs/wasm/src/command/files.rs +++ b/wasm/src/command/files.rs @@ -1,6 +1,6 @@ //! Creating, loading, deleting, moving, copying and pasting files and their elements. -use gpx_engine::{self as engine, Action, Command}; +use gpx_engine::api; use wasm_bindgen::prelude::*; use crate::convert::*; @@ -14,12 +14,7 @@ use crate::ts::*; /// hold that trackpoint (at `ele`, 0 by default). #[wasm_bindgen] pub fn new_file(name: &str, lng: Option, lat: Option, ele: Option) -> Outcome { - let trackpoint = lng.zip(lat).map(|(lng, lat)| engine::NewTrackpoint { - lng, - lat, - ele: ele.unwrap_or_default(), - }); - edit(Command::New(engine::New { name, trackpoint })) + run(|e| api::new_file(e, name, lng, lat, ele)) } /// Loads files as a single command (one undo step). The files are in `data`, one after the @@ -28,49 +23,33 @@ pub fn new_file(name: &str, lng: Option, lat: Option, ele: Option /// skipped, the first one that was read is selected. #[wasm_bindgen] pub fn load_files(data: &[u8], lengths: &[u32], names: Vec) -> Outcome { - if lengths.len() != names.len() { - return fail(format!( - "{} lengths for {} names", - lengths.len(), - names.len() - )); - } - let mut files = Vec::with_capacity(names.len()); - let mut offset = 0; - for (length, name) in lengths.iter().zip(&names) { - let end = offset + *length as usize; - let Some(data) = data.get(offset..end) else { - return fail(format!("the data of {name} is shorter than its length")); - }; - files.push(engine::Load { data, name }); - offset = end; - } - edit(Command::LoadFiles(engine::LoadFiles { files })) + let lengths: Vec = lengths.iter().map(|length| *length as usize).collect(); + run(|e| api::load_files(e, data, &lengths, &names)) } #[wasm_bindgen] pub fn duplicate() -> Outcome { - edit(Command::Duplicate(engine::Duplicate)) + run(api::duplicate) } /// Deletes the selected elements. With `whole_files`, the files holding the selected elements are /// deleted instead, even if only a track or a waypoint is selected. #[wasm_bindgen] pub fn delete(whole_files: bool) -> Outcome { - edit(Command::Delete(engine::Delete { whole_files })) + run(|e| api::delete(e, whole_files)) } #[wasm_bindgen] pub fn delete_all() -> Outcome { - edit(Command::DeleteAll(engine::DeleteAll)) + run(api::delete_all) } /// Moves the files to `index` among the other files, in the given order (not undoable). /// `file_ids_bytes`: concatenated 16-byte UUIDs. #[wasm_bindgen] pub fn reorder(file_ids_bytes: &[u8], index: usize) -> Outcome { - match file_ids(file_ids_bytes) { - Some(file_ids) => execute(Action::Reorder { file_ids, index }), + match uuid_list(file_ids_bytes) { + Some(ids) => run(|e| api::reorder(e, &ids, index)), None => fail("the file ids are not a list of UUIDs"), } } @@ -80,13 +59,13 @@ pub fn reorder(file_ids_bytes: &[u8], index: usize) -> Outcome { /// Puts the selected elements in the clipboard. Nothing happens when nothing is selected. #[wasm_bindgen] pub fn copy() -> Outcome { - execute(Action::Copy) + run(api::copy) } /// Like `copy`, but the elements are moved when they are pasted. #[wasm_bindgen] pub fn cut() -> Outcome { - execute(Action::Cut) + run(api::cut) } /// Pastes the clipboard according to the selection (see `can_paste`). What is pasted is always @@ -95,13 +74,13 @@ pub fn cut() -> Outcome { /// the first time (and the clipboard is not cut after that), then it is copied. #[wasm_bindgen] pub fn paste() -> Outcome { - edit(Command::Paste(engine::Paste)) + run(api::paste) } /// Whether the clipboard can be pasted with the current selection. #[wasm_bindgen] pub fn can_paste() -> bool { - with_engine(|e| e.can_paste()).unwrap_or(false) + with_engine(api::can_paste).unwrap_or(false) } /// Moves elements (given as a `Selection`) to a place of the file tree, keeping their ids, like @@ -113,8 +92,9 @@ pub fn can_paste() -> bool { /// waypoints of a file. #[wasm_bindgen] pub fn move_elements(what: Selection, to: MoveTarget) -> Outcome { - match parse_selection(&what).zip(parse_move_target(&to)) { - Some((what, to)) => edit(Command::Move(engine::Move { what, to })), + // a negative index is the start, an infinite one is the end + match from_js::(&what).zip(from_js::(&to)) { + Some((what, to)) => run(|e| api::move_elements(e, what, to)), None => fail("the elements or the target are not valid"), } } diff --git a/gpx-rs/wasm/src/command/history.rs b/wasm/src/command/history.rs similarity index 62% rename from gpx-rs/wasm/src/command/history.rs rename to wasm/src/command/history.rs index 3dcfaf15e..60e341a5a 100644 --- a/gpx-rs/wasm/src/command/history.rs +++ b/wasm/src/command/history.rs @@ -1,30 +1,29 @@ //! Undo and redo. -use gpx_engine::Action; +use gpx_engine::api; use wasm_bindgen::prelude::*; -use crate::ts::Outcome; - use crate::session::*; +use crate::ts::Outcome; // Undo-redo #[wasm_bindgen] pub fn undo() -> Outcome { - execute(Action::Undo) + run(api::undo) } #[wasm_bindgen] pub fn redo() -> Outcome { - execute(Action::Redo) + run(api::redo) } #[wasm_bindgen] pub fn can_undo() -> bool { - with_engine(|e| e.can_undo()).unwrap_or(false) + with_engine(api::can_undo).unwrap_or(false) } #[wasm_bindgen] pub fn can_redo() -> bool { - with_engine(|e| e.can_redo()).unwrap_or(false) + with_engine(api::can_redo).unwrap_or(false) } diff --git a/gpx-rs/wasm/src/command/mod.rs b/wasm/src/command/mod.rs similarity index 100% rename from gpx-rs/wasm/src/command/mod.rs rename to wasm/src/command/mod.rs diff --git a/wasm/src/command/routing.rs b/wasm/src/command/routing.rs new file mode 100644 index 000000000..83a32caeb --- /dev/null +++ b/wasm/src/command/routing.rs @@ -0,0 +1,66 @@ +//! The commands that refer to trackpoints by their index in the routing buffers. + +use gpx_engine::api; +use wasm_bindgen::prelude::*; + +use crate::convert::*; +use crate::ids::*; +use crate::session::*; +use crate::ts::*; +use crate::types::*; + +// Routing +// +// The indices of the trackpoints and of the anchors are the ones of the routing buffers: they only +// mean something for the `revision` they were read with (`routing_revision`), and the commands +// that use them do nothing if the selection is not the one of that revision anymore. + +/// Replaces the trackpoints `start..end` of the selection by the given ones, in a single segment +/// (see `RoutingBuffer`). A pure insertion if `start == end`, a pure removal without new points. +/// `attributes` holds the surface, highway, SAC scale and MTB scale of the new points, as +/// intervals, and `anchors` the indices among the new points of the ones that become anchors. +/// Does nothing, and says why, if the revision of the routing buffers is not the given one. +#[wasm_bindgen] +#[allow(clippy::too_many_arguments)] +pub fn route( + revision: u32, + start: u32, + end: u32, + lng: &[f64], + lat: &[f64], + ele: &[f64], + attributes: RouteAttributes, + anchors: &[u32], +) -> Outcome { + let Some(attributes) = from_js::(&attributes) else { + return fail("the attributes of the route are not valid"); + }; + if !same_len(lng, lat, ele) { + return fail("lng, lat and ele differ in length"); + } + run(|e| api::route(e, revision, start, end, lng, lat, ele, &attributes, anchors)) +} + +/// Makes an anchor of the point of the selected segments that is the closest to the coordinates, +/// inserting a trackpoint on the path if none is there. Does nothing, and says why, if the +/// revision of the routing buffers is not the given one. +#[wasm_bindgen] +pub fn insert_anchor(revision: u32, lng: f64, lat: f64) -> Outcome { + run(|e| api::insert_anchor(e, revision, lng, lat)) +} + +/// Makes the trackpoint `index` of the selection the start of its segment, which is a loop (the +/// trackpoints before it go to the end, and the loop closes on it). Does nothing, and says why, if the revision of the +/// routing buffers is not the given one. +#[wasm_bindgen] +pub fn change_loop_start(revision: u32, index: u32) -> Outcome { + run(|e| api::change_loop_start(e, revision, index)) +} + +/// Cuts the file, the track or the segment (see `SplitType`) of the trackpoint `at` of the +/// selection in two, there: the trackpoint ends the first part and starts the second one. Does +/// nothing, and returns false, if the revision of the routing buffers is not the given one. +#[wasm_bindgen] +pub fn split(revision: u32, at: u32, split_type: SplitType) -> Outcome { + run(|e| api::split(e, revision, at, split_type.into())) +} diff --git a/gpx-rs/wasm/src/command/tools.rs b/wasm/src/command/tools.rs similarity index 64% rename from gpx-rs/wasm/src/command/tools.rs rename to wasm/src/command/tools.rs index b4914ca30..55a1ef58c 100644 --- a/gpx-rs/wasm/src/command/tools.rs +++ b/wasm/src/command/tools.rs @@ -1,6 +1,6 @@ //! The tools: reversing, cropping, reducing, timestamps, merging, cleaning... -use gpx_engine::{self as engine, Command, LngLat, LngLatBounds}; +use gpx_engine::api; use wasm_bindgen::prelude::*; use crate::ts::Outcome; @@ -12,31 +12,28 @@ use crate::types::*; #[wasm_bindgen] pub fn reverse() -> Outcome { - edit(Command::Reverse(engine::Reverse)) + run(api::reverse) } /// Adds to each selected segment a reversed copy of itself (without repeating its last point), so that it goes back to where it /// started. #[wasm_bindgen] pub fn round_trip() -> Outcome { - edit(Command::RoundTrip(engine::RoundTrip)) + run(api::round_trip) } /// Keeps the trackpoints `start` to `end` (both included) of the selection, counted over its /// segments one after the other, and removes the others (and what is left empty). #[wasm_bindgen] pub fn crop(start: u32, end: u32) -> Outcome { - if start > end { - return fail(format!("the range {start}..{end} is reversed")); - } - edit(Command::Crop(engine::Crop { start, end })) + run(|e| api::crop(e, start, end)) } /// Removes the trackpoints of the selection that are less than `tolerance` meters away from the /// line of their neighbours, except the ends of the segments. #[wasm_bindgen] pub fn reduce(tolerance: f64) -> Outcome { - edit(Command::Reduce(engine::Reduce { tolerance })) + run(|e| api::reduce(e, tolerance)) } /// Sets the timestamps of the selection, which starts at `start_time` (ms since the epoch): the @@ -44,41 +41,32 @@ pub fn reduce(tolerance: f64) -> Outcome { /// previous one at `speed` (km/h). #[wasm_bindgen(js_name = changeTimestamps)] pub fn change_timestamps(start_time: f64, speed: f64, ratio: f64) -> Outcome { - edit(Command::Time(engine::Time { - start_time: start_time as i64, - kind: engine::TimeKind::Change { speed, ratio }, - })) + run(|e| api::change_timestamps(e, start_time, speed, ratio)) } /// Makes up timestamps for the selection, which starts at `start_time` (ms since the epoch) and /// lasts `total_time` seconds. #[wasm_bindgen(js_name = createArtificialTimestamps)] pub fn create_artificial_timestamps(start_time: f64, total_time: f64) -> Outcome { - edit(Command::Time(engine::Time { - start_time: start_time as i64, - kind: engine::TimeKind::Artificial { total_time }, - })) + run(|e| api::create_artificial_timestamps(e, start_time, total_time)) } /// Merges the selection into its first element (see `MergeType`). `remove_gaps` brings the parts /// that are connected closer in time, if they have timestamps. #[wasm_bindgen] pub fn merge(type_: MergeType, remove_gaps: bool) -> Outcome { - edit(Command::Merge(engine::Merge { - type_: type_.into(), - remove_gaps, - })) + run(|e| api::merge(e, type_.into(), remove_gaps)) } #[wasm_bindgen] pub fn extract() -> Outcome { - edit(Command::Extract(engine::Extract)) + run(api::extract) } /// Sets the elevation of the trackpoints of the selection, one elevation per trackpoint. #[wasm_bindgen] pub fn elevation(ele: &[f64]) -> Outcome { - edit(Command::Elevation(engine::Elevation { ele })) + run(|e| api::elevation(e, ele)) } #[wasm_bindgen] @@ -91,19 +79,5 @@ pub fn clean( trkpt: bool, wpt: bool, ) -> Outcome { - edit(Command::Clean(engine::Clean { - bounds: LngLatBounds { - sw: LngLat { - lng: west, - lat: south, - }, - ne: LngLat { - lng: east, - lat: north, - }, - }, - type_: type_.into(), - trkpt, - wpt, - })) + run(|e| api::clean(e, west, south, east, north, type_.into(), trkpt, wpt)) } diff --git a/wasm/src/command/waypoints.rs b/wasm/src/command/waypoints.rs new file mode 100644 index 000000000..ba3f6e1fe --- /dev/null +++ b/wasm/src/command/waypoints.rs @@ -0,0 +1,76 @@ +//! Creating, editing, moving and deleting waypoints. + +use gpx_engine::api; +use wasm_bindgen::prelude::*; + +use crate::ts::Outcome; + +use crate::ids::*; +use crate::session::*; + +#[wasm_bindgen] +pub fn new_waypoint( + lng: f64, + lat: f64, + ele: f64, + name: &str, + desc: &str, + icon: &str, + link: &str, +) -> Outcome { + run(|e| api::new_waypoint(e, lng, lat, ele, name, desc, icon, link)) +} + +/// Moves a waypoint of a file, whatever is selected. +#[wasm_bindgen] +pub fn move_waypoint(file_id: &str, waypoint_id: &str, lng: f64, lat: f64, ele: f64) -> Outcome { + match parse_uuid(file_id).zip(parse_uuid(waypoint_id)) { + Some((file_id, waypoint_id)) => { + run(|e| api::move_waypoint(e, file_id, waypoint_id, lng, lat, ele)) + } + None => fail("the file or waypoint id is not a UUID"), + } +} + +/// Deletes a waypoint of a file, whatever is selected. +#[wasm_bindgen] +pub fn delete_waypoint(file_id: &str, waypoint_id: &str) -> Outcome { + match parse_uuid(file_id).zip(parse_uuid(waypoint_id)) { + Some((file_id, waypoint_id)) => run(|e| api::delete_waypoint(e, file_id, waypoint_id)), + None => fail("the file or waypoint id is not a UUID"), + } +} + +/// Changes a waypoint of a file, whatever is selected: its name, description, icon, link, position +/// and elevation. The strings that are empty remove the field. +#[wasm_bindgen] +#[allow(clippy::too_many_arguments)] +pub fn update_waypoint( + file_id: &str, + waypoint_id: &str, + lng: f64, + lat: f64, + ele: f64, + name: &str, + desc: &str, + icon: &str, + link: &str, +) -> Outcome { + match parse_uuid(file_id).zip(parse_uuid(waypoint_id)) { + Some((file_id, waypoint_id)) => run(|e| { + api::update_waypoint( + e, + file_id, + waypoint_id, + lng, + lat, + ele, + name, + desc, + icon, + link, + ) + }), + None => fail("the file or waypoint id is not a UUID"), + } +} diff --git a/wasm/src/convert.rs b/wasm/src/convert.rs new file mode 100644 index 000000000..6482bb039 --- /dev/null +++ b/wasm/src/convert.rs @@ -0,0 +1,28 @@ +//! Turning what the shared interface (`gpx_engine::api`) gives into JS objects and back. + +use gpx_engine::api; +use serde::{Serialize, de::DeserializeOwned}; +use wasm_bindgen::prelude::*; + +use crate::ts::Outcome; + +/// A plain JS object (absent fields are `undefined`) with what `value` holds. +pub(crate) fn to_js(value: &T) -> JsValue { + value + .serialize(&serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true)) + .expect("the data can be turned into a JS value") +} + +/// What `value` holds, `None` if it is not what `T` is. +pub(crate) fn from_js(value: &JsValue) -> Option { + serde_wasm_bindgen::from_value(value.clone()).ok() +} + +/// `undefined` when there is nothing. +pub(crate) fn optional_js(value: Option) -> JsValue { + value.as_ref().map_or(JsValue::UNDEFINED, to_js) +} + +pub(crate) fn outcome_js(outcome: &api::Outcome) -> Outcome { + to_js(outcome).unchecked_into() +} diff --git a/gpx-rs/wasm/src/idb.rs b/wasm/src/idb.rs similarity index 100% rename from gpx-rs/wasm/src/idb.rs rename to wasm/src/idb.rs diff --git a/wasm/src/ids.rs b/wasm/src/ids.rs new file mode 100644 index 000000000..7dde0b2b7 --- /dev/null +++ b/wasm/src/ids.rs @@ -0,0 +1,17 @@ +//! Ids, which cross as strings (one id) or as concatenated 16-byte UUIDs (several). + +use uuid::Uuid; + +pub(crate) fn uuid_list(bytes: &[u8]) -> Option> { + let (chunks, rest) = bytes.as_chunks::<16>(); + rest.is_empty() + .then(|| chunks.iter().map(|c| Uuid::from_bytes(*c)).collect()) +} + +pub(crate) fn parse_uuid(id: &str) -> Option { + Uuid::parse_str(id).ok() +} + +pub(crate) fn same_len(a: &[f64], b: &[f64], c: &[f64]) -> bool { + a.len() == b.len() && b.len() == c.len() +} diff --git a/gpx-rs/wasm/src/lib.rs b/wasm/src/lib.rs similarity index 100% rename from gpx-rs/wasm/src/lib.rs rename to wasm/src/lib.rs diff --git a/gpx-rs/wasm/src/persistence.rs b/wasm/src/persistence.rs similarity index 91% rename from gpx-rs/wasm/src/persistence.rs rename to wasm/src/persistence.rs index 41708b8cf..96e7e36fc 100644 --- a/gpx-rs/wasm/src/persistence.rs +++ b/wasm/src/persistence.rs @@ -3,11 +3,11 @@ use std::cell::{Cell, RefCell}; use std::rc::Rc; -use gpx_engine::{self as engine, Engine, Persister, Storage}; -use js_sys::Object; +use gpx_engine::{self as engine, Engine, Persister, Storage, api}; +use serde::Serialize; use wasm_bindgen::prelude::*; -use crate::convert::{outcome_object, set}; +use crate::convert::{outcome_js, to_js}; use crate::idb::IdbStorage; use crate::session::*; use crate::ts::{Outcome, StorageOpened}; @@ -137,7 +137,7 @@ pub async fn open_storage( let mut persister = Persister::default(); let outcome = restore_engine(persister.restore(&storage).await).map_err(to_js_error)?; let newer = outcome.is_none(); - let outcome = outcome.unwrap_or_else(|| outcome_object(&engine::Outcome::default(), None)); + let outcome = outcome.unwrap_or_else(api::Outcome::failed_none); let settings = storage.load_settings().await.map_err(to_js_error)?; PERSISTER.with(|slot| *slot.borrow_mut() = Some(persister)); @@ -153,14 +153,17 @@ pub async fn open_storage( notify(|listeners| &listeners.newer); } - let settings_object = Object::new(); - for (key, value) in settings { - set(&settings_object, &key, value); + /// What opening the storage found, see `StorageOpened` in `ts`. + #[derive(Serialize)] + struct Opened { + settings: std::collections::BTreeMap, + outcome: api::Outcome, } - let opened = Object::new(); - set(&opened, "settings", settings_object); - set(&opened, "outcome", outcome); - Ok(opened.unchecked_into()) + Ok(to_js(&Opened { + settings: settings.into_iter().collect(), + outcome, + }) + .unchecked_into()) } /// Overwrites the files of the engine with what was read, and tells what that did. If what is @@ -168,12 +171,12 @@ pub async fn open_storage( /// returned: the persister does not write anymore (see `Persister::save`). fn restore_engine( read: Result, -) -> Result, engine::StorageError> { +) -> Result, engine::StorageError> { match read { Ok(restored) => Ok(Some( match with_engine_mut(|engine| engine.restore(restored)) { - Some(outcome) => outcome_object(&outcome, None), - None => outcome_object(&engine::Outcome::default(), None), + Some(outcome) => api::Outcome::new(&outcome, None), + None => api::Outcome::failed_none(), }, )), Err(engine::StorageError::Newer) => Ok(None), @@ -211,19 +214,21 @@ pub async fn sync_storage() -> Result { storage(), PERSISTER.with(|persister| persister.borrow_mut().take()), ) else { - return Ok(outcome_object(&engine::Outcome::default(), None)); + return Ok(outcome_js(&api::Outcome::failed_none())); }; SAVING.set(true); let result = async { if !persister.is_behind(&*storage).await? { - return Ok(outcome_object(&engine::Outcome::default(), None)); + return Ok(outcome_js(&api::Outcome::failed_none())); } let outcome = restore_engine(persister.restore(&*storage).await)?; DIRTY.set(false); if outcome.is_none() { notify(|listeners| &listeners.newer); } - Ok(outcome.unwrap_or_else(|| outcome_object(&engine::Outcome::default(), None))) + Ok(outcome_js( + &outcome.unwrap_or_else(api::Outcome::failed_none), + )) } .await; PERSISTER.with(|slot| *slot.borrow_mut() = Some(persister)); diff --git a/wasm/src/queries/coordinates.rs b/wasm/src/queries/coordinates.rs new file mode 100644 index 000000000..cbf07ef3b --- /dev/null +++ b/wasm/src/queries/coordinates.rs @@ -0,0 +1,33 @@ +//! The coordinates of the trackpoints and waypoints, as flat typed arrays. + +use gpx_engine::api; +use js_sys::Float64Array; +use wasm_bindgen::prelude::*; + +use crate::ids::*; +use crate::session::*; + +// Coordinates buffers +// +// Like the statistics buffers, these are copies. They are flat `[lng, lat, ...]` arrays. Compare `rev` / `waypointsRev` of the file structures with the previous ones to know which +// buffers actually changed. + +fn coordinates( + id: Option, + f: impl FnOnce(&gpx_engine::Engine, uuid::Uuid) -> &[f64], +) -> Float64Array { + id.and_then(|id| with_engine(|e| Float64Array::from(f(e, id)))) + .unwrap_or_else(|| Float64Array::new_with_length(0)) +} + +/// Coordinates of the trackpoints of a segment (empty if the id is unknown). +#[wasm_bindgen] +pub fn segment_coordinates(segment_id: &str) -> Float64Array { + coordinates(parse_uuid(segment_id), api::segment_coordinates) +} + +/// Coordinates of the waypoints of a file (empty if the id is unknown). +#[wasm_bindgen] +pub fn waypoint_coordinates(file_id: &str) -> Float64Array { + coordinates(parse_uuid(file_id), api::waypoint_coordinates) +} diff --git a/wasm/src/queries/files.rs b/wasm/src/queries/files.rs new file mode 100644 index 000000000..5cc64df1d --- /dev/null +++ b/wasm/src/queries/files.rs @@ -0,0 +1,125 @@ +//! The files, their structure and statistics, the selection and the clipboard. + +// File order and structures +// +// The files are described by one ordered list of ids and one structure object per file, so that +// the UI can react to what changed only: after each action, the outcome tells which structures to +// (re)read or drop and whether the order changed. Ids are UUID strings. The trackpoints of a +// segment and the waypoints of a file are in the same order as the coordinates of their buffers +// (see below). + +use gpx_engine::{ExportOptions, api}; +use wasm_bindgen::prelude::*; + +use crate::convert::*; +use crate::ids::*; +use crate::session::*; +use crate::ts::*; + +/// Ids of the files in display order. +#[wasm_bindgen] +pub fn file_order() -> FileOrder { + to_js(&with_engine(api::file_order).unwrap_or_default()).unchecked_into() +} + +/// Structure of a file, `undefined` if the id is unknown. +#[wasm_bindgen] +pub fn file_structure(file_id: &str) -> FileStructure { + let structure = + parse_uuid(file_id).and_then(|id| with_engine(|e| api::file_structure(e, id)).flatten()); + optional_js(structure).unchecked_into() +} + +/// Global statistics of a file, `undefined` if the id is unknown. +#[wasm_bindgen] +pub fn file_statistics(file_id: &str) -> GlobalStatistics { + let statistics = + parse_uuid(file_id).and_then(|id| with_engine(|e| api::file_statistics(e, id)).flatten()); + optional_js(statistics).unchecked_into() +} + +/// Global statistics of the whole selection (the arrays of the statistics buffers cover it). +#[wasm_bindgen] +pub fn selection_statistics() -> GlobalStatistics { + optional_js(with_engine(api::selection_statistics)).unchecked_into() +} + +/// A file as GPX (UTF-8 bytes), `undefined` if it does not exist. The options say which data of the trackpoints +/// to keep, and with `as_route` that the segments are written as routes (`rte`) instead of tracks: +/// the anchors are the route points, and the trackpoints between them the points of their path. +#[wasm_bindgen] +#[allow(clippy::too_many_arguments)] +pub fn export_file( + file_id: &str, + time: bool, + hr: bool, + cad: bool, + atemp: bool, + power: bool, + osm: bool, + as_route: bool, +) -> Option> { + let id = parse_uuid(file_id)?; + let options = ExportOptions { + time, + hr, + cad, + atemp, + power, + osm, + as_route, + }; + with_engine(|e| api::export_file(e, id, options)).flatten() +} + +/// The data that some files have, so that the ones that none has are not offered when exporting +/// them: a bit for the time (1), heart rate (2), cadence (4), temperature (8), power (16) and the +/// OpenStreetMap data (32). `file_ids` are concatenated UUIDs. +#[wasm_bindgen] +pub fn exportable_data(file_ids: &[u8]) -> u8 { + let Some(ids) = uuid_list(file_ids) else { + return 0; + }; + with_engine(|e| api::exportable_data(e, &ids)).unwrap_or(0) +} + +/// Global statistics of the trackpoints from `start` to `end` (both included) of the selection, +/// as indexed by the statistics buffers. `undefined` when the range is not in the selection. +#[wasm_bindgen] +pub fn slice_statistics(start: usize, end: usize) -> GlobalStatistics { + optional_js(with_engine(|e| api::slice_statistics(e, start, end)).flatten()).unchecked_into() +} + +/// The current selection (ids are UUID strings). +#[wasm_bindgen] +pub fn selection() -> Selection { + to_js(&with_engine(api::selection).unwrap_or(api::Selection::Empty)).unchecked_into() +} + +/// All the data of a waypoint of a file, `undefined` if it does not exist. +#[wasm_bindgen] +pub fn waypoint(file_id: &str, waypoint_id: &str) -> WaypointDetails { + let details = parse_uuid(file_id) + .zip(parse_uuid(waypoint_id)) + .and_then(|(file_id, id)| with_engine(|e| api::waypoint(e, file_id, id)).flatten()); + optional_js(details).unchecked_into() +} + +/// A trackpoint of a segment of a file, `undefined` if it does not exist. +#[wasm_bindgen] +pub fn trackpoint(file_id: &str, segment_id: &str, index: usize) -> TrackpointDetails { + let details = + parse_uuid(file_id) + .zip(parse_uuid(segment_id)) + .and_then(|(file_id, segment_id)| { + with_engine(|e| api::trackpoint(e, file_id, segment_id, index)).flatten() + }); + optional_js(details).unchecked_into() +} + +/// What was copied or cut and is waiting to be pasted, `undefined` if nothing: the kind of the +/// elements and their ids (UUID strings). +#[wasm_bindgen] +pub fn clipboard() -> Clipboard { + optional_js(with_engine(api::clipboard).flatten()).unchecked_into() +} diff --git a/gpx-rs/wasm/src/queries/mod.rs b/wasm/src/queries/mod.rs similarity index 100% rename from gpx-rs/wasm/src/queries/mod.rs rename to wasm/src/queries/mod.rs diff --git a/gpx-rs/wasm/src/queries/routing.rs b/wasm/src/queries/routing.rs similarity index 69% rename from gpx-rs/wasm/src/queries/routing.rs rename to wasm/src/queries/routing.rs index d9a98afa7..796944058 100644 --- a/gpx-rs/wasm/src/queries/routing.rs +++ b/wasm/src/queries/routing.rs @@ -1,6 +1,6 @@ //! The anchors of the routing tool among the trackpoints of the selection. -use gpx_engine as engine; +use gpx_engine::api; use js_sys::{Uint8Array, Uint32Array}; use wasm_bindgen::prelude::*; @@ -13,42 +13,36 @@ use crate::ts::*; // The anchors of the routing tool among the trackpoints of the selection (the numbering is the one // of the statistics buffers). The ends of the segments are always anchors. -pub(crate) fn with_routing(f: impl FnOnce(&engine::RoutingBuffer) -> T) -> Option { - ENGINE.with(|engine| engine.borrow().as_ref().map(|e| f(e.routing()))) -} - /// The index in the selection of each anchor. #[wasm_bindgen] pub fn anchor_indices() -> Uint32Array { - with_routing(|r| Uint32Array::from(&r.anchor_indices[..])) + with_engine(|e| Uint32Array::from(api::anchor_indices(e))) .unwrap_or_else(|| Uint32Array::new_with_length(0)) } /// The lowest map zoom level at which each anchor is shown. #[wasm_bindgen] pub fn anchor_zooms() -> Uint8Array { - with_routing(|r| Uint8Array::from(&r.anchor_zooms[..])) + with_engine(|e| Uint8Array::from(api::anchor_zooms(e))) .unwrap_or_else(|| Uint8Array::new_with_length(0)) } /// Index in the selection of the first trackpoint of each selected segment that has some. #[wasm_bindgen] pub fn segment_starts() -> Uint32Array { - with_routing(|r| Uint32Array::from(&r.segment_starts[..])) + with_engine(|e| Uint32Array::from(api::segment_starts(e))) .unwrap_or_else(|| Uint32Array::new_with_length(0)) } /// The id of each selected segment that has trackpoints, as `segment_starts`. #[wasm_bindgen] pub fn segment_ids() -> StringList { - with_routing(|r| array(&r.segment_ids, |id| id.0.to_string().into())) - .unwrap_or_default() - .unchecked_into() + to_js(&with_engine(api::segment_ids).unwrap_or_default()).unchecked_into() } /// Changes when the selected segments or their trackpoints change: the indices in the selection /// are only valid while it stays the same. #[wasm_bindgen] pub fn routing_revision() -> u32 { - with_routing(|r| r.revision).unwrap_or_default() + with_engine(api::routing_revision).unwrap_or_default() } diff --git a/wasm/src/queries/statistics.rs b/wasm/src/queries/statistics.rs new file mode 100644 index 000000000..7595b1aa3 --- /dev/null +++ b/wasm/src/queries/statistics.rs @@ -0,0 +1,105 @@ +//! The statistics of the selection, as typed arrays, and the categories they refer to. + +use gpx_engine::api; +use js_sys::{BigInt64Array, Float64Array, Uint8Array, Uint32Array}; +use wasm_bindgen::prelude::*; + +use crate::convert::*; +use crate::session::*; +use crate::ts::*; + +// Statistics buffers +// +// Each getter returns a copy of the buffer, as a new JS typed array. + +macro_rules! stats_getter { + ($name:ident, $array:ident) => { + #[wasm_bindgen] + pub fn $name() -> $array { + with_engine(|e| $array::from(api::$name(e))) + .unwrap_or_else(|| $array::new_with_length(0)) + } + }; +} + +stats_getter!(total_distance, Float64Array); +stats_getter!(elevation_gain, Float64Array); +stats_getter!(elevation_loss, Float64Array); +stats_getter!(slope, Float64Array); +stats_getter!(slope_segment_slope, Float64Array); +stats_getter!(slope_segment_distance, Float64Array); +stats_getter!(lng, Float64Array); +stats_getter!(lat, Float64Array); +stats_getter!(ele, Float64Array); + +// The optional values are `undefined` when no trackpoint of the selection has one, else they have +// an entry per trackpoint, the missing ones being NaN (`NO_TIME` for the timestamps, 0 before the +// first trackpoint that has them for the cumulative distances and durations). +macro_rules! optional_stats_getter { + ($name:ident, $array:ident) => { + #[wasm_bindgen] + pub fn $name() -> Option<$array> { + with_engine(|e| api::$name(e).map($array::from)).flatten() + } + }; +} + +optional_stats_getter!(moving_distance, Float64Array); +// Durations since the start of the selection, in milliseconds. +optional_stats_getter!(total_time, BigInt64Array); +optional_stats_getter!(moving_time, BigInt64Array); +optional_stats_getter!(speed, Float64Array); +// Timestamps in milliseconds since the epoch (`NO_TIME`, the smallest i64, when missing). +optional_stats_getter!(timestamps, BigInt64Array); + +/// Value of the timestamps of the trackpoints that have none. +#[wasm_bindgen] +pub fn no_time() -> i64 { + gpx_engine::NO_TIME +} + +optional_stats_getter!(hr, Float64Array); +optional_stats_getter!(cad, Float64Array); +optional_stats_getter!(atemp, Float64Array); +optional_stats_getter!(power, Float64Array); + +// Surface, highway, SAC scale and MTB scale of the trackpoints, as intervals of trackpoints that +// have the same value: `_starts` holds the index of the first trackpoint of each interval, +// and `_values` its value (0 when unknown, else 1 + the index in `surfaces()`, +// `highways()`, `sac_scales()` or `mtb_scales()`). A value that is unknown for every trackpoint is +// a single interval. +macro_rules! categories_getters { + ($starts:ident, $values:ident, $names:ident) => { + #[wasm_bindgen] + pub fn $starts() -> Uint32Array { + with_engine(|e| Uint32Array::from(api::$starts(e))) + .unwrap_or_else(|| Uint32Array::new_with_length(0)) + } + + #[wasm_bindgen] + pub fn $values() -> Uint8Array { + with_engine(|e| Uint8Array::from(api::$values(e))) + .unwrap_or_else(|| Uint8Array::new_with_length(0)) + } + + /// The names of the category, in the order of their codes. The list only grows: a name + /// keeps its index. + #[wasm_bindgen] + pub fn $names() -> StringList { + to_js(&with_engine(|e| api::$names(e).to_vec()).unwrap_or_default()).unchecked_into() + } + }; +} + +categories_getters!(surface_starts, surface_values, surfaces); +categories_getters!(highway_starts, highway_values, highways); +categories_getters!(sac_scale_starts, sac_scale_values, sac_scales); +categories_getters!(mtb_scale_starts, mtb_scale_values, mtb_scales); + +/// For each trackpoint of the selection (numbered as in the statistics), the highest tolerance +/// (m) of `reduce` that keeps it: infinite for the ends of the segments. +#[wasm_bindgen] +pub fn reduction_distances() -> Float64Array { + with_engine(|e| Float64Array::from(&api::reduction_distances(e)[..])) + .unwrap_or_else(|| Float64Array::new_with_length(0)) +} diff --git a/gpx-rs/wasm/src/selection.rs b/wasm/src/selection.rs similarity index 53% rename from gpx-rs/wasm/src/selection.rs rename to wasm/src/selection.rs index 89f428b10..f21338d79 100644 --- a/gpx-rs/wasm/src/selection.rs +++ b/wasm/src/selection.rs @@ -1,12 +1,11 @@ //! Changing what is selected. -use gpx_engine::{self as engine, Action}; +use gpx_engine::api; use wasm_bindgen::prelude::*; -use crate::ts::Outcome; - use crate::ids::*; use crate::session::*; +use crate::ts::Outcome; use crate::types::SelectMode; // Selection @@ -15,12 +14,10 @@ use crate::types::SelectMode; /// selecting nothing deselects everything. See `SelectMode`. #[wasm_bindgen] pub fn select(file_ids_bytes: &[u8], mode: SelectMode) -> Outcome { - select_elements( - file_ids(file_ids_bytes).map(|ids| engine::Selection::File { - file_ids: ids.into_iter().collect(), - }), - mode, - ) + match uuid_list(file_ids_bytes) { + Some(ids) => run(|e| api::select(e, &ids, mode.into())), + None => fail("the ids are not UUIDs"), + } } /// Selects all the elements of the same kind as the selected ones, in the same place: all the @@ -28,7 +25,7 @@ pub fn select(file_ids_bytes: &[u8], mode: SelectMode) -> Outcome { /// files when nothing is selected. #[wasm_bindgen] pub fn select_all() -> Outcome { - execute(Action::SelectAll) + run(api::select_all) } /// Moves the selection to the next (`down`) or previous element of the same kind, like the arrow @@ -36,7 +33,7 @@ pub fn select_all() -> Outcome { /// otherwise it replaces it. #[wasm_bindgen] pub fn arrow_select(down: bool, add: bool) -> Outcome { - execute(Action::ArrowSelect { down, add }) + run(|e| api::arrow_select(e, down, add)) } // The elements below cross as the id of their file (UUID string) and, like the files, as @@ -44,27 +41,12 @@ pub fn arrow_select(down: bool, add: bool) -> Outcome { // with the selection when it holds elements of the same kind in the same place (same file, same // track for segments), otherwise they replace it. -pub(crate) fn select_elements(selection: Option, mode: SelectMode) -> Outcome { - match selection { - Some(selection) => execute(Action::Select { - selection, - mode: mode.into(), - }), - None => fail("the ids are not UUIDs"), - } -} - #[wasm_bindgen] pub fn select_tracks(file_id: &str, track_ids_bytes: &[u8], mode: SelectMode) -> Outcome { - select_elements( - parse_file_id(file_id) - .zip(uuid_list(track_ids_bytes)) - .map(|(file_id, ids)| engine::Selection::Track { - file_id, - trk_ids: ids.into_iter().map(engine::TrackId).collect(), - }), - mode, - ) + match parse_uuid(file_id).zip(uuid_list(track_ids_bytes)) { + Some((file_id, ids)) => run(|e| api::select_tracks(e, file_id, &ids, mode.into())), + None => fail("the ids are not UUIDs"), + } } #[wasm_bindgen] @@ -74,37 +56,30 @@ pub fn select_segments( segment_ids_bytes: &[u8], mode: SelectMode, ) -> Outcome { - select_elements( - parse_file_id(file_id) - .zip(uuid::Uuid::parse_str(track_id).ok()) - .zip(uuid_list(segment_ids_bytes)) - .map(|((file_id, trk_id), ids)| engine::Selection::TrackSegment { - file_id, - trk_id: engine::TrackId(trk_id), - trkseg_ids: ids.into_iter().map(engine::TrackSegmentId).collect(), - }), - mode, - ) + match parse_uuid(file_id) + .zip(parse_uuid(track_id)) + .zip(uuid_list(segment_ids_bytes)) + { + Some(((file_id, track_id), ids)) => { + run(|e| api::select_segments(e, file_id, track_id, &ids, mode.into())) + } + None => fail("the ids are not UUIDs"), + } } /// Selects the node standing for all the waypoints of a file. #[wasm_bindgen] pub fn select_waypoint_group(file_id: &str, mode: SelectMode) -> Outcome { - select_elements( - parse_file_id(file_id).map(|file_id| engine::Selection::Waypoints { file_id }), - mode, - ) + match parse_uuid(file_id) { + Some(file_id) => run(|e| api::select_waypoint_group(e, file_id, mode.into())), + None => fail("the ids are not UUIDs"), + } } #[wasm_bindgen] pub fn select_waypoints(file_id: &str, waypoint_ids_bytes: &[u8], mode: SelectMode) -> Outcome { - select_elements( - parse_file_id(file_id) - .zip(uuid_list(waypoint_ids_bytes)) - .map(|(file_id, ids)| engine::Selection::Waypoint { - file_id, - wpt_ids: ids.into_iter().map(engine::WaypointId).collect(), - }), - mode, - ) + match parse_uuid(file_id).zip(uuid_list(waypoint_ids_bytes)) { + Some((file_id, ids)) => run(|e| api::select_waypoints(e, file_id, &ids, mode.into())), + None => fail("the ids are not UUIDs"), + } } diff --git a/gpx-rs/wasm/src/session.rs b/wasm/src/session.rs similarity index 50% rename from gpx-rs/wasm/src/session.rs rename to wasm/src/session.rs index 24b4b326e..2366f6bf9 100644 --- a/gpx-rs/wasm/src/session.rs +++ b/wasm/src/session.rs @@ -1,11 +1,11 @@ -//! The engine that every function talks to, and the way actions reach it. +//! The engine that every function talks to, and the way commands reach it. use std::cell::RefCell; -use gpx_engine::{self as engine, Action, Command, Engine}; +use gpx_engine::{Engine, api}; use wasm_bindgen::prelude::*; -use crate::convert::outcome_object; +use crate::convert::outcome_js; use crate::persistence::schedule_save; use crate::ts::Outcome; @@ -15,25 +15,20 @@ thread_local! { /// What a call that does nothing returns, when the reason is in its arguments rather than in the /// engine: the call is not made. -pub(crate) fn fail(message: impl AsRef) -> Outcome { - outcome_object(&engine::Outcome::default(), Some(message.as_ref())) +pub(crate) fn fail(message: impl Into) -> Outcome { + outcome_js(&api::Outcome::failed(message)) } -/// Runs an action and tells what it did, see `Outcome` in `ts`. -pub(crate) fn execute(action: Action) -> Outcome { - let outcome = match with_engine_mut(|engine| engine.execute(action)) { - Some(Ok(outcome)) => outcome, - Some(Err(error)) => return fail(error.to_string()), - None => return fail("the engine is not started"), +/// Makes a call of the shared interface that edits the files or changes the selection, and tells +/// what it did, see `Outcome` in `ts`. What changed in the files is saved. +pub(crate) fn run(call: impl FnOnce(&mut Engine) -> api::Outcome) -> Outcome { + let Some(outcome) = with_engine_mut(call) else { + return fail("the engine is not started"); }; - if outcome.changed && (outcome.diff.is_some() || outcome.order_changed) { + if outcome.saves { schedule_save(); } - outcome_object(&outcome, None) -} - -pub(crate) fn edit(command: Command) -> Outcome { - execute(Action::Edit(command)) + outcome_js(&outcome) } #[wasm_bindgen] diff --git a/gpx-rs/wasm/src/tests.rs b/wasm/src/tests.rs similarity index 98% rename from gpx-rs/wasm/src/tests.rs rename to wasm/src/tests.rs index f4f79ed1f..ab084d18f 100644 --- a/gpx-rs/wasm/src/tests.rs +++ b/wasm/src/tests.rs @@ -13,9 +13,9 @@ use crate::session::*; use crate::ts::Outcome; use crate::types::*; -const SIMPLE: &[u8] = include_bytes!("../../engine/data/simple.gpx"); -const WITH_TIME: &[u8] = include_bytes!("../../engine/data/with_time.gpx"); -const WITH_SURFACE: &[u8] = include_bytes!("../../engine/data/with_surface.gpx"); +const SIMPLE: &[u8] = include_bytes!("../../gpx-rs/data/simple.gpx"); +const WITH_TIME: &[u8] = include_bytes!("../../gpx-rs/data/with_time.gpx"); +const WITH_SURFACE: &[u8] = include_bytes!("../../gpx-rs/data/with_surface.gpx"); thread_local! { static LAST: std::cell::RefCell = const { std::cell::RefCell::new(JsValue::UNDEFINED) }; diff --git a/gpx-rs/wasm/src/ts.rs b/wasm/src/ts.rs similarity index 100% rename from gpx-rs/wasm/src/ts.rs rename to wasm/src/ts.rs diff --git a/gpx-rs/wasm/src/types.rs b/wasm/src/types.rs similarity index 100% rename from gpx-rs/wasm/src/types.rs rename to wasm/src/types.rs diff --git a/website/package-lock.json b/website/package-lock.json index 5d80bbb81..6ebd6cc74 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -15,7 +15,7 @@ "chart.js": "^4.5.1", "chartjs-plugin-zoom": "^2.2.0", "file-saver": "^2.0.5", - "gpx-rs": "file:../gpx-rs/pkg", + "gpx-wasm": "file:../wasm/pkg", "jszip": "^3.10.1", "mapillary-js": "^4.1.2", "maplibre-gl": "^5.21.1", @@ -92,6 +92,11 @@ }, "../gpx-rs/pkg": { "name": "gpx-rs", + "version": "0.1.0", + "extraneous": true + }, + "../wasm/pkg": { + "name": "gpx-wasm", "version": "0.1.0" }, "node_modules/@algolia/abtesting": { @@ -3948,8 +3953,8 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gpx-rs": { - "resolved": "../gpx-rs/pkg", + "node_modules/gpx-wasm": { + "resolved": "../wasm/pkg", "link": true }, "node_modules/graceful-fs": { diff --git a/website/package.json b/website/package.json index b8756d0ca..f4a51d1e3 100644 --- a/website/package.json +++ b/website/package.json @@ -74,7 +74,7 @@ "chart.js": "^4.5.1", "chartjs-plugin-zoom": "^2.2.0", "file-saver": "^2.0.5", - "gpx-rs": "file:../gpx-rs/pkg", + "gpx-wasm": "file:../wasm/pkg", "jszip": "^3.10.1", "mapillary-js": "^4.1.2", "maplibre-gl": "^5.21.1", diff --git a/website/src/lib/engine/backend/tauri.ts b/website/src/lib/engine/backend/tauri.ts index 45b34d477..764a81a4f 100644 --- a/website/src/lib/engine/backend/tauri.ts +++ b/website/src/lib/engine/backend/tauri.ts @@ -1,5 +1,5 @@ import { invoke } from '@tauri-apps/api/core'; -import type { RouteAttributes, StorageOpened } from 'gpx-rs'; +import type { RouteAttributes, StorageOpened } from 'gpx-wasm'; import type { Backend } from './types'; /** An array that a command returns as its bytes. */ diff --git a/website/src/lib/engine/backend/types.ts b/website/src/lib/engine/backend/types.ts index 1ab899e40..491136efe 100644 --- a/website/src/lib/engine/backend/types.ts +++ b/website/src/lib/engine/backend/types.ts @@ -9,13 +9,13 @@ import type { StorageOpened, TrackpointDetails, WaypointDetails, -} from 'gpx-rs'; +} from 'gpx-wasm'; import type { MergeType, SelectMode, SplitType } from '../types'; /** * What the facade needs of the engine of the files, wherever it runs: the WASM module in the * browser (`wasm.ts`), or the Rust process of the desktop app, through its commands (`tauri.ts`). - * Both give the same results: see `gpx-rs/wasm` for what each function does. + * Both give the same results: see `gpx-rs/src/api` for what each function does. * * Everything is asynchronous, because the desktop app is reached through messages. Ids are * hyphenated UUID strings, the arrays of numbers are typed arrays, and what the functions that diff --git a/website/src/lib/engine/backend/wasm.ts b/website/src/lib/engine/backend/wasm.ts index 033092ed5..9da58b5c7 100644 --- a/website/src/lib/engine/backend/wasm.ts +++ b/website/src/lib/engine/backend/wasm.ts @@ -1,7 +1,7 @@ import type { Backend } from './types'; import type { MergeType, SelectMode, SplitType } from '../types'; -type Wasm = typeof import('gpx-rs'); +type Wasm = typeof import('gpx-wasm'); /** A call of the module, which is synchronous, as an asynchronous one. */ const lift = @@ -21,9 +21,9 @@ function idsToBytes(ids: string[]): Uint8Array { return bytes; } -/** The engine in the page, as the WebAssembly module of `gpx-rs`. */ +/** The engine in the page, as the WebAssembly module of `wasm`. */ export async function createWasmBackend(): Promise { - const w: Wasm = await import('gpx-rs'); + const w: Wasm = await import('gpx-wasm'); w.start(); const mode = (m: SelectMode) => diff --git a/website/src/lib/engine/engine.test.ts b/website/src/lib/engine/engine.test.ts index b880385af..ddd1a42c6 100644 --- a/website/src/lib/engine/engine.test.ts +++ b/website/src/lib/engine/engine.test.ts @@ -1,5 +1,5 @@ -// The frontend side of the engine, against the real engine (the WASM module of `gpx-rs`, which has -// to be built: `wasm-pack build wasm --target bundler --out-dir ../pkg` in `gpx-rs`). +// The frontend side of the engine, against the real engine (the WASM module of `wasm`, which has +// to be built: `wasm-pack build --target bundler` in `wasm`). import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { beforeEach, describe, it } from 'node:test'; @@ -16,7 +16,7 @@ import { function fixture(name: string): Uint8Array { return new Uint8Array( - readFileSync(new URL(`../../../../gpx-rs/engine/data/${name}.gpx`, import.meta.url)) + readFileSync(new URL(`../../../../gpx-rs/data/${name}.gpx`, import.meta.url)) ); } diff --git a/website/src/lib/engine/engine.ts b/website/src/lib/engine/engine.ts index 62cdb70b5..bbaa02b77 100644 --- a/website/src/lib/engine/engine.ts +++ b/website/src/lib/engine/engine.ts @@ -9,7 +9,7 @@ import { } from '$lib/engine/file-visibility'; import { categoryIntervals } from '$lib/engine/trackpoint-categories'; import { selectedElementIds, type FileTreeNode } from '$lib/engine/selection-helpers'; -import type { RouteAttributes } from 'gpx-rs'; +import type { RouteAttributes } from 'gpx-wasm'; import { loadBackend, type Backend } from './backend'; import { readFileState } from './file-state'; import { StatisticsReader } from './statistics'; diff --git a/website/src/lib/engine/file-visibility.test.ts b/website/src/lib/engine/file-visibility.test.ts index 84b6db04a..5443261d1 100644 --- a/website/src/lib/engine/file-visibility.test.ts +++ b/website/src/lib/engine/file-visibility.test.ts @@ -1,6 +1,6 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import type { FileStructure } from 'gpx-rs'; +import type { FileStructure } from 'gpx-wasm'; import { isHidden, isSegmentHidden, diff --git a/website/src/lib/engine/file-visibility.ts b/website/src/lib/engine/file-visibility.ts index d2f3c7dfd..8b413c2df 100644 --- a/website/src/lib/engine/file-visibility.ts +++ b/website/src/lib/engine/file-visibility.ts @@ -1,4 +1,4 @@ -import type { FileStructure } from 'gpx-rs'; +import type { FileStructure } from 'gpx-wasm'; /** * Visibility the user set explicitly, by element id (file, track, segment, waypoint, or the diff --git a/website/src/lib/engine/selection-helpers.test.ts b/website/src/lib/engine/selection-helpers.test.ts index 4e6b59d96..b23279a01 100644 --- a/website/src/lib/engine/selection-helpers.test.ts +++ b/website/src/lib/engine/selection-helpers.test.ts @@ -1,6 +1,6 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import type { Clipboard, FileStructure, Selection } from 'gpx-rs'; +import type { Clipboard, FileStructure, Selection } from 'gpx-wasm'; import type { VisibilityState } from '$lib/engine/file-visibility'; import { elementId, diff --git a/website/src/lib/engine/selection-helpers.ts b/website/src/lib/engine/selection-helpers.ts index 5f47b276a..0184e76db 100644 --- a/website/src/lib/engine/selection-helpers.ts +++ b/website/src/lib/engine/selection-helpers.ts @@ -1,4 +1,4 @@ -import type { Clipboard, Selection } from 'gpx-rs'; +import type { Clipboard, Selection } from 'gpx-wasm'; import { isHidden, waypointsKey, type VisibilityState } from '$lib/engine/file-visibility'; /** diff --git a/website/src/lib/engine/storage.ts b/website/src/lib/engine/storage.ts index a1d7f70c1..4b4abee4b 100644 --- a/website/src/lib/engine/storage.ts +++ b/website/src/lib/engine/storage.ts @@ -1,6 +1,6 @@ import { LEGACY_IMPORTED_KEY, readLegacyData } from '$lib/storage/legacy-import'; import { parseVisibility, VISIBILITY_KEY, type Visibility } from '$lib/engine/file-visibility'; -import type { Outcome, StorageOpened } from 'gpx-rs'; +import type { Outcome, StorageOpened } from 'gpx-wasm'; import type { Backend } from './backend'; /** Name of the IndexedDB database that keeps the files and the settings. */ diff --git a/website/src/lib/engine/types.ts b/website/src/lib/engine/types.ts index a605794b4..7e643ce76 100644 --- a/website/src/lib/engine/types.ts +++ b/website/src/lib/engine/types.ts @@ -2,7 +2,7 @@ import type { Readable } from 'svelte/store'; import type { Feature, FeatureCollection, LineString, Point } from 'geojson'; import type { Visibility } from '$lib/engine/file-visibility'; import type { CategoryIntervals } from '$lib/engine/trackpoint-categories'; -import type { FileStructure, GlobalStatistics } from 'gpx-rs'; +import type { FileStructure, GlobalStatistics } from 'gpx-wasm'; export type { Clipboard, @@ -17,7 +17,7 @@ export type { TrackNode, SegmentNode, WaypointNode, -} from 'gpx-rs'; +} from 'gpx-wasm'; /** * How a merge puts the selected elements together: `connect` makes a single segment of their diff --git a/website/src/lib/scripts/example-statistics.ts b/website/src/lib/scripts/example-statistics.ts index 3fd56ad04..3a8fe7a2f 100644 --- a/website/src/lib/scripts/example-statistics.ts +++ b/website/src/lib/scripts/example-statistics.ts @@ -6,8 +6,8 @@ * * npm run generate:example * - * The engine has to be built (`wasm-pack build wasm --target bundler --out-dir ../pkg`, in - * `gpx-rs`). + * The engine has to be built (`wasm-pack build --target bundler`, in + * `wasm`). */ import fs from 'fs'; import path from 'path'; @@ -16,26 +16,26 @@ import prettier from 'prettier'; const here = path.dirname(fileURLToPath(import.meta.url)); const example = path.resolve(here, '../data/example'); -const packageDirectory = path.resolve(here, '../../../../gpx-rs/pkg'); +const packageDirectory = path.resolve(here, '../../../../wasm/pkg'); export const exampleGPX = path.join(example, 'example.gpx'); export const exampleData = path.join(example, 'example-data.ts'); /** Whether the engine is built. */ export function engineIsBuilt() { - return fs.existsSync(path.join(packageDirectory, 'gpx_rs_bg.wasm')); + return fs.existsSync(path.join(packageDirectory, 'gpx_wasm_bg.wasm')); } /** Loads the engine in Node: the package is made for bundlers, which import the wasm file. */ async function loadEngine() { - const glue = await import(pathToFileURL(path.join(packageDirectory, 'gpx_rs_bg.js')).href); + const glue = await import(pathToFileURL(path.join(packageDirectory, 'gpx_wasm_bg.js')).href); const { instance } = await WebAssembly.instantiate( - fs.readFileSync(path.join(packageDirectory, 'gpx_rs_bg.wasm')), - { './gpx_rs_bg.js': glue } + fs.readFileSync(path.join(packageDirectory, 'gpx_wasm_bg.wasm')), + { './gpx_wasm_bg.js': glue } ); glue.__wbg_set_wasm(instance.exports); (instance.exports.__wbindgen_start as () => void)(); - return glue as typeof import('gpx-rs'); + return glue as typeof import('gpx-wasm'); } type Values = Float64Array | BigInt64Array | undefined;