├── .github └── workflows │ └── test.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── README.qmd ├── README_files └── figure-commonmark │ ├── thresholddemo-1.png │ ├── thresholddemo-2.png │ └── thresholddemo-3.png ├── data ├── od.csv ├── od_destinations.csv ├── od_schools.csv ├── output_destinations_differ_50.geojson ├── output_max10.geojson ├── output_max50.geojson ├── road_network.geojson ├── schools.geojson ├── zones.geojson └── zones_combined.geojson ├── r ├── .Rbuildignore ├── .gitignore ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── R │ └── jitter.R ├── README.Rmd ├── README.md ├── README_files │ └── figure-gfm │ │ ├── jitter-1.png │ │ └── jitter-2.png ├── man │ └── jitter.Rd └── odjitter.Rproj ├── references.bib └── src ├── lib.rs ├── main.rs ├── scrape.rs └── tests.rs /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Install Rust 16 | uses: hecrj/setup-rust-action@v1 17 | with: 18 | rust-version: stable 19 | - name: Run tests 20 | run: | 21 | cargo test 22 | cargo fmt --check 23 | cargo clippy 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | *.swp 3 | .Rproj.user 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.72" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" 10 | 11 | [[package]] 12 | name = "approx" 13 | version = "0.4.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" 16 | dependencies = [ 17 | "num-traits", 18 | ] 19 | 20 | [[package]] 21 | name = "atomic-polyfill" 22 | version = "0.1.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28" 25 | dependencies = [ 26 | "critical-section", 27 | ] 28 | 29 | [[package]] 30 | name = "atty" 31 | version = "0.2.14" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 34 | dependencies = [ 35 | "hermit-abi", 36 | "libc", 37 | "winapi", 38 | ] 39 | 40 | [[package]] 41 | name = "autocfg" 42 | version = "1.1.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 45 | 46 | [[package]] 47 | name = "bitflags" 48 | version = "1.3.2" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 51 | 52 | [[package]] 53 | name = "bitflags" 54 | version = "2.3.3" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" 57 | 58 | [[package]] 59 | name = "byteorder" 60 | version = "1.4.3" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 63 | 64 | [[package]] 65 | name = "cc" 66 | version = "1.0.79" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 69 | 70 | [[package]] 71 | name = "cfg-if" 72 | version = "1.0.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 75 | 76 | [[package]] 77 | name = "clap" 78 | version = "3.0.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "d17bf219fcd37199b9a29e00ba65dfb8cd5b2688b7297ec14ff829c40ac50ca9" 81 | dependencies = [ 82 | "atty", 83 | "bitflags 1.3.2", 84 | "clap_derive", 85 | "indexmap", 86 | "lazy_static", 87 | "os_str_bytes", 88 | "strsim", 89 | "termcolor", 90 | "textwrap", 91 | ] 92 | 93 | [[package]] 94 | name = "clap_derive" 95 | version = "3.0.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "e1b9752c030a14235a0bd5ef3ad60a1dcac8468c30921327fc8af36b20c790b9" 98 | dependencies = [ 99 | "heck", 100 | "proc-macro-error", 101 | "proc-macro2", 102 | "quote", 103 | "syn 1.0.83", 104 | ] 105 | 106 | [[package]] 107 | name = "critical-section" 108 | version = "1.1.1" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "6548a0ad5d2549e111e1f6a11a6c2e2d00ce6a3dafe22948d67c2b443f775e52" 111 | 112 | [[package]] 113 | name = "csv" 114 | version = "1.2.2" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086" 117 | dependencies = [ 118 | "csv-core", 119 | "itoa", 120 | "ryu", 121 | "serde", 122 | ] 123 | 124 | [[package]] 125 | name = "csv-core" 126 | version = "0.1.10" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" 129 | dependencies = [ 130 | "memchr", 131 | ] 132 | 133 | [[package]] 134 | name = "earcutr" 135 | version = "0.4.2" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "0812b44697951d35fde8fcb0da81c9de7e809e825a66bbf1ecb79d9829d4ca3d" 138 | dependencies = [ 139 | "itertools", 140 | "num-traits", 141 | ] 142 | 143 | [[package]] 144 | name = "either" 145 | version = "1.9.0" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 148 | 149 | [[package]] 150 | name = "errno" 151 | version = "0.3.1" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 154 | dependencies = [ 155 | "errno-dragonfly", 156 | "libc", 157 | "windows-sys", 158 | ] 159 | 160 | [[package]] 161 | name = "errno-dragonfly" 162 | version = "0.1.2" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 165 | dependencies = [ 166 | "cc", 167 | "libc", 168 | ] 169 | 170 | [[package]] 171 | name = "fallible-streaming-iterator" 172 | version = "0.1.9" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 175 | 176 | [[package]] 177 | name = "fastrand" 178 | version = "2.0.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" 181 | 182 | [[package]] 183 | name = "flatbuffers" 184 | version = "23.5.26" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "4dac53e22462d78c16d64a1cd22371b54cc3fe94aa15e7886a2fa6e5d1ab8640" 187 | dependencies = [ 188 | "bitflags 1.3.2", 189 | "rustc_version", 190 | ] 191 | 192 | [[package]] 193 | name = "flatgeobuf" 194 | version = "3.26.1" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "8c928a1f1759cf2f15af3da7a429bfec16de6ce26c8beb1ff95bef9429208dbd" 197 | dependencies = [ 198 | "byteorder", 199 | "fallible-streaming-iterator", 200 | "flatbuffers", 201 | "geozero", 202 | "log", 203 | "tempfile", 204 | ] 205 | 206 | [[package]] 207 | name = "float_next_after" 208 | version = "1.0.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" 211 | 212 | [[package]] 213 | name = "fs-err" 214 | version = "2.9.0" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "0845fa252299212f0389d64ba26f34fa32cfe41588355f21ed507c59a0f64541" 217 | 218 | [[package]] 219 | name = "geo" 220 | version = "0.26.0" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "1645cf1d7fea7dac1a66f7357f3df2677ada708b8d9db8e9b043878930095a96" 223 | dependencies = [ 224 | "earcutr", 225 | "float_next_after", 226 | "geo-types", 227 | "geographiclib-rs", 228 | "log", 229 | "num-traits", 230 | "robust", 231 | "rstar", 232 | ] 233 | 234 | [[package]] 235 | name = "geo-types" 236 | version = "0.7.11" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "9705398c5c7b26132e74513f4ee7c1d7dafd786004991b375c172be2be0eecaa" 239 | dependencies = [ 240 | "approx", 241 | "num-traits", 242 | "rstar", 243 | "serde", 244 | ] 245 | 246 | [[package]] 247 | name = "geographiclib-rs" 248 | version = "0.2.3" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "8ea804e7bd3c6a4ca6a01edfa35231557a8a81d4d3f3e1e2b650d028c42592be" 251 | dependencies = [ 252 | "lazy_static", 253 | ] 254 | 255 | [[package]] 256 | name = "geojson" 257 | version = "0.24.1" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "a5d728c1df1fbf328d74151efe6cb0586f79ee813346ea981add69bd22c9241b" 260 | dependencies = [ 261 | "geo-types", 262 | "log", 263 | "serde", 264 | "serde_json", 265 | "thiserror", 266 | ] 267 | 268 | [[package]] 269 | name = "geozero" 270 | version = "0.10.0" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "937818b9c084b253f929b5f5dbe050e744331d94ceb0a908b08873bcb2da3066" 273 | dependencies = [ 274 | "geojson", 275 | "log", 276 | "serde_json", 277 | "thiserror", 278 | ] 279 | 280 | [[package]] 281 | name = "getrandom" 282 | version = "0.2.3" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 285 | dependencies = [ 286 | "cfg-if", 287 | "libc", 288 | "wasi", 289 | ] 290 | 291 | [[package]] 292 | name = "hash32" 293 | version = "0.2.1" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" 296 | dependencies = [ 297 | "byteorder", 298 | ] 299 | 300 | [[package]] 301 | name = "hashbrown" 302 | version = "0.11.2" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 305 | 306 | [[package]] 307 | name = "heapless" 308 | version = "0.7.16" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743" 311 | dependencies = [ 312 | "atomic-polyfill", 313 | "hash32", 314 | "rustc_version", 315 | "spin", 316 | "stable_deref_trait", 317 | ] 318 | 319 | [[package]] 320 | name = "heck" 321 | version = "0.3.3" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 324 | dependencies = [ 325 | "unicode-segmentation", 326 | ] 327 | 328 | [[package]] 329 | name = "hermit-abi" 330 | version = "0.1.19" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 333 | dependencies = [ 334 | "libc", 335 | ] 336 | 337 | [[package]] 338 | name = "indexmap" 339 | version = "1.7.0" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" 342 | dependencies = [ 343 | "autocfg", 344 | "hashbrown", 345 | ] 346 | 347 | [[package]] 348 | name = "itertools" 349 | version = "0.10.5" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 352 | dependencies = [ 353 | "either", 354 | ] 355 | 356 | [[package]] 357 | name = "itoa" 358 | version = "1.0.1" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" 361 | 362 | [[package]] 363 | name = "lazy_static" 364 | version = "1.4.0" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 367 | 368 | [[package]] 369 | name = "libc" 370 | version = "0.2.147" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 373 | 374 | [[package]] 375 | name = "libm" 376 | version = "0.2.7" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" 379 | 380 | [[package]] 381 | name = "linux-raw-sys" 382 | version = "0.4.3" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" 385 | 386 | [[package]] 387 | name = "lock_api" 388 | version = "0.4.10" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" 391 | dependencies = [ 392 | "autocfg", 393 | "scopeguard", 394 | ] 395 | 396 | [[package]] 397 | name = "log" 398 | version = "0.4.19" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" 401 | 402 | [[package]] 403 | name = "memchr" 404 | version = "2.4.1" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 407 | 408 | [[package]] 409 | name = "num-traits" 410 | version = "0.2.14" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" 413 | dependencies = [ 414 | "autocfg", 415 | "libm", 416 | ] 417 | 418 | [[package]] 419 | name = "odjitter" 420 | version = "0.1.0" 421 | dependencies = [ 422 | "anyhow", 423 | "clap", 424 | "csv", 425 | "flatgeobuf", 426 | "fs-err", 427 | "geo", 428 | "geo-types", 429 | "geojson", 430 | "geozero", 431 | "ordered-float", 432 | "rand", 433 | "rstar", 434 | "serde_json", 435 | ] 436 | 437 | [[package]] 438 | name = "ordered-float" 439 | version = "3.7.0" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "2fc2dbde8f8a79f2102cc474ceb0ad68e3b80b85289ea62389b60e66777e4213" 442 | dependencies = [ 443 | "num-traits", 444 | ] 445 | 446 | [[package]] 447 | name = "os_str_bytes" 448 | version = "6.0.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" 451 | dependencies = [ 452 | "memchr", 453 | ] 454 | 455 | [[package]] 456 | name = "ppv-lite86" 457 | version = "0.2.15" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" 460 | 461 | [[package]] 462 | name = "proc-macro-error" 463 | version = "1.0.4" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 466 | dependencies = [ 467 | "proc-macro-error-attr", 468 | "proc-macro2", 469 | "quote", 470 | "syn 1.0.83", 471 | "version_check", 472 | ] 473 | 474 | [[package]] 475 | name = "proc-macro-error-attr" 476 | version = "1.0.4" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 479 | dependencies = [ 480 | "proc-macro2", 481 | "quote", 482 | "version_check", 483 | ] 484 | 485 | [[package]] 486 | name = "proc-macro2" 487 | version = "1.0.66" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 490 | dependencies = [ 491 | "unicode-ident", 492 | ] 493 | 494 | [[package]] 495 | name = "quote" 496 | version = "1.0.32" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" 499 | dependencies = [ 500 | "proc-macro2", 501 | ] 502 | 503 | [[package]] 504 | name = "rand" 505 | version = "0.8.4" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" 508 | dependencies = [ 509 | "libc", 510 | "rand_chacha", 511 | "rand_core", 512 | "rand_hc", 513 | ] 514 | 515 | [[package]] 516 | name = "rand_chacha" 517 | version = "0.3.1" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 520 | dependencies = [ 521 | "ppv-lite86", 522 | "rand_core", 523 | ] 524 | 525 | [[package]] 526 | name = "rand_core" 527 | version = "0.6.3" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 530 | dependencies = [ 531 | "getrandom", 532 | ] 533 | 534 | [[package]] 535 | name = "rand_hc" 536 | version = "0.3.1" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" 539 | dependencies = [ 540 | "rand_core", 541 | ] 542 | 543 | [[package]] 544 | name = "redox_syscall" 545 | version = "0.3.5" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 548 | dependencies = [ 549 | "bitflags 1.3.2", 550 | ] 551 | 552 | [[package]] 553 | name = "robust" 554 | version = "1.1.0" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "cbf4a6aa5f6d6888f39e980649f3ad6b666acdce1d78e95b8a2cb076e687ae30" 557 | 558 | [[package]] 559 | name = "rstar" 560 | version = "0.11.0" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "73111312eb7a2287d229f06c00ff35b51ddee180f017ab6dec1f69d62ac098d6" 563 | dependencies = [ 564 | "heapless", 565 | "num-traits", 566 | "smallvec", 567 | ] 568 | 569 | [[package]] 570 | name = "rustc_version" 571 | version = "0.4.0" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 574 | dependencies = [ 575 | "semver", 576 | ] 577 | 578 | [[package]] 579 | name = "rustix" 580 | version = "0.38.4" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" 583 | dependencies = [ 584 | "bitflags 2.3.3", 585 | "errno", 586 | "libc", 587 | "linux-raw-sys", 588 | "windows-sys", 589 | ] 590 | 591 | [[package]] 592 | name = "ryu" 593 | version = "1.0.9" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" 596 | 597 | [[package]] 598 | name = "scopeguard" 599 | version = "1.2.0" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 602 | 603 | [[package]] 604 | name = "semver" 605 | version = "1.0.18" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" 608 | 609 | [[package]] 610 | name = "serde" 611 | version = "1.0.177" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "63ba2516aa6bf82e0b19ca8b50019d52df58455d3cf9bdaf6315225fdd0c560a" 614 | dependencies = [ 615 | "serde_derive", 616 | ] 617 | 618 | [[package]] 619 | name = "serde_derive" 620 | version = "1.0.177" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "401797fe7833d72109fedec6bfcbe67c0eed9b99772f26eb8afd261f0abc6fd3" 623 | dependencies = [ 624 | "proc-macro2", 625 | "quote", 626 | "syn 2.0.27", 627 | ] 628 | 629 | [[package]] 630 | name = "serde_json" 631 | version = "1.0.104" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" 634 | dependencies = [ 635 | "itoa", 636 | "ryu", 637 | "serde", 638 | ] 639 | 640 | [[package]] 641 | name = "smallvec" 642 | version = "1.7.0" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" 645 | 646 | [[package]] 647 | name = "spin" 648 | version = "0.9.8" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 651 | dependencies = [ 652 | "lock_api", 653 | ] 654 | 655 | [[package]] 656 | name = "stable_deref_trait" 657 | version = "1.2.0" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 660 | 661 | [[package]] 662 | name = "strsim" 663 | version = "0.10.0" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 666 | 667 | [[package]] 668 | name = "syn" 669 | version = "1.0.83" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "23a1dfb999630e338648c83e91c59a4e9fb7620f520c3194b6b89e276f2f1959" 672 | dependencies = [ 673 | "proc-macro2", 674 | "quote", 675 | "unicode-xid", 676 | ] 677 | 678 | [[package]] 679 | name = "syn" 680 | version = "2.0.27" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" 683 | dependencies = [ 684 | "proc-macro2", 685 | "quote", 686 | "unicode-ident", 687 | ] 688 | 689 | [[package]] 690 | name = "tempfile" 691 | version = "3.7.0" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" 694 | dependencies = [ 695 | "cfg-if", 696 | "fastrand", 697 | "redox_syscall", 698 | "rustix", 699 | "windows-sys", 700 | ] 701 | 702 | [[package]] 703 | name = "termcolor" 704 | version = "1.1.2" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 707 | dependencies = [ 708 | "winapi-util", 709 | ] 710 | 711 | [[package]] 712 | name = "textwrap" 713 | version = "0.14.2" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" 716 | 717 | [[package]] 718 | name = "thiserror" 719 | version = "1.0.30" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" 722 | dependencies = [ 723 | "thiserror-impl", 724 | ] 725 | 726 | [[package]] 727 | name = "thiserror-impl" 728 | version = "1.0.30" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" 731 | dependencies = [ 732 | "proc-macro2", 733 | "quote", 734 | "syn 1.0.83", 735 | ] 736 | 737 | [[package]] 738 | name = "unicode-ident" 739 | version = "1.0.11" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 742 | 743 | [[package]] 744 | name = "unicode-segmentation" 745 | version = "1.8.0" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" 748 | 749 | [[package]] 750 | name = "unicode-xid" 751 | version = "0.2.2" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 754 | 755 | [[package]] 756 | name = "version_check" 757 | version = "0.9.3" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" 760 | 761 | [[package]] 762 | name = "wasi" 763 | version = "0.10.2+wasi-snapshot-preview1" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 766 | 767 | [[package]] 768 | name = "winapi" 769 | version = "0.3.9" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 772 | dependencies = [ 773 | "winapi-i686-pc-windows-gnu", 774 | "winapi-x86_64-pc-windows-gnu", 775 | ] 776 | 777 | [[package]] 778 | name = "winapi-i686-pc-windows-gnu" 779 | version = "0.4.0" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 782 | 783 | [[package]] 784 | name = "winapi-util" 785 | version = "0.1.5" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 788 | dependencies = [ 789 | "winapi", 790 | ] 791 | 792 | [[package]] 793 | name = "winapi-x86_64-pc-windows-gnu" 794 | version = "0.4.0" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 797 | 798 | [[package]] 799 | name = "windows-sys" 800 | version = "0.48.0" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 803 | dependencies = [ 804 | "windows-targets", 805 | ] 806 | 807 | [[package]] 808 | name = "windows-targets" 809 | version = "0.48.1" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" 812 | dependencies = [ 813 | "windows_aarch64_gnullvm", 814 | "windows_aarch64_msvc", 815 | "windows_i686_gnu", 816 | "windows_i686_msvc", 817 | "windows_x86_64_gnu", 818 | "windows_x86_64_gnullvm", 819 | "windows_x86_64_msvc", 820 | ] 821 | 822 | [[package]] 823 | name = "windows_aarch64_gnullvm" 824 | version = "0.48.0" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 827 | 828 | [[package]] 829 | name = "windows_aarch64_msvc" 830 | version = "0.48.0" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 833 | 834 | [[package]] 835 | name = "windows_i686_gnu" 836 | version = "0.48.0" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 839 | 840 | [[package]] 841 | name = "windows_i686_msvc" 842 | version = "0.48.0" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 845 | 846 | [[package]] 847 | name = "windows_x86_64_gnu" 848 | version = "0.48.0" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 851 | 852 | [[package]] 853 | name = "windows_x86_64_gnullvm" 854 | version = "0.48.0" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 857 | 858 | [[package]] 859 | name = "windows_x86_64_msvc" 860 | version = "0.48.0" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 863 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "odjitter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Dustin Carlino 58 | 59 | OPTIONS: 60 | -h, --help Print help information 61 | -V, --version Print version information 62 | 63 | SUBCOMMANDS: 64 | disaggregate Fully disaggregate input desire lines into output representing one trip 65 | each, with a `mode` column 66 | help Print this message or the help of the given subcommand(s) 67 | jitter Import raw data and build an activity model for a region 68 | 69 | As shown in the output above the `odjitter` command line tools has 70 | subcommands: `disaggregate` and `jitter`. The main difference between 71 | these commands is that `jitter` returns OD pairs representing multiple 72 | trips or fractions of a trip. `disaggregate`, by contrast, returns data 73 | representing single trips. 74 | 75 | ## Docker 76 | 77 | Alternatively, you can run through Docker: `docker run -t abstreet/odjitter `. See below for command line usage, or start with `help`. 78 | 79 | NOTE: There's no maintenance guarantee the Docker image has up-to-date changes from this repository. File an issue if you think the Docker version is out-of-date and you need something newer. 80 | 81 | (For maintainers only: to build and push a new version, `docker build -t odjitter . && docker tag odjitter abstreet/odjitter:latest && docker push abstreet/odjitter:latest`.) 82 | 83 | # `jitter` OD data 84 | 85 | To jitter OD data you need a minimum of three inputs, examples of which 86 | are provided in the [`data/` 87 | folder](https://github.com/dabreegster/odjitter/tree/main/data) of this 88 | repo, the first few lines of which are illustrated below: 89 | 90 | 1. A [.csv 91 | file](https://github.com/dabreegster/odjitter/blob/main/data/od.csv) 92 | containing OD data with two columns containing zone IDs (specified 93 | with `--origin-key=geo_code1 --destination-key=geo_code2` by 94 | default) and other columns representing trip counts: 95 | 96 | | geo_code1 | geo_code2 | all | from_home | train | bus | car_driver | car_passenger | bicycle | foot | other | 97 | |:----------|:----------|----:|----------:|------:|----:|-----------:|--------------:|--------:|-----:|------:| 98 | | S02001616 | S02001616 | 82 | 0 | 0 | 3 | 6 | 0 | 2 | 71 | 0 | 99 | | S02001616 | S02001620 | 188 | 0 | 0 | 42 | 26 | 3 | 11 | 105 | 1 | 100 | | S02001616 | S02001621 | 99 | 0 | 0 | 13 | 7 | 3 | 15 | 61 | 0 | 101 | 102 | 2. A [.geojson 103 | file](https://github.com/dabreegster/odjitter/blob/main/data/zones.geojson) 104 | representing zones that contains values matching the zone IDs in the 105 | OD data (the field containing zone IDs is specified with 106 | `--zone-name-key=InterZone` by default): 107 | 108 | ``` bash 109 | head -6 data/zones.geojson 110 | ``` 111 | 112 | { 113 | "type": "FeatureCollection", 114 | "name": "zones_min", 115 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 116 | "features": [ 117 | { "type": "Feature", "properties": { "InterZone": "S02001616", "Name": "Merchiston and Greenhill", "TotPop2011": 5018, "ResPop2011": 4730, "HHCnt2011": 2186, "StdAreaHa": 126.910911, "StdAreaKm2": 1.269109, "Shape_Leng": 9073.5402482000009, "Shape_Area": 1269109.10155 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.2040366, 55.9333372 ], [ -3.2036354, 55.9321624 ], [ -3.2024036, 55.9321874 ], [ -3.2019838, 55.9315586 ], [ -3.2005071, 55.9317411 ], [ -3.199902, 55.931113 ], [ -3.2033504, 55.9308279 ], [ -3.2056319, 55.9309507 ], [ -3.2094979, 55.9308666 ], [ -3.2109753, 55.9299985 ], [ -3.2107073, 55.9285904 ], [ -3.2124928, 55.927854 ], [ -3.2125633, 55.9264661 ], [ -3.2094928, 55.9265616 ], [ -3.212929, 55.9260741 ], [ -3.2130774, 55.9264384 ], [ -3.2183973, 55.9252709 ], [ -3.2208941, 55.925282 ], [ -3.2242732, 55.9258683 ], [ -3.2279975, 55.9277452 ], [ -3.2269867, 55.928489 ], [ -3.2267625, 55.9299817 ], [ -3.2254561, 55.9307854 ], [ -3.224148, 55.9300725 ], [ -3.2197791, 55.9315472 ], [ -3.2222706, 55.9339127 ], [ -3.2224909, 55.934809 ], [ -3.2197844, 55.9354692 ], [ -3.2204535, 55.936195 ], [ -3.218362, 55.9368806 ], [ -3.2165749, 55.937069 ], [ -3.215582, 55.9380761 ], [ -3.2124132, 55.9355465 ], [ -3.212774, 55.9347972 ], [ -3.2119068, 55.9341947 ], [ -3.210138, 55.9349668 ], [ -3.208051, 55.9347716 ], [ -3.2083105, 55.9364224 ], [ -3.2053546, 55.9381495 ], [ -3.2046077, 55.9395298 ], [ -3.20356, 55.9380951 ], [ -3.2024323, 55.936318 ], [ -3.2029121, 55.935831 ], [ -3.204832, 55.9357555 ], [ -3.2040366, 55.9333372 ] ] ] ] } }, 118 | 119 | 3. One or more [.geojson 120 | file](https://github.com/dabreegster/odjitter/blob/main/data/road_network.geojson) 121 | representing geographic entities (e.g. road networks) from which 122 | origin and destination points are sampled 123 | 124 | ``` bash 125 | head -6 data/road_network.geojson 126 | ``` 127 | 128 | { 129 | "type": "FeatureCollection", 130 | "name": "road_network_min", 131 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 132 | "features": [ 133 | { "type": "Feature", "properties": { "osm_id": "3468", "name": "Albyn Place", "highway": "tertiary", "waterway": null, "aerialway": null, "barrier": null, "man_made": null, "access": null, "bicycle": null, "service": null, "z_order": 4, "other_tags": "\"lit\"=>\"yes\",\"lanes\"=>\"3\",\"maxspeed\"=>\"20 mph\",\"sidewalk\"=>\"both\",\"lanes:forward\"=>\"2\",\"lanes:backward\"=>\"1\"" }, "geometry": { "type": "LineString", "coordinates": [ [ -3.207438, 55.9533584 ], [ -3.2065953, 55.9535098 ] ] } }, 134 | 135 | The `jitter` command requires you to set the maximum number of trips for 136 | all trips in the jittered result, with the argument 137 | \`disaggregation-threshold\`\`. A value of 1 will create a line for 138 | every trip in the dataset, a value above the maximum number of trips in 139 | the ‘all’ column in the OD data will result in a jittered dataset that 140 | has the same number of desire lines (the geographic representation of OD 141 | pairs) as in the input (50 in this case). 142 | 143 | With reference to the test data in this repo, you can run the `jitter` 144 | command line tool as follows: 145 | 146 | ``` bash 147 | odjitter jitter --od-csv-path data/od.csv \ 148 | --zones-path data/zones.geojson \ 149 | --subpoints-origins-path data/road_network.geojson \ 150 | --subpoints-destinations-path data/road_network.geojson \ 151 | --disaggregation-threshold 50 \ 152 | --output-path data/output_max50.geojson 153 | ``` 154 | 155 | Scraped 7 zones from data/zones.geojson 156 | Scraped 5073 subpoints from data/road_network.geojson 157 | Scraped 5073 subpoints from data/road_network.geojson 158 | Disaggregating OD data 159 | Wrote data/output_max50.geojson 160 | 161 | Try running it with a different `disaggregation-threshold` value (10 in 162 | the command below): 163 | 164 | ``` bash 165 | odjitter jitter --od-csv-path data/od.csv \ 166 | --zones-path data/zones.geojson \ 167 | --subpoints-origins-path data/road_network.geojson \ 168 | --subpoints-destinations-path data/road_network.geojson \ 169 | --disaggregation-threshold 10 \ 170 | --output-path data/output_max10.geojson 171 | ``` 172 | 173 | Scraped 7 zones from data/zones.geojson 174 | Scraped 5073 subpoints from data/road_network.geojson 175 | Scraped 5073 subpoints from data/road_network.geojson 176 | Disaggregating OD data 177 | Wrote data/output_max10.geojson 178 | 179 | You can run odjitter on OD datasets in which the features in the origins 180 | are different from the features in the destinations, e.g. if you have 181 | data on movement between residential areas and parks. However, you need 182 | to first combine the geographic dataset representing origins and the 183 | geographic destinations representing destinations into a single object. 184 | An example of this type of this is is demonstrated in the code chunk 185 | below. 186 | 187 | ``` bash 188 | odjitter jitter --od-csv-path data/od_destinations.csv \ 189 | --zones-path data/zones_combined.geojson \ 190 | --subpoints-origins-path data/road_network.geojson \ 191 | --subpoints-destinations-path data/road_network.geojson \ 192 | --disaggregation-threshold 50 \ 193 | --output-path data/output_destinations_differ_50.geojson 194 | ``` 195 | 196 | Scraped 9 zones from data/zones_combined.geojson 197 | Scraped 5073 subpoints from data/road_network.geojson 198 | Scraped 5073 subpoints from data/road_network.geojson 199 | Disaggregating OD data 200 | Wrote data/output_destinations_differ_50.geojson 201 | 202 | # Outputs 203 | 204 | The figure below shows the output of the `jitter` commands above 205 | visually, with the left image showing unjittered results with origins 206 | and destinations going to zone centroids (as in many if not most 207 | visualisations of desire lines between zones), the central image showing 208 | the result after setting `disaggregation-threshold` argument to 50, and 209 | the right hand figure showing the result after setting 210 | `disaggregation-threshold` to 10. 211 | 212 | You can call the Rust code from R, as illustrated by the code below 213 | which generates the datasets shown in the figures below. 214 | 215 | ``` r 216 | remotes::install_github("dabreegster/odjitter", subdir = "r") 217 | # Note: code to generate the visualisation below 218 | od = readr::read_csv("data/od.csv") 219 | zones = sf::read_sf("data/zones.geojson") 220 | network = sf::read_sf("data/road_network.geojson") 221 | od_sf = od::od_to_sf(od, zones) 222 | odjittered_max_50 = odjitter::jitter(od, zones, network, disaggregation_threshold = 50) 223 | odjittered_max_10 = odjitter::jitter(od, zones, network, disaggregation_threshold = 10) 224 | ``` 225 | 226 | ![Demonstration of the effect of the disaggregation threshold on the 227 | number of desire 228 | lines](README_files/figure-commonmark/thresholddemo-1.png) 229 | 230 | Note: `odjitter` uses a random number generator to sample points, so the 231 | output will change each time you run it, unless you set the `rng-seed`, 232 | as documented in the next section. 233 | 234 | The `subpoints-origins-path` and `subpoints-destinations-path` can be 235 | used to generate jittered desire lines that start from or go to 236 | particular points, defined in .geojson files. We will demonstrate this 237 | on a simple imaginary example: 238 | 239 | ``` bash 240 | head data/od_schools.csv 241 | ``` 242 | 243 | origin,destination,walk,bike,other,car 244 | S02001616,S02001616,232,8,70,0 245 | S02001620,S02001616,87,3,26,223 246 | S02001621,S02001616,80,3,24,250 247 | S02001622,S02001616,64,2,19,348 248 | S02001623,S02001616,52,2,15,464 249 | S02001656,S02001616,62,2,19,366 250 | S02001660,S02001616,77,3,23,266 251 | S02001616,S02001620,7,0,2,17 252 | S02001620,S02001620,18,1,5,0 253 | 254 | Set the origin, destination, and threshold keys (to car meaning that the 255 | max n. car trips per OD pair is 10 in this case) as follows: 256 | 257 | ``` bash 258 | odjitter jitter --od-csv-path data/od_schools.csv \ 259 | --zones-path data/zones.geojson \ 260 | --origin-key origin \ 261 | --destination-key destination \ 262 | --subpoints-origins-path data/road_network.geojson \ 263 | --subpoints-destinations-path data/schools.geojson \ 264 | --disaggregation-key car \ 265 | --disaggregation-threshold 10 \ 266 | --output-path output_max10_schools.geojson 267 | ``` 268 | 269 | Scraped 7 zones from data/zones.geojson 270 | Scraped 5073 subpoints from data/road_network.geojson 271 | Scraped 31 subpoints from data/schools.geojson 272 | Disaggregating OD data 273 | Wrote output_max10_schools.geojson 274 | 275 | You can also set weights associated with each origin and destination in 276 | the input data. The following example weights trips to schools 277 | proportional to the values in the ‘weight’ key for each imaginary data 278 | point represented in the `schools.geojson` object: 279 | 280 | ``` bash 281 | odjitter jitter --od-csv-path data/od_schools.csv \ 282 | --zones-path data/zones.geojson \ 283 | --origin-key origin \ 284 | --destination-key destination \ 285 | --subpoints-origins-path data/road_network.geojson \ 286 | --subpoints-destinations-path data/schools.geojson \ 287 | --disaggregation-key car \ 288 | --disaggregation-threshold 10 \ 289 | --weight-key-destinations weight \ 290 | --output-path output_max10_schools_with_weights.geojson 291 | ``` 292 | 293 | Scraped 7 zones from data/zones.geojson 294 | Scraped 5073 subpoints from data/road_network.geojson 295 | Scraped 31 subpoints from data/schools.geojson 296 | Disaggregating OD data 297 | Wrote output_max10_schools_with_weights.geojson 298 | 299 | # `disaggregate` OD data 300 | 301 | Sometimes it’s useful to convert aggregate OD datasets into movement 302 | data at the trip level, with one record per trip or stage. 303 | Microsumulation or agent-based modelling in transport simulation 304 | software such as [A/B Street](https://github.com/a-b-street/abstreet) is 305 | an example where disaggregate data may be needed. The `disaggregate` 306 | command does this full disaggregation work, as demonstrated below. 307 | 308 | ``` bash 309 | odjitter disaggregate --od-csv-path data/od.csv \ 310 | --zones-path data/zones.geojson \ 311 | --output-path output_individual.geojson 312 | ``` 313 | 314 | Scraped 7 zones from data/zones.geojson 315 | Disaggregating OD data 316 | Wrote output_individual.geojson 317 | 318 | ``` bash 319 | head output_individual.geojson 320 | rm output_individual.geojson 321 | ``` 322 | 323 | {"type":"FeatureCollection", "features":[ 324 | {"geometry":{"coordinates":[[-3.2263977926488985,55.92783397974489],[-3.2097949190090564,55.931894382403456]],"type":"LineString"},"properties":{"mode":"bus"},"type":"Feature"}, 325 | {"geometry":{"coordinates":[[-3.214452310499139,55.926362026835776],[-3.2099011140196207,55.93444441681924]],"type":"LineString"},"properties":{"mode":"bus"},"type":"Feature"}, 326 | {"geometry":{"coordinates":[[-3.2135400698085235,55.93035182421444],[-3.2244453899330097,55.9290303580713]],"type":"LineString"},"properties":{"mode":"bus"},"type":"Feature"}, 327 | {"geometry":{"coordinates":[[-3.226151458277428,55.92976711232548],[-3.213318278973036,55.93497592560621]],"type":"LineString"},"properties":{"mode":"bicycle"},"type":"Feature"}, 328 | {"geometry":{"coordinates":[[-3.2185705494212358,55.926226845455034],[-3.2115019430114167,55.93197469392582]],"type":"LineString"},"properties":{"mode":"bicycle"},"type":"Feature"}, 329 | {"geometry":{"coordinates":[[-3.2064406813843465,55.93266248325375],[-3.2190062133419635,55.92613966571992]],"type":"LineString"},"properties":{"mode":"car_driver"},"type":"Feature"}, 330 | {"geometry":{"coordinates":[[-3.226009373785913,55.9285088488262],[-3.2149550551179176,55.93495382922043]],"type":"LineString"},"properties":{"mode":"car_driver"},"type":"Feature"}, 331 | {"geometry":{"coordinates":[[-3.2152401192504443,55.932554427847144],[-3.214478335328521,55.933957525733355]],"type":"LineString"},"properties":{"mode":"car_driver"},"type":"Feature"}, 332 | {"geometry":{"coordinates":[[-3.218021802161658,55.92963564155289],[-3.22510485680737,55.92984949438051]],"type":"LineString"},"properties":{"mode":"car_driver"},"type":"Feature"}, 333 | 334 | # Details 335 | 336 | For full details on the arguments of each of `odjitter`’s subcommands 337 | can be viewed with the `--help` flag: 338 | 339 | ``` bash 340 | odjitter jitter --help 341 | odjitter disaggregate --help 342 | ``` 343 | 344 | odjitter-jitter 345 | Import raw data and build an activity model for a region 346 | 347 | USAGE: 348 | odjitter jitter [OPTIONS] --od-csv-path --zones-path --output-path --disaggregation-threshold 349 | 350 | OPTIONS: 351 | --deduplicate-pairs 352 | Prevent duplicate (origin, destination) pairs from appearing in the output. This may 353 | increase memory and runtime requirements. Note the duplication uses the floating point 354 | precision of the input data, and only consider geometry (not any properties) 355 | 356 | --destination-key 357 | Which column in the OD row specifies the zone where trips ends? [default: geo_code2] 358 | 359 | --disaggregation-key 360 | Which column in the OD row specifies the total number of trips to disaggregate? 361 | [default: all] 362 | 363 | --disaggregation-threshold 364 | What's the maximum number of trips per output OD row that's allowed? If an input OD row 365 | contains less than this, it will appear in the output without transformation. Otherwise, 366 | the input row is repeated until the sum matches the original value, but each output row 367 | obeys this maximum 368 | 369 | -h, --help 370 | Print help information 371 | 372 | --min-distance-meters 373 | Guarantee that jittered origin and destination points are at least this distance apart 374 | [default: 1.0] 375 | 376 | --od-csv-path 377 | The path to a CSV file with aggregated origin/destination data 378 | 379 | --origin-key 380 | Which column in the OD row specifies the zone where trips originate? [default: 381 | geo_code1] 382 | 383 | --output-path 384 | The path to a GeoJSON file where the output will be written 385 | 386 | --rng-seed 387 | By default, the output will be different every time the tool is run, based on a 388 | different random number generator seed. Specify this to get deterministic behavior, 389 | given the same input 390 | 391 | --subpoints-destinations-path 392 | The path to a GeoJSON file to use for sampling subpoints for destination zones. If this 393 | isn't specified, random points within each zone will be used instead 394 | 395 | --subpoints-origins-path 396 | The path to a GeoJSON file to use for sampling subpoints for origin zones. If this isn't 397 | specified, random points within each zone will be used instead 398 | 399 | --weight-key-destinations 400 | If specified, this column will be used to more frequently choose subpoints in 401 | `subpoints_destinations_path` with a higher weight value. Otherwise all subpoints will 402 | be equally likely to be chosen 403 | 404 | --weight-key-origins 405 | If specified, this column will be used to more frequently choose subpoints in 406 | `subpoints_origins_path` with a higher weight value. Otherwise all subpoints will be 407 | equally likely to be chosen 408 | 409 | --zone-name-key 410 | In the zones GeoJSON file, which property is the name of a zone [default: InterZone] 411 | 412 | --zones-path 413 | The path to a GeoJSON file with named zones 414 | odjitter-disaggregate 415 | Fully disaggregate input desire lines into output representing one trip each, with a `mode` column 416 | 417 | USAGE: 418 | odjitter disaggregate [OPTIONS] --od-csv-path --zones-path --output-path 419 | 420 | OPTIONS: 421 | --deduplicate-pairs 422 | Prevent duplicate (origin, destination) pairs from appearing in the output. This may 423 | increase memory and runtime requirements. Note the duplication uses the floating point 424 | precision of the input data, and only consider geometry (not any properties) 425 | 426 | --destination-key 427 | Which column in the OD row specifies the zone where trips ends? [default: geo_code2] 428 | 429 | -h, --help 430 | Print help information 431 | 432 | --min-distance-meters 433 | Guarantee that jittered origin and destination points are at least this distance apart 434 | [default: 1.0] 435 | 436 | --od-csv-path 437 | The path to a CSV file with aggregated origin/destination data 438 | 439 | --origin-key 440 | Which column in the OD row specifies the zone where trips originate? [default: 441 | geo_code1] 442 | 443 | --output-path 444 | The path to a GeoJSON file where the output will be written 445 | 446 | --rng-seed 447 | By default, the output will be different every time the tool is run, based on a 448 | different random number generator seed. Specify this to get deterministic behavior, 449 | given the same input 450 | 451 | --subpoints-destinations-path 452 | The path to a GeoJSON file to use for sampling subpoints for destination zones. If this 453 | isn't specified, random points within each zone will be used instead 454 | 455 | --subpoints-origins-path 456 | The path to a GeoJSON file to use for sampling subpoints for origin zones. If this isn't 457 | specified, random points within each zone will be used instead 458 | 459 | --weight-key-destinations 460 | If specified, this column will be used to more frequently choose subpoints in 461 | `subpoints_destinations_path` with a higher weight value. Otherwise all subpoints will 462 | be equally likely to be chosen 463 | 464 | --weight-key-origins 465 | If specified, this column will be used to more frequently choose subpoints in 466 | `subpoints_origins_path` with a higher weight value. Otherwise all subpoints will be 467 | equally likely to be chosen 468 | 469 | --zone-name-key 470 | In the zones GeoJSON file, which property is the name of a zone [default: InterZone] 471 | 472 | --zones-path 473 | The path to a GeoJSON file with named zones 474 | 475 | # Similar work 476 | 477 | The technique is implemented in the function 478 | [`od_jitter()`](https://itsleeds.github.io/od/reference/od_jitter.html) 479 | from the R package [`od`](https://itsleeds.github.io/od/index.html). The 480 | functionality contained in this repo is an extended and much faster 481 | implementation: according to our benchmarks on a large dataset it was 482 | around 1000 times faster than the R implementation. 483 | 484 | # References 485 | 486 |
487 | 488 |
489 | 490 | Lovelace, Robin, Rosa Félix, and Dustin Carlino. 2022. “Jittering: A 491 | Computationally Efficient Method for Generating Realistic Route Networks 492 | from Origin-Destination Data.” *Findings*, April, 33873. 493 | . 494 | 495 |
496 | 497 |
498 | -------------------------------------------------------------------------------- /README.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | format: gfm 3 | bibliography: references.bib 4 | --- 5 | 6 | ```{r eval=FALSE, echo=FALSE} 7 | # Build the paper: 8 | Rscript -e 'rmarkdown::render("README.Rmd")' 9 | ``` 10 | 11 | # odjitter 12 | 13 | NOTE: This project is deprecated. Please use [od2net](https://github.com/Urban-Analytics-Technology-Platform/od2net) directly to generate route networks from OD data. 14 | 15 | This repo contains the `odjitter` crate that implements a 'jittering' technique for pre-processing origin-destination (OD) data and an associated R interface package (see the [r](r/) subdirectory). 16 | We hope to support other languages in the future (see [issue #23](https://github.com/dabreegster/odjitter/issues/23)). 17 | 18 | ## What is jittering? 19 | 20 | Jittering is a method that takes OD data in a .csv file plus zones and geographic datasets representing trip start and end points in .geojson files and outputs geographic lines representing movement between the zones that can be stored as GeoJSON files. 21 | The name comes from jittering in a [data visualisation context](https://ggplot2-book.org/layers.html?q=noise#position), which refers to the addition of random noise to the location of points, preventing them overlapping. 22 | 23 | ## Why jitter? 24 | 25 | For a more detailed description of the method and an explanation of why it is useful, especially when modeling active modes that require dense active travel networks, see the paper [Jittering: A Computationally Efficient Method for Generating Realistic Route Networks from Origin-Destination Data](https://findingspress.org/article/33873-jittering-a-computationally-efficient-method-for-generating-realistic-route-networks-from-origin-destination-data) [@lovelace_jittering_2022b]. 26 | 27 | # Installation 28 | 29 | Install the package from the system command line as follows (you need to have installed and set-up [cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) first): 30 | 31 | ```bash 32 | cargo install --git https://github.com/dabreegster/odjitter 33 | ``` 34 | 35 | To check the package installation worked, you can run `odjitter` command without arguments. 36 | If it prints the following message congratulations, it works 🎉 37 | 38 | ```{r, engine='bash', error=TRUE} 39 | odjitter 40 | ``` 41 | 42 | As shown in the output above the `odjitter` command line tools has subcommands: `disaggregate` and `jitter`. 43 | The main difference between these commands is that `jitter` returns OD pairs representing multiple trips or fractions of a trip. 44 | `disaggregate`, by contrast, returns data representing single trips. 45 | 46 | ## Docker 47 | 48 | Alternatively, you can run through Docker: `docker run -t abstreet/odjitter `. See below for command line usage, or start with `help`. 49 | 50 | NOTE: There's no maintenance guarantee the Docker image has up-to-date changes from this repository. File an issue if you think the Docker version is out-of-date and you need something newer. 51 | 52 | (For maintainers only: to build and push a new version, `docker build -t odjitter . && docker tag odjitter abstreet/odjitter:latest && docker push abstreet/odjitter:latest`.) 53 | 54 | # `jitter` OD data 55 | 56 | To jitter OD data you need a minimum of three inputs, examples of which are provided in the [`data/` folder](https://github.com/dabreegster/odjitter/tree/main/data) of this repo, the first few lines of which are illustrated below: 57 | 58 | 1. A [.csv file](https://github.com/dabreegster/odjitter/blob/main/data/od.csv) containing OD data with two columns containing zone IDs (specified with `--origin-key=geo_code1 --destination-key=geo_code2` by default) and other columns representing trip counts: 59 | 60 | 61 | |geo_code1 |geo_code2 | all| from_home| train| bus| car_driver| car_passenger| bicycle| foot| other| 62 | |:---------|:---------|---:|---------:|-----:|---:|----------:|-------------:|-------:|----:|-----:| 63 | |S02001616 |S02001616 | 82| 0| 0| 3| 6| 0| 2| 71| 0| 64 | |S02001616 |S02001620 | 188| 0| 0| 42| 26| 3| 11| 105| 1| 65 | |S02001616 |S02001621 | 99| 0| 0| 13| 7| 3| 15| 61| 0| 66 | 67 | 2. A [.geojson file](https://github.com/dabreegster/odjitter/blob/main/data/zones.geojson) representing zones that contains values matching the zone IDs in the OD data (the field containing zone IDs is specified with `--zone-name-key=InterZone` by default): 68 | 69 | ```{bash} 70 | head -6 data/zones.geojson 71 | ``` 72 | 73 | 3. One or more [.geojson file](https://github.com/dabreegster/odjitter/blob/main/data/road_network.geojson) representing geographic entities (e.g. road networks) from which origin and destination points are sampled 74 | 75 | ```{bash} 76 | head -6 data/road_network.geojson 77 | ``` 78 | 79 | The `jitter` command requires you to set the maximum number of trips for all trips in the jittered result, with the argument `disaggregation-threshold``. 80 | A value of 1 will create a line for every trip in the dataset, a value above the maximum number of trips in the 'all' column in the OD data will result in a jittered dataset that has the same number of desire lines (the geographic representation of OD pairs) as in the input (50 in this case). 81 | 82 | With reference to the test data in this repo, you can run the `jitter` command line tool as follows: 83 | 84 | ```{bash} 85 | odjitter jitter --od-csv-path data/od.csv \ 86 | --zones-path data/zones.geojson \ 87 | --subpoints-origins-path data/road_network.geojson \ 88 | --subpoints-destinations-path data/road_network.geojson \ 89 | --disaggregation-threshold 50 \ 90 | --output-path data/output_max50.geojson 91 | ``` 92 | 93 | Try running it with a different `disaggregation-threshold` value (10 in the command below): 94 | 95 | ```{bash} 96 | odjitter jitter --od-csv-path data/od.csv \ 97 | --zones-path data/zones.geojson \ 98 | --subpoints-origins-path data/road_network.geojson \ 99 | --subpoints-destinations-path data/road_network.geojson \ 100 | --disaggregation-threshold 10 \ 101 | --output-path data/output_max10.geojson 102 | ``` 103 | 104 | You can run odjitter on OD datasets in which the features in the origins are different from the features in the destinations, e.g. if you have data on movement between residential areas and parks. 105 | However, you need to first combine the geographic dataset representing origins and the geographic destinations representing destinations into a single object. 106 | An example of this type of this is is demonstrated in the code chunk below. 107 | 108 | ```{bash} 109 | odjitter jitter --od-csv-path data/od_destinations.csv \ 110 | --zones-path data/zones_combined.geojson \ 111 | --subpoints-origins-path data/road_network.geojson \ 112 | --subpoints-destinations-path data/road_network.geojson \ 113 | --disaggregation-threshold 50 \ 114 | --output-path data/output_destinations_differ_50.geojson 115 | ``` 116 | 117 | # Outputs 118 | 119 | The figure below shows the output of the `jitter` commands above visually, with the left image showing unjittered results with origins and destinations going to zone centroids (as in many if not most visualisations of desire lines between zones), the central image showing the result after setting `disaggregation-threshold` argument to 50, and the right hand figure showing the result after setting `disaggregation-threshold` to 10. 120 | 121 | You can call the Rust code from R, as illustrated by the code below which generates the datasets shown in the figures below. 122 | 123 | ```{r, message=FALSE} 124 | #| echo: true 125 | remotes::install_github("dabreegster/odjitter", subdir = "r") 126 | # Note: code to generate the visualisation below 127 | od = readr::read_csv("data/od.csv") 128 | zones = sf::read_sf("data/zones.geojson") 129 | network = sf::read_sf("data/road_network.geojson") 130 | od_sf = od::od_to_sf(od, zones) 131 | odjittered_max_50 = odjitter::jitter(od, zones, network, disaggregation_threshold = 50) 132 | odjittered_max_10 = odjitter::jitter(od, zones, network, disaggregation_threshold = 10) 133 | ``` 134 | 135 | ```{r fig.width=8, fig.height=2, message=FALSE} 136 | #| echo: false 137 | #| label: thresholddemo 138 | #| fig-cap: "Demonstration of the effect of the disaggregation threshold on the number of desire lines" 139 | library(ggplot2) 140 | odjittered_long = rbind( 141 | od_sf |> dplyr::transmute(type = "Unjittered"), 142 | odjittered_max_50 |> dplyr::transmute(type = "--disaggregation-threshold 50"), 143 | odjittered_max_10 |> dplyr::transmute(type = "--disaggregation-threshold 10") 144 | ) 145 | # Convert type to ordered factor so that it is plotted in the correct order: 146 | odjittered_long$type = factor(odjittered_long$type, levels = c("Unjittered", "--disaggregation-threshold 50", "--disaggregation-threshold 10")) 147 | odjittered_long |> 148 | ggplot() + 149 | geom_sf() + 150 | geom_sf(data = zones, fill = NA, color = "grey") + 151 | geom_sf(data = network, fill = NA, color = "red") + 152 | facet_wrap(~type) + 153 | theme_void() 154 | ``` 155 | 156 | Note: `odjitter` uses a random number generator to sample points, so the output will change each time you run it, unless you set the `rng-seed`, as documented in the next section. 157 | 158 | The `subpoints-origins-path` and `subpoints-destinations-path` can be used to generate jittered desire lines that start from or go to particular points, defined in .geojson files. 159 | We will demonstrate this on a simple imaginary example: 160 | 161 | ```{bash} 162 | head data/od_schools.csv 163 | ``` 164 | 165 | Set the origin, destination, and threshold keys (to car meaning that the max n. car trips per OD pair is 10 in this case) as follows: 166 | 167 | ```{bash} 168 | odjitter jitter --od-csv-path data/od_schools.csv \ 169 | --zones-path data/zones.geojson \ 170 | --origin-key origin \ 171 | --destination-key destination \ 172 | --subpoints-origins-path data/road_network.geojson \ 173 | --subpoints-destinations-path data/schools.geojson \ 174 | --disaggregation-key car \ 175 | --disaggregation-threshold 10 \ 176 | --output-path output_max10_schools.geojson 177 | ``` 178 | 179 | You can also set weights associated with each origin and destination in the input data. 180 | The following example weights trips to schools proportional to the values in the 'weight' key for each imaginary data point represented in the `schools.geojson` object: 181 | 182 | ```{bash} 183 | odjitter jitter --od-csv-path data/od_schools.csv \ 184 | --zones-path data/zones.geojson \ 185 | --origin-key origin \ 186 | --destination-key destination \ 187 | --subpoints-origins-path data/road_network.geojson \ 188 | --subpoints-destinations-path data/schools.geojson \ 189 | --disaggregation-key car \ 190 | --disaggregation-threshold 10 \ 191 | --weight-key-destinations weight \ 192 | --output-path output_max10_schools_with_weights.geojson 193 | ``` 194 | 195 | # `disaggregate` OD data 196 | 197 | Sometimes it's useful to convert aggregate OD datasets into movement data at the trip level, with one record per trip or stage. 198 | Microsumulation or agent-based modelling in transport simulation software such as [A/B Street](https://github.com/a-b-street/abstreet) is an example where disaggregate data may be needed. 199 | The `disaggregate` command does this full disaggregation work, as demonstrated below. 200 | 201 | ```{bash} 202 | odjitter disaggregate --od-csv-path data/od.csv \ 203 | --zones-path data/zones.geojson \ 204 | --output-path output_individual.geojson 205 | ``` 206 | 207 | ```{bash} 208 | head output_individual.geojson 209 | rm output_individual.geojson 210 | ``` 211 | 212 | 213 | # Details 214 | 215 | For full details on the arguments of each of `odjitter`'s subcommands can be viewed with the `--help` flag: 216 | 217 | ```{bash} 218 | odjitter jitter --help 219 | odjitter disaggregate --help 220 | ``` 221 | 222 | # Similar work 223 | 224 | The technique is implemented in the function [`od_jitter()`](https://itsleeds.github.io/od/reference/od_jitter.html) from the R package [`od`](https://itsleeds.github.io/od/index.html). 225 | The functionality contained in this repo is an extended and much faster implementation: according to our benchmarks on a large dataset it was around 1000 times faster than the R implementation. 226 | 227 | 228 | # References 229 | 230 | ```{bash, echo=FALSE} 231 | rm output_max* 232 | ``` 233 | -------------------------------------------------------------------------------- /README_files/figure-commonmark/thresholddemo-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabreegster/odjitter/98a7a6e03bc54bc79d3b3abbde91b6f79173ff1a/README_files/figure-commonmark/thresholddemo-1.png -------------------------------------------------------------------------------- /README_files/figure-commonmark/thresholddemo-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabreegster/odjitter/98a7a6e03bc54bc79d3b3abbde91b6f79173ff1a/README_files/figure-commonmark/thresholddemo-2.png -------------------------------------------------------------------------------- /README_files/figure-commonmark/thresholddemo-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabreegster/odjitter/98a7a6e03bc54bc79d3b3abbde91b6f79173ff1a/README_files/figure-commonmark/thresholddemo-3.png -------------------------------------------------------------------------------- /data/od.csv: -------------------------------------------------------------------------------- 1 | geo_code1,geo_code2,all,from_home,train,bus,car_driver,car_passenger,bicycle,foot,other 2 | S02001616,S02001616,82,0,0,3,6,0,2,71,0 3 | S02001616,S02001620,188,0,0,42,26,3,11,105,1 4 | S02001616,S02001621,99,0,0,13,7,3,15,61,0 5 | S02001616,S02001622,228,0,1,92,23,2,38,71,1 6 | S02001616,S02001623,31,0,0,8,0,0,14,8,1 7 | S02001616,S02001656,56,0,0,23,5,1,7,20,0 8 | S02001616,S02001660,128,0,0,41,18,2,5,59,3 9 | S02001620,S02001616,71,0,0,15,2,1,6,47,0 10 | S02001620,S02001620,324,0,1,8,8,2,3,301,1 11 | S02001620,S02001621,156,0,0,7,3,1,11,134,0 12 | S02001620,S02001622,324,0,1,44,6,0,15,257,1 13 | S02001620,S02001623,61,0,0,5,2,0,5,47,2 14 | S02001620,S02001656,61,0,0,7,0,1,3,50,0 15 | S02001620,S02001660,133,0,2,13,5,0,2,109,2 16 | S02001621,S02001616,40,0,0,10,4,0,6,20,0 17 | S02001621,S02001620,151,0,0,7,2,0,7,133,2 18 | S02001621,S02001621,191,0,0,4,5,2,8,172,0 19 | S02001621,S02001622,356,0,0,36,9,2,23,286,0 20 | S02001621,S02001623,51,0,0,1,0,0,6,44,0 21 | S02001621,S02001656,34,0,1,13,2,0,1,17,0 22 | S02001621,S02001660,101,0,0,23,7,0,8,62,1 23 | S02001622,S02001616,23,0,0,7,3,3,2,7,1 24 | S02001622,S02001620,109,0,0,6,4,0,2,97,0 25 | S02001622,S02001621,134,0,0,12,3,0,1,118,0 26 | S02001622,S02001622,433,0,3,17,10,4,6,383,10 27 | S02001622,S02001623,64,0,0,2,1,0,2,59,0 28 | S02001622,S02001656,48,0,0,1,0,1,0,46,0 29 | S02001622,S02001660,97,0,1,14,7,0,7,68,0 30 | S02001623,S02001616,29,0,0,14,4,0,5,6,0 31 | S02001623,S02001620,93,0,0,19,5,1,7,60,1 32 | S02001623,S02001621,188,0,0,3,7,0,12,166,0 33 | S02001623,S02001622,275,0,0,22,6,1,9,236,1 34 | S02001623,S02001623,98,0,0,2,3,3,2,88,0 35 | S02001623,S02001656,26,0,0,6,3,1,1,15,0 36 | S02001623,S02001660,78,0,8,26,2,1,6,35,0 37 | S02001656,S02001616,12,0,0,6,3,0,0,3,0 38 | S02001656,S02001620,180,0,0,15,7,0,0,155,3 39 | S02001656,S02001621,52,0,0,12,6,0,4,30,0 40 | S02001656,S02001622,255,0,0,27,10,1,8,208,1 41 | S02001656,S02001623,24,0,0,3,4,1,2,14,0 42 | S02001656,S02001656,85,0,0,2,3,1,1,78,0 43 | S02001656,S02001660,129,0,2,7,6,0,4,110,0 44 | S02001660,S02001616,43,0,0,8,13,0,5,17,0 45 | S02001660,S02001620,336,0,2,18,20,2,6,286,2 46 | S02001660,S02001621,59,0,0,10,7,0,5,37,0 47 | S02001660,S02001622,326,0,2,67,18,4,13,220,2 48 | S02001660,S02001623,38,0,0,16,4,0,3,15,0 49 | S02001660,S02001656,105,0,0,8,17,2,3,74,1 50 | S02001660,S02001660,350,0,6,30,27,3,4,280,0 51 | -------------------------------------------------------------------------------- /data/od_destinations.csv: -------------------------------------------------------------------------------- 1 | geo_code1,geo_code2,all,from_home,train,bus,car_driver,car_passenger,bicycle,foot,other 2 | S02001616,DS02001616,82,0,0,3,6,0,2,71,0 3 | S02001616,DS02001620,188,0,0,42,26,3,11,105,1 4 | S02001616,DS02001620,99,0,0,13,7,3,15,61,0 5 | S02001616,DS02001616,228,0,1,92,23,2,38,71,1 6 | S02001616,DS02001616,31,0,0,8,0,0,14,8,1 7 | S02001616,DS02001616,56,0,0,23,5,1,7,20,0 8 | S02001616,DS02001620,128,0,0,41,18,2,5,59,3 9 | S02001620,DS02001616,71,0,0,15,2,1,6,47,0 10 | S02001620,DS02001620,324,0,1,8,8,2,3,301,1 11 | S02001620,DS02001620,156,0,0,7,3,1,11,134,0 12 | S02001620,DS02001620,324,0,1,44,6,0,15,257,1 13 | S02001620,DS02001620,61,0,0,5,2,0,5,47,2 14 | S02001620,DS02001620,61,0,0,7,0,1,3,50,0 15 | S02001620,DS02001616,133,0,2,13,5,0,2,109,2 16 | S02001621,DS02001616,40,0,0,10,4,0,6,20,0 17 | S02001621,DS02001616,151,0,0,7,2,0,7,133,2 18 | S02001621,DS02001616,191,0,0,4,5,2,8,172,0 19 | S02001621,DS02001620,356,0,0,36,9,2,23,286,0 20 | S02001621,DS02001616,51,0,0,1,0,0,6,44,0 21 | S02001621,DS02001620,34,0,1,13,2,0,1,17,0 22 | S02001621,DS02001616,101,0,0,23,7,0,8,62,1 23 | S02001622,DS02001616,23,0,0,7,3,3,2,7,1 24 | S02001622,DS02001620,109,0,0,6,4,0,2,97,0 25 | S02001622,DS02001620,134,0,0,12,3,0,1,118,0 26 | S02001622,DS02001616,433,0,3,17,10,4,6,383,10 27 | S02001622,DS02001616,64,0,0,2,1,0,2,59,0 28 | S02001622,DS02001616,48,0,0,1,0,1,0,46,0 29 | S02001622,DS02001620,97,0,1,14,7,0,7,68,0 30 | S02001623,DS02001616,29,0,0,14,4,0,5,6,0 31 | S02001623,DS02001616,93,0,0,19,5,1,7,60,1 32 | S02001623,DS02001616,188,0,0,3,7,0,12,166,0 33 | S02001623,DS02001616,275,0,0,22,6,1,9,236,1 34 | S02001623,DS02001616,98,0,0,2,3,3,2,88,0 35 | S02001623,DS02001620,26,0,0,6,3,1,1,15,0 36 | S02001623,DS02001616,78,0,8,26,2,1,6,35,0 37 | S02001656,DS02001620,12,0,0,6,3,0,0,3,0 38 | S02001656,DS02001620,180,0,0,15,7,0,0,155,3 39 | S02001656,DS02001620,52,0,0,12,6,0,4,30,0 40 | S02001656,DS02001616,255,0,0,27,10,1,8,208,1 41 | S02001656,DS02001620,24,0,0,3,4,1,2,14,0 42 | S02001656,DS02001616,85,0,0,2,3,1,1,78,0 43 | S02001656,DS02001620,129,0,2,7,6,0,4,110,0 44 | S02001660,DS02001616,43,0,0,8,13,0,5,17,0 45 | S02001660,DS02001616,336,0,2,18,20,2,6,286,2 46 | S02001660,DS02001620,59,0,0,10,7,0,5,37,0 47 | S02001660,DS02001616,326,0,2,67,18,4,13,220,2 48 | S02001660,DS02001620,38,0,0,16,4,0,3,15,0 49 | S02001660,DS02001620,105,0,0,8,17,2,3,74,1 50 | S02001660,DS02001616,350,0,6,30,27,3,4,280,0 51 | -------------------------------------------------------------------------------- /data/od_schools.csv: -------------------------------------------------------------------------------- 1 | origin,destination,walk,bike,other,car 2 | S02001616,S02001616,232,8,70,0 3 | S02001620,S02001616,87,3,26,223 4 | S02001621,S02001616,80,3,24,250 5 | S02001622,S02001616,64,2,19,348 6 | S02001623,S02001616,52,2,15,464 7 | S02001656,S02001616,62,2,19,366 8 | S02001660,S02001616,77,3,23,266 9 | S02001616,S02001620,7,0,2,17 10 | S02001620,S02001620,18,1,5,0 11 | S02001621,S02001620,9,0,3,10 12 | S02001622,S02001620,9,0,3,11 13 | S02001623,S02001620,5,0,2,26 14 | S02001656,S02001620,9,0,3,11 15 | S02001660,S02001620,9,0,3,10 16 | S02001616,S02001621,51,2,15,158 17 | S02001620,S02001621,73,2,22,84 18 | S02001621,S02001621,147,5,44,0 19 | S02001622,S02001621,73,2,22,85 20 | S02001623,S02001621,55,2,17,141 21 | S02001656,S02001621,56,2,17,138 22 | S02001660,S02001621,49,2,15,169 23 | S02001616,S02001622,0,0,0,2 24 | S02001620,S02001622,1,0,0,1 25 | S02001621,S02001622,1,0,0,1 26 | S02001622,S02001622,1,0,0,0 27 | S02001623,S02001622,1,0,0,1 28 | S02001656,S02001622,1,0,0,1 29 | S02001660,S02001622,0,0,0,1 30 | S02001616,S02001623,2,0,1,18 31 | S02001620,S02001623,3,0,1,13 32 | S02001621,S02001623,3,0,1,9 33 | S02001622,S02001623,3,0,1,9 34 | S02001623,S02001623,9,0,3,0 35 | S02001656,S02001623,3,0,1,13 36 | S02001660,S02001623,2,0,1,18 37 | -------------------------------------------------------------------------------- /data/schools.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "name": "schools", 4 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 5 | "features": [ 6 | { "type": "Feature", "properties": { "id": "4982227", "weight": 2562.0 }, "geometry": { "type": "Point", "coordinates": [ -3.200137126772652, 55.936629811732757 ] } }, 7 | { "type": "Feature", "properties": { "id": "4990581", "weight": 4853.0 }, "geometry": { "type": "Point", "coordinates": [ -3.217489469502242, 55.930059117314109 ] } }, 8 | { "type": "Feature", "properties": { "id": "5038806", "weight": 844.0 }, "geometry": { "type": "Point", "coordinates": [ -3.224854846692416, 55.958006332958313 ] } }, 9 | { "type": "Feature", "properties": { "id": "5648757", "weight": 4209.0 }, "geometry": { "type": "Point", "coordinates": [ -3.194495965796563, 55.946018501872388 ] } }, 10 | { "type": "Feature", "properties": { "id": "23213650", "weight": 4652.0 }, "geometry": { "type": "Point", "coordinates": [ -3.2271901677683, 55.953951952413576 ] } }, 11 | { "type": "Feature", "properties": { "id": "23608191", "weight": 422.0 }, "geometry": { "type": "Point", "coordinates": [ -3.210468679547871, 55.937316010583956 ] } }, 12 | { "type": "Feature", "properties": { "id": "23608284", "weight": 2879.0 }, "geometry": { "type": "Point", "coordinates": [ -3.220080890889042, 55.928686159733502 ] } }, 13 | { "type": "Feature", "properties": { "id": "24148749", "weight": 2379.0 }, "geometry": { "type": "Point", "coordinates": [ -3.229702731379482, 55.93999106384809 ] } }, 14 | { "type": "Feature", "properties": { "id": "26382384", "weight": 1121.0 }, "geometry": { "type": "Point", "coordinates": [ -3.201596787787371, 55.936972846891059 ] } }, 15 | { "type": "Feature", "properties": { "id": "26601887", "weight": 658.0 }, "geometry": { "type": "Point", "coordinates": [ -3.204512688995408, 55.929291585848389 ] } }, 16 | { "type": "Feature", "properties": { "id": "28791842", "weight": 1447.0 }, "geometry": { "type": "Point", "coordinates": [ -3.158853697309511, 55.930961262420396 ] } }, 17 | { "type": "Feature", "properties": { "id": "29554133", "weight": 867.0 }, "geometry": { "type": "Point", "coordinates": [ -3.180959614228994, 55.927098575090852 ] } }, 18 | { "type": "Feature", "properties": { "id": "40736806", "weight": 1554.0 }, "geometry": { "type": "Point", "coordinates": [ -3.202540868021173, 55.929059708044768 ] } }, 19 | { "type": "Feature", "properties": { "id": "41908989", "weight": 299.0 }, "geometry": { "type": "Point", "coordinates": [ -3.177277893093219, 55.951718490801369 ] } }, 20 | { "type": "Feature", "properties": { "id": "42248840", "weight": 45.0 }, "geometry": { "type": "Point", "coordinates": [ -3.185107078709511, 55.949134134421271 ] } }, 21 | { "type": "Feature", "properties": { "id": "55153094", "weight": 475.0 }, "geometry": { "type": "Point", "coordinates": [ -3.187666427888981, 55.938377788540421 ] } }, 22 | { "type": "Feature", "properties": { "id": "61319556", "weight": 117.0 }, "geometry": { "type": "Point", "coordinates": [ -3.166402291779167, 55.925456290811219 ] } }, 23 | { "type": "Feature", "properties": { "id": "90488962", "weight": 607.0 }, "geometry": { "type": "Point", "coordinates": [ -3.206475215751834, 55.943396834113393 ] } }, 24 | { "type": "Feature", "properties": { "id": "90608205", "weight": 531.0 }, "geometry": { "type": "Point", "coordinates": [ -3.169838251803213, 55.956134128142224 ] } }, 25 | { "type": "Feature", "properties": { "id": "96097647", "weight": 5524.0 }, "geometry": { "type": "Point", "coordinates": [ -3.232223578580274, 55.950563870575856 ] } }, 26 | { "type": "Feature", "properties": { "id": "102209118", "weight": 181.0 }, "geometry": { "type": "Point", "coordinates": [ -3.176298934825939, 55.939876857461904 ] } }, 27 | { "type": "Feature", "properties": { "id": "106346427", "weight": 397.0 }, "geometry": { "type": "Point", "coordinates": [ -3.223070968391445, 55.942144572893731 ] } }, 28 | { "type": "Feature", "properties": { "id": "184267411", "weight": 459.0 }, "geometry": { "type": "Point", "coordinates": [ -3.207184087972836, 55.940561201909553 ] } }, 29 | { "type": "Feature", "properties": { "id": "189541453", "weight": 1147.0 }, "geometry": { "type": "Point", "coordinates": [ -3.235913675984021, 55.948012096778534 ] } }, 30 | { "type": "Feature", "properties": { "id": "210367694", "weight": 66.0 }, "geometry": { "type": "Point", "coordinates": [ -3.206324141096306, 55.928744341272456 ] } }, 31 | { "type": "Feature", "properties": { "id": "237034355", "weight": 890.0 }, "geometry": { "type": "Point", "coordinates": [ -3.214744480244675, 55.940813016167695 ] } }, 32 | { "type": "Feature", "properties": { "id": "277205646", "weight": 25.0 }, "geometry": { "type": "Point", "coordinates": [ -3.228833508163836, 55.953738868421908 ] } }, 33 | { "type": "Feature", "properties": { "id": "317005844", "weight": 968.0 }, "geometry": { "type": "Point", "coordinates": [ -3.223600038845251, 55.931524174694133 ] } }, 34 | { "type": "Feature", "properties": { "id": "333398361", "weight": 500.0 }, "geometry": { "type": "Point", "coordinates": [ -3.227789449160034, 55.931735923727565 ] } }, 35 | { "type": "Feature", "properties": { "id": "440492889", "weight": 126.0 }, "geometry": { "type": "Point", "coordinates": [ -3.211196229874437, 55.937011979969547 ] } }, 36 | { "type": "Feature", "properties": { "id": "655975021", "weight": 694.0 }, "geometry": { "type": "Point", "coordinates": [ -3.197998070379549, 55.943466952003654 ] } } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /data/zones.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "name": "zones_min", 4 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 5 | "features": [ 6 | { "type": "Feature", "properties": { "InterZone": "S02001616", "Name": "Merchiston and Greenhill", "TotPop2011": 5018, "ResPop2011": 4730, "HHCnt2011": 2186, "StdAreaHa": 126.910911, "StdAreaKm2": 1.269109, "Shape_Leng": 9073.5402482000009, "Shape_Area": 1269109.10155 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.2040366, 55.9333372 ], [ -3.2036354, 55.9321624 ], [ -3.2024036, 55.9321874 ], [ -3.2019838, 55.9315586 ], [ -3.2005071, 55.9317411 ], [ -3.199902, 55.931113 ], [ -3.2033504, 55.9308279 ], [ -3.2056319, 55.9309507 ], [ -3.2094979, 55.9308666 ], [ -3.2109753, 55.9299985 ], [ -3.2107073, 55.9285904 ], [ -3.2124928, 55.927854 ], [ -3.2125633, 55.9264661 ], [ -3.2094928, 55.9265616 ], [ -3.212929, 55.9260741 ], [ -3.2130774, 55.9264384 ], [ -3.2183973, 55.9252709 ], [ -3.2208941, 55.925282 ], [ -3.2242732, 55.9258683 ], [ -3.2279975, 55.9277452 ], [ -3.2269867, 55.928489 ], [ -3.2267625, 55.9299817 ], [ -3.2254561, 55.9307854 ], [ -3.224148, 55.9300725 ], [ -3.2197791, 55.9315472 ], [ -3.2222706, 55.9339127 ], [ -3.2224909, 55.934809 ], [ -3.2197844, 55.9354692 ], [ -3.2204535, 55.936195 ], [ -3.218362, 55.9368806 ], [ -3.2165749, 55.937069 ], [ -3.215582, 55.9380761 ], [ -3.2124132, 55.9355465 ], [ -3.212774, 55.9347972 ], [ -3.2119068, 55.9341947 ], [ -3.210138, 55.9349668 ], [ -3.208051, 55.9347716 ], [ -3.2083105, 55.9364224 ], [ -3.2053546, 55.9381495 ], [ -3.2046077, 55.9395298 ], [ -3.20356, 55.9380951 ], [ -3.2024323, 55.936318 ], [ -3.2029121, 55.935831 ], [ -3.204832, 55.9357555 ], [ -3.2040366, 55.9333372 ] ] ] ] } }, 7 | { "type": "Feature", "properties": { "InterZone": "S02001620", "Name": "Tollcross", "TotPop2011": 6233, "ResPop2011": 5502, "HHCnt2011": 2957, "StdAreaHa": 63.303879, "StdAreaKm2": 0.633036, "Shape_Leng": 5898.5141959000002, "Shape_Area": 633038.76668300002 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.2052525, 55.9502007 ], [ -3.2032608, 55.949096 ], [ -3.2028665, 55.9482553 ], [ -3.2012459, 55.9476331 ], [ -3.2011897, 55.9468519 ], [ -3.2000373, 55.946351 ], [ -3.1979054, 55.946794 ], [ -3.1976403, 55.9459879 ], [ -3.1965526, 55.9460254 ], [ -3.1964383, 55.9454335 ], [ -3.2001162, 55.9447598 ], [ -3.2005496, 55.9442794 ], [ -3.2000394, 55.943314 ], [ -3.2019769, 55.9428009 ], [ -3.201605, 55.9420004 ], [ -3.2033302, 55.942042 ], [ -3.2039421, 55.9404939 ], [ -3.2044523, 55.9418177 ], [ -3.2091262, 55.9410239 ], [ -3.2109932, 55.941338 ], [ -3.2085542, 55.9421886 ], [ -3.2097296, 55.9426766 ], [ -3.2099716, 55.9438623 ], [ -3.2088338, 55.9441577 ], [ -3.2094254, 55.9453997 ], [ -3.2107189, 55.9458872 ], [ -3.2122399, 55.9448569 ], [ -3.2129972, 55.9460355 ], [ -3.2162396, 55.9462911 ], [ -3.2152857, 55.9470193 ], [ -3.2080646, 55.9501183 ], [ -3.2068929, 55.9498185 ], [ -3.2052525, 55.9502007 ] ] ] ] } }, 8 | { "type": "Feature", "properties": { "InterZone": "S02001621", "Name": "Meadows and Southside", "TotPop2011": 6556, "ResPop2011": 5888, "HHCnt2011": 2653, "StdAreaHa": 91.921185, "StdAreaKm2": 0.919211, "Shape_Leng": 7123.2063949000003, "Shape_Area": 919211.84024100006 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.1869983, 55.9456595 ], [ -3.186045, 55.9444135 ], [ -3.1847027, 55.9436968 ], [ -3.1806737, 55.9440848 ], [ -3.1793587, 55.9429652 ], [ -3.1801618, 55.9419961 ], [ -3.1784201, 55.9417446 ], [ -3.1800522, 55.9399754 ], [ -3.1783862, 55.9401782 ], [ -3.1775032, 55.9393349 ], [ -3.178783, 55.9384599 ], [ -3.1781716, 55.9372632 ], [ -3.1788169, 55.9377858 ], [ -3.1808806, 55.9377391 ], [ -3.1812858, 55.9388117 ], [ -3.1826149, 55.9400138 ], [ -3.1827103, 55.9405161 ], [ -3.1856037, 55.9398504 ], [ -3.1880464, 55.9396292 ], [ -3.1901138, 55.9396991 ], [ -3.195452, 55.9404473 ], [ -3.1951522, 55.9395516 ], [ -3.1982982, 55.9392965 ], [ -3.2009096, 55.9388398 ], [ -3.2023573, 55.9381407 ], [ -3.20356, 55.9380951 ], [ -3.2046077, 55.9395298 ], [ -3.2039421, 55.9404939 ], [ -3.2033302, 55.942042 ], [ -3.201605, 55.9420004 ], [ -3.2019769, 55.9428009 ], [ -3.2000394, 55.943314 ], [ -3.2005496, 55.9442794 ], [ -3.2001162, 55.9447598 ], [ -3.1964383, 55.9454335 ], [ -3.1965526, 55.9460254 ], [ -3.1948333, 55.9468687 ], [ -3.1932024, 55.9464263 ], [ -3.1920134, 55.9452517 ], [ -3.1893563, 55.9457985 ], [ -3.1878112, 55.9465861 ], [ -3.1869983, 55.9456595 ] ] ] ] } }, 9 | { "type": "Feature", "properties": { "InterZone": "S02001622", "Name": "Old Town, Princes Street and Leith Street", "TotPop2011": 5651, "ResPop2011": 4508, "HHCnt2011": 2586, "StdAreaHa": 125.111221, "StdAreaKm2": 1.251113, "Shape_Leng": 7540.1286719, "Shape_Area": 1251112.1593800001 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.1845072, 55.9579392 ], [ -3.183012, 55.9573704 ], [ -3.182731, 55.9564198 ], [ -3.1846794, 55.9551881 ], [ -3.1855039, 55.9542504 ], [ -3.1853834, 55.9530698 ], [ -3.1817339, 55.9531318 ], [ -3.1808038, 55.9515144 ], [ -3.1821642, 55.9512869 ], [ -3.1822507, 55.9501493 ], [ -3.1813229, 55.9492539 ], [ -3.1821767, 55.9483739 ], [ -3.1860519, 55.9472119 ], [ -3.1856965, 55.9466286 ], [ -3.1869361, 55.9462441 ], [ -3.1869983, 55.9456595 ], [ -3.1878112, 55.9465861 ], [ -3.1893563, 55.9457985 ], [ -3.1920134, 55.9452517 ], [ -3.1932024, 55.9464263 ], [ -3.1948333, 55.9468687 ], [ -3.1965526, 55.9460254 ], [ -3.1976403, 55.9459879 ], [ -3.1979054, 55.946794 ], [ -3.2000373, 55.946351 ], [ -3.2011897, 55.9468519 ], [ -3.2012459, 55.9476331 ], [ -3.2028665, 55.9482553 ], [ -3.2032608, 55.949096 ], [ -3.2052525, 55.9502007 ], [ -3.2027425, 55.95097 ], [ -3.2026251, 55.9518337 ], [ -3.2012224, 55.952045 ], [ -3.2004019, 55.9529605 ], [ -3.1999696, 55.9535001 ], [ -3.1970063, 55.9541835 ], [ -3.1923543, 55.9548941 ], [ -3.1922015, 55.9555379 ], [ -3.1935939, 55.9560276 ], [ -3.1935992, 55.9567194 ], [ -3.1902117, 55.9570813 ], [ -3.1891891, 55.9575796 ], [ -3.1855592, 55.9580764 ], [ -3.1845072, 55.9579392 ] ] ] ] } }, 10 | { "type": "Feature", "properties": { "InterZone": "S02001623", "Name": "Canongate, Southside and Dumbiedykes", "TotPop2011": 5915, "ResPop2011": 5125, "HHCnt2011": 2894, "StdAreaHa": 281.293243, "StdAreaKm2": 2.812933, "Shape_Leng": 9388.3575805, "Shape_Area": 2812932.4399799998 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.1578199, 55.9546165 ], [ -3.1557118, 55.9542769 ], [ -3.1532431, 55.9531229 ], [ -3.1518734, 55.9528392 ], [ -3.1499866, 55.9518595 ], [ -3.150062, 55.9500797 ], [ -3.1490698, 55.9474023 ], [ -3.1492326, 55.9458733 ], [ -3.1487247, 55.9454827 ], [ -3.1503687, 55.9431491 ], [ -3.149873, 55.9420935 ], [ -3.1511271, 55.9417313 ], [ -3.1525876, 55.9407742 ], [ -3.1553616, 55.940919 ], [ -3.1573275, 55.9402716 ], [ -3.1599207, 55.9397261 ], [ -3.1618362, 55.9395373 ], [ -3.1619513, 55.9391049 ], [ -3.1597452, 55.9386764 ], [ -3.1613867, 55.9384273 ], [ -3.1647598, 55.9387999 ], [ -3.1662566, 55.9385341 ], [ -3.1682987, 55.937769 ], [ -3.1685954, 55.9385839 ], [ -3.1679367, 55.9400997 ], [ -3.1695321, 55.9409741 ], [ -3.1751664, 55.94252 ], [ -3.1774207, 55.9429556 ], [ -3.1784745, 55.9423178 ], [ -3.1801618, 55.9419961 ], [ -3.1793587, 55.9429652 ], [ -3.1806737, 55.9440848 ], [ -3.1847027, 55.9436968 ], [ -3.186045, 55.9444135 ], [ -3.1869983, 55.9456595 ], [ -3.1869361, 55.9462441 ], [ -3.1856965, 55.9466286 ], [ -3.1860519, 55.9472119 ], [ -3.1821767, 55.9483739 ], [ -3.1813229, 55.9492539 ], [ -3.1822507, 55.9501493 ], [ -3.1821642, 55.9512869 ], [ -3.1808038, 55.9515144 ], [ -3.1817339, 55.9531318 ], [ -3.179976, 55.9532564 ], [ -3.1779749, 55.9538057 ], [ -3.1739804, 55.9546434 ], [ -3.1735195, 55.9526441 ], [ -3.1723974, 55.9515496 ], [ -3.1721942, 55.950653 ], [ -3.1705658, 55.9508212 ], [ -3.1635581, 55.9521005 ], [ -3.1621749, 55.9524371 ], [ -3.1578199, 55.9546165 ] ] ] ] } }, 11 | { "type": "Feature", "properties": { "InterZone": "S02001656", "Name": "New Town West", "TotPop2011": 3569, "ResPop2011": 3558, "HHCnt2011": 1934, "StdAreaHa": 51.070007, "StdAreaKm2": 0.510698, "Shape_Leng": 5681.8274259, "Shape_Area": 510700.06846099999 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.203484, 55.9582677 ], [ -3.2023597, 55.9577395 ], [ -3.1979293, 55.9584565 ], [ -3.197652, 55.9574066 ], [ -3.1954201, 55.9574381 ], [ -3.1935992, 55.9567194 ], [ -3.1935939, 55.9560276 ], [ -3.1922015, 55.9555379 ], [ -3.1923543, 55.9548941 ], [ -3.1970063, 55.9541835 ], [ -3.1999696, 55.9535001 ], [ -3.2004019, 55.9529605 ], [ -3.2014907, 55.9536041 ], [ -3.2013262, 55.9543622 ], [ -3.2034478, 55.9540899 ], [ -3.2093543, 55.9529629 ], [ -3.2090229, 55.9523064 ], [ -3.2094965, 55.9512905 ], [ -3.2105804, 55.9511988 ], [ -3.2135413, 55.952167 ], [ -3.2141704, 55.9528257 ], [ -3.2098019, 55.954477 ], [ -3.2091422, 55.9559211 ], [ -3.2075657, 55.9564749 ], [ -3.207787, 55.9574073 ], [ -3.2058846, 55.9586722 ], [ -3.203484, 55.9582677 ] ] ] ] } }, 12 | { "type": "Feature", "properties": { "InterZone": "S02001660", "Name": "Deans Village", "TotPop2011": 6021, "ResPop2011": 5899, "HHCnt2011": 3184, "StdAreaHa": 149.389344, "StdAreaKm2": 1.493893, "Shape_Leng": 8154.0464244000004, "Shape_Area": 1493893.4591000001 }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.2149544, 55.9538243 ], [ -3.2141704, 55.9528257 ], [ -3.2135413, 55.952167 ], [ -3.2105804, 55.9511988 ], [ -3.2094965, 55.9512905 ], [ -3.2090229, 55.9523064 ], [ -3.2093543, 55.9529629 ], [ -3.2034478, 55.9540899 ], [ -3.2013262, 55.9543622 ], [ -3.2014907, 55.9536041 ], [ -3.2004019, 55.9529605 ], [ -3.2012224, 55.952045 ], [ -3.2026251, 55.9518337 ], [ -3.2027425, 55.95097 ], [ -3.2052525, 55.9502007 ], [ -3.2068929, 55.9498185 ], [ -3.2080646, 55.9501183 ], [ -3.2152857, 55.9470193 ], [ -3.2162396, 55.9462911 ], [ -3.2175997, 55.9454788 ], [ -3.2225062, 55.9445555 ], [ -3.2318615, 55.9424485 ], [ -3.2334962, 55.944074 ], [ -3.2347312, 55.9443923 ], [ -3.2336754, 55.9452282 ], [ -3.2354394, 55.9458125 ], [ -3.2356809, 55.9463582 ], [ -3.2335851, 55.9484278 ], [ -3.233993, 55.9491695 ], [ -3.2297559, 55.9508921 ], [ -3.2317092, 55.9523041 ], [ -3.2260953, 55.9531299 ], [ -3.221834, 55.9541067 ], [ -3.2220092, 55.9550933 ], [ -3.2206833, 55.9537588 ], [ -3.2174054, 55.9538451 ], [ -3.2163873, 55.9550861 ], [ -3.2149544, 55.9538243 ] ] ] ] } } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /data/zones_combined.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "name": "zones_combined", 4 | "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, 5 | "features": [ 6 | { "type": "Feature", "properties": { "InterZone": "S02001616" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.2040366, 55.9333372 ], [ -3.2036354, 55.9321624 ], [ -3.2024036, 55.9321874 ], [ -3.2019838, 55.9315586 ], [ -3.2005071, 55.9317411 ], [ -3.199902, 55.931113 ], [ -3.2033504, 55.9308279 ], [ -3.2056319, 55.9309507 ], [ -3.2094979, 55.9308666 ], [ -3.2109753, 55.9299985 ], [ -3.2107073, 55.9285904 ], [ -3.2124928, 55.927854 ], [ -3.2125633, 55.9264661 ], [ -3.2094928, 55.9265616 ], [ -3.212929, 55.9260741 ], [ -3.2130774, 55.9264384 ], [ -3.2183973, 55.9252709 ], [ -3.2208941, 55.925282 ], [ -3.2242732, 55.9258683 ], [ -3.2279975, 55.9277452 ], [ -3.2269867, 55.928489 ], [ -3.2267625, 55.9299817 ], [ -3.2254561, 55.9307854 ], [ -3.224148, 55.9300725 ], [ -3.2197791, 55.9315472 ], [ -3.2222706, 55.9339127 ], [ -3.2224909, 55.934809 ], [ -3.2197844, 55.9354692 ], [ -3.2204535, 55.936195 ], [ -3.218362, 55.9368806 ], [ -3.2165749, 55.937069 ], [ -3.215582, 55.9380761 ], [ -3.2124132, 55.9355465 ], [ -3.212774, 55.9347972 ], [ -3.2119068, 55.9341947 ], [ -3.210138, 55.9349668 ], [ -3.208051, 55.9347716 ], [ -3.2083105, 55.9364224 ], [ -3.2053546, 55.9381495 ], [ -3.2046077, 55.9395298 ], [ -3.20356, 55.9380951 ], [ -3.2024323, 55.936318 ], [ -3.2029121, 55.935831 ], [ -3.204832, 55.9357555 ], [ -3.2040366, 55.9333372 ] ] ] ] } }, 7 | { "type": "Feature", "properties": { "InterZone": "S02001620" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.2052525, 55.9502007 ], [ -3.2032608, 55.949096 ], [ -3.2028665, 55.9482553 ], [ -3.2012459, 55.9476331 ], [ -3.2011897, 55.9468519 ], [ -3.2000373, 55.946351 ], [ -3.1979054, 55.946794 ], [ -3.1976403, 55.9459879 ], [ -3.1965526, 55.9460254 ], [ -3.1964383, 55.9454335 ], [ -3.2001162, 55.9447598 ], [ -3.2005496, 55.9442794 ], [ -3.2000394, 55.943314 ], [ -3.2019769, 55.9428009 ], [ -3.201605, 55.9420004 ], [ -3.2033302, 55.942042 ], [ -3.2039421, 55.9404939 ], [ -3.2044523, 55.9418177 ], [ -3.2091262, 55.9410239 ], [ -3.2109932, 55.941338 ], [ -3.2085542, 55.9421886 ], [ -3.2097296, 55.9426766 ], [ -3.2099716, 55.9438623 ], [ -3.2088338, 55.9441577 ], [ -3.2094254, 55.9453997 ], [ -3.2107189, 55.9458872 ], [ -3.2122399, 55.9448569 ], [ -3.2129972, 55.9460355 ], [ -3.2162396, 55.9462911 ], [ -3.2152857, 55.9470193 ], [ -3.2080646, 55.9501183 ], [ -3.2068929, 55.9498185 ], [ -3.2052525, 55.9502007 ] ] ] ] } }, 8 | { "type": "Feature", "properties": { "InterZone": "S02001621" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.1869983, 55.9456595 ], [ -3.186045, 55.9444135 ], [ -3.1847027, 55.9436968 ], [ -3.1806737, 55.9440848 ], [ -3.1793587, 55.9429652 ], [ -3.1801618, 55.9419961 ], [ -3.1784201, 55.9417446 ], [ -3.1800522, 55.9399754 ], [ -3.1783862, 55.9401782 ], [ -3.1775032, 55.9393349 ], [ -3.178783, 55.9384599 ], [ -3.1781716, 55.9372632 ], [ -3.1788169, 55.9377858 ], [ -3.1808806, 55.9377391 ], [ -3.1812858, 55.9388117 ], [ -3.1826149, 55.9400138 ], [ -3.1827103, 55.9405161 ], [ -3.1856037, 55.9398504 ], [ -3.1880464, 55.9396292 ], [ -3.1901138, 55.9396991 ], [ -3.195452, 55.9404473 ], [ -3.1951522, 55.9395516 ], [ -3.1982982, 55.9392965 ], [ -3.2009096, 55.9388398 ], [ -3.2023573, 55.9381407 ], [ -3.20356, 55.9380951 ], [ -3.2046077, 55.9395298 ], [ -3.2039421, 55.9404939 ], [ -3.2033302, 55.942042 ], [ -3.201605, 55.9420004 ], [ -3.2019769, 55.9428009 ], [ -3.2000394, 55.943314 ], [ -3.2005496, 55.9442794 ], [ -3.2001162, 55.9447598 ], [ -3.1964383, 55.9454335 ], [ -3.1965526, 55.9460254 ], [ -3.1948333, 55.9468687 ], [ -3.1932024, 55.9464263 ], [ -3.1920134, 55.9452517 ], [ -3.1893563, 55.9457985 ], [ -3.1878112, 55.9465861 ], [ -3.1869983, 55.9456595 ] ] ] ] } }, 9 | { "type": "Feature", "properties": { "InterZone": "S02001622" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.1845072, 55.9579392 ], [ -3.183012, 55.9573704 ], [ -3.182731, 55.9564198 ], [ -3.1846794, 55.9551881 ], [ -3.1855039, 55.9542504 ], [ -3.1853834, 55.9530698 ], [ -3.1817339, 55.9531318 ], [ -3.1808038, 55.9515144 ], [ -3.1821642, 55.9512869 ], [ -3.1822507, 55.9501493 ], [ -3.1813229, 55.9492539 ], [ -3.1821767, 55.9483739 ], [ -3.1860519, 55.9472119 ], [ -3.1856965, 55.9466286 ], [ -3.1869361, 55.9462441 ], [ -3.1869983, 55.9456595 ], [ -3.1878112, 55.9465861 ], [ -3.1893563, 55.9457985 ], [ -3.1920134, 55.9452517 ], [ -3.1932024, 55.9464263 ], [ -3.1948333, 55.9468687 ], [ -3.1965526, 55.9460254 ], [ -3.1976403, 55.9459879 ], [ -3.1979054, 55.946794 ], [ -3.2000373, 55.946351 ], [ -3.2011897, 55.9468519 ], [ -3.2012459, 55.9476331 ], [ -3.2028665, 55.9482553 ], [ -3.2032608, 55.949096 ], [ -3.2052525, 55.9502007 ], [ -3.2027425, 55.95097 ], [ -3.2026251, 55.9518337 ], [ -3.2012224, 55.952045 ], [ -3.2004019, 55.9529605 ], [ -3.1999696, 55.9535001 ], [ -3.1970063, 55.9541835 ], [ -3.1923543, 55.9548941 ], [ -3.1922015, 55.9555379 ], [ -3.1935939, 55.9560276 ], [ -3.1935992, 55.9567194 ], [ -3.1902117, 55.9570813 ], [ -3.1891891, 55.9575796 ], [ -3.1855592, 55.9580764 ], [ -3.1845072, 55.9579392 ] ] ] ] } }, 10 | { "type": "Feature", "properties": { "InterZone": "S02001623" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.1578199, 55.9546165 ], [ -3.1557118, 55.9542769 ], [ -3.1532431, 55.9531229 ], [ -3.1518734, 55.9528392 ], [ -3.1499866, 55.9518595 ], [ -3.150062, 55.9500797 ], [ -3.1490698, 55.9474023 ], [ -3.1492326, 55.9458733 ], [ -3.1487247, 55.9454827 ], [ -3.1503687, 55.9431491 ], [ -3.149873, 55.9420935 ], [ -3.1511271, 55.9417313 ], [ -3.1525876, 55.9407742 ], [ -3.1553616, 55.940919 ], [ -3.1573275, 55.9402716 ], [ -3.1599207, 55.9397261 ], [ -3.1618362, 55.9395373 ], [ -3.1619513, 55.9391049 ], [ -3.1597452, 55.9386764 ], [ -3.1613867, 55.9384273 ], [ -3.1647598, 55.9387999 ], [ -3.1662566, 55.9385341 ], [ -3.1682987, 55.937769 ], [ -3.1685954, 55.9385839 ], [ -3.1679367, 55.9400997 ], [ -3.1695321, 55.9409741 ], [ -3.1751664, 55.94252 ], [ -3.1774207, 55.9429556 ], [ -3.1784745, 55.9423178 ], [ -3.1801618, 55.9419961 ], [ -3.1793587, 55.9429652 ], [ -3.1806737, 55.9440848 ], [ -3.1847027, 55.9436968 ], [ -3.186045, 55.9444135 ], [ -3.1869983, 55.9456595 ], [ -3.1869361, 55.9462441 ], [ -3.1856965, 55.9466286 ], [ -3.1860519, 55.9472119 ], [ -3.1821767, 55.9483739 ], [ -3.1813229, 55.9492539 ], [ -3.1822507, 55.9501493 ], [ -3.1821642, 55.9512869 ], [ -3.1808038, 55.9515144 ], [ -3.1817339, 55.9531318 ], [ -3.179976, 55.9532564 ], [ -3.1779749, 55.9538057 ], [ -3.1739804, 55.9546434 ], [ -3.1735195, 55.9526441 ], [ -3.1723974, 55.9515496 ], [ -3.1721942, 55.950653 ], [ -3.1705658, 55.9508212 ], [ -3.1635581, 55.9521005 ], [ -3.1621749, 55.9524371 ], [ -3.1578199, 55.9546165 ] ] ] ] } }, 11 | { "type": "Feature", "properties": { "InterZone": "S02001656" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.203484, 55.9582677 ], [ -3.2023597, 55.9577395 ], [ -3.1979293, 55.9584565 ], [ -3.197652, 55.9574066 ], [ -3.1954201, 55.9574381 ], [ -3.1935992, 55.9567194 ], [ -3.1935939, 55.9560276 ], [ -3.1922015, 55.9555379 ], [ -3.1923543, 55.9548941 ], [ -3.1970063, 55.9541835 ], [ -3.1999696, 55.9535001 ], [ -3.2004019, 55.9529605 ], [ -3.2014907, 55.9536041 ], [ -3.2013262, 55.9543622 ], [ -3.2034478, 55.9540899 ], [ -3.2093543, 55.9529629 ], [ -3.2090229, 55.9523064 ], [ -3.2094965, 55.9512905 ], [ -3.2105804, 55.9511988 ], [ -3.2135413, 55.952167 ], [ -3.2141704, 55.9528257 ], [ -3.2098019, 55.954477 ], [ -3.2091422, 55.9559211 ], [ -3.2075657, 55.9564749 ], [ -3.207787, 55.9574073 ], [ -3.2058846, 55.9586722 ], [ -3.203484, 55.9582677 ] ] ] ] } }, 12 | { "type": "Feature", "properties": { "InterZone": "S02001660" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.2149544, 55.9538243 ], [ -3.2141704, 55.9528257 ], [ -3.2135413, 55.952167 ], [ -3.2105804, 55.9511988 ], [ -3.2094965, 55.9512905 ], [ -3.2090229, 55.9523064 ], [ -3.2093543, 55.9529629 ], [ -3.2034478, 55.9540899 ], [ -3.2013262, 55.9543622 ], [ -3.2014907, 55.9536041 ], [ -3.2004019, 55.9529605 ], [ -3.2012224, 55.952045 ], [ -3.2026251, 55.9518337 ], [ -3.2027425, 55.95097 ], [ -3.2052525, 55.9502007 ], [ -3.2068929, 55.9498185 ], [ -3.2080646, 55.9501183 ], [ -3.2152857, 55.9470193 ], [ -3.2162396, 55.9462911 ], [ -3.2175997, 55.9454788 ], [ -3.2225062, 55.9445555 ], [ -3.2318615, 55.9424485 ], [ -3.2334962, 55.944074 ], [ -3.2347312, 55.9443923 ], [ -3.2336754, 55.9452282 ], [ -3.2354394, 55.9458125 ], [ -3.2356809, 55.9463582 ], [ -3.2335851, 55.9484278 ], [ -3.233993, 55.9491695 ], [ -3.2297559, 55.9508921 ], [ -3.2317092, 55.9523041 ], [ -3.2260953, 55.9531299 ], [ -3.221834, 55.9541067 ], [ -3.2220092, 55.9550933 ], [ -3.2206833, 55.9537588 ], [ -3.2174054, 55.9538451 ], [ -3.2163873, 55.9550861 ], [ -3.2149544, 55.9538243 ] ] ] ] } }, 13 | { "type": "Feature", "properties": { "InterZone": "DS02001616" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.2040366, 55.9333372 ], [ -3.2036354, 55.9321624 ], [ -3.2024036, 55.9321874 ], [ -3.2019838, 55.9315586 ], [ -3.2005071, 55.9317411 ], [ -3.199902, 55.931113 ], [ -3.2033504, 55.9308279 ], [ -3.2056319, 55.9309507 ], [ -3.2094979, 55.9308666 ], [ -3.2109753, 55.9299985 ], [ -3.2107073, 55.9285904 ], [ -3.2124928, 55.927854 ], [ -3.2125633, 55.9264661 ], [ -3.2094928, 55.9265616 ], [ -3.212929, 55.9260741 ], [ -3.2130774, 55.9264384 ], [ -3.2183973, 55.9252709 ], [ -3.2208941, 55.925282 ], [ -3.2242732, 55.9258683 ], [ -3.2279975, 55.9277452 ], [ -3.2269867, 55.928489 ], [ -3.2267625, 55.9299817 ], [ -3.2254561, 55.9307854 ], [ -3.224148, 55.9300725 ], [ -3.2197791, 55.9315472 ], [ -3.2222706, 55.9339127 ], [ -3.2224909, 55.934809 ], [ -3.2197844, 55.9354692 ], [ -3.2204535, 55.936195 ], [ -3.218362, 55.9368806 ], [ -3.2165749, 55.937069 ], [ -3.215582, 55.9380761 ], [ -3.2124132, 55.9355465 ], [ -3.212774, 55.9347972 ], [ -3.2119068, 55.9341947 ], [ -3.210138, 55.9349668 ], [ -3.208051, 55.9347716 ], [ -3.2083105, 55.9364224 ], [ -3.2053546, 55.9381495 ], [ -3.2046077, 55.9395298 ], [ -3.20356, 55.9380951 ], [ -3.2024323, 55.936318 ], [ -3.2029121, 55.935831 ], [ -3.204832, 55.9357555 ], [ -3.2040366, 55.9333372 ] ] ] ] } }, 14 | { "type": "Feature", "properties": { "InterZone": "DS02001620" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.2052525, 55.9502007 ], [ -3.2032608, 55.949096 ], [ -3.2028665, 55.9482553 ], [ -3.2012459, 55.9476331 ], [ -3.2011897, 55.9468519 ], [ -3.2000373, 55.946351 ], [ -3.1979054, 55.946794 ], [ -3.1976403, 55.9459879 ], [ -3.1965526, 55.9460254 ], [ -3.1964383, 55.9454335 ], [ -3.2001162, 55.9447598 ], [ -3.2005496, 55.9442794 ], [ -3.2000394, 55.943314 ], [ -3.2019769, 55.9428009 ], [ -3.201605, 55.9420004 ], [ -3.2033302, 55.942042 ], [ -3.2039421, 55.9404939 ], [ -3.2044523, 55.9418177 ], [ -3.2091262, 55.9410239 ], [ -3.2109932, 55.941338 ], [ -3.2085542, 55.9421886 ], [ -3.2097296, 55.9426766 ], [ -3.2099716, 55.9438623 ], [ -3.2088338, 55.9441577 ], [ -3.2094254, 55.9453997 ], [ -3.2107189, 55.9458872 ], [ -3.2122399, 55.9448569 ], [ -3.2129972, 55.9460355 ], [ -3.2162396, 55.9462911 ], [ -3.2152857, 55.9470193 ], [ -3.2080646, 55.9501183 ], [ -3.2068929, 55.9498185 ], [ -3.2052525, 55.9502007 ] ] ] ] } }, 15 | { "type": "Feature", "properties": { "InterZone": "S02001616" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.2040366, 55.9333372 ], [ -3.2036354, 55.9321624 ], [ -3.2024036, 55.9321874 ], [ -3.2019838, 55.9315586 ], [ -3.2005071, 55.9317411 ], [ -3.199902, 55.931113 ], [ -3.2033504, 55.9308279 ], [ -3.2056319, 55.9309507 ], [ -3.2094979, 55.9308666 ], [ -3.2109753, 55.9299985 ], [ -3.2107073, 55.9285904 ], [ -3.2124928, 55.927854 ], [ -3.2125633, 55.9264661 ], [ -3.2094928, 55.9265616 ], [ -3.212929, 55.9260741 ], [ -3.2130774, 55.9264384 ], [ -3.2183973, 55.9252709 ], [ -3.2208941, 55.925282 ], [ -3.2242732, 55.9258683 ], [ -3.2279975, 55.9277452 ], [ -3.2269867, 55.928489 ], [ -3.2267625, 55.9299817 ], [ -3.2254561, 55.9307854 ], [ -3.224148, 55.9300725 ], [ -3.2197791, 55.9315472 ], [ -3.2222706, 55.9339127 ], [ -3.2224909, 55.934809 ], [ -3.2197844, 55.9354692 ], [ -3.2204535, 55.936195 ], [ -3.218362, 55.9368806 ], [ -3.2165749, 55.937069 ], [ -3.215582, 55.9380761 ], [ -3.2124132, 55.9355465 ], [ -3.212774, 55.9347972 ], [ -3.2119068, 55.9341947 ], [ -3.210138, 55.9349668 ], [ -3.208051, 55.9347716 ], [ -3.2083105, 55.9364224 ], [ -3.2053546, 55.9381495 ], [ -3.2046077, 55.9395298 ], [ -3.20356, 55.9380951 ], [ -3.2024323, 55.936318 ], [ -3.2029121, 55.935831 ], [ -3.204832, 55.9357555 ], [ -3.2040366, 55.9333372 ] ] ] ] } }, 16 | { "type": "Feature", "properties": { "InterZone": "S02001620" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.2052525, 55.9502007 ], [ -3.2032608, 55.949096 ], [ -3.2028665, 55.9482553 ], [ -3.2012459, 55.9476331 ], [ -3.2011897, 55.9468519 ], [ -3.2000373, 55.946351 ], [ -3.1979054, 55.946794 ], [ -3.1976403, 55.9459879 ], [ -3.1965526, 55.9460254 ], [ -3.1964383, 55.9454335 ], [ -3.2001162, 55.9447598 ], [ -3.2005496, 55.9442794 ], [ -3.2000394, 55.943314 ], [ -3.2019769, 55.9428009 ], [ -3.201605, 55.9420004 ], [ -3.2033302, 55.942042 ], [ -3.2039421, 55.9404939 ], [ -3.2044523, 55.9418177 ], [ -3.2091262, 55.9410239 ], [ -3.2109932, 55.941338 ], [ -3.2085542, 55.9421886 ], [ -3.2097296, 55.9426766 ], [ -3.2099716, 55.9438623 ], [ -3.2088338, 55.9441577 ], [ -3.2094254, 55.9453997 ], [ -3.2107189, 55.9458872 ], [ -3.2122399, 55.9448569 ], [ -3.2129972, 55.9460355 ], [ -3.2162396, 55.9462911 ], [ -3.2152857, 55.9470193 ], [ -3.2080646, 55.9501183 ], [ -3.2068929, 55.9498185 ], [ -3.2052525, 55.9502007 ] ] ] ] } }, 17 | { "type": "Feature", "properties": { "InterZone": "S02001621" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.1869983, 55.9456595 ], [ -3.186045, 55.9444135 ], [ -3.1847027, 55.9436968 ], [ -3.1806737, 55.9440848 ], [ -3.1793587, 55.9429652 ], [ -3.1801618, 55.9419961 ], [ -3.1784201, 55.9417446 ], [ -3.1800522, 55.9399754 ], [ -3.1783862, 55.9401782 ], [ -3.1775032, 55.9393349 ], [ -3.178783, 55.9384599 ], [ -3.1781716, 55.9372632 ], [ -3.1788169, 55.9377858 ], [ -3.1808806, 55.9377391 ], [ -3.1812858, 55.9388117 ], [ -3.1826149, 55.9400138 ], [ -3.1827103, 55.9405161 ], [ -3.1856037, 55.9398504 ], [ -3.1880464, 55.9396292 ], [ -3.1901138, 55.9396991 ], [ -3.195452, 55.9404473 ], [ -3.1951522, 55.9395516 ], [ -3.1982982, 55.9392965 ], [ -3.2009096, 55.9388398 ], [ -3.2023573, 55.9381407 ], [ -3.20356, 55.9380951 ], [ -3.2046077, 55.9395298 ], [ -3.2039421, 55.9404939 ], [ -3.2033302, 55.942042 ], [ -3.201605, 55.9420004 ], [ -3.2019769, 55.9428009 ], [ -3.2000394, 55.943314 ], [ -3.2005496, 55.9442794 ], [ -3.2001162, 55.9447598 ], [ -3.1964383, 55.9454335 ], [ -3.1965526, 55.9460254 ], [ -3.1948333, 55.9468687 ], [ -3.1932024, 55.9464263 ], [ -3.1920134, 55.9452517 ], [ -3.1893563, 55.9457985 ], [ -3.1878112, 55.9465861 ], [ -3.1869983, 55.9456595 ] ] ] ] } }, 18 | { "type": "Feature", "properties": { "InterZone": "S02001622" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.1845072, 55.9579392 ], [ -3.183012, 55.9573704 ], [ -3.182731, 55.9564198 ], [ -3.1846794, 55.9551881 ], [ -3.1855039, 55.9542504 ], [ -3.1853834, 55.9530698 ], [ -3.1817339, 55.9531318 ], [ -3.1808038, 55.9515144 ], [ -3.1821642, 55.9512869 ], [ -3.1822507, 55.9501493 ], [ -3.1813229, 55.9492539 ], [ -3.1821767, 55.9483739 ], [ -3.1860519, 55.9472119 ], [ -3.1856965, 55.9466286 ], [ -3.1869361, 55.9462441 ], [ -3.1869983, 55.9456595 ], [ -3.1878112, 55.9465861 ], [ -3.1893563, 55.9457985 ], [ -3.1920134, 55.9452517 ], [ -3.1932024, 55.9464263 ], [ -3.1948333, 55.9468687 ], [ -3.1965526, 55.9460254 ], [ -3.1976403, 55.9459879 ], [ -3.1979054, 55.946794 ], [ -3.2000373, 55.946351 ], [ -3.2011897, 55.9468519 ], [ -3.2012459, 55.9476331 ], [ -3.2028665, 55.9482553 ], [ -3.2032608, 55.949096 ], [ -3.2052525, 55.9502007 ], [ -3.2027425, 55.95097 ], [ -3.2026251, 55.9518337 ], [ -3.2012224, 55.952045 ], [ -3.2004019, 55.9529605 ], [ -3.1999696, 55.9535001 ], [ -3.1970063, 55.9541835 ], [ -3.1923543, 55.9548941 ], [ -3.1922015, 55.9555379 ], [ -3.1935939, 55.9560276 ], [ -3.1935992, 55.9567194 ], [ -3.1902117, 55.9570813 ], [ -3.1891891, 55.9575796 ], [ -3.1855592, 55.9580764 ], [ -3.1845072, 55.9579392 ] ] ] ] } }, 19 | { "type": "Feature", "properties": { "InterZone": "S02001623" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.1578199, 55.9546165 ], [ -3.1557118, 55.9542769 ], [ -3.1532431, 55.9531229 ], [ -3.1518734, 55.9528392 ], [ -3.1499866, 55.9518595 ], [ -3.150062, 55.9500797 ], [ -3.1490698, 55.9474023 ], [ -3.1492326, 55.9458733 ], [ -3.1487247, 55.9454827 ], [ -3.1503687, 55.9431491 ], [ -3.149873, 55.9420935 ], [ -3.1511271, 55.9417313 ], [ -3.1525876, 55.9407742 ], [ -3.1553616, 55.940919 ], [ -3.1573275, 55.9402716 ], [ -3.1599207, 55.9397261 ], [ -3.1618362, 55.9395373 ], [ -3.1619513, 55.9391049 ], [ -3.1597452, 55.9386764 ], [ -3.1613867, 55.9384273 ], [ -3.1647598, 55.9387999 ], [ -3.1662566, 55.9385341 ], [ -3.1682987, 55.937769 ], [ -3.1685954, 55.9385839 ], [ -3.1679367, 55.9400997 ], [ -3.1695321, 55.9409741 ], [ -3.1751664, 55.94252 ], [ -3.1774207, 55.9429556 ], [ -3.1784745, 55.9423178 ], [ -3.1801618, 55.9419961 ], [ -3.1793587, 55.9429652 ], [ -3.1806737, 55.9440848 ], [ -3.1847027, 55.9436968 ], [ -3.186045, 55.9444135 ], [ -3.1869983, 55.9456595 ], [ -3.1869361, 55.9462441 ], [ -3.1856965, 55.9466286 ], [ -3.1860519, 55.9472119 ], [ -3.1821767, 55.9483739 ], [ -3.1813229, 55.9492539 ], [ -3.1822507, 55.9501493 ], [ -3.1821642, 55.9512869 ], [ -3.1808038, 55.9515144 ], [ -3.1817339, 55.9531318 ], [ -3.179976, 55.9532564 ], [ -3.1779749, 55.9538057 ], [ -3.1739804, 55.9546434 ], [ -3.1735195, 55.9526441 ], [ -3.1723974, 55.9515496 ], [ -3.1721942, 55.950653 ], [ -3.1705658, 55.9508212 ], [ -3.1635581, 55.9521005 ], [ -3.1621749, 55.9524371 ], [ -3.1578199, 55.9546165 ] ] ] ] } }, 20 | { "type": "Feature", "properties": { "InterZone": "S02001656" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.203484, 55.9582677 ], [ -3.2023597, 55.9577395 ], [ -3.1979293, 55.9584565 ], [ -3.197652, 55.9574066 ], [ -3.1954201, 55.9574381 ], [ -3.1935992, 55.9567194 ], [ -3.1935939, 55.9560276 ], [ -3.1922015, 55.9555379 ], [ -3.1923543, 55.9548941 ], [ -3.1970063, 55.9541835 ], [ -3.1999696, 55.9535001 ], [ -3.2004019, 55.9529605 ], [ -3.2014907, 55.9536041 ], [ -3.2013262, 55.9543622 ], [ -3.2034478, 55.9540899 ], [ -3.2093543, 55.9529629 ], [ -3.2090229, 55.9523064 ], [ -3.2094965, 55.9512905 ], [ -3.2105804, 55.9511988 ], [ -3.2135413, 55.952167 ], [ -3.2141704, 55.9528257 ], [ -3.2098019, 55.954477 ], [ -3.2091422, 55.9559211 ], [ -3.2075657, 55.9564749 ], [ -3.207787, 55.9574073 ], [ -3.2058846, 55.9586722 ], [ -3.203484, 55.9582677 ] ] ] ] } }, 21 | { "type": "Feature", "properties": { "InterZone": "S02001660" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.2149544, 55.9538243 ], [ -3.2141704, 55.9528257 ], [ -3.2135413, 55.952167 ], [ -3.2105804, 55.9511988 ], [ -3.2094965, 55.9512905 ], [ -3.2090229, 55.9523064 ], [ -3.2093543, 55.9529629 ], [ -3.2034478, 55.9540899 ], [ -3.2013262, 55.9543622 ], [ -3.2014907, 55.9536041 ], [ -3.2004019, 55.9529605 ], [ -3.2012224, 55.952045 ], [ -3.2026251, 55.9518337 ], [ -3.2027425, 55.95097 ], [ -3.2052525, 55.9502007 ], [ -3.2068929, 55.9498185 ], [ -3.2080646, 55.9501183 ], [ -3.2152857, 55.9470193 ], [ -3.2162396, 55.9462911 ], [ -3.2175997, 55.9454788 ], [ -3.2225062, 55.9445555 ], [ -3.2318615, 55.9424485 ], [ -3.2334962, 55.944074 ], [ -3.2347312, 55.9443923 ], [ -3.2336754, 55.9452282 ], [ -3.2354394, 55.9458125 ], [ -3.2356809, 55.9463582 ], [ -3.2335851, 55.9484278 ], [ -3.233993, 55.9491695 ], [ -3.2297559, 55.9508921 ], [ -3.2317092, 55.9523041 ], [ -3.2260953, 55.9531299 ], [ -3.221834, 55.9541067 ], [ -3.2220092, 55.9550933 ], [ -3.2206833, 55.9537588 ], [ -3.2174054, 55.9538451 ], [ -3.2163873, 55.9550861 ], [ -3.2149544, 55.9538243 ] ] ] ] } }, 22 | { "type": "Feature", "properties": { "InterZone": "DS02001616" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.2040366, 55.9333372 ], [ -3.2036354, 55.9321624 ], [ -3.2024036, 55.9321874 ], [ -3.2019838, 55.9315586 ], [ -3.2005071, 55.9317411 ], [ -3.199902, 55.931113 ], [ -3.2033504, 55.9308279 ], [ -3.2056319, 55.9309507 ], [ -3.2094979, 55.9308666 ], [ -3.2109753, 55.9299985 ], [ -3.2107073, 55.9285904 ], [ -3.2124928, 55.927854 ], [ -3.2125633, 55.9264661 ], [ -3.2094928, 55.9265616 ], [ -3.212929, 55.9260741 ], [ -3.2130774, 55.9264384 ], [ -3.2183973, 55.9252709 ], [ -3.2208941, 55.925282 ], [ -3.2242732, 55.9258683 ], [ -3.2279975, 55.9277452 ], [ -3.2269867, 55.928489 ], [ -3.2267625, 55.9299817 ], [ -3.2254561, 55.9307854 ], [ -3.224148, 55.9300725 ], [ -3.2197791, 55.9315472 ], [ -3.2222706, 55.9339127 ], [ -3.2224909, 55.934809 ], [ -3.2197844, 55.9354692 ], [ -3.2204535, 55.936195 ], [ -3.218362, 55.9368806 ], [ -3.2165749, 55.937069 ], [ -3.215582, 55.9380761 ], [ -3.2124132, 55.9355465 ], [ -3.212774, 55.9347972 ], [ -3.2119068, 55.9341947 ], [ -3.210138, 55.9349668 ], [ -3.208051, 55.9347716 ], [ -3.2083105, 55.9364224 ], [ -3.2053546, 55.9381495 ], [ -3.2046077, 55.9395298 ], [ -3.20356, 55.9380951 ], [ -3.2024323, 55.936318 ], [ -3.2029121, 55.935831 ], [ -3.204832, 55.9357555 ], [ -3.2040366, 55.9333372 ] ] ] ] } }, 23 | { "type": "Feature", "properties": { "InterZone": "DS02001620" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -3.2052525, 55.9502007 ], [ -3.2032608, 55.949096 ], [ -3.2028665, 55.9482553 ], [ -3.2012459, 55.9476331 ], [ -3.2011897, 55.9468519 ], [ -3.2000373, 55.946351 ], [ -3.1979054, 55.946794 ], [ -3.1976403, 55.9459879 ], [ -3.1965526, 55.9460254 ], [ -3.1964383, 55.9454335 ], [ -3.2001162, 55.9447598 ], [ -3.2005496, 55.9442794 ], [ -3.2000394, 55.943314 ], [ -3.2019769, 55.9428009 ], [ -3.201605, 55.9420004 ], [ -3.2033302, 55.942042 ], [ -3.2039421, 55.9404939 ], [ -3.2044523, 55.9418177 ], [ -3.2091262, 55.9410239 ], [ -3.2109932, 55.941338 ], [ -3.2085542, 55.9421886 ], [ -3.2097296, 55.9426766 ], [ -3.2099716, 55.9438623 ], [ -3.2088338, 55.9441577 ], [ -3.2094254, 55.9453997 ], [ -3.2107189, 55.9458872 ], [ -3.2122399, 55.9448569 ], [ -3.2129972, 55.9460355 ], [ -3.2162396, 55.9462911 ], [ -3.2152857, 55.9470193 ], [ -3.2080646, 55.9501183 ], [ -3.2068929, 55.9498185 ], [ -3.2052525, 55.9502007 ] ] ] ] } } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /r/.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | -------------------------------------------------------------------------------- /r/.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | -------------------------------------------------------------------------------- /r/DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: odjitter 2 | Title: Conversion of OD Data Geographic Desire Lines with Distributed Start and End Points 3 | Version: 0.0.0.9000 4 | Authors@R: 5 | c( 6 | person(given = "Robin", 7 | family = "Lovelace", 8 | role = c("aut", "cre"), 9 | email = "rob00x@gmail.com", 10 | comment = c(ORCID = "0000-0001-5679-6536")), 11 | person(given = "Dustin", 12 | family = "Carlino", 13 | role = "aut")) 14 | Description: Provide an interface to the odjitter Rust crate 15 | for processing origin-destination data. 16 | License: Apache License (>= 2) 17 | Encoding: UTF-8 18 | Roxygen: list(markdown = TRUE) 19 | RoxygenNote: 7.2.3 20 | Imports: 21 | glue, 22 | readr, 23 | sf 24 | -------------------------------------------------------------------------------- /r/LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | _<>_ 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ### APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | Copyright [yyyy] [name of copyright owner] 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | you may not use this file except in compliance with the License. 186 | You may obtain a copy of the License at 187 | 188 | http://www.apache.org/licenses/LICENSE-2.0 189 | 190 | Unless required by applicable law or agreed to in writing, software 191 | distributed under the License is distributed on an "AS IS" BASIS, 192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | See the License for the specific language governing permissions and 194 | limitations under the License. 195 | -------------------------------------------------------------------------------- /r/NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(jitter) 4 | -------------------------------------------------------------------------------- /r/R/jitter.R: -------------------------------------------------------------------------------- 1 | #' Jitter OD data using the Rust crate odjitter 2 | #' 3 | #' @param od Origin-destination data 4 | #' @param zones Zones with `zone_name_key` corresponding to values in the first 5 | #' (and maybe second also) column in `od` 6 | #' @param zones_d Destination zones, the first column of which corresponds to 7 | #' values in the second column of `od` 8 | #' @param subpoints Geographic dataset from which jittered desire lines start and end 9 | #' @param zone_name_key The name of the key linking zones to the `od` data 10 | #' @param origin_key The name of the column in the OD data representing origins 11 | #' @param destination_key OD data column representing zone of destination 12 | #' @param subpoints_origins Geographic dataset from which jittered desire lines start 13 | #' @param subpoints_destinations Geographic dataset from which jittered desire lines end 14 | #' @param disaggregation_key The name of the column in the OD dataset determining disaggregation 15 | #' @param disaggregation_threshold What's the maximum number of trips per output OD row that's allowed? 16 | #' @param min_distance_meters Minimum distance between OD pairs 17 | #' @param rng_seed Integer for deterministic jittering 18 | #' @param weight_key_destinations Column in `subpoints_destinations` with weights 19 | #' @param weight_key_origins Column in `subpoints_origins` with weights 20 | #' @param od_csv_path Where the CSV file is stored (usually irrelevant) 21 | #' @param output_path Where to save the output (usually irrelevant) 22 | #' @param subpoints_destinations_path Location of subpoints file (usually irrelevant) 23 | #' @param subpoints_origins_path Location of subpoints file (usually irrelevant) 24 | #' @param zones_path Path to zones (usually irrelevant) 25 | #' @param data_dir The directory where intermediate 26 | #' data files will be saved. `tempdir()` by default. 27 | #' @param show_command Show the command line call for jittering? 28 | #' Set to FALSE by default, set it to TRUE for debugging/educational purposes. 29 | #' @param deduplicate_pairs Return only unique OD pairs? TRUE by default. 30 | #' @return An `sf` object with the jittered result 31 | #' @export 32 | #' 33 | #' @examples 34 | #' od = readr::read_csv("https://github.com/dabreegster/odjitter/raw/main/data/od.csv") 35 | #' zones = sf::read_sf("https://github.com/dabreegster/odjitter/raw/main/data/zones.geojson") 36 | #' road_network = sf::read_sf("https://github.com/dabreegster/odjitter/raw/main/data/road_network.geojson") 37 | #' od_jittered = jitter(od, zones, subpoints = road_network) 38 | #' od_jittered = jitter(od, zones, subpoints = road_network, show_command = TRUE) 39 | #' od_jittered = jitter( 40 | #' od, 41 | #' zones, 42 | #' subpoints = road_network, 43 | #' disaggregation_threshold = 50 44 | #' ) 45 | #' zones_d = zones[1:2, ] 46 | #' zones_d[[1]] = c("d1", "d2") 47 | #' od[[2]] = sample(c("d1", "d2"), nrow(od), TRUE) 48 | #' od_jittered = jitter( 49 | #' od, 50 | #' zones, 51 | #' zones_d = zones_d, 52 | #' subpoints = road_network, 53 | #' disaggregation_threshold = 50 54 | #' ) 55 | #' plot(od_jittered) 56 | #' # j_schools = jitter( 57 | #' # od = sf::read_sf("data-raw/school-example/od_to_jitter.geojson"), 58 | #' # subpoints_origins = sf::read_sf("data-raw/school-example/subpoints.geojson"), 59 | #' # subpoints_destinations = sf::st_read("data-raw/school-example/subpoints_destinations.geojson"), 60 | #' # zones = sf::read_sf("data-raw/school-example/zones.geojson"), 61 | #' # zones_d = sf::read_sf("data-raw/school-example/zones_d.geojson"), 62 | #' # disaggregation_threshold = 50 63 | #' # ) 64 | #' # summary(j_schools) 65 | #' # \dontrun{ 66 | #' # od_jittered = jitter( 67 | #' # od, 68 | #' # zones, 69 | #' # zones_d = zones_d, 70 | #' # subpoints = road_network, 71 | #' # disaggregation_threshold = 50, 72 | #' # odjitter_location = r"(powershell C:\Users\geoevid\.cargo\bin\odjitter.exe)" 73 | #' # ) 74 | #' # plot(od_jittered) 75 | #' } 76 | jitter = function( 77 | od, 78 | zones, 79 | subpoints = NULL, 80 | zones_d = NULL, 81 | zone_name_key = NULL, 82 | origin_key = NULL, 83 | destination_key = NULL, 84 | subpoints_origins = subpoints, 85 | subpoints_destinations = subpoints, 86 | disaggregation_key = "all", 87 | disaggregation_threshold = 10000, 88 | min_distance_meters = 1, 89 | weight_key_destinations = NULL, 90 | weight_key_origins = NULL, 91 | rng_seed = round(runif(n = 1) * 1e5), 92 | od_csv_path = NULL, 93 | output_path = NULL, 94 | subpoints_origins_path = NULL, 95 | subpoints_destinations_path = NULL, 96 | zones_path = NULL, 97 | data_dir = tempdir(), 98 | show_command = FALSE, 99 | deduplicate_pairs = TRUE, 100 | odjitter_location = "odjitter" 101 | ) { 102 | installed = odjitter_is_installed(odjitter_location) 103 | # powershell = installed == "PowerShell" 104 | # if(powershell) { 105 | # installed = system("odjitter --help", intern = TRUE) 106 | # } 107 | if(!installed) { 108 | message("Cannot find the odjitter command on your computer") 109 | stop( 110 | "# Try installing it with the following command:\n", 111 | "cargo install --git https://github.com/dabreegster/odjitter\n", 112 | "# You need to have installed cargo" 113 | ) 114 | } 115 | # Convert deduplicate_pairs to a string: 116 | if(deduplicate_pairs) { 117 | deduplicate_pairs = "--deduplicate-pairs" 118 | } else { 119 | deduplicate_pairs = "" 120 | } 121 | 122 | # assigning null variables to appropriate values 123 | if(is.null(zone_name_key)) zone_name_key = names(zones)[1] 124 | if(is.null(origin_key)) origin_key = names(od)[1] 125 | if(is.null(destination_key)) destination_key = names(od)[2] 126 | if(!is.null(zones_d)) { 127 | zones = zones[1] 128 | zones_d = zones_d[1] 129 | names(zones_d) = names(zones) 130 | zones = rbind(zones, zones_d) 131 | } 132 | geometry_type = sf::st_geometry_type(zones) 133 | if(length(unique(geometry_type)) > 1) { 134 | zones = sf::st_cast(zones, "MULTIPOLYGON") 135 | } 136 | if(is.null(od_csv_path)) od_csv_path = file.path(data_dir, "od.csv") 137 | if(is.null(zones_path)) zones_path = file.path(data_dir, "zones.geojson") 138 | if(!is.null(subpoints)) { 139 | subpoints_origins_path = subpoints_destinations_path = file.path(data_dir, "subpoints.geojson") 140 | sf::write_sf(subpoints, subpoints_origins_path, delete_dsn = TRUE) 141 | } else { 142 | subpoints_origins_path = file.path(data_dir, "subpoints_origins.geojson") 143 | sf::write_sf(subpoints_origins, subpoints_origins_path, delete_dsn = TRUE) 144 | subpoints_destinations_path = file.path(data_dir, "subpoints_destinations.geojson") 145 | sf::write_sf(subpoints_destinations, subpoints_destinations_path, delete_dsn = TRUE) 146 | } 147 | if(is.null(output_path)) { 148 | output_path = file.path(data_dir, "od_jittered.geojson") 149 | } 150 | 151 | disaggregation_key_exists = any(names(od) %in% disaggregation_key) 152 | if(!disaggregation_key_exists && disaggregation_key == "all") { 153 | disaggregation_key = names(od)[3] 154 | } 155 | # prevent numeric values: 156 | od[[origin_key]] = paste0("jitter", od[[origin_key]]) 157 | od[[destination_key]] = paste0("jitter", od[[destination_key]]) 158 | zones[[zone_name_key]] = paste0("jitter", zones[[zone_name_key]]) 159 | 160 | readr::write_csv(od, od_csv_path) 161 | sf::write_sf(zones, file.path(data_dir, "zones.geojson"), delete_dsn = TRUE) 162 | 163 | msg = glue::glue("{odjitter_location} jitter --od-csv-path {od_csv_path} \\ 164 | --zones-path {zones_path} \\ 165 | --zone-name-key {zone_name_key} \\ 166 | --origin-key {origin_key} \\ 167 | --destination-key {destination_key} \\ 168 | --subpoints-origins-path {subpoints_origins_path} \\ 169 | --subpoints-destinations-path {subpoints_destinations_path} \\ 170 | --disaggregation-key {disaggregation_key} \\ 171 | --disaggregation-threshold {disaggregation_threshold} \\ 172 | --rng-seed {rng_seed} \\ 173 | {deduplicate_pairs} \\ 174 | --output-path {output_path}") 175 | if(show_command) { 176 | message("command sent to the system:") 177 | cat(msg) 178 | } 179 | system(msg) 180 | res = sf::read_sf(output_path) 181 | res[[origin_key]] = gsub("jitter", "", x = res[[origin_key]]) 182 | res[[destination_key]] = gsub("jitter", "", x = res[[destination_key]]) 183 | res[names(od)] 184 | } 185 | 186 | odjitter_is_installed = function(odjitter_location) { 187 | # if(Sys.info()[['sysname']] == "Windows") { 188 | # sysoutput = system("powershell", intern = TRUE) 189 | # if(sysoutput[1] == "Windows PowerShell") { 190 | # return("PowerShell") 191 | # } 192 | # } 193 | sysoutput = system(paste0(odjitter_location, " --help"), intern = TRUE) 194 | grepl(pattern = "odjitter", x = sysoutput[1]) 195 | } 196 | -------------------------------------------------------------------------------- /r/README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>" 11 | ) 12 | ``` 13 | 14 | # odjitter R package 15 | 16 | 17 | 18 | 19 | The goal of this {odjitter} R package is to provide an R interface to the [odjitter](https://github.com/dabreegster/odjitter) Rust crate for processing origin-destination data. 20 | 21 | Install the development version as follows 22 | 23 | ```{r, eval=FALSE} 24 | remotes::install_github("dabreegster/odjitter", subdir = "r") 25 | ``` 26 | 27 | ## R interface to `odjitter` Rust crate via system commands 28 | 29 | ```{r} 30 | library(odjitter) 31 | ``` 32 | 33 | ```{r jitter, out.width="50%", fig.show='hold'} 34 | od = readr::read_csv("https://github.com/dabreegster/odjitter/raw/main/data/od.csv") 35 | zones = sf::read_sf("https://github.com/dabreegster/odjitter/raw/main/data/zones.geojson") 36 | names(zones)[1] = "geo_code" 37 | road_network = sf::read_sf("https://github.com/dabreegster/odjitter/raw/main/data/road_network.geojson") 38 | od_unjittered = od::od_to_sf(od, zones) 39 | set.seed(42) # for reproducibility 40 | od_jittered = jitter(od, zones, subpoints = road_network) 41 | nrow(od_unjittered) 42 | nrow(od_jittered) 43 | plot(od_unjittered) 44 | plot(od_jittered) 45 | ``` 46 | 47 | ## Allowing duplicate OD pairs 48 | 49 | By default the `jitter` function will remove duplicate OD pairs. 50 | This can be disabled by setting `deduplicate_pairs = FALSE`. 51 | 52 | ```{r} 53 | # Default behaviour (no duplicates): 54 | od_jittered = jitter( 55 | od, 56 | zones, 57 | subpoints = road_network, 58 | disaggregation_threshold = 1, 59 | show_command = TRUE, 60 | ) 61 | summary(duplicated(od_jittered$geometry)) 62 | ``` 63 | 64 | ```{r} 65 | 66 | ```{r} 67 | # Larger example: 68 | od_jittered = jitter( 69 | od, 70 | zones, 71 | subpoints = road_network, 72 | disaggregation_threshold = 1, 73 | show_command = TRUE, 74 | deduplicate_pairs = FALSE 75 | ) 76 | summary(duplicated(od_jittered$geometry)) 77 | ``` 78 | 79 | ## R interface to Rust via rextendr (not currently working) 80 | 81 | The development of the package was done using the development version of the `rextendr` package. 82 | 83 | ```{r, eval=FALSE} 84 | remotes::install_github("extendr/rextendr") 85 | ``` 86 | 87 | The package template was created as follows: 88 | 89 | ```{r, eval=FALSE} 90 | usethis::use_description() 91 | rextendr::use_extendr() 92 | ``` 93 | 94 | The odjitter Rust crate ported into the src/rust folder. 95 | 96 | ```{r, eval=FALSE, echo=FALSE} 97 | list.files("src/rust/") 98 | file.edit("~/orgs/atumWorld/odjitter/src/lib.rs") 99 | file.edit("~/orgs/atumWorld/odjitter/Cargo.toml") 100 | file.edit("src/rust/Cargo.toml") 101 | ``` 102 | 103 | 104 | ```{bash, echo=FALSE, eval=FALSE} 105 | cp -Rv ~/orgs/atumWorld/odjitter/src/scrape.rs src/rust/src/ 106 | ``` 107 | 108 | -------------------------------------------------------------------------------- /r/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # odjitter R package 5 | 6 | 7 | 8 | 9 | 10 | The goal of this {odjitter} R package is to provide an R interface to 11 | the [odjitter](https://github.com/dabreegster/odjitter) Rust crate for 12 | processing origin-destination data. 13 | 14 | Install the development version as follows 15 | 16 | ``` r 17 | remotes::install_github("dabreegster/odjitter", subdir = "r") 18 | ``` 19 | 20 | ## R interface to `odjitter` Rust crate via system commands 21 | 22 | ``` r 23 | library(odjitter) 24 | #> 25 | #> Attaching package: 'odjitter' 26 | #> The following object is masked from 'package:base': 27 | #> 28 | #> jitter 29 | ``` 30 | 31 | ``` r 32 | od = readr::read_csv("https://github.com/dabreegster/odjitter/raw/main/data/od.csv") 33 | #> Rows: 49 Columns: 11 34 | #> ── Column specification ──────────────────────────────────────────────────────── 35 | #> Delimiter: "," 36 | #> chr (2): geo_code1, geo_code2 37 | #> dbl (9): all, from_home, train, bus, car_driver, car_passenger, bicycle, foo... 38 | #> 39 | #> ℹ Use `spec()` to retrieve the full column specification for this data. 40 | #> ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message. 41 | zones = sf::read_sf("https://github.com/dabreegster/odjitter/raw/main/data/zones.geojson") 42 | names(zones)[1] = "geo_code" 43 | road_network = sf::read_sf("https://github.com/dabreegster/odjitter/raw/main/data/road_network.geojson") 44 | od_unjittered = od::od_to_sf(od, zones) 45 | #> 0 origins with no match in zone ids 46 | #> 0 destinations with no match in zone ids 47 | #> points not in od data removed. 48 | set.seed(42) # for reproducibility 49 | od_jittered = jitter(od, zones, subpoints = road_network) 50 | nrow(od_unjittered) 51 | #> [1] 49 52 | nrow(od_jittered) 53 | #> [1] 49 54 | plot(od_unjittered) 55 | #> Warning: plotting the first 9 out of 11 attributes; use max.plot = 11 to plot 56 | #> all 57 | plot(od_jittered) 58 | #> Warning: plotting the first 9 out of 11 attributes; use max.plot = 11 to plot 59 | #> all 60 | ``` 61 | 62 | 63 | 64 | ## Allowing duplicate OD pairs 65 | 66 | By default the `jitter` function will remove duplicate OD pairs. This 67 | can be disabled by setting `deduplicate_pairs = FALSE`. 68 | 69 | ``` r 70 | # Default behaviour (no duplicates): 71 | od_jittered = jitter( 72 | od, 73 | zones, 74 | subpoints = road_network, 75 | disaggregation_threshold = 1, 76 | show_command = TRUE, 77 | ) 78 | #> command sent to the system: 79 | #> odjitter jitter --od-csv-path /tmp/RtmpfqvyCC/od.csv --zones-path /tmp/RtmpfqvyCC/zones.geojson --zone-name-key geo_code --origin-key geo_code1 --destination-key geo_code2 --subpoints-origins-path /tmp/RtmpfqvyCC/subpoints.geojson --subpoints-destinations-path /tmp/RtmpfqvyCC/subpoints.geojson --disaggregation-key all --disaggregation-threshold 1 --rng-seed 93708 --deduplicate-pairs --output-path /tmp/RtmpfqvyCC/od_jittered.geojson 80 | summary(duplicated(od_jittered$geometry)) 81 | #> Mode FALSE 82 | #> logical 6555 83 | ``` 84 | 85 | ``` r 86 | # Larger example: 87 | od_jittered = jitter( 88 | od, 89 | zones, 90 | subpoints = road_network, 91 | disaggregation_threshold = 1, 92 | show_command = TRUE, 93 | deduplicate_pairs = FALSE 94 | ) 95 | #> command sent to the system: 96 | #> odjitter jitter --od-csv-path /tmp/RtmpfqvyCC/od.csv --zones-path /tmp/RtmpfqvyCC/zones.geojson --zone-name-key geo_code --origin-key geo_code1 --destination-key geo_code2 --subpoints-origins-path /tmp/RtmpfqvyCC/subpoints.geojson --subpoints-destinations-path /tmp/RtmpfqvyCC/subpoints.geojson --disaggregation-key all --disaggregation-threshold 1 --rng-seed 28614 --output-path /tmp/RtmpfqvyCC/od_jittered.geojson 97 | summary(duplicated(od_jittered$geometry)) 98 | #> Mode FALSE TRUE 99 | #> logical 6545 10 100 | ``` 101 | 102 | ## R interface to Rust via rextendr (not currently working) 103 | 104 | The development of the package was done using the development version of 105 | the `rextendr` package. 106 | 107 | ``` r 108 | remotes::install_github("extendr/rextendr") 109 | ``` 110 | 111 | The package template was created as follows: 112 | 113 | ``` r 114 | usethis::use_description() 115 | rextendr::use_extendr() 116 | ``` 117 | 118 | The odjitter Rust crate ported into the src/rust folder. 119 | -------------------------------------------------------------------------------- /r/README_files/figure-gfm/jitter-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabreegster/odjitter/98a7a6e03bc54bc79d3b3abbde91b6f79173ff1a/r/README_files/figure-gfm/jitter-1.png -------------------------------------------------------------------------------- /r/README_files/figure-gfm/jitter-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabreegster/odjitter/98a7a6e03bc54bc79d3b3abbde91b6f79173ff1a/r/README_files/figure-gfm/jitter-2.png -------------------------------------------------------------------------------- /r/man/jitter.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/jitter.R 3 | \name{jitter} 4 | \alias{jitter} 5 | \title{Jitter OD data using the Rust crate odjitter} 6 | \usage{ 7 | jitter( 8 | od, 9 | zones, 10 | subpoints = NULL, 11 | zones_d = NULL, 12 | zone_name_key = NULL, 13 | origin_key = NULL, 14 | destination_key = NULL, 15 | subpoints_origins = subpoints, 16 | subpoints_destinations = subpoints, 17 | disaggregation_key = "all", 18 | disaggregation_threshold = 10000, 19 | min_distance_meters = 1, 20 | weight_key_destinations = NULL, 21 | weight_key_origins = NULL, 22 | rng_seed = round(runif(n = 1) * 1e+05), 23 | od_csv_path = NULL, 24 | output_path = NULL, 25 | subpoints_origins_path = NULL, 26 | subpoints_destinations_path = NULL, 27 | zones_path = NULL, 28 | data_dir = tempdir(), 29 | show_command = FALSE, 30 | deduplicate_pairs = TRUE, 31 | odjitter_location = "odjitter" 32 | ) 33 | } 34 | \arguments{ 35 | \item{od}{Origin-destination data} 36 | 37 | \item{zones}{Zones with \code{zone_name_key} corresponding to values in the first 38 | (and maybe second also) column in \code{od}} 39 | 40 | \item{subpoints}{Geographic dataset from which jittered desire lines start and end} 41 | 42 | \item{zones_d}{Destination zones, the first column of which corresponds to 43 | values in the second column of \code{od}} 44 | 45 | \item{zone_name_key}{The name of the key linking zones to the \code{od} data} 46 | 47 | \item{origin_key}{The name of the column in the OD data representing origins} 48 | 49 | \item{destination_key}{OD data column representing zone of destination} 50 | 51 | \item{subpoints_origins}{Geographic dataset from which jittered desire lines start} 52 | 53 | \item{subpoints_destinations}{Geographic dataset from which jittered desire lines end} 54 | 55 | \item{disaggregation_key}{The name of the column in the OD dataset determining disaggregation} 56 | 57 | \item{disaggregation_threshold}{What's the maximum number of trips per output OD row that's allowed?} 58 | 59 | \item{min_distance_meters}{Minimum distance between OD pairs} 60 | 61 | \item{weight_key_destinations}{Column in \code{subpoints_destinations} with weights} 62 | 63 | \item{weight_key_origins}{Column in \code{subpoints_origins} with weights} 64 | 65 | \item{rng_seed}{Integer for deterministic jittering} 66 | 67 | \item{od_csv_path}{Where the CSV file is stored (usually irrelevant)} 68 | 69 | \item{output_path}{Where to save the output (usually irrelevant)} 70 | 71 | \item{subpoints_origins_path}{Location of subpoints file (usually irrelevant)} 72 | 73 | \item{subpoints_destinations_path}{Location of subpoints file (usually irrelevant)} 74 | 75 | \item{zones_path}{Path to zones (usually irrelevant)} 76 | 77 | \item{data_dir}{The directory where intermediate 78 | data files will be saved. \code{tempdir()} by default.} 79 | 80 | \item{show_command}{Show the command line call for jittering? 81 | Set to FALSE by default, set it to TRUE for debugging/educational purposes.} 82 | 83 | \item{deduplicate_pairs}{Return only unique OD pairs? TRUE by default.} 84 | } 85 | \value{ 86 | An \code{sf} object with the jittered result 87 | } 88 | \description{ 89 | Jitter OD data using the Rust crate odjitter 90 | } 91 | \examples{ 92 | od = readr::read_csv("https://github.com/dabreegster/odjitter/raw/main/data/od.csv") 93 | zones = sf::read_sf("https://github.com/dabreegster/odjitter/raw/main/data/zones.geojson") 94 | road_network = sf::read_sf("https://github.com/dabreegster/odjitter/raw/main/data/road_network.geojson") 95 | od_jittered = jitter(od, zones, subpoints = road_network) 96 | od_jittered = jitter(od, zones, subpoints = road_network, show_command = TRUE) 97 | od_jittered = jitter( 98 | od, 99 | zones, 100 | subpoints = road_network, 101 | disaggregation_threshold = 50 102 | ) 103 | zones_d = zones[1:2, ] 104 | zones_d[[1]] = c("d1", "d2") 105 | od[[2]] = sample(c("d1", "d2"), nrow(od), TRUE) 106 | od_jittered = jitter( 107 | od, 108 | zones, 109 | zones_d = zones_d, 110 | subpoints = road_network, 111 | disaggregation_threshold = 50 112 | ) 113 | plot(od_jittered) 114 | # j_schools = jitter( 115 | # od = sf::read_sf("data-raw/school-example/od_to_jitter.geojson"), 116 | # subpoints_origins = sf::read_sf("data-raw/school-example/subpoints.geojson"), 117 | # subpoints_destinations = sf::st_read("data-raw/school-example/subpoints_destinations.geojson"), 118 | # zones = sf::read_sf("data-raw/school-example/zones.geojson"), 119 | # zones_d = sf::read_sf("data-raw/school-example/zones_d.geojson"), 120 | # disaggregation_threshold = 50 121 | # ) 122 | # summary(j_schools) 123 | # \dontrun{ 124 | # od_jittered = jitter( 125 | # od, 126 | # zones, 127 | # zones_d = zones_d, 128 | # subpoints = road_network, 129 | # disaggregation_threshold = 50, 130 | # odjitter_location = r"(powershell C:\Users\geoevid\.cargo\bin\odjitter.exe)" 131 | # ) 132 | # plot(od_jittered) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /r/odjitter.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | BuildType: Package 16 | PackageUseDevtools: Yes 17 | PackageInstallArgs: --no-multiarch --with-keep.source 18 | -------------------------------------------------------------------------------- /references.bib: -------------------------------------------------------------------------------- 1 | @article{lovelace_jittering_2022b, 2 | title = {Jittering: {{A Computationally Efficient Method}} for {{Generating Realistic Route Networks}} from {{Origin-Destination Data}}}, 3 | shorttitle = {Jittering}, 4 | author = {Lovelace, Robin and Félix, Rosa and Carlino, Dustin}, 5 | date = {2022-04-08}, 6 | journaltitle = {Findings}, 7 | shortjournal = {Findings}, 8 | pages = {33873}, 9 | publisher = {{Findings Press}}, 10 | doi = {10.32866/001c.33873}, 11 | url = {https://findingspress.org/article/33873-jittering-a-computationally-efficient-method-for-generating-realistic-route-networks-from-origin-destination-data}, 12 | urldate = {2022-05-05}, 13 | abstract = {Origin-destination (OD) datasets are often represented as ‘desire lines’ between zone centroids. This paper presents a ‘jittering’ approach to pre-processing and conversion of OD data into geographic desire lines that (1) samples unique origin and destination locations for each OD pair, and (2) splits ‘large’ OD pairs into ‘sub-OD’ pairs. Reproducible findings, based on the open source \_odjitter\_ Rust crate, show that route networks generated from jittered desire lines are more geographically diffuse than route networks generated by ‘unjittered’ data. We conclude that the approach is a computationally efficient and flexible way to simulate transport patterns, particularly relevant for modelling active modes. Further work is needed to validate the approach and to find optimal settings for sampling and disaggregation.}, 14 | langid = {english}, 15 | file = {/home/robin/Zotero/storage/MW7B8GVG/Lovelace et al. - 2022 - Jittering A Computationally Efficient Method for .pdf;/home/robin/Zotero/storage/QZU3J666/33873-jittering-a-computationally-efficient-method-for-generating-realistic-route-networks-from.html} 16 | } 17 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate transforms origin/destination data aggregated by zone into a disaggregated form, by 2 | //! sampling specific points from the zone. 3 | //! 4 | //! TODO: Motivate and explain with a full example. 5 | 6 | mod scrape; 7 | #[cfg(test)] 8 | mod tests; 9 | 10 | use std::collections::{BTreeMap, HashMap, HashSet}; 11 | use std::io::BufReader; 12 | use std::path::Path; 13 | 14 | use anyhow::{bail, Result}; 15 | use fs_err::File; 16 | use geo::algorithm::bounding_rect::BoundingRect; 17 | use geo::algorithm::contains::Contains; 18 | use geo::algorithm::haversine_distance::HaversineDistance; 19 | use geo_types::{LineString, MultiPolygon, Point, Rect}; 20 | use geojson::{Feature, FeatureReader}; 21 | use ordered_float::NotNan; 22 | use rand::prelude::SliceRandom; 23 | use rand::rngs::StdRng; 24 | use rand::Rng; 25 | use rstar::{RTree, RTreeObject, AABB}; 26 | use serde_json::{Map, Value}; 27 | 28 | pub use self::scrape::scrape_points; 29 | 30 | pub struct Options { 31 | /// How to pick points from origin zones 32 | pub subsample_origin: Subsample, 33 | /// How to pick points from destination zones 34 | pub subsample_destination: Subsample, 35 | /// Which column in the OD row specifies the zone where trips originate? 36 | pub origin_key: String, 37 | /// Which column in the OD row specifies the zone where trips ends? 38 | pub destination_key: String, 39 | /// Guarantee that jittered points are at least this distance apart. 40 | pub min_distance_meters: f64, 41 | /// Prevent duplicate (origin, destination) pairs from appearing in the output. This may 42 | /// increase memory and runtime requirements. Note the duplication uses the floating point 43 | /// precision of the input data, and only consider geometry (not any properties). 44 | pub deduplicate_pairs: bool, 45 | } 46 | 47 | /// Specifies how specific points should be generated within a zone. 48 | pub enum Subsample { 49 | /// Pick points uniformly at random within the zone's shape. 50 | /// 51 | /// Note that "within" excludes points directly on the zone's boundary. 52 | RandomPoints, 53 | /// Sample from points within the zone's shape, where each point has a relative weight. 54 | /// 55 | /// Note that "within" excludes points directly on the zone's boundary. If a point lies in more 56 | /// than one zone, it'll be assigned to any of those zones arbitrarily. (This means the input 57 | /// zones overlap.) 58 | WeightedPoints(Vec), 59 | } 60 | 61 | /// A point with an associated relative weight. Higher weights are more likely to be sampled. 62 | #[derive(Clone)] 63 | pub struct WeightedPoint { 64 | pub point: Point, 65 | pub weight: f64, 66 | } 67 | 68 | impl RTreeObject for WeightedPoint { 69 | type Envelope = AABB<[f64; 2]>; 70 | 71 | fn envelope(&self) -> Self::Envelope { 72 | AABB::from_point([self.point.x(), self.point.y()]) 73 | } 74 | } 75 | 76 | /// This method transforms aggregate origin/destination pairs into a disaggregated form, by 77 | /// sampling specific points from the zone. 78 | /// 79 | /// The input is a CSV file, with each row representing trips between an origin and destination, 80 | /// expressed as a named zone. The columns in the CSV file can break down the number of trips by 81 | /// different modes (like walking, cycling, etc). 82 | /// 83 | /// Each input row is repeated some number of times, based on `disaggregation_threshold`. If the 84 | /// row originally represents 100 trips and `disaggregation_threshold` is 5, then the row will be 85 | /// repeated 20 times. Each time, the origin and destination will be transformed from the entire 86 | /// zone to a specific point within the zone, determined using the specified `Subsample`. 87 | /// 88 | /// The output LineStrings are provided by callback. 89 | /// 90 | /// Note this assumes assumes all input is in the WGS84 coordinate system, and uses the Haversine 91 | /// formula to calculate distances. 92 | /// 93 | /// # Arguments 94 | /// 95 | /// * `disaggregation_threshold` - What's the maximum number of trips per output OD row that's 96 | /// allowed? If an input OD row contains less than this, it will appear in the output without 97 | /// transformation. Otherwise, the input row is repeated until the sum matches the original value, 98 | /// but each output row obeys this maximum. 99 | /// * `disaggregation_key` - Which column in the OD row specifies the total number of trips to 100 | /// disaggregate? 101 | pub fn jitter, F: FnMut(Feature) -> Result<()>>( 102 | csv_path: P, 103 | zones: &HashMap>, 104 | disaggregation_threshold: usize, 105 | disaggregation_key: String, 106 | rng: &mut StdRng, 107 | options: Options, 108 | mut output: F, 109 | ) -> Result<()> { 110 | // TODO Don't allow disaggregation_threshold to be 0 111 | let csv_path = csv_path.as_ref(); 112 | 113 | let points_per_origin_zone: Option>> = 114 | if let Subsample::WeightedPoints(points) = options.subsample_origin { 115 | Some(points_per_polygon(points, zones)) 116 | } else { 117 | None 118 | }; 119 | let points_per_destination_zone: Option>> = 120 | if let Subsample::WeightedPoints(points) = options.subsample_destination { 121 | Some(points_per_polygon(points, zones)) 122 | } else { 123 | None 124 | }; 125 | 126 | let mut seen_pairs: HashSet = HashSet::new(); 127 | 128 | println!("Disaggregating OD data"); 129 | for rec in csv::Reader::from_reader(File::open(csv_path)?).deserialize() { 130 | // It's tempting to deserialize directly into a serde_json::Map and 131 | // auto-detect strings and numbers. But sadly, some input data has zone names that look 132 | // numeric, and even contain leading zeros, which'll be lost. So first just grab raw 133 | // strings 134 | let string_map: HashMap = rec?; 135 | 136 | // How many times will we jitter this one row? 137 | let repeat = if let Some(count) = string_map 138 | .get(&disaggregation_key) 139 | .and_then(|count| count.parse::().ok()) 140 | { 141 | // If disaggregation_key is 0 for this row, don't scale the counts, but still preserve 142 | // the row (and jitter it just once) 143 | if count == 0.0 { 144 | 1.0 145 | } else { 146 | (count / disaggregation_threshold as f64).ceil() 147 | } 148 | } else { 149 | bail!( 150 | "{} doesn't have a {} column or the value isn't numeric; set disaggregation_key properly", 151 | csv_path.display(), 152 | disaggregation_key 153 | ); 154 | }; 155 | 156 | // Transform to a JSON map 157 | let mut json_map: Map = Map::new(); 158 | for (key, value) in string_map { 159 | let json_value = if key == options.origin_key || key == options.destination_key { 160 | // Never treat the origin/destination key as numeric 161 | Value::String(value) 162 | } else if let Ok(x) = value.parse::() { 163 | // Scale all of the numeric values. Note the unwrap is safe -- we should never wind 164 | // up with NaN or infinity 165 | Value::Number(serde_json::Number::from_f64(x / repeat).unwrap()) 166 | } else { 167 | Value::String(value) 168 | }; 169 | json_map.insert(key, json_value); 170 | } 171 | 172 | let origin_id = if let Some(Value::String(id)) = json_map.get(&options.origin_key) { 173 | id 174 | } else { 175 | bail!( 176 | "{} doesn't have a {} column; set origin_key properly", 177 | csv_path.display(), 178 | options.origin_key 179 | ); 180 | }; 181 | let destination_id = if let Some(Value::String(id)) = json_map.get(&options.destination_key) 182 | { 183 | id 184 | } else { 185 | bail!( 186 | "{} doesn't have a {} column; set destination_key properly", 187 | csv_path.display(), 188 | options.destination_key 189 | ); 190 | }; 191 | 192 | let origin_zone = if let Some(zone) = zones.get(origin_id) { 193 | zone 194 | } else { 195 | bail!("Unknown origin zone {origin_id}"); 196 | }; 197 | let destination_zone = if let Some(zone) = zones.get(destination_id) { 198 | zone 199 | } else { 200 | bail!("Unknown destination zone {destination_id}"); 201 | }; 202 | let origin_sampler = Subsampler::new(&points_per_origin_zone, origin_zone, &origin_id)?; 203 | let destination_sampler = Subsampler::new( 204 | &points_per_destination_zone, 205 | destination_zone, 206 | &destination_id, 207 | )?; 208 | 209 | if options.deduplicate_pairs { 210 | if let (Some(num_origin), Some(num_destination)) = ( 211 | origin_sampler.num_points(), 212 | destination_sampler.num_points(), 213 | ) { 214 | if repeat as usize > num_origin * num_destination { 215 | bail!("{repeat} unique pairs requested from {origin_id} ({num_origin} subpoints) to {destination_id} ({num_destination} subpoints), but this is impossible"); 216 | } 217 | } 218 | } 219 | 220 | for _ in 0..repeat as usize { 221 | loop { 222 | let o = origin_sampler.sample(rng); 223 | let d = destination_sampler.sample(rng); 224 | if o.haversine_distance(&d) >= options.min_distance_meters { 225 | if options.deduplicate_pairs { 226 | let pair = hashify(o, d); 227 | if seen_pairs.contains(&pair) { 228 | continue; 229 | } else { 230 | seen_pairs.insert(pair); 231 | } 232 | } 233 | 234 | output(to_geojson(o, d, json_map.clone()))?; 235 | break; 236 | } 237 | } 238 | } 239 | } 240 | Ok(()) 241 | } 242 | 243 | /// This method transforms aggregate origin/destination pairs into a fully disaggregated form, by 244 | /// sampling specific points from the zone. 245 | /// 246 | /// The input is a CSV file, with each row representing trips between an origin and destination, 247 | /// expressed as a named zone. All numeric columns in the CSV file are interpreted as a number of 248 | /// trips by different modes (like walking, cycling, etc). 249 | /// 250 | /// Each input row is repeated some number of times, based on the counts in each mode column. The 251 | /// output will have a new `mode` column set to that. 252 | /// 253 | /// The output LineStrings are provided by callback. 254 | /// 255 | /// Note this assumes assumes all input is in the WGS84 coordinate system, and uses the Haversine 256 | /// formula to calculate distances. 257 | /// 258 | pub fn disaggregate, F: FnMut(Feature) -> Result<()>>( 259 | csv_path: P, 260 | zones: &HashMap>, 261 | rng: &mut StdRng, 262 | options: Options, 263 | mut output: F, 264 | ) -> Result<()> { 265 | let csv_path = csv_path.as_ref(); 266 | 267 | let points_per_origin_zone: Option>> = 268 | if let Subsample::WeightedPoints(points) = options.subsample_origin { 269 | Some(points_per_polygon(points, zones)) 270 | } else { 271 | None 272 | }; 273 | let points_per_destination_zone: Option>> = 274 | if let Subsample::WeightedPoints(points) = options.subsample_destination { 275 | Some(points_per_polygon(points, zones)) 276 | } else { 277 | None 278 | }; 279 | 280 | println!("Disaggregating OD data"); 281 | for rec in csv::Reader::from_reader(File::open(csv_path)?).deserialize() { 282 | // It's tempting to deserialize directly into a serde_json::Map and 283 | // auto-detect strings and numbers. But sadly, some input data has zone names that look 284 | // numeric, and even contain leading zeros, which'll be lost. So first just grab raw 285 | // strings 286 | let mut string_map: HashMap = rec?; 287 | 288 | let origin_id = if let Some(id) = string_map.remove(&options.origin_key) { 289 | id 290 | } else { 291 | bail!( 292 | "{} doesn't have a {} column; set origin_key properly", 293 | csv_path.display(), 294 | options.origin_key 295 | ); 296 | }; 297 | let destination_id = if let Some(id) = string_map.remove(&options.destination_key) { 298 | id 299 | } else { 300 | bail!( 301 | "{} doesn't have a {} column; set destination_key properly", 302 | csv_path.display(), 303 | options.destination_key 304 | ); 305 | }; 306 | let origin_zone = if let Some(zone) = zones.get(&origin_id) { 307 | zone 308 | } else { 309 | bail!("Unknown origin zone {origin_id}"); 310 | }; 311 | let destination_zone = if let Some(zone) = zones.get(&destination_id) { 312 | zone 313 | } else { 314 | bail!("Unknown destination zone {destination_id}"); 315 | }; 316 | let origin_sampler = Subsampler::new(&points_per_origin_zone, origin_zone, &origin_id)?; 317 | let destination_sampler = Subsampler::new( 318 | &points_per_destination_zone, 319 | destination_zone, 320 | &destination_id, 321 | )?; 322 | 323 | let mut seen_pairs: HashSet = HashSet::new(); 324 | 325 | // Interpret all columns except origin_key and destination_key as numeric, split by mode 326 | for (mode, value) in string_map { 327 | if let Ok(count) = value.parse::() { 328 | // TODO How should we treat fractional input? 329 | let count = count as usize; 330 | 331 | if options.deduplicate_pairs { 332 | if let (Some(num_origin), Some(num_destination)) = ( 333 | origin_sampler.num_points(), 334 | destination_sampler.num_points(), 335 | ) { 336 | if count > num_origin * num_destination { 337 | bail!("{count} unique pairs requested for {mode} from {origin_id} ({num_origin} subpoints) to {destination_id} ({num_destination} subpoints), but this is impossible"); 338 | } 339 | } 340 | } 341 | 342 | for _ in 0..count { 343 | loop { 344 | let o = origin_sampler.sample(rng); 345 | let d = destination_sampler.sample(rng); 346 | if o.haversine_distance(&d) >= options.min_distance_meters { 347 | if options.deduplicate_pairs { 348 | let pair = hashify(o, d); 349 | if seen_pairs.contains(&pair) { 350 | continue; 351 | } else { 352 | seen_pairs.insert(pair); 353 | } 354 | } 355 | 356 | let mut json_map: Map = Map::new(); 357 | json_map.insert("mode".to_string(), Value::String(mode.clone())); 358 | output(to_geojson(o, d, json_map))?; 359 | break; 360 | } 361 | } 362 | } 363 | } 364 | } 365 | } 366 | Ok(()) 367 | } 368 | 369 | /// Extract multipolygon zones from a GeoJSON file, using the provided `name_key` as the key in the 370 | /// resulting map. 371 | pub fn load_zones( 372 | geojson_path: &str, 373 | name_key: &str, 374 | ) -> Result>> { 375 | let reader = FeatureReader::from_reader(BufReader::new(File::open(geojson_path)?)); 376 | let mut zones: HashMap> = HashMap::new(); 377 | for feature in reader.features() { 378 | let feature = feature?; 379 | if let Some(zone_name) = feature 380 | .property(name_key) 381 | .and_then(|x| x.as_str()) 382 | .map(|x| x.to_string()) 383 | { 384 | let gj_geom: geojson::Geometry = feature.geometry.unwrap(); 385 | let geo_geometry: geo_types::Geometry = gj_geom.try_into().unwrap(); 386 | if let geo_types::Geometry::MultiPolygon(mp) = geo_geometry { 387 | zones.insert(zone_name, mp); 388 | } else if let geo_types::Geometry::Polygon(p) = geo_geometry { 389 | zones.insert(zone_name, p.into()); 390 | } 391 | } else { 392 | bail!( 393 | "Feature doesn't have a string zone name {}: {:?}", 394 | name_key, 395 | feature 396 | ); 397 | } 398 | } 399 | Ok(zones) 400 | } 401 | 402 | // TODO Share with rampfs 403 | fn points_per_polygon( 404 | points: Vec, 405 | polygons: &HashMap>, 406 | ) -> BTreeMap> { 407 | let tree = RTree::bulk_load(points); 408 | 409 | let mut output = BTreeMap::new(); 410 | for (key, polygon) in polygons { 411 | let mut pts_inside = Vec::new(); 412 | let bounds = polygon.bounding_rect().unwrap(); 413 | let min = bounds.min(); 414 | let max = bounds.max(); 415 | let envelope: AABB<[f64; 2]> = AABB::from_corners([min.x, min.y], [max.x, max.y]); 416 | for pt in tree.locate_in_envelope(&envelope) { 417 | if polygon.contains(&pt.point) { 418 | pts_inside.push(pt.clone()); 419 | } 420 | } 421 | output.insert(key.clone(), pts_inside); 422 | } 423 | output 424 | } 425 | 426 | fn to_geojson(pt1: Point, pt2: Point, properties: Map) -> Feature { 427 | let line_string: LineString = vec![pt1, pt2].into(); 428 | Feature { 429 | geometry: Some(geojson::Geometry { 430 | value: geojson::Value::from(&line_string), 431 | bbox: None, 432 | foreign_members: None, 433 | }), 434 | properties: Some(properties), 435 | bbox: None, 436 | id: None, 437 | foreign_members: None, 438 | } 439 | } 440 | 441 | enum Subsampler<'a> { 442 | RandomPoints(&'a MultiPolygon, Rect), 443 | WeightedPoints(&'a Vec), 444 | } 445 | 446 | impl<'a> Subsampler<'a> { 447 | fn new( 448 | points_per_zone: &'a Option>>, 449 | zone_polygon: &'a MultiPolygon, 450 | zone_id: &str, 451 | ) -> Result> { 452 | if let Some(points_per_zone) = points_per_zone { 453 | if let Some(points) = points_per_zone.get(zone_id) { 454 | if !points.is_empty() { 455 | return Ok(Subsampler::WeightedPoints(points)); 456 | } 457 | } 458 | bail!("No subpoints for zone {}", zone_id); 459 | } else { 460 | match zone_polygon.bounding_rect() { 461 | Some(bounds) => Ok(Subsampler::RandomPoints(zone_polygon, bounds)), 462 | None => bail!("can't calculate bounding box for zone {}", zone_id), 463 | } 464 | } 465 | } 466 | 467 | fn sample(&self, rng: &mut StdRng) -> Point { 468 | match self { 469 | Subsampler::RandomPoints(polygon, bounds) => loop { 470 | let x = rng.gen_range(bounds.min().x..=bounds.max().x); 471 | let y = rng.gen_range(bounds.min().y..=bounds.max().y); 472 | let pt = Point::new(x, y); 473 | if polygon.contains(&pt) { 474 | return pt; 475 | } 476 | }, 477 | Subsampler::WeightedPoints(points) => { 478 | // TODO Sample with replacement or not? 479 | // TODO If there are no two subpoints that're greater than this distance, we'll 480 | // infinite loop. Detect upfront, or maybe just give up after a fixed number of 481 | // attempts? 482 | points.choose_weighted(rng, |pt| pt.weight).unwrap().point 483 | } 484 | } 485 | } 486 | 487 | /// No result for random points in a polygon (infinite, unless the polygon is extremely 488 | /// degenerate). For weighted points, returns the number of them. 489 | fn num_points(&self) -> Option { 490 | match self { 491 | Subsampler::RandomPoints(_, _) => None, 492 | Subsampler::WeightedPoints(points) => Some(points.len()), 493 | } 494 | } 495 | } 496 | 497 | type ODPair = [NotNan; 4]; 498 | fn hashify(o: Point, d: Point) -> ODPair { 499 | // We can't collect into an array, so write this a bit manually 500 | [ 501 | NotNan::new(o.x()).unwrap(), 502 | NotNan::new(o.y()).unwrap(), 503 | NotNan::new(d.x()).unwrap(), 504 | NotNan::new(d.y()).unwrap(), 505 | ] 506 | } 507 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufWriter; 2 | 3 | use anyhow::Result; 4 | use clap::Parser; 5 | use fs_err::File; 6 | use geojson::FeatureWriter; 7 | use rand::rngs::StdRng; 8 | use rand::SeedableRng; 9 | 10 | #[derive(Parser)] 11 | #[clap(about, version, author)] 12 | struct Args { 13 | #[clap(subcommand)] 14 | action: Action, 15 | } 16 | 17 | #[derive(clap::Subcommand)] 18 | enum Action { 19 | /// Import raw data and build an activity model for a region 20 | Jitter { 21 | #[clap(flatten)] 22 | common: CommonArgs, 23 | 24 | /// What's the maximum number of trips per output OD row that's allowed? If an input OD row 25 | /// contains less than this, it will appear in the output without transformation. Otherwise, 26 | /// the input row is repeated until the sum matches the original value, but each output row 27 | /// obeys this maximum. 28 | #[clap(long)] 29 | disaggregation_threshold: usize, 30 | 31 | /// Which column in the OD row specifies the total number of trips to disaggregate? 32 | #[clap(long, default_value = "all")] 33 | disaggregation_key: String, 34 | }, 35 | /// Fully disaggregate input desire lines into output representing one trip each, with a `mode` 36 | /// column. 37 | Disaggregate { 38 | #[clap(flatten)] 39 | common: CommonArgs, 40 | }, 41 | } 42 | 43 | #[derive(Clone, Parser)] 44 | struct CommonArgs { 45 | /// The path to a CSV file with aggregated origin/destination data 46 | #[clap(long)] 47 | od_csv_path: String, 48 | 49 | /// The path to a GeoJSON file with named zones 50 | #[clap(long)] 51 | zones_path: String, 52 | 53 | /// The path to a file where the output will be written 54 | #[clap(long)] 55 | output_path: String, 56 | 57 | /// Output a FlatGeobuf file (without an index) if true, or a GeoJSON file by default 58 | #[clap(long)] 59 | output_fgb: bool, 60 | 61 | /// The path to a GeoJSON file to use for sampling subpoints for origin zones. If this isn't 62 | /// specified, random points within each zone will be used instead. 63 | #[clap(long)] 64 | subpoints_origins_path: Option, 65 | /// If specified, this column will be used to more frequently choose subpoints in 66 | /// `subpoints_origins_path` with a higher weight value. Otherwise all subpoints will be 67 | /// equally likely to be chosen. 68 | #[clap(long)] 69 | weight_key_origins: Option, 70 | 71 | /// The path to a GeoJSON file to use for sampling subpoints for destination zones. If this 72 | /// isn't specified, random points within each zone will be used instead. 73 | #[clap(long)] 74 | subpoints_destinations_path: Option, 75 | /// If specified, this column will be used to more frequently choose subpoints in 76 | /// `subpoints_destinations_path` with a higher weight value. Otherwise all subpoints will be 77 | /// equally likely to be chosen. 78 | #[clap(long)] 79 | weight_key_destinations: Option, 80 | 81 | /// In the zones GeoJSON file, which property is the name of a zone 82 | #[clap(long, default_value = "InterZone")] 83 | zone_name_key: String, 84 | /// Which column in the OD row specifies the zone where trips originate? 85 | #[clap(long, default_value = "geo_code1")] 86 | origin_key: String, 87 | /// Which column in the OD row specifies the zone where trips ends? 88 | #[clap(long, default_value = "geo_code2")] 89 | destination_key: String, 90 | /// By default, the output will be different every time the tool is run, based on a different 91 | /// random number generator seed. Specify this to get deterministic behavior, given the same 92 | /// input. 93 | #[clap(long)] 94 | rng_seed: Option, 95 | /// Guarantee that jittered origin and destination points are at least this distance apart. 96 | #[clap(long, default_value = "1.0")] 97 | min_distance_meters: f64, 98 | /// Prevent duplicate (origin, destination) pairs from appearing in the output. This may 99 | /// increase memory and runtime requirements. Note the duplication uses the floating point 100 | /// precision of the input data, and only consider geometry (not any properties). 101 | #[clap(long)] 102 | deduplicate_pairs: bool, 103 | } 104 | 105 | fn main() -> Result<()> { 106 | let args = Args::parse(); 107 | // TODO Remove the clone 108 | let common = match args.action { 109 | Action::Jitter { ref common, .. } => common.clone(), 110 | Action::Disaggregate { ref common, .. } => common.clone(), 111 | }; 112 | let output_path = common.output_path.clone(); 113 | 114 | if common.output_fgb { 115 | let mut fgb = flatgeobuf::FgbWriter::create_with_options( 116 | "odjitter", 117 | flatgeobuf::GeometryType::LineString, 118 | flatgeobuf::FgbWriterOptions { 119 | write_index: false, 120 | ..Default::default() 121 | }, 122 | )?; 123 | let write_feature = |feature| { 124 | // TODO Is there a cheaper way to make a GeozeroDatasource, or something else we should 125 | // generate from the API? 126 | fgb.add_feature(geozero::geojson::GeoJson(&serde_json::to_string(&feature)?))?; 127 | Ok(()) 128 | }; 129 | run(args, common, write_feature)?; 130 | println!("Writing {output_path}"); 131 | let mut file = std::io::BufWriter::new(File::create(&output_path)?); 132 | fgb.write(&mut file)?; 133 | } else { 134 | // Write GeoJSON to a file. Instead of collecting the whole FeatureCollection in memory, write 135 | // each feature as we get it. 136 | let mut writer = FeatureWriter::from_writer(BufWriter::new(File::create(&output_path)?)); 137 | let write_feature = |feature| { 138 | writer.write_feature(&feature)?; 139 | Ok(()) 140 | }; 141 | 142 | run(args, common, write_feature)?; 143 | } 144 | 145 | println!("Wrote {output_path}"); 146 | Ok(()) 147 | } 148 | 149 | fn run Result<()>>( 150 | args: Args, 151 | common: CommonArgs, 152 | write_feature: F, 153 | ) -> Result<()> { 154 | let zones = odjitter::load_zones(&common.zones_path, &common.zone_name_key)?; 155 | println!("Scraped {} zones from {}", zones.len(), common.zones_path); 156 | 157 | let subsample_origin = if let Some(ref path) = common.subpoints_origins_path { 158 | let subpoints = odjitter::scrape_points(path, common.weight_key_origins)?; 159 | println!("Scraped {} subpoints from {}", subpoints.len(), path); 160 | odjitter::Subsample::WeightedPoints(subpoints) 161 | } else { 162 | odjitter::Subsample::RandomPoints 163 | }; 164 | let subsample_destination = if let Some(ref path) = common.subpoints_destinations_path { 165 | let subpoints = odjitter::scrape_points(path, common.weight_key_destinations)?; 166 | println!("Scraped {} subpoints from {}", subpoints.len(), path); 167 | odjitter::Subsample::WeightedPoints(subpoints) 168 | } else { 169 | odjitter::Subsample::RandomPoints 170 | }; 171 | 172 | let options = odjitter::Options { 173 | subsample_origin, 174 | subsample_destination, 175 | origin_key: common.origin_key, 176 | destination_key: common.destination_key, 177 | min_distance_meters: common.min_distance_meters, 178 | deduplicate_pairs: common.deduplicate_pairs, 179 | }; 180 | let mut rng = if let Some(seed) = common.rng_seed { 181 | StdRng::seed_from_u64(seed) 182 | } else { 183 | StdRng::from_entropy() 184 | }; 185 | 186 | match args.action { 187 | Action::Jitter { 188 | disaggregation_threshold, 189 | disaggregation_key, 190 | .. 191 | } => { 192 | odjitter::jitter( 193 | common.od_csv_path, 194 | &zones, 195 | disaggregation_threshold, 196 | disaggregation_key, 197 | &mut rng, 198 | options, 199 | write_feature, 200 | )?; 201 | } 202 | Action::Disaggregate { .. } => { 203 | odjitter::disaggregate(common.od_csv_path, &zones, &mut rng, options, write_feature)?; 204 | } 205 | } 206 | Ok(()) 207 | } 208 | -------------------------------------------------------------------------------- /src/scrape.rs: -------------------------------------------------------------------------------- 1 | use std::io::BufReader; 2 | 3 | use anyhow::{bail, Result}; 4 | use fs_err::File; 5 | use geo::CoordsIter; 6 | use geo_types::Geometry; 7 | use geojson::FeatureReader; 8 | 9 | use crate::WeightedPoint; 10 | 11 | /// Extract all points from a GeoJSON file. If `weight_key` is specified, use this numeric property 12 | /// per feature as a relative weight for the point. If unspecified, every point will be equally 13 | /// weighted. 14 | /// 15 | /// TODO: Note that the returned points are not deduplicated. 16 | pub fn scrape_points(path: &str, weight_key: Option) -> Result> { 17 | let reader = FeatureReader::from_reader(BufReader::new(File::open(path)?)); 18 | let mut points = Vec::new(); 19 | for feature in reader.features() { 20 | let feature = feature?; 21 | let weight = if let Some(ref key) = weight_key { 22 | if let Some(weight) = feature.property(key).and_then(|x| x.as_f64()) { 23 | weight 24 | } else { 25 | bail!("Feature doesn't have a numeric {} key: {:?}", key, feature); 26 | } 27 | } else { 28 | 1.0 29 | }; 30 | if let Some(geom) = feature.geometry { 31 | let geom: Geometry = geom.try_into()?; 32 | for pt in geom.coords_iter() { 33 | points.push(WeightedPoint { 34 | point: pt.into(), 35 | weight, 36 | }); 37 | } 38 | } 39 | } 40 | Ok(points) 41 | } 42 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use geo_types::Point; 4 | use geojson::Feature; 5 | use ordered_float::NotNan; 6 | use rand::rngs::StdRng; 7 | use rand::SeedableRng; 8 | use serde_json::{Map, Value}; 9 | 10 | use crate::{disaggregate, jitter, load_zones, scrape_points, Options, Subsample}; 11 | 12 | #[test] 13 | fn test_sums_match() { 14 | let zones = load_zones("data/zones.geojson", "InterZone").unwrap(); 15 | let input_sums = sum_trips_input("data/od.csv", &["all", "car_driver", "foot"]); 16 | 17 | for disaggregation_threshold in [1, 10, 100, 1000] { 18 | let subpoints = scrape_points("data/road_network.geojson", None).unwrap(); 19 | let options = Options { 20 | subsample_origin: Subsample::WeightedPoints(subpoints.clone()), 21 | subsample_destination: Subsample::WeightedPoints(subpoints), 22 | origin_key: "geo_code1".to_string(), 23 | destination_key: "geo_code2".to_string(), 24 | min_distance_meters: 1.0, 25 | deduplicate_pairs: false, 26 | }; 27 | let mut rng = StdRng::seed_from_u64(42); 28 | let mut output = Vec::new(); 29 | let disaggregation_key = "all".to_string(); 30 | jitter( 31 | "data/od.csv", 32 | &zones, 33 | disaggregation_threshold, 34 | disaggregation_key, 35 | &mut rng, 36 | options, 37 | |feature| { 38 | output.push(feature); 39 | Ok(()) 40 | }, 41 | ) 42 | .unwrap(); 43 | 44 | for (column, input_sum) in &input_sums { 45 | let input_sum = *input_sum; 46 | let output_sum = sum_trips_output(&output, column); 47 | let epsilon = 1e-6; 48 | assert!( 49 | (input_sum - output_sum).abs() < epsilon, 50 | "Number of {} trips in input {} and jittered output {} don't match for disaggregation_threshold = {}", 51 | column, 52 | input_sum, 53 | output_sum, 54 | disaggregation_threshold 55 | ); 56 | } 57 | } 58 | } 59 | 60 | #[test] 61 | fn test_different_subpoints() { 62 | let zones = load_zones("data/zones.geojson", "InterZone").unwrap(); 63 | let destination_subpoints = 64 | scrape_points("data/schools.geojson", Some("weight".to_string())).unwrap(); 65 | // Keep a copy of the schools as a set 66 | let schools: HashSet<_> = destination_subpoints 67 | .iter() 68 | .map(|pt| hashify_point(pt.point)) 69 | .collect(); 70 | 71 | let options = Options { 72 | subsample_origin: Subsample::RandomPoints, 73 | subsample_destination: Subsample::WeightedPoints(destination_subpoints), 74 | origin_key: "origin".to_string(), 75 | destination_key: "destination".to_string(), 76 | min_distance_meters: 1.0, 77 | deduplicate_pairs: false, 78 | }; 79 | let disaggregation_threshold = 1; 80 | let disaggregation_key = "walk".to_string(); 81 | let mut rng = StdRng::seed_from_u64(42); 82 | let mut output = Vec::new(); 83 | jitter( 84 | "data/od_schools.csv", 85 | &zones, 86 | disaggregation_threshold, 87 | disaggregation_key, 88 | &mut rng, 89 | options, 90 | |feature| { 91 | output.push(feature); 92 | Ok(()) 93 | }, 94 | ) 95 | .unwrap(); 96 | 97 | // Verify that all destinations match one of the schools 98 | for feature in &output { 99 | if let Some(geojson::Value::LineString(ls)) = 100 | feature.geometry.as_ref().map(|geom| &geom.value) 101 | { 102 | let pt = ls.last().unwrap(); 103 | if !schools.contains(&hashify_point(Point::new(pt[0], pt[1]))) { 104 | panic!( 105 | "An output feature doesn't end at a school subpoint: {:?}", 106 | feature 107 | ); 108 | } 109 | } else { 110 | panic!("Output geometry isn't a LineString: {:?}", feature.geometry); 111 | } 112 | } 113 | 114 | // Also make sure sums match, so rows are preserved properly. This input data has 0 for some 115 | // disaggregation_key rows. (Ideally this would be a separate test) 116 | let input_sums = sum_trips_input("data/od_schools.csv", &["walk", "bike", "other", "car"]); 117 | for (column, input_sum) in input_sums { 118 | let output_sum = sum_trips_output(&output, &column); 119 | let epsilon = 1e-6; 120 | assert!( 121 | (input_sum - output_sum).abs() < epsilon, 122 | "Number of {} trips in input {} and jittered output {} don't match", 123 | column, 124 | input_sum, 125 | output_sum, 126 | ); 127 | } 128 | } 129 | 130 | #[test] 131 | fn test_disaggregate() { 132 | let zones = load_zones("data/zones.geojson", "InterZone").unwrap(); 133 | let options = Options { 134 | subsample_origin: Subsample::RandomPoints, 135 | subsample_destination: Subsample::RandomPoints, 136 | origin_key: "geo_code1".to_string(), 137 | destination_key: "geo_code2".to_string(), 138 | min_distance_meters: 1.0, 139 | deduplicate_pairs: false, 140 | }; 141 | let mut rng = StdRng::seed_from_u64(42); 142 | let mut output = Vec::new(); 143 | disaggregate("data/od.csv", &zones, &mut rng, options, |feature| { 144 | output.push(feature); 145 | Ok(()) 146 | }) 147 | .unwrap(); 148 | 149 | // Note "all" has no special meaning to the disaggregate call. The user should probably remove 150 | // it from the input or ignore it in the output. 151 | let input_sums = sum_trips_input("data/od.csv", &["all", "car_driver", "foot"]); 152 | let mut sums_per_mode: HashMap = HashMap::new(); 153 | for feature in output { 154 | let mode = feature 155 | .property("mode") 156 | .unwrap() 157 | .as_str() 158 | .unwrap() 159 | .to_string(); 160 | *sums_per_mode.entry(mode).or_insert(0) += 1; 161 | } 162 | for (mode, input_sum) in input_sums { 163 | let output_sum = sums_per_mode[&mode]; 164 | assert!( 165 | input_sum as usize == output_sum, 166 | "Number of {} trips in input {} and disaggregated output {} don't match", 167 | mode, 168 | input_sum, 169 | output_sum, 170 | ); 171 | } 172 | } 173 | 174 | #[test] 175 | fn test_deduplicate_pairs() { 176 | let zones = load_zones("data/zones.geojson", "InterZone").unwrap(); 177 | let subpoints = scrape_points("data/road_network.geojson", None).unwrap(); 178 | 179 | for deduplicate_pairs in [false, true] { 180 | let options = Options { 181 | subsample_origin: Subsample::WeightedPoints(subpoints.clone()), 182 | subsample_destination: Subsample::WeightedPoints(subpoints.clone()), 183 | origin_key: "geo_code1".to_string(), 184 | destination_key: "geo_code2".to_string(), 185 | min_distance_meters: 1.0, 186 | deduplicate_pairs, 187 | }; 188 | let mut rng = StdRng::seed_from_u64(42); 189 | let mut output = Vec::new(); 190 | let disaggregation_threshold = 1; 191 | let disaggregation_key = "all".to_string(); 192 | jitter( 193 | "data/od.csv", 194 | &zones, 195 | disaggregation_threshold, 196 | disaggregation_key, 197 | &mut rng, 198 | options, 199 | |feature| { 200 | output.push(feature); 201 | Ok(()) 202 | }, 203 | ) 204 | .unwrap(); 205 | 206 | let mut unique_pairs: HashSet>> = HashSet::new(); 207 | 208 | for feature in &output { 209 | if let Some(geojson::Value::LineString(ls)) = 210 | feature.geometry.as_ref().map(|geom| &geom.value) 211 | { 212 | unique_pairs.insert( 213 | ls.iter() 214 | .flatten() 215 | .map(|x| NotNan::new(*x).unwrap()) 216 | .collect(), 217 | ); 218 | } 219 | } 220 | 221 | let anything_deduped = output.len() != unique_pairs.len(); 222 | if anything_deduped == deduplicate_pairs { 223 | panic!( 224 | "With deduplicate_pairs={}, we got {} LineStrings, with {} unique geometries", 225 | deduplicate_pairs, 226 | output.len(), 227 | unique_pairs.len() 228 | ); 229 | } 230 | } 231 | } 232 | 233 | // TODO Test zone names that look numeric and contain leading 0's 234 | 235 | fn sum_trips_input(csv_path: &str, keys: &[&str]) -> HashMap { 236 | let mut totals = HashMap::new(); 237 | for key in keys { 238 | totals.insert(key.to_string(), 0.0); 239 | } 240 | for rec in csv::Reader::from_path(csv_path).unwrap().deserialize() { 241 | let map: Map = rec.unwrap(); 242 | for key in keys { 243 | if let Value::Number(x) = &map[*key] { 244 | // or_insert is redundant 245 | let total = totals.entry(key.to_string()).or_insert(0.0); 246 | *total += x.as_f64().unwrap(); 247 | } 248 | } 249 | } 250 | totals 251 | } 252 | 253 | // TODO Refactor helpers -- probably also return a HashMap here 254 | fn sum_trips_output(features: &[Feature], disaggregation_key: &str) -> f64 { 255 | let mut total = 0.0; 256 | for feature in features { 257 | total += feature 258 | .property(disaggregation_key) 259 | .unwrap() 260 | .as_f64() 261 | .unwrap(); 262 | } 263 | total 264 | } 265 | 266 | fn hashify_point(pt: Point) -> Point> { 267 | Point::new(NotNan::new(pt.x()).unwrap(), NotNan::new(pt.y()).unwrap()) 268 | } 269 | --------------------------------------------------------------------------------