├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENCE.txt ├── README.md ├── example.osh.pbf └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | /*.[tc]sv 4 | /*.[tc]sv.gz 5 | /*.[tc]sv.zst 6 | .*.swp 7 | /*.opl 8 | /*.osm 9 | /*.osm.pbf 10 | /*.osm.xml 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.8.0 2 | 3 | * Update dependencies 4 | * Attempted performance improvements with SmolStr & SmallVec. 5 | 6 | # v0.7.0 - 2025-05-06 7 | 8 | * `-t/`--tag` option changed to accept a KEY=VALUE argument and matches on the 9 | key & value of the OSM tag. 10 | `-k`/`--key` works the way `--tag` used to work. 11 | * `tag\_count\_delta` is now included by default 12 | 13 | # v0.6.0 - 26 October 2023 14 | 15 | * Minor --help output improvements 16 | 17 | # v0.5.0 - 26 March 2022 18 | 19 | * Output in TSV format possible 20 | * Output columns can now be specified on the command line, and reordered. The 21 | method to specify epoch timestamps & changeset tags has been changed 22 | * New output columns: 23 | * `tag_count_delta` which can easy tell you if the tag was 24 | added, removed, or merely changed 25 | * `raw_id` ID of the object just as a number 26 | * `object_type_short` & `object_type_long` the object type (either `n`/`w`/`r`, or `node`/`way`/`relation`) 27 | * Can now filter by object type with `-T nwr`/`--object-type nwr` 28 | 29 | # v0.4.0 - 8 November 2021 30 | 31 | * Add `--tag` to only show changes that affect specific OSM tags. Useful to 32 | create smaller CSV files 33 | * Add `--changeset-tag` to include a column for the tag of the changeset that 34 | made that change, which needs a pre-processed changeset file created by `osmio`. 35 | * Update `osmio` dependency 36 | * Improvments to `--help` (etc.) output 37 | 38 | # v0.3.0 - 21 Jan 2020 39 | 40 | * The output timestamp can be switched to unix epoch timestamp format, which is 41 | ~15+ faster. Default is still the regular RFC3339 format 42 | 43 | * Improvements to info messages printed to user: 44 | 45 | * Use `--log-frequency SEC` to control how often to print status message 46 | * Info message at end with how long it took to run 47 | * Progress messages include the ETA & how long it'll take to run 48 | 49 | * Internal refactorings, resulting in increased performance 50 | 51 | # v0.2.0 - 11 Jan 2020 52 | 53 | * Escape newlines etc. 54 | * Reorder columns, swapping `new_value` & `old_value`, & `new_version` & `old_version` 55 | * New `id` column format, merging object type & id 56 | 57 | # v0.1.0 - 6 Jan 2020 58 | 59 | * Initial version 60 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "adler2" 7 | version = "2.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "1.1.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "anstream" 37 | version = "0.6.18" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 40 | dependencies = [ 41 | "anstyle", 42 | "anstyle-parse", 43 | "anstyle-query", 44 | "anstyle-wincon", 45 | "colorchoice", 46 | "is_terminal_polyfill", 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle" 52 | version = "1.0.10" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 55 | 56 | [[package]] 57 | name = "anstyle-parse" 58 | version = "0.2.6" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 61 | dependencies = [ 62 | "utf8parse", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle-query" 67 | version = "1.1.2" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 70 | dependencies = [ 71 | "windows-sys", 72 | ] 73 | 74 | [[package]] 75 | name = "anstyle-wincon" 76 | version = "3.0.7" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 79 | dependencies = [ 80 | "anstyle", 81 | "once_cell", 82 | "windows-sys", 83 | ] 84 | 85 | [[package]] 86 | name = "anyhow" 87 | version = "1.0.98" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 90 | 91 | [[package]] 92 | name = "atty" 93 | version = "0.2.14" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 96 | dependencies = [ 97 | "hermit-abi", 98 | "libc", 99 | "winapi", 100 | ] 101 | 102 | [[package]] 103 | name = "autocfg" 104 | version = "1.4.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 107 | 108 | [[package]] 109 | name = "bitflags" 110 | version = "1.3.2" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 113 | 114 | [[package]] 115 | name = "bitflags" 116 | version = "2.9.0" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 119 | 120 | [[package]] 121 | name = "borsh" 122 | version = "1.5.7" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" 125 | dependencies = [ 126 | "cfg_aliases", 127 | ] 128 | 129 | [[package]] 130 | name = "bumpalo" 131 | version = "3.17.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 134 | 135 | [[package]] 136 | name = "byteorder" 137 | version = "1.5.0" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 140 | 141 | [[package]] 142 | name = "bytes" 143 | version = "0.4.12" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" 146 | dependencies = [ 147 | "byteorder", 148 | "iovec", 149 | ] 150 | 151 | [[package]] 152 | name = "bzip2" 153 | version = "0.4.4" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" 156 | dependencies = [ 157 | "bzip2-sys", 158 | "libc", 159 | ] 160 | 161 | [[package]] 162 | name = "bzip2-sys" 163 | version = "0.1.13+1.0.8" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" 166 | dependencies = [ 167 | "cc", 168 | "pkg-config", 169 | ] 170 | 171 | [[package]] 172 | name = "cc" 173 | version = "1.2.21" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" 176 | dependencies = [ 177 | "shlex", 178 | ] 179 | 180 | [[package]] 181 | name = "cfg-if" 182 | version = "1.0.0" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 185 | 186 | [[package]] 187 | name = "cfg_aliases" 188 | version = "0.2.1" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 191 | 192 | [[package]] 193 | name = "chrono" 194 | version = "0.4.41" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 197 | dependencies = [ 198 | "android-tzdata", 199 | "iana-time-zone", 200 | "js-sys", 201 | "num-traits", 202 | "wasm-bindgen", 203 | "windows-link", 204 | ] 205 | 206 | [[package]] 207 | name = "clap" 208 | version = "3.2.25" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" 211 | dependencies = [ 212 | "atty", 213 | "bitflags 1.3.2", 214 | "clap_lex", 215 | "indexmap", 216 | "strsim 0.10.0", 217 | "termcolor", 218 | "textwrap", 219 | ] 220 | 221 | [[package]] 222 | name = "clap_lex" 223 | version = "0.2.4" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 226 | dependencies = [ 227 | "os_str_bytes", 228 | ] 229 | 230 | [[package]] 231 | name = "colorchoice" 232 | version = "1.0.3" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 235 | 236 | [[package]] 237 | name = "core-foundation-sys" 238 | version = "0.8.7" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 241 | 242 | [[package]] 243 | name = "crc32fast" 244 | version = "1.4.2" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 247 | dependencies = [ 248 | "cfg-if", 249 | ] 250 | 251 | [[package]] 252 | name = "csv" 253 | version = "1.3.1" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" 256 | dependencies = [ 257 | "csv-core", 258 | "itoa", 259 | "ryu", 260 | "serde", 261 | ] 262 | 263 | [[package]] 264 | name = "csv-core" 265 | version = "0.1.12" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" 268 | dependencies = [ 269 | "memchr", 270 | ] 271 | 272 | [[package]] 273 | name = "darling" 274 | version = "0.20.11" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 277 | dependencies = [ 278 | "darling_core", 279 | "darling_macro", 280 | ] 281 | 282 | [[package]] 283 | name = "darling_core" 284 | version = "0.20.11" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 287 | dependencies = [ 288 | "fnv", 289 | "ident_case", 290 | "proc-macro2", 291 | "quote", 292 | "strsim 0.11.1", 293 | "syn", 294 | ] 295 | 296 | [[package]] 297 | name = "darling_macro" 298 | version = "0.20.11" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 301 | dependencies = [ 302 | "darling_core", 303 | "quote", 304 | "syn", 305 | ] 306 | 307 | [[package]] 308 | name = "derive_builder" 309 | version = "0.20.2" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" 312 | dependencies = [ 313 | "derive_builder_macro", 314 | ] 315 | 316 | [[package]] 317 | name = "derive_builder_core" 318 | version = "0.20.2" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" 321 | dependencies = [ 322 | "darling", 323 | "proc-macro2", 324 | "quote", 325 | "syn", 326 | ] 327 | 328 | [[package]] 329 | name = "derive_builder_macro" 330 | version = "0.20.2" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" 333 | dependencies = [ 334 | "derive_builder_core", 335 | "syn", 336 | ] 337 | 338 | [[package]] 339 | name = "do_every" 340 | version = "0.1.0" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "b3dbbe5c75b0897e82a36a419c329c1e7becea7ee2aa59515609008b4db95ad6" 343 | 344 | [[package]] 345 | name = "env_filter" 346 | version = "0.1.3" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 349 | dependencies = [ 350 | "log", 351 | "regex", 352 | ] 353 | 354 | [[package]] 355 | name = "env_logger" 356 | version = "0.11.8" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 359 | dependencies = [ 360 | "anstream", 361 | "anstyle", 362 | "env_filter", 363 | "jiff", 364 | "log", 365 | ] 366 | 367 | [[package]] 368 | name = "fallible-iterator" 369 | version = "0.3.0" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" 372 | 373 | [[package]] 374 | name = "fallible-streaming-iterator" 375 | version = "0.1.9" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 378 | 379 | [[package]] 380 | name = "flate2" 381 | version = "1.1.1" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 384 | dependencies = [ 385 | "crc32fast", 386 | "miniz_oxide", 387 | ] 388 | 389 | [[package]] 390 | name = "fnv" 391 | version = "1.0.7" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 394 | 395 | [[package]] 396 | name = "foldhash" 397 | version = "0.1.5" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 400 | 401 | [[package]] 402 | name = "hashbrown" 403 | version = "0.12.3" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 406 | 407 | [[package]] 408 | name = "hashbrown" 409 | version = "0.15.3" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 412 | dependencies = [ 413 | "foldhash", 414 | ] 415 | 416 | [[package]] 417 | name = "hashlink" 418 | version = "0.10.0" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 421 | dependencies = [ 422 | "hashbrown 0.15.3", 423 | ] 424 | 425 | [[package]] 426 | name = "hermit-abi" 427 | version = "0.1.19" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 430 | dependencies = [ 431 | "libc", 432 | ] 433 | 434 | [[package]] 435 | name = "iana-time-zone" 436 | version = "0.1.63" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 439 | dependencies = [ 440 | "android_system_properties", 441 | "core-foundation-sys", 442 | "iana-time-zone-haiku", 443 | "js-sys", 444 | "log", 445 | "wasm-bindgen", 446 | "windows-core", 447 | ] 448 | 449 | [[package]] 450 | name = "iana-time-zone-haiku" 451 | version = "0.1.2" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 454 | dependencies = [ 455 | "cc", 456 | ] 457 | 458 | [[package]] 459 | name = "ident_case" 460 | version = "1.0.1" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 463 | 464 | [[package]] 465 | name = "indexmap" 466 | version = "1.9.3" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 469 | dependencies = [ 470 | "autocfg", 471 | "hashbrown 0.12.3", 472 | ] 473 | 474 | [[package]] 475 | name = "iovec" 476 | version = "0.1.4" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" 479 | dependencies = [ 480 | "libc", 481 | ] 482 | 483 | [[package]] 484 | name = "is_terminal_polyfill" 485 | version = "1.70.1" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 488 | 489 | [[package]] 490 | name = "iter-progress" 491 | version = "0.8.0" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "97059d64dd4e3a8e16696f6c0be50c1d5da3a709983f39b73fd7f84f120c5cd4" 494 | 495 | [[package]] 496 | name = "itoa" 497 | version = "1.0.15" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 500 | 501 | [[package]] 502 | name = "jiff" 503 | version = "0.2.13" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "f02000660d30638906021176af16b17498bd0d12813dbfe7b276d8bc7f3c0806" 506 | dependencies = [ 507 | "jiff-static", 508 | "log", 509 | "portable-atomic", 510 | "portable-atomic-util", 511 | "serde", 512 | ] 513 | 514 | [[package]] 515 | name = "jiff-static" 516 | version = "0.2.13" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "f3c30758ddd7188629c6713fc45d1188af4f44c90582311d0c8d8c9907f60c48" 519 | dependencies = [ 520 | "proc-macro2", 521 | "quote", 522 | "syn", 523 | ] 524 | 525 | [[package]] 526 | name = "js-sys" 527 | version = "0.3.77" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 530 | dependencies = [ 531 | "once_cell", 532 | "wasm-bindgen", 533 | ] 534 | 535 | [[package]] 536 | name = "libc" 537 | version = "0.2.172" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 540 | 541 | [[package]] 542 | name = "libsqlite3-sys" 543 | version = "0.33.0" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa" 546 | dependencies = [ 547 | "pkg-config", 548 | "vcpkg", 549 | ] 550 | 551 | [[package]] 552 | name = "log" 553 | version = "0.4.27" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 556 | 557 | [[package]] 558 | name = "memchr" 559 | version = "2.7.4" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 562 | 563 | [[package]] 564 | name = "miniz_oxide" 565 | version = "0.8.8" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 568 | dependencies = [ 569 | "adler2", 570 | ] 571 | 572 | [[package]] 573 | name = "num-traits" 574 | version = "0.2.19" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 577 | dependencies = [ 578 | "autocfg", 579 | ] 580 | 581 | [[package]] 582 | name = "once_cell" 583 | version = "1.21.3" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 586 | 587 | [[package]] 588 | name = "os_str_bytes" 589 | version = "6.6.1" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" 592 | 593 | [[package]] 594 | name = "osm-tag-csv-history" 595 | version = "0.8.0-rc1" 596 | dependencies = [ 597 | "anyhow", 598 | "clap", 599 | "csv", 600 | "do_every", 601 | "env_logger", 602 | "flate2", 603 | "log", 604 | "osmio", 605 | "read-progress", 606 | "rusqlite", 607 | "serde_json", 608 | "smallvec", 609 | "smol_str", 610 | ] 611 | 612 | [[package]] 613 | name = "osmio" 614 | version = "0.15.0-rc1" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "03529e3c83036bbd646dd47b51b5fd7fd81b3dc97630bc86c61e2db7888d7756" 617 | dependencies = [ 618 | "anyhow", 619 | "byteorder", 620 | "bzip2", 621 | "chrono", 622 | "derive_builder", 623 | "flate2", 624 | "iter-progress", 625 | "protobuf", 626 | "quick-xml", 627 | "separator", 628 | "serde", 629 | "serde_json", 630 | "smallvec", 631 | "smol_str", 632 | "xml-rs", 633 | ] 634 | 635 | [[package]] 636 | name = "pkg-config" 637 | version = "0.3.32" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 640 | 641 | [[package]] 642 | name = "portable-atomic" 643 | version = "1.11.0" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 646 | 647 | [[package]] 648 | name = "portable-atomic-util" 649 | version = "0.2.4" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 652 | dependencies = [ 653 | "portable-atomic", 654 | ] 655 | 656 | [[package]] 657 | name = "proc-macro2" 658 | version = "1.0.95" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 661 | dependencies = [ 662 | "unicode-ident", 663 | ] 664 | 665 | [[package]] 666 | name = "protobuf" 667 | version = "2.8.2" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "70731852eec72c56d11226c8a5f96ad5058a3dab73647ca5f7ee351e464f2571" 670 | dependencies = [ 671 | "bytes", 672 | ] 673 | 674 | [[package]] 675 | name = "quick-xml" 676 | version = "0.33.0" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "0ca7dd09b5f4a9029c35e323b086d0a68acdc673317b9c4d002c6f1d4a7278c6" 679 | dependencies = [ 680 | "memchr", 681 | ] 682 | 683 | [[package]] 684 | name = "quote" 685 | version = "1.0.40" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 688 | dependencies = [ 689 | "proc-macro2", 690 | ] 691 | 692 | [[package]] 693 | name = "read-progress" 694 | version = "0.2.0" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "63a0efaaa8dbadf44b31410bab6b4ccee7b42d317fceac58a4a110f10d2b2511" 697 | 698 | [[package]] 699 | name = "regex" 700 | version = "1.11.1" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 703 | dependencies = [ 704 | "aho-corasick", 705 | "memchr", 706 | "regex-automata", 707 | "regex-syntax", 708 | ] 709 | 710 | [[package]] 711 | name = "regex-automata" 712 | version = "0.4.9" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 715 | dependencies = [ 716 | "aho-corasick", 717 | "memchr", 718 | "regex-syntax", 719 | ] 720 | 721 | [[package]] 722 | name = "regex-syntax" 723 | version = "0.8.5" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 726 | 727 | [[package]] 728 | name = "rusqlite" 729 | version = "0.35.0" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b" 732 | dependencies = [ 733 | "bitflags 2.9.0", 734 | "fallible-iterator", 735 | "fallible-streaming-iterator", 736 | "hashlink", 737 | "libsqlite3-sys", 738 | "smallvec", 739 | ] 740 | 741 | [[package]] 742 | name = "rustversion" 743 | version = "1.0.20" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 746 | 747 | [[package]] 748 | name = "ryu" 749 | version = "1.0.20" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 752 | 753 | [[package]] 754 | name = "separator" 755 | version = "0.4.1" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "f97841a747eef040fcd2e7b3b9a220a7205926e60488e673d9e4926d27772ce5" 758 | 759 | [[package]] 760 | name = "serde" 761 | version = "1.0.219" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 764 | dependencies = [ 765 | "serde_derive", 766 | ] 767 | 768 | [[package]] 769 | name = "serde_derive" 770 | version = "1.0.219" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 773 | dependencies = [ 774 | "proc-macro2", 775 | "quote", 776 | "syn", 777 | ] 778 | 779 | [[package]] 780 | name = "serde_json" 781 | version = "1.0.140" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 784 | dependencies = [ 785 | "itoa", 786 | "memchr", 787 | "ryu", 788 | "serde", 789 | ] 790 | 791 | [[package]] 792 | name = "shlex" 793 | version = "1.3.0" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 796 | 797 | [[package]] 798 | name = "smallvec" 799 | version = "1.15.0" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 802 | 803 | [[package]] 804 | name = "smol_str" 805 | version = "0.3.2" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d" 808 | dependencies = [ 809 | "borsh", 810 | "serde", 811 | ] 812 | 813 | [[package]] 814 | name = "strsim" 815 | version = "0.10.0" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 818 | 819 | [[package]] 820 | name = "strsim" 821 | version = "0.11.1" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 824 | 825 | [[package]] 826 | name = "syn" 827 | version = "2.0.101" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 830 | dependencies = [ 831 | "proc-macro2", 832 | "quote", 833 | "unicode-ident", 834 | ] 835 | 836 | [[package]] 837 | name = "termcolor" 838 | version = "1.4.1" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 841 | dependencies = [ 842 | "winapi-util", 843 | ] 844 | 845 | [[package]] 846 | name = "textwrap" 847 | version = "0.16.2" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" 850 | 851 | [[package]] 852 | name = "unicode-ident" 853 | version = "1.0.18" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 856 | 857 | [[package]] 858 | name = "utf8parse" 859 | version = "0.2.2" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 862 | 863 | [[package]] 864 | name = "vcpkg" 865 | version = "0.2.15" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 868 | 869 | [[package]] 870 | name = "wasm-bindgen" 871 | version = "0.2.100" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 874 | dependencies = [ 875 | "cfg-if", 876 | "once_cell", 877 | "rustversion", 878 | "wasm-bindgen-macro", 879 | ] 880 | 881 | [[package]] 882 | name = "wasm-bindgen-backend" 883 | version = "0.2.100" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 886 | dependencies = [ 887 | "bumpalo", 888 | "log", 889 | "proc-macro2", 890 | "quote", 891 | "syn", 892 | "wasm-bindgen-shared", 893 | ] 894 | 895 | [[package]] 896 | name = "wasm-bindgen-macro" 897 | version = "0.2.100" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 900 | dependencies = [ 901 | "quote", 902 | "wasm-bindgen-macro-support", 903 | ] 904 | 905 | [[package]] 906 | name = "wasm-bindgen-macro-support" 907 | version = "0.2.100" 908 | source = "registry+https://github.com/rust-lang/crates.io-index" 909 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 910 | dependencies = [ 911 | "proc-macro2", 912 | "quote", 913 | "syn", 914 | "wasm-bindgen-backend", 915 | "wasm-bindgen-shared", 916 | ] 917 | 918 | [[package]] 919 | name = "wasm-bindgen-shared" 920 | version = "0.2.100" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 923 | dependencies = [ 924 | "unicode-ident", 925 | ] 926 | 927 | [[package]] 928 | name = "winapi" 929 | version = "0.3.9" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 932 | dependencies = [ 933 | "winapi-i686-pc-windows-gnu", 934 | "winapi-x86_64-pc-windows-gnu", 935 | ] 936 | 937 | [[package]] 938 | name = "winapi-i686-pc-windows-gnu" 939 | version = "0.4.0" 940 | source = "registry+https://github.com/rust-lang/crates.io-index" 941 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 942 | 943 | [[package]] 944 | name = "winapi-util" 945 | version = "0.1.9" 946 | source = "registry+https://github.com/rust-lang/crates.io-index" 947 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 948 | dependencies = [ 949 | "windows-sys", 950 | ] 951 | 952 | [[package]] 953 | name = "winapi-x86_64-pc-windows-gnu" 954 | version = "0.4.0" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 957 | 958 | [[package]] 959 | name = "windows-core" 960 | version = "0.61.0" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" 963 | dependencies = [ 964 | "windows-implement", 965 | "windows-interface", 966 | "windows-link", 967 | "windows-result", 968 | "windows-strings", 969 | ] 970 | 971 | [[package]] 972 | name = "windows-implement" 973 | version = "0.60.0" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 976 | dependencies = [ 977 | "proc-macro2", 978 | "quote", 979 | "syn", 980 | ] 981 | 982 | [[package]] 983 | name = "windows-interface" 984 | version = "0.59.1" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 987 | dependencies = [ 988 | "proc-macro2", 989 | "quote", 990 | "syn", 991 | ] 992 | 993 | [[package]] 994 | name = "windows-link" 995 | version = "0.1.1" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 998 | 999 | [[package]] 1000 | name = "windows-result" 1001 | version = "0.3.2" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" 1004 | dependencies = [ 1005 | "windows-link", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "windows-strings" 1010 | version = "0.4.0" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" 1013 | dependencies = [ 1014 | "windows-link", 1015 | ] 1016 | 1017 | [[package]] 1018 | name = "windows-sys" 1019 | version = "0.59.0" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1022 | dependencies = [ 1023 | "windows-targets", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "windows-targets" 1028 | version = "0.52.6" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1031 | dependencies = [ 1032 | "windows_aarch64_gnullvm", 1033 | "windows_aarch64_msvc", 1034 | "windows_i686_gnu", 1035 | "windows_i686_gnullvm", 1036 | "windows_i686_msvc", 1037 | "windows_x86_64_gnu", 1038 | "windows_x86_64_gnullvm", 1039 | "windows_x86_64_msvc", 1040 | ] 1041 | 1042 | [[package]] 1043 | name = "windows_aarch64_gnullvm" 1044 | version = "0.52.6" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1047 | 1048 | [[package]] 1049 | name = "windows_aarch64_msvc" 1050 | version = "0.52.6" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1053 | 1054 | [[package]] 1055 | name = "windows_i686_gnu" 1056 | version = "0.52.6" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1059 | 1060 | [[package]] 1061 | name = "windows_i686_gnullvm" 1062 | version = "0.52.6" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1065 | 1066 | [[package]] 1067 | name = "windows_i686_msvc" 1068 | version = "0.52.6" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1071 | 1072 | [[package]] 1073 | name = "windows_x86_64_gnu" 1074 | version = "0.52.6" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1077 | 1078 | [[package]] 1079 | name = "windows_x86_64_gnullvm" 1080 | version = "0.52.6" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1083 | 1084 | [[package]] 1085 | name = "windows_x86_64_msvc" 1086 | version = "0.52.6" 1087 | source = "registry+https://github.com/rust-lang/crates.io-index" 1088 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1089 | 1090 | [[package]] 1091 | name = "xml-rs" 1092 | version = "0.8.26" 1093 | source = "registry+https://github.com/rust-lang/crates.io-index" 1094 | checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" 1095 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "osm-tag-csv-history" 3 | version = "0.8.0-rc1" 4 | authors = ["Amanda McCann "] 5 | edition = "2024" 6 | license = "AGPL-3.0+" 7 | repository = "https://github.com/amandasaurus/osm-tag-csv-history" 8 | description = "Use CSV tools to see who's mapping what in OpenStreetMap." 9 | keywords = ["openstreetmap", "osm"] 10 | 11 | [dependencies] 12 | csv = "1.3.1" 13 | anyhow = "1.0.98" 14 | log = "0.4.27" 15 | clap = "3.2.25" 16 | env_logger = "0.11" 17 | flate2 = "1.1.1" 18 | do_every = "0.1.0" 19 | read-progress = "0.2.0" 20 | osmio = "0.15.0-rc1" 21 | rusqlite = "0.35.0" 22 | serde_json = "1.0.140" 23 | smol_str = "0.3.2" 24 | smallvec = "1.15.0" 25 | 26 | [profile.dev] 27 | opt-level = 3 28 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | 2 | GNU AFFERO GENERAL PUBLIC LICENSE 3 | Version 3, 19 November 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The GNU Affero General Public License is a free, copyleft license for 12 | software and other kinds of works, specifically designed to ensure 13 | cooperation with the community in the case of network server software. 14 | 15 | The licenses for most software and other practical works are designed 16 | to take away your freedom to share and change the works. By contrast, 17 | our General Public Licenses are intended to guarantee your freedom to 18 | share and change all versions of a program--to make sure it remains free 19 | software for all its users. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | them if you wish), that you receive source code or can get it if you 25 | want it, that you can change the software or use pieces of it in new 26 | free programs, and that you know you can do these things. 27 | 28 | Developers that use our General Public Licenses protect your rights 29 | with two steps: (1) assert copyright on the software, and (2) offer 30 | you this License which gives you legal permission to copy, distribute 31 | and/or modify the software. 32 | 33 | A secondary benefit of defending all users' freedom is that 34 | improvements made in alternate versions of the program, if they 35 | receive widespread use, become available for other developers to 36 | incorporate. Many developers of free software are heartened and 37 | encouraged by the resulting cooperation. However, in the case of 38 | software used on network servers, this result may fail to come about. 39 | The GNU General Public License permits making a modified version and 40 | letting the public access it on a server without ever releasing its 41 | source code to the public. 42 | 43 | The GNU Affero General Public License is designed specifically to 44 | ensure that, in such cases, the modified source code becomes available 45 | to the community. It requires the operator of a network server to 46 | provide the source code of the modified version running there to the 47 | users of that server. Therefore, public use of a modified version, on 48 | a publicly accessible server, gives the public access to the source 49 | code of the modified version. 50 | 51 | An older license, called the Affero General Public License and 52 | published by Affero, was designed to accomplish similar goals. This is 53 | a different license, not a version of the Affero GPL, but Affero has 54 | released a new version of the Affero GPL which permits relicensing under 55 | this license. 56 | 57 | The precise terms and conditions for copying, distribution and 58 | modification follow. 59 | 60 | TERMS AND CONDITIONS 61 | 62 | 0. Definitions. 63 | 64 | "This License" refers to version 3 of the GNU Affero General Public License. 65 | 66 | "Copyright" also means copyright-like laws that apply to other kinds of 67 | works, such as semiconductor masks. 68 | 69 | "The Program" refers to any copyrightable work licensed under this 70 | License. Each licensee is addressed as "you". "Licensees" and 71 | "recipients" may be individuals or organizations. 72 | 73 | To "modify" a work means to copy from or adapt all or part of the work 74 | in a fashion requiring copyright permission, other than the making of an 75 | exact copy. The resulting work is called a "modified version" of the 76 | earlier work or a work "based on" the earlier work. 77 | 78 | A "covered work" means either the unmodified Program or a work based 79 | on the Program. 80 | 81 | To "propagate" a work means to do anything with it that, without 82 | permission, would make you directly or secondarily liable for 83 | infringement under applicable copyright law, except executing it on a 84 | computer or modifying a private copy. Propagation includes copying, 85 | distribution (with or without modification), making available to the 86 | public, and in some countries other activities as well. 87 | 88 | To "convey" a work means any kind of propagation that enables other 89 | parties to make or receive copies. Mere interaction with a user through 90 | a computer network, with no transfer of a copy, is not conveying. 91 | 92 | An interactive user interface displays "Appropriate Legal Notices" 93 | to the extent that it includes a convenient and prominently visible 94 | feature that (1) displays an appropriate copyright notice, and (2) 95 | tells the user that there is no warranty for the work (except to the 96 | extent that warranties are provided), that licensees may convey the 97 | work under this License, and how to view a copy of this License. If 98 | the interface presents a list of user commands or options, such as a 99 | menu, a prominent item in the list meets this criterion. 100 | 101 | 1. Source Code. 102 | 103 | The "source code" for a work means the preferred form of the work 104 | for making modifications to it. "Object code" means any non-source 105 | form of a work. 106 | 107 | A "Standard Interface" means an interface that either is an official 108 | standard defined by a recognized standards body, or, in the case of 109 | interfaces specified for a particular programming language, one that 110 | is widely used among developers working in that language. 111 | 112 | The "System Libraries" of an executable work include anything, other 113 | than the work as a whole, that (a) is included in the normal form of 114 | packaging a Major Component, but which is not part of that Major 115 | Component, and (b) serves only to enable use of the work with that 116 | Major Component, or to implement a Standard Interface for which an 117 | implementation is available to the public in source code form. A 118 | "Major Component", in this context, means a major essential component 119 | (kernel, window system, and so on) of the specific operating system 120 | (if any) on which the executable work runs, or a compiler used to 121 | produce the work, or an object code interpreter used to run it. 122 | 123 | The "Corresponding Source" for a work in object code form means all 124 | the source code needed to generate, install, and (for an executable 125 | work) run the object code and to modify the work, including scripts to 126 | control those activities. However, it does not include the work's 127 | System Libraries, or general-purpose tools or generally available free 128 | programs which are used unmodified in performing those activities but 129 | which are not part of the work. For example, Corresponding Source 130 | includes interface definition files associated with source files for 131 | the work, and the source code for shared libraries and dynamically 132 | linked subprograms that the work is specifically designed to require, 133 | such as by intimate data communication or control flow between those 134 | subprograms and other parts of the work. 135 | 136 | The Corresponding Source need not include anything that users 137 | can regenerate automatically from other parts of the Corresponding 138 | Source. 139 | 140 | The Corresponding Source for a work in source code form is that 141 | same work. 142 | 143 | 2. Basic Permissions. 144 | 145 | All rights granted under this License are granted for the term of 146 | copyright on the Program, and are irrevocable provided the stated 147 | conditions are met. This License explicitly affirms your unlimited 148 | permission to run the unmodified Program. The output from running a 149 | covered work is covered by this License only if the output, given its 150 | content, constitutes a covered work. This License acknowledges your 151 | rights of fair use or other equivalent, as provided by copyright law. 152 | 153 | You may make, run and propagate covered works that you do not 154 | convey, without conditions so long as your license otherwise remains 155 | in force. You may convey covered works to others for the sole purpose 156 | of having them make modifications exclusively for you, or provide you 157 | with facilities for running those works, provided that you comply with 158 | the terms of this License in conveying all material for which you do 159 | not control copyright. Those thus making or running the covered works 160 | for you must do so exclusively on your behalf, under your direction 161 | and control, on terms that prohibit them from making any copies of 162 | your copyrighted material outside their relationship with you. 163 | 164 | Conveying under any other circumstances is permitted solely under 165 | the conditions stated below. Sublicensing is not allowed; section 10 166 | makes it unnecessary. 167 | 168 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 169 | 170 | No covered work shall be deemed part of an effective technological 171 | measure under any applicable law fulfilling obligations under article 172 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 173 | similar laws prohibiting or restricting circumvention of such 174 | measures. 175 | 176 | When you convey a covered work, you waive any legal power to forbid 177 | circumvention of technological measures to the extent such circumvention 178 | is effected by exercising rights under this License with respect to 179 | the covered work, and you disclaim any intention to limit operation or 180 | modification of the work as a means of enforcing, against the work's 181 | users, your or third parties' legal rights to forbid circumvention of 182 | technological measures. 183 | 184 | 4. Conveying Verbatim Copies. 185 | 186 | You may convey verbatim copies of the Program's source code as you 187 | receive it, in any medium, provided that you conspicuously and 188 | appropriately publish on each copy an appropriate copyright notice; 189 | keep intact all notices stating that this License and any 190 | non-permissive terms added in accord with section 7 apply to the code; 191 | keep intact all notices of the absence of any warranty; and give all 192 | recipients a copy of this License along with the Program. 193 | 194 | You may charge any price or no price for each copy that you convey, 195 | and you may offer support or warranty protection for a fee. 196 | 197 | 5. Conveying Modified Source Versions. 198 | 199 | You may convey a work based on the Program, or the modifications to 200 | produce it from the Program, in the form of source code under the 201 | terms of section 4, provided that you also meet all of these conditions: 202 | 203 | a) The work must carry prominent notices stating that you modified 204 | it, and giving a relevant date. 205 | 206 | b) The work must carry prominent notices stating that it is 207 | released under this License and any conditions added under section 208 | 7. This requirement modifies the requirement in section 4 to 209 | "keep intact all notices". 210 | 211 | c) You must license the entire work, as a whole, under this 212 | License to anyone who comes into possession of a copy. This 213 | License will therefore apply, along with any applicable section 7 214 | additional terms, to the whole of the work, and all its parts, 215 | regardless of how they are packaged. This License gives no 216 | permission to license the work in any other way, but it does not 217 | invalidate such permission if you have separately received it. 218 | 219 | d) If the work has interactive user interfaces, each must display 220 | Appropriate Legal Notices; however, if the Program has interactive 221 | interfaces that do not display Appropriate Legal Notices, your 222 | work need not make them do so. 223 | 224 | A compilation of a covered work with other separate and independent 225 | works, which are not by their nature extensions of the covered work, 226 | and which are not combined with it such as to form a larger program, 227 | in or on a volume of a storage or distribution medium, is called an 228 | "aggregate" if the compilation and its resulting copyright are not 229 | used to limit the access or legal rights of the compilation's users 230 | beyond what the individual works permit. Inclusion of a covered work 231 | in an aggregate does not cause this License to apply to the other 232 | parts of the aggregate. 233 | 234 | 6. Conveying Non-Source Forms. 235 | 236 | You may convey a covered work in object code form under the terms 237 | of sections 4 and 5, provided that you also convey the 238 | machine-readable Corresponding Source under the terms of this License, 239 | in one of these ways: 240 | 241 | a) Convey the object code in, or embodied in, a physical product 242 | (including a physical distribution medium), accompanied by the 243 | Corresponding Source fixed on a durable physical medium 244 | customarily used for software interchange. 245 | 246 | b) Convey the object code in, or embodied in, a physical product 247 | (including a physical distribution medium), accompanied by a 248 | written offer, valid for at least three years and valid for as 249 | long as you offer spare parts or customer support for that product 250 | model, to give anyone who possesses the object code either (1) a 251 | copy of the Corresponding Source for all the software in the 252 | product that is covered by this License, on a durable physical 253 | medium customarily used for software interchange, for a price no 254 | more than your reasonable cost of physically performing this 255 | conveying of source, or (2) access to copy the 256 | Corresponding Source from a network server at no charge. 257 | 258 | c) Convey individual copies of the object code with a copy of the 259 | written offer to provide the Corresponding Source. This 260 | alternative is allowed only occasionally and noncommercially, and 261 | only if you received the object code with such an offer, in accord 262 | with subsection 6b. 263 | 264 | d) Convey the object code by offering access from a designated 265 | place (gratis or for a charge), and offer equivalent access to the 266 | Corresponding Source in the same way through the same place at no 267 | further charge. You need not require recipients to copy the 268 | Corresponding Source along with the object code. If the place to 269 | copy the object code is a network server, the Corresponding Source 270 | may be on a different server (operated by you or a third party) 271 | that supports equivalent copying facilities, provided you maintain 272 | clear directions next to the object code saying where to find the 273 | Corresponding Source. Regardless of what server hosts the 274 | Corresponding Source, you remain obligated to ensure that it is 275 | available for as long as needed to satisfy these requirements. 276 | 277 | e) Convey the object code using peer-to-peer transmission, provided 278 | you inform other peers where the object code and Corresponding 279 | Source of the work are being offered to the general public at no 280 | charge under subsection 6d. 281 | 282 | A separable portion of the object code, whose source code is excluded 283 | from the Corresponding Source as a System Library, need not be 284 | included in conveying the object code work. 285 | 286 | A "User Product" is either (1) a "consumer product", which means any 287 | tangible personal property which is normally used for personal, family, 288 | or household purposes, or (2) anything designed or sold for incorporation 289 | into a dwelling. In determining whether a product is a consumer product, 290 | doubtful cases shall be resolved in favor of coverage. For a particular 291 | product received by a particular user, "normally used" refers to a 292 | typical or common use of that class of product, regardless of the status 293 | of the particular user or of the way in which the particular user 294 | actually uses, or expects or is expected to use, the product. A product 295 | is a consumer product regardless of whether the product has substantial 296 | commercial, industrial or non-consumer uses, unless such uses represent 297 | the only significant mode of use of the product. 298 | 299 | "Installation Information" for a User Product means any methods, 300 | procedures, authorization keys, or other information required to install 301 | and execute modified versions of a covered work in that User Product from 302 | a modified version of its Corresponding Source. The information must 303 | suffice to ensure that the continued functioning of the modified object 304 | code is in no case prevented or interfered with solely because 305 | modification has been made. 306 | 307 | If you convey an object code work under this section in, or with, or 308 | specifically for use in, a User Product, and the conveying occurs as 309 | part of a transaction in which the right of possession and use of the 310 | User Product is transferred to the recipient in perpetuity or for a 311 | fixed term (regardless of how the transaction is characterized), the 312 | Corresponding Source conveyed under this section must be accompanied 313 | by the Installation Information. But this requirement does not apply 314 | if neither you nor any third party retains the ability to install 315 | modified object code on the User Product (for example, the work has 316 | been installed in ROM). 317 | 318 | The requirement to provide Installation Information does not include a 319 | requirement to continue to provide support service, warranty, or updates 320 | for a work that has been modified or installed by the recipient, or for 321 | the User Product in which it has been modified or installed. Access to a 322 | network may be denied when the modification itself materially and 323 | adversely affects the operation of the network or violates the rules and 324 | protocols for communication across the network. 325 | 326 | Corresponding Source conveyed, and Installation Information provided, 327 | in accord with this section must be in a format that is publicly 328 | documented (and with an implementation available to the public in 329 | source code form), and must require no special password or key for 330 | unpacking, reading or copying. 331 | 332 | 7. Additional Terms. 333 | 334 | "Additional permissions" are terms that supplement the terms of this 335 | License by making exceptions from one or more of its conditions. 336 | Additional permissions that are applicable to the entire Program shall 337 | be treated as though they were included in this License, to the extent 338 | that they are valid under applicable law. If additional permissions 339 | apply only to part of the Program, that part may be used separately 340 | under those permissions, but the entire Program remains governed by 341 | this License without regard to the additional permissions. 342 | 343 | When you convey a copy of a covered work, you may at your option 344 | remove any additional permissions from that copy, or from any part of 345 | it. (Additional permissions may be written to require their own 346 | removal in certain cases when you modify the work.) You may place 347 | additional permissions on material, added by you to a covered work, 348 | for which you have or can give appropriate copyright permission. 349 | 350 | Notwithstanding any other provision of this License, for material you 351 | add to a covered work, you may (if authorized by the copyright holders of 352 | that material) supplement the terms of this License with terms: 353 | 354 | a) Disclaiming warranty or limiting liability differently from the 355 | terms of sections 15 and 16 of this License; or 356 | 357 | b) Requiring preservation of specified reasonable legal notices or 358 | author attributions in that material or in the Appropriate Legal 359 | Notices displayed by works containing it; or 360 | 361 | c) Prohibiting misrepresentation of the origin of that material, or 362 | requiring that modified versions of such material be marked in 363 | reasonable ways as different from the original version; or 364 | 365 | d) Limiting the use for publicity purposes of names of licensors or 366 | authors of the material; or 367 | 368 | e) Declining to grant rights under trademark law for use of some 369 | trade names, trademarks, or service marks; or 370 | 371 | f) Requiring indemnification of licensors and authors of that 372 | material by anyone who conveys the material (or modified versions of 373 | it) with contractual assumptions of liability to the recipient, for 374 | any liability that these contractual assumptions directly impose on 375 | those licensors and authors. 376 | 377 | All other non-permissive additional terms are considered "further 378 | restrictions" within the meaning of section 10. If the Program as you 379 | received it, or any part of it, contains a notice stating that it is 380 | governed by this License along with a term that is a further 381 | restriction, you may remove that term. If a license document contains 382 | a further restriction but permits relicensing or conveying under this 383 | License, you may add to a covered work material governed by the terms 384 | of that license document, provided that the further restriction does 385 | not survive such relicensing or conveying. 386 | 387 | If you add terms to a covered work in accord with this section, you 388 | must place, in the relevant source files, a statement of the 389 | additional terms that apply to those files, or a notice indicating 390 | where to find the applicable terms. 391 | 392 | Additional terms, permissive or non-permissive, may be stated in the 393 | form of a separately written license, or stated as exceptions; 394 | the above requirements apply either way. 395 | 396 | 8. Termination. 397 | 398 | You may not propagate or modify a covered work except as expressly 399 | provided under this License. Any attempt otherwise to propagate or 400 | modify it is void, and will automatically terminate your rights under 401 | this License (including any patent licenses granted under the third 402 | paragraph of section 11). 403 | 404 | However, if you cease all violation of this License, then your 405 | license from a particular copyright holder is reinstated (a) 406 | provisionally, unless and until the copyright holder explicitly and 407 | finally terminates your license, and (b) permanently, if the copyright 408 | holder fails to notify you of the violation by some reasonable means 409 | prior to 60 days after the cessation. 410 | 411 | Moreover, your license from a particular copyright holder is 412 | reinstated permanently if the copyright holder notifies you of the 413 | violation by some reasonable means, this is the first time you have 414 | received notice of violation of this License (for any work) from that 415 | copyright holder, and you cure the violation prior to 30 days after 416 | your receipt of the notice. 417 | 418 | Termination of your rights under this section does not terminate the 419 | licenses of parties who have received copies or rights from you under 420 | this License. If your rights have been terminated and not permanently 421 | reinstated, you do not qualify to receive new licenses for the same 422 | material under section 10. 423 | 424 | 9. Acceptance Not Required for Having Copies. 425 | 426 | You are not required to accept this License in order to receive or 427 | run a copy of the Program. Ancillary propagation of a covered work 428 | occurring solely as a consequence of using peer-to-peer transmission 429 | to receive a copy likewise does not require acceptance. However, 430 | nothing other than this License grants you permission to propagate or 431 | modify any covered work. These actions infringe copyright if you do 432 | not accept this License. Therefore, by modifying or propagating a 433 | covered work, you indicate your acceptance of this License to do so. 434 | 435 | 10. Automatic Licensing of Downstream Recipients. 436 | 437 | Each time you convey a covered work, the recipient automatically 438 | receives a license from the original licensors, to run, modify and 439 | propagate that work, subject to this License. You are not responsible 440 | for enforcing compliance by third parties with this License. 441 | 442 | An "entity transaction" is a transaction transferring control of an 443 | organization, or substantially all assets of one, or subdividing an 444 | organization, or merging organizations. If propagation of a covered 445 | work results from an entity transaction, each party to that 446 | transaction who receives a copy of the work also receives whatever 447 | licenses to the work the party's predecessor in interest had or could 448 | give under the previous paragraph, plus a right to possession of the 449 | Corresponding Source of the work from the predecessor in interest, if 450 | the predecessor has it or can get it with reasonable efforts. 451 | 452 | You may not impose any further restrictions on the exercise of the 453 | rights granted or affirmed under this License. For example, you may 454 | not impose a license fee, royalty, or other charge for exercise of 455 | rights granted under this License, and you may not initiate litigation 456 | (including a cross-claim or counterclaim in a lawsuit) alleging that 457 | any patent claim is infringed by making, using, selling, offering for 458 | sale, or importing the Program or any portion of it. 459 | 460 | 11. Patents. 461 | 462 | A "contributor" is a copyright holder who authorizes use under this 463 | License of the Program or a work on which the Program is based. The 464 | work thus licensed is called the contributor's "contributor version". 465 | 466 | A contributor's "essential patent claims" are all patent claims 467 | owned or controlled by the contributor, whether already acquired or 468 | hereafter acquired, that would be infringed by some manner, permitted 469 | by this License, of making, using, or selling its contributor version, 470 | but do not include claims that would be infringed only as a 471 | consequence of further modification of the contributor version. For 472 | purposes of this definition, "control" includes the right to grant 473 | patent sublicenses in a manner consistent with the requirements of 474 | this License. 475 | 476 | Each contributor grants you a non-exclusive, worldwide, royalty-free 477 | patent license under the contributor's essential patent claims, to 478 | make, use, sell, offer for sale, import and otherwise run, modify and 479 | propagate the contents of its contributor version. 480 | 481 | In the following three paragraphs, a "patent license" is any express 482 | agreement or commitment, however denominated, not to enforce a patent 483 | (such as an express permission to practice a patent or covenant not to 484 | sue for patent infringement). To "grant" such a patent license to a 485 | party means to make such an agreement or commitment not to enforce a 486 | patent against the party. 487 | 488 | If you convey a covered work, knowingly relying on a patent license, 489 | and the Corresponding Source of the work is not available for anyone 490 | to copy, free of charge and under the terms of this License, through a 491 | publicly available network server or other readily accessible means, 492 | then you must either (1) cause the Corresponding Source to be so 493 | available, or (2) arrange to deprive yourself of the benefit of the 494 | patent license for this particular work, or (3) arrange, in a manner 495 | consistent with the requirements of this License, to extend the patent 496 | license to downstream recipients. "Knowingly relying" means you have 497 | actual knowledge that, but for the patent license, your conveying the 498 | covered work in a country, or your recipient's use of the covered work 499 | in a country, would infringe one or more identifiable patents in that 500 | country that you have reason to believe are valid. 501 | 502 | If, pursuant to or in connection with a single transaction or 503 | arrangement, you convey, or propagate by procuring conveyance of, a 504 | covered work, and grant a patent license to some of the parties 505 | receiving the covered work authorizing them to use, propagate, modify 506 | or convey a specific copy of the covered work, then the patent license 507 | you grant is automatically extended to all recipients of the covered 508 | work and works based on it. 509 | 510 | A patent license is "discriminatory" if it does not include within 511 | the scope of its coverage, prohibits the exercise of, or is 512 | conditioned on the non-exercise of one or more of the rights that are 513 | specifically granted under this License. You may not convey a covered 514 | work if you are a party to an arrangement with a third party that is 515 | in the business of distributing software, under which you make payment 516 | to the third party based on the extent of your activity of conveying 517 | the work, and under which the third party grants, to any of the 518 | parties who would receive the covered work from you, a discriminatory 519 | patent license (a) in connection with copies of the covered work 520 | conveyed by you (or copies made from those copies), or (b) primarily 521 | for and in connection with specific products or compilations that 522 | contain the covered work, unless you entered into that arrangement, 523 | or that patent license was granted, prior to 28 March 2007. 524 | 525 | Nothing in this License shall be construed as excluding or limiting 526 | any implied license or other defenses to infringement that may 527 | otherwise be available to you under applicable patent law. 528 | 529 | 12. No Surrender of Others' Freedom. 530 | 531 | If conditions are imposed on you (whether by court order, agreement or 532 | otherwise) that contradict the conditions of this License, they do not 533 | excuse you from the conditions of this License. If you cannot convey a 534 | covered work so as to satisfy simultaneously your obligations under this 535 | License and any other pertinent obligations, then as a consequence you may 536 | not convey it at all. For example, if you agree to terms that obligate you 537 | to collect a royalty for further conveying from those to whom you convey 538 | the Program, the only way you could satisfy both those terms and this 539 | License would be to refrain entirely from conveying the Program. 540 | 541 | 13. Remote Network Interaction; Use with the GNU General Public License. 542 | 543 | Notwithstanding any other provision of this License, if you modify the 544 | Program, your modified version must prominently offer all users 545 | interacting with it remotely through a computer network (if your version 546 | supports such interaction) an opportunity to receive the Corresponding 547 | Source of your version by providing access to the Corresponding Source 548 | from a network server at no charge, through some standard or customary 549 | means of facilitating copying of software. This Corresponding Source 550 | shall include the Corresponding Source for any work covered by version 3 551 | of the GNU General Public License that is incorporated pursuant to the 552 | following paragraph. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the work with which it is combined will remain governed by version 560 | 3 of the GNU General Public License. 561 | 562 | 14. Revised Versions of this License. 563 | 564 | The Free Software Foundation may publish revised and/or new versions of 565 | the GNU Affero General Public License from time to time. Such new versions 566 | will be similar in spirit to the present version, but may differ in detail to 567 | address new problems or concerns. 568 | 569 | Each version is given a distinguishing version number. If the 570 | Program specifies that a certain numbered version of the GNU Affero General 571 | Public License "or any later version" applies to it, you have the 572 | option of following the terms and conditions either of that numbered 573 | version or of any later version published by the Free Software 574 | Foundation. If the Program does not specify a version number of the 575 | GNU Affero General Public License, you may choose any version ever published 576 | by the Free Software Foundation. 577 | 578 | If the Program specifies that a proxy can decide which future 579 | versions of the GNU Affero General Public License can be used, that proxy's 580 | public statement of acceptance of a version permanently authorizes you 581 | to choose that version for the Program. 582 | 583 | Later license versions may give you additional or different 584 | permissions. However, no additional obligations are imposed on any 585 | author or copyright holder as a result of your choosing to follow a 586 | later version. 587 | 588 | 15. Disclaimer of Warranty. 589 | 590 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 591 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 592 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 593 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 594 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 595 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 596 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 597 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 598 | 599 | 16. Limitation of Liability. 600 | 601 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 602 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 603 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 604 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 605 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 606 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 607 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 608 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 609 | SUCH DAMAGES. 610 | 611 | 17. Interpretation of Sections 15 and 16. 612 | 613 | If the disclaimer of warranty and limitation of liability provided 614 | above cannot be given local legal effect according to their terms, 615 | reviewing courts shall apply local law that most closely approximates 616 | an absolute waiver of all civil liability in connection with the 617 | Program, unless a warranty or assumption of liability accompanies a 618 | copy of the Program in return for a fee. 619 | 620 | END OF TERMS AND CONDITIONS 621 | 622 | How to Apply These Terms to Your New Programs 623 | 624 | If you develop a new program, and you want it to be of the greatest 625 | possible use to the public, the best way to achieve this is to make it 626 | free software which everyone can redistribute and change under these terms. 627 | 628 | To do so, attach the following notices to the program. It is safest 629 | to attach them to the start of each source file to most effectively 630 | state the exclusion of warranty; and each file should have at least 631 | the "copyright" line and a pointer to where the full notice is found. 632 | 633 | 634 | 635 | Copyright (C) 2020 Amanda McCann 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU Affero General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU Affero General Public License for more details. 646 | 647 | You should have received a copy of the GNU Affero General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If your software can interact with users remotely through a computer 653 | network, you should also make sure that it provides a way for users to 654 | get its source. For example, if your program is a web application, its 655 | interface could display a "Source" link that leads users to an archive 656 | of the code. There are many ways you could offer source, and different 657 | solutions will be better for different programs; see section 13 for the 658 | specific requirements. 659 | 660 | You should also get your employer (if you work as a programmer) or school, 661 | if any, to sign a "copyright disclaimer" for the program, if necessary. 662 | For more information on this, and how to apply and follow the GNU AGPL, see 663 | . 664 | 665 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # osm-tag-csv-history 2 | 3 | ![Crates.io](https://img.shields.io/crates/v/osm-tag-csv-history) 4 | 5 | Use CSV tools to see who's mapping what in OpenStreetMap. 6 | 7 | Given a OSM history file, it produces a CSV file, where each row refers to a 8 | change (addition, removal or modification) to a tag all OSM objects in an OSM 9 | data file with history. 10 | 11 | ## Getting data 12 | 13 | [Planet.OpenStreetMap.org](https://planet.openstreetmap.org/planet/full-history/) 14 | provides a “full history” file, updated every week, where you can download the 15 | [latest full history file (⚠ 99+ GB! 16 | ⚠)](https://planet.openstreetmap.org/pbf/full-history/history-latest.osm.pbf), 17 | although it's quite large. 18 | 19 | Download it over BitTorrent with: 20 | 21 | aria2c --seed-time 0 https://planet.openstreetmap.org/pbf/full-history/history-latest.osm.pbf.torrent 22 | 23 | Geofabrik provides an [download 24 | service](https://osm-internal.download.geofabrik.de/) which includes full 25 | history files for lots of regions & countries. You must log into that with your 26 | OpenStreetMap account. You can also use this tool on regular, non-history, OSM 27 | data files. 28 | 29 | ## Installation 30 | 31 | If you have Rust installed, you can install it with: 32 | 33 | cargo install osm-tag-csv-history 34 | 35 | You can download prebuild binary released from the [Github release page](https://github.com/amandasaurus/osm-tag-csv-history/releases), (e.g. [download the v0.3.0 release](https://github.com/amandasaurus/osm-tag-csv-history/releases/download/v0.3.0/osm-tag-csv-history)). 36 | 37 | ## Usage 38 | 39 | osm-tag-csv-history -i mydata.osm.pbf -o mydata.csv.gz 40 | 41 | The output is automatically compressed with gzip if the file ends in `.gz`. `.csv` filename for CSV files, `.tsv` for TSV (tab separated). 42 | 43 | ### Tag Filtering 44 | 45 | By default, all tag changes are included. With the `--key`/`-k` argument, only any changes to those tag keys are included in the output 46 | 47 | To produce a CSV with only changes to the `highway` or `building` tag, run this command 48 | 49 | osm-tag-csv-history -i mydata.osm.pbf -o mydata.csv -k highway -k building 50 | 51 | ### Object Type Filtering 52 | 53 | By default, all OSM objects in the file are included. With `--object-types`/`-T` only some can be output, e.g. ` -T wr ` for only ways & relations. 54 | 55 | ### User (ID) Type Filtering 56 | 57 | Use `--uid` to only output object changes by this OSM users (can be specified multiple times) 58 | 59 | 60 | ### Changeset tag column 61 | 62 | ### Example 63 | 64 | Many programmes can use CSV files. It's also possible to use hacky unix command 65 | line programmes to calculate who's adding fuel stations (`amenity=fuel` in OSM) 66 | in Ireland: 67 | 68 | osm-tag-csv-history -i ./ireland-and-northern-ireland-internal.osh.pbf -o - --no-header | grep '^amenity,fuel,' | cut -d, -f9 | sort | uniq -c | sort -n | tail -n 20 69 | 70 | 71 | Here can find all times someone has upgraded a building from `building=yes` to 72 | something else. 73 | 74 | osm-tag-csv-history -i data.osh.pbf -o - --no-header | grep -P '^building,[^,]+,yes,' | cat -n 75 | 76 | And with some other command line commands, we can get a list of who's doing the 77 | most to make OSM more descriptive by upgrading `building=yes`. 78 | 79 | osm-tag-csv-history -i data.osh.pbf -o - --no-header | grep -P '^building,[^,]+,yes,' | xsv select 8 | sort | uniq -c | sort -n | tail -n 20 80 | 81 | #### Using with `osmium getid` 82 | 83 | The `id` column (column 4) can be used [by `osmium-tool` to filter an OSM file by object id](https://osmcode.org/osmium-tool/manual.html#getting-osm-objects-by-id). This is how you get a file of all the pet shops in OSM in a file: 84 | 85 | osm-tag-csv-history -i country-latest.osm.pbf -o - --no-header | grep '^shop,pet,' | xsv select 4 | osmium getid -i - country-latest.osm.pbf -o pets.osm.pbf -r 86 | 87 | (For this simple case, [`osmiums`'s tag 88 | filtering](https://osmcode.org/osmium-tool/manual.html#filtering-by-tags) is 89 | probably better) 90 | 91 | ### Non-history files 92 | 93 | This programme can run on non-history files just fine. The `old_value`, and 94 | `old_version` will be empty. This can be a way to convert OSM data into CSV 95 | format for further processing. This tool will not make any OSM API requests. 96 | 97 | ### Using on privacy preserving files. 98 | 99 | The [Geofabrik Public Download Service](http://download.geofabrik.de/) provides 100 | non-history files which do not include some metadata, like usernames, uids or 101 | changeset\_ids. This tool can run on them and just give an empty value for 102 | username, and `0` for uid & changeset\_id. 103 | 104 | If you have an OSM account, you can get full metada from the 105 | [internal](https://osm-internal.download.geofabrik.de/index.html) service. 106 | 107 | ## Output file format 108 | 109 | Records are separated by a newline (`\n`). A header line is included by default, but it 110 | can be turned off with `--no-header` (or forcibly included with `--header`). 111 | 112 | If any string (e.g. tag value, username) has a newline or characters like that, 113 | it will be escaped with a backslash (i.e. a newline is written as 2 characters, 114 | `\` then `n`). 115 | 116 | ### Columns 117 | 118 | The columns can be changed with `--columns`/`-C`, e.g (` -C key,new_value,uid `). 119 | 120 | The default value is `key,new_value,old_value,id,new_version,old_version,datetime,username,uid,changeset_id` 121 | 122 | Default values, in order 123 | 124 | * `key` The tag key 125 | * `new_value` The current/new version. `""` (empty string) if the current 126 | version doesn't have this key (i.e. it has been removed from the object) 127 | * `old_value` The previous value. `""` (empty string) if the previous version 128 | didn't have this key 129 | * `id` The object type and id. First character is the type (`n`/`w`/`r`), then 130 | the id. `n123` is node with id 123. This format is used [by `osmium-tool` to filter an OSM file by object id](https://osmcode.org/osmium-tool/manual.html#getting-osm-objects-by-id) 131 | * `new_version` The current/new version number 132 | * `old_version` The previous version number. `""` (empty string) for the first version of an object 133 | * `tag_count_delta`: `0` if the tag is changed, `+1` if the tag is added, `-1` 134 | if the tag was removed. This is a more robust way to determine if a tag was 135 | added or removed. Think of it as “the change in the number of OSM objects 136 | with this key” 137 | * `datetime` Date time (RFC3339 format in UTC) the object was created. 138 | * `username` The username of the user who changes it (remember: in OSM, users 139 | can change their username, UIDs remain constant) 140 | * `uid` The user id of the user. 141 | * `changeset_id` Changeset id where this change was made 142 | 143 | #### Other available columns: 144 | 145 | * `object_type_short`/`object_type_long` OSM type of the object (`n`/`w`/`r`, or `node`/`way`/`relation`) 146 | * `raw_id` OSM id of the object 147 | * `epoch_datetime` Date time (Unix epoch time) the object was created. This is 148 | how the data is stored in an OSM PBF file. This (rather than the ISO string 149 | `datetime`) makes processing about 15% faster (because the conversion of 150 | epoch seconds in integer to ISO datetime format string doesn't need to be 151 | done) 152 | 153 | ### Example 154 | 155 | Imagine this simple file ([`example.osh.pbf`](./example.osh.pbf)). 156 | 157 | ```xml 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | ``` 195 | 196 | NB: This programme cannot read XML files, only PBF. This file was converted to PBF with `osmium cat example.osm.xml -o example.osh.pbf`. 197 | 198 | Running `osm-tag-csv-history` on it produces this CSV file (formatted here as a table by with [`csvtomd`](https://csvtomd.com/)). 199 | 200 | | key | new_value | old_value | id | new_version | old_version | tag_count_delta | iso_datetime | username | uid | changeset_id | 201 | | ---------- | ---------- | --------- | -- | ----------- | ----------- | --------------- | -------------------- | -------- | --- | ------------ | 202 | | name | Nice City | | n1 | 1 | | +1 | 2019-01-01T00:00:00Z | Alice | 12 | 2 | 203 | | place | city | | n1 | 1 | | +1 | 2019-01-01T00:00:00Z | Alice | 12 | 2 | 204 | | population | 1000000 | | n1 | 2 | 1 | +1 | 2019-03-01T12:30:00Z | Bob | 2 | 10 | 205 | | amenity | restaurant | | n2 | 1 | | +1 | 2019-04-01T00:00:00Z | Alice | 12 | 20 | 206 | | name | TastyEats | | n2 | 1 | | +1 | 2019-04-01T00:00:00Z | Alice | 12 | 20 | 207 | | cuisine | regional | | n2 | 2 | 1 | +1 | 2019-04-01T02:00:00Z | Alice | 12 | 21 | 208 | | cuisine | burger | regional | n2 | 3 | 2 | 0 | 2019-04-01T03:00:00Z | Alice | 12 | 22 | 209 | | amenity | bench | | n3 | 1 | | +1 | 2019-04-01T00:00:00Z | Alice | 12 | 50 | 210 | | amenity | | bench | n3 | 2 | 1 | -1 | 2019-06-01T00:00:00Z | Alice | 12 | 100 | 211 | 212 | 213 | Some things to note: 214 | 215 | * There can be more than one record (line) per version (n1 v1 has 2 lines, one for each tag that was added). 216 | * If no tags are changed, then there are no lines. There is no line for node 2 v4 because the location, not the tags was changed. 217 | * An empty value for `old_version` means there was no previous, or earlier, version. 218 | * When an object (and hence tag) is deleted, the previous value is in `old_value`, and the `new_value` is empty, as for n3 v2. 219 | 220 | ## Possible useful tools 221 | 222 | The following other tools might be useful: 223 | 224 | * [`xsv`](https://github.com/BurntSushi/xsv). a command line tool for slicing & filtering CSV data. 225 | * [`osmium`](https://osmcode.org/osmium-tool/) a programme to process OSM data. You can use this to filter an OSM history file to a certain area, or time range. 226 | * [`datamash`](https://www.gnu.org/software/datamash/), command line CSV statistical tool. 227 | 228 | ## Misc 229 | 230 | Copyright 2020→2025, GNU Affero General Public Licence (AGPL) v3 or later. See [LICENCE.txt](./LICENCE.txt). 231 | Source code is on [Github](https://github.com/amandasaurus/osm-tag-csv-history). 232 | 233 | The output file should be viewed as a Derived Database of the OpenStreetMap database, and hence under the [ODbL 1.0](https://opendatacommons.org/licenses/odbl/) licence, the same as the [OpenStreetMap copyright](https://www.openstreetmap.org/copyright) 234 | -------------------------------------------------------------------------------- /example.osh.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amandasaurus/osm-tag-csv-history/fa03dcca6b6999bf5d60d334e7facb7be206a69b/example.osh.pbf -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | extern crate csv; 4 | extern crate env_logger; 5 | extern crate osmio; 6 | #[macro_use] 7 | extern crate anyhow; 8 | extern crate clap; 9 | extern crate do_every; 10 | extern crate flate2; 11 | extern crate read_progress; 12 | extern crate rusqlite; 13 | extern crate serde_json; 14 | extern crate smallvec; 15 | extern crate smol_str; 16 | 17 | use std::borrow::Cow; 18 | use std::cmp::Ordering; 19 | use std::collections::{BTreeMap, HashMap}; 20 | use std::fs::File; 21 | use std::io::BufReader; 22 | use std::str::FromStr; 23 | use std::time::Instant; 24 | 25 | use clap::{App, Arg}; 26 | use osmio::{OSMObj, OSMObjBase, OSMObjectType, OSMReader}; 27 | 28 | use anyhow::{Context, Result}; 29 | use flate2::Compression; 30 | use flate2::write::GzEncoder; 31 | use read_progress::ReaderWithSize; 32 | use rusqlite::{Connection, OptionalExtension}; 33 | use smallvec::SmallVec; 34 | use smol_str::SmolStr; 35 | 36 | #[allow(clippy::upper_case_acronyms)] 37 | enum OutputFormat { 38 | CSV, 39 | TSV, 40 | } 41 | 42 | #[derive(Debug, PartialEq)] 43 | enum Column { 44 | Key, 45 | NewValue, 46 | OldValue, 47 | Value, 48 | Id, 49 | RawId, 50 | ObjectTypeShort, 51 | ObjectTypeLong, 52 | NewVersion, 53 | OldVersion, 54 | IsoDatetime, 55 | EpochDatetime, 56 | Username, 57 | Uid, 58 | ChangesetId, 59 | 60 | ChangesetTag(String), 61 | 62 | TagCountDelta, 63 | ValueCountDelta, 64 | } 65 | 66 | impl FromStr for Column { 67 | type Err = anyhow::Error; 68 | fn from_str(val: &str) -> Result { 69 | match val.to_lowercase().trim() { 70 | "key" => Ok(Column::Key), 71 | "new_value" => Ok(Column::NewValue), 72 | "old_value" => Ok(Column::OldValue), 73 | "value" => Ok(Column::Value), 74 | "id" => Ok(Column::Id), 75 | "raw_id" | "osm_raw_id" => Ok(Column::RawId), 76 | "new_version" => Ok(Column::NewVersion), 77 | "old_version" => Ok(Column::OldVersion), 78 | "datetime" | "iso_datetime" | "iso_timestamp" => Ok(Column::IsoDatetime), 79 | "epoch" | "epoch_datetime" | "epoch_timestamp" => Ok(Column::EpochDatetime), 80 | "username" => Ok(Column::Username), 81 | "uid" => Ok(Column::Uid), 82 | "changeset_id" => Ok(Column::ChangesetId), 83 | col if col.starts_with("changeset.") => Ok(Column::ChangesetTag( 84 | col.strip_prefix("changeset.").unwrap().to_string(), 85 | )), 86 | "tag_count_delta" => Ok(Column::TagCountDelta), 87 | "value_count_delta" => Ok(Column::ValueCountDelta), 88 | "object_type_short" | "osm_type_short" => Ok(Column::ObjectTypeShort), 89 | "object_type_long" | "osm_type_long" => Ok(Column::ObjectTypeLong), 90 | 91 | col => Err(anyhow::anyhow!("Unknown column value: {}", col)), 92 | } 93 | } 94 | } 95 | 96 | impl Column { 97 | fn is_changeset_tag(&self) -> bool { 98 | matches!(self, Column::ChangesetTag(_)) 99 | } 100 | 101 | fn header(&self) -> Cow { 102 | match self { 103 | Column::Key => "key".into(), 104 | Column::NewValue => "new_value".into(), 105 | Column::OldValue => "old_value".into(), 106 | Column::Value => "value".into(), 107 | Column::Id => "id".into(), 108 | Column::RawId => "raw_id".into(), 109 | Column::NewVersion => "new_version".into(), 110 | Column::OldVersion => "old_version".into(), 111 | Column::IsoDatetime => "iso_datetime".into(), 112 | Column::EpochDatetime => "epoch_datetime".into(), 113 | Column::Username => "username".into(), 114 | Column::Uid => "uid".into(), 115 | Column::ChangesetId => "changeset_id".into(), 116 | Column::ChangesetTag(t) => format!("changeset_{}", t).into(), 117 | Column::TagCountDelta => "tag_count_delta".into(), 118 | Column::ValueCountDelta => "value_count_delta".into(), 119 | Column::ObjectTypeShort => "object_type_short".into(), 120 | Column::ObjectTypeLong => "object_type_long".into(), 121 | } 122 | } 123 | } 124 | 125 | enum LineType { 126 | OldNewValue, 127 | SeparateLines, 128 | } 129 | 130 | fn main() -> Result<()> { 131 | let matches = App::new("osm-tag-csv-history") 132 | .version(env!("CARGO_PKG_VERSION")) 133 | .about("Create a CSV file detailing tagging changes in an OSM file") 134 | 135 | .arg(Arg::with_name("input") 136 | .short('i').long("input") 137 | .value_name("INPUT.osh.pbf") 138 | .help("Input file to convert.") 139 | .long_help("Read OSM data from this file. If it's a .osh.pbf history file, the full history will be output. Regular non-history files can be processed too") 140 | .takes_value(true).required(true) 141 | ) 142 | 143 | .arg(Arg::with_name("output") 144 | .short('o').long("output") 145 | .value_name("OUTPUT.csv[.gz]") 146 | .help("Where to write the output. Use - for stdout. with auto compression (default), if this file ends with .gz, then it will be gzip compressed") 147 | .takes_value(true).required(true) 148 | ) 149 | 150 | .arg(Arg::with_name("verbosity") 151 | .short('v').multiple_occurrences(true) 152 | .help("Increase verbosity") 153 | ) 154 | 155 | .arg(Arg::with_name("header") 156 | .long("header") 157 | .takes_value(false).required(false) 158 | .help("Include a CSV header (default)") 159 | .conflicts_with("no-header") 160 | ) 161 | 162 | .arg(Arg::with_name("no-header") 163 | .long("no-header") 164 | .takes_value(false).required(false) 165 | .help("Do not include a CSV header") 166 | .conflicts_with("header") 167 | ) 168 | 169 | .arg(Arg::with_name("compression") 170 | .short('c').long("compression") 171 | .takes_value(true).required(false) 172 | .possible_values(["none", "auto", "gzip"]) 173 | .hidden_short_help(true) 174 | .default_value("auto") 175 | .value_name("{none,auto,gzip}") 176 | .help("Should the output file be compressed?") 177 | .long_help("Should the CSV output be compress?\nnone = don't compress the output\ngzip = always compress output with gzip\nauto (default) = uncompressed unless the output filename ends in .gz") 178 | ) 179 | 180 | .arg(Arg::with_name("log-frequency") 181 | .long("log-frequency") 182 | .value_name("SEC") 183 | .takes_value(true).required(false) 184 | .hidden_short_help(true) 185 | .default_value("10") 186 | .help("with -v, how often (in sec.) to print progress messages") 187 | ) 188 | 189 | .arg(Arg::with_name("key") 190 | .short('k').long("k") 191 | .value_name("KEY") 192 | .takes_value(true).required(false) 193 | .multiple(true).number_of_values(1) 194 | .help("Only include changes to this tag key (can be specified multiple times)") 195 | ) 196 | .arg(Arg::with_name("tag") 197 | .short('t').long("tag") 198 | .value_name("KEY=VALUE") 199 | .takes_value(true).required(false) 200 | .multiple(true).number_of_values(1) 201 | .help("Only include changes with this KEY & VALUE (can be specified multiple times)") 202 | ) 203 | 204 | 205 | .arg(Arg::with_name("changeset_filename") 206 | .long("changesets") 207 | .value_name("changesets-latest.osm.bz2") 208 | .takes_value(true).required(false) 209 | .help("Filename of the changeset file") 210 | ) 211 | 212 | .arg(Arg::with_name("uid") 213 | .long("uid") 214 | .value_name("USERID") 215 | .takes_value(true).required(false) 216 | .multiple(true).number_of_values(1) 217 | .help("Only include changes made by this OSM user (by userid)") 218 | ) 219 | 220 | 221 | .arg(Arg::with_name("output_format") 222 | .long("output-format") 223 | .takes_value(true).required(false) 224 | .help("output format") 225 | .possible_values(["auto", "csv", "tsv"]) 226 | .hidden_short_help(true) 227 | .default_value("auto") 228 | ) 229 | 230 | .arg(Arg::with_name("columns") 231 | .short('C').long("columns") 232 | .value_name("COL,COL,...") 233 | .takes_value(true).required(false) 234 | .default_value("key,new_value,old_value,id,new_version,old_version,tag_count_delta,iso_datetime,username,uid,changeset_id") 235 | .long_help("Output the following columns, in order: 236 | key: Tag key 237 | new_value: Old value of the tag 238 | old_value: New value of the tag 239 | id: OSM object type & id (e.g. w123) 240 | raw_id: just the numeric if 241 | object_type_short, osm_type_short: N, W, R for the object 242 | object_type_long, osm_type_long: node, way, relation for the object 243 | new_version: Old version number 244 | old_version: New version number: 245 | iso_datetime, datetime, iso_timestamp: ISO Timestamp of the new object 246 | epoch, epoch_datetime, epoch_timestamp: Unix Epoch timestamp (seconds since 1 Jan 1970) of the new object 247 | username: Username of the new object 248 | uid: UID of new object. 249 | changeset_id: Changeset ID of the new object 250 | changeset.TAG: TAG of the changeset 251 | tag_count_delta: What is the totaly change to the number 252 | ") 253 | ) 254 | 255 | .arg(Arg::with_name("object-types") 256 | .short('T').long("object-types") 257 | .value_name("[nwr]") 258 | .help("Only include these OSM Object types") 259 | .long_help("Only include these OSM Object types. Specify a letter for each type (n)ode/(w)way/(r)elation, e.g. -T wr = only ways & relations") 260 | .takes_value(true).required(false) 261 | .default_value("nwr") 262 | ) 263 | 264 | .arg(Arg::with_name("line-type") 265 | .long("line-type") 266 | .takes_value(true) 267 | .value_parser(["oldnew", "separate"]) 268 | .default_value("oldnew") 269 | ) 270 | 271 | 272 | .get_matches(); 273 | 274 | env_logger::builder() 275 | .filter_level(match matches.occurrences_of("verbosity") { 276 | 0 => log::LevelFilter::Warn, 277 | 1 => log::LevelFilter::Info, 278 | 2 => log::LevelFilter::Debug, 279 | _ => log::LevelFilter::Trace, 280 | }) 281 | .init(); 282 | 283 | let input_path = matches.value_of("input").unwrap(); 284 | info!("Begining processing of {}", input_path); 285 | 286 | let log_frequency: f32 = matches.value_of("log-frequency").unwrap().parse()?; 287 | 288 | let file = 289 | File::open(input_path).with_context(|| format!("opening input file {}", input_path))?; 290 | let mut osm_obj_reader = 291 | osmio::pbf::PBFReader::new(BufReader::new(ReaderWithSize::from_file(file)?)); 292 | let mut objects_iter = osm_obj_reader.objects(); 293 | 294 | let only_include_keys: SmallVec<[SmolStr; 2]> = matches 295 | .get_many::("key") 296 | .into_iter() 297 | .flatten() 298 | .cloned() 299 | .collect(); 300 | 301 | let only_include_tags: SmallVec<[(SmolStr, SmolStr); 2]> = matches 302 | .get_many("tag") 303 | .into_iter() 304 | .flatten() 305 | .map(|kv: &String| { 306 | let mut parts = kv.splitn(2, "=").map(SmolStr::from); 307 | (parts.next().unwrap(), parts.next().unwrap()) 308 | }) 309 | .collect(); 310 | 311 | let only_include_uids: Option> = match matches.values_of("uid") { 312 | None => None, 313 | Some(vals) => Some(vals.map(|u| Ok(u.parse()?)).collect::>()?), 314 | }; 315 | 316 | let only_include_types = match matches.value_of("object-types") { 317 | None => (true, true, true), 318 | Some(object_types) => { 319 | let object_types = object_types.to_lowercase(); 320 | ( 321 | object_types.contains('n'), 322 | object_types.contains('w'), 323 | object_types.contains('r'), 324 | ) 325 | } 326 | }; 327 | 328 | let columns: SmallVec<[Column; 12]> = matches 329 | .value_of("columns") 330 | .unwrap() 331 | .split(',') 332 | .map(|col_str| col_str.parse()) 333 | .collect::>()?; 334 | debug!("columns: {:?}", columns); 335 | 336 | let line_type = if columns.iter().any(|c| *c == Column::ValueCountDelta) { 337 | LineType::SeparateLines 338 | } else { 339 | LineType::OldNewValue 340 | }; 341 | 342 | if !only_include_tags.is_empty() { 343 | info!( 344 | "Only including changes to these {} tag(s): {:?}", 345 | only_include_tags.len(), 346 | only_include_tags 347 | ); 348 | } 349 | if !only_include_keys.is_empty() { 350 | info!( 351 | "Only including changes to these {} keys(s): {:?}", 352 | only_include_keys.len(), 353 | only_include_keys 354 | ); 355 | } 356 | 357 | if let Some(only_include_uids) = only_include_uids.as_ref() { 358 | info!( 359 | "Only including changes made by user id {:?}", 360 | only_include_uids 361 | ); 362 | } 363 | 364 | // MUST be replaced with above columns 365 | // changesets? 366 | let changeset_lookup = if columns.iter().any(Column::is_changeset_tag) { 367 | let lookup = 368 | ChangesetTagLookup::from_filename(matches.value_of("changeset_filename").unwrap())?; 369 | debug!( 370 | "Reading changeset sqlite from {}", 371 | matches.value_of("changeset_filename").unwrap() 372 | ); 373 | Some(lookup) 374 | } else { 375 | None 376 | }; 377 | 378 | let include_header = match ( 379 | matches.is_present("header"), 380 | matches.is_present("no-header"), 381 | ) { 382 | (false, false) => true, 383 | (true, false) => true, 384 | (false, true) => false, 385 | (true, true) => unreachable!(), 386 | }; 387 | 388 | let output_format = match ( 389 | matches.value_of("output_format"), 390 | matches.value_of("output"), 391 | ) { 392 | (Some("csv"), _) => OutputFormat::CSV, 393 | (Some("tsv"), _) => OutputFormat::TSV, 394 | (Some("auto"), Some("-")) => OutputFormat::CSV, 395 | (Some("auto"), Some(filename)) if filename.starts_with("/dev/fd/") => OutputFormat::CSV, 396 | (Some("auto"), Some(filename)) 397 | if filename.ends_with(".csv") || filename.ends_with(".csv.gz") => 398 | { 399 | OutputFormat::CSV 400 | } 401 | (Some("auto"), Some(filename)) 402 | if filename.ends_with(".tsv") || filename.ends_with(".tsv.gz") => 403 | { 404 | OutputFormat::TSV 405 | } 406 | (format, filename) => unreachable!( 407 | "Unable to determine output format: format={:?} filename={:?}", 408 | format, filename 409 | ), 410 | }; 411 | 412 | let output_path = matches.value_of("output").unwrap(); 413 | let output_writer: Box = if output_path == "-" { 414 | Box::new(std::io::stdout()) 415 | } else { 416 | Box::new(File::create(matches.value_of("output").unwrap())?) 417 | }; 418 | let output_writer = match matches.value_of("compression") { 419 | Some("auto") => { 420 | if output_path == "-" || output_path.starts_with("/dev/fd/") { 421 | // stdout, so no compression 422 | trace!("Output is '-' or a FD, no compression"); 423 | output_writer 424 | } else if output_path.ends_with(".csv.gz") || output_path.ends_with(".tsv.gz") { 425 | trace!("Output file ends with .[ct]sv.gz so using regular gzip"); 426 | Box::new(GzEncoder::new(output_writer, Compression::default())) 427 | } else if output_path.ends_with(".csv") || output_path.ends_with(".tsv") { 428 | // uncompressed 429 | trace!("Output file ends with .[ct]sv so no compression"); 430 | output_writer 431 | } else { 432 | bail!( 433 | "Cannot auto-detect output compression format: {:?}", 434 | output_path 435 | ); 436 | } 437 | } 438 | Some("none") => output_writer, 439 | Some("gzip") => Box::new(GzEncoder::new(output_writer, Compression::default())), 440 | _ => unreachable!(), 441 | }; 442 | let mut output = csv::WriterBuilder::new(); 443 | match output_format { 444 | OutputFormat::CSV => {} 445 | OutputFormat::TSV => { 446 | output.delimiter(b'\t'); 447 | } 448 | } 449 | let mut output = output.from_writer(output_writer); 450 | 451 | if include_header { 452 | trace!("Writing CSV header"); 453 | for c in columns.iter() { 454 | output.write_field(c.header().as_ref())?; 455 | } 456 | 457 | output.write_record(None::<&[u8]>)?; 458 | } 459 | 460 | let mut curr = objects_iter.next().unwrap(); 461 | let mut last: Option = None; 462 | 463 | let mut num_objects = 0; 464 | 465 | let mut time_counter = do_every::DoEvery::new(); 466 | 467 | let mut field_bytes = Vec::with_capacity(25); 468 | let mut utf8_bytes_buffer = vec![0; 4]; 469 | let started_processing = Instant::now(); 470 | let mut passes_uid_check; 471 | let mut passes_type_check; 472 | 473 | loop { 474 | // Logging output 475 | num_objects += 1; 476 | if num_objects % 1000 == 0 && time_counter.should_do_every_sec(log_frequency) { 477 | let reader = objects_iter.inner().inner().get_ref(); 478 | info!( 479 | "Running: {:.3}% done ETA: {} est. total: {}", 480 | reader.fraction() * 100., 481 | reader 482 | .eta() 483 | .map(|d| format_time(&d)) 484 | .unwrap_or_else(|| "N/A".to_string()), 485 | reader 486 | .est_total_time() 487 | .map(|d| format_time(&d)) 488 | .unwrap_or_else(|| "N/A".to_string()), 489 | ); 490 | num_objects = 1; 491 | } 492 | 493 | passes_uid_check = if let (Some(this_uid), Some(only_include_uids)) = 494 | (curr.uid(), only_include_uids.as_ref()) 495 | { 496 | // We have uid's & we're filtering based on uids 497 | only_include_uids.iter().any(|u| u == &this_uid) 498 | } else { 499 | true 500 | }; 501 | 502 | passes_type_check = matches!( 503 | (curr.object_type(), only_include_types), 504 | (OSMObjectType::Node, (true, _, _)) 505 | | (OSMObjectType::Way, (_, true, _)) 506 | | (OSMObjectType::Relation, (_, _, true)) 507 | ); 508 | 509 | let has_tags = match last { 510 | None => curr.tagged(), 511 | Some(ref l) => l.tagged() || curr.tagged(), 512 | }; 513 | let process_object = has_tags && passes_uid_check && passes_type_check; 514 | 515 | // The 'only_include_tags' could be checked here to speed it up 516 | 517 | if process_object { 518 | let (last_tags, last_version) = match last { 519 | None => (None, "".to_string()), 520 | Some(ref last) => { 521 | ensure!( 522 | sorted_objects(last, &curr) == Ordering::Less, 523 | "Non sorted input" 524 | ); 525 | if last.object_type() == curr.object_type() && last.id() == curr.id() { 526 | ( 527 | Some(last.tags().collect::>()), 528 | last.version().unwrap().to_string(), 529 | ) 530 | } else { 531 | (None, "".to_string()) 532 | } 533 | } 534 | }; 535 | 536 | let curr_tags: BTreeMap<_, _> = curr.tags().collect(); 537 | let mut keys: Vec<_> = curr_tags.keys().collect(); 538 | if let Some(ref lt) = last_tags { 539 | keys.extend(lt.keys()); 540 | } 541 | keys.sort(); 542 | keys.dedup(); 543 | 544 | let mut last_value: &str; 545 | let mut last_value_existed; 546 | let mut curr_value: &str; 547 | let mut curr_value_exists; 548 | 549 | for key in keys.into_iter() { 550 | // Should we skip this tag? 551 | if !only_include_keys.is_empty() && !only_include_keys.iter().any(|k| key == k) { 552 | continue; 553 | } 554 | if let Some(&value) = last_tags.as_ref().and_then(|lt| lt.get(key)) { 555 | last_value = value; 556 | last_value_existed = true; 557 | } else { 558 | last_value = ""; 559 | last_value_existed = false; 560 | }; 561 | 562 | if let Some(value) = curr_tags.get(key) { 563 | curr_value = value; 564 | curr_value_exists = true; 565 | } else { 566 | curr_value = ""; 567 | curr_value_exists = false; 568 | }; 569 | if last_value == curr_value { 570 | continue; 571 | } 572 | //dbg!(key); dbg!(last_value); dbg!(curr_value); 573 | //dbg!(&only_include_tags); 574 | if !only_include_tags.is_empty() 575 | && !only_include_tags 576 | .iter() 577 | .any(|(k, v)| k == key && (v == last_value || v == curr_value)) 578 | { 579 | continue; 580 | } 581 | 582 | trace!( 583 | "Write tag change {} {:?} → {:?} ({}→{})", 584 | key, last_value, curr_value, last_value_existed, curr_value_exists, 585 | ); 586 | 587 | let mut i: u8 = 0; 588 | 589 | loop { 590 | match (&line_type, i) { 591 | (LineType::OldNewValue, 0) => {} 592 | (LineType::OldNewValue, 1) => { 593 | break; 594 | } 595 | (LineType::OldNewValue, _) => { 596 | unreachable!() 597 | } 598 | (LineType::SeparateLines, 0) => { 599 | if !last_value_existed { 600 | i += 1; 601 | continue; 602 | } 603 | } 604 | (LineType::SeparateLines, 1) => { 605 | if !curr_value_exists { 606 | i += 1; 607 | continue; 608 | } 609 | } 610 | (LineType::SeparateLines, 2) => { 611 | break; 612 | } 613 | (LineType::SeparateLines, _) => { 614 | unreachable!() 615 | } 616 | } 617 | 618 | for column in columns.iter() { 619 | field_bytes.clear(); 620 | match column { 621 | Column::Key => { 622 | encode_field(key, &mut field_bytes, &mut utf8_bytes_buffer); 623 | } 624 | Column::NewValue => { 625 | encode_field(curr_value, &mut field_bytes, &mut utf8_bytes_buffer); 626 | } 627 | Column::OldValue => { 628 | encode_field(last_value, &mut field_bytes, &mut utf8_bytes_buffer); 629 | } 630 | Column::Value => { 631 | encode_field( 632 | match i { 633 | 0 => last_value, 634 | 1 => curr_value, 635 | _ => unreachable!(), 636 | }, 637 | &mut field_bytes, 638 | &mut utf8_bytes_buffer, 639 | ); 640 | } 641 | Column::Id => { 642 | field_bytes.extend( 643 | format!("{:?}{}", curr.object_type(), curr.id()) 644 | .as_str() 645 | .bytes(), 646 | ); 647 | } 648 | Column::RawId => { 649 | field_bytes.extend(curr.id().to_string().as_str().bytes()) 650 | } 651 | Column::NewVersion => { 652 | field_bytes.extend(curr.version().unwrap().to_string().bytes()); 653 | } 654 | Column::OldVersion => { 655 | field_bytes.extend(last_version.as_str().bytes()); 656 | } 657 | Column::IsoDatetime => { 658 | field_bytes.extend( 659 | curr.timestamp().as_ref().unwrap().to_iso_string().bytes(), 660 | ); 661 | } 662 | Column::EpochDatetime => { 663 | field_bytes.extend( 664 | curr.timestamp() 665 | .as_ref() 666 | .unwrap() 667 | .to_epoch_number() 668 | .to_string() 669 | .bytes(), 670 | ); 671 | } 672 | Column::Username => { 673 | encode_field( 674 | curr.user().unwrap(), 675 | &mut field_bytes, 676 | &mut utf8_bytes_buffer, 677 | ); 678 | } 679 | Column::Uid => { 680 | field_bytes.extend(curr.uid().unwrap().to_string().bytes()); 681 | } 682 | Column::ChangesetId => { 683 | field_bytes 684 | .extend(curr.changeset_id().unwrap().to_string().bytes()); 685 | } 686 | Column::ObjectTypeShort => { 687 | field_bytes.extend(match curr.object_type() { 688 | OSMObjectType::Node => b"n", 689 | OSMObjectType::Way => b"w", 690 | OSMObjectType::Relation => b"r", 691 | }); 692 | } 693 | Column::ObjectTypeLong => { 694 | field_bytes.extend(match curr.object_type() { 695 | OSMObjectType::Node => b"node".iter(), 696 | OSMObjectType::Way => b"way".iter(), 697 | OSMObjectType::Relation => b"relation".iter(), 698 | }); 699 | } 700 | Column::ChangesetTag(changeset_tag) => { 701 | match changeset_lookup 702 | .as_ref() 703 | .unwrap() 704 | .tags(curr.changeset_id().unwrap())? 705 | { 706 | None => { 707 | trace!( 708 | "No tags found for changeset {:?}", 709 | curr.changeset_id() 710 | ); 711 | } 712 | Some(tags_for_changeset) => { 713 | if let Some(v) = tags_for_changeset 714 | .iter() 715 | .filter_map(|(k, v)| { 716 | if k == changeset_tag { Some(v) } else { None } 717 | }) 718 | .next() 719 | { 720 | field_bytes.extend(v.bytes()); 721 | } 722 | } 723 | } 724 | } 725 | Column::TagCountDelta => { 726 | field_bytes.extend(match (last_value_existed, curr_value_exists) { 727 | (false, false) => unreachable!(), 728 | (false, true) => b"+1".iter(), 729 | (true, false) => b"-1".iter(), 730 | (true, true) => b"0".iter(), 731 | }); 732 | } 733 | 734 | Column::ValueCountDelta => { 735 | field_bytes.extend(match i { 736 | 0 => b"-1".iter(), 737 | 1 => b"+1".iter(), 738 | _ => unreachable!(), 739 | }); 740 | } 741 | } 742 | output.write_field(&field_bytes)?; 743 | } 744 | 745 | output.write_record(None::<&[u8]>)?; 746 | 747 | i += 1; 748 | } 749 | } 750 | } 751 | 752 | last = Some(curr); 753 | curr = match objects_iter.next() { 754 | None => { 755 | break; 756 | } 757 | Some(o) => o, 758 | }; 759 | } 760 | 761 | info!( 762 | "Finished in {}", 763 | format_time(&(Instant::now() - started_processing)) 764 | ); 765 | Ok(()) 766 | } 767 | 768 | fn encode_field(field: &str, bytes: &mut Vec, utf8_bytes_buffer: &mut [u8]) { 769 | bytes.clear(); 770 | 771 | for c in field.chars() { 772 | if c == '\t' { 773 | bytes.push(b'\\'); 774 | bytes.push(b't'); 775 | } else if c == '\n' { 776 | bytes.push(b'\\'); 777 | bytes.push(b'n'); 778 | } else { 779 | c.encode_utf8(utf8_bytes_buffer); 780 | bytes.extend(&utf8_bytes_buffer[..c.len_utf8()]); 781 | } 782 | } 783 | } 784 | 785 | fn sorted_objects(a: &impl OSMObj, b: &impl OSMObj) -> std::cmp::Ordering { 786 | a.object_type() 787 | .cmp(&b.object_type()) 788 | .then(a.id().cmp(&b.id())) 789 | .then(a.version().cmp(&b.version())) 790 | } 791 | 792 | pub fn format_time(duration: &std::time::Duration) -> String { 793 | let sec = duration.as_secs_f32().round() as u64; 794 | if sec < 60 { 795 | format!("{:2}s", sec) 796 | } else { 797 | let (min, sec) = (sec / 60, sec % 60); 798 | if min < 60 { 799 | format!("{:2}m{:02}s", min, sec) 800 | } else { 801 | let (hr, min) = (min / 60, min % 60); 802 | if hr < 24 { 803 | format!("{}h{:02}m{:02}s", hr, min, sec) 804 | } else { 805 | let (day, hr) = (hr / 24, hr % 24); 806 | format!("{}d{}h{:02}m{:02}s", day, hr, min, sec) 807 | } 808 | } 809 | } 810 | } 811 | 812 | struct ChangesetTagLookup { 813 | conn: Connection, 814 | } 815 | 816 | impl ChangesetTagLookup { 817 | fn from_filename(filename: &str) -> Result { 818 | let conn = Connection::open(filename)?; 819 | Ok(ChangesetTagLookup { conn }) 820 | } 821 | 822 | fn tags(&self, cid: u32) -> Result>> { 823 | let res: Option> = self 824 | .conn 825 | .query_row( 826 | "select other_tags from changeset_tags where id = ?1;", 827 | [cid], 828 | |row| row.get(0), 829 | ) 830 | .optional()?; 831 | match res { 832 | None => Ok(None), 833 | Some(tags) => { 834 | let tags: Vec<(String, String)> = serde_json::from_slice(&tags)?; 835 | Ok(Some(tags)) 836 | } 837 | } 838 | } 839 | } 840 | --------------------------------------------------------------------------------