├── .gitignore ├── LICENSE ├── PostProcessing.txt ├── README.md ├── pom.xml ├── samples ├── map.osm ├── map2.osm.bz2 ├── one-street.osm ├── one-way-backward.osm ├── one-way-forward.osm ├── parking-and-streets.osm ├── parking.osm └── two-street.osm └── src ├── main ├── assembly │ ├── docs.xml │ └── procedures.xml └── java │ └── org │ └── neo4j │ └── gis │ ├── osm │ ├── OSMImportTool.java │ ├── importer │ │ ├── OSMInput.java │ │ └── PrintingImportLogicMonitor.java │ ├── model │ │ └── OSMModel.java │ └── procedures │ │ └── OSMProcedures.java │ └── spatial │ └── SpatialConstants.java └── test └── java └── org └── neo4j └── gis └── osm ├── OSMImportToolTest.java ├── model ├── OSMModelIntegrationTest.java ├── OSMModelTest.java └── TestOSMModel.java └── procedures └── OSMProceduresTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | *~ 11 | *.iml 12 | .idea 13 | bad.log 14 | logs/ 15 | 16 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 17 | !/.mvn/wrapper/maven-wrapper.jar 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 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 Affero 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 special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU 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 General Public License for more details. 646 | 647 | You should have received a copy of the GNU 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 the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /PostProcessing.txt: -------------------------------------------------------------------------------- 1 | Problems: 2 | ========= 3 | 4 | Broken model: 5 | * Duplicate relationships 6 | * Disconnected ways (first node missing) 7 | 8 | Less optimal model: 9 | * No distance calculation on NEXT 10 | * Complex geometries 11 | 12 | Duplicate relationships 13 | ----------------------- 14 | 15 | Custom BadCollector to note duplicate nodes and use this to search for duplicate relationships. 16 | 17 | Disconnected ways (first node missing) 18 | -------------------------------------- 19 | 20 | Custom BadCollector to take note of duplicate ways, and then use that as a set to post-process looking for missing FIRST_NODE 21 | relationships and then searching for OSMWayNode's with same way_osm_id properties, and re-connecting the way. 22 | 23 | Taking note of CollectBadRelationships will also help find possible broken ways. 24 | 25 | No distance calculation on NEXT 26 | ------------------------------- 27 | 28 | Post-processing. This can be calculated on demand as part of a query, but will be slow. Better to traverse all ways and fill 29 | in the distance on NEXT and the total distance on way tags or way properties. 30 | 31 | Complex geometries 32 | ------------------ 33 | 34 | Post processing. 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OSM for Neo4j 2 | 3 | An OpenStreetMap data model and importer for Neo4j. 4 | 5 | There exists an OSM data model and importer in the 'Neo4j Spatial' plugin project that supports 6 | Neo4j 1.x, 2.x and 3.x. However that tool has a few concerns: 7 | 8 | * It does not scale. The use of a lucene index for the OSM-id to Neo4j node-id mapping for creating 9 | relationships puts a ceiling on the effective data size loadable. Typically users only load cities 10 | or at most very small countries. Large countries are very hard to load, and the entire planet completely 11 | out of reach. 12 | * It is entirely unrelated to the new spatial index built into Neo4j 3.4. It was designed to work exclusively 13 | with the RTree index and GeometryEncoder GIS interface of the _Neo4j Spatial_ project. 14 | * It was designed to reflect the actual OSM graph model which supports a single editable graph of all data. 15 | This makes it a very complex model which is not suitable for some use cases like routing. 16 | 17 | There is interest in getting a good OSM data model for many use cases 18 | (sandbox, existing OSM users of Neo4j Spatial, etc.) and this leads us to want to create a new model 19 | and importer with the following characteristics: 20 | 21 | * Fast and scalable (using the parallel batch importer introduced in Neo4j 2.x) 22 | * Possible to use without 'Neo4j Spatial' (ie. can be used on Neo4j 3.4 built-in spatial index). 23 | * Can replace the older OSMImporter in Neo4j Spatial (ie. should work with Neo4j Spatial also). 24 | * Support for a wider range of use cases, including routing 25 | 26 | ## Building 27 | 28 | This is a maven project so it can be built using: 29 | 30 | mvn clean install 31 | 32 | This will run all tests which involves importing some OSM files. If you want to skip that: 33 | 34 | mvn clean install -DskipTests 35 | 36 | The build will produce two jars and copy them to the local maven repository: 37 | 38 | * `target/osm-0.2.3-neo4j-4.1.6.jar` is aimed to be used as a dependency in maven projects that depend on this library 39 | * `target/osm-0.2.3-neo4j-4.1.6-procedures.jar` including procedures, this jar can be copied directly into a Neo4j installation's `plugins` folder 40 | 41 | We plan to make a third jar `target/osm-0.2.3-neo4j-4.1.6-all.jar` including all dependencies to faciliate running the command-line importer. 42 | But until then you need to copy and reference all dependencies as described below. 43 | 44 | ## Running 45 | 46 | Get all dependencies together: 47 | 48 | mvn dependency:copy-dependencies 49 | 50 | To run with the jar at `target/osm-0.2.3-neo4j-4.1.6.jar`: 51 | 52 | java -Xms1280m -Xmx1280m \ 53 | -cp "target/osm-0.2.3-neo4j-4.1.6.jar:target/dependency/*" org.neo4j.gis.osm.OSMImportTool \ 54 | --skip-duplicate-nodes --delete --into target/neo4j --database map2 samples/map2.osm.bz2 55 | 56 | This will import the `samples/map2.osm.bz2` file into the database at `target/neo4j/data/databases/map2`. 57 | You can pass more than one file on the command-line to import multiple files into the same database. 58 | 59 | The values you pass to the JVM memory settings should be based on the needs of the files being imported. 60 | For very large files, use a high fraction of available machine memory. The example values above `-Xms1280m -Xmx1280m` 61 | were sufficient to import all of Scandinavia: Sweden, Finland, Iceland, Norway and Denmark, which combined had BZ2 files of 1.5GB. 62 | 63 | The entire US North-East has a BZ2 file of about 1.2G and so should import with similar settings. 64 | 65 | ## Changes specific to Neo4j 4.0 66 | 67 | The above command has changed since the Neo4j 3.5 release. The `--into` argument now describes the root directory 68 | of the Neo4j installation that contains `data/databases` and `data/transactions` subdirectories. The full path 69 | to the database itself will be that root, with `data/databases/` and the database name appended. It is also worth noting 70 | that the database transaction logs will be in `data/transactions/` with the database name appended. If you need to copy 71 | the database into a separate Neo4j installation, you should copy both directories. For example: 72 | 73 | cp -a target/neo4j/data/databases/map2 $NEO4J_HOME/data/databases/ 74 | cp -a target/neo4j/data/transactions/map2 $NEO4J_HOME/data/transactions/ 75 | 76 | If you are running an Enterprise version of Neo4j, you will need to run `CREATE DATABASE map2` to mount this new database into the new multi-database server. 77 | This will _mount_ the database into the server without requiring a server restart. 78 | See the Neo4j documentation for more details on how to run the new administration commands. 79 | At the time of writing, the appropriate section could be found at https://neo4j.com/docs/cypher-manual/current/administration. 80 | 81 | If you are running the community version of Neo4j, you will need to edit the neo4j.conf file to change the 82 | value of `dbms.default_database` to the new database name and restart the server. 83 | As was the case in Neo4j 3.x versions, the community server allows you to have as many databases as you wish, but you can only run one of them at a time. 84 | The one exception to this is the new `system` database, which is not relevant to this spatial library 85 | except in that it is the reason for the small changes in directory structure described above, 86 | which necessitated the change in command-line options for the `OSMImportTool` utility. 87 | 88 | ## Other command-line options 89 | 90 | There are many available command-line options inherited from the `neo4j-import` tool on which this was based. 91 | Run with the `--help` option to see the complete list. 92 | 93 | Common options are: 94 | 95 | ``` 96 | --into 97 | The root of the DBMS into which to do the import. 98 | --database 99 | Database name to import into. Must not contain existing database. 100 | --delete 101 | Whether or not to delete the existing database before creating a new one. 102 | Default value: false 103 | --range 104 | Optional filter for including only points within the specified range 105 | --skip-duplicate-nodes 106 | Whether or not to skip importing nodes that have the same id/group. In the event 107 | of multiple nodes within the same group having the same id, the first 108 | encountered will be imported whereas consecutive such nodes will be skipped. 109 | Skipped nodes will be logged, containing at most number of entities specified by 110 | bad-tolerance, unless otherwise specified by skip-bad-entries-loggingoption. 111 | Default value: false 112 | ``` 113 | 114 | ## Procedures 115 | 116 | To help build graphs that can be used for routing, two procedures have been added: 117 | 118 | * `spatial.osm.routeIntersection(node,false,false,false)` 119 | * `spatial.osm.routePointOfInterest(node,ways)` 120 | 121 | These can be installed into an installation of Neo4j by copying the `osm-0.2.3-neo4j-4.1.6-procedures.jar` file into the `plugins` folder, and restarting the database. 122 | 123 | ### Creating a routing graph of intersections 124 | 125 | First identify nodes that are interections where a traveller can make a choice: 126 | 127 | MATCH (n:OSMNode) 128 | WHERE size((n)<-[:NODE]-(:OSMWayNode)-[:NEXT]-(:OSMWayNode)) > 2 129 | AND NOT (n:Intersection) 130 | WITH n LIMIT 100 131 | MATCH (n)<-[:NODE]-(wn:OSMWayNode), (wn)<-[:NEXT*0..100]-(wx), 132 | (wx)<-[:FIRST_NODE]-(w:OSMWay)-[:TAGS]->(wt:OSMTags) 133 | WHERE exists(wt.highway) 134 | SET n:Intersection 135 | RETURN count(*); 136 | 137 | Then create a routing graph of `:ROUTE` relationships between the `:Intersection` nodes: 138 | 139 | MATCH (x:Intersection) WITH x LIMIT 100 140 | CALL spatial.osm.routeIntersection(x,false,false,false) 141 | YIELD fromNode, toNode, fromRel, toRel, distance, length, count 142 | WITH fromNode, toNode, fromRel, toRel, distance, length, count 143 | MERGE (fromNode)-[r:ROUTE {fromRel:id(fromRel),toRel:id(toRel)}]->(toNode) 144 | ON CREATE SET r.distance = distance, r.length = length, r.count = count 145 | RETURN count(*); 146 | 147 | ### Find points of interest and add to the routing graph 148 | 149 | Using a selection of tags appropriate for your app, find nodes that are points of interest and connect them to the graph: 150 | 151 | ``` 152 | UNWIND ["restaurant","fast_food","cafe","bar","pub","ice_cream","cinema"] AS amenity 153 | MATCH (x:OSMNode)-[:TAGS]->(t:OSMTags) 154 | WHERE t.amenity = amenity AND NOT (x)-[:ROUTE]->() 155 | WITH x, x.location as poi LIMIT 100 156 | MATCH (n:OSMNode) 157 | WHERE distance(poi, n.location) < 100 158 | WITH x, n 159 | MATCH (n)<-[:NODE]-(wn:OSMWayNode), (wn)<-[:NEXT*0..10]-(wx), 160 | (wx)<-[:FIRST_NODE]-(w:OSMWay)-[:TAGS]->(wt:OSMTags) 161 | WITH x, w, wt 162 | WHERE exists(wt.highway) 163 | WITH x, collect(w) as ways 164 | CALL spatial.osm.routePointOfInterest(x,ways) YIELD node 165 | SET x:PointOfInterest 166 | RETURN count(node); 167 | ``` 168 | 169 | Link the points of interest sub-graph into the routing sub-graph: 170 | 171 | MATCH (x:Routable:OSMNode) 172 | WHERE NOT (x)-[:ROUTE]->(:Intersection) WITH x LIMIT 100 173 | CALL spatial.osm.routeIntersection(x,true,false,false) 174 | YIELD fromNode, toNode, fromRel, toRel, distance, length, count 175 | WITH fromNode, toNode, fromRel, toRel, distance, length, count 176 | MERGE (fromNode)-[r:ROUTE {fromRel:id(fromRel),toRel:id(toRel)}]->(toNode) 177 | ON CREATE SET r.distance = distance, r.length = length, r.count = count 178 | RETURN count(*); 179 | 180 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | org.neo4j.gis 5 | osm 6 | jar 7 | 0.2.3-neo4j-4.1.6 8 | OpenStreetMap for Neo4j 9 | OpenStreetMap data model and importer tool for Neo4j 10 | 11 | 12 | UTF-8 13 | 11 14 | 1.19 15 | 1.16.1 16 | 4.1.6 17 | 3.1 18 | 3.1.0 19 | 2.5.4 20 | 21 | 22 | 23 | Neo4j AB 24 | https://neo4j.com 25 | 26 | 27 | 28 | 29 | craigtaverner 30 | Neo4j AB 31 | https://neo4j.com 32 | 33 | 34 | 35 | https://github.com/neo4j-contrib/osm 36 | 37 | 38 | https://github.com/neo4j-contrib/osm 39 | 40 | 41 | 42 | 43 | GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 44 | https://www.gnu.org/licenses/gpl.txt 45 | 46 | Note that this license is for the project itself, and not for its dependencies. 47 | See the included NOTICE.txt file for further details. 48 | 49 | repo 50 | 51 | 52 | 53 | 54 | 55 | org.neo4j 56 | neo4j 57 | ${neo4j.version} 58 | provided 59 | 60 | 61 | 62 | 63 | org.openjdk.jmh 64 | jmh-core 65 | ${jmh.version} 66 | test 67 | 68 | 69 | 70 | 71 | junit 72 | junit 73 | 4.12 74 | test 75 | 76 | 77 | org.hamcrest 78 | hamcrest-core 79 | 1.3 80 | test 81 | 82 | 83 | org.hamcrest 84 | hamcrest-library 85 | 1.3 86 | test 87 | 88 | 89 | org.mockito 90 | mockito-core 91 | 1.10.19 92 | test 93 | 94 | 95 | org.neo4j 96 | neo4j-kernel 97 | ${neo4j.version} 98 | test-jar 99 | test 100 | 101 | 102 | org.neo4j 103 | neo4j-io 104 | ${neo4j.version} 105 | test-jar 106 | test 107 | 108 | 109 | org.neo4j.community 110 | it-test-support 111 | ${neo4j.version} 112 | test 113 | 114 | 115 | 116 | 117 | 118 | 119 | org.apache.maven.plugins 120 | maven-compiler-plugin 121 | ${maven-compiler-plugin.version} 122 | 123 | 124 | ${javac.target} 125 | ${javac.target} 126 | ${javac.target} 127 | 128 | 129 | 130 | org.apache.maven.plugins 131 | maven-source-plugin 132 | 2.2.1 133 | 134 | 135 | attach-sources 136 | 137 | jar 138 | 139 | 140 | 141 | 142 | 143 | org.apache.maven.plugins 144 | maven-javadoc-plugin 145 | ${maven-source-plugin-version} 146 | 147 | 148 | attach-javadocs 149 | 150 | jar 151 | 152 | 153 | 154 | 155 | false 156 | 157 | 158 | 159 | maven-assembly-plugin 160 | ${maven-assembly-plugin.version} 161 | 162 | 163 | src/main/assembly/procedures.xml 164 | 165 | 166 | 167 | 168 | make-assembly 169 | 170 | package 171 | 172 | 173 | single 174 | 175 | 176 | 177 | 178 | 179 | maven-surefire-plugin 180 | 2.18.1 181 | 182 | 1 183 | 1 184 | false 185 | -server -Xms1024m -Xmx2048m 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | repo 194 | https://raw.github.com/neo4j-contrib/m2/master/releases 195 | 196 | 197 | snapshot-repo 198 | https://raw.github.com/neo4j-contrib/m2/master/snapshots 199 | 200 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /samples/map2.osm.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j-contrib/osm/407e379818f003ca69dc125208dabf85e0112855/samples/map2.osm.bz2 -------------------------------------------------------------------------------- /samples/one-street.osm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /samples/one-way-backward.osm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/one-way-forward.osm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/parking-and-streets.osm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /samples/parking.osm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/two-street.osm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/main/assembly/docs.xml: -------------------------------------------------------------------------------- 1 | 22 | 23 | docs 24 | false 25 | 26 | jar 27 | 28 | 29 | 30 | true 31 | target/docs 32 | /dev 33 | 34 | 35 | src/docs 36 | / 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/main/assembly/procedures.xml: -------------------------------------------------------------------------------- 1 | 22 | 23 | procedures 24 | 25 | jar 26 | 27 | false 28 | 29 | 30 | / 31 | true 32 | true 33 | true 34 | runtime 35 | 36 | 37 | 38 | 39 | metaInf-services 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/main/java/org/neo4j/gis/osm/OSMImportTool.java: -------------------------------------------------------------------------------- 1 | package org.neo4j.gis.osm; 2 | 3 | import org.neo4j.common.Validator; 4 | import org.neo4j.configuration.Config; 5 | import org.neo4j.configuration.GraphDatabaseSettings; 6 | import org.neo4j.configuration.SettingValueParsers; 7 | import org.neo4j.function.Predicates; 8 | import org.neo4j.gis.osm.importer.OSMInput; 9 | import org.neo4j.gis.osm.importer.PrintingImportLogicMonitor; 10 | import org.neo4j.internal.batchimport.BatchImporter; 11 | import org.neo4j.internal.batchimport.BatchImporterFactory; 12 | import org.neo4j.internal.batchimport.Configuration; 13 | import org.neo4j.internal.batchimport.ImportLogic; 14 | import org.neo4j.internal.batchimport.cache.idmapping.string.DuplicateInputIdException; 15 | import org.neo4j.internal.batchimport.input.BadCollector; 16 | import org.neo4j.internal.batchimport.input.Collector; 17 | import org.neo4j.internal.batchimport.input.InputException; 18 | import org.neo4j.internal.batchimport.staging.ExecutionMonitor; 19 | import org.neo4j.internal.batchimport.staging.ExecutionMonitors; 20 | import org.neo4j.internal.batchimport.staging.SpectrumExecutionMonitor; 21 | import org.neo4j.internal.helpers.Args; 22 | import org.neo4j.internal.helpers.Exceptions; 23 | import org.neo4j.io.IOUtils; 24 | import org.neo4j.io.fs.DefaultFileSystemAbstraction; 25 | import org.neo4j.io.fs.FileSystemAbstraction; 26 | import org.neo4j.io.fs.FileUtils; 27 | import org.neo4j.io.layout.DatabaseLayout; 28 | import org.neo4j.io.layout.Neo4jLayout; 29 | import org.neo4j.io.os.OsBeanUtil; 30 | import org.neo4j.io.pagecache.tracing.DefaultPageCacheTracer; 31 | import org.neo4j.io.pagecache.tracing.PageCacheTracer; 32 | import org.neo4j.kernel.impl.store.format.RecordFormatSelector; 33 | import org.neo4j.kernel.impl.transaction.log.files.TransactionLogInitializer; 34 | import org.neo4j.kernel.impl.util.Validators; 35 | import org.neo4j.kernel.internal.Version; 36 | import org.neo4j.kernel.lifecycle.LifeSupport; 37 | import org.neo4j.logging.internal.LogService; 38 | import org.neo4j.logging.internal.StoreLogService; 39 | import org.neo4j.memory.EmptyMemoryTracker; 40 | import org.neo4j.scheduler.JobScheduler; 41 | 42 | import java.io.*; 43 | import java.nio.charset.Charset; 44 | import java.nio.file.Path; 45 | import java.util.ArrayList; 46 | import java.util.Arrays; 47 | import java.util.List; 48 | import java.util.Map; 49 | import java.util.concurrent.TimeUnit; 50 | import java.util.function.Function; 51 | 52 | import static java.nio.charset.Charset.defaultCharset; 53 | import static org.apache.commons.lang3.ArrayUtils.isEmpty; 54 | import static org.neo4j.configuration.GraphDatabaseSettings.logs_directory; 55 | import static org.neo4j.configuration.GraphDatabaseSettings.store_internal_log_path; 56 | import static org.neo4j.internal.batchimport.AdditionalInitialIds.EMPTY; 57 | import static org.neo4j.internal.batchimport.Configuration.*; 58 | import static org.neo4j.internal.batchimport.input.BadCollector.BAD_FILE_NAME; 59 | import static org.neo4j.internal.batchimport.input.Collectors.*; 60 | import static org.neo4j.internal.helpers.Exceptions.throwIfUnchecked; 61 | import static org.neo4j.internal.helpers.Strings.TAB; 62 | import static org.neo4j.io.ByteUnit.bytes; 63 | import static org.neo4j.io.ByteUnit.mebiBytes; 64 | import static org.neo4j.kernel.impl.scheduler.JobSchedulerFactory.createScheduler; 65 | import static org.neo4j.kernel.impl.store.PropertyType.EMPTY_BYTE_ARRAY; 66 | 67 | public class OSMImportTool { 68 | 69 | private static final String UNLIMITED = "unlimited"; 70 | 71 | enum Options { 72 | HOME_DIR("into", null, 73 | "", 74 | "The root of the DBMS into which to do the import."), 75 | DB_NAME("database", null, 76 | "", 77 | "Database name to import into. " + "Must not contain existing database.", true), 78 | DELETE_DB("delete", Boolean.FALSE, "", 79 | "Whether or not to delete the existing database before creating a new one."), 80 | INPUT_ENCODING("input-encoding", null, 81 | "", 82 | "Character set that input data is encoded in. Provided value must be one out of the available " 83 | + "character sets in the JVM, as provided by Charset#availableCharsets(). " 84 | + "If no input encoding is provided, the default character set of the JVM will be used.", 85 | true), 86 | PROCESSORS("processors", null, 87 | "", 88 | "(advanced) Max number of processors used by the importer. Defaults to the number of " 89 | + "available processors reported by the JVM" 90 | + availableProcessorsHint() 91 | + ". There is a certain amount of minimum threads needed so for that reason there " 92 | + "is no lower bound for this value. For optimal performance this value shouldn't be " 93 | + "greater than the number of available processors."), 94 | STACKTRACE("stacktrace", false, 95 | "", 96 | "Enable printing of error stack traces."), 97 | BAD_TOLERANCE("bad-tolerance", UNLIMITED, 98 | "", 99 | "Number of bad entries before the import is considered failed. This tolerance threshold is " 100 | + "about relationships refering to missing nodes. Format errors in input data are " 101 | + "still treated as errors"), 102 | SKIP_BAD_ENTRIES_LOGGING("skip-bad-entries-logging", Boolean.FALSE, "", 103 | "Whether or not to skip logging bad entries detected during import."), 104 | SKIP_BAD_RELATIONSHIPS("skip-bad-relationships", Boolean.TRUE, 105 | "", 106 | "Whether or not to skip importing relationships that refers to missing node ids, i.e. either " 107 | + "start or end node id/group referring to node that wasn't specified by the " 108 | + "node input data. " 109 | + "Skipped nodes will be logged" 110 | + ", containing at most number of entites specified by " + BAD_TOLERANCE.key() + ", unless " 111 | + "otherwise specified by " + SKIP_BAD_ENTRIES_LOGGING.key() + " option."), 112 | RANGE("range", null, 113 | "", 114 | "Optional filter for including only points within the specified range" 115 | ), 116 | SKIP_DUPLICATE_NODES("skip-duplicate-nodes", Boolean.FALSE, 117 | "", 118 | "Whether or not to skip importing nodes that have the same id/group. In the event of multiple " 119 | + "nodes within the same group having the same id, the first encountered will be imported " 120 | + "whereas consecutive such nodes will be skipped. " 121 | + "Skipped nodes will be logged" 122 | + ", containing at most number of entities specified by " + BAD_TOLERANCE.key() + ", unless " 123 | + "otherwise specified by " + SKIP_BAD_ENTRIES_LOGGING.key() + "option."), 124 | ADDITIONAL_CONFIG("additional-config", null, 125 | "", 126 | "(advanced) File specifying database-specific configuration. For more information consult " 127 | + "manual about available configuration options for a neo4j configuration file. " 128 | + "Only configuration affecting store at time of creation will be read.", true), 129 | MAX_MEMORY("max-memory", null, 130 | "", 131 | "(advanced) Maximum memory that importer can use for various data structures and caching " + 132 | "to improve performance. If left as unspecified (null) it is set to " + DEFAULT_MAX_MEMORY_PERCENT + 133 | "% of (free memory on machine - max JVM memory). " + 134 | "Values can be plain numbers, like 10000000 or e.g. 20G for 20 gigabyte, or even e.g. 70%."), 135 | CACHE_ON_HEAP("cache-on-heap", 136 | DEFAULT.allowCacheAllocationOnHeap(), 137 | "Whether or not to allow allocating memory for the cache on heap", 138 | "(advanced) Whether or not to allow allocating memory for the cache on heap. " + 139 | "If 'false' then caches will still be allocated off-heap, but the additional free memory " + 140 | "inside the JVM will not be allocated for the caches. This to be able to have better control " + 141 | "over the heap memory"), 142 | HIGH_IO("high-io", null, "Assume a high-throughput storage subsystem", 143 | "(advanced) Ignore environment-based heuristics, and assume that the target storage subsystem can " + 144 | "support parallel IO with high throughput."), 145 | DETAILED_PROGRESS("detailed-progress", Boolean.FALSE, "true/false", "Use the old detailed 'spectrum' progress printing"), 146 | TRACE_PAGE_CACHE("trace-page-cache", Boolean.FALSE, "true/false", "Trace the counts of page cache usage"); 147 | 148 | private final String key; 149 | private final Object defaultValue; 150 | private final String usage; 151 | private final String description; 152 | private final boolean keyAndUsageGoTogether; 153 | private final boolean supported; 154 | 155 | Options(String key, Object defaultValue, String usage, String description) { 156 | this(key, defaultValue, usage, description, false, false); 157 | } 158 | 159 | Options(String key, Object defaultValue, String usage, String description, boolean supported) { 160 | this(key, defaultValue, usage, description, supported, false); 161 | } 162 | 163 | Options(String key, Object defaultValue, String usage, String description, boolean supported, boolean keyAndUsageGoTogether) { 164 | this.key = key; 165 | this.defaultValue = defaultValue; 166 | this.usage = usage; 167 | this.description = description; 168 | this.supported = supported; 169 | this.keyAndUsageGoTogether = keyAndUsageGoTogether; 170 | } 171 | 172 | String key() { 173 | return key; 174 | } 175 | 176 | String argument() { 177 | return "--" + key(); 178 | } 179 | 180 | void printUsage(PrintStream out) { 181 | out.println(argument() + spaceInBetweenArgumentAndUsage() + usage); 182 | for (String line : splitLongLine(descriptionWithDefaultValue().replace("`", ""), 80)) { 183 | out.println("\t" + line); 184 | } 185 | } 186 | 187 | private String spaceInBetweenArgumentAndUsage() { 188 | return keyAndUsageGoTogether ? "" : " "; 189 | } 190 | 191 | String descriptionWithDefaultValue() { 192 | String result = description; 193 | if (defaultValue != null) { 194 | if (!result.endsWith(".")) { 195 | result += "."; 196 | } 197 | result += " Default value: " + defaultValue; 198 | } 199 | return result; 200 | } 201 | 202 | String manPageEntry() { 203 | String filteredDescription = descriptionWithDefaultValue().replace(availableProcessorsHint(), ""); 204 | String usageString = (usage.length() > 0) ? spaceInBetweenArgumentAndUsage() + usage : ""; 205 | return "*" + argument() + usageString + "*::\n" + filteredDescription + "\n\n"; 206 | } 207 | 208 | String manualEntry() { 209 | return "[[import-tool-option-" + key() + "]]\n" + manPageEntry() + "//^\n\n"; 210 | } 211 | 212 | Object defaultValue() { 213 | return defaultValue; 214 | } 215 | 216 | private static String availableProcessorsHint() { 217 | return " (in your case " + Runtime.getRuntime().availableProcessors() + ")"; 218 | } 219 | 220 | public boolean isSupportedOption() { 221 | return this.supported; 222 | } 223 | } 224 | 225 | public static void main(String[] incomingArguments) throws Exception { 226 | main(incomingArguments, false); 227 | } 228 | 229 | public static final Validator DIRECTORY_IS_WRITABLE = value -> 230 | { 231 | if (value.mkdirs()) { // It's OK, we created the directory right now, which means we have write access to it 232 | return; 233 | } 234 | 235 | File test = new File(value, "_______test___"); 236 | try { 237 | test.createNewFile(); 238 | } catch (IOException e) { 239 | throw new IllegalArgumentException("Directory '" + value + "' not writable: " + e.getMessage()); 240 | } finally { 241 | test.delete(); 242 | } 243 | }; 244 | 245 | public static void main(String[] incomingArguments, boolean defaultSettingsSuitableForTests) throws Exception { 246 | PrintStream out = System.out; 247 | PrintStream err = System.err; 248 | Args args = Args.parse(incomingArguments); 249 | 250 | if (isEmpty(incomingArguments) || asksForUsage(args)) { 251 | printUsage(out); 252 | return; 253 | } 254 | 255 | String[] osmFiles; 256 | boolean enableStacktrace; 257 | Number processors; 258 | long badTolerance; 259 | Charset inputEncoding; 260 | boolean skipBadRelationships; 261 | boolean skipDuplicateNodes; 262 | boolean ignoreExtraColumns; 263 | boolean skipBadEntriesLogging; 264 | Config dbConfig; 265 | OutputStream badOutput = null; 266 | org.neo4j.internal.batchimport.Configuration configuration; 267 | File badFile = null; 268 | Long maxMemory; 269 | Boolean defaultHighIO; 270 | InputStream in; 271 | 272 | try (FileSystemAbstraction fs = new DefaultFileSystemAbstraction()) { 273 | File homeDir = args.interpretOption(Options.HOME_DIR.key(), Converters.mandatory(), Converters.toFile(), DIRECTORY_IS_WRITABLE); 274 | homeDir.mkdirs(); 275 | var homeLayout = Neo4jLayout.of(homeDir); 276 | var databaseName = args.get(Options.DB_NAME.key(), "osm"); 277 | var databaseLayout = homeLayout.databaseLayout(databaseName); 278 | boolean deleteDb = args.getBoolean(Options.DELETE_DB.key(), Boolean.FALSE, Boolean.TRUE); 279 | if (deleteDb) { 280 | FileUtils.deleteRecursively(databaseLayout.databaseDirectory()); 281 | FileUtils.deleteRecursively(databaseLayout.getTransactionLogsDirectory()); 282 | } 283 | Config config = Config.defaults(GraphDatabaseSettings.neo4j_home, Path.of(homeDir.getAbsolutePath())); 284 | Path logsDir = config.get(GraphDatabaseSettings.logs_directory); 285 | fs.mkdirs(logsDir.toFile()); 286 | 287 | skipBadEntriesLogging = args.getBoolean(Options.SKIP_BAD_ENTRIES_LOGGING.key(), (Boolean) Options.SKIP_BAD_ENTRIES_LOGGING.defaultValue(), false); 288 | if (!skipBadEntriesLogging) { 289 | badFile = new File(homeDir, BAD_FILE_NAME); 290 | badOutput = new BufferedOutputStream(fs.openAsOutputStream(badFile, false)); 291 | } 292 | OSMRange range = args.interpretOption(Options.RANGE.key(), Converters.optional(), Converters.toRange(), RANGE_IS_VALID); 293 | osmFiles = args.orphansAsArray(); 294 | if (osmFiles.length == 0) { 295 | throw new IllegalArgumentException("No OSM files specified"); 296 | } 297 | String maxMemoryString = args.get(Options.MAX_MEMORY.key(), null); 298 | maxMemory = parseMaxMemory(maxMemoryString); 299 | 300 | enableStacktrace = args.getBoolean(Options.STACKTRACE.key(), Boolean.FALSE, Boolean.TRUE); 301 | processors = args.getNumber(Options.PROCESSORS.key(), null); 302 | badTolerance = parseNumberOrUnlimited(args, Options.BAD_TOLERANCE); 303 | inputEncoding = Charset.forName(args.get(Options.INPUT_ENCODING.key(), defaultCharset().name())); 304 | 305 | skipBadRelationships = args.getBoolean(Options.SKIP_BAD_RELATIONSHIPS.key(), (Boolean) Options.SKIP_BAD_RELATIONSHIPS.defaultValue(), true); 306 | skipDuplicateNodes = args.getBoolean(Options.SKIP_DUPLICATE_NODES.key(), (Boolean) Options.SKIP_DUPLICATE_NODES.defaultValue(), true); 307 | defaultHighIO = args.getBoolean(Options.HIGH_IO.key(), (Boolean) Options.HIGH_IO.defaultValue(), true); 308 | 309 | Collector badCollector = getBadCollector(badTolerance, skipBadRelationships, skipDuplicateNodes, skipBadEntriesLogging, badOutput); 310 | 311 | dbConfig = loadDbConfig(args.interpretOption(Options.ADDITIONAL_CONFIG.key(), Converters.optional(), Converters.toFile(), f -> Validators.REGEX_FILE_EXISTS.validate(f.getAbsolutePath()))); 312 | boolean allowCacheOnHeap = args.getBoolean(Options.CACHE_ON_HEAP.key(), (Boolean) Options.CACHE_ON_HEAP.defaultValue()); 313 | configuration = importConfiguration(processors, defaultSettingsSuitableForTests, maxMemory, homeDir, allowCacheOnHeap, defaultHighIO); 314 | in = defaultSettingsSuitableForTests ? new ByteArrayInputStream(EMPTY_BYTE_ARRAY) : System.in; 315 | boolean detailedProgress = args.getBoolean(Options.DETAILED_PROGRESS.key(), (Boolean) Options.DETAILED_PROGRESS.defaultValue()); 316 | boolean tracePageCache = args.getBoolean(Options.TRACE_PAGE_CACHE.key(), (Boolean) Options.TRACE_PAGE_CACHE.defaultValue()); 317 | doImport(out, err, in, databaseLayout, logsDir.toFile(), badFile, fs, osmFiles, enableStacktrace, dbConfig, badOutput, badCollector, configuration, detailedProgress, tracePageCache, range); 318 | } 319 | } 320 | 321 | static class OSMRange implements OSMInput.RangeFilter { 322 | double[] range = new double[4]; 323 | ArrayList errors = new ArrayList<>(); 324 | 325 | OSMRange(String rangeSpec) { 326 | if (rangeSpec == null) { 327 | errors.add("Range was null"); 328 | } else { 329 | String[] fields = rangeSpec.split("\\s*[\\,\\;]\\s*"); 330 | if (fields.length == 4) { 331 | for (int i = 0; i < range.length; i++) { 332 | try { 333 | range[i] = Double.parseDouble(fields[i]); 334 | } catch (Exception e) { 335 | errors.add("Failed to parse double from field[" + i + "]: '" + fields[i] + "'"); 336 | } 337 | } 338 | } else { 339 | errors.add("Did not have exactly four fields in '" + rangeSpec + "'"); 340 | } 341 | } 342 | } 343 | 344 | public boolean withinRange(double[] coordinate) { 345 | return coordinate[0] >= range[0] && coordinate[1] >= range[1] && coordinate[0] <= range[2] && coordinate[1] <= range[3]; 346 | } 347 | 348 | boolean isValid() { 349 | return errors.size() == 0; 350 | } 351 | 352 | String error() { 353 | return Arrays.toString(errors.toArray()); 354 | } 355 | } 356 | 357 | static class Converters { 358 | public static Function mandatory() { 359 | return key -> 360 | { 361 | throw new IllegalArgumentException("Missing argument '" + key + "'"); 362 | }; 363 | } 364 | 365 | public static Function optional() { 366 | return from -> null; 367 | } 368 | 369 | public static Function toFile() { 370 | return File::new; 371 | } 372 | 373 | public static Function toRange() { 374 | return OSMRange::new; 375 | } 376 | } 377 | 378 | public static final Validator RANGE_IS_VALID = value -> { 379 | if (!value.isValid()) { 380 | throw new IllegalArgumentException("Range '" + value + "' is not valid: " + value.error()); 381 | } 382 | }; 383 | 384 | public static void doImport(PrintStream out, PrintStream err, InputStream in, DatabaseLayout databaseLayout, File logsDir, File badFile, 385 | FileSystemAbstraction fs, String[] osmFiles, 386 | boolean enableStacktrace, 387 | Config dbConfig, OutputStream badOutput, 388 | Collector badCollector, Configuration configuration, 389 | boolean detailedProgress, boolean tracePageCache, 390 | OSMRange range) throws IOException { 391 | boolean success; 392 | LifeSupport life = new LifeSupport(); 393 | 394 | Config config = Config.newBuilder().fromConfig(dbConfig).set(logs_directory, Path.of(logsDir.getCanonicalPath())).build(); 395 | Path internalLogFile = config.get(store_internal_log_path); 396 | LogService logService = life.add(StoreLogService.withInternalLog(internalLogFile.toFile()).build(fs)); 397 | final JobScheduler jobScheduler = life.add(createScheduler()); 398 | 399 | life.start(); 400 | ExecutionMonitor executionMonitor = detailedProgress 401 | ? new SpectrumExecutionMonitor(2, TimeUnit.SECONDS, out, SpectrumExecutionMonitor.DEFAULT_WIDTH) 402 | : ExecutionMonitors.defaultVisible(); 403 | ImportLogic.Monitor importMonitor = new PrintingImportLogicMonitor(out, err); 404 | var cacheTracer = tracePageCache ? new DefaultPageCacheTracer() : PageCacheTracer.NULL; 405 | BatchImporter importer = BatchImporterFactory.withHighestPriority().instantiate(databaseLayout, 406 | fs, 407 | null, // no external page cache 408 | cacheTracer, 409 | configuration, 410 | logService, 411 | executionMonitor, 412 | EMPTY, 413 | dbConfig, 414 | RecordFormatSelector.selectForConfig(dbConfig, logService.getInternalLogProvider()), 415 | importMonitor, 416 | jobScheduler, 417 | badCollector, 418 | TransactionLogInitializer.getLogFilesInitializer(), 419 | EmptyMemoryTracker.INSTANCE 420 | ); 421 | printOverview(databaseLayout.databaseDirectory(), osmFiles, configuration, out); 422 | success = false; 423 | try { 424 | importer.doImport(new OSMInput(fs, osmFiles, configuration, range)); 425 | success = true; 426 | } catch (Exception e) { 427 | throw andPrintError("Import error", e, enableStacktrace, err); 428 | } finally { 429 | long numberOfBadEntries = badCollector.badEntries(); 430 | badCollector.close(); 431 | IOUtils.closeAll(badOutput); 432 | 433 | if (badFile != null) { 434 | if (numberOfBadEntries > 0) { 435 | System.out.println("There were bad entries which were skipped and logged into " + 436 | badFile.getAbsolutePath()); 437 | } 438 | } 439 | 440 | life.shutdown(); 441 | 442 | if (tracePageCache) { 443 | System.out.println("Page cache counts:"); 444 | System.out.println(" faults: " + cacheTracer.faults()); 445 | System.out.println(" pins: " + cacheTracer.pins()); 446 | System.out.println(" unpins: " + cacheTracer.unpins()); 447 | System.out.println(" hits: " + cacheTracer.hits()); 448 | System.out.println(" flushes: " + cacheTracer.flushes()); 449 | } 450 | 451 | if (!success) { 452 | err.println("WARNING Import failed. The store files in " + databaseLayout.databaseDirectory().getAbsolutePath() + 453 | " are left as they are, although they are likely in an unusable state. " + 454 | "Starting a database on these store files will likely fail or observe inconsistent records so " + 455 | "start at your own risk or delete the store manually"); 456 | } 457 | } 458 | } 459 | 460 | public static org.neo4j.internal.batchimport.Configuration importConfiguration( 461 | Number processors, boolean defaultSettingsSuitableForTests, Long maxMemory, File homeDir, 462 | boolean allowCacheOnHeap, Boolean defaultHighIO) { 463 | return new org.neo4j.internal.batchimport.Configuration() { 464 | @Override 465 | public long pageCacheMemory() { 466 | return defaultSettingsSuitableForTests ? mebiBytes(8) : DEFAULT.pageCacheMemory(); 467 | } 468 | 469 | @Override 470 | public int maxNumberOfProcessors() { 471 | return processors != null ? processors.intValue() : DEFAULT.maxNumberOfProcessors(); 472 | } 473 | 474 | @Override 475 | public long maxMemoryUsage() { 476 | return maxMemory != null ? maxMemory : DEFAULT.maxMemoryUsage(); 477 | } 478 | 479 | @Override 480 | public boolean highIO() { 481 | return defaultHighIO != null ? defaultHighIO : FileUtils.highIODevice(homeDir.toPath()); 482 | } 483 | 484 | @Override 485 | public boolean allowCacheAllocationOnHeap() { 486 | return allowCacheOnHeap; 487 | } 488 | }; 489 | } 490 | 491 | private static String manualReference(ManualPage page, Anchor anchor) { 492 | // Docs are versioned major.minor-suffix, so drop the patch version. 493 | String[] versionParts = Version.getNeo4jVersion().split("-"); 494 | versionParts[0] = versionParts[0].substring(0, 3); 495 | String docsVersion = String.join("-", versionParts); 496 | 497 | return " https://neo4j.com/docs/operations-manual/" + docsVersion + "/" + page.getReference(anchor); 498 | } 499 | 500 | /** 501 | * Method name looks strange, but look at how it's used and you'll see why it's named like that. 502 | * 503 | * @param stackTrace whether or not to also print the stack trace of the error. 504 | * @param err 505 | */ 506 | private static RuntimeException andPrintError(String typeOfError, Exception e, boolean stackTrace, PrintStream err) { 507 | // List of common errors that can be explained to the user 508 | if (DuplicateInputIdException.class.equals(e.getClass())) { 509 | printErrorMessage("Duplicate input ids that would otherwise clash can be put into separate id space, " + 510 | "read more about how to use id spaces in the manual:" + 511 | manualReference(ManualPage.IMPORT_TOOL_FORMAT, Anchor.ID_SPACES), e, stackTrace, 512 | err); 513 | } else if (Exceptions.contains(e, Predicates.instanceOfAny(InputException.class))) { 514 | printErrorMessage("Error in input data", e, stackTrace, err); 515 | } 516 | // Fallback to printing generic error and stack trace 517 | else { 518 | printErrorMessage(typeOfError + ": " + e.getMessage(), e, true, err); 519 | } 520 | err.println(); 521 | 522 | // Mute the stack trace that the default exception handler would have liked to print. 523 | // Calling System.exit( 1 ) or similar would be convenient on one hand since we can set 524 | // a specific exit code. On the other hand It's very inconvenient to have any System.exit 525 | // call in code that is tested. 526 | Thread.currentThread().setUncaughtExceptionHandler((t, e1) -> 527 | { 528 | /* Shhhh */ 529 | }); 530 | throwIfUnchecked(e); 531 | return new RuntimeException(e); // throw in order to have process exit with !0 532 | } 533 | 534 | private static void printErrorMessage(String string, Exception e, boolean stackTrace, PrintStream err) { 535 | err.println(string); 536 | err.println("Caused by:" + e.getMessage()); 537 | if (stackTrace) { 538 | e.printStackTrace(err); 539 | } 540 | } 541 | 542 | private static Long parseMaxMemory(String maxMemoryString) { 543 | if (maxMemoryString != null) { 544 | maxMemoryString = maxMemoryString.trim(); 545 | if (maxMemoryString.endsWith("%")) { 546 | int percent = Integer.parseInt(maxMemoryString.substring(0, maxMemoryString.length() - 1)); 547 | long result = calculateMaxMemoryFromPercent(percent); 548 | if (!canDetectFreeMemory()) { 549 | System.err.println("WARNING: amount of free memory couldn't be detected so defaults to " + 550 | bytes(result) + ". For optimal performance instead explicitly specify amount of " + 551 | "memory that importer is allowed to use using " + Options.MAX_MEMORY.argument()); 552 | } 553 | return result; 554 | } 555 | return SettingValueParsers.parseLongWithUnit(maxMemoryString); 556 | } 557 | return null; 558 | } 559 | 560 | private static Collector getBadCollector(long badTolerance, boolean skipBadRelationships, boolean skipDuplicateNodes, boolean skipBadEntriesLogging, OutputStream badOutput) { 561 | int collect = collect(skipBadRelationships, skipDuplicateNodes, false); 562 | return skipBadEntriesLogging ? silentBadCollector(badTolerance, collect) : badCollector(badOutput, badTolerance, collect); 563 | } 564 | 565 | private static long parseNumberOrUnlimited(Args args, Options option) { 566 | String value = args.get(option.key(), option.defaultValue().toString()); 567 | return UNLIMITED.equals(value) ? BadCollector.UNLIMITED_TOLERANCE : Long.parseLong(value); 568 | } 569 | 570 | private static Config loadDbConfig(File file) { 571 | return file != null && file.exists() ? Config.newBuilder().fromFile(file).build() : Config.defaults(); 572 | } 573 | 574 | private static void printOverview(File storeDir, String[] osmFiles, org.neo4j.internal.batchimport.Configuration configuration, PrintStream out) { 575 | out.println("Neo4j version: " + Version.getNeo4jVersion()); 576 | out.println("Importing the contents of these OSM files into " + storeDir + ":"); 577 | for (String file : osmFiles) { 578 | printIndented(file, out); 579 | } 580 | out.println(); 581 | out.println("Available resources:"); 582 | printIndented("Total machine memory: " + bytes(OsBeanUtil.getTotalPhysicalMemory()), out); 583 | printIndented("Free machine memory: " + bytes(OsBeanUtil.getFreePhysicalMemory()), out); 584 | printIndented("Max heap memory : " + bytes(Runtime.getRuntime().maxMemory()), out); 585 | printIndented("Processors: " + configuration.maxNumberOfProcessors(), out); 586 | printIndented("Configured max memory: " + bytes(configuration.maxMemoryUsage()), out); 587 | printIndented("High-IO: " + configuration.highIO(), out); 588 | out.println(); 589 | } 590 | 591 | private static void printIndented(Object value, PrintStream out) { 592 | out.println(" " + value); 593 | } 594 | 595 | private static void printUsage(PrintStream out) { 596 | out.println("Neo4j OpenStreetMap Import Tool"); 597 | for (String line : splitLongLine("osm-import is used to create a new Neo4j database from data in OSM XML files.", 80)) { 598 | out.println("\t" + line); 599 | } 600 | out.println("Usage:"); 601 | for (Options option : Options.values()) { 602 | option.printUsage(out); 603 | } 604 | 605 | out.println("Example:"); 606 | out.println(TAB + "bin/osm-import --into osm.db sweden-latest.osm.bz2 denmark-latest.osm norway-latest.osm "); 607 | } 608 | 609 | private static boolean asksForUsage(Args args) { 610 | for (String orphan : args.orphans()) { 611 | if (isHelpKey(orphan)) { 612 | return true; 613 | } 614 | } 615 | 616 | for (Map.Entry option : args.asMap().entrySet()) { 617 | if (isHelpKey(option.getKey())) { 618 | return true; 619 | } 620 | } 621 | return args.orphans().size() == 0; 622 | } 623 | 624 | private static boolean isHelpKey(String key) { 625 | return key.equals("?") || key.equals("help"); 626 | } 627 | 628 | private enum ManualPage { 629 | IMPORT_TOOL_FORMAT("tools/import/file-header-format/"); 630 | 631 | private final String page; 632 | 633 | ManualPage(String page) { 634 | this.page = page; 635 | } 636 | 637 | public String getReference(Anchor anchor) { 638 | // As long as the the operations manual is single-page we only use the anchor. 639 | return page + "#" + anchor.anchor; 640 | } 641 | } 642 | 643 | /* 644 | * Copied from org.neo4j.internal.helpers.Args, which had this method up to Neo4j 4.1, but removed in 4.2. 645 | */ 646 | public static String[] splitLongLine( String description, int maxLength ) 647 | { 648 | List lines = new ArrayList<>(); 649 | while ( !description.isEmpty() ) 650 | { 651 | String line = description.substring( 0, Math.min( maxLength, description.length() ) ); 652 | int position = line.indexOf( '\n' ); 653 | if ( position > -1 ) 654 | { 655 | line = description.substring( 0, position ); 656 | lines.add( line ); 657 | description = description.substring( position ); 658 | if ( !description.isEmpty() ) 659 | { 660 | description = description.substring( 1 ); 661 | } 662 | } 663 | else 664 | { 665 | position = description.length() > maxLength ? 666 | findSpaceBefore( description, maxLength ) : description.length(); 667 | line = description.substring( 0, position ); 668 | lines.add( line ); 669 | description = description.substring( position ); 670 | } 671 | } 672 | return lines.toArray( new String[0] ); 673 | } 674 | 675 | private static int findSpaceBefore( String description, int position ) 676 | { 677 | while ( !Character.isWhitespace( description.charAt( position ) ) ) 678 | { 679 | position--; 680 | } 681 | return position + 1; 682 | } 683 | 684 | private enum Anchor { 685 | ID_SPACES("import-tool-id-spaces"), 686 | RELATIONSHIP("import-tool-header-format-rels"); 687 | 688 | private final String anchor; 689 | 690 | Anchor(String anchor) { 691 | this.anchor = anchor; 692 | } 693 | } 694 | } 695 | -------------------------------------------------------------------------------- /src/main/java/org/neo4j/gis/osm/importer/PrintingImportLogicMonitor.java: -------------------------------------------------------------------------------- 1 | package org.neo4j.gis.osm.importer; 2 | 3 | import org.neo4j.internal.batchimport.ImportLogic; 4 | 5 | import java.io.PrintStream; 6 | 7 | import static org.neo4j.io.ByteUnit.bytes; 8 | 9 | public class PrintingImportLogicMonitor implements ImportLogic.Monitor { 10 | private final PrintStream out; 11 | private final PrintStream err; 12 | 13 | public PrintingImportLogicMonitor(PrintStream out, PrintStream err) { 14 | this.out = out; 15 | this.err = err; 16 | } 17 | 18 | @Override 19 | public void doubleRelationshipRecordUnitsEnabled() { 20 | out.println("Will use double record units for all relationships"); 21 | } 22 | 23 | @Override 24 | public void mayExceedNodeIdCapacity(long capacity, long estimatedCount) { 25 | err.printf("WARNING: estimated number of relationships %d may exceed capacity %d of selected record format%n", 26 | estimatedCount, capacity); 27 | } 28 | 29 | @Override 30 | public void mayExceedRelationshipIdCapacity(long capacity, long estimatedCount) { 31 | err.printf("WARNING: estimated number of nodes %d may exceed capacity %d of selected record format%n", 32 | estimatedCount, capacity); 33 | } 34 | 35 | @Override 36 | public void insufficientHeapSize(long optimalMinimalHeapSize, long heapSize) { 37 | err.printf("WARNING: heap size %s may be too small to complete this import. Suggested heap size is %s", 38 | bytes(heapSize), bytes(optimalMinimalHeapSize)); 39 | } 40 | 41 | @Override 42 | public void abundantHeapSize(long optimalMinimalHeapSize, long heapSize) { 43 | err.printf("WARNING: heap size %s is unnecessarily large for completing this import.%n" + 44 | "The abundant heap memory will leave less memory for off-heap importer caches. Suggested heap size is %s", 45 | bytes(heapSize), bytes(optimalMinimalHeapSize)); 46 | } 47 | 48 | @Override 49 | public void insufficientAvailableMemory(long estimatedCacheSize, long optimalMinimalHeapSize, long availableMemory) { 50 | err.printf("WARNING: %s memory may not be sufficient to complete this import. Suggested memory distribution is:%n" + 51 | "heap size: %s%n" + 52 | "minimum free and available memory excluding heap size: %s", 53 | bytes(availableMemory), bytes(optimalMinimalHeapSize), bytes(estimatedCacheSize)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/org/neo4j/gis/osm/model/OSMModel.java: -------------------------------------------------------------------------------- 1 | package org.neo4j.gis.osm.model; 2 | 3 | import org.neo4j.graphdb.*; 4 | import org.neo4j.graphdb.spatial.Point; 5 | import org.neo4j.graphdb.traversal.Evaluators; 6 | import org.neo4j.graphdb.traversal.TraversalDescription; 7 | import org.neo4j.kernel.impl.traversal.MonoDirectionalTraversalDescription; 8 | import org.neo4j.values.storable.CRSCalculator; 9 | import org.neo4j.values.storable.CoordinateReferenceSystem; 10 | import org.neo4j.values.storable.PointValue; 11 | import org.neo4j.values.storable.Values; 12 | 13 | import java.util.*; 14 | 15 | /** 16 | * This class provides an API onto the OpenStreetMap model stored in a Neo4j database. 17 | */ 18 | public class OSMModel { 19 | public static final RelationshipType TAGS = RelationshipType.withName("TAGS"); 20 | public static final RelationshipType FIRST_NODE = RelationshipType.withName("FIRST_NODE"); 21 | public static final RelationshipType NEXT = RelationshipType.withName("NEXT"); 22 | public static final RelationshipType NODE = RelationshipType.withName("NODE"); 23 | public static final RelationshipType ROUTE = RelationshipType.withName("ROUTE"); 24 | public static final Label Routable = Label.label("Routable"); 25 | public static final Label Intersection = Label.label("Intersection"); 26 | public static final Label OSMWay = Label.label("OSMWay"); 27 | public static final Label OSMWayNode = Label.label("OSMWayNode"); 28 | public static final Label OSMNode = Label.label("OSMNode"); 29 | public static final Label OSMTags = Label.label("OSMTags"); 30 | 31 | /** 32 | * Create a wrapper object exposing internal location specific features of a node. 33 | */ 34 | public LocatedNode located(Node node) { 35 | return new LocatedNode(node); 36 | } 37 | 38 | /** 39 | * Load all nodes comprising this way from the database into an in-memory structure. 40 | * 41 | * Internally this will access the database using the Node API, which assumes the node object was created 42 | * with the same transaction that is currently live. If this is not the case, exceptions will be thrown. 43 | * So be sure to correctly use only Nodes found in the current transaction. This is generally true if this 44 | * code is used from procedures, but easy to get wrong in embedded code, or tests. 45 | * @param node representing the OSM way 46 | * @return an OSMWay object encapsulating all way information, including nodes comprising the way 47 | */ 48 | public OSMWay way(Node node) { 49 | return new OSMWay(node); 50 | } 51 | 52 | public IntersectionRoutes intersectionRoutes(Node osmNode, Relationship relToStartWayNode, Node startOsmWayNode, boolean addLabels) { 53 | return new IntersectionRoutes(osmNode, relToStartWayNode, startOsmWayNode, addLabels); 54 | } 55 | 56 | /** 57 | * A wrapped class to facilitate exposing location specific attributes of OSM nodes. 58 | * Internally it will use the API provided by the Node interface to read the location 59 | * property, and this assumes the Node object was created (found) with the same transaction currently active. 60 | */ 61 | public static class LocatedNode { 62 | private final Node node; 63 | private final PointValue point; 64 | 65 | LocatedNode(Node node) { 66 | this.node = node; 67 | this.point = readPoint(node); 68 | } 69 | 70 | @Override 71 | public String toString() { 72 | return "Node[" + node.getId() + "]:" + point; 73 | } 74 | 75 | public Node node() { 76 | return node; 77 | } 78 | 79 | public PointValue point() { 80 | return point; 81 | } 82 | 83 | private PointValue readPoint(Node node) { 84 | if (node.hasProperty("location")) { 85 | Object location = node.getProperty("location"); 86 | if (location instanceof PointValue) { 87 | return (PointValue) location; 88 | } else if (location instanceof Point) { 89 | return Values.point((Point) location); 90 | } else { 91 | throw new IllegalArgumentException("Node does not contain a 'location' property of the right type: " + node); 92 | } 93 | } else { 94 | throw new IllegalArgumentException("Node does not contain a 'location' property"); 95 | } 96 | } 97 | } 98 | 99 | public class OSMWay { 100 | String name; 101 | public Node wayNode; 102 | public ArrayList wayNodes; 103 | public ArrayList nodes; 104 | HashMap seenNodes; 105 | OSMWay(Node wayNode) { 106 | if (!wayNode.hasLabel(OSMWay)) 107 | throw new IllegalArgumentException("Way node does not have :OSMWay label: " + wayNode); 108 | Relationship tagsRel = wayNode.getSingleRelationship(TAGS, Direction.OUTGOING); 109 | if (tagsRel == null) 110 | throw new IllegalArgumentException("Way node does not have outgoing :TAGS relationship: " + wayNode); 111 | this.wayNode = wayNode; 112 | Node tagsNode = tagsRel.getEndNode(); 113 | if (wayNode.hasProperty("name")) name = wayNode.getProperty("name").toString(); 114 | else if (tagsNode.hasProperty("name")) name = tagsNode.getProperty("name").toString(); 115 | this.wayNodes = new ArrayList<>(); 116 | this.nodes = new ArrayList<>(); 117 | this.seenNodes = new HashMap<>(); 118 | Node firstNode = wayNode.getSingleRelationship(FIRST_NODE, Direction.OUTGOING).getEndNode(); 119 | TraversalDescription wayNodes = (new MonoDirectionalTraversalDescription()).breadthFirst().relationships(NEXT, Direction.OUTGOING).evaluator(Evaluators.toDepth(20)); 120 | for (Path path : wayNodes.traverse(firstNode)) { 121 | Node endNode = path.endNode(); 122 | Node osmNode = endNode.getSingleRelationship(NODE, Direction.OUTGOING).getEndNode(); 123 | LocatedNode node; 124 | if (seenNodes.containsKey(osmNode.getId())) { 125 | node = seenNodes.get(osmNode.getId()); 126 | } else { 127 | node = new LocatedNode(osmNode); 128 | this.seenNodes.put(osmNode.getId(), node); 129 | } 130 | this.wayNodes.add(endNode); 131 | this.nodes.add(node); 132 | } 133 | } 134 | 135 | public OSMWayDistance closeTo(LocatedNode poi){ 136 | return new OSMWayDistance(this, poi); 137 | } 138 | 139 | public String getName() { 140 | return (name == null) ? "" : name; 141 | } 142 | 143 | @Override 144 | public String toString() { 145 | return "OSMWay[" + wayNode.getId() + "]: name:" + name + ", length:" + nodes.size(); 146 | } 147 | } 148 | 149 | public class OSMWayDistance { 150 | OSMWay way; 151 | LocatedNode node; 152 | DistanceResult closest; 153 | 154 | private OSMWayDistance(OSMWay way, LocatedNode node) { 155 | this.way = way; 156 | this.node = node; 157 | calculateClosestDistance(); 158 | } 159 | 160 | private void calculateClosestDistance() { 161 | closest = new DistanceResult(node); 162 | for (int i = 0; i < way.nodes.size(); i++) { 163 | LocatedNode n = way.nodes.get(i); 164 | if (!n.point.getCoordinateReferenceSystem().equals(closest.crs)) { 165 | throw new IllegalArgumentException("Cannot compare points of different crs: " + n.point.getCoordinateReferenceSystem() + " != " + closest.crs); 166 | } 167 | double distance = closest.calculator.distance(node.point, n.point); 168 | if (distance < closest.nodeDistance) { 169 | closest.nodeDistance = distance; 170 | closest.closestNodeIndex = i; 171 | } 172 | } 173 | closest.locationMaker = closest.makeLocationMaker(); 174 | } 175 | 176 | public LocationMaker getLocationMaker() { 177 | return closest.getLocationMaker(); 178 | } 179 | 180 | public class DistanceResult { 181 | LocatedNode node; 182 | double nodeDistance; 183 | int closestNodeIndex; 184 | CoordinateReferenceSystem crs; 185 | public CRSCalculator calculator; 186 | private LocationMaker locationMaker; 187 | 188 | DistanceResult(LocatedNode node) { 189 | this.node = node; 190 | crs = node.point.getCoordinateReferenceSystem(); 191 | calculator = crs.getCalculator(); 192 | nodeDistance = Double.MAX_VALUE; 193 | closestNodeIndex = -1; 194 | } 195 | 196 | @Override 197 | public String toString() { 198 | Object closest = (closestNodeIndex < 0 || closestNodeIndex >= way.nodes.size()) ? "null" : way.nodes.get(closestNodeIndex); 199 | return "DistanceResult: distance:" + nodeDistance + " from " + node + " to " + closest; 200 | } 201 | 202 | LocationMaker getLocationMaker() { 203 | return this.locationMaker; 204 | } 205 | 206 | private LocationMaker makeLocationMaker() { 207 | if (closestNodeIndex < 0) { 208 | throw new IllegalStateException("No closest node known - has closestDistanceTo(node) not been called?"); 209 | } 210 | if (closestNodeIndex == 0) { 211 | if (way.nodes.size() > 1) { 212 | return makeLocationMaker(0, 1); 213 | } else { 214 | return new LocationExists(node.node, way.nodes.get(0).node, nodeDistance); 215 | } 216 | } 217 | if (closestNodeIndex == way.nodes.size() - 1) { 218 | return makeLocationMaker(closestNodeIndex - 1, closestNodeIndex); 219 | } else { 220 | double left = calculator.distance(way.nodes.get(closestNodeIndex - 1).point, node.point); 221 | double right = calculator.distance(way.nodes.get(closestNodeIndex + 1).point, node.point); 222 | if (left < right) { 223 | return makeLocationMaker(closestNodeIndex - 1, closestNodeIndex); 224 | } else { 225 | return makeLocationMaker(closestNodeIndex, closestNodeIndex + 1); 226 | } 227 | } 228 | } 229 | 230 | private LocationMaker makeLocationMaker(int leftIndex, int rightIndex) { 231 | LocatedNode left = way.nodes.get(leftIndex); 232 | LocatedNode right = way.nodes.get(rightIndex); 233 | Triangle triangle = new Triangle(node.point, left.point, right.point); 234 | if (triangle.leftAngle() > 85.0) { 235 | return new LocationExists(node.node, left.node, calculator.distance(node.point, left.point)); 236 | } else if (triangle.rightAngle() > 85.0) { 237 | return new LocationExists(node.node, right.node, calculator.distance(node.point, right.point)); 238 | } else if (triangle.apexAngle() > 175) { 239 | return new LocationIsPoint(node.node, 240 | left.node, calculator.distance(left.point, node.point), 241 | right.node, calculator.distance(left.point, node.point)); 242 | } else { 243 | PointValue projected = triangle.project(); 244 | return new LocationInterpolated(calculator, node, projected, left, right); 245 | } 246 | } 247 | } 248 | } 249 | 250 | public interface LocationMaker { 251 | double getDistance(); 252 | Node process(Transaction tx); 253 | } 254 | 255 | public static class LocationExists implements LocationMaker { 256 | Node node; 257 | Node poi; 258 | double distance; 259 | 260 | private LocationExists(Node poi, Node node, double distance) { 261 | this.node = node; 262 | this.poi = poi; 263 | this.distance = distance; 264 | } 265 | 266 | @Override 267 | public double getDistance() { 268 | return distance; 269 | } 270 | 271 | @Override 272 | public Node process(Transaction ignore) { 273 | System.out.println("\t\tConnecting existing node: " + node); 274 | node.addLabel(Routable); 275 | Relationship rel = poi.createRelationshipTo(node, ROUTE); 276 | rel.setProperty("distance", this.distance); 277 | return node; 278 | } 279 | } 280 | 281 | /** 282 | * This class represents the case where the point of interest lies between two nodes, but not on the line 283 | * between them so we create a new node on the street (between the two nodes) and link the original node 284 | * to that, and that to the street nodes in the routable graph. 285 | */ 286 | public static class LocationInterpolated implements LocationMaker { 287 | public LocatedNode left; 288 | public LocatedNode right; 289 | public PointValue point; 290 | public LocatedNode poi; 291 | private CRSCalculator calculator; 292 | private double distance; 293 | 294 | private LocationInterpolated(CRSCalculator calculator, LocatedNode poi, PointValue point, LocatedNode left, LocatedNode right) { 295 | this.left = left; 296 | this.right = right; 297 | this.point = point; 298 | this.poi = poi; 299 | this.calculator = calculator; 300 | this.distance = calculator.distance(point, poi.point); 301 | } 302 | 303 | @Override 304 | public double getDistance() { 305 | return distance; 306 | } 307 | 308 | @Override 309 | public Node process(Transaction tx) { 310 | Node node; 311 | left.node.addLabel(Routable); 312 | right.node.addLabel(Routable); 313 | node = tx.createNode(Routable); 314 | node.setProperty("location", point); 315 | createConnection(node, left.node, calculator.distance(point, left.point)); 316 | createConnection(node, right.node, calculator.distance(point, right.point)); 317 | createConnection(poi.node, node, this.distance); 318 | System.out.println("\t\tCreating interpolated node: " + node); 319 | return node; 320 | } 321 | 322 | private void createConnection(Node a, Node b, double distance) { 323 | Relationship rel = a.createRelationshipTo(b, ROUTE); 324 | rel.setProperty("distance", distance); 325 | } 326 | } 327 | 328 | /** 329 | * This class represents the case where the point of interest lies on the line between two street nodes 330 | * and so we can simply link it directly to those nodes as part of the routable graph. 331 | */ 332 | public static class LocationIsPoint implements LocationMaker { 333 | public Node left; 334 | public Node right; 335 | public Node node; 336 | public double leftDist; 337 | public double rightDist; 338 | 339 | private LocationIsPoint(Node node, Node left, double leftDist, Node right, double rightDist) { 340 | this.left = left; 341 | this.right = right; 342 | this.node = node; 343 | this.leftDist = leftDist; 344 | this.rightDist = rightDist; 345 | } 346 | 347 | @Override 348 | public double getDistance() { 349 | return 0.0; 350 | } 351 | 352 | @Override 353 | public Node process(Transaction tx) { 354 | System.out.println("\t\tLinking point of interest node: " + node); 355 | left.addLabel(Routable); 356 | right.addLabel(Routable); 357 | node.addLabel(Routable); 358 | Relationship leftRel = node.createRelationshipTo(left, ROUTE); 359 | leftRel.setProperty("distance", leftDist); 360 | Relationship rightRel = node.createRelationshipTo(right, ROUTE); 361 | rightRel.setProperty("distance", rightDist); 362 | return node; 363 | } 364 | } 365 | 366 | public static class ClosestWay implements Comparator { 367 | public int compare(OSMWayDistance o1, OSMWayDistance o2) { 368 | return Double.compare(o1.closest.locationMaker.getDistance(), o2.closest.locationMaker.getDistance()); 369 | } 370 | } 371 | 372 | public static class Triangle { 373 | public PointValue apex; 374 | public PointValue left; 375 | public PointValue right; 376 | 377 | public Triangle(PointValue apex, PointValue left, PointValue right) { 378 | this.apex = apex; 379 | this.left = left; 380 | this.right = right; 381 | } 382 | 383 | static double angleTo(PointValue origin, PointValue point) { 384 | double dx = point.coordinate()[0] - origin.coordinate()[0]; 385 | double dy = point.coordinate()[1] - origin.coordinate()[1]; 386 | double theta = 180.0 / Math.PI * Math.atan2(dy, dx); 387 | return theta; 388 | } 389 | 390 | static double angle(PointValue origin, PointValue a, PointValue b) { 391 | double thetaA = angleTo(origin, a); 392 | double thetaB = angleTo(origin, b); 393 | double angle = Math.abs(thetaA - thetaB); 394 | if (angle > 180.0) angle = 360.0 - angle; 395 | if (angle < -180.0) angle = 360.0 + angle; 396 | return angle; 397 | } 398 | 399 | double leftAngle() { 400 | return Math.abs(angle(left, apex, right)); 401 | } 402 | 403 | double rightAngle() { 404 | return Math.abs(angle(right, apex, left)); 405 | } 406 | 407 | double apexAngle() { 408 | return Math.abs(angle(apex, left, right)); 409 | } 410 | 411 | static double dist(PointValue origin, PointValue point) { 412 | double dx = point.coordinate()[0] - origin.coordinate()[0]; 413 | double dy = point.coordinate()[1] - origin.coordinate()[1]; 414 | return Math.sqrt(dx * dx + dy * dy); 415 | } 416 | 417 | PointValue project() { 418 | double gap = dist(left, right); 419 | double d = dist(left, apex); 420 | double theta = Math.PI * leftAngle() / 180.0; 421 | double f = d * Math.cos(theta); 422 | double rightFactor = Math.round(1000.0 * f / gap) / 1000.0; 423 | double leftFactor = 1.0 - rightFactor; 424 | double[] coordinates = new double[apex.coordinate().length]; 425 | for (int i = 0; i < apex.coordinate().length; i++) { 426 | coordinates[i] += leftFactor * left.coordinate()[i]; 427 | coordinates[i] += rightFactor * right.coordinate()[i]; 428 | } 429 | return Values.pointValue(apex.getCoordinateReferenceSystem(), coordinates); 430 | } 431 | } 432 | 433 | /** 434 | * A complete route from a starting node to an ending intersection node. Is created based from a chain of 435 | * PathSegments that are used to build up the route. 436 | */ 437 | public static class IntersectionRoute { 438 | public Node fromNode; 439 | public Node wayNode; 440 | public Node toNode; 441 | public double distance; 442 | public long length; 443 | public long count; 444 | public Relationship fromRel; 445 | public Relationship toRel; 446 | 447 | public IntersectionRoute(Node node, Relationship wayNodeRel, Node wayNode, IntersectionRoutes.PathSegment pathSegment) { 448 | this.fromNode = node; 449 | this.fromRel = wayNodeRel; 450 | this.wayNode = wayNode; 451 | this.toNode = pathSegment.lastSegment().osmNode; 452 | this.toRel = pathSegment.lastRel(); 453 | this.distance = pathSegment.totalDistance(); 454 | this.length = pathSegment.totalLength(); 455 | this.count = pathSegment.countSegments(); 456 | } 457 | 458 | public ArrayList getExistingRoutes() { 459 | ArrayList existingRoutes = new ArrayList<>(); 460 | for (Relationship rel : this.fromNode.getRelationships(Direction.BOTH, OSMModel.ROUTE)) { 461 | if (rel.getOtherNode(fromNode).equals(this.toNode)) { 462 | existingRoutes.add(rel); 463 | } 464 | } 465 | return existingRoutes; 466 | } 467 | 468 | public Relationship mergeRouteRelationship() { 469 | ArrayList toDelete = new ArrayList<>(); 470 | for (Relationship rel : this.fromNode.getRelationships(Direction.BOTH, OSMModel.ROUTE)) { 471 | if (rel.getOtherNode(fromNode).equals(this.toNode)) { 472 | if (getRelIdFromProperty(rel, "fromRel") == fromRel.getId() && getRelIdFromProperty(rel, "toRel") == toRel.getId()) { 473 | toDelete.add(rel); 474 | } 475 | } 476 | } 477 | toDelete.forEach(Relationship::delete); 478 | Relationship rel = fromNode.createRelationshipTo(toNode, OSMModel.ROUTE); 479 | rel.setProperty("fromRel", fromRel.getId()); 480 | rel.setProperty("toRel", toRel.getId()); 481 | return rel; 482 | } 483 | 484 | private long getRelIdFromProperty(Relationship rel, String property) { 485 | if (rel.hasProperty(property)) { 486 | return (Long) rel.getProperty(property); 487 | } else { 488 | return -1; 489 | } 490 | } 491 | 492 | @Override 493 | public String toString() { 494 | return "IntersectionRoute:from(" + fromNode + ")via(" + wayNode + ")to(" + toNode + ")distance(" + distance + ")"; 495 | } 496 | 497 | } 498 | 499 | public static class IntersectionRoutes { 500 | private Node fromNode; 501 | private Node wayNode; 502 | private Relationship fromRel; 503 | private boolean addLabels; 504 | private int maxDepth; 505 | private HashSet previouslySeen; 506 | public List routes; 507 | 508 | public IntersectionRoutes(Node node, Relationship wayNodeRel, Node wayNode, boolean addLabels) { 509 | this.fromNode = node; 510 | this.fromRel = wayNodeRel; 511 | this.wayNode = wayNode; 512 | this.addLabels = addLabels; 513 | this.maxDepth = 20; 514 | this.previouslySeen = new HashSet<>(); 515 | this.routes = new ArrayList<>(); 516 | } 517 | 518 | @Override 519 | public String toString() { 520 | return "IntersectionRoutes:from(" + fromNode + ")via(" + wayNode + ")"; 521 | } 522 | 523 | public boolean process(Transaction tx) { 524 | System.out.println("Searching for route from OSMNode:" + fromNode + " via OSMWayNode:" + wayNode); 525 | for (PathSegmentTree tree : findIntersections(tx, wayNode, 0)) { 526 | for (PathSegment path : tree.asPathSegments()) { 527 | routes.add(new IntersectionRoute(fromNode, fromRel, wayNode, path)); 528 | } 529 | } 530 | return routes.size() > 0; 531 | } 532 | 533 | 534 | static class PathSegment { 535 | Node fromWayNode; 536 | Node toWayNode; 537 | Relationship lastRel; 538 | Node osmNode; 539 | double distance; 540 | int length; 541 | PathSegment nextSegment; 542 | 543 | PathSegment(PathSegmentTree parent, PathSegment child) { 544 | this.fromWayNode = parent.fromWayNode; 545 | this.toWayNode = parent.toWayNode; 546 | this.lastRel = parent.lastRel; 547 | this.osmNode = parent.osmNode; 548 | this.distance = parent.distance; 549 | this.length = parent.length; 550 | this.nextSegment = child; 551 | } 552 | 553 | double totalDistance() { 554 | return distance + ((nextSegment != null) ? nextSegment.totalDistance() : 0); 555 | } 556 | 557 | int totalLength() { 558 | return length + ((nextSegment != null) ? nextSegment.totalLength() : 0); 559 | } 560 | 561 | Relationship lastRel() { 562 | return lastSegment().lastRel; 563 | } 564 | 565 | PathSegment lastSegment() { 566 | return (nextSegment == null) ? this : nextSegment.lastSegment(); 567 | } 568 | 569 | int countSegments() { 570 | return 1 + ((nextSegment == null) ? 0 : nextSegment.countSegments()); 571 | } 572 | 573 | public String toString() { 574 | return "PathSegment[from:" + fromWayNode + ", to:" + toWayNode + ", length:" + length + ", distance:" + distance + "]" + (nextSegment == null ? "" : ".." + nextSegment); 575 | } 576 | } 577 | 578 | static class PathSegmentTree { 579 | Node fromWayNode; 580 | Direction direction; 581 | Node toWayNode; 582 | Relationship lastRel; 583 | Node osmNode; 584 | double distance; 585 | int length; 586 | List childSegments; 587 | 588 | PathSegmentTree(Node fromWayNode, Direction direction) { 589 | this.fromWayNode = fromWayNode; 590 | this.direction = direction; 591 | this.toWayNode = null; 592 | this.osmNode = null; 593 | this.distance = 0; 594 | this.length = 0; 595 | this.childSegments = null; 596 | } 597 | 598 | List asPathSegments() { 599 | ArrayList pathSegments = new ArrayList<>(); 600 | if (childSegments != null && childSegments.size() > 0) { 601 | for (PathSegmentTree child : childSegments) { 602 | for (PathSegment childSegment : child.asPathSegments()) { 603 | pathSegments.add(new PathSegment(this, childSegment)); 604 | } 605 | } 606 | } else { 607 | pathSegments.add(new PathSegment(this, null)); 608 | } 609 | return pathSegments; 610 | } 611 | 612 | boolean process(Transaction tx) { 613 | traverseToFirstIntersection(tx); 614 | return osmNode != null; 615 | } 616 | 617 | private void traverseToFirstIntersection(Transaction tx) { 618 | this.distance = 0; 619 | this.length = 0; 620 | TraversalDescription traversalDescription = new MonoDirectionalTraversalDescription().depthFirst().relationships(OSMModel.NEXT, direction); 621 | for (Path path : traversalDescription.traverse(fromWayNode)) { 622 | if (path.length() > 0) { 623 | toWayNode = path.endNode(); 624 | Relationship rel = path.lastRelationship(); 625 | if (rel.hasProperty("distance")) { 626 | distance += (double) rel.getProperty("distance"); 627 | length++; 628 | lastRel = toWayNode.getSingleRelationship(OSMModel.NODE, Direction.OUTGOING); 629 | osmNode = lastRel.getEndNode(); 630 | if (osmNode.hasLabel(OSMModel.Intersection)) { 631 | // stop searching 632 | break; 633 | } 634 | } else { 635 | System.out.println("spatial.osm.routeIntersection(): Missing 'distance' on " + rel); 636 | osmNode = null; 637 | break; 638 | } 639 | } 640 | } 641 | if (toWayNode == null) { 642 | // TODO: Probably we started at the last node for this direction, so need not print anything here 643 | System.out.println("spatial.osm.routeIntersection(): No " + direction + " path found from OSMWayNode(" + fromWayNode + ")"); 644 | } else if (osmNode == null) { 645 | System.out.println("spatial.osm.routeIntersection(): No intersection node found in " + direction + " path found from OSMWayNode(" + fromWayNode + ")"); 646 | } 647 | } 648 | 649 | ArrayList nextWayRels() { 650 | ArrayList relationships = new ArrayList<>(); 651 | if (osmNode != null) { 652 | for (Relationship rel : osmNode.getRelationships(Direction.INCOMING, OSMModel.NODE)) { 653 | if (!rel.equals(lastRel)) { 654 | relationships.add(rel); 655 | } 656 | } 657 | } 658 | return relationships; 659 | } 660 | 661 | public String toString() { 662 | return "PathSegmentTree[from:" + fromWayNode + ", to:" + toWayNode + ", length:" + length + ", distance:" + distance + "]" + (childSegments == null ? "" : ".. and " + childSegments.size() + " child branches"); 663 | } 664 | } 665 | 666 | private List findIntersections(Transaction tx, Node startNode, int depth) { 667 | List pathSegmentTrees = new ArrayList<>(); 668 | for (Direction direction : new Direction[]{Direction.OUTGOING, Direction.INCOMING}) { 669 | PathSegmentTree pathSegmentTree = findIntersection(tx, startNode, direction, depth); 670 | if (pathSegmentTree != null && pathSegmentTree.osmNode != null) { 671 | pathSegmentTrees.add(pathSegmentTree); 672 | } 673 | } 674 | return pathSegmentTrees; 675 | } 676 | 677 | private PathSegmentTree findIntersection(Transaction tx, Node startNode, Direction direction, int depth) { 678 | PathSegmentTree pathSegment = new PathSegmentTree(startNode, direction); 679 | if (depth < maxDepth && pathSegment.process(tx)) { 680 | if (previouslySeen.contains(pathSegment.osmNode)) { 681 | System.out.println("\tAlready processed potential intersection node, rejecting cyclic route: " + pathSegment.osmNode); 682 | return null; 683 | } 684 | previouslySeen.add(pathSegment.osmNode); 685 | if (pathSegment.osmNode.hasLabel(OSMModel.Intersection)) { 686 | System.out.println("\tFound labeled intersection: " + pathSegment.osmNode); 687 | return pathSegment; 688 | } else { 689 | ArrayList rels = pathSegment.nextWayRels(); 690 | if (rels.size() > 1) { 691 | // Not a chain, but an intersection, let's stop here 692 | if (addLabels) { 693 | System.out.println("\tFound unlabeled intersection (will add label and include): " + pathSegment.osmNode); 694 | pathSegment.osmNode.addLabel(OSMModel.Intersection); 695 | return pathSegment; 696 | } else { 697 | System.out.println("\tFound unlabeled intersection (will not add label, rejecting): " + pathSegment.osmNode); 698 | return null; 699 | } 700 | } else if (rels.size() == 1) { 701 | // This is a connection in a chain, keep looking in the same direction 702 | // TODO: Look in two directions (branching the chain, so needs a different storage than nextSegement) 703 | System.out.println("\tFound chain link at " + pathSegment.osmNode + ", searching further..."); 704 | Node nextWayNode = rels.get(0).getStartNode(); 705 | pathSegment.childSegments = findIntersections(tx, nextWayNode, depth + 1); 706 | return pathSegment; 707 | } else { 708 | // the end of a chain? 709 | return null; 710 | } 711 | } 712 | } else { 713 | return null; 714 | } 715 | } 716 | } 717 | } 718 | -------------------------------------------------------------------------------- /src/main/java/org/neo4j/gis/osm/procedures/OSMProcedures.java: -------------------------------------------------------------------------------- 1 | package org.neo4j.gis.osm.procedures; 2 | 3 | import org.neo4j.gis.osm.model.OSMModel; 4 | import org.neo4j.graphdb.*; 5 | import org.neo4j.internal.kernel.api.exceptions.ProcedureException; 6 | import org.neo4j.kernel.api.exceptions.Status; 7 | import org.neo4j.procedure.*; 8 | 9 | import java.util.*; 10 | import java.util.stream.Stream; 11 | 12 | public class OSMProcedures { 13 | @Context 14 | public Transaction tx; 15 | 16 | @Description("Given a point of interest node, and a collection of candidate ways to search, find or create a node on the way closest to the point of interest. " + 17 | "The returned node could be an existing node on the closest way, if such a node is close enough to the interpolation point. " + 18 | "If the interpolation point is not close to an existing node, one will be created at that point, and connected with ROUTE relationships to the adjacent existing nodes. " + 19 | "In both cases the closest node will be connected to the original point of interest with a ROUTE relationship, allowing subsequent shortest path routing on the graph.") 20 | @Procedure(value = "spatial.osm.routePointOfInterest", mode = Mode.WRITE) 21 | public Stream findRouteToPointOfInterest(@Name("OSMNode") Node node, @Name("OSMWays") List ways) throws ProcedureException { 22 | try { 23 | OSMModel osm = new OSMModel(); 24 | OSMModel.LocatedNode poi = osm.located(node); 25 | OSMModel.OSMWayDistance closestWay = ways.stream().map(osm::way).map(w -> w.closeTo(poi)).min(new OSMModel.ClosestWay()).orElse(null); 26 | if (closestWay == null) { 27 | throw new ProcedureException(Status.Procedure.ProcedureCallFailed, "Failed to find closest way from list of %d ways to node %s", ways.size(), node); 28 | } 29 | System.out.println("spatial.osm.routePointOfInterest(" + node + ") located closest way: " + closestWay); 30 | OSMModel.LocationMaker locationMaker = closestWay.getLocationMaker(); 31 | Node connected = locationMaker.process(tx); 32 | System.out.println("spatial.osm.routePointOfInterest(" + node + ") created connected node: " + connected); 33 | return Stream.of(new PointRouteResult(connected)); 34 | } catch (NullPointerException e) { 35 | e.printStackTrace(); 36 | throw e; 37 | } 38 | } 39 | 40 | @Procedure(value = "spatial.osm.routeIntersection", mode = Mode.WRITE) 41 | public Stream findStreetRoute(@Name("OSMNode") Node node, @Name("deleteExistingRoutes") boolean deleteExistingRoutes, @Name("createNewRoutes") boolean createNewRoutes, @Name("addLabels") boolean addLabels) throws ProcedureException { 42 | try { 43 | OSMModel osm = new OSMModel(); 44 | ArrayList routesToSearch = new ArrayList<>(); 45 | for (Relationship rel : node.getRelationships(Direction.INCOMING, OSMModel.NODE)) { 46 | routesToSearch.add(osm.intersectionRoutes(node, rel, rel.getStartNode(), addLabels)); 47 | } 48 | ArrayList routesFound = new ArrayList<>(); 49 | Collections.reverse(routesToSearch); 50 | for (OSMModel.IntersectionRoutes routes : routesToSearch) { 51 | if (routes.process(tx)) { 52 | for(OSMModel.IntersectionRoute route:routes.routes) { 53 | if (!deleteExistingRoutes && route.getExistingRoutes().size() > 0) { 54 | System.out.println("Already have existing routes between " + route.fromNode + " and " + route.toNode); 55 | } else { 56 | if (deleteExistingRoutes) { 57 | route.getExistingRoutes().forEach(Relationship::delete); 58 | } 59 | if (createNewRoutes) { 60 | route.mergeRouteRelationship(); 61 | } 62 | routesFound.add(new IntersectionRouteResult(route)); 63 | } 64 | } 65 | } else { 66 | System.out.println("Failed to find a routes for " + routes); 67 | } 68 | } 69 | System.out.println("Found " + routesFound.size() + " routes from " + node); 70 | return routesFound.stream(); 71 | } catch (NullPointerException e) { 72 | e.printStackTrace(); 73 | throw e; 74 | } 75 | } 76 | 77 | public static class IntersectionRouteResult { 78 | public Node fromNode; 79 | public Node wayNode; 80 | public Node toNode; 81 | public double distance; 82 | public long length; 83 | public long count; 84 | public Relationship fromRel; 85 | public Relationship toRel; 86 | 87 | public IntersectionRouteResult(OSMModel.IntersectionRoute route) { 88 | this.fromNode = route.fromNode; 89 | this.fromRel = route.fromRel; 90 | this.wayNode = route.wayNode; 91 | this.toNode = route.toNode; 92 | this.toRel = route.toRel; 93 | this.distance = route.distance; 94 | this.length = route.length; 95 | this.count = route.count; 96 | } 97 | } 98 | 99 | public class PointRouteResult { 100 | public Node node; 101 | 102 | public PointRouteResult(Node routeNode) { 103 | this.node = routeNode; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/org/neo4j/gis/spatial/SpatialConstants.java: -------------------------------------------------------------------------------- 1 | package org.neo4j.gis.spatial; 2 | 3 | public interface SpatialConstants { 4 | 5 | // OpenGIS geometry type numbers 6 | 7 | int GTYPE_GEOMETRY = 0; 8 | int GTYPE_POINT = 1; 9 | int GTYPE_LINESTRING = 2; 10 | int GTYPE_POLYGON = 3; 11 | int GTYPE_MULTIPOINT = 4; 12 | int GTYPE_MULTILINESTRING = 5; 13 | int GTYPE_MULTIPOLYGON = 6; 14 | 15 | } -------------------------------------------------------------------------------- /src/test/java/org/neo4j/gis/osm/OSMImportToolTest.java: -------------------------------------------------------------------------------- 1 | package org.neo4j.gis.osm; 2 | 3 | import org.junit.BeforeClass; 4 | import org.junit.Test; 5 | import org.neo4j.configuration.GraphDatabaseSettings; 6 | import org.neo4j.dbms.api.DatabaseManagementService; 7 | import org.neo4j.graphdb.*; 8 | import org.neo4j.io.fs.FileUtils; 9 | import org.neo4j.io.layout.Neo4jLayout; 10 | import org.neo4j.kernel.impl.traversal.MonoDirectionalTraversalDescription; 11 | import org.neo4j.test.TestDatabaseManagementServiceBuilder; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.util.ArrayList; 16 | import java.util.Arrays; 17 | import java.util.HashMap; 18 | import java.util.Map; 19 | import java.util.function.BiConsumer; 20 | 21 | import static java.lang.String.format; 22 | import static org.hamcrest.CoreMatchers.equalTo; 23 | import static org.hamcrest.MatcherAssert.assertThat; 24 | import static org.hamcrest.Matchers.hasKey; 25 | import static org.hamcrest.Matchers.isOneOf; 26 | import static org.junit.Assert.fail; 27 | import static org.neo4j.internal.helpers.collection.MapUtil.map; 28 | 29 | public class OSMImportToolTest { 30 | 31 | private static Neo4jLayout home = Neo4jLayout.of(new File("target/import-test")); 32 | 33 | @BeforeClass 34 | public static void ensureClean() throws IOException { 35 | // Previous runs leave databases that are incompatible with newer runs 36 | FileUtils.deleteRecursively(home.homeDirectory()); 37 | } 38 | 39 | @Test 40 | public void testOneStreet() throws IOException { 41 | importAndAssert("one-street", (db, stats) -> { 42 | stats.put("expectedOSMNodes", 8L); 43 | stats.put("expectedOSMWayNodes", 8L); 44 | stats.put("expectedOSMWays", 1L); 45 | assertOSMModel(db, stats); 46 | }); 47 | } 48 | 49 | @Test 50 | public void testOneWayStreet() throws IOException { 51 | importAndAssert("one-way-forward", (db, stats) -> { 52 | stats.put("expectedOSMNodes", 4L); 53 | stats.put("expectedOSMWayNodes", 4L); 54 | stats.put("expectedOSMWays", 1L); 55 | assertOSMModel(db, stats); 56 | assertOSMWay(db, Direction.OUTGOING, 72090582, new long[]{857081476, 857081950, 857081819, 857081796}); 57 | }); 58 | } 59 | 60 | @Test 61 | public void testOneWayStreetBackwards() throws IOException { 62 | importAndAssert("one-way-backward", (db, stats) -> { 63 | stats.put("expectedOSMNodes", 4L); 64 | stats.put("expectedOSMWayNodes", 4L); 65 | stats.put("expectedOSMWays", 1L); 66 | assertOSMModel(db, stats); 67 | assertOSMWay(db, Direction.INCOMING, 72090582, new long[]{857081476, 857081950, 857081819, 857081796}); 68 | }); 69 | } 70 | 71 | @Test 72 | public void testTwoStreet() throws IOException { 73 | importAndAssert("two-street", (db, stats) -> { 74 | stats.put("expectedOSMNodes", 24L); 75 | stats.put("expectedOSMWayNodes", 24L); 76 | stats.put("expectedOSMWays", 2L); 77 | assertOSMModel(db, stats); 78 | }); 79 | } 80 | 81 | @Test 82 | public void testParking() throws IOException { 83 | importAndAssert("parking", (db, stats) -> { 84 | stats.put("expectedOSMNodes", 4L); 85 | stats.put("expectedOSMWayNodes", 4L); 86 | stats.put("expectedOSMWays", 1L); 87 | assertOSMModel(db, stats); 88 | }); 89 | } 90 | 91 | @Test 92 | public void testParkingAndStreets() throws IOException { 93 | importAndAssert("parking-and-streets", (db, stats) -> { 94 | stats.put("expectedOSMNodes", 17L); 95 | stats.put("expectedOSMWayNodes", 26L); 96 | stats.put("expectedOSMWays", 7L); 97 | stats.put("expectedOSMRelations", 1L); 98 | stats.put("expectedOSMRelationMembers", 3L); 99 | assertOSMModel(db, stats); 100 | }); 101 | } 102 | 103 | @Test 104 | public void testOSM() throws IOException { 105 | importAndAssert("map", (db, stats) -> { 106 | stats.put("expectedOSMNodes", 2334L); 107 | stats.put("nodesWithTags", 202L); 108 | stats.put("expectedOSMWayNodes", 2588L - stats.get("closedWays")); 109 | stats.put("expectedOSMWays", 167L); 110 | stats.put("expectedOSMRelations", 6L); 111 | stats.put("expectedOSMRelationMembers", 40L); // 424 are defined, but only 40 exist in same file 112 | assertOSMModel(db, stats); 113 | }); 114 | } 115 | 116 | @Test 117 | public void testOSM2() throws IOException { 118 | importAndAssert("map2", (db, stats) -> { 119 | stats.put("expectedOSMTags", 8796L); 120 | stats.put("expectedOSMNodes", 43630L); 121 | stats.put("expectedOSMWayNodes", 50703L); 122 | stats.put("expectedOSMWays", 7023L); 123 | stats.put("expectedOSMRelations", 115L); 124 | stats.put("expectedOSMRelationMembers", 626L); 125 | stats.put("expectedNextRels", 46903L); 126 | assertOSMModel(db, stats); 127 | }, true); 128 | } 129 | 130 | @Test 131 | public void testMultiOSM() throws IOException { 132 | importAndAssert(new String[]{"map", "map2"}, (db, stats) -> { 133 | stats.put("expectedOSMTags", 9170L); 134 | stats.put("expectedOSMNodes", 45964L); 135 | stats.put("expectedOSMWayNodes", 53273L); 136 | stats.put("expectedOSMWays", 7190L); 137 | stats.put("expectedOSMRelations", 120L); 138 | assertOSMModel(db, stats, true); // merging OSM models currently duplicates relationships 139 | }); 140 | } 141 | 142 | private File findOSMFile(String name) { 143 | for (String ext : new String[]{".osm.bz2", ".osm"}) { 144 | File file = new File("samples/" + name + ext); 145 | if (file.exists()) return file; 146 | } 147 | throw new IllegalArgumentException("Cannot find import file for '" + name + "'"); 148 | } 149 | 150 | private void importAndAssert(String name, BiConsumer> assertions) throws IOException { 151 | importAndAssert(name, assertions, false); 152 | } 153 | 154 | private void importAndAssert(String name, BiConsumer> assertions, boolean tracePageCache) throws IOException { 155 | File osmFile = findOSMFile(name); 156 | if (tracePageCache) { 157 | importAndAssert(name, osmFile.getName(), assertions, "--trace-page-cache", "--into", home.homeDirectory().getCanonicalPath(), "--database", name, osmFile.getCanonicalPath()); 158 | } else { 159 | importAndAssert(name, osmFile.getName(), assertions, "--into", home.homeDirectory().getCanonicalPath(), "--database", name, osmFile.getCanonicalPath()); 160 | } 161 | } 162 | 163 | private void importAndAssert(String[] files, BiConsumer> assertions) throws IOException { 164 | String name = "multi-import"; 165 | String[] args = new String[files.length + 5]; 166 | args[0] = "--skip-duplicate-nodes"; 167 | args[1] = "--into"; 168 | args[2] = home.homeDirectory().getCanonicalPath(); 169 | args[3] = "--database"; 170 | args[4] = name; 171 | for (int i = 0; i < files.length; i++) { 172 | try { 173 | args[i + 5] = findOSMFile(files[i]).getCanonicalPath(); 174 | } catch (IOException e) { 175 | throw new RuntimeException(e); 176 | } 177 | } 178 | importAndAssert(name, Arrays.toString(files), assertions, args); 179 | } 180 | 181 | private void importAndAssert(String name, String description, BiConsumer> assertions, String ... args) { 182 | try { 183 | var storeDir = prepareStore(name); 184 | OSMImportTool.main(args); 185 | System.out.println("\nFinished importing " + description + " into " + storeDir + " - analysing database ..."); 186 | assertImportedCorrectly(name, assertions); 187 | } catch (Exception e) { 188 | System.err.println("Failed to import " + description + " into '" + name + "': " + e); 189 | e.printStackTrace(System.err); 190 | throw new RuntimeException(e); 191 | } 192 | } 193 | 194 | private File prepareStore(String name) throws IOException { 195 | var storeDir = home.databaseLayout(name).databaseDirectory(); 196 | FileUtils.deleteRecursively(storeDir); 197 | if (storeDir.mkdirs()) { 198 | System.out.println("Created store directory: " + storeDir); 199 | } 200 | return storeDir; 201 | } 202 | 203 | private void assertImportedCorrectly(String name, BiConsumer> assertions) { 204 | // We are testing against community, and that cannot have multiple databases 205 | DatabaseManagementService databases = new TestDatabaseManagementServiceBuilder(home) 206 | .setConfig(GraphDatabaseSettings.default_database, name) 207 | .setConfig(GraphDatabaseSettings.fail_on_missing_files, false).build(); 208 | GraphDatabaseService db = databases.database(name); 209 | Map stats = debugOSMModel(db); 210 | assertions.accept(db, stats); 211 | databases.shutdown(); 212 | } 213 | 214 | private Map debugOSMModel(GraphDatabaseService db) { 215 | HashMap stats = new HashMap<>(); 216 | stats.put("nonWayNodes", countResult(db, "MATCH (n:OSMNode) WHERE NOT exists((n)<--(:OSMWayNode)) RETURN count(n) AS count")); 217 | debugLine(stats.get("nonWayNodes"), "Nodes with label 'OSMNode' that are not part of any ways"); 218 | stats.put("sharedWayNodes", countResult(db, "MATCH (n:OSMNode)<-[r:NODE]-() WITH n, count(r) as ways WHERE ways > 1 RETURN count(n) AS count")); 219 | debugLine(stats.get("sharedWayNodes"), "Nodes with label 'OSMNode' that are part of more than one way"); 220 | stats.put("closedWays", countResult(db, "MATCH (n:OSMWayNode)<-[:FIRST_NODE]-(), ()-[:NEXT]->(n)-[:NEXT]->() RETURN count(n) AS count")); 221 | debugLine(stats.get("closedWays"), "Ways that are closed (have same first and end way node)"); 222 | for (String label : new String[]{"OSMNode", "OSMWay", "OSMWayNode", "OSMTags"}) { 223 | debugLine(countNodesWithLabel(db, label), "Nodes with label '" + label + "'"); 224 | } 225 | for (String type : new String[]{"TAGS", "FIRST_NODE", "NEXT", "NODE"}) { 226 | debugLine(countRelationshipsWithType(db, type), "Relationships of type '" + type + "'"); 227 | } 228 | try (Transaction tx = db.beginTx()) { 229 | Result result = tx.execute("MATCH ()-[:NODE]->(n:OSMNode)<-[:NODE]-() WITH n, count(n) AS ways RETURN ways, count(ways) AS count"); 230 | if (result.hasNext()) { 231 | System.out.println(format("%8s%8s", "ways", "count")); 232 | while (result.hasNext()) { 233 | Map record = result.next(); 234 | System.out.println(format("%8d%8d", Long.parseLong(record.get("ways").toString()), Long.parseLong(record.get("count").toString()))); 235 | } 236 | } 237 | tx.commit(); 238 | } 239 | return stats; 240 | } 241 | 242 | private void assertOSMModel(GraphDatabaseService db, Map stats) { 243 | assertOSMModel(db, stats, false); 244 | } 245 | 246 | private void assertOSMModel(GraphDatabaseService db, Map stats, boolean ignoreRelationships) { 247 | long expectedOSMNodes = getFromStats(stats, "expectedOSMNodes", 0); 248 | long expectedOSMWayNodes = getFromStats(stats, "expectedOSMWayNodes", expectedOSMNodes); 249 | long expectedOSMWays = getFromStats(stats, "expectedOSMWays", 0); 250 | long expectedOSMRelations = getFromStats(stats, "expectedOSMRelations", 0); 251 | long expectedOSMRelationMembers = getFromStats(stats, "expectedOSMRelationMembers", 0); 252 | long nodesWithTags = getFromStats(stats, "nodesWithTags", 0); 253 | long closedWays = getFromStats(stats, "closedWays", 0); 254 | long expectedOSMTags = getFromStats(stats, "expectedOSMTags", nodesWithTags + expectedOSMWays + expectedOSMRelations); 255 | long expectedNextRels = getFromStats(stats, "expectedNextRels", expectedOSMWayNodes - expectedOSMWays + closedWays); 256 | map( 257 | "OSMNode", expectedOSMNodes, 258 | "OSMWay", expectedOSMWays, 259 | "OSMRelation", expectedOSMRelations, 260 | "OSMWayNode", expectedOSMWayNodes, 261 | "OSMTags", expectedOSMTags 262 | ).forEach( 263 | (label, count) -> assertThat("Expected specific number of '" + label + "' nodes", countNodesWithLabel(db, label), equalTo(count)) 264 | ); 265 | if (!ignoreRelationships) { 266 | map( 267 | "TAGS", expectedOSMTags, 268 | "FIRST_NODE", expectedOSMWays, 269 | "NEXT", expectedNextRels, 270 | "NODE", expectedOSMWayNodes, 271 | "MEMBER", expectedOSMRelationMembers 272 | ).forEach( 273 | (type, count) -> assertThat("Expected specific number of '" + type + "' relationships", countRelationshipsWithType(db, type), equalTo(count)) 274 | ); 275 | } 276 | } 277 | 278 | private void assertOneWay(Node node, Direction direction, String[] forwardValues, String[] backwardValues) { 279 | Map properties = node.getAllProperties(); 280 | assertThat("Expected one-way tag", properties, hasKey("oneway")); 281 | String oneway = properties.get("oneway").toString(); 282 | if (direction == Direction.OUTGOING) { 283 | assertThat("Expected forward direction oneway to be valid entry", oneway, isOneOf(forwardValues)); 284 | } else if (direction == Direction.INCOMING) { 285 | assertThat("Expected reverse direction oneway to be '-1'", oneway, isOneOf(backwardValues)); 286 | } 287 | 288 | } 289 | 290 | private String[] a(String... fields) { 291 | return fields; 292 | } 293 | 294 | private void assertOSMWay(GraphDatabaseService db, Direction direction, int wayId, long[] expectedNodeIds) { 295 | try (Transaction tx = db.beginTx()) { 296 | Node way = tx.findNode(Label.label("OSMWay"), "way_osm_id", wayId); 297 | if (way != null) { 298 | assertOneWay(way, direction, a("FORWARD"), a("BACKWARD")); 299 | if (way.hasRelationship(Direction.OUTGOING, RelationshipType.withName("TAGS"))) { 300 | Node tags = way.getSingleRelationship(RelationshipType.withName("TAGS"), Direction.OUTGOING).getEndNode(); 301 | assertOneWay(tags, direction, a("yes", "true", "1"), a("-1")); 302 | } 303 | if (way.hasRelationship(Direction.OUTGOING, RelationshipType.withName("FIRST_NODE"))) { 304 | Node firstNode = way.getSingleRelationship(RelationshipType.withName("FIRST_NODE"), Direction.OUTGOING).getEndNode(); 305 | ArrayList wayNodes = new ArrayList<>(expectedNodeIds.length); 306 | for (Path path : new MonoDirectionalTraversalDescription().depthFirst().relationships(RelationshipType.withName("NEXT"), direction).traverse(firstNode)) { 307 | Node endNode = path.endNode(); 308 | if (wayNodes.size() > 0 && endNode.getId() == firstNode.getId()) { 309 | break; 310 | } 311 | wayNodes.add(endNode); 312 | } 313 | long[] nodeIds = new long[wayNodes.size()]; 314 | int wayNodeIndex = 0; 315 | for (Node wayNode : wayNodes) { 316 | Node node = wayNode.getSingleRelationship(RelationshipType.withName("NODE"), Direction.OUTGOING).getEndNode(); 317 | nodeIds[wayNodeIndex] = (Long) node.getProperty("node_osm_id"); 318 | wayNodeIndex++; 319 | } 320 | assertThat("Should have correct node ID order", nodeIds, equalTo(expectedNodeIds)); 321 | } else { 322 | fail("Way has no outgoing FIRST_NODE relationship"); 323 | } 324 | } else { 325 | fail("No way found with way_osm_id = " + wayId); 326 | } 327 | tx.commit(); 328 | } 329 | } 330 | 331 | private long getFromStats(Map stats, String key, long ifNull) { 332 | return stats.getOrDefault(key, ifNull); 333 | } 334 | 335 | private void debugLine(long count, String message) { 336 | System.out.println(format("%8d\t%s", count, message)); 337 | } 338 | 339 | private long countNodesWithLabel(GraphDatabaseService db, String label) { 340 | return countResult(db, "MATCH (n:" + label + ") RETURN count(n) AS count"); 341 | } 342 | 343 | private long countRelationshipsWithType(GraphDatabaseService db, String type) { 344 | return countResult(db, "MATCH ()-[r:" + type + "]->() RETURN count(r) AS count"); 345 | } 346 | 347 | private long countResult(GraphDatabaseService db, String query) { 348 | try (Transaction tx = db.beginTx()) { 349 | Result result = tx.execute(query); 350 | assertThat("Expected query to return a result", result.hasNext(), equalTo(true)); 351 | Map record = result.next(); 352 | assertThat("Expected record to have a count field", record, hasKey("count")); 353 | return (Long) record.get("count"); 354 | } 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/test/java/org/neo4j/gis/osm/model/OSMModelIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package org.neo4j.gis.osm.model; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.neo4j.dbms.api.DatabaseManagementService; 7 | import org.neo4j.graphdb.*; 8 | import org.neo4j.test.TestDatabaseManagementServiceBuilder; 9 | import org.neo4j.values.storable.PointValue; 10 | 11 | import java.util.ArrayList; 12 | import java.util.HashSet; 13 | 14 | import static org.hamcrest.MatcherAssert.assertThat; 15 | import static org.hamcrest.Matchers.*; 16 | import static org.junit.Assert.assertNotNull; 17 | import static org.junit.Assert.fail; 18 | 19 | public class OSMModelIntegrationTest { 20 | 21 | private DatabaseManagementService databases; 22 | private GraphDatabaseService db; 23 | 24 | @Before 25 | public void setup() { 26 | this.databases = new TestDatabaseManagementServiceBuilder().impermanent().build(); 27 | this.db = databases.database("neo4j"); 28 | try (Transaction tx = db.beginTx()) { 29 | TestOSMModel osm = new TestOSMModel(tx); 30 | osm.buildSquare(10); 31 | osm.buildMultiChain("ChainTopRight", 10, 10, 10, 5, 1, 1); 32 | osm.buildMultiChain("ChainBottomRight", 10, 0, 10, 5, 1, -1); 33 | osm.buildMultiChain("ChainTopLeft", 0, 10, 10, 5, -1, 1); 34 | osm.buildMultiChain("ChainBottomLeft", 0, 0, 10, 5, -1, -1); 35 | osm.addIntersectionLabels(); 36 | tx.commit(); 37 | } 38 | } 39 | 40 | @After 41 | public void shutdown() { 42 | this.databases.shutdown(); 43 | } 44 | 45 | @Test 46 | public void shouldFindClosest() { 47 | try (Transaction tx = db.beginTx()) { 48 | TestOSMModel osm = new TestOSMModel(tx); 49 | assertFindWay(osm, "Left", 2, 112000, 1, 2); 50 | assertFindWay(osm, "Top", 2, 112000, 2, 9); 51 | assertFindWay(osm, "Right", 2, 112000, 9, 2); 52 | assertFindWay(osm, "Bottom", 8, 112000, 8, 1); 53 | tx.commit(); 54 | } 55 | } 56 | 57 | @Test 58 | public void shouldFindClosestFurther() { 59 | try (Transaction tx = db.beginTx()) { 60 | TestOSMModel osm = new TestOSMModel(tx); 61 | assertFindWay(osm, "Bottom", 5, 112000, 5, 1); 62 | assertFindWay(osm, "Bottom", 5, 223000, 5, 2); 63 | assertFindWay(osm, "Bottom", 5, 334000, 5, 3); 64 | assertFindWay(osm, "Bottom", 5, 446000, 5, 4); 65 | tx.commit(); 66 | } 67 | } 68 | 69 | @Test 70 | public void shouldFindClosestCloser() { 71 | try (Transaction tx = db.beginTx()) { 72 | TestOSMModel osm = new TestOSMModel(tx); 73 | assertFindWay(osm, "Bottom", 5, 112000, 5, 1); 74 | assertFindWay(osm, "Bottom", 5, 56000, 5, 0.5); 75 | assertFindWay(osm, "Bottom", 5, 11200, 5, 0.1); 76 | tx.commit(); 77 | } 78 | } 79 | 80 | @Test 81 | public void shouldInterpolateNewNode() { 82 | assertFindWayAndInterpolatedPoint("Bottom", new int[]{4, 5}, 112000, 4.5, 1); 83 | assertFindWayAndInterpolatedPoint("Bottom", new int[]{4, 5}, 56000, 4.5, 0.5); 84 | assertFindWayAndInterpolatedPoint("Bottom", new int[]{4, 5}, 11200, 4.5, 0.1); 85 | assertFindWayAndInterpolatedPoint("Bottom", new int[]{4, 5}, 1120, 4.5, 0.01); 86 | assertFindWayAndInterpolatedPoint("Bottom", new int[]{4, 5}, 112, 4.5, 0.001); 87 | assertFindWayAndInterpolatedPoint("Bottom", new int[]{4, 5}, 112, 4.5, -0.001); 88 | assertFindWayAndInterpolatedPoint("Bottom", new int[]{4, 5}, 1120, 4.5, -0.01); 89 | assertFindWayAndInterpolatedPoint("Bottom", new int[]{4, 5}, 11200, 4.5, -0.1); 90 | assertFindWayAndInterpolatedPoint("Bottom", new int[]{4, 5}, 56000, 4.5, -0.5); 91 | assertFindWayAndInterpolatedPoint("Bottom", new int[]{4, 5}, 112000, 4.5, -1); 92 | } 93 | 94 | @Test 95 | public void shouldInterpolateNewNode2() { 96 | assertFindWayAndInterpolatedPoint("Right", new int[]{7, 8}, 221000, 8, 7.5); 97 | assertFindWayAndInterpolatedPoint("Right", new int[]{7, 8}, 112000, 9, 7.5); 98 | assertFindWayAndInterpolatedPoint("Right", new int[]{7, 8}, 10, 10, 7.5); 99 | assertFindWayAndInterpolatedPoint("Right", new int[]{7, 8}, 112000, 11, 7.5); 100 | assertFindWayAndInterpolatedPoint("Right", new int[]{7, 8}, 221000, 12, 7.5); 101 | } 102 | 103 | @Test 104 | public void shouldFollowConnectedChain() { 105 | try (Transaction tx = db.beginTx()) { 106 | TestOSMModel osm = new TestOSMModel(tx); 107 | OSMModel.OSMWay chain0 = osm.getWay("ChainTopRight-0"); 108 | Node startNode = chain0.wayNodes.get(0); 109 | Relationship rel = startNode.getSingleRelationship(OSMModel.NODE, Direction.OUTGOING); 110 | for (int i = 0; i < 5; i++) { 111 | OSMModel.IntersectionRoutes.PathSegmentTree pathSegment = new OSMModel.IntersectionRoutes.PathSegmentTree(startNode, Direction.OUTGOING); 112 | assertThat("Should be able to follow path segment " + i, pathSegment.process(tx), equalTo(true)); 113 | assertThat("Should have found a path segment " + i + " of length 10", pathSegment.length, equalTo(10)); 114 | ArrayList nextRels = pathSegment.nextWayRels(); 115 | if (i < 4) { 116 | assertThat("Should only have 1 possible next relationship to follow for path segment " + i, nextRels.size(), equalTo(1)); 117 | startNode = nextRels.get(0).getStartNode(); 118 | } else { 119 | assertThat("Should only have 2 possible next relationships to follow for path segment " + i, nextRels.size(), equalTo(2)); 120 | } 121 | } 122 | tx.commit(); 123 | } 124 | } 125 | 126 | @Test 127 | public void shouldFindIntersections() { 128 | try (Transaction tx = db.beginTx()) { 129 | TestOSMModel osm = new TestOSMModel(tx); 130 | OSMModel.OSMWay chain0 = osm.getWay("ChainTopRight-0"); 131 | OSMModel.OSMWay chain5u = osm.getWay("ChainTopRight-5u"); 132 | OSMModel.OSMWay chain5d = osm.getWay("ChainTopRight-5d"); 133 | Node startNode = chain0.wayNodes.get(0); 134 | Relationship rel = startNode.getSingleRelationship(OSMModel.NODE, Direction.OUTGOING); 135 | OSMModel.IntersectionRoutes routes = osm.intersectionRoutes(rel.getEndNode(), rel, startNode, true); 136 | assertThat("Should succeed in finding an intersection", routes.process(tx), equalTo(true)); 137 | assertThat("Should find one route from", routes.routes.size(), equalTo(1)); 138 | OSMModel.IntersectionRoute route = routes.routes.get(0); 139 | assertThat("Should find intersection to first node of chain-5u", route.toNode, equalTo(chain5u.nodes.get(0).node())); 140 | assertThat("Should find intersection to first node of chain-5d", route.toNode, equalTo(chain5d.nodes.get(0).node())); 141 | assertThat("Last chain relationship should point to first node of next chain", route.toRel.getEndNode(), equalTo(chain5u.nodes.get(0).node())); 142 | tx.commit(); 143 | } 144 | } 145 | 146 | private void assertFindWay(TestOSMModel osm, String name, int expectedNode, double maxDist, double... coords) { 147 | OSMModel.OSMWay expectedWay = osm.getWay(name); 148 | OSMModel.LocatedNode poi = osm.makeNode(coords); 149 | OSMModel.OSMWayDistance closest = osm.ways.stream().map(w->w.closeTo(poi)).min(new OSMModel.ClosestWay()).orElse(null); 150 | assertNotNull("Expected to find a closest way, but was null", closest); 151 | assertThat("Found way with wrong name", closest.way.getName(), equalTo(name)); 152 | assertThat("Found wrong way", closest.way, equalTo(expectedWay)); 153 | OSMModel.OSMWayDistance.DistanceResult distanceResult = closest.closest; 154 | assertThat("Distance to found node is too long", distanceResult.nodeDistance, lessThan(maxDist)); 155 | assertThat("Not the closest node expected from index " + expectedNode, distanceResult.closestNodeIndex, equalTo(expectedNode)); 156 | OSMModel.LocationMaker location = distanceResult.getLocationMaker(); 157 | assertThat(location, instanceOf(OSMModel.LocationExists.class)); 158 | OSMModel.LocatedNode expectedClosestNode = expectedWay.nodes.get(expectedNode); 159 | assertThat("Should find the expected node", ((OSMModel.LocationExists) location).node, equalTo(expectedClosestNode.node())); 160 | } 161 | 162 | private void assertFindWayAndInterpolatedPoint(String name, int[] expectedPair, double maxDist, double... coords) { 163 | // TODO consider reverting to multiple transactions as in 3.5 version 164 | try (Transaction tx = db.beginTx()) { 165 | TestOSMModel osm = new TestOSMModel(tx); 166 | OSMModel.OSMWay expectedWay = osm.getWay(name); 167 | OSMModel.LocatedNode poi = osm.makeNode(coords); 168 | OSMModel.OSMWayDistance closest = osm.ways.stream().map(w -> w.closeTo(poi)).min(new OSMModel.ClosestWay()).orElse(null); 169 | assertNotNull("Expected to find a closest way, but was null", closest); 170 | assertThat("Found way with wrong name", closest.way.getName(), equalTo(name)); 171 | assertThat("Found wrong way", closest.way, equalTo(expectedWay)); 172 | OSMModel.OSMWayDistance.DistanceResult distanceResult = closest.closest; 173 | OSMModel.LocatedNode left = expectedWay.nodes.get(expectedPair[0]); 174 | OSMModel.LocatedNode right = expectedWay.nodes.get(expectedPair[1]); 175 | OSMModel.LocationMaker location = distanceResult.getLocationMaker(); 176 | OSMModel.Triangle triangle; 177 | Node node; 178 | node = location.process(tx); 179 | PointValue point = (PointValue) node.getProperty("location"); 180 | triangle = new OSMModel.Triangle(point, left.point(), right.point()); 181 | Node leftNode = null; 182 | Node rightNode = null; 183 | if (location instanceof OSMModel.LocationInterpolated) { 184 | OSMModel.LocationInterpolated interpolated = (OSMModel.LocationInterpolated) location; 185 | leftNode = interpolated.left.node(); 186 | rightNode = interpolated.right.node(); 187 | Node connected = poi.node().getSingleRelationship(OSMModel.ROUTE, Direction.OUTGOING).getEndNode(); 188 | assertThat("Should be connected to new node", connected, equalTo(node)); 189 | } else if (location instanceof OSMModel.LocationIsPoint) { 190 | OSMModel.LocationIsPoint interpolated = (OSMModel.LocationIsPoint) location; 191 | leftNode = interpolated.left; 192 | rightNode = interpolated.right; 193 | } else { 194 | fail("Unknown location type: " + location.getClass().getSimpleName()); 195 | } 196 | assertThat("Projected point should be on the line between the two original points", triangle.apexAngle(), closeTo(180.0, 5.0)); 197 | assertThat("Should find the expected left node", leftNode, equalTo(left.node())); 198 | assertThat("Should find the expected right node", rightNode, equalTo(right.node())); 199 | double distance = distanceResult.calculator.distance(triangle.apex, poi.point()); 200 | assertThat("Distance to interpolated node is too long", distance, lessThan(maxDist)); 201 | assertConnectedNodes(node, leftNode, rightNode); 202 | tx.commit(); 203 | } 204 | } 205 | 206 | private void assertConnectedNodes(Node node, Node left, Node right) { 207 | HashSet nodes = new HashSet<>(); 208 | for (Relationship rel : node.getRelationships(Direction.OUTGOING, OSMModel.ROUTE)) { 209 | assertThat("Route relationship should have distance property", rel.hasProperty("distance"), equalTo(true)); 210 | nodes.add(rel.getEndNode()); 211 | } 212 | assertThat("Should be connected to two nodes", nodes.size(), equalTo(2)); 213 | assertThat("Should contain left and right node", nodes, contains(left, right)); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/test/java/org/neo4j/gis/osm/model/OSMModelTest.java: -------------------------------------------------------------------------------- 1 | package org.neo4j.gis.osm.model; 2 | 3 | import org.junit.Test; 4 | import org.neo4j.values.storable.CoordinateReferenceSystem; 5 | import org.neo4j.values.storable.PointValue; 6 | import org.neo4j.values.storable.Values; 7 | 8 | import static org.hamcrest.MatcherAssert.assertThat; 9 | import static org.hamcrest.Matchers.closeTo; 10 | import static org.hamcrest.Matchers.equalTo; 11 | 12 | public class OSMModelTest { 13 | 14 | @Test 15 | public void shouldCalculateAnglesInTriangle() { 16 | PointValue[] left = new PointValue[]{ 17 | Values.pointValue(CoordinateReferenceSystem.Cartesian, 0, 0), 18 | Values.pointValue(CoordinateReferenceSystem.Cartesian, 0, 0) 19 | }; 20 | PointValue[] right = new PointValue[]{ 21 | Values.pointValue(CoordinateReferenceSystem.Cartesian, 10, 0), 22 | Values.pointValue(CoordinateReferenceSystem.Cartesian, 10, -10) 23 | }; 24 | for (int x = 10; x >= -10; x--) { 25 | PointValue[] apex = new PointValue[]{ 26 | Values.pointValue(CoordinateReferenceSystem.Cartesian, x, 10), 27 | Values.pointValue(CoordinateReferenceSystem.Cartesian, 10 + x, 10 - x) 28 | }; 29 | double[] leftAngle = new double[apex.length]; 30 | double[] rightAngle = new double[apex.length]; 31 | double[] apexAngle = new double[apex.length]; 32 | OSMModel.Triangle[] triangle = new OSMModel.Triangle[apex.length]; 33 | for (int i = 0; i < apex.length; i++) { 34 | triangle[i] = new OSMModel.Triangle(apex[i], left[i], right[i]); 35 | leftAngle[i] = triangle[i].leftAngle(); 36 | rightAngle[i] = triangle[i].rightAngle(); 37 | apexAngle[i] = triangle[i].apexAngle(); 38 | double total = leftAngle[i] + rightAngle[i] + apexAngle[i]; 39 | assertThat("Should sum to 180", total, closeTo(180.0, 0.001)); 40 | //System.out.println("X:" + x + "\tProj:" + triangle[i].project() + "\tleft:" + leftAngle[i] + "\tright:" + rightAngle[i] + "\tapex:" + apexAngle[i]); 41 | if (i == 0) { 42 | double[] projected = triangle[i].project().coordinate(); 43 | assertThat("Projection should lie on z axis with same value of x as apex", projected[0], equalTo((double) x)); 44 | assertThat("Projection should lie on z axis with same value of x as apex", projected[1], equalTo(0.0)); 45 | } 46 | if (i == 1) { 47 | double[] projected = triangle[i].project().coordinate(); 48 | assertThat("Projection should lie on the 1:-1 diagonal with x and y being -10 from apex", projected[0], equalTo(apex[i].coordinate()[0] - 10)); 49 | assertThat("Projection should lie on the 1:-1 diagonal with x and y being -10 from apex", projected[1], equalTo(apex[i].coordinate()[1] - 10)); 50 | } 51 | if (i > 0) { 52 | assertThat("Should have same left angle", leftAngle[i], closeTo(leftAngle[0], 0.001)); 53 | assertThat("Should have same right angle", rightAngle[i], closeTo(rightAngle[0], 0.001)); 54 | assertThat("Should have same apex angle", apexAngle[i], closeTo(apexAngle[0], 0.001)); 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/org/neo4j/gis/osm/model/TestOSMModel.java: -------------------------------------------------------------------------------- 1 | package org.neo4j.gis.osm.model; 2 | 3 | import org.neo4j.graphdb.*; 4 | import org.neo4j.values.storable.CRSCalculator; 5 | import org.neo4j.values.storable.CoordinateReferenceSystem; 6 | import org.neo4j.values.storable.PointValue; 7 | import org.neo4j.values.storable.Values; 8 | 9 | import java.util.ArrayList; 10 | import java.util.HashMap; 11 | import java.util.Iterator; 12 | 13 | public class TestOSMModel extends OSMModel { 14 | ArrayList ways; 15 | HashMap nodes; 16 | 17 | private Transaction tx; 18 | 19 | public TestOSMModel(Transaction tx) { 20 | this.tx = tx; 21 | this.nodes = new HashMap<>(); 22 | this.ways = new ArrayList<>(); 23 | } 24 | 25 | public void buildSquare(int size) { 26 | ways.add(makeHorizontalWay(size, "Top", 0, size, 1)); 27 | ways.add(makeHorizontalWay(size, "Bottom", 0, 0, 1)); 28 | ways.add(makeVerticalWay(size, "Left", 0, 0, 1)); 29 | ways.add(makeVerticalWay(size, "Right", size, 0, 1)); 30 | } 31 | 32 | public void buildMultiChain(String name, double xbase, double ybase, int size, int count, int xdir, int ydir) { 33 | for (int i = 0; i < count; i++) { 34 | int x = xdir * size * ((i + 1) / 2); 35 | int y = ydir * size * (i / 2); 36 | if (i % 2 == 0) { 37 | ways.add(makeHorizontalWay(size, name + "-" + i, x + xbase, y + ybase, xdir)); 38 | } else { 39 | ways.add(makeVerticalWay(size, name + "-" + i, x + xbase, y + ybase, ydir)); 40 | } 41 | } 42 | int x = xdir * size * ((count + 1) / 2); 43 | int y = ydir * size * (count / 2); 44 | ways.add(makeVerticalWay(size, name + "-" + count + "u", x + xbase, y + ybase, 1)); 45 | ways.add(makeVerticalWay(size, name + "-" + count + "d", x + xbase, y + ybase, -1)); 46 | } 47 | 48 | public void addIntersectionLabels() { 49 | ResourceIterator routable = tx.findNodes(OSMModel.Routable); 50 | while (routable.hasNext()) { 51 | Node node = routable.next(); 52 | Iterator routes = node.getRelationships(Direction.INCOMING, OSMModel.NODE).iterator(); 53 | int count = 0; 54 | while (routes.hasNext()) { 55 | Node wayNode = routes.next().getStartNode(); 56 | for (Relationship ignore : wayNode.getRelationships(Direction.BOTH, OSMModel.NEXT)) { 57 | count++; 58 | } 59 | } 60 | if (count > 2) { 61 | node.addLabel(OSMModel.Intersection); 62 | System.out.println("Added Intersection label to " + node); 63 | } 64 | } 65 | } 66 | 67 | public OSMModel.OSMWay getWay(String name) { 68 | if(ways.isEmpty()) loadWays(); 69 | for (OSMModel.OSMWay way : ways) { 70 | if (way.getName().equals(name)) return way; 71 | } 72 | return null; 73 | } 74 | 75 | private OSMModel.OSMWay makeHorizontalWay(int size, String name, double x, double y, int dir) { 76 | OSMModel.LocatedNode[] nodes = new OSMModel.LocatedNode[size + 1]; 77 | for (int i = 0; i <= size; i++) { 78 | nodes[i] = makeNode( x + i * dir, y); 79 | } 80 | return makeWay(name, nodes); 81 | } 82 | 83 | private OSMModel.OSMWay makeVerticalWay(int size, String name, double x, double y, int dir) { 84 | OSMModel.LocatedNode[] nodes = new OSMModel.LocatedNode[size + 1]; 85 | for (int i = 0; i <= size; i++) { 86 | nodes[i] = makeNode( x, y + i * dir); 87 | } 88 | return makeWay(name, nodes); 89 | } 90 | 91 | private void loadWays() { 92 | ways.clear(); 93 | ResourceIterator wayNodes = tx.findNodes(OSMModel.OSMWay); 94 | while (wayNodes.hasNext()) { 95 | Node wayNode = wayNodes.next(); 96 | String name = (String) wayNode.getProperty("name"); 97 | if (name == null) { 98 | throw new IllegalStateException("Existing way is missing 'name' property: " + wayNode); 99 | } 100 | ways.add(way(wayNode)); 101 | } 102 | } 103 | 104 | private OSMModel.OSMWay makeWay(String name, OSMModel.LocatedNode... nodes) { 105 | CRSCalculator calculator = CoordinateReferenceSystem.WGS84.getCalculator(); 106 | Node wayNode = tx.createNode(OSMModel.OSMWay); 107 | wayNode.setProperty("name", name); 108 | Node tags = tx.createNode(OSMModel.OSMTags); 109 | tags.setProperty("name", name); 110 | tags.setProperty("highway", "residential"); 111 | wayNode.createRelationshipTo(tags, OSMModel.TAGS); 112 | Node previous = null; 113 | PointValue previousPoint = null; 114 | for (OSMModel.LocatedNode node : nodes) { 115 | Node proxy = tx.createNode(OSMModel.OSMWayNode); 116 | proxy.createRelationshipTo(node.node(), OSMModel.NODE); 117 | if (previous == null) { 118 | wayNode.createRelationshipTo(proxy, OSMModel.FIRST_NODE); 119 | } else { 120 | Relationship next = previous.createRelationshipTo(proxy, OSMModel.NEXT); 121 | next.setProperty("distance", calculator.distance(node.point(), previousPoint)); 122 | } 123 | previous = proxy; 124 | previousPoint = node.point(); 125 | } 126 | return this.way(wayNode); 127 | } 128 | 129 | public OSMModel.LocatedNode makeNode(double... coords) { 130 | PointValue point = Values.pointValue(CoordinateReferenceSystem.WGS84, coords); 131 | OSMModel.LocatedNode located = nodes.get(point); 132 | if (located == null) { 133 | Node node = tx.createNode(OSMModel.Routable); 134 | node.setProperty("location", point); 135 | located = this.located(node); 136 | nodes.put(point, located); 137 | } 138 | return located; 139 | } 140 | } -------------------------------------------------------------------------------- /src/test/java/org/neo4j/gis/osm/procedures/OSMProceduresTest.java: -------------------------------------------------------------------------------- 1 | package org.neo4j.gis.osm.procedures; 2 | 3 | import org.junit.After; 4 | import org.junit.Assert; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.neo4j.dbms.api.DatabaseManagementService; 8 | import org.neo4j.exceptions.KernelException; 9 | import org.neo4j.gis.osm.model.OSMModel; 10 | import org.neo4j.gis.osm.model.TestOSMModel; 11 | import org.neo4j.graphdb.*; 12 | import org.neo4j.graphdb.traversal.TraversalDescription; 13 | import org.neo4j.kernel.api.procedure.GlobalProcedures; 14 | import org.neo4j.kernel.impl.traversal.MonoDirectionalTraversalDescription; 15 | import org.neo4j.kernel.internal.GraphDatabaseAPI; 16 | import org.neo4j.test.TestDatabaseManagementServiceBuilder; 17 | 18 | import java.util.ArrayList; 19 | import java.util.HashSet; 20 | import java.util.LinkedHashMap; 21 | import java.util.Map; 22 | import java.util.function.Consumer; 23 | 24 | import static org.hamcrest.CoreMatchers.containsString; 25 | import static org.hamcrest.MatcherAssert.assertThat; 26 | import static org.hamcrest.Matchers.*; 27 | import static org.junit.Assert.*; 28 | import static org.neo4j.gis.osm.model.OSMModel.Routable; 29 | 30 | public class OSMProceduresTest { 31 | private DatabaseManagementService databases; 32 | private GraphDatabaseService db; 33 | //private TestOSMModel osm; 34 | 35 | @Before 36 | public void setUp() throws KernelException { 37 | databases = new TestDatabaseManagementServiceBuilder().impermanent().build(); 38 | db = databases.database("neo4j"); 39 | registerProcedure(db, OSMProcedures.class); 40 | try (Transaction tx = db.beginTx()) { 41 | TestOSMModel osm = new TestOSMModel(tx); 42 | osm.buildSquare(10); 43 | osm.buildMultiChain("ChainTopRight", 10, 10, 10, 5, 1, 1); 44 | osm.buildMultiChain("ChainBottomRight", 10, 0, 10, 5, 1, -1); 45 | osm.buildMultiChain("ChainTopLeft", 0, 10, 10, 5, -1, 1); 46 | osm.buildMultiChain("ChainBottomLeft", 0, 0, 10, 5, -1, -1); 47 | osm.addIntersectionLabels(); 48 | tx.commit(); 49 | } 50 | try (Transaction tx = db.beginTx()) { 51 | var result = tx.execute("MATCH (n) RETURN count(n)"); 52 | result.accept((row)-> true); 53 | tx.commit(); 54 | } 55 | } 56 | 57 | @After 58 | public void tearDown() { 59 | databases.shutdown(); 60 | } 61 | 62 | public static void testCall(Transaction tx, String call, Consumer> consumer) { 63 | testCall(tx, call, null, consumer); 64 | } 65 | 66 | private static Map map(Object... values) { 67 | Map map = new LinkedHashMap<>(); 68 | for (int i = 0; i < values.length; i += 2) { 69 | map.put(values[i].toString(), values[i + 1]); 70 | } 71 | return map; 72 | } 73 | 74 | private static void testCall(Transaction tx, String call, Map params, Consumer> consumer) { 75 | testCall(tx, call, params, consumer, true); 76 | } 77 | 78 | private static void testCallFails(Transaction tx, String call, Map params, String error) { 79 | try { 80 | testResult(tx, call, params, (res) -> { 81 | while (res.hasNext()) { 82 | res.next(); 83 | } 84 | }); 85 | fail("Expected an exception containing '" + error + "', but no exception was thrown"); 86 | } catch (Exception e) { 87 | Assert.assertThat(e.getMessage(), containsString(error)); 88 | } 89 | } 90 | 91 | private static void testCall(Transaction tx, String call, Map params, Consumer> consumer, boolean onlyOne) { 92 | testResult(tx, call, params, (res) -> { 93 | if (res.hasNext()) { 94 | Map row = res.next(); 95 | consumer.accept(row); 96 | } 97 | if (onlyOne) { 98 | Assert.assertFalse(res.hasNext()); 99 | } 100 | }); 101 | } 102 | 103 | private static void testCallCount(Transaction tx, String call, Map params, int count) { 104 | testResult(tx, call, params, (res) -> { 105 | int numLeft = count; 106 | while (numLeft > 0) { 107 | assertTrue("Expected " + count + " results but found only " + (count - numLeft), res.hasNext()); 108 | res.next(); 109 | numLeft--; 110 | } 111 | Assert.assertFalse("Expected " + count + " results but there are more", res.hasNext()); 112 | }); 113 | } 114 | 115 | private static void testResult(Transaction tx, String call, Consumer resultConsumer) { 116 | testResult(tx, call, null, resultConsumer); 117 | } 118 | 119 | private static void testResult(Transaction tx, String call, Map params, Consumer resultConsumer) { 120 | Map p = (params == null) ? map() : params; 121 | resultConsumer.accept(tx.execute(call, p)); 122 | } 123 | 124 | private static void registerProcedure(GraphDatabaseService db, Class procedure) throws KernelException { 125 | ((GraphDatabaseAPI) db).getDependencyResolver().resolveDependency(GlobalProcedures.class).registerProcedure(procedure); 126 | } 127 | 128 | @Test 129 | public void shouldInterpolateNewNode() { 130 | assertFindWayAndInterpolatedPoint("Bottom", 4.5, 1); 131 | assertFindWayAndInterpolatedPoint("Bottom", 4.5, 0.5); 132 | assertFindWayAndInterpolatedPoint("Bottom", 4.5, 0.1); 133 | assertFindWayAndInterpolatedPoint("Bottom", 4.5, 0.01); 134 | assertFindWayAndInterpolatedPoint("Bottom", 4.5, 0.001); 135 | assertFindWayAndInterpolatedPoint("Bottom", 4.5, -0.001); 136 | assertFindWayAndInterpolatedPoint("Bottom", 4.5, -0.01); 137 | assertFindWayAndInterpolatedPoint("Bottom", 4.5, -0.1); 138 | assertFindWayAndInterpolatedPoint("Bottom", 4.5, -0.5); 139 | assertFindWayAndInterpolatedPoint("Bottom", 4.5, -1); 140 | } 141 | 142 | @Test 143 | public void shouldFindIntersections() { 144 | assertFoundIntersections("ChainTopRight", 3); 145 | assertFoundIntersections("ChainTopLeft", 3); 146 | assertFoundIntersections("ChainBottomLeft", 3); 147 | assertFoundIntersections("ChainBottomRight", 3); 148 | } 149 | 150 | private void assertFoundIntersections(String branch, int count) { 151 | try (Transaction tx = db.beginTx()) { 152 | TestOSMModel osm = new TestOSMModel(tx); 153 | OSMModel.OSMWay chain0 = osm.getWay(branch + "-0"); 154 | OSMModel.OSMWay chain5u = osm.getWay(branch + "-5u"); 155 | OSMModel.OSMWay chain5d = osm.getWay(branch + "-5d"); 156 | Node startNode = chain0.nodes.get(0).node(); 157 | ArrayList found = new ArrayList<>(); 158 | testResult(tx, "CALL spatial.osm.routeIntersection($osmNode,true,true,true) YIELD fromNode, wayNode, toNode, distance, fromRel, toRel RETURN fromNode, wayNode, toNode, distance, fromRel, toRel", 159 | map("osmNode", startNode), res -> { 160 | while (res.hasNext()) { 161 | Map r = res.next(); 162 | assertThat(branch + " should have correct keys from return", r.keySet(), containsInAnyOrder("fromNode", "wayNode", "toNode", "distance", "fromRel", "toRel")); 163 | Node fromNode = (Node) r.get("fromNode"); 164 | Node toNode = (Node) r.get("toNode"); 165 | double distance = (Double) r.get("distance"); 166 | assertThat(branch + " should have distance", distance, greaterThan(1000000.0)); 167 | Relationship fromRel = (Relationship) r.get("fromRel"); 168 | Relationship toRel = (Relationship) r.get("toRel"); 169 | assertThat(branch + " route should start with defined start node", startNode, equalTo(fromNode)); 170 | found.add(toNode); 171 | Relationship route = null; 172 | for (Relationship rel : startNode.getRelationships(Direction.OUTGOING, OSMModel.ROUTE)) { 173 | if (rel.getEndNode().equals(toNode)) { 174 | route = rel; 175 | break; 176 | } 177 | } 178 | assertNotNull(branch + " route relationship ending at " + toNode + " should exist", route); 179 | assertThat(branch + " route relationship should have found wayNode relationship id", route.getProperty("fromRel"), equalTo(fromRel.getId())); 180 | assertThat(branch + " route relationship should have found wayNode relationship id", route.getProperty("toRel"), equalTo(toRel.getId())); 181 | } 182 | }); 183 | assertThat(found.size(), equalTo(count)); 184 | assertThat(branch + " intersection to be at end chain", found, hasItem(chain5d.nodes.get(0).node())); 185 | assertThat(branch + " intersection to be at end chain", found, hasItem(chain5u.nodes.get(0).node())); 186 | tx.commit(); 187 | } 188 | } 189 | 190 | private void assertFindWayAndInterpolatedPoint(String name, double... coords) { 191 | try (Transaction tx = db.beginTx()) { 192 | // TODO this test does nothing with the way, and should assert on that 193 | TestOSMModel osm = new TestOSMModel(tx); 194 | OSMModel.OSMWay expectedWay = osm.getWay(name); 195 | assertNotNull("Should find a way with name '" + name + "'", expectedWay); 196 | OSMModel.LocatedNode poi = osm.makeNode(coords); 197 | testCall(tx, "MATCH (w:OSMWay) WITH collect(w) AS ways CALL spatial.osm.routePointOfInterest($node,ways) YIELD node RETURN node", 198 | map("node", poi.node()), r -> { 199 | assertThat(r.keySet(), contains("node")); 200 | Node routeNode = (Node) r.get("node"); 201 | if (routeNode == poi.node()) { 202 | // The point of interest lies on the closest way, so we have connected it within the route 203 | HashSet connected = new HashSet<>(); 204 | for (Relationship rel : poi.node().getRelationships(Direction.OUTGOING, OSMModel.ROUTE)) { 205 | Node osmNode = rel.getEndNode(); 206 | assertThat("Expected to have label 'Routable' but found " + osmNode.getLabels(), osmNode.hasLabel(Routable), equalTo(true)); 207 | connected.add(osmNode); 208 | } 209 | assertThat(connected.size(), equalTo(2)); 210 | assertNodeWithinWay(connected.iterator().next(), expectedWay); 211 | } else { 212 | // The point of interest is connected to a new interpolated point on the closest way 213 | Node connected = poi.node().getSingleRelationship(OSMModel.ROUTE, Direction.OUTGOING).getEndNode(); 214 | assertThat(routeNode, equalTo(connected)); 215 | assertNodeWithinWay(connected, expectedWay); 216 | } 217 | }); 218 | tx.commit(); 219 | } 220 | } 221 | 222 | private void assertNodeWithinWay(Node node, OSMModel.OSMWay expectedWay){ 223 | TraversalDescription findWayNode = new MonoDirectionalTraversalDescription().depthFirst() 224 | .relationships(OSMModel.ROUTE, Direction.OUTGOING) // Routable nodes always connected outgoing to normal OSMNode 225 | .relationships(OSMModel.NODE, Direction.INCOMING) // OSMNode connected INCOMING to OSMWayNode 226 | .relationships(OSMModel.NEXT, Direction.INCOMING) // follow OSMWayNode INCOMING chain back to first OSMWayNode 227 | .relationships(OSMModel.FIRST_NODE, Direction.INCOMING); // Finally find OSMWay from INCOMING FIRST_NODE 228 | for (Path p : findWayNode.traverse(node)) { 229 | Node last = p.endNode(); 230 | System.out.println("Found node: " + last); 231 | for (Label l : last.getLabels()) { 232 | System.out.println("\tLabel: " + l); 233 | } 234 | for (Relationship r : last.getRelationships()) { 235 | System.out.println("\tRelationship: (" + r.getStartNode() + ") -[:" + r.getType() + "]-> (" + r.getEndNode() + ")"); 236 | } 237 | if (last.hasLabel(OSMModel.OSMWay)) { 238 | assertThat("Expected the connected way to have the correct name", last.getProperty("name"), equalTo(expectedWay.getName())); 239 | if (last.getId() == expectedWay.wayNode.getId()) { 240 | return; 241 | } 242 | } 243 | } 244 | fail("Did not find any matching way node for way '" + expectedWay.getName() + "' when searching from node: " + node); 245 | } 246 | } 247 | 248 | --------------------------------------------------------------------------------