├── .github └── workflows │ └── demo.yml ├── .gitignore ├── CHANGES.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── debug-graph ├── Cargo.toml └── src │ └── main.rs ├── demo.gif ├── dev.md ├── examples ├── geojson-to-route-snapper ├── import.html ├── index.html ├── osm-to-route-snapper ├── route-snapper ├── serve_locally.sh └── southwark.bin ├── geojson-to-route-snapper ├── Cargo.toml └── src │ ├── lib.rs │ └── main.rs ├── osm-to-route-snapper ├── Cargo.toml └── src │ ├── lib.rs │ └── main.rs ├── route-snapper-graph ├── Cargo.toml └── src │ └── lib.rs ├── route-snapper-ts ├── .gitignore ├── package-lock.json ├── package.json ├── src │ ├── index.ts │ └── split.ts └── tsconfig.json ├── route-snapper ├── Cargo.toml ├── README.md ├── lib.js └── src │ ├── lib.rs │ └── tests.rs └── user_guide.md /.github/workflows/demo.yml: -------------------------------------------------------------------------------- 1 | name: Deploy demo 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Install wasm-pack 14 | uses: jetli/wasm-pack-action@v0.3.0 15 | 16 | - name: Build web app 17 | run: | 18 | wasm-pack build --release --target web route-snapper 19 | wasm-pack build --release --target web osm-to-route-snapper 20 | wasm-pack build --release --target web geojson-to-route-snapper 21 | cp route-snapper/lib.js route-snapper/pkg 22 | 23 | mkdir -p publish/route-snapper 24 | mkdir -p publish/osm-to-route-snapper 25 | mkdir -p publish/geojson-to-route-snapper 26 | cp examples/*.html examples/southwark.bin publish 27 | cp -Rv route-snapper/pkg publish/route-snapper 28 | cp -Rv osm-to-route-snapper/pkg publish/osm-to-route-snapper/ 29 | cp -Rv geojson-to-route-snapper/pkg publish/geojson-to-route-snapper/ 30 | rm -f publish/route-snapper/pkg/.gitignore 31 | rm -f publish/osm-to-route-snapper/pkg/.gitignore 32 | rm -f publish/geojson-to-route-snapper/pkg/.gitignore 33 | 34 | - name: Publish 35 | uses: peaceiris/actions-gh-pages@v3 36 | with: 37 | github_token: ${{ secrets.GITHUB_TOKEN }} 38 | publish_dir: ./publish/ 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | target/ 3 | pkg/ 4 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | This package is changing quickly due to evolving requirements from its main 4 | user. Before `1.0`, expect the API to bounce all over the place. Please open a 5 | Github issue if you're actively using this and want to give feedback on API 6 | changes. 7 | 8 | ## Unreleased 9 | 10 | - Upgrade Rust geo dependencies 11 | 12 | ## 0.4.9 13 | 14 | - Trim freehand coordinate precision in stateless API 15 | 16 | ## 0.4.8 17 | 18 | - Fix midpoints next to a snapped waypoint 19 | 20 | ## 0.4.7 21 | 22 | - Adjust getExtraNodes: skip first and last node, since they're waypoints already 23 | - For getExtraNodes, include a point between freehand waypoints 24 | 25 | ## 0.4.6 26 | 27 | - Fix switching between area mode and new stateless APIs 28 | 29 | ## 0.4.5 30 | 31 | - Upgrade Rust geo dependencies 32 | - Dedupe full_path output; it sometimes had duplicate adjacent nodes 33 | 34 | ## 0.4.4 35 | 36 | - Add two new experimental stateless APIs. Do not use yet. 37 | 38 | ## 0.4.3 39 | 40 | - Upgrade Rust geo dependencies 41 | 42 | ## 0.4.2 43 | 44 | - The OSM importer can now handle MultiPolygon boundaries for clipping 45 | - The final route feature has a new `full_path` property, with every snapped and freehand node 46 | 47 | ## 0.4.1 48 | 49 | - Removed `fetchWithProgress`, which is a general-purpose utility method that 50 | should come from another library 51 | - Improved the tool that makes graphs from GeoJSON files by splitting 52 | LineStrings that touch at interior points 53 | - Added `debugSnappableNodes` as a faster alternative to `debugRenderGraph` 54 | 55 | ## 0.4.0 56 | 57 | - More details in `debugRenderGraph` 58 | - Add a new tool to make graphs from a GeoJSON file 59 | - Allow edges to have custom costs in each direction. This is a breaking change 60 | to the binary graph format! 61 | 62 | ## 0.3.0 63 | 64 | - Internally store WGS84 coordinates instead of Mercator (Breaking change to 65 | the binary graph format!) 66 | 67 | ## 0.2.5 68 | 69 | - Undo support 70 | 71 | ## 0.2.4 72 | 73 | - Include road labels for waypoints in interactive output 74 | - Add an `addSnappedWaypoint` API for use with a geocoder 75 | 76 | ## 0.2.3 77 | 78 | - Always start in snap mode for a new route, even if the user last used freehand mode. 79 | - Color lines to show freehand/snapped as well 80 | 81 | ## 0.2.2 82 | 83 | - Change the `renderGeojson` output to distinguish snapped and freehand waypoints 84 | - Include a `cursor` property in the `renderGeojson` output 85 | - Distinguish hovered points in the `renderGeojson` output 86 | - Use a keypress to toggle snap/freehand mode, instead of holding down a key 87 | - Convert existing nodes between snapped/freehand 88 | 89 | ## 0.2.1 90 | 91 | - Added a `routeNameForWaypoints` API 92 | 93 | ## 0.2.0 94 | 95 | - Include road names in the graph, and auto-populate a route name. (Breaking 96 | change to the binary graph format!) 97 | 98 | ## 0.1.15 99 | 100 | - Output GeoJSON precision is now trimmed to 6 decimal places 101 | - `fetchWithProgress` now takes a callback to return the progress as a percentage to the user. 102 | - Add `debugRenderGraph` and `changeGraph` APIs 103 | - Split `setConfig` into `setRouteConfig` and `setAreaMode` to prevent incorrect configuration 104 | 105 | ## 0.1.14 106 | 107 | - Fix bugs where built-in controls can get out-of-sync with current settings 108 | - By default, don't keep drawing more points to the end of the route 109 | - Fix bug that created a new waypoint when clicking (and not dragging) an 110 | intermediate point 111 | - Double click to end a route 112 | 113 | ## 0.1.13 114 | 115 | - Add an optional mode to draw closed areas 116 | 117 | ## 0.1.12 118 | 119 | - Adjust when the `activate` event is fired 120 | 121 | ## 0.1.11 122 | 123 | - Add a `start` method, so the tool can be programmatically controlled 124 | - Add a button to cancel from drawing or editing a route 125 | 126 | ## 0.1.10 127 | 128 | - Backfill missing `waypoints` properties if possible when calling 129 | `editExisting`. If you input routes drawn before 0.1.8, this will help, but 130 | may imperfectly restore the previous route. 131 | - Fix some race conditions at startup 132 | 133 | ## 0.1.9 134 | 135 | - Add a `stop` method, so the tool can be programmatically controlled 136 | 137 | ## 0.1.8 138 | 139 | - Add an `editExisting` method to modify previously drawn routes 140 | - The output now includes a `waypoints` property, used for `editExisting` 141 | 142 | ## 0.1.7 143 | 144 | - Add a `setConfig` method 145 | - Add optional behavior to avoid a route doubling back on itself 146 | - The output now includes a `length_meters` property 147 | 148 | ## 0.1.6 149 | 150 | - Added README to NPM package 151 | 152 | ## 0.1.5 153 | 154 | - If the graph is disconnected, draw straight lines between points instead of breaking 155 | 156 | ## 0.1.4 157 | 158 | - Fix missing lines when switching between freehand and snapped points 159 | - Fix bugs snapping to wrong point when many are clustered together 160 | - Always snap to the nearest point, no matter how far it is 161 | 162 | ## 0.1.3 163 | 164 | - Improved styling for draggable points 165 | - Start a `tearDown` method 166 | 167 | ## 0.1.2 168 | 169 | - fetchWithProgress takes an element, not ID, to work better with Svelte 170 | - Added `isActive()` method and new events `activate` and `no-new-route` 171 | 172 | ## 0.1.1 173 | 174 | - No more top-level await required; callers must call `await init` themselves 175 | 176 | ## 0.1.0 177 | 178 | - Initial NPM package 179 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "adler2" 7 | version = "2.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 10 | 11 | [[package]] 12 | name = "ahash" 13 | version = "0.8.11" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 16 | dependencies = [ 17 | "cfg-if", 18 | "once_cell", 19 | "version_check", 20 | "zerocopy", 21 | ] 22 | 23 | [[package]] 24 | name = "aho-corasick" 25 | version = "1.1.3" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 28 | dependencies = [ 29 | "memchr", 30 | ] 31 | 32 | [[package]] 33 | name = "allocator-api2" 34 | version = "0.2.18" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 37 | 38 | [[package]] 39 | name = "anstream" 40 | version = "0.6.18" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 43 | dependencies = [ 44 | "anstyle", 45 | "anstyle-parse", 46 | "anstyle-query", 47 | "anstyle-wincon", 48 | "colorchoice", 49 | "is_terminal_polyfill", 50 | "utf8parse", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle" 55 | version = "1.0.10" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 58 | 59 | [[package]] 60 | name = "anstyle-parse" 61 | version = "0.2.6" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 64 | dependencies = [ 65 | "utf8parse", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-query" 70 | version = "1.1.2" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 73 | dependencies = [ 74 | "windows-sys 0.59.0", 75 | ] 76 | 77 | [[package]] 78 | name = "anstyle-wincon" 79 | version = "3.0.7" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 82 | dependencies = [ 83 | "anstyle", 84 | "once_cell", 85 | "windows-sys 0.59.0", 86 | ] 87 | 88 | [[package]] 89 | name = "anyhow" 90 | version = "1.0.97" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 93 | 94 | [[package]] 95 | name = "approx" 96 | version = "0.5.1" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" 99 | dependencies = [ 100 | "num-traits", 101 | ] 102 | 103 | [[package]] 104 | name = "autocfg" 105 | version = "1.1.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 108 | 109 | [[package]] 110 | name = "bincode" 111 | version = "1.3.3" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 114 | dependencies = [ 115 | "serde", 116 | ] 117 | 118 | [[package]] 119 | name = "bitflags" 120 | version = "2.9.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 123 | 124 | [[package]] 125 | name = "bumpalo" 126 | version = "3.11.1" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" 129 | 130 | [[package]] 131 | name = "byteorder" 132 | version = "1.4.3" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 135 | 136 | [[package]] 137 | name = "cfg-if" 138 | version = "1.0.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 141 | 142 | [[package]] 143 | name = "clap" 144 | version = "4.5.35" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" 147 | dependencies = [ 148 | "clap_builder", 149 | "clap_derive", 150 | ] 151 | 152 | [[package]] 153 | name = "clap_builder" 154 | version = "4.5.35" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" 157 | dependencies = [ 158 | "anstream", 159 | "anstyle", 160 | "clap_lex", 161 | "strsim", 162 | ] 163 | 164 | [[package]] 165 | name = "clap_derive" 166 | version = "4.5.32" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 169 | dependencies = [ 170 | "heck", 171 | "proc-macro2", 172 | "quote", 173 | "syn 2.0.100", 174 | ] 175 | 176 | [[package]] 177 | name = "clap_lex" 178 | version = "0.7.4" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 181 | 182 | [[package]] 183 | name = "colorchoice" 184 | version = "1.0.3" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 187 | 188 | [[package]] 189 | name = "console_error_panic_hook" 190 | version = "0.1.7" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" 193 | dependencies = [ 194 | "cfg-if", 195 | "wasm-bindgen", 196 | ] 197 | 198 | [[package]] 199 | name = "console_log" 200 | version = "1.0.0" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" 203 | dependencies = [ 204 | "log", 205 | "web-sys", 206 | ] 207 | 208 | [[package]] 209 | name = "crc32fast" 210 | version = "1.4.2" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 213 | dependencies = [ 214 | "cfg-if", 215 | ] 216 | 217 | [[package]] 218 | name = "crossbeam-deque" 219 | version = "0.8.5" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 222 | dependencies = [ 223 | "crossbeam-epoch", 224 | "crossbeam-utils", 225 | ] 226 | 227 | [[package]] 228 | name = "crossbeam-epoch" 229 | version = "0.9.18" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 232 | dependencies = [ 233 | "crossbeam-utils", 234 | ] 235 | 236 | [[package]] 237 | name = "crossbeam-utils" 238 | version = "0.8.20" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 241 | 242 | [[package]] 243 | name = "debug-graph" 244 | version = "0.1.0" 245 | dependencies = [ 246 | "bincode", 247 | "geo", 248 | "geojson", 249 | "route-snapper-graph", 250 | "serde_json", 251 | ] 252 | 253 | [[package]] 254 | name = "earcutr" 255 | version = "0.4.2" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "0812b44697951d35fde8fcb0da81c9de7e809e825a66bbf1ecb79d9829d4ca3d" 258 | dependencies = [ 259 | "itertools", 260 | "num-traits", 261 | ] 262 | 263 | [[package]] 264 | name = "either" 265 | version = "1.8.1" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 268 | 269 | [[package]] 270 | name = "equivalent" 271 | version = "1.0.1" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 274 | 275 | [[package]] 276 | name = "errno" 277 | version = "0.3.11" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 280 | dependencies = [ 281 | "libc", 282 | "windows-sys 0.59.0", 283 | ] 284 | 285 | [[package]] 286 | name = "fastrand" 287 | version = "2.3.0" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 290 | 291 | [[package]] 292 | name = "fixedbitset" 293 | version = "0.4.2" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" 296 | 297 | [[package]] 298 | name = "flate2" 299 | version = "1.1.1" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 302 | dependencies = [ 303 | "crc32fast", 304 | "miniz_oxide", 305 | ] 306 | 307 | [[package]] 308 | name = "float_next_after" 309 | version = "1.0.0" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" 312 | 313 | [[package]] 314 | name = "geo" 315 | version = "0.30.0" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "4416397671d8997e9a3e7ad99714f4f00a22e9eaa9b966a5985d2194fc9e02e1" 318 | dependencies = [ 319 | "earcutr", 320 | "float_next_after", 321 | "geo-types", 322 | "geographiclib-rs", 323 | "i_overlay", 324 | "log", 325 | "num-traits", 326 | "robust", 327 | "rstar", 328 | "spade", 329 | ] 330 | 331 | [[package]] 332 | name = "geo-types" 333 | version = "0.7.16" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "62ddb1950450d67efee2bbc5e429c68d052a822de3aad010d28b351fbb705224" 336 | dependencies = [ 337 | "approx", 338 | "num-traits", 339 | "rayon", 340 | "rstar", 341 | "serde", 342 | ] 343 | 344 | [[package]] 345 | name = "geographiclib-rs" 346 | version = "0.2.3" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "8ea804e7bd3c6a4ca6a01edfa35231557a8a81d4d3f3e1e2b650d028c42592be" 349 | dependencies = [ 350 | "lazy_static", 351 | ] 352 | 353 | [[package]] 354 | name = "geojson" 355 | version = "0.24.2" 356 | source = "git+https://github.com/georust/geojson#5380bfded95dd5c020ddd62b0769899b50fab42a" 357 | dependencies = [ 358 | "geo-types", 359 | "log", 360 | "serde", 361 | "serde_json", 362 | "thiserror 2.0.12", 363 | ] 364 | 365 | [[package]] 366 | name = "geojson-to-route-snapper" 367 | version = "0.1.0" 368 | dependencies = [ 369 | "anyhow", 370 | "bincode", 371 | "clap", 372 | "console_error_panic_hook", 373 | "geo", 374 | "geojson", 375 | "route-snapper-graph", 376 | "serde", 377 | "wasm-bindgen", 378 | "web-sys", 379 | ] 380 | 381 | [[package]] 382 | name = "getrandom" 383 | version = "0.3.2" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 386 | dependencies = [ 387 | "cfg-if", 388 | "libc", 389 | "r-efi", 390 | "wasi", 391 | ] 392 | 393 | [[package]] 394 | name = "hash32" 395 | version = "0.3.1" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" 398 | dependencies = [ 399 | "byteorder", 400 | ] 401 | 402 | [[package]] 403 | name = "hashbrown" 404 | version = "0.14.5" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 407 | dependencies = [ 408 | "ahash", 409 | "allocator-api2", 410 | ] 411 | 412 | [[package]] 413 | name = "heapless" 414 | version = "0.8.0" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" 417 | dependencies = [ 418 | "hash32", 419 | "stable_deref_trait", 420 | ] 421 | 422 | [[package]] 423 | name = "heck" 424 | version = "0.5.0" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 427 | 428 | [[package]] 429 | name = "home" 430 | version = "0.5.11" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 433 | dependencies = [ 434 | "windows-sys 0.59.0", 435 | ] 436 | 437 | [[package]] 438 | name = "i_float" 439 | version = "1.7.0" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "85df3a416829bb955fdc2416c7b73680c8dcea8d731f2c7aa23e1042fe1b8343" 442 | dependencies = [ 443 | "serde", 444 | ] 445 | 446 | [[package]] 447 | name = "i_key_sort" 448 | version = "0.2.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "347c253b4748a1a28baf94c9ce133b6b166f08573157e05afe718812bc599fcd" 451 | 452 | [[package]] 453 | name = "i_overlay" 454 | version = "2.0.5" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "0542dfef184afdd42174a03dcc0625b6147fb73e1b974b1a08a2a42ac35cee49" 457 | dependencies = [ 458 | "i_float", 459 | "i_key_sort", 460 | "i_shape", 461 | "i_tree", 462 | "rayon", 463 | ] 464 | 465 | [[package]] 466 | name = "i_shape" 467 | version = "1.7.0" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "0a38f5a42678726718ff924f6d4a0e79b129776aeed298f71de4ceedbd091bce" 470 | dependencies = [ 471 | "i_float", 472 | "serde", 473 | ] 474 | 475 | [[package]] 476 | name = "i_tree" 477 | version = "0.8.3" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "155181bc97d770181cf9477da51218a19ee92a8e5be642e796661aee2b601139" 480 | 481 | [[package]] 482 | name = "indexmap" 483 | version = "2.0.2" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" 486 | dependencies = [ 487 | "equivalent", 488 | "hashbrown", 489 | ] 490 | 491 | [[package]] 492 | name = "is_terminal_polyfill" 493 | version = "1.70.1" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 496 | 497 | [[package]] 498 | name = "itertools" 499 | version = "0.10.5" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 502 | dependencies = [ 503 | "either", 504 | ] 505 | 506 | [[package]] 507 | name = "itoa" 508 | version = "1.0.4" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" 511 | 512 | [[package]] 513 | name = "js-sys" 514 | version = "0.3.64" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" 517 | dependencies = [ 518 | "wasm-bindgen", 519 | ] 520 | 521 | [[package]] 522 | name = "lazy_static" 523 | version = "1.4.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 526 | 527 | [[package]] 528 | name = "libc" 529 | version = "0.2.171" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 532 | 533 | [[package]] 534 | name = "libm" 535 | version = "0.2.5" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" 538 | 539 | [[package]] 540 | name = "linux-raw-sys" 541 | version = "0.4.15" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 544 | 545 | [[package]] 546 | name = "linux-raw-sys" 547 | version = "0.9.3" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" 550 | 551 | [[package]] 552 | name = "log" 553 | version = "0.4.20" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 556 | 557 | [[package]] 558 | name = "memchr" 559 | version = "2.7.4" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 562 | 563 | [[package]] 564 | name = "memmap2" 565 | version = "0.5.10" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" 568 | dependencies = [ 569 | "libc", 570 | ] 571 | 572 | [[package]] 573 | name = "miniz_oxide" 574 | version = "0.8.7" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" 577 | dependencies = [ 578 | "adler2", 579 | ] 580 | 581 | [[package]] 582 | name = "num-traits" 583 | version = "0.2.15" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 586 | dependencies = [ 587 | "autocfg", 588 | "libm", 589 | ] 590 | 591 | [[package]] 592 | name = "once_cell" 593 | version = "1.20.2" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 596 | 597 | [[package]] 598 | name = "osm-reader" 599 | version = "0.1.0" 600 | source = "git+https://github.com/a-b-street/osm-reader?rev=803817ddda8eec0ca7052b6b43e5ce70376fbf6c#803817ddda8eec0ca7052b6b43e5ce70376fbf6c" 601 | dependencies = [ 602 | "anyhow", 603 | "osmpbf", 604 | "roxmltree", 605 | ] 606 | 607 | [[package]] 608 | name = "osm-to-route-snapper" 609 | version = "0.1.0" 610 | dependencies = [ 611 | "anyhow", 612 | "bincode", 613 | "clap", 614 | "console_error_panic_hook", 615 | "console_log", 616 | "geo", 617 | "geojson", 618 | "log", 619 | "osm-reader", 620 | "route-snapper-graph", 621 | "simple_logger", 622 | "wasm-bindgen", 623 | "web-sys", 624 | ] 625 | 626 | [[package]] 627 | name = "osmpbf" 628 | version = "0.3.5" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "24a10e4363077e1537509aba888a1bf8015a691500ba1902d07983accdc21bdc" 631 | dependencies = [ 632 | "byteorder", 633 | "flate2", 634 | "memmap2", 635 | "protobuf", 636 | "protobuf-codegen", 637 | "rayon", 638 | ] 639 | 640 | [[package]] 641 | name = "petgraph" 642 | version = "0.6.4" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" 645 | dependencies = [ 646 | "fixedbitset", 647 | "indexmap", 648 | ] 649 | 650 | [[package]] 651 | name = "proc-macro2" 652 | version = "1.0.94" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 655 | dependencies = [ 656 | "unicode-ident", 657 | ] 658 | 659 | [[package]] 660 | name = "protobuf" 661 | version = "3.7.2" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" 664 | dependencies = [ 665 | "once_cell", 666 | "protobuf-support", 667 | "thiserror 1.0.37", 668 | ] 669 | 670 | [[package]] 671 | name = "protobuf-codegen" 672 | version = "3.7.2" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" 675 | dependencies = [ 676 | "anyhow", 677 | "once_cell", 678 | "protobuf", 679 | "protobuf-parse", 680 | "regex", 681 | "tempfile", 682 | "thiserror 1.0.37", 683 | ] 684 | 685 | [[package]] 686 | name = "protobuf-parse" 687 | version = "3.7.2" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" 690 | dependencies = [ 691 | "anyhow", 692 | "indexmap", 693 | "log", 694 | "protobuf", 695 | "protobuf-support", 696 | "tempfile", 697 | "thiserror 1.0.37", 698 | "which", 699 | ] 700 | 701 | [[package]] 702 | name = "protobuf-support" 703 | version = "3.7.2" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" 706 | dependencies = [ 707 | "thiserror 1.0.37", 708 | ] 709 | 710 | [[package]] 711 | name = "quote" 712 | version = "1.0.40" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 715 | dependencies = [ 716 | "proc-macro2", 717 | ] 718 | 719 | [[package]] 720 | name = "r-efi" 721 | version = "5.2.0" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 724 | 725 | [[package]] 726 | name = "rayon" 727 | version = "1.10.0" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 730 | dependencies = [ 731 | "either", 732 | "rayon-core", 733 | ] 734 | 735 | [[package]] 736 | name = "rayon-core" 737 | version = "1.12.1" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 740 | dependencies = [ 741 | "crossbeam-deque", 742 | "crossbeam-utils", 743 | ] 744 | 745 | [[package]] 746 | name = "regex" 747 | version = "1.11.1" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 750 | dependencies = [ 751 | "aho-corasick", 752 | "memchr", 753 | "regex-automata", 754 | "regex-syntax", 755 | ] 756 | 757 | [[package]] 758 | name = "regex-automata" 759 | version = "0.4.9" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 762 | dependencies = [ 763 | "aho-corasick", 764 | "memchr", 765 | "regex-syntax", 766 | ] 767 | 768 | [[package]] 769 | name = "regex-syntax" 770 | version = "0.8.5" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 773 | 774 | [[package]] 775 | name = "robust" 776 | version = "1.1.0" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "cbf4a6aa5f6d6888f39e980649f3ad6b666acdce1d78e95b8a2cb076e687ae30" 779 | 780 | [[package]] 781 | name = "route-snapper" 782 | version = "0.4.9" 783 | dependencies = [ 784 | "bincode", 785 | "console_error_panic_hook", 786 | "console_log", 787 | "geo", 788 | "geojson", 789 | "log", 790 | "petgraph", 791 | "route-snapper-graph", 792 | "rstar", 793 | "serde", 794 | "serde-wasm-bindgen", 795 | "serde_json", 796 | "wasm-bindgen", 797 | "web-sys", 798 | ] 799 | 800 | [[package]] 801 | name = "route-snapper-graph" 802 | version = "0.1.0" 803 | dependencies = [ 804 | "geo", 805 | "serde", 806 | ] 807 | 808 | [[package]] 809 | name = "roxmltree" 810 | version = "0.19.0" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" 813 | 814 | [[package]] 815 | name = "rstar" 816 | version = "0.12.0" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "133315eb94c7b1e8d0cb097e5a710d850263372fd028fff18969de708afc7008" 819 | dependencies = [ 820 | "heapless", 821 | "num-traits", 822 | "smallvec", 823 | ] 824 | 825 | [[package]] 826 | name = "rustix" 827 | version = "0.38.44" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 830 | dependencies = [ 831 | "bitflags", 832 | "errno", 833 | "libc", 834 | "linux-raw-sys 0.4.15", 835 | "windows-sys 0.59.0", 836 | ] 837 | 838 | [[package]] 839 | name = "rustix" 840 | version = "1.0.5" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 843 | dependencies = [ 844 | "bitflags", 845 | "errno", 846 | "libc", 847 | "linux-raw-sys 0.9.3", 848 | "windows-sys 0.59.0", 849 | ] 850 | 851 | [[package]] 852 | name = "ryu" 853 | version = "1.0.11" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 856 | 857 | [[package]] 858 | name = "serde" 859 | version = "1.0.188" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" 862 | dependencies = [ 863 | "serde_derive", 864 | ] 865 | 866 | [[package]] 867 | name = "serde-wasm-bindgen" 868 | version = "0.6.0" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "30c9933e5689bd420dc6c87b7a1835701810cbc10cd86a26e4da45b73e6b1d78" 871 | dependencies = [ 872 | "js-sys", 873 | "serde", 874 | "wasm-bindgen", 875 | ] 876 | 877 | [[package]] 878 | name = "serde_derive" 879 | version = "1.0.188" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" 882 | dependencies = [ 883 | "proc-macro2", 884 | "quote", 885 | "syn 2.0.100", 886 | ] 887 | 888 | [[package]] 889 | name = "serde_json" 890 | version = "1.0.107" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" 893 | dependencies = [ 894 | "itoa", 895 | "ryu", 896 | "serde", 897 | ] 898 | 899 | [[package]] 900 | name = "simple_logger" 901 | version = "4.3.3" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "8e7e46c8c90251d47d08b28b8a419ffb4aede0f87c2eea95e17d1d5bacbf3ef1" 904 | dependencies = [ 905 | "log", 906 | "windows-sys 0.48.0", 907 | ] 908 | 909 | [[package]] 910 | name = "smallvec" 911 | version = "1.13.2" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 914 | 915 | [[package]] 916 | name = "spade" 917 | version = "2.12.1" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "93f5ef1f863aca7d1d7dda7ccfc36a0a4279bd6d3c375176e5e0712e25cb4889" 920 | dependencies = [ 921 | "hashbrown", 922 | "num-traits", 923 | "robust", 924 | "smallvec", 925 | ] 926 | 927 | [[package]] 928 | name = "stable_deref_trait" 929 | version = "1.2.0" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 932 | 933 | [[package]] 934 | name = "strsim" 935 | version = "0.11.1" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 938 | 939 | [[package]] 940 | name = "syn" 941 | version = "1.0.107" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 944 | dependencies = [ 945 | "proc-macro2", 946 | "quote", 947 | "unicode-ident", 948 | ] 949 | 950 | [[package]] 951 | name = "syn" 952 | version = "2.0.100" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 955 | dependencies = [ 956 | "proc-macro2", 957 | "quote", 958 | "unicode-ident", 959 | ] 960 | 961 | [[package]] 962 | name = "tempfile" 963 | version = "3.19.1" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 966 | dependencies = [ 967 | "fastrand", 968 | "getrandom", 969 | "once_cell", 970 | "rustix 1.0.5", 971 | "windows-sys 0.59.0", 972 | ] 973 | 974 | [[package]] 975 | name = "thiserror" 976 | version = "1.0.37" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" 979 | dependencies = [ 980 | "thiserror-impl 1.0.37", 981 | ] 982 | 983 | [[package]] 984 | name = "thiserror" 985 | version = "2.0.12" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 988 | dependencies = [ 989 | "thiserror-impl 2.0.12", 990 | ] 991 | 992 | [[package]] 993 | name = "thiserror-impl" 994 | version = "1.0.37" 995 | source = "registry+https://github.com/rust-lang/crates.io-index" 996 | checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" 997 | dependencies = [ 998 | "proc-macro2", 999 | "quote", 1000 | "syn 1.0.107", 1001 | ] 1002 | 1003 | [[package]] 1004 | name = "thiserror-impl" 1005 | version = "2.0.12" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1008 | dependencies = [ 1009 | "proc-macro2", 1010 | "quote", 1011 | "syn 2.0.100", 1012 | ] 1013 | 1014 | [[package]] 1015 | name = "unicode-ident" 1016 | version = "1.0.5" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 1019 | 1020 | [[package]] 1021 | name = "utf8parse" 1022 | version = "0.2.2" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1025 | 1026 | [[package]] 1027 | name = "version_check" 1028 | version = "0.9.5" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1031 | 1032 | [[package]] 1033 | name = "wasi" 1034 | version = "0.14.2+wasi-0.2.4" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 1037 | dependencies = [ 1038 | "wit-bindgen-rt", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "wasm-bindgen" 1043 | version = "0.2.87" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" 1046 | dependencies = [ 1047 | "cfg-if", 1048 | "wasm-bindgen-macro", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "wasm-bindgen-backend" 1053 | version = "0.2.87" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" 1056 | dependencies = [ 1057 | "bumpalo", 1058 | "log", 1059 | "once_cell", 1060 | "proc-macro2", 1061 | "quote", 1062 | "syn 2.0.100", 1063 | "wasm-bindgen-shared", 1064 | ] 1065 | 1066 | [[package]] 1067 | name = "wasm-bindgen-macro" 1068 | version = "0.2.87" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" 1071 | dependencies = [ 1072 | "quote", 1073 | "wasm-bindgen-macro-support", 1074 | ] 1075 | 1076 | [[package]] 1077 | name = "wasm-bindgen-macro-support" 1078 | version = "0.2.87" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" 1081 | dependencies = [ 1082 | "proc-macro2", 1083 | "quote", 1084 | "syn 2.0.100", 1085 | "wasm-bindgen-backend", 1086 | "wasm-bindgen-shared", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "wasm-bindgen-shared" 1091 | version = "0.2.87" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" 1094 | 1095 | [[package]] 1096 | name = "web-sys" 1097 | version = "0.3.64" 1098 | source = "registry+https://github.com/rust-lang/crates.io-index" 1099 | checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" 1100 | dependencies = [ 1101 | "js-sys", 1102 | "wasm-bindgen", 1103 | ] 1104 | 1105 | [[package]] 1106 | name = "which" 1107 | version = "4.4.2" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" 1110 | dependencies = [ 1111 | "either", 1112 | "home", 1113 | "once_cell", 1114 | "rustix 0.38.44", 1115 | ] 1116 | 1117 | [[package]] 1118 | name = "windows-sys" 1119 | version = "0.48.0" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1122 | dependencies = [ 1123 | "windows-targets 0.48.5", 1124 | ] 1125 | 1126 | [[package]] 1127 | name = "windows-sys" 1128 | version = "0.59.0" 1129 | source = "registry+https://github.com/rust-lang/crates.io-index" 1130 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1131 | dependencies = [ 1132 | "windows-targets 0.52.6", 1133 | ] 1134 | 1135 | [[package]] 1136 | name = "windows-targets" 1137 | version = "0.48.5" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1140 | dependencies = [ 1141 | "windows_aarch64_gnullvm 0.48.5", 1142 | "windows_aarch64_msvc 0.48.5", 1143 | "windows_i686_gnu 0.48.5", 1144 | "windows_i686_msvc 0.48.5", 1145 | "windows_x86_64_gnu 0.48.5", 1146 | "windows_x86_64_gnullvm 0.48.5", 1147 | "windows_x86_64_msvc 0.48.5", 1148 | ] 1149 | 1150 | [[package]] 1151 | name = "windows-targets" 1152 | version = "0.52.6" 1153 | source = "registry+https://github.com/rust-lang/crates.io-index" 1154 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1155 | dependencies = [ 1156 | "windows_aarch64_gnullvm 0.52.6", 1157 | "windows_aarch64_msvc 0.52.6", 1158 | "windows_i686_gnu 0.52.6", 1159 | "windows_i686_gnullvm", 1160 | "windows_i686_msvc 0.52.6", 1161 | "windows_x86_64_gnu 0.52.6", 1162 | "windows_x86_64_gnullvm 0.52.6", 1163 | "windows_x86_64_msvc 0.52.6", 1164 | ] 1165 | 1166 | [[package]] 1167 | name = "windows_aarch64_gnullvm" 1168 | version = "0.48.5" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1171 | 1172 | [[package]] 1173 | name = "windows_aarch64_gnullvm" 1174 | version = "0.52.6" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1177 | 1178 | [[package]] 1179 | name = "windows_aarch64_msvc" 1180 | version = "0.48.5" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1183 | 1184 | [[package]] 1185 | name = "windows_aarch64_msvc" 1186 | version = "0.52.6" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1189 | 1190 | [[package]] 1191 | name = "windows_i686_gnu" 1192 | version = "0.48.5" 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" 1194 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1195 | 1196 | [[package]] 1197 | name = "windows_i686_gnu" 1198 | version = "0.52.6" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1201 | 1202 | [[package]] 1203 | name = "windows_i686_gnullvm" 1204 | version = "0.52.6" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1207 | 1208 | [[package]] 1209 | name = "windows_i686_msvc" 1210 | version = "0.48.5" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1213 | 1214 | [[package]] 1215 | name = "windows_i686_msvc" 1216 | version = "0.52.6" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1219 | 1220 | [[package]] 1221 | name = "windows_x86_64_gnu" 1222 | version = "0.48.5" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1225 | 1226 | [[package]] 1227 | name = "windows_x86_64_gnu" 1228 | version = "0.52.6" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1231 | 1232 | [[package]] 1233 | name = "windows_x86_64_gnullvm" 1234 | version = "0.48.5" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1237 | 1238 | [[package]] 1239 | name = "windows_x86_64_gnullvm" 1240 | version = "0.52.6" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1243 | 1244 | [[package]] 1245 | name = "windows_x86_64_msvc" 1246 | version = "0.48.5" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1249 | 1250 | [[package]] 1251 | name = "windows_x86_64_msvc" 1252 | version = "0.52.6" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1255 | 1256 | [[package]] 1257 | name = "wit-bindgen-rt" 1258 | version = "0.39.0" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 1261 | dependencies = [ 1262 | "bitflags", 1263 | ] 1264 | 1265 | [[package]] 1266 | name = "zerocopy" 1267 | version = "0.7.35" 1268 | source = "registry+https://github.com/rust-lang/crates.io-index" 1269 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1270 | dependencies = [ 1271 | "zerocopy-derive", 1272 | ] 1273 | 1274 | [[package]] 1275 | name = "zerocopy-derive" 1276 | version = "0.7.35" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1279 | dependencies = [ 1280 | "proc-macro2", 1281 | "quote", 1282 | "syn 2.0.100", 1283 | ] 1284 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "debug-graph", 4 | "geojson-to-route-snapper", 5 | "osm-to-route-snapper", 6 | "route-snapper", 7 | "route-snapper-graph", 8 | ] 9 | 10 | resolver = "2" 11 | 12 | [workspace.package] 13 | edition = "2021" 14 | 15 | [workspace.dependencies] 16 | geo = "0.30.0" 17 | geojson = { git = "https://github.com/georust/geojson", features = ["geo-types"] } 18 | 19 | # For local development, build dependencies in release mode once, but otherwise 20 | # use dev profile and avoid wasm-opt. 21 | [profile.dev.package."*"] 22 | opt-level = 3 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MapLibre route snapper 2 | 3 | [![NPM](https://img.shields.io/npm/v/route-snapper)](https://www.npmjs.com/package/route-snapper) 4 | 5 | This plugin lets you draw routes and polygon areas in MapLibre GL that snap to some network (streets, usually). Unlike similar plugins that send a request to a remote API for routing, this one does the routing client-side. This works by loading a pre-built file covering a fixed area and calculating the routes locally. 6 | 7 | [Demo](https://dabreegster.github.io/route_snapper) 8 | 9 | ![Demo](demo.gif) 10 | 11 | ## Usage 12 | 13 | See the [user guide](user_guide.md) for full details and examples. 14 | 15 | 1. Build a graph file covering some fixed area 16 | 2. `npm install route-snapper` 17 | 3. Construct the `RouteSnapper` object, passing in the graph file a MapLibre map 18 | 4. Listen to events to use the drawn routes 19 | 20 | ## Development 21 | 22 | `route-snapper` is written in Rust, compiled to WASM to run in the browser, and has a simple Javascript wrapper library. You need [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) and Python (to run a local server). 23 | 24 | ``` 25 | cd examples 26 | ./serve_locally.sh 27 | ``` 28 | 29 | ## Contributing 30 | 31 | There are many ideas for improving this plugin, such as customizing the instructions, controls, and route style, and generating graph files on-the-fly from vector tile data. Check out the [issues](https://github.com/dabreegster/route_snapper/issues) or start your own. 32 | 33 | This project follows the [Rust code of conduct](https://www.rust-lang.org/policies/code-of-conduct) and is Apache 2.0 licensed. 34 | 35 | ## Related work 36 | 37 | This tool started life in fall 2021 through [Ungap the Map](https://a-b-street.github.io/docs/software/ungap_the_map/index.html), for sketching potential cycle lanes along existing roads. It used a custom UI and map rendering library built on top of OpenGL. A year later, the idea was adapted to work in [ATIP](https://github.com/acteng/atip), using MapLibre GL. The functionality for dragging waypoints is partly inspired by [Felt](https://felt.com), Google Maps, and similar products. 38 | 39 | Other projects with client-side routing: [ngraph.path](https://github.com/anvaka/ngraph.path.demo), [geojson-path-finder](https://github.com/perliedman/geojson-path-finder) 40 | -------------------------------------------------------------------------------- /debug-graph/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "debug-graph" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | bincode = "1.3.3" 8 | geo = { workspace = true } 9 | geojson = { workspace = true } 10 | route-snapper-graph = { path = "../route-snapper-graph" } 11 | serde_json = "1.0.107" 12 | -------------------------------------------------------------------------------- /debug-graph/src/main.rs: -------------------------------------------------------------------------------- 1 | use geo::{line_measures::LengthMeasurable, Haversine}; 2 | use geojson::{Feature, Geometry}; 3 | use route_snapper_graph::RouteSnapperMap; 4 | 5 | fn main() { 6 | let args: Vec = std::env::args().collect(); 7 | if args.len() != 2 { 8 | println!("Pass in a snap.bin file"); 9 | std::process::exit(1); 10 | } 11 | let bytes = std::fs::read(&args[1]).unwrap(); 12 | let mut map: RouteSnapperMap = bincode::deserialize(&bytes).unwrap(); 13 | 14 | // TODO Move this to route_snapper_graph 15 | if !map.override_forward_costs.is_empty() && map.override_forward_costs.len() != map.edges.len() 16 | { 17 | panic!("override_forward_costs length doesn't match edges length",); 18 | } 19 | if !map.override_backward_costs.is_empty() 20 | && map.override_backward_costs.len() != map.edges.len() 21 | { 22 | panic!("override_backward_costs length doesn't match edges length",); 23 | } 24 | 25 | for (idx, edge) in map.edges.iter_mut().enumerate() { 26 | edge.length_meters = edge.geometry.length(&Haversine); 27 | 28 | if map.override_forward_costs.is_empty() { 29 | edge.forward_cost = Some(edge.length_meters); 30 | } else { 31 | edge.forward_cost = map.override_forward_costs[idx]; 32 | } 33 | 34 | if map.override_backward_costs.is_empty() { 35 | edge.backward_cost = Some(edge.length_meters); 36 | } else { 37 | edge.backward_cost = map.override_backward_costs[idx]; 38 | } 39 | } 40 | 41 | // This is a copy of renderGraph from the WASM API. Browsers seem to have limits for how large 42 | // a dynamically-generated file they can download. Sharing the code for this method without 43 | // bloating dependencies isn't straightforward. 44 | let mut features = Vec::new(); 45 | for (idx, edge) in map.edges.iter().enumerate() { 46 | let mut f = Feature::from(Geometry::from(&edge.geometry)); 47 | f.set_property("edge_id", idx); 48 | f.set_property("node1", edge.node1.0); 49 | f.set_property("node2", edge.node2.0); 50 | f.set_property("length_meters", edge.length_meters); 51 | f.set_property("forward_cost", edge.forward_cost); 52 | f.set_property("backward_cost", edge.backward_cost); 53 | f.set_property("name", edge.name.clone()); 54 | features.push(f); 55 | } 56 | for (idx, pt) in map.nodes.iter().enumerate() { 57 | let mut f = Feature::from(Geometry::from(geojson::Value::Point(vec![pt.x, pt.y]))); 58 | f.set_property("node_id", idx); 59 | features.push(f); 60 | } 61 | let gj = geojson::GeoJson::from(features.into_iter().collect::()); 62 | std::fs::write("debug.geojson", serde_json::to_string_pretty(&gj).unwrap()).unwrap(); 63 | } 64 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabreegster/route_snapper/2f4086f9e11f12664f8007566650dae99775b759/demo.gif -------------------------------------------------------------------------------- /dev.md: -------------------------------------------------------------------------------- 1 | ## Publishing a new version 2 | 3 | To release a new version of : 4 | 5 | 1. Bump the version number in `route-snapper/Cargo.toml` 6 | 2. Make sure `router-snapper/pkg/` has the release build with `--target web` and that `lib.js` is in there. If you `cd examples; ./serve_locally.sh`, then this'll happen 7 | 3. **Important**! Manually edit `route-snapper/pkg/package.json` and add `lib.js` to `files`. I can't figure out how to make `wasm-pack` do this. 8 | 4. `cd route-snapper/pkg; npm pack` to sanity check the contents. Then `npm publish` 9 | 5. Update the changelog 10 | -------------------------------------------------------------------------------- /examples/geojson-to-route-snapper: -------------------------------------------------------------------------------- 1 | ../geojson-to-route-snapper -------------------------------------------------------------------------------- /examples/import.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Create route-snapper graph files from OSM 6 | 10 | 11 | 15 | 45 | 46 | 47 |
48 |
49 | Option A: 50 | 54 |
55 |

56 | Option B: 57 | Use the polygon tool on the top-right to select an area to import. 58 | (Double click or press enter to finish.) Wait a bit, then your browser 59 | should download a file. Use "Change graph file" in the 60 | main tool to load it. 61 |

62 |

63 |
64 |

65 | Thanks to 66 | Overpass 71 | for making OpenStreetMap extracts easy! 72 |

73 |
74 | 75 | 76 | 81 |
82 | 83 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Route snapper demo 6 | 10 | 11 | 15 | 49 | 50 | 51 |
52 | 55 |
56 | 60 |
61 |
62 | 63 |
64 |
65 | 68 |
69 |
70 | 73 |
74 |
75 | 78 |
79 |
Click an existing route to edit
80 |
Inactive
81 |
82 | 85 |
86 |
87 |
88 |
Route tool loading...
89 | 300 | 301 | 302 | -------------------------------------------------------------------------------- /examples/osm-to-route-snapper: -------------------------------------------------------------------------------- 1 | ../osm-to-route-snapper -------------------------------------------------------------------------------- /examples/route-snapper: -------------------------------------------------------------------------------- 1 | ../route-snapper -------------------------------------------------------------------------------- /examples/serve_locally.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | wasm-pack build --release --target web ../route-snapper 5 | wasm-pack build --release --target web ../osm-to-route-snapper 6 | wasm-pack build --release --target web ../geojson-to-route-snapper 7 | cp ../route-snapper/lib.js ../route-snapper/pkg 8 | python3 -m http.server --directory . 9 | -------------------------------------------------------------------------------- /examples/southwark.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabreegster/route_snapper/2f4086f9e11f12664f8007566650dae99775b759/examples/southwark.bin -------------------------------------------------------------------------------- /geojson-to-route-snapper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "geojson-to-route-snapper" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | 9 | [dependencies] 10 | anyhow = "1.0.75" 11 | bincode = "1.3.3" 12 | geo = { workspace = true } 13 | geojson = { workspace = true } 14 | route-snapper-graph = { path = "../route-snapper-graph" } 15 | serde = { version = "1.0.188", features = ["derive"] } 16 | 17 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 18 | clap = { version = "4.4.6", features = ["derive"] } 19 | 20 | [target.'cfg(target_arch = "wasm32")'.dependencies] 21 | console_error_panic_hook = "0.1.6" 22 | wasm-bindgen = "0.2.87" 23 | web-sys = { version = "0.3.64", features = ["console"] } 24 | -------------------------------------------------------------------------------- /geojson-to-route-snapper/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::{bail, Result}; 4 | use geo::{Coord, CoordsIter, LineString}; 5 | use geojson::de::deserialize_geometry; 6 | use serde::Deserialize; 7 | 8 | use route_snapper_graph::{Edge, NodeID, RouteSnapperMap}; 9 | 10 | /// Converts GeoJSON into a graph for use with the route snapper. See the user guide for 11 | /// requirements about the GeoJSON file. 12 | pub fn convert_geojson(input_string: String) -> Result { 13 | let input: Vec = 14 | geojson::de::deserialize_feature_collection_str_to_vec(&input_string)?; 15 | 16 | let mut map = RouteSnapperMap { 17 | nodes: Vec::new(), 18 | edges: Vec::new(), 19 | override_forward_costs: Vec::new(), 20 | override_backward_costs: Vec::new(), 21 | }; 22 | 23 | // Count how many lines reference each point 24 | let mut point_counter: HashMap<(isize, isize), usize> = HashMap::new(); 25 | for edge in &input { 26 | for pt in edge.geometry.coords() { 27 | *point_counter.entry(hashify_point(*pt)).or_insert(0) += 1; 28 | } 29 | } 30 | 31 | // Split each LineString into edges 32 | let mut node_id_lookup: HashMap<(isize, isize), NodeID> = HashMap::new(); 33 | for edge in input { 34 | let mut point1 = edge.geometry.0[0]; 35 | let mut pts = Vec::new(); 36 | 37 | let num_points = edge.geometry.coords_count(); 38 | 39 | for (idx, pt) in edge.geometry.into_inner().into_iter().enumerate() { 40 | pts.push(pt); 41 | // Edges start/end at intersections between two LineStrings. The endpoints of the 42 | // LineString also count as intersections. 43 | let is_endpoint = idx == 0 44 | || idx == num_points - 1 45 | || *point_counter.get(&hashify_point(pt)).unwrap() > 1; 46 | if is_endpoint && pts.len() > 1 { 47 | let geometry = LineString::new(std::mem::take(&mut pts)); 48 | 49 | let next_id = NodeID(node_id_lookup.len() as u32); 50 | let node1_id = *node_id_lookup 51 | .entry(hashify_point(point1)) 52 | .or_insert_with(|| { 53 | map.nodes.push(point1); 54 | next_id 55 | }); 56 | let next_id = NodeID(node_id_lookup.len() as u32); 57 | let node2_id = *node_id_lookup.entry(hashify_point(pt)).or_insert_with(|| { 58 | map.nodes.push(pt); 59 | next_id 60 | }); 61 | map.edges.push(Edge { 62 | node1: node1_id, 63 | node2: node2_id, 64 | geometry, 65 | name: edge.name.clone(), 66 | 67 | length_meters: 0.0, 68 | forward_cost: None, 69 | backward_cost: None, 70 | }); 71 | map.override_forward_costs.push(edge.forward_cost); 72 | map.override_backward_costs.push(edge.backward_cost); 73 | 74 | // Start the next edge 75 | point1 = pt; 76 | pts.push(pt); 77 | } 78 | } 79 | } 80 | 81 | if map.override_forward_costs.iter().all(|x| x.is_none()) { 82 | bail!("No edges set forward_cost. The input is probably incorrect."); 83 | } 84 | if map.override_backward_costs.iter().all(|x| x.is_none()) { 85 | bail!("No edges set backward_cost. The input is probably incorrect."); 86 | } 87 | 88 | Ok(map) 89 | } 90 | 91 | #[derive(Deserialize)] 92 | pub struct InputEdge { 93 | #[serde(deserialize_with = "deserialize_geometry")] 94 | geometry: LineString, 95 | name: Option, 96 | forward_cost: Option, 97 | backward_cost: Option, 98 | } 99 | 100 | fn hashify_point(pt: Coord) -> (isize, isize) { 101 | ((pt.x * 1_000_000.0) as isize, (pt.y * 1_000_000.0) as isize) 102 | } 103 | 104 | #[cfg(target_arch = "wasm32")] 105 | use std::sync::Once; 106 | #[cfg(target_arch = "wasm32")] 107 | use wasm_bindgen::prelude::*; 108 | 109 | #[cfg(target_arch = "wasm32")] 110 | static START: Once = Once::new(); 111 | 112 | #[cfg(target_arch = "wasm32")] 113 | #[wasm_bindgen()] 114 | pub fn convert(input_string: String) -> Result, JsValue> { 115 | START.call_once(|| { 116 | console_error_panic_hook::set_once(); 117 | }); 118 | 119 | let snapper = 120 | convert_geojson(input_string).map_err(|err| JsValue::from_str(&err.to_string()))?; 121 | Ok(bincode::serialize(&snapper).unwrap()) 122 | } 123 | -------------------------------------------------------------------------------- /geojson-to-route-snapper/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::BufWriter; 3 | 4 | use clap::Parser; 5 | use geojson_to_route_snapper::convert_geojson; 6 | 7 | #[derive(Parser)] 8 | struct Args { 9 | /// Path to a .geojson file to convert 10 | #[arg(long)] 11 | input: String, 12 | 13 | /// Output file to write 14 | #[arg(long, default_value = "snap.bin")] 15 | output: String, 16 | } 17 | 18 | fn main() { 19 | let args = Args::parse(); 20 | let snapper = convert_geojson(std::fs::read_to_string(&args.input).unwrap()).unwrap(); 21 | 22 | let output = BufWriter::new(File::create(args.output).unwrap()); 23 | bincode::serialize_into(output, &snapper).unwrap(); 24 | } 25 | -------------------------------------------------------------------------------- /osm-to-route-snapper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "osm-to-route-snapper" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | 9 | [dependencies] 10 | anyhow = "1.0.75" 11 | bincode = "1.3.3" 12 | geo = { workspace = true } 13 | geojson = { workspace = true } 14 | log = "0.4.20" 15 | osm-reader = { git = "https://github.com/a-b-street/osm-reader", rev="803817ddda8eec0ca7052b6b43e5ce70376fbf6c" } 16 | route-snapper-graph = { path = "../route-snapper-graph" } 17 | 18 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 19 | clap = { version = "4.4.6", features = ["derive"] } 20 | simple_logger = { version = "4.3.0", default-features = false } 21 | 22 | [target.'cfg(target_arch = "wasm32")'.dependencies] 23 | console_error_panic_hook = "0.1.6" 24 | console_log = "1.0.0" 25 | wasm-bindgen = "0.2.87" 26 | web-sys = { version = "0.3.64", features = ["console"] } 27 | -------------------------------------------------------------------------------- /osm-to-route-snapper/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::Result; 4 | use geo::{ 5 | line_measures::LengthMeasurable, BooleanOps, Contains, Coord, Haversine, Intersects, 6 | LineString, MultiLineString, MultiPolygon, 7 | }; 8 | use log::{debug, info}; 9 | use osm_reader::{Element, WayID}; 10 | 11 | use route_snapper_graph::{Edge, NodeID, RouteSnapperMap}; 12 | 13 | /// Convert input OSM PBF or XML data into a RouteSnapperMap, extracting all highway center-lines. 14 | /// If a boundary polygon or multipolygon is specified, clips roads to this boundary. 15 | pub fn convert_osm( 16 | input_bytes: Vec, 17 | boundary_gj: Option, 18 | road_names: bool, 19 | ) -> Result { 20 | info!("Scraping OSM data"); 21 | let (nodes, ways) = scrape_elements(&input_bytes, road_names)?; 22 | info!( 23 | "Got {} nodes and {} ways. Splitting into edges", 24 | nodes.len(), 25 | ways.len(), 26 | ); 27 | 28 | let mut boundary = None; 29 | if let Some(gj_string) = boundary_gj { 30 | let gj: geojson::Feature = gj_string.parse()?; 31 | let boundary_geo: MultiPolygon = if matches!( 32 | gj.geometry.as_ref().unwrap().value, 33 | geojson::Value::Polygon(_) 34 | ) { 35 | MultiPolygon(vec![gj.try_into()?]) 36 | } else { 37 | gj.try_into()? 38 | }; 39 | boundary = Some(boundary_geo); 40 | } 41 | 42 | let mut map = split_edges(nodes, ways, boundary.as_ref()); 43 | if let Some(boundary) = boundary { 44 | clip(&mut map, boundary); 45 | } 46 | Ok(map) 47 | } 48 | 49 | struct Way { 50 | name: Option, 51 | nodes: Vec, 52 | } 53 | 54 | fn scrape_elements( 55 | input_bytes: &[u8], 56 | road_names: bool, 57 | ) -> Result<(HashMap, HashMap)> { 58 | // Scrape every node ID -> Coord 59 | let mut nodes = HashMap::new(); 60 | // Scrape every routable road 61 | let mut ways = HashMap::new(); 62 | 63 | osm_reader::parse(input_bytes, |elem| match elem { 64 | Element::Node { id, lon, lat, .. } => { 65 | nodes.insert(id, Coord { x: lon, y: lat }); 66 | } 67 | Element::Way { id, node_ids, tags } => { 68 | if tags.contains_key("highway") { 69 | // TODO When the name is missing, we could fallback on other OSM tags. See 70 | // map_model::Road::get_name in A/B Street. 71 | let name = if road_names { 72 | tags.get("name").map(|x| x.to_string()) 73 | } else { 74 | None 75 | }; 76 | ways.insert( 77 | id, 78 | Way { 79 | name, 80 | nodes: node_ids, 81 | }, 82 | ); 83 | } 84 | } 85 | Element::Relation { .. } => {} 86 | })?; 87 | 88 | Ok((nodes, ways)) 89 | } 90 | 91 | fn split_edges( 92 | nodes: HashMap, 93 | ways: HashMap, 94 | boundary: Option<&MultiPolygon>, 95 | ) -> RouteSnapperMap { 96 | let mut map = RouteSnapperMap { 97 | nodes: Vec::new(), 98 | edges: Vec::new(), 99 | override_forward_costs: Vec::new(), 100 | override_backward_costs: Vec::new(), 101 | }; 102 | 103 | // Count how many ways reference each node 104 | let mut node_counter: HashMap = HashMap::new(); 105 | for way in ways.values() { 106 | for node in &way.nodes { 107 | *node_counter.entry(*node).or_insert(0) += 1; 108 | } 109 | } 110 | 111 | // Split each way into edges 112 | let mut node_id_lookup = HashMap::new(); 113 | for way in ways.into_values() { 114 | let mut node1 = way.nodes[0]; 115 | let mut pts = Vec::new(); 116 | 117 | let num_nodes = way.nodes.len(); 118 | for (idx, node) in way.nodes.into_iter().enumerate() { 119 | pts.push(nodes[&node]); 120 | // Edges start/end at intersections between two ways. The endpoints of the way also 121 | // count as intersections. 122 | let is_endpoint = 123 | idx == 0 || idx == num_nodes - 1 || *node_counter.get(&node).unwrap() > 1; 124 | if is_endpoint && pts.len() > 1 { 125 | let geometry = LineString::new(std::mem::take(&mut pts)); 126 | let mut add_road = true; 127 | if let Some(boundary) = boundary { 128 | // If this road doesn't intersect the boundary at all, skip it 129 | if !boundary.contains(&geometry) 130 | && !boundary.iter().any(|p| p.exterior().intersects(&geometry)) 131 | { 132 | add_road = false; 133 | } 134 | } 135 | 136 | if add_road { 137 | let next_id = NodeID(node_id_lookup.len() as u32); 138 | let node1_id = *node_id_lookup.entry(node1).or_insert_with(|| { 139 | map.nodes.push(geometry.0[0]); 140 | next_id 141 | }); 142 | let next_id = NodeID(node_id_lookup.len() as u32); 143 | let node2_id = *node_id_lookup.entry(node).or_insert_with(|| { 144 | map.nodes.push(*geometry.0.last().unwrap()); 145 | next_id 146 | }); 147 | map.edges.push(Edge { 148 | node1: node1_id, 149 | node2: node2_id, 150 | geometry, 151 | name: way.name.clone(), 152 | 153 | length_meters: 0.0, 154 | forward_cost: None, 155 | backward_cost: None, 156 | }); 157 | } 158 | 159 | // Start the next edge 160 | node1 = node; 161 | pts.push(nodes[&node]); 162 | } 163 | } 164 | } 165 | 166 | info!( 167 | "{} nodes and {} edges total", 168 | map.nodes.len(), 169 | map.edges.len() 170 | ); 171 | map 172 | } 173 | 174 | fn clip(map: &mut RouteSnapperMap, boundary: MultiPolygon) { 175 | // Fix edges crossing the boundary. Edges totally outside the boundary are skipped earlier 176 | // during split_edges. 177 | for edge in &mut map.edges { 178 | if boundary 179 | .iter() 180 | .any(|p| p.exterior().intersects(&edge.geometry)) 181 | { 182 | let invert = false; 183 | let mut multi_line_string = 184 | boundary.clip(&MultiLineString::from(edge.geometry.clone()), invert); 185 | // If we have multiple pieces, that's hard to deal with 186 | debug!( 187 | "Shortening {:?} from {} to {}", 188 | edge.name, 189 | edge.geometry.length(&Haversine), 190 | multi_line_string.0[0].length(&Haversine) 191 | ); 192 | edge.geometry = multi_line_string.0.remove(0); 193 | // Fix both nodes; if there's no change, doesn't matter 194 | map.nodes[edge.node1.0 as usize] = *edge.geometry.coords().next().unwrap(); 195 | map.nodes[edge.node2.0 as usize] = *edge.geometry.coords().next_back().unwrap(); 196 | } 197 | } 198 | } 199 | 200 | #[cfg(target_arch = "wasm32")] 201 | use std::sync::Once; 202 | #[cfg(target_arch = "wasm32")] 203 | use wasm_bindgen::prelude::*; 204 | 205 | #[cfg(target_arch = "wasm32")] 206 | static START: Once = Once::new(); 207 | 208 | #[cfg(target_arch = "wasm32")] 209 | #[wasm_bindgen()] 210 | pub fn convert(input_bytes: Vec, boundary_geojson: String) -> Result, JsValue> { 211 | START.call_once(|| { 212 | console_log::init_with_level(log::Level::Info).unwrap(); 213 | console_error_panic_hook::set_once(); 214 | }); 215 | 216 | let road_names = true; 217 | let snapper = convert_osm(input_bytes, Some(boundary_geojson), road_names) 218 | .map_err(|err| JsValue::from_str(&err.to_string()))?; 219 | Ok(bincode::serialize(&snapper).unwrap()) 220 | } 221 | -------------------------------------------------------------------------------- /osm-to-route-snapper/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::BufWriter; 3 | 4 | use clap::Parser; 5 | use osm_to_route_snapper::convert_osm; 6 | 7 | #[derive(Parser)] 8 | struct Args { 9 | /// Path to a .osm.pbf or .xml file to convert 10 | #[arg(long)] 11 | input: String, 12 | 13 | /// Path to GeoJSON file with the boundary to clip the input to 14 | #[arg(short, long)] 15 | boundary: Option, 16 | 17 | /// Output file to write 18 | #[arg(long, default_value = "snap.bin")] 19 | output: String, 20 | 21 | /// Omit road names from the output, saving some space. 22 | #[clap(long)] 23 | no_road_names: bool, 24 | } 25 | 26 | fn main() { 27 | simple_logger::init_with_level(log::Level::Info).unwrap(); 28 | let args = Args::parse(); 29 | let snapper = convert_osm( 30 | std::fs::read(&args.input).unwrap(), 31 | args.boundary 32 | .map(|path| std::fs::read_to_string(path).unwrap()), 33 | !args.no_road_names, 34 | ) 35 | .unwrap(); 36 | 37 | let output = BufWriter::new(File::create(args.output).unwrap()); 38 | bincode::serialize_into(output, &snapper).unwrap(); 39 | } 40 | -------------------------------------------------------------------------------- /route-snapper-graph/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "route-snapper-graph" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | geo = { workspace = true } 8 | serde = { version = "1.0.188", features = ["derive"] } 9 | -------------------------------------------------------------------------------- /route-snapper-graph/src/lib.rs: -------------------------------------------------------------------------------- 1 | use geo::{Coord, LineString}; 2 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 3 | 4 | #[derive(Serialize, Deserialize)] 5 | pub struct RouteSnapperMap { 6 | #[serde( 7 | serialize_with = "serialize_coords", 8 | deserialize_with = "deserialize_coords" 9 | )] 10 | pub nodes: Vec, 11 | pub edges: Vec, 12 | 13 | /// If empty, edges will have a forwards/backwards cost of their `length_meters` by default. If 14 | /// non-empty, this must match the length of `edges` and specify a cost per edge. If a cost is 15 | /// `None`, that edge won't be routable in the specified direction. 16 | pub override_forward_costs: Vec>, 17 | pub override_backward_costs: Vec>, 18 | } 19 | 20 | #[derive(Serialize, Deserialize)] 21 | pub struct Edge { 22 | pub node1: NodeID, 23 | pub node2: NodeID, 24 | #[serde( 25 | serialize_with = "serialize_linestring", 26 | deserialize_with = "deserialize_linestring" 27 | )] 28 | pub geometry: LineString, 29 | pub name: Option, 30 | 31 | /// This will be calculated from the geometry. Don't serialize to minimize file sizes. 32 | #[serde(skip_serializing, skip_deserializing)] 33 | pub length_meters: f64, 34 | /// These will be calculated from `override_forward_costs`, `override_backward_costs`, and 35 | /// `length_meters`. Don't serialize to minimize file sizes. 36 | #[serde(skip_serializing, skip_deserializing)] 37 | pub forward_cost: Option, 38 | #[serde(skip_serializing, skip_deserializing)] 39 | pub backward_cost: Option, 40 | } 41 | 42 | #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 43 | pub struct EdgeID(pub u32); 44 | #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 45 | pub struct NodeID(pub u32); 46 | 47 | impl RouteSnapperMap { 48 | pub fn edge(&self, id: EdgeID) -> &Edge { 49 | &self.edges[id.0 as usize] 50 | } 51 | pub fn node(&self, id: NodeID) -> Coord { 52 | self.nodes[id.0 as usize] 53 | } 54 | } 55 | 56 | fn serialize_coords(coords: &Vec, s: S) -> Result { 57 | let mut flattened: Vec = Vec::new(); 58 | for pt in coords { 59 | flattened.push(serialize_f64(pt.x)); 60 | flattened.push(serialize_f64(pt.y)); 61 | } 62 | flattened.serialize(s) 63 | } 64 | 65 | fn deserialize_coords<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { 66 | let flattened = >::deserialize(d)?; 67 | let mut pts = Vec::new(); 68 | for pair in flattened.chunks(2) { 69 | pts.push(Coord { 70 | x: deserialize_f64(pair[0]), 71 | y: deserialize_f64(pair[1]), 72 | }); 73 | } 74 | Ok(pts) 75 | } 76 | 77 | fn serialize_linestring(linestring: &LineString, s: S) -> Result { 78 | serialize_coords(&linestring.0, s) 79 | } 80 | 81 | fn deserialize_linestring<'de, D: Deserializer<'de>>(d: D) -> Result { 82 | let pts = deserialize_coords(d)?; 83 | Ok(LineString::new(pts)) 84 | } 85 | 86 | /// Serializes a trimmed `f64` as an `i32` to save space. 87 | fn serialize_f64(x: f64) -> i32 { 88 | // 6 decimal places gives about 10cm of precision 89 | (x * 1_000_000.0).round() as i32 90 | } 91 | 92 | /// Deserializes a trimmed `f64` from an `i32`. 93 | fn deserialize_f64(x: i32) -> f64 { 94 | x as f64 / 1_000_000.0 95 | } 96 | -------------------------------------------------------------------------------- /route-snapper-ts/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /route-snapper-ts/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "route-snapper-ts", 3 | "version": "0.0.7", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "route-snapper-ts", 9 | "version": "0.0.7", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "@turf/helpers": "^6.5.0", 13 | "@turf/length": "^6.5.0", 14 | "@turf/line-slice": "^6.5.0", 15 | "@turf/line-split": "^6.5.0", 16 | "@turf/nearest-point-on-line": "^6.5.0", 17 | "maplibre-gl": "^4.0.0", 18 | "route-snapper": "^0.4.2" 19 | }, 20 | "devDependencies": { 21 | "@types/geojson": "^7946.0.14", 22 | "prettier": "^3.2.5", 23 | "typescript": "^5.4.5" 24 | } 25 | }, 26 | "node_modules/@mapbox/geojson-rewind": { 27 | "version": "0.5.2", 28 | "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", 29 | "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", 30 | "dependencies": { 31 | "get-stream": "^6.0.1", 32 | "minimist": "^1.2.6" 33 | }, 34 | "bin": { 35 | "geojson-rewind": "geojson-rewind" 36 | } 37 | }, 38 | "node_modules/@mapbox/jsonlint-lines-primitives": { 39 | "version": "2.0.2", 40 | "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", 41 | "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", 42 | "engines": { 43 | "node": ">= 0.6" 44 | } 45 | }, 46 | "node_modules/@mapbox/point-geometry": { 47 | "version": "0.1.0", 48 | "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", 49 | "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==" 50 | }, 51 | "node_modules/@mapbox/tiny-sdf": { 52 | "version": "2.0.6", 53 | "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz", 54 | "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==" 55 | }, 56 | "node_modules/@mapbox/unitbezier": { 57 | "version": "0.0.1", 58 | "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", 59 | "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" 60 | }, 61 | "node_modules/@mapbox/vector-tile": { 62 | "version": "1.3.1", 63 | "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", 64 | "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", 65 | "dependencies": { 66 | "@mapbox/point-geometry": "~0.1.0" 67 | } 68 | }, 69 | "node_modules/@mapbox/whoots-js": { 70 | "version": "3.1.0", 71 | "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", 72 | "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", 73 | "engines": { 74 | "node": ">=6.0.0" 75 | } 76 | }, 77 | "node_modules/@maplibre/maplibre-gl-style-spec": { 78 | "version": "20.2.0", 79 | "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.2.0.tgz", 80 | "integrity": "sha512-BTw6/3ysowky22QMtNDjElp+YLwwvBDh3xxnq1izDFjTtUERm5nYSihlNZ6QaxXb+6lX2T2t0hBEjheAI+kBEQ==", 81 | "dependencies": { 82 | "@mapbox/jsonlint-lines-primitives": "~2.0.2", 83 | "@mapbox/unitbezier": "^0.0.1", 84 | "json-stringify-pretty-compact": "^4.0.0", 85 | "minimist": "^1.2.8", 86 | "quickselect": "^2.0.0", 87 | "rw": "^1.3.3", 88 | "sort-object": "^3.0.3", 89 | "tinyqueue": "^2.0.3" 90 | }, 91 | "bin": { 92 | "gl-style-format": "dist/gl-style-format.mjs", 93 | "gl-style-migrate": "dist/gl-style-migrate.mjs", 94 | "gl-style-validate": "dist/gl-style-validate.mjs" 95 | } 96 | }, 97 | "node_modules/@turf/bbox": { 98 | "version": "6.5.0", 99 | "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-6.5.0.tgz", 100 | "integrity": "sha512-RBbLaao5hXTYyyg577iuMtDB8ehxMlUqHEJiMs8jT1GHkFhr6sYre3lmLsPeYEi/ZKj5TP5tt7fkzNdJ4GIVyw==", 101 | "dependencies": { 102 | "@turf/helpers": "^6.5.0", 103 | "@turf/meta": "^6.5.0" 104 | }, 105 | "funding": { 106 | "url": "https://opencollective.com/turf" 107 | } 108 | }, 109 | "node_modules/@turf/bearing": { 110 | "version": "6.5.0", 111 | "resolved": "https://registry.npmjs.org/@turf/bearing/-/bearing-6.5.0.tgz", 112 | "integrity": "sha512-dxINYhIEMzgDOztyMZc20I7ssYVNEpSv04VbMo5YPQsqa80KO3TFvbuCahMsCAW5z8Tncc8dwBlEFrmRjJG33A==", 113 | "dependencies": { 114 | "@turf/helpers": "^6.5.0", 115 | "@turf/invariant": "^6.5.0" 116 | }, 117 | "funding": { 118 | "url": "https://opencollective.com/turf" 119 | } 120 | }, 121 | "node_modules/@turf/destination": { 122 | "version": "6.5.0", 123 | "resolved": "https://registry.npmjs.org/@turf/destination/-/destination-6.5.0.tgz", 124 | "integrity": "sha512-4cnWQlNC8d1tItOz9B4pmJdWpXqS0vEvv65bI/Pj/genJnsL7evI0/Xw42RvEGROS481MPiU80xzvwxEvhQiMQ==", 125 | "dependencies": { 126 | "@turf/helpers": "^6.5.0", 127 | "@turf/invariant": "^6.5.0" 128 | }, 129 | "funding": { 130 | "url": "https://opencollective.com/turf" 131 | } 132 | }, 133 | "node_modules/@turf/distance": { 134 | "version": "6.5.0", 135 | "resolved": "https://registry.npmjs.org/@turf/distance/-/distance-6.5.0.tgz", 136 | "integrity": "sha512-xzykSLfoURec5qvQJcfifw/1mJa+5UwByZZ5TZ8iaqjGYN0vomhV9aiSLeYdUGtYRESZ+DYC/OzY+4RclZYgMg==", 137 | "dependencies": { 138 | "@turf/helpers": "^6.5.0", 139 | "@turf/invariant": "^6.5.0" 140 | }, 141 | "funding": { 142 | "url": "https://opencollective.com/turf" 143 | } 144 | }, 145 | "node_modules/@turf/helpers": { 146 | "version": "6.5.0", 147 | "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz", 148 | "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==", 149 | "funding": { 150 | "url": "https://opencollective.com/turf" 151 | } 152 | }, 153 | "node_modules/@turf/invariant": { 154 | "version": "6.5.0", 155 | "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz", 156 | "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==", 157 | "dependencies": { 158 | "@turf/helpers": "^6.5.0" 159 | }, 160 | "funding": { 161 | "url": "https://opencollective.com/turf" 162 | } 163 | }, 164 | "node_modules/@turf/length": { 165 | "version": "6.5.0", 166 | "resolved": "https://registry.npmjs.org/@turf/length/-/length-6.5.0.tgz", 167 | "integrity": "sha512-5pL5/pnw52fck3oRsHDcSGrj9HibvtlrZ0QNy2OcW8qBFDNgZ4jtl6U7eATVoyWPKBHszW3dWETW+iLV7UARig==", 168 | "dependencies": { 169 | "@turf/distance": "^6.5.0", 170 | "@turf/helpers": "^6.5.0", 171 | "@turf/meta": "^6.5.0" 172 | }, 173 | "funding": { 174 | "url": "https://opencollective.com/turf" 175 | } 176 | }, 177 | "node_modules/@turf/line-intersect": { 178 | "version": "6.5.0", 179 | "resolved": "https://registry.npmjs.org/@turf/line-intersect/-/line-intersect-6.5.0.tgz", 180 | "integrity": "sha512-CS6R1tZvVQD390G9Ea4pmpM6mJGPWoL82jD46y0q1KSor9s6HupMIo1kY4Ny+AEYQl9jd21V3Scz20eldpbTVA==", 181 | "dependencies": { 182 | "@turf/helpers": "^6.5.0", 183 | "@turf/invariant": "^6.5.0", 184 | "@turf/line-segment": "^6.5.0", 185 | "@turf/meta": "^6.5.0", 186 | "geojson-rbush": "3.x" 187 | }, 188 | "funding": { 189 | "url": "https://opencollective.com/turf" 190 | } 191 | }, 192 | "node_modules/@turf/line-segment": { 193 | "version": "6.5.0", 194 | "resolved": "https://registry.npmjs.org/@turf/line-segment/-/line-segment-6.5.0.tgz", 195 | "integrity": "sha512-jI625Ho4jSuJESNq66Mmi290ZJ5pPZiQZruPVpmHkUw257Pew0alMmb6YrqYNnLUuiVVONxAAKXUVeeUGtycfw==", 196 | "dependencies": { 197 | "@turf/helpers": "^6.5.0", 198 | "@turf/invariant": "^6.5.0", 199 | "@turf/meta": "^6.5.0" 200 | }, 201 | "funding": { 202 | "url": "https://opencollective.com/turf" 203 | } 204 | }, 205 | "node_modules/@turf/line-slice": { 206 | "version": "6.5.0", 207 | "resolved": "https://registry.npmjs.org/@turf/line-slice/-/line-slice-6.5.0.tgz", 208 | "integrity": "sha512-vDqJxve9tBHhOaVVFXqVjF5qDzGtKWviyjbyi2QnSnxyFAmLlLnBfMX8TLQCAf2GxHibB95RO5FBE6I2KVPRuw==", 209 | "dependencies": { 210 | "@turf/helpers": "^6.5.0", 211 | "@turf/invariant": "^6.5.0", 212 | "@turf/nearest-point-on-line": "^6.5.0" 213 | }, 214 | "funding": { 215 | "url": "https://opencollective.com/turf" 216 | } 217 | }, 218 | "node_modules/@turf/line-split": { 219 | "version": "6.5.0", 220 | "resolved": "https://registry.npmjs.org/@turf/line-split/-/line-split-6.5.0.tgz", 221 | "integrity": "sha512-/rwUMVr9OI2ccJjw7/6eTN53URtGThNSD5I0GgxyFXMtxWiloRJ9MTff8jBbtPWrRka/Sh2GkwucVRAEakx9Sw==", 222 | "dependencies": { 223 | "@turf/bbox": "^6.5.0", 224 | "@turf/helpers": "^6.5.0", 225 | "@turf/invariant": "^6.5.0", 226 | "@turf/line-intersect": "^6.5.0", 227 | "@turf/line-segment": "^6.5.0", 228 | "@turf/meta": "^6.5.0", 229 | "@turf/nearest-point-on-line": "^6.5.0", 230 | "@turf/square": "^6.5.0", 231 | "@turf/truncate": "^6.5.0", 232 | "geojson-rbush": "3.x" 233 | }, 234 | "funding": { 235 | "url": "https://opencollective.com/turf" 236 | } 237 | }, 238 | "node_modules/@turf/meta": { 239 | "version": "6.5.0", 240 | "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-6.5.0.tgz", 241 | "integrity": "sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA==", 242 | "dependencies": { 243 | "@turf/helpers": "^6.5.0" 244 | }, 245 | "funding": { 246 | "url": "https://opencollective.com/turf" 247 | } 248 | }, 249 | "node_modules/@turf/nearest-point-on-line": { 250 | "version": "6.5.0", 251 | "resolved": "https://registry.npmjs.org/@turf/nearest-point-on-line/-/nearest-point-on-line-6.5.0.tgz", 252 | "integrity": "sha512-WthrvddddvmymnC+Vf7BrkHGbDOUu6Z3/6bFYUGv1kxw8tiZ6n83/VG6kHz4poHOfS0RaNflzXSkmCi64fLBlg==", 253 | "dependencies": { 254 | "@turf/bearing": "^6.5.0", 255 | "@turf/destination": "^6.5.0", 256 | "@turf/distance": "^6.5.0", 257 | "@turf/helpers": "^6.5.0", 258 | "@turf/invariant": "^6.5.0", 259 | "@turf/line-intersect": "^6.5.0", 260 | "@turf/meta": "^6.5.0" 261 | }, 262 | "funding": { 263 | "url": "https://opencollective.com/turf" 264 | } 265 | }, 266 | "node_modules/@turf/square": { 267 | "version": "6.5.0", 268 | "resolved": "https://registry.npmjs.org/@turf/square/-/square-6.5.0.tgz", 269 | "integrity": "sha512-BM2UyWDmiuHCadVhHXKIx5CQQbNCpOxB6S/aCNOCLbhCeypKX5Q0Aosc5YcmCJgkwO5BERCC6Ee7NMbNB2vHmQ==", 270 | "dependencies": { 271 | "@turf/distance": "^6.5.0", 272 | "@turf/helpers": "^6.5.0" 273 | }, 274 | "funding": { 275 | "url": "https://opencollective.com/turf" 276 | } 277 | }, 278 | "node_modules/@turf/truncate": { 279 | "version": "6.5.0", 280 | "resolved": "https://registry.npmjs.org/@turf/truncate/-/truncate-6.5.0.tgz", 281 | "integrity": "sha512-pFxg71pLk+eJj134Z9yUoRhIi8vqnnKvCYwdT4x/DQl/19RVdq1tV3yqOT3gcTQNfniteylL5qV1uTBDV5sgrg==", 282 | "dependencies": { 283 | "@turf/helpers": "^6.5.0", 284 | "@turf/meta": "^6.5.0" 285 | }, 286 | "funding": { 287 | "url": "https://opencollective.com/turf" 288 | } 289 | }, 290 | "node_modules/@types/geojson": { 291 | "version": "7946.0.14", 292 | "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", 293 | "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" 294 | }, 295 | "node_modules/@types/geojson-vt": { 296 | "version": "3.2.5", 297 | "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", 298 | "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", 299 | "dependencies": { 300 | "@types/geojson": "*" 301 | } 302 | }, 303 | "node_modules/@types/junit-report-builder": { 304 | "version": "3.0.2", 305 | "resolved": "https://registry.npmjs.org/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz", 306 | "integrity": "sha512-R5M+SYhMbwBeQcNXYWNCZkl09vkVfAtcPIaCGdzIkkbeaTrVbGQ7HVgi4s+EmM/M1K4ZuWQH0jGcvMvNePfxYA==" 307 | }, 308 | "node_modules/@types/mapbox__point-geometry": { 309 | "version": "0.1.4", 310 | "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", 311 | "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==" 312 | }, 313 | "node_modules/@types/mapbox__vector-tile": { 314 | "version": "1.3.4", 315 | "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", 316 | "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", 317 | "dependencies": { 318 | "@types/geojson": "*", 319 | "@types/mapbox__point-geometry": "*", 320 | "@types/pbf": "*" 321 | } 322 | }, 323 | "node_modules/@types/pbf": { 324 | "version": "3.0.5", 325 | "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", 326 | "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==" 327 | }, 328 | "node_modules/@types/supercluster": { 329 | "version": "7.1.3", 330 | "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", 331 | "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", 332 | "dependencies": { 333 | "@types/geojson": "*" 334 | } 335 | }, 336 | "node_modules/arr-union": { 337 | "version": "3.1.0", 338 | "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", 339 | "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", 340 | "engines": { 341 | "node": ">=0.10.0" 342 | } 343 | }, 344 | "node_modules/assign-symbols": { 345 | "version": "1.0.0", 346 | "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", 347 | "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", 348 | "engines": { 349 | "node": ">=0.10.0" 350 | } 351 | }, 352 | "node_modules/bytewise": { 353 | "version": "1.1.0", 354 | "resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz", 355 | "integrity": "sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==", 356 | "dependencies": { 357 | "bytewise-core": "^1.2.2", 358 | "typewise": "^1.0.3" 359 | } 360 | }, 361 | "node_modules/bytewise-core": { 362 | "version": "1.2.3", 363 | "resolved": "https://registry.npmjs.org/bytewise-core/-/bytewise-core-1.2.3.tgz", 364 | "integrity": "sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==", 365 | "dependencies": { 366 | "typewise-core": "^1.2" 367 | } 368 | }, 369 | "node_modules/earcut": { 370 | "version": "2.2.4", 371 | "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", 372 | "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" 373 | }, 374 | "node_modules/extend-shallow": { 375 | "version": "2.0.1", 376 | "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", 377 | "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", 378 | "dependencies": { 379 | "is-extendable": "^0.1.0" 380 | }, 381 | "engines": { 382 | "node": ">=0.10.0" 383 | } 384 | }, 385 | "node_modules/geojson-rbush": { 386 | "version": "3.2.0", 387 | "resolved": "https://registry.npmjs.org/geojson-rbush/-/geojson-rbush-3.2.0.tgz", 388 | "integrity": "sha512-oVltQTXolxvsz1sZnutlSuLDEcQAKYC/uXt9zDzJJ6bu0W+baTI8LZBaTup5afzibEH4N3jlq2p+a152wlBJ7w==", 389 | "dependencies": { 390 | "@turf/bbox": "*", 391 | "@turf/helpers": "6.x", 392 | "@turf/meta": "6.x", 393 | "@types/geojson": "7946.0.8", 394 | "rbush": "^3.0.1" 395 | } 396 | }, 397 | "node_modules/geojson-rbush/node_modules/@types/geojson": { 398 | "version": "7946.0.8", 399 | "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", 400 | "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==" 401 | }, 402 | "node_modules/geojson-vt": { 403 | "version": "3.2.1", 404 | "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", 405 | "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==" 406 | }, 407 | "node_modules/get-stream": { 408 | "version": "6.0.1", 409 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", 410 | "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", 411 | "engines": { 412 | "node": ">=10" 413 | }, 414 | "funding": { 415 | "url": "https://github.com/sponsors/sindresorhus" 416 | } 417 | }, 418 | "node_modules/get-value": { 419 | "version": "2.0.6", 420 | "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", 421 | "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", 422 | "engines": { 423 | "node": ">=0.10.0" 424 | } 425 | }, 426 | "node_modules/gl-matrix": { 427 | "version": "3.4.3", 428 | "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", 429 | "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" 430 | }, 431 | "node_modules/global-prefix": { 432 | "version": "3.0.0", 433 | "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", 434 | "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", 435 | "dependencies": { 436 | "ini": "^1.3.5", 437 | "kind-of": "^6.0.2", 438 | "which": "^1.3.1" 439 | }, 440 | "engines": { 441 | "node": ">=6" 442 | } 443 | }, 444 | "node_modules/ieee754": { 445 | "version": "1.2.1", 446 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 447 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 448 | "funding": [ 449 | { 450 | "type": "github", 451 | "url": "https://github.com/sponsors/feross" 452 | }, 453 | { 454 | "type": "patreon", 455 | "url": "https://www.patreon.com/feross" 456 | }, 457 | { 458 | "type": "consulting", 459 | "url": "https://feross.org/support" 460 | } 461 | ] 462 | }, 463 | "node_modules/ini": { 464 | "version": "1.3.8", 465 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 466 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" 467 | }, 468 | "node_modules/is-extendable": { 469 | "version": "0.1.1", 470 | "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", 471 | "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", 472 | "engines": { 473 | "node": ">=0.10.0" 474 | } 475 | }, 476 | "node_modules/is-plain-object": { 477 | "version": "2.0.4", 478 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", 479 | "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", 480 | "dependencies": { 481 | "isobject": "^3.0.1" 482 | }, 483 | "engines": { 484 | "node": ">=0.10.0" 485 | } 486 | }, 487 | "node_modules/isexe": { 488 | "version": "2.0.0", 489 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 490 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 491 | }, 492 | "node_modules/isobject": { 493 | "version": "3.0.1", 494 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", 495 | "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", 496 | "engines": { 497 | "node": ">=0.10.0" 498 | } 499 | }, 500 | "node_modules/json-stringify-pretty-compact": { 501 | "version": "4.0.0", 502 | "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", 503 | "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==" 504 | }, 505 | "node_modules/kdbush": { 506 | "version": "4.0.2", 507 | "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", 508 | "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==" 509 | }, 510 | "node_modules/kind-of": { 511 | "version": "6.0.3", 512 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", 513 | "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", 514 | "engines": { 515 | "node": ">=0.10.0" 516 | } 517 | }, 518 | "node_modules/maplibre-gl": { 519 | "version": "4.2.0", 520 | "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.2.0.tgz", 521 | "integrity": "sha512-x5GgYyKKn5UDvbUZFK7ng3Pq829/uYWDSVN/itZoP2slWSzKbjIXKi/Qhz5FnYiMXwpRgM08UIcVjtn1PLK9Tg==", 522 | "dependencies": { 523 | "@mapbox/geojson-rewind": "^0.5.2", 524 | "@mapbox/jsonlint-lines-primitives": "^2.0.2", 525 | "@mapbox/point-geometry": "^0.1.0", 526 | "@mapbox/tiny-sdf": "^2.0.6", 527 | "@mapbox/unitbezier": "^0.0.1", 528 | "@mapbox/vector-tile": "^1.3.1", 529 | "@mapbox/whoots-js": "^3.1.0", 530 | "@maplibre/maplibre-gl-style-spec": "^20.2.0", 531 | "@types/geojson": "^7946.0.14", 532 | "@types/geojson-vt": "3.2.5", 533 | "@types/junit-report-builder": "^3.0.2", 534 | "@types/mapbox__point-geometry": "^0.1.4", 535 | "@types/mapbox__vector-tile": "^1.3.4", 536 | "@types/pbf": "^3.0.5", 537 | "@types/supercluster": "^7.1.3", 538 | "earcut": "^2.2.4", 539 | "geojson-vt": "^3.2.1", 540 | "gl-matrix": "^3.4.3", 541 | "global-prefix": "^3.0.0", 542 | "kdbush": "^4.0.2", 543 | "murmurhash-js": "^1.0.0", 544 | "pbf": "^3.2.1", 545 | "potpack": "^2.0.0", 546 | "quickselect": "^2.0.0", 547 | "supercluster": "^8.0.1", 548 | "tinyqueue": "^2.0.3", 549 | "vt-pbf": "^3.1.3" 550 | }, 551 | "engines": { 552 | "node": ">=16.14.0", 553 | "npm": ">=8.1.0" 554 | }, 555 | "funding": { 556 | "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" 557 | } 558 | }, 559 | "node_modules/minimist": { 560 | "version": "1.2.8", 561 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 562 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 563 | "funding": { 564 | "url": "https://github.com/sponsors/ljharb" 565 | } 566 | }, 567 | "node_modules/murmurhash-js": { 568 | "version": "1.0.0", 569 | "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", 570 | "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==" 571 | }, 572 | "node_modules/pbf": { 573 | "version": "3.2.1", 574 | "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", 575 | "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", 576 | "dependencies": { 577 | "ieee754": "^1.1.12", 578 | "resolve-protobuf-schema": "^2.1.0" 579 | }, 580 | "bin": { 581 | "pbf": "bin/pbf" 582 | } 583 | }, 584 | "node_modules/potpack": { 585 | "version": "2.0.0", 586 | "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", 587 | "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==" 588 | }, 589 | "node_modules/prettier": { 590 | "version": "3.2.5", 591 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", 592 | "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", 593 | "dev": true, 594 | "bin": { 595 | "prettier": "bin/prettier.cjs" 596 | }, 597 | "engines": { 598 | "node": ">=14" 599 | }, 600 | "funding": { 601 | "url": "https://github.com/prettier/prettier?sponsor=1" 602 | } 603 | }, 604 | "node_modules/protocol-buffers-schema": { 605 | "version": "3.6.0", 606 | "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", 607 | "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" 608 | }, 609 | "node_modules/quickselect": { 610 | "version": "2.0.0", 611 | "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", 612 | "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" 613 | }, 614 | "node_modules/rbush": { 615 | "version": "3.0.1", 616 | "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", 617 | "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", 618 | "dependencies": { 619 | "quickselect": "^2.0.0" 620 | } 621 | }, 622 | "node_modules/resolve-protobuf-schema": { 623 | "version": "2.1.0", 624 | "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", 625 | "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", 626 | "dependencies": { 627 | "protocol-buffers-schema": "^3.3.1" 628 | } 629 | }, 630 | "node_modules/route-snapper": { 631 | "version": "0.4.2", 632 | "resolved": "https://registry.npmjs.org/route-snapper/-/route-snapper-0.4.2.tgz", 633 | "integrity": "sha512-Mxs3r6SMlelsBHUwb/yFtz1cKmUl7nGAEV+8OKh0/sxCBhKWVPWFCVcVjTa6RKjFxSOaEJUl5eA5pFORNYNLxw==" 634 | }, 635 | "node_modules/rw": { 636 | "version": "1.3.3", 637 | "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", 638 | "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" 639 | }, 640 | "node_modules/set-value": { 641 | "version": "2.0.1", 642 | "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", 643 | "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", 644 | "dependencies": { 645 | "extend-shallow": "^2.0.1", 646 | "is-extendable": "^0.1.1", 647 | "is-plain-object": "^2.0.3", 648 | "split-string": "^3.0.1" 649 | }, 650 | "engines": { 651 | "node": ">=0.10.0" 652 | } 653 | }, 654 | "node_modules/sort-asc": { 655 | "version": "0.2.0", 656 | "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz", 657 | "integrity": "sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==", 658 | "engines": { 659 | "node": ">=0.10.0" 660 | } 661 | }, 662 | "node_modules/sort-desc": { 663 | "version": "0.2.0", 664 | "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.2.0.tgz", 665 | "integrity": "sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==", 666 | "engines": { 667 | "node": ">=0.10.0" 668 | } 669 | }, 670 | "node_modules/sort-object": { 671 | "version": "3.0.3", 672 | "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-3.0.3.tgz", 673 | "integrity": "sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==", 674 | "dependencies": { 675 | "bytewise": "^1.1.0", 676 | "get-value": "^2.0.2", 677 | "is-extendable": "^0.1.1", 678 | "sort-asc": "^0.2.0", 679 | "sort-desc": "^0.2.0", 680 | "union-value": "^1.0.1" 681 | }, 682 | "engines": { 683 | "node": ">=0.10.0" 684 | } 685 | }, 686 | "node_modules/split-string": { 687 | "version": "3.1.0", 688 | "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", 689 | "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", 690 | "dependencies": { 691 | "extend-shallow": "^3.0.0" 692 | }, 693 | "engines": { 694 | "node": ">=0.10.0" 695 | } 696 | }, 697 | "node_modules/split-string/node_modules/extend-shallow": { 698 | "version": "3.0.2", 699 | "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", 700 | "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", 701 | "dependencies": { 702 | "assign-symbols": "^1.0.0", 703 | "is-extendable": "^1.0.1" 704 | }, 705 | "engines": { 706 | "node": ">=0.10.0" 707 | } 708 | }, 709 | "node_modules/split-string/node_modules/is-extendable": { 710 | "version": "1.0.1", 711 | "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", 712 | "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", 713 | "dependencies": { 714 | "is-plain-object": "^2.0.4" 715 | }, 716 | "engines": { 717 | "node": ">=0.10.0" 718 | } 719 | }, 720 | "node_modules/supercluster": { 721 | "version": "8.0.1", 722 | "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", 723 | "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", 724 | "dependencies": { 725 | "kdbush": "^4.0.2" 726 | } 727 | }, 728 | "node_modules/tinyqueue": { 729 | "version": "2.0.3", 730 | "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", 731 | "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" 732 | }, 733 | "node_modules/typescript": { 734 | "version": "5.4.5", 735 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", 736 | "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", 737 | "dev": true, 738 | "bin": { 739 | "tsc": "bin/tsc", 740 | "tsserver": "bin/tsserver" 741 | }, 742 | "engines": { 743 | "node": ">=14.17" 744 | } 745 | }, 746 | "node_modules/typewise": { 747 | "version": "1.0.3", 748 | "resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz", 749 | "integrity": "sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==", 750 | "dependencies": { 751 | "typewise-core": "^1.2.0" 752 | } 753 | }, 754 | "node_modules/typewise-core": { 755 | "version": "1.2.0", 756 | "resolved": "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz", 757 | "integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==" 758 | }, 759 | "node_modules/union-value": { 760 | "version": "1.0.1", 761 | "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", 762 | "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", 763 | "dependencies": { 764 | "arr-union": "^3.1.0", 765 | "get-value": "^2.0.6", 766 | "is-extendable": "^0.1.1", 767 | "set-value": "^2.0.1" 768 | }, 769 | "engines": { 770 | "node": ">=0.10.0" 771 | } 772 | }, 773 | "node_modules/vt-pbf": { 774 | "version": "3.1.3", 775 | "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", 776 | "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", 777 | "dependencies": { 778 | "@mapbox/point-geometry": "0.1.0", 779 | "@mapbox/vector-tile": "^1.3.1", 780 | "pbf": "^3.2.1" 781 | } 782 | }, 783 | "node_modules/which": { 784 | "version": "1.3.1", 785 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 786 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 787 | "dependencies": { 788 | "isexe": "^2.0.0" 789 | }, 790 | "bin": { 791 | "which": "bin/which" 792 | } 793 | } 794 | } 795 | } 796 | -------------------------------------------------------------------------------- /route-snapper-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "route-snapper-ts", 3 | "description": "Draw routes in MapLibre snapped to a street network using client-side routing. TypeScript bindings", 4 | "version": "0.0.8", 5 | "license": "Apache-2.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/dabreegster/route_snapper" 9 | }, 10 | "module": "./dist/index.js", 11 | "types": "./dist/index.d.ts", 12 | "files": [ 13 | "dist/" 14 | ], 15 | "scripts": { 16 | "tsc": "tsc -p tsconfig.json", 17 | "prepublishOnly": "npm run tsc", 18 | "prepare": "npm run prepublishOnly", 19 | "fmt": "prettier --write ." 20 | }, 21 | "devDependencies": { 22 | "@types/geojson": "^7946.0.14", 23 | "prettier": "^3.2.5", 24 | "typescript": "^5.4.5" 25 | }, 26 | "dependencies": { 27 | "maplibre-gl": "^4.0.0", 28 | "route-snapper": "^0.4.2", 29 | "@turf/helpers": "^6.5.0", 30 | "@turf/length": "^6.5.0", 31 | "@turf/line-slice": "^6.5.0", 32 | "@turf/line-split": "^6.5.0", 33 | "@turf/nearest-point-on-line": "^6.5.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /route-snapper-ts/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Feature, GeoJSON, LineString, Polygon, Position } from "geojson"; 2 | import type { Map, MapMouseEvent } from "maplibre-gl"; 3 | import init, { JsRouteSnapper } from "route-snapper"; 4 | import { splitRoute } from "./split"; 5 | 6 | export { init, splitRoute }; 7 | 8 | const snapDistancePixels = 30; 9 | 10 | interface Writable { 11 | set(value: T): void; 12 | } 13 | 14 | export interface RouteProps { 15 | waypoints: Waypoint[]; 16 | length_meters: number; 17 | route_name: string; 18 | full_path: Node[]; 19 | } 20 | 21 | export type Node = { snapped: number } | { free: [number, number] }; 22 | 23 | export interface AreaProps { 24 | waypoints: Waypoint[]; 25 | } 26 | 27 | export interface Waypoint { 28 | lon: number; 29 | lat: number; 30 | snapped: boolean; 31 | } 32 | 33 | export class RouteTool { 34 | map: Map; 35 | inner: JsRouteSnapper; 36 | active: boolean; 37 | eventListenersSuccess: (( 38 | f: Feature | Feature, 39 | ) => void)[]; 40 | eventListenersUpdated: (( 41 | f: Feature | Feature, 42 | ) => void)[]; 43 | eventListenersFailure: (() => void)[]; 44 | 45 | routeToolGj: Writable; 46 | snapMode: Writable; 47 | undoLength: Writable; 48 | 49 | constructor( 50 | map: Map, 51 | graphBytes: Uint8Array, 52 | routeToolGj: Writable, 53 | snapMode: Writable, 54 | undoLength: Writable, 55 | ) { 56 | this.map = map; 57 | console.time("Deserialize and setup JsRouteSnapper"); 58 | this.inner = new JsRouteSnapper(graphBytes); 59 | console.timeEnd("Deserialize and setup JsRouteSnapper"); 60 | this.active = false; 61 | this.eventListenersSuccess = []; 62 | this.eventListenersUpdated = []; 63 | this.eventListenersFailure = []; 64 | 65 | this.routeToolGj = routeToolGj; 66 | this.snapMode = snapMode; 67 | this.undoLength = undoLength; 68 | 69 | this.map.on("mousemove", this.onMouseMove); 70 | this.map.on("click", this.onClick); 71 | this.map.on("dblclick", this.onDoubleClick); 72 | this.map.on("dragstart", this.onDragStart); 73 | this.map.on("mouseup", this.onMouseUp); 74 | document.addEventListener("keydown", this.onKeyDown); 75 | document.addEventListener("keypress", this.onKeyPress); 76 | } 77 | 78 | tearDown() { 79 | this.map.off("mousemove", this.onMouseMove); 80 | this.map.off("click", this.onClick); 81 | this.map.off("dblclick", this.onDoubleClick); 82 | this.map.off("dragstart", this.onDragStart); 83 | this.map.off("mouseup", this.onMouseUp); 84 | document.removeEventListener("keydown", this.onKeyDown); 85 | document.removeEventListener("keypress", this.onKeyPress); 86 | } 87 | 88 | onMouseMove = (e: MapMouseEvent) => { 89 | if (!this.active) { 90 | return; 91 | } 92 | const nearbyPoint: [number, number] = [ 93 | e.point.x - snapDistancePixels, 94 | e.point.y, 95 | ]; 96 | const circleRadiusMeters = this.map 97 | .unproject(e.point) 98 | .distanceTo(this.map.unproject(nearbyPoint)); 99 | if ( 100 | this.inner.onMouseMove(e.lngLat.lng, e.lngLat.lat, circleRadiusMeters) 101 | ) { 102 | this.redraw(); 103 | // TODO We'll call this too frequently 104 | this.dataUpdated(); 105 | } 106 | }; 107 | 108 | onClick = () => { 109 | if (!this.active) { 110 | return; 111 | } 112 | this.inner.onClick(); 113 | this.redraw(); 114 | this.dataUpdated(); 115 | }; 116 | 117 | onDoubleClick = (e: MapMouseEvent) => { 118 | if (!this.active) { 119 | return; 120 | } 121 | // When we finish, we'll re-enable doubleClickZoom, but we don't want this to zoom in 122 | e.preventDefault(); 123 | // Double clicks happen as [click, click, dblclick]. The first click adds a 124 | // point, the second immediately deletes it, and so we simulate a third 125 | // click to add it again. 126 | this.inner.onClick(); 127 | this.finish(); 128 | }; 129 | 130 | onDragStart = () => { 131 | if (!this.active) { 132 | return; 133 | } 134 | if (this.inner.onDragStart()) { 135 | this.map.dragPan.disable(); 136 | } 137 | }; 138 | 139 | onMouseUp = () => { 140 | if (!this.active) { 141 | return; 142 | } 143 | if (this.inner.onMouseUp()) { 144 | this.map.dragPan.enable(); 145 | } 146 | }; 147 | 148 | onKeyDown = (e: KeyboardEvent) => { 149 | if (!this.active) { 150 | return; 151 | } 152 | 153 | // Ignore keypresses if the user is focused on a form 154 | let tag = (e.target as HTMLElement).tagName; 155 | if (tag == "INPUT" || tag == "TEXTAREA") { 156 | return; 157 | } 158 | 159 | if (e.key == "Escape") { 160 | e.stopPropagation(); 161 | this.cancel(); 162 | } 163 | }; 164 | 165 | onKeyPress = (e: KeyboardEvent) => { 166 | if (!this.active) { 167 | return; 168 | } 169 | 170 | // Ignore keypresses if the user is focused on a form 171 | let tag = (e.target as HTMLElement).tagName; 172 | if (tag == "INPUT" || tag == "TEXTAREA") { 173 | return; 174 | } 175 | 176 | if (e.key == "Enter") { 177 | e.stopPropagation(); 178 | this.finish(); 179 | } else if (e.key == "s" || e.key == "S") { 180 | e.stopPropagation(); 181 | this.inner.toggleSnapMode(); 182 | this.redraw(); 183 | } else if (e.key == "z" && e.ctrlKey) { 184 | this.undo(); 185 | } 186 | }; 187 | 188 | // Activate the tool with blank state. 189 | startRoute() { 190 | // If we were already active, don't do anything 191 | // TODO Or... error? Why'd this happen? 192 | if (this.active) { 193 | return; 194 | } 195 | 196 | this.active = true; 197 | 198 | // Otherwise, shift+click breaks 199 | this.map.boxZoom.disable(); 200 | // Otherwise, double clicking to finish breaks 201 | this.map.doubleClickZoom.disable(); 202 | } 203 | 204 | // Activate the tool with blank state. 205 | startArea() { 206 | // If we were already active, don't do anything 207 | // TODO Or... error? Why'd this happen? 208 | if (this.active) { 209 | return; 210 | } 211 | 212 | this.inner.setAreaMode(); 213 | this.active = true; 214 | this.map.boxZoom.disable(); 215 | this.map.doubleClickZoom.disable(); 216 | } 217 | 218 | // Deactivate the tool, clearing all state. No events are fired for eventListenersFailure. 219 | stop() { 220 | this.active = false; 221 | this.inner.clearState(); 222 | this.redraw(); 223 | this.map.boxZoom.enable(); 224 | this.map.doubleClickZoom.enable(); 225 | } 226 | 227 | // This takes a GeoJSON feature previously returned. It must have all 228 | // properties returned originally. If waypoints are missing (maybe because 229 | // the route was produced by a different tool, or an older version of this 230 | // tool), the edited line-string may differ from the input. 231 | editExistingRoute(feature: Feature) { 232 | if (this.active) { 233 | window.alert("Bug: editExistingRoute called when tool is already active"); 234 | } 235 | 236 | if (!feature.properties.waypoints) { 237 | // Only use the first and last points as waypoints, and assume they're 238 | // snapped. This only works for the simplest cases. 239 | feature.properties.waypoints = [ 240 | { 241 | lon: feature.geometry.coordinates[0][0], 242 | lat: feature.geometry.coordinates[0][1], 243 | snapped: true, 244 | }, 245 | { 246 | lon: feature.geometry.coordinates[ 247 | feature.geometry.coordinates.length - 1 248 | ][0], 249 | lat: feature.geometry.coordinates[ 250 | feature.geometry.coordinates.length - 1 251 | ][1], 252 | snapped: true, 253 | }, 254 | ]; 255 | } 256 | 257 | this.startRoute(); 258 | this.inner.editExisting(feature.properties.waypoints); 259 | this.redraw(); 260 | } 261 | 262 | // This only handles features previously returned by this tool. 263 | editExistingArea(feature: Feature) { 264 | if (this.active) { 265 | window.alert("Bug: editExistingArea called when tool is already active"); 266 | } 267 | 268 | if (!feature.properties.waypoints) { 269 | window.alert( 270 | "Bug: editExistingArea called for a polygon not produced by the route-snapper", 271 | ); 272 | } 273 | 274 | this.startArea(); 275 | this.inner.editExisting(feature.properties.waypoints); 276 | this.redraw(); 277 | } 278 | 279 | addEventListenerSuccess( 280 | callback: ( 281 | f: Feature | Feature, 282 | ) => void, 283 | ) { 284 | this.eventListenersSuccess.push(callback); 285 | } 286 | addEventListenerUpdated( 287 | callback: ( 288 | f: Feature | Feature, 289 | ) => void, 290 | ) { 291 | this.eventListenersUpdated.push(callback); 292 | } 293 | addEventListenerFailure(callback: () => void) { 294 | this.eventListenersFailure.push(callback); 295 | } 296 | clearEventListeners() { 297 | this.eventListenersSuccess = []; 298 | this.eventListenersUpdated = []; 299 | this.eventListenersFailure = []; 300 | } 301 | 302 | isActive(): boolean { 303 | return this.active; 304 | } 305 | 306 | // Either a success or failure event will happen, depending on current state 307 | finish() { 308 | let rawJSON = this.inner.toFinalFeature(); 309 | if (rawJSON) { 310 | // Pass copies to each callback 311 | for (let cb of this.eventListenersSuccess) { 312 | cb( 313 | JSON.parse(rawJSON) as 314 | | Feature 315 | | Feature, 316 | ); 317 | } 318 | } else { 319 | for (let cb of this.eventListenersFailure) { 320 | cb(); 321 | } 322 | } 323 | this.stop(); 324 | } 325 | 326 | // This stops the tool and fires a failure event 327 | cancel() { 328 | this.inner.clearState(); 329 | this.finish(); 330 | } 331 | 332 | setRouteConfig(config: { 333 | avoid_doubling_back: boolean; 334 | extend_route: boolean; 335 | }) { 336 | this.inner.setRouteConfig(config); 337 | this.redraw(); 338 | } 339 | 340 | addSnappedWaypoint(pt: Position) { 341 | this.inner.addSnappedWaypoint(pt[0], pt[1]); 342 | this.redraw(); 343 | } 344 | 345 | undo() { 346 | this.inner.undo(); 347 | this.redraw(); 348 | } 349 | 350 | toggleSnapMode() { 351 | this.inner.toggleSnapMode(); 352 | this.redraw(); 353 | } 354 | 355 | private redraw() { 356 | let gj = JSON.parse(this.inner.renderGeojson()); 357 | this.routeToolGj.set(gj); 358 | this.map.getCanvas().style.cursor = gj.cursor; 359 | this.snapMode.set(gj.snap_mode); 360 | this.undoLength.set(gj.undo_length); 361 | } 362 | 363 | private dataUpdated() { 364 | let rawJSON = this.inner.toFinalFeature(); 365 | if (rawJSON) { 366 | // Pass copies to each callback 367 | for (let cb of this.eventListenersUpdated) { 368 | cb( 369 | JSON.parse(rawJSON) as 370 | | Feature 371 | | Feature, 372 | ); 373 | } 374 | } 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /route-snapper-ts/src/split.ts: -------------------------------------------------------------------------------- 1 | import { point } from "@turf/helpers"; 2 | import length from "@turf/length"; 3 | import lineSlice from "@turf/line-slice"; 4 | import lineSplit from "@turf/line-split"; 5 | import nearestPointOnLine from "@turf/nearest-point-on-line"; 6 | import type { Feature, LineString, Point, Position } from "geojson"; 7 | import type { RouteProps } from "./index"; 8 | 9 | // Splits a LineString produced from route mode by a point on (or near) the 10 | // line. If successful, returns two new features. The properties of the input 11 | // feature are copied, then `length_meters` and `waypoints` are fixed. The 12 | // feature ID is not modified. 13 | export function splitRoute( 14 | input: Feature, 15 | splitPoint: Feature, 16 | ): [Feature, Feature] | null { 17 | let result = lineSplit(input, splitPoint); 18 | if (result.features.length != 2) { 19 | return null; 20 | } 21 | let piece1 = result.features[0] as Feature; 22 | let piece2 = result.features[1] as Feature; 23 | 24 | // lineSplit may introduce unnecessary coordinate precision 25 | piece1.geometry.coordinates = piece1.geometry.coordinates.map(setPrecision); 26 | piece2.geometry.coordinates = piece2.geometry.coordinates.map(setPrecision); 27 | 28 | // The properties get lost. Deep copy everything to both 29 | piece1.properties = JSON.parse(JSON.stringify(input.properties)); 30 | piece2.properties = JSON.parse(JSON.stringify(input.properties)); 31 | 32 | fixRouteProperties(input, piece1, piece2, splitPoint); 33 | 34 | return [piece1, piece2]; 35 | } 36 | 37 | function fixRouteProperties( 38 | original: Feature, 39 | piece1: Feature, 40 | piece2: Feature, 41 | splitPt: Feature, 42 | ) { 43 | // Fix length 44 | piece1.properties.length_meters = 45 | length(piece1, { units: "kilometers" }) * 1000.0; 46 | piece2.properties.length_meters = 47 | length(piece2, { units: "kilometers" }) * 1000.0; 48 | 49 | piece1.properties.waypoints = []; 50 | piece2.properties.waypoints = []; 51 | 52 | let splitDist = distanceAlongLine(original, splitPt); 53 | let firstPiece = true; 54 | // TODO Can we iterate over an array's contents and get the index at the same time? 55 | let i = 0; 56 | for (let waypt of original.properties.waypoints!) { 57 | let wayptDist = distanceAlongLine(original, point([waypt.lon, waypt.lat])); 58 | if (firstPiece) { 59 | if (wayptDist < splitDist) { 60 | piece1.properties.waypoints.push(waypt); 61 | } else { 62 | // We found where the split occurs. We'll insert a new waypoint 63 | // representing the split at the end of piece1 and the beginning of 64 | // piece2. Should that new waypoint be snapped or freehand? There are 65 | // 4 cases for where the split (|) happens with regards to a 66 | // (s)napped and (f)reehand point: 67 | // 68 | // 1) s | s 69 | // 2) s | f 70 | // 3) f | s 71 | // 4) f | f 72 | // 73 | // Only in case 1 should the new waypoint introduced at (|) be 74 | // snapped. 75 | // TODO Problem: in case 1, what if we split in the middle of a road, 76 | // far from an intersection? 77 | 78 | // Note i > 0; splitDist can't be before the first waypoint (distance 0) 79 | // TODO Edge case: somebody manages to exactly click a waypoint 80 | let snapped = 81 | waypt.snapped && original.properties.waypoints![i - 1].snapped; 82 | 83 | piece1.properties.waypoints.push({ 84 | lon: splitPt.geometry.coordinates[0], 85 | lat: splitPt.geometry.coordinates[1], 86 | snapped, 87 | }); 88 | 89 | firstPiece = false; 90 | piece2.properties.waypoints.push({ 91 | lon: splitPt.geometry.coordinates[0], 92 | lat: splitPt.geometry.coordinates[1], 93 | snapped, 94 | }); 95 | piece2.properties.waypoints.push(waypt); 96 | } 97 | } else { 98 | piece2.properties.waypoints.push(waypt); 99 | } 100 | i++; 101 | } 102 | } 103 | 104 | // Returns the distance of a point along a line-string from the start, in 105 | // meters. The point should be roughly on the line. 106 | function distanceAlongLine(line: Feature, point: Feature) { 107 | // TODO Is there a cheaper way to do this? 108 | let start = line.geometry.coordinates[0]; 109 | let sliced = lineSlice(start, point, line); 110 | return length(sliced, { units: "kilometers" }) * 1000.0; 111 | } 112 | 113 | // Per https://datatracker.ietf.org/doc/html/rfc7946#section-11.2, 6 decimal 114 | // places (10cm) is plenty of precision 115 | function setPrecision(pt: Position): Position { 116 | return [Math.round(pt[0] * 10e6) / 10e6, Math.round(pt[1] * 10e6) / 10e6]; 117 | } 118 | -------------------------------------------------------------------------------- /route-snapper-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "ES2020", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "outDir": "./dist", 10 | "declaration": true 11 | }, 12 | "include": ["./src"] 13 | } 14 | -------------------------------------------------------------------------------- /route-snapper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "route-snapper" 3 | version = "0.4.9" 4 | edition = "2021" 5 | description = "Draw routes in MapLibre snapped to a street network using client-side routing" 6 | repository = "https://github.com/dabreegster/route_snapper" 7 | license = "Apache-2.0" 8 | 9 | [lib] 10 | crate-type = ["cdylib", "rlib"] 11 | 12 | [dependencies] 13 | bincode = "1.3.1" 14 | console_error_panic_hook = "0.1.6" 15 | console_log = "1.0.0" 16 | geo = { workspace = true } 17 | geojson = { workspace = true } 18 | log = "0.4.20" 19 | petgraph = "0.6.4" 20 | route-snapper-graph = { path = "../route-snapper-graph" } 21 | rstar = "0.12.0" 22 | serde = "1.0.188" 23 | serde-wasm-bindgen = "0.6.0" 24 | serde_json = "1.0.107" 25 | wasm-bindgen = "0.2.87" 26 | web-sys = { version = "0.3.64", features = ["console"] } 27 | -------------------------------------------------------------------------------- /route-snapper/README.md: -------------------------------------------------------------------------------- 1 | ../user_guide.md -------------------------------------------------------------------------------- /route-snapper/lib.js: -------------------------------------------------------------------------------- 1 | import init, { JsRouteSnapper } from "./route_snapper.js"; 2 | 3 | export { init }; 4 | 5 | export class RouteSnapper { 6 | constructor(map, graphBytes, controlDiv) { 7 | const circleRadiusPixels = 10; 8 | const snapDistancePixels = 30; 9 | 10 | this.controlDiv = controlDiv; 11 | this.map = map; 12 | console.time("Deserialize and setup JsRouteSnapper"); 13 | this.inner = new JsRouteSnapper(graphBytes); 14 | console.timeEnd("Deserialize and setup JsRouteSnapper"); 15 | console.log("JsRouteSnapper ready, waiting for idle event"); 16 | this.active = false; 17 | // Indicates the idle event has been received, and the source/layers are set up 18 | this.loaded = false; 19 | 20 | // on(load) is a bad trigger, because downloading the RouteSnapper input 21 | // can race. Just wait for the map to be usable. 22 | this.map.once("idle", () => { 23 | console.log("JsRouteSnapper now usable"); 24 | this.map.addSource("route-snapper", { 25 | type: "geojson", 26 | data: { 27 | type: "FeatureCollection", 28 | features: [], 29 | }, 30 | }); 31 | this.map.addLayer({ 32 | id: "route-points", 33 | source: "route-snapper", 34 | filter: ["in", "$type", "Point"], 35 | type: "circle", 36 | paint: { 37 | "circle-radius": [ 38 | "match", 39 | ["get", "type"], 40 | "node", 41 | circleRadiusPixels / 2.0, 42 | // other 43 | circleRadiusPixels, 44 | ], 45 | "circle-color": [ 46 | "match", 47 | ["get", "type"], 48 | "snapped-waypoint", 49 | "red", 50 | "free-waypoint", 51 | "blue", 52 | // other (node) 53 | "black", 54 | ], 55 | "circle-opacity": ["case", ["has", "hovered"], 0.5, 1.0], 56 | }, 57 | }); 58 | this.map.addLayer({ 59 | id: "route-lines", 60 | source: "route-snapper", 61 | filter: ["in", "$type", "LineString"], 62 | type: "line", 63 | layout: { 64 | "line-cap": "round", 65 | "line-join": "round", 66 | }, 67 | paint: { 68 | "line-color": ["case", ["get", "snapped"], "red", "blue"], 69 | "line-width": 2.5, 70 | }, 71 | }); 72 | this.map.addLayer({ 73 | id: "route-polygons", 74 | source: "route-snapper", 75 | filter: ["in", "$type", "Polygon"], 76 | type: "fill", 77 | paint: { 78 | "fill-color": "black", 79 | "fill-opacity": 0.4, 80 | }, 81 | }); 82 | this.loaded = true; 83 | 84 | this.map.on("mousemove", (e) => { 85 | if (!this.active) { 86 | return; 87 | } 88 | const nearbyPoint = { x: e.point.x - snapDistancePixels, y: e.point.y }; 89 | const circleRadiusMeters = this.map 90 | .unproject(e.point) 91 | .distanceTo(this.map.unproject(nearbyPoint)); 92 | if ( 93 | this.inner.onMouseMove(e.lngLat.lng, e.lngLat.lat, circleRadiusMeters) 94 | ) { 95 | this.#redraw(); 96 | } 97 | }); 98 | 99 | this.map.on("click", () => { 100 | if (!this.active) { 101 | return; 102 | } 103 | this.inner.onClick(); 104 | this.#redraw(); 105 | }); 106 | 107 | this.map.on("dblclick", () => { 108 | if (!this.active) { 109 | return; 110 | } 111 | // Treat it like a click, to possibly add a final point 112 | this.inner.onClick(); 113 | // But then finish 114 | this.#finishSnapping(); 115 | }); 116 | 117 | this.map.on("dragstart", (e) => { 118 | if (!this.active) { 119 | return; 120 | } 121 | if (this.inner.onDragStart()) { 122 | this.map.dragPan.disable(); 123 | } 124 | }); 125 | 126 | this.map.on("mouseup", (e) => { 127 | if (!this.active) { 128 | return; 129 | } 130 | if (this.inner.onMouseUp()) { 131 | this.map.dragPan.enable(); 132 | } 133 | }); 134 | 135 | document.addEventListener("keypress", (e) => { 136 | if (!this.active) { 137 | return; 138 | } 139 | if (e.key == "Enter") { 140 | e.preventDefault(); 141 | this.#finishSnapping(); 142 | } else if (e.key == "s") { 143 | e.preventDefault(); 144 | this.inner.toggleSnapMode(); 145 | this.#redraw(); 146 | } else if (e.key == "z" && e.ctrlKey) { 147 | e.preventDefault(); 148 | this.inner.undo(); 149 | this.#redraw(); 150 | } 151 | }); 152 | 153 | this.stop(); 154 | }); 155 | } 156 | 157 | // Change the underlying graph after initially creating RouteSnapper. 158 | changeGraph(graphBytes) { 159 | console.time("Deserialize and setup JsRouteSnapper with new graph"); 160 | this.inner = new JsRouteSnapper(graphBytes); 161 | console.timeEnd("Deserialize and setup JsRouteSnapper with new graph"); 162 | } 163 | 164 | isActive() { 165 | return this.active; 166 | } 167 | 168 | // Destroy resources attached to the map. Warning, this doesn't yet handle 169 | // event listeners! 170 | tearDown() { 171 | if (!this.loaded) { 172 | // TODO Can we cancel the map.on(idle) event? 173 | return; 174 | } 175 | this.map.removeLayer("route-points"); 176 | this.map.removeLayer("route-lines"); 177 | this.map.removeSource("route-snapper"); 178 | // TODO Remove the event listeners on document and map 179 | } 180 | 181 | // This takes a GeoJSON feature previously returned from the new-route event. 182 | // It must have all properties returned originally. If waypoints are missing 183 | // (maybe because the route was produced by a different tool, or an older 184 | // version of this tool), the edited line-string may differ from the input. 185 | // 186 | // Note no events are fired by calling this. 187 | editExisting(feature) { 188 | if (!this.loaded) { 189 | // TODO This is an unlikely race condition. What should we do? 190 | console.error( 191 | "editExisting called before the map idle event received. Not starting tool." 192 | ); 193 | return; 194 | } 195 | 196 | if (!feature.properties.waypoints) { 197 | // Only use the first and last points as waypoints, and assume they're 198 | // snapped. This only works for the simplest cases. 199 | feature.properties.waypoints = [ 200 | { 201 | lon: feature.geometry.coordinates[0][0], 202 | lat: feature.geometry.coordinates[0][1], 203 | snapped: true, 204 | }, 205 | { 206 | lon: feature.geometry.coordinates[ 207 | feature.geometry.coordinates.length - 1 208 | ][0], 209 | lat: feature.geometry.coordinates[ 210 | feature.geometry.coordinates.length - 1 211 | ][1], 212 | snapped: true, 213 | }, 214 | ]; 215 | } 216 | 217 | this.start(); 218 | 219 | if (feature.geometry.type == "Polygon") { 220 | this.inner.setAreaMode(); 221 | } 222 | 223 | this.inner.editExisting(feature.properties.waypoints); 224 | this.#redraw(); 225 | } 226 | 227 | // Deactivate the tool, clearing all state. No events (`no-new-route`) are fired. 228 | stop() { 229 | if (!this.loaded) { 230 | return; 231 | } 232 | 233 | this.active = false; 234 | 235 | this.inner.clearState(); 236 | this.#redraw(); 237 | 238 | this.controlDiv.innerHTML = ``; 239 | document.getElementById("start-button").onclick = () => { 240 | this.controlDiv.dispatchEvent(new CustomEvent("activate")); 241 | this.start(); 242 | }; 243 | } 244 | 245 | // Activate the tool. 246 | start() { 247 | // If we were already active, don't do anything 248 | if (this.active) { 249 | return; 250 | } 251 | 252 | this.active = true; 253 | 254 | this.controlDiv.innerHTML = ` 255 |
256 | 257 | 258 | 259 |
260 | 261 |
262 | 266 |
267 |
268 | 272 |
273 |
274 | 278 |
279 | 280 |
281 | Snapping to transport network 282 |
283 | 284 |

Waypoint names (unordered):

285 |
    286 | 287 |
      288 |
    • Click green points on the transport network
      to create snapped routes
    • 289 |
    • Press s to toggle snapping / freehand mode
    • 290 |
    • Click and drag any point to move it
    • 291 |
    • Click a red waypoint to delete it
    • 292 |
    • Press Control+Z to undo
    • 293 |
    • Press Enter or double click to finish route
    • 294 |
    • Press Escape to cancel and discard route
    • 295 |
    296 | 297 |
    298 | 299 | 300 |
    301 | `; 302 | 303 | document.getElementById("finish-route-button").onclick = () => { 304 | this.#finishSnapping(); 305 | }; 306 | document.getElementById("undo-button").onclick = () => { 307 | this.inner.undo(); 308 | this.#redraw(); 309 | }; 310 | document.getElementById("cancel-button").onclick = () => { 311 | this.controlDiv.dispatchEvent(new CustomEvent("no-new-route")); 312 | this.stop(); 313 | }; 314 | let avoidDoublingBack = document.getElementById("avoidDoublingBack"); 315 | let areaMode = document.getElementById("areaMode"); 316 | let extendRoute = document.getElementById("extendRoute"); 317 | avoidDoublingBack.onclick = () => { 318 | this.inner.setRouteConfig({ 319 | avoid_doubling_back: avoidDoublingBack.checked, 320 | extend_route: extendRoute.checked, 321 | }); 322 | this.#redraw(); 323 | }; 324 | extendRoute.onclick = avoidDoublingBack.onclick; 325 | areaMode.onclick = () => { 326 | if (areaMode.checked) { 327 | avoidDoublingBack.checked = true; 328 | extendRoute.checked = true; 329 | this.inner.setAreaMode(); 330 | } else { 331 | this.inner.setRouteConfig({ 332 | avoid_doubling_back: avoidDoublingBack.checked, 333 | extend_route: extendRoute.checked, 334 | }); 335 | } 336 | this.#redraw(); 337 | }; 338 | 339 | document.getElementById("add-waypoint-button").onclick = () => { 340 | let value = document.getElementById("add-waypoint-value").value; 341 | let parts = value.split(/\s*,\s*/).map(parseFloat); 342 | if ( 343 | parts.length == 2 && 344 | !Number.isNaN(parts[0]) && 345 | !Number.isNaN(parts[1]) 346 | ) { 347 | this.inner.addSnappedWaypoint(parts[0], parts[1]); 348 | this.#redraw(); 349 | } else { 350 | window.alert("Invalid input, no waypoint added"); 351 | } 352 | }; 353 | 354 | // Sync checkboxes with the tool's current state, from the last time it was used 355 | let config = JSON.parse(this.inner.getConfig()); 356 | avoidDoublingBack.checked = config.avoid_doubling_back; 357 | extendRoute.checked = config.extend_route; 358 | areaMode.checked = config.area_mode; 359 | } 360 | 361 | // Render the graph as GeoJSON points and line-strings, for debugging. 362 | debugRenderGraph() { 363 | return this.inner.debugRenderGraph(); 364 | } 365 | 366 | // Given waypoint properties, calculate the route name. 367 | routeNameForWaypoints(waypoints) { 368 | return this.inner.routeNameForWaypoints(waypoints); 369 | } 370 | 371 | #finishSnapping() { 372 | // Update the source-of-truth in drawControls 373 | const rawJSON = this.inner.toFinalFeature(); 374 | if (rawJSON) { 375 | this.controlDiv.dispatchEvent( 376 | new CustomEvent("new-route", { detail: JSON.parse(rawJSON) }) 377 | ); 378 | } else { 379 | this.controlDiv.dispatchEvent(new CustomEvent("no-new-route")); 380 | } 381 | this.stop(); 382 | } 383 | 384 | #redraw() { 385 | if (this.loaded) { 386 | let gj = JSON.parse(this.inner.renderGeojson()); 387 | this.map.getSource("route-snapper").setData(gj); 388 | this.map.getCanvas().style.cursor = gj.cursor; 389 | 390 | let undoButton = document.getElementById("undo-button"); 391 | if (undoButton) { 392 | if (gj.undo_length > 0) { 393 | undoButton.disabled = false; 394 | undoButton.textContent = `Undo (${gj.undo_length})`; 395 | } else { 396 | undoButton.textContent = "Undo"; 397 | undoButton.disabled = true; 398 | } 399 | } 400 | 401 | // TODO Detect changes, don't do this constantly? 402 | let snapDiv = document.getElementById("snap_mode"); 403 | if (snapDiv) { 404 | if (gj.snap_mode) { 405 | snapDiv.style = "background: red; color: white; padding: 8px"; 406 | snapDiv.innerHTML = "Snapping to transport network"; 407 | } else { 408 | snapDiv.style = "background: blue; color: white; padding: 8px"; 409 | snapDiv.innerHTML = "Drawing freehand points"; 410 | } 411 | } 412 | 413 | let list = document.getElementById("waypoint_list"); 414 | if (list) { 415 | list.innerHTML = ""; 416 | for (let f of gj.features) { 417 | if (f.properties.name) { 418 | let li = document.createElement("li"); 419 | li.innerText = f.properties.name; 420 | list.appendChild(li); 421 | } 422 | } 423 | } 424 | } 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /route-snapper/src/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | // The NodeIDs depend on the real southwark.bin graph! If the path between two nodes happens to 4 | // include a third node, then a test may be confusing, because it could look like an intermediate 5 | // point. 6 | const WAYPT1: Waypoint = Waypoint::Snapped(NodeID(10)); 7 | const WAYPT2: Waypoint = Waypoint::Snapped(NodeID(20)); 8 | const WAYPT3: Waypoint = Waypoint::Snapped(NodeID(30)); 9 | const WAYPT4: Waypoint = Waypoint::Snapped(NodeID(40)); 10 | const WAYPT5: Waypoint = Waypoint::Snapped(NodeID(50)); 11 | 12 | // TODO Test freehand points with all of the variations of tests below 13 | 14 | #[test] 15 | fn test_route_delete() { 16 | let map_bytes = std::fs::read("../examples/southwark.bin").unwrap(); 17 | let mut snapper = JsRouteSnapper::new(&map_bytes).unwrap(); 18 | 19 | // Add a waypoint 20 | must_mouseover_waypt(&mut snapper, WAYPT1); 21 | snapper.on_click(); 22 | assert_eq!(snapper.route.waypoints, vec![WAYPT1]); 23 | 24 | // Click again to delete it -- shouldn't work, it's the only one 25 | snapper.on_click(); 26 | assert_eq!(snapper.route.waypoints, vec![WAYPT1]); 27 | 28 | // Add a second 29 | must_mouseover_waypt(&mut snapper, WAYPT2); 30 | snapper.on_click(); 31 | assert_eq!(snapper.route.waypoints, vec![WAYPT1, WAYPT2]); 32 | 33 | // Delete the first waypoint 34 | must_mouseover_waypt(&mut snapper, WAYPT1); 35 | snapper.on_click(); 36 | assert_eq!(snapper.route.waypoints, vec![WAYPT2]); 37 | } 38 | 39 | #[test] 40 | fn test_route_extend() { 41 | let map_bytes = std::fs::read("../examples/southwark.bin").unwrap(); 42 | let mut snapper = JsRouteSnapper::new(&map_bytes).unwrap(); 43 | // setRouteConfig is a WASM API awkward to call; just set directly 44 | snapper.router.config.extend_route = true; 45 | 46 | // Add many waypoints 47 | must_mouseover_waypt(&mut snapper, WAYPT1); 48 | snapper.on_click(); 49 | assert_eq!(snapper.route.waypoints, vec![WAYPT1]); 50 | 51 | must_mouseover_waypt(&mut snapper, WAYPT2); 52 | snapper.on_click(); 53 | assert_eq!(snapper.route.waypoints, vec![WAYPT1, WAYPT2]); 54 | 55 | must_mouseover_waypt(&mut snapper, WAYPT3); 56 | snapper.on_click(); 57 | assert_eq!(snapper.route.waypoints, vec![WAYPT1, WAYPT2, WAYPT3]); 58 | 59 | must_mouseover_waypt(&mut snapper, WAYPT4); 60 | snapper.on_click(); 61 | assert_eq!( 62 | snapper.route.waypoints, 63 | vec![WAYPT1, WAYPT2, WAYPT3, WAYPT4] 64 | ); 65 | 66 | // TODO Drag 67 | } 68 | 69 | #[test] 70 | fn test_route_extend_then_delete() { 71 | let map_bytes = std::fs::read("../examples/southwark.bin").unwrap(); 72 | let mut snapper = JsRouteSnapper::new(&map_bytes).unwrap(); 73 | // setRouteConfig is a WASM API awkward to call; just set directly 74 | snapper.router.config.extend_route = true; 75 | 76 | // Add many waypoints 77 | for waypt in [WAYPT1, WAYPT2, WAYPT3, WAYPT4] { 78 | must_mouseover_waypt(&mut snapper, waypt); 79 | snapper.on_click(); 80 | } 81 | assert_eq!( 82 | snapper.route.waypoints, 83 | vec![WAYPT1, WAYPT2, WAYPT3, WAYPT4] 84 | ); 85 | 86 | // Clicking an existing waypoint will delete it 87 | must_mouseover_waypt(&mut snapper, WAYPT2); 88 | snapper.on_click(); 89 | assert_eq!(snapper.route.waypoints, vec![WAYPT1, WAYPT3, WAYPT4]); 90 | } 91 | 92 | #[test] 93 | fn test_route_dont_extend() { 94 | let map_bytes = std::fs::read("../examples/southwark.bin").unwrap(); 95 | let mut snapper = JsRouteSnapper::new(&map_bytes).unwrap(); 96 | // setRouteConfig is a WASM API awkward to call; just set directly 97 | snapper.router.config.extend_route = false; 98 | 99 | // The first two waypoints work normally 100 | must_mouseover_waypt(&mut snapper, WAYPT1); 101 | snapper.on_click(); 102 | assert_eq!(snapper.route.waypoints, vec![WAYPT1]); 103 | 104 | must_mouseover_waypt(&mut snapper, WAYPT2); 105 | snapper.on_click(); 106 | assert_eq!(snapper.route.waypoints, vec![WAYPT1, WAYPT2]); 107 | 108 | // But then we can't add another one 109 | optionally_mouseover_waypt(&mut snapper, WAYPT3); 110 | snapper.on_click(); 111 | assert_eq!(snapper.route.waypoints, vec![WAYPT1, WAYPT2]); 112 | 113 | // Delete the first waypoint 114 | must_mouseover_waypt(&mut snapper, WAYPT1); 115 | snapper.on_click(); 116 | assert_eq!(snapper.route.waypoints, vec![WAYPT2]); 117 | 118 | // Then add a different endpoint 119 | must_mouseover_waypt(&mut snapper, WAYPT4); 120 | snapper.on_click(); 121 | assert_eq!(snapper.route.waypoints, vec![WAYPT2, WAYPT4]); 122 | 123 | // TODO Drag 124 | } 125 | 126 | #[test] 127 | fn test_area() { 128 | let map_bytes = std::fs::read("../examples/southwark.bin").unwrap(); 129 | let mut snapper = JsRouteSnapper::new(&map_bytes).unwrap(); 130 | snapper.set_area_mode(); 131 | 132 | // The first two points just make a line 133 | must_mouseover_waypt(&mut snapper, WAYPT1); 134 | snapper.on_click(); 135 | assert_eq!(snapper.route.waypoints, vec![WAYPT1]); 136 | 137 | must_mouseover_waypt(&mut snapper, WAYPT2); 138 | snapper.on_click(); 139 | assert_eq!(snapper.route.waypoints, vec![WAYPT1, WAYPT2]); 140 | 141 | // The third creates the polygon, and the first and last waypoints are now the same 142 | must_mouseover_waypt(&mut snapper, WAYPT3); 143 | snapper.on_click(); 144 | assert_eq!( 145 | snapper.route.waypoints, 146 | vec![WAYPT1, WAYPT2, WAYPT3, WAYPT1] 147 | ); 148 | 149 | // Can't delete an intermediate waypoint if there aren't enough 150 | // TODO Actually not true; decide whether the behavior now is what we want or not 151 | /*must_mouseover_waypt(&mut snapper, WAYPT2); 152 | snapper.on_click(); 153 | assert_eq!(snapper.route.waypoints, vec![WAYPT1, WAYPT2, WAYPT3, WAYPT1]);*/ 154 | 155 | // Drag something in between 1 and 2 156 | let intermediate = find_intermediate_point(&snapper, WAYPT1, WAYPT2); 157 | // Just make sure it's not one of the arbitrary IDs we chose. Manually figured out this ID. 158 | assert_eq!(intermediate, Waypoint::Snapped(NodeID(6235))); 159 | drag(&mut snapper, intermediate, WAYPT4); 160 | // We should've introduced a new waypoint 161 | assert_eq!( 162 | snapper.route.waypoints, 163 | vec![WAYPT1, WAYPT4, WAYPT2, WAYPT3, WAYPT1] 164 | ); 165 | 166 | // Due to a current limitation, we can't delete the first/last waypoint 167 | must_mouseover_waypt(&mut snapper, WAYPT1); 168 | snapper.on_click(); 169 | assert_eq!( 170 | snapper.route.waypoints, 171 | vec![WAYPT1, WAYPT4, WAYPT2, WAYPT3, WAYPT1] 172 | ); 173 | 174 | // If we modify the first point, the last stays in sync 175 | drag(&mut snapper, WAYPT1, WAYPT5); 176 | assert_eq!( 177 | snapper.route.waypoints, 178 | vec![WAYPT5, WAYPT4, WAYPT2, WAYPT3, WAYPT5] 179 | ); 180 | 181 | // We can delete an intermediate point 182 | must_mouseover_waypt(&mut snapper, WAYPT2); 183 | snapper.on_click(); 184 | assert_eq!( 185 | snapper.route.waypoints, 186 | vec![WAYPT5, WAYPT4, WAYPT3, WAYPT5] 187 | ); 188 | } 189 | 190 | // Simulate the mouse being somewhere 191 | fn optionally_mouseover_waypt(snapper: &mut JsRouteSnapper, waypt: Waypoint) { 192 | let pt = unhash_pt(snapper.to_pt(waypt)); 193 | let circle_radius_meters = 1.0; 194 | snapper.on_mouse_move(pt.x, pt.y, circle_radius_meters); 195 | } 196 | 197 | // Simulate the mouse being somewhere, then check the tool is hovering on that waypoint 198 | fn must_mouseover_waypt(snapper: &mut JsRouteSnapper, waypt: Waypoint) { 199 | optionally_mouseover_waypt(snapper, waypt); 200 | assert_eq!(snapper.mode, Mode::Hovering(waypt)); 201 | } 202 | 203 | // After the route containts two waypoints, find some point in the middle of the two 204 | fn find_intermediate_point(snapper: &JsRouteSnapper, pt1: Waypoint, pt2: Waypoint) -> Waypoint { 205 | // First get the full path without PathEntry::Edges 206 | let all_points: Vec = snapper 207 | .route 208 | .full_path 209 | .iter() 210 | .flat_map(|path_entry| path_entry.to_waypt()) 211 | .collect(); 212 | 213 | let idx1 = all_points.iter().position(|x| *x == pt1).unwrap(); 214 | let idx2 = all_points.iter().position(|x| *x == pt2).unwrap(); 215 | assert!(idx1 < idx2); 216 | // Pick something in the middle 217 | let middle = idx1 + ((idx2 - idx1) as f64 / 2.0) as usize; 218 | assert_ne!(middle, idx1); 219 | all_points[middle] 220 | } 221 | 222 | fn drag(snapper: &mut JsRouteSnapper, from: Waypoint, to: Waypoint) { 223 | must_mouseover_waypt(snapper, from); 224 | snapper.on_drag_start(); 225 | optionally_mouseover_waypt(snapper, to); 226 | snapper.on_mouse_up(); 227 | } 228 | -------------------------------------------------------------------------------- /user_guide.md: -------------------------------------------------------------------------------- 1 | # route-snapper user guide 2 | 3 | ## Building a graph file 4 | 5 | The plugin can draw routes on any 6 | [graph](https://github.com/dabreegster/route_snapper/blob/main/route-snapper-graph/src/lib.rs) 7 | that has coordinates defined for the edges. 8 | 9 | ### From OpenStreetMap data 10 | 11 | A common use case is routing along a street network. You can create an example 12 | file from OpenStreetMap data. The easiest way to do this for smaller areas is 13 | [in your web browser](https://dabreegster.github.io/route_snapper/import.html). 14 | 15 | For larger areas, you need an `.osm.xml` or `.osm.pbf` file, and optionally a 16 | GeoJSON file with one polygon or multipolygon representing the boundary of your 17 | area. You'll need to [install Rust](https://www.rust-lang.org/tools/install) to 18 | run this: 19 | 20 | ``` 21 | cd osm-to-route-snapper 22 | cargo run --release \ 23 | -i path_to_osm.xml \ 24 | [-b path_to_boundary.geojson] 25 | ``` 26 | 27 | ### From custom GeoJSON files 28 | 29 | If you have a GeoJSON file with LineStrings representing routable edges in a 30 | network, you can turn this into a graph too. Try first [in your web 31 | browser](https://dabreegster.github.io/route_snapper/import.html) using the 32 | button at the top. For larger areas, install Rust and then: 33 | 34 | ``` 35 | cd geojson-to-route-snapper 36 | cargo run --release -- --input path_to_network.geojson 37 | ``` 38 | 39 | For routing to work, the LineStrings must share points with other LineStrings. 40 | If you have one long LineString touching many others and the points are the 41 | same, these will be split into edges automatically. LineStrings that cross but 42 | don't share points (within 7 decimal places) won't connect. 43 | 44 | Each LineString has some properties: 45 | 46 | - an optional numeric `forward_cost` and `backward_cost`. 47 | 48 | Costs **must** be specified for some of the edges in the file. If a cost is 49 | missing, the edge won't be routable in that direction. Use `null` to indicate 50 | the edge isn't routable at all in that direction. 51 | 52 | - an optional string `name`. 53 | 54 | Unlike the OpenStreetMap importer, distance is not used as a default cost. 55 | 56 | ## Adding to a MapLibre app 57 | 58 | See [the end-to-end 59 | example](https://github.com/dabreegster/route_snapper/blob/main/examples/index.html). 60 | 61 | ### Installation 62 | 63 | If you're using NPM, do `npm i route-snapper` and then in your JS: 64 | 65 | ``` 66 | import { init, RouteSnapper } from "route-snapper/lib.js"; 67 | ``` 68 | 69 | You can also load from a CDN: 70 | 71 | ``` 72 | import { init, RouteSnapper } from "https://unpkg.com/route-snapper/lib.js"; 73 | ``` 74 | 75 | ### Setup 76 | 77 | To initialize the WASM library, you have to `await init()`. 78 | 79 | You'll need to get the raw graph file you built. You can do this however you like, such as using [fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch). 80 | 81 | To create the route snapper, you need a MapLibre map (it can be initialized or not), the graph, and a `div` element for the plugin to render its controls. From the [example](https://github.com/dabreegster/route_snapper/blob/main/examples/index.html), it might look like this: 82 | 83 | ``` 84 | await init(); 85 | 86 | let resp = await fetch(url); 87 | let graphBytes = await resp.arrayBuffer(); 88 | let routeSnapper = new RouteSnapper( 89 | map, 90 | new Uint8Array(graphBytes), 91 | document.getElementById("snap-tool") 92 | ); 93 | ``` 94 | 95 | ### Events 96 | 97 | The above is all you need to get the tool working. To actually get the resulting GeoJSON line-string that the user draws, you listen to the `new-route` event on the `div` element that you passed into the constructor: 98 | 99 | ``` 100 | document.getElementById("snap-tool").addEventListener("new-route", (e) => { 101 | // A GeoJSON LineString feature with no properties set 102 | console.log(e.detail); 103 | }); 104 | ``` 105 | 106 | There are other events you may care about: 107 | 108 | - `activate`: The user clicked the button to start drawing a route 109 | - `no-new-route`: The user started drawing a route, but cancelled or otherwise 110 | didn't produce any valid result 111 | 112 | Note `activate` isn't fired if you manually call `start()` or `editExisting()`, 113 | only when the button is pressed. These details are subject to change before the 114 | next major version. 115 | 116 | ### API 117 | 118 | There are a few methods on the `RouteSnapper` object you can call: 119 | 120 | - `isActive()` returns true when the tool is active and interpreting mouse events 121 | - `tearDown()` cleans up the internal sources and layers added to the map. 122 | (Note it doesn't yet clean up event listeners!) 123 | - `setRouteConfig` to change some settings for drawing routes 124 | - `avoid_doubling_back` (disabled by default): When possible, avoid edges 125 | already crossed for handling intermediate waypoints 126 | - `extend_route` (disabled by default): The user can keep clicking to extend the end of the route. When false, the user can only draw two endpoints, then drag intermediate points. 127 | - `setAreaMode()` changes to producing polygons instead of line-strings. 128 | - `editExisting` to restart the tool with a previously created route. See notes 129 | in [the example](https://github.com/dabreegster/route_snapper/blob/main/examples/index.html) 130 | about how to call it. 131 | - `start` activates the tool. It has no effect if the tool is already started. 132 | - `stop` deactivates the tool and clears all state 133 | - `debugRenderGraph` returns GeoJSON points and line-strings to debug the graph used for routing. 134 | - `changeGraph` can be used after initialization to change the loaded graph. It 135 | takes `graphBytes`, same as the constructor. 136 | - `routeNameForWaypoints` takes the `feature.properties.waypoints` and returns 137 | a name describing the first and last waypoint (useful only for snapped 138 | waypoints). 139 | 140 | ### WASM API 141 | 142 | If you're using the WASM API directly, the best reference is currently [the code](https://github.com/dabreegster/route_snapper/blob/main/route-snapper/src/lib.rs). Some particulars: 143 | 144 | - `renderGeojson` returns a GeoJSON FeatureCollection to render the current state of the tool. 145 | - It'll include LineStrings showing the confirmed route and also any speculative addition, based on the current state. The LineStrings will have a boolean `snapped` property, which is false if either end touches a freehand point. 146 | - In area mode, it'll have a Polygon once there are at least 3 points. 147 | - It'll include a Point for every graph node involved in the current route. These will have a `type` property that's either `snapped-waypoint`, `free-waypoint`, or just `node` to indicate a draggable node that hasn't been touched yet. One Point may also have a `"hovered": true` property to indicate the mouse is currently on that Point. Points may also have a `name` property with the road names for that intersection. 148 | - The GeoJSON object will have some additional foreign members: 149 | - `cursor`, indicating the current mode of the tool. The values can be set to `map.getCanvas().style.cursor` as desired. 150 | - `inherit`: The user is just idling on the map, not interacting with the map 151 | - `pointer`: The user is hovering on some node 152 | - `grabbing`: The user is actively dragging a node 153 | - `crosshair`: The user is choosing a location for a new freehand point. If they click, the point will be added. 154 | - A boolean `snap_mode` 155 | - A numeric `undo_length` 156 | - `toggleSnapMode` attempts to switch between snapping and freehand drawing. It may not succeed. 157 | - `addSnappedWaypoint` adds a new waypoint to the end of the route, snapping to the nearest node. It's useful for clients to hook up a geocoder and add a point by address. Unsupported in area mode. 158 | - `debugSnappableNodes` returns a FeatureCollection of Points with no properties, for showing the user all snappable nodes 159 | 160 | ### MapLibre gotchas 161 | 162 | You must specify `boxZoom: false` when creating your 163 | [Map](https://maplibre.org/maplibre-gl-js-docs/api/map/), or shift-click for 164 | drawing freehand points won't work. Likewise, you need to disable 165 | `doubleClickZoom` so that you can double click to end a route. 166 | 167 | ### Using with mapbox-gl-draw 168 | 169 | For a full example in a Svelte app, see [here](https://github.com/acteng/atip/blob/dcfd6efbc6e5f25060ddd8f449bae5ac1bca672a/components/DrawControls.svelte). 170 | 171 | [mapbox-gl-draw](https://github.com/mapbox/mapbox-gl-draw) is a common plugin 172 | for drawing things on a map. There are a few tricks to making `route-snapper` 173 | work with it. While the user is drawing a route, you probably don't want 174 | `mapbox-gl-draw` to interpret mouse events if the route happens to cross some 175 | drawn object. 176 | 177 | First you can create a "static mode" using something like [this](https://github.com/mapbox/mapbox-gl-draw-static-mode), to disable all controls for clicking objects and dragging points around. Then you can switch to this whenever the route plugin is active: 178 | 179 | ``` 180 | document.getElementById("snap-tool").addEventListener("activate", () => { 181 | // Disable interactions with other drawn objects 182 | drawControls.changeMode("static"); 183 | }); 184 | document.getElementById("snap-tool").addEventListener("no-new-route", () => { 185 | // Reactivate interactions 186 | drawControls.changeMode("simple_select"); 187 | }); 188 | ``` 189 | 190 | If you want `mapbox-gl-draw` to manage line-strings that the tool produces, you can do this: 191 | 192 | ``` 193 | document.getElementById("snap-tool").addEventListener("new-route", () => { 194 | let feature = e.detail; 195 | let ids = drawControls.add(feature); 196 | // Act like we've selected the line-string we just drew 197 | drawControls.changeMode("direct_select", { 198 | featureId: ids[0], 199 | }); 200 | ``` 201 | 202 | ## Routing caveats 203 | 204 | The routes calculated by the tool are based on the input graph. The default 205 | option described above pulls in road segments from OpenStreetMap for many 206 | modes, including tram or light-rail, walking or cycling only paths, and 207 | `highway=construction`. The "optimal" paths drawn by the tool are based on 208 | Euclidean distance -- no speed limits, safety of following the route by some 209 | user, etc is attempted. The route may violate one-way restrictions. In other 210 | words, if you're using the defaults, you will get routes that shouldn't 211 | actually be followed in the real world for many reasons. 212 | 213 | This default is designed for one particular use case: drawing potential new 214 | active travel routes along existing roads. The user designing these proposed 215 | routes is expected to understand the properties of the roads selected, and 216 | incorporate appropriate changes in their larger work. The route snapper UI 217 | emphasizes adjusting waypoints easily, letting the user quickly "mold" whatever 218 | they have in mind. 219 | 220 | If you'd like to use this library for other purposes (like offline routing for 221 | end-users), you'll need to generate custom graphs from GeoJSON. See the section 222 | above and please file an issue if you have any trouble. 223 | --------------------------------------------------------------------------------