start with chatgpt
This commit is contained in:
commit
c78520ee65
7 changed files with 2086 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
target/
|
||||||
949
Cargo.lock
generated
Normal file
949
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,949 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allocator-api2"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell_polyfill",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.104"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "approx"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "as-slice"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array 0.12.4",
|
||||||
|
"generic-array 0.13.3",
|
||||||
|
"generic-array 0.14.9",
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "borderpoi-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"clap",
|
||||||
|
"geo",
|
||||||
|
"geo-types",
|
||||||
|
"geojson",
|
||||||
|
"gpx",
|
||||||
|
"rstar 0.13.0",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.6.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.6.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "critical-section"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-deque"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-epoch"
|
||||||
|
version = "0.9.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "earcut"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88459a2a8e3a514b6e6de38cf3aaa9250a894cb098f74a932db77fcc8341b6d0"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "float_next_after"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37007738a80ea34f969af54a3390dd72cacdef654974cfd449c9f6f72dbaac10"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "geo"
|
||||||
|
version = "0.33.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30eb1fdc57c1e5cfd11826fe0caec4b9dc7901f3758263bb506228d88c8d9e9a"
|
||||||
|
dependencies = [
|
||||||
|
"earcut",
|
||||||
|
"float_next_after",
|
||||||
|
"geo-types",
|
||||||
|
"geographiclib-rs",
|
||||||
|
"i_overlay",
|
||||||
|
"log",
|
||||||
|
"num-traits",
|
||||||
|
"rand",
|
||||||
|
"rand_pcg",
|
||||||
|
"robust",
|
||||||
|
"rstar 0.12.2",
|
||||||
|
"sif-itree",
|
||||||
|
"spade",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "geo-types"
|
||||||
|
version = "0.7.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "777d18aa0f12f8b285331cd867133ee14422b3f023f6d388034c47d43e28786a"
|
||||||
|
dependencies = [
|
||||||
|
"approx",
|
||||||
|
"num-traits",
|
||||||
|
"rayon",
|
||||||
|
"rstar 0.10.0",
|
||||||
|
"rstar 0.11.0",
|
||||||
|
"rstar 0.12.2",
|
||||||
|
"rstar 0.13.0",
|
||||||
|
"rstar 0.8.4",
|
||||||
|
"rstar 0.9.3",
|
||||||
|
"serde",
|
||||||
|
"spade",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "geographiclib-rs"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c5a7f08910fd98737a6eda7568e7c5e645093e073328eeef49758cfe8b0489c7"
|
||||||
|
dependencies = [
|
||||||
|
"libm",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "geojson"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "510c094bfc76ea34d02eee00833254945b70491d79a9c0b050abed6eaa799ffb"
|
||||||
|
dependencies = [
|
||||||
|
"geo-types",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
"tinyvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gpx"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfabaf0e8a17a6fb7977fac3bd5846488462edb9f8b246605835483a5501e698"
|
||||||
|
dependencies = [
|
||||||
|
"geo-types",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"time",
|
||||||
|
"xml-rs",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hash32"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hash32"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hash32"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heapless"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "634bd4d29cbf24424d0a4bfcbf80c6960129dc24424752a7d1d1390607023422"
|
||||||
|
dependencies = [
|
||||||
|
"as-slice",
|
||||||
|
"generic-array 0.14.9",
|
||||||
|
"hash32 0.1.1",
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heapless"
|
||||||
|
version = "0.7.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-polyfill",
|
||||||
|
"hash32 0.2.1",
|
||||||
|
"rustc_version",
|
||||||
|
"spin",
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heapless"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
|
||||||
|
dependencies = [
|
||||||
|
"hash32 0.3.1",
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "i_float"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "813145bb0ad5b60f55cbbf3c74cdceda1c0a9d253b35c4cc36ae0df7887cb78f"
|
||||||
|
dependencies = [
|
||||||
|
"libm",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "i_key_sort"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d73d122b937fca067feb0ad74f62388920272b27c356d4df2d0cfdd59e044cf0"
|
||||||
|
dependencies = [
|
||||||
|
"rayon",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "i_overlay"
|
||||||
|
version = "4.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8dd314b4668e2b3a12508f2e125558c82a6c0a8636fa5107a900f79ce414e450"
|
||||||
|
dependencies = [
|
||||||
|
"i_float",
|
||||||
|
"i_key_sort",
|
||||||
|
"i_shape",
|
||||||
|
"i_tree",
|
||||||
|
"rayon",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "i_shape"
|
||||||
|
version = "1.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bfa9eac533d7509a8ab87672b60ac610c17240f9ea4851d26227689fdfe349c8"
|
||||||
|
dependencies = [
|
||||||
|
"i_float",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "i_tree"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4804bdc1dc124eb7e1aa9e144ecc04096bcf787a10a15fa44af682b51f0f6cce"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[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.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||||
|
|
||||||
|
[[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_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pdqselect"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ec91767ecc0a0bbe558ce8c9da33c068066c57ecc8bb8477ef8c1ad3ef77c27"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_pcg"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"rayon-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rayon-core"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "robust"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rstar"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a45c0e8804d37e4d97e55c6f258bc9ad9c5ee7b07437009dd152d764949a27c"
|
||||||
|
dependencies = [
|
||||||
|
"heapless 0.6.1",
|
||||||
|
"num-traits",
|
||||||
|
"pdqselect",
|
||||||
|
"serde",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rstar"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b40f1bfe5acdab44bc63e6699c28b74f75ec43afb59f3eda01e145aff86a25fa"
|
||||||
|
dependencies = [
|
||||||
|
"heapless 0.7.17",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rstar"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f39465655a1e3d8ae79c6d9e007f4953bfc5d55297602df9dc38f9ae9f1359a"
|
||||||
|
dependencies = [
|
||||||
|
"heapless 0.7.17",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rstar"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "73111312eb7a2287d229f06c00ff35b51ddee180f017ab6dec1f69d62ac098d6"
|
||||||
|
dependencies = [
|
||||||
|
"heapless 0.7.17",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rstar"
|
||||||
|
version = "0.12.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb"
|
||||||
|
dependencies = [
|
||||||
|
"heapless 0.8.0",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rstar"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5912b862fa5ffb462607bfd1e35036c458c537921f508c8235a83d5f3987edfe"
|
||||||
|
dependencies = [
|
||||||
|
"heapless 0.8.0",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc_version"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||||
|
dependencies = [
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "sif-itree"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7f45b8998ced5134fb1d75732c77842a3e888f19c1ff98481822e8fbfbf930b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spade"
|
||||||
|
version = "2.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown",
|
||||||
|
"num-traits",
|
||||||
|
"robust",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.119"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl 2.0.20",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.55"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
|
"serde_core",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"tinyvec_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec_macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[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 = "xml-rs"
|
||||||
|
version = "0.8.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
23
Cargo.toml
Normal file
23
Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "borderpoi-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Find administrative border crossings along a GPX track"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "borderpoi-rs"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
|
|
||||||
|
geo = "0.33.1"
|
||||||
|
geo-types = "0.7.19"
|
||||||
|
geojson = { version = "1.0.0", features = ["geo-types"] }
|
||||||
|
gpx = "0.10.0"
|
||||||
|
rstar = "0.13.0"
|
||||||
|
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
330
README.md
Normal file
330
README.md
Normal file
|
|
@ -0,0 +1,330 @@
|
||||||
|
# borderpoi-rs
|
||||||
|
|
||||||
|
`borderpoi-rs` finds administrative border crossings along a GPX track.
|
||||||
|
|
||||||
|
It is designed for long-distance cycling, bikepacking and ultracycling.
|
||||||
|
|
||||||
|
The program reads a GPX track and a GeoJSON file containing administrative
|
||||||
|
boundary polygons. It detects every county/district transition, preserves
|
||||||
|
repeated visits, and adds a GPX waypoint for every border crossing.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- GPX input
|
||||||
|
- GPX output
|
||||||
|
- stdin/stdout support
|
||||||
|
- GeoJSON boundary input
|
||||||
|
- R*-tree spatial index
|
||||||
|
- repeated county visits are preserved
|
||||||
|
- one GPX POI per border crossing
|
||||||
|
- no distance calculation
|
||||||
|
- no GDAL dependency
|
||||||
|
- no native GIS library dependency
|
||||||
|
|
||||||
|
## Input boundary data
|
||||||
|
|
||||||
|
The boundary GeoJSON must contain Polygon or MultiPolygon features.
|
||||||
|
|
||||||
|
Coordinates must be WGS84 / EPSG:4326.
|
||||||
|
|
||||||
|
For BKG VG250 data, the relevant layer is normally:
|
||||||
|
|
||||||
|
VG250_KRS
|
||||||
|
|
||||||
|
The default properties expected by `borderpoi-rs` are:
|
||||||
|
|
||||||
|
GEN
|
||||||
|
AGS
|
||||||
|
|
||||||
|
`GEN` is used as the displayed county name.
|
||||||
|
|
||||||
|
`AGS` is used as the administrative identifier.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### GPX file to GPX file
|
||||||
|
|
||||||
|
borderpoi-rs \
|
||||||
|
--track route.gpx \
|
||||||
|
--boundaries VG250_KRS.geojson \
|
||||||
|
--output route-with-borders.gpx
|
||||||
|
|
||||||
|
### stdin to stdout
|
||||||
|
|
||||||
|
cat route.gpx \
|
||||||
|
| borderpoi-rs \
|
||||||
|
--boundaries VG250_KRS.geojson \
|
||||||
|
> route-with-borders.gpx
|
||||||
|
|
||||||
|
### Explicit stdin/stdout
|
||||||
|
|
||||||
|
`-` can be used explicitly:
|
||||||
|
|
||||||
|
borderpoi-rs \
|
||||||
|
--track - \
|
||||||
|
--boundaries VG250_KRS.geojson \
|
||||||
|
--output -
|
||||||
|
|
||||||
|
### Unix pipeline
|
||||||
|
|
||||||
|
The program writes diagnostic information to stderr and GPX data to stdout.
|
||||||
|
|
||||||
|
Therefore this works:
|
||||||
|
|
||||||
|
borderpoi-rs \
|
||||||
|
--boundaries VG250_KRS.geojson \
|
||||||
|
< route.gpx \
|
||||||
|
> route-with-borders.gpx
|
||||||
|
|
||||||
|
The county report can still be seen in the terminal.
|
||||||
|
|
||||||
|
## Command line
|
||||||
|
|
||||||
|
Usage: borderpoi-rs [OPTIONS] --boundaries <BOUNDARIES>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-t, --track <TRACK>
|
||||||
|
Input GPX track. Reads from stdin when omitted or set to '-'.
|
||||||
|
|
||||||
|
-b, --boundaries <BOUNDARIES>
|
||||||
|
Boundary GeoJSON.
|
||||||
|
|
||||||
|
The GeoJSON must contain Polygon/MultiPolygon geometries
|
||||||
|
in WGS84 / EPSG:4326.
|
||||||
|
|
||||||
|
Required.
|
||||||
|
|
||||||
|
--name-field <NAME_FIELD>
|
||||||
|
Property containing the county name.
|
||||||
|
|
||||||
|
[default: GEN]
|
||||||
|
|
||||||
|
--id-field <ID_FIELD>
|
||||||
|
Property containing the administrative identifier.
|
||||||
|
|
||||||
|
[default: AGS]
|
||||||
|
|
||||||
|
-o, --output <OUTPUT>
|
||||||
|
Output GPX. Writes to stdout when omitted or set to '-'.
|
||||||
|
|
||||||
|
-h, --help
|
||||||
|
Print help.
|
||||||
|
|
||||||
|
-V, --version
|
||||||
|
Print version.
|
||||||
|
|
||||||
|
## County sequence
|
||||||
|
|
||||||
|
The output preserves the order in which counties are visited.
|
||||||
|
|
||||||
|
Repeated visits are intentionally preserved.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
Roth
|
||||||
|
Neumarkt i.d.OPf.
|
||||||
|
Regensburg
|
||||||
|
Neumarkt i.d.OPf.
|
||||||
|
Roth
|
||||||
|
|
||||||
|
is reported as exactly that.
|
||||||
|
|
||||||
|
It is not reduced to:
|
||||||
|
|
||||||
|
Roth
|
||||||
|
Neumarkt i.d.OPf.
|
||||||
|
Regensburg
|
||||||
|
|
||||||
|
This is important for bikepacking and ultracycling routes that cross
|
||||||
|
administrative boundaries multiple times.
|
||||||
|
|
||||||
|
## Border POIs
|
||||||
|
|
||||||
|
For every transition, a waypoint is added to the output GPX.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
Border 01: Landkreis Roth -> Landkreis Neumarkt i.d.OPf.
|
||||||
|
Border 02: Landkreis Neumarkt i.d.OPf. -> Landkreis Regensburg
|
||||||
|
Border 03: Landkreis Regensburg -> Landkreis Neumarkt i.d.OPf.
|
||||||
|
Border 04: Landkreis Neumarkt i.d.OPf. -> Landkreis Roth
|
||||||
|
|
||||||
|
Each waypoint contains:
|
||||||
|
|
||||||
|
- crossing number
|
||||||
|
- source county
|
||||||
|
- destination county
|
||||||
|
- source AGS
|
||||||
|
- destination AGS
|
||||||
|
- `administrative_boundary` as GPX waypoint type
|
||||||
|
|
||||||
|
## Spatial index
|
||||||
|
|
||||||
|
All county geometries are inserted into an R*-tree.
|
||||||
|
|
||||||
|
For every GPX track segment the algorithm performs:
|
||||||
|
|
||||||
|
GPX segment
|
||||||
|
|
|
||||||
|
v
|
||||||
|
segment bounding box
|
||||||
|
|
|
||||||
|
v
|
||||||
|
R*-tree lookup
|
||||||
|
|
|
||||||
|
v
|
||||||
|
candidate county polygons
|
||||||
|
|
|
||||||
|
v
|
||||||
|
exact point-in-polygon test
|
||||||
|
|
|
||||||
|
v
|
||||||
|
county transition
|
||||||
|
|
|
||||||
|
v
|
||||||
|
exact boundary intersection
|
||||||
|
|
|
||||||
|
v
|
||||||
|
GPX waypoint
|
||||||
|
|
||||||
|
This avoids testing every GPX segment against every county polygon.
|
||||||
|
|
||||||
|
This is particularly useful for long GPX tracks.
|
||||||
|
|
||||||
|
## Coordinate reference system
|
||||||
|
|
||||||
|
`borderpoi-rs` expects the boundary GeoJSON to use:
|
||||||
|
|
||||||
|
EPSG:4326 / WGS84
|
||||||
|
|
||||||
|
GPX coordinates are also WGS84.
|
||||||
|
|
||||||
|
No CRS transformation is performed inside the program.
|
||||||
|
|
||||||
|
This is intentional: it removes the GDAL dependency and keeps the
|
||||||
|
application entirely Rust-native.
|
||||||
|
|
||||||
|
## Why no GDAL?
|
||||||
|
|
||||||
|
The previous implementation used GDAL to read the BKG GeoPackage.
|
||||||
|
|
||||||
|
That caused the Rust build to depend on the system GDAL version.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
gdal 0.19.0
|
||||||
|
gdal-sys 0.12.0
|
||||||
|
system GDAL 3.13.2
|
||||||
|
|
||||||
|
and required generated GDAL bindings.
|
||||||
|
|
||||||
|
`borderpoi-rs` does not actually need GDAL for its runtime operation.
|
||||||
|
|
||||||
|
The recommended workflow is therefore:
|
||||||
|
|
||||||
|
BKG VG250
|
||||||
|
|
|
||||||
|
| one-time conversion
|
||||||
|
v
|
||||||
|
WGS84 GeoJSON
|
||||||
|
|
|
||||||
|
v
|
||||||
|
borderpoi-rs
|
||||||
|
|
||||||
|
The conversion from the original BKG dataset can be performed with
|
||||||
|
GDAL/QGIS once, but the resulting command line tool has no GDAL
|
||||||
|
dependency.
|
||||||
|
|
||||||
|
## Building with Nix
|
||||||
|
|
||||||
|
Enter the development environment:
|
||||||
|
|
||||||
|
nix develop
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
cargo run --release -- \
|
||||||
|
--boundaries VG250_KRS.geojson \
|
||||||
|
< route.gpx \
|
||||||
|
> route-with-borders.gpx
|
||||||
|
|
||||||
|
## Building entirely with Nix
|
||||||
|
|
||||||
|
First generate the lock file:
|
||||||
|
|
||||||
|
cargo generate-lockfile
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
nix build
|
||||||
|
|
||||||
|
The resulting executable is:
|
||||||
|
|
||||||
|
./result/bin/borderpoi-rs
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
./result/bin/borderpoi-rs \
|
||||||
|
--boundaries VG250_KRS.geojson \
|
||||||
|
--track route.gpx \
|
||||||
|
--output route-with-borders.gpx
|
||||||
|
|
||||||
|
## Formatting and linting
|
||||||
|
|
||||||
|
Format:
|
||||||
|
|
||||||
|
cargo fmt
|
||||||
|
|
||||||
|
Check:
|
||||||
|
|
||||||
|
cargo check
|
||||||
|
|
||||||
|
Run Clippy:
|
||||||
|
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
|
## Input assumptions
|
||||||
|
|
||||||
|
The current implementation assumes:
|
||||||
|
|
||||||
|
1. GPX coordinates are WGS84.
|
||||||
|
2. Boundary coordinates are WGS84.
|
||||||
|
3. Boundary features are Polygon or MultiPolygon.
|
||||||
|
4. The GPX track normally lies inside a county polygon.
|
||||||
|
5. Administrative boundary geometries are topologically valid.
|
||||||
|
|
||||||
|
Tracks that run exactly along a county boundary are inherently ambiguous.
|
||||||
|
|
||||||
|
Such a section is deliberately not interpreted as a sequence of
|
||||||
|
crossings.
|
||||||
|
|
||||||
|
## Output contract
|
||||||
|
|
||||||
|
stdout:
|
||||||
|
|
||||||
|
output GPX only
|
||||||
|
|
||||||
|
stderr:
|
||||||
|
|
||||||
|
diagnostic messages
|
||||||
|
county sequence
|
||||||
|
border crossing report
|
||||||
|
|
||||||
|
This makes the program suitable for Unix pipelines.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
borderpoi-rs \
|
||||||
|
--boundaries counties.geojson \
|
||||||
|
< route.gpx \
|
||||||
|
> result.gpx
|
||||||
|
|
||||||
|
while the report remains visible on the terminal.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1786384358,
|
||||||
|
"narHash": "sha256-RzPPiWeUtuvymnpuEWsdtzli5w4kjZs49FqEs3/1u+I=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "2fcb964de67fcf60b43471c55d5d99e61a9ccb5a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
52
flake.nix
Normal file
52
flake.nix
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
description = "borderpoi-rs - GPX administrative border crossing detector";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
rustc
|
||||||
|
cargo
|
||||||
|
rustfmt
|
||||||
|
clippy
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
echo "borderpoi-rs development environment"
|
||||||
|
echo "Rust: $(rustc --version)"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
packages.default =
|
||||||
|
pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = "borderpoi-rs";
|
||||||
|
version = "0.1.0";
|
||||||
|
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
cargoLock = {
|
||||||
|
lockFile = ./Cargo.lock;
|
||||||
|
};
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description =
|
||||||
|
"Find administrative border crossings along a GPX track";
|
||||||
|
|
||||||
|
mainProgram =
|
||||||
|
"borderpoi-rs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
670
src/main.rs
Normal file
670
src/main.rs
Normal file
|
|
@ -0,0 +1,670 @@
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use geo::{algorithm::bounding_rect::BoundingRect, Contains, Coord, Geometry, LineString, Point};
|
||||||
|
|
||||||
|
use geojson::{Feature, GeoJson};
|
||||||
|
|
||||||
|
use gpx::{read, write, Gpx, Waypoint};
|
||||||
|
|
||||||
|
use rstar::{RTree, RTreeObject, AABB};
|
||||||
|
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
|
collections::HashSet,
|
||||||
|
fs::File,
|
||||||
|
io::{self, BufReader, BufWriter},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(
|
||||||
|
name = "borderpoi-rs",
|
||||||
|
version,
|
||||||
|
about = "Find administrative border crossings along a GPX track"
|
||||||
|
)]
|
||||||
|
struct Args {
|
||||||
|
/// Input GPX track. Reads from stdin when omitted or set to '-'.
|
||||||
|
#[arg(short, long)]
|
||||||
|
track: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Boundary GeoJSON in WGS84 / EPSG:4326.
|
||||||
|
#[arg(short, long)]
|
||||||
|
boundaries: PathBuf,
|
||||||
|
|
||||||
|
/// GeoJSON property containing the county name.
|
||||||
|
#[arg(long, default_value = "GEN")]
|
||||||
|
name_field: String,
|
||||||
|
|
||||||
|
/// GeoJSON property containing the administrative identifier.
|
||||||
|
#[arg(long, default_value = "AGS")]
|
||||||
|
id_field: String,
|
||||||
|
|
||||||
|
/// Output GPX. Writes to stdout when omitted or set to '-'.
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct County {
|
||||||
|
name: String,
|
||||||
|
ags: String,
|
||||||
|
geometry: Geometry<f64>,
|
||||||
|
envelope: AABB<[f64; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RTreeObject for County {
|
||||||
|
type Envelope = AABB<[f64; 2]>;
|
||||||
|
|
||||||
|
fn envelope(&self) -> Self::Envelope {
|
||||||
|
self.envelope
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct TrackPoint {
|
||||||
|
lon: f64,
|
||||||
|
lat: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Crossing {
|
||||||
|
point: Point<f64>,
|
||||||
|
from: County,
|
||||||
|
to: County,
|
||||||
|
segment_index: usize,
|
||||||
|
position: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct BoundaryHit {
|
||||||
|
point: Point<f64>,
|
||||||
|
position: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Main
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
eprintln!("borderpoi-rs");
|
||||||
|
eprintln!("============");
|
||||||
|
eprintln!();
|
||||||
|
|
||||||
|
let gpx = read_gpx(args.track.as_deref())?;
|
||||||
|
|
||||||
|
let track = extract_track_points(&gpx)?;
|
||||||
|
|
||||||
|
eprintln!("Track points: {}", track.len());
|
||||||
|
|
||||||
|
let counties = load_counties(&args.boundaries, &args.name_field, &args.id_field)?;
|
||||||
|
|
||||||
|
eprintln!("Loaded counties: {}", counties.len());
|
||||||
|
|
||||||
|
let tree = RTree::bulk_load(counties);
|
||||||
|
|
||||||
|
eprintln!("Spatial index: R*-tree");
|
||||||
|
|
||||||
|
let crossings = find_crossings(&track, &tree)?;
|
||||||
|
|
||||||
|
print_report(&crossings);
|
||||||
|
|
||||||
|
let mut output_gpx = gpx;
|
||||||
|
|
||||||
|
append_crossing_waypoints(&mut output_gpx, &crossings);
|
||||||
|
|
||||||
|
write_gpx(&output_gpx, args.output.as_deref())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// GPX I/O
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn read_gpx(path: Option<&Path>) -> Result<Gpx> {
|
||||||
|
match path {
|
||||||
|
Some(path) if path.as_os_str() != "-" => {
|
||||||
|
let file = File::open(path)
|
||||||
|
.with_context(|| format!("Failed to open GPX: {}", path.display()))?;
|
||||||
|
|
||||||
|
read(BufReader::new(file)).context("Failed to parse GPX")
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
let stdin = io::stdin();
|
||||||
|
read(stdin.lock()).context("Failed to parse GPX from stdin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_gpx(gpx: &Gpx, path: Option<&Path>) -> Result<()> {
|
||||||
|
match path {
|
||||||
|
Some(path) if path.as_os_str() != "-" => {
|
||||||
|
let file = File::create(path)
|
||||||
|
.with_context(|| format!("Failed to create output GPX: {}", path.display()))?;
|
||||||
|
|
||||||
|
write(gpx, BufWriter::new(file)).context("Failed to write GPX")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
let stdout = io::stdout();
|
||||||
|
|
||||||
|
write(gpx, BufWriter::new(stdout.lock())).context("Failed to write GPX to stdout")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_track_points(gpx: &Gpx) -> Result<Vec<TrackPoint>> {
|
||||||
|
let mut points = Vec::new();
|
||||||
|
|
||||||
|
for track in &gpx.tracks {
|
||||||
|
for segment in &track.segments {
|
||||||
|
for waypoint in &segment.points {
|
||||||
|
let point = waypoint.point();
|
||||||
|
|
||||||
|
points.push(TrackPoint {
|
||||||
|
lon: point.x(),
|
||||||
|
lat: point.y(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if points.len() < 2 {
|
||||||
|
bail!("The GPX contains fewer than two track points.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(points)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// GeoJSON
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn load_counties(path: &Path, name_field: &str, id_field: &str) -> Result<Vec<County>> {
|
||||||
|
let file =
|
||||||
|
File::open(path).with_context(|| format!("Failed to open GeoJSON: {}", path.display()))?;
|
||||||
|
|
||||||
|
let geojson: GeoJson = serde_json::from_reader(BufReader::new(file))
|
||||||
|
.context("Failed to parse boundary GeoJSON")?;
|
||||||
|
|
||||||
|
let features = match geojson {
|
||||||
|
GeoJson::FeatureCollection(collection) => collection.features,
|
||||||
|
|
||||||
|
GeoJson::Feature(feature) => {
|
||||||
|
vec![feature]
|
||||||
|
}
|
||||||
|
|
||||||
|
GeoJson::Geometry(_) => {
|
||||||
|
bail!("Boundary GeoJSON must be a FeatureCollection or Feature.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut counties = Vec::with_capacity(features.len());
|
||||||
|
|
||||||
|
for feature in features {
|
||||||
|
counties.push(feature_to_county(&feature, name_field, id_field)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
if counties.is_empty() {
|
||||||
|
bail!("No boundary features were found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(counties)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn feature_to_county(feature: &Feature, name_field: &str, id_field: &str) -> Result<County> {
|
||||||
|
let properties = feature
|
||||||
|
.properties
|
||||||
|
.as_ref()
|
||||||
|
.context("Boundary feature has no properties.")?;
|
||||||
|
|
||||||
|
let name = property_as_string(
|
||||||
|
properties
|
||||||
|
.get(name_field)
|
||||||
|
.with_context(|| format!("Missing name field '{}'.", name_field))?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let ags = property_as_string(
|
||||||
|
properties
|
||||||
|
.get(id_field)
|
||||||
|
.with_context(|| format!("Missing ID field '{}'.", id_field))?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let geojson_geometry = feature
|
||||||
|
.geometry
|
||||||
|
.as_ref()
|
||||||
|
.context("Boundary feature has no geometry.")?;
|
||||||
|
|
||||||
|
let geometry: Geometry<f64> = geojson_geometry
|
||||||
|
.try_into()
|
||||||
|
.context("Failed to convert GeoJSON geometry.")?;
|
||||||
|
|
||||||
|
match geometry {
|
||||||
|
Geometry::Polygon(_) | Geometry::MultiPolygon(_) => {}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
bail!("Feature '{}' is not a Polygon or MultiPolygon.", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bbox = geometry
|
||||||
|
.bounding_rect()
|
||||||
|
.with_context(|| format!("Feature '{}' has no bounding box.", name))?;
|
||||||
|
|
||||||
|
let envelope = AABB::from_corners([bbox.min().x, bbox.min().y], [bbox.max().x, bbox.max().y]);
|
||||||
|
|
||||||
|
Ok(County {
|
||||||
|
name,
|
||||||
|
ags,
|
||||||
|
geometry,
|
||||||
|
envelope,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn property_as_string(value: &Value) -> Result<String> {
|
||||||
|
match value {
|
||||||
|
Value::String(value) => Ok(value.clone()),
|
||||||
|
|
||||||
|
Value::Number(value) => Ok(value.to_string()),
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
bail!("Expected string or number property, got {}.", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Track processing
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Find all county transitions along the complete track.
|
||||||
|
///
|
||||||
|
/// Every track segment is processed independently. A segment may cross
|
||||||
|
/// multiple administrative boundaries. All intersections are therefore
|
||||||
|
/// collected, sorted along the segment, and converted into transitions.
|
||||||
|
fn find_crossings(track: &[TrackPoint], tree: &RTree<County>) -> Result<Vec<Crossing>> {
|
||||||
|
let mut crossings = Vec::new();
|
||||||
|
|
||||||
|
for (segment_index, pair) in track.windows(2).enumerate() {
|
||||||
|
let start = Point::new(pair[0].lon, pair[0].lat);
|
||||||
|
|
||||||
|
let end = Point::new(pair[1].lon, pair[1].lat);
|
||||||
|
|
||||||
|
let segment = LineString::from(vec![
|
||||||
|
Coord {
|
||||||
|
x: start.x(),
|
||||||
|
y: start.y(),
|
||||||
|
},
|
||||||
|
Coord {
|
||||||
|
x: end.x(),
|
||||||
|
y: end.y(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
let bbox = segment
|
||||||
|
.bounding_rect()
|
||||||
|
.context("Track segment has no bounding box.")?;
|
||||||
|
|
||||||
|
let envelope =
|
||||||
|
AABB::from_corners([bbox.min().x, bbox.min().y], [bbox.max().x, bbox.max().y]);
|
||||||
|
|
||||||
|
let candidates: Vec<County> = tree
|
||||||
|
.locate_in_envelope_intersecting(envelope)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if candidates.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hits = collect_segment_hits(start, end, &candidates);
|
||||||
|
|
||||||
|
if hits.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let transitions = reconstruct_transitions(start, end, &candidates, &hits);
|
||||||
|
|
||||||
|
for transition in transitions {
|
||||||
|
crossings.push(Crossing {
|
||||||
|
point: transition.point,
|
||||||
|
from: transition.from,
|
||||||
|
to: transition.to,
|
||||||
|
segment_index,
|
||||||
|
position: transition.position,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(deduplicate_crossings(crossings))
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Boundary intersections
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn collect_segment_hits(
|
||||||
|
start: Point<f64>,
|
||||||
|
end: Point<f64>,
|
||||||
|
candidates: &[County],
|
||||||
|
) -> Vec<BoundaryHit> {
|
||||||
|
let start_coord = Coord {
|
||||||
|
x: start.x(),
|
||||||
|
y: start.y(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let end_coord = Coord {
|
||||||
|
x: end.x(),
|
||||||
|
y: end.y(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut hits = Vec::new();
|
||||||
|
|
||||||
|
for county in candidates {
|
||||||
|
collect_geometry_intersections(&county.geometry, start_coord, end_coord, &mut hits);
|
||||||
|
}
|
||||||
|
|
||||||
|
hits.sort_by(|a, b| {
|
||||||
|
a.position
|
||||||
|
.partial_cmp(&b.position)
|
||||||
|
.unwrap_or(Ordering::Equal)
|
||||||
|
});
|
||||||
|
|
||||||
|
deduplicate_hits(hits)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_geometry_intersections(
|
||||||
|
geometry: &Geometry<f64>,
|
||||||
|
start: Coord<f64>,
|
||||||
|
end: Coord<f64>,
|
||||||
|
hits: &mut Vec<BoundaryHit>,
|
||||||
|
) {
|
||||||
|
match geometry {
|
||||||
|
Geometry::Polygon(polygon) => {
|
||||||
|
collect_ring_intersections(polygon.exterior(), start, end, hits);
|
||||||
|
|
||||||
|
for interior in polygon.interiors() {
|
||||||
|
collect_ring_intersections(interior, start, end, hits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Geometry::MultiPolygon(multipolygon) => {
|
||||||
|
for polygon in &multipolygon.0 {
|
||||||
|
collect_ring_intersections(polygon.exterior(), start, end, hits);
|
||||||
|
|
||||||
|
for interior in polygon.interiors() {
|
||||||
|
collect_ring_intersections(interior, start, end, hits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_ring_intersections(
|
||||||
|
ring: &LineString<f64>,
|
||||||
|
start: Coord<f64>,
|
||||||
|
end: Coord<f64>,
|
||||||
|
hits: &mut Vec<BoundaryHit>,
|
||||||
|
) {
|
||||||
|
for edge in ring.lines() {
|
||||||
|
if let Some((position, point)) = segment_intersection(start, end, edge.start, edge.end) {
|
||||||
|
hits.push(BoundaryHit { point, position });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn segment_intersection(
|
||||||
|
p: Coord<f64>,
|
||||||
|
p2: Coord<f64>,
|
||||||
|
q: Coord<f64>,
|
||||||
|
q2: Coord<f64>,
|
||||||
|
) -> Option<(f64, Point<f64>)> {
|
||||||
|
let r = Coord {
|
||||||
|
x: p2.x - p.x,
|
||||||
|
y: p2.y - p.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
let s = Coord {
|
||||||
|
x: q2.x - q.x,
|
||||||
|
y: q2.y - q.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
let denominator = cross(r, s);
|
||||||
|
|
||||||
|
if denominator.abs() < 1e-14 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let qp = Coord {
|
||||||
|
x: q.x - p.x,
|
||||||
|
y: q.y - p.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
let t = cross(qp, s) / denominator;
|
||||||
|
let u = cross(qp, r) / denominator;
|
||||||
|
|
||||||
|
const EPSILON: f64 = 1e-10;
|
||||||
|
|
||||||
|
if !(-EPSILON..=1.0 + EPSILON).contains(&t) || !(-EPSILON..=1.0 + EPSILON).contains(&u) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let point = Point::new(p.x + t * r.x, p.y + t * r.y);
|
||||||
|
|
||||||
|
Some((t.clamp(0.0, 1.0), point))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cross(a: Coord<f64>, b: Coord<f64>) -> f64 {
|
||||||
|
a.x * b.y - a.y * b.x
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deduplicate_hits(hits: Vec<BoundaryHit>) -> Vec<BoundaryHit> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
for hit in hits {
|
||||||
|
let duplicate = result
|
||||||
|
.iter()
|
||||||
|
.any(|existing: &BoundaryHit| (existing.position - hit.position).abs() < 1e-9);
|
||||||
|
|
||||||
|
if !duplicate {
|
||||||
|
result.push(hit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// County reconstruction
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Transition {
|
||||||
|
point: Point<f64>,
|
||||||
|
position: f64,
|
||||||
|
from: County,
|
||||||
|
to: County,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reconstruct the county sequence between all boundary intersections.
|
||||||
|
///
|
||||||
|
/// The county immediately before an intersection is sampled slightly before
|
||||||
|
/// the intersection; the county immediately after is sampled slightly after
|
||||||
|
/// it. This avoids ambiguity when the intersection lies exactly on a shared
|
||||||
|
/// polygon boundary.
|
||||||
|
fn reconstruct_transitions(
|
||||||
|
start: Point<f64>,
|
||||||
|
end: Point<f64>,
|
||||||
|
candidates: &[County],
|
||||||
|
hits: &[BoundaryHit],
|
||||||
|
) -> Vec<Transition> {
|
||||||
|
let mut transitions = Vec::new();
|
||||||
|
|
||||||
|
for hit in hits {
|
||||||
|
let before_t = (hit.position - 1e-8).max(0.0);
|
||||||
|
|
||||||
|
let after_t = (hit.position + 1e-8).min(1.0);
|
||||||
|
|
||||||
|
let before = interpolate(start, end, before_t);
|
||||||
|
|
||||||
|
let after = interpolate(start, end, after_t);
|
||||||
|
|
||||||
|
let from = county_at_point(before, candidates);
|
||||||
|
|
||||||
|
let to = county_at_point(after, candidates);
|
||||||
|
|
||||||
|
let (Some(from), Some(to)) = (from, to) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if from.ags == to.ags {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
transitions.push(Transition {
|
||||||
|
point: hit.point,
|
||||||
|
position: hit.position,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
transitions
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interpolate(start: Point<f64>, end: Point<f64>, t: f64) -> Point<f64> {
|
||||||
|
Point::new(
|
||||||
|
start.x() + t * (end.x() - start.x()),
|
||||||
|
start.y() + t * (end.y() - start.y()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn county_at_point(point: Point<f64>, candidates: &[County]) -> Option<County> {
|
||||||
|
candidates
|
||||||
|
.iter()
|
||||||
|
.find(|county| county.geometry.contains(&point))
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Crossing cleanup
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn deduplicate_crossings(crossings: Vec<Crossing>) -> Vec<Crossing> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
|
||||||
|
for crossing in crossings {
|
||||||
|
let duplicate = result.iter().any(|existing: &Crossing| {
|
||||||
|
existing.segment_index == crossing.segment_index
|
||||||
|
&& existing.from.ags == crossing.from.ags
|
||||||
|
&& existing.to.ags == crossing.to.ags
|
||||||
|
&& same_point(existing.point, crossing.point)
|
||||||
|
});
|
||||||
|
|
||||||
|
if !duplicate {
|
||||||
|
result.push(crossing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.sort_by(|a, b| {
|
||||||
|
a.segment_index.cmp(&b.segment_index).then_with(|| {
|
||||||
|
a.position
|
||||||
|
.partial_cmp(&b.position)
|
||||||
|
.unwrap_or(Ordering::Equal)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn same_point(a: Point<f64>, b: Point<f64>) -> bool {
|
||||||
|
let dx = a.x() - b.x();
|
||||||
|
let dy = a.y() - b.y();
|
||||||
|
|
||||||
|
dx * dx + dy * dy < 1e-10
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Report
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn print_report(crossings: &[Crossing]) {
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("County visits");
|
||||||
|
eprintln!("=============");
|
||||||
|
|
||||||
|
if crossings.is_empty() {
|
||||||
|
eprintln!("No county crossings found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut visits = Vec::new();
|
||||||
|
|
||||||
|
visits.push(crossings[0].from.clone());
|
||||||
|
|
||||||
|
for crossing in crossings {
|
||||||
|
visits.push(crossing.to.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index, county) in visits.iter().enumerate() {
|
||||||
|
eprintln!("{:3} {} [{}]", index + 1, county.name, county.ags);
|
||||||
|
}
|
||||||
|
|
||||||
|
let unique: HashSet<String> = visits.iter().map(|county| county.ags.clone()).collect();
|
||||||
|
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("Visits: {}", visits.len());
|
||||||
|
|
||||||
|
eprintln!("Unique counties: {}", unique.len());
|
||||||
|
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("Border crossings");
|
||||||
|
eprintln!("=================");
|
||||||
|
|
||||||
|
for (index, crossing) in crossings.iter().enumerate() {
|
||||||
|
eprintln!(
|
||||||
|
"{:3} {} -> {}",
|
||||||
|
index + 1,
|
||||||
|
crossing.from.name,
|
||||||
|
crossing.to.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// GPX POIs
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn append_crossing_waypoints(gpx: &mut Gpx, crossings: &[Crossing]) {
|
||||||
|
for (index, crossing) in crossings.iter().enumerate() {
|
||||||
|
let mut waypoint = Waypoint::new(crossing.point);
|
||||||
|
|
||||||
|
waypoint.name = Some(format!(
|
||||||
|
"Border {:02}: {} -> {}",
|
||||||
|
index + 1,
|
||||||
|
crossing.from.name,
|
||||||
|
crossing.to.name
|
||||||
|
));
|
||||||
|
|
||||||
|
waypoint.description = Some(format!(
|
||||||
|
"Administrative border crossing: {} [{}] -> {} [{}]",
|
||||||
|
crossing.from.name, crossing.from.ags, crossing.to.name, crossing.to.ags
|
||||||
|
));
|
||||||
|
|
||||||
|
waypoint.comment = Some(format!("{} -> {}", crossing.from.name, crossing.to.name));
|
||||||
|
|
||||||
|
waypoint.type_ = Some("administrative_boundary".to_string());
|
||||||
|
|
||||||
|
gpx.waypoints.push(waypoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue